mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-11 23:52:59 -10:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74ed838daf | ||
|
|
976027ea53 | ||
|
|
916bcd1ceb | ||
|
|
8ffbf6b5df | ||
|
|
6530aa1641 | ||
|
|
5cc279af74 | ||
|
|
12d76b3862 | ||
|
|
9507c9c392 | ||
|
|
65fad4c52c | ||
|
|
28bb63315f | ||
|
|
8d18daeb96 | ||
|
|
1cdb240933 | ||
|
|
55f9db9941 | ||
|
|
007c1e4b60 | ||
|
|
1f4f56164c | ||
|
|
df10280204 | ||
|
|
42ec043c3e | ||
|
|
fe740c843d | ||
|
|
804651e418 | ||
|
|
0e03a5c29e | ||
|
|
619826c20b | ||
|
|
4e7c4a0d47 | ||
|
|
25093ba8cc | ||
|
|
a92a2dd921 | ||
|
|
23ce6331d4 | ||
|
|
a55d98a50c | ||
|
|
a23e88394b | ||
|
|
083ba1f085 | ||
|
|
d412c456cf | ||
|
|
29a4d62feb | ||
|
|
1b8c9de5b6 | ||
|
|
86d418b21b | ||
|
|
fff6786e17 | ||
|
|
da01b97dbd | ||
|
|
72f92a18cf | ||
|
|
e58b0a2a62 | ||
|
|
2d049d51c6 |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
# GitHub's language bar: this C++ table of game texts (its <?name?> placeholders) is not PHP.
|
||||
bbsc/emulator/src/core/bbsc_messages.inc linguist-language=C++
|
||||
@@ -12,7 +12,7 @@ body:
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The launcher's **About** page shows it.
|
||||
description: The launcher's **About** page shows it, with the build of a test build; so does the first line of the log.
|
||||
placeholder: 0.51-linux.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -44,3 +44,6 @@ navigation-map*/
|
||||
atlas-tools/
|
||||
# Hunter's Atlas's own art (shipped; the launcher converts it for the game)
|
||||
!tools/hunters-atlas/art/*.png
|
||||
|
||||
# The players' guide, built by mkdocs (mkdocs.yml)
|
||||
_site/
|
||||
@@ -6,8 +6,6 @@ THIS PROJECT IS NOT RELATED TO SHADPS4. ALL QUESTIONS RELATED TO THIS PROJECT SH
|
||||
> 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:
|
||||
@@ -17,12 +15,12 @@ directly on the PC:
|
||||
- **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.
|
||||
- memory and the GPU's work are moving to the PC model: the *New memory and translation model* is
|
||||
used by default on AMD GPUs; other GPUs keep the old memory model, as in 0.3 (launcher →
|
||||
*Advanced → Memory and translation model*).
|
||||
|
||||
One step remains to the full Wine + DXVK model: make the new mode the default and remove the old
|
||||
memory model (see below).
|
||||
One step remains to the full Wine + DXVK model: make the new mode the default on every GPU 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
|
||||
@@ -39,11 +37,12 @@ Mesa/RADV) has been tested thoroughly.
|
||||
- **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 → *Advanced → Memory and translation model*):
|
||||
- **Switch off (the default) — as in 0.3.** The old memory model: VRAM copies of the game's
|
||||
- **Three modes** (launcher → *Advanced → Memory and translation model*): *Auto* (the default:
|
||||
new on AMD, old on others), *New* and *Old (as in 0.3)*.
|
||||
- **Old (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
|
||||
- **New memory and translation model.** 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;
|
||||
@@ -60,10 +59,10 @@ Mesa/RADV) has been tested thoroughly.
|
||||
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
|
||||
[0.5-pre4 changes](https://github.com/deadinside28/bloodborne_pc/blob/master/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
|
||||
reported, the settings menu as in 0.3. [0.5-pre3](https://github.com/deadinside28/bloodborne_pc/blob/master/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](https://github.com/deadinside28/bloodborne_pc/blob/master/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.
|
||||
@@ -103,11 +102,11 @@ Mesa/RADV) has been tested thoroughly.
|
||||
| | 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 |
|
||||
| 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: ~8k lines) |
|
||||
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | On AMD by default the *New memory and translation model*: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused; on other GPUs the same model as in 0.3 |
|
||||
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | With the old model the same; with the new one translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
|
||||
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
|
||||
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
|
||||
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~500 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 |
|
||||
@@ -133,10 +132,13 @@ the graphics side.
|
||||
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).
|
||||
performs them. The game's occlusion queries (~60 a frame) can be
|
||||
Vulkan occlusion queries (`BB_OCCLUSION_QUERIES=1`, opt-in; by default a fixed "everything
|
||||
visible" sequence written in command-stream order, `BB_ZPASS_IN_ORDER=0` turns the ordering off);
|
||||
predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
|
||||
`BB_PM4_SELFTEST=1`). Details: upstream bbport's
|
||||
[EMULATION_REMOVAL_PLAN.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/EMULATION_REMOVAL_PLAN.ru.md),
|
||||
step 4 (in Russian).
|
||||
|
||||
"Port" here means a build for this one game, not a rewrite of its source code, which the project
|
||||
neither has nor includes.
|
||||
@@ -144,7 +146,8 @@ 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.
|
||||
On NVIDIA the new memory model is still being measured with testers, so *Auto* keeps the 0.3
|
||||
model there.
|
||||
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
|
||||
@@ -164,12 +167,12 @@ neither has nor includes.
|
||||
## Build and run
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
|
||||
git clone --recursive https://github.com/ayozetr/bloodborne_linux.git && cd bloodborne_linux
|
||||
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*):
|
||||
or the launcher (pick the game folder, settings, *Play*):
|
||||
|
||||
```bash
|
||||
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
|
||||
@@ -193,8 +196,8 @@ mouse camera* (`mouse_camera=direct`) the mouse turns the camera itself, 1:1, wi
|
||||
zone or turn curve (the game's 1.09 code is hooked while the window holds the mouse; locked on, the
|
||||
mouse moves the stick again so a flick switches targets). *Graphics → Display → Field of view* (`fov`, 30-90 degrees, the game's
|
||||
43) scales every camera of the game's camera table, as a layer under the mods (`scripts/fov.py`; a mod's own
|
||||
`gameparam.parambnd.dcx` wins). 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 *Multiplayer → Online → Skip the “play online / offline” choice*,
|
||||
`gameparam.parambnd.dcx` wins). The *Play online / offline* screen is skipped: the game opens its main menu online when online
|
||||
play is on, offline otherwise (a game patch; the launcher's *Multiplayer → Online → Skip the “play online / offline” choice*,
|
||||
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Graphics → Display → Monitor* in the launcher
|
||||
(`BB_DISPLAY=<number or part of the name>`) picks the one for the game.
|
||||
|
||||
@@ -202,7 +205,7 @@ Add-ons: `BB_DLC_DIR=/path/to/dlc` (the launcher's *Setup → DLC folder*) holds
|
||||
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.
|
||||
folder already there is kept. The European Game of the Year 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
|
||||
@@ -227,7 +230,7 @@ wrapper folder, or the game folders such as `chr/` directly; file name case does
|
||||
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).
|
||||
switched on and off in the launcher. See [mods and patches](docs/MODS.en.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.
|
||||
@@ -236,29 +239,31 @@ reported to the game as an installed add-on.
|
||||
**Game language:** the PS4 system language, *Setup → Game 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).
|
||||
2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian; without it, English: following the desktop's
|
||||
locale is the launcher's job).
|
||||
**Game region:** *Setup → 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
|
||||
launcher (*Mods → Menus and debugging*) or in the 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
|
||||
For the game debug menu, install the `adhoc` folder of
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253)
|
||||
(`adhoc/font/DbgFont14h.ccm` and `.tpf`) into the game's `dvdroot_ps4/`, or as a mod, first.
|
||||
Without them (or with empty files) the debug menu patch is left out at start and the log says
|
||||
why. 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, Back + d-pad left / right a click on that half (while Back is bound
|
||||
to the touchpad only, the default). 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.
|
||||
By default GPU occlusion queries return synthetic "visible" counters (`BB_OCCLUSION_QUERIES=1`
|
||||
counts real samples). Free camera allows visual investigation; it does not change occlusion
|
||||
culling.
|
||||
|
||||
**Upscaler assets** (not included; FSR 3.1 needs none):
|
||||
|
||||
@@ -287,7 +292,9 @@ umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downl
|
||||
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>`.
|
||||
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`. From source, `tools/fsr4cap/build_assets.sh` takes its tools from
|
||||
the system (MinGW GCC for the capture program run under Proton, CMake, Ninja, Python 3,
|
||||
SPIRV-Tools, Git) or from Nix; the AppImage has them prebuilt.
|
||||
|
||||
**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
|
||||
@@ -313,9 +320,6 @@ launcher: *Graphics → Upscaler → DLSS: NVIDIA's library → Choose a file…
|
||||
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
|
||||
@@ -369,8 +373,8 @@ 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
|
||||
RSS, images and guest blocks in VRAM), `BB_PC_MODEL=1/0` (the new memory and translation
|
||||
model / the old one as in 0.3; unset: new on AMD, old elsewhere), `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
|
||||
@@ -387,8 +391,8 @@ 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).
|
||||
More in [docs/](docs); what changed in each release: the
|
||||
[releases page](https://github.com/ayozetr/bloodborne_linux/releases).
|
||||
|
||||
**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,
|
||||
@@ -411,15 +415,15 @@ build without it plays offline only.
|
||||
| `patches/` | Community patches for Bloodborne |
|
||||
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
|
||||
| `tests/` | Loader, runtime, patch and renderer tests |
|
||||
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](docs/motion_vectors.md), [roadmap](docs/ROADMAP.md)) |
|
||||
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/motion_vectors.md), [roadmap](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/ROADMAP.md)) |
|
||||
|
||||
Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
|
||||
|
||||
## 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.
|
||||
- The new memory and translation model on by default on NVIDIA and Intel too (it already is on
|
||||
AMD) once tested 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.
|
||||
@@ -435,8 +439,9 @@ Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
|
||||
|
||||
## 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:
|
||||
This fork is licensed under the **GNU GPL v3 or later** ([LICENSE](LICENSE)). bbport, which it is
|
||||
built on, is GPL-2.0-or-later and contains code from shadPS4 (GPL-2.0-or-later); the fork also
|
||||
includes code from Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine), GPL-3.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
|
||||
|
||||
-423
@@ -1,423 +0,0 @@
|
||||
ДАННЫЙ ПРОЕКТ НЕ ИМЕЕТ ОТНОШЕНИЯ К SHADPS4. ВСЕ ВОПРОСЫ, СВЯЗАННЫЕ С ЭТИМ ПРОЕКТОМ - ПИШИТЕ НА ДИСКОРД СЕРВЕР https://discord.gg/KYZRKk9CB, А НЕ НА СЕРВЕР SHADPS4
|
||||
|
||||
# bbport — нативный порт Bloodborne для Linux
|
||||
|
||||
> README оригинального проекта (deadinside28/bloodborne_pc), сохранён ради технических
|
||||
> подробностей. README этого форка — [README.md](README.md) (на английском).
|
||||
|
||||
[English](BBPORT.md) · **Русский**
|
||||
|
||||
bbport — это аналог Wine + DXVK, только для одной игры: *Bloodborne* для PlayStation 4
|
||||
(CUSA03173, версия игры 1.09) на ПК с Linux x86-64. Оригинальный исполняемый файл игры
|
||||
запускается прямо на ПК:
|
||||
|
||||
- **как в Wine**, x86-64-код игры выполняется на процессоре напрямую, а системные библиотеки PS4
|
||||
заменяет собственный рантайм, написанный под эту игру;
|
||||
- **как в DXVK**, графика игры переводится в Vulkan — рендером на основе
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4), сильно доработанным под Bloodborne, вплоть
|
||||
до временного апскейлинга AMD FSR 3.1, FSR 4 и FSR 4.1.1;
|
||||
- память и работа видеочипа на AMD по умолчанию устроены как у игры для ПК (новая модель памяти
|
||||
и трансляции); NVIDIA и другие видеокарты пока работают со старой моделью памяти, как в 0.3.
|
||||
В лаунчере *Модель памяти* переключается в обе стороны.
|
||||
|
||||
До полной модели Wine + DXVK остался один шаг: новая модель и на NVIDIA, затем удаление старой
|
||||
модели памяти (см. ниже).
|
||||
|
||||
> **Файлов игры в репозитории нет.** Нужен свой дамп Bloodborne с обновлением 1.09: любое издание
|
||||
> из магазина (CUSA00900 в США, CUSA03173 — европейское Game of the Year, ...): у них один и тот же
|
||||
> исполняемый файл.
|
||||
> Проект не связан с Sony Interactive Entertainment, FromSoftware и AMD.
|
||||
|
||||
**Состояние: экспериментальный, играбельный.** Игра запускается, загружает сохранения и
|
||||
проходится (Сон Охотника и несколько локаций Ярнама) со звуком,
|
||||
геймпадом и сохранением. Полное прохождение не проверено; основательно проверена одна машина
|
||||
(Linux, AMD Radeon RX 7800 XT, Mesa/RADV).
|
||||
|
||||
## Что сделано
|
||||
|
||||
- **Нативное исполнение.** eboot заранее превращается в плоский образ памяти, в который как
|
||||
нативный код прилинкованы libc и libSceFios2 из PS4. Никакой эмуляции процессора и
|
||||
трансляции инструкций: код игры работает на полной скорости.
|
||||
- **Две модели памяти** (лаунчер → *Дополнительно → Модель памяти и трансляции*; *Авто*, по
|
||||
умолчанию, выбирает по видеокарте):
|
||||
- **Старая модель памяти, как в 0.3, — по умолчанию на NVIDIA, Intel и других.** Копии памяти
|
||||
игры в VRAM, записи отслеживаются защитой страниц. Со всеми исправлениями после 0.3
|
||||
(векторы движения и апскейлер, мерцание при включённом DoF, повреждённый кэш шейдеров).
|
||||
- **Новая модель памяти и трансляции — по умолчанию на AMD.** Память
|
||||
игры находится в оперативной памяти, и GPU читает её на месте, как буферы игры для ПК; то,
|
||||
что он читает часто, держится в видеопамяти и освобождается, когда перестаёт использоваться
|
||||
(текстуры — через 20 с, буферы — через 60 с). Модуль памяти прослойки отслеживает записи CPU
|
||||
в блоки с зеркалами для согласования данных; вся память, видимая GPU, не копируется.
|
||||
Работа командного процессора PS4 переводится в команды Vulkan, а не
|
||||
эмулируется на процессоре: записи в память (`WRITE_DATA`, DMA) выполняет видеочип в порядке
|
||||
потока команд, метки (fences) пишет сам видеочип в конце конвейера, в той же точке потока,
|
||||
что и командный процессор PS4.
|
||||
На AMD память игры держится в sparse-буфере (арене); на NVIDIA, Intel и других — через модуль
|
||||
памяти прослойки: привязывается на месте, копии в VRAM — отдельные буферы, без
|
||||
sparse-перепривязок (на NVIDIA sparse binding держал видеочип секундами); `BB_LAYER_MEMORY=1/0`
|
||||
— выбрать вручную. Проверено на RX 7800 XT. На NVIDIA её можно выбрать
|
||||
вручную (*Новая*); у тестеров результат зависит от сцены, в части сцен медленнее старой
|
||||
модели, поэтому там она пока не по умолчанию. На Intel не проверялась. Без лаунчера —
|
||||
`BB_PC_MODEL=1/0` (не задано — по видеокарте, как *Авто*).
|
||||
|
||||
[Изменения в 0.5](docs/CHANGES_0.5.md); пререлизы: [pre4](docs/CHANGES_0.5-pre4.md),
|
||||
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
|
||||
|
||||
Неиспользуемые текстуры освобождаются в обоих режимах, поэтому видеопамять больше не растёт
|
||||
с каждой локацией.
|
||||
- **Разблокированная частота кадров.** Патчи сообщества (`patches/Bloodborne.xml`) переводят
|
||||
симуляцию на реальное время кадра; ~90 FPS в 4K с FSR 4 Balanced на RX 7800 XT, ~150 FPS в
|
||||
1440p с FSR 4 Quality. Есть режимы 30/60/90 FPS.
|
||||
- **Временной апскейлинг под эту игру.** Буфера скоростей в Bloodborne нет, поэтому порт
|
||||
считает векторы движения сам: движение камеры — по глубине и матрицам сцены, движение
|
||||
объектов (персонажи, ткань, оружие) — по позициям вершин прошлого кадра. Сцена сдвигается
|
||||
субпиксельным jitter (Halton) и рисуется в уменьшенном разрешении, апскейлер достраивает
|
||||
вывод (720p для Steam Deck, 1080p, 1440p или 2160p), интерфейс рисуется сразу в выходном разрешении.
|
||||
- **FSR 3.1** (FireBurn/FSR-Vulkan).
|
||||
- **FSR 4 (INT8, модель v07)** на видеокартах с нужными возможностями шейдеров Vulkan,
|
||||
включая RDNA2/3 (см. требования).
|
||||
- **FSR 4.1.1**: DLL AMD 4.1.1 один раз записывается под vkd3d-proton, а её проходы
|
||||
воспроизводятся нативно в Vulkan; результат **побитово совпадает** с DLL. Два варианта, как
|
||||
в самой DLL: INT8 на любой видеокарте с нужными возможностями шейдеров и матрицы FP8 на
|
||||
RDNA4 (RX 9000; выбирается сам). Ассеты собираются на твоей машине из одной твоей DLL
|
||||
4.1.x: в лаунчере кнопка *FSR 4.1.1 из своей DLL AMD → Выбрать DLL…* (2–5 минут, на RDNA4
|
||||
вдвое дольше; нужен свежий Proton), или `tools/fsr4cap`.
|
||||
- Быстрее шейдеров самой AMD на RDNA3: финальные проходы FSR 4 и 4.1.1 переписаны с записью
|
||||
через shared-память (в 3.5 и 2.3 раза быстрее, побитово так же); FSR 4 в 4K на RX 7800 XT
|
||||
стоит ~4 мс вместо ~6.
|
||||
- **Многопоточная обработка GPU-команд.** Поток команд PS4 разбирается в одном потоке, а
|
||||
привязка ресурсов и запись draw — в другом (двухстадийный конвейер), плюс отдельный поток
|
||||
записи команд Vulkan и вспомогательные потоки для копий памяти. В начале проекта единственный
|
||||
поток GPU ограничивал игру ~26 FPS; сейчас 90–150 FPS в зависимости от разрешения и сцены.
|
||||
- **Настройки в меню самой игры:** *Система → Изображение / Эффекты* (после *Экран/звук*) — их
|
||||
строит и рисует сама игра: разрешение вывода, апскейлер, пресет, резкость, счётчик FPS,
|
||||
детализация моделей и эффекты игры, на языке игры.
|
||||
- **Меню поверх игры** (Insert или L3+R3) в стиле диалогов игры: всё то же и расширенные
|
||||
настройки; геймпад, клавиатура и мышь.
|
||||
- **Лаунчер на GTK4** и **AppImage** для Steam Deck.
|
||||
|
||||
## Чем отличается от shadPS4
|
||||
|
||||
| | shadPS4 | bbport |
|
||||
|---|---|---|
|
||||
| Охват | Универсальный эмулятор PS4, много игр | Одна игра: Bloodborne v1.09 |
|
||||
| Загрузка | Свой ELF-загрузчик и эмуляция ядра во время работы | eboot заранее превращается (`scripts/`) в образ с прилинкованными libc/Fios2 PS4; загрузчик на C отображает его и передаёт управление игре (загрузчик и рантайм — ~5 тыс. строк) |
|
||||
| Память | Вид памяти PS4 со стороны GPU хранится копиями в VRAM, синхронизация через отслеживание записей защитой страниц | По умолчанию та же модель (как в 0.3); в режиме *Новая модель памяти и трансляции*: память игры в оперативной памяти, GPU использует её на месте, часто читаемое — в VRAM, освобождается, когда не нужно |
|
||||
| Командный процессор | Эмулируется: записи в память, DMA и метки выполняет процессор во время разбора команд | По умолчанию так же; в новом режиме переводится в команды Vulkan, их выполняет видеочип в порядке потока, метки — после реального завершения работы |
|
||||
| Системные библиотеки | Широкий HLE ОС PS4 | Небольшой рантайм (`src/runtime_*.c`) ровно под то, что вызывает Bloodborne: память, потоки, синхронизация, файлы, звук (включая ATRAC9), геймпад, сохранения, AppContent |
|
||||
| GPU | Видеоядро и рекомпилятор шейдеров shadPS4 | То же ядро (вложено, GPL) с ~200 помеченными правками (`bbport:`) и новыми модулями: двухстадийный конвейер draw, мемоизация состояния прохода и наборов текстур, уменьшенные цели сцены, векторы движения, FSR 3.1/4/4.1.1, захват кадра, профайлер GPU |
|
||||
| Поток GPU | Один поток обрабатывает весь поток команд (узкое место в Bloodborne) | Разбор и запись draw идут в разных потоках; работа масштабируется под число потоков (в том числе Steam Deck) |
|
||||
| Апскейлинг | — | Временной (FSR 3.1, FSR 4, FSR 4.1.1) на собственных векторах движения и jitter игры |
|
||||
| Патчи игры | Патч-файлы применяет эмулятор | Те же патчи сообщества компилируются при запуске (`scripts/patches.py`); разрешение рендера, эффекты и FPS — из лаунчера |
|
||||
|
||||
Без shadPS4 bbport бы не было: его рендерер и рекомпилятор шейдеров — основа графической части.
|
||||
|
||||
### Wine + DXVK для одной игры
|
||||
|
||||
- **Процессор.** Процессор PS4 — x86-64, поэтому код игры выполняется прямо на процессоре ПК, без
|
||||
эмуляции и перевода инструкций.
|
||||
- **Системные библиотеки.** Как Wine подменяет Windows API, так рантайм bbport (`src/runtime_*.c`)
|
||||
реализует ровно те функции ОС PS4, которые вызывает Bloodborne: память, потоки, файлы, звук,
|
||||
геймпад, сохранения.
|
||||
- **Память.** В новом режиме — как у игры для ПК: данные игры в оперативной памяти, видеопамять
|
||||
используется так, как её использует игра для ПК. Пока режим экспериментальный.
|
||||
- **Графика.** Поток команд видеочипа PS4 (PM4) — это записанные вызовы графического API игры:
|
||||
его пишут 99 функций статически слинкованной libGnm, так что разбор потока и перевод самих
|
||||
вызовов дают одно и то же. Шейдеры GCN переводятся в SPIR-V. В новом режиме работа командного
|
||||
процессора (записи в память, DMA, метки) тоже переводится в команды Vulkan; метки пишет сам
|
||||
видеочип. Дескрипторы ресурсов (текстур, буферов) транслятор читает на процессоре, как DXVK
|
||||
читает привязки ресурсов: текстуру PS4 нужно перевести из раскладки PS4 в образ Vulkan до
|
||||
отрисовки, поэтому процессор должен знать её дескриптор заранее — это трансляция, а не
|
||||
эмуляция. Таблицы дескрипторов (дампы движка констант) читает только транслятор, поэтому их
|
||||
выполняет процессор. Occlusion-запросы игры (~60 за кадр) — настоящие запросы Vulkan; предикация, `COPY_DATA`,
|
||||
`COND_EXEC` тоже переводятся (проверка — `BB_PM4_SELFTEST=1`).
|
||||
Подробно — [docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), «Шаг 4».
|
||||
|
||||
«Порт» здесь — сборка под одну игру, а не переписанный исходный код: исходного кода игры у
|
||||
проекта нет, и он его не содержит.
|
||||
|
||||
## Требования
|
||||
|
||||
- Linux x86-64, видеокарта с Vulkan 1.3. Проверено: AMD RX 7800 XT, Mesa 26 (RADV).
|
||||
Режим *Новая модель памяти и трансляции* на NVIDIA пока не проверен.
|
||||
FSR 4 / 4.1.1 требуют shader Float16, Int8/Int16, целочисленных dot-произведений,
|
||||
linear compute derivatives и extended storage image formats; FSR 4.1.1 дополнительно
|
||||
требует `VK_VALVE_shader_mixed_float_dot_product`. Неподдерживаемый вариант заменяется
|
||||
на FSR 3.1 до первого кадра и недоступен в игровом меню.
|
||||
- Свой расшифрованный дамп игры: папка `CUSA03173` (eboot.bin, sce_module, ...), версия 1.09.
|
||||
Дамп обновления — отдельная папка: скопируйте её поверх базовой игры с заменой файлов. Одна
|
||||
базовая игра (1.00) падает при запуске (guest offset 0x20348b8); лаунчер и `run.sh` проверяют
|
||||
исполняемый файл и пишут, чего не хватает (`BB_SKIP_GAME_CHECK=1` отключает проверку).
|
||||
Другие издания (американское `CUSA00900`, азиатское `CUSA03023`, ...) запускают тот же
|
||||
исполняемый файл 1.09. В некоторых дампах в него уже вшит патч 60 FPS от Lance McDonald, из-за которого игра
|
||||
падает при открытии меню жестов: проверка тогда просит чистый eboot.bin (у bbport свои 60 FPS).
|
||||
- Для сборки: GCC, CMake, Ninja, Python 3, glslang, SDL3, заголовки Vulkan и библиотеки из
|
||||
`shell.nix`. С [Nix](https://nixos.org) всё берётся из `shell.nix` автоматически.
|
||||
|
||||
## Сборка и запуск
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
|
||||
bash build.sh # собирает out/bb-probe и out/gpu/libbbgpu.so
|
||||
BB_GAME_DIR=/путь/к/CUSA03173 bash run.sh
|
||||
```
|
||||
|
||||
или через лаунчер (выбрать папку игры, настройки, «Запустить»):
|
||||
|
||||
```bash
|
||||
bash launcher/bb-launcher.sh # launcher/install-desktop.sh добавит его в меню приложений
|
||||
```
|
||||
|
||||
По умолчанию папка игры ищется рядом с репозиторием (`../CUSA03173`). Сохранения и кэш шейдеров
|
||||
— в `user/` (в лаунчере, *Начальная настройка → Сохранения*, можно выбрать другую папку,
|
||||
экспортировать сохранения в .zip и импортировать их из такого .zip, shadPS4, другого порта или
|
||||
выгруженные с PS4 без шифрования; текущие сначала копируются в
|
||||
`user/savedata-backups/`, туда же по желанию копия перед каждым запуском игры, с выбранным числом
|
||||
копий; `scripts/save_import.py`), настройки — в `bbport.ini`. Геймпад через
|
||||
SDL3 (если их несколько, нужный выбирается в лаунчере: *Управление → Геймпад → Контроллер*,
|
||||
`BB_GAMEPAD=<GUID или часть имени>`; если не выбран, управляет тот, на котором нажали кнопку или
|
||||
двинули стик). Клавиатура тоже работает, и при подключённом геймпаде
|
||||
(на Steam Deck он есть всегда); обе раскладки меняются в лаунчере (*Управление*); с зажатым левым Alt клавиши движения дают шаг, а не бег (`key.walk`). Имя персонажа
|
||||
вводится с клавиатуры в окне поверх игры. Тачпад: левая половина (Tab, Back/Select) открывает
|
||||
жесты, правая (Backspace) — личные вещи. На геймпаде без тачпада (Xbox) Back + крестовина
|
||||
влево / вправо нажимает левую / правую половину. В лаунчере (*Управление*) можно поменять стики
|
||||
местами и инвертировать камеру геймпада (`pad_swap_sticks`, `pad_invert_look_x`, `pad_invert_look_y` в bbport.ini). *Прямая камера мышью*
|
||||
(`mouse_camera=direct`): мышь поворачивает камеру сама, 1:1, без мёртвой зоны и кривой стика (код
|
||||
игры 1.09 перехватывается, пока окно держит мышь; при захвате цели мышь снова двигает стик). *Графика → Экран → Поле зрения* (`fov`, 30–90 градусов, в игре 43)
|
||||
масштабирует все камеры таблицы камер игры, слоем под модами (`scripts/fov.py`; собственный
|
||||
`gameparam.parambnd.dcx` мода важнее). PSN нет: экран «Играть по сети / вне сети» пропускается,
|
||||
игра сразу открывает главное меню вне сети (патч игры; в лаунчере *Мультиплеер → По сети → Пропускать
|
||||
выбор «по сети / вне сети»*, `BB_SKIP_NETWORK_CHOICE=0` — показывать). При нескольких мониторах нужный выбирается в
|
||||
лаунчере: *Графика → Экран → Монитор* (`BB_DISPLAY=<номер или часть имени>`).
|
||||
|
||||
**Смена разрешения и пресетов:** для вывода не 1080p (720p на Steam Deck, 1440p, 4K) вся игра
|
||||
рисуется в разрешении пресета, которое задаёт патч при запуске, — это самый быстрый путь.
|
||||
Смена вывода или пресета в игровом меню тогда требует «Применить и перезапустить игру».
|
||||
Настройка «Смена разрешения на лету» (лаунчер, игровое меню, `bbport.ini`
|
||||
`live_resolution=0|1|auto`; **по умолчанию выключена**) вместо этого оставляет игре 1080p и
|
||||
масштабирует её цели на ходу: 720p/1080p/1440p/4K и пресеты меняются без перезапуска. Это
|
||||
дороже: игра считает, что рисует 1080p, и рисует больше (например, мелких источников света
|
||||
примерно в 8 раз больше), часть целей копируется между размерами — включай только на мощных
|
||||
настольных видеокартах (`auto` включает её на дискретных GPU с 8+ ГБ, кроме NVIDIA старше
|
||||
Turing). Вывод 1080p и TAA всегда идут живым путём.
|
||||
|
||||
**TAA:** отдельный пункт сглаживания в лаунчере и игровом меню, переключается на ходу.
|
||||
Работает в разрешении вывода без модели FSR; пресет FSR сохраняется для обратного
|
||||
переключения. Native AA у FSR выполняет реконструкцию поверх полного разрешения и
|
||||
может быть тяжелее выключенного сглаживания. TAA тоже добавляет обработку кадра.
|
||||
|
||||
**Моды:** лаунчер подключает распакованные моды из отдельных папок (с `dvdroot_ps4/`, с лишней
|
||||
папкой-обёрткой или сразу с папками игры вроде `chr/`; регистр имён не важен), с переключателями
|
||||
и порядком загрузки. **Сторонние патчи:** XML-файлы в формате shadPS4 в папке `patches/` каталога
|
||||
данных, включаются в лаунчере. Подробнее — [моды и патчи](docs/MODS.md).
|
||||
**DLC:** дампы дополнений клади в `user/addcont/<title id>/<метка>/` (как в shadPS4, например
|
||||
`user/addcont/CUSA03173/SPEXPANSIONDLC03` для The Old Hunters): каждая такая папка сообщается
|
||||
игре как установленное дополнение.
|
||||
|
||||
**Язык лаунчера:** русский, английский, бразильский португальский, испанский, итальянский, французский, немецкий или упрощённый китайский (по умолчанию — как в системе).
|
||||
|
||||
**Свободная камера и debug menu игры** (v1.09): включи соответствующие галочки в лаунчере
|
||||
или игровом меню и перезапусти игру. Для камеры используется
|
||||
[патч Lance McDonald из GoldHEN](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
|
||||
удерживай Cross и нажимай L3 для переключения режимов (клавиатура: Space + Z).
|
||||
Камере шрифты не нужны; с патчем Enemy Control она несовместима.
|
||||
Для debug menu сначала установи `DbgFont14h.ccm` и `DbgFont14h.tpf` из
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) в
|
||||
`dvdroot_ps4/font/` внутри папки игры. Если файлов нет или они пустые, запуск остановится
|
||||
с объяснением. Меню открывается левым touchpad / Tab (с включённым debug menu левая половина
|
||||
больше не открывает жесты); правый touchpad — Backspace.
|
||||
Координаты касания передаются с SDL-геймпадов; Back/Select заменяет левый клик на геймпадах
|
||||
без сенсорной поверхности. Меню настроек порта по-прежнему открывается Insert / L3+R3.
|
||||
|
||||
GPU-запросы occlusion пока возвращают искусственные счётчики пикселей
|
||||
(`PixelPipeStatDump`), а `IT_SET_PREDICATION` не реализован. Свободная камера позволяет
|
||||
проверять видимость, но не добавляет GPU occlusion culling.
|
||||
|
||||
**Ассеты апскейлеров** (в репозитории их нет; FSR 3.1 они не нужны):
|
||||
|
||||
```bash
|
||||
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, собраны Q2RTX из исходников AMD)
|
||||
# FSR 4.1.1 — из своих DLL AMD (например, FSR4_LATEST у OptiScaler), нужен GE-Proton 10 или новее:
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
|
||||
```
|
||||
|
||||
**FSR 4.1.1 из своей DLL.** Проще всего — в лаунчере: *Графика → Апскейлер → FSR 4.1.1 из своей DLL AMD →
|
||||
Выбрать DLL…* и указать `amd_fidelityfx_upscaler_dx12.dll` версии 4.1.x: из папки `FSR4_LATEST`
|
||||
OptiScaler или из игры с FSR 4.1. Этого одного файла достаточно: DLL AMD сама экспортирует
|
||||
FidelityFX API, загрузчик не нужен (через `amd_fidelityfx_loader_dx12.dll` записывалась бы только
|
||||
DLL без этих функций, и тогда лаунчер его спросит). DLL проверяется сразу, без долгой записи, и не подходят: официальная FSR 4.0.x
|
||||
(например, 4.0.3 из Pragmata — AMD включает её только на RDNA4, под Proton на других видеокартах
|
||||
она не запускается) и сборки сообщества (4.0.2b, 4.1.1b и подобные INT8-сборки OptiScaler:
|
||||
другая модель и число проходов). Дальше DLL запускается под Proton и записывается в 20
|
||||
разрешениях (ход виден в строке и в разделе «Журнал»; на RDNA4 — второй раз, для варианта FP8), проходы переводятся в SPIR-V, и FSR 4.1.1
|
||||
сразу выбирается. Proton выбирается сам — первый, под которым DLL включает FSR 4.1: GE-Proton 10
|
||||
или новее, Proton-CachyOS, Proton Experimental (проверено: GE-Proton 11, Proton-CachyOS 11,
|
||||
Experimental от октября 2026; Proton 11.0 из Steam и GE-Proton 9 — нет). Он запускается в
|
||||
рантайме Steam, который требует (Steam ставит его при первом запуске любой игры с этим Proton),
|
||||
или через umu-launcher, если он установлен. На NixOS umu-launcher берётся из `nix-shell` (в первый
|
||||
раз скачивается, ~1,7 ГБ); AppImage запускает запись на самой системе, через systemd пользователя
|
||||
(`systemd-run --user`), так как системный `/nix` ей не виден. Запись на Steam Deck пока не проверена; если DLL не включит
|
||||
там FSR 4.1, собери на ПК и скопируй папку `fsr4_411`. То же из командной строки:
|
||||
`--build-fsr411 <DLL>` у лаунчера (и у AppImage).
|
||||
|
||||
**DLSS (NVIDIA GeForce RTX).** SDK DLSS от NVIDIA не входит в репозиторий: скачай
|
||||
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (достаточно `include/` и
|
||||
`lib/Linux_x86_64`) и собери с `DLSS_SDK_ROOT=/путь/к/DLSS bash build.sh`. Тогда собирается и мост
|
||||
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, единственный код, использующий SDK), а
|
||||
`libnvidia-ngx-dlss.so.<версия>` от NVIDIA копируется рядом с `out/bb-probe`; остальное даёт
|
||||
драйвер. Затем выбери *DLSS* в лаунчере или во внутриигровом меню: пресет задаёт разрешение
|
||||
рендера, Native AA — это DLAA. DLSS получает те же входы, что и FSR 3.1 (цвет сцены, глубину,
|
||||
векторы движения, 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) выбирается в лаунчере:
|
||||
*Графика → Апскейлер → DLSS: библиотека NVIDIA → Выбрать файл…*, как DLL для FSR 4.1.1; она копируется в
|
||||
`<user>/dlss/`, и игра берёт её первой. `nvngx_dlss.dll` из игр — библиотека для Windows: драйвер
|
||||
NVIDIA на Linux её не загружает.
|
||||
|
||||
Скрипт из исходников берёт инструменты из системы (MinGW GCC, CMake, Ninja, Python 3,
|
||||
SPIRV-Tools, Git) или из Nix; в AppImage они уже собраны.
|
||||
|
||||
**FP8 на RDNA4.** Когда vkd3d-proton даёт кооперативные матрицы FP8 (RDNA4), DLL запускает
|
||||
другой вариант своих проходов: другие шейдеры и веса, другие размеры диспатчей. Сборка
|
||||
записывает оба: INT8 в `fsr4_411/`, FP8 в `fsr4_411/fp8/`, а игра сама выбирает FP8, если у
|
||||
видеокарты есть матрицы FP8 (`VK_EXT_shader_float8`); в журнале — `Upscaler: FSR 4.1.1 replay,
|
||||
FP8 …`. `BB_FSR411_VARIANT=int8` — принудительно INT8. Проверено на RX 7800 XT через эмуляцию FP8
|
||||
на FP16 в vkd3d-proton (`BB_FSR4CAP_FP8=1` записывает его и там, `fp8emu/`): побитово как DLL;
|
||||
**на самой RDNA4 ещё не запускалось.**
|
||||
|
||||
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`; данные в `~/.local/share/bbport`, `--play` запускает
|
||||
игру без окна лаунчера (игровой режим). Модели FSR 4.1.1 в пакет не входят: собери их кнопкой
|
||||
лаунчера (см. выше), они лягут в `~/.local/share/bbport/fsr4_411` (`BB_PACKAGE_FSR411=1` кладёт
|
||||
локальную `fsr4_411` в AppImage для своих устройств). На Steam Deck выбирай вывод 1280×720 (игра 16:9, на
|
||||
экране 1280×800 будут узкие полосы).
|
||||
|
||||
**Добавление AppImage в Steam** («Добавить стороннюю игру»): ничего настраивать не нужно,
|
||||
инструмент совместимости не важен. (Steam подгружает свой оверлей во все сторонние игры; AppImage
|
||||
убирает его до запуска своих программ, поэтому оверлея Steam в игре нет.) Если Steam запускает игры без FUSE (NixOS: FHS-песочница Steam; AppImage тогда завершается с
|
||||
*Cannot mount AppImage*), в параметрах запуска укажи
|
||||
|
||||
```
|
||||
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
|
||||
```
|
||||
|
||||
Тогда AppImage при первом запуске распакуется (~2 ГБ, `~/.cache/appimage_extracted_*`, ~10 с),
|
||||
а дальше будет запускать эту копию; новая версия AppImage распакуется заново, старую копию можно
|
||||
удалить. Без `TMPDIR` распаковка шла бы в `/tmp` Steam, а он там в оперативной памяти.
|
||||
` --play` после `%command%` — запуск без лаунчера.
|
||||
|
||||
**NVIDIA в AppImage:** пакет автоматически ищет установленный на машине 64-битный NVIDIA
|
||||
Vulkan-драйвер и подключает его вместе с комплектными AMD/Intel-драйверами. Используются
|
||||
библиотеки NVIDIA с этой машины, чтобы их версия совпадала с модулем ядра. На обычных Linux
|
||||
дистрибутивах они доступны в `/usr/lib*`; на NixOS внутренний `/nix/store` AppImage может скрывать
|
||||
драйвер хоста. В этом случае библиотеки NVIDIA нужно скопировать в доступную папку и указать
|
||||
`BB_NVIDIA_LIB_DIR=/путь/к/библиотекам` (манифест NVIDIA тоже должен быть доступен).
|
||||
С драйвером NVIDIA хоста AppImage передаёт и файлы EGL-вендоров хоста, NVIDIA первым
|
||||
(`__EGL_VENDOR_LIBRARY_FILENAMES`; без них ICD NVIDIA на некоторых системах не отдавал
|
||||
`vkCreateInstance`, #107; `BB_NVIDIA_EGL=0` — выключить). Явные `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` сохраняются. Проверка драйвера внутри пакета:
|
||||
|
||||
```bash
|
||||
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
|
||||
```
|
||||
|
||||
Пользователь подтвердил запуск с FSR 3 на GTX 1060 6GB (Fedora 44, NVIDIA 580.178.04);
|
||||
выбор FSR 4 приводил к чёрному окну. На этой конфигурации используй FSR 3.
|
||||
|
||||
MangoHud включён в AppImage; включи его галочку в лаунчере. Если MangoHud установлен ещё и в
|
||||
системе или включён оверлей производительности Steam (игровой режим Steam Deck), рисуется только
|
||||
один оверлей (два давали двоящийся текст со сдвигом).
|
||||
При запуске из исходников MangoHud нужно установить отдельно. Диагностический запуск
|
||||
с `VK_LOADER_LAYERS_DISABLE=~implicit~` отключает и MangoHud.
|
||||
|
||||
Полезные переменные: `BB_FRAME_STATS=1` (статистика кадров, в том числе строка `Memory:` —
|
||||
VRAM, GTT, RSS, текстуры и блоки памяти игры в VRAM), `BB_PC_MODEL=1` (новая модель памяти и трансляции;
|
||||
по умолчанию 0 — старая модель, как в 0.3), `BB_ANISO=N` (анизотропная фильтрация текстур сцены; по умолчанию
|
||||
16, 0 — как в игре),
|
||||
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (сколько неиспользуемые текстуры / буферы
|
||||
остаются в VRAM; 20 / 60), `BB_BREADCRUMBS=0` (без меток GPU; с ними зависание видеокарты
|
||||
называет отрисовку или dispatch, на которой она встала), `BB_GPU_PROFILE=1` (время GPU по
|
||||
проходам), `BB_FSR4_PROFILE=1` (время каждого прохода FSR 4), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
|
||||
`BB_FRAMES_AHEAD=N` (на сколько кадров поток команд может опережать GPU; по умолчанию 1, 0 — без
|
||||
ограничения), `BB_PRESENT_THREAD=0` (показ в потоке vblank, как раньше), `BB_LIVE_RES=1` (смена
|
||||
разрешения без перезапуска вместо патча при запуске для вывода не 1080p),
|
||||
`BB_PAD_RECORD=файл` / `BB_PAD_REPLAY=файл` (запись маршрута по F9 и его повтор в тестах),
|
||||
`BB_GC_BUDGET_MB=N` (бюджет кэша текстур, как на встроенных GPU), `BB_VRAM_LIMIT_MB=N` (бюджет
|
||||
VRAM считается не больше N МиБ: проверка поведения карт с малой VRAM), `BB_ASYNC_PIPELINES=0`
|
||||
(новые конвейеры компилируются сразу, игра ждёт; по умолчанию — в фоне), `BB_PRESENT_DUMP_TRIGGER=файл`
|
||||
с `BB_PRESENT_DUMP_COUNT=N` (снимок N показанных кадров подряд),
|
||||
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (вариант FSR 4.1.1; по умолчанию FP8, где у видеокарты есть
|
||||
матрицы FP8), `BB_READBACKS=0|1|2` (чтение игрой памяти, записанной GPU: по умолчанию 1, 2 —
|
||||
точно и медленно, 0 — выключено).
|
||||
Подробнее — в [docs/](docs); последние изменения — [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md),
|
||||
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
|
||||
|
||||
**Игра по сети (shadNet).** Отдельный модуль `out/gpu/libbbnet.so` (`gpu/bbnet`: сетевые и PSN-библиотеки
|
||||
shadPS4 из shadp2p, форка shadPS4, с клиентом shadNet); собирается, когда есть protobuf, nlohmann_json,
|
||||
OpenSSL, zlib и miniupnpc (`shell.nix`), входит в AppImage. Включается в лаунчере, *Мультиплеер → По сети*
|
||||
(`BB_ONLINE=1`): колокола и призывы идут через сервер shadNet с учётной записью shadNet (Online ID и
|
||||
пароль), сообщения, пятна крови и призраки — через игровой сервер. Seamless Hunt (по умолчанию) даёт
|
||||
оба; пул The Hunter's Dream — через bbbridge; или свой сервер (`docs/SERVER.md`); публичный сервер shadPS4 (`srv.shadps4.net:31313`) больше не
|
||||
принимает сборки bbport.
|
||||
Офлайн модуль не загружается; сборка без него играет только офлайн.
|
||||
|
||||
## Устройство репозитория
|
||||
|
||||
| Путь | Содержимое |
|
||||
|---|---|
|
||||
| `src/` | Загрузчик (`probe.c`) и HLE-рантайм |
|
||||
| `scripts/` | Подготовка образа игры, линковка модулей, компилятор патчей |
|
||||
| `gpu/` | Библиотека рендерера: видеоядро shadPS4 с правками порта (`gpu/VENDOR.txt`), прослойки, меню ImGui, рантайм FSR 4.1.1 (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
|
||||
| `launcher/`, `packaging/` | Лаунчер GTK4; пакет Nix и AppImage |
|
||||
| `patches/` | Патчи сообщества для Bloodborne |
|
||||
| `tools/` | Утилиты разработчика: скриптовые запуски, A/B-переключатели, бенчмарк FSR, переписывание шейдеров FSR 4, `fsr4cap` (запись и извлечение FSR 4.1.1) |
|
||||
| `tests/` | Тесты загрузчика, рантайма, патчей и рендерера |
|
||||
| `docs/` | Заметки и замеры ([апскейлер](docs/upscaler.md), [многопоточный GPU](docs/parallel_gpu.md), [векторы движения](docs/motion_vectors.md), [план](docs/ROADMAP.md), [журнал разработки](docs/DEVELOPMENT_LOG.ru.md)) |
|
||||
|
||||
Тесты: `bash build.sh --test`, `python3 -m unittest discover -s tests` и
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
|
||||
|
||||
## Что будет дальше
|
||||
|
||||
- Новая модель памяти и трансляции по умолчанию и на NVIDIA, когда она станет там не медленнее;
|
||||
после этого старая модель удаляется.
|
||||
- Текстуры — объекты, которые создаются при загрузке (меньше угадывания по адресам, загрузка
|
||||
вне кадра).
|
||||
- Шейдеры переводятся заранее, при установке, а не во время игры.
|
||||
- Больше параллельности CPU в обработке GPU-команд (дальнейшее разделение стадии записи draw)
|
||||
с масштабированием на все потоки — важнее всего для Steam Deck.
|
||||
- Async compute для апскейлера (в 4K кадр упирается в GPU).
|
||||
- XeSS (суперразрешение) и генерация кадров XeFG через помощник под Wine с общей памятью Vulkan
|
||||
(прототип моста памяти — `tools/bridge_helper`); входы апскейлера
|
||||
в виде, совместимом с подходом OptiScaler.
|
||||
- Генерация кадров (сначала FSR 3.1 FG), маски реактивности и прозрачности для частиц и тумана.
|
||||
- Исправление гонок в шейдерах FSR 4.1.1 от AMD при ширине вывода, не кратной 64 (например,
|
||||
1600×900) — так же, как уже исправлена гонка у левого края в FSR 4 v07 при 1080p.
|
||||
- Проверка AppImage на Steam Deck; вывод HDR.
|
||||
|
||||
## Благодарности и лицензии
|
||||
|
||||
bbport распространяется по **GNU GPL v2 или новее** ([LICENSE](LICENSE)) — в нём код shadPS4
|
||||
(GPL-2.0-or-later). Сторонние компоненты сохраняют свои лицензии:
|
||||
видеоядро и рекомпилятор шейдеров [shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+),
|
||||
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
|
||||
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) от FireBurn (MIT; FSR 3.1 на Vulkan и
|
||||
провайдер FSR 4 v07), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
|
||||
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), шрифты DejaVu,
|
||||
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, для сборки ассетов FSR 4.1.1).
|
||||
Патчи игры — Kyo, Lance McDonald, auser1337, illusion, emoose и другие участники сообщества
|
||||
(`patches/Bloodborne.xml`). Мост DLSS (`gpu/dlss_bridge`, MIT) и загрузчик взяты из
|
||||
[Windows-порта Supermedo](https://github.com/Supermedo/bloodborne_pc), адаптированного из
|
||||
[IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
|
||||
DLL и данные модели FSR 4 от AMD, а также SDK и библиотеки DLSS от NVIDIA здесь не
|
||||
распространяются.
|
||||
@@ -1,281 +1,622 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
<https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
0. Definitions.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
1. Source Code.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
2. Basic Permissions.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
14. Revised Versions of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
NO WARRANTY
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
@@ -287,15 +628,15 @@ free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
@@ -303,36 +644,31 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Moe Ghoul>, 1 April 1989
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
[](#features)
|
||||
[](https://ko-fi.com/ayozetr)
|
||||
|
||||
@@ -17,6 +17,9 @@ projects ([credits](docs/CREDITS.md)).
|
||||
> 1.09, from a retail release ([which ones](docs/COMPATIBILITY.md)). This project is not
|
||||
> affiliated with Sony Interactive Entertainment or FromSoftware.
|
||||
|
||||
**[The players' guide](https://ayozetr.github.io/bloodborne_linux/)**: installing, playing online and
|
||||
seamless co-op, the Steam Deck, and what to do when something goes wrong.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiplayer**: online messages, bloodstains, ghosts, bells and summons, on
|
||||
@@ -129,10 +132,13 @@ to some people. If it's useful to you and you'd like to support it, you can on
|
||||
|
||||
Built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28, with
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4)'s renderer, and with work from many people and
|
||||
projects: everyone is in [docs/CREDITS.md](docs/CREDITS.md). Components and their licenses:
|
||||
projects: everyone is in [docs/CREDITS.md](docs/CREDITS.md). It includes code from
|
||||
Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine). Components and their licenses:
|
||||
[BBPORT.md](BBPORT.md#credits-and-licenses).
|
||||
|
||||
## License
|
||||
|
||||
GNU GPL v2 or later ([LICENSE](LICENSE)). Third-party components keep their own licenses. NVIDIA
|
||||
GNU GPL v3 or later ([LICENSE](LICENSE)). Files taken from other projects keep their own notices:
|
||||
most are GPL-2.0-or-later (bbport, shadPS4), which allows them to be distributed under
|
||||
GPL-3.0-or-later. Third-party components keep their own licenses. NVIDIA
|
||||
and DLSS are trademarks of NVIDIA Corporation.
|
||||
+1
-1
@@ -16,5 +16,5 @@ bbbridge.exe
|
||||
out/
|
||||
__pycache__/
|
||||
|
||||
# Release packages (tools\make_release.ps1)
|
||||
# Release packages
|
||||
dist/
|
||||
+44
-83
@@ -1,92 +1,53 @@
|
||||
# bbbridge (C++)
|
||||
# bbbridge
|
||||
|
||||
Play **co-op with bbhost players** on **The Hunter's Dream** while using
|
||||
**bbport** (Supermedo's `bloodborne_pc` for Windows, DLSS / FSR 3.1 / FSR 4).
|
||||
Co-op on **The Hunter's Dream** pool, with players of [bbhost](https://github.com/droogie/bbhost).
|
||||
|
||||
bbbridge looks like a shadNet server to bbport and talks to The Hunter's Dream
|
||||
the way bbhost does: matching (rooms, bells, summons), the P2P traffic between
|
||||
the two clients (different packet framing, bbport's signaling handshake,
|
||||
The Hunter's Dream's STUN and relay), and the game's own server traffic
|
||||
(account token, play log for the live map). bbport itself is not modified.
|
||||
The game speaks shadNet, the online service shadPS4 uses; The Hunter's Dream speaks bbhost's
|
||||
protocol. bbbridge sits between them on your own computer: to the game it is a shadNet server, and
|
||||
to The Hunter's Dream it is one more bbhost client. It carries the matchmaking (rooms, bells,
|
||||
summons), the traffic between the two games (with The Hunter's Dream's STUN and relay, so no router
|
||||
port has to be opened) and the account. The game itself is not changed.
|
||||
|
||||
## Layout
|
||||
bbbridge is by [MaskZeus](https://github.com/MaskZeus/bbbridge). This folder holds its source, built
|
||||
for Linux.
|
||||
|
||||
## How the port uses it
|
||||
|
||||
You never start it yourself:
|
||||
|
||||
1. In the launcher's **Multiplayer → Online** tab, choose **The Hunter's Dream (with bbhost players)**.
|
||||
2. Sign in with Discord (the launcher shows a code and opens The Hunter's Dream's page), or make an
|
||||
account without Discord with **Create**, and keep the recovery code it shows.
|
||||
3. Press **Play**: `run.sh` starts `bbbridge-cli` for that game and stops it when the game closes.
|
||||
|
||||
The account and the bridge's log are in `user/bbbridge/`. More in
|
||||
[docs/ONLINE.md](../docs/ONLINE.md#the-hunters-dream-with-bbhosts-players).
|
||||
|
||||
> This connection is unofficial: The Hunter's Dream's team has not approved bbbridge and does not
|
||||
> support this port. Report problems in this repository's
|
||||
> [issues](https://github.com/ayozetr/bloodborne_linux/issues).
|
||||
|
||||
## Building and testing
|
||||
|
||||
`build.sh` builds it with the rest of the port, into `out/bbbridge/`. On its own:
|
||||
|
||||
```sh
|
||||
cmake -S bbbridge -B out/bbbridge -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
ninja -C out/bbbridge bbbridge-cli bbbridge-unit
|
||||
out/bbbridge/bbbridge-unit # framing, signaling, STUN, protobuf
|
||||
python3 bbbridge/tests/e2e/test_e2e.py out/bbbridge/bbbridge-cli # against fake servers, offline
|
||||
```
|
||||
E:\bloodborne\ bbport
|
||||
└─ bbbridge\ this folder
|
||||
├─ bbbridge.exe the app (built)
|
||||
├─ bbbridge_config.json settings (optional)
|
||||
├─ bbbridge_state.json your account: token + recovery code - never share it
|
||||
├─ bbbridge.log log of the last run (previous: bbbridge.prev.log)
|
||||
├─ src\ C++ sources
|
||||
├─ tests\ unit tests (C++) and end-to-end tests (Python)
|
||||
├─ third_party\ nlohmann/json 3.11.3, Dear ImGui 1.91.5
|
||||
└─ bin\ bbbridge-cli.exe (headless), bbbridge-unit.exe
|
||||
```
|
||||
<img width="798" height="718" alt="resim" src="https://github.com/user-attachments/assets/672a0038-4ead-4572-9293-b6c3936ed128" />
|
||||
|
||||
| Folder | Contents |
|
||||
|---|---|
|
||||
| `src/` | The bridge (`bridge.cpp`, `p2p.cpp`, `bbhost_client.cpp`…) and `main_cli.cpp`, the program the port runs |
|
||||
| `tests/` | Unit tests (C++) and end-to-end tests (Python) |
|
||||
| `third_party/` | nlohmann/json and Dear ImGui (MIT) |
|
||||
|
||||
## Using it
|
||||
`src/app.cpp` is bbbridge's own window, which only builds on Windows; the port does not use it.
|
||||
|
||||
1. Put the `bbbridge` folder inside bbport's folder (next to `Bloodborne.exe`)
|
||||
and start `bbbridge.exe` (close the bbport launcher first). bbbridge uses
|
||||
the bbport it sits in; anywhere else it asks which bbport to use
|
||||
(**Change...** picks another one later).
|
||||
2. **Sign in with Discord**: the app shows a code and opens The Hunter's Dream;
|
||||
sign in there with your Discord account and approve the code. Your account
|
||||
there (the same one bbhost uses) becomes bbport's online id. No password is
|
||||
stored; the account's token goes to `bbbridge_state.json`.
|
||||
3. The **Online** switch goes to **The Hunter's Dream (bbhost)** by itself
|
||||
once you are signed in (and the live map patch is added; untick it if you
|
||||
like).
|
||||
4. Press **PLAY**. The app starts the bridge if needed and launches the game
|
||||
with bbport's saved settings. You can also start the game from the bbport
|
||||
launcher - as long as bbbridge is open. Graphics, mods and cheats stay in
|
||||
the bbport launcher (**bbport settings...**) or the in-game menu (Insert).
|
||||
## License
|
||||
|
||||
Move the **Online** switch to **shadNet** to go back to bbport's own shadNet
|
||||
co-op pool: bbport gets its own server and shadNet account back (the ones it
|
||||
had before; a shadNet account is registered at
|
||||
[shadnet.shadps4.net](https://shadnet.shadps4.net) and entered in the bbport
|
||||
launcher's *Online co-op*). PLAY follows the switch: right, the bbhost pool
|
||||
with your Discord sign-in; left, bbport's own shadNet - no sign-in needed.
|
||||
Running bbhost and bbport with the same account at the same time is not
|
||||
supported.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Failed to acquire Bloodborne server information" with bbbridge closed**:
|
||||
bbbridge 0.3.1 and older (and the Python 0.2.0) left
|
||||
`user\host_overrides.custom.json` in bbport's folder (bbport's user folder),
|
||||
which sends the game's server traffic to bbbridge even when it is not
|
||||
running. Start bbbridge 0.3.2 or newer once (it removes the file when it is
|
||||
not needed), or delete that file by hand. From 0.3.2 the file exists only
|
||||
while bbbridge runs with the switch on The Hunter's Dream.
|
||||
- **Nobody comes when I ring a bell**: a Small Resonant Bell is answered only
|
||||
by a host ringing the Beckoning Bell in the same area, near your level (and
|
||||
the other way round). The live map at
|
||||
[thehuntersdream.com/map](https://thehuntersdream.com/map) shows who is
|
||||
ringing which bell where.
|
||||
|
||||
## Building (Visual Studio 2022/2026)
|
||||
|
||||
Workload **Desktop development with C++** (MSVC, Windows SDK, CMake tools).
|
||||
|
||||
- Visual Studio: *File > Open > Folder...* this folder, pick the **x64 Release**
|
||||
preset, *Build > Build All*. `bbbridge.exe` is copied here, the CLI and the
|
||||
unit tests to `bin\`.
|
||||
- Command line (Developer PowerShell):
|
||||
`cmake --preset x64-release` then `cmake --build --preset x64-release`
|
||||
|
||||
## Tests
|
||||
|
||||
- `bin\bbbridge-unit.exe` - framing, signaling, STUN, protobuf, account id.
|
||||
- `python tests\e2e\test_e2e.py` - the bridge exe against a fake bbhost
|
||||
server, a fake STUN/relay server and fake game sockets (no internet).
|
||||
|
||||
## Credits and license
|
||||
|
||||
The protocol comes from **bbhost** and The Hunter's Dream by droogie (GPL-3.0);
|
||||
**bloodborne_pc / bbport** by Supermedo on deadinside28's port and shadPS4.
|
||||
bbbridge is GPL-3.0-or-later. nlohmann/json and Dear ImGui are MIT-licensed.
|
||||
Not affiliated with Sony, FromSoftware, Supermedo or The Hunter's Dream.
|
||||
GPL-3.0-or-later (`LICENSE`); nlohmann/json and Dear ImGui are MIT (`THIRD_PARTY_NOTICES.txt`).
|
||||
The protocol comes from bbhost and The Hunter's Dream by droogie. Not affiliated with Sony,
|
||||
FromSoftware or The Hunter's Dream.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Builds the release and packs what players download: dist\bbbridge-<version>-windows.zip
|
||||
# holding a "bbbridge" folder to drop into bbport's folder (next to Bloodborne.exe).
|
||||
# Never includes an account file, logs or build output.
|
||||
# powershell -ExecutionPolicy Bypass -File tools\make_release.ps1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$vc = 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat'
|
||||
if (-not (Test-Path $vc)) {
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
$vc = Join-Path (& $vswhere -latest -products * -property installationPath) 'VC\Auxiliary\Build\vcvars64.bat'
|
||||
}
|
||||
cmd /c "`"$vc`" >nul && cd /d `"$root`" && cmake --preset x64-release >nul && cmake --build --preset x64-release --target bbbridge"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'build failed' }
|
||||
|
||||
$version = (Select-String -Path "$root\CMakeLists.txt" -Pattern 'project\(bbbridge VERSION ([0-9.]+)').Matches[0].Groups[1].Value
|
||||
$stage = Join-Path $root "dist\stage\bbbridge"
|
||||
Remove-Item (Join-Path $root 'dist\stage') -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force $stage | Out-Null
|
||||
Copy-Item "$root\build\x64-release\bbbridge.exe", "$root\README.md", "$root\CHANGELOG.md", "$root\LICENSE", "$root\THIRD_PARTY_NOTICES.txt" $stage
|
||||
$zip = Join-Path $root "dist\bbbridge-$version-windows.zip"
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $stage -DestinationPath $zip
|
||||
Remove-Item (Join-Path $root 'dist\stage') -Recurse -Force
|
||||
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
"$hash $(Split-Path -Leaf $zip)" | Set-Content -Encoding ascii "$zip.sha256"
|
||||
Write-Output "$zip ($([math]::Round((Get-Item $zip).Length / 1MB, 2)) MB) sha256 $hash"
|
||||
+68
-4
@@ -1,8 +1,8 @@
|
||||
# bbport's changes to BBSC
|
||||
|
||||
This folder holds [BBSC](https://github.com/Classmg22/BBSC) 0.0.1 by CLS_AMG (GPL-2.0-or-later),
|
||||
as published at commit 315f76c, with the changes below made for the Linux port of bbport. Changed
|
||||
lines in BBSC's own files are marked `bbport:` in comments.
|
||||
This folder holds [BBSC](https://github.com/Classmg22/BBSC) 0.0.2 by CLS_AMG (GPL-2.0-or-later),
|
||||
as published at commit 4f11faf (tag v0.0.2; 0.0.1 was commit 315f76c), with the changes below made
|
||||
for the Linux port of bbport. Changed lines in BBSC's own files are marked `bbport:` in comments.
|
||||
|
||||
## 2026-10-15
|
||||
|
||||
@@ -61,4 +61,68 @@ lines in BBSC's own files are marked `bbport:` in comments.
|
||||
instead of 15, one host that close, 3 checks in a row), and "the only one hosting on the
|
||||
server" is gone: it was a stranger whenever the real host was not on the relay;
|
||||
- the hosts that rang are forgotten when the session ends.
|
||||
BBSC 0.0.2's timeline log is not taken.
|
||||
BBSC 0.0.2's timeline log was not taken then (it is now, see "BBSC 0.0.2" below).
|
||||
|
||||
## 2026-10-21: BBSC 0.0.2
|
||||
|
||||
The rest of BBSC 0.0.2 (commit 4f11faf), merged with BBSC's own changes from 0.0.1 kept as they
|
||||
are, and bbport's changes above kept on top of them.
|
||||
|
||||
- **Taken as published:** `CREDITS.txt`, `SUPPORTERS.txt`, `emulator/mod/mod.json` (version 0.0.2,
|
||||
the new "Bells anywhere" option, the Discord option's text), `emulator/mod/bbsc_channel.cpp`,
|
||||
`emulator/mod/bbsc_discord.cpp`, `emulator/mod/bbsc_mod.cpp` (both Windows only, not built
|
||||
here), `emulator/src/core/bbsc_cinematic.{h,cpp}` (not built here), `bbsc_patch_tables.h`,
|
||||
`bbsc_persistence.h` (the new default-on switches `SHADPS4_BBSC_AREA_LIST_CAP`,
|
||||
`SHADPS4_BBSC_WORLD_SYNC_ALL`, `SHADPS4_BBSC_TIMELINE`, `SHADPS4_BBSC_BELLS_ANYWHERE`), the
|
||||
shim's `np_signaling_bbsc.h` and `server/gameserver/`.
|
||||
- **Taken with bbport's changes kept:** `emulator/src/core/bloodborne_re.{h,cpp}` (the watch
|
||||
camera of a dead guest, the kit's bells anywhere, the host's time of night for the guest, the
|
||||
area list cap against "Panic 265", the boss, kick and drop fixes, the kit from the game's own
|
||||
tables, the frame rate and Discord data, the timeline), `emulator/mod/bbsc_http.cpp` and
|
||||
`emulator/mod/bbsc_relay.cpp` (the relay's write queue).
|
||||
- **The aimed placement and the host lock** (2026-10-19 and the first 2026-10-21 entry above) are
|
||||
now BBSC's own code, under its names: `OnAimedHostPlacement`, `RelaySendAimedPlacement`,
|
||||
`AimedPlacementType` (0x31), `own_user_id` / `placement_aimed`, and the claim told from the
|
||||
search by URL (`/summon_messenger/request` vs `/get`). The wire format is unchanged. Gone with
|
||||
them: bbport's own names (`OnDirectedPlacement`, `RelaySendPlacementTo`, `PlacementToType`) and
|
||||
its reading of a `UserId` / `TargetUserId` sent as a string.
|
||||
- **The co-op kit, late** (2026-10-15) stays: 0.0.2 first looks in the game's own tables for 3
|
||||
minutes; the search of every copy behind it keeps going after 3 minutes as before.
|
||||
- **Linux:**
|
||||
- `emulator/mod/linux/bbsc_mod_linux.cpp`: version 0.0.2; the import slot reads and writes
|
||||
0.0.2 calls `ReadSlotSafely` / `WriteSlotSafely` (through bbport's map of the game's memory);
|
||||
the timeline's start lines; the timeline file goes next to the log
|
||||
(`<user dir>/bbsc/logs/bbsc-timeline-*.txt`), and only the newest 5 are kept, as the logs.
|
||||
- **Watch camera:** the F1 free camera stays out of the Linux build (every key, button and the
|
||||
mouse also reach the game through bbport's pad, so flying it would move the hunter; L3+R3 is
|
||||
bbport's menu). The watch camera of 0.0.2 only comes on by itself while our hunter is dead in
|
||||
co-op, when those inputs do nothing in the game, so it is in: `bbsc_mod_linux.cpp` reads its
|
||||
keys through SDL like the F8 key, on the game thread and only while it is on: Tab / E / left
|
||||
click next hunter, Shift+Tab / Q previous, F fly freely and back; the controller's R1 / L1 and
|
||||
sticks; arrow keys and W / S. F1 turns it off for the rest of that death. The mouse's movement
|
||||
is not read (it is bbport's mouse look). Switches `SHADPS4_BBSC_CINEMATIC` and
|
||||
`SHADPS4_BBSC_SPECTATE` as on Windows.
|
||||
- `emulator/mod/bbsc_http.cpp`: 0.0.2's overrides file list (`HostOverridesFiles`) is Windows
|
||||
only; on Linux the online module still applies the overrides, and the relay keeps its own list.
|
||||
- `emulator/mod/bbsc_relay.cpp`: a send to a closed link fails instead of raising SIGPIPE
|
||||
(`MSG_NOSIGNAL`), and the writer's 10 s send timeout is set as a `timeval`.
|
||||
- `emulator/mod/linux/bbsc_win32.h`: `WSAGetLastError` and `SD_BOTH`, which the relay uses now.
|
||||
- **Translations:** the new texts ("An item was added to your items." and the watch camera's) in
|
||||
every language. `bbsc_i18n.cpp` also translates a text with numbers through its `{n}` /
|
||||
`{count}` form ("Watching hunter 1 of 2.").
|
||||
- **Not taken:**
|
||||
- BBSC's Discord status (`bbsc_discord.cpp`): bbport has its own; the build and boss timer data
|
||||
are gathered but not shown.
|
||||
- the files of BBSC's own emulator build (`emulator/src/core/bbsc_relay_client.cpp`, the
|
||||
emulator's network, signaling, pad and module files, `emulator/CMakeLists.txt`, the launcher's
|
||||
`emulator_settings.h`) and the patch for BBport's Windows build (`bbport/`): bbport builds
|
||||
none of them, and the game logic here does not need them.
|
||||
|
||||
## 2026-10-23
|
||||
|
||||
- **Documentation.** `README.md`, `server/README.md` (in place of `server/README.txt`) and
|
||||
`server/gameserver/README.md` describe BBSC as this port builds, runs and hosts it.
|
||||
- **Windows host package removed.** The `.cmd` scripts and `server/tools/*.ps1` set up BBSC's
|
||||
Windows server package; this port hosts with `tools/shadnet-server`, `tools/bbsc-server` and
|
||||
`docs/SERVER.md`.
|
||||
- **SUPPORTERS.txt** keeps its heading and list, without the donation links.
|
||||
+3
-1
@@ -20,7 +20,9 @@ SPECIAL THANKS
|
||||
split-screen Bloodborne on one PC; BBSC runs in it
|
||||
https://github.com/framilano/BloodborneNucleusCoopHandler
|
||||
Fahad (fhody125) - No Dream For The Hunter
|
||||
the game server and ss.info groundwork BBSC's servers grew from
|
||||
the game server and ss.info groundwork BBSC's servers grew from,
|
||||
and the fixed Chalice Dungeon data (NoDreamForTheHunterBB, published by Mob)
|
||||
https://github.com/Mob100percent/NoDreamForTheHunterBB
|
||||
|
||||
BUILT ON
|
||||
shadPS4 - the PS4 emulator (shadPS4 team and contributors, GPL-2.0)
|
||||
|
||||
+47
-74
@@ -1,90 +1,63 @@
|
||||
# Bloodborne Seamless Co-op (BBSC)
|
||||
# BBSC: Bloodborne Seamless Co-op
|
||||
|
||||
Play the whole of Bloodborne together. BBSC is a mod for the [shadPS4](https://github.com/shadps4-emu/shadPS4)
|
||||
emulator that keeps a co-op session alive through deaths, lamps, area changes, bosses and the Hunter's
|
||||
Dream - up to five hunters, each keeping their own character.
|
||||
[BBSC](https://github.com/Classmg22/BBSC) by CLS_AMG keeps a co-op session together through deaths,
|
||||
lamps, area changes, bosses and the Hunter's Dream, for up to five hunters who each keep their own
|
||||
character. This folder holds BBSC 0.0.2 built for this port: the port's runtime loads it into the
|
||||
game as `libbbsc.so`.
|
||||
|
||||
**Downloads:** Nexus Mods (link coming with the release) · **Community:** [Discord](https://discord.gg/Df7BCmfWdt)
|
||||
## What it does
|
||||
|
||||
> 0.0.1 is the first public test. Back up your saves before playing.
|
||||
- The session survives deaths, lamps, areas, bosses and trips to the Hunter's Dream; guests follow
|
||||
the host and come back with the group.
|
||||
- Up to five hunters: the host and four guests.
|
||||
- Everyone keeps their own levels, echoes and items; the host's boss kills and shortcuts can be
|
||||
shared with the guests' worlds.
|
||||
- Enemies get stronger for every guest.
|
||||
- A guest who dies watches a partner until the group comes back, sees the host's time of night,
|
||||
and can ring the co-op bells anywhere.
|
||||
- Its own bells, with no Insight cost: the **Hunt Bell** (call others), the **Kin Bell** (join
|
||||
someone), the **Bloodlust Bell** (invade), the **Duel Bell** (PvP on or off) and the **Parting
|
||||
Blank** (end the session).
|
||||
|
||||
## Features
|
||||
## Using it
|
||||
|
||||
- One co-op session that lasts through deaths, lamps, area changes, bosses and the Hunter's Dream
|
||||
- Up to 5 hunters: the host and 4 guests
|
||||
- Guests follow the host between areas and lamps and respawn with the group
|
||||
- Everyone keeps their own levels, echoes and items
|
||||
- Shared progress (optional): the host's boss kills and shortcuts are added to the guest's world
|
||||
- Guests get boss rewards and enemy drops; items already picked up do not come back
|
||||
- Enemy scaling by the number of hunters
|
||||
- A co-op kit with no Insight cost: Hunt Bell, Kin Bell, Bloodlust Bell, Parting Blank, Duel Bell (PvP toggle)
|
||||
- Clear co-op messages in place of the vanilla "beckoner / resonance" lines
|
||||
- Public server, self-hosted servers (Radmin VPN, Tailscale, LAN, VPS) and split screen (Nucleus Co-op)
|
||||
- Runs in shadPS4 and in the BBport Windows build
|
||||
In the launcher's **Multiplayer → Online** tab, turn on BBSC in the **Seamless co-op (BBSC)** group;
|
||||
**BBSC settings** holds its options. Every player in the session needs BBSC, the same version. How much of it works
|
||||
depends on the server (Seamless Hunt, a group's own server, or The Hunter's Dream): see
|
||||
[docs/ONLINE.md](../docs/ONLINE.md#seamless-co-op-bbsc). To host a server for your group, see
|
||||
[server/](server/README.md).
|
||||
|
||||
## Requirements
|
||||
## What is in this folder
|
||||
|
||||
- Your own Bloodborne **CUSA03173, version 01.09** - no game files are included
|
||||
- Windows (the server also runs on Linux)
|
||||
|
||||
## Downloads
|
||||
|
||||
| File | For |
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `BBSC-<version>-Play.zip` | Players: launcher + emulator + BBSC, connects to the public server |
|
||||
| `BBSC-<version>-Server.zip` | Hosting your own server (Windows) |
|
||||
| `BBSC-<version>-Nucleus.zip` | Split screen with the [Bloodborne Nucleus Co-op Handler](https://github.com/framilano/BloodborneNucleusCoopHandler) |
|
||||
| `BBSC-<version>-BBport.zip` | Test build for the [BBport Windows build](https://github.com/Supermedo/bloodborne_pc) - same server as shadPS4 players |
|
||||
| `emulator/src/core/` | BBSC's game code (`bloodborne_re.cpp`: the session, the bells, world sync) |
|
||||
| `emulator/mod/` | The mod around it: relay, network channel, HTTP, settings (`mod.json`) |
|
||||
| `emulator/mod/linux/` | The Linux side, written for this port: start-up, hooks, the Win32 names the mod uses |
|
||||
| `server/` | Hosting a server: BBSC's game server and shadNet's settings |
|
||||
| `translations/` | BBSC's texts in the game's languages |
|
||||
| `third_party/` | fmt and xbyak |
|
||||
| `CHANGES-bbport.md` | Every change this port made to BBSC, by date |
|
||||
|
||||
## Quick start
|
||||
`build.sh` builds it into `out/bbsc/libbbsc.so`; on its own:
|
||||
|
||||
1. Unzip the Play package and keep its files together.
|
||||
2. Run `PLAY BBSC.cmd`. Your shadPS4 settings, patches and saves are used as they are.
|
||||
3. Launcher > Settings > Accounts > Create Account on Server.
|
||||
4. Host rings the Hunt Bell, guests ring the Kin Bell.
|
||||
```sh
|
||||
cmake -S bbsc -B out/bbsc -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
ninja -C out/bbsc
|
||||
```
|
||||
|
||||
## Repository layout
|
||||
`LICENSE.txt`, `NOTICE.txt` and `SUPPORTERS.txt` are copied next to `libbbsc.so`, and BBSC stays off
|
||||
without them, as it does in its own builds.
|
||||
|
||||
| Folder | Contents |
|
||||
|---|---|
|
||||
| `emulator/` | The shadPS4 fork (shadp2p based). BBSC game code: `src/core/bloodborne_re.cpp`; the mod DLL: `mod/` |
|
||||
| `launcher/` | The Qt launcher fork (Mods and Accounts tabs) |
|
||||
| `server/` | Host package scripts and the BBSC game server (`bbsc_gameserver.py`) |
|
||||
| `tools/` | Game data readers and helper tools |
|
||||
| `docs/` | Architecture, world sync, networking, known limitations, changelog |
|
||||
## Reporting problems
|
||||
|
||||
## Building
|
||||
|
||||
- Emulator: `build/build-shadp2p.cmd` (Windows, Visual Studio 2022, CMake, Ninja)
|
||||
- Mod DLL: `emulator/mod/build-mod.cmd`
|
||||
- Game server: Python 3, no extra packages (`server/gameserver/bbsc_gameserver.py --shadnet <address>`)
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Post in `#bug-reports` on the [Discord](https://discord.gg/Df7BCmfWdt) with the version, host or guest,
|
||||
what happened and when, and both logs: the BBSC log (`mods/CUSA03173/bbsc/logs/`) and the emulator log
|
||||
(`user/log/shad_log.txt` or `%APPDATA%\shadPS4\log\`).
|
||||
|
||||
## Credits
|
||||
|
||||
- **CLS_AMG** - BBSC
|
||||
- **framilano (fra98_)** - Bloodborne Nucleus Co-op Handler; tester
|
||||
- **Wozzardman** - [shadp2p](https://github.com/Wozzardman/shadp2p) and shadnet-p2p, the online fork BBSC is built on
|
||||
- **Fahad (fhody125)** - No Dream For The Hunter, the game server groundwork (MIT)
|
||||
- **shadPS4 team and contributors** - the emulator; **shadNet team** - shadNet
|
||||
- **The Hunter's Dream team**
|
||||
- **deadinside28** (bbport) and **Supermedo** (its Windows build)
|
||||
- **Testers** - Hardwater, Onesadeye, aboood888848, Fahad, Mob, lelial, J2mF, Kukar3x
|
||||
- Supporters: see `SUPPORTERS.txt`
|
||||
|
||||
## Support
|
||||
|
||||
BBSC is free for everyone. If you want to support the time that goes into it:
|
||||
[Patreon](https://www.patreon.com/c/CLS_AMG) or [Buy Me a Coffee](https://buymeacoffee.com/j2mf).
|
||||
In this repository's [issues](https://github.com/ayozetr/bloodborne_linux/issues), with the game's
|
||||
log from the launcher's **Log** page. Problems that also happen in BBSC's own builds belong in
|
||||
[BBSC's repository](https://github.com/Classmg22/BBSC).
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0-or-later, like shadPS4. See `LICENSE` and `NOTICE.md`. Third-party parts keep their own licenses.
|
||||
|
||||
Bloodborne is developed by FromSoftware and published by Sony Interactive Entertainment. BBSC is a free
|
||||
fan project and is not affiliated with or endorsed by either.
|
||||
GPL-2.0-or-later (`LICENSE`, `NOTICE.md`); the third-party parts keep their own licenses. BBSC's
|
||||
credits are in `CREDITS.txt`, and this port's in [docs/CREDITS.md](../docs/CREDITS.md).
|
||||
Bloodborne is by FromSoftware and Sony Interactive Entertainment; BBSC is a fan project, not
|
||||
affiliated with either.
|
||||
+3
-6
@@ -4,19 +4,16 @@ BBSC SUPPORTERS
|
||||
BBSC is free for everyone, and every build and feature reaches everyone on the same day.
|
||||
These people support the time that goes into it. Thank you.
|
||||
|
||||
Support BBSC:
|
||||
- Patreon: https://www.patreon.com/c/CLS_AMG
|
||||
- Buy Me a Coffee: https://buymeacoffee.com/j2mf
|
||||
|
||||
|
||||
WORKSHOP
|
||||
--------
|
||||
(no one yet)
|
||||
aboood888848
|
||||
Prohorov Ilja
|
||||
|
||||
|
||||
HUNTER
|
||||
------
|
||||
(no one yet)
|
||||
.ζ͜长𐌏𐌕𐌏ᚱ𐌆
|
||||
|
||||
|
||||
LAMPLIGHT
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
|
||||
namespace BbscMod {
|
||||
u64 EbootAddress(); // bbsc_mod.cpp
|
||||
bool ReadSlotSafely(u64 address, u64* value); // bbsc_mod.cpp: no fault if unreadable
|
||||
bool WriteSlotSafely(u64 address, u64 value);
|
||||
void GuardSlot(u64 slot_offset, u64 wrapper, void** original, const char* name);
|
||||
void RelayDispatch(const std::vector<u8>& wrapped); // bbsc_relay.cpp
|
||||
} // namespace BbscMod
|
||||
@@ -174,17 +176,16 @@ s32 PS4_SYSV_ABI ConnectionStatus(s32 ctx, s32 conn, u32* status, u32* addr, u16
|
||||
|
||||
template <typename Fn>
|
||||
bool Swap(u64 slot_offset, Fn wrapper, Fn& original, const char* name) {
|
||||
auto* slot = reinterpret_cast<u64*>(BbscMod::EbootAddress() + slot_offset);
|
||||
const u64 slot = BbscMod::EbootAddress() + slot_offset;
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
const u64 value = *slot;
|
||||
if (value != 0 &&
|
||||
u64 value = 0;
|
||||
if (BbscMod::ReadSlotSafely(slot, &value) && value != 0 &&
|
||||
(value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
original = reinterpret_cast<Fn>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(slot, 8, PAGE_READWRITE, &old);
|
||||
*slot = reinterpret_cast<u64>(wrapper);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(slot, 8, old, &ignored);
|
||||
if (!BbscMod::WriteSlotSafely(slot, reinterpret_cast<u64>(wrapper))) {
|
||||
Sleep(1);
|
||||
continue; // page not writable right now: try again
|
||||
}
|
||||
BbscMod::GuardSlot(slot_offset, reinterpret_cast<u64>(wrapper),
|
||||
reinterpret_cast<void**>(&original), name);
|
||||
return true;
|
||||
@@ -196,8 +197,9 @@ bool Swap(u64 slot_offset, Fn wrapper, Fn& original, const char* name) {
|
||||
|
||||
template <typename Fn>
|
||||
Fn ReadSlot(u64 slot_offset) {
|
||||
const u64 value = *reinterpret_cast<const u64*>(BbscMod::EbootAddress() + slot_offset);
|
||||
if (value == 0 || (value >= BbscMod::EbootAddress() && value < BbscMod::EbootAddress() + EbootSize)) {
|
||||
u64 value = 0;
|
||||
if (!BbscMod::ReadSlotSafely(BbscMod::EbootAddress() + slot_offset, &value) || value == 0 ||
|
||||
(value >= BbscMod::EbootAddress() && value < BbscMod::EbootAddress() + EbootSize)) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<Fn>(value);
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// BBSC Discord status: "Playing BBSC - In Central Yharnam - Hosting (2 of 5)", or the boss during a
|
||||
// boss fight. Talks to the Discord app's local pipe (\\.\pipe\discord-ipc-N) itself - no Discord
|
||||
// library: a frame is {u32 opcode, u32 length, JSON}; opcode 0 = handshake with the application ID,
|
||||
// opcode 1 = a command (SET_ACTIVITY). The game state comes from bloodborne_re.cpp's game tick
|
||||
// BBSC Discord status: "Playing BBSC - In Central Yharnam - Hosting - Arcane build (2 of 5)", the
|
||||
// boss and a fight timer during a boss fight, "Dead - watching hunter 1 of 2" while spectating.
|
||||
// Talks to the Discord app's local pipe (\\.\pipe\discord-ipc-N) itself - no Discord library: a
|
||||
// frame is {u32 opcode, u32 length, JSON}; opcode 0 = handshake with the application ID, opcode 1 =
|
||||
// a command (SET_ACTIVITY). The game state comes from bloodborne_re.cpp's game tick
|
||||
// (GetBbscPresence). Off with the Mods tab switch "Discord status" (SHADPS4_BBSC_DISCORD=0).
|
||||
// No Discord running = nothing happens; it tries again every 20 s.
|
||||
// 2026-10-20: compiled into the emulator build too (before, only the mod showed it - the shadPS4
|
||||
// package had only shadPS4's own "Playing Bloodborne"); the emulator then leaves its own status off.
|
||||
|
||||
#include "core/bloodborne_re.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "core/bloodborne_re.h"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
}
|
||||
#include "common/logging/log.h"
|
||||
#include "core/bbsc_persistence.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* DiscordApplicationId = "1554935855674691645"; // "BBSC" (CLS_AMG)
|
||||
|
||||
std::atomic<bool> discord_running{false};
|
||||
|
||||
bool DiscordStatusEnabled() {
|
||||
const char* value = std::getenv("SHADPS4_BBSC_DISCORD");
|
||||
return value == nullptr || (std::string{value} != "0" && std::string{value} != "false");
|
||||
@@ -113,7 +121,7 @@ public:
|
||||
return false;
|
||||
}
|
||||
if (header[0] == 2) { // CLOSE: wrong application ID or Discord shutting down
|
||||
BbscMod::LogPlain(("Discord status: Discord closed the connection: " + body).c_str());
|
||||
LOG_INFO(Debug, "Discord status: Discord closed the connection: {}", body);
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
@@ -144,6 +152,7 @@ std::string ActivityJson(const Core::Bloodborne::BbscPresence& p, std::int64_t g
|
||||
if (!p.in_game) {
|
||||
details = "At the title screen";
|
||||
} else if (!p.boss.empty()) {
|
||||
// The timer counts from when the boss bar appeared (Discord shows it as "mm:ss elapsed").
|
||||
details = "Boss fight: " + p.boss;
|
||||
large_text = "In " + p.area;
|
||||
start = p.boss_since_s;
|
||||
@@ -152,6 +161,20 @@ std::string ActivityJson(const Core::Bloodborne::BbscPresence& p, std::int64_t g
|
||||
}
|
||||
if (p.in_game) {
|
||||
state = p.role == 1 ? "Hosting" : p.role == 2 ? "Co-op guest" : "Solo hunter";
|
||||
if (p.dead) {
|
||||
if (p.spectating && p.watch_count > 0) {
|
||||
state += fmt::format(" - dead, watching hunter {} of {}", p.watching, p.watch_count);
|
||||
} else if (p.spectating) {
|
||||
state += " - dead, spectating";
|
||||
} else {
|
||||
state += " - dead";
|
||||
}
|
||||
} else if (!p.build.empty()) {
|
||||
state += " - " + p.build;
|
||||
}
|
||||
if (!p.build.empty() && (p.dead || !p.boss.empty())) {
|
||||
large_text += " - " + p.build;
|
||||
}
|
||||
}
|
||||
std::string activity = "{\"details\":" + Json(details);
|
||||
if (!state.empty()) {
|
||||
@@ -187,7 +210,7 @@ DWORD WINAPI DiscordThread(LPVOID) {
|
||||
continue;
|
||||
}
|
||||
if (!logged_connect) {
|
||||
BbscMod::LogPlain("Discord status: connected to Discord");
|
||||
LOG_INFO(Debug, "Discord status: connected to Discord");
|
||||
logged_connect = true;
|
||||
}
|
||||
sent.clear();
|
||||
@@ -212,17 +235,53 @@ DWORD WINAPI DiscordThread(LPVOID) {
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace BbscMod {
|
||||
namespace Core::Bbsc {
|
||||
|
||||
void StartDiscordStatus() {
|
||||
bool DiscordStatusRunning() {
|
||||
return discord_running.load();
|
||||
}
|
||||
|
||||
void StartDiscordStatus(std::string_view serial) {
|
||||
#ifndef BBSC_MOD
|
||||
// The emulator build: only for the game BBSC knows, and only while its own BBSC runs (the mod
|
||||
// shows the status itself when bbsc-launch switches these off).
|
||||
if (serial != "CUSA03173" || !SwitchEnabled("SHADPS4_BLOODBORNE_RE_TRACE") ||
|
||||
!SwitchEnabled("SHADPS4_BBSC_PERSIST")) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (!DiscordStatusEnabled()) {
|
||||
LogPlain("Discord status: off (Mods tab)");
|
||||
LOG_INFO(Debug, "Discord status: off (Mods tab)");
|
||||
return;
|
||||
}
|
||||
if (discord_running.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, DiscordThread, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
LogPlain("Discord status: on - shows when the Discord app is running");
|
||||
LOG_INFO(Debug, "Discord status: on - shows when the Discord app is running");
|
||||
} else {
|
||||
discord_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Core::Bbsc
|
||||
|
||||
#else // not Windows: no Discord pipe
|
||||
|
||||
namespace Core::Bbsc {
|
||||
bool DiscordStatusRunning() {
|
||||
return false;
|
||||
}
|
||||
void StartDiscordStatus(std::string_view) {}
|
||||
} // namespace Core::Bbsc
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef BBSC_MOD
|
||||
namespace BbscMod {
|
||||
void StartDiscordStatus() {
|
||||
Core::Bbsc::StartDiscordStatus("CUSA03173");
|
||||
}
|
||||
} // namespace BbscMod
|
||||
#endif
|
||||
+176
-88
@@ -18,10 +18,14 @@
|
||||
// - URLs: the entries of host_overrides.json (same file and format as the emulator's) are
|
||||
// applied here too, so a stock emulator reaches the BBSC game server and summon broker.
|
||||
// - /summon_messenger/*: header X-ShadPS4-Bloodborne-Seamless: 1 (the broker's seamless mode).
|
||||
// - summon search/request (a host looking for signs): the host's placement header, as the
|
||||
// emulator sent it, AND the same text to every other player through the BBSC relay - the game
|
||||
// - summon request (a host ringing one sign): the host's placement header, AND the same text
|
||||
// through the BBSC relay aimed at that sign's UserId only (TargetUserId of the claim) - the game
|
||||
// does not import sceHttpGetAllResponseHeaders, so the guest cannot read it from the broker's
|
||||
// reply; the relay brings it instead (bbsc_relay.cpp -> BbscMod::OnHostPlacement).
|
||||
// reply; the relay brings it instead (bbsc_relay.cpp -> BbscMod::OnAimedHostPlacement).
|
||||
// 0.0.1 hotfix (2026-10-13): the search (/get) goes out unchanged. A search with a placement made the
|
||||
// broker lock every sign outside the host's map ("Preparing") for that host, and the 0.0.1
|
||||
// relay broadcast made waiting guests follow random hosts - on the public server nobody's
|
||||
// Kin Bell sign could be rung.
|
||||
// - summon create (a guest's sign): the AreaId becomes the host's map once a host's placement
|
||||
// is known (connect-first), and the first seamless create of this run sends a forced delete
|
||||
// for the same SessionId/UserId first (stale broker record after a crash).
|
||||
@@ -43,7 +47,9 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
@@ -58,11 +64,16 @@
|
||||
#include "common/types.h"
|
||||
#include "core/bloodborne_re.h"
|
||||
|
||||
// One line in this run's BBSC timeline (bloodborne_re.cpp, "Timeline").
|
||||
#define BBSC_TIMELINE(kind, ...) Core::Bloodborne::BbscTimelineEvent(kind, fmt::format(__VA_ARGS__))
|
||||
|
||||
namespace BbscMod {
|
||||
void RelaySendPlacement(const std::string& placement);
|
||||
void RelaySendPlacementTo(s64 target_user, const std::string& placement); // bbport
|
||||
void RelaySendAimedPlacement(s64 target_user, const std::string& placement);
|
||||
u64 EbootAddress(); // bbsc_mod.cpp: where the game image is (0 until found)
|
||||
bool ReadSlotSafely(u64 address, u64* value); // bbsc_mod.cpp: no fault if unreadable
|
||||
bool WriteSlotSafely(u64 address, u64 value);
|
||||
void GuardSlot(u64 slot_offset, u64 wrapper, void** original, const char* name); // bbsc_mod.cpp
|
||||
std::vector<std::wstring> HostOverridesFiles(); // below: the emulator's overrides file candidates
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -79,9 +90,9 @@ constexpr u64 SlotReadData = 0x053E5668;
|
||||
// works; one that has not after this long is a dead claim.
|
||||
constexpr s64 ClaimJoinWaitMs = 20'000;
|
||||
constexpr s64 PlacementStaleMs = 20'000;
|
||||
// bbport: a placement sent to our own sign by the host that rang it stays longer (BBSC's bbsc.dll
|
||||
// of 2026-10-17: 90 s), as that host keeps the claim open while the game joins.
|
||||
constexpr s64 DirectedPlacementStaleMs = 90'000;
|
||||
// An aimed placement (0.0.1 hotfix) comes once per claim; the game re-posts its sign about every 30 s,
|
||||
// so it has to outlive that re-post.
|
||||
constexpr s64 AimedPlacementStaleMs = 90'000;
|
||||
|
||||
using CreateConnectionFn = int PS4_SYSV_ABI (*)(int, const char*, bool);
|
||||
using CreateRequestFn = int PS4_SYSV_ABI (*)(int, s32, const char*, u64);
|
||||
@@ -108,18 +119,16 @@ std::atomic<s64> placement_received_ms{0};
|
||||
// guest sent its own placement as "a host looking for signs". The other waiting guests switched
|
||||
// their sign's map between the real host (Dream) and a guest in Central Yharnam, and the broker
|
||||
// hid every sign outside the searcher's map (Preparing) - nobody past the 3rd was summoned.
|
||||
// bbport (issue #17): a host now sends its placement only to the sign it rings (below), so a
|
||||
// waiting guest's own polling sends nothing.
|
||||
// Only a player without a sign of its own (or a host, state 3) sends the placement now, and a
|
||||
// waiting guest follows one host until that host goes quiet.
|
||||
std::atomic<s64> own_sign_created_ms{0};
|
||||
std::atomic<s64> own_sign_deleted_ms{0};
|
||||
constexpr s64 OwnSignMs = 60'000; // the game posts its sign again about every 30 s
|
||||
std::atomic<u32> placement_sender{0};
|
||||
// bbport (issue #17): which sign is ours, and whether the placement we hold was sent to it.
|
||||
// Following the first host on the server that looked for signs tied a guest to a stranger on a
|
||||
// busy server (12 hosts on Seamless Hunt in one session): its sign went to that host's map and
|
||||
// the friend never found it. As in BBSC's bbsc.dll of 2026-10-17, a host now sends its placement
|
||||
// to the one sign it rings, and a guest takes only a placement sent to its own sign.
|
||||
std::atomic<s64> own_sign_user{0};
|
||||
std::atomic<bool> placement_directed{false};
|
||||
std::atomic<s64> claim_delivered_ms{0}; // our sign's reply carried a host's claim (0 = none open)
|
||||
std::atomic<bool> was_in_session{false};
|
||||
std::atomic<s64> own_user_id{0}; // UserId of our own sign (from its /create body)
|
||||
std::atomic<bool> placement_aimed{false}; // the placement came aimed at our sign
|
||||
|
||||
s64 NowMs() {
|
||||
return static_cast<s64>(GetTickCount64());
|
||||
@@ -152,15 +161,7 @@ void LoadOverrides() {
|
||||
switch_env && switch_env[0] == '0' ? "BBSC_URL_OVERRIDES=0" : "bbhost");
|
||||
return;
|
||||
}
|
||||
// The emulator's own rule: a "user" folder in the folder it was started in wins (portable).
|
||||
wchar_t cwd[MAX_PATH]{};
|
||||
GetCurrentDirectoryW(MAX_PATH, cwd);
|
||||
std::vector<std::wstring> candidates{std::wstring(cwd) + L"\\user\\host_overrides.json",
|
||||
dir + L"user\\host_overrides.json"};
|
||||
if (const wchar_t* appdata = _wgetenv(L"APPDATA")) {
|
||||
candidates.push_back(std::wstring(appdata) + L"\\shadPS4\\host_overrides.json");
|
||||
}
|
||||
for (const auto& path : candidates) {
|
||||
for (const auto& path : BbscMod::HostOverridesFiles()) {
|
||||
const std::string json = ReadFile(path);
|
||||
std::vector<std::string> strings;
|
||||
for (size_t at = json.find('"'); at != std::string::npos; at = json.find('"', at)) {
|
||||
@@ -175,11 +176,15 @@ void LoadOverrides() {
|
||||
overrides.emplace_back(strings[i], strings[i + 1]);
|
||||
}
|
||||
if (!overrides.empty()) {
|
||||
LOG_INFO(Mod, "HTTP: {} URL override(s) from host_overrides.json", overrides.size());
|
||||
LOG_INFO(Mod, "HTTP: {} URL override(s) from {}", overrides.size(),
|
||||
std::filesystem::path(path).string());
|
||||
BBSC_TIMELINE("START", "HTTP: {} URL override(s) from {}", overrides.size(),
|
||||
std::filesystem::path(path).string());
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG_INFO(Mod, "HTTP: no host_overrides.json - URLs unchanged");
|
||||
BBSC_TIMELINE("START", "HTTP: no host_overrides.json - URLs unchanged");
|
||||
}
|
||||
#else
|
||||
// bbport: the online module applies the user folder's host_overrides itself, to every URL the game
|
||||
@@ -234,9 +239,15 @@ bool IsSummon(const std::string& url) {
|
||||
bool IsSummonCreate(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/create");
|
||||
}
|
||||
bool IsSummonDelete(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/delete");
|
||||
}
|
||||
|
||||
bool IsSummonClaim(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/request");
|
||||
}
|
||||
bool IsSummonSearch(const std::string& url) {
|
||||
return Contains(url, "/summon_messenger/request") || Contains(url, "/summon_messenger/get");
|
||||
return Contains(url, "/summon_messenger/get");
|
||||
}
|
||||
|
||||
// Public servers (2026-10-09): The Hunter's Dream (and the other community servers) let mods in as
|
||||
@@ -426,6 +437,8 @@ void ClearStaleRecord(const std::string& url, const std::string& body) {
|
||||
const DWORD status = Post(removal_url, removal);
|
||||
LOG_INFO(Mod, "HTTP: stale summon cleanup for session {} user {}: status {}", session, user,
|
||||
status);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: stale summon cleanup for session {} user {}: status {}", session, user,
|
||||
status);
|
||||
}
|
||||
|
||||
// ---- Stuck claims (2026-10-06) -------------------------------------------------------------------
|
||||
@@ -465,6 +478,7 @@ void WatchSessionForStaleClaims() {
|
||||
was_in_session = true;
|
||||
if (claim_delivered_ms.exchange(0) != 0) {
|
||||
LOG_INFO(Mod, "HTTP: the delivered claim became a join (session state {})", state);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: the delivered claim became a join (session state {})", state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -474,6 +488,7 @@ void WatchSessionForStaleClaims() {
|
||||
if (was_in_session.exchange(false)) {
|
||||
stale_cleared = false;
|
||||
LOG_INFO(Mod, "HTTP: session over - the next sign first clears our old broker record");
|
||||
BBSC_TIMELINE("JOIN", "HTTP: session over - the next sign first clears our old broker record");
|
||||
}
|
||||
const s64 delivered = claim_delivered_ms.load();
|
||||
if (delivered != 0 && NowMs() - delivered > ClaimJoinWaitMs) {
|
||||
@@ -482,6 +497,9 @@ void WatchSessionForStaleClaims() {
|
||||
LOG_WARNING(Mod, "HTTP: a host's claim was delivered {} s ago and never became a join - "
|
||||
"the next sign first clears it",
|
||||
(NowMs() - delivered) / 1000);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: a host's claim was delivered {} s ago and never became a join - "
|
||||
"the next sign first clears it",
|
||||
(NowMs() - delivered) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,7 +523,10 @@ int PS4_SYSV_ABI CreateRequestWithURL(int connection, s32 method, const char* ur
|
||||
// A slot's value when it holds an emulator function (outside the eboot), else 0.
|
||||
u64 CurrentSlot(u64 slot_offset) {
|
||||
const u64 base = BbscMod::EbootAddress();
|
||||
const u64 value = *reinterpret_cast<const volatile u64*>(base + slot_offset);
|
||||
u64 value = 0;
|
||||
if (!BbscMod::ReadSlotSafely(base + slot_offset, &value)) {
|
||||
return 0;
|
||||
}
|
||||
return value != 0 && (value < base || value >= base + EbootSize) ? value : 0;
|
||||
}
|
||||
|
||||
@@ -518,6 +539,22 @@ int PS4_SYSV_ABI SendRequest(int request, const void* data, u64 size) {
|
||||
}
|
||||
}
|
||||
WatchSessionForStaleClaims();
|
||||
// Fewer messages / bloodstains per list (Panic 265 at Gascoigne / Cathedral Ward, see
|
||||
// CapBbscAreaListRequest). Not on public servers: their API calls go out unchanged.
|
||||
if (Contains(url, "/message_area") && !IsPublicServer(url) && data != nullptr && size != 0) {
|
||||
if (const u64 length = CurrentSlot(SlotSetContentLength); length != 0) {
|
||||
original_set_content_length = reinterpret_cast<SetContentLengthFn>(length);
|
||||
}
|
||||
std::string body(static_cast<const char*>(data), static_cast<size_t>(size));
|
||||
const std::string changed = Core::Bloodborne::CapBbscAreaListRequest(url, body);
|
||||
if (!changed.empty() && original_set_content_length != nullptr) {
|
||||
original_set_content_length(request, body.size());
|
||||
LOG_INFO(Mod, "HTTP: {}", changed);
|
||||
// The emulator copies the body during the call, so a local buffer is fine.
|
||||
return original_send(request, body.data(), body.size());
|
||||
}
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
if (!IsSummon(url)) {
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
@@ -526,6 +563,8 @@ int PS4_SYSV_ABI SendRequest(int request, const void* data, u64 size) {
|
||||
if (!logged.exchange(true)) {
|
||||
LOG_INFO(Mod, "HTTP: summon requests go to a public server - sent unchanged (no "
|
||||
"seamless headers, connect-first or cleanup)");
|
||||
BBSC_TIMELINE("NET", "HTTP: summon requests go to a public server - sent unchanged (no "
|
||||
"seamless headers, connect-first or cleanup)");
|
||||
}
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
@@ -541,59 +580,63 @@ int PS4_SYSV_ABI SendRequest(int request, const void* data, u64 size) {
|
||||
return original_send(request, data, size); // (review 2026-10-08) the slots never filled
|
||||
}
|
||||
original_add_header(request, "X-ShadPS4-Bloodborne-Seamless", "1", 0);
|
||||
if (IsSummonSearch(url)) {
|
||||
// bbport (issue #17): a host ringing a sign sends its placement to that sign's owner only
|
||||
// (its TargetUserId), with the header for the broker; nothing goes to the whole server.
|
||||
if (IsSummonDelete(url)) {
|
||||
own_sign_deleted_ms = NowMs();
|
||||
}
|
||||
if (IsSummonClaim(url)) {
|
||||
// The host rang one sign: the broker gets our placement with the claim and only that
|
||||
// sign's player hears it over the relay (it moves its sign into our map, then the broker
|
||||
// delivers). Without a target nothing is sent - a claim with a placement nobody follows
|
||||
// would stay "Claimed" until it times out.
|
||||
const std::string body = data != nullptr && size != 0
|
||||
? std::string(static_cast<const char*>(data), static_cast<size_t>(size))
|
||||
? std::string(static_cast<const char*>(data),
|
||||
static_cast<size_t>(size))
|
||||
: std::string{};
|
||||
std::string target_text = JsonNumber(body, "TargetUserId");
|
||||
if (target_text.empty()) {
|
||||
target_text = JsonString(body, "TargetUserId");
|
||||
}
|
||||
if (!target_text.empty()) {
|
||||
const s64 target = std::strtoll(target_text.c_str(), nullptr, 10);
|
||||
const auto placement = Core::Bloodborne::GetSeamlessHostPlacementHeader();
|
||||
if (placement && target > 0) {
|
||||
original_add_header(request, "X-ShadPS4-Bloodborne-Host-Placement",
|
||||
placement->c_str(), 0);
|
||||
BbscMod::RelaySendPlacementTo(target, *placement);
|
||||
LOG_INFO(Mod, "HTTP: ringing the sign of user {} - our placement {} sent to it",
|
||||
target, *placement);
|
||||
} else {
|
||||
LOG_INFO(Mod, "HTTP: ringing a sign - no placement sent (target user {}, "
|
||||
"placement {})",
|
||||
target_text, placement ? "known" : "none");
|
||||
}
|
||||
const std::string target_text = JsonNumber(body, "TargetUserId");
|
||||
const s64 target = target_text.empty() ? 0 : std::atoll(target_text.c_str());
|
||||
const auto placement = Core::Bloodborne::GetSeamlessHostPlacementHeader();
|
||||
if (target > 0 && placement) {
|
||||
original_add_header(request, "X-ShadPS4-Bloodborne-Host-Placement", placement->c_str(),
|
||||
0);
|
||||
BbscMod::RelaySendAimedPlacement(target, *placement);
|
||||
LOG_INFO(Mod, "HTTP: ringing the sign of user {} - our placement {} sent to it", target,
|
||||
*placement);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: ringing the sign of user {} - our placement {} sent to it", target,
|
||||
*placement);
|
||||
} else {
|
||||
LOG_INFO(Mod, "HTTP: ringing a sign - no placement sent (target user {}, placement {})",
|
||||
target_text.empty() ? std::string("unknown") : target_text,
|
||||
placement ? "known" : "none");
|
||||
BBSC_TIMELINE("JOIN", "HTTP: ringing a sign - no placement sent (target user {}, placement {})",
|
||||
target_text.empty() ? std::string("unknown") : target_text,
|
||||
placement ? "known" : "none");
|
||||
}
|
||||
return original_send(request, data, size);
|
||||
}
|
||||
if (IsSummonSearch(url)) {
|
||||
return original_send(request, data, size); // searches go out unchanged (0.0.1 hotfix)
|
||||
}
|
||||
if (IsSummonCreate(url)) {
|
||||
own_sign_created_ms = NowMs();
|
||||
}
|
||||
if (IsSummonCreate(url) && data != nullptr && size != 0) {
|
||||
std::string body(static_cast<const char*>(data), static_cast<size_t>(size));
|
||||
ClearStaleRecord(url, body);
|
||||
// bbport (issue #17): the UserId our sign is posted under; a host ringing it sends its
|
||||
// placement to that id.
|
||||
std::string user = JsonNumber(body, "UserId");
|
||||
if (user.empty()) {
|
||||
user = JsonString(body, "UserId");
|
||||
}
|
||||
if (!user.empty()) {
|
||||
const s64 id = std::strtoll(user.c_str(), nullptr, 10);
|
||||
if (id > 0 && own_sign_user.exchange(id) != id) {
|
||||
if (const std::string user = JsonNumber(body, "UserId"); !user.empty()) {
|
||||
const s64 id = std::atoll(user.c_str());
|
||||
if (own_user_id.exchange(id) != id) {
|
||||
LOG_INFO(Mod, "HTTP: our sign's user id is {}", id);
|
||||
}
|
||||
}
|
||||
// A host's placement that has not been refreshed for a while is a host that stopped
|
||||
// looking: forget it, as the emulator did when a create reply carried none.
|
||||
const s64 received = placement_received_ms.load();
|
||||
const s64 stale = placement_directed.load() ? DirectedPlacementStaleMs : PlacementStaleMs;
|
||||
const s64 stale = placement_aimed.load() ? AimedPlacementStaleMs : PlacementStaleMs;
|
||||
if (received != 0 && NowMs() - received > stale) {
|
||||
placement_received_ms = 0;
|
||||
placement_aimed = false;
|
||||
placement_sender = 0;
|
||||
placement_directed = false;
|
||||
Core::Bloodborne::ClearSeamlessHostPlacementHeader();
|
||||
LOG_INFO(Mod, "HTTP: no word from that host for a while - the sign goes back to our "
|
||||
"own map");
|
||||
}
|
||||
if (RewriteAreaId(body)) {
|
||||
original_set_content_length(request, body.size());
|
||||
@@ -646,6 +689,9 @@ int PS4_SYSV_ABI ReadData(int request, void* data, u64 size) {
|
||||
LOG_INFO(Mod, "HTTP: a host's claim came with our sign ({} bytes) - waiting for the "
|
||||
"join",
|
||||
read);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: a host's claim came with our sign ({} bytes) - waiting for the "
|
||||
"join",
|
||||
read);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,16 +711,16 @@ int PS4_SYSV_ABI DeleteRequest(int request) {
|
||||
// the game could call the wrapper before the original was stored). False if the slot never filled.
|
||||
template <typename Fn>
|
||||
bool Swap(u64 slot_offset, Fn wrapper, Fn& original, const char* name) {
|
||||
auto* slot = reinterpret_cast<u64*>(BbscMod::EbootAddress() + slot_offset);
|
||||
const u64 slot = BbscMod::EbootAddress() + slot_offset;
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
const u64 value = *slot;
|
||||
if (value != 0 && (value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
u64 value = 0;
|
||||
if (BbscMod::ReadSlotSafely(slot, &value) && value != 0 &&
|
||||
(value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
original = reinterpret_cast<Fn>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(slot, 8, PAGE_READWRITE, &old);
|
||||
*slot = reinterpret_cast<u64>(wrapper);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(slot, 8, old, &ignored);
|
||||
if (!BbscMod::WriteSlotSafely(slot, reinterpret_cast<u64>(wrapper))) {
|
||||
Sleep(1);
|
||||
continue; // page not writable right now: try again
|
||||
}
|
||||
BbscMod::GuardSlot(slot_offset, reinterpret_cast<u64>(wrapper),
|
||||
reinterpret_cast<void**>(&original), name);
|
||||
return true;
|
||||
@@ -686,10 +732,11 @@ bool Swap(u64 slot_offset, Fn wrapper, Fn& original, const char* name) {
|
||||
|
||||
// The two slots read, not swapped: waits for them as Swap does (they are filled at the same time).
|
||||
u64 WaitForSlot(u64 slot_offset) {
|
||||
const auto* slot = reinterpret_cast<const u64*>(BbscMod::EbootAddress() + slot_offset);
|
||||
const u64 slot = BbscMod::EbootAddress() + slot_offset;
|
||||
for (int i = 0; i < 60'000; ++i) {
|
||||
const u64 value = *slot;
|
||||
if (value != 0 && (value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
u64 value = 0;
|
||||
if (BbscMod::ReadSlotSafely(slot, &value) && value != 0 &&
|
||||
(value < BbscMod::EbootAddress() || value >= BbscMod::EbootAddress() + EbootSize)) {
|
||||
return value;
|
||||
}
|
||||
Sleep(1);
|
||||
@@ -701,44 +748,82 @@ u64 WaitForSlot(u64 slot_offset) {
|
||||
|
||||
namespace BbscMod {
|
||||
|
||||
#ifdef _WIN32
|
||||
// The emulator's overrides file, in the order the emulator looks for it:
|
||||
// 1. SHADPS4_HTTP_HOST_OVERRIDES_JSON - BBport (run.py) passes the file it uses this way: its own
|
||||
// user\host_overrides.custom.json when present, else a user\host_overrides.json it writes for
|
||||
// The Hunter's Dream. 0.0.1 read only the second -> BBport players with our server in the
|
||||
// custom file ran BBSC in public mode (vanilla summons, no relay) and could not join anyone
|
||||
// (2026-10-13 public reports).
|
||||
// 2. user\host_overrides.custom.json (BBport layout, if the variable is missing).
|
||||
// 3. user\host_overrides.json in the start folder / beside the exe (portable), then %APPDATA%.
|
||||
std::vector<std::wstring> HostOverridesFiles() {
|
||||
std::vector<std::wstring> files;
|
||||
if (const wchar_t* env = _wgetenv(L"SHADPS4_HTTP_HOST_OVERRIDES_JSON");
|
||||
env != nullptr && env[0] != L'\0') {
|
||||
files.emplace_back(env);
|
||||
}
|
||||
wchar_t exe[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
||||
std::wstring dir{exe};
|
||||
dir = dir.substr(0, dir.find_last_of(L"\\/") + 1);
|
||||
wchar_t cwd[MAX_PATH]{};
|
||||
GetCurrentDirectoryW(MAX_PATH, cwd);
|
||||
const std::wstring start = std::wstring(cwd) + L"\\";
|
||||
for (const std::wstring& base : {start, dir, dir + L"..\\"}) {
|
||||
files.push_back(base + L"user\\host_overrides.custom.json");
|
||||
}
|
||||
files.push_back(start + L"user\\host_overrides.json");
|
||||
files.push_back(dir + L"user\\host_overrides.json");
|
||||
if (const wchar_t* appdata = _wgetenv(L"APPDATA")) {
|
||||
files.push_back(std::wstring(appdata) + L"\\shadPS4\\host_overrides.json");
|
||||
}
|
||||
return files;
|
||||
}
|
||||
#endif
|
||||
|
||||
// The relay asks before connecting (bbsc_relay.cpp): public servers run no BBSC relay.
|
||||
bool IsPublicServerAddress(const std::string& url_or_host) {
|
||||
return IsPublicServer(url_or_host);
|
||||
}
|
||||
|
||||
// A host's placement from the relay: what the broker's reply header carried in the emulator
|
||||
// build. bbport (issue #17): a placement sent to the whole server is ignored, as BBSC's bbsc.dll of
|
||||
// 2026-10-17 does ("an old host - it must update BBSC"): following any host on a busy server sent
|
||||
// a guest's sign to a stranger's map.
|
||||
// build. Used only by a player who is not in a session yet (a guest waiting to be summoned).
|
||||
void OnHostPlacement(const std::string& placement, u32 sender) {
|
||||
// Older hosts sent their placement to everyone whenever they searched; waiting guests then
|
||||
// followed random hosts. Now we only follows a placement aimed at our own sign.
|
||||
static std::atomic<bool> logged{false};
|
||||
if (!logged.exchange(true)) {
|
||||
LOG_INFO(Mod, "HTTP: placement broadcast from {:08x} ignored (an old host - it must "
|
||||
"update BBSC): {}",
|
||||
sender, placement);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: placement broadcast from {:08x} ignored (an old host - it must "
|
||||
"update BBSC): {}",
|
||||
sender, placement);
|
||||
}
|
||||
}
|
||||
|
||||
// bbport (issue #17): the placement of the host that rang our sign, sent to its UserId. Used only
|
||||
// by a player who is not in a session yet (a guest waiting to be summoned).
|
||||
void OnDirectedPlacement(s64 target_user, const std::string& placement, u32 sender) {
|
||||
const s64 own = own_sign_user.load();
|
||||
// A host rang our sign (0.0.1 hotfix): its placement, aimed at our sign's UserId. The next post of our
|
||||
// sign moves it into the host's map and the broker delivers the claim.
|
||||
void OnAimedHostPlacement(s64 target_user, const std::string& placement, u32 sender) {
|
||||
const s64 own = own_user_id.load();
|
||||
if (own == 0 || target_user != own) {
|
||||
return; // another player's sign
|
||||
return; // another player's sign was rung
|
||||
}
|
||||
const s32 state = SessionState();
|
||||
if (state == 3 || state == 4 || state == 6) {
|
||||
return; // hosting, joining or already a guest
|
||||
}
|
||||
// Our host by who it is: its world flags are taken before the join load, and the guest locks
|
||||
// onto it among the other hosts on the server (bloodborne_re.cpp, BbscRangHost; BBSC 0.0.2).
|
||||
// Its world flags are taken before the join load (bloodborne_re.cpp, "the host that rang").
|
||||
Core::Bloodborne::NoteBbscHostRangOurSign(sender);
|
||||
if (Core::Bloodborne::SetSeamlessHostPlacementHeader(placement)) {
|
||||
placement_directed = true;
|
||||
placement_sender = sender;
|
||||
placement_aimed = true;
|
||||
placement_received_ms = NowMs();
|
||||
LOG_INFO(Mod, "HTTP: host {:08x} rang our sign (user {}) - placement {}", sender,
|
||||
target_user, placement);
|
||||
LOG_INFO(Mod, "HTTP: host {:08x} rang our sign (user {}) - placement {}", sender, own,
|
||||
placement);
|
||||
BBSC_TIMELINE("JOIN", "HTTP: host {:08x} rang our sign (user {}) - placement {}", sender, own,
|
||||
placement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,6 +845,9 @@ void InstallHttpWrappers() {
|
||||
LOG_INFO(Mod, "HTTP: game's HTTP calls {} (URL overrides, seamless summon, connect-first, stale "
|
||||
"cleanup)",
|
||||
ok ? "wrapped" : "NOT wrapped - an import slot was never filled");
|
||||
BBSC_TIMELINE("START", "HTTP: game's HTTP calls {} (URL overrides, seamless summon, connect-first, stale "
|
||||
"cleanup)",
|
||||
ok ? "wrapped" : "NOT wrapped - an import slot was never filled");
|
||||
}
|
||||
|
||||
} // namespace BbscMod
|
||||
@@ -40,6 +40,9 @@
|
||||
#include "core/bbsc_cinematic.h"
|
||||
#include "core/bbsc_patch_tables.h"
|
||||
#include "core/bloodborne_re.h"
|
||||
|
||||
// One line in this run's BBSC timeline (bloodborne_re.cpp, "Timeline").
|
||||
#define BBSC_TIMELINE(kind, ...) Core::Bloodborne::BbscTimelineEvent(kind, fmt::format(__VA_ARGS__))
|
||||
#include "core/debugger.h"
|
||||
#include "core/libraries/np/np_matching2/np_matching2_mm.h"
|
||||
#include "core/memory.h"
|
||||
@@ -48,7 +51,7 @@ using namespace Core::Bbsc::PatchTables;
|
||||
|
||||
// Public version (2026-10-11): 0.MINOR.PATCH while testing - PATCH for bug fixes, MINOR for new
|
||||
// features, 1.0.0 the finished release. Keep it equal to "version" in mod.json.
|
||||
#define BBSC_VERSION "0.0.1"
|
||||
#define BBSC_VERSION "0.0.2"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
@@ -328,11 +331,16 @@ DWORD WINAPI Worker(void*) {
|
||||
OpenLog();
|
||||
wchar_t exe[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
||||
BbscMod::LogPlain("BBSC mod loaded - version " BBSC_VERSION);
|
||||
// The build time tells two DLLs of one version apart in a tester's log (2026-10-18 triage: no
|
||||
// log said which 0.0.1 DLL a report came from).
|
||||
BbscMod::LogPlain("BBSC mod loaded - version " BBSC_VERSION " (built " __DATE__ " " __TIME__ ")");
|
||||
if (!NoticesPresent()) {
|
||||
LOG_ERROR(Mod, "LICENSE.txt / NOTICE.txt / SUPPORTERS.txt missing or changed in {} - BBSC "
|
||||
"is off",
|
||||
Common::FS::PathToUTF8String(mod_dir));
|
||||
BBSC_TIMELINE("START", "LICENSE.txt / NOTICE.txt / SUPPORTERS.txt missing or changed in {} - BBSC "
|
||||
"is off",
|
||||
Common::FS::PathToUTF8String(mod_dir));
|
||||
MessageBoxW(nullptr,
|
||||
L"LICENSE.txt, NOTICE.txt or SUPPORTERS.txt is missing from the BBSC mod "
|
||||
L"folder (or was changed), so BBSC is off.\n\nThese files have to travel with "
|
||||
@@ -361,6 +369,9 @@ DWORD WINAPI Worker(void*) {
|
||||
LOG_INFO(Mod, "Public game server ({}): vanilla summons - seamless summon patches and the "
|
||||
"5-hunter search off (the session itself stays seamless)",
|
||||
server);
|
||||
BBSC_TIMELINE("START", "Public game server ({}): vanilla summons - seamless summon patches and the "
|
||||
"5-hunter search off (the session itself stays seamless)",
|
||||
server);
|
||||
}
|
||||
}
|
||||
BuildSites();
|
||||
@@ -371,6 +382,9 @@ DWORD WINAPI Worker(void*) {
|
||||
LOG_ERROR(Mod, "No Bloodborne CUSA03173 01.09 image in memory after {} s - not this "
|
||||
"game or not this version; nothing was changed",
|
||||
WaitForImageMs / 1000);
|
||||
BBSC_TIMELINE("START", "No Bloodborne CUSA03173 01.09 image in memory after {} s - not this "
|
||||
"game or not this version; nothing was changed",
|
||||
WaitForImageMs / 1000);
|
||||
return 0;
|
||||
}
|
||||
Sleep(50);
|
||||
@@ -431,6 +445,7 @@ DWORD WINAPI Worker(void*) {
|
||||
written = SyncSites();
|
||||
}
|
||||
LOG_INFO(Mod, "Image verified after {} ms: {} byte patch sites written", verified, written);
|
||||
BBSC_TIMELINE("START", "Image verified after {} ms: {} byte patch sites written", verified, written);
|
||||
|
||||
Core::Bloodborne::InstallReverseEngineeringTrace();
|
||||
BbscMod::StartRelay();
|
||||
@@ -475,6 +490,63 @@ u64 EbootAddress() {
|
||||
return EbootBase;
|
||||
}
|
||||
|
||||
// Import slot reads and writes that never take the game down (2026-10-19, BBport player, 0.0.2
|
||||
// test): the game closed 3 s after start, five times in a row - "Fault: read access to
|
||||
// 00000008053e4f48 at bbsc.dll+0x1933": BBSC read the Sendto slot while BBport's loader had that
|
||||
// page unreadable (it links the eboot's imports itself at start). A slot is only touched while
|
||||
// its page is committed and readable, and a fault in between (the page changing right after the
|
||||
// check) is caught here. Used by the slot guard below and by the swaps in bbsc_http.cpp and
|
||||
// bbsc_channel.cpp.
|
||||
namespace {
|
||||
bool SlotPageReadable(u64 address) {
|
||||
MEMORY_BASIC_INFORMATION info{};
|
||||
if (VirtualQuery(reinterpret_cast<void*>(address), &info, sizeof(info)) == 0 ||
|
||||
info.State != MEM_COMMIT || (info.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0) {
|
||||
return false;
|
||||
}
|
||||
constexpr DWORD Readable = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ |
|
||||
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
return (info.Protect & Readable) != 0;
|
||||
}
|
||||
|
||||
bool TryReadSlot(const volatile u64* slot, u64* value) {
|
||||
__try {
|
||||
*value = *slot;
|
||||
return true;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryWriteSlot(volatile u64* slot, u64 value) {
|
||||
__try {
|
||||
*slot = value;
|
||||
return true;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ReadSlotSafely(u64 address, u64* value) {
|
||||
return SlotPageReadable(address) &&
|
||||
TryReadSlot(reinterpret_cast<const volatile u64*>(address), value);
|
||||
}
|
||||
|
||||
bool WriteSlotSafely(u64 address, u64 value) {
|
||||
if (!SlotPageReadable(address)) {
|
||||
return false;
|
||||
}
|
||||
DWORD old = 0;
|
||||
if (!VirtualProtect(reinterpret_cast<void*>(address), 8, PAGE_READWRITE, &old)) {
|
||||
return false;
|
||||
}
|
||||
const bool written = TryWriteSlot(reinterpret_cast<volatile u64*>(address), value);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(reinterpret_cast<void*>(address), 8, old, &ignored);
|
||||
return written;
|
||||
}
|
||||
|
||||
// Import slots the DLL swapped for its wrappers (bbsc_http.cpp, bbsc_channel.cpp). The official
|
||||
// shadPS4 links the eboot's imports again when the game loads a library later (2026-10-10: no
|
||||
// wrapper ever ran on the nightly - the slots were written back to the emulator's functions), so a
|
||||
@@ -492,22 +564,29 @@ std::mutex guarded_mutex;
|
||||
std::vector<GuardedSlot> guarded_slots;
|
||||
|
||||
DWORD WINAPI SlotGuardThread(void*) {
|
||||
int skipped = 0;
|
||||
for (;;) {
|
||||
Sleep(5);
|
||||
std::scoped_lock lock{guarded_mutex};
|
||||
for (auto& guarded : guarded_slots) {
|
||||
auto* slot = reinterpret_cast<volatile u64*>(EbootBase + guarded.slot_offset);
|
||||
const u64 value = *slot;
|
||||
const u64 slot = EbootBase + guarded.slot_offset;
|
||||
u64 value = 0;
|
||||
if (!ReadSlotSafely(slot, &value)) {
|
||||
if (++skipped == 1) {
|
||||
LOG_INFO(Mod, "Import {}: its page is not readable right now (the emulator is "
|
||||
"still linking) - checked again later",
|
||||
guarded.name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (value == guarded.wrapper || value == 0 ||
|
||||
(value >= EbootBase && value < EbootBase + EbootSize)) {
|
||||
continue;
|
||||
}
|
||||
*reinterpret_cast<void* volatile*>(guarded.original) = reinterpret_cast<void*>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(const_cast<u64*>(slot), 8, PAGE_READWRITE, &old);
|
||||
*slot = guarded.wrapper;
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(const_cast<u64*>(slot), 8, old, &ignored);
|
||||
if (!WriteSlotSafely(slot, guarded.wrapper)) {
|
||||
continue; // the next round tries again
|
||||
}
|
||||
if (++guarded.rewritten <= 3) {
|
||||
LOG_INFO(Mod, "Import {} was linked again by the emulator - wrapper put back ({}x)",
|
||||
guarded.name, guarded.rewritten);
|
||||
@@ -732,6 +811,7 @@ struct CinePad {
|
||||
u32 last_buttons = 0;
|
||||
float lx = 0.0f, ly = 0.0f, rx = 0.0f, ry = 0.0f, l2 = 0.0f, r2 = 0.0f;
|
||||
int speed_steps = 0, smooth_steps = 0;
|
||||
int switch_steps = 0; // R1 / L1 presses: the hunter to watch while dead
|
||||
bool zoom_reset = false;
|
||||
};
|
||||
CinePad cine_pad;
|
||||
@@ -773,6 +853,7 @@ int PS4_SYSV_ABI PadReadState(s32 handle, u8* data) {
|
||||
cine_pad.speed_steps += ((pressed & PadUp) ? 1 : 0) - ((pressed & PadDown) ? 1 : 0);
|
||||
cine_pad.smooth_steps += ((pressed & PadRight) ? 1 : 0) - ((pressed & PadLeft) ? 1 : 0);
|
||||
cine_pad.zoom_reset = cine_pad.zoom_reset || (pressed & PadTriangle) != 0;
|
||||
cine_pad.switch_steps += ((pressed & PadR1) ? 1 : 0) - ((pressed & PadL1) ? 1 : 0);
|
||||
// The game sees a controller at rest (connected, nothing pressed).
|
||||
const u32 rest_buttons = buttons & 0x80000000u;
|
||||
std::memcpy(data, &rest_buttons, 4);
|
||||
@@ -852,6 +933,15 @@ CinematicInput TakeCinematicInput() {
|
||||
(CineKeyPressed(VK_OEM_4, down(VK_OEM_4)) ? 1 : 0) + p.smooth_steps;
|
||||
cine_pad.speed_steps = 0;
|
||||
cine_pad.smooth_steps = 0;
|
||||
// Watching a living hunter while dead: Tab / E / left click next, Shift+Tab / Q previous,
|
||||
// R1 / L1 on the controller; F flies freely instead and back (bloodborne_re.cpp).
|
||||
const bool shift = down(VK_SHIFT);
|
||||
c.switch_target = (CineKeyPressed(VK_TAB, down(VK_TAB)) ? (shift ? -1 : 1) : 0) +
|
||||
(CineKeyPressed('E', down('E')) ? 1 : 0) -
|
||||
(CineKeyPressed('Q', down('Q')) ? 1 : 0) +
|
||||
(CineKeyPressed(VK_LBUTTON, down(VK_LBUTTON)) ? 1 : 0) + p.switch_steps;
|
||||
cine_pad.switch_steps = 0;
|
||||
c.toggle_free = CineKeyPressed('F', down('F'));
|
||||
c.pad_move_x = p.lx;
|
||||
c.pad_move_y = -p.ly; // stick up = smaller y = forward
|
||||
c.pad_rise = ((p.buttons & PadR1) ? 1.0f : 0.0f) - ((p.buttons & PadL1) ? 1.0f : 0.0f);
|
||||
@@ -902,6 +992,11 @@ void CinematicForceOn() {
|
||||
cine_looking = false;
|
||||
}
|
||||
|
||||
// The mod reads the controller itself (PadReadState: the whole pad goes to the camera while it is
|
||||
// on), so watching needs nothing more here.
|
||||
void CinematicSetWatching(bool) {}
|
||||
void CinematicPadRead(unsigned, unsigned char, unsigned char, unsigned char, unsigned char) {}
|
||||
|
||||
} // namespace Bbsc
|
||||
} // namespace Core
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
@@ -39,8 +42,9 @@
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
void OnHostPlacement(const std::string& placement, u32 sender);
|
||||
void OnDirectedPlacement(s64 target_user, const std::string& placement, u32 sender); // bbport
|
||||
void OnAimedHostPlacement(s64 target_user, const std::string& placement, u32 sender);
|
||||
bool IsPublicServerAddress(const std::string& url_or_host); // bbsc_http.cpp
|
||||
std::vector<std::wstring> HostOverridesFiles(); // bbsc_http.cpp
|
||||
u32 P2PChannelSend(const void* data, u32 size); // bbsc_channel.cpp
|
||||
u32 P2PChannelPeerCount();
|
||||
}
|
||||
@@ -49,10 +53,9 @@ namespace {
|
||||
|
||||
constexpr u16 DefaultRelayPort = 20444;
|
||||
constexpr u8 FollowType = 0x20;
|
||||
constexpr u8 PlacementType = 0x30; // "SHAD" 0x30 + a host's placement text (bbsc_http.cpp)
|
||||
// bbport: "SHAD" 0x31 + "<target UserId>|<placement>": a host's placement for the one sign it
|
||||
// rings (BBSC's bbsc.dll of 2026-10-17; 0x30 broadcasts are ignored since, issue #17).
|
||||
constexpr u8 PlacementToType = 0x31;
|
||||
constexpr u8 PlacementType = 0x30; // "SHAD" 0x30 + a host's placement text (before the hotfix, ignored now)
|
||||
// "SHAD" 0x31 + "<target UserId>|<placement>": a host rang that sign (0.0.1 hotfix, bbsc_http.cpp)
|
||||
constexpr u8 AimedPlacementType = 0x31;
|
||||
constexpr u8 LifeType = 0x23; // "SHAD" 0x23 + u32 player, u8 dead, u32 sequence
|
||||
constexpr u8 ScaleType = 0x24; // "SHAD" 0x24 + 5 x u16 the host's scaling percentages
|
||||
constexpr u8 PvpType = 0x25; // "SHAD" 0x25 + u8 kind (0 host state, 1 guest asks) + u8 on + u32
|
||||
@@ -85,6 +88,17 @@ static_assert(sizeof(FollowPacket) == 0x22);
|
||||
std::mutex send_mutex;
|
||||
std::mutex dispatch_mutex; // the relay thread and the P2P channel's thread both deliver frames
|
||||
SOCKET relay = INVALID_SOCKET;
|
||||
// Frames wait here for the writer thread (2026-10-13): a public player's link dropped exactly 10 s
|
||||
// after every reconnect while hosting - the 1/s world flags filled the socket while the server
|
||||
// was stuck, and the game thread's 300 ms send timeout closed the link. Now the game thread only
|
||||
// queues; a full queue drops frames instead of the link. All under send_mutex.
|
||||
std::condition_variable send_cv;
|
||||
std::deque<std::vector<char>> send_queue;
|
||||
size_t send_queue_bytes = 0;
|
||||
constexpr size_t SendQueueLimit = 256 * 1024;
|
||||
SOCKET writing = INVALID_SOCKET; // the socket the writer is sending on right now
|
||||
u32 dropped_frames = 0; // since this connection began
|
||||
std::string lost_reason; // why the writer gave up on this connection
|
||||
std::atomic<int> peers{0};
|
||||
std::atomic<bool> started{false};
|
||||
u32 own_id = 0; // set once in StartRelay, never 0
|
||||
@@ -92,6 +106,7 @@ std::atomic<bool> logged_old_sender{false};
|
||||
|
||||
void Log(const std::string& text) {
|
||||
BbscMod::LogPlain(("Relay: " + text).c_str());
|
||||
Core::Bloodborne::BbscTimelineEvent("RELAY", text); // status changes only, see the callers
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -136,18 +151,7 @@ bool FindServer(std::string& host, u16& port) {
|
||||
return true;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t exe[MAX_PATH]{};
|
||||
GetModuleFileNameW(nullptr, exe, MAX_PATH);
|
||||
std::wstring dir{exe};
|
||||
dir = dir.substr(0, dir.find_last_of(L"\\/") + 1);
|
||||
// The emulator's own rule: a "user" folder in the folder it was started in wins (portable).
|
||||
wchar_t cwd[MAX_PATH]{};
|
||||
GetCurrentDirectoryW(MAX_PATH, cwd);
|
||||
std::vector<std::wstring> candidates{std::wstring(cwd) + L"\\user\\host_overrides.json",
|
||||
dir + L"user\\host_overrides.json"};
|
||||
if (const wchar_t* appdata = _wgetenv(L"APPDATA")) {
|
||||
candidates.push_back(std::wstring(appdata) + L"\\shadPS4\\host_overrides.json");
|
||||
}
|
||||
for (const auto& path : BbscMod::HostOverridesFiles()) {
|
||||
#else
|
||||
// bbport: the overrides file run.sh hands the online module (SHADPS4_HTTP_HOST_OVERRIDES_JSON,
|
||||
// scripts/online.py), else the ones in the user folder.
|
||||
@@ -157,8 +161,8 @@ bool FindServer(std::string& host, u16& port) {
|
||||
if (const char* given = std::getenv("SHADPS4_HTTP_HOST_OVERRIDES_JSON"); given && *given) {
|
||||
candidates.insert(candidates.begin(), given);
|
||||
}
|
||||
#endif
|
||||
for (const auto& path : candidates) {
|
||||
#endif
|
||||
host = HostFromOverrides(ReadFile(path));
|
||||
if (!host.empty()) {
|
||||
return true;
|
||||
@@ -167,11 +171,14 @@ bool FindServer(std::string& host, u16& port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int last_read_result = 0; // 0 = the server closed the link, < 0 = recv error (relay thread only)
|
||||
|
||||
bool ReadAll(SOCKET s, void* data, int size) {
|
||||
auto* at = static_cast<char*>(data);
|
||||
while (size > 0) {
|
||||
const int got = recv(s, at, size, 0);
|
||||
if (got <= 0) {
|
||||
last_read_result = got == 0 ? 0 : -WSAGetLastError();
|
||||
return false;
|
||||
}
|
||||
at += got;
|
||||
@@ -208,21 +215,71 @@ u32 SendFrame(const void* data, u32 size) {
|
||||
if (relay == INVALID_SOCKET) {
|
||||
return BbscMod::P2PChannelSend(frame.data() + 4, wrapped);
|
||||
}
|
||||
int left = static_cast<int>(frame.size());
|
||||
const char* at = frame.data();
|
||||
while (left > 0) {
|
||||
const int sent = send(relay, at, left, 0);
|
||||
if (sent <= 0) {
|
||||
closesocket(relay);
|
||||
relay = INVALID_SOCKET;
|
||||
return BbscMod::P2PChannelSend(frame.data() + 4, wrapped);
|
||||
if (send_queue_bytes + frame.size() > SendQueueLimit) {
|
||||
if (dropped_frames++ == 0) {
|
||||
Log("the server is not taking our frames (" + std::to_string(send_queue_bytes) +
|
||||
" bytes waiting) - dropping new ones until it does");
|
||||
}
|
||||
at += sent;
|
||||
left -= sent;
|
||||
return static_cast<u32>(peers.load());
|
||||
}
|
||||
send_queue_bytes += frame.size();
|
||||
send_queue.push_back(std::move(frame));
|
||||
send_cv.notify_all();
|
||||
return static_cast<u32>(peers.load());
|
||||
}
|
||||
|
||||
std::string SocketError(const char* what) {
|
||||
return std::string(what) + " (WSA error " + std::to_string(WSAGetLastError()) + ")";
|
||||
}
|
||||
|
||||
// Sends the queued frames, one socket at a time. A failed send shuts the socket down; the relay
|
||||
// thread sees that in recv and reconnects.
|
||||
DWORD WINAPI WriterThread(void*) {
|
||||
for (;;) {
|
||||
std::vector<char> frame;
|
||||
SOCKET s = INVALID_SOCKET;
|
||||
{
|
||||
std::unique_lock lock{send_mutex};
|
||||
send_cv.wait(lock, [] { return !send_queue.empty(); });
|
||||
frame = std::move(send_queue.front());
|
||||
send_queue.pop_front();
|
||||
send_queue_bytes -= frame.size();
|
||||
s = relay;
|
||||
if (s == INVALID_SOCKET) {
|
||||
continue;
|
||||
}
|
||||
writing = s;
|
||||
}
|
||||
std::string problem;
|
||||
int left = static_cast<int>(frame.size());
|
||||
const char* at = frame.data();
|
||||
while (left > 0) {
|
||||
#ifdef _WIN32
|
||||
const int sent = send(s, at, left, 0);
|
||||
#else
|
||||
// bbport: a link the server closed fails the send instead of raising SIGPIPE, which
|
||||
// would end the game's process.
|
||||
const int sent = static_cast<int>(send(s, at, left, MSG_NOSIGNAL));
|
||||
#endif
|
||||
if (sent <= 0) {
|
||||
problem = SocketError("send failed");
|
||||
break;
|
||||
}
|
||||
at += sent;
|
||||
left -= sent;
|
||||
}
|
||||
std::scoped_lock lock{send_mutex};
|
||||
writing = INVALID_SOCKET;
|
||||
if (!problem.empty() && relay == s) {
|
||||
if (lost_reason.empty()) {
|
||||
lost_reason = problem;
|
||||
}
|
||||
shutdown(s, SD_BOTH);
|
||||
}
|
||||
send_cv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void Dispatch(const std::vector<u8>& wrapped) {
|
||||
if (wrapped.size() == 2 && wrapped[0] == PeersType) {
|
||||
peers = wrapped[1];
|
||||
@@ -304,13 +361,12 @@ void Dispatch(const std::vector<u8>& wrapped) {
|
||||
} else if (frame[4] == PlacementType) {
|
||||
// A host looking for signs: for players not in a session yet, any host on the server.
|
||||
BbscMod::OnHostPlacement(std::string(frame.begin() + 5, frame.end()), sender);
|
||||
} else if (frame[4] == PlacementToType) {
|
||||
// bbport: the placement of the host that rang one sign: "<target UserId>|<placement>".
|
||||
} else if (frame[4] == AimedPlacementType) {
|
||||
const std::string text(frame.begin() + 5, frame.end());
|
||||
const size_t bar = text.find('|');
|
||||
if (bar != std::string::npos && bar != 0) {
|
||||
const s64 target = std::strtoll(text.substr(0, bar).c_str(), nullptr, 10);
|
||||
BbscMod::OnDirectedPlacement(target, text.substr(bar + 1), sender);
|
||||
BbscMod::OnAimedHostPlacement(std::atoll(text.substr(0, bar).c_str()),
|
||||
text.substr(bar + 1), sender);
|
||||
}
|
||||
} else if (frame[4] == Libraries::Np::NpSignaling::kSignalingBbscWorldFlagsType) {
|
||||
if (from_our_host()) {
|
||||
@@ -369,38 +425,68 @@ DWORD WINAPI RelayThread(void*) {
|
||||
}
|
||||
BOOL on = TRUE;
|
||||
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<const char*>(&on), sizeof(on));
|
||||
// Frames are sent from the game thread (review 2026-10-08): a stalled server must not
|
||||
// freeze the game in send() - after 300 ms the frame fails and the link is rebuilt.
|
||||
const DWORD send_timeout_ms = 300;
|
||||
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast<const char*>(&on), sizeof(on));
|
||||
// Only the writer thread sends now (2026-10-13), so a slow server no longer stalls the
|
||||
// game; a send stuck 10 s means the link is dead.
|
||||
#ifdef _WIN32
|
||||
const DWORD send_timeout_ms = 10'000;
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&send_timeout_ms),
|
||||
sizeof(send_timeout_ms));
|
||||
#else
|
||||
const timeval send_timeout{10, 0}; // bbport: SO_SNDTIMEO takes a timeval here
|
||||
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout));
|
||||
#endif
|
||||
{
|
||||
std::scoped_lock lock{send_mutex};
|
||||
relay = s;
|
||||
lost_reason.clear();
|
||||
dropped_frames = 0;
|
||||
}
|
||||
last_problem.clear();
|
||||
Log("connected to " + host + ":" + std::to_string(port));
|
||||
const u64 connected_at = GetTickCount64();
|
||||
u64 frames_in = 0;
|
||||
std::string reason;
|
||||
for (;;) {
|
||||
u32 size = 0;
|
||||
if (!ReadAll(s, &size, 4) || size > 64 * 1024) {
|
||||
if (!ReadAll(s, &size, 4)) {
|
||||
break;
|
||||
}
|
||||
if (size > 64 * 1024) {
|
||||
reason = "oversized frame from the server (" + std::to_string(size) + " bytes)";
|
||||
break;
|
||||
}
|
||||
std::vector<u8> frame(size);
|
||||
if (size != 0 && !ReadAll(s, frame.data(), static_cast<int>(size))) {
|
||||
break;
|
||||
}
|
||||
++frames_in;
|
||||
std::scoped_lock dispatch_lock{dispatch_mutex};
|
||||
Dispatch(frame);
|
||||
}
|
||||
u32 dropped = 0;
|
||||
{
|
||||
std::scoped_lock lock{send_mutex};
|
||||
if (relay == s) {
|
||||
closesocket(relay);
|
||||
relay = INVALID_SOCKET;
|
||||
std::unique_lock lock{send_mutex};
|
||||
if (!lost_reason.empty()) {
|
||||
reason = lost_reason; // the writer gave up first
|
||||
}
|
||||
relay = INVALID_SOCKET;
|
||||
shutdown(s, SD_BOTH); // wakes a writer still in send() on it
|
||||
send_cv.wait(lock, [s] { return writing != s; });
|
||||
closesocket(s);
|
||||
send_queue.clear();
|
||||
send_queue_bytes = 0;
|
||||
dropped = dropped_frames;
|
||||
}
|
||||
if (reason.empty()) {
|
||||
reason = last_read_result == 0
|
||||
? std::string("the server closed the connection")
|
||||
: "read failed (WSA error " + std::to_string(-last_read_result) + ")";
|
||||
}
|
||||
peers = 0;
|
||||
Log("connection lost - reconnecting");
|
||||
Log("connection lost after " + std::to_string((GetTickCount64() - connected_at) / 1000) +
|
||||
" s, " + std::to_string(frames_in) + " frames in, " + std::to_string(dropped) +
|
||||
" dropped out: " + reason + " - reconnecting");
|
||||
Sleep(2'000);
|
||||
}
|
||||
}
|
||||
@@ -433,20 +519,10 @@ void RelayDispatch(const std::vector<u8>& wrapped) {
|
||||
Dispatch(wrapped);
|
||||
}
|
||||
|
||||
void RelaySendPlacement(const std::string& placement) {
|
||||
void RelaySendAimedPlacement(s64 target_user, const std::string& placement) {
|
||||
std::string frame = "SHAD";
|
||||
frame.push_back(static_cast<char>(PlacementType));
|
||||
frame += placement;
|
||||
SendFrame(frame.data(), static_cast<u32>(frame.size()));
|
||||
}
|
||||
|
||||
// bbport: a host's placement for the sign it rings only (the sign's owner, by its UserId).
|
||||
void RelaySendPlacementTo(s64 target_user, const std::string& placement) {
|
||||
std::string frame = "SHAD";
|
||||
frame.push_back(static_cast<char>(PlacementToType));
|
||||
frame += std::to_string(target_user);
|
||||
frame.push_back('|');
|
||||
frame += placement;
|
||||
frame.push_back(static_cast<char>(AimedPlacementType));
|
||||
frame += std::to_string(target_user) + "|" + placement;
|
||||
SendFrame(frame.data(), static_cast<u32>(frame.size()));
|
||||
}
|
||||
|
||||
@@ -465,6 +541,9 @@ void StartRelay() {
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, RelayThread, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
}
|
||||
if (HANDLE thread = CreateThread(nullptr, 0, WriterThread, nullptr, 0, nullptr)) {
|
||||
CloseHandle(thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace BbscMod
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
// questions are answered from bbport's own map of the game's memory (runtime_memory_vma_info, no
|
||||
// system call); the log goes to <user dir>/bbsc/logs/ and its warnings to the game's log; the F1 /
|
||||
// L3+R3 cinematic camera and BBSC's own Discord status are not in this build (bbport has its own
|
||||
// Discord status, and L3+R3 opens bbport's menu).
|
||||
// Discord status, and L3+R3 opens bbport's menu). Only BBSC 0.0.2's watch camera of a dead guest
|
||||
// is, with its keys read through SDL (see "Watch camera" below).
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <sys/mman.h>
|
||||
@@ -23,6 +24,7 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -50,8 +52,11 @@ extern "C" int runtime_memory_vma_info(uintptr_t address, int* prot, int* type,
|
||||
|
||||
using namespace Core::Bbsc::PatchTables;
|
||||
|
||||
// One line in this run's BBSC timeline (bloodborne_re.cpp, "Timeline").
|
||||
#define BBSC_TIMELINE(kind, ...) Core::Bloodborne::BbscTimelineEvent(kind, fmt::format(__VA_ARGS__))
|
||||
|
||||
// Keep it equal to "version" in mod.json and to the Windows mod it plays with.
|
||||
#define BBSC_VERSION "0.0.1"
|
||||
#define BBSC_VERSION "0.0.2"
|
||||
|
||||
namespace BbscMod {
|
||||
void LogPlain(const char* text);
|
||||
@@ -159,6 +164,7 @@ void OpenLog() {
|
||||
}
|
||||
std::filesystem::create_directories(data_dir / "logs", ignored);
|
||||
KeepNewest(data_dir / "logs", ".log", 4); // with this one, 5
|
||||
KeepNewest(data_dir / "logs", ".txt", 4); // BBSC 0.0.2's timelines (bbsc-timeline-*.txt)
|
||||
KeepNewest(data_dir / "captures" / "bloodborne-re", ".jsonl", 4);
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::tm local{};
|
||||
@@ -341,11 +347,15 @@ bool NoticesPresent() {
|
||||
|
||||
DWORD WINAPI Worker(void*) {
|
||||
OpenLog();
|
||||
BbscMod::LogPlain("BBSC mod loaded - version " BBSC_VERSION " (bbport, Linux)");
|
||||
BbscMod::LogPlain("BBSC mod loaded - version " BBSC_VERSION " (bbport, Linux; built " __DATE__
|
||||
" " __TIME__ ")");
|
||||
if (!NoticesPresent()) {
|
||||
LOG_ERROR(Mod, "LICENSE.txt / NOTICE.txt / SUPPORTERS.txt missing or changed in {} - BBSC "
|
||||
"is off",
|
||||
mod_dir.string());
|
||||
BBSC_TIMELINE("START", "LICENSE.txt / NOTICE.txt / SUPPORTERS.txt missing or changed in {} "
|
||||
"- BBSC is off",
|
||||
mod_dir.string());
|
||||
return 0;
|
||||
}
|
||||
LOG_INFO(Mod, "BBSC mod in pid {}", static_cast<int>(getpid()));
|
||||
@@ -362,6 +372,10 @@ DWORD WINAPI Worker(void*) {
|
||||
LOG_INFO(Mod, "Public game server ({}): vanilla summons - seamless summon patches and the "
|
||||
"5-hunter search off (the session itself stays seamless)",
|
||||
server);
|
||||
BBSC_TIMELINE("START", "Public game server ({}): vanilla summons - seamless summon "
|
||||
"patches and the 5-hunter search off (the session itself stays "
|
||||
"seamless)",
|
||||
server);
|
||||
}
|
||||
}
|
||||
BuildSites();
|
||||
@@ -380,6 +394,9 @@ DWORD WINAPI Worker(void*) {
|
||||
"No Bloodborne CUSA03173 01.09 image after {} s (first unknown site: {} at "
|
||||
"{:#x}) - not this game or not this version; nothing was changed",
|
||||
WaitForImageMs / 1000, bad ? bad->name : "?", bad ? bad->offset : 0);
|
||||
BBSC_TIMELINE("START", "No Bloodborne CUSA03173 01.09 image after {} s - not this game "
|
||||
"or not this version; nothing was changed",
|
||||
WaitForImageMs / 1000);
|
||||
return 0;
|
||||
}
|
||||
Sleep(1);
|
||||
@@ -402,6 +419,7 @@ DWORD WINAPI Worker(void*) {
|
||||
written = SyncSites();
|
||||
}
|
||||
LOG_INFO(Mod, "Image verified after {} ms: {} byte patch sites written", verified, written);
|
||||
BBSC_TIMELINE("START", "Image verified after {} ms: {} byte patch sites written", verified, written);
|
||||
|
||||
Core::Bloodborne::InstallReverseEngineeringTrace();
|
||||
BbscMod::StartRelay();
|
||||
@@ -450,6 +468,29 @@ u64 EbootAddress() {
|
||||
return EbootBase;
|
||||
}
|
||||
|
||||
// BBSC 0.0.2's safe import slot reads and writes (bbsc_http.cpp, bbsc_channel.cpp): on Windows a
|
||||
// slot page could be unreadable while the emulator linked the imports. Here the slot is only
|
||||
// touched while bbport's map says it is readable, as everything else this library reads.
|
||||
bool ReadSlotSafely(u64 address, u64* value) {
|
||||
if (!Readable(address, sizeof(u64))) {
|
||||
return false;
|
||||
}
|
||||
*value = *reinterpret_cast<const volatile u64*>(address);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteSlotSafely(u64 address, u64 value) {
|
||||
if (!Readable(address, sizeof(u64))) {
|
||||
return false;
|
||||
}
|
||||
DWORD old = 0;
|
||||
if (!VirtualProtect(reinterpret_cast<void*>(address), 8, PAGE_READWRITE, &old)) {
|
||||
return false;
|
||||
}
|
||||
*reinterpret_cast<volatile u64*>(address) = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Import slots swapped for wrappers (bbsc_http.cpp, bbsc_channel.cpp): put back if anything links
|
||||
// them again (bbport links the imports once, before the game runs; kept as the Windows mod has it).
|
||||
namespace {
|
||||
@@ -468,16 +509,19 @@ DWORD WINAPI SlotGuardThread(void*) {
|
||||
Sleep(5);
|
||||
std::scoped_lock lock{guarded_mutex};
|
||||
for (auto& guarded : guarded_slots) {
|
||||
auto* slot = reinterpret_cast<volatile u64*>(EbootBase + guarded.slot_offset);
|
||||
const u64 value = *slot;
|
||||
const u64 slot = EbootBase + guarded.slot_offset;
|
||||
u64 value = 0;
|
||||
if (!ReadSlotSafely(slot, &value)) {
|
||||
continue;
|
||||
}
|
||||
if (value == guarded.wrapper || value == 0 ||
|
||||
(value >= EbootBase && value < EbootBase + EbootSize)) {
|
||||
continue;
|
||||
}
|
||||
*reinterpret_cast<void* volatile*>(guarded.original) = reinterpret_cast<void*>(value);
|
||||
DWORD old = 0;
|
||||
VirtualProtect(const_cast<u64*>(slot), 8, PAGE_READWRITE, &old);
|
||||
*slot = guarded.wrapper;
|
||||
if (!WriteSlotSafely(slot, guarded.wrapper)) {
|
||||
continue; // the next round tries again
|
||||
}
|
||||
if (++guarded.rewritten <= 3) {
|
||||
LOG_INFO(Mod, "Import {} was linked again - wrapper put back ({}x)", guarded.name,
|
||||
guarded.rewritten);
|
||||
@@ -563,12 +607,165 @@ void SetHostLampPatches(bool hosting) {
|
||||
count);
|
||||
}
|
||||
|
||||
// The cinematic camera is not in the Linux build: it stays off.
|
||||
CinematicInput TakeCinematicInput() {
|
||||
return CinematicInput{};
|
||||
// ---- Watch camera (BBSC 0.0.2) -------------------------------------------------------------------
|
||||
// The F1 free camera is not in the Linux build: the Windows mod reads the keyboard and mouse for it
|
||||
// and hands the game a controller at rest, while here every key and button also reaches the game
|
||||
// through bbport's own pad (src/runtime_pad.c: WASD move, E / Q / F / Tab are game buttons) and
|
||||
// L3+R3 opens bbport's menu - flying the camera would move the hunter too. BBSC 0.0.2's watch
|
||||
// camera is different: it only comes on by itself while our hunter is dead in co-op (bloodborne_re.cpp,
|
||||
// CinematicForceOn after the death has played out), when the game has nothing to do with those
|
||||
// keys. So that camera is here, read through SDL like the F8 key (the game's window already uses
|
||||
// it), on the game thread, and only while the camera is on:
|
||||
// Tab / E / left click next hunter, Shift+Tab / Q previous, F fly freely and back; the
|
||||
// controller's R1 / L1 and right stick (left stick: closer / farther); arrow keys turn, W / S
|
||||
// closer / farther; flying freely: WASD, Space / E up, C / Q down, Shift fast, Alt slow.
|
||||
// F1 turns it off for the rest of that death (it is bbport's "let the mouse go" key too).
|
||||
// Mouse movement is not read: SDL's relative mouse state is bbport's own mouse look.
|
||||
namespace {
|
||||
void* WatchSdl(const char* name) {
|
||||
return dlsym(RTLD_DEFAULT, name);
|
||||
}
|
||||
void CinematicForceOff() {}
|
||||
void CinematicForceOn() {}
|
||||
|
||||
// SDL3 scancodes and gamepad enums (SDL_scancode.h, SDL_gamepad.h).
|
||||
enum : int {
|
||||
ScA = 4, ScC = 6, ScD = 7, ScE = 8, ScF = 9, ScQ = 20, ScS = 22, ScW = 26, ScTab = 43,
|
||||
ScSpace = 44, ScF1 = 58, ScRight = 79, ScLeft = 80, ScDown = 81, ScUp = 82, ScLShift = 225,
|
||||
ScLAlt = 226, ScRShift = 229, ScRAlt = 230, ScCount = 231
|
||||
};
|
||||
constexpr int PadL1 = 9, PadR1 = 10; // SDL_GAMEPAD_BUTTON_LEFT / RIGHT_SHOULDER
|
||||
constexpr int AxisLY = 1, AxisRX = 2, AxisRY = 3; // SDL_GAMEPAD_AXIS_*
|
||||
|
||||
std::mutex watch_mutex;
|
||||
bool watch_on = false; // switched on by the game (a death in co-op), off by F1 or respawn
|
||||
bool watch_watching = false; // the game says it is watching a hunter
|
||||
std::array<bool, ScCount> watch_keys_before{};
|
||||
bool watch_click_before = false;
|
||||
bool watch_l1_before = false, watch_r1_before = false;
|
||||
|
||||
float WatchAxis(int value) {
|
||||
const float v = static_cast<float>(value) / 32767.0f;
|
||||
constexpr float Dead = 0.15f;
|
||||
return std::abs(v) < Dead
|
||||
? 0.0f
|
||||
: std::clamp((v - std::copysign(Dead, v)) / (1.0f - Dead), -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
// The first controller the game's runtime has open (SDL_GetGamepadFromID gives only open ones).
|
||||
void* WatchGamepad() {
|
||||
using List = unsigned* (*)(int*);
|
||||
using FromId = void* (*)(unsigned);
|
||||
using Free = void (*)(void*);
|
||||
static const auto list = reinterpret_cast<List>(WatchSdl("SDL_GetGamepads"));
|
||||
static const auto from_id = reinterpret_cast<FromId>(WatchSdl("SDL_GetGamepadFromID"));
|
||||
static const auto release = reinterpret_cast<Free>(WatchSdl("SDL_free"));
|
||||
if (list == nullptr || from_id == nullptr || release == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
int count = 0;
|
||||
unsigned* ids = list(&count);
|
||||
void* pad = nullptr;
|
||||
for (int i = 0; ids != nullptr && i < count && pad == nullptr; ++i) {
|
||||
pad = from_id(ids[i]);
|
||||
}
|
||||
release(ids);
|
||||
return pad;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CinematicInput TakeCinematicInput() {
|
||||
std::scoped_lock lock{watch_mutex};
|
||||
CinematicInput in{};
|
||||
if (!watch_on) {
|
||||
return in;
|
||||
}
|
||||
in.enabled = true;
|
||||
using KeyState = const bool* (*)(int*);
|
||||
using Focus = void* (*)();
|
||||
using MouseState = unsigned (*)(float*, float*);
|
||||
static const auto key_state = reinterpret_cast<KeyState>(WatchSdl("SDL_GetKeyboardState"));
|
||||
static const auto focus = reinterpret_cast<Focus>(WatchSdl("SDL_GetKeyboardFocus"));
|
||||
static const auto mouse_state = reinterpret_cast<MouseState>(WatchSdl("SDL_GetMouseState"));
|
||||
int count = 0;
|
||||
const bool* keys = key_state != nullptr && focus != nullptr && focus() != nullptr
|
||||
? key_state(&count)
|
||||
: nullptr;
|
||||
std::array<bool, ScCount> now{};
|
||||
for (int i = 0; keys != nullptr && i < ScCount && i < count; ++i) {
|
||||
now[i] = keys[i];
|
||||
}
|
||||
const auto pressed = [&](int key) { return now[key] && !watch_keys_before[key]; };
|
||||
if (pressed(ScF1)) {
|
||||
watch_on = false;
|
||||
watch_watching = false;
|
||||
watch_keys_before = now;
|
||||
LOG_INFO(Debug, "BBSC watch camera: off (F1)");
|
||||
return CinematicInput{};
|
||||
}
|
||||
const bool shift = now[ScLShift] || now[ScRShift];
|
||||
in.switch_target = (pressed(ScTab) ? (shift ? -1 : 1) : 0) + (pressed(ScE) ? 1 : 0) -
|
||||
(pressed(ScQ) ? 1 : 0);
|
||||
in.toggle_free = pressed(ScF);
|
||||
const bool click = keys != nullptr && mouse_state != nullptr &&
|
||||
(mouse_state(nullptr, nullptr) & 1u) != 0; // SDL_BUTTON_LMASK
|
||||
in.switch_target += click && !watch_click_before ? 1 : 0;
|
||||
watch_click_before = click;
|
||||
in.forward = now[ScW];
|
||||
in.back = now[ScS];
|
||||
in.left = now[ScA];
|
||||
in.right = now[ScD];
|
||||
in.up = now[ScSpace] || (now[ScE] && !watch_watching);
|
||||
in.down = now[ScC] || (now[ScQ] && !watch_watching);
|
||||
in.fast = shift;
|
||||
in.slow = now[ScLAlt] || now[ScRAlt];
|
||||
in.look_up = now[ScUp];
|
||||
in.look_down = now[ScDown];
|
||||
in.look_left = now[ScLeft];
|
||||
in.look_right = now[ScRight];
|
||||
watch_keys_before = now;
|
||||
using Button = bool (*)(void*, int);
|
||||
using Axis = short (*)(void*, int);
|
||||
static const auto button = reinterpret_cast<Button>(WatchSdl("SDL_GetGamepadButton"));
|
||||
static const auto axis = reinterpret_cast<Axis>(WatchSdl("SDL_GetGamepadAxis"));
|
||||
void* pad = button != nullptr && axis != nullptr ? WatchGamepad() : nullptr;
|
||||
if (pad != nullptr) {
|
||||
const bool l1 = button(pad, PadL1), r1 = button(pad, PadR1);
|
||||
if (watch_watching) {
|
||||
in.switch_target += (r1 && !watch_r1_before ? 1 : 0) - (l1 && !watch_l1_before ? 1 : 0);
|
||||
}
|
||||
watch_l1_before = l1;
|
||||
watch_r1_before = r1;
|
||||
in.pad_look_x = WatchAxis(axis(pad, AxisRX));
|
||||
in.pad_look_y = -WatchAxis(axis(pad, AxisRY));
|
||||
in.pad_move_y = -WatchAxis(axis(pad, AxisLY)); // stick up = closer
|
||||
}
|
||||
return in;
|
||||
}
|
||||
|
||||
void CinematicForceOff() {
|
||||
std::scoped_lock lock{watch_mutex};
|
||||
watch_on = false;
|
||||
watch_watching = false;
|
||||
}
|
||||
|
||||
void CinematicForceOn() {
|
||||
if (!Core::Bbsc::SwitchEnabled("SHADPS4_BBSC_CINEMATIC")) {
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock{watch_mutex};
|
||||
if (!watch_on) {
|
||||
// Keys already held (the attack that killed us) do not count as presses.
|
||||
watch_keys_before.fill(true);
|
||||
watch_click_before = watch_l1_before = watch_r1_before = true;
|
||||
}
|
||||
watch_on = true;
|
||||
}
|
||||
|
||||
void CinematicSetWatching(bool watching) {
|
||||
std::scoped_lock lock{watch_mutex};
|
||||
watch_watching = watching;
|
||||
}
|
||||
|
||||
void CinematicPadRead(unsigned, unsigned char, unsigned char, unsigned char, unsigned char) {}
|
||||
|
||||
} // namespace Bbsc
|
||||
} // namespace Core
|
||||
|
||||
@@ -109,6 +109,11 @@ struct WSADATA {};
|
||||
inline int WSAStartup(unsigned short, WSADATA*) {
|
||||
return 0;
|
||||
}
|
||||
// BBSC 0.0.2's relay: the error of the last socket call, and shutdown's "both ways".
|
||||
inline int WSAGetLastError() {
|
||||
return errno;
|
||||
}
|
||||
#define SD_BOTH SHUT_RDWR
|
||||
// shutdown first: a thread blocked in recv on the socket wakes up (close alone leaves it there).
|
||||
inline int closesocket(SOCKET s) {
|
||||
shutdown(s, SHUT_RDWR);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "bbsc",
|
||||
"name": "BBSC - Bloodborne Seamless Co-op",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"author": "CLS_AMG; seamless summon by Wozzardman; server format by fhody125",
|
||||
"description": "One co-op session across areas, deaths, lamps, bosses and the Hunter's Dream. Bloodborne CUSA03173 v1.09 only. Warps and world sync go through the host's game server relay (port 20444). Off = the vanilla online game.",
|
||||
"games": ["CUSA03173"],
|
||||
@@ -24,6 +24,7 @@
|
||||
{"id": "solid_npcs", "name": "Solid NPCs", "description": "A guest sees NPCs solid, not as grey ghosts", "type": "bool", "default": true, "env": "SHADPS4_BBSC_SOLID_NPCS"},
|
||||
{"id": "death_penalty", "name": "Bloodstain on death", "description": "A kept death still drops a bloodstain and echoes", "type": "bool", "default": true, "env": "SHADPS4_BBSC_DEATH_PENALTY"},
|
||||
{"id": "free_bell", "name": "Free Beckoning Bell", "description": "The bell costs no Insight", "type": "bool", "default": true, "env": "SHADPS4_BBSC_FREE_BELL"},
|
||||
{"id": "bells_anywhere", "name": "Bells anywhere", "description": "The kit's bells ring anywhere, the Hunter's Dream included (your role, Insight and being online still count)", "type": "bool", "default": true, "env": "SHADPS4_BBSC_BELLS_ANYWHERE"},
|
||||
{"id": "five_hunters", "name": "Five hunters", "description": "The host may summon up to 4 guests", "type": "bool", "default": true, "env": "SHADPS4_BBSC_FIVE_HUNTERS"},
|
||||
{"id": "messages", "name": "Co-op texts", "description": "Texts that fit seamless play (no \"Returning to your world\")", "type": "bool", "default": true, "env": "SHADPS4_BBSC_MESSAGES"},
|
||||
{"id": "load_cover", "name": "Black fade on joins", "description": "The guest's loads happen behind a black fade", "type": "bool", "default": true, "env": "SHADPS4_BBSC_LOAD_COVER"},
|
||||
@@ -36,7 +37,7 @@
|
||||
{"id": "scale_enemy_stagger", "name": "Enemy stagger resistance per guest (%)", "description": "Harder to stagger normal enemies, per guest", "type": "int", "default": 15, "min": 0, "max": 200, "env": "BBSC_SCALE_ENEMY_STAGGER"},
|
||||
{"id": "scale_boss_stagger", "name": "Boss stagger resistance per guest (%)", "description": "Harder to stagger bosses, per guest", "type": "int", "default": 20, "min": 0, "max": 200, "env": "BBSC_SCALE_BOSS_STAGGER"},
|
||||
{"id": "shared_progress", "name": "Shared progress", "description": "When a guest goes home, bosses, shortcuts, doors and lamps the host has done are added to the guest's own world (adds only, never removes; NPC quests and your character are not touched; saves backed up first)", "type": "bool", "default": true, "env": "SHADPS4_BBSC_SHARED_PROGRESS"},
|
||||
{"id": "discord", "name": "Discord status", "description": "Shows BBSC, your area, boss fights and the number of hunters on your Discord profile", "type": "bool", "default": true, "env": "SHADPS4_BBSC_DISCORD"},
|
||||
{"id": "discord", "name": "Discord status", "description": "Shows BBSC, your area, your build, boss fights with a timer, when you are dead or spectating, and the number of hunters on your Discord profile", "type": "bool", "default": true, "env": "SHADPS4_BBSC_DISCORD"},
|
||||
{"id": "p2p_channel", "name": "P2P channel", "description": "Sends BBSC's own messages (follow, world sync, scaling, leaving) straight to the other players over the game's own connection when there is no BBSC relay - needed on public servers (The Hunter's Dream)", "type": "bool", "default": true, "env": "SHADPS4_BBSC_P2P_CHANNEL"},
|
||||
{"id": "relay", "name": "Relay address", "description": "host:20444 - leave empty to use the game server address from host_overrides.json (JOIN-HOST sets it)", "type": "text", "default": "", "env": "BBSC_RELAY"}
|
||||
]
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
namespace Libraries::Np::NpSignaling {
|
||||
|
||||
inline constexpr u8 kSignalingBbscWorldFlagsType = 0x22;
|
||||
inline constexpr u8 kSignalingBbscPvpType = 0x25;
|
||||
|
||||
// One chunk of a host world-flag snapshot: up to 8 event-flag groups (1000 flags, 125 bytes each)
|
||||
// of the host's current map block, in the fixed order of Core::Bloodborne's BbscWorldGroupKey.
|
||||
@@ -35,6 +36,15 @@ struct SignalingBbscWorldFlags {
|
||||
u8 groups[kBbscWorldFlagGroupsPerChunk][kBbscWorldFlagGroupBytes]{};
|
||||
};
|
||||
static_assert(sizeof(SignalingBbscWorldFlags) == 18 + 8 * 125);
|
||||
|
||||
struct SignalingBbscPvp {
|
||||
u8 magic[4] = {'S', 'H', 'A', 'D'};
|
||||
u8 type = kSignalingBbscPvpType;
|
||||
u8 kind = 0;
|
||||
u8 on = 0;
|
||||
u32 sequence = 0;
|
||||
};
|
||||
static_assert(sizeof(SignalingBbscPvp) == 11);
|
||||
#pragma pack(pop)
|
||||
|
||||
} // namespace Libraries::Np::NpSignaling
|
||||
@@ -7,7 +7,12 @@
|
||||
// F1 on/off | hold right mouse: look | WASD move | E / Space up, Q / C down
|
||||
// Shift fast, Alt slow | wheel: speed | Z / X zoom, V reset zoom | , . roll, / reset roll
|
||||
// arrow keys: look | [ ] smoothing (glide) less / more
|
||||
// Dead in co-op it watches a living hunter instead (bloodborne_re.cpp BbscWatchCamera): Tab / E /
|
||||
// left click next hunter, Shift+Tab / Q previous, F fly freely and back; the controller's R1 / L1
|
||||
// and right stick reach it only then (CinematicPadRead - the F1 camera leaves the pad to the hunter).
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <mutex>
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
@@ -23,6 +28,17 @@ namespace {
|
||||
std::mutex cine_mutex;
|
||||
CinematicInput cine_state;
|
||||
bool looking = false;
|
||||
bool watching = false; // the game thread says the camera watches a hunter
|
||||
unsigned pad_last = 0; // pad buttons at the last read
|
||||
int pad_switch = 0; // R1 / L1 presses while watching
|
||||
float pad_rx = 0.0f, pad_ry = 0.0f, pad_ly = 0.0f;
|
||||
|
||||
float PadAxis(unsigned char value) {
|
||||
const float v = (static_cast<float>(value) - 128.0f) / 127.0f;
|
||||
constexpr float Dead = 0.15f;
|
||||
return std::abs(v) < Dead ? 0.0f
|
||||
: std::clamp((v - std::copysign(Dead, v)) / (1.0f - Dead), -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
bool CinematicEnabledBySwitch() {
|
||||
static const bool enabled = SwitchEnabled("SHADPS4_BBSC_CINEMATIC");
|
||||
@@ -101,11 +117,25 @@ bool CinematicHandleEvent(const SDL_Event& event, SDL_Window* window) {
|
||||
if (key >= SDLK_F2 && key <= SDLK_F12) {
|
||||
return false; // the emulator's own hotkeys (fullscreen, fps, pause...) keep working
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
if (key == SDLK_TAB) {
|
||||
cine_state.switch_target += (event.key.mod & SDL_KMOD_SHIFT) != 0 ? -1 : 1;
|
||||
} else if (key == SDLK_E) {
|
||||
++cine_state.switch_target;
|
||||
} else if (key == SDLK_Q) {
|
||||
--cine_state.switch_target;
|
||||
} else if (key == SDLK_F) {
|
||||
cine_state.toggle_free = true;
|
||||
}
|
||||
}
|
||||
SetKey(key, event.type == SDL_EVENT_KEY_DOWN);
|
||||
return true;
|
||||
}
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
if (event.button.button == SDL_BUTTON_LEFT && event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
++cine_state.switch_target;
|
||||
}
|
||||
if (event.button.button == SDL_BUTTON_RIGHT) {
|
||||
looking = event.type == SDL_EVENT_MOUSE_BUTTON_DOWN;
|
||||
if (window != nullptr) {
|
||||
@@ -133,6 +163,15 @@ bool CinematicHandleEvent(const SDL_Event& event, SDL_Window* window) {
|
||||
CinematicInput TakeCinematicInput() {
|
||||
std::scoped_lock lock{cine_mutex};
|
||||
CinematicInput out = cine_state;
|
||||
if (watching) {
|
||||
out.pad_look_x = pad_rx;
|
||||
out.pad_look_y = -pad_ry;
|
||||
out.pad_move_y = -pad_ly; // stick up = closer
|
||||
out.switch_target += pad_switch;
|
||||
}
|
||||
pad_switch = 0;
|
||||
cine_state.switch_target = 0;
|
||||
cine_state.toggle_free = false;
|
||||
cine_state.mouse_dx = 0.0f;
|
||||
cine_state.mouse_dy = 0.0f;
|
||||
cine_state.wheel = 0;
|
||||
@@ -155,4 +194,29 @@ void CinematicForceOn() {
|
||||
}
|
||||
}
|
||||
|
||||
void CinematicSetWatching(bool on) {
|
||||
std::scoped_lock lock{cine_mutex};
|
||||
if (watching != on) {
|
||||
pad_switch = 0;
|
||||
pad_rx = pad_ry = pad_ly = 0.0f;
|
||||
}
|
||||
watching = on;
|
||||
}
|
||||
|
||||
void CinematicPadRead(unsigned buttons, unsigned char rx, unsigned char ry, unsigned char lx,
|
||||
unsigned char ly) {
|
||||
(void)lx;
|
||||
std::scoped_lock lock{cine_mutex};
|
||||
constexpr unsigned L1 = 0x400, R1 = 0x800;
|
||||
const unsigned pressed = buttons & ~pad_last;
|
||||
pad_last = buttons;
|
||||
if (!cine_state.enabled || !watching) {
|
||||
return;
|
||||
}
|
||||
pad_switch += ((pressed & R1) != 0 ? 1 : 0) - ((pressed & L1) != 0 ? 1 : 0);
|
||||
pad_rx = PadAxis(rx);
|
||||
pad_ry = PadAxis(ry);
|
||||
pad_ly = PadAxis(ly);
|
||||
}
|
||||
|
||||
} // namespace Core::Bbsc
|
||||
@@ -31,6 +31,10 @@ struct CinematicInput {
|
||||
// down/up, L2/R2 zoom out/in.
|
||||
float pad_move_x = 0.0f, pad_move_y = 0.0f, pad_rise = 0.0f;
|
||||
float pad_look_x = 0.0f, pad_look_y = 0.0f, pad_zoom = 0.0f;
|
||||
// Watching a living hunter while dead (2026-10-20): presses since the last call, + next /
|
||||
// - previous (Tab, E, left click, R1 / Shift+Tab, Q, L1), and F (watch <-> fly freely).
|
||||
int switch_target = 0;
|
||||
bool toggle_free = false;
|
||||
};
|
||||
|
||||
// Game thread: the current keys plus everything accumulated since the last call.
|
||||
@@ -42,4 +46,13 @@ void CinematicForceOff();
|
||||
// Game thread: switches the camera on (you died in co-op); nothing when it is already on.
|
||||
void CinematicForceOn();
|
||||
|
||||
// Game thread: the camera is watching a living hunter (you are dead). Emulator build: only then
|
||||
// does the controller reach the camera (CinematicPadRead) - the F1 camera leaves it to the hunter.
|
||||
void CinematicSetWatching(bool watching);
|
||||
|
||||
// Emulator build, pad thread (scePadReadState): the game's pad data, read only. While watching,
|
||||
// R1 / L1 switch the hunter and the right stick turns the camera.
|
||||
void CinematicPadRead(unsigned buttons, unsigned char rx, unsigned char ry, unsigned char lx,
|
||||
unsigned char ly);
|
||||
|
||||
} // namespace Core::Bbsc
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "core/bbsc_i18n.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
@@ -24,6 +25,9 @@ constexpr Translation Translations[] = {
|
||||
|
||||
constexpr std::u16string_view BossTag = u"<?npcNameId@";
|
||||
constexpr std::u16string_view BossPlaceholder = u"{boss}";
|
||||
// A text with numbers (BBSC 0.0.2's "Watching hunter 1 of 2."): its first number is {n}, its
|
||||
// second {count}.
|
||||
constexpr std::array<std::u16string_view, 2> NumberPlaceholders{u"{n}", u"{count}"};
|
||||
|
||||
// The PS4 system language codes of the game (BB_LANGUAGE): -1 for English and the others.
|
||||
int GameLanguage() {
|
||||
@@ -84,15 +88,41 @@ const char16_t* Text(const char16_t* english) {
|
||||
const std::u16string_view view{english};
|
||||
const size_t tag = view.find(BossTag);
|
||||
const size_t tag_end = tag != std::u16string_view::npos ? view.find(u"?>", tag) : tag;
|
||||
if (tag_end == std::u16string_view::npos) {
|
||||
return english;
|
||||
}
|
||||
static std::mutex mutex;
|
||||
static std::unordered_map<std::u16string_view, std::u16string> made;
|
||||
static std::unordered_map<std::u16string, std::u16string> made;
|
||||
std::scoped_lock lock{mutex};
|
||||
if (const auto found = made.find(view); found != made.end()) {
|
||||
if (const auto found = made.find(std::u16string{view}); found != made.end()) {
|
||||
return found->second.c_str();
|
||||
}
|
||||
if (tag_end == std::u16string_view::npos) {
|
||||
// A text with numbers: its {n} / {count} form's translation, with the same numbers. The
|
||||
// text may sit in a buffer that is written again, so the key is a copy of it.
|
||||
std::u16string form;
|
||||
std::array<std::u16string, NumberPlaceholders.size()> numbers;
|
||||
size_t count = 0;
|
||||
for (size_t at = 0; at < view.size();) {
|
||||
if (view[at] >= u'0' && view[at] <= u'9' && count < numbers.size()) {
|
||||
size_t end = at;
|
||||
while (end < view.size() && view[end] >= u'0' && view[end] <= u'9') {
|
||||
++end;
|
||||
}
|
||||
numbers[count] = std::u16string{view.substr(at, end - at)};
|
||||
form += NumberPlaceholders[count++];
|
||||
at = end;
|
||||
} else {
|
||||
form += view[at++];
|
||||
}
|
||||
}
|
||||
const char16_t* text = count != 0 ? Lookup(form, language) : nullptr;
|
||||
if (text == nullptr) {
|
||||
return english;
|
||||
}
|
||||
std::u16string out{text};
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
out = Replace(out, NumberPlaceholders[i], numbers[i]);
|
||||
}
|
||||
return made.emplace(std::u16string{view}, std::move(out)).first->second.c_str();
|
||||
}
|
||||
const std::u16string_view boss = view.substr(tag, tag_end + 2 - tag);
|
||||
std::u16string form{view};
|
||||
form.replace(tag, boss.size(), BossPlaceholder);
|
||||
@@ -100,7 +130,8 @@ const char16_t* Text(const char16_t* english) {
|
||||
if (text == nullptr) {
|
||||
return english;
|
||||
}
|
||||
return made.emplace(view, Replace(text, BossPlaceholder, boss)).first->second.c_str();
|
||||
return made.emplace(std::u16string{view}, Replace(text, BossPlaceholder, boss))
|
||||
.first->second.c_str();
|
||||
}
|
||||
|
||||
std::u16string TextWith(const char16_t* english_template, std::u16string_view boss) {
|
||||
|
||||
@@ -274,7 +274,7 @@ constexpr std::array<ByteEdit, 17> ByteEdits{{
|
||||
// instruction (0x018C52FF), so the guest's copy gives the guest's insight too; host and solo
|
||||
// never took the jump anyway.
|
||||
{"Echoes.GuestBossInsight", 0x018C52F9, {0x0F, 0x84}, {0xEB, 0x04}, Group::Echoes},
|
||||
// Five hunters (test 2026-10-08, host with 2 guests: a third player's co-op sign got
|
||||
// Five hunters (test 2026-10-08, host hardwater with 2 guests: a third player's co-op sign got
|
||||
// 18 empty sign lists in a row, the same player's invader sign was found at once). The host's
|
||||
// sign search 0x01872360 asks the server for co-op signs (type 7) only while one of vanilla's
|
||||
// two co-op slots is free - the slot table 0x0186FE40 fills [rbp-0x9C] / [rbp-0x90] with the
|
||||
@@ -360,7 +360,7 @@ constexpr std::array<WideEdit, 11> WideEdits{{
|
||||
// (The Kin Bell already works in the Dream through the general path - left as it is.)
|
||||
{"KitBells.HuntBellChecks", 0x0157F857, {0xC8, 0x00, 0x00, 0x00}, {0xB0, 0x0F, 0x00, 0x00},
|
||||
Group::KitBells},
|
||||
// Three players, 2026-10-08 (host, guest cls_amg; a tester's game had crashed but his
|
||||
// Three players, 2026-10-08 (host hardwater, guest cls_amg; abood's game had crashed but his
|
||||
// sign was still listed): the host summoned the dead sign and sat in CreateWait for 6 minutes,
|
||||
// then its own game raised "OnIrregularLeaveSession" (0x017897DA) and ended the session for the
|
||||
// guest too. That is the reliable-packet send 0x017894E0: a packet it cannot deliver to a member
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Core::Bbsc {
|
||||
// any environment variable, so the build runs straight from a launcher. Everything still only
|
||||
// applies to CUSA03173 01.09 (checked where each feature installs). Experimental switches stay
|
||||
// off, and so is the boot intro (SHADPS4_BBSC_INTRO=1 brings it back). Setting a variable always wins: "0" turns a default off, any other value turns it on.
|
||||
inline constexpr std::array<std::string_view, 31> DefaultOnSwitches{
|
||||
inline constexpr std::array<std::string_view, 35> DefaultOnSwitches{
|
||||
"SHADPS4_BBSC_SHARED_PROGRESS", // merge the host's progress into the guest's world (adds only)
|
||||
"SHADPS4_BBSC_WARP_TOGETHER", // the guest follows the host through a lamp warp
|
||||
"SHADPS4_BBSC_WORLD_SYNC", // ...and loads the host's map with the host's world flags
|
||||
@@ -46,6 +46,10 @@ inline constexpr std::array<std::string_view, 31> DefaultOnSwitches{
|
||||
"SHADPS4_BBSC_CINEMATIC", // F1: free camera for filming (keyboard + mouse)
|
||||
"SHADPS4_BBSC_SCALING", // enemy/boss HP, damage, stagger per guest (like ERSC)
|
||||
"SHADPS4_BBSC_INVADE_ANYWHERE", // every world open to invaders (Bell Maiden SpEffect 9020)
|
||||
"SHADPS4_BBSC_AREA_LIST_CAP", // fewer messages/bloodstains per request (Panic 265 crash)
|
||||
"SHADPS4_BBSC_WORLD_SYNC_ALL", // the host's flags for every area, not just the one it is in
|
||||
"SHADPS4_BBSC_TIMELINE", // a short bbsc-timeline-*.txt of what happened, next to the logs
|
||||
"SHADPS4_BBSC_BELLS_ANYWHERE", // the kit's bells ring anywhere (Dream too); role / Insight still count
|
||||
};
|
||||
|
||||
// False in the vanilla build (CMake BBSC_VANILLA): the emulator carries no BBSC of its own and
|
||||
|
||||
@@ -81,3 +81,7 @@
|
||||
{u"PvP is OFF.", {u"PvP DESACTIVADO.", u"PvP DÉSACTIVÉ.", u"PvP ist AUS.", u"PvP DISATTIVATO.", u"PvP DESATIVADO.", u"PvP ВЫКЛЮЧЕНО."}},
|
||||
{u"PvP needs another player in your game.", {u"El PvP requiere otro jugador en tu partida.", u"Le PvP requiert un autre joueur dans la partie.", u"PvP braucht einen weiteren Spieler in deinem Spiel.", u"Il PvP richiede un altro giocatore in partita.", u"O PvP precisa de outro jogador no seu jogo.", u"Для PvP нужен ещё один игрок в вашей игре."}},
|
||||
{u"Your own world was put back\n(the game had closed while you were a guest).\nRest at a lamp or reload to see it.", {u"Se ha restaurado tu propio mundo\n(el juego se cerró mientras eras invitado).\nDescansa en un farol o recarga para verlo.", u"Votre monde a été restauré\n(le jeu s'est fermé pendant que vous étiez invité).\nReposez-vous à une lanterne ou rechargez.", u"Deine eigene Welt wurde wiederhergestellt\n(das Spiel wurde geschlossen, während du Gast warst).\nRaste an einer Laterne oder lade neu, um sie zu sehen.", u"Il tuo mondo è stato ripristinato\n(il gioco si era chiuso mentre eri ospite).\nRiposa a una lanterna o ricarica per vederlo.", u"Seu próprio mundo foi restaurado\n(o jogo fechou enquanto você era convidado).\nDescanse num lampião ou recarregue para vê-lo.", u"Ваш собственный мир восстановлен\n(игра закрылась, пока вы были гостем).\nОтдохните у фонаря или перезапустите игру."}},
|
||||
{u"An item was added to your items.", {u"Se ha añadido un objeto a tus objetos.", u"Un objet a été ajouté à vos objets.", u"Ein Gegenstand wurde zu deinen Gegenständen hinzugefügt.", u"Un oggetto è stato aggiunto agli oggetti.", u"Um item foi adicionado aos seus itens.", u"Предмет добавлен в инвентарь."}},
|
||||
{u"Watching hunter {n} of {count}.\nTab / R1: next hunter.\nF: free camera.", {u"Observando al cazador {n} de {count}.\nTab / R1: siguiente cazador.\nF: cámara libre.", u"Vous suivez le chasseur {n} sur {count}.\nTab / R1 : chasseur suivant.\nF : caméra libre.", u"Du beobachtest Jäger {n} von {count}.\nTab / R1: nächster Jäger.\nF: freie Kamera.", u"Stai osservando il cacciatore {n} di {count}.\nTab / R1: cacciatore successivo.\nF: telecamera libera.", u"Observando o caçador {n} de {count}.\nTab / R1: próximo caçador.\nF: câmera livre.", u"Вы наблюдаете за охотником {n} из {count}.\nTab / R1: следующий охотник.\nF: свободная камера."}},
|
||||
{u"Watching the living hunter.\nF: free camera.", {u"Observando al cazador con vida.\nF: cámara libre.", u"Vous suivez le chasseur encore en vie.\nF : caméra libre.", u"Du beobachtest den lebenden Jäger.\nF: freie Kamera.", u"Stai osservando il cacciatore ancora in vita.\nF: telecamera libera.", u"Observando o caçador vivo.\nF: câmera livre.", u"Вы наблюдаете за живым охотником.\nF: свободная камера."}},
|
||||
{u"Watching hunter {n} of {count}.", {u"Observando al cazador {n} de {count}.", u"Vous suivez le chasseur {n} sur {count}.", u"Du beobachtest Jäger {n} von {count}.", u"Stai osservando il cacciatore {n} di {count}.", u"Observando o caçador {n} de {count}.", u"Вы наблюдаете за охотником {n} из {count}."}},
|
||||
File diff suppressed because it is too large.
Load diff
@@ -14,8 +14,15 @@ namespace Core::Bloodborne {
|
||||
std::optional<std::string> GetSeamlessHostPlacementHeader();
|
||||
bool SetSeamlessHostPlacementHeader(std::string_view value);
|
||||
void ClearSeamlessHostPlacementHeader();
|
||||
// BBSC timeline: one line in this run's bbsc-timeline-*.txt ("kind" like NET, RELAY, JOIN). Any
|
||||
// thread; reads no game memory.
|
||||
void BbscTimelineEvent(std::string_view kind, std::string_view text);
|
||||
// BBSC connect-first cross-map summon: the host map a pending sign should advertise, if any.
|
||||
std::optional<std::uint32_t> GetBbscConnectFirstAreaId();
|
||||
// BBSC message / bloodstain lists: lowers every "GetCount" of a .../message_area request body to
|
||||
// BBSC's cap (big lists run the game's heap dry - Panic 265). Returns what changed for the log, or
|
||||
// an empty string when the request is not one of those or nothing was over the cap.
|
||||
std::string CapBbscAreaListRequest(std::string_view path, std::string& body);
|
||||
void TraceMatching2LeaveRoom(std::uintptr_t return_address, std::uint64_t room_id);
|
||||
// BBSC warp together: a "the host is in this map" packet arrived from the signaling peer. Called
|
||||
// on the signaling receive thread, so it only records the message; TickBbscFollowHost acts on it.
|
||||
@@ -41,7 +48,8 @@ void OnBbscProgressPacket(const std::uint32_t* flags, std::size_t count);
|
||||
void OnBbscHostBossPacket(std::int32_t boss);
|
||||
void NoteBbscRelayHost(std::uint32_t sender, std::uint32_t packed_map, float x, float y, float z);
|
||||
std::uint32_t GetBbscRelayHost();
|
||||
// BBSC 0.0.2: `sender` (a host) rang our sign - an aimed placement (relay "SHAD" 0x31) was for us.
|
||||
// The relay sender of a host that rang our sign (aimed placement, mod): a joining guest takes that
|
||||
// host's packets before it can match the host's position (busy servers). Relay thread.
|
||||
void NoteBbscHostRangOurSign(std::uint32_t sender);
|
||||
std::int32_t GetBbscRelaySessionState();
|
||||
void InstallSeamlessCoopPatches();
|
||||
@@ -56,7 +64,19 @@ struct BbscPresence {
|
||||
int hunters = 1; // players in the session
|
||||
int max_hunters = 3;
|
||||
std::int64_t boss_since_s = 0; // unix time the boss bar appeared
|
||||
std::string build; // "Arcane build", "Quality build", ... (empty: no build yet)
|
||||
bool dead = false; // the local player is down
|
||||
bool spectating = false; // down in a co-op session while the others fight on
|
||||
int watching = 0; // the watch camera's hunter (1-based), 0 = free camera
|
||||
int watch_count = 0; // hunters it can watch
|
||||
};
|
||||
BbscPresence GetBbscPresence();
|
||||
|
||||
} // namespace Core::Bloodborne
|
||||
|
||||
namespace Core::Bbsc {
|
||||
// BBSC's Discord status (mod/bbsc_discord.cpp, both builds; Windows only). The emulator build
|
||||
// starts it from the eboot load; the emulator then leaves its own Discord status off.
|
||||
void StartDiscordStatus(std::string_view serial);
|
||||
bool DiscordStatusRunning();
|
||||
} // namespace Core::Bbsc
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
rem Makes a server card (address + registration key) to send to your friends.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\generate-card.ps1"
|
||||
@@ -1,4 +0,0 @@
|
||||
@echo off
|
||||
rem For the player who is NOT hosting: connects your game to the host's servers.
|
||||
rem Drag the host's card (BBSC-card-....json) onto this file, or double-click and paste the address.
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\join-host.ps1" -Address "%~1"
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
rem Creates an account on this PC's server (START-HOST first).
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\new-account.ps1"
|
||||
@@ -0,0 +1,66 @@
|
||||
# Hosting BBSC for your group
|
||||
|
||||
Seamless co-op with everything on (summons across areas, five hunters) needs two servers that
|
||||
your group shares:
|
||||
|
||||
| Server | What it does | Where it comes from |
|
||||
|---|---|---|
|
||||
| **shadNet** | Accounts, matchmaking, bells and summons | [shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) by Wozzardman |
|
||||
| **BBSC's game server** | The server list, messages, bloodstains and BBSC's relay | [`gameserver/`](gameserver/README.md), in this folder |
|
||||
|
||||
Both can run on one always-on computer (a home server, a VPS, or one of the players' PCs) that every
|
||||
player reaches through [Tailscale](https://tailscale.com), so no port is opened to the Internet.
|
||||
|
||||
## Quick start
|
||||
|
||||
The full guide, step by step, is [docs/SERVER.md](../../docs/SERVER.md). In short, on the server:
|
||||
|
||||
```sh
|
||||
# shadNet, in seamless mode
|
||||
docker build -t bbport-shadnet tools/shadnet-server
|
||||
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 \
|
||||
-e SHADNET_BLOODBORNE_SEAMLESS_COOP=1 bbport-shadnet
|
||||
|
||||
# BBSC's game server, next to it
|
||||
docker build -t bbport-bbsc-server tools/bbsc-server
|
||||
docker run -d --name bbsc-server --restart unless-stopped --network host \
|
||||
-v bbsc-data:/data -e BBSC_HOST=100.x.y.z bbport-bbsc-server
|
||||
```
|
||||
|
||||
`100.x.y.z` is the server's Tailscale address (`tailscale ip -4`). Without Docker, the game server
|
||||
is one Python file with nothing to install:
|
||||
|
||||
```sh
|
||||
python3 bbsc/server/gameserver/bbsc_gameserver.py --shadnet 100.x.y.z --host 100.x.y.z
|
||||
```
|
||||
|
||||
Then every player, in the launcher's **Multiplayer → Online** tab:
|
||||
|
||||
1. chooses **Your own server** with the server's address, and makes an account on it;
|
||||
2. sets **Game server** to `http://100.x.y.z:20443`;
|
||||
3. turns on BBSC in the **Seamless co-op (BBSC)** group.
|
||||
|
||||
## Ports
|
||||
|
||||
| Port | Protocol | Server | Used for |
|
||||
|---|---|---|---|
|
||||
| 31313 | TCP | shadNet | Sign-in and matchmaking |
|
||||
| 31314 | UDP | shadNet | Finding each other's address (STUN) |
|
||||
| 31315 | TCP | shadNet | Its web API: summon signs |
|
||||
| 20443 | TCP | Game server | The server list, messages, bloodstains |
|
||||
| 20444 | TCP | Game server | BBSC's relay: warps, world sync, enemy scaling |
|
||||
| 18671 | TCP | Game server | Its bbhost-compatible service (`--no-bbhost` turns it off) |
|
||||
| 3478 | UDP | Game server | Relay for players who cannot reach each other directly |
|
||||
|
||||
## What is in this folder
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `gameserver/` | BBSC's game server (`bbsc_gameserver.py`), its `ss.info` template and notice text |
|
||||
| `server/` | shadNet's settings as BBSC expects them (seamless co-op on, its ports, worlds and scoreboards) |
|
||||
|
||||
## Keep private
|
||||
|
||||
shadNet's registration key and its account database (the `shadnet-data` volume), and the game
|
||||
server's data (`bbsc-data`), belong to your group: do not share them or publish them.
|
||||
@@ -1,62 +0,0 @@
|
||||
BBSC - HOST SERVER PACKAGE
|
||||
==========================
|
||||
Lets you host BBSC (Bloodborne Seamless Co-op) for your friends: the shadNet server (accounts,
|
||||
matchmaking, co-op summons) and the BBSC game server (messages, bloodstains, server list).
|
||||
Every player, you included, also needs the BBSC game package (BBSC-<build>.zip: launcher +
|
||||
emulator + BBSC mod) and their own copy of Bloodborne. No game files are included here.
|
||||
|
||||
WHAT YOU NEED
|
||||
- Windows 10/11
|
||||
- One network you and your friends are all on: Radmin VPN, Hamachi, Tailscale, ZeroTier,
|
||||
or the same house (same router / Wi-Fi).
|
||||
Not a privacy VPN (ProtonVPN, NordVPN...) - friends cannot reach you through those.
|
||||
|
||||
FIRST TIME (HOST)
|
||||
1. Unzip this folder somewhere permanent (e.g. C:\BBSC-Host). Do not run it from inside the zip.
|
||||
2. Double-click START-HOST.cmd
|
||||
- if this PC is on more than one network it asks which one your friends use
|
||||
(START-HOST-OTHER-NETWORK.cmd asks again later)
|
||||
- say Yes to the Windows prompt (installs the C++ runtime, opens the firewall for VPN
|
||||
addresses and your own local network only - nothing is opened to the internet;
|
||||
older BBSC firewall rules that only allowed Radmin are updated)
|
||||
- it creates your own empty account database and your own registration key
|
||||
- it points YOUR game at your servers too
|
||||
3. Double-click GENERATE-CARD.cmd. It asks a name, uses the same address as START-HOST
|
||||
(Enter) and makes cards\BBSC-card-<name>.json with the address and your registration key.
|
||||
Send that file to your friends in a DM - it holds your key, never post it publicly.
|
||||
4. Your account: BBSC launcher > Settings > Accounts, pick your card, type a name and
|
||||
password, Create Account on Server. (Or NEW-ACCOUNT.cmd, for yourself or a friend.)
|
||||
|
||||
YOUR FRIENDS
|
||||
1. Drag your card onto JOIN-HOST.cmd (in the BBSC game package). It connects their game to
|
||||
your servers and adds the card to their launcher.
|
||||
(No card? Double-click JOIN-HOST and paste the address START-HOST showed.)
|
||||
2. BBSC launcher > Settings > Accounts > pick the card > name + password >
|
||||
Create Account on Server. Accounts from other hosts do not exist on your server.
|
||||
|
||||
EVERY TIME (HOST)
|
||||
Double-click START-HOST.cmd and keep the two small server windows open while you play.
|
||||
Changed network (e.g. Radmin to Hamachi)? START-HOST-OTHER-NETWORK.cmd, then GENERATE-CARD
|
||||
again and send the new card.
|
||||
|
||||
playit.gg
|
||||
Account creation works through a playit tunnel, but co-op needs ports 31313-31315, 20443 and
|
||||
20444 with the same numbers on one address, which playit's free tunnels do not give. Use
|
||||
Radmin VPN, Hamachi or Tailscale for playing.
|
||||
|
||||
KEEP PRIVATE
|
||||
server\shadnet.cfg (your registration key), server\db\ (everyone's accounts),
|
||||
server\host-address.txt and cards\ (your key again) are yours.
|
||||
Never share them or zip them up for someone else - give out a fresh copy of this package.
|
||||
|
||||
LOGS
|
||||
server\logs\ - send these with a bug report.
|
||||
|
||||
CREDITS
|
||||
See CREDITS.txt. shadNet server: shadPS4 team / Wozzardman (shadnet-p2p). Server list format
|
||||
and game server groundwork: Fahad (fhody125), No Dream For The Hunter.
|
||||
|
||||
SOURCE CODE (GPL-2.0-or-later)
|
||||
shadnet.exe and shadnet-sample.exe are built from https://github.com/Wozzardman/shadnet-p2p
|
||||
(commit 5a849a1). The emulator BBSC runs on is built from https://github.com/Wozzardman/shadp2p
|
||||
with the BBSC changes. Source for the BBSC changes is available from CLS_AMG on request.
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
rem Like START-HOST, but asks again which network your friends use (e.g. switched from Radmin to Hamachi).
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\start-host.ps1" -ChangeNetwork
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
rem Starts both BBSC servers on this PC (Radmin VPN, Hamachi, Tailscale, ZeroTier or home network).
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\start-host.ps1"
|
||||
@@ -1,96 +1,86 @@
|
||||
# BBSC game server
|
||||
|
||||
What makes Bloodborne believe it is online, without depending on anybody else's machine.
|
||||
The server that makes Bloodborne believe it is online, run by your own group instead of somebody
|
||||
else.
|
||||
|
||||
> Need help with any of this, or with shadPS4 and Bloodborne on PC generally?
|
||||
> Join the **Bloodborne PC** Discord — it is the main community for running this game on PC.
|
||||
## Why it exists
|
||||
|
||||
## Why this exists
|
||||
|
||||
Before Bloodborne enables a single online feature it fetches:
|
||||
Before Bloodborne turns on any online feature it fetches its **server list**:
|
||||
|
||||
```
|
||||
https://ss4.scej-network.jp:20443/bb-eu/ss.info
|
||||
```
|
||||
|
||||
That file is **base64-encoded XML**, and it is the **server list**. A `<gameurl2>` block names one
|
||||
URL per game API — login, notices, blood messages, ghosts, summoning, play logs — plus the timing
|
||||
and matching parameters the online systems run on. Everything the game does afterwards goes
|
||||
wherever that file says.
|
||||
|
||||
Private servers work by redirecting that one URL. Previously it pointed at `thehuntersdream.com`,
|
||||
so their `ss.info` decided where *our* co-op went. On 2026-09-21 their backend stopped responding
|
||||
(TCP connected, no HTTP reply, on both 18671 and 20443) and co-op stopped working for us with it.
|
||||
|
||||
Serving our own removes that dependency for good.
|
||||
That file is base64-encoded XML. Its `<gameurl2>` block names one URL per game service (sign-in,
|
||||
notices, messages, ghosts, summons, play logs) plus the timing and matching values the online
|
||||
systems run on, and the game goes wherever it says. A private server works by answering that one
|
||||
address. This one does, so a group's co-op does not depend on anybody else's machine staying up.
|
||||
|
||||
## What it does
|
||||
|
||||
| Endpoints | Where they go |
|
||||
| --- | --- |
|
||||
| the four `api_SummonData*` | the real shadnet server — this is where co-op actually happens |
|
||||
| everything else | this process, which answers empty-but-valid JSON so the game stops asking |
|
||||
|
||||
Blood messages, ghosts, notices and channels are **not implemented**. They get well-formed empty
|
||||
replies, which is enough to keep the game happy; this does not pretend to provide them.
|
||||
|
||||
Each player runs their own copy on `127.0.0.1`. Only the summon endpoints are shared, which is why
|
||||
the shadnet address is the one thing you have to set.
|
||||
| Service | Where it goes |
|
||||
|---|---|
|
||||
| The four `api_SummonData*` (summon signs) | The group's shadNet server, where co-op happens |
|
||||
| Messages and bloodstains | Kept here and shared between the players (`--data`) |
|
||||
| BBSC's relay (port 20444) | Here: warps, world sync and enemy scaling for BBSC |
|
||||
| bbhost's services (port 18671, STUN/relay on UDP 3478) | Here, for players who cannot reach each other directly |
|
||||
| Everything else (notices, play logs…) | Answered here, with empty but valid replies where nothing is kept |
|
||||
|
||||
## Running it
|
||||
|
||||
```
|
||||
python bbsc_gameserver.py --shadnet 100.101.102.103
|
||||
It is one Python 3 file with nothing to install:
|
||||
|
||||
```sh
|
||||
python3 bbsc_gameserver.py --shadnet 100.x.y.z --host 100.x.y.z
|
||||
```
|
||||
|
||||
Then in `%APPDATA%\shadPS4\host_overrides.json`:
|
||||
Most groups run it with Docker next to their shadNet server instead: see
|
||||
[the hosting guide](../README.md) and [docs/SERVER.md](../../../docs/SERVER.md#seamless-co-op-bbsc).
|
||||
Players then set **Game server** to `http://100.x.y.z:20443` in the launcher's
|
||||
**Multiplayer → Online** tab, which points the game's `ss.info` request here.
|
||||
|
||||
```json
|
||||
{ "https://ss4.scej-network.jp:20443": "http://127.0.0.1:20443" }
|
||||
```
|
||||
|
||||
Standard library only — no Flask — so a stock Python runs it. Leave the window open while playing;
|
||||
it logs every request it answers, which is the fastest way to see whether the game is really
|
||||
talking to us.
|
||||
It logs every request it answers (`logs/gameserver-*.log`), the quickest way to see whether the
|
||||
game is really talking to it.
|
||||
|
||||
| Option | Effect |
|
||||
| --- | --- |
|
||||
| `--shadnet ADDR` | **required** — the machine running `shadnet.exe` |
|
||||
| `--summon-interval N` | seconds between summon-sign checks (retail 35 / 30) |
|
||||
| `--level-range N` | co-op level window (retail ±20) |
|
||||
| `--host` / `--port` | where to listen (default `127.0.0.1:20443`) |
|
||||
| `--advertise ADDR` | address to write into `ss.info` when it differs from `--host` |
|
||||
| `--verbose` | log request headers and bodies |
|
||||
|---|---|
|
||||
| `--shadnet ADDR` | **Required:** the shadNet server's address |
|
||||
| `--host` / `--port` | Where to listen (`127.0.0.1:20443`) |
|
||||
| `--advertise ADDR` | The address written into `ss.info`, when it differs from `--host` |
|
||||
| `--relay-port N` | BBSC's relay port (20444; 0 turns it off) |
|
||||
| `--summon-interval N` | Seconds between summon-sign checks (retail 35; 5 for quick summons) |
|
||||
| `--level-range N` | Co-op level window (retail 20; 999 matches any level) |
|
||||
| `--no-playlog` | No play-log uploads |
|
||||
| `--no-bbhost` | Without bbhost's services |
|
||||
| `--data FILE` | Where messages and bloodstains are kept (`online-data.json`) |
|
||||
| `--log-dir DIR` | Where the logs go (`logs/`) |
|
||||
| `--verbose` | Log request headers and bodies |
|
||||
|
||||
Defaults change nothing except the addresses. Retuning is opt-in.
|
||||
The defaults change nothing but the addresses: retuning is opt-in.
|
||||
|
||||
## Retail tuning values
|
||||
|
||||
Taken from the real `ss.info`, recorded here because they are not visible anywhere in the game and
|
||||
several of them explain behaviour we spent a long time blaming on the emulator.
|
||||
From the real `ss.info`. They are not visible anywhere in the game, and several explain behaviour
|
||||
that looks like a bug of the port.
|
||||
|
||||
| Tag | Retail | What it controls |
|
||||
| --- | --- | --- |
|
||||
| `SummonDataGetListInterval2` | 35 | seconds between checks for a summon sign |
|
||||
| `SummonDataCreateInterval2` | 30 | how often a host re-posts its sign |
|
||||
| `SummonDataGetListGuardTimer2` | 60 | guard timer on the sign list |
|
||||
| `SummonDataGetListGetMaxCount2` | 20 | most signs returned at once |
|
||||
| `SummonDataCoopMatchingLevelUpperAbs2` | 20 | co-op level window, upper |
|
||||
| `SummonDataCoopMatchingLevelLowerAbs2` | −20 | co-op level window, lower |
|
||||
| `SummonDataBloodMadHunterPercent2` | 5 | blood-mad hunter chance |
|
||||
| `ReloadServerStatusInfoInterval2` | 300 | how often the game re-reads `ss.info` |
|
||||
|---|---|---|
|
||||
| `SummonDataGetListInterval2` | 35 | Seconds between checks for a summon sign |
|
||||
| `SummonDataCreateInterval2` | 30 | How often a host posts its sign again |
|
||||
| `SummonDataGetListGuardTimer2` | 60 | Guard timer on the sign list |
|
||||
| `SummonDataGetListGetMaxCount2` | 20 | Most signs returned at once |
|
||||
| `SummonDataCoopMatchingLevelUpperAbs2` | 20 | Co-op level window, upper |
|
||||
| `SummonDataCoopMatchingLevelLowerAbs2` | −20 | Co-op level window, lower |
|
||||
| `SummonDataBloodMadHunterPercent2` | 5 | Blood-mad hunter chance |
|
||||
| `ReloadServerStatusInfoInterval2` | 300 | How often the game reads `ss.info` again |
|
||||
|
||||
**`SummonDataGetListInterval2` is what "quick summoning" means.** A sign can take over half a
|
||||
minute to appear on retail settings, and no amount of work inside the game could have changed it —
|
||||
the value lives in this file.
|
||||
`SummonDataGetListInterval2` is what "quick summoning" means: on retail values a sign can take over
|
||||
half a minute to show up, and nothing inside the game changes that.
|
||||
|
||||
## Credit
|
||||
## Credits
|
||||
|
||||
The `ss.info` format, the endpoint list and the JSON response shapes come from
|
||||
[NoDreamForHunter](https://github.com/fhody125-web) by **fhody125**, MIT licensed — see
|
||||
`LICENSE.NoDreamForHunter`. He handed over the project and a real `ss.info` when thehuntersdream
|
||||
went down, which is the only reason any of this was possible. `ss_info_template.xml` is his file
|
||||
with the shadnet address left as a placeholder.
|
||||
|
||||
This script is a standalone reimplementation rather than a fork: no Flask dependency, and it can
|
||||
retune the summon parameters on the way out.
|
||||
By CLS_AMG, part of [BBSC](https://github.com/Classmg22/BBSC) (GPL-2.0-or-later). The `ss.info`
|
||||
format, the list of services and the shapes of the replies come from
|
||||
[NoDreamForHunter](https://github.com/fhody125-web) by fhody125 (MIT, `LICENSE.NoDreamForHunter`);
|
||||
`ss_info_template.xml` is his file with the shadNet address left as a placeholder. This server is a
|
||||
standalone rewrite of it, with no dependencies.
|
||||
@@ -422,8 +422,10 @@ def _matches(request, sign):
|
||||
types = {f.get("SummonType") for f in request.get("SummonTypeList") or [] if "SummonType" in f}
|
||||
if types and sign.get("SummonType") not in types:
|
||||
return False
|
||||
# The password must be the same on both sides, as in the game (2026-10-19): a search with no
|
||||
# password used to match passworded signs too. None/absent = no password.
|
||||
word = request.get("SummonWord") or ""
|
||||
if word and word != (sign.get("SummonWord") or ""):
|
||||
if word != (sign.get("SummonWord") or ""):
|
||||
return False
|
||||
level, theirs = request.get("MatchingLevel"), sign.get("MatchingLevel")
|
||||
if not word and isinstance(level, int) and isinstance(theirs, int) and level >= 0 and theirs >= 0:
|
||||
@@ -621,6 +623,13 @@ SHAD_MAGIC = b"BBR1"
|
||||
class Relay:
|
||||
def __init__(self):
|
||||
self.sock = None
|
||||
# Observability only. No changes to the BBHOST or BBR1 wire protocols.
|
||||
self.datagrams_in = 0
|
||||
self.bytes_in = 0
|
||||
self.datagrams_out = 0
|
||||
self.bytes_out = 0
|
||||
self.send_errors = 0
|
||||
self.unrecognized_in = 0
|
||||
self.by_token = {} # token -> port
|
||||
self.ports = {} # port -> {"token", "addr": (ip, port), "seen"}
|
||||
self.frames = 0
|
||||
@@ -628,13 +637,48 @@ class Relay:
|
||||
self.shad_frames = 0
|
||||
self.shad_missing = {} # (src, dst) -> (last told, last logged) for unregistered dst
|
||||
|
||||
def start(self, bind, port):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.bind((bind, port))
|
||||
def start(self, bind, port, rcvbuf_bytes=0):
|
||||
# Explicit opt-in only. Linux doubles SO_RCVBUF for accounting and caps
|
||||
# the requested value using net.core.rmem_max (unless SO_RCVBUFFORCE).
|
||||
# Log the observed value; changing sysctl alone does not resize a socket.
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
if rcvbuf_bytes:
|
||||
if not 8192 <= rcvbuf_bytes <= 64 * 1024 * 1024:
|
||||
raise ValueError("UDP receive buffer must be 8192..67108864 bytes")
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, rcvbuf_bytes)
|
||||
sock.bind((bind, port))
|
||||
except Exception:
|
||||
sock.close()
|
||||
raise
|
||||
self.sock = sock
|
||||
actual = sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
|
||||
if rcvbuf_bytes:
|
||||
say("UDP SO_RCVBUF: requested {} bytes; kernel reports {} bytes (Linux accounting includes 2x)".format(
|
||||
rcvbuf_bytes, actual))
|
||||
if actual < rcvbuf_bytes * 2:
|
||||
say("UDP buffer capped by kernel; check net.core.rmem_max before tuning")
|
||||
threading.Thread(target=self._loop, daemon=True, name="bbhost-stun").start()
|
||||
say("STUN + relay on udp {}:{} (relay ports {}-{}, players reach it at {})".format(
|
||||
bind, port, OPTS["relay_ports"][0], OPTS["relay_ports"][1], OPTS["public_ip"] or "?"))
|
||||
|
||||
def _send_packet(self, data, addr):
|
||||
"""Exactly the original sendto(data, addr), with counters only."""
|
||||
try:
|
||||
result = self.sock.sendto(data, addr)
|
||||
except OSError:
|
||||
self.send_errors += 1
|
||||
raise
|
||||
self.datagrams_out += 1
|
||||
self.bytes_out += result
|
||||
return result
|
||||
|
||||
def counters(self):
|
||||
"""Cumulative traffic counters for read-only periodic diagnostics."""
|
||||
return {k: getattr(self, k) for k in (
|
||||
"datagrams_in", "bytes_in", "datagrams_out", "bytes_out",
|
||||
"send_errors", "unrecognized_in", "frames", "shad_frames")}
|
||||
|
||||
def expire(self, now):
|
||||
with LOCK:
|
||||
for port, slot in list(self.ports.items()):
|
||||
@@ -669,7 +713,13 @@ class Relay:
|
||||
try:
|
||||
data, addr = self.sock.recvfrom(65536)
|
||||
except OSError:
|
||||
# Do not spin at 100% CPU after a socket is closed.
|
||||
if self.sock.fileno() < 0:
|
||||
return
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
self.datagrams_in += 1
|
||||
self.bytes_in += len(data)
|
||||
try:
|
||||
if data[:4] == SHAD_MAGIC:
|
||||
self._shad(data, addr)
|
||||
@@ -677,6 +727,8 @@ class Relay:
|
||||
self._forward(data, addr)
|
||||
elif len(data) >= 20 and data[0:2] == b"\x00\x01":
|
||||
self._binding(data, addr)
|
||||
else:
|
||||
self.unrecognized_in += 1
|
||||
except Exception as e: # one bad datagram must not stop the relay
|
||||
say("stun/relay error from {}: {!r}".format(addr, e))
|
||||
|
||||
@@ -692,7 +744,7 @@ class Relay:
|
||||
target = self.ports.get(dst)
|
||||
to = target and target.get("addr")
|
||||
if to:
|
||||
self.sock.sendto(bytes([0xFB, ord("r")]) + struct.pack("!H", src_port) + data[12:], to)
|
||||
self._send_packet(bytes([0xFB, ord("r")]) + struct.pack("!H", src_port) + data[12:], to)
|
||||
self.frames += 1
|
||||
|
||||
# shadPS4 P2P relay (BBSC fork, 2026-10-12). Players whose routers or CGNAT stop direct P2P
|
||||
@@ -713,7 +765,7 @@ class Relay:
|
||||
if old is None or old["addr"] != addr:
|
||||
say("shadPS4 relay: {} registered from {}:{}".format(
|
||||
npid.rstrip(b"\0").decode("ascii", "replace"), addr[0], addr[1]))
|
||||
self.sock.sendto(SHAD_MAGIC + b"\x02", addr)
|
||||
self._send_packet(SHAD_MAGIC + b"\x02", addr)
|
||||
elif kind == 0x03 and len(data) >= 37:
|
||||
dst, src = data[5:21], data[21:37]
|
||||
with LOCK:
|
||||
@@ -732,11 +784,11 @@ class Relay:
|
||||
tell, logged = now - last[0] >= 10, now - last[1] >= 60
|
||||
self.shad_missing[key] = (now if tell else last[0], now if logged else last[1])
|
||||
if to:
|
||||
self.sock.sendto(SHAD_MAGIC + b"\x04" + src + data[37:], to)
|
||||
self._send_packet(SHAD_MAGIC + b"\x04" + src + data[37:], to)
|
||||
self.shad_frames += 1
|
||||
else:
|
||||
if tell:
|
||||
self.sock.sendto(SHAD_MAGIC + b"\x05" + dst, addr)
|
||||
self._send_packet(SHAD_MAGIC + b"\x05" + dst, addr)
|
||||
if logged:
|
||||
say("shadPS4 relay: {} -> {} dropped - {} is not on the relay (older emulator?)".format(
|
||||
src.rstrip(b"\0").decode("ascii", "replace"),
|
||||
@@ -773,7 +825,7 @@ class Relay:
|
||||
mapped = (ip, port)
|
||||
value = struct.pack("!BBH", 0, 1, mapped[1]) + socket.inet_aton(mapped[0])
|
||||
attrs = struct.pack("!HH", 0x0001, len(value)) + value + attrs
|
||||
self.sock.sendto(struct.pack("!HH", 0x0101, len(attrs)) + txid + attrs, addr)
|
||||
self._send_packet(struct.pack("!HH", 0x0101, len(attrs)) + txid + attrs, addr)
|
||||
|
||||
|
||||
RELAY = Relay()
|
||||
|
||||
@@ -24,6 +24,7 @@ it runs on a stock Python, and it can retune the summon parameters on the way ou
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import collections
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
@@ -239,6 +240,25 @@ MESSAGES_PER_REGION = 100 # newest kept per region
|
||||
TOMBS_PER_REGION = 40
|
||||
TOMBS_PER_USER_REGION = 3 # a player's older bloodstains in the same region drop out
|
||||
TOMB_MAX_AGE = 7 * 24 * 3600
|
||||
# Reply size cap (2026-10-18). Reports (valriv55, Gascoigne / Cathedral Ward, host and guest, solo
|
||||
# too): "[Dantelion2 Panic] (null)(265) Transaction failed" every time right after a
|
||||
# tomb_messenger/message_area reply of ~155 KB; replies of 97 KB / 106 KB never crashed.
|
||||
# Lists are cut, newest first, so the whole reply stays under this many bytes of JSON.
|
||||
# --tomb-max-response-bytes (SeamlessHunt's flag, 2026-10-17) sets it for messages and bloodstains;
|
||||
# 0 there = no cap. Without the flag the 64 KB default applies.
|
||||
AREA_REPLY_BUDGET = 64 * 1024
|
||||
|
||||
def _within_budget(entries, budget=None):
|
||||
budget = AREA_REPLY_BUDGET if budget is None else budget
|
||||
if not budget:
|
||||
return list(entries)
|
||||
kept, size = [], 512 # room for the reply's other fields
|
||||
for entry in entries:
|
||||
size += len(json.dumps(entry, separators=(",", ":"))) + 1
|
||||
if size > budget:
|
||||
break
|
||||
kept.append(entry)
|
||||
return kept
|
||||
|
||||
def _as_int(value, default=0):
|
||||
try:
|
||||
@@ -367,6 +387,10 @@ def blood_area(req):
|
||||
here = [m for m in messages if _region(m) == key and m["UserId"] != user]
|
||||
here.sort(key=lambda m: m["BloodMessId"], reverse=True)
|
||||
found += [_message_entry(m) for m in here[:limit]]
|
||||
total = len(found)
|
||||
found = _within_budget(found)
|
||||
if len(found) < total:
|
||||
print(" messages: reply cut to {} of {} (size cap)".format(len(found), total), flush=True)
|
||||
areas = ["{}/{}".format(*_region(a)[:2]) for a in body.get("AreaInfoList") or []]
|
||||
stored = sorted({"{}/{}".format(m["AreaId"], m["AreaRegionId"]) for m in messages})
|
||||
print(" messages: {} for user {} (asked {}; stored in {})".format(
|
||||
@@ -488,6 +512,11 @@ def tomb_area(req):
|
||||
here = [t for t in tombs if _region(t) == key and t["UserId"] != user]
|
||||
here.sort(key=lambda t: t["TombMessId"], reverse=True)
|
||||
found += [_tomb_entry(t) for t in here[:limit]]
|
||||
total = len(found)
|
||||
found = _within_budget(found)
|
||||
if len(found) < total:
|
||||
print(" bloodstains: reply cut to {} of {} (size cap)".format(len(found), total),
|
||||
flush=True)
|
||||
areas = ["{}/{}".format(*_region(a)[:2]) for a in body.get("AreaInfoList") or []]
|
||||
print(" bloodstains: {} for user {} (asked {}; {} stored)".format(
|
||||
len(found), user, ",".join(areas) or "-", len(tombs)), flush=True)
|
||||
@@ -527,16 +556,42 @@ def tomb_delete(req):
|
||||
# (FormDataVersion, FormData, ChannelId, ShareLevel, Status, DiscernmentWord, RitualLevel,
|
||||
# HolyGrailTypeId, FixedOrGeneral, SubFeatureFlag, CreateUserId, CreateCharaId, CreateDate,
|
||||
# LastPlayDate, TurnoutLevel, WishMaterialList); the reply parser 0x01E7F240 reads ChannelId and
|
||||
# DiscernmentWord at the top level of an upload / share reply. Search (used by the ritual itself,
|
||||
# GetCount 1) stays empty on purpose: answering it could swap a player's freshly made dungeon for
|
||||
# someone else's. Unknown ids are never reported in LostChannelIdList (dungeons made before this
|
||||
# server stored them must not be dropped from anyone's save).
|
||||
# DiscernmentWord at the top level of an upload / share reply. Search answers only the fixed
|
||||
# dungeons (FIXED_CHALICES below); any other search stays empty on purpose: answering it could swap
|
||||
# a player's freshly made dungeon for someone else's. Unknown ids are never reported in
|
||||
# LostChannelIdList (dungeons made before this server stored them must not be dropped from anyone's
|
||||
# save).
|
||||
GLYPH_LETTERS = "abcdefghijkmnpqrstuvwxyz23456789"
|
||||
CHANNEL_SKIP = ("SessionId", "MessageId", "UserId", "CharaId")
|
||||
# keys the server owns: an upload never overwrites them
|
||||
CHANNEL_SERVER_KEYS = ("ChannelId", "DiscernmentWord", "CreateUserId", "CreateCharaId",
|
||||
"CreateDate", "LastPlayDate")
|
||||
|
||||
# Fixed chalice dungeons (2026-10-19). The altar ritual of a FIXED chalice (Pthumeru, Hintertomb,
|
||||
# Loran, Isz) asks /channel/search for its dungeon: FixedOrGeneralList [{"FixedOrGeneral": 2}],
|
||||
# HolyGrailTypeId, RitualLevel, SubFeatureFlag 256, GetCount 1 (server log 2026-09-29 00:17:07,
|
||||
# Pthumeru level 1; that player was back at the login 18 s later). This server answered with an
|
||||
# empty list, so the ritual had no dungeon. The ten fixed dungeons' FormData is from
|
||||
# NoDreamForTheHunterBB by Fahad (github.com/Mob100percent/NoDreamForTheHunterBB, GPL-3.0), taken
|
||||
# from thehuntersdream.com. Each becomes one shared channel here (made at its first search, owner 0
|
||||
# so no player's share / upload changes it), so every player gets the same ChannelId and glyph.
|
||||
# Every entry: SubFeatureFlag 256, FixedOrGeneral 2, FormDataVersion 0, ShareLevel 2, Status 1,
|
||||
# UnlockFlagList [0, <bit>, 0] (the bit the game sends in UnlockedFlagList), no WishMaterialList.
|
||||
FIXED_CHALICE_SUB_FEATURE = 256
|
||||
FIXED_CHALICES = [
|
||||
# (HolyGrailTypeId, RitualLevel, unlock bit, FormData)
|
||||
(0, 1, 1, "BQAAAIwAAAAAWgod3RcAAP////////////////////8+pk47WgAAAP////////////////////+g3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(0, 2, 128, "BQAAAIwAAAAAWhQdQRgAAP////////////////////8+pk47WgAAAEimTjtbAAAA//////////+g3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(0, 3, 16384, "BQAAAIwAAAAAWh4dpRgAAKBHXzs9AAAAoOaDO1UAAAA+pk47WgAAAEimTjtbAAAA//////////+g3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(0, 4, 2097152, "BQAAAIwAAAAAWigdCRkAAEDOYDs+AAAAQANtO0YAAAA+pk47WgAAAEimTjtbAAAAXKZOO10AAACg3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(0, 5, 268435456, "BQAAAIwAAAAAWjIdbRkAAEDOYDs+AAAA4POGO1cAAAA+pk47WgAAAEimTjtbAAAAXKZOO10AAACg3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(1, 2, 256, "BQAAAIwAAAAAWhUdSxgAAP////////////////////+spk47XgAAAMB1ETsKAAAA//////////+g3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(1, 3, 32768, "BQAAAIwAAAAAWh8drxgAAP//////////QG2FO1YAAACspk47XgAAALamTjtfAAAAwHUROwoAAACg3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(2, 4, 8388608, "BQAAAIwAAAAAWiodHRkAAP////////////////////8kp047YAAAAP////////////////////+g3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(2, 5, 1073741824, "BQAAAIwAAAAAWjQdgRkAAP//////////gHqIO1gAAAAkp047YAAAAC6nTjthAAAA//////////+g3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
(3, 5, 2147483648, "BQAAAIwAAAAAWjUdixkAAD6mTjtaAAAAIAGKO1kAAACSp047YgAAAEimTjtbAAAArKZOO14AAACg3UY7LQAAAP///////////////////////////////y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="),
|
||||
]
|
||||
|
||||
|
||||
def _date(when):
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(when))
|
||||
@@ -635,7 +690,64 @@ def channel_word_search(req):
|
||||
found = [_channel_entry(c) for c in STORE.data["channels"].values()
|
||||
if word and c.get("DiscernmentWord") == word]
|
||||
print(" chalice: glyph search '{}' -> {} found".format(word, len(found)), flush=True)
|
||||
return empty("ChannelWordSearchResponse", ChannelList=found)
|
||||
# Both shapes: NoDreamForTheHunterBB answers a glyph search with the dungeon's fields at the top
|
||||
# level, this server used a ChannelList; which one the game reads is not checked yet. Unread keys
|
||||
# are ignored by the game's parser.
|
||||
return empty("ChannelWordSearchResponse", ChannelList=found, **(found[0] if found else {}))
|
||||
|
||||
|
||||
def _fixed_channel(grail, level, bit, form_data):
|
||||
"""The shared channel of one fixed chalice dungeon, made on first use. Call with STORE.lock."""
|
||||
channels = STORE.data["channels"]
|
||||
key = "{}-{}".format(grail, level)
|
||||
known = channels.get(str(STORE.data.setdefault("fixed_channels", {}).get(key)))
|
||||
if known is not None:
|
||||
return known, False
|
||||
channel_id = STORE.next_id()
|
||||
channel = {"ChannelId": channel_id, "_created": time.time(), "_played": time.time(),
|
||||
"DiscernmentWord": _new_glyph({c.get("DiscernmentWord") for c in channels.values()}),
|
||||
"CreateUserId": 0, "CreateCharaId": 0,
|
||||
"FixedOrGeneral": 2, "FormData": form_data, "FormDataVersion": 0,
|
||||
"HolyGrailTypeId": grail, "RitualLevel": level,
|
||||
"SubFeatureFlag": FIXED_CHALICE_SUB_FEATURE, "ShareLevel": 2, "Status": 1,
|
||||
"TurnoutLevel": 0, "WishMaterialList": [],
|
||||
"UnlockFlagList": [{"UnlockFlag": 0}, {"UnlockFlag": bit}, {"UnlockFlag": 0}]}
|
||||
channels[str(channel_id)] = channel
|
||||
STORE.data["fixed_channels"][key] = channel_id
|
||||
return channel, True
|
||||
|
||||
|
||||
def channel_search(req):
|
||||
body = _request(req)
|
||||
kinds = {_as_int(x.get("FixedOrGeneral"), -1) for x in body.get("FixedOrGeneralList") or []
|
||||
if isinstance(x, dict)}
|
||||
# a field the game leaves out or sends as null does not narrow the search
|
||||
wanted = {key: _as_int(body[key], -1) for key in ("HolyGrailTypeId", "RitualLevel",
|
||||
"SubFeatureFlag", "Status")
|
||||
if body.get(key) is not None}
|
||||
limit = max(_as_int(body.get("GetCount"), 1), 1)
|
||||
found, made = [], []
|
||||
if 2 in kinds:
|
||||
with STORE.lock:
|
||||
for grail, level, bit, form_data in FIXED_CHALICES:
|
||||
have = {"HolyGrailTypeId": grail, "RitualLevel": level,
|
||||
"SubFeatureFlag": FIXED_CHALICE_SUB_FEATURE, "Status": 1}
|
||||
if any(have[key] != value for key, value in wanted.items()):
|
||||
continue
|
||||
channel, new = _fixed_channel(grail, level, bit, form_data)
|
||||
if new:
|
||||
made.append(channel["ChannelId"])
|
||||
found.append(_channel_entry(channel))
|
||||
if len(found) >= limit:
|
||||
break
|
||||
if made:
|
||||
STORE.save()
|
||||
print(" chalice: search {} {} -> {}{}".format(
|
||||
sorted(kinds), wanted,
|
||||
", ".join("fixed dungeon {} glyph {}".format(c["ChannelId"], c["DiscernmentWord"])
|
||||
for c in found) or "nothing (only fixed dungeons are answered)",
|
||||
" (new: {})".format(made) if made else ""), flush=True)
|
||||
return empty("ChannelSearchResponse", ChannelList=found)
|
||||
|
||||
|
||||
# Server notice (welcome message): server-notice.txt next to the messages file (--data). First line =
|
||||
@@ -651,7 +763,11 @@ def normal_notice(req):
|
||||
title, text = lines[0].strip(), "\n".join(lines[1:]).strip()
|
||||
if title or text:
|
||||
stamp = int(os.path.getmtime(path))
|
||||
notices.append({"NoticeId": stamp, "Title": title, "Notice": text,
|
||||
# Id + base64 text: confirmed by a server owner (2026-10-16) - with NoticeId and plain
|
||||
# text the game showed "There is no new information."
|
||||
notices.append({"Id": stamp, "NoticeId": stamp,
|
||||
"Title": base64.b64encode(title.encode("utf-8")).decode("ascii"),
|
||||
"Notice": base64.b64encode(text.encode("utf-8")).decode("ascii"),
|
||||
"CheckTime": stamp})
|
||||
except OSError:
|
||||
pass
|
||||
@@ -696,10 +812,10 @@ ROUTES = {
|
||||
"/chair_messenger/get": lambda req: empty("ChairMessGetListResponse", ChairMessCount=0),
|
||||
"/chair_messenger/update": lambda req: empty("ChairMessRespawnPointNoticeResponse"),
|
||||
|
||||
# chalice dungeon glyphs (channels), shared; search stays empty (see channel_upload)
|
||||
# chalice dungeon glyphs (channels), shared; search answers only the fixed dungeons
|
||||
"/channel/upload": channel_upload,
|
||||
"/channel/share": channel_share,
|
||||
"/channel/search": lambda req: empty("ChannelSearchResponse"),
|
||||
"/channel/search": channel_search,
|
||||
"/channel/word_search": channel_word_search,
|
||||
"/channel/get_details_info": channel_get_details,
|
||||
"/channel/get_info": channel_get_info,
|
||||
@@ -824,26 +940,84 @@ class Handler(BaseHTTPRequestHandler):
|
||||
do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = _handle
|
||||
|
||||
class Tee:
|
||||
"""Everything printed goes to the console window and to the log file."""
|
||||
"""Everything printed goes to the log file at once and to the console window through a queue.
|
||||
|
||||
def __init__(self, *streams):
|
||||
self.streams = streams
|
||||
2026-10-20 (Eryk's server, 17:19:22-17:21:26): nothing was answered for two minutes while
|
||||
shadNet ran on, and both players' games hung waiting. Every request prints before it replies,
|
||||
and a paused console window (a click into it - Windows QuickEdit - stops every write until a
|
||||
key is pressed) stopped every one of them. The console now has its own writer thread and a
|
||||
bounded queue, so a paused window only delays the console; the log file keeps every line.
|
||||
"""
|
||||
|
||||
CONSOLE_QUEUE_MAX = 5000
|
||||
|
||||
def __init__(self, console, log):
|
||||
self.console = console
|
||||
self.log = log
|
||||
self.lock = threading.Lock()
|
||||
self.queue = collections.deque()
|
||||
self.dropped = 0
|
||||
self.wake = threading.Event()
|
||||
threading.Thread(target=self._console_writer, daemon=True).start()
|
||||
|
||||
def write(self, text):
|
||||
for stream in self.streams:
|
||||
with self.lock:
|
||||
try:
|
||||
stream.write(text)
|
||||
self.log.write(text)
|
||||
except Exception:
|
||||
pass
|
||||
if len(self.queue) < self.CONSOLE_QUEUE_MAX:
|
||||
self.queue.append(text)
|
||||
else:
|
||||
self.dropped += 1
|
||||
self.wake.set()
|
||||
return len(text)
|
||||
|
||||
def flush(self):
|
||||
for stream in self.streams:
|
||||
with self.lock:
|
||||
try:
|
||||
stream.flush()
|
||||
self.log.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _console_writer(self):
|
||||
while True:
|
||||
self.wake.wait()
|
||||
with self.lock:
|
||||
text = "".join(self.queue)
|
||||
self.queue.clear()
|
||||
dropped, self.dropped = self.dropped, 0
|
||||
self.wake.clear()
|
||||
if dropped:
|
||||
text += (" (console: {} line(s) not shown while this window was paused - they "
|
||||
"are in the log file)\n".format(dropped))
|
||||
if self.console is None or not text:
|
||||
continue
|
||||
try:
|
||||
self.console.write(text)
|
||||
self.console.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def disable_quick_edit():
|
||||
"""Windows: a click into the console window must not pause the server (QuickEdit off).
|
||||
Text can still be selected through the window menu (Edit > Mark)."""
|
||||
if os.name != "nt":
|
||||
return
|
||||
try:
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
handle = kernel32.GetStdHandle(-10) # STD_INPUT_HANDLE
|
||||
mode = ctypes.c_uint32()
|
||||
if kernel32.GetConsoleMode(handle, ctypes.byref(mode)) and mode.value & 0x0040:
|
||||
# clear ENABLE_QUICK_EDIT_MODE (0x40), keep ENABLE_EXTENDED_FLAGS (0x80) so it sticks
|
||||
kernel32.SetConsoleMode(handle, (mode.value & ~0x0040) | 0x0080)
|
||||
print("console: click-to-select (QuickEdit) is off - a click in this window no longer "
|
||||
"pauses the server")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def open_log(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
@@ -865,62 +1039,320 @@ def open_log(log_dir):
|
||||
import socket
|
||||
import struct
|
||||
|
||||
RELAY_CLIENTS = {}
|
||||
RELAY_CLIENTS = {} # conn -> RelayClient
|
||||
RELAY_LOCK = threading.Lock()
|
||||
# Immutable snapshot rebuilt on connects/disconnects, not on every frame.
|
||||
# Keep connection keys, so the sender exclusion remains exactly "is not conn".
|
||||
RELAY_ROSTER = ()
|
||||
RELAY_QUEUE_LIMIT = 512 * 1024 # bytes waiting for one player before he is dropped as too slow
|
||||
# Stage 3A: coalesce ALREADY-QUEUED frames for each client into bounded TCP writes.
|
||||
# No queue wait is introduced. Wire bytes, per-peer FIFO order and recipient list do not change.
|
||||
RELAY_WRITE_BATCH_MAX_BYTES = 64 * 1024
|
||||
RELAY_WRITE_BATCH_MAX_FRAMES = 64
|
||||
|
||||
# Share a single immutable encoded frame across recipient queues. With
|
||||
# --relay-session-routing, high-rate session-state packets are sent only to the
|
||||
# host and guests that advertised the same session in the existing 0x40 envelope.
|
||||
# Discovery/follow packets and old or malformed frames retain global delivery.
|
||||
RELAY_STATS_LOCK = threading.Lock()
|
||||
RELAY_STATS = collections.Counter()
|
||||
# Index of currently recorded session hints, protected by RELAY_LOCK.
|
||||
RELAY_HINT_COUNTS = collections.Counter()
|
||||
RELAY_HINTED_CLIENTS = 0
|
||||
RELAY_SESSION_TYPES = frozenset((0x22, 0x23, 0x24, 0x25, 0x26, 0x27))
|
||||
RELAY_DISCOVERY_TYPES = frozenset((0x20, 0x30))
|
||||
|
||||
|
||||
def _relay_send(conn, payload):
|
||||
conn.sendall(struct.pack("<I", len(payload)) + payload)
|
||||
def _relay_stats_enabled():
|
||||
return bool(getattr(ARGS, "relay_tcp_stats_interval", 0) > 0)
|
||||
|
||||
|
||||
def _relay_parse_envelope(payload):
|
||||
"""Untrusted observational hint, NOT an authentication/permission check.
|
||||
|
||||
Client format: SHAD 0x40, sender uint32 LE, host-session uint32 LE,
|
||||
nested SHAD plus inner packet type. Older/malformed frames stay global.
|
||||
"""
|
||||
if (len(payload) < 18 or payload[:5] != b"SHAD\x40" or
|
||||
payload[13:17] != b"SHAD"):
|
||||
return None
|
||||
sender, session = struct.unpack_from("<II", payload, 5)
|
||||
if not sender:
|
||||
return None
|
||||
return sender, session, payload[17]
|
||||
|
||||
|
||||
def _relay_record(**values):
|
||||
with RELAY_STATS_LOCK:
|
||||
RELAY_STATS.update(values)
|
||||
|
||||
|
||||
def _relay_snapshot():
|
||||
with RELAY_STATS_LOCK:
|
||||
return RELAY_STATS.copy()
|
||||
|
||||
|
||||
def _relay_log_stats(interval):
|
||||
previous = _relay_snapshot()
|
||||
while True:
|
||||
time.sleep(interval)
|
||||
now = _relay_snapshot()
|
||||
delta = now.copy()
|
||||
delta.subtract(previous)
|
||||
previous = now
|
||||
with RELAY_LOCK:
|
||||
clients = len(RELAY_CLIENTS)
|
||||
print(("bbsc TCP relay stats {}s: clients={}; rx_frames={} / {} bytes; "
|
||||
"enqueued_frames={} / {} bytes; fanout={:.2f}; "
|
||||
"observed_session_frames={}; discovery_frames={}; opaque_frames={}; "
|
||||
"shadow_would_skip={} / {} bytes; slow_disconnects={}; send_errors={}").format(
|
||||
interval, clients, delta["rx_frames"], delta["rx_bytes"],
|
||||
delta["queued_frames"], delta["queued_bytes"],
|
||||
delta["queued_frames"] / max(delta["rx_frames"], 1),
|
||||
delta["session_frames"], delta["discovery_frames"],
|
||||
delta["opaque_frames"], delta["shadow_skip_frames"],
|
||||
delta["shadow_skip_bytes"], delta["slow_disconnects"],
|
||||
delta["send_errors"]), flush=True)
|
||||
|
||||
|
||||
|
||||
def _relay_register_client(conn, client):
|
||||
"""Register a socket, refreshing the snapshot and observational hints."""
|
||||
global RELAY_ROSTER, RELAY_HINTED_CLIENTS
|
||||
with RELAY_LOCK:
|
||||
prior = RELAY_CLIENTS.get(conn)
|
||||
if prior is not None and prior.session_hint is not None:
|
||||
RELAY_HINT_COUNTS[prior.session_hint] -= 1
|
||||
RELAY_HINTED_CLIENTS -= 1
|
||||
if not RELAY_HINT_COUNTS[prior.session_hint]:
|
||||
del RELAY_HINT_COUNTS[prior.session_hint]
|
||||
RELAY_CLIENTS[conn] = client
|
||||
if client.session_hint is not None:
|
||||
RELAY_HINT_COUNTS[client.session_hint] += 1
|
||||
RELAY_HINTED_CLIENTS += 1
|
||||
RELAY_ROSTER = tuple(RELAY_CLIENTS.items())
|
||||
return len(RELAY_CLIENTS)
|
||||
|
||||
|
||||
def _relay_unregister_client(conn):
|
||||
"""Drop a socket, including its monitoring-only session-hint entry."""
|
||||
global RELAY_ROSTER, RELAY_HINTED_CLIENTS
|
||||
with RELAY_LOCK:
|
||||
old_client = RELAY_CLIENTS.pop(conn, None)
|
||||
if old_client is not None and old_client.session_hint is not None:
|
||||
RELAY_HINT_COUNTS[old_client.session_hint] -= 1
|
||||
RELAY_HINTED_CLIENTS -= 1
|
||||
if RELAY_HINT_COUNTS[old_client.session_hint] <= 0:
|
||||
del RELAY_HINT_COUNTS[old_client.session_hint]
|
||||
RELAY_ROSTER = tuple(RELAY_CLIENTS.items())
|
||||
return len(RELAY_CLIENTS)
|
||||
|
||||
|
||||
def _relay_forward(conn, payload):
|
||||
"""Forward one unchanged frame globally or to its advertised session.
|
||||
|
||||
Session hints are routing hints, not authentication. They only narrow who
|
||||
receives ordinary state frames; discovery, follow and opaque traffic remain
|
||||
global for compatibility with joining and older clients.
|
||||
"""
|
||||
global RELAY_HINTED_CLIENTS
|
||||
observe = _relay_stats_enabled()
|
||||
skip_count = 0
|
||||
category = "opaque_frames"
|
||||
routing = bool(getattr(ARGS, "relay_session_routing", False))
|
||||
envelope = _relay_parse_envelope(payload)
|
||||
with RELAY_LOCK:
|
||||
# Atomic reference to the old-style membership snapshot, preserving
|
||||
# insertion order and socket identities without per-frame list copies.
|
||||
roster = RELAY_ROSTER
|
||||
sender_client = RELAY_CLIENTS.get(conn)
|
||||
if envelope is not None and sender_client is not None and (observe or routing):
|
||||
sender, session, kind = envelope
|
||||
sender_client.sender_hint = sender
|
||||
sender_client.seen_envelope = True
|
||||
old_hint = sender_client.session_hint
|
||||
new_hint = session or None
|
||||
if new_hint != old_hint:
|
||||
if old_hint is not None:
|
||||
RELAY_HINT_COUNTS[old_hint] -= 1
|
||||
RELAY_HINTED_CLIENTS -= 1
|
||||
if RELAY_HINT_COUNTS[old_hint] <= 0:
|
||||
del RELAY_HINT_COUNTS[old_hint]
|
||||
if new_hint is not None:
|
||||
RELAY_HINT_COUNTS[new_hint] += 1
|
||||
RELAY_HINTED_CLIENTS += 1
|
||||
sender_client.session_hint = new_hint
|
||||
if kind in RELAY_DISCOVERY_TYPES:
|
||||
category = "discovery_frames"
|
||||
elif session and kind in RELAY_SESSION_TYPES:
|
||||
category = "session_frames"
|
||||
# Sender has just been indexed under this session, so
|
||||
# counting all other known sessions excludes the sender.
|
||||
skip_count = RELAY_HINTED_CLIENTS - RELAY_HINT_COUNTS.get(session, 0)
|
||||
if routing:
|
||||
# The host identifies itself with sender == session; guests
|
||||
# advertise session == host. A modern client that has sent a
|
||||
# valid envelope but still reports session zero keeps receiving
|
||||
# early snapshots until it advertises a nonzero party. Completely
|
||||
# silent/legacy sockets are not flooded with every world's state.
|
||||
roster = tuple(
|
||||
(peer_conn, peer) for peer_conn, peer in roster
|
||||
if (peer_conn is conn or
|
||||
(peer.seen_envelope and peer.session_hint is None) or
|
||||
peer.session_hint == session or
|
||||
peer.sender_hint == session))
|
||||
# The encoded bytes and per-recipient FIFO ordering remain unchanged.
|
||||
frame = struct.pack("<I", len(payload)) + payload if roster else b""
|
||||
queued = 0
|
||||
for peer_conn, client in roster:
|
||||
if peer_conn is not conn:
|
||||
queued += client.send_framed(frame)
|
||||
if observe:
|
||||
_relay_record(rx_frames=1, rx_bytes=4 + len(payload),
|
||||
queued_frames=queued, queued_bytes=queued * (4 + len(payload)),
|
||||
shadow_skip_frames=skip_count,
|
||||
shadow_skip_bytes=skip_count * (4 + len(payload)),
|
||||
**{category: 1})
|
||||
return queued
|
||||
|
||||
|
||||
class RelayClient:
|
||||
"""One player's outgoing frames (2026-10-13). Each player has his own writer thread, so a
|
||||
player whose game stopped reading (crash, dead link) can no longer stall the reader of everyone
|
||||
else: before, the sender's thread blocked in sendall to him, stopped reading the sender, and
|
||||
the sender's game dropped the link ~10 s later - every 10 s while hosting."""
|
||||
|
||||
def __init__(self, conn, name):
|
||||
self.conn = conn
|
||||
self.name = name
|
||||
self.queue = collections.deque()
|
||||
self.queued = 0
|
||||
self.cond = threading.Condition()
|
||||
self.closed = False
|
||||
self.reason = ""
|
||||
self.sender_hint = None
|
||||
self.session_hint = None
|
||||
self.seen_envelope = False
|
||||
threading.Thread(target=self._writer, daemon=True).start()
|
||||
|
||||
def send(self, payload):
|
||||
# Retain original API for peer-count (0x7f) announcements.
|
||||
return self.send_framed(struct.pack("<I", len(payload)) + payload)
|
||||
|
||||
def send_framed(self, data):
|
||||
"""Queue already length-prefixed immutable bytes; return accepted/not accepted."""
|
||||
with self.cond:
|
||||
if self.closed:
|
||||
return False
|
||||
if self.queued + len(data) > RELAY_QUEUE_LIMIT:
|
||||
self._fail_locked("too slow ({} bytes waiting)".format(self.queued))
|
||||
return False
|
||||
was_empty = not self.queue
|
||||
self.queue.append(data)
|
||||
self.queued += len(data)
|
||||
# A sleeping writer can exist only while the queue is empty.
|
||||
if was_empty:
|
||||
self.cond.notify()
|
||||
return True
|
||||
|
||||
def _fail_locked(self, reason):
|
||||
if not self.reason:
|
||||
self.reason = reason
|
||||
if _relay_stats_enabled():
|
||||
if reason.startswith("too slow"):
|
||||
_relay_record(slow_disconnects=1)
|
||||
elif reason.startswith("send failed"):
|
||||
_relay_record(send_errors=1)
|
||||
self.closed = True
|
||||
self.queue.clear()
|
||||
self.queued = 0
|
||||
self.cond.notify()
|
||||
try:
|
||||
self.conn.shutdown(socket.SHUT_RDWR) # ends his reader too
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def fail(self, reason):
|
||||
with self.cond:
|
||||
self._fail_locked(reason)
|
||||
|
||||
def _writer(self):
|
||||
while True:
|
||||
with self.cond:
|
||||
while not self.queue and not self.closed:
|
||||
self.cond.wait()
|
||||
if self.closed:
|
||||
return
|
||||
# Drain only frames already waiting. This adds no artificial
|
||||
# latency and leaves the same 512-KiB enqueue/backpressure rule.
|
||||
data = self.queue.popleft()
|
||||
self.queued -= len(data)
|
||||
# The common case is a single frame with an otherwise empty
|
||||
# queue: avoid allocating a list or copying its bytes.
|
||||
if self.queue and len(data) < RELAY_WRITE_BATCH_MAX_BYTES:
|
||||
batch = [data]
|
||||
size = len(data)
|
||||
while (self.queue and len(batch) < RELAY_WRITE_BATCH_MAX_FRAMES and
|
||||
size + len(self.queue[0]) <= RELAY_WRITE_BATCH_MAX_BYTES):
|
||||
frame = self.queue.popleft()
|
||||
batch.append(frame)
|
||||
size += len(frame)
|
||||
self.queued -= len(frame)
|
||||
if len(batch) > 1:
|
||||
data = b"".join(batch)
|
||||
# Preserve the exact concatenated length-prefixed TCP stream.
|
||||
try:
|
||||
self.conn.sendall(data)
|
||||
except OSError as error:
|
||||
self.fail("send failed ({})".format(error))
|
||||
return
|
||||
|
||||
|
||||
def _relay_announce():
|
||||
with RELAY_LOCK:
|
||||
clients = list(RELAY_CLIENTS.items())
|
||||
for conn, _ in clients:
|
||||
try:
|
||||
_relay_send(conn, bytes([0x7F, min(len(clients) - 1, 255)]))
|
||||
except OSError:
|
||||
pass
|
||||
clients = list(RELAY_CLIENTS.values())
|
||||
for client in clients:
|
||||
client.send(bytes([0x7F, min(len(clients) - 1, 255)]))
|
||||
|
||||
|
||||
def _relay_client(conn, address):
|
||||
name = "{}:{}".format(*address)
|
||||
with RELAY_LOCK:
|
||||
RELAY_CLIENTS[conn] = name
|
||||
print(" relay: {} connected ({} player(s))".format(name, len(RELAY_CLIENTS)), flush=True)
|
||||
client = RelayClient(conn, name)
|
||||
count = _relay_register_client(conn, client)
|
||||
print(" relay: {} connected ({} player(s))".format(name, count), flush=True)
|
||||
_relay_announce()
|
||||
frames = 0
|
||||
reason = ""
|
||||
started = time.time()
|
||||
try:
|
||||
while True:
|
||||
header = _recv_exact(conn, 4)
|
||||
if header is None:
|
||||
reason = "closed by the player"
|
||||
break
|
||||
size = struct.unpack("<I", header)[0]
|
||||
if size > 64 * 1024:
|
||||
reason = "oversized frame ({} bytes)".format(size)
|
||||
break
|
||||
payload = _recv_exact(conn, size) if size else b""
|
||||
if payload is None:
|
||||
reason = "closed by the player"
|
||||
break
|
||||
frames += 1
|
||||
with RELAY_LOCK:
|
||||
others = [c for c in RELAY_CLIENTS if c is not conn]
|
||||
for other in others:
|
||||
try:
|
||||
_relay_send(other, payload)
|
||||
except OSError:
|
||||
pass
|
||||
except OSError:
|
||||
pass
|
||||
_relay_forward(conn, payload)
|
||||
except OSError as error:
|
||||
reason = "read failed ({})".format(error)
|
||||
finally:
|
||||
with RELAY_LOCK:
|
||||
RELAY_CLIENTS.pop(conn, None)
|
||||
left = len(RELAY_CLIENTS)
|
||||
left = _relay_unregister_client(conn)
|
||||
if client.reason:
|
||||
reason = client.reason # the writer gave up first
|
||||
client.fail(reason or "gone")
|
||||
try:
|
||||
conn.close()
|
||||
except OSError:
|
||||
pass
|
||||
print(" relay: {} left after {} frames ({} player(s))".format(name, frames, left),
|
||||
flush=True)
|
||||
print(" relay: {} left after {} frames, {:.0f} s ({}) ({} player(s))".format(
|
||||
name, frames, time.time() - started, reason, left), flush=True)
|
||||
_relay_announce()
|
||||
|
||||
|
||||
@@ -944,10 +1376,18 @@ def start_relay(host, port):
|
||||
while True:
|
||||
conn, address = listener.accept()
|
||||
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
conn.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
if hasattr(socket, "TCP_USER_TIMEOUT"): # Linux: unacked data for 20 s = dead link
|
||||
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 20000)
|
||||
threading.Thread(target=_relay_client, args=(conn, address), daemon=True).start()
|
||||
|
||||
threading.Thread(target=accept_loop, daemon=True).start()
|
||||
print("BBSC relay on {}:{} (for the BBSC mod)".format(host, port))
|
||||
print("BBSC relay on {}:{} (for the BBSC mod); immutable frame reuse enabled".format(
|
||||
host, port))
|
||||
interval = getattr(ARGS, "relay_tcp_stats_interval", 0)
|
||||
if interval > 0:
|
||||
threading.Thread(target=_relay_log_stats, args=(interval,), daemon=True,
|
||||
name="bbsc-tcp-stats").start()
|
||||
|
||||
|
||||
class QuietHTTPServer(ThreadingHTTPServer):
|
||||
@@ -985,7 +1425,28 @@ def start_bbhost():
|
||||
return
|
||||
if not public:
|
||||
print("bbhost: no public address (set --advertise or --bbhost-public-ip): relay off")
|
||||
bbhost_service.RELAY.start(ARGS.bbhost_stun_bind or ARGS.host, ARGS.bbhost_stun_port)
|
||||
bbhost_service.RELAY.start(ARGS.bbhost_stun_bind or ARGS.host, ARGS.bbhost_stun_port,
|
||||
rcvbuf_bytes=ARGS.bbhost_udp_rcvbuf)
|
||||
if ARGS.bbhost_udp_stats_interval:
|
||||
interval = ARGS.bbhost_udp_stats_interval
|
||||
if not 5 <= interval <= 3600:
|
||||
raise ValueError("UDP stats interval must be 5..3600 seconds")
|
||||
|
||||
def log_udp_stats():
|
||||
before = bbhost_service.RELAY.counters()
|
||||
while True:
|
||||
time.sleep(interval)
|
||||
after = bbhost_service.RELAY.counters()
|
||||
delta = {key: after[key] - before[key] for key in after}
|
||||
before = after
|
||||
print("bbhost UDP stats {}s: rx={} datagrams / {} bytes; tx={} datagrams / {} bytes; "
|
||||
"shad forwarded={}; bbhost forwarded={}; send_errors={}; unrecognized={}".format(
|
||||
interval, delta["datagrams_in"], delta["bytes_in"],
|
||||
delta["datagrams_out"], delta["bytes_out"],
|
||||
delta["shad_frames"], delta["frames"],
|
||||
delta["send_errors"], delta["unrecognized_in"]), flush=True)
|
||||
|
||||
threading.Thread(target=log_udp_stats, daemon=True, name="bbhost-udp-stats").start()
|
||||
|
||||
def follow_address():
|
||||
# a DuckDNS name can change address; the relay addresses must follow it
|
||||
@@ -1001,6 +1462,7 @@ def start_bbhost():
|
||||
|
||||
def main():
|
||||
global ARGS, SS_INFO_B64, SS_INFO_LOCAL_B64, SS_INFO_BBHOST_B64, SS_INFO_BBHOST_LOCAL_B64
|
||||
global AREA_REPLY_BUDGET
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--shadnet", required=True,
|
||||
@@ -1009,6 +1471,12 @@ def main():
|
||||
parser.add_argument("--port", type=int, default=20443)
|
||||
parser.add_argument("--relay-port", type=int, default=20444,
|
||||
help="BBSC mod relay (TCP); 0 turns it off")
|
||||
parser.add_argument("--relay-tcp-stats-interval", type=int, default=0,
|
||||
help="TCP fan-out and untrusted-session SHADOW telemetry seconds; "
|
||||
"0 disables statistics. Routing stays global.")
|
||||
parser.add_argument("--relay-session-routing", action="store_true",
|
||||
help="route high-rate BBSC state frames only within their advertised "
|
||||
"host session; discovery/follow/legacy frames remain global")
|
||||
parser.add_argument("--advertise",
|
||||
help="address to put in ss.info if it differs from --host")
|
||||
parser.add_argument("--summon-interval", type=int, default=0,
|
||||
@@ -1017,6 +1485,9 @@ def main():
|
||||
help="co-op level window (vanilla 20; try 999 to match at any level)")
|
||||
parser.add_argument("--no-playlog", action="store_true",
|
||||
help="turn off the play-log uploads to Amazon S3 (recommended)")
|
||||
parser.add_argument("--tomb-max-response-bytes", type=int, default=None,
|
||||
help="message / bloodstain reply budget in bytes (default 65536 - the "
|
||||
"Dantelion2 Panic 265 fix); 0 = no cap")
|
||||
parser.add_argument("--verbose", action="store_true", help="log headers and request bodies")
|
||||
parser.add_argument("--no-bbhost", dest="bbhost", action="store_false",
|
||||
help="turn off the bbhost players' services (bbhost_service.py)")
|
||||
@@ -1024,6 +1495,10 @@ def main():
|
||||
help="bbhost's session/account port (bbhost's default 18671; 0 = only --port)")
|
||||
parser.add_argument("--bbhost-stun-port", type=int, default=3478,
|
||||
help="bbhost STUN + relay, UDP (0 = off)")
|
||||
parser.add_argument("--bbhost-udp-rcvbuf", type=int, default=0,
|
||||
help="requested UDP SO_RCVBUF bytes; 0 leaves OS default (safe default)")
|
||||
parser.add_argument("--bbhost-udp-stats-interval", type=int, default=0,
|
||||
help="periodic UDP traffic counters, seconds; 0 disables logging")
|
||||
parser.add_argument("--bbhost-stun-bind", default="",
|
||||
help="address the STUN socket binds to (default: --host)")
|
||||
parser.add_argument("--bbhost-public-ip", default="",
|
||||
@@ -1040,10 +1515,19 @@ def main():
|
||||
parser.add_argument("--data", default=os.path.join(data_home, "online-data.json"),
|
||||
help="where shared messages and bloodstains are kept between runs")
|
||||
ARGS = parser.parse_args()
|
||||
if ARGS.tomb_max_response_bytes and not 4096 <= ARGS.tomb_max_response_bytes <= 2*1024*1024:
|
||||
parser.error("--tomb-max-response-bytes must be 0 or 4096..2097152")
|
||||
if ARGS.bbhost_udp_stats_interval and not 5 <= ARGS.bbhost_udp_stats_interval <= 3600:
|
||||
parser.error("--bbhost-udp-stats-interval must be 0 or 5..3600")
|
||||
if ARGS.bbhost_udp_rcvbuf and not 8192 <= ARGS.bbhost_udp_rcvbuf <= 64*1024*1024:
|
||||
parser.error("--bbhost-udp-rcvbuf must be 0 or 8192..67108864")
|
||||
if ARGS.tomb_max_response_bytes is not None:
|
||||
AREA_REPLY_BUDGET = ARGS.tomb_max_response_bytes
|
||||
|
||||
log_path = open_log(ARGS.log_dir)
|
||||
print("BBSC game server - {} - log: {}".format(
|
||||
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), log_path))
|
||||
disable_quick_edit()
|
||||
|
||||
global STORE
|
||||
STORE = OnlineStore(ARGS.data)
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# Shared by START-HOST and GENERATE-CARD: which networks friends can reach this PC on, and the one
|
||||
# address the host uses (server\host-address.txt), so the servers, the host's own game and the
|
||||
# card all point at the same place.
|
||||
|
||||
# The firewall rules START-HOST makes let in: Radmin VPN, Hamachi, Tailscale and the local
|
||||
# networks of this PC (home Wi-Fi, ZeroTier). Nothing from the internet. playit.gg comes in
|
||||
# through its own agent on this PC, so it needs no rule.
|
||||
$BbscFirewallRanges = @("26.0.0.0/8", "25.0.0.0/8", "100.64.0.0/10", "LocalSubnet")
|
||||
|
||||
# Privacy VPNs (ProtonVPN, NordVPN...) hide this PC; friends cannot connect through them.
|
||||
$BbscPrivacyVpn = 'Proton|Nord|Mullvad|Surfshark|Express|Windscribe|Private Internet|PIA|CyberGhost|IPVanish|hide\.me|TunnelBear|Cloudflare|WARP'
|
||||
|
||||
function Get-BbscKind($ip, $alias, $desc) {
|
||||
$o = $ip.Split('.') | ForEach-Object { [int]$_ }
|
||||
$both = "$alias $desc"
|
||||
if ($both -match 'Radmin' -or $o[0] -eq 26) { return "Radmin VPN" }
|
||||
if ($both -match 'Hamachi' -or $o[0] -eq 25) { return "Hamachi" }
|
||||
if ($both -match 'Tailscale' -or ($o[0] -eq 100 -and $o[1] -ge 64 -and $o[1] -le 127)) { return "Tailscale" }
|
||||
if ($both -match 'ZeroTier') { return "ZeroTier" }
|
||||
if ($both -match $BbscPrivacyVpn) { return $null }
|
||||
if ($o[0] -eq 10 -or ($o[0] -eq 192 -and $o[1] -eq 168) -or ($o[0] -eq 172 -and $o[1] -ge 16 -and $o[1] -le 31)) {
|
||||
if ($desc -match 'WireGuard|TAP-Windows|OpenVPN|Wintun|VPN') { return $null } # some other VPN tunnel
|
||||
return "Home network, same house only ($alias)"
|
||||
}
|
||||
return "Other ($alias)"
|
||||
}
|
||||
|
||||
# Every address on this PC friends could use; VPNs first. Hidden privacy VPNs go to $script:BbscSkipped.
|
||||
function Get-BbscNetworks {
|
||||
$script:BbscSkipped = @()
|
||||
@(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.IPAddress -notlike "127.*" -and $_.IPAddress -notlike "169.254.*" } |
|
||||
ForEach-Object {
|
||||
$desc = (Get-NetAdapter -InterfaceIndex $_.InterfaceIndex -ErrorAction SilentlyContinue).InterfaceDescription
|
||||
$kind = Get-BbscKind $_.IPAddress $_.InterfaceAlias $desc
|
||||
if ($kind) {
|
||||
[pscustomobject]@{ Ip = $_.IPAddress; Kind = $kind }
|
||||
} else {
|
||||
$script:BbscSkipped += $_.InterfaceAlias
|
||||
}
|
||||
} | Sort-Object { if ($_.Kind -like "Home*" -or $_.Kind -like "Other*") { 1 } else { 0 } })
|
||||
}
|
||||
|
||||
function Get-BbscSavedAddress([string]$server) {
|
||||
$file = Join-Path $server "host-address.txt"
|
||||
if (Test-Path $file) { return ([IO.File]::ReadAllText($file)).Trim() }
|
||||
return ""
|
||||
}
|
||||
|
||||
function Save-BbscAddress([string]$server, [string]$address) {
|
||||
[IO.File]::WriteAllText((Join-Path $server "host-address.txt"), $address)
|
||||
}
|
||||
|
||||
# Lists the networks and asks. Returns the address friends use (no port), or "" if cancelled.
|
||||
function Select-BbscAddress([string]$current) {
|
||||
$found = Get-BbscNetworks
|
||||
Write-Host ""
|
||||
Write-Host " How do your friends reach this PC?" -ForegroundColor Cyan
|
||||
for ($i = 0; $i -lt $found.Count; $i++) {
|
||||
$mark = if ($found[$i].Ip -eq $current) { " (in use now)" } else { "" }
|
||||
Write-Host (" {0}. {1,-44} {2}{3}" -f ($i + 1), $found[$i].Kind, $found[$i].Ip, $mark)
|
||||
}
|
||||
if ($script:BbscSkipped.Count -gt 0) {
|
||||
Write-Host (" (not listed: {0} - a privacy VPN, friends cannot reach you through it)" -f ($script:BbscSkipped -join ", ")) -ForegroundColor DarkGray
|
||||
}
|
||||
$typeIndex = $found.Count + 1
|
||||
Write-Host (" {0}. Type an address (a domain, a public IP with ports forwarded...)" -f $typeIndex)
|
||||
$pick = 0
|
||||
while ($pick -lt 1 -or $pick -gt $typeIndex) {
|
||||
$answer = (Read-Host " Number").Trim()
|
||||
[int]::TryParse($answer, [ref]$pick) | Out-Null
|
||||
}
|
||||
if ($pick -lt $typeIndex) { return $found[$pick - 1].Ip }
|
||||
Write-Host ""
|
||||
Write-Host " The address only, no port. Friends need ports 31313-31315, 20443 and 20444 on it" -ForegroundColor Gray
|
||||
Write-Host " with the SAME numbers (a playit.gg tunnel with other port numbers will not work" -ForegroundColor Gray
|
||||
Write-Host " for co-op - use Radmin VPN, Hamachi or Tailscale instead)." -ForegroundColor Gray
|
||||
$address = ""
|
||||
while (-not $address) { $address = ((Read-Host " Address").Trim() -replace ':\d+$', '') }
|
||||
return $address
|
||||
}
|
||||
|
||||
# The address START-HOST and GENERATE-CARD use: the saved one if this PC still has it (or it is a
|
||||
# typed name), else the only VPN address, else ask.
|
||||
function Resolve-BbscAddress([string]$server, [switch]$Ask) {
|
||||
$saved = Get-BbscSavedAddress $server
|
||||
$found = Get-BbscNetworks
|
||||
$valid = $saved -and (($saved -notmatch '^\d{1,3}(\.\d{1,3}){3}$') -or ($found.Ip -contains $saved))
|
||||
if ($valid -and -not $Ask) { return $saved }
|
||||
if (-not $Ask) {
|
||||
$vpn = @($found | Where-Object { $_.Kind -notlike "Home*" -and $_.Kind -notlike "Other*" })
|
||||
if ($vpn.Count -eq 1) { Save-BbscAddress $server $vpn[0].Ip; return $vpn[0].Ip }
|
||||
if ($saved -and -not $valid) {
|
||||
Write-Host " [!] This PC no longer has $saved (VPN off?)." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
$address = Select-BbscAddress $saved
|
||||
if ($address) { Save-BbscAddress $server $address }
|
||||
return $address
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
# BBSC host: makes a server card - one small file friends load in the BBSC launcher
|
||||
# (Settings > Accounts > Import Card / Open Cards Folder) to get this server's address and
|
||||
# registration key without typing them.
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$server = Join-Path $root "server"
|
||||
$cards = Join-Path $root "cards"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " BBSC - SERVER CARD" -ForegroundColor Red
|
||||
Write-Host " ------------------" -ForegroundColor DarkRed
|
||||
Write-Host ""
|
||||
|
||||
# 1. Name
|
||||
$default = "$env:USERNAME's BBSC server"
|
||||
$name = (Read-Host " Server name (Enter = $default)").Trim()
|
||||
if (-not $name) { $name = $default }
|
||||
|
||||
# 2. Address: the one START-HOST uses (server\host-address.txt); Enter keeps it
|
||||
. (Join-Path $PSScriptRoot "bbsc-network.ps1")
|
||||
$current = Get-BbscSavedAddress $server
|
||||
$address = ""
|
||||
if ($current) {
|
||||
$answer = (Read-Host " Address friends use: $current (Enter = keep, C = change)").Trim()
|
||||
if ($answer -notmatch '^[cC]') { $address = $current }
|
||||
}
|
||||
if (-not $address) {
|
||||
$address = Select-BbscAddress $current
|
||||
if (-not $address) { exit 1 }
|
||||
if ($address -ne $current) {
|
||||
Save-BbscAddress $server $address
|
||||
if (Get-Process shadnet, BBSC-GameServer -ErrorAction SilentlyContinue) {
|
||||
Write-Host " [!] The servers are running on $current - close them and double-click START-HOST" -ForegroundColor Yellow
|
||||
Write-Host " again so they use $address too." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
$kind = (Get-BbscNetworks | Where-Object { $_.Ip -eq $address } | Select-Object -First 1).Kind
|
||||
if (-not $kind) { $kind = "Typed" }
|
||||
$address = "${address}:31313"
|
||||
|
||||
# 3. Registration key (START-HOST makes it the first time)
|
||||
$cfg = Join-Path $server "shadnet.cfg"
|
||||
$key = ""
|
||||
if (Test-Path $cfg) {
|
||||
$line = Get-Content $cfg | Where-Object { $_ -like "RegistrationSecretKey=*" } | Select-Object -First 1
|
||||
if ($line) { $key = ($line -replace '^RegistrationSecretKey=', '').Trim() }
|
||||
}
|
||||
if (-not $key) {
|
||||
Write-Host ""
|
||||
Write-Host " [!] This server has no registration key yet. Run START-HOST once, then make the card" -ForegroundColor Yellow
|
||||
Write-Host " again - without a key anyone who reaches the server can make accounts." -ForegroundColor Yellow
|
||||
if ((Read-Host " Make a card without a key anyway? (y/N)").Trim() -notmatch '^[yY]') { exit 1 }
|
||||
}
|
||||
|
||||
# 4. The card
|
||||
New-Item -ItemType Directory -Force $cards | Out-Null
|
||||
$safe = ($name -replace '[^A-Za-z0-9 _-]', '').Trim() -replace '\s+', '-'
|
||||
if (-not $safe) { $safe = "server" }
|
||||
$file = Join-Path $cards "BBSC-card-$safe.json"
|
||||
$card = [ordered]@{
|
||||
name = $name
|
||||
shadnet = $address
|
||||
registration_key = $key
|
||||
network = $kind
|
||||
created = (Get-Date -Format "yyyy-MM-dd")
|
||||
}
|
||||
[IO.File]::WriteAllText($file, ($card | ConvertTo-Json), (New-Object Text.UTF8Encoding($false)))
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " [OK] Card made: $file" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " ============================================" -ForegroundColor Cyan
|
||||
Write-Host " Send the card file to your friends (Discord DM)." -ForegroundColor White
|
||||
Write-Host " PRIVATE: it holds your registration key - anyone with it" -ForegroundColor White
|
||||
Write-Host " can make accounts on your server. Never post it publicly." -ForegroundColor White
|
||||
Write-Host " Friends: drag it onto JOIN-HOST.cmd (connects the game and adds" -ForegroundColor Gray
|
||||
Write-Host " the card), then launcher > Settings > Accounts > Create Account." -ForegroundColor Gray
|
||||
Write-Host " ============================================" -ForegroundColor Cyan
|
||||
Start-Process explorer.exe "/select,`"$file`""
|
||||
Read-Host "`n Press Enter to close"
|
||||
@@ -1,151 +0,0 @@
|
||||
# Points this PC's Bloodborne (shadPS4) at a BBSC host's servers. Takes the host's address or the
|
||||
# host's server card (GENERATE-CARD's BBSC-card-*.json - drag it onto JOIN-HOST.cmd). Works on
|
||||
# every network the host package supports: Radmin VPN (26.x), Hamachi (25.x), Tailscale
|
||||
# (100.64-127.x), ZeroTier / same house (192.168.x, 10.x, 172.16-31.x) and named addresses.
|
||||
# Old settings are backed up next to the originals. A card is also copied into the settings'
|
||||
# servers\ folder, so the BBSC launcher's Settings > Accounts lists it for Create Account.
|
||||
#
|
||||
# shadPS4 keeps its settings in a "user" folder in the folder it starts from when there is one
|
||||
# (portable), otherwise in %APPDATA%\shadPS4. The redirect has to be in the one the game uses, or
|
||||
# the game asks Sony's old server for its server list and says "Failed to acquire Bloodborne
|
||||
# server information". So this writes it into every settings folder it can find, and asks for
|
||||
# the shadPS4.exe of a portable install it cannot find by itself.
|
||||
param([string]$Address = "", [string]$Card = "", [switch]$NoPause)
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Done([int]$code) { if (-not $NoPause) { Read-Host "`n Press Enter to close" }; exit $code }
|
||||
|
||||
function Get-NetworkKind([string]$a) {
|
||||
if ($a -notmatch '^\d{1,3}(\.\d{1,3}){3}$') {
|
||||
if ($a -match '^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$' -and $a -match '[A-Za-z]') { return "a named address" }
|
||||
return $null
|
||||
}
|
||||
if ($a -match '^26\.') { return "Radmin VPN" }
|
||||
if ($a -match '^25\.') { return "Hamachi" }
|
||||
if ($a -match '^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.') { return "Tailscale" }
|
||||
if ($a -match '^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)') { return "ZeroTier or the same router / Wi-Fi" }
|
||||
if ($a -match '^(0|127)\.') { return $null }
|
||||
return "the internet (ports forwarded)"
|
||||
}
|
||||
|
||||
if (-not $Address -and -not $Card) {
|
||||
Write-Host ""
|
||||
Write-Host " BBSC - CONNECT TO A HOST" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host " Paste the host's address, or drag their card file (BBSC-card-....json) in here." -ForegroundColor Gray
|
||||
$Address = (Read-Host " Address or card").Trim().Trim('"')
|
||||
}
|
||||
if (-not $Card -and $Address -match '\.json$') { $Card = $Address; $Address = "" }
|
||||
$cardData = $null
|
||||
if ($Card) {
|
||||
try { $cardData = Get-Content -LiteralPath $Card -Raw | ConvertFrom-Json } catch { }
|
||||
if (-not $cardData -or -not $cardData.shadnet) {
|
||||
Write-Host " [X] '$Card' is not a BBSC server card." -ForegroundColor Red
|
||||
Done 1
|
||||
}
|
||||
$Address = [string]$cardData.shadnet
|
||||
Write-Host " Card: $($cardData.name) ($($cardData.network))" -ForegroundColor Gray
|
||||
}
|
||||
$port = 31313
|
||||
if ($Address -match '^(.+):(\d+)$') { $Address = $Matches[1]; $port = [int]$Matches[2] }
|
||||
$kind = Get-NetworkKind $Address
|
||||
if (-not $kind) {
|
||||
Write-Host " [X] '$Address' is not a host address. It looks like 26.x.x.x (Radmin), 25.x.x.x" -ForegroundColor Red
|
||||
Write-Host " (Hamachi), 100.x.x.x (Tailscale), 192.168.x.x (same house) or a name." -ForegroundColor Red
|
||||
Done 1
|
||||
}
|
||||
if ($port -ne 31313) {
|
||||
Write-Host " [!] The host's shadNet port is $port, not 31313 (a playit.gg tunnel?). Accounts work," -ForegroundColor Yellow
|
||||
Write-Host " but co-op also needs 31315, 20443 and 20444 on the same address - ask the host" -ForegroundColor Yellow
|
||||
Write-Host " to use Radmin VPN, Hamachi or Tailscale if joining fails." -ForegroundColor Yellow
|
||||
}
|
||||
# Every shadPS4 settings folder we can find.
|
||||
$folders = New-Object System.Collections.Generic.List[string]
|
||||
function Add-Folder([string]$path) {
|
||||
if ($path -and (Test-Path $path) -and -not $folders.Contains((Resolve-Path $path).Path)) {
|
||||
$folders.Add((Resolve-Path $path).Path)
|
||||
}
|
||||
}
|
||||
Add-Folder (Join-Path $env:APPDATA "shadPS4")
|
||||
Add-Folder (Join-Path (Split-Path $PSScriptRoot -Parent) "user") # the BBSC game package
|
||||
Get-Process shadPS4 -ErrorAction SilentlyContinue | ForEach-Object { # a running shadPS4
|
||||
if ($_.Path) { Add-Folder (Join-Path (Split-Path $_.Path -Parent) "user") }
|
||||
}
|
||||
$exe = ""
|
||||
if (-not $NoPause) {
|
||||
Write-Host ""
|
||||
Write-Host " Portable shadPS4 (its own 'user' folder next to shadPS4.exe)? Drag that shadPS4.exe" -ForegroundColor Gray
|
||||
$exe = (Read-Host " into this window and press Enter - or just press Enter").Trim().Trim('"')
|
||||
}
|
||||
if ($exe) {
|
||||
$exeDir = if (Test-Path $exe -PathType Container) { $exe } else { Split-Path $exe -Parent }
|
||||
$user = Join-Path $exeDir "user"
|
||||
if (-not (Test-Path $user)) { New-Item -ItemType Directory -Force $user | Out-Null }
|
||||
Add-Folder $user
|
||||
}
|
||||
if ($folders.Count -eq 0) {
|
||||
Write-Host " [X] No shadPS4 settings found. Start the game once, then run this again." -ForegroundColor Red
|
||||
Done 1
|
||||
}
|
||||
$stamp = Get-Date -Format yyyyMMdd-HHmmss
|
||||
|
||||
foreach ($dir in $folders) {
|
||||
Write-Host ""
|
||||
Write-Host " Settings in $dir" -ForegroundColor Cyan
|
||||
|
||||
# 1. host_overrides.json: the game's server list and summon service go to the host.
|
||||
$overrides = Join-Path $dir "host_overrides.json"
|
||||
if (Test-Path $overrides) { Copy-Item $overrides "$overrides.bak-$stamp" }
|
||||
$json = "{`r`n `"https://ss4.scej-network.jp:20443`": `"http://${Address}:20443`",`r`n `"http://thehuntersdream.com:18671/summon_messenger`": `"http://${Address}:31315`"`r`n}`r`n"
|
||||
[IO.File]::WriteAllText($overrides, $json)
|
||||
Write-Host " [OK] server list -> $Address ($kind)" -ForegroundColor Green
|
||||
|
||||
# 2. Bloodborne's own shadNet server (per-game settings, CUSA03173).
|
||||
$game = Join-Path $dir "custom_configs\CUSA03173.json"
|
||||
$set = $false
|
||||
if (Test-Path $game) {
|
||||
$text = [IO.File]::ReadAllText($game)
|
||||
$new = $text -replace '"shadnet_server"\s*:\s*"[^"]*"', "`"shadnet_server`": `"${Address}:${port}`""
|
||||
$new = $new -replace '"shadnet_webapi_server"\s*:\s*"[^"]*"', "`"shadnet_webapi_server`": `"http://${Address}:31315`""
|
||||
if ($new -match [regex]::Escape("${Address}:${port}")) {
|
||||
if ($new -ne $text) { Copy-Item $game "$game.bak-$stamp"; [IO.File]::WriteAllText($game, $new) }
|
||||
Write-Host " [OK] Bloodborne's shadNet server -> ${Address}:${port}" -ForegroundColor Green
|
||||
$set = $true
|
||||
}
|
||||
}
|
||||
# 3. The card, for the launcher's Settings > Accounts (Create Account on Server).
|
||||
if ($Card) {
|
||||
$servers = Join-Path $dir "servers"
|
||||
New-Item -ItemType Directory -Force $servers | Out-Null
|
||||
Copy-Item -LiteralPath $Card (Join-Path $servers (Split-Path $Card -Leaf)) -Force
|
||||
Write-Host " [OK] card added - launcher: Settings > Accounts > pick it > Create Account" -ForegroundColor Green
|
||||
}
|
||||
if (-not $set) {
|
||||
Write-Host " [!] Check by hand: launcher -> Bloodborne settings -> shadNet server ${Address}:${port}," -ForegroundColor Yellow
|
||||
Write-Host " WebAPI server http://${Address}:31315" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# 4. Can we reach the host's two servers?
|
||||
Write-Host ""
|
||||
$ok = $true
|
||||
try {
|
||||
$status = Invoke-RestMethod "http://${Address}:31315/status" -TimeoutSec 6
|
||||
if ($status.ok) { Write-Host " [OK] shadNet server answers" -ForegroundColor Green }
|
||||
} catch {
|
||||
$ok = $false
|
||||
Write-Host " [X] shadNet server (${Address}:31315) does not answer." -ForegroundColor Red
|
||||
}
|
||||
try {
|
||||
$info = Invoke-WebRequest "http://${Address}:20443/bb-eu/ss.info" -TimeoutSec 6 -UseBasicParsing
|
||||
if ($info.StatusCode -eq 200) { Write-Host " [OK] game server answers (server list)" -ForegroundColor Green }
|
||||
} catch {
|
||||
$ok = $false
|
||||
Write-Host " [X] game server (${Address}:20443) does not answer." -ForegroundColor Red
|
||||
}
|
||||
if ($ok) {
|
||||
Write-Host " Ready - restart the game if it is open." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Is the host's START-HOST running, and are you both connected ($kind)?" -ForegroundColor Yellow
|
||||
}
|
||||
Done 0
|
||||
@@ -1,19 +0,0 @@
|
||||
# Creates a BBSC account on THIS PC's shadnet server (start the servers first).
|
||||
$ErrorActionPreference = "Stop"
|
||||
$server = Join-Path (Split-Path $PSScriptRoot -Parent) "server"
|
||||
$key = (Get-Content (Join-Path $server "shadnet.cfg") | Where-Object { $_ -like "RegistrationSecretKey=*" }) -replace "^RegistrationSecretKey=", ""
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " BBSC - NEW ACCOUNT on this server" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
$name = (Read-Host " Username (letters, numbers, - or _)").Trim()
|
||||
$secure = Read-Host " Password" -AsSecureString
|
||||
$ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
||||
try { $pass = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) }
|
||||
$mail = (Read-Host " Email (any address, it is not checked)").Trim()
|
||||
|
||||
& (Join-Path $server "shadnet-sample.exe") 127.0.0.1 31313 register $name $pass $mail $key
|
||||
$pass = $null
|
||||
Write-Host ""
|
||||
Write-Host " If it says the account was created: in shadPS4 set shadNet ID = $name and that password." -ForegroundColor Cyan
|
||||
Read-Host "`n Press Enter to close"
|
||||
@@ -1,118 +0,0 @@
|
||||
# BBSC host: one double-click. First time: installs the C++ runtime and opens the firewall (one
|
||||
# admin prompt). Every time: starts both servers and points this PC's game at them.
|
||||
# Works on Radmin VPN, Hamachi, Tailscale, ZeroTier or a home network (tools\bbsc-network.ps1);
|
||||
# -ChangeNetwork asks again which one friends use.
|
||||
param([switch]$Setup, [switch]$ChangeNetwork)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
$server = Join-Path $root "server"
|
||||
. (Join-Path $PSScriptRoot "bbsc-network.ps1")
|
||||
$rules = @(@("BBSC shadnet 31313", "TCP", 31313), @("BBSC matching 31314", "UDP", 31314),
|
||||
@("BBSC webapi 31315", "TCP", 31315), @("BBSC game server 20443", "TCP", 20443),
|
||||
@("BBSC relay 20444", "TCP", 20444), @("BBSC bbhost sessions 18671", "TCP", 18671),
|
||||
@("BBSC bbhost STUN 3478", "UDP", 3478))
|
||||
|
||||
function Test-VcRuntime {
|
||||
$key = "HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64"
|
||||
return (Test-Path $key) -and ((Get-ItemProperty $key -ErrorAction SilentlyContinue).Installed -eq 1)
|
||||
}
|
||||
# Every rule exists and lets in every VPN range (older packages made Radmin-only rules).
|
||||
function Test-Firewall {
|
||||
foreach ($r in $rules) {
|
||||
$rule = Get-NetFirewallRule -DisplayName $r[0] -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $rule) { return $false }
|
||||
$have = @(($rule | Get-NetFirewallAddressFilter).RemoteAddress)
|
||||
foreach ($range in $BbscFirewallRanges) {
|
||||
$short = $range -replace '^(\d+)\.0\.0\.0/8$', '$1.0.0.0/255.0.0.0' -replace '^100\.64\.0\.0/10$', '100.64.0.0/255.192.0.0'
|
||||
if (-not ($have -contains $range -or $have -contains $short)) { return $false }
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
# Elevated half: only runs once, when something is missing.
|
||||
if ($Setup) {
|
||||
if (-not (Test-VcRuntime)) {
|
||||
Start-Process (Join-Path $server "vc_redist.x64.exe") -ArgumentList "/install /quiet /norestart" -Wait
|
||||
}
|
||||
foreach ($r in $rules) {
|
||||
if (Get-NetFirewallRule -DisplayName $r[0] -ErrorAction SilentlyContinue) {
|
||||
Set-NetFirewallRule -DisplayName $r[0] -RemoteAddress $BbscFirewallRanges
|
||||
} else {
|
||||
# VPN ranges and this PC's own networks only, so nothing is opened to the internet.
|
||||
New-NetFirewallRule -DisplayName $r[0] -Direction Inbound -Protocol $r[1] -LocalPort $r[2] `
|
||||
-RemoteAddress $BbscFirewallRanges -Action Allow | Out-Null
|
||||
}
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " BBSC HOST" -ForegroundColor Red
|
||||
Write-Host " ---------" -ForegroundColor DarkRed
|
||||
|
||||
$ip = Resolve-BbscAddress $server -Ask:$ChangeNetwork
|
||||
if (-not $ip) {
|
||||
Write-Host " [X] No network picked. Turn on Radmin VPN / Hamachi / Tailscale, then START-HOST again." -ForegroundColor Red
|
||||
Read-Host "`n Press Enter to close"; exit 1
|
||||
}
|
||||
$kind = (Get-BbscNetworks | Where-Object { $_.Ip -eq $ip } | Select-Object -First 1).Kind
|
||||
if (-not $kind) { $kind = "typed address" }
|
||||
Write-Host " Network: $kind ($ip) - to change it: START-HOST-OTHER-NETWORK.cmd" -ForegroundColor Gray
|
||||
|
||||
if (-not (Test-VcRuntime) -or -not (Test-Firewall)) {
|
||||
Write-Host " First time: setting up (say Yes to the Windows prompt)..." -ForegroundColor Gray
|
||||
try {
|
||||
Start-Process powershell.exe -Verb RunAs -Wait -WindowStyle Hidden -ArgumentList `
|
||||
"-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -Setup"
|
||||
} catch {
|
||||
Write-Host " [X] Setup needs the Yes on the Windows prompt. Double-click START-HOST again." -ForegroundColor Red
|
||||
Read-Host "`n Press Enter to close"; exit 1
|
||||
}
|
||||
if (Test-Firewall) {
|
||||
Write-Host " [OK] Setup done" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [!] Firewall rules not complete - friends may not get in. Run START-HOST again." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# First start of a fresh package: this host's own account registration key (NEW-ACCOUNT reads it).
|
||||
$cfg = Join-Path $server "shadnet.cfg"
|
||||
$cfgText = [IO.File]::ReadAllText($cfg)
|
||||
if ($cfgText -match '(?m)^RegistrationSecretKey=\s*$') {
|
||||
$bytes = New-Object byte[] 20
|
||||
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
|
||||
$newKey = -join ($bytes | ForEach-Object { $_.ToString("x2") })
|
||||
$cfgText = $cfgText -replace '(?m)^RegistrationSecretKey=\s*$', "RegistrationSecretKey=$newKey"
|
||||
[IO.File]::WriteAllText($cfg, $cfgText)
|
||||
Write-Host " [OK] New server: own registration key created" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Get-Process shadnet, BBSC-GameServer -ErrorAction SilentlyContinue | Stop-Process -Force
|
||||
$stamp = Get-Date -Format yyyyMMdd-HHmmss
|
||||
New-Item -ItemType Directory -Force (Join-Path $server "logs") | Out-Null
|
||||
Start-Process cmd.exe -WorkingDirectory $server -WindowStyle Minimized -ArgumentList `
|
||||
"/c title BBSC shadnet server & set SHADNET_BLOODBORNE_SEAMLESS_COOP=1 & set SHADNET_BLOODBORNE_RE_TRACE=1 & shadnet.exe > `"logs\shadnet-$stamp.log`" 2>&1"
|
||||
Start-Sleep -Seconds 2
|
||||
Start-Process (Join-Path $server "BBSC-GameServer.exe") -WorkingDirectory $server -WindowStyle Minimized -ArgumentList `
|
||||
"--shadnet $ip --host 0.0.0.0 --advertise $ip --summon-interval 5 --no-playlog --log-dir `"$server\logs`""
|
||||
Write-Host " [OK] Servers running (two minimized windows - leave them open while you play)" -ForegroundColor Green
|
||||
|
||||
# This PC's own game uses the servers too.
|
||||
& (Join-Path $PSScriptRoot "join-host.ps1") -Address $ip -NoPause
|
||||
|
||||
$cards = @(Get-ChildItem (Join-Path $root "cards") -Filter *.json -ErrorAction SilentlyContinue |
|
||||
Where-Object { ((Get-Content $_.FullName -Raw | ConvertFrom-Json).shadnet -replace ':\d+$', '') -eq $ip })
|
||||
Set-Clipboard -Value $ip -ErrorAction SilentlyContinue
|
||||
Write-Host ""
|
||||
Write-Host " ============================================" -ForegroundColor Cyan
|
||||
Write-Host " Send this to your friend: $ip" -ForegroundColor White
|
||||
Write-Host " (already copied - just paste it in Discord)" -ForegroundColor Gray
|
||||
Write-Host " They double-click JOIN-HOST and paste it." -ForegroundColor Gray
|
||||
if ($cards.Count -gt 0) {
|
||||
Write-Host " Or DM them your card: cards\$($cards[0].Name)" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host " Or make a card (address + key in one file): GENERATE-CARD" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host " ============================================" -ForegroundColor Cyan
|
||||
Read-Host "`n Press Enter to close this window (the servers keep running)"
|
||||
@@ -79,5 +79,9 @@
|
||||
"PvP is ON.\\nPlayers can hurt each other.": "PvP ist AN.\\nSpieler können einander verletzen.",
|
||||
"PvP is OFF.": "PvP ist AUS.",
|
||||
"PvP needs another player in your game.": "PvP braucht einen weiteren Spieler in deinem Spiel.",
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Deine eigene Welt wurde wiederhergestellt\\n(das Spiel wurde geschlossen, während du Gast warst).\\nRaste an einer Laterne oder lade neu, um sie zu sehen."
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Deine eigene Welt wurde wiederhergestellt\\n(das Spiel wurde geschlossen, während du Gast warst).\\nRaste an einer Laterne oder lade neu, um sie zu sehen.",
|
||||
"An item was added to your items.": "Ein Gegenstand wurde zu deinen Gegenständen hinzugefügt.",
|
||||
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Du beobachtest Jäger {n} von {count}.\\nTab / R1: nächster Jäger.\\nF: freie Kamera.",
|
||||
"Watching the living hunter.\\nF: free camera.": "Du beobachtest den lebenden Jäger.\\nF: freie Kamera.",
|
||||
"Watching hunter {n} of {count}.": "Du beobachtest Jäger {n} von {count}."
|
||||
}
|
||||
@@ -79,5 +79,9 @@
|
||||
"PvP is ON.\\nPlayers can hurt each other.": "PvP ACTIVADO.\\nLos jugadores pueden herirse entre sí.",
|
||||
"PvP is OFF.": "PvP DESACTIVADO.",
|
||||
"PvP needs another player in your game.": "El PvP requiere otro jugador en tu partida.",
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Se ha restaurado tu propio mundo\\n(el juego se cerró mientras eras invitado).\\nDescansa en un farol o recarga para verlo."
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Se ha restaurado tu propio mundo\\n(el juego se cerró mientras eras invitado).\\nDescansa en un farol o recarga para verlo.",
|
||||
"An item was added to your items.": "Se ha añadido un objeto a tus objetos.",
|
||||
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Observando al cazador {n} de {count}.\\nTab / R1: siguiente cazador.\\nF: cámara libre.",
|
||||
"Watching the living hunter.\\nF: free camera.": "Observando al cazador con vida.\\nF: cámara libre.",
|
||||
"Watching hunter {n} of {count}.": "Observando al cazador {n} de {count}."
|
||||
}
|
||||
@@ -79,5 +79,9 @@
|
||||
"PvP is ON.\\nPlayers can hurt each other.": "PvP ACTIVÉ.\\nLes joueurs peuvent se blesser.",
|
||||
"PvP is OFF.": "PvP DÉSACTIVÉ.",
|
||||
"PvP needs another player in your game.": "Le PvP requiert un autre joueur dans la partie.",
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Votre monde a été restauré\\n(le jeu s'est fermé pendant que vous étiez invité).\\nReposez-vous à une lanterne ou rechargez."
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Votre monde a été restauré\\n(le jeu s'est fermé pendant que vous étiez invité).\\nReposez-vous à une lanterne ou rechargez.",
|
||||
"An item was added to your items.": "Un objet a été ajouté à vos objets.",
|
||||
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Vous suivez le chasseur {n} sur {count}.\\nTab / R1 : chasseur suivant.\\nF : caméra libre.",
|
||||
"Watching the living hunter.\\nF: free camera.": "Vous suivez le chasseur encore en vie.\\nF : caméra libre.",
|
||||
"Watching hunter {n} of {count}.": "Vous suivez le chasseur {n} sur {count}."
|
||||
}
|
||||
@@ -3,8 +3,9 @@
|
||||
"""bbport: makes bbsc/emulator/src/core/bbsc_translations.inc from the translations of BBSC's texts.
|
||||
|
||||
Each <language>.json here maps BBSC's English text (as written in its C++ sources, "\\n" for a line
|
||||
break; "{boss}" where a text names one boss with <?npcNameId@N?>) to that language. The .inc is
|
||||
the table core/bbsc_i18n.cpp looks them up in; run this after changing a .json:
|
||||
break; "{boss}" where a text names one boss with <?npcNameId@N?>; "{n}" and "{count}" for a
|
||||
text's first and second number) to that language. The .inc is the table core/bbsc_i18n.cpp looks
|
||||
them up in; run this after changing a .json:
|
||||
|
||||
python3 bbsc/translations/gen_translations.py
|
||||
"""
|
||||
@@ -15,7 +16,7 @@ HERE = Path(__file__).resolve().parent
|
||||
OUT = HERE.parent / 'emulator/src/core/bbsc_translations.inc'
|
||||
# The column order of bbsc_i18n.cpp's Language.
|
||||
LANGUAGES = ('es', 'fr', 'de', 'it', 'pt', 'ru')
|
||||
TAGS = ('<?joinName?>', '<?hostName?>', '<?deadName?>', '<?leaveName?>', '{boss}')
|
||||
TAGS = ('<?joinName?>', '<?hostName?>', '<?deadName?>', '<?leaveName?>', '{boss}', '{n}', '{count}')
|
||||
|
||||
|
||||
def literal(text):
|
||||
|
||||
@@ -79,5 +79,9 @@
|
||||
"PvP is ON.\\nPlayers can hurt each other.": "PvP ATTIVO.\\nI giocatori possono ferirsi a vicenda.",
|
||||
"PvP is OFF.": "PvP DISATTIVATO.",
|
||||
"PvP needs another player in your game.": "Il PvP richiede un altro giocatore in partita.",
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Il tuo mondo è stato ripristinato\\n(il gioco si era chiuso mentre eri ospite).\\nRiposa a una lanterna o ricarica per vederlo."
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Il tuo mondo è stato ripristinato\\n(il gioco si era chiuso mentre eri ospite).\\nRiposa a una lanterna o ricarica per vederlo.",
|
||||
"An item was added to your items.": "Un oggetto è stato aggiunto agli oggetti.",
|
||||
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Stai osservando il cacciatore {n} di {count}.\\nTab / R1: cacciatore successivo.\\nF: telecamera libera.",
|
||||
"Watching the living hunter.\\nF: free camera.": "Stai osservando il cacciatore ancora in vita.\\nF: telecamera libera.",
|
||||
"Watching hunter {n} of {count}.": "Stai osservando il cacciatore {n} di {count}."
|
||||
}
|
||||
@@ -79,5 +79,9 @@
|
||||
"PvP is ON.\\nPlayers can hurt each other.": "PvP ATIVADO.\\nOs jogadores podem se ferir.",
|
||||
"PvP is OFF.": "PvP DESATIVADO.",
|
||||
"PvP needs another player in your game.": "O PvP precisa de outro jogador no seu jogo.",
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Seu próprio mundo foi restaurado\\n(o jogo fechou enquanto você era convidado).\\nDescanse num lampião ou recarregue para vê-lo."
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Seu próprio mundo foi restaurado\\n(o jogo fechou enquanto você era convidado).\\nDescanse num lampião ou recarregue para vê-lo.",
|
||||
"An item was added to your items.": "Um item foi adicionado aos seus itens.",
|
||||
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Observando o caçador {n} de {count}.\\nTab / R1: próximo caçador.\\nF: câmera livre.",
|
||||
"Watching the living hunter.\\nF: free camera.": "Observando o caçador vivo.\\nF: câmera livre.",
|
||||
"Watching hunter {n} of {count}.": "Observando o caçador {n} de {count}."
|
||||
}
|
||||
@@ -79,5 +79,9 @@
|
||||
"PvP is ON.\\nPlayers can hurt each other.": "PvP ВКЛЮЧЕНО.\\nИгроки могут ранить друг друга.",
|
||||
"PvP is OFF.": "PvP ВЫКЛЮЧЕНО.",
|
||||
"PvP needs another player in your game.": "Для PvP нужен ещё один игрок в вашей игре.",
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Ваш собственный мир восстановлен\\n(игра закрылась, пока вы были гостем).\\nОтдохните у фонаря или перезапустите игру."
|
||||
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Ваш собственный мир восстановлен\\n(игра закрылась, пока вы были гостем).\\nОтдохните у фонаря или перезапустите игру.",
|
||||
"An item was added to your items.": "Предмет добавлен в инвентарь.",
|
||||
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Вы наблюдаете за охотником {n} из {count}.\\nTab / R1: следующий охотник.\\nF: свободная камера.",
|
||||
"Watching the living hunter.\\nF: free camera.": "Вы наблюдаете за живым охотником.\\nF: свободная камера.",
|
||||
"Watching hunter {n} of {count}.": "Вы наблюдаете за охотником {n} из {count}."
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
# Изменения в 0.4 (после 0.3, с 2026-10-06)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, Yahar'gul стоя, 1080p без лимита FPS; «4 ядра» — `CPUS=0-3,8-11`
|
||||
(4 ядра / 8 потоков, как у Steam Deck).
|
||||
|
||||
## Модель памяти ПК — снова по умолчанию, и для NVIDIA
|
||||
|
||||
- `BB_GUEST_IN_PLACE=1` по умолчанию (`run.sh`). В лаунчере вместо «Новой модели памяти» —
|
||||
переключатель *Старая модель памяти* (ключ `legacy_memory_model`): модель 0.2 как запасной вариант.
|
||||
- NVIDIA отображает dma-buf только со смещения 0. Раньше порт тогда возвращался к старой модели;
|
||||
теперь у таких драйверов каждое выделение прямой памяти игры — отдельный кусок памяти Vulkan
|
||||
(игра делает 14 выделений и каждое отображает один раз целиком, со смещения 0). Проверка при
|
||||
запуске пишет «the driver's dma-buf maps at offset 0 only: one chunk per direct memory allocation».
|
||||
`BB_GUEST_WHOLE_CHUNKS=1` включает этот режим и на других драйверах (проверка без NVIDIA):
|
||||
на RX 7800 XT 188–190 FPS, картинка в порядке. **На NVIDIA не проверено.**
|
||||
|
||||
## Уровень API: libGnm игры
|
||||
|
||||
Игра собрана со статической libGnm: пакеты PM4 пишет не драйвер, а сама игра, через функции
|
||||
`sce::Gnm::DrawCommandBuffer` / `DispatchCommandBuffer` / `ConstantCommandBuffer` в
|
||||
`0x1068000–0x10a0000` и `0x11fd000–0x11fe000`. Это и есть графический API игры — уровень, на
|
||||
котором работают слои вроде DXVK.
|
||||
|
||||
- `tools/re/`: поиск пакетов PM4 в коде (`pm4_scan.py`), граф вызовов по границам функций из
|
||||
`.eh_frame` (`callgraph.py`), сигнатуры функций по регистрам (`gnm_sigs.py`), таблица перехватов
|
||||
(`gen_gnm_table.py` → `gpu/shim/bbport_gnm_table.inc`), имена функций (`gnm_names.txt`).
|
||||
- `gpu/shim/bbport_gnm_hooks.cpp`: перехват 513 функций libGnm в начале (jmp на заглушку рядом с
|
||||
образом, перенесённый пролог), до запуска кода игры; каждая точка сверяется с eboot 1.09.
|
||||
- `BB_GNM_CENSUS=1` — счёт вызовов (`BB_GNM_CENSUS_FILE` — полный список в файл).
|
||||
- `BB_GNM_OBSERVE=1` — каждый метод командного буфера отмечает пакеты, которые записал;
|
||||
при отправке проверяется, откуда пришёл каждый пакет. Итог: **100,0 % пакетов (99,9 % слов)
|
||||
пишут известные вызовы API** — командный поток игры целиком описывается вызовами Gnm.
|
||||
- `BB_GNM_TRACE=адрес[:число]` — аргументы, вызывающий код и записанные пакеты.
|
||||
- Найдено: игра вызывает 99 методов командных буферов; обратный вызов резервирования
|
||||
`0x26af230` начинает новый кусок буфера (список кусков уходит в отправку отдельными DCB);
|
||||
`writeDataInline` (`0x1073b80`) из GX обнуляет 192 байта по разу на проход; метки EOP
|
||||
(`writeAtEndOfPipe`, значение 1) ставит распределитель памяти GPU, `waitOnAddress` ждёт их
|
||||
же — это барьер в порядке очереди, а не ожидание CPU.
|
||||
|
||||
## Быстрее на слабых процессорах
|
||||
|
||||
- `WRITE_DATA` и DMA ждали все отложенные копии хоста (22 мкс на каждый `WRITE_DATA`,
|
||||
~35 за кадр, 12 % времени потока записи отрисовок на 4 ядрах). Теперь ждут только копии,
|
||||
которые читают записываемый диапазон (`Scheduler::WaitHostCopiesFor`, таблица источников по
|
||||
64 КиБ); командный процессор PS4 тоже не ждёт предыдущих отрисовок перед такой записью.
|
||||
Ожидания: 37 → 2 за кадр. `BB_HOST_COPY_WAITS=all` — прежнее поведение.
|
||||
- Проверка одновременной записи в планировщик (`ProducerScope`) включается только с
|
||||
`BB_PRODUCER_CHECK=1` (лаунчер: *Диагностика вылетов*).
|
||||
- 4 ядра: 158 → 167 FPS.
|
||||
- `BB_SECTIONS=1` показывает и задачи стадии B по функциям.
|
||||
|
||||
## Исправления
|
||||
|
||||
- Свёрнутое окно (Win+D, «Показать рабочий стол») больше не завершает игру (issue #31):
|
||||
поверхность 0×0 не попадает ни в размер кадра, ни в swapchain; кадры, пока окно свёрнуто,
|
||||
не выводятся. Проверено только, что обычный вывод не изменился: в Hyprland окно не
|
||||
сворачивается до 0×0.
|
||||
|
||||
## Сравнение с 0.3
|
||||
|
||||
Лаунчер, раздел «Для разработчика»: *Синхронизация как в 0.3* (`BB_AS_0_3=1` в `run.sh`)
|
||||
возвращает то, что изменилось после релиза: старая модель памяти, `WRITE_DATA`/DMA ждут все
|
||||
копии хоста (`BB_HOST_COPY_WAITS=all`), проверка одновременной записи (`BB_PRODUCER_CHECK=1`).
|
||||
Yahar'gul стоя, 1080p: 123 FPS против ~186 с новой моделью памяти и трансляции. Из командной
|
||||
строки: `bash tools/play_ab.sh v03 BB_AS_0_3=1`.
|
||||
|
||||
## Глубина резкости (DoF) снова работает (issue #44)
|
||||
|
||||
С DoF и любым апскейлером (FSR 3/4/4.1.1, TAA) в дальнем размытии — небо, туман — появлялись
|
||||
тёмные прямоугольники с резкими краями, полосы и перевёрнутая копия сцены; в 0.2 этого не было.
|
||||
Причина — прицельные ожидания дампов констант из 0.3 (`DumpConstRam` ждёт поток записи отрисовок,
|
||||
только если пересекается с их чтениями): в список чтений попадали лишь константные буферы, а
|
||||
вершины и индексы поток записи тоже читает из памяти игры. Игра пишет мелкие данные отрисовок
|
||||
(в том числе полноэкранные квады DoF) через движок констант, и дамп для следующих отрисовок
|
||||
перезаписывал их раньше, чем их успевали прочитать. Окно открывалось, когда поток записи
|
||||
задерживался прямо перед проходами DoF — на проходах апскейлера. Теперь диапазоны вершин и
|
||||
индексов тоже в списке (неподготовленные отрисовки ждут целиком). Ожиданий стало ~850/с вместо
|
||||
~150/с, FPS в проверенных местах не изменился. `BB_CONSTRAM_PIPE=0` — полное ожидание на каждом
|
||||
дампе (A/B).
|
||||
|
||||
Возможно, это же чинит мерцающие значки и глифы интерфейса (issue #11) — не проверено.
|
||||
|
||||
## Кэш шейдеров больше не ломает запуск (issues #28, #38, #39)
|
||||
|
||||
- Файлы кэша пишутся под временным именем и переименовываются: вылет или отключение питания
|
||||
оставляют старый файл или никакого, а не обрезанный.
|
||||
- Обрезанная запись при чтении больше не останавливает игру (`Invalid or corrupted
|
||||
deserialization container`, код 23): повреждённые записи считаются, кэш пересобирается.
|
||||
То же для SPIR-V с неверным заголовком и для пайплайнов, которые драйвер отверг при
|
||||
предзагрузке. Код SRT walker из кэша регистрируется только после проверки длины.
|
||||
|
||||
## Прочее
|
||||
|
||||
- Процессор без AVX, BMI1, MOVBE, LZCNT, POPCNT или SSE4.2 (Intel до Haswell) — понятное
|
||||
сообщение при запуске вместо `Guest fault (signal 4)`, код 132 (issue #26).
|
||||
`BB_SKIP_CPU_CHECK=1` — запустить всё равно.
|
||||
- `tools/fsr4cap/build_assets.sh` молча завершался, если в одной из папок Steam не было
|
||||
GE-Proton (issue #40).
|
||||
- Копии памяти игры, поставленные в прямом режиме записи (отрисовка после проходов апскейлера,
|
||||
`BB_VK_RECORD_THREAD=0`), выполняются сразу, а не со следующей отправкой.
|
||||
- Внутриигровое меню на английском или русском — переключатель «Language» в меню (PR #36);
|
||||
по умолчанию язык лаунчера. Лаунчер на бразильском португальском (PR #10); строки без перевода
|
||||
— на английском. README: адрес репозитория в команде клонирования (PR #43).
|
||||
- Переключатель `BB_TOGGLE_FILE`: бит 31 отключает сборщик текстур; второе число в файле —
|
||||
временные биты экспериментов (`BbToggle::Experiment`).
|
||||
|
||||
## Векторы движения (ночь на 2026-10-07, проверяется)
|
||||
|
||||
- Знак оси Y векторов камеры берётся из вьюпорта G-buffer каждый кадр (`BB_CAMERA_Y=up/down` —
|
||||
принудительно), а не задан константой: 0.2 и 0.3 ошибались каждая в своих местах
|
||||
(issues #41, #42, #47, #18).
|
||||
- Если в кадре несколько проходов G-buffer, камера берётся у того, чья глубина идёт в векторы.
|
||||
- FSR 4 на пресетах с апскейлом получает кадр в линейном свете (`BB_FSR4_LINEAR=0` — как раньше).
|
||||
- Маска реактивности работает и с FSR 4: его результат смешивается с текущим кадром на
|
||||
эффектах (туман, свет, частицы).
|
||||
|
||||
## Поток записи отрисовок не ждёт потоки записи команд (2026-10-07)
|
||||
|
||||
- Презентер (барьер кадра, FSR 1, постобработка) записывается так же, как отрисовки, на потоках
|
||||
записи команд. Раньше он брал командный буфер напрямую: поток отрисовок ждал, пока потоки
|
||||
записи закончат всё переданное (~90 мкс каждый кадр), и сам отправлял кадр (~45 мкс).
|
||||
- Где команды всё же пишутся напрямую (проходы апскейлера: векторы движения, TAA, FSR 3/4,
|
||||
резкость), они идут в отдельный сегмент со своим пулом команд, после переданных сегментов,
|
||||
без ожидания (`BB_DIRECT_SEGMENT=0` — ожидание, как раньше). Сигналы и упорядоченные задачи
|
||||
в таком сегменте остаются в очереди, чтобы не обогнать более ранние метки.
|
||||
- Поток показа ждёт, пока отправка кадра (с потока записи) ушла в очередь, прежде чем ставить
|
||||
своё ожидание на неё: иначе одна очередь ждала бы более позднюю отправку (зависание GPU при
|
||||
запуске, найдено на этой работе).
|
||||
- DMA в память, которую GPU использует на месте (модель ПК) или которую писал GPU, — это копия
|
||||
на GPU в порядке потока; поток отрисовок больше не ждёт перед ней копии и отложенные сигналы
|
||||
(~16 мкс на каждую, ~900 в секунду: 4-байтовые счётчики GDS).
|
||||
- Исправлены два давних редких случая записи в уже отправленный командный буфер: смена размера
|
||||
при резкости больше 1 (`Finish()` посреди прохода апскейлера) и смена пресета FSR 4 в меню.
|
||||
- `BB_FINAL_DUMP_TRIGGER` (`KIND=final tools/frame.sh`): снимок кадра после FSR и постобработки.
|
||||
|
||||
Замеры (Hunter's Dream, стоя, 4 ядра / 8 потоков, модель ПК): 143–144 → 146 FPS; поток
|
||||
отрисовок больше не блокируется на записи команд (было 1,8–3,3 % времени). 16 потоков: модель ПК
|
||||
~186, старая модель 144–151 (разброс между запусками ±3 %, без разницы с прежней сборкой).
|
||||
|
||||
## Режим работы в лаунчере (2026-10-07)
|
||||
|
||||
- Группа *Режим работы*, переключатель *Новая модель памяти и трансляции* (`BB_PC_MODEL=1`),
|
||||
по умолчанию выключен. Выключен — старая модель памяти, как в 0.3, со всеми исправлениями
|
||||
после неё. Включён — память игры используется видеочипом на месте, работа командного
|
||||
процессора переводится в команды Vulkan (экспериментально).
|
||||
- Только видеокарты AMD: лаунчер находит их по `/sys/class/drm/card*/device/vendor` (0x1002) и
|
||||
без AMD делает переключатель недоступным; библиотека GPU сама держит режим выключенным, если
|
||||
выбранное устройство Vulkan не AMD (гибридные ноутбуки). `BB_PC_MODEL_ANY_GPU=1` — включить
|
||||
всё равно, для проверки.
|
||||
- Прежний переключатель *Как в версии 0.3* переехал в раздел «Для разработчика» под именем
|
||||
*Синхронизация как в 0.3*: это откат для поиска регрессий, а не один из двух режимов.
|
||||
- README (оба) описывают оба режима, таблицу отличий от shadPS4 и планы.
|
||||
|
||||
## FSR 4.1.1 из своей DLL — кнопкой в лаунчере (2026-10-07)
|
||||
|
||||
- *Апскейлер → FSR 4.1.1 из своей DLL AMD → Выбрать DLL…*: выбираешь
|
||||
`amd_fidelityfx_upscaler_dx12.dll` 4.1.x, лаунчер собирает ассеты (`tools/fsr4cap/build_assets.sh`)
|
||||
в фоне, ход — в подписи строки и во вкладке «Журнал», в конце FSR 4.1.1 выбирается. Отмена той
|
||||
же кнопкой. Без окна: `--build-fsr411 <DLL> [загрузчик]`.
|
||||
- DLL проверяется до записи (`tools/fsr4cap/dll_info.py`: версия из свойств файла, модели FSR 4
|
||||
по именам шейдеров). Сборки сообщества OptiScaler (4.0.2b/c: модель `v07_i8`, 16 проходов вместо
|
||||
29, другие веса) пока не подходят — об этом сразу сообщение, без записи. Загрузчик ищется рядом
|
||||
с DLL и папкой выше под обоими именами (`amd_fidelityfx_loader_dx12.dll`, у OptiScaler —
|
||||
`amd_fidelityfx_dx12.dll`), иначе лаунчер его спрашивает.
|
||||
- Proton запускается через umu-run, если он установлен, иначе в контейнере Steam, который требует
|
||||
этот Proton (`toolmanifest.vdf`: Steam Linux Runtime 4 для GE-Proton 11) — umu больше не нужен.
|
||||
- AppImage содержит собранные заранее dxil-spirv и fsr4cap.exe (Nix), SPIRV-Tools: компиляторы и
|
||||
сеть не нужны, только GE-Proton. Рабочая папка (префикс Proton ~300 МБ) удаляется после сборки.
|
||||
- Заголовки FidelityFX API (MIT) лежат в `tools/fsr4cap/ffx`: SDK больше не клонируется.
|
||||
- Новый набор заменяет старый только целиком; коды выхода скрипта (2–7) лаунчер показывает
|
||||
понятным текстом. Собранная ранее dxil-spirv, которая не запускается (дерево переехало),
|
||||
собирается заново.
|
||||
|
||||
Проверено: из исходников (Nix, umu) и пакет (Steam Linux Runtime 4 в FHS-окружении) — результат
|
||||
побайтно совпадает с прежним `fsr4_411`; кнопка в лаунчере (headless-сессия): ход «N из 20»,
|
||||
отказ для 4.0.2c. Не проверено: AppImage на обычном дистрибутиве / Steam Deck (на NixOS AppImage
|
||||
скрывает системный `/nix` и FHS-ссылки).
|
||||
- После первой проверки (DLL из Pragmata): это официальная FSR 4.0.3 с той же моделью
|
||||
`v07_fp8_no_scale`, но её провайдер AMD показывает только на RDNA4 — на RX 7800 XT под любым
|
||||
Proton DLL предлагает лишь FSR 3.1.5 и 2.3.4. Теперь 4.0.x отклоняется сразу с объяснением.
|
||||
Отклоняется и «загрузчик», который на самом деле DLL апскейлера (так получилось в видео:
|
||||
загрузчика рядом не было, и в окне выбора оказалась та же DLL).
|
||||
- Proton выбирается перебором: первый, под которым DLL включает FSR 4.1 (GE-Proton 10+,
|
||||
Proton-CachyOS, Proton Experimental, затем Proton 11.0/10.0 из Steam), у каждого свой префикс.
|
||||
Проверено на RX 7800 XT: GE-Proton 11-6, Proton-CachyOS 11 (июль), Experimental (октябрь) —
|
||||
да; Proton 11.0-2c и GE-Proton 9 — нет. На Steam Deck был только GE-Proton 9.
|
||||
- На NixOS AppImage сразу говорит, что рантайм Steam там не запустить (код 8), вместо
|
||||
непонятного «не запустился под Proton».
|
||||
|
||||
## FSR 4.1.1: одна DLL и вариант FP8 для RDNA4 (2026-10-07, день)
|
||||
|
||||
- **Загрузчик больше не нужен.** DLL апскейлера AMD сама экспортирует FidelityFX API
|
||||
(`ffxCreateContext` …), и `fsr4cap.exe` загружает её напрямую; `dll_info.py` проверяет экспорт
|
||||
по таблице PE. Загрузчик спрашивается только у DLL без этих функций (таких пока не встречалось).
|
||||
Запись с DLL напрямую совпадает с записью через загрузчик (кроме двух байт неинициализированной
|
||||
константы в кадре 0), набор — побайтно.
|
||||
- Повторная сборка из AppImage падала: `cp: cannot create regular file …/fsr4cap.exe: Permission
|
||||
denied` — копия файла из AppImage только для чтения, и вторая копия поверх неё не создаётся.
|
||||
Теперь старый файл удаляется перед копированием (и DLL, если она только для чтения).
|
||||
- **FP8 на RDNA4.** На RDNA4 vkd3d-proton даёт DLL матрицы FP8, и она запускает другой вариант
|
||||
проходов (issue #12): те же 29 проходов, константы и буферы, но шейдеры на WMMA FP8, свои веса и
|
||||
своя сетка групп (у каждого прохода свой тайл: 16×8, 32×1, 16×4 …; `_post` — ширина тензора
|
||||
выровнена до 32, а не до 4). Сборка после INT8 пробует FP8 на первом размере и, если DLL
|
||||
отдаёт другие шейдеры (RDNA4), записывает все 20 размеров во второй раз (`fp8/capture_*`).
|
||||
`extract.py` проверяет правила обоих вариантов и пишет `fsr4_411/fp8/` (матрицы FP8,
|
||||
`VK_EXT_shader_float8`) и `fsr4_411/fp8emu/` (FP8 через FP16-матрицы, как vkd3d-proton на
|
||||
RDNA3 — только для проверки). Игра выбирает FP8 сама, если у видеокарты есть матрицы FP8 и
|
||||
набор собран; `BB_FSR411_VARIANT=int8|fp8|fp8emu` — принудительно. В строке
|
||||
`Upscaler: FSR 4.1.1 replay, …` теперь указан вариант.
|
||||
- Проходы с матрицами запускаются с подгруппой 32 и полными подгруппами, как у vkd3d-proton
|
||||
(эвристика размера волны у них совпадает с использованием матриц — `extract.py` это проверяет).
|
||||
Устройство игры включает `VK_KHR_cooperative_matrix`, `VK_EXT_shader_float8`,
|
||||
`vulkanMemoryModel`, `computeFullSubgroups`, если они есть.
|
||||
- Проверено на RX 7800 XT: FP8 с эмуляцией (GE-Proton 11-6, `BB_FSR4CAP_FP8=1`) — DLL и повтор
|
||||
**побайтно одинаковы** на 7 размерах, обе модели, 4 кадра (`VARIANT=fp8emu verify.sh`; с
|
||||
dxil-spirv той же версии, что в vkd3d-proton GE-Proton 11-6 — наша закреплённая новее и
|
||||
иначе округляет преобразования FP8 в эмуляции). INT8 — как прежде (1600×900 расходится у
|
||||
правого края из-за гонки в самой DLL). На RDNA4 вживую **не проверено**: проходы модели FP8
|
||||
от обеих версий dxil-spirv одинаковы, различаются только `_post`.
|
||||
- На RDNA3 FP8 по умолчанию не записывается: эмуляция почти вдвое медленнее INT8 (1440p Balanced,
|
||||
RX 7800 XT: 3,35 мс против 1,82 мс). `BB_FSR4CAP_FP8=1` —
|
||||
записать и там, `BB_FSR4CAP_FP8=0` — не записывать нигде.
|
||||
|
||||
## Управление и игровое меню (2026-10-07)
|
||||
|
||||
- **Меню жестов с тачпада.** Касания тачпада приходили с id 0 и без времени удержания, и игра
|
||||
их не принимала: ни тачпад, ни Back, ни Tab не открывали жесты. Теперь каждое новое касание
|
||||
получает свой id (1–127), как у DualShock 4, и время удержания. Проверено в игре: левая половина
|
||||
тачпада — «Жесты», правая — «Личные вещи» (с включённым debug menu левая половина открывает его).
|
||||
В лаунчере это подписано у привязок тачпада.
|
||||
- **Клавиатура работает и при подключённом геймпаде.** Раньше она управляла игрой, только когда
|
||||
геймпада нет (а на Steam Deck он есть всегда); работали лишь Tab и Backspace. Теперь кнопки
|
||||
клавиатуры добавляются к кнопкам геймпада, а зажатая клавиша движения/камеры отклоняет стик до
|
||||
конца. Вживую с клавиатурой не проверено (только сборка и тесты).
|
||||
- **Меню настроек (Insert / L3+R3) помнит своё место** — и между открытиями, и между запусками
|
||||
(`menu_pos` в bbport.ini, доля экрана: переживает смену разрешения). Раньше оно каждый раз
|
||||
открывалось у левого края.
|
||||
- **Курсор мыши над меню.** Пока меню открыто, показывается системный курсор (в полноэкранном
|
||||
режиме и после 3 с без движения он скрыт), а меню сразу знает, где мышь, а не после первого
|
||||
движения.
|
||||
- **Настройки из меню больше не стирают назначения кнопок.** Меню записывало bbport.ini
|
||||
целиком, только со своими ключами: назначения клавиатуры и геймпада из лаунчера (`key.*`,
|
||||
`pad.*`) и другие его ключи пропадали после любого изменения в игре. Теперь меню меняет только
|
||||
свои строки, остальное оставляет, файл заменяется целиком (обрыв записи оставляет старый).
|
||||
Тест: `ninja -C out/gpu settings-test`.
|
||||
|
||||
## BB_READBACKS=2 больше не зависает при запуске
|
||||
|
||||
Точный режим обратного чтения защищает страницы, записанные GPU, и от чтения. Их читали и
|
||||
потоки копирования порта (выгрузка в VRAM: `bb:Copy*`, `bb:VkRecorder`): чтение падало в
|
||||
обработчик сбоев, тот ставил обратное чтение в очередь потоку GPU и ждал, а поток GPU в это время
|
||||
ждал поток записи отрисовок, который ждал эти копии. Теперь копии порта в точном режиме читают
|
||||
память игры через второе отображение тех же страниц, которое защита не трогает. Проверено: с
|
||||
`BB_READBACKS=2` игра доходит до титульного экрана и работает (раньше вставала на загрузке).
|
||||
В самой игре в этом режиме не проверено. `BB_PROBE=<обёртка>` в run.sh — запуск под gdb и т. п.
|
||||
и из исходников.
|
||||
|
||||
## FSR 4.1.1 из AppImage на NixOS (2026-10-07, вечер)
|
||||
|
||||
AppImage работает в своём пространстве монтирования с chroot, где её `/nix/store` заменяет
|
||||
системный: изнутри не видны ни `umu-run`, ни `nix-shell`, ни FHS-окружение Steam, и кнопка
|
||||
сборки на NixOS заканчивалась ошибкой 8. Теперь `capture_all.sh` на NixOS без `umu-run`:
|
||||
- если есть `nix-shell` (из исходников, на самой системе) — перезапускается в
|
||||
`nix-shell -p umu-launcher`;
|
||||
- в AppImage — копирует себя и `proton.sh` в рабочую папку (`~/.local/share/bbport/fsr4cap`,
|
||||
её видят обе стороны) и запускает запись на системе через `systemd-run --user --wait --pipe`
|
||||
в login-shell (PATH и NIX_PATH пользователя); ход и код выхода приходят обратно, отмена
|
||||
останавливает службу. Перевод в SPIR-V — уже внутри AppImage. `systemd-run` из
|
||||
systemd-minimal (он и так был в пакете) добавлен в PATH лаунчера.
|
||||
|
||||
Проверено на этом ПК (NixOS): кнопка без окна (`--build-fsr411`) из AppImage собирает FSR 4.1.1
|
||||
до конца, набор побайтно как эталонный, рабочая папка удалена; отмена посреди записи
|
||||
останавливает службу на системе.
|
||||
@@ -1,107 +0,0 @@
|
||||
# 0.5-pre2 — pre-release for testing
|
||||
|
||||
Second 0.5 prerelease, built from the `testing` branch. This release focuses on the experimental
|
||||
memory and GPU command translation path. Changes are listed relative to
|
||||
[0.5-pre1](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1).
|
||||
|
||||
## English
|
||||
|
||||
- **New memory module for NVIDIA and other non-AMD GPUs.** When the experimental mode is enabled,
|
||||
the layer binds chunks of game memory directly and keeps frequently used data in separate VRAM
|
||||
buffers. It does not use `vkQueueBindSparse`. The launcher now exposes the mode on all GPU
|
||||
vendors; the startup capability check is retained. AMD still selects the sparse arena by
|
||||
default within this mode; `BB_LAYER_MEMORY=1` selects the new module manually.
|
||||
- **Memory coherence fixes.** CPU writes to mirrored blocks are tracked selectively. Buffer
|
||||
bindings and command-processor copies use the current GPU data, CPU writes invalidate stale
|
||||
GPU ownership, and mixed ranges can use refreshed VRAM fringes. Promotion respects the VRAM
|
||||
budget, and cached binding decisions reduce repeated checks and locking.
|
||||
- **Original GPU copy shader enabled by default in the new model.** The game's copy shader is
|
||||
translated to SPIR-V and reads its copy list on the GPU. Its writes update both the current
|
||||
GPU view and game memory, keeping VRAM mirrors and CPU consumers consistent.
|
||||
`BB_COPY_SHADER_NATIVE=0` retains the HLE path for comparison.
|
||||
- **Portable GPU completion labels.** Supported EOP/EOS/RELEASE_MEM labels can be written by
|
||||
standard Vulkan commands on GPUs without `VK_AMD_buffer_marker`. Prior GPU results become
|
||||
visible before the completion label; the AMD fast path remains available. The portable path
|
||||
can be forced for testing with `BB_GPU_LABELS_PORTABLE=1`.
|
||||
- **Synchronization and bounds fixes.** Corrected update/inline-data stage dependencies and
|
||||
index-buffer read tracking. Paged buffer accesses reject invalid ranges before accessing the
|
||||
page table. Vulkan initialization also enables the required surface extension for headless
|
||||
validation.
|
||||
- **Shader-cache migration.** The shader binary format is now version 9. Binary, metadata and
|
||||
pipeline versions are checked together before loading the cache, preventing old and new
|
||||
shader variants from being mixed. AppImage startup chooses the GPU cache directory before
|
||||
loading the GPU library, following the selected user-data directory.
|
||||
- **Regression checks.** Added GPU tests for paged copies, mirror/guest writes, range guards and
|
||||
32/64-bit portable labels. The PM4 occlusion check now spans multiple submissions to avoid
|
||||
false failures when a single submission contains no draws.
|
||||
|
||||
**How to test:** enable *New memory and translation model* in the launcher, or set
|
||||
`BB_PC_MODEL=1`. The experimental mode remains **off by default**. For testing the new module
|
||||
on AMD, also set `BB_LAYER_MEMORY=1`.
|
||||
|
||||
**Validation and limits:** runtime tests, packaged Vulkan-driver discovery tests, the new GPU
|
||||
tests and all eight in-game PM4 checks passed. GPU and AppImage checks used an RX 7800 XT/RADV;
|
||||
the portable label path was forced on AMD. Physical NVIDIA/Intel hardware and NVIDIA host-memory
|
||||
import have not been tested. Full-game Vulkan validation still reports graphics-pipeline and
|
||||
image-layout errors. A consistent FPS improvement over the previous copy path has not been
|
||||
established, and long gameplay sessions still need testing.
|
||||
|
||||
The earlier 0.5 changes remain included: settings in the game's System menu, display/monitor
|
||||
selection, effect and patch controls, the updated overlay, launcher log export, Chinese
|
||||
translation, DLC add-on folders and atomic save-file writes. See the
|
||||
[0.5-pre1 notes](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1) for details.
|
||||
|
||||
The AppImage contains no game files. Proprietary FSR 4 model assets and NVIDIA's DLSS SDK/libraries
|
||||
are not bundled. `SHA256SUMS` accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
- **Новый модуль памяти для NVIDIA и остальных не-AMD GPU.** При включённом экспериментальном
|
||||
режиме прослойка привязывает куски памяти игры напрямую и держит часто используемые данные
|
||||
в отдельных буферах VRAM. `vkQueueBindSparse` не используется. Переключатель в лаунчере
|
||||
доступен для всех производителей GPU, стартовая проверка возможностей драйвера сохранена.
|
||||
На AMD внутри нового режима по умолчанию остаётся sparse-арена; новый модуль можно выбрать
|
||||
через `BB_LAYER_MEMORY=1`.
|
||||
- **Согласование памяти CPU и GPU.** Записи CPU в блоки с зеркалами отслеживаются выборочно.
|
||||
Привязки буферов и копии командного процессора используют актуальные данные GPU; новые записи
|
||||
CPU снимают устаревшие пометки GPU. Смешанные диапазоны могут использовать обновляемые края
|
||||
зеркал VRAM. Переносы соблюдают бюджет видеопамяти, кэш решений уменьшает повторные проверки
|
||||
и блокировки.
|
||||
- **Оригинальный GPU copy shader по умолчанию в новой модели.** Шейдер игры транслируется
|
||||
в SPIR-V и сам читает список копий на видеокарте. Результат записывается и в актуальное
|
||||
GPU-представление, и в память игры: зеркала VRAM и потребители на CPU видят согласованные
|
||||
данные. `BB_COPY_SHADER_NATIVE=0` оставляет HLE для сравнения.
|
||||
- **Переносимые метки завершения GPU.** Поддерживаемые метки EOP/EOS/RELEASE_MEM записываются
|
||||
стандартными командами Vulkan на GPU без `VK_AMD_buffer_marker`. Предшествующие результаты
|
||||
GPU становятся видимыми до метки завершения. Быстрый путь AMD сохранён;
|
||||
`BB_GPU_LABELS_PORTABLE=1` принудительно выбирает переносимый путь для проверки.
|
||||
- **Синхронизация и границы доступа.** Исправлены зависимости update/inline data и учёт чтения
|
||||
index buffer. Недопустимые диапазоны отклоняются до обращения к таблице страниц. Для
|
||||
headless-проверки Vulkan включается необходимое расширение surface.
|
||||
- **Миграция кэша шейдеров.** Формат shader binary поднят до версии 9. Версии binary, metadata
|
||||
и pipeline проверяются вместе до загрузки кэша, чтобы старые и новые варианты шейдеров
|
||||
не смешивались. AppImage выбирает GPU-каталог до загрузки GPU-библиотеки, с учётом выбранного
|
||||
каталога пользовательских данных.
|
||||
- **Проверки регрессий.** Добавлены GPU-тесты копирования через таблицу страниц, записей
|
||||
в зеркало и память игры, границ доступа и переносимых 32/64-битных меток. PM4-проверка
|
||||
occlusion теперь охватывает несколько отправок команд, чтобы отправка без отрисовок
|
||||
не давала ложный FAIL.
|
||||
|
||||
**Как проверить:** включить *Новую модель памяти и трансляции* в лаунчере или задать
|
||||
`BB_PC_MODEL=1`. Экспериментальный режим по умолчанию **выключен**. Для нового модуля на AMD
|
||||
также нужен `BB_LAYER_MEMORY=1`.
|
||||
|
||||
**Проверки и ограничения:** пройдены тесты рантайма, поиска Vulkan-драйвера в пакете, новые
|
||||
GPU-тесты и все восемь PM4-проверок в игре. GPU и AppImage проверялись на RX 7800 XT/RADV;
|
||||
переносимые метки принудительно включались на AMD. Физические NVIDIA/Intel и импорт host memory
|
||||
на NVIDIA пока не проверены. Полная Vulkan-валидация игры ещё сообщает об ошибках графических
|
||||
конвейеров и раскладок изображений. Стабильный прирост FPS относительно предыдущего пути
|
||||
копирования не подтверждён; длительное прохождение также требует проверки.
|
||||
|
||||
Все изменения первого prerelease 0.5 включены: настройки в меню System самой игры, выбор
|
||||
дисплея/монитора, управление эффектами и патчами, обновлённый оверлей, экспорт логов лаунчера,
|
||||
китайский перевод, папки DLC и атомарная запись сохранений. Подробности — в
|
||||
[описании 0.5-pre1](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1).
|
||||
|
||||
Игровых файлов в AppImage нет. Проприетарные модели FSR 4 и SDK/библиотеки DLSS не включены.
|
||||
К загрузке приложен `SHA256SUMS`.
|
||||
@@ -1,103 +0,0 @@
|
||||
# 0.5-pre3 — pre-release for testing
|
||||
|
||||
Third 0.5 prerelease, built from the `testing` branch. It addresses the first hardware logs of the
|
||||
new memory module on NVIDIA (GTX 1660 Ti laptop, 6 GB, driver 615.71). Changes are listed relative
|
||||
to [0.5-pre2](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre2). Details and
|
||||
measurements: [NVIDIA_MEMORY_MODEL.ru.md](NVIDIA_MEMORY_MODEL.ru.md).
|
||||
|
||||
## English
|
||||
|
||||
- **VRAM copies were disabled on NVIDIA.** The fixed-arena workaround for NVIDIA's sparse binding
|
||||
was also applied to the memory module, which has no sparse arena: it made no VRAM copies of the
|
||||
game's memory at all (10–15 FPS on the GTX 1660 Ti). The workaround no longer applies to the
|
||||
module; `BB_FIXED_ARENA=0` is no longer needed.
|
||||
- **One VRAM budget for textures and copies of the game's memory.** The texture collector counts
|
||||
all of our VRAM. On cards below ~9 GB its critical mark (3.9 GB on the 6 GB card) was below what
|
||||
the copies and the textures need together, so it kept evicting textures in use (155–1255 every
|
||||
5 s, stutters) while the copies kept growing up to 90 % of the driver's budget. Copies now stop
|
||||
384 MiB short of that mark; close to it the module frees room itself, first the unused room of
|
||||
its copies, then copies not used for 3 s (`BB_VRAM_PRESSURE_IDLE_SECONDS`).
|
||||
- **Fewer reallocations of VRAM copies.** A copy that met newly loaded data was replaced by a
|
||||
bigger one each time: loading a level allocated 15–27 GB for 1.3 GB of data, with up to 7.4 GB
|
||||
of VRAM in use at once, and streaming areas did the same while playing. A growing copy now gets
|
||||
room to grow (up to 64 MiB, `BB_LAYER_MIRROR_GROWTH_MB`): 5–10 GB, and the worst loading frame of
|
||||
a test route went from 555 to 87 ms (RX 7800 XT). Replaced copies are freed as soon as the GPU is
|
||||
done with them.
|
||||
- **Command-processor writes (WRITE_DATA)** update the VRAM copy as well, instead of sending the
|
||||
block back to system memory (with watched blocks, below, up to 2 GB a minute went back and forth).
|
||||
- **Watched blocks** (`BB_LAYER_WATCH`, on by default). Memory the CPU writes without announcing
|
||||
it was always read by the GPU from system memory, over the bus on NVIDIA. It now gets a VRAM
|
||||
copy for reading bindings too; a write trap catches the next CPU write and the block is uploaded
|
||||
again before its next use. This also replaces most volatile-block refreshes (RX 7800 XT:
|
||||
1.3 GB/s of copies → 36 MB/s; the NVIDIA log showed 455 MB/s). Blocks the CPU writes nearly every
|
||||
frame still stay in system memory: uploading them before draws ended render passes (GPU frame
|
||||
4.8 → 7.2 ms on the RX 7800 XT); `BB_LAYER_WATCH_HOT=1` turns that on for experiments.
|
||||
- **File reads retry on EFAULT** (a page protected again between the runtime's touch and the
|
||||
kernel's copy).
|
||||
- **Testing aids:** `BB_VRAM_LIMIT_MB=N` takes the driver's VRAM budget as at most N MiB (5600
|
||||
gives the texture-collector marks of a 6 GB card); `BB_RESIDENCY_MIX=1` also says why blocks are
|
||||
not watched; the `Layer memory:` lines report watched blocks and write traps by kind.
|
||||
|
||||
**How to test:** enable *New memory and translation model* in the launcher, or set
|
||||
`BB_PC_MODEL=1`; the mode remains **off by default**. Logs with `BB_FRAME_STATS=1
|
||||
BB_GPU_PROFILE=1` at one place, once as is, once with `BB_LAYER_WATCH=0` and once with the mode off,
|
||||
show where the time goes.
|
||||
|
||||
**Validation and limits:** RX 7800 XT/RADV (PCIe 3.0) with the memory module forced on and
|
||||
`BB_VRAM_LIMIT_MB=5600`: model of 0.3 ~195 FPS, module ~195–213 FPS (GPU 4.6–5.2 ms a frame; runs
|
||||
vary by about ±10 %), VRAM kept under the collector's critical mark, a recorded route without
|
||||
frames over 25 ms, a 3-minute walk without errors, unchanged images; runtime, unit (101) and GPU
|
||||
tests passed. Not tested on physical NVIDIA or Intel hardware: on AMD reads of the game's memory
|
||||
in place cost little, so whether NVIDIA now reaches the speed of the 0.3 model is not established.
|
||||
|
||||
The AppImage contains no game files. Proprietary FSR 4 model assets and NVIDIA's DLSS SDK/libraries
|
||||
are not bundled. `SHA256SUMS` accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
- **На NVIDIA не было копий в VRAM.** Обход sparse binding на NVIDIA («фиксированная арена»)
|
||||
применялся и к модулю памяти, у которого sparse-арены нет: модуль вообще не копировал память
|
||||
игры в VRAM (10–15 FPS на GTX 1660 Ti). Теперь обход на модуль не действует,
|
||||
`BB_FIXED_ARENA=0` больше не нужен.
|
||||
- **Общий бюджет VRAM для текстур и копий памяти игры.** Сборщик кэша текстур считает всю нашу
|
||||
VRAM. На картах меньше ~9 ГБ его критическая отметка (3,9 ГБ на 6 ГБ) оказалась ниже того, что
|
||||
нужно копиям и текстурам вместе: он постоянно выселял используемые текстуры (155–1255 за 5 с,
|
||||
фризы), а копии росли до 90 % бюджета драйвера. Теперь копии встают за 384 МиБ до отметки, а
|
||||
рядом с ней модуль сам освобождает место: сначала неиспользуемый запас своих копий, затем
|
||||
копии, не нужные 3 с (`BB_VRAM_PRESSURE_IDLE_SECONDS`).
|
||||
- **Меньше перевыделений копий.** Копия, к которой примыкали новые данные, каждый раз заменялась
|
||||
большей: загрузка уровня выделяла 15–27 ГБ на 1,3 ГБ данных, до 7,4 ГБ VRAM одновременно; то же
|
||||
при подгрузке районов во время игры. Растущая копия теперь получает запас под рост (до 64 МиБ,
|
||||
`BB_LAYER_MIRROR_GROWTH_MB`): 5–10 ГБ, худший кадр загрузки в тестовом маршруте 555 → 87 мс
|
||||
(RX 7800 XT). Заменённые копии освобождаются сразу, как только видеочип с ними закончил.
|
||||
- **Записи командного процессора (WRITE_DATA)** обновляют и копию в VRAM, а не возвращают блок
|
||||
в системную память (с наблюдаемыми блоками, ниже, туда и обратно ходило до 2 ГБ в минуту).
|
||||
- **Наблюдаемые блоки** (`BB_LAYER_WATCH`, по умолчанию включено). Память, в которую процессор
|
||||
пишет без предупреждения, видеочип всегда читал из системной памяти, на NVIDIA через шину.
|
||||
Теперь для читающих привязок у неё тоже есть копия в VRAM: ловушка записи ловит следующую
|
||||
запись процессора, и блок перезаливается перед следующим использованием. Это же заменило
|
||||
большую часть обновлений летучих блоков (RX 7800 XT: 1,3 ГБ/с копий → 36 МБ/с; в логе с NVIDIA
|
||||
было 455 МБ/с). Блоки, которые процессор пишет почти каждый кадр, остаются в системной памяти:
|
||||
их заливка перед отрисовками завершала render pass (кадр видеочипа 4,8 → 7,2 мс на RX 7800 XT);
|
||||
`BB_LAYER_WATCH_HOT=1` включает это для экспериментов.
|
||||
- **Чтение файлов повторяется при EFAULT** (страница снова защищена между касанием рантайма и
|
||||
копированием ядром).
|
||||
- **Для проверок:** `BB_VRAM_LIMIT_MB=N` — бюджет VRAM драйвера считается не больше N МиБ (5600
|
||||
даёт пороги сборщика текстур карты на 6 ГБ); `BB_RESIDENCY_MIX=1` объясняет, почему блоки не
|
||||
наблюдаются; строки `Layer memory:` показывают наблюдаемые блоки и срабатывания ловушек по видам.
|
||||
|
||||
**Как проверить:** включить *Новую модель памяти и трансляции* в лаунчере или задать
|
||||
`BB_PC_MODEL=1`; режим по-прежнему **выключен по умолчанию**. Логи с `BB_FRAME_STATS=1
|
||||
BB_GPU_PROFILE=1` в одном месте — как есть, с `BB_LAYER_WATCH=0` и с выключенным режимом —
|
||||
покажут, куда уходит время.
|
||||
|
||||
**Проверки и ограничения:** RX 7800 XT/RADV (PCIe 3.0), модуль памяти включён вручную,
|
||||
`BB_VRAM_LIMIT_MB=5600`: модель 0.3 ~195 FPS, модуль ~195–213 FPS (видеочип 4,6–5,2 мс за кадр;
|
||||
разброс между запусками около ±10 %), VRAM держится ниже критической отметки сборщика,
|
||||
записанный маршрут без кадров дольше 25 мс, 3 минуты прогулки без ошибок, картинка не
|
||||
изменилась; тесты рантайма, юнит-тесты (101) и GPU-тесты пройдены. На физических NVIDIA и Intel
|
||||
не проверялось: на AMD чтения памяти игры на месте почти бесплатны, поэтому достигает ли NVIDIA
|
||||
теперь скорости модели 0.3, не установлено.
|
||||
|
||||
Игровых файлов в AppImage нет. Проприетарные модели FSR 4 и SDK/библиотеки DLSS не включены.
|
||||
К загрузке приложен `SHA256SUMS`.
|
||||
@@ -1,147 +0,0 @@
|
||||
# 0.5-pre4 — pre-release for testing
|
||||
|
||||
Fourth 0.5 prerelease, built from the `testing` branch, after the first tests of 0.5-pre3 on
|
||||
NVIDIA (GTX 1060 6 GB, GTX 1660 Ti 6 GB and a 4 GB card). Changes are listed relative to
|
||||
[0.5-pre3](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre3). Details and
|
||||
measurements: [NVIDIA_MEMORY_MODEL.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/testing/docs/NVIDIA_MEMORY_MODEL.ru.md).
|
||||
|
||||
## English
|
||||
|
||||
- **The new memory model on NVIDIA no longer stutters while areas stream in.** On NVIDIA (and
|
||||
any GPU other than AMD) the new model keeps VRAM copies of the game's memory itself. After
|
||||
each run of blocks it copied to VRAM, it rewrote the page table entries of the whole copy (up
|
||||
to 160 000 entries in one frame), looking up every block in the runtime's memory map under a
|
||||
lock the game's threads hold while they map memory: the GPU thread waited 30–70 ms in frames
|
||||
where an area streamed in. Only the entries that changed are written now, with one lookup per
|
||||
mapping. On a recorded route (RX 7800 XT forced into this path, five runs) the 99th percentile
|
||||
frame went from 11–13 ms to 5.1–7.5 ms (9.4 ms in a run where the GPU itself ran slower), the
|
||||
worst frame from 32–57 ms to 9–16 ms, close to AMD's default path (5.2–6.0 ms).
|
||||
- **No more copying the same memory to VRAM and back every frame.** Where the GPU writes
|
||||
memory the CPU also wrote, the write goes to the game's memory; a reading binding then copied
|
||||
the range to VRAM again, the next writing binding sent it back, every frame (seen at one
|
||||
spot with 42 FPS: about 200 MB/s each way). A range sent
|
||||
back for the GPU's writes a second time now stays where it is. Write traps on VRAM copies are
|
||||
set and cleared with one call per run of blocks instead of one per 64 KiB.
|
||||
- **Fewer shader-compilation hitches.** A new graphics pipeline of a pass that draws into a
|
||||
render target used in each of the last 8 frames is compiled on worker threads while its draws
|
||||
go without it for a frame or two. Translating a new shader and compute pipelines still happen at once, and so
|
||||
do passes rendered once and loading screens. `BB_ASYNC_PIPELINES=0` restores the old
|
||||
behaviour, `BB_ASYNC_PIPELINE_THREADS=N` sets the worker count. The drivers' own shader caches
|
||||
are kept between sessions (NVIDIA: no cleanup, 10 GB; Mesa: 4 GB). As before, the first start
|
||||
of a new build compiles its pipeline cache again.
|
||||
- **4 GB cards: the new memory model no longer collapses to ~5 FPS.** 0.5-pre3 stopped copying
|
||||
the game's memory to VRAM short of the texture collector's critical mark; on a 4 GB card the
|
||||
textures alone held the usage there, so no copy was made again and everything was read over
|
||||
the bus after a few minutes. VRAM copies now have their own share (25 % of the driver's
|
||||
budget, `BB_LAYER_MIRROR_FLOOR_PERCENT`) that the texture collector's mark does not take.
|
||||
Cards with 4 GB are still faster with the 0.3 memory model.
|
||||
- **No VRAM left behind after travelling.** Copies of an area left behind stayed in VRAM (large
|
||||
bindings over all memory marked every copy as in use), and the copies of the new area waited,
|
||||
read over the bus (about 10 FPS less after a lamp travel on a GTX 1060). When VRAM is short,
|
||||
copies not bound directly for 3 s now go back first, and copies with memory left without
|
||||
valid data are made again over their valid data only, giving that memory back.
|
||||
- **Damaged game extractions are reported instead of hanging (#81).** Some extraction tools
|
||||
(LibOrbisPkg PkgTool on .NET 6 or newer) leave parts of compressed sectors stale without an
|
||||
error; the game then hung while loading shaders (`shaderBinarySize … is not equal to Program
|
||||
size`). The launcher and the game's preparation now unpack the shader files (under a second)
|
||||
and say that the extraction is damaged. `scripts/game_check.py GAME_DIR` checks every file.
|
||||
- **NVIDIA driver diagnostics (#107).** `--vulkan-info` now lists the NVIDIA libraries linked
|
||||
for the package, the kernel module, the `/dev/nvidia*` nodes, hybrid-graphics variables and
|
||||
the NVIDIA libraries the dynamic linker searched for and did not load: one log shows why the
|
||||
host driver gave no `vkCreateInstance` inside the AppImage.
|
||||
- **A crash window in command recording closed.** Handing recorded commands to the recording
|
||||
threads left the current command chunk empty for a moment; anything recording on that thread
|
||||
then (a fault handler) read a null chunk. The same crash was reported for a Windows build
|
||||
(#100). The replacement chunk is now swapped in before the old one leaves.
|
||||
- **The in-game settings menu (Insert, L3+R3) looks as in 0.3 and 0.4 again:** one movable window
|
||||
with sections, check boxes, sliders and `(?)` hints, keyboard and gamepad navigation. Its
|
||||
content is that of 0.5 (DLSS, debug views, current hints).
|
||||
|
||||
**How to test:** enable *New memory and translation model* in the launcher, or set
|
||||
`BB_PC_MODEL=1`; the mode remains **off by default**. On AMD it uses the sparse arena;
|
||||
`BB_LAYER_MEMORY=1` forces NVIDIA's path there. Logs with `BB_FRAME_STATS=1` show compiles per 5 s,
|
||||
a line `GPU: N pipelines compiled in the background, M draws went without theirs`, and for
|
||||
frames over 40 ms where the new model's VRAM copies spent their time (`layer memory ms`).
|
||||
|
||||
**Validation and limits:** RX 7800 XT/RADV, a recorded 25 s route: AMD's default path p99
|
||||
5.2–6.0 ms; NVIDIA's path forced, before/after as above; no frames over 20 ms in either. A 4-minute
|
||||
session on NVIDIA's path with the VRAM budget limited to 4.5 GB (`BB_VRAM_LIMIT_MB=4608`): 177 FPS
|
||||
on average, p99 9.7 ms, VRAM at most 3.0 GB; 11 of the 12 frames over 40 ms were compiles or the
|
||||
loading screen. Runtime, unit (106) and GPU tests passed. NVIDIA's own path (the game's memory imported as
|
||||
host memory) cannot run on AMD: the gains there are not measured yet, logs from NVIDIA testers
|
||||
are welcome. Physical Intel GPUs are not tested.
|
||||
|
||||
The AppImage contains no game files. Proprietary FSR 4 model assets and NVIDIA's DLSS SDK/libraries
|
||||
are not bundled. `SHA256SUMS` accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
- **Новая модель памяти на NVIDIA больше не дёргается при подгрузке локаций.** На NVIDIA (и
|
||||
любой видеокарте, кроме AMD) новая модель сама держит копии памяти игры в VRAM. После каждого
|
||||
куска, скопированного в VRAM, она переписывала записи таблицы страниц по всей копии (до 160 000
|
||||
записей за кадр) и для каждого блока искала его в карте памяти рантайма под блокировкой,
|
||||
которую держат потоки игры, когда отображают память: поток видеокарты ждал по 30–70 мс в
|
||||
кадрах, где подгружалась локация. Теперь пишутся только изменившиеся записи, с одним поиском
|
||||
на отображение. На записанном маршруте (RX 7800 XT, принудительно на этом пути, пять прогонов)
|
||||
99-й процентиль кадра — 5,1–7,5 мс вместо 11–13 (9,4 мс в прогоне, где медленнее работала сама
|
||||
видеокарта), худший кадр — 9–16 мс вместо 32–57, близко к обычному пути AMD (5,2–6,0 мс).
|
||||
- **Память больше не гоняется в VRAM и обратно каждый кадр.** Где видеокарта пишет в память,
|
||||
в которую писал и процессор, запись идёт в память игры; привязка на чтение снова копировала
|
||||
этот диапазон в VRAM, следующая привязка на запись отправляла обратно — и так каждый кадр
|
||||
(замечено в одном месте при 42 FPS: около 200 МБ/с в каждую сторону). Диапазон, который вернулся из-за записи видеокарты второй раз, теперь
|
||||
остаётся на месте. Ловушки записи на копиях в VRAM ставятся и снимаются одним вызовом на
|
||||
участок, а не на каждые 64 КБ.
|
||||
- **Меньше рывков от компиляции шейдеров.** Новый графический конвейер прохода, который рисует
|
||||
в цель рендера, использованную в каждом из последних 8 кадров, собирается в фоновых потоках, а
|
||||
его отрисовки кадр-другой идут без него. Перевод нового шейдера и compute-конвейеры
|
||||
по-прежнему компилируются сразу, как и разовые проходы и экраны загрузки.
|
||||
`BB_ASYNC_PIPELINES=0` — как раньше, `BB_ASYNC_PIPELINE_THREADS=N` — число потоков. Дисковые
|
||||
кэши шейдеров драйверов сохраняются между сессиями (NVIDIA — без очистки, 10 ГБ; Mesa — 4 ГБ).
|
||||
Как и раньше, при первом запуске новой сборки кэш конвейеров собирается заново.
|
||||
- **Карты на 4 ГБ: новая модель памяти больше не падает до ~5 FPS.** В 0.5-pre3 копии памяти
|
||||
игры в VRAM останавливались, не доходя до критической отметки сборщика текстур; на карте 4 ГБ
|
||||
одни текстуры держали память там, копии больше не создавались, и через несколько минут всё
|
||||
читалось через шину. Теперь у копий есть своя доля VRAM (25 % бюджета драйвера,
|
||||
`BB_LAYER_MIRROR_FLOOR_PERCENT`), которую отметка сборщика текстур не забирает. На картах
|
||||
4 ГБ модель памяти 0.3 по-прежнему быстрее.
|
||||
- **После перемещений VRAM не остаётся занятой.** Копии покинутой локации оставались в VRAM
|
||||
(большие привязки на всю память помечали каждую копию как используемую), а копии новой ждали
|
||||
и читались через шину (около 10 FPS меньше после телепорта к лампе на GTX 1060). Теперь при
|
||||
нехватке VRAM сначала уходят копии, к которым напрямую не привязывались 3 с, а копии, в
|
||||
которых осталась память без действительных данных, пересоздаются только поверх
|
||||
действительных данных и отдают эту память.
|
||||
- **Повреждённая распаковка игры теперь называется прямо, а не приводит к зависанию (#81).**
|
||||
Некоторые распаковщики (LibOrbisPkg PkgTool на .NET 6 и новее) без ошибки оставляют части
|
||||
сжатых секторов устаревшими; игра зависала на загрузке шейдеров (`shaderBinarySize … is not
|
||||
equal to Program size`). Лаунчер и подготовка игры теперь распаковывают файлы шейдеров (меньше
|
||||
секунды) и сообщают, что распаковка повреждена. `scripts/game_check.py ПАПКА_ИГРЫ` проверяет
|
||||
все файлы.
|
||||
- **Диагностика драйвера NVIDIA (#107).** `--vulkan-info` теперь показывает библиотеки NVIDIA,
|
||||
подключённые для пакета, модуль ядра, узлы `/dev/nvidia*`, переменные гибридной графики и
|
||||
библиотеки NVIDIA, которые динамический компоновщик искал и не загрузил: по одному логу видно,
|
||||
почему драйвер хоста не отдал `vkCreateInstance` внутри AppImage.
|
||||
- **Закрыто окно для вылета при записи команд.** При передаче записанных команд потокам записи
|
||||
текущий кусок команд на мгновение оставался пустым; если в этот момент на том же потоке
|
||||
что-то записывало (обработчик ошибки доступа), оно читало пустой указатель. Такой же вылет был
|
||||
у сборки для Windows (#100). Теперь новый кусок подставляется до того, как уходит старый.
|
||||
- **Меню настроек в игре (Insert, L3+R3) снова выглядит как в 0.3 и 0.4:** одно перемещаемое
|
||||
окно с разделами, флажками, ползунками и подсказками `(?)`, управление клавиатурой и
|
||||
геймпадом. Содержимое — как в 0.5 (DLSS, отладочные виды, актуальные подсказки).
|
||||
|
||||
**Как проверить:** включить *Новую модель памяти и трансляции* в лаунчере или задать
|
||||
`BB_PC_MODEL=1`; режим по-прежнему **выключен по умолчанию**. На AMD он работает через
|
||||
sparse-арену; `BB_LAYER_MEMORY=1` включает на AMD путь NVIDIA. В логе с `BB_FRAME_STATS=1` —
|
||||
число компиляций за 5 с, строка `GPU: N pipelines compiled in the background, M draws went
|
||||
without theirs` и для кадров дольше 40 мс — на что ушло время копий новой модели в VRAM
|
||||
(`layer memory ms`).
|
||||
|
||||
**Проверки и ограничения:** RX 7800 XT/RADV, записанный маршрут на 25 с: обычный путь AMD —
|
||||
p99 5,2–6,0 мс; путь NVIDIA, принудительно, — до и после, как выше; ни одного кадра дольше
|
||||
20 мс. Сессия на 4 минуты на пути NVIDIA с бюджетом VRAM 4,5 ГБ (`BB_VRAM_LIMIT_MB=4608`):
|
||||
в среднем 177 FPS, p99 9,7 мс, VRAM не больше 3,0 ГБ; 11 из 12 кадров дольше 40 мс — компиляция
|
||||
или экран загрузки. Тесты рантайма, юнит-тесты (106) и GPU-тесты пройдены. Собственный путь NVIDIA (память
|
||||
игры импортируется как память хоста) на AMD не запустить: выигрыш там ещё не измерен, логи от
|
||||
тестеров с NVIDIA приветствуются. Физические Intel не проверялись.
|
||||
|
||||
Игровых файлов в AppImage нет. Проприетарные модели FSR 4 и SDK/библиотеки DLSS не включены.
|
||||
К загрузке приложен `SHA256SUMS`.
|
||||
@@ -1,172 +0,0 @@
|
||||
**English** · [Русский](#русский)
|
||||
|
||||
bbport is the counterpart of Wine + DXVK for a single game: Bloodborne (CUSA03173, v1.09) on Linux.
|
||||
The game's x86-64 code runs directly on the CPU, a runtime written for this game replaces the PS4
|
||||
system libraries, and the graphics are translated to Vulkan.
|
||||
|
||||
Questions about this project: Discord https://discord.gg/KYZRKk9CB — not the shadPS4 server.
|
||||
|
||||
### What's new since 0.4
|
||||
|
||||
Details and measurements of each step are in the prerelease notes:
|
||||
[pre1](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1),
|
||||
[pre2](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre2),
|
||||
[pre3](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre3),
|
||||
[pre4](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre4).
|
||||
|
||||
**The new memory and translation model — the default on AMD**
|
||||
- Launcher: *Mode → Memory and translation model*. *Auto* (the default) gives AMD GPUs the new
|
||||
model and NVIDIA, Intel and others the memory model of 0.3 with all its fixes; *New* and *Old*
|
||||
choose by hand. Without the launcher: `BB_PC_MODEL=1/0` (unset: as *Auto*).
|
||||
- The GPU uses the game's memory where it is, as a PC game's buffers; what it reads often is kept
|
||||
in VRAM — on AMD in a sparse buffer (the arena), on other GPUs by the layer's memory module
|
||||
without sparse rebinding (`BB_LAYER_MEMORY=1/0` by hand). The PS4 command processor's work is
|
||||
translated rather than emulated: memory writes and fences are done by the GPU in stream order,
|
||||
`COPY_DATA`, `COND_EXEC`, predication, `MEM_SEMAPHORE` and the GPU clock (100 MHz, as on the
|
||||
PS4) follow the GPU's timeline.
|
||||
- The game's occlusion queries as Vulkan queries are available with `BB_OCCLUSION_QUERIES=1`, off
|
||||
by default: with an upscaler the scene is drawn smaller and jittered, and a lamp's glow then
|
||||
flashed grey over the whole frame now and then. Off, everything counts as visible, as in the
|
||||
0.3 model.
|
||||
- The layer's memory module (NVIDIA and others): some launches no longer run the GPU 1.3–1.7 times
|
||||
slower (copies that broke render passes up to ~100 times a frame).
|
||||
- On NVIDIA the new model can be chosen by hand; testers' results differ by scene (some scenes are
|
||||
slower than with the old model), so it is not the default there yet. Logs are welcome.
|
||||
|
||||
**Settings inside the game's own menu**
|
||||
- *System* has *Display* and *Effects*, real pages of the game's menu, on the title screen and
|
||||
in the pause menu: output resolution, quality preset and sharpness as sliders, FPS counter,
|
||||
model detail and upscaler; chromatic aberration, depth of field, motion blur, SSAO, dynamic
|
||||
shadows and SSR. The game's patches and its own AA are in the overlay menu and the launcher.
|
||||
- The overlay menu (Insert / L3+R3) looks as in 0.3 again.
|
||||
- No "Play online / offline" screen: there is no PSN, the game opens its main menu offline.
|
||||
|
||||
**Smoother play**
|
||||
- New graphics pipelines of passes drawn every frame compile in the background: fewer hitches.
|
||||
Only geometry goes without its pipeline meanwhile (it shows a frame later); full-screen passes
|
||||
(lighting, fog, post-processing) wait for theirs — skipping one left whole frames grey. The
|
||||
drivers' shader caches are kept between sessions. In the game's menu the output resolution and
|
||||
preset sliders apply once they rest for 0.4 s, not at every step.
|
||||
- Cards with little VRAM: FPS no longer falls the longer you play (the texture collector evicted
|
||||
textures used a moment ago and read them again over the bus: 32 915 in ~7 minutes in an NVIDIA
|
||||
log); the game no longer stops when VRAM for its memory copies runs out; VRAM is given back
|
||||
after travelling.
|
||||
|
||||
**New**
|
||||
- DLC: add-on folders under `user/addcont/` are reported as installed; saves with The Old Hunters
|
||||
load (#33, PR #84).
|
||||
- DLSS on Linux for NVIDIA RTX when built with NVIDIA's DLSS SDK (PR #83; not in the AppImage).
|
||||
- Launcher: monitor choice (#69), *Copy* and *Export log…* (PR #78), Simplified Chinese (PR #75).
|
||||
- Damaged game extractions are reported instead of hanging while loading shaders (#81);
|
||||
`scripts/game_check.py GAME_DIR` checks every file.
|
||||
- NVIDIA in the AppImage: the host's EGL vendor files are passed along (without them NVIDIA's
|
||||
driver gave no `vkCreateInstance` on some hosts, #107; `BB_NVIDIA_EGL=0`: off); `--vulkan-info`
|
||||
prints the driver report together with the summary.
|
||||
- AMD Polaris, Vega and their APUs under RADV: `RADV_DEBUG=nohiz` is set automatically (black
|
||||
halos and vanishing geometry, #54, #99; your own `RADV_DEBUG` or `BB_AUTO_NOHIZ=0` keeps it off).
|
||||
- The FSR 4.1.1 build also finds Proton installed system-wide (PR #112). The bundled Dear ImGui
|
||||
is private to the GPU library: Vulkan layers with their own copy no longer crash the game
|
||||
(PR #113).
|
||||
|
||||
**Fixes**
|
||||
- Saves are no longer damaged by a crash during saving (#64).
|
||||
- FSR 4.1.1 build: Proton runs without Steam's container where it cannot start (#61).
|
||||
- The item picture on the loading screen at outputs other than 1080p (#67).
|
||||
- GPU hangs from garbage indirect dispatch sizes (#34); a crash window while recording commands
|
||||
(the same crash was reported for a Windows build, #100).
|
||||
- Small mips of tiled textures are detiled the way the GPU stores them (from shadPS4 #5196).
|
||||
- Chinese/Japanese/Korean Windows locales in the game preparation (#55); an `eboot.bin` dumped as a
|
||||
plain ELF (#49); stick calibration for Switch Pro clones (#70, PR #71); `BB_FSR4_STATS=1`
|
||||
(PR #80).
|
||||
|
||||
**Known limits:** one machine was tested thoroughly (RX 7800 XT, Mesa/RADV). Banding in dark
|
||||
gradients is not confirmed fixed. The Windows build is a separate port.
|
||||
|
||||
The AppImage contains no game files. Proprietary FSR 4 model assets and NVIDIA's DLSS SDK/libraries
|
||||
are not bundled. `SHA256SUMS` accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
bbport — аналог Wine + DXVK для одной игры: Bloodborne (CUSA03173, v1.09) на Linux. Код игры x86-64
|
||||
выполняется прямо на процессоре, системные библиотеки PS4 заменяет рантайм, написанный под эту игру,
|
||||
графика переводится в Vulkan.
|
||||
|
||||
Вопросы по проекту: Discord https://discord.gg/KYZRKk9CB — не сервер shadPS4.
|
||||
|
||||
### Что нового после 0.4
|
||||
|
||||
Подробности и замеры каждого шага — в описаниях пререлизов:
|
||||
[pre1](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1),
|
||||
[pre2](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre2),
|
||||
[pre3](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre3),
|
||||
[pre4](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre4).
|
||||
|
||||
**Новая модель памяти и трансляции — по умолчанию на AMD**
|
||||
- Лаунчер: *Режим работы → Модель памяти и трансляции*. *Авто* (по умолчанию) даёт видеокартам
|
||||
AMD новую модель, а NVIDIA, Intel и остальным — модель памяти 0.3 со всеми исправлениями;
|
||||
*Новая* и *Старая* — вручную. Без лаунчера — `BB_PC_MODEL=1/0` (не задано — как *Авто*).
|
||||
- Видеокарта работает с памятью игры на месте, как с буферами игры для ПК; то, что она читает
|
||||
часто, держится в VRAM — на AMD в sparse-буфере (арене), на остальных видеокартах модулем
|
||||
памяти прослойки без sparse-перепривязок (`BB_LAYER_MEMORY=1/0` — вручную). Работа командного
|
||||
процессора PS4 переводится, а не эмулируется: записи в память и метки делает видеочип в порядке
|
||||
потока, `COPY_DATA`, `COND_EXEC`, предикация, `MEM_SEMAPHORE` и часы видеочипа (100 МГц, как у
|
||||
PS4) — во времени видеочипа.
|
||||
- Occlusion-запросы игры как запросы Vulkan — по `BB_OCCLUSION_QUERIES=1`, по умолчанию выключены:
|
||||
с апскейлером сцена рисуется меньше и со сдвигом, и свечение лампы время от времени вспыхивало
|
||||
серым на весь кадр. Без них всё считается видимым, как в модели 0.3.
|
||||
- Модуль памяти прослойки (NVIDIA и другие): некоторые запуски больше не гоняют видеокарту в
|
||||
1,3–1,7 раза медленнее (копирования, разрывавшие проходы рендера до ~100 раз за кадр).
|
||||
- На NVIDIA новую модель можно выбрать вручную; у тестеров результат зависит от сцены (в части
|
||||
сцен медленнее старой модели), поэтому там она пока не по умолчанию. Логи приветствуются.
|
||||
|
||||
**Настройки — в меню самой игры**
|
||||
- В *Системе* — *Изображение* и *Эффекты*, настоящие страницы меню игры, и на титульном экране,
|
||||
и в паузе: разрешение вывода, пресет качества и резкость ползунками, счётчик FPS, детализация
|
||||
моделей и апскейлер; хроматическая аберрация, глубина резкости, размытие в движении, SSAO,
|
||||
динамические тени и SSR. Патчи игры и её собственное сглаживание — в меню оверлея и лаунчере.
|
||||
- Меню оверлея (Insert / L3+R3) снова выглядит как в 0.3.
|
||||
- Без экрана «Играть онлайн / офлайн»: PSN нет, игра сразу открывает главное меню офлайн.
|
||||
|
||||
**Ровнее**
|
||||
- Новые графические конвейеры проходов, которые рисуются каждый кадр, собираются в фоне: меньше
|
||||
рывков. Без своего конвейера в это время остаётся только геометрия (она появляется кадром
|
||||
позже); полноэкранные проходы (освещение, туман, постобработка) ждут свой — пропуск такого
|
||||
прохода давал целые серые кадры. Дисковые кэши шейдеров драйверов сохраняются между сессиями. В
|
||||
меню игры ползунки разрешения вывода и пресета применяются, когда простоят 0,4 с, а не на
|
||||
каждом шаге.
|
||||
- Карты с малым объёмом VRAM: FPS больше не падает со временем (сборщик текстур выгружал только что
|
||||
использованные текстуры и читал их снова через шину: 32 915 за ~7 минут в логе с NVIDIA); игра
|
||||
не останавливается, когда кончается VRAM для копий её памяти; после перемещений VRAM
|
||||
освобождается.
|
||||
|
||||
**Новое**
|
||||
- DLC: папки дополнений в `user/addcont/` сообщаются игре как установленные, сохранения с The Old
|
||||
Hunters загружаются (#33, PR #84).
|
||||
- DLSS на Linux для NVIDIA RTX при сборке с SDK DLSS от NVIDIA (PR #83; в AppImage не входит).
|
||||
- Лаунчер: выбор монитора (#69), *Копировать* и *Экспорт лога…* (PR #78), упрощённый китайский
|
||||
(PR #75).
|
||||
- Повреждённая распаковка игры называется прямо, а не приводит к зависанию на загрузке шейдеров
|
||||
(#81); `scripts/game_check.py ПАПКА_ИГРЫ` проверяет все файлы.
|
||||
- NVIDIA в AppImage: передаются файлы EGL-вендоров хоста (без них драйвер NVIDIA на некоторых
|
||||
системах не отдавал `vkCreateInstance`, #107; `BB_NVIDIA_EGL=0` — выключить); `--vulkan-info`
|
||||
печатает отчёт о драйвере вместе со сводкой.
|
||||
- AMD Polaris, Vega и их APU под RADV: `RADV_DEBUG=nohiz` ставится сам (чёрные ореолы и пропадающая
|
||||
геометрия, #54, #99; свой `RADV_DEBUG` или `BB_AUTO_NOHIZ=0` — не ставить).
|
||||
- Сборка FSR 4.1.1 находит и Proton, установленный в систему (PR #112). Встроенный Dear ImGui
|
||||
скрыт внутри библиотеки GPU: слои Vulkan со своей копией больше не роняют игру (PR #113).
|
||||
|
||||
**Исправления**
|
||||
- Сохранения не портятся при вылете во время сохранения (#64).
|
||||
- Сборка FSR 4.1.1: Proton запускается без контейнера Steam там, где тот не стартует (#61).
|
||||
- Картинка предмета на экране загрузки при выводе не в 1080p (#67).
|
||||
- Зависания видеочипа от мусорных размеров непрямых запусков (#34); окно для вылета при записи
|
||||
команд (такой же вылет был у сборки для Windows, #100).
|
||||
- Мелкие мипы тайловых текстур раскладываются так, как их хранит видеокарта (из shadPS4 #5196).
|
||||
- Китайская/японская/корейская локали Windows при подготовке игры (#55); `eboot.bin`, снятый как
|
||||
обычный ELF (#49); калибровка стиков у клонов Switch Pro (#70, PR #71); `BB_FSR4_STATS=1` (PR #80).
|
||||
|
||||
**Ограничения:** подробно проверена одна машина (RX 7800 XT, Mesa/RADV). Полосы в тёмных
|
||||
градиентах — исправление не подтверждено. Сборка для Windows — отдельный порт.
|
||||
|
||||
Игровых файлов в AppImage нет. Проприетарные модели FSR 4 и SDK/библиотеки DLSS не включены.
|
||||
К загрузке приложен `SHA256SUMS`.
|
||||
@@ -1,109 +0,0 @@
|
||||
**English** · [Русский](#русский)
|
||||
|
||||
bbport is the counterpart of Wine + DXVK for a single game: Bloodborne (v1.09) on Linux.
|
||||
Changes since [0.5](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5); details and
|
||||
measurements: [CHANGES_after_0.5.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_after_0.5.md) (Russian).
|
||||
|
||||
Questions about this project: Discord https://discord.gg/KYZRKk9CB — not the shadPS4 server.
|
||||
|
||||
### New
|
||||
|
||||
- **Online play (shadNet)**, as a separate module (`libbbnet.so`, loaded only when online play is
|
||||
on): messages, bloodstains and ghosts from The Hunter's Dream, bells and summons through a
|
||||
shadNet server (`srv.shadps4.net:31313` by default) with a shadNet account. Launcher → *Online*:
|
||||
servers, account, UPnP, connection test. The router port is opened by UPnP (#121). The same
|
||||
servers as the Windows port. The network and PSN libraries come from shadp2p (a shadPS4 fork,
|
||||
GPL-2.0) by way of the Linux and Windows forks of bbport.
|
||||
- **Mouse look**: click into the game to take the mouse, F1 releases it; mouse buttons and the
|
||||
wheel can be bound like keys (Mouse Left = R1, Mouse Right = L2, Middle = R3, side buttons
|
||||
R2/L1, wheel = quick items). Launcher → *Controls*: mouse look, sensitivity, invert Y (#51, #52).
|
||||
- **DLSS in the AppImage** (NVIDIA GeForce RTX): the bridge and NVIDIA's DLSS 310.9.1 library are
|
||||
included (NVIDIA's RTX SDK license allows it within an application; its text is in the package).
|
||||
A newer `libnvidia-ngx-dlss.so.*` can be chosen in the launcher (*Upscaler → DLSS: NVIDIA's
|
||||
library*), like the FSR 4.1.1 DLL.
|
||||
- **Every retail release** of Bloodborne with update 1.09 is accepted (US CUSA00900, EU, UK, JP,
|
||||
Asia and The Old Hunters editions; PR #117); an `eboot.bin` with a 60 fps patch baked in is
|
||||
reported.
|
||||
- **Quit the game** from the overlay (Insert / L3+R3; press twice) — #74, #116.
|
||||
|
||||
### Fixes
|
||||
|
||||
- The US release (and other regions) crashed with code 139 on entering the world in the new
|
||||
memory model: they ran without the game's profile (#119).
|
||||
- Motion blur smeared the whole frame while the camera turned in the new memory model (#122).
|
||||
- Cards with 4 GB (NVIDIA): "Failed to allocate 64 MiB for guest memory copies" stopped the game;
|
||||
now it frees textures sooner and uses other memory instead.
|
||||
- The main menu flickered with output resolutions above 1080p (live resolution).
|
||||
- Online, "Could not get information from the Bloodborne server" in the AppImage on NixOS (HTTPS
|
||||
certificates).
|
||||
- NVIDIA on Wayland: the swapchain is recreated through `oldSwapchain` (PR #93).
|
||||
- Building from git after updating from 0.4: "patch failed: src/ffx_vk_fsr4_v07.c" (#124).
|
||||
- `bb-launcher.sh --play` through nix-shell (PR #120); FSR 4.1.1 recording from the AppImage on
|
||||
SteamOS (PR #89).
|
||||
|
||||
### Smaller download
|
||||
|
||||
The AppImage is **399 MB instead of 925 MB**: only Mesa's Vulkan drivers (RADV without LLVM and
|
||||
ANV) instead of all of Mesa, no debug info in the game binary, MangoHud without its 32-bit half,
|
||||
the launcher's GTK without GStreamer.
|
||||
|
||||
**Known limits:** online login and summons were not tested by the author (no shadNet account);
|
||||
the UDP side of the public shadNet server did not answer in #121. Mouse look and DLSS need
|
||||
testing on more systems.
|
||||
|
||||
The AppImage contains no game files. AMD's FSR 4 model assets are not bundled. `SHA256SUMS`
|
||||
accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
bbport — аналог Wine + DXVK для одной игры: Bloodborne (v1.09) на Linux. Изменения после
|
||||
[0.5](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5); подробности и замеры —
|
||||
[CHANGES_after_0.5.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_after_0.5.md).
|
||||
|
||||
Вопросы по проекту: Discord https://discord.gg/KYZRKk9CB — не сервер shadPS4.
|
||||
|
||||
### Новое
|
||||
|
||||
- **Игра по сети (shadNet)** — отдельным модулем (`libbbnet.so`, загружается, только когда онлайн
|
||||
включён): сообщения, пятна крови и призраки — с The Hunter's Dream, колокола и призывы — через
|
||||
сервер shadNet (по умолчанию `srv.shadps4.net:31313`) с учётной записью shadNet. Лаунчер →
|
||||
*Онлайн*: серверы, учётная запись, UPnP, проверка соединения. Порт на роутере открывается по UPnP
|
||||
(#121). Серверы те же, что у Windows-версии. Сетевые и PSN-библиотеки — из shadp2p (форк shadPS4,
|
||||
GPL-2.0), через Linux- и Windows-форки bbport.
|
||||
- **Камера мышью**: щелчок в окне игры захватывает мышь, F1 — отпускает; кнопки мыши и колесо
|
||||
назначаются как клавиши (левая — R1, правая — L2, средняя — R3, боковые — R2/L1, колесо —
|
||||
быстрые предметы). Лаунчер → *Управление*: камера мышью, чувствительность, инверсия (#51, #52).
|
||||
- **DLSS в AppImage** (NVIDIA GeForce RTX): мост и библиотека DLSS 310.9.1 от NVIDIA в пакете
|
||||
(лицензия RTX SDK разрешает это в составе приложения; её текст — в пакете). Более новую
|
||||
`libnvidia-ngx-dlss.so.*` можно выбрать в лаунчере (*Апскейлер → DLSS: библиотека NVIDIA*), как DLL
|
||||
для FSR 4.1.1.
|
||||
- **Любое розничное издание** Bloodborne с обновлением 1.09 (США CUSA00900, Европа, Великобритания,
|
||||
Япония, Азия и издания с The Old Hunters; PR #117); о `eboot.bin` со вшитым патчем 60 FPS
|
||||
сообщается.
|
||||
- **Выход из игры** из оверлея (Insert / L3+R3; нажать дважды) — #74, #116.
|
||||
|
||||
### Исправления
|
||||
|
||||
- Американское и другие издания падали с кодом 139 при входе в мир в новой модели памяти: они шли
|
||||
без профиля игры (#119).
|
||||
- Размытие в движении размазывало весь кадр при повороте камеры в новой модели памяти (#122).
|
||||
- Карты на 4 ГБ (NVIDIA): «Failed to allocate 64 MiB for guest memory copies» останавливало игру;
|
||||
теперь текстуры освобождаются раньше, используется другая память.
|
||||
- Мерцание главного меню при выводе выше 1080p (живая смена разрешения).
|
||||
- Онлайн: «Не удалось получить информацию от сервера Bloodborne» в AppImage на NixOS (сертификаты
|
||||
HTTPS).
|
||||
- NVIDIA на Wayland: swapchain пересоздаётся через `oldSwapchain` (PR #93).
|
||||
- Сборка из git после обновления с 0.4: «patch failed: src/ffx_vk_fsr4_v07.c» (#124).
|
||||
- `bb-launcher.sh --play` через nix-shell (PR #120); запись FSR 4.1.1 из AppImage на SteamOS (PR #89).
|
||||
|
||||
### Меньше скачивать
|
||||
|
||||
AppImage — **399 МБ вместо 925 МБ**: из Mesa только Vulkan-драйверы (RADV без LLVM и ANV), без
|
||||
отладочной информации в бинарнике игры, MangoHud без 32-битной части, GTK лаунчера без GStreamer.
|
||||
|
||||
**Ограничения:** вход в shadNet и призыв автор не проверял (нет учётной записи); UDP-часть
|
||||
публичного сервера shadNet в #121 не отвечала. Камеру мышью и DLSS нужно проверить на большем числе
|
||||
систем.
|
||||
|
||||
Файлов игры в AppImage нет. Ассеты моделей FSR 4 от AMD не входят. К загрузке прилагается
|
||||
`SHA256SUMS`.
|
||||
@@ -1,160 +0,0 @@
|
||||
# Изменения 2026-10-02 (локально, не опубликовано)
|
||||
|
||||
Что сделано, как проверить, и что из этого подтверждено замерами. Все замеры — на RX 7800 XT /
|
||||
RADV; «слабый CPU» — та же машина, ограниченная `taskset` до 4 ядер / 8 потоков.
|
||||
|
||||
## Производительность и плавность
|
||||
|
||||
### Steam Deck и слабые GPU: путь из релиза 0.1 снова по умолчанию
|
||||
|
||||
Для вывода не 1080p (720p на Деке, 1440p, 2160p) вся игра снова рисуется в разрешении пресета
|
||||
(патч при запуске), как в 0.1. Живое масштабирование оставляло постобработку в 1080p и каждый
|
||||
кадр копировало цели сцены туда-обратно: на Деке и GTX 1060 это давало 7–8 FPS.
|
||||
|
||||
- Выбор пути — настройка «Смена разрешения на лету» (лаунчер, меню, `bbport.ini`
|
||||
`live_resolution=auto|0|1`, применяется после перезапуска). **Авто** (было по умолчанию; с 03.10 по умолчанию «выключена») включает
|
||||
живую смену на дискретных GPU с 8+ ГБ памяти, кроме NVIDIA старше Turing: на RX 7800 XT
|
||||
разрешение и пресеты снова меняются в меню на лету, а Steam Deck и GTX 1060 получают быстрый
|
||||
путь. Решение пишется в журнал (`GPU: … live resolution changes on/off`); `BB_LIVE_RES=0/1`
|
||||
перекрывает всё.
|
||||
- Смена пресета, разрешения вывода или переход на TAA в меню при этом пути — через кнопку
|
||||
«Применить и перезапустить игру».
|
||||
- Глубина/stencil уменьшенных целей на GPU без `VK_EXT_shader_stencil_export` (Pascal) снова
|
||||
в полном размере, без 9-проходного копирования.
|
||||
|
||||
### Рваный фреймтайм при упоре в GPU (NVIDIA)
|
||||
|
||||
Две причины, обе исправлены:
|
||||
|
||||
1. Показ кадра (`vkAcquireNextImage`, `vkQueuePresent`) выполнялся в потоке vblank игры. Когда
|
||||
GPU загружен, эти вызовы блокируются (на NVIDIA особенно), и vblank-события игры приходили
|
||||
неравномерно. Теперь показ — в отдельном потоке `bb:Present` (`BB_PRESENT_THREAD=0` — как
|
||||
раньше).
|
||||
2. Поток команд GPU убегал вперёд и упирался в случайные ресурсы, флипы шли пачками. Теперь он
|
||||
опережает GPU не больше чем на `BB_FRAMES_AHEAD` кадров (по умолчанию 1; 0 — без ограничения).
|
||||
|
||||
4K без апскейлера, ~78 FPS, медленный поворот камеры (`Frame pacing` в журнале, окна по 5 с):
|
||||
|
||||
| | FPS | разброс кадра | p99 | кадров > 1.5× медианы |
|
||||
|---|---|---|---|---|
|
||||
| было | 79.5 | 9.9 мс | 25.8 мс | ~125 |
|
||||
| отдельный поток показа | 79.4 | 5.9 мс | 25.0 мс | ~80 |
|
||||
| + опережение 1 кадр | 77.4 | **0.9 мс** | **14.6 мс** | **0** |
|
||||
|
||||
На слабом CPU (720p FSR 3 Performance, без лимита) опережение на FPS не влияет (166 FPS при 0, 1
|
||||
и 2), а отдельный поток показа дал +3.5% (160 → 166 FPS).
|
||||
|
||||
Новая строка `Frame pacing:` в `BB_FRAME_STATS=1`: медиана, разброс, p99 и число всплесков.
|
||||
|
||||
### Кэш шейдеров отключался навсегда (фризы при входе в каждую новую область)
|
||||
|
||||
Кэш пайплайнов при запуске сравнивает сохранённый `Shader::Profile` с текущим. В исходном
|
||||
shadPS4 при несовпадении кэш **закрывается на всю сессию** и ничего не записывает; файл профиля
|
||||
так и остаётся старым, поэтому дальше кэш не работает никогда, и каждая сессия компилирует все
|
||||
шейдеры заново. Сегодня профиль изменился (флаг для Pascal), и с ~12:16 кэш у всех запусков был
|
||||
выключен — включая забег у арены Людвига: в окне фриза 64 компиляции на 114 мс.
|
||||
|
||||
Теперь несовместимый кэш очищается и строится заново (одна пересборка, как при обновлении). В
|
||||
журнале: `Pipeline cache isn't compatible with current system: rebuilding it`. Проверка: забег
|
||||
по маршруту дважды подряд — 241 компиляция в первом, 28 во втором.
|
||||
|
||||
Тот же фриз на выходе с арены (115 мс, забег игрока): кроме компиляций — регистрация 1057
|
||||
текстур и загрузка 220 МБ изображений и 245 МБ буферов за один кадр (защита страниц 17 мс,
|
||||
резидентность 10.5 мс, создание изображений 7.9 мс, остальное — копирование). Эту часть ещё
|
||||
предстоит разнести по кадрам или потокам.
|
||||
|
||||
### CPU на слабых процессорах
|
||||
|
||||
Профиль (`perf`, 4 ядра / 8 потоков, 720p FSR 3 Performance, без лимита, ходьба): ~166 FPS,
|
||||
предел — второй этап конвейера (`bb:DrawRec`, занят 88–90%; первый этап ждёт его ~40%).
|
||||
Время второго этапа размазано по привязке ресурсов и записи draw, единственного горячего места
|
||||
нет. Для Деки и Haswell (ядра примерно вдвое медленнее) это порядка 85–100 FPS — выше 60, то
|
||||
есть средний FPS там упирается не в CPU. Главная проблема слабых CPU — фризы подгрузки (см.
|
||||
«Инструменты»).
|
||||
|
||||
- Поток записи Vulkan выбирал время активного ожидания по числу всех потоков машины, а не
|
||||
доступных процессу: под ограничением ядер он крутился 200 мкс вместо 20.
|
||||
- Ожидание второго этапа теперь тоже зависит от числа ядер (`BB_PIPE_SPIN_US`); на FPS не
|
||||
влияет (166.3–166.8 при 0, 50 и 200 мкс), оставлено короче для машин с 8 потоками.
|
||||
- Ридбэки (Relaxed, защищают лица от «взрыва» вершин): в той же сцене 166.4 FPS против 166.3 с
|
||||
`BB_READBACKS=0` — ничего не стоят, выключать их незачем.
|
||||
|
||||
## Стабильность
|
||||
|
||||
### Вылет `Guest fault … 0x263b8e7` с двухэтапным конвейером
|
||||
|
||||
`sceGnmSubmitDone` не блокировал игру, если поток команд уже **разобрал** все буферы
|
||||
(`num_submits == 0`), хотя второй этап конвейера и отложенные метки fence ещё не записали память.
|
||||
Игра освобождала объекты с метками, поздняя запись портила кучу. Добавлен признак
|
||||
`work_retired`: GPU считается свободным только после опустошения конвейера и записи меток.
|
||||
|
||||
Проверка: 40 минут на 4 ядрах / 8 потоках без падения. Вылет редкий (старая сборка на моей машине
|
||||
тоже прожила 40 минут), поэтому подтверждением будет только отсутствие вылета у вас.
|
||||
|
||||
## Изображение
|
||||
|
||||
- **Резкость до 2.0** (лаунчер, меню). До 1 — RCAS самого апскейлера, выше — ещё один проход
|
||||
RCAS по готовому кадру (FSR 3, FSR 4, FSR 4.1.1, в том числе вывод 720p на Деке; у TAA — свой).
|
||||
- **Ctrl+клик по ползунку в меню** теперь позволяет ввести число (раньше клавиатура только
|
||||
стирала: в меню не передавался текстовый ввод SDL).
|
||||
- **Мерцающие объекты на NVIDIA GTX 10xx (Pascal)** — гробы на мосту и т. п. (shadPS4 PR #1644
|
||||
чинит это только на Turing и новее через барицентрики). Для GPU NVIDIA без
|
||||
`VK_KHR_fragment_shader_barycentric` рекомпилятор теперь умножает интерполированное значение
|
||||
на 1 + 2⁻¹⁶ перед переводом в целое: 0.99999994 снова даёт 1. Нашёл этот приём в 27 шейдерах
|
||||
Bloodborne; на AMD с принудительным включением (`BB_INTERP_INT_FIX=1`) картинка та же.
|
||||
**Нужна проверка на GTX 10xx.** Кэш шейдеров один раз пересоберётся у всех (новая версия).
|
||||
- **Исправления рекомпилятора шейдеров из свежего shadPS4** (вышли после версии, от которой взят
|
||||
код порта): `V_CVT_PK_U8_F32`, `V_BFM_B32`, `V_ALIGNBIT/ALIGNBYTE_B32` (сдвиг на 32 при нулевом
|
||||
выравнивании давал неопределённый результат), перенос SCC в `S_ADDC/S_SUBB_U32`, 32-битные
|
||||
литералы в 64-битных числах, режим плавающей точки compute-шейдеров, селекторы P10/P20 у
|
||||
`V_INTERP_MOV_F32`. Список — в `gpu/VENDOR.txt`. Такие инструкции встречаются в упаковке
|
||||
глубины/теней; связь с багами теней (shadPS4 #4384, #3465) не подтверждена — проверьте те места.
|
||||
- **TAA.** Пробовал: смешивание в тонмапленном пространстве, обрезку истории в YCoCg, обрезку
|
||||
по дисперсии, восстановление текущего кадра фильтром 3×3. Сравнение в одном запуске с
|
||||
переключением на лету (неподвижная камера и поворот): ни одна техника не дала выигрыша сверх
|
||||
разброса между замерами, обрезка YCoCg и фильтр 3×3 — хуже. По умолчанию остался прежний
|
||||
алгоритм; техники доступны для экспериментов (`BB_TOGGLE_FILE`, биты 51–54). Нужен пример,
|
||||
где именно TAA нестабилен (место, что видно), чтобы чинить конкретно это.
|
||||
|
||||
## Моды, патчи, лаунчер
|
||||
|
||||
- **Моды проверены в игре**: мод, подменяющий тексты меню, работает (пути в верхнем регистре,
|
||||
папка-обёртка). Новое: регистр имён не важен, принимаются `Мод/CUSA03173/dvdroot_ps4`,
|
||||
папка-обёртка архива и моды без `dvdroot_ps4` (сразу `chr/`, `parts/`…). В журнале:
|
||||
`Mods: N game files replaced, M added`.
|
||||
- **Сторонние XML-патчи shadPS4**: папка `patches/` в каталоге данных, группа «Сторонние патчи» в
|
||||
лаунчере, выбор в `patches.json`. Подробности — [MODS.md](MODS.md).
|
||||
- **60/90 FPS** больше не падают с `patch overlaps a relocation`: патчи, заменяющие указатели,
|
||||
загрузчик переносит на реальную базу образа.
|
||||
- **Лаунчер на английском** (по умолчанию — язык системы; переключатель вверху страницы).
|
||||
- Подпись «Ограничение FPS»: 0 — по частоте экрана, не выше 120 Гц.
|
||||
|
||||
## Память
|
||||
|
||||
15 циклов «Выйти из игры → Продолжить» (перезагрузка уровня), 720p FSR 3:
|
||||
|
||||
| циклы | RSS процесса | VRAM |
|
||||
|---|---|---|
|
||||
| 0 → 7 | +179 МБ | +751 МБ |
|
||||
| 11 → 15 | +8 МБ | +15 МБ |
|
||||
|
||||
Рост затухает: при повторных загрузках это прогрев кэшей (пайплайны, текстуры), а не
|
||||
неограниченная утечка. `heaptrack` на 8 таких циклах тоже не нашёл источников, растущих от
|
||||
загрузки к загрузке (крупные — один раз при запуске: таблица страниц 279 МБ, пул текстурного
|
||||
кэша 92 МБ). Кэш текстур по замыслу освобождает память только у бюджета VRAM. 40-минутный
|
||||
прогон в одной локации: RSS ~2.64 ГБ, VRAM ~4.29 ГБ всё время, без роста. Не проверено:
|
||||
долгий проход через много разных локаций (рост кэша до бюджета там ожидаем) — для этого нужен
|
||||
записанный маршрут.
|
||||
|
||||
## Инструменты для тестов
|
||||
|
||||
- **Запись маршрута**: `BB_PAD_RECORD=/путь/route.txt` — F9 в игре начинает и останавливает запись
|
||||
геймпада/клавиатуры. `BB_PAD_REPLAY=/путь/route.txt` + слово `replay` в `BB_PAD_FILE` —
|
||||
воспроизведение с того же момента. Так можно один раз пробежать маршрут (от лампы у Людвига к
|
||||
другой лампе), а потом повторять его в замерах фризов. Воспроизведение проверено; запуск записи
|
||||
клавишей F9 автоматически проверить нельзя — проверьте, пожалуйста.
|
||||
|
||||
Как записать: отдохнуть у лампы у Людвига, выйти в меню и «Продолжить» (персонаж появится у
|
||||
лампы), в лаунчере в «Доп. переменные» указать `BB_PAD_RECORD=/home/<вы>/route.txt`, после
|
||||
появления нажать F9, пробежать до другой лампы, F9. Сохранение тоже понадобится (папка
|
||||
`user/savedata`), чтобы старт был в той же точке.
|
||||
@@ -1,107 +0,0 @@
|
||||
# Изменения 2026-10-03 (локально, не опубликовано)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, ограничение `taskset` до 4 ядер / 8 потоков, настройки Steam Deck
|
||||
(вывод 1280×720, FSR 3 Performance, сцена 640×360), без лимита FPS. Стенд:
|
||||
`out/bb-probe` из исходников, сохранение `docs/savedata_route`, скриптовый вход в уровень.
|
||||
|
||||
## Смена разрешения на лету — по умолчанию выключена
|
||||
|
||||
`live_resolution` без значения (новая установка, лаунчер, меню) теперь означает «выключена»:
|
||||
для вывода не 1080p — патч разрешения при старте, как в 0.1. `auto` (проверка видеокарты) и `1`
|
||||
выбираются явно. Тесты `test_run_settings`.
|
||||
|
||||
## AppImage без моделей FSR 4.1.1
|
||||
|
||||
Модели извлекаются из DLL AMD и в публичный пакет не входят (как в 0.1): `packaging/default.nix`
|
||||
кладёт `fsr4_411` только с `BB_PACKAGE_FSR411=1`; иначе их ищут в каталоге данных.
|
||||
|
||||
## Steam Deck: запуск и сборщик текстур
|
||||
|
||||
- **AppImage не запускала игру на NixOS** (код 127 без сообщения): `run.sh` читал
|
||||
`live_resolution` через `sed`, которого нет в окружении AppImage. Теперь только встроенные
|
||||
команды bash; тест `test_live_resolution_without_sed_or_grep`.
|
||||
- **Сборщик текстур на встроенных GPU** (`TextureCache::GarbageCollectImages`). Бюджет считался
|
||||
как «все кучи минус 8 ГБ для системы» (на Деке ~1 ГБ), а занятость — по кучам GTT, где лежат и
|
||||
буферы с памятью игры (~4 ГБ). Сборщик постоянно работал в «агрессивном» режиме: на каждой
|
||||
отправке команд (~66 за кадр) выкидывал до 40 текстур, не использованных 2–3 кадра, и снова
|
||||
их загружал, а изменённые GPU асинхронно записывал обратно в память игры. Теперь на встроенных
|
||||
GPU пороги — доля текущего бюджета драйвера (`VK_EXT_memory_budget`): сбор с 70%, нехватка с
|
||||
85%, критично с 95%. В журнале при нехватке: `Texture cache: memory pressure, …`.
|
||||
`BB_GC_BUDGET_MB=N` — то же правило с заданным бюджетом на любом GPU (проверка на ПК).
|
||||
- **Запись вытесненной текстуры обратно в память игры теперь синхронная**, пока страницы ещё
|
||||
защищены. Отложенная запись приходила после снятия защиты — поверх того, что игра успела туда
|
||||
записать (например, кучу после выгрузки локации). Возможная причина `Guest fault 0x263b8e7` на
|
||||
Деке при подгрузке карт; **на ПК этот вылет тоже был один раз за ~10 прогонов, его причина не
|
||||
найдена** (порог нехватки памяти на ПК ~11 ГБ, сборщик туда не попадает).
|
||||
|
||||
## Живая смена разрешения дешевле
|
||||
|
||||
Раньше уменьшались только цели 1920×1080; постобработка игры в полуразрешении (960×540, в том
|
||||
числе цепочка bloom с mip-уровнями) шла в полном размере и каждый кадр пересэмплировала цвет
|
||||
сцены. На Деке один такой проход стоил ~8.4 мс (против ~1.4 мс при патче разрешения).
|
||||
|
||||
- Цели 960×540 уменьшаются тем же коэффициентом, что сцена (ProxySize). Пересчёт FragCoord в
|
||||
шейдерах не меняется: 960/320 = 1920/640.
|
||||
- Mip-цепочки полуразрешения: отдельная уменьшенная копия на каждый уровень (ключ — изображение и
|
||||
уровень); выборка одного уровня читает копию, выборка всей цепочки — родное изображение после
|
||||
пересэмплирования.
|
||||
- Переключатель `BB_TOGGLE_FILE` бит 50 (`SceneHalfRes`) — прежнее поведение.
|
||||
- `BB_SCENE_DEBUG`: ещё печатает пересэмплирования (`Scene resolve/fill`) и чтения уменьшенных
|
||||
целей в полном размере с причиной (`Scene native read`).
|
||||
- Тест: `scene-resolution-test` — копия mip-уровня 2 цепочки 960×540 и её возврат в родной уровень.
|
||||
|
||||
Результат на ПК (4c/8t): занятость GPU живого пути 3.41 → 3.23 мс на кадр, проход `5a619b89` —
|
||||
320×180 вместо 960×540. Картинка совпадает с режимом патча (bloom и туман считаются в
|
||||
пониженном разрешении, как при патче). Осталось: ~9 пересэмплирований и ~7 обратных заливок
|
||||
цвета сцены 1080p за кадр (копирования, `9a9cf8a9`, композиция UI).
|
||||
|
||||
**Почему живой путь всё равно дороже на CPU:** игра считает экран 1080p и рисует в проходе
|
||||
освещения (`vs 8747a367`) 335 источников света вместо 42 при патче (мелкие источники
|
||||
отбрасываются по размеру на экране): 975 draw-вызовов на кадр против 679. Это решение самой игры.
|
||||
|
||||
## TAA: мерцание тонких объектов вдали
|
||||
|
||||
Прутья окон, перила и края обломков мерцали: в фазе jitter, где тонкий объект не попадает в
|
||||
центр пикселя, проверка глубины отбрасывала историю (в ней объект ближе текущего фона). Теперь
|
||||
при движении меньше пикселя такая история остаётся и ограничивается цветом соседей (как в TAA без
|
||||
отбрасывания по глубине). Переключатель бит 55 — прежнее правило.
|
||||
|
||||
Метрика: 30 кадров подряд при неподвижной камере (`BB_PRESENT_DUMP_COUNT=30`), средняя вторая
|
||||
разность яркости по времени (мерцание кадр-к-кадру, плавная анимация почти не влияет), ×1e-3:
|
||||
|
||||
| | среднее | p99.9 |
|
||||
|---|---|---|
|
||||
| было (2 прогона) | 0.367 / 0.381 | 23.2 / 22.0 |
|
||||
| стало (2 прогона) | 0.266 / 0.293 | 12.2 / 13.1 |
|
||||
| без TAA и jitter | 0.428 | 16.8 |
|
||||
|
||||
Мягкое ограничение по цвету для неподвижных пикселей и обрезка по дисперсии мерцание не
|
||||
уменьшили (дисперсия — увеличила). Тесты `taa-shader-test`: добавлены «thin line missed by this
|
||||
frame keeps history», «static disocclusion is clipped», «moving disocclusion is rejected».
|
||||
|
||||
## Апскейлеры: сверка с аудитом от 2026-10-02
|
||||
|
||||
Аудит смотрел версию с GitHub (`cc001f9`); по локальному коду:
|
||||
|
||||
- Native AA вне 1080p (2.1) — уже исправлено (`w > ow`).
|
||||
- FSR 4 v07 и HDR-флаг (2.2): провайдер сохраняет флаги контекста, но не читает их — флаг на
|
||||
результат не влияет. На пути с патчем вход действительно LDR (после тонмаппинга) — ограничение
|
||||
самого пути.
|
||||
- Небо (4.1) и порог глубины объектных векторов (4.4) — уже исправлены (поворот без переноса,
|
||||
относительный порог, полная точность глубины).
|
||||
- **Mip bias (4.7) — сделано:** в проходах G-buffer (5+ целей) при уменьшенной сцене смещение
|
||||
LOD сэмплеров log2(рендер / вывод) (на Деке −1); тени, постобработка и UI без изменений. В
|
||||
журнале `Upscaler: scene texture LOD bias -1.00`. Переключатель бит 57.
|
||||
- Не сделано: классификация частиц для jitter (4.2), сброс истории на смену камеры (4.8).
|
||||
|
||||
## CPU на слабых процессорах: замер
|
||||
|
||||
Режим патча, 4c/8t, 209 FPS. Потоки (% одного ядра, пользователь + ядро): `bb:DrawRec` 72 + 14,
|
||||
поток команд GPU 56 + 20, `bb:VkRecorder` 72 + 1 (в основном время RADV), основной поток игры
|
||||
46 + 10, четыре потока подготовки draw по ~20. Всего занято ~545% из 800%: предел —
|
||||
последовательная цепочка «поток команд → запись draw → запись Vulkan». Дальнейший рост требует
|
||||
распараллелить запись Vulkan (несколько командных буферов) — крупная переделка, не начата.
|
||||
|
||||
## Инструменты
|
||||
|
||||
- `BB_PRESENT_DUMP_COUNT=N` — `BB_PRESENT_DUMP_TRIGGER` снимает N кадров подряд.
|
||||
@@ -1,145 +0,0 @@
|
||||
# Изменения 2026-10-04 — 2026-10-06 (версия 0.3)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, Mesa 26, вывод 1080p без лимита FPS, если не сказано иначе.
|
||||
|
||||
## Модель памяти ПК — экспериментальная, по умолчанию выключена
|
||||
|
||||
Включается переключателем лаунчера «Новая модель памяти (экспериментально)» или
|
||||
`BB_GUEST_IN_PLACE=1`; по умолчанию работает модель 0.2. Проверена только на RX 7800 XT и Steam
|
||||
Deck, может вылетать; на NVIDIA dma-buf не отображается со смещением, и порт возвращается к
|
||||
старой модели. Прямая память игры выделяется через Vulkan
|
||||
(dma-buf, видимая GPU системная память), и GPU читает её на месте, как буферы игры для ПК. Копий
|
||||
всей памяти в VRAM и отслеживания записей через защиту страниц больше нет. Метки готовности
|
||||
(EOP/EOS) пишутся, когда GPU действительно выполнил работу.
|
||||
|
||||
- Кадры перекрываются: первые две отправки команд кадра (`BB_SUBMIT_AHEAD=2`) не ждут конца
|
||||
предыдущего кадра. При K ≥ 8 куча игры течёт (~2 объекта за кадр), поэтому есть сторож: рост
|
||||
живых выделений на 100 000 выключает перекрытие до конца сессии.
|
||||
- Отправка команд асинхронная (`BB_ASYNC_SUBMIT`): command buffer'ы сегментов закрывают их потоки
|
||||
записи, сама отправка — упорядоченная задача на них же.
|
||||
- Сигналы GPU копятся на стадии записи draw и публикуются пачкой; ожидания ConstRam — точечные.
|
||||
- Замеры, Yahar'gul, стоя: старая модель 138,7 → 143,6 FPS, новая 101 → 152–155 FPS. Тяжёлое
|
||||
место (~2000 draw за кадр): 95,5 → 115–124 FPS; там теперь упор в стадию записи draw.
|
||||
|
||||
Ключи настроек `pc_memory` и `old_memory_model` больше не читаются: новый ключ `new_memory_model`
|
||||
по умолчанию выключен у всех.
|
||||
|
||||
## Видеопамять больше не растёт от локации к локации
|
||||
|
||||
До исправлений VRAM за 13 минут игры вырос с 3,8 до 7,3 ГБ, теперь держится на 4–5,5 ГБ.
|
||||
|
||||
- **Сборщик текстур не освобождал ничего.** Десять самых старых текстур, в которые рисовал
|
||||
GPU, расходовали весь лимит прохода. Теперь такие текстуры пропускаются и уходят в конец
|
||||
очереди; без нехватки памяти выгружаются текстуры, не использованные `BB_GC_IDLE_SECONDS`
|
||||
(20) секунд. Возраст считается в секундах: прежние 16 отправок команд (~3 кадра) выкидывали
|
||||
всё, что ненадолго ушло с экрана.
|
||||
- **Блоки памяти игры в VRAM никогда не освобождались.** Теперь блоки, не использованные
|
||||
`BB_VRAM_IDLE_SECONDS` (60) секунд, возвращаются в память игры и снова переносятся в VRAM при
|
||||
обращении (без этого просадка 90 → 55 FPS: GPU читал их по PCIe). Блоки памяти, которую игра
|
||||
снимает с отображения, освобождаются. Свободные слоты используются и многоблочными запросами;
|
||||
новые блоки занимают самые заполненные куски по 64 МБ, почти пустые выселяются, пустые
|
||||
возвращаются драйверу.
|
||||
- VMA берёт память блоками по 64 МБ вместо 256 (`BB_VMA_BLOCK_MB`), текстуры от 4 МБ получают
|
||||
отдельное выделение (`BB_VMA_DEDICATED_MB`): удалённые текстуры не держат большие блоки.
|
||||
- Статистика кадров: строка `Memory:` — VRAM и GTT игры по данным ядра, RSS, блоки VMA, все
|
||||
изображения Vulkan, кэш текстур, блоки памяти игры в VRAM, работа сборщика.
|
||||
`tools/load_watch.sh` пишет VRAM/GTT/RSS каждые 5 с.
|
||||
|
||||
## Зависания GPU: найдено место
|
||||
|
||||
Метки GPU (`vk_breadcrumbs.*`, `VK_AMD_buffer_marker`; `BB_BREADCRUMBS=0` — выключить): до и
|
||||
после каждой отрисовки, dispatch и прохода порта GPU пишет номер команды. При потере устройства
|
||||
журнал называет команду, на которой GPU встал, её шейдеры и параметры.
|
||||
|
||||
Найдено (сессия ~20 минут): зависание в непрямом dispatch шейдера частиц `cs 2da7fe60` сразу после
|
||||
`cs 4ca76892` (1×1×1, генератор случайных чисел эмиттеров, счётчик в GDS). Число итераций циклов
|
||||
`2da7fe60` берётся из памяти. Аргументы непрямых dispatch теперь копируются GPU в буфер, видимый
|
||||
процессору: отчёт показывает, что GPU прочитал, а статистика — самые большие непрямые dispatch.
|
||||
**Причина ещё не устранена**: нужен следующий случай с этими данными.
|
||||
|
||||
Зависание больше не выглядит как тихая заморозка картинки при играющем звуке (Steam Deck, бой с
|
||||
Германом): ожидания GPU замечают потерю устройства, а сторож через 10 с без прогресса GPU пишет
|
||||
в журнал отчёт меток и останавливает игру с понятной ошибкой.
|
||||
|
||||
## Падения и запуск
|
||||
|
||||
- Гостевой сбой `0x263b8e7` (частый на Steam Deck): метки EOP писались через `memcpy`, который
|
||||
записывает 4–7 байт двумя перекрывающимися словами. Игра успевала освободить блок между ними.
|
||||
Теперь метки пишутся одной атомарной записью (`store_once`, `StoreLabel`). На Deck не проверено.
|
||||
- NVIDIA с новой моделью памяти падала при запуске: драйвер не отображает dma-buf со смещением.
|
||||
Перед включением модели порт проверяет это и при неудаче остаётся на старой модели.
|
||||
- Проверка файлов игры (`scripts/game_check.py`, issue #7, #13, #14): хеш загруженного образа
|
||||
eboot сравнивается с CUSA03173 1.09; без обновления, со старым eboot или другим изданием запуск
|
||||
останавливается с объяснением, лаунчер показывает то же. `BB_SKIP_GAME_CHECK=1` — обход.
|
||||
|
||||
## Лаунчер и ввод
|
||||
|
||||
- Выпадающие списки с длинными вариантами («Фоновая загрузка в видеопамять», конвейер GPU,
|
||||
чтение данных GPU, контроллер) показывают короткие названия, а пояснение к выбранному —
|
||||
строкой под заголовком: текст больше не обрезается.
|
||||
|
||||
- Выбор контроллера (issue #15): *Управление → Контроллер* (`BB_GAMEPAD`, GUID или часть
|
||||
имени); выбранный берётся, как только подключится. Список — `bb-gpu-capabilities --gamepads`.
|
||||
- Ввод имени персонажа в окне поверх игры (issue #19, патч из issue); ввод в игру на это время
|
||||
нейтрален (Backspace не нажимает тачпад).
|
||||
- Курсор скрыт в полноэкранном режиме и через 3 с без движения мыши в окне (issue #3).
|
||||
- **Переназначение клавиатуры и геймпада**: *Управление → Клавиатура / Геймпад*, кнопка
|
||||
«Назначить» ждёт нажатия. В `bbport.ini` — строки `key.<кнопка>=` и `pad.<кнопка>=` с именами
|
||||
SDL; нет строки — назначение по умолчанию.
|
||||
|
||||
## Патчи игры
|
||||
|
||||
- **Sprint Fix (High FPS)**: выше ~147 FPS (а иногда и выше ~90) бег
|
||||
падал до половины скорости — детектор стены игры мерил расстояние за кадр. Патч включается
|
||||
всегда вместе с режимами 90 FPS и «без ограничения».
|
||||
- **Чёрный тонмаппинг на процессорах Intel** (темнее, чем на PS4; сильнее всего в DLC): код игры
|
||||
выполняется нативно, а приближённые float-инструкции Intel дают другие значения. Патч
|
||||
исправления включается сам на процессорах Intel; `BB_INTEL_TONEMAP_FIX=0/1` — принудительно.
|
||||
|
||||
## Сборка ассетов FSR 4.1.1
|
||||
|
||||
- Новейший GE-Proton выбирается по версии (`ls | tail -1` брал 9-x вместо 10-x; issue #4), также
|
||||
`~/.steam/root` и Flatpak; захват без кадров останавливает сборку с объяснением.
|
||||
- На RDNA4 DLL, вероятно, выбирает FP8-вариант шейдеров (issue #12): захват скрывает от
|
||||
vkd3d-proton `VK_KHR_cooperative_matrix`, `VK_NV_cooperative_matrix2`, `VK_EXT_shader_float8`. На
|
||||
RX 7800 XT захват совпадает побайтно; на RDNA4 не проверено. `extract.py` при нарушении правил
|
||||
ничего не пишет и объясняет причину.
|
||||
- Инструменты из системы без Nix; Nix без канала — `channel:nixos-unstable` (issue #22).
|
||||
|
||||
## Апскейлеры: мыло и полосы на лестницах
|
||||
|
||||
- **Векторы камеры: перевёрнутая вертикаль.** `camera_motion.comp` и проверка глубины в
|
||||
`taa.comp` считали, что ось +y вида направлена вверх по экрану; у игры она вниз (по строкам).
|
||||
Ошибка затрагивала наклон камеры и вертикальное движение, поворот влево-вправо был верным.
|
||||
Поэтому видно было там, где камера покачивается вверх-вниз: на лестнице к собору от гробницы
|
||||
Идон и в канализации. FSR 4 и 4.1.1 давали полосатое мыло, FSR 3.1 — общее размытие, а персонаж
|
||||
с векторами объектов оставался чётким. Проверка: на статичных мешах, у которых есть и векторы
|
||||
объектов (позиции вершин с GPU), новые векторы камеры совпадают с ними в пределах 0,05 px на
|
||||
17 кадрах бега по лестнице, прежние расходились на 1,3–3,7 px. В игре на FSR 4.1.1 1080p
|
||||
Performance «стало сильно лучше».
|
||||
- **Анизотропная фильтрация 16x** для текстур отрисовок сцены (`BB_ANISO`, по умолчанию 16;
|
||||
0 — фильтры игры). Игра ставит большинству текстур 1x, а при апскейле поверхности под острым
|
||||
углом теряют детали. Переключатель для A/B на лету — бит 61 в `BB_TOGGLE_FILE`.
|
||||
- Дамп апскейлера (`BB_DUMP_TRIGGER`) работает и на HDR-пути, пишет матрицы камеры кадра;
|
||||
`tools/dump_view.py` переводит дамп в PNG.
|
||||
|
||||
## MangoHud: двойной оверлей
|
||||
|
||||
Текст MangoHud двоился со сдвигом и разными значениями: рисовались два оверлея. Встроенный в
|
||||
AppImage слой называется `VK_LAYER_MANGOHUD_overlay_64_x86_64` (nixpkgs), системный — иначе,
|
||||
и загрузчик Vulkan грузил оба. На Steam Deck в игровом режиме то же дают оверлей
|
||||
производительности Steam (mangoapp) и MangoHud из лаунчера. Теперь `run.sh` при `MANGOHUD=1`
|
||||
не включает MangoHud в игре, если запущен mangoapp, и отключает встроенный слой, если в системе
|
||||
есть свой (`VK_LOADER_LAYERS_DISABLE`). На NixOS без системного MangoHud не воспроизводилось.
|
||||
|
||||
## Окклюжн куллинг (ветка rainmakerv3/shadPS4 `opts`)
|
||||
|
||||
Коммит «correct queries» заменяет фальшивый счётчик ZPASS_DONE настоящими occlusion queries
|
||||
Vulkan. В Соборном округе Bloodborne делает ~0–8 таких запросов за 5 с и не использует
|
||||
предикацию (`SET_PREDICATION` — 0), то есть отрисовки по ним не отсекает: переносить нечего.
|
||||
Сетевой поток в офлайне (коммит про epoll в той же ветке) у нас ядро не занимает.
|
||||
|
||||
## Тесты
|
||||
|
||||
`tests/test_game_check.py`; `build.sh --test` снова собирается (заглушка
|
||||
`runtime_memory_note_write` в `tests/test_file_mods.c`).
|
||||
@@ -1,434 +0,0 @@
|
||||
# Изменения после 0.4 (с 2026-10-08)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, Кошмар охотника у лампы (сохранение пользователя), 1080p без лимита
|
||||
FPS, новая модель (`BB_PC_MODEL=1`); «4 ядра» — `CPUS=0-3,8-11` (4 ядра / 8 потоков).
|
||||
Подробности и что осталось — [EMULATION_REMOVAL_PLAN.ru.md](EMULATION_REMOVAL_PLAN.ru.md),
|
||||
«Шаг 3».
|
||||
|
||||
## Метки EOP/EOS пишет видеочип (новая модель)
|
||||
|
||||
- `EVENT_WRITE_EOP` без прерывания, `EVENT_WRITE_EOS` и `RELEASE_MEM` очередей compute:
|
||||
буферный маркер Vulkan (`VK_AMD_buffer_marker`, конец конвейера) прямо в память игры, в той же
|
||||
точке потока, где их пишет командный процессор PS4. Раньше каждую метку писал поток процессора
|
||||
после завершения всей отправки Vulkan — ~330 меток за кадр, 61 тыс. в секунду.
|
||||
- 64-битная метка — две 32-битные записи, старшая половина первой (кто видит новую младшую,
|
||||
видит и новую старшую).
|
||||
- Метки с прерыванием, а также без `VK_AMD_buffer_marker` (NVIDIA) и с `BB_FREE_CHECK=1` — как
|
||||
раньше. `BB_GPU_LABELS=0` — как раньше везде.
|
||||
- EOS с записью из GDS: копия видеочипом в порядке потока вместо ожидания всего видеочипа
|
||||
(`Finish()`) и записи процессором.
|
||||
- Статистика: строка `EOP fences` показывает, сколько меток за кадр записал видеочип.
|
||||
- FPS тот же (16 потоков: 233; 4 ядра: ~217 против ~215), картинка та же; 8 минут ходьбы — см.
|
||||
ниже.
|
||||
|
||||
## Перехваты кода игры без ловушек
|
||||
|
||||
- Точки, где игра выделяет память GPU (аллокатор диапазонов, три места кучи) и где загрузчики
|
||||
копируют ресурсы (`memcpy`), раньше были `int3`: каждый вызов — сигнал ядра (SIGTRAP), ~1200
|
||||
в секунду только у аллокатора. Теперь переход в заглушку рядом с образом игры: заглушка
|
||||
вызывает обработчик и выполняет перекрытые инструкции; вызов `memcpy` идёт через трамплин.
|
||||
- Байты переписываются поверх уже стоящего `int3` (поток, наткнувшийся на него в этот момент,
|
||||
уходит в заглушку). `BB_GUEST_HOOK_TRAPS=1` — ловушки, как раньше. Диагностические точки
|
||||
(`BB_FRAME_STATS`, `BB_HEAP_SITES`) остались ловушками.
|
||||
|
||||
## Новая модель памяти на NVIDIA
|
||||
|
||||
- Новый способ отдать память игры видеочипу — импорт памяти хоста
|
||||
(`VK_EXT_external_memory_host`): кусок прямой памяти остаётся в memfd рантайма, видеочип
|
||||
импортирует его второе отображение. Процессор работает с обычной кэшируемой памятью, видеочип —
|
||||
через шину, как с upload heap игры для ПК. Освобождённая память в таких кусках обнуляется, а не
|
||||
выбрасывается из memfd (видеочип держит эти страницы).
|
||||
- При запуске самопроверка в точности как в работе: импорт куска memfd, привязка к sparse-буферу,
|
||||
запись видеочипом — чтение процессором через другое отображение, запись процессором — копия
|
||||
видеочипом; sparse-буфер для такой памяти должен принимать и VRAM. Не прошла — dma-buf (на
|
||||
NVIDIA — «кусок на выделение»), для которого теперь меряется скорость чтения процессором через
|
||||
отображение драйвера (меньше 1 ГБ/с — не используется: код игры работает на этой памяти). Не
|
||||
прошло и это — старая модель. В журнале: `Guest memory: host memory imported by the GPU`,
|
||||
`host memory import does not work here: …`, `CPU reads through the driver's dma-buf: N MB/s`.
|
||||
- Порядок: на AMD — dma-buf (как раньше; RADV не импортирует memfd —
|
||||
`ErrorInvalidExternalHandle`, проверено), на остальных — сначала импорт. `BB_GUEST_MEMORY=host`
|
||||
или `dmabuf` выбирает сам.
|
||||
- Арена (sparse-буфер памяти игры) создаётся с тем типом внешней памяти, который выбран; если
|
||||
драйвер с ним не пускает в арену VRAM — старая модель с сообщением в журнале.
|
||||
- Лаунчер: переключатель *Новая модель памяти и трансляции* доступен на AMD и NVIDIA (тексты на
|
||||
трёх языках). Без лаунчера — `BB_PC_MODEL=1`.
|
||||
- **На NVIDIA не проверено** (у автора только AMD): нужен журнал с NVIDIA.
|
||||
|
||||
## Диагностика
|
||||
|
||||
- `BB_DCB_STATS=1`: кроме структуры буферов команд — пакеты PM4 в секунду по типам, события
|
||||
`EVENT_WRITE` по типам, EOP по источнику данных и прерыванию, EOS по команде, `WAIT_REG_MEM` по
|
||||
месту и движку, и чем закончились ожидания (метка VO, метка в порядке потока, выполнено сразу,
|
||||
ждали — и сколько мс в секунду).
|
||||
|
||||
## Шейдер копирования игры: записи остаются в памяти игры; трансляция вместо подмены — по выбору
|
||||
|
||||
- Вычислительный шейдер копирования игры (`cs fefebf9f`, 2–50 запусков за кадр) выполнялся
|
||||
подменой: процессор при записи команд читал список копий из памяти игры и делал копии своим
|
||||
шейдером. Теперь его можно транслировать как есть (`BB_COPY_SHADER_NATIVE=1`, только новая
|
||||
модель): список читает видеочип во время выполнения, как на PS4.
|
||||
- Найдено: рядом с тем, куда он копирует, лежат код шейдеров и таблицы описателей, которые наш
|
||||
транслятор читает процессором. Пока эти блоки в памяти игры, всё верно; копия в VRAM (правило
|
||||
«видеочип пишет это 3 кадра из 60») давала транслятору устаревший код — чёрная сцена. Подмена
|
||||
держала их в памяти игры случайно (её диапазоны каждый раз разные). Теперь записи этого шейдера
|
||||
в обоих режимах остаются в памяти игры (`force_writes_in_place`).
|
||||
- По умолчанию — подмена: родной шейдер на 4 ядрах стоит потоку записи отрисовок ~0,3 мкс на
|
||||
пакет (+10 %), видеочипу +2 %. Бит 2 второго числа `BB_TOGGLE_FILE` переключает на ходу.
|
||||
|
||||
## Константы на месте (по выбору) и привязка буферов
|
||||
|
||||
- `BB_CONSTANTS_IN_PLACE=1` (новая модель): маленькие буферы только для чтения (константы)
|
||||
видеочип читает прямо из памяти игры, а не из копии, сделанной при разборе (кольцо констант) или
|
||||
при записи (стрим-буфер). Копии защищали отрисовку от записей командного процессора, которые
|
||||
процессор выполнял «наперёд»; в новой модели эти записи выполняет видеочип в порядке потока,
|
||||
так что копия больше не нужна для правильности. Замер (4 ядра, у лампы): разбор −0,8 мкс на
|
||||
отрисовку, видеочип +3 % (константы идут по PCIe), FPS тот же — поэтому выключено по умолчанию.
|
||||
Бит 1 второго числа `BB_TOGGLE_FILE` переключает на ходу.
|
||||
- Привязка буфера только для чтения, уже лежащего в памяти игры, запоминается до изменения
|
||||
резидентности (без обхода блоков на каждую привязку); проверки «на месте ли буфер» для
|
||||
диагностики `BB_TRACE_SHADER` больше не выполняются, когда она выключена (были две на привязку).
|
||||
|
||||
## Исправление
|
||||
|
||||
- Реестр обработчиков ошибок доступа к памяти терял обработчик с тем же приоритетом, что у уже
|
||||
зарегистрированного (множество упорядочивалось только по приоритету). Порядок теперь явный:
|
||||
диагностическая ловушка VRAM, менеджер страниц, SRT walker.
|
||||
|
||||
## Диагностика (новая)
|
||||
|
||||
- `BB_RES_STATS=1`: откуда шейдеры берут описатели ресурсов — из регистров или из таблиц в памяти
|
||||
(SRT walker), по видам, на привязку.
|
||||
- `BB_VRAM_ACCESS_TRAP=1` (+ `BB_VRAM_TRAP_RANGE=адрес,размер`): блоки памяти игры, перенесённые
|
||||
в VRAM, закрываются от процессора; печатаются места кода, которые к ним обращаются (кто читает
|
||||
устаревшую копию).
|
||||
- `BB_WATCH=адрес,размер`: что транслятор читает из диапазона (указатели в пользовательских
|
||||
данных, буферы, код шейдеров). `BB_TRACE_ADDR=адрес,размер` (с `BB_TRACE_SIZE`): привязки над
|
||||
диапазоном с самого начала. `BB_TILE_BACK_LOG=1`: образы, записанные обратно в память, потому
|
||||
что их читают как текстурный буфер. `BB_COPY_SHADER_TRACE=1`: буферы шейдера копирования.
|
||||
|
||||
## Occlusion-запросы игры — настоящие запросы Vulkan (новая модель)
|
||||
|
||||
- Bloodborne делает около 60 occlusion-запросов за кадр (`ZPASS_DONE`); раньше они получали
|
||||
фиктивную последовательность «всё видно». Теперь сэмплы считают запросы Vulkan, а результаты
|
||||
записывает видеочип в память игры в формате PS4 (счётчики блоков глубины с битом готовности).
|
||||
Проходы рендера не разрываются: запрос режется на границах проходов и буферов команд, запись
|
||||
результатов откладывается до ближайшей точки вне прохода и делается одним запуском шейдера. На
|
||||
4 ядрах разница с фиктивным счётчиком — в пределах шума. `BB_OCCLUSION_QUERIES=0` — как раньше.
|
||||
- `COPY_DATA` (константы, регистры, часы, память, GDS) — записи и копии видеочипа в порядке потока;
|
||||
`COND_EXEC` — условие по метке в порядке потока или после работы видеочипа до пакета;
|
||||
`SET_PREDICATION` (ZPass, Bool64/32) — отрисовки и запуски с битом предиката пропускаются, если
|
||||
предикат не выполнен. Bloodborne последние три не использует; это для переноса других игр.
|
||||
- `BB_PM4_SELFTEST=1`: самопроверка всего этого в работающей игре (8 проверок, печатает PASS/FAIL).
|
||||
- Исправлена взаимоблокировка при чтении процессором данных видеочипа из потока разбора.
|
||||
|
||||
## Шейдер копирования игры — по умолчанию как есть
|
||||
|
||||
- Шейдер копирования игры (`cs fefebf9f`, ~36 запусков за кадр) теперь по умолчанию переводится и
|
||||
выполняется сам: список копий читает видеочип. `BB_COPY_SHADER_NATIVE=0` — прежняя подмена (список
|
||||
читает процессор). Цена на 4 ядрах — +6 % к потоку записи отрисовок, видеочип и FPS те же.
|
||||
- Чтобы цена была такой: запись видеочипа в память ищет затронутую текстуру только по первому байту
|
||||
(нужна лишь текстура, начинающаяся с этого адреса; раньше обходился весь диапазон под мьютексом), а
|
||||
запись поверх страниц, которые процессор не менял и которые уже помечены как данные видеочипа,
|
||||
проходит без блокировки трекера.
|
||||
- Подмены «копирование текстуры через буферы» и «очистка текстуры через буфер» оставлены: путь через
|
||||
память (выгрузить отрисованный кадр в память, выполнить шейдер игры, перезалить текстуру) сейчас
|
||||
даёт застывшую картинку — содержимое целей рендера не доходит до памяти игры, а при рендере сцены в
|
||||
пониженном разрешении его там и нет. `BB_HLE_OFF=1|2|4` — опыт с отключением подмен,
|
||||
`BB_HLE_STATS=1` — сколько раз они срабатывают (~4, ~15 и ~10 за кадр).
|
||||
- Опыт `BB_GUEST_VRAM=1`: вся память игры в VRAM через Resizable BAR — 16 FPS уже в меню (процессор
|
||||
читает её через PCIe), не годится.
|
||||
|
||||
## Сохранения не портятся при вылете (#64)
|
||||
|
||||
- Bloodborne сохраняется примерно раз в секунду: удаляет `backupN`, переименовывает `userdataN` в
|
||||
`backupN` и пишет новый `userdataN` (1,3 МБ). Вылет или снятие процесса посреди записи оставляли
|
||||
обрезанный `userdataN`.
|
||||
- Теперь файл сохранения (`/savedataN/...`), открытый на запись, пишется во временную копию
|
||||
(`<имя>.<n>.bbtmp` рядом), а при закрытии она сбрасывается на диск (`fsync`) и одной операцией
|
||||
`rename` встаёт на место файла. Вылет посреди записи оставляет прежний файл (или его отсутствие,
|
||||
если игра уже переименовала его в `backupN`: тогда игра сама читает `backupN` — проверено).
|
||||
- Пока файл открыт, операции по пути (`stat`, `access`, `truncate`, повторное открытие) видят копию:
|
||||
игра проверяет размер записываемого файла по пути.
|
||||
- Копия не попадает в списки каталога; оставшиеся после вылета копии удаляются при монтировании.
|
||||
- `param.bin` и `icon0.png` записываются так же (временный файл, `fsync`, `rename`).
|
||||
- `BB_SAVE_TRACE=1` — все операции игры с файлами сохранений в журнал.
|
||||
|
||||
## Подготовка файлов игры и геймпад
|
||||
|
||||
- `prepare.py` и остальные скрипты, лаунчер: текстовые файлы читаются и пишутся в UTF-8 явно, вывод в
|
||||
консоль не падает на символах вне её кодировки (#55: китайская, японская, корейская локаль Windows,
|
||||
`™` в названии игры).
|
||||
- `eboot.bin` (и модули `sce_module`) принимаются и обычным ELF, без обёртки SELF: так их пишут
|
||||
некоторые программы дампа (#49). Проверено: ELF, извлечённый из нашего eboot, даёт тот же `boot.bin`
|
||||
байт в байт.
|
||||
- Стики (PR #71; #70): клон Switch Pro, у которого все четыре оси в покое смещены на
|
||||
~±16380 и ход вдвое меньше, калибруется — нейтраль берётся из спокойного окна после подключения,
|
||||
ход каждой оси — по её собственному диапазону. Мёртвая зона как в shadPS4: `BB_PAD_DEADZONE`
|
||||
(по умолчанию 5 из 127) и `BB_PAD_DEADZONE_OUTER` (127). `BB_PAD_CENTER=lx,ly,rx,ry` — нейтраль
|
||||
вручную, `BB_PAD_CENTER_CAL=0` — без замера.
|
||||
Отличие от PR: нейтраль принимается, только если смещены все четыре оси (иначе обычный геймпад,
|
||||
подключённый с зажатым стиком, запомнил бы этот наклон как нейтраль до переподключения); пока
|
||||
нейтраль не известна, стики читаются как раньше, а не стоят на месте.
|
||||
- README.ru.md: адрес репозитория в команде `git clone` (PR #43; в английском README он уже был).
|
||||
|
||||
## Зависания видеочипа: непрямые запуски, VRAM, сведения о видеокарте
|
||||
|
||||
- Непрямые запуски (#34, комментарий к #32; новая модель памяти): дважды игра теряла устройство на
|
||||
симуляции частиц, получившей вместо числа групп мусор (1,2 и 3,8 млрд групп; у нас в логах от
|
||||
2026-10-06 такое тоже было — 1,4 млрд и 6,4 млн, обошлось). Настоящие числа у Bloodborne — не больше
|
||||
нескольких сотен. Теперь перед каждым непрямым запуском шейдер из одного потока
|
||||
(`indirect_guard.comp`) копирует числа групп в наш буфер, обнуляя неправдоподобные (больше
|
||||
пределов устройства или `BB_INDIRECT_GUARD_MAX` групп всего, по умолчанию 4M), и запуск идёт из
|
||||
этой копии. Худшее, что теперь бывает, — пропущенный шаг частиц вместо вылета; пропуски
|
||||
печатаются строкой `Indirect dispatches: N skipped ...`. `BB_INDIRECT_GUARD=0` — как раньше.
|
||||
Причина мусора в памяти игры не найдена (только новая модель, редко). Цена: в пределах шума
|
||||
(183–184 FPS, видеочип 5,42–5,47 мс в одной сцене). Проверено с порогом 10 (пропуски
|
||||
срабатывают, игра идёт) и под слоем валидации (новых ошибок нет).
|
||||
- Перенос блоков памяти игры в VRAM теперь учитывает бюджет видеопамяти драйвера: если занято больше
|
||||
`BB_VRAM_PROMOTE_PERCENT` (по умолчанию 90 %), новые копии не создаются — блоки читаются на месте
|
||||
(медленнее, но без переполнения VRAM на картах с 8 ГБ), а перенос возобновляется, когда память
|
||||
освободится. Строка в логе при переключении. Число «MiB of loaded blocks moved ... to VRAM» —
|
||||
накопительное (сколько перенесено за сессию), а не занятое место; занятое — в строке `Memory:`.
|
||||
- В начале лога теперь строка с видеокартой, объёмом VRAM, драйвером и версией Vulkan (в отчётах
|
||||
о зависаниях, как #56, её не было).
|
||||
- #56 (старая модель, зависание при загрузке карты): два лога — два разных места остановки (обычная
|
||||
отрисовка из 5 индексов и копирования между проходами), шейдер из первого у нас работает 4–12 раз
|
||||
за кадр без проблем, те же патчи (Disable DoF, Motion Blur) у нас включены. Воспроизвести не
|
||||
удалось; без модели видеокарты и версии Mesa дальше не продвинуться (теперь они будут в логе).
|
||||
|
||||
## Баги из issues (2026-10-08, день)
|
||||
|
||||
- **Сборка FSR 4.1.1 там, где контейнер Steam не запускается (#61).** Bazzite + 9070 XT: ни один
|
||||
Proton «не запустил FSR 4.1», причина в списке была пустой, а в конце лога — `bwrap: No permissions
|
||||
to create a new namespace`: контейнер Steam (pressure-vessel, и под umu-run тоже) не может создать
|
||||
пространство имён — так бывает, когда сам лаунчер запущен в песочнице (Flatpak, ярлык Steam с
|
||||
рантаймом) или ядро это запрещает. Теперь при такой ошибке Proton запускается напрямую, без
|
||||
контейнера (сообщение `Steam's container could not start here (...): running Proton without it`,
|
||||
дальше до конца сборки так же); `BB_FSR4CAP_RUNNER=direct` — сразу так.
|
||||
Причина отказа каждого Proton — последняя непустая строка журнала записи, иначе ошибка контейнера
|
||||
или Wine (раньше — последняя строка, часто пустая). Один и тот же Proton больше не пробуется дважды
|
||||
по путям `/home/...` и `/var/home/...` (Fedora Atomic): одинаковые папки узнаются по устройству и
|
||||
inode, а не по `realpath`.
|
||||
Проверено: запись внутри `bwrap --disable-userns` (та же ошибка контейнера) — запасной путь
|
||||
записал кадры FSR 4.1.1 под GE-Proton 11-6, результат совпадает с обычной записью побайтно (кроме
|
||||
адресов указателей в `trace.txt`).
|
||||
- **MangoHud снова работает с системным MangoHud (#66).** С 0.3 `run.sh` при системном MangoHud
|
||||
отключал встроенный в AppImage слой по имени (`VK_LOADER_LAYERS_DISABLE`), но у системного слоя
|
||||
обычно то же имя (`VK_LAYER_MANGOHUD_overlay_64_x86_64`) — загрузчик Vulkan отключал оба. Теперь
|
||||
встроенный MangoHud убирается из путей поиска слоёв (`XDG_DATA_DIRS`), а системный остаётся.
|
||||
Проверено загрузчиком Vulkan (`VK_LOADER_DEBUG=layer`): раньше «forced disabled», теперь слой
|
||||
вставляется.
|
||||
- **Картинка предмета на экране загрузки не пропадает (#67, патч bmy).** Маски Scaleform рисуются
|
||||
без записи цвета; при выводе не 1080p они попадали в глубину игры, а следующая отрисовка UI
|
||||
переключалась на глубину UI размера вывода с очищенным трафаретом — картинка не проходила тест.
|
||||
Теперь отрисовки UI без цели цвета тоже идут в глубину UI. В игре не проверено (на macOS у автора —
|
||||
да).
|
||||
- **Выбор монитора (#69).** Окно и полноэкранный режим шли на монитор, который SDL считает основным.
|
||||
*Экран → Монитор* в лаунчере (`BB_DISPLAY`: номер или часть имени; список —
|
||||
`bb-gpu-capabilities --displays`), в логе при запуске — строки `Display N: ...` с отметкой
|
||||
выбранного. Проверено на Hyprland с двумя мониторами: полноэкранное окно открывается на выбранном
|
||||
(обычное окно на Wayland размещает композитор).
|
||||
- **Дебаг-меню: шрифты из мода находятся.** Патч дебаг-меню читает шрифты из
|
||||
`dvdroot_ps4/adhoc/font/DbgFont14h.ccm` и `.tpf` (и шейдеры `adhoc/FontShader/debugFont_*`),
|
||||
причём игра запрашивает имена в нижнем регистре (`dbgfont14h.ccm`, найдено с новым
|
||||
`BB_FILE_TRACE=1`). Проверка в `patches.py` искала их в `dvdroot_ps4/font` с точным регистром, а
|
||||
рантайм на Linux различал регистр — шрифты не находились ни модом, ни в дампе, и запуск падал с
|
||||
трассировкой Python. Теперь: рантайм, не найдя файл игры, ищет его без учёта регистра (как PS4 и
|
||||
shadPS4; только при промахе, без затрат для найденных файлов); проверка смотрит в `adhoc/`
|
||||
без учёта регистра; без шрифтов патч дебаг-меню не применяется (строка `Patches: debug menu off:
|
||||
...`, подсказка, если шрифты лежат в старом `dvdroot_ps4/font`), игра запускается; папка `adhoc`
|
||||
в моде узнаётся как папка игры. Проверено: мод `Adhoc/Font/DbgFont14h.*` открывается игрой
|
||||
(`Mods: open /app0/dvdroot_ps4/adhoc/font/dbgfont14h.ccm -> .../Adhoc/Font/DbgFont14h.ccm`).
|
||||
С настоящими шрифтами меню не проверено (их нет).
|
||||
- `BB_FILE_TRACE=1`: каждый путь, который игра открывает, проверяет или читает (`File trace: ...`).
|
||||
- **Экрана «Играть по сети / вне сети» больше нет.** У порта нет PSN; теперь игра сразу открывает
|
||||
главное меню в режиме вне сети — при запуске и после «Выйти из игры», с сохранением и без. Патч игры
|
||||
«Skip Online/Offline Choice» (`patches/Bloodborne.xml`): функция, которая строит этот диалог
|
||||
(`0x1b39030`, найдена по ID строк 401010/401011 из `menu.msgbnd`), возвращает то же, что вернул бы
|
||||
её пункт «ИГРАТЬ ВНЕ СЕТИ» (вызов его обработчика `0x1b49cd0`, vtable `0x533be30`): следующий
|
||||
диалог цепочки, так что выбор не показывается. Включается `patches.py` по умолчанию (лаунчер:
|
||||
*Эффекты игры → Пропускать выбор*, `BB_SKIP_NETWORK_CHOICE=0` — показывать). Прежний вариант
|
||||
(порт нажимал «крест», экран мелькал) удалён. `tools/restart.sh` по-прежнему показывает выбор
|
||||
(его сценарий сам нажимает на титуле). Проверено скриншотами: запуск без ввода — сразу главное
|
||||
меню; «Продолжить» → «Выйти из игры» — сразу главное меню.
|
||||
- Память при повторной загрузке локации (6 циклов «Продолжить → выйти», новая модель): RSS
|
||||
707 → 733 МиБ, VRAM выходит на ~3,7 ГБ; старая модель (4 цикла): RSS 3735 → 3795 МиБ, VRAM ~3 ГБ.
|
||||
Утечки на гигабайты (сообщение из Discord про телепорты) так не видно; телепорт между разными
|
||||
локациями не проверен.
|
||||
- **Нехватка видеопамяти у копий памяти игры не роняет игру.** Старая модель держит копии памяти
|
||||
игры в блоках VRAM по 64 МиБ; на картах с 4 ГБ (RTX 3050 mobile, сообщение из Discord) выделение
|
||||
блока могло вернуть `ErrorOutOfDeviceMemory`, и игра останавливалась (`vk_platform.h:73`, код 23).
|
||||
Теперь такой блок берётся из системной памяти, которую GPU читает по шине (строка `Guest memory:
|
||||
VRAM is full ...`): медленнее, но без вылета. `BB_RESIDENCY_VRAM_LIMIT_MB=N` — проверка (VRAM
|
||||
«кончается» после N МиБ блоков). Проверено с 32 МиБ: игра идёт, картинка верна, 28 FPS (всё в
|
||||
системной памяти; в жизни туда уходит только излишек). То ли это место, где упал отчёт из
|
||||
Discord, не доказано — строки перед остановкой в нём нет.
|
||||
- **«Чтение данных GPU: Precise» больше не замораживает первую катсцену.** Новая игра → катсцена
|
||||
стояла на первом кадре (звук шёл): видеоплеер писал кадр ролика в память, которую GPU писал
|
||||
раньше; обработчик попросил поток GPU о чтении, а тот, снимая копию байтов игры для «двойников»
|
||||
(`BB_GPU_WRITE_TWINS`) под их мьютексом, сам попал на защищённую от чтения страницу и в
|
||||
обработчике ждал тот же мьютекс. Найдено по стекам потоков (SIGUSR2). Теперь байты игры для
|
||||
«двойников» читаются через вид без защиты (`ReadBacking`), а с `BB_READBACKS=2` «двойники»
|
||||
выключены: страница с байтами GPU, в которую игре можно писать, была бы «только для записи»
|
||||
(следующей остановкой был `page_manager.cpp: ... write-only`). Проверено: новая игра с
|
||||
`BB_READBACKS=2` и без — катсцена идёт до создания персонажа.
|
||||
- **Меню настроек — как в ПК-играх, в стиле Bloodborne.** Вместо окна ImGui поверх игры
|
||||
(Insert / L3+R3): игра затемнена, по центру панель с орнаментными линиями, как у диалогов самой
|
||||
игры (шрифт с засечками DejaVu Serif, встроен рядом с DejaVu Sans), заголовок и вкладки
|
||||
«Изображение / Апскейлер / Эффекты игры / Дополнительно» (L1/R1, Q/E, PageUp/PageDown, мышь).
|
||||
Каждая настройка — строка «название ◀ значение ▶»: вверх/вниз (крестовина, стик, стрелки) —
|
||||
выбор строки, влево/вправо — значение, Крест/Enter — следующее значение, мышь — клик по
|
||||
стрелкам или строке, ползунки тянутся мышью. Описание выбранной настройки — внизу (вместо «(?)»),
|
||||
там же предупреждение о перезапуске; строка «Применить и перезапустить игру» — когда нужно.
|
||||
Подсказки кнопок — в подвале; Esc / Круг / Insert / L3+R3 — закрыть. Недоступные значения
|
||||
(FSR 4 без поддержки) пропускаются, зависимые строки гаснут (пресет при TAA, сила резкости без
|
||||
резкости, маска реактивности). Фокус — свой (навигация ImGui до строк не доходила).
|
||||
`BB_MENU_KEYS_FILE=<файл>` — сценарное управление меню для проверок (`toggle up down left right
|
||||
enter back l1 r1 kup kdown`).
|
||||
- **Настройки порта — родные страницы в меню игры «Система».** После «Экран/звук» появились пункты
|
||||
«Изображение», «Эффекты игры» и «Патчи игры»: их строит и рисует сама игра (её строки, списки,
|
||||
ползунок, подсказка внизу, «По умолчанию» с подтверждением), и на титульном экране, и в паузе.
|
||||
«Изображение»: разрешение вывода, апскейлер (в списке только то, что эта видеокарта и сборка
|
||||
умеют: без NVIDIA нет DLSS), пресет, резкость (ползунок 0–10, 0 — выкл, 10 = 2.0), счётчик FPS —
|
||||
меняются сразу и пишутся в `bbport.ini`. «Эффекты игры» / «Патчи игры»: детализация моделей и
|
||||
патчи-эффекты (после перезапуска). «По умолчанию» ставит значения порта по умолчанию. Тексты на
|
||||
языке самой игры (русский — если её тексты на кириллице, иначе английский). Как устроено
|
||||
(`gpu/shim/bbport_game_menu.cpp`): перехват поиска текстов игры (свои id 0x7F0000+) и добавления
|
||||
пункта в меню (0x1b4e2a0, после «Экран/звук»); страница — макет «ControllSetting» со своими
|
||||
строками и заголовком; последний аргумент строк игры — указатель на значение по умолчанию.
|
||||
Каждая перехваченная функция сверяется побайтно, `BB_GAME_MENU=0` — выключить. Проверено: 16
|
||||
открытий страниц — рост памяти как у родных страниц игры; оверлей (Insert / L3+R3) остался.
|
||||
- Ровность 60 FPS со временем (сообщение из Discord: «через некоторое время кадры неровные»):
|
||||
6 минут стоя, `BB_FPS=60` — каждую минуту 16,67 мс, разброс 0,00–0,01 мс, ни одного кадра
|
||||
дольше 20 мс (кроме загрузки в первой минуте). На RX 7800 XT не воспроизводится.
|
||||
|
||||
## Влитые PR (2026-10-08, вечер)
|
||||
|
||||
- **PR #84: DLC.** Папки `<user>/addcont/<title>/<метка>` (как у shadPS4) сообщаются игре
|
||||
как установленные дополнения: сохранения с The Old Hunters загружаются, области DLC доступны
|
||||
(#33). Данные дополнений не монтируются — The Old Hunters нужна только метка, данные в патче
|
||||
1.09. При вливании: `_GNU_SOURCE` под `#ifndef` (тест собирает с `-D_GNU_SOURCE` и `-Werror`), в
|
||||
тест AppContent — заглушка папки пользователя и проверка: папка `addcont/CUSA00900/SPEXPANSIONDLC03`
|
||||
даёт одну запись «установлено».
|
||||
- **PR #80:** `BB_FSR4_STATS=1` больше не ломает пайплайны FSR 4 v07 без
|
||||
`VK_EXT_pipeline_executable_statistics`. Патч к подмодулю FSR-Vulkan изменился: рабочую копию
|
||||
подмодуля нужно вернуть (`git -C gpu/third_party/fsr-vulkan checkout -- src/ffx_vk_fsr4_v07.c`), и
|
||||
`build.sh` наложит новую версию.
|
||||
- **PR #78:** лаунчер — кнопки «Копировать» и «Экспорт лога…» на вкладке «Журнал».
|
||||
- **PR #75:** лаунчер на упрощённом китайском (строки, добавленные сегодня, — на
|
||||
английском до перевода).
|
||||
- **PR #83: DLSS на Linux.** Мост `gpu/dlss_bridge` (MIT, единственный код с SDK NVIDIA)
|
||||
собирается с `DLSS_SDK_ROOT=<github.com/NVIDIA/DLSS> bash build.sh` в `out/libbbport_dlss.so`,
|
||||
рядом копируется библиотека NVIDIA; игра загружает мост при запуске. Новый апскейлер `dlss`
|
||||
(лаунчер, меню, `bbport.ini`): те же входы, что у FSR 3.1, Native AA = DLAA, при ошибке — FSR 3.1.
|
||||
Без моста и библиотеки, на не-NVIDIA или с `BB_DLSS=0` ничего не меняется (DLSS в меню недоступен).
|
||||
При вливании: пункт DLSS и его описание перенесены в новое меню, строка в списке апскейлеров
|
||||
лаунчера — после строки языков PR #75. Проверено здесь: сборка без SDK, игра на AMD (FSR 4.1.1,
|
||||
FSR 4 v07 со статистикой). С DLSS не проверено (нет NVIDIA); автор — RTX 4090. В AppImage мост и
|
||||
библиотека NVIDIA не входят.
|
||||
|
||||
## После 0.5-pre1 (2026-10-09)
|
||||
|
||||
- **Новая модель памяти снова только для AMD.** Лог с GTX 1660 Ti (драйвер 615.71, Hyprland):
|
||||
импорт памяти хоста прошёл проверку при запуске, но после первого переноса блоков в VRAM
|
||||
(`84 MiB of loaded blocks moved`) — `GPU watchdog: submission 4114 not finished after 10 s`:
|
||||
видеочип закончил шейдер копирования и не начинал следующий запуск. Отправка ждёт семафор
|
||||
перепривязки памяти (`vkQueueBindSparse` арены), и на NVIDIA каждая такая перепривязка держала
|
||||
видеочип 10 секунд и дольше; рабочий стол вставал вместе с ним, со стороны — «зависает вся
|
||||
система». Проверка при запуске этого не ловит (там одна маленькая привязка). Теперь на NVIDIA
|
||||
переключатель недоступен, у кого он был включён — старая модель (лаунчер передаёт
|
||||
`BB_PC_MODEL=0`, библиотека GPU тоже не включает её на NVIDIA); `BB_PC_MODEL_ANY_GPU=1` — всё же
|
||||
попробовать. README и тексты лаунчера исправлены.
|
||||
- `BB_RESIDENCY_REPORT=<файл>` (диагностика новой модели): когда файл появляется, в журнал —
|
||||
блоки памяти игры, которые видеочип недавно читал на месте, а не из VRAM, и почему они не
|
||||
перенесены (процессор пишет часто; байты, о записи которых мы не знаем; ждут переноса).
|
||||
У лампы в Кошмаре охотника: ~1,2 ГБ таких блоков памяти Garlic целиком «неизвестны».
|
||||
- Сообщение «первая загрузка медленнее, после телепорта 90 FPS» (RX 7900 XTX, новая модель): здесь
|
||||
не воспроизвелось — первая и повторная загрузка (выход в титул, «Продолжить») 131,7 и 131,9 FPS
|
||||
на RX 7800 XT; переносы в VRAM после загрузки заканчиваются за секунды. Нужен лог тестера.
|
||||
|
||||
## Новая модель на NVIDIA: разбор (2026-10-09, продолжение)
|
||||
|
||||
- **Причина зависания найдена.** Это известная регрессия проприетарного драйвера NVIDIA на Linux
|
||||
(с 555 и до сих пор, у тестера 615): время `vkQueueBindSparse` растёт с числом страниц, **уже**
|
||||
привязанных во всех sparse-ресурсах, и вызов держит глобальную блокировку (форум разработчиков
|
||||
NVIDIA, «Sparse texture binding is painfully slow»: 9200 страниц — 3,6 с; исправления нет).
|
||||
В арене новой модели — гигабайты страниц по 64 КиБ (у тестера 863 МиБ привязано на месте ещё
|
||||
до первых переносов), так что каждый перенос блоков в VRAM стоил секунды. Игра сама заметила
|
||||
остановку видеочипа ещё до первого переноса («Stall during rendering at flush 1»).
|
||||
- **Почему не обойти «одной правкой».** Замер `BB_RESIDENCY_MIX=1` (новая диагностика): ~17 тыс.
|
||||
привязок в секунду (~85 за кадр) — диапазоны в сотни МБ, где одновременно блоки в VRAM и на
|
||||
месте (шейдеры, привязанные ко всей памяти; треть — поверх данных, записанных видеочипом).
|
||||
Одна sparse-арена смешивает их бесплатно; схема «VRAM-арена + отдельные буферы памяти игры»
|
||||
их не обслужит. Без перепривязок вообще (`BB_FIXED_ARENA=1`, всё читается на месте через шину):
|
||||
**48,6 FPS против ~205** на RX 7800 XT — хуже старой модели.
|
||||
- Итог: новая модель остаётся только для AMD. Для NVIDIA нужна либо починка драйвера (или
|
||||
Mesa NVK, где sparse работает нормально), либо программная подкачка страниц в транслированных
|
||||
шейдерах вместо sparse — многодневная переделка.
|
||||
- Добавлено: `BB_FIXED_ARENA=1/0` (арена держит первые привязки: без переносов в VRAM, блоки вне
|
||||
кусков памяти игры — в системной памяти; по умолчанию — при импорте памяти хоста, т. е. на NVIDIA
|
||||
с `BB_PC_MODEL_ANY_GPU=1`); строка в журнале `vkQueueBindSparse of N ranges took X ms` (дольше
|
||||
50 мс, в обеих моделях); в отчёте сторожа видеочипа — `Arena binds: ...` (ждут ли отправки
|
||||
незавершённую привязку); `BB_RESIDENCY_MIX=1` — привязки «на месте / VRAM / смешанные» за 2 с.
|
||||
|
||||
## Новая модель на всех видеокартах: модуль памяти прослойки (2026-10-09)
|
||||
|
||||
- **Новая модель включается на любой видеокарте.** На AMD — как раньше, sparse-арена. На NVIDIA,
|
||||
Intel и остальных — модуль памяти прослойки (`gpu/layer`, docs/MEMORY_MODULE_PLAN.ru.md): память
|
||||
игры привязывается на месте через буферы её кусков, копии в VRAM — отдельные буферы («зеркала»),
|
||||
перенос — копия плюс учёт в модуле. `vkQueueBindSparse`, из-за которого на NVIDIA зависали игра
|
||||
и рабочий стол, в этом режиме не вызывается ни разу. Вручную: `BB_LAYER_MEMORY=1/0`. Лаунчер
|
||||
больше не прячет переключатель на не-AMD; README и подсказка (три языка) исправлены. **На
|
||||
NVIDIA не проверено** — нужен лог тестера.
|
||||
- Замеры на RX 7800 XT, стоя: было 48–60 FPS в режиме модуля, стало ~205 (sparse ~205–238, старая
|
||||
модель ~220). В другом месте: модуль 167,5, sparse 164,5, старая модель 146. 5 минут ходьбы с
|
||||
подгрузкой областей — без ошибок, 166–240 FPS.
|
||||
- Что мешало (найдено по профилю видеочипа):
|
||||
- **HLE копий и очисток образов не срабатывало.** Детекторы `IsComputeImageCopy` /
|
||||
`IsComputeImageClear` требуют у шейдера ровно два буфера, а в режиме модуля каждому шейдеру
|
||||
с буферами игры добавлялся третий — таблица страниц. Копия памяти целей рендеринга
|
||||
(`cs 3d5ebf4e`) и очистка (`cs 8b355b5a`, 31 запуск за кадр) шли настоящими шейдерами через
|
||||
память, а образы перезагружались из неё каждый кадр (~16 образов 1080p вместо ~2,6).
|
||||
Теперь таблица страниц не считается (`ShaderBufferCount`): 60 → 136 FPS.
|
||||
- **Смешанные диапазоны.** Диапазон, у которого все блоки в VRAM, кроме одного с «неизвестными»
|
||||
байтами (край, промежуток между образами), целиком читался через PCIe (8 МБ на привязку).
|
||||
Теперь такой блок «летучий» в зеркале: обновляется из памяти игры перед каждой привязкой, то,
|
||||
что запуск записал, копируется обратно после запуска. 139 → 207 FPS. `BB_LAYER_VOLATILE=0` —
|
||||
выключить.
|
||||
- Перенос в VRAM при первой привязке диапазона известных данных (как делала арена); он
|
||||
соблюдает `BB_GARLIC_VRAM=0` и бюджет VRAM (`BB_VRAM_PROMOTE_PERCENT`, важно для карт с 6 ГБ).
|
||||
- Стартовая проверка импорта памяти хоста в режиме модуля проверяет запись видеочипа и процессора
|
||||
через сам импортированный буфер, без sparse-буфера.
|
||||
- Диагностика: `BB_RESIDENCY_MIX=2` — ещё и самые тяжёлые привязки на месте / смешанные с
|
||||
разбором блоков (VRAM, подходят, «динамические», неизвестные байты); в строке `Layer memory:` —
|
||||
число вызовов `vkQueueBindSparse` и байты, скопированные обратно.
|
||||
|
||||
## Модуль памяти: взрывы вершин и скорость (2026-10-09, день)
|
||||
|
||||
Итог работы над новой моделью для NVIDIA — в `docs/NVIDIA_MEMORY_MODEL.ru.md`: ловушки записи на
|
||||
блоках с копией в VRAM, чтения через модуль везде, где есть копия, данные процессора больше не
|
||||
затираются данными видеочипа, запись видеочипа в VRAM только в блоки, которые процессор не трогал
|
||||
(`BB_LAYER_GPU_WRITES`), шейдер копирования в режиме модуля через HLE, кэши ответов модуля.
|
||||
|
||||
## 0.5-pre2: оригинальное GPU-копирование и переносимые метки
|
||||
|
||||
В итоговой сборке шейдер копирования в новой модели теперь нативный по умолчанию: его записи
|
||||
согласованы между памятью игры и зеркалами VRAM. Добавлены стандартные Vulkan-метки завершения
|
||||
для GPU без `VK_AMD_buffer_marker`, исправлены зависимости записи/чтения буферов и миграция
|
||||
всего кэша при изменении формата. Подробное описание релиза и проверок:
|
||||
[CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md).
|
||||
|
||||
## 0.5-pre3: модуль памяти на NVIDIA
|
||||
|
||||
По первым логам с GTX 1660 Ti: копии в VRAM снова включены для модуля (их выключал обход
|
||||
sparse-арены), общий бюджет VRAM с кэшем текстур, запас роста копий вместо их перевыделения,
|
||||
записи WRITE_DATA в копию, наблюдаемые блоки (`BB_LAYER_WATCH`), повтор чтения файлов при EFAULT.
|
||||
Подробности: [CHANGES_0.5-pre3.md](CHANGES_0.5-pre3.md) и
|
||||
[NVIDIA_MEMORY_MODEL.ru.md](NVIDIA_MEMORY_MODEL.ru.md).
|
||||
|
||||
## 0.5-pre4: компиляция шейдеров в фоне, VRAM на картах 4–6 ГБ, меню как в 0.3
|
||||
|
||||
Новые конвейеры проходов, рисуемых каждый кадр, компилируются в фоновых потоках
|
||||
(`BB_ASYNC_PIPELINES`); у копий памяти игры в VRAM своя доля бюджета (регрессия pre3 на картах
|
||||
4 ГБ); после перемещений VRAM освобождается от копий покинутой локации; меню настроек в игре —
|
||||
снова одно окно, как в 0.3 и 0.4. Подробности: [CHANGES_0.5-pre4.md](CHANGES_0.5-pre4.md).
|
||||
@@ -1,159 +0,0 @@
|
||||
# Изменения после 0.5 (с 2026-10-10)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, сохранение пользователя (Соборный округ), 1080p.
|
||||
|
||||
## Все розничные издания получают профиль игры (#119)
|
||||
|
||||
- Профиль Bloodborne (`gpu/games/bloodborne.cpp`) выбирался только для CUSA03173. Американское
|
||||
издание (CUSA00900) и остальные регионы запускают тот же исполняемый файл 1.09, но шли без
|
||||
профиля, и новая модель падала при входе в мир: `Guest fault ... at guest offset 0x2085xxx,
|
||||
address (nil)` в аллокаторе игры (#119, RX 7600, US-издание).
|
||||
- Воспроизведено у себя на CUSA03173 с `BB_GAME_PROFILE=none`: то же падение (`0x208596a`) при
|
||||
входе в мир. С профилем, но без перехватов (`BB_GUEST_HOOKS=0`) — не падает: спасает
|
||||
`DynamicWriters` — код аллокатора, чьи служебные данные лежат в памяти GPU; без этой отметки
|
||||
их блоки уходят в VRAM-копию.
|
||||
- Профиль, проверка игры (`scripts/game_check.py`), патчи (`scripts/patches.py`), «звуковой
|
||||
фикс» сохранений и папки модов теперь знают все издания: CUSA00900, CUSA00207, CUSA00208,
|
||||
CUSA00299, CUSA01363 и издания с The Old Hunters CUSA03179, CUSA03173, CUSA03014, CUSA03023.
|
||||
- Проверка игры: другие регионы базовой игры без обновления просят 1.09 вместо «поддерживается
|
||||
только CUSA03173»; eboot.bin со вшитым патчем 60 FPS (падает в меню жестов) — отдельное
|
||||
сообщение (PR #117).
|
||||
- Осталось: общий путь без профиля должен не падать сам (служебные данные аллокатора в
|
||||
VRAM-копии). Сейчас он нужен только для чужих игр.
|
||||
|
||||
## Размытие в движении на весь кадр в новой модели (#122)
|
||||
|
||||
- С включённым размытием в движении (по умолчанию) кадр при повороте камеры размазывался целиком,
|
||||
в старой модели — нет (#122, RX 9060 XT). Воспроизведено: резкость кадра во время поворота
|
||||
(стандартное отклонение лапласиана) 6,9 против 8,5–12 в старой модели.
|
||||
- Поиск: `BB_GARLIC_VRAM_RANGE=начало,конец` (новый диагностический ключ: VRAM-копии только в
|
||||
этом диапазоне) и деление диапазона пополам — блок 64 КиБ `0x104ff00000`. Его читает только
|
||||
видеочип (буфер 11,4 МиБ каждый кадр), записей буферами и процессором нет: это память
|
||||
рендер-таргета, который игра потом читает как буфер.
|
||||
- Причина: выгрузка изображения в память игры шла через staging и отложенную запись процессора,
|
||||
когда у этой памяти была VRAM-копия; копия обновлялась лишь после той записи (инвалидацией) —
|
||||
буфер читал данные прошлых кадров. Теперь видеочип копирует изображение и в арену (VRAM-копию)
|
||||
в порядке потока; в память игры — как раньше. И `IsRegionGpuModified` считает изменённым то,
|
||||
что видеочип записал в VRAM-копию (`gpu_written_bytes`, не на месте): без этого обновление после
|
||||
записи процессора поверх затирало копию старыми байтами (`BB_GPU_DATA_CHECK=0` — как раньше).
|
||||
Нужны обе правки. После: 11,9–13,0, как в старой модели; FPS тот же (127–131 в обоих
|
||||
вариантах, по три запуска).
|
||||
|
||||
## Нехватка VRAM на 4 ГБ больше не останавливает игру
|
||||
|
||||
- RTX 3050 4 ГБ, старая модель (Авто на NVIDIA), 0.5: три падения за вечер —
|
||||
`Failed to allocate 64 MiB for guest memory copies: ErrorOutOfDeviceMemory`. Запасного типа
|
||||
памяти (системной) для арены нет: разреженные буферы NVIDIA берут только VRAM. В pre5
|
||||
сборщик текстур стал держать текстуры по 5 с (FPS не падал со временем), и на 4 ГБ VRAM
|
||||
заканчивалась.
|
||||
- Теперь: блок поменьше (ровно запрошенный), затем любой другой тип памяти, к которому можно
|
||||
привязать арену; и 30 с сборщик под давлением снова выселяет по отправкам, как до 0.5. Он же
|
||||
делает так сам, когда занято больше 92 % бюджета драйвера (`VK_EXT_memory_budget`, раз в
|
||||
секунду). Проверить можно только на такой карте.
|
||||
|
||||
## Выход из игры с геймпада (#74, #116)
|
||||
|
||||
- Оверлей (Insert или L3+R3): кнопка «Выйти из игры», второе нажатие подтверждает; игра
|
||||
закрывается как по кнопке окна.
|
||||
|
||||
## Мышь
|
||||
|
||||
- Камера мышью: щелчок в окне игры захватывает мышь (относительный режим SDL), F1 или уход из
|
||||
окна — отпускает; меню настроек и ввод имени тоже отпускают. Движение мыши — правый стик:
|
||||
отклонение по скорости мыши (~1000 пикселей/с — полный поворот при чувствительности 1), за
|
||||
60 мс спадает; движущаяся мышь сразу проходит мёртвую зону игры (`BB_MOUSE_DEADZONE`, 16 из
|
||||
127), так что медленное прицеливание тоже поворачивает камеру. Одинаково при любом FPS.
|
||||
- Кнопки мыши и колесо — те же «клавиши» в привязках: `Mouse Left` R1, `Mouse Right` L2 (огнестрел),
|
||||
`Mouse Middle` R3 (захват цели), `Mouse X1` R2, `Mouse X2` L1, колесо — крестовина вверх/вниз
|
||||
(быстрые предметы); каждое деление колеса — короткое нажатие. Кнопки работают только при
|
||||
захваченной мыши: щелчок, который её захватывает, не атакует.
|
||||
- bbport.ini: `mouse_look=0/1`, `mouse_sensitivity` (0.05–20), `mouse_invert_y`; лаунчер —
|
||||
*Управление*: «Камера мышью», чувствительность, инверсия; «Клавиатура и мышь» — назначение
|
||||
кнопок, «Назначить» принимает и кнопку мыши/колесо. Основа — PR #108 (виртуальный стик от
|
||||
движения мыши); раскладка клавиатуры не менялась.
|
||||
|
||||
## Прочее
|
||||
|
||||
- Swapchain пересоздаётся через `oldSwapchain` (PR #93): на NVIDIA под Wayland пересоздание
|
||||
падало с «Surface already has a tearing controller».
|
||||
- `launcher/bb-launcher.sh` передаёт аргументы (`--play`) и через nix-shell (PR #120).
|
||||
- Запись FSR 4.1.1 из AppImage на SteamOS и других дистрибутивах (PR #89): запись на хосте
|
||||
через systemd, повтор с GPU, представленным как RX 7800 XT, если DLL не предлагает FSR 4.1.
|
||||
- Лаунчер: у DLSS — подсказка, есть ли он в этой сборке (в AppImage его нет: DLSS SDK от NVIDIA
|
||||
не распространяется, игра включает FSR 3.1).
|
||||
|
||||
## DLSS в AppImage и своя библиотека NVIDIA
|
||||
|
||||
- Сборка с `DLSS_SDK_ROOT` (DLSS SDK 310.9.1 с github.com/NVIDIA/DLSS) кладёт в пакет мост
|
||||
`libbbport_dlss.so`, `libnvidia-ngx-dlss.so.310.9.1` и `NVIDIA-DLSS-LICENSE.txt` (лицензия RTX
|
||||
SDK разрешает распространение в составе приложения, не отдельно). Тестовый AppImage:
|
||||
`dist/…-test-1010-dlss.AppImage`.
|
||||
- Лаунчер, *Апскейлер → DLSS: библиотека NVIDIA → Выбрать файл…* — как DLL для FSR 4.1.1:
|
||||
`libnvidia-ngx-dlss.so.<версия>` копируется в `<user>/dlss/`, игра берёт её раньше встроенной
|
||||
(лог: `DLSS: NVIDIA's library from …`). Выбранная `nvngx_dlss.dll` — подсказка, что нужна
|
||||
библиотека для Linux. Мост ищется рядом с `bb-probe`, затем в `<user>/dlss/`.
|
||||
- Мост собирается и с защитными флагами Nix (`-Werror=format-security`).
|
||||
- Проверено на RX 7800 XT только до «not an NVIDIA GPU»: путь к библиотеке (встроенной и
|
||||
своей) выбирается верно. Сам DLSS — на карте NVIDIA RTX.
|
||||
|
||||
## Сборка из git после обновления с 0.4 (#124)
|
||||
|
||||
- `build.sh` останавливался: «patch failed: src/ffx_vk_fsr4_v07.c:35» — в подмодуле FSR-Vulkan
|
||||
оставался применённым патч прошлой версии, а новый не применялся ни вперёд, ни назад. Теперь
|
||||
подмодуль в этом случае сбрасывается и патчи применяются заново (проверено на копии в таком
|
||||
состоянии).
|
||||
|
||||
## Онлайн (shadNet) — отдельным модулем
|
||||
|
||||
- `gpu/bbnet` → `out/gpu/libbbnet.so`: сетевые и PSN-библиотеки shadPS4 из shadp2p (форк shadPS4,
|
||||
GPL-2.0) с клиентом shadNet, перенесены из Linux-форка bbport с онлайном (а тот — из Windows-порта).
|
||||
Модуль собирается отдельной целью, его C++ скрыт (`-fvisibility=hidden`, `-Bsymbolic`), наружу —
|
||||
только `bbnet_init` и `bbnet_resolve` (`gpu/bbnet/bbnet.h`). Свои заголовки модуля
|
||||
(`gpu/bbnet/include`) только дополняют общие: в `gpu/shim` и `gpu/shadps4` ничего не менялось.
|
||||
- Рантайм (`src/runtime_net.c`): с `BB_ONLINE=1` на первом сетевом/PSN-импорте загружает модуль
|
||||
(`dlopen`, `RTLD_LOCAL`; `BB_NET_MODULE` — другой путь), отдаёт ему данные игры и гостевые потоки
|
||||
для обратных вызовов NP; трофеи, голос и диалоги магазина остаются в рантайме. Без `BB_ONLINE`
|
||||
модуль не загружается; без модуля или если он не стартовал — офлайн, как раньше.
|
||||
- Сборка: protobuf, nlohmann_json, OpenSSL, zlib, miniupnpc (`shell.nix`); без них модуль не
|
||||
собирается (`BB_NET=OFF` — вручную), порт собирается как раньше. AppImage содержит модуль.
|
||||
- `run.sh`: перенаправления адресов (`scripts/online.py`: сервер игры → The Hunter's Dream,
|
||||
знаки призыва → WebAPI shadNet), сервер shadNet по умолчанию `srv.shadps4.net:31313`.
|
||||
- Лаунчер: страница *Онлайн* (сервер игры, сервер кооператива и WebAPI, учётная запись shadNet,
|
||||
UPnP, проверка соединения); `settings.json` — только для владельца (0600, там пароль).
|
||||
- UPnP: порт P2P теперь открывается на роутере, когда транспорт готов, и закрывается при выходе
|
||||
(#121: `AddMapping` не вызывался нигде).
|
||||
- Проверено: `tests/test_online.c` (импорты сети и NP → модуль, трофеи → рантайм); игра с
|
||||
`BB_ONLINE=1` без учётной записи доходит до уровня (модуль: 1399 функций; «no users connected
|
||||
to shadNet»). Вход в shadNet и призыв не проверены: нет учётной записи, а UDP-часть публичного
|
||||
сервера, по #121, не отвечает.
|
||||
- HTTPS в AppImage: «Не удалось получить информацию от сервера Bloodborne» — проверка сертификата
|
||||
The Hunter's Dream проваливалась (`SSL server verification failed`): OpenSSL из Nix берёт корневые
|
||||
сертификаты из `/etc/ssl/certs/ca-certificates.crt`, а на NixOS это ссылка в `/nix/store` хоста,
|
||||
которого внутри AppImage не видно. Обёртки пакета теперь задают `SSL_CERT_FILE` на свой пакет
|
||||
сертификатов (`nss-cacert`), если указанный не читается; `run.sh` из исходников берёт системный.
|
||||
Лаунчер: текст ошибки в проверке соединения не терялся из-за разметки (`<urlopen error …>`), значки
|
||||
— из темы Adwaita.
|
||||
|
||||
## Мерцание главного меню при выводе выше 1080p
|
||||
|
||||
- С живой сменой разрешения (вывод 3840×2160 на мониторе 2560×1080) главное меню мерцало: часть
|
||||
кадров показывалась в родных 1920×1080 игры, остальные — с интерфейсом в разрешении вывода
|
||||
(4 из 50 снимков). `BB_UI_TRACE=1` (новая диагностика: отрисовки кадров без интерфейса в
|
||||
разрешении вывода) показал: пока меню анимирует подсветку, игра рисует его другими шейдерами
|
||||
Scaleform (`bb4d5f8c`, `e94b5b06`, `9951d199`, `44072322`, `a334a845`), а не `09957251`; их не было в
|
||||
списке шейдеров интерфейса (`ui_composition.h`), и такие кадры не собирались в разрешении
|
||||
вывода. После: 0 из 80; в уровне картинка та же.
|
||||
|
||||
## AppImage меньше
|
||||
|
||||
- В пакете были весь Mesa с LLVM (~800 МБ: нужны только Vulkan-драйверы RADV и ANV), полный GCC
|
||||
и заголовки Vulkan (их пути в отладочной информации `bb-probe`), 32-битная половина MangoHud
|
||||
(вторые glibc, gtk3, icu, perl) и GStreamer у GTK4 лаунчера. Теперь: свои RADV (без LLVM, ACO)
|
||||
и ANV без остального Mesa, `bb-probe` без отладочной информации, MangoHud 64-битный, GTK4 без
|
||||
GStreamer, печати и tracker.
|
||||
- Ещё: SDL3 без зашитого zenity (тот тянул вторую GTK4 с GStreamer; теперь zenity хоста, если
|
||||
есть), libdecor без плагина GTK 3 (у SDL3 и у glfw MangoHud; рамки рисует его cairo-плагин),
|
||||
MangoHud без xdg-utils (Perl; `xdg-open` хоста). Сборка своей Mesa включает Dozen только потому,
|
||||
что рецепт nixpkgs ждёт его выход `spirv2dxil`; в пакет он не попадает.
|
||||
- Итог: AppImage 925 → 399 МБ, пакет без сжатия 2,85 → 1,06 ГБ. Проверено: лаунчер открывается,
|
||||
игра из пакета стартует на встроенном RADV до титульного экрана.
|
||||
+20
-10
@@ -1,18 +1,18 @@
|
||||
# Credits
|
||||
|
||||
Everyone whose work is in this fork, and every project it took something from. Everyone listed
|
||||
under *People* and *Help and testing* also appears in the launcher's **About** section. Components and their licenses: [BBPORT.md](../BBPORT.md#credits-and-licenses).
|
||||
under *People* and *Help and testing* also appears in the launcher's **About** section. Components and their licenses: [bbport's notes](../BBPORT.md#credits-and-licenses).
|
||||
|
||||
## Projects
|
||||
|
||||
| Project | What this fork takes from it |
|
||||
|---|---|
|
||||
| [deadinside28/bloodborne_pc](https://github.com/deadinside28/bloodborne_pc) | **bbport**, the native Linux port everything here is built on. Its README is [BBPORT.md](../BBPORT.md). |
|
||||
| [deadinside28/bloodborne_pc](https://github.com/deadinside28/bloodborne_pc) | **bbport**, the native Linux port everything here is built on. Its README is [here](../BBPORT.md). |
|
||||
| [Supermedo/bloodborne_pc](https://github.com/Supermedo/bloodborne_pc) | The Windows port: online play, DLSS, the cheats page, Add to Steam, the icon, the direct mouse camera, ultrawide, Quit to Desktop, the keyboard sprint and many fixes. |
|
||||
| [Awakenchan/LanternEngine](https://github.com/Awakenchan/LanternEngine) (formerly BloodBorneButPc) | Video memory on 4 GB cards, crash fixes, fewer stutters while areas load, stick options, save import and the version selector. |
|
||||
| [Awakenchan/LanternEngine](https://github.com/Awakenchan/LanternEngine) (formerly BloodBorneButPc) | Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine) (GPL-3.0-or-later): video memory on 4 GB cards, crash and freeze fixes, fewer stutters while areas load, stick options, save import and the version selector. |
|
||||
| [shadPS4](https://github.com/shadps4-emu/shadPS4) | The renderer and shader recompiler, and the online code. |
|
||||
| [Wozzardman/shadp2p](https://github.com/Wozzardman/shadp2p) | The network and PSN libraries for online play. |
|
||||
| [Wozzardman/shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) | The shadNet server for a group's own co-op server ([SERVER.md](SERVER.md)). |
|
||||
| [Wozzardman/shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) | The shadNet server for a group's own co-op server ([hosting your own server](SERVER.md)). |
|
||||
| [MaskZeus/bbbridge](https://github.com/MaskZeus/bbbridge) | Co-op on The Hunter's Dream's pool ([bbbridge/](../bbbridge/)). |
|
||||
| [Classmg22/BBSC](https://github.com/Classmg22/BBSC) | Seamless Co-op, by CLS_AMG, with Wozzardman's seamless summon ([bbsc/](../bbsc/)). |
|
||||
| [fhody125-web/NoDreamForHunter](https://github.com/fhody125-web/NoDreamForHunter) | The base of BBSC's game server. |
|
||||
@@ -37,8 +37,9 @@ under *People* and *Help and testing* also appears in the launcher's **About** s
|
||||
loading, black images, the upscaler, the performance line in the log).
|
||||
- **[Awakenchan](https://github.com/Awakenchan)**: video memory on 4 GB cards and crash fixes,
|
||||
fewer stutters while areas load, stick options, the controller in use, save import, the
|
||||
version selector, occlusion counters in command order, and vertex shaders with the right
|
||||
bindings.
|
||||
version selector, occlusion counters in command order, vertex shaders with the right
|
||||
bindings, freezes in memory allocation and draw preparation, index buffers of indirect draws,
|
||||
ZPASS queries in order, and camera motion vectors without NaN.
|
||||
- **[Classmg22](https://github.com/Classmg22)** (CLS_AMG): BBSC, Seamless Co-op.
|
||||
- **[MaskZeus](https://github.com/MaskZeus)**: bbbridge.
|
||||
- **[Wozzardman](https://github.com/Wozzardman)**: the online libraries and the co-op server.
|
||||
@@ -63,8 +64,8 @@ under *People* and *Help and testing* also appears in the launcher's **About** s
|
||||
- **[GoncaloLobo0](https://github.com/GoncaloLobo0)**: crashes while loading areas.
|
||||
- **[bmy](https://github.com/bmy)**: the interface outside 1080p, motion vectors for the large
|
||||
beasts, and a memory fault.
|
||||
- **[Mrsuss60](https://github.com/Mrsuss60)**: crashes when loading some maps, and the gestures
|
||||
menu from the keyboard.
|
||||
- **[Mrsuss60](https://github.com/Mrsuss60)**: crashes when loading some maps, the gestures
|
||||
menu from the keyboard, and image changes for the upscaler.
|
||||
- **[SrShadowy](https://github.com/SrShadowy)**: the on-screen keyboard for the character's name,
|
||||
and the shader compiling notice.
|
||||
- **[Obrikash](https://github.com/Obrikash)**: the game starts when DLSS cannot be used.
|
||||
@@ -82,6 +83,7 @@ under *People* and *Help and testing* also appears in the launcher's **About** s
|
||||
- **[AndiMajore](https://github.com/AndiMajore)**: crash debugging (`BB_FAULT_WAIT`).
|
||||
- **[AwesomeObserver](https://github.com/AwesomeObserver)**: large recorded command groups, and
|
||||
the swapchain after display changes.
|
||||
- **[iannitram](https://github.com/iannitram)**: FSR 3.1 without colour noise on the Ryzen Z1 (ROG Ally, Legion Go).
|
||||
|
||||
## Help and testing
|
||||
|
||||
@@ -96,7 +98,15 @@ under *People* and *Help and testing* also appears in the launcher's **About** s
|
||||
- **[shauryasagar](https://github.com/shauryasagar)** and **[ZeroSTF](https://github.com/ZeroSTF)**: the Hunter's Dream going black after long sessions.
|
||||
- **[popcar2](https://github.com/popcar2)**: Quit to Desktop with online play on, and ideas for the launcher.
|
||||
- **[shaneo88](https://github.com/shaneo88)**: shadPS4's patch file in the patches folder.
|
||||
- **[n3vrs0ft](https://github.com/n3vrs0ft)**: the "Transaction failed" crash on Seamless Hunt, and the logs that stopped a BBSC guest from following a stranger's world.
|
||||
- **[n3vrs0ft](https://github.com/n3vrs0ft)**: the "Transaction failed" crash on Seamless Hunt, the logs that stopped a BBSC guest from following a stranger's world, and testing BBSC 0.0.2 with a Windows player.
|
||||
- **[cappp](https://github.com/cappp)**: the "Transaction failed" crash on Seamless Hunt.
|
||||
- **[simo1730](https://github.com/simo1730)**: the Trophies page not finding the dump's files.
|
||||
- **[Nahnahchi](https://github.com/Nahnahchi)**: logs and tests of a GPU freeze in a boss fight on NVIDIA, and of a crash in Yahar'gul.
|
||||
- **[Nahnahchi](https://github.com/Nahnahchi)**: logs and tests of a GPU freeze in a boss fight on NVIDIA, a crash in Yahar'gul, stretched models, a crash on the Steam Deck, effects turning black, rainbow textures and crashes in the game's memory manager.
|
||||
- **[thomasgarafano1](https://github.com/thomasgarafano1)**: Hunter's Atlas not preparing the map with a GOTY dump, and testing the fix.
|
||||
- **[TacticalBill](https://github.com/TacticalBill)**: marking trophies already earned on the PS4.
|
||||
- **[holydingus](https://github.com/holydingus)**: BBSC's items not given to a new character.
|
||||
- **[OPGZ2](https://github.com/OPGZ2)**: the launcher's blank window at start.
|
||||
- **[kuday248](https://github.com/kuday248)**: settings from the in-game menu that the launcher overwrote.
|
||||
- **[BlazinKarp](https://github.com/BlazinKarp)**: the Steam overlay in the game.
|
||||
- **[molnar-a](https://github.com/molnar-a)**: a crash on the Steam Deck after lamp travel, and testing the fix.
|
||||
- **[gibs2193](https://github.com/gibs2193)**: the mouse camera turning slower sideways than up and down.
|
||||
@@ -1,188 +0,0 @@
|
||||
# Depth-Adaptive TAA Improvements
|
||||
|
||||
**Дата:** 2026-10-01
|
||||
**Статус:** Исторический эксперимент, заменён исправлением temporal reprojection.
|
||||
|
||||
Описанные ниже пороги и веса больше не используются. Уменьшение веса истории вдали
|
||||
усиливало мерцание; сравнение device depth между разными камерами и хранение глубины
|
||||
в FP16 не позволяли корректно отличать дальние поверхности. Актуальный разбор,
|
||||
изменения и результаты проверок: [TEMPORAL_REVIEW_2026-10-01.md](TEMPORAL_REVIEW_2026-10-01.md).
|
||||
|
||||
## Проблема
|
||||
|
||||
Текущая реализация TAA использует фиксированные пороги для:
|
||||
1. Определения disocclusion (раскрытие новых поверхностей): `0.0005`
|
||||
2. Временного веса накопления: `0.9` → `0.65` в зависимости от движения
|
||||
|
||||
Эти параметры не учитывают глубину пикселя, что приводит к проблемам:
|
||||
- **Далёкие объекты (depth → 1.0)**: временная нестабильность, мерцание
|
||||
- **Близкие объекты (depth → 0.0)**: чрезмерное размытие при быстром движении
|
||||
|
||||
## Решение: Адаптивные пороги по глубине
|
||||
|
||||
### 1. Depth-Adaptive Disocclusion Threshold
|
||||
|
||||
**Файл:** `gpu/shadps4/video_core/host_shaders/taa.comp`
|
||||
|
||||
```glsl
|
||||
float depthThreshold(float depth) {
|
||||
// Near: 0.001, Far (>0.95): 0.0002
|
||||
return mix(0.001, 0.0002, smoothstep(0.7, 0.95, depth));
|
||||
}
|
||||
```
|
||||
|
||||
**Логика:**
|
||||
- Близкие поверхности (depth < 0.7): порог `0.001` — допускает небольшие расхождения
|
||||
- Дальние поверхности (depth > 0.95): порог `0.0002` — строже, меньше false positives
|
||||
- Плавный переход через `smoothstep(0.7, 0.95, depth)`
|
||||
|
||||
**Применение:**
|
||||
```glsl
|
||||
float depthDiff = abs(history.a - depth);
|
||||
float threshold = depthThreshold(depth);
|
||||
if (depthDiff < threshold && ...) {
|
||||
// Accept history
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Depth-Adaptive Temporal Weight
|
||||
|
||||
```glsl
|
||||
float temporalWeight(float depth, float motionLength) {
|
||||
// Base weight decreases for distant objects: 0.9 near, 0.8 far
|
||||
float baseWeight = mix(0.9, 0.8, smoothstep(0.7, 0.95, depth));
|
||||
// Motion reduces weight more aggressively for distant pixels
|
||||
float motionFactor = mix(0.65, 0.5, smoothstep(0.7, 0.95, depth));
|
||||
return mix(baseWeight, motionFactor, clamp(motionLength / 16.0, 0.0, 1.0));
|
||||
}
|
||||
```
|
||||
|
||||
**Логика:**
|
||||
- **Базовый вес без движения:**
|
||||
- Близко: `0.9` (агрессивное накопление истории)
|
||||
- Далеко: `0.8` (консервативнее, меньше мерцания)
|
||||
|
||||
- **При быстром движении (motionLength > 16px):**
|
||||
- Близко: снижается до `0.65`
|
||||
- Далеко: снижается до `0.5` (ещё консервативнее)
|
||||
|
||||
**Применение:**
|
||||
```glsl
|
||||
float motionLength = length(motion);
|
||||
float weight = temporalWeight(depth, motionLength);
|
||||
result = mix(current, clipped, weight);
|
||||
```
|
||||
|
||||
### 3. Motion Vector Validation (camera_motion.comp)
|
||||
|
||||
**Файл:** `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
|
||||
|
||||
```glsl
|
||||
// Depth-adaptive validation: distant surfaces need tighter depth matching.
|
||||
float depth_threshold = mix(0.001, 0.0003, smoothstep(0.7, 0.95, depth));
|
||||
if (object_motion.b > 0.99 && abs(object_motion.a - depth) < depth_threshold &&
|
||||
!any(isnan(object_motion.rg)) && !any(isinf(object_motion.rg))) {
|
||||
result = object_motion.rg;
|
||||
}
|
||||
```
|
||||
|
||||
**Что изменено:**
|
||||
- Старый порог: фиксированный `0.001`
|
||||
- Новый порог: `0.001` → `0.0003` для далёких поверхностей
|
||||
- Предотвращает использование object motion vectors от неправильных пикселей
|
||||
|
||||
### 4. Улучшенная визуализация debug режимов
|
||||
|
||||
**Файл:** `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
|
||||
|
||||
Режимы `BB_DEBUG_MOTION=1` теперь структурированы:
|
||||
```glsl
|
||||
if (mode == 1) {
|
||||
// Raw depth visualization
|
||||
imageStore(color_img, pixel, vec4(fract(depth * 4.0), depth, depth < 0.0 ? 1.0 : 0.0, 1.0));
|
||||
} else if (mode == 2) {
|
||||
// View-space depth visualization (z / 50)
|
||||
const float v = z / 50.0;
|
||||
imageStore(color_img, pixel, vec4(fract(v * 4.0), v, v < 0.0 ? 1.0 : 0.0, 1.0));
|
||||
}
|
||||
```
|
||||
|
||||
## Ожидаемые улучшения
|
||||
|
||||
### Дальние объекты
|
||||
- ✅ Меньше мерцания на далёких стенах и небе
|
||||
- ✅ Более стабильная история при небольших движениях камеры
|
||||
- ✅ Меньше ложных disocclusion от численных ошибок глубины
|
||||
|
||||
### Близкие объекты
|
||||
- ✅ Меньше размытия при быстрых движениях (персонаж, оружие)
|
||||
- ✅ Более точные object motion vectors
|
||||
- ✅ Лучший баланс между накоплением и отзывчивостью
|
||||
|
||||
### Адаптивная зона (depth 0.7-0.95)
|
||||
- Плавный переход между режимами
|
||||
- Предотвращает резкие артефакты на границе
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Визуальная проверка
|
||||
```bash
|
||||
# TAA в нативном разрешении
|
||||
BB_UPSCALER=taa bash run.sh
|
||||
|
||||
# Проверить:
|
||||
# 1. Мерцание далёких стен (Hunter's Dream, верхние этажи)
|
||||
# 2. Размытие оружия при быстром повороте
|
||||
# 3. Стабильность при медленном движении камеры
|
||||
# 4. Disocclusion при обходе углов
|
||||
```
|
||||
|
||||
### Debug режимы
|
||||
```bash
|
||||
# Визуализация глубины
|
||||
BB_DEBUG_MOTION=1 bash run.sh
|
||||
# Toggle: (1<<21) = raw depth, (1<<22) = view-space z
|
||||
|
||||
# Проверка motion vectors
|
||||
# Toggle: (1<<20) = blended motion, (1<<23) = reprojection, (1<<24) = difference
|
||||
```
|
||||
|
||||
### A/B сравнение
|
||||
```bash
|
||||
# Создать файл для runtime-переключения
|
||||
echo "0" > /tmp/bb_toggle
|
||||
|
||||
# В игре переключать биты для сравнения
|
||||
# Потребуется временный механизм для старых/новых весов
|
||||
```
|
||||
|
||||
## Потенциальные риски
|
||||
|
||||
1. **Слишком консервативные веса на далёких объектах**
|
||||
- Может увеличить смазывание при быстрых поворотах
|
||||
- Решение: настроить `baseWeight` (0.8 → 0.85?)
|
||||
|
||||
2. **Слишком строгий порог глубины для object motion**
|
||||
- Может отбрасывать валидные векторы
|
||||
- Решение: `0.0003` → `0.0005` для depth > 0.95
|
||||
|
||||
3. **Smoothstep диапазон 0.7-0.95 может быть неоптимальным**
|
||||
- Bloodborne: near=0.05, far=3000
|
||||
- Большинство геометрии в диапазоне 0.1-0.9
|
||||
- Решение: профилирование распределения глубины в типичных сценах
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
1. ✅ Реализовать depth-adaptive пороги
|
||||
2. ⏳ Собрать и протестировать в игре
|
||||
3. ⏳ Собрать статистику распределения глубины
|
||||
4. ⏳ Настроить пороги на основе реальных данных
|
||||
5. ⏳ Добавить runtime-переключение для A/B тестов
|
||||
6. ⏳ Документировать финальные параметры
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [upscaler.md](upscaler.md) — общая архитектура апскейлера
|
||||
- [motion_vectors.md](motion_vectors.md) — векторы движения
|
||||
- TAA shader: `gpu/shadps4/video_core/host_shaders/taa.comp`
|
||||
- Camera motion: `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
|
||||
@@ -1,447 +0,0 @@
|
||||
# Bloodborne: первый эксперимент нативного запуска
|
||||
|
||||
## Частота кадров, звук, диагностика (2026-09-25)
|
||||
|
||||
- **FPS**: `BB_FPS=uncap` (по умолчанию) применяет патч «Uncap FPS++»
|
||||
(Lance McDonald, Kyo) из `patches/Bloodborne.xml`: интервал флипа 1 и
|
||||
шаги симуляции от реального дельтатайма кадра. Vblank при этом следует за
|
||||
частотой монитора (`BB_VBLANK_HZ=0`). `BB_FPS=60`/`90` — патчи с
|
||||
фиксированным шагом, `BB_FPS=30` — без патчей. Дополнительные патчи из XML:
|
||||
`BB_PATCHES="Skip Intro;Disable Motion Blur (perf increase)"`.
|
||||
`patches.py` компилирует их в `out/patches.bin`; адреса в XML — это
|
||||
виртуальные адреса PS4 (база eboot 0x400000), загрузчик пишет их по
|
||||
смещению `адрес-0x400000` после релокаций (`--patches`).
|
||||
- **Звук**: `sceAudioOutOutput` теперь возвращает управление строго раз в
|
||||
период буфера (как железо). Раньше вызовы шли пачками раз в квант PipeWire
|
||||
(21 мс), поток вывода FMOD обгонял микшер и проигрывал незаписанные половины
|
||||
512-кадровых блоков — щелчок каждые 10.7 мс.
|
||||
- **Производительность потока GPU** (`shadPS4:GpuCommandProcessor` — узкое
|
||||
место: ~97% одного ядра). Замер в одной сцене: 26.5 → 47 FPS.
|
||||
- `runtime_memory.c`: таблица регионов под rwlock вместо рекурсивного
|
||||
мьютекса (18% времени поток GPU ждал его), плюс потоковый кеш регионов,
|
||||
проверяемый по счётчику поколений таблицы, — запросы видеоядра на каждый
|
||||
draw идут без блокировки.
|
||||
- `vk_scheduler.cpp`: `PopPendingOperations` на каждом draw делал ioctl
|
||||
опроса timeline-семафора; теперь только при непустой очереди и не чаще
|
||||
раза в 250 мкс.
|
||||
- `fetch_shader.cpp`: разбор fetch-шейдера кешируется по адресу кода с
|
||||
проверкой копии кода.
|
||||
- `region_manager.h`: без изменённых страниц в диапазоне защита не
|
||||
пересчитывается.
|
||||
- Запись команд Vulkan в отдельном потоке `bb:VkRecorder`
|
||||
(`Scheduler::Record`, команды и их данные в блоках по 128 КиБ; draw,
|
||||
dispatch, дескрипторы, барьеры, копирования, dynamic state, детайлинг).
|
||||
`CommandBuffer()` для непереведённого кода синхронизируется и включает
|
||||
прямую запись до конца draw. `BB_VK_RECORD_THREAD=0` — старое поведение.
|
||||
- Кеш описаний текстур (`ImageDesc`) по T# и флагам ресурса, кеш
|
||||
`FindImage`, проверка изменённых страниц без блокировки для привязок
|
||||
только на чтение, загрузка гостевых данных буферов в потоке записи.
|
||||
- Замер в одной сцене: все оптимизации 64 FPS, без них 40 FPS (×1.6).
|
||||
- `BB_TOGGLE_FILE=<файл>`: маска выключенных оптимизаций, меняется во время
|
||||
игры (1 кеш регионов, 2 кеш fetch-шейдеров, 4 ранний выход трекинга
|
||||
страниц, 8 лимит опроса драйвера, 16 поток записи, 32 кеш текстур,
|
||||
64 проверка страниц без блокировки, 128 кеш FindImage, 256 загрузка
|
||||
буферов в потоке записи, 512 мемо трекера барьеров) — для поиска
|
||||
регрессий без перезапуска.
|
||||
- **Звук**: при старте для Bloodborne выставляется флаг `0x204E` в
|
||||
`SPRJ0005/userdata0010` (хак rainvmaker из форка Diegolix29): с нулевым
|
||||
флагом часть звуков, например оружие игрока, не проигрывается.
|
||||
`BB_SOUND_HACK=0` отключает.
|
||||
Профилирование без perf: загрузчик по SIGUSR2 печатает RIP, `rdi` (адрес
|
||||
мьютекса при ожидании futex) и цепочку кадров потока; GPU-библиотека
|
||||
собирается с `-fno-omit-frame-pointer`.
|
||||
- **Диагностика**: `BB_FRAME_STATS=1` — FPS, худший кадр и время компиляции
|
||||
шейдеров/пайплайнов раз в 5 с; `BB_AUDIO_STATS=1` — ритм и заполнение
|
||||
очереди звука; `BB_AUDIO_DUMP=путь` — сырой PCM каждого порта.
|
||||
|
||||
## Текущее состояние (2026-09-25)
|
||||
|
||||
Оригинальный x86-64 код `eboot.bin` выполняется напрямую на Linux и проходит
|
||||
глобальные конструкторы, инициализацию памяти и запуск собственной системы
|
||||
потоков игры: создано 13 гостевых потоков (`FD4JobWorker:0`, `EzWorkPool_*` ×9,
|
||||
`MOMainThread`, логгер). Игра выделяет ~4.8 ГиБ direct- и ~337 МиБ
|
||||
flexible-памяти. Текущая остановка — `sceAjmInitialize` (аудиокодек ATRAC9),
|
||||
caller `0xd18a6a`.
|
||||
|
||||
Изменения этого этапа:
|
||||
|
||||
- `runtime_memory.c` переписан: единый sparse memfd на весь direct-пул
|
||||
(5056 МиБ, как `sceKernelGetDirectMemorySize` на PS4), таблица регионов
|
||||
с разбиением, flexible memory (448 МиБ), `ReserveVirtualRange`, `Mprotect`,
|
||||
`QueryMemoryProtection`, `VirtualQuery`, `GetDirectMemoryType`, `BatchMap(2)`,
|
||||
частичные unmap/release, `MAP_FIXED`/`NO_OVERWRITE`, GPU-биты защиты.
|
||||
- **Все видимые гостю адреса размещаются ниже 1 ТиБ**: образ, стеки потоков,
|
||||
главный стек, трамплины (`runtime_low_map`), гостевая память
|
||||
(`0x10_0000_0000..0xfc_0000_0000`), кучи host-объектов (non-PIE + `mallopt`).
|
||||
Игра упаковывает указатели в 40 бит: например, lock по адресу `0x20368c0`
|
||||
хранит владельца как `scePthreadSelf() & 0xffffffffff` и сравнивает с полным
|
||||
значением. С хэндлами `0x7f…` это давало вечную взаимоблокировку.
|
||||
- Watchdog (`--timeout`) печатает RIP и цепочку кадров всех потоков
|
||||
(смещения относительно образа и `bb-probe` для `addr2line`).
|
||||
`BB_TRACE_SEMA=1` печатает блокирующие ожидания семафоров.
|
||||
`got_import.py <адрес GOT>` называет импорт за PLT-заглушкой.
|
||||
- Сеть в режиме «кабель отключён»: HTTP-шаблоны/соединения/запросы создаются,
|
||||
отправка возвращает сетевую ошибку; NpWebApi/Matching2/Signaling/Score —
|
||||
«не в PSN»; голосовой чат — порты без звука.
|
||||
|
||||
Следующие подсистемы по порядку появления: Ajm (ATRAC9 через ffmpeg),
|
||||
Fios2, SaveData, Rtc, Pad, AudioOut, AvPlayer, VideoOut и GnmDriver.
|
||||
|
||||
## Предыдущее описание
|
||||
|
||||
|
||||
**Оригинальный x86-64 код `eboot.bin` выполняется непосредственно процессором
|
||||
Linux и проходит начальный участок конструкторов.** Теперь он возвращается из
|
||||
`_init_env`, регистрирует три обработчика `atexit` и 279 C++-деструкторов,
|
||||
проходит 1416 guard инициализации, создаёт 1422 mutex, один rwlock и 17 семафоров,
|
||||
выделяет/отображает 116 МиБ для игрового аллокатора.
|
||||
Оригинальная `libc.prx` загружается вместе с eboot, её инициализатор возвращает 0,
|
||||
конструктор `std::ios_base::Init` пройден. Следующая контролируемая остановка —
|
||||
`sceSysmoduleLoadModule(0xb4)` (`g8cM39EUZ6o#M#N`, caller `0x2026149`).
|
||||
Отдельный тест Vulkan выполняет команду заполнения буфера и проверяет
|
||||
возвращённые 4096 байт. shadPS4 в этом эксперименте не запускается.
|
||||
|
||||
**Это ещё не рекомпиляция всей игры и не играбельный порт.** Нет окна игры,
|
||||
меню, отрисовки игровых ресурсов или работающего игрового цикла. Vulkan пока
|
||||
не соединён с графическими командами игры. Контролируемая остановка на очередном
|
||||
неподдерживаемом импорте — измеримая граница прототипа; запуск игры не завершён.
|
||||
|
||||
## Запуск в текущем окружении
|
||||
|
||||
Из `game_files`:
|
||||
|
||||
```bash
|
||||
bash native_probe/run.sh --software
|
||||
```
|
||||
|
||||
Скрипт находит локальные Python, GCC и Vulkan SDK, подготавливает образ,
|
||||
собирает загрузчик и запускает его. На этой машине инструменты найдены
|
||||
в `/nix/store`; ничего скачивать или устанавливать не понадобилось.
|
||||
`--software` выбирает Lavapipe и отключает неявные Vulkan layers для теста.
|
||||
В проверенном окружении `/dev/dri` отсутствует, поэтому реальный GPU не проверен.
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
Vulkan: llvmpipe (LLVM 21.1.8, 256 bits); command submission + 4096-byte readback PASS
|
||||
Mapped 91973152 bytes, 4 segments; applied 237083 relocations
|
||||
Starting native libc.prx at image offset 0x56e0000; fallback exports=202
|
||||
Runtime: application heap API registered
|
||||
Native libc module initializer returned 0
|
||||
Entering original x86-64 code at guest offset 0xa0
|
||||
Runtime: _init_env returned (verified libc implementation: RET)
|
||||
Runtime: direct allocation offset=0x0, size=121634816 bytes
|
||||
Runtime: mapped direct memory, size=121634816, alignment=0x200000
|
||||
Runtime: semaphore id=1, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=2, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=3, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=4, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=5, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=6, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=7, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=8, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=9, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=10, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=11, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=12, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=13, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=14, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=15, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=16, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=17, attr=2, initial=0, max=1
|
||||
STOP: first unsupported PS4 import: g8cM39EUZ6o#M#N (index 249)
|
||||
API: sceSysmoduleLoadModule
|
||||
Caller return offset: 0x2026149; first argument: 0xb4
|
||||
Runtime: _init_env=1, atexit=3, __cxa_atexit=279, registered handlers=282
|
||||
Runtime: static guards acquired=1416, released=1416
|
||||
Runtime: mutexes created=1422, locks=3208, unlocks=3208
|
||||
Runtime: rwlocks created=1, reads=21, writes=1, unlocks=22
|
||||
Runtime: semaphores created=17, deleted=0, acquired=0, signals=0, timeouts=0
|
||||
Runtime: direct memory allocations=1, maps=1, live=121634816, budget=536870912
|
||||
Runtime: memory operations=9884
|
||||
Original guest entry instructions executed; game initialization is incomplete.
|
||||
```
|
||||
|
||||
**Код завершения 20 означает ожидаемую остановку на отсутствующей реализации
|
||||
PS4 API.** Он намеренно отличается от 0. Ошибки загрузки возвращают 1;
|
||||
явно неподдерживаемые режимы runtime — 21, срабатывание stack protector — 22.
|
||||
Linux-сигналы — 128 + номер сигнала. На выполнение гостевого кода в Linux
|
||||
установлен таймер 10 секунд.
|
||||
|
||||
На системе с доступным GPU можно попробовать `bash native_probe/run.sh`.
|
||||
Это использует обычный выбор устройства Vulkan; аппаратный результат здесь
|
||||
не проверен. Для повторного запуска уже собранных файлов:
|
||||
|
||||
```bash
|
||||
native_probe/out/bb-probe native_probe/out/boot-libc.bin --cpu-only
|
||||
native_probe/out/bb-probe --vulkan-only
|
||||
```
|
||||
|
||||
Базовый образ без нативной libc сохраняется в `out/boot.bin` для сравнения
|
||||
с прежней остановкой на `std::ios_base::Init`.
|
||||
|
||||
Из каталога `native_probe` команда остаётся прежней: `bash run.sh --software`.
|
||||
Для воспроизведения самой ранней остановки на `_init_env` используйте
|
||||
`out/bb-probe out/boot.bin --cpu-only --strict-imports` из этого каталога.
|
||||
|
||||
Последняя команда использует обычные настройки Vulkan. Для программного
|
||||
драйвера задайте `VK_DRIVER_FILES` на его JSON и
|
||||
`VK_LOADER_LAYERS_DISABLE='~implicit~'`, как делает `run.sh --software`.
|
||||
|
||||
## Что найдено в файлах
|
||||
|
||||
| Параметр | Результат |
|
||||
|---|---|
|
||||
| Игра | Bloodborne, CUSA03173 |
|
||||
| Версия из `param.sfo` | APP_VER 01.09, VERSION 01.00 |
|
||||
| Контейнер | SELF, 8 записей сегментов |
|
||||
| CPU | x86-64, ELF64 little-endian, PS4/FreeBSD ABI |
|
||||
| Тип ELF | ET_SCE_DYNEXEC (`0xfe10`) |
|
||||
| Точка входа | `0xa0` относительно базы загрузки |
|
||||
| Загружаемые сегменты | 2: код/данные только для чтения и изменяемые данные/BSS |
|
||||
| Образ в памяти | 91 042 036 байт |
|
||||
| Импорты | 686 уникальных импортированных символов |
|
||||
| Зависимости DT_NEEDED | 42 модуля |
|
||||
| Перемещения адресов | 231 478 RELATIVE, 2 838 ABS64, 23 GLOB_DAT, 661 JUMP_SLOT |
|
||||
| Ресурсы `dvdroot_ps4` | 31 322 968 005 байт |
|
||||
|
||||
Среди зависимостей: libc, libkernel, GnmDriver, VideoOut, Pad, Fios2,
|
||||
AudioOut, SaveData, Sysmodule, сетевые и диалоговые API. В `sce_module`
|
||||
имеются только семь файлов; наличие файлов модулей не заменяет поддержку их ABI.
|
||||
|
||||
Используемые сегменты SELF доступны открытым текстом. Извлечение не выполняет
|
||||
расшифровку. Неподдерживаемые сжатые/зашифрованные сегменты отклоняются.
|
||||
Один незагружаемый сегмент метаданных (`program header 9`, тип `0x6fffff01`)
|
||||
не представлен в доступных SELF-сегментах. В диагностическом `eboot.elf` его
|
||||
диапазон заполнен нулями: **это не побайтово полное восстановление ELF**.
|
||||
Все сегменты, нужные этому эксперименту, проверены на наличие.
|
||||
|
||||
Подробности, SHA-256 исходного eboot, список зависимостей, NID, сегментов
|
||||
и количество ресурсов по каталогам находятся в `out/analysis.json`.
|
||||
Имена нескольких импортов проверены вычислением NID из предполагаемых имён;
|
||||
результаты записаны в `import_name_hints`. Для первого вызова `_init_env`
|
||||
получен NID `bzQExy189ZI`, следующий вызов соответствует `atexit`.
|
||||
|
||||
## Как устроен прототип
|
||||
|
||||
`prepare.py` читает SELF и SFO, проверяет границы, восстанавливает доступные
|
||||
сегменты, разбирает PS4 dynamic/symbol/RELA-таблицы и формирует `out/boot.bin`.
|
||||
Исходные файлы открываются только для чтения. Все результаты — в `out/`.
|
||||
|
||||
Дополнительно проверяется фактический экспорт `_init_env` в `sce_module/libc.prx`:
|
||||
NID `bzQExy189ZI`, адрес `0x5fef0`, размер 1, байт `c3` (RET).
|
||||
SHA-256 libc и доказательство записываются в `analysis.json/libc_evidence`.
|
||||
Первоначальное предположение о сложной инициализации внутри `_init_env`
|
||||
для этого дампа не подтвердилось. Возврат соответствует реальному коду функции.
|
||||
Формат `BBPROBE2` содержит флаг этой проверки; без флага runtime отключён.
|
||||
Старые образы `BBPROBE1` читаются с отключённым runtime.
|
||||
|
||||
`link_libc.py` создаёт отдельный `out/boot-libc.bin` формата `BBPROBE3`:
|
||||
добавляет два сегмента оригинальной libc, её перемещения адресов и 202
|
||||
резервные привязки экспортов. Сопоставляет NID вместе с именами и версиями
|
||||
библиотеки/модуля: локальные суффиксы libc `#C#A` и eboot `#q#q` различаются.
|
||||
Реализованные host-контракты имеют приоритет. Остальные совпавшие импорты
|
||||
ссылаются на настоящий код/данные libc, включая объекты и таблицы C++.
|
||||
Исходные файлы игры не изменяются; сведения о привязках и SHA-256 libc
|
||||
записываются в `out/libc-link.json`.
|
||||
|
||||
Перед входом в eboot выполняется `DT_INIT` libc. Поддержан TLS этой библиотеки:
|
||||
модуль 2, шаблон 544 байта, блок 1184 байта на host-поток, обнулённый остаток;
|
||||
14 перемещений `DTPMOD64` записывают номер модуля, а не указатель на образ.
|
||||
`__tls_get_addr` проверяет модуль и границы. Полный TLS всех модулей PS4,
|
||||
его освобождение при завершении гостевых потоков и прямой доступ через FS
|
||||
не реализованы. Для текущего одного гостевого потока блок живёт до выхода.
|
||||
`sceKernelGetProcParam` возвращает исходный перемещённый procparam eboot;
|
||||
`_sceKernelRtldSetApplicationHeapAPI` сохраняет таблицу callbacks аллокатора.
|
||||
|
||||
`runtime_thread.c` реализует self и запросы init/get/getaffinity/destroy
|
||||
атрибутов текущего гостевого потока. Для этого прототипа существует один
|
||||
виртуальный CPU (маска 1); это не маска host CPU и не настройка его affinity.
|
||||
Создание гостевых потоков и изменение атрибутов остаются неподдержанными.
|
||||
|
||||
`probe.c` выделяет память, копирует сегменты и применяет перемещения адресов.
|
||||
Неизвестные функции направляются в исполняемые перехватчики, печатающие NID
|
||||
и завершающие процесс. Неподдерживаемые импортированные данные направляются в недоступные
|
||||
страницы: их использование вызывает ошибку, а не возвращает фиктивные данные.
|
||||
После загрузки устанавливаются права страниц по ELF, код получает права RX.
|
||||
Затем выполняется прямой вызов оригинальной точки входа по SysV ABI.
|
||||
Подготовки аргументов достаточно для наблюдаемого участка; полноценный стек
|
||||
запуска ядра PS4, исключения и общий механизм загрузки зависимых модулей
|
||||
не реализованы; пока специально подключена только libc.
|
||||
|
||||
`runtime.c` реализует проверенный возврат из `_init_env`, реестр `atexit`/
|
||||
`__cxa_atexit` с обратным порядком и фильтрацией DSO, `__cxa_finalize`,
|
||||
`exit`, guard acquire/release/abort, случайный process-local stack canary,
|
||||
аварийный обработчик повреждения стека, четыре функции работы с памятью и `strlen`.
|
||||
Сопоставление выполняется по полному NID с library/module suffix именно
|
||||
этого eboot, без автоматического «вернуть 0» для неизвестных импортов.
|
||||
Реестр завершения и guards рассчитаны на текущий один гостевой поток;
|
||||
рекурсивная/конкурентная инициализация guard останавливает выполнение с кодом 21.
|
||||
|
||||
`runtime_mutex.c` связывает PS4 mutex/атрибуты с Linux pthread, преобразует
|
||||
типы и коды ошибок, поддерживает рекурсивные и статически созданные mutex.
|
||||
Приоритетные протоколы, PS4-планировщик и гостевые потоки не реализованы.
|
||||
Opaque handles предназначены для реализованных API; двоичный layout
|
||||
внутренних PS4 mutex не воспроизводится. Наблюдаемый участок libc работает
|
||||
через эти API; прямой доступ к внутренностям mutex потребует отдельной реализации.
|
||||
|
||||
POSIX-импорты mutex из `libScePosix` используют тот же backend через
|
||||
отдельные wrappers: возвращают положительные PS4 errno, а не отрицательные
|
||||
коды `scePthread`. Двухаргументный `pthread_mutex_init` не читает отсутствующий
|
||||
третий аргумент. Linux errno не передаётся гостю напрямую.
|
||||
|
||||
`runtime_sema.c` реализует kernel create/delete, wait/poll, signal/cancel.
|
||||
Handles — уникальные 32-битные ID; удалённые ID не переиспользуются.
|
||||
Проверены счётчики и переполнение, таймаут в микросекундах с монотонными
|
||||
часами, резервирование токенов до пробуждения, отмена и удаление с ожидающими
|
||||
host-потоками. Объект освобождается после возврата всех ожидающих вызовов.
|
||||
В режиме FIFO обслуживаются подходящие запросы в порядке очереди; запрос
|
||||
меньшего размера может пройти ожидающий запрос, для которого токенов ещё нет.
|
||||
Это соответствует использованной справочной модели shadPS4; сопоставление
|
||||
на реальной PS4 пока не выполнено.
|
||||
Атрибуты 0/2 допускают создание и неблокирующие операции, но блокирующее
|
||||
ожидание явно останавливает прототип (21): приоритетный PS4-планировщик ещё
|
||||
отсутствует. Ненулевые option parameters также не реализованы. В текущем
|
||||
запуске игра создала 17 семафоров с attr=2, initial=0, max=1, но пока не
|
||||
выполняла wait/signal; эти операции проверены отдельными C-тестами.
|
||||
|
||||
`runtime_time.c` поддерживает POSIX `gettimeofday`: время берётся у host,
|
||||
поля копируются в явные PS4 timeval/timezone. Проверены границы полей и
|
||||
диапазон микросекунд. Неожиданная ошибка host API останавливает прототип:
|
||||
полный POSIX errno/TLS bridge ещё не реализован.
|
||||
|
||||
`runtime_rwlock.c` поддерживает init/destroy, read/write, try-read/try-write,
|
||||
timed-read/timed-write и unlock поверх Linux pthread. Реестр синхронизирует
|
||||
инициализацию статических handles, отслеживает владельцев, читателей и
|
||||
ожидающие вызовы: уничтожение занятой блокировки возвращает EBUSY,
|
||||
чужой unlock — EPERM, таймаут — Orbis ETIMEDOUT. Ожидание не удерживает
|
||||
mutex реестра. Проверена одновременная работа нескольких host-потоков,
|
||||
но создание гостевых потоков пока отсутствует. Поддержаны только атрибуты
|
||||
по умолчанию; нестандартные атрибуты останавливают прототип с кодом 21.
|
||||
|
||||
`runtime_memory.c` даёт **ограниченный CPU-пул 512 МиБ для исследования**.
|
||||
Это выбранный бюджет прототипа, не объём RAM PS4. Выделение возвращает
|
||||
виртуальный физический offset; Linux memfd обеспечивает реальное общее
|
||||
хранилище для нескольких отображений и выравнивание 16 КиБ/2 МиБ.
|
||||
Реализованы allocate/map, полное unmap и полное release. Поддержаны типы
|
||||
памяти 0/3 для CPU, permissions read/write, flags=0, автоматический адрес;
|
||||
иной режим останавливается с диагностикой. Частичные release/unmap пока
|
||||
возвращают EINVAL. GPU coherency, резервирование фиксированных адресов,
|
||||
полное адресное пространство PS4 и подключение этого пула к Vulkan отсутствуют.
|
||||
|
||||
`vulkan_smoke.c` создаёт instance/device/queue, отправляет `vkCmdFillBuffer`,
|
||||
ставит барьер для чтения CPU, ожидает завершения и сравнивает все слова буфера.
|
||||
Это проверка будущего графического backend, не переводчик GNM.
|
||||
|
||||
## Сборка на других системах
|
||||
|
||||
Linux x86-64: Python 3, GCC/Clang, Vulkan headers и loader. Обычно достаточно:
|
||||
|
||||
```bash
|
||||
python3 native_probe/prepare.py CUSA03173
|
||||
python3 native_probe/link_libc.py CUSA03173
|
||||
bash native_probe/build.sh
|
||||
```
|
||||
|
||||
Скрипт поддерживает `CC`, `VULKAN_INCLUDE`, `VULKAN_LIB`;
|
||||
при наличии `pkg-config vulkan` использует его настройки.
|
||||
|
||||
Для Windows в исходнике есть ветка `VirtualAlloc`/`VirtualProtect` и явный
|
||||
SysV ABI. Предполагается **MinGW-w64 GCC/Clang x86-64**, не MSVC.
|
||||
Эта ветка **не собрана и не проверена** в текущем Linux-окружении.
|
||||
Пример ручной сборки в окружении MinGW с установленным Vulkan SDK:
|
||||
|
||||
```text
|
||||
python native_probe/prepare.py CUSA03173
|
||||
gcc -std=c11 -O2 -Wall -Wextra native_probe/probe.c native_probe/runtime.c native_probe/runtime_mutex.c native_probe/runtime_thread.c native_probe/runtime_rwlock.c native_probe/runtime_sema.c native_probe/runtime_time.c native_probe/runtime_memory.c native_probe/vulkan_smoke.c -I<SDK>/Include -L<SDK>/Lib -lvulkan-1 -o native_probe/out/bb-probe.exe
|
||||
native_probe/out/bb-probe.exe native_probe/out/boot.bin
|
||||
```
|
||||
|
||||
Пути SDK заменяются фактическими; бинарник Linux непереносим на Windows.
|
||||
Диагностика сигналов и таймер в текущем исходнике реализованы только для Linux.
|
||||
Новые mutex/rwlock/semaphore/time/direct-memory backends также реализованы только для Linux;
|
||||
Windows дойдёт до более раннего неподдерживаемого импорта.
|
||||
|
||||
## Проверки
|
||||
|
||||
```bash
|
||||
bash native_probe/build.sh --test
|
||||
python3 -m unittest discover -s native_probe -v
|
||||
```
|
||||
|
||||
Пройдено 35 Python-тестов, включая запуск набора C-проверок runtime:
|
||||
границы SELF/образа, проверка инструкции `_init_env`, прямое исполнение
|
||||
x86-64, перемещения адресов, режим strict, отключение неподтверждённого runtime,
|
||||
диагностика сбоев. C-проверки охватывают обратный порядок деструкторов,
|
||||
DSO и повторную финализацию, регистрацию при финализации и рост реестра,
|
||||
guard acquire/abort/release, ошибки и рекурсивность mutex, выравнивание,
|
||||
непересечение физических выделений, shared aliasing и повторное выделение
|
||||
очищенной памяти. Stack protector и рекурсивный guard проверены в subprocess.
|
||||
Дополнительно проверены жизненный цикл rwlock, повторные read-захваты,
|
||||
чужой unlock, занятое уничтожение, несколько одновременных читателей,
|
||||
ожидающий writer, преобразование таймаутов и `strlen` с NUL внутри строки.
|
||||
Добавлены проверки нативного инициализатора/экспорта, приоритета host API,
|
||||
отключения libc в strict-режиме, прав исполнения и границ новых metadata,
|
||||
совпадения идентичности при разных локальных ID, отказа при другой версии
|
||||
библиотеки и обработки TLS-релокаций. C-тест проверяет отдельные TLS-блоки
|
||||
в host-потоках, шаблон/BSS, запросы атрибутов и регистрацию heap API.
|
||||
Отдельный `sema-test` проверяет 32-битную запись handle без порчи соседнего
|
||||
поля, устаревшие ID, переполнение, таймаут, очередь из нескольких host-потоков,
|
||||
резервирование токенов, отмену/удаление с ожидающими и явный отказ от
|
||||
приоритетного блокирующего ожидания. Проверены положительные ошибки POSIX
|
||||
mutex, рекурсивный захват и выходные структуры `gettimeofday`.
|
||||
Linux-сборка выполнена с `-Wall -Wextra -Werror`.
|
||||
Актуальный интеграционный запуск настоящего eboot записан в `out/run.log`;
|
||||
результат старого режима — в `out/strict-run.log`.
|
||||
|
||||
## Следующие этапы настоящего порта
|
||||
|
||||
1. Реализовать загрузку/реестр системных модулей и необходимые контракты
|
||||
AppContent. Игра вызвала `sceSysmoduleLoadModule(0xb4)` из `0x2026149`;
|
||||
по справочной таблице этот ID означает `libSceAppContent`. Такого файла
|
||||
среди семи локальных `sce_module/*.prx` нет. Потребуется реализация
|
||||
используемых API и состояния модуля; простой возврат успеха не добавлен.
|
||||
2. Продолжить конструкторы, дополнить память/синхронизацию, затем реализовать
|
||||
TLS остальных модулей, запуск потоков и загрузку необходимых модулей.
|
||||
3. Дойти до `main`, добавить файловую систему (`/app0` → каталог ресурсов)
|
||||
и фиксировать каждый следующий неподдерживаемый контракт.
|
||||
4. Перевести GNM/PM4-команды, форматы ресурсов и AMD GCN-шейдеры в Vulkan/SPIR-V;
|
||||
затем связать VideoOut с окном и swapchain. Наличие Vulkan само по себе
|
||||
не делает графический код PS4 совместимым.
|
||||
5. Для статической рекомпиляции восстанавливать функции и контрольный поток,
|
||||
таблицы виртуальных вызовов, исключения и ABI; проверять поведение сравнением
|
||||
с исходным машинным кодом. На x86-64 прямое исполнение с совместимым runtime
|
||||
уже позволяет исследовать запуск без предварительного перевода всего CPU-кода.
|
||||
|
||||
Приоритет следующего изменения — Sysmodule/AppContent и следующие конструкторы.
|
||||
|
||||
## Справочные источники
|
||||
|
||||
Формат SELF и правила соотнесения сегментов сверены с
|
||||
[заголовками загрузчика shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/loader/elf.h)
|
||||
и [загрузкой сегментов](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/loader/elf.cpp).
|
||||
PS4 dynamic-таблицы и вычисление NID сверены с
|
||||
[module.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/module.cpp),
|
||||
контекст запуска — с [linker.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/linker.cpp).
|
||||
Сигнатуры mutex и значения типов сверены с
|
||||
[mutex.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/mutex.cpp)
|
||||
и [pthread.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/pthread.h),
|
||||
rwlock — с [rwlock.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/rwlock.cpp),
|
||||
сигнатуры direct memory — с
|
||||
[memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/memory.cpp).
|
||||
Семафоры сверены с
|
||||
[semaphore.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/semaphore.cpp),
|
||||
структуры времени — с
|
||||
[time.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/time.h).
|
||||
Идентификатор AppContent — с
|
||||
[sysmodule_table.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/sysmodule/sysmodule_table.h).
|
||||
Новые контракты регистрации heap API и procparam сверены с
|
||||
[linker.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/linker.h)
|
||||
и [process.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp),
|
||||
атрибуты потоков — с
|
||||
[pthread_attr.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/pthread_attr.cpp).
|
||||
Тело `_init_env` и устройство guards проверены дизассемблированием локальной libc.
|
||||
Реализация прототипа написана отдельно; зависимости на исполняемый shadPS4 нет.
|
||||
|
||||
Каталог `out/` содержит производные данные игры, имеет `.gitignore` и не нужен
|
||||
для распространения исходников инструмента.
|
||||
@@ -1,355 +0,0 @@
|
||||
# Уход от эмуляции: что сделано и что дальше (2026-10-06)
|
||||
|
||||
Исторический план и замеры по этапам разработки. В `0.5-pre2` новая модель остаётся
|
||||
экспериментальной и выключена по умолчанию; для NVIDIA и других не-AMD GPU добавлен модуль
|
||||
памяти без sparse-перепривязок. Актуальное состояние и проверки:
|
||||
[CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md).
|
||||
|
||||
## Где проходит граница API
|
||||
|
||||
Bloodborne собран со статической libGnm. Пакеты PM4 пишет не драйвер, а сама игра, функциями
|
||||
`sce::Gnm::*CommandBuffer` (`0x1068000–0x10a0000`, `0x11fd000–0x11fe000`). Перехват всех таких
|
||||
функций (`BB_GNM_OBSERVE=1`) показал: 100 % отправляемых пакетов пишут известные вызовы API, всего
|
||||
99 методов. Gnm — API, который пишет регистры: `setDepthStencilControl` = один `SET_CONTEXT_REG`.
|
||||
Поэтому разбор PM4 от этих 99 функций и трансляция самих вызовов дают одно и то же; PM4 здесь —
|
||||
формат записи вызовов, как командный список. Он уже разбирается параллельно (DrawPreparation).
|
||||
|
||||
Эмуляция железа — не разбор пакетов, а то, что делается вместо GPU:
|
||||
|
||||
| Что | Состояние |
|
||||
|---|---|
|
||||
| Модель памяти 0.2 (копии в VRAM, защита страниц, обратное чтение, ранние метки) | Запасной вариант. Модель ПК — по умолчанию, для NVIDIA режим «кусок на выделение». Удалить код 0.2 после проверки на NVIDIA. |
|
||||
| Метки EOP/EOS | Модель ПК: пишутся после завершения работы GPU (честно). |
|
||||
| `WaitRegMem` на метки той же очереди | Транслируется как выполненное ожидание (порядок очереди); ждать CPU не нужно. |
|
||||
| `WRITE_DATA`/DMA ждали все копии хоста | Ждут только пересекающиеся копии (как командный процессор, который не ждёт шейдеров). |
|
||||
| Constant Engine (CE RAM, дампы, счётчики) | Выполняется на CPU в порядке потока: наш разборщик читает описатели из памяти. |
|
||||
| SRT walker (код шейдера GCN, исполняемый на CPU) | Нужен по той же причине. |
|
||||
| Учёт записей GPU для разборщика (`PendingWriteOverlaps`, `IsRegionGpuModified`) | ~15 % потока GpuComm, та же причина. |
|
||||
| Кэш текстур по адресу + хеш | Та же причина плюс отсутствие объектов «текстура» в PM4. |
|
||||
|
||||
Почти вся оставшаяся эмуляция существует потому, что **транслятор читает описатели ресурсов
|
||||
(T#/V#/S#) из памяти игры на CPU во время разбора**. Драйвер ПК и DXVK так не делают: память
|
||||
приложения читает GPU во время выполнения.
|
||||
|
||||
## Дальше, по порядку
|
||||
|
||||
1. **NVIDIA.** Тестовая сборка `dist/Bloodborne-bbport-x86_64-test-1006.AppImage` (модель ПК по
|
||||
умолчанию, режим «кусок на выделение»). Если работает — удалить модель 0.2 целиком
|
||||
(`page_manager`, `fault_manager`, обратное чтение, BB_UFFD, предзагрузка, ранние метки).
|
||||
2. **Распараллелить стадию B** (запись отрисовок; на 4 ядрах занята на 99 %, ~3,3 мкс на
|
||||
отрисовку, равномерно по привязке буферов/текстур, началу прохода, вершинам). Варианты:
|
||||
(а) несколько потоков стадии B по диапазонам отрисовок внутри прохода рендеринга (у каждого
|
||||
свой сегмент команд; общие кэши уже под блокировками; порядок барьеров — по проходам);
|
||||
(б) разрешение привязок (буферы/текстуры → Vulkan) на стадии A, где 28 % простоя, а B только
|
||||
записывает. Нужны прогулочные тесты.
|
||||
3. **Описатели на GPU (главный шаг к «как DXVK»).** Шейдер читает T#/V#/S# сам (буферы на месте
|
||||
и BDA уже есть); текстуры — bindless-массив, T# → индекс через таблицу на GPU, которую
|
||||
ведёт кэш текстур при создании образов. Тогда не нужны SRT walker, CE на CPU, учёт ожидающих
|
||||
записей; `WRITE_DATA`/DMA/дампы CE становятся записями GPU в порядке потока. Большой проект.
|
||||
4. **Текстуры как объекты.** Перехват `Gnm::Texture::init*` и загрузчика TPF
|
||||
(`CSGXTextureCreateFragment`): образ создаётся при загрузке, а не угадывается по адресу;
|
||||
хеши и наложения адресов уходят. Ассеты можно заранее перевести в линейный вид (детайлинг
|
||||
один раз при установке).
|
||||
5. **Шейдеры заранее.** Все шейдеры GCN из файлов игры → SPIR-V при установке; без компиляции
|
||||
во время игры.
|
||||
|
||||
Что остаётся трансляцией, как у DXVK: шейдеры GCN → SPIR-V, описатели и форматы → Vulkan.
|
||||
|
||||
## Шаг 1 (2026-10-07): движок констант больше не синхронизируется с записью отрисовок
|
||||
|
||||
Замеры (`BB_CE_STATS=1`, Яр'гул): ~14 000 `DumpConstRam` в секунду, ~4–5 МБ/с, всего 25 страниц
|
||||
(кольцо ~100 КБ). Каждый дамп читается ровно одним указателем из user data — это таблицы
|
||||
описателей для SRT walker. Константы, вершины, индексы, большие буферы из этих страниц не
|
||||
читаются; шейдеров, читающих память самостоятельно (`uses_dma`), нет. То есть дампы CE нужны
|
||||
только транслятору — это построение таблиц описателей, а не данные для GPU.
|
||||
|
||||
Ожидания стадии A на дампах (`WaitForPendingReads`) были нужны только потому, что стадия B для
|
||||
отрисовок без подготовки сама перечитывала таблицы V# (описатели вершинных буферов) — к тому
|
||||
времени перезаписанные следующим дампом (~15 % V# оказывались чужими: баг DoF, issue #44).
|
||||
|
||||
Сделано:
|
||||
- стадия A снимает V# в пакет отрисовки, стадия B берёт их оттуда и таблицы не читает;
|
||||
- источник DMA отмечается точным диапазоном, а не «читает неизвестно что».
|
||||
|
||||
Итог: ожиданий на дампах ~850/с → ~46/с, стадия A ждёт 1,5 % времени вместо ~30 %
|
||||
(16 потоков). На 4 ядрах / 8 потоках FPS тот же (~170): там узкое место — стадия B
|
||||
(3,4 мкс на пакет; больше всего — `ObtainBuffer` 0,57 мкс: сопоставление адресов памяти игры с
|
||||
буферами GPU).
|
||||
|
||||
Записи командного процессора (`BB_CE_STATS`): `WRITE_DATA` ~8 400/с в 2 страницы (читаются как
|
||||
константы ~240/с), DMA ~1 000/с в 15 страниц. Они пишут данные для GPU, и их перевод — команды
|
||||
GPU в порядке потока (`vkCmdUpdateBuffer`, у DMA для памяти на месте уже так). Для `WRITE_DATA`
|
||||
это упирается в правило переноса часто записываемых GPU блоков в VRAM — делать вместе с
|
||||
упрощением модели памяти.
|
||||
|
||||
## Шаг 2 (2026-10-07): синхронизации стадии B
|
||||
|
||||
Стадия B (запись отрисовок) — узкое место на 4 ядрах (занята 99 %). Задачи кадра на ней:
|
||||
- сигнал flip = подготовка кадра презентером; 600 тыс. тактов на кадр, из них работы ~40 тыс.,
|
||||
остальное — ожидание потоков записи команд (`CommandBuffer()`) и синхронная отправка. Теперь
|
||||
~50 тыс.: презентер записывается через `Record`, отправка асинхронная;
|
||||
- проходы апскейлера пишут команды напрямую — для них отдельный сегмент без ожидания;
|
||||
- DMA (~900/с, 4 байта) ждали копии и сигналы даже когда запись делает GPU — ждут только для
|
||||
записей CPU.
|
||||
|
||||
Что осталось на стадии B (мкс на пакет, 3,5 всего): привязка ресурсов 1,0 (буферы 0,4, текстуры
|
||||
0,45), `ObtainBuffer` 0,6, начало прохода 0,26, вершины 0,34, задачи 0,2. Дальше —
|
||||
разделение стадии B или привязка ресурсов на стадии A (см. п. 2 выше).
|
||||
|
||||
## Шаг 3 (2026-10-08): метки пишет видеочип, перехваты без сигналов, NVIDIA
|
||||
|
||||
Что игра реально отправляет (новый счётчик `BB_DCB_STATS=1`: пакеты PM4 по типам, события,
|
||||
метки, исходы `WAIT_REG_MEM`; Кошмар охотника, у лампы, ~185 FPS): `EVENT_WRITE_EOP` 61 тыс./с
|
||||
(~330 за кадр, все без прерывания, 32- и 64-битные метки), `WAIT_REG_MEM` 31 тыс./с (все по
|
||||
памяти и все выполнены по меткам, ожидающим в порядке потока — процессор не ждёт ни разу),
|
||||
`EVENT_WRITE_EOS` 371/с, `WRITE_DATA` 3,5 тыс./с, `DUMP_CONST_RAM` 6,6 тыс./с, сбросы кэшей
|
||||
(`EVENT_WRITE`, `ACQUIRE_MEM`) — около 70 тыс./с. Не встречаются: occlusion-запросы
|
||||
(`ZPASS_DONE`), `SET_PREDICATION`, `COND_EXEC`, `MEM_SEMAPHORE`, `REWIND`, `COPY_DATA`, метки
|
||||
со временем GPU, отправки в очереди compute.
|
||||
|
||||
Сделано:
|
||||
- **Метки EOP/EOS пишет видеочип** (`Rasterizer::WriteLabelOnGpu`, `BB_GPU_LABELS=0` — как
|
||||
раньше): буферный маркер Vulkan (`VK_AMD_buffer_marker`, конец конвейера) прямо в память игры
|
||||
(кусок гостевой памяти), в той же точке потока, где метку пишет командный процессор PS4.
|
||||
64-битная метка — две 32-битные записи, старшая половина первой. Раньше метку писал поток
|
||||
процессора после завершения всей отправки Vulkan (61 тыс. записей в секунду). Метки с
|
||||
прерыванием по-прежнему сигналятся с процессора (прерывание — оттуда). То же для меток
|
||||
`RELEASE_MEM` очередей compute. EOS с записью из GDS — копия видеочипом в порядке потока вместо
|
||||
`Finish()` и чтения процессором.
|
||||
- **Перехваты кода игры без ловушек** (`gpu/shim/bbport_guest_hooks.cpp`, `BB_GUEST_HOOK_TRAPS=1`
|
||||
— ловушки `int3`): точки загрузчиков, аллокатора диапазонов и кучи GPU переходят в заглушки
|
||||
рядом с образом, которые вызывают обработчик и выполняют перекрытые инструкции; вызовы `memcpy`
|
||||
загрузчиков идут через трамплин. Раньше каждый вызов был сигналом ядра (SIGTRAP), ~1200 в
|
||||
секунду только у аллокатора.
|
||||
- **Новая модель на NVIDIA** (`gpu/shim/bbport_guest_memory.cpp`): кроме кусков dma-buf (AMD) —
|
||||
режим «память хоста, импортированная видеочипом» (`VK_EXT_external_memory_host`): кусок
|
||||
остаётся в memfd рантайма, видеочип импортирует его второе отображение. При запуске
|
||||
самопроверка в точности как в работе: импорт memfd, привязка к sparse-буферу, запись
|
||||
видеочипом и чтение процессором через другое отображение, и обратно. Не прошла — dma-buf
|
||||
(у NVIDIA «кусок на выделение»), для которого теперь меряется скорость чтения процессором
|
||||
(некэшируемое отображение замедлило бы код игры; меньше 1 ГБ/с — не используется). Не прошло
|
||||
и это — старая модель. Выбор: `BB_GUEST_MEMORY=host|dmabuf`. Переключатель в лаунчере доступен
|
||||
на AMD и NVIDIA. На AMD (RADV) импорт memfd драйвер не принимает — там dma-buf, как раньше.
|
||||
На NVIDIA не проверено: в журнале видно, какой путь выбран и почему.
|
||||
|
||||
Замеры (у лампы): 16 потоков — 233 FPS и с метками видеочипа, и без; 4 ядра / 8 потоков —
|
||||
~217 против ~215 FPS (в пределах шума; выигрыш — свободный поток сигналов).
|
||||
|
||||
### Что ещё осталось и почему
|
||||
|
||||
Всё, что осталось от эмуляции, держится на одном: **транслятор читает описатели ресурсов
|
||||
(V#/T#/S#) и таблицы с ними из памяти игры во время разбора**. Из-за этого:
|
||||
- `DUMP_CONST_RAM` выполняет процессор (дампы читает только транслятор — это его таблицы
|
||||
описателей, видеочип их не читает ни разу);
|
||||
- DMA и `WRITE_DATA` в память не «на месте» выполняет процессор (туда приходят код шейдеров и
|
||||
описатели, которые транслятор читает сразу);
|
||||
- SRT walker (код шейдера, переведённый в x86) обходит таблицы на процессоре;
|
||||
- шейдер специализируется по полям описателей (шаг, формат, перестановка каналов, тип
|
||||
изображения), конвейер выбирается по регистрам;
|
||||
- текстуры узнаются по адресу и формату из T#.
|
||||
|
||||
Перенос на видеочип — новый режим рекомпилятора: шейдер сам читает V# (адрес через BDA-арену,
|
||||
формат и шаг — во время выполнения), изображения через bindless-массив с таблицей T# → индекс,
|
||||
барьеры из сбросов кэшей самой игры (`ACQUIRE_MEM`/`EVENT_WRITE`), резидентность всей памяти
|
||||
сразу при отображении. Это несколько дней работы с риском для картинки: не начато.
|
||||
|
||||
Остальное — не эмуляция в смысле «вместо видеочипа»: перевод GCN → SPIR-V во время игры (как у
|
||||
DXVK), синтетический vblank (HLE VideoOut), заглушки для неиспользуемых игрой пакетов
|
||||
(occlusion — фиктивный счётчик, `SET_PREDICATION`, `COND_EXEC`, `MEM_SEMAPHORE`).
|
||||
|
||||
## Шаг 4 (2026-10-08): где проходит граница между эмуляцией и трансляцией
|
||||
|
||||
Замер (`BB_RES_STATS=1`, Кошмар охотника у лампы): ~97 тыс. привязок ресурсов в секунду; почти у
|
||||
каждой (0,96 стадии на привязку) описатели лежат в таблицах в памяти, а не в регистрах: на одну
|
||||
привязку буферы — 1,3 из регистров и 2,8 из памяти, текстуры — 0,6 и 3,3, сэмплеры — 1,0 и 0,9.
|
||||
|
||||
**Описатели на видеочипе для текстур сделать правильно нельзя.** Текстура PS4 лежит в памяти в
|
||||
раскладке PS4 (tiling), которой нет в Vulkan: её нужно перевести в образ Vulkan, а для этого
|
||||
процессор должен знать T# (адрес, формат, размеры) до того, как отрисовка выполнится. Если шейдер
|
||||
будет сам читать T# из памяти и искать образ в таблице на видеочипе, то при первом использовании
|
||||
текстуры образа ещё нет — правильного запасного пути у видеочипа нет (только кадр с неверной
|
||||
текстурой). Поэтому чтение описателей процессором (SRT walker, таблицы, которые заполняет движок
|
||||
констант) — не эмуляция, а трансляция привязки ресурсов, как у DXVK и vkd3d, которые тоже читают
|
||||
привязки на процессоре и создают по ним объекты Vulkan. Для буферов описатели на видеочипе
|
||||
возможны (BDA), но выигрыш — только запись дескрипторов (~0,1 мкс на пакет), если не переделать
|
||||
резидентность и барьеры целиком; не начато.
|
||||
|
||||
Что из эмуляции на самом деле осталось, и используется ли это Bloodborne:
|
||||
|
||||
| Что | Bloodborne |
|
||||
|---|---|
|
||||
| Ожидание флипа (метка VideoOut) блокирует поток разбора | да — это синхронизация с дисплеем, как ожидание в Present у DXVK |
|
||||
| Счётчик occlusion-запросов — фиктивный | **да**, ~60 событий `ZPASS_DONE` за кадр (в счётчике пакетов их не было видно: они во вложенных буферах); переведено — «Шаг 5» |
|
||||
| `SET_PREDICATION`, `COND_EXEC`, `COPY_DATA` не транслируются | нет; переведено — «Шаг 5» |
|
||||
| `MEM_SEMAPHORE`, `REWIND` — ожидание на процессоре | нет |
|
||||
| Очереди compute (ASC) — корутины в одном потоке | нет (0 отправок за кадр) |
|
||||
| Запись процессором в память, видимую видеочипу, «объявляется» (обёртки `memcpy`, перехваты загрузчиков, кучи, аллокатора, чтение файлов) | да — для копий в VRAM и текстур; `BB_WRITE_VERIFY` на маршруте не нашёл необъявленных записей, кроме ожидаемых |
|
||||
| Шейдер копирования игры — подмена (список читается процессором) | да; теперь можно транслировать (`BB_COPY_SHADER_NATIVE=1`) |
|
||||
|
||||
Строки «нет» нужны только при переносе на другие игры (Driveclub, inFAMOUS: там асинхронный
|
||||
compute и occlusion есть) — проверить их на Bloodborne нельзя.
|
||||
|
||||
Шейдер копирования (`cs fefebf9f`): подмену можно заменить трансляцией самого шейдера. Она нашла
|
||||
настоящую опасность новой модели: рядом с тем, куда он копирует, лежат код шейдеров и таблицы
|
||||
описателей, которые транслятор читает процессором; если эти блоки уходят в VRAM («видеочип пишет
|
||||
это 3 кадра из 60»), транслятор читает устаревший код (чёрная сцена). Подмена обходила это
|
||||
случайно. Теперь записи этого шейдера всегда остаются в памяти игры. Общее правило «то, что
|
||||
видеочип пишет, но сам не читает, остаётся в памяти игры» (как readback heap) пробовалось: ломает
|
||||
производительность (видеочип 4,2 → 6,1 мс за кадр — так попадают в оперативную память и области
|
||||
размером с цели рендера), убрано.
|
||||
|
||||
Константы: копирование в кольцо при разборе было нужно, пока записи командного процессора
|
||||
выполнялись процессором «наперёд». В новой модели оно больше не нужно для правильности —
|
||||
`BB_CONSTANTS_IN_PLACE=1` читает их на месте; по умолчанию выключено (видеочип +3 % из-за PCIe,
|
||||
FPS не выше). То есть кольцо констант теперь — оптимизация (копия в VRAM), а не эмуляция.
|
||||
|
||||
Дальше по трансляции для Bloodborne делать почти нечего; дальнейшая работа — производительность
|
||||
(стадия B на слабых процессорах) и перенос: настоящие запросы occlusion (`vkCmdBeginQuery` +
|
||||
копия результата видеочипом в память игры), предикация (`VK_EXT_conditional_rendering`),
|
||||
очереди compute как очередь Vulkan с семафорами — когда появится игра, которая их использует.
|
||||
|
||||
## Шаг 5 (2026-10-08): occlusion-запросы, COPY_DATA, COND_EXEC, предикация
|
||||
|
||||
Поправка к шагам 3 и 4: Bloodborne **использует** occlusion-запросы — около 60 событий
|
||||
`ZPASS_DONE` за кадр (~14 тыс. в секунду у лампы в Кошмаре охотника). Счётчик пакетов их не
|
||||
показал: он смотрел только буферы команд верхнего уровня. До сих пор игра получала фиктивную
|
||||
последовательность «всё видно».
|
||||
|
||||
Сделано (только новая модель; `BB_OCCLUSION_QUERIES=0` — как раньше, бит 4 второго числа
|
||||
`BB_TOGGLE_FILE` — переключение на ходу):
|
||||
|
||||
- **Occlusion-запросы → запросы Vulkan** (`vk_occlusion.{h,cpp}`, `occlusion_resolve.comp`). На
|
||||
PS4 каждый блок глубины ведёт счётчик сэмплов, прошедших тест глубины; событие `ZPASS_DONE`
|
||||
записывает счётчики (16 байт между блоками, бит готовности 63), игра берёт разность «после −
|
||||
до». Здесь с первого события всегда активен запрос Vulkan; он режется на отрезки на каждом
|
||||
событии и на каждой границе, через которую запрос нельзя протянуть (начало и конец прохода
|
||||
рендера, буфер команд — `Scheduler::CarriedScope`, Suspend/Resume). Событие запоминает, сколько
|
||||
отрезков закончилось до него. В ближайшей точке вне прохода результаты отрезков копируются, и
|
||||
один запуск шейдера складывает их по порядку и пишет каждому ожидающему событию его накопленное
|
||||
значение в память игры через адрес видеочипа (BDA). Проходы рендера не разрываются; цена на
|
||||
4 ядрах — в пределах шума (220 против 219 FPS с фиктивным счётчиком, видеочип 4,54 против
|
||||
4,56 мс). Результаты появляются чуть позже, чем на PS4, до этого бит готовности снят — игра
|
||||
проверяет его, как на PS4. Счётчики всегда остаются в памяти игры (их читает процессор).
|
||||
- **COPY_DATA**: значения, известные при разборе (константа, регистр, часы), записывает видеочип
|
||||
в порядке потока, как `WRITE_DATA`; копии из памяти и GDS — копии видеочипа в порядке потока,
|
||||
как `DMA_DATA`. Запись в регистр — пока предупреждение.
|
||||
- **COND_EXEC**: условие берётся из ожидающей метки в порядке потока, иначе — после того, как
|
||||
видеочип выполнил работу до пакета (раньше читалось при разборе, впереди видеочипа).
|
||||
- **SET_PREDICATION** (ZPass, Bool64, Bool32; CONTINUE — ИЛИ с предыдущим): пропускаются
|
||||
отрисовки и запуски, у которых в заголовке стоит бит предиката. Предикат вычисляется при
|
||||
первом таком пакете, после того как видеочип выполнил работу до него (правильно, но с
|
||||
ожиданием видеочипа; Bloodborne предикацию не использует). `VK_EXT_conditional_rendering` не
|
||||
подошёл: он пропускал бы и наши внутренние запуски (детайлинг текстур, копии), и не переходит
|
||||
через границы буферов команд.
|
||||
|
||||
Проверка: `BB_PM4_SELFTEST=1` — через ~40 с игры в отправки игры подмешиваются тестовые пакеты
|
||||
(в память, выделенную самой проверкой), через несколько секунд результаты сверяются: occlusion
|
||||
по отправке с отрисовками (> 0 сэмплов) и пустой интервал (0), `COPY_DATA` (32 и 64 бита,
|
||||
память → память в порядке, часы), `COND_EXEC` (ложь пропускает запись, истина выполняет),
|
||||
предикация (ZPass и Bool32: 2 из 4 пустых предикатных отрисовок пропущены). Все 8 — PASS. Слой
|
||||
валидации Vulkan на 67 тыс. событий не нашёл нарушений правил запросов.
|
||||
|
||||
Попутно найдена и исправлена взаимоблокировка: чтение памяти для процессора из потока разбора
|
||||
(`ReadMemory`) отправляло задачу этому же потоку и ждало её.
|
||||
|
||||
Что осталось без перевода: очереди compute (ASC) по-прежнему выполняются в том же потоке, что и
|
||||
графика, и отправляются в ту же очередь Vulkan (это допустимая сериализация); `MEM_SEMAPHORE`
|
||||
меняет счётчик процессором при разборе; время видеочипа в метках и `COPY_DATA` — часы процессора.
|
||||
Bloodborne ничего из этого не использует.
|
||||
|
||||
## Дополнение 2026-10-09: copy shader в модуле памяти и GPU-метки без AMD-расширения
|
||||
|
||||
В `BB_PC_MODEL=1 BB_LAYER_MEMORY=1` оригинальный `fefebf9f` теперь исполняется по умолчанию: paged stores обновляют память игры и актуальные зеркала. HLE доступен через `BB_COPY_SHADER_NATIVE=0`.
|
||||
|
||||
Без `VK_AMD_buffer_marker` метки EOP/EOS и применимый RELEASE_MEM пишет GPU стандартными Vulkan updates с барьерами. AMD сохраняет быстрые markers; `BB_GPU_LABELS_PORTABLE=1` позволяет проверить переносимый вариант. Подробности и аппаратные ограничения: [TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md).
|
||||
|
||||
## План от 2026-10-10: что осталось и как перевести на трансляцию
|
||||
|
||||
Требование: одинаково для AMD и NVIDIA и без опоры на код именно Bloodborne (адреса функций игры,
|
||||
хеши её шейдеров) там, где модель трансляции отвечает за правильность.
|
||||
|
||||
| Что осталось | Где | Почему это эмуляция или привязка |
|
||||
|---|---|---|
|
||||
| Модель памяти 0.3 (копии в VRAM с защитой страниц, обратное чтение, `fault_manager`, BB_UFFD, предзагрузка) | **по умолчанию** | делает вместо видеочипа то, чего у ПК нет: одна память |
|
||||
| Два пути новой модели: sparse-арена (AMD) и модуль памяти (остальные) | новая модель | sparse-перепривязки — путь одного производителя (на NVIDIA держат видеочип секундами) |
|
||||
| Записи загрузчиков в текстуры и буферы видны видеочипу через перехваты кода Bloodborne (`memcpy` игры в 0x216d420/0x2171d60, аллокатор 0x26aa070, куча TLSF) | новая модель | в другой игре этих адресов нет: записи остались бы незамеченными |
|
||||
| `MEM_SEMAPHORE` меняется при разборе (впереди видеочипа), время видеочипа в метках и `COPY_DATA` — часы процессора, очереди compute — в одном потоке с графикой | командный процессор | Bloodborne не использует; нужно для других игр |
|
||||
| CE и SRT walker читают описатели на процессоре, кэш текстур по адресу, синтетический vblank | — | трансляция привязок и платформенный слой, как у DXVK/vkd3d (шаг 4): остаётся |
|
||||
|
||||
Этапы:
|
||||
|
||||
1. **Один путь памяти для всех GPU.** Модуль памяти — по умолчанию и на AMD (на маршруте 2026-10-09
|
||||
вровень со sparse-ареной: p99 5,1–7,5 мс против 5,2–6,0). Sparse-арена — запасной вариант
|
||||
`BB_LAYER_MEMORY=0`. Заодно на AMD по умолчанию исполняется сам шейдер копирования игры вместо
|
||||
подмены по его хешу.
|
||||
2. **Согласование памяти без перехватов игры.** Записи процессора в память, которую видеочип
|
||||
держит как образ или копию, ловятся общим механизмом (ловушки записи по 64 КиБ, как у модуля),
|
||||
перехваты Bloodborne остаются ускорением (меньше срабатываний ловушек). Проверка: прогон с
|
||||
`BB_GUEST_HOOKS=0` и `BB_WRITE_VERIFY=1` (необъявленные записи), картинка и время кадра.
|
||||
3. **Остатки командного процессора — на видеочип.** `MEM_SEMAPHORE`: сигнал — запись видеочипа в
|
||||
порядке потока; время видеочипа — метки времени Vulkan, переведённые в часы PS4 (100 МГц) и
|
||||
записанные видеочипом. Проверка — `BB_PM4_SELFTEST` (Bloodborne их не использует). Очереди
|
||||
compute как отдельная очередь Vulkan — после, когда будет игра для проверки.
|
||||
4. **Новая модель по умолчанию** (после логов с NVIDIA на 0.5-pre4), затем удаление модели 0.3 и
|
||||
sparse-арены.
|
||||
|
||||
### Ход работ (2026-10-10)
|
||||
|
||||
- **Этап 1 сделан:** модуль памяти по умолчанию на всех GPU (на AMD тоже; sparse-арена —
|
||||
`BB_LAYER_MEMORY=0`), 0 вызовов `vkQueueBindSparse`.
|
||||
- **Модульность:** профили игр `gpu/games/` (`game_profile.h`, `bloodborne.cpp`): перехваты кода
|
||||
игры, её код, пишущий данные каждый кадр, хеш шейдера копирования — только там. Профиль
|
||||
выбирается по TITLE_ID; `BB_GAME_PROFILE=none` — игра как незнакомая, `BB_GUEST_HOOKS=0` — без
|
||||
перехватов.
|
||||
- **Этап 2, запись:** общий учёт ловушек записи в рантайме (`runtime_memory_trap`, причины по
|
||||
страницам; `gpu/layer/bblayer_write_traps.h`): копии модуля в VRAM и текстуры ловят записи на
|
||||
одних страницах, не мешая друг другу; `mprotect` игры ловушки сохраняет, новое отображение
|
||||
начинается без них (тест в `tests/test_runtime.c`). Страницы под текстурами под ловушками по
|
||||
умолчанию (`BB_IMAGE_TRAPS=0` — как раньше), записи libc и перехваченных загрузчиков
|
||||
объявляются до записи (без срабатываний). На маршруте с профилем — как без ловушек (4,18 мс
|
||||
видеочипа), 4–29 срабатываний за 5 с.
|
||||
- **Найдено попутно — разброс «некоторые запуски в 1,3–1,7 раза медленнее»:** «летучие» блоки
|
||||
модуля (блоки, в которые пишет процессор, в общей копии диапазона в VRAM) обновлялись копией
|
||||
перед каждой привязкой на чтение; копия разрывает проход рендера, и объёмы света (по отрисовке на
|
||||
каждый) рвали свой проход до ~100 раз за кадр (`BB_PASS_BREAK_TRACE=1`: 92–109 тыс. разрывов за
|
||||
5 с из `Runtime::CopyBuffer` против ~700). Летучие блоки выключены по умолчанию
|
||||
(`BB_LAYER_VOLATILE=1` — включить; тогда чтения внутри открытого прохода идут на месте): 10 из 10
|
||||
прогонов быстрые. Порядок «результаты occlusion до последующих меток»
|
||||
(`OcclusionQueries::WriteBeforeLabel`) оставлен как поведение PS4, но причиной не был: игра читает
|
||||
результаты через 2–10 мс и находит их записанными (`BB_OCCLUSION_READ_TRACE=1`, ловушки на
|
||||
чтение в рантайме — причины от 16 делают страницу без доступа).
|
||||
- **Незнакомая игра (этап 2, чтение):** прежний вывод «без профиля — снова медленный режим» был
|
||||
тем же разбросом из-за летучих блоков (один прогон). Без профиля маршрут идёт так же быстро
|
||||
(3 из 3, 4,29–4,30 мс) и выглядит так же. Ловушки на чтение для данных, которые есть только в
|
||||
VRAM (копия обратно при чтении процессором), остаются нужны для правильности в других играх;
|
||||
механизм причин «без доступа» в рантайме уже есть и проверен тестом.
|
||||
- **Этап 3 сделан (кроме отдельной очереди compute):**
|
||||
- `MEM_SEMAPHORE`: сигнал — в порядке потока, после завершения работы видеочипа перед ним
|
||||
(`RunMemSemaphoreSignal`, как метки EOP); ожидание сбрасывает конвейер записи и отправляет
|
||||
записанное, как `WAIT_REG_MEM`. `BB_MEM_SEMAPHORE_ORDER=0` — сигнал при разборе (как раньше).
|
||||
- Часы видеочипа: у PS4 100 МГц; теперь одна шкала для процессора и видеочипа — монотонные часы
|
||||
хоста в тиках по 10 нс (`GetGpuClock64`, раньше — настенные наносекунды). Метки EOP/RELEASE_MEM
|
||||
с часами и `COPY_DATA` из часов пишет сам видеочип: метка времени Vulkan → копия из запроса →
|
||||
короткий шейдер (`gpu_timestamp.comp`) переводит в шкалу PS4 и пишет в память игры
|
||||
(`vk_timestamps.cpp`). Привязка тиков к часам хоста — один раз, на простаивающем видеочипе
|
||||
(на RX 7800 XT точность 152 мкс). `BB_GPU_TIMESTAMPS=0` — пишет процессор.
|
||||
- Проверка — `BB_PM4_SELFTEST=1`: 11 из 11; новые проверки «сигнал не раньше предшествующей
|
||||
метки» (со старым порядком — FAIL), «ожидание и списание», «часы 100 МГц в шкале процессора»
|
||||
(2 с между метками: 1,961–1,999 с). Bloodborne ни то, ни другое не использует.
|
||||
- **Этап 2, чтение — готово, включается `BB_LAYER_READ_TRAPS=1`:** то, что видеочип записал только в
|
||||
копию в VRAM (запись привязки в зеркало), под ловушкой «без доступа» (причина `VramData` в
|
||||
рантайме); первое касание процессора (игра, транслятор, libc рантайма) копирует данные блока
|
||||
назад через очередь обратного чтения (`FastReadback`, как в модели 0.3: ждёт только тик этой
|
||||
записи) и снимает ловушку. Запись в файл из такой страницы сначала её читает (`touch_for_read`),
|
||||
чтение файла в неё — уже касалось (`touch_for_write`). На маршруте: 60 МиБ под ловушками, 0–1
|
||||
касание за 10 с, время видеочипа то же (4,28–4,30 мс). Bloodborne такие данные процессором не
|
||||
читает, поэтому само копирование назад здесь не проверено — по умолчанию выключено до игры,
|
||||
которой это нужно.
|
||||
- **Пересмотрено перед 0.5 (вечер 2026-10-10):** на AMD по умолчанию снова sparse-арена
|
||||
(`BB_LAYER_MEMORY=1` — модуль). Модуль шёл вровень с ареной на маршруте в Ярнаме, но в Сне
|
||||
охотника держал вне VRAM ~1 ГБ данных области (диапазоны с несколькими «неизвестными» блоками
|
||||
без летучих блоков остаются на месте): 100 FPS против 154 с ареной; с летучими блоками — 120, но
|
||||
с ~225 тыс. срабатываний ловушек в секунду. Нужен доступ по блокам для смешанных диапазонов.
|
||||
Occlusion-запросы как запросы Vulkan — по выбору (`BB_OCCLUSION_QUERIES=1`): с апскейлером
|
||||
(сцена меньше и со сдвигом) свечение лампы вспыхивало серым на весь кадр; счёт нужно сделать
|
||||
устойчивым к этому. Фоновая компиляция больше не пропускает полноэкранные проходы (серые кадры).
|
||||
+295
-256
@@ -3,14 +3,14 @@
|
||||
The port reads a few hundred environment variables. Some are for players (upscaler, frame rate,
|
||||
controls). Most are diagnostics and developer switches. This page lists every variable found in
|
||||
`src/`, `gpu/`, `bbsc/`, `scripts/`, `launcher/`, `run.sh`, `packaging/` and `tools/`, with what the
|
||||
code does with it. The information comes from the code on branch `feature/bbsc`. When a comment and
|
||||
code does with it. The information comes from the code on `main` (0.51-linux.16). When a comment and
|
||||
the code disagree, the code wins.
|
||||
|
||||
## How to set them
|
||||
|
||||
**In the launcher:** go to *Advanced → Developer → Extra variables (NAME=value, space-separated)*.
|
||||
The settings key is `extra_env` in the launcher's `settings.json`. The field is parsed in
|
||||
`game_environment()` (`launcher/bbport_launcher.py:557`):
|
||||
`game_environment()` (`launcher/bbport_launcher.py:706`):
|
||||
|
||||
```python
|
||||
for item in s["extra_env"].split():
|
||||
@@ -42,23 +42,29 @@ AppImage keeps its data (bbport.ini, saves, logs) in `BB_DATA_DIR`, by default
|
||||
**bbport.ini:** many graphics options live in `bbport.ini` and the in-game menu, not in the
|
||||
environment. A few variables override those keys at start: `BB_UPSCALER`, `BB_UPSCALE_PRESET`,
|
||||
`BB_FSR_SHARPNESS`, `BB_JITTER`, `BB_REACTIVE*` and `BB_OBJECT_MOTION`
|
||||
(`gpu/shim/bbport_settings.cpp:180-195`). Values set this way stay in force until the next start.
|
||||
(`gpu/shim/bbport_settings.cpp:192-208`). Values set this way stay in force until the next start.
|
||||
|
||||
## Precedence
|
||||
|
||||
The environment is built in this order. A later step wins.
|
||||
|
||||
1. **The environment the launcher (or the AppImage) was started with.**
|
||||
2. **The launcher's own settings** (`game_environment()`, `launcher/bbport_launcher.py:490-556`).
|
||||
2. **The launcher's own settings** (`game_environment()`, `launcher/bbport_launcher.py:632-711`).
|
||||
- Some are always written: `BB_GAME_DIR`, `BB_MODS_*`, `BB_PROMPTS`, `BB_GESTURES_NO_MOTION`,
|
||||
`BB_PATCHES_*`, `BB_LANGUAGE`, `BB_REGION`, `BB_MENU_LANGUAGE`, `BB_FULLSCREEN`,
|
||||
`BB_PRESENT_MODE`, `BB_SKIP_NETWORK_CHOICE`, `BB_FPS` and `BB_DISCORD`. For these, a shell value
|
||||
`BB_PRESENT_MODE`, `BB_SKIP_NETWORK_CHOICE`, `BB_FPS`, `BB_DISCORD` and `BB_STEAM_OVERLAY`. For these, a shell value
|
||||
is replaced.
|
||||
- Others are written only when the setting is on or not empty: `BB_HDR`, `BB_FPS_LIMIT`,
|
||||
`BB_DRAW_PIPE`, `BB_READBACKS`, `BB_PREUPLOAD`, `BB_PC_MODEL`, `BB_GPU_PROFILE` and others. For
|
||||
these, a shell value survives while the setting is off.
|
||||
`BB_DRAW_PIPE`, `BB_READBACKS`, `BB_PREUPLOAD`, `BB_PC_MODEL`, `BB_GPU_PROFILE`,
|
||||
`BB_PAD_DEADZONE` (only when the dead zone is not 4 %), `BB_PAD_LIGHTBAR` and others. For these, a shell value
|
||||
survives while the setting is off.
|
||||
- **Hunter's Atlas** (`atlas_environment()`, `launcher/bbport_launcher.py:611`): always
|
||||
`BB_ATLAS_ENABLED` (0 when the map is off or not prepared); with the map on also `BB_MAP_DIR`,
|
||||
`BB_MAP_ART_DIR`, `BB_MAP_FONT`, `BB_MAP_CATALOG`, `BB_MAP_CONFIG`, `BB_MAP_TRAVEL_ENABLED`,
|
||||
`BB_MAP_ITEMS_ENABLED`, `BB_MAP_ITEM_CATEGORIES`, `BB_MAP_FOG_ENABLED=0` and
|
||||
`BB_MAP_NPC_MODE=all`.
|
||||
3. **Online** (only when *Play online* is on and the online module exists): `scripts/online.py`
|
||||
`environment()` adds `BB_ONLINE`, `BB_SHADNET_*`, `BB_UPNP`, `BB_COMMUNITY_SERVER`,
|
||||
`environment()` adds `BB_ONLINE`, `BB_SHADNET_*`, `BB_UPNP`, `BB_COMMUNITY_SERVER`, `BB_SEAMLESSHUNT_TOMBS`,
|
||||
`SHADPS4_P2P_*` and the BBSC options (`scripts/bbsc.py`).
|
||||
4. **Extra variables come last** and override everything above, including the launcher's own
|
||||
settings and the BBSC options.
|
||||
@@ -66,8 +72,8 @@ The environment is built in this order. A later step wins.
|
||||
force a few:
|
||||
- `BB_AS_0_3=1` forces `BB_GUEST_IN_PLACE=0`, `BB_HOST_COPY_WAITS=all` and `BB_PRODUCER_CHECK=1`.
|
||||
- `BB_SAVE_LOG=1` forces `BB_FRAME_STATS=1`.
|
||||
- `BB_AUTO_RENDER_RES=1` makes `run.sh` **unset** `BB_RENDER_RES` and `BB_OUTPUT_RES`, so do not
|
||||
set it by hand.
|
||||
- `BB_AUTO_RENDER_RES=1` makes `run.sh` **unset** `BB_RENDER_RES`, `BB_OUTPUT_RES` and `BB_UI_RES`,
|
||||
so do not set it by hand.
|
||||
- When `bbport.ini` asks for a non-1080p output without live resolution and `BB_RENDER_RES` is
|
||||
unset, `run.sh` exports `BB_RENDER_RES`, `BB_OUTPUT_RES` and `BB_DMEM_MB` itself.
|
||||
- `BB_BBSC_LIB` is unset unless `BB_BBSC=1` and `BB_ONLINE=1`.
|
||||
@@ -79,8 +85,10 @@ The environment is built in this order. A later step wins.
|
||||
`BB_GPU_WRITE_TWINS=1`, `BB_GPU_WRITE_TWINS_MAX=65536`, `BB_PREUPLOAD=1`, `BB_UFFD=0`,
|
||||
`BB_GUEST_IN_PLACE=$BB_PC_MODEL`, `BB_VBLANK_HZ` (from `BB_FPS`) and `BB_PC_MODEL` (1 on AMD,
|
||||
0 elsewhere).
|
||||
6. **libbbsc** sets `BBSC_MOD_SEAMLESS=0` and `SHADPS4_BBSC_FIVE_HUNTERS=0` itself on the public
|
||||
server, whatever was asked (`bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:305`).
|
||||
6. **libbbsc** sets `BBSC_MOD_SEAMLESS=0` and `SHADPS4_BBSC_FIVE_HUNTERS=0` itself on a public game
|
||||
server (The Hunter's Dream, nodreamforthehunter.com, shadps4.net or scej-network.jp; Seamless
|
||||
Hunt is not one), whatever was asked (`bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:360`, the list
|
||||
in `bbsc/emulator/mod/bbsc_http.cpp:245`).
|
||||
|
||||
Value formats vary from one variable to another:
|
||||
|
||||
@@ -127,21 +135,21 @@ behaviour when the variable is unset. In the tables:
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_CJK_FONT` | path to a font file (unset: fontconfig's choice for zh-cn) | Font merged into the in-game menu font for Simplified Chinese texts; without one, fc-match picks a system font (Noto Sans CJK, WenQuanYi...), else Chinese text shows as boxes. | `gpu/shim/bbport_overlay.cpp:1073` |
|
||||
| `BB_DISPLAY` | monitor number (1, 2, ... in SDL order) or part of its name (unset: SDL's primary display) | **Launcher:** Graphics > Display > Monitor. The monitor the window and fullscreen go to (issue #69); `bb-gpu-capabilities --displays` lists them, and the log says which one was taken. No match: the primary one. | `gpu/shim/window.cpp:16`, `launcher/bbport_launcher.py:515` |
|
||||
| `BB_FULLSCREEN` | 1 or 0 (windowed) | **Launcher:** Graphics > Display > Fullscreen. Creates the game window fullscreen when the value starts with 1. | `gpu/shim/window.cpp:72`, `launcher/bbport_launcher.py:510` |
|
||||
| `BB_GPU_ID` | Vulkan device index (-1: automatic ranking) | Which GPU the game uses, by its index in the Vulkan device list; out of range asserts. bb-gpu-capabilities follows it too. | `gpu/shim/core/emulator_settings.h:22`, `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:146`, `tools/gpu_capabilities.c:248` |
|
||||
| `BB_HDR` | 1/y/t or unset (off) | **Launcher:** Graphics > Display > Allow HDR (set only when on). Allows an HDR swapchain (the colour-space extension and an HDR surface format) when the display offers it. | `gpu/shim/core/emulator_settings.h:60`, `gpu/shadps4/video_core/renderer_vulkan/vk_swapchain.cpp:246`, `launcher/bbport_launcher.py:517` |
|
||||
| `BB_CJK_FONT` | path to a font file (unset: fontconfig's choice for zh-cn) | Font merged into the in-game menu font for Simplified Chinese texts; without one, fc-match picks a system font (Noto Sans CJK, WenQuanYi...), else Chinese text shows as boxes. | `gpu/shim/bbport_overlay.cpp:1145` |
|
||||
| `BB_DISPLAY` | monitor number (1, 2, ... in SDL order) or part of its name (unset: SDL's primary display) | **Launcher:** Graphics > Display > Monitor. The monitor the window and fullscreen go to (issue #69); `bb-gpu-capabilities --displays` lists them, and the log says which one was taken. No match: the primary one. | `gpu/shim/window.cpp:16`, `launcher/bbport_launcher.py:657` |
|
||||
| `BB_FULLSCREEN` | 1 or 0 (windowed) | **Launcher:** Graphics > Display > Fullscreen. Creates the game window fullscreen when the value starts with 1. | `gpu/shim/window.cpp:116`, `launcher/bbport_launcher.py:652` |
|
||||
| `BB_GPU_ID` | Vulkan device index (-1: automatic ranking) | Which GPU the game uses, by its index in the Vulkan device list; out of range asserts. bb-gpu-capabilities follows it too. | `gpu/shim/core/emulator_settings.h:22`, `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:146`, `tools/gpu_capabilities.c:257` |
|
||||
| `BB_HDR` | 1/y/t or unset (off) | **Launcher:** Graphics > Display > Allow HDR (set only when on). Allows an HDR swapchain (the colour-space extension and an HDR surface format) when the display offers it. | `gpu/shim/core/emulator_settings.h:60`, `gpu/shadps4/video_core/renderer_vulkan/vk_swapchain.cpp:246`, `launcher/bbport_launcher.py:665` |
|
||||
| `BB_LIVE_RES` | 0 \| 1 (default: bbport.ini live_resolution, else off) | For output sizes other than 1080p: 1 = live resolution changes (guest at 1080p, host targets scaled at run time, change without restart; slower), 0 = startup resolution patch. Overrides bbport.ini live_resolution=0/1/auto. Outputs wider than 16:9 always take the startup patch. | `run.sh:75-122` |
|
||||
| `BB_MENU_LANGUAGE` | en, ru, es, it, fr, de, pt_BR, zh_CN (first two letters compared; unset: LC_ALL/LC_MESSAGES/LANG/LANGUAGE) | **Launcher:** the launcher's own language. Language of the in-game menu until one is chosen in it (bbport.ini menu_language wins). | `gpu/shim/bbport_settings.cpp:151`, `launcher/bbport_launcher.py:509` |
|
||||
| `BB_MENU_LANGUAGE` | en, ru, es, it, fr, de, pt_BR, zh_CN (first two letters compared; unset: LC_ALL/LC_MESSAGES/LANG/LANGUAGE) | **Launcher:** the launcher's own language. Language of the in-game menu until one is chosen in it (bbport.ini menu_language wins). | `gpu/shim/bbport_settings.cpp:165`, `launcher/bbport_launcher.py:651` |
|
||||
| `BB_NVIDIA_EGL` | 0 = off (default on) | Packaged builds with the host NVIDIA driver: sets __EGL_VENDOR_LIBRARY_FILENAMES to the host's EGL vendor files, NVIDIA first (issue #107); 0 leaves it unchanged. | `launcher/bbport_vulkan.py:116-124` |
|
||||
| `BB_NVIDIA_LIB_DIR` | directory (default none) | Packaged builds: an extra directory searched for the host NVIDIA Vulkan driver library. | `launcher/bbport_vulkan.py:74-76` |
|
||||
| `BB_OUTPUT_RES` | WxH | **Internal** (run.sh exports it from bbport.ini output_res for non-1080p outputs without live resolution; unset again on restart). Output size the upscaler fills when the scene is rendered at a startup-patched size (BB_RENDER_RES); also passed to scripts/patches.py. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:199`, `run.sh:116`, `run.sh:170` |
|
||||
| `BB_PRESENT_MODE` | Mailbox, Fifo, FifoRelaxed, Immediate (Mailbox) | **Launcher:** Graphics > Display > Present mode. Vulkan present mode; a mode the driver lacks falls back to Fifo. Any other value logs an error and uses Mailbox. | `gpu/shim/core/emulator_settings.h:25`, `gpu/shadps4/video_core/renderer_vulkan/vk_swapchain.cpp:281`, `launcher/bbport_launcher.py:511` |
|
||||
| `BB_OUTPUT_RES` | WxH | **Internal** (run.sh exports it from bbport.ini output_res for non-1080p outputs without live resolution; unset again on restart). Output size the upscaler fills when the scene is rendered at a startup-patched size (BB_RENDER_RES); also passed to scripts/patches.py. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:199`, `run.sh:119`, `run.sh:190` |
|
||||
| `BB_PRESENT_MODE` | Mailbox, Fifo, FifoRelaxed, Immediate (Mailbox) | **Launcher:** Graphics > Display > Present mode. Vulkan present mode; a mode the driver lacks falls back to Fifo. Any other value logs an error and uses Mailbox. | `gpu/shim/core/emulator_settings.h:25`, `gpu/shadps4/video_core/renderer_vulkan/vk_swapchain.cpp:281`, `launcher/bbport_launcher.py:653` |
|
||||
| `BB_PRESENT_THREAD` | 0 = off; else on (on) | Swapchain work runs on a separate swap thread next to the present thread; 0 runs it on the present thread. | `gpu/shadps4/core/libraries/videoout/driver.cpp:76-81` |
|
||||
| `BB_RENDER_RES` | WxH (unset = live resolution path; scene at the menu's preset) | Explicitly sets the game's render resolution with a patch at start (the older path). Fixes the scene size: presets and resolution then need a restart, and TAA at another size is refused. Above 1920x1080 run.sh raises BB_DMEM_MB to 9152. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:208`, `run.sh:71`, `run.sh:124` |
|
||||
| `BB_UI_RES` | WxH (unset: 1920x1080) | **Internal** (run.sh exports it from `patches.py --print-scaled` for outputs wider than 16:9 on the startup patch, unless bbport.ini aspect_patch=0; unset again on restart). The game's UI coordinate space: 1080 high and as wide as the output's aspect ratio (2580x1080 for 3440x1440), the HUD canvas width patches.py writes (ultrawide_writes). The UI composition recognizes UI draws by this size and scales them to the output; scripts/hud_layout.py lays out the menu movies for it. | `gpu/shadps4/video_core/renderer_vulkan/ui_composition.h:24`, `run.sh:119-122`, `run.sh:135-143` |
|
||||
| `LC_ALL / LC_MESSAGES / LANG / LANGUAGE` | locale | In-game menu language when BB_MENU_LANGUAGE is unset; the launcher also derives its UI and game language from the system locale. | `gpu/shim/bbport_settings.cpp:151-163`, `launcher/bbport_i18n.py:4944-4968` |
|
||||
| `BB_RENDER_RES` | WxH (unset = live resolution path; scene at the menu's preset) | Explicitly sets the game's render resolution with a patch at start (the older path). Fixes the scene size: presets and resolution then need a restart, and TAA at another size is refused. Above 1920x1080 run.sh raises BB_DMEM_MB to 9152. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:208`, `run.sh:71`, `run.sh:119` |
|
||||
| `BB_UI_RES` | WxH (unset: 1920x1080) | **Internal** (run.sh exports it from `patches.py --print-scaled` for outputs wider than 16:9 on the startup patch, unless bbport.ini aspect_patch=0; unset again on restart). The game's UI coordinate space: 1080 high and as wide as the output's aspect ratio (2580x1080 for 3440x1440), the HUD canvas width patches.py writes (ultrawide_writes). The UI composition recognizes UI draws by this size and scales them to the output; scripts/hud_layout.py lays out the menu movies for it. | `gpu/shadps4/video_core/renderer_vulkan/ui_composition.h:24`, `run.sh:119-122`, `run.sh:124` |
|
||||
| `LC_ALL / LC_MESSAGES / LANG / LANGUAGE` | locale | In-game menu language when BB_MENU_LANGUAGE is unset; the launcher also derives its UI and game language from the system locale. | `gpu/shim/bbport_settings.cpp:151-163`, `launcher/bbport_i18n.py:4984-5010` |
|
||||
| `VK_ADD_DRIVER_FILES` | ICD JSON list | Packaged builds: prepended to the bundled driver list. | `launcher/bbport_vulkan.py:72-73` |
|
||||
| `VK_DRIVER_FILES` | ICD JSON list (default: loader's) | Vulkan loader's driver list. run.sh --software sets Lavapipe; packaged builds set it to the host NVIDIA ICD plus bundled Mesa unless you set it (or VK_ICD_FILENAMES) yourself; dropped when it points into Steam's pressure-vessel. | `run.sh:4-13`, `launcher/bbport_vulkan.py:66-80`, `packaging/bbport-entry.c:37-49` |
|
||||
| `VK_ICD_FILENAMES` | ICD JSON list | Older name of VK_DRIVER_FILES; if set, packaged builds leave driver selection alone. | `launcher/bbport_vulkan.py:66-67` |
|
||||
@@ -151,109 +159,110 @@ behaviour when the variable is unset. In the tables:
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_ANISO` | integer (16); 0 = the game's own samplers | Mipmapped linear samplers of graphics draws get this anisotropic filtering ratio instead of the PS4's (sharper floors, stairs and walls at grazing angles). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3509` |
|
||||
| `BB_AUTO_NOHIZ` | 0 = off (default 1) | On GCN 3–5 GPUs under RADV (Polaris, Vega and their APUs) run.sh sets RADV_DEBUG=nohiz to avoid black halos/missing geometry from depth compression; 0 disables that. Ignored if you set RADV_DEBUG yourself. | `run.sh:184-195` |
|
||||
| `BB_ANISO` | integer (16); 0 = the game's own samplers | Mipmapped linear samplers of graphics draws get this anisotropic filtering ratio instead of the PS4's (sharper floors, stairs and walls at grazing angles). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3595` |
|
||||
| `BB_AUTO_NOHIZ` | 0 = off (default 1) | On GCN 3–5 GPUs under RADV (Polaris, Vega and their APUs) run.sh sets RADV_DEBUG=nohiz to avoid black halos/missing geometry from depth compression; 0 disables that. Ignored if you set RADV_DEBUG yourself. | `run.sh:212` |
|
||||
| `BB_DLSS` | 0 = off; unset = on when available | NVIDIA DLSS through the optional bridge library (libbbport_dlss.so + NVIDIA's libnvidia-ngx-dlss). 0 disables it; without the libraries or on other GPUs nothing changes. | `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.cpp:176`, `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.h:8` |
|
||||
| `BB_FSR1` | 1/y/t or 0 (off) | shadPS4's FSR 1 pass in the presenter (spatial upscale of the final image); separate from the port's upscalers (bbport.ini upscaler). | `gpu/shim/core/emulator_settings.h:59`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:175` |
|
||||
| `BB_FSR411_DIR` | directory (unset = fsr4_411 next to run.sh) | **Internal** (run.sh sets it to <data>/fsr4_411 when that exists). Where the FSR 4.1.1 assets built by tools/fsr4cap/build_assets.sh are looked up. | `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.cpp:244`, `run.sh:34`, `launcher/bbport_launcher.py:357` |
|
||||
| `BB_FSR411_DIR` | directory (unset = fsr4_411 next to run.sh) | **Internal** (run.sh sets it to <data>/fsr4_411 when that exists). Where the FSR 4.1.1 assets built by tools/fsr4cap/build_assets.sh are looked up. | `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.cpp:244`, `run.sh:34`, `launcher/bbport_launcher.py:462` |
|
||||
| `BB_FSR411_VARIANT` | int8 / fp8emu (unset = FP8 when the GPU supports it and its assets exist, else INT8) | Forces which FSR 4.1.1 model variant runs: int8 skips FP8; fp8emu uses the emulated-FP8 set (needs FP16 cooperative matrices). | `gpu/shadps4/video_core/renderer_vulkan/fsr411/fsr411.cpp:295` |
|
||||
| `BB_FSR4_DIR` | directory (fsr4_shaders) | Where the FSR 4 (v07) SPIR-V passes and weights from tools/fetch_fsr4_assets.sh are loaded from. | `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.cpp:26`, `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.h:7` |
|
||||
| `BB_FSR4_LINEAR` | 0 = off; unset/other = on | FSR 4 gets the game's tonemapped sRGB frame decoded to linear light and the output encoded again. 0 (or toggle bit 63): the encoded frame as before (A/B). | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1824` |
|
||||
| `BB_FSR4_LINEAR` | 0 = off; unset/other = on | FSR 4 gets the game's tonemapped sRGB frame decoded to linear light and the output encoded again. 0 (or toggle bit 63): the encoded frame as before (A/B). | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1853` |
|
||||
| `BB_FSR4_OPT` | 0 = originals; unset/other = optimized | FSR 4 uses the fixed/faster passes from fsr4_shaders/opt/ (tools/fsr4_optimize.sh) when present. 0 keeps AMD's original passes. | `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.cpp:126` |
|
||||
| `BB_FSR_SHARPNESS` | number 0–2 (bbport.ini sharpness, default 0.5) | Upscaler sharpening (RCAS) strength; overrides the bbport.ini/menu value "sharpness" at start. Above 1 an extra sharpen pass adds the rest. | `gpu/shim/bbport_settings.cpp:188`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1269` |
|
||||
| `BB_INTEL_TONEMAP_FIX` | 0 \| 1 (default: on for Intel CPUs, off otherwise) | Applies the community "Intel Black Tonemap Fix" patch (tone mapping turns black on Intel CPUs, DLC areas most); 0/1 forces it off or on. | `scripts/patches.py:59-62`, `scripts/patches.py:94-96` |
|
||||
| `BB_INTERP_INT_FIX` | 1 = on, 0 = off (unset = on for GPUs without fragment-shader barycentrics, e.g. older NVIDIA/Pascal) | Forces the integer-interpolation workaround in translated shaders on or off. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:407` |
|
||||
| `BB_JITTER` | 0/1 (1; overrides bbport.ini jitter) | Camera jitter for the temporal upscalers. | `gpu/shim/bbport_settings.cpp:187`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:354` |
|
||||
| `BB_FSR_SHARPNESS` | number 0–2 (bbport.ini sharpness, default 0.5) | Upscaler sharpening (RCAS) strength; overrides the bbport.ini/menu value "sharpness" at start. Above 1 an extra sharpen pass adds the rest. | `gpu/shim/bbport_settings.cpp:199`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1276` |
|
||||
| `BB_INTEL_TONEMAP_FIX` | 0 \| 1 (default: on for Intel CPUs, off otherwise) | Applies the community "Intel Black Tonemap Fix" patch (tone mapping turns black on Intel CPUs, DLC areas most); 0/1 forces it off or on. | `scripts/patches.py:59-62`, `scripts/patches.py:100` |
|
||||
| `BB_INTERP_INT_FIX` | 1 = on, 0 = off (unset = on for GPUs without fragment-shader barycentrics, e.g. older NVIDIA/Pascal) | Forces the integer-interpolation workaround in translated shaders on or off. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:414` |
|
||||
| `BB_JITTER` | 0/1 (1; overrides bbport.ini jitter) | Camera jitter for the temporal upscalers. | `gpu/shim/bbport_settings.cpp:199`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:354` |
|
||||
| `BB_MOTION_LARGE` | 0 = off; unset/other = on | Large bone palettes (16–64 KiB, the large beasts) get object motion vectors like small skeletons. 0 leaves them to camera vectors as before. | `gpu/shadps4/video_core/renderer_vulkan/motion_history.h:46` |
|
||||
| `BB_OBJECT_MOTION` | 0/1 (bbport.ini object_motion, off by default) | Per-object motion vectors for the temporal upscaler: less ghosting on characters, about 10% FPS. The variable overrides bbport.ini at start. | `gpu/shim/bbport_settings.cpp:191`, `gpu/shadps4/video_core/renderer_vulkan/vk_object_motion.cpp:58` |
|
||||
| `BB_OCCLUSION_QUERIES` | 1 = on (off; BB_PM4_SELFTEST=1 also turns it on) | Opt-in: the game's occlusion queries become real Vulkan occlusion queries. Off: counters are a fixed "everything visible" sequence (the 0.3 model), because with an upscaler jittered tests made light glows flash. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:238` |
|
||||
| `BB_OBJECT_MOTION` | 0/1 (bbport.ini object_motion, off by default) | Per-object motion vectors for the temporal upscaler: less ghosting on characters, about 10% FPS. The variable overrides bbport.ini at start. | `gpu/shim/bbport_settings.cpp:202`, `gpu/shadps4/video_core/renderer_vulkan/vk_object_motion.cpp:58` |
|
||||
| `BB_OCCLUSION_QUERIES` | 1 = on (off; BB_PM4_SELFTEST=1 also turns it on) | Opt-in: the game's occlusion queries become real Vulkan occlusion queries. Off: counters are a fixed "everything visible" sequence (the 0.3 model), because with an upscaler jittered tests made light glows flash. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:253` |
|
||||
| `BB_ZPASS_IN_ORDER` | 0 = off; unset/other = on | Without BB_OCCLUSION_QUERIES: the stand-in occlusion counts are written in order with the draws, after the DMA that zeroes the query's block, as on the PS4. Written ahead of it, the game's queries never finished and it made new ones until it read a null handle (issue #23). 0: written at decode, as before. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:588` |
|
||||
| `BB_RCAS` | 1/y/t or 0 (1) | RCAS sharpening in shadPS4's presenter FSR 1 pass (only with BB_FSR1); not the in-game Sharpening (RCAS) setting. | `gpu/shim/core/emulator_settings.h:65`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:176` |
|
||||
| `BB_RCAS_ATTENUATION` | integer, thousandths (250 = 0.25) | RCAS attenuation of shadPS4's presenter FSR 1 pass. | `gpu/shim/core/emulator_settings.h:26`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:178` |
|
||||
| `BB_REACTIVE` | 0/1 (bbport.ini reactive, off by default) | Reactive mask for the upscaler (opt-in): on Bloodborne's thin mist it trades trails for jitter shimmer. Overrides bbport.ini at start. | `gpu/shim/bbport_settings.cpp:189`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:947` |
|
||||
| `BB_REACTIVE_MAX` | 0-1 (0.9; overrides bbport.ini reactive_max) | Upper bound of the TAA reactive mask (never fully reactive). | `gpu/shim/bbport_settings.cpp:188`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1064` |
|
||||
| `BB_REACTIVE_SCALE` | 0-16 (1; overrides bbport.ini reactive_scale) | Reactive mask = relative colour change times this scale. | `gpu/shim/bbport_settings.cpp:187`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1064` |
|
||||
| `BB_REACTIVE_THRESHOLD` | 0-1 (0.2; overrides bbport.ini reactive_threshold) | Reactive mask is zero below this colour change. | `gpu/shim/bbport_settings.cpp:188`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1064` |
|
||||
| `BB_UPSCALER` | off / fsr3 / fsr4 / fsr411 / taa / dlss / none (unset = bbport.ini upscaler / in-game menu) | Overrides the upscaler chosen in bbport.ini/menu at start; any unknown value means off. none also disables the upscaler machinery entirely (no camera tracking, the menu cannot turn it on). scripts/patches.py only distinguishes fsr3 from everything else. | `gpu/shim/bbport_settings.cpp:181`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:190`, `scripts/patches.py:422` |
|
||||
| `BB_UPSCALE_PRESET` | 0 Native AA, 1 Quality, 2 Balanced, 3 Performance, 4 Ultra Performance (overrides bbport.ini preset) | Upscaler preset at start. Note: run.sh's startup render size still comes from bbport.ini; only `patches.py --print-preset-size` honours it. | `gpu/shim/bbport_settings.cpp:188`, `scripts/patches.py:424` |
|
||||
| `RADV_DEBUG` | RADV flags (run.sh may set nohiz) | Mesa RADV debug flags; if you set it, run.sh's automatic nohiz for GCN 3–5 (BB_AUTO_NOHIZ) is skipped. | `run.sh:184-195` |
|
||||
| `BB_REACTIVE` | 0/1 (bbport.ini reactive, off by default) | Reactive mask for the upscaler (opt-in): on Bloodborne's thin mist it trades trails for jitter shimmer. Overrides bbport.ini at start. | `gpu/shim/bbport_settings.cpp:200`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:954` |
|
||||
| `BB_REACTIVE_MAX` | 0-1 (0.9; overrides bbport.ini reactive_max) | Upper bound of the TAA reactive mask (never fully reactive). | `gpu/shim/bbport_settings.cpp:201`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1071` |
|
||||
| `BB_REACTIVE_SCALE` | 0-16 (1; overrides bbport.ini reactive_scale) | Reactive mask = relative colour change times this scale. | `gpu/shim/bbport_settings.cpp:200`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1071` |
|
||||
| `BB_REACTIVE_THRESHOLD` | 0-1 (0.2; overrides bbport.ini reactive_threshold) | Reactive mask is zero below this colour change. | `gpu/shim/bbport_settings.cpp:201`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1071` |
|
||||
| `BB_UPSCALER` | off / fsr3 / fsr4 / fsr411 / taa / dlss / none (unset = bbport.ini upscaler / in-game menu) | Overrides the upscaler chosen in bbport.ini/menu at start; any unknown value means off. none also disables the upscaler machinery entirely (no camera tracking, the menu cannot turn it on). scripts/patches.py only distinguishes fsr3 from everything else. | `gpu/shim/bbport_settings.cpp:192`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:190`, `scripts/patches.py:511` |
|
||||
| `BB_UPSCALE_PRESET` | 0 Native AA, 1 Quality, 2 Balanced, 3 Performance, 4 Ultra Performance (overrides bbport.ini preset) | Upscaler preset at start. Note: run.sh's startup render size still comes from bbport.ini; only `patches.py --print-preset-size` honours it. | `gpu/shim/bbport_settings.cpp:202`, `scripts/patches.py:513` |
|
||||
| `RADV_DEBUG` | RADV flags (run.sh may set nohiz) | Mesa RADV debug flags; if you set it, run.sh's automatic nohiz for GCN 3–5 (BB_AUTO_NOHIZ) is skipped. | `run.sh:212` |
|
||||
|
||||
## Performance & memory
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_ASYNC_PIPELINES` | 0 = off; anything else / unset = on | New graphics pipelines of passes drawn every frame compile on worker threads while their draws are skipped for a frame or two, instead of the game standing still for the driver. 0: compiled on the GPU thread, the game waits. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:729`, `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.h:212` |
|
||||
| `BB_ASYNC_PIPELINE_THREADS` | integer ≥1 (unset = CPU threads / 4, clamped 1–4) | Number of worker threads for BB_ASYNC_PIPELINES. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:638` |
|
||||
| `BB_ASYNC_PIPELINES` | 0 = off; anything else / unset = on | New graphics pipelines of passes drawn every frame compile on worker threads while their draws are skipped for a frame or two, instead of the game standing still for the driver. 0: compiled on the GPU thread, the game waits. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:734`, `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.h:212` |
|
||||
| `BB_ASYNC_PIPELINE_THREADS` | integer ≥1 (unset = CPU threads / 4, clamped 1–4) | Number of worker threads for BB_ASYNC_PIPELINES. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:643` |
|
||||
| `BB_ASYNC_SUBMIT` | 0 = off; unset/other = on | Submissions go out from the recording threads once their segments are recorded, instead of the GPU thread waiting for them (~65 µs each). Only with recording worker threads. | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:150` |
|
||||
| `BB_CONSTANTS_IN_PLACE` | 1 = on (off) | With BB_GUEST_IN_PLACE: small read-only buffers (constants) are read by the GPU where the game wrote them instead of being copied when the draw is decoded or recorded. Experiment toggle bit 1 inverts it at run time. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:507`, `gpu/shadps4/video_core/buffer_cache/buffer.h:57` |
|
||||
| `BB_CONSTRAM_PIPE` | 0 = off; unset/other = on | With the two-stage draw pipe: before the command processor writes guest memory with the CPU (constant RAM dump), it waits only for the queued packets that read those bytes, not all of them. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:883`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:114` |
|
||||
| `BB_COPY_GPU_BUFFERS` | first char 1/y/t = on; run.sh exports 1 when unset (code default without run.sh: off) | **Internal.** The command buffers of each submission are copied when the game submits them and decoded from the copy (the game reused that memory before the GPU thread got to it: "Unimplemented PM4 type 0" after loading a save). 0 decodes guest memory directly. | `run.sh:221-224`, `gpu/shim/core/emulator_settings.h:56`, `gpu/shadps4/video_core/amdgpu/liverpool.h:252` |
|
||||
| `BB_CONSTRAM_PIPE` | 0 = off; unset/other = on | With the two-stage draw pipe: before the command processor writes guest memory with the CPU (constant RAM dump), it waits only for the queued packets that read those bytes, not all of them. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:900`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:114` |
|
||||
| `BB_COPY_GPU_BUFFERS` | first char 1/y/t = on; run.sh exports 1 when unset (code default without run.sh: off) | **Internal.** The command buffers of each submission are copied when the game submits them and decoded from the copy (the game reused that memory before the GPU thread got to it: "Unimplemented PM4 type 0" after loading a save). 0 decodes guest memory directly. | `run.sh:276`, `gpu/shim/core/emulator_settings.h:56`, `gpu/shadps4/video_core/amdgpu/liverpool.h:252` |
|
||||
| `BB_COPY_MERGE_KB` | integer KiB (64) | Maximum gap between buffer copies of the game's copy shader (HLE path) that are merged into one batch. | `gpu/shadps4/video_core/renderer_vulkan/vk_shader_hle.cpp:203` |
|
||||
| `BB_COPY_SHADER_NATIVE` | 0 = HLE; unset/other = native | With the game's memory in place, the game's buffer copy shader runs as itself (translated). 0: the copy list is read on the CPU and copied by the port (older HLE path). Experiment bit 2 inverts it at run time. | `gpu/shadps4/video_core/renderer_vulkan/vk_shader_hle.cpp:272` |
|
||||
| `BB_COPY_THREADS` | 0-16 (a quarter of the available hardware threads, 1-4) | Number of threads that split large guest memory copies. | `gpu/shim/bbport_copy.cpp:30`, `src/runtime_memory.c:692` |
|
||||
| `BB_COPY_THREADS` | 0-16 (a quarter of the available hardware threads, 1-4) | Number of threads that split large guest memory copies. | `gpu/shim/bbport_copy.cpp:30`, `src/runtime_memory.c:738` |
|
||||
| `BB_DIRECT_SEGMENT` | 0 = off; unset/other = on | With BB_ASYNC_SUBMIT, commands recorded on the GPU thread (e.g. upscaler passes) get their own command-buffer segment instead of waiting for the recording threads (~90 µs). 0: the wait. | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:647` |
|
||||
| `BB_DMEM_MB` | MiB, 5056–16384 (default 5056; run.sh sets 9152 for render sizes above 1080p) | Direct memory size reported to the game (retail PS4: 5056 MiB); the resolution patches above 1080p need more or the game exits at start. | `src/runtime_memory.c:22-30`, `run.sh:139`, `run.sh:144-148` |
|
||||
| `BB_DRAW_PIPE` | 1 = on, 0 = off (unset = on with 8+ CPU threads) | **Launcher:** Advanced > Performance > "Two-stage GPU pipeline" (Auto/On/Off). Two-stage GPU command pipeline (decode and record on separate threads): 20–30% faster; turn off if unstable. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:813`, `launcher/bbport_launcher.py:523` |
|
||||
| `BB_FAULT_SKIP_TEXTURES` | 0 = off; unset/other = on | A write fault on a page no tracked image protects skips the texture cache lock (guest threads no longer wait 0.3–0.6 s behind texture uploads). 0: every fault takes the lock, as before. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:4254` |
|
||||
| `BB_DMEM_MB` | MiB, 5056–16384 (default 5056; run.sh sets 9152 for render sizes above 1080p) | Direct memory size reported to the game (retail PS4: 5056 MiB); the resolution patches above 1080p need more or the game exits at start. | `src/runtime_memory.c:22-30`, `run.sh:132`, `run.sh:132` |
|
||||
| `BB_DRAW_PIPE` | 1 = on, 0 = off (unset = on with 8+ CPU threads) | **Launcher:** Advanced > Performance > "Two-stage GPU pipeline" (Auto/On/Off). Two-stage GPU command pipeline (decode and record on separate threads): 20–30% faster; turn off if unstable. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:830`, `launcher/bbport_launcher.py:670` |
|
||||
| `BB_FAULT_SKIP_TEXTURES` | 0 = off; unset/other = on | A write fault on a page no tracked image protects skips the texture cache lock (guest threads no longer wait 0.3–0.6 s behind texture uploads). 0: every fault takes the lock, as before. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:4339` |
|
||||
| `BB_FAULT_WINDOW` | KiB, clamped 4-1024 and rounded up to a power of two (256) | Size of the window unprotected around a guest write fault (write tracking), so neighbouring writes do not fault again. Window at or below one tracker page = feature off. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:691-698` |
|
||||
| `BB_FPS_LIMIT` | frames per second; 0 or unset = no limit | **Launcher:** Graphics > Frame rate > FPS limit (set only when > 0). Frames per second the present thread lets through. | `gpu/shim/core/emulator_settings.h:51`, `launcher/bbport_launcher.py:521` |
|
||||
| `BB_FPS_LIMIT` | frames per second; 0 or unset = no limit | **Launcher:** Graphics > Frame rate > FPS limit (set only when > 0). Frames per second the present thread lets through. | `gpu/shim/core/emulator_settings.h:51`, `launcher/bbport_launcher.py:668` |
|
||||
| `BB_FRAMES_AHEAD` | integer ≥0 (1); 0 = no bound | The GPU command thread runs at most this many guest frames ahead of the GPU, so GPU-bound frames finish at an even rate instead of in bursts. | `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:475` |
|
||||
| `BB_GARLIC_VRAM` | 0 = off; any other value on (unset = on, except on integrated GPUs) | With the PC memory model, Garlic memory (what the GPU reads a lot: geometry, textures) gets a VRAM copy kept current like a default heap; 0 keeps it in place too. Ignored when the fixed arena is in use. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:474-479`, `2189-2192` |
|
||||
| `BB_GC_IDLE_SECONDS` | seconds, clamped 1-63 (20) | Texture collector: below the pressure mark an image is evicted once unused for this long (the textures of an area left behind). | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1343`, `1367-1371` |
|
||||
| `BB_GC_PRESSURE_IDLE_SECONDS` | seconds, clamped 0-63 (5) | Texture collector: under VRAM pressure an image is evicted once unused for this long (over the critical mark for 1 s first, then by submissions). | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1372-1381` |
|
||||
| `BB_GC_IDLE_SECONDS` | seconds, clamped 1-63 (20) | Texture collector: below the pressure mark an image is evicted once unused for this long (the textures of an area left behind). | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1396`, `1367-1371` |
|
||||
| `BB_GC_PRESSURE_IDLE_SECONDS` | seconds, clamped 0-63 (5) | Texture collector: under VRAM pressure an image is evicted once unused for this long (over the critical mark for 1 s first, then by submissions). | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1430-1439` |
|
||||
| `BB_GC_PRESSURE_PERCENT` | percent, clamped 10-75 (60) | Texture collector, dedicated GPUs below 8 GB: the pressure mark is at least this share of the budget (was 40 %, about what the game holds outside textures, so such cards were under pressure all session). The critical mark is unchanged. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:62-76` |
|
||||
| `BB_GC_TILE_BACK` | 0 = off; else on (on) | Texture collector: past the critical mark, a GPU-written tiled image unused for a minute is tiled back into the game's memory before it is freed (issues #13, #14). 0 = dropped unwritten as before, which left the Hunter's Dream black. Multisampled images are always dropped. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1422-1441`, `201-250` |
|
||||
| `BB_GPU_COMMAND_WRITES` | 0 = off; unset/other = on | With BB_GUEST_IN_PLACE: the game's WRITE_DATA commands are performed by the GPU in stream order, as the PS4 command processor does, instead of a CPU store at decode time. 0: CPU store. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:212`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:201` |
|
||||
| `BB_GC_KEEP_GPU_WRITTEN` | 0 = off; else on (on) | Texture collector: images the GPU wrote stay in VRAM until past the critical mark and unused for a minute, linear ones like tiled ones (issue #30). 0: linear ones are downloaded and freed under mere pressure, as before. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1504` |
|
||||
| `BB_GC_TILE_BACK` | 0 = off; else on (on) | Texture collector: past the critical mark, a GPU-written tiled image unused for a minute is tiled back into the game's memory before it is freed (issues #13, #14), unless that memory changed since the GPU began writing the image (the game's heap had reused it: written back, it broke the heap, a crash at 0x2085xxx, #25, #32, #35; GPU-written linear images freed past the critical mark are checked the same way, with either memory model); such an image is dropped. 0 = dropped unwritten as before, which left the Hunter's Dream black. Multisampled images are always dropped. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1486-1521`, `201-284` |
|
||||
| `BB_GPU_COMMAND_WRITES` | 0 = off; unset/other = on | With BB_GUEST_IN_PLACE: the game's WRITE_DATA commands are performed by the GPU in stream order, as the PS4 command processor does, instead of a CPU store at decode time. 0: CPU store. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:212`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:204` |
|
||||
| `BB_GPU_LABELS` | 0 = off; 1 = on; unset = on except on AMD GFX8 (Polaris, RX 4xx/5xx) | With BB_GUEST_IN_PLACE and honest labels: end-of-pipe labels (fences) are written by the GPU itself into the game's memory. 0: by a CPU thread after the GPU. GFX8 stalled with GPU-written labels (upstream #203), so it gets the CPU's unless 1. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:301-316` |
|
||||
| `BB_GPU_SPIN_US` | microseconds, negative = 0 (200 with 12+ hardware threads, else 0) | The GPU command thread spins this long before sleeping when it runs out of work, so the next of the ~20 submissions of a frame is picked up without a condition-variable wake-up. 0 = no spinning (saves CPU on small machines). | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:163-171` |
|
||||
| `BB_GPU_TIMESTAMPS` | 0 = off; unset/other = on | With BB_GUEST_IN_PLACE: the game's GPU timestamps (100 MHz clock) are written by the GPU in stream order. 0: written by the CPU. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:280` |
|
||||
| `BB_GPU_WRITE_TWINS` | 1 = on; run.sh exports 1 when unset (code default without run.sh: off) | **Internal.** A guest write into a page holding bytes the GPU is still writing (counters, small compute outputs) does not wait for the GPU: the guest's bytes are kept as a "twin". Not with BB_READBACKS=2. | `run.sh:225-227`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:291-306` |
|
||||
| `BB_GPU_WRITE_TWINS_MAX` | bytes per page; run.sh exports 65536 when unset (code default without run.sh: 1024) | **Internal.** Only pages with at most this many GPU-written bytes get twins; larger outputs (vertices the CPU reads back, FaceGen) keep waiting for the GPU. | `run.sh:227`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:296`, `308-313` |
|
||||
| `BB_GUEST_GPU_MEMORY` | 1 = on (off; implied by BB_GUEST_IN_PLACE=1 on a capable GPU) | Guest direct memory comes from GPU-visible Vulkan chunks (dma-buf, or host import on NVIDIA) instead of the memfd, so the GPU reads the game's data in place. Disables BB_UFFD (dma-buf cannot be write-protected by userfaultfd). | `gpu/shim/bbport_guest_memory.cpp:700-718`, `gpu/shadps4/video_core/page_manager.cpp:723-728`, `run.sh:153` |
|
||||
| `BB_GUEST_IN_PLACE` | 1 = on, anything else = off | **Internal** (run.sh: = BB_PC_MODEL, forced 0 by BB_AS_0_3=1; set by hand overrides the memory model). The GPU uses the game's direct memory where it is (GPU-visible chunks) instead of copies in VRAM, and command-processor writes/labels/timestamps are done by the GPU in order. Part of the new memory model (BB_PC_MODEL=1). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:515`, `gpu/shadps4/video_core/buffer_cache/buffer.h:38`, `run.sh:199` |
|
||||
| `BB_GPU_TIMESTAMPS` | 0 = off; unset/other = on | With BB_GUEST_IN_PLACE: the game's GPU timestamps (100 MHz clock) are written by the GPU in stream order. 0: written by the CPU. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:290` |
|
||||
| `BB_GPU_WRITE_TWINS` | 1 = on; run.sh exports 1 when unset (code default without run.sh: off) | **Internal.** A guest write into a page holding bytes the GPU is still writing (counters, small compute outputs) does not wait for the GPU: the guest's bytes are kept as a "twin". Not with BB_READBACKS=2. | `run.sh:279`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:291-306` |
|
||||
| `BB_GPU_WRITE_TWINS_MAX` | bytes per page; run.sh exports 65536 when unset (code default without run.sh: 1024) | **Internal.** Only pages with at most this many GPU-written bytes get twins; larger outputs (vertices the CPU reads back, FaceGen) keep waiting for the GPU. | `run.sh:279`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:296`, `308-313` |
|
||||
| `BB_GUEST_GPU_MEMORY` | 1 = on (off; implied by BB_GUEST_IN_PLACE=1 on a capable GPU) | Guest direct memory comes from GPU-visible Vulkan chunks (dma-buf, or host import on NVIDIA) instead of the memfd, so the GPU reads the game's data in place. Disables BB_UFFD (dma-buf cannot be write-protected by userfaultfd). | `gpu/shim/bbport_guest_memory.cpp:700-718`, `gpu/shadps4/video_core/page_manager.cpp:723-728`, `run.sh:195` |
|
||||
| `BB_GUEST_IN_PLACE` | 1 = on, anything else = off | **Internal** (run.sh: = BB_PC_MODEL, forced 0 by BB_AS_0_3=1; set by hand overrides the memory model). The GPU uses the game's direct memory where it is (GPU-visible chunks) instead of copies in VRAM, and command-processor writes/labels/timestamps are done by the GPU in order. Part of the new memory model (BB_PC_MODEL=1). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:515`, `gpu/shadps4/video_core/buffer_cache/buffer.h:38`, `run.sh:239` |
|
||||
| `BB_GUEST_MEMORY` | dmabuf, host, or unset (AMD: dma-buf chunks; other GPUs: host memory import first) | How the game's direct memory is made GPU-visible in the new memory model: dma-buf chunks or imported host memory (VK_EXT_external_memory_host). `host` fails instead of falling back when import does not work. | `gpu/shim/bbport_guest_memory.cpp:668` |
|
||||
| `BB_GUEST_VRAM` | 1 or unset (off) | Experiment: the game's direct memory in VRAM the CPU maps through the PCI BAR (Resizable BAR), as GDDR is the PS4's one memory: no VRAM copies. | `gpu/shim/bbport_guest_memory.cpp:37` |
|
||||
| `BB_HONEST_FLUSH_US` | integer µs (2000; a header comment says 1000) | With honest labels: at the end of a guest submission, recorded work that signals wait for is submitted at most once per this interval while the GPU still has work. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:383` |
|
||||
| `BB_HONEST_IDLE_BACKLOG` | integer bytes (65536) | With honest labels: work is not submitted early "because the GPU is idle" while this many bytes of draws are still queued for the recording thread. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:391` |
|
||||
| `BB_HONEST_IDLE_FLUSH` | 0 = off; unset/other = on | With honest labels: signalled work is submitted at once when the GPU has finished everything submitted (instead of waiting for BB_HONEST_FLUSH_US). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:395` |
|
||||
| `BB_HONEST_FLUSH_US` | integer µs (2000; a header comment says 1000) | With honest labels: at the end of a guest submission, recorded work that signals wait for is submitted at most once per this interval while the GPU still has work. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:400` |
|
||||
| `BB_HONEST_IDLE_BACKLOG` | integer bytes (65536) | With honest labels: work is not submitted early "because the GPU is idle" while this many bytes of draws are still queued for the recording thread. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:416` |
|
||||
| `BB_HONEST_IDLE_FLUSH` | 0 = off; unset/other = on | With honest labels: signalled work is submitted at once when the GPU has finished everything submitted (instead of waiting for BB_HONEST_FLUSH_US). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:412` |
|
||||
| `BB_HONEST_LABELS` | 1 = on (unset = on only with BB_GUEST_IN_PLACE) | Fences/labels (EOP, EOS, mem semaphores) are written once the GPU has finished the work before them, as the hardware does, instead of when that work is recorded. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:98`, `gpu/shadps4/video_core/amdgpu/liverpool.cpp:448` |
|
||||
| `BB_HOST_COPY_WAITS` | all (unset = only when needed) | **Internal** (run.sh sets all with BB_AS_0_3=1). all: before every guest write, wait for all host copies (the old 0.3 behaviour). Default: wait only when a host copy may still read the written range. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:130`, `run.sh:197` |
|
||||
| `BB_HOST_COPY_WAITS` | all (unset = only when needed) | **Internal** (run.sh sets all with BB_AS_0_3=1). all: before every guest write, wait for all host copies (the old 0.3 behaviour). Default: wait only when a host copy may still read the written range. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:130`, `run.sh:239` |
|
||||
| `BB_INDIRECT_GUARD` | 0 = off; unset/other = on | Guards indirect compute dispatches: implausible group counts (garbage from particle simulations that caused device lost) are zeroed and the dispatch skipped, and counted in the log. 0: the game's counts as they are. | `gpu/shadps4/video_core/renderer_vulkan/vk_indirect_guard.cpp:57`, `gpu/shadps4/video_core/renderer_vulkan/vk_indirect_guard.h:11` |
|
||||
| `BB_INDIRECT_DRAW_GUARD` | 0 = off; unset/other = on | Guards indirect draws: a vertex (or index) count times the instance count past BB_INDIRECT_DRAW_GUARD_MAX (garbage that would keep the GPU busy past the driver's timeout, issue #19) is zeroed and the draw skipped, and counted in the log. An open render pass is paused for the check and continued. 0: the game's arguments as they are. | `gpu/shadps4/video_core/renderer_vulkan/vk_indirect_draw_guard.cpp:60`, `gpu/shadps4/video_core/renderer_vulkan/vk_indirect_draw_guard.h:13` |
|
||||
| `BB_INDIRECT_DRAW_GUARD_MAX` | integer 1–4294967295, decimal or 0x hex (16777216); invalid keeps the default | Most vertices (or indices) times instances one indirect draw may run before BB_INDIRECT_DRAW_GUARD skips it. | `gpu/shadps4/video_core/renderer_vulkan/vk_indirect_draw_guard.cpp:42` |
|
||||
| `BB_INDIRECT_GUARD_MAX` | integer 1–4294967295, decimal or 0x hex (4194304); invalid keeps the default | Most workgroups one indirect dispatch may launch before BB_INDIRECT_GUARD skips it. | `gpu/shadps4/video_core/renderer_vulkan/vk_indirect_guard.cpp:35` |
|
||||
| `BB_LAYER_MEMORY` | 1 = on, 0 = off (unset = chosen by the memory model per GPU) | The layer's memory module: the game's memory bound in place with VRAM copies, without sparse binding (NVIDIA's sparse binding stalls the GPU for seconds). 0: AMD's sparse arena. See BB_PC_MODEL. | `gpu/shim/bbport_guest_memory.cpp:546`, `run.sh:162` |
|
||||
| `BB_LAYER_MIRROR_FLOOR_PERCENT` | percent, max 90 (25) | Layer memory module: share of the driver's VRAM budget the VRAM mirrors keep against the texture collector; above it they yield to images, below it images yield. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3824-3833` |
|
||||
| `BB_LAYER_MEMORY` | 1 = on, 0 = off (unset = chosen by the memory model per GPU) | The layer's memory module: the game's memory bound in place with VRAM copies, without sparse binding (NVIDIA's sparse binding stalls the GPU for seconds). 0: AMD's sparse arena. See BB_PC_MODEL. | `gpu/shim/bbport_guest_memory.cpp:546`, `run.sh:204` |
|
||||
| `BB_LAYER_MIRROR_FLOOR_PERCENT` | percent, max 90 (25) | Layer memory module: share of the driver's VRAM budget the VRAM mirrors keep against the texture collector; above it they yield to images, below it images yield. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3936` |
|
||||
| `BB_LAYER_MIRROR_GROWTH_MB` | MiB; 0 = none (64) | Layer memory module: the most room a growing VRAM mirror gets above its current end. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:610-616` |
|
||||
| `BB_LAYER_RELOAD_RESET` | 0 = off; else on (on) | Layer memory module: a block the GPU once wrote in place that the loaders then write whole may have a VRAM copy again (dynamic blocks piled up over area loads and lamp travel, lowering the frame rate). 0: kept in place for good, as before. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2560` |
|
||||
| `BB_LAYER_UNMAP_RESET` | 0 = off; else on (on) | Layer memory module: blocks the game unmaps whole lose what kept them in place (CPU writes, GPU writes in place, failed watches); memory mapped there again starts as new. 0: as before. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3272` |
|
||||
| `BB_LAYER_RELOAD_RESET` | 0 = off; else on (on) | Layer memory module: a block the GPU once wrote in place that the loaders then write whole may have a VRAM copy again (dynamic blocks piled up over area loads and lamp travel, lowering the frame rate). 0: kept in place for good, as before. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2596` |
|
||||
| `BB_LAYER_UNMAP_RESET` | 0 = off; else on (on) | Layer memory module: blocks the game unmaps whole lose what kept them in place (CPU writes, GPU writes in place, failed watches); memory mapped there again starts as new. 0: as before. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3308` |
|
||||
| `BB_LAYER_READ_TRAPS` | 1 = on (off) | What the GPU writes into a VRAM copy only is made no-access for the CPU until it is copied back, so the CPU never reads stale data. Off by default for now. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:1285` |
|
||||
| `BB_LAYER_WATCH` | 0 = off; else on (on; needs BB_LAYER_WRITE_TRAPS) | Layer memory module: blocks the CPU writes unannounced get VRAM copies too, write-trapped and uploaded again after the CPU's next write. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:410-424` |
|
||||
| `BB_PC_MODEL` | 1 = new \| 0 = 0.3 model (default: 1 on AMD GPUs, 0 elsewhere) | **Launcher:** Advanced → "Memory and translation model" (Auto leaves it to run.sh). Memory model: 1 = the GPU uses the game's memory in place and the command processor's work is translated (implies BB_GUEST_IN_PLACE=1); 0 = 0.3 model (VRAM copies, write tracking). A value in extra variables wins over "Auto". | `run.sh:156-183`, `run.sh:199`, `launcher/bbport_launcher.py:534-540` |
|
||||
| `BB_PC_MODEL` | 1 = new \| 0 = 0.3 model (default: 1 on AMD GPUs, 0 elsewhere) | **Launcher:** Advanced → "Memory and translation model" (Auto leaves it to run.sh). Memory model: 1 = the GPU uses the game's memory in place and the command processor's work is translated (implies BB_GUEST_IN_PLACE=1); 0 = 0.3 model (VRAM copies, write tracking). A value in extra variables wins over "Auto". | `run.sh:212`, `run.sh:199`, `launcher/bbport_launcher.py:684` |
|
||||
| `BB_PC_MODEL_ANY_GPU` | 1 or unset (off) | Lets the new memory model's sparse arena run on non-AMD GPUs that would otherwise fall back to the 0.3 model (NVIDIA: rebinding memory held the GPU for seconds). | `gpu/shim/bbport_guest_memory.cpp:513`, `gpu/shim/bbport_guest_memory.h:35` |
|
||||
| `BB_PIPELINE_CACHE` | 1/y/t or 0 (1) | The port's pipeline cache: shaders are compiled again at startup from the stored list (warm-up). 0 turns it off. | `gpu/shim/core/emulator_settings.h:64`, `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_serialization.cpp:330` |
|
||||
| `BB_PIPE_SIGNALS` | 0 = off; unset/other = on (with honest labels and the draw pipe) | Fences, GPU-idle/frame signals and their submissions are handed to the draw recording thread as ordered tasks instead of draining it first (~15 waits a frame). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:359`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:234` |
|
||||
| `BB_PIPE_SIGNALS` | 0 = off; unset/other = on (with honest labels and the draw pipe) | Fences, GPU-idle/frame signals and their submissions are handed to the draw recording thread as ordered tasks instead of draining it first (~15 waits a frame). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:376`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:234` |
|
||||
| `BB_PIPE_SPIN_US` | integer µs ≥0 (200 with 12+ CPU threads, else 50) | How long the draw-pipe consumer spins waiting for work before sleeping. Lower values leave more CPU to the game on few-core machines (Steam Deck). | `gpu/shadps4/video_core/renderer_vulkan/vk_draw_pipe.h:209` |
|
||||
| `BB_PREP_WORKERS` | integer 0–16 (half the CPU threads, clamped 1–8) | Number of idle-priority worker threads that prepare draws speculatively ahead of the GPU thread (pipeline and resource selection). 0 = none. | `gpu/shadps4/video_core/renderer_vulkan/vk_draw_prep.cpp:54` |
|
||||
| `BB_PREUPLOAD` | 0 = off, 1 = rewritten memory already in VRAM only (no new VRAM), 2 = all of the game's GPU memory (~3 GB more VRAM); without run.sh unset = 0 | **Launcher:** Advanced > Performance > "Background pre-upload into VRAM" (run.sh exports 1 when unset). Background upload of the game's GPU memory into VRAM ahead of use: fewer hitches when areas load. Always off with BB_GUEST_IN_PLACE (the GPU reads the game's memory itself). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:386`, `run.sh:155`, `launcher/bbport_launcher.py:527` |
|
||||
| `BB_PREUPLOAD_MB` | integer MiB per frame (16) | Most memory the background pre-upload (BB_PREUPLOAD) puts into VRAM per frame. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1602` |
|
||||
| `BB_READBACKS` | 0 = off, 1 = relaxed, 2 = precise (1 = relaxed) | **Launcher:** Advanced > Performance > "GPU data readbacks by the CPU" (Relaxed = unset, Off = 0, Precise = 2). shadPS4's GPU readback mode. Relaxed by default: without readbacks FaceGen reads stale GPU-written vertices (vertex explosions). 2 also read-protects GPU-written pages and disables BB_GPU_WRITE_TWINS. | `gpu/shim/core/emulator_settings.h:27-29`, `launcher/bbport_launcher.py:165`, `524-525` |
|
||||
| `BB_READBACK_LINEAR` | 1/y/t or 0 (off) | shadPS4's readback of linear (untiled or narrow) images to guest memory. | `gpu/shim/core/emulator_settings.h:66`, `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:784` |
|
||||
| `BB_READBACK_QUEUE` | 0 = off; unset/other = on | Uses a separate compute/transfer queue to read back GPU data finished long ago, without waiting behind the queued frame. 0 turns it off. | `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:452` |
|
||||
| `BB_PREUPLOAD` | 0 = off, 1 = rewritten memory already in VRAM only (no new VRAM), 2 = all of the game's GPU memory (~3 GB more VRAM); without run.sh unset = 0 | **Launcher:** Advanced > Performance > "Background pre-upload into VRAM" (run.sh exports 1 when unset). Background upload of the game's GPU memory into VRAM ahead of use: fewer hitches when areas load. Always off with BB_GUEST_IN_PLACE (the GPU reads the game's memory itself). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:386`, `run.sh:197`, `launcher/bbport_launcher.py:674` |
|
||||
| `BB_PREUPLOAD_MB` | integer MiB per frame (16) | Most memory the background pre-upload (BB_PREUPLOAD) puts into VRAM per frame. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1625` |
|
||||
| `BB_READBACKS` | 0 = off, 1 = relaxed, 2 = precise (1 = relaxed) | **Launcher:** Advanced > Performance > "GPU data readbacks by the CPU" (Relaxed = unset, Off = 0, Precise = 2). shadPS4's GPU readback mode. Relaxed by default: without readbacks FaceGen reads stale GPU-written vertices (vertex explosions). 2 also read-protects GPU-written pages and disables BB_GPU_WRITE_TWINS. | `gpu/shim/core/emulator_settings.h:27-29`, `launcher/bbport_launcher.py:672`, `524-525` |
|
||||
| `BB_READBACK_LINEAR` | 1/y/t or 0 (off) | shadPS4's readback of linear (untiled or narrow) images to guest memory. | `gpu/shim/core/emulator_settings.h:66`, `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:812` |
|
||||
| `BB_READBACK_QUEUE` | 0 = off; unset/other = on | Uses a separate compute/transfer queue to read back GPU data finished long ago, without waiting behind the queued frame. 0 turns it off. | `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:499` |
|
||||
| `BB_STAGING_KEEP_MB` | integer MiB (512) | How much of the staging upload ring is kept (and populated at startup) instead of being trimmed when idle; re-creating blocks during streaming bursts caused stutter. | `gpu/shadps4/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp:27` |
|
||||
| `BB_SUBMIT_AHEAD` | integer >= 0 (2) | The first K submissions of a frame do not wait for the previous frame, so translation overlaps the GPU (1080p Yahar'gul: 101 -> 154 FPS). 8 or more leaked the game's heap; if the heap grows anyway K becomes 0 for the session. | `gpu/shadps4/core/libraries/gnmdriver/gnmdriver.cpp:134-149` |
|
||||
| `BB_TEXTURE_HELPER` | 1 = on, 0 = off (unset = off) | A spinning helper thread binds textures for the draw recording thread. Measured neutral on 16 threads, so opt-in. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2165` |
|
||||
| `BB_UFFD` | first char 1 = on (run.sh: 0) | **Internal** (run.sh exports 0 when unset). Write tracking with userfaultfd write-protection instead of mprotect (no address-space write lock); falls back to mprotect if the kernel lacks it. Off by default: the Steam Deck crashed with it. Ignored with BB_GUEST_GPU_MEMORY=1. | `run.sh:216-220`, `gpu/shadps4/video_core/page_manager.cpp:545-551`, `723-737` |
|
||||
| `BB_VBLANK_HZ` | Hz; 0 or >120 = uncapped (60 if unset in the library) | **Internal** (run.sh derives it from BB_FPS (uncap 480, 90 90, else 60). Vblank rate. Above 120 Hz (or 0) a finished frame is presented at once instead of on the next vblank tick; 60/90 are the fixed-timestep presets. | `gpu/shim/core/emulator_settings.h:30`, `run.sh:228` |
|
||||
| `BB_TEXTURE_HELPER` | 1 = on, 0 = off (unset = off) | A spinning helper thread binds textures for the draw recording thread. Measured neutral on 16 threads, so opt-in. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2195` |
|
||||
| `BB_UFFD` | first char 1 = on (run.sh: 0) | **Internal** (run.sh exports 0 when unset). Write tracking with userfaultfd write-protection instead of mprotect (no address-space write lock); falls back to mprotect if the kernel lacks it. Off by default: the Steam Deck crashed with it. Ignored with BB_GUEST_GPU_MEMORY=1. | `run.sh:272`, `gpu/shadps4/video_core/page_manager.cpp:545-551`, `723-737` |
|
||||
| `BB_VBLANK_HZ` | Hz; 0 or >120 = uncapped (60 if unset in the library) | **Internal** (run.sh derives it from BB_FPS (uncap 480, 90 90, else 60). Vblank rate. Above 120 Hz (or 0) a finished frame is presented at once instead of on the next vblank tick; 60/90 are the fixed-timestep presets. | `gpu/shim/core/emulator_settings.h:30`, `run.sh:280` |
|
||||
| `BB_VK_RECORD_THREAD` | 0 = off; unset/other = on | Vulkan commands are recorded on dedicated recording threads. 0 records on the calling thread (suspected in vertex explosions, issue #39). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:120` |
|
||||
| `BB_VK_RECORD_THREADS` | integer 1–8 (3 with 12+ CPU threads, 2 with 8+, else 1) | Number of Vulkan recording threads; the command stream is cut into segments recorded in parallel. | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:97` |
|
||||
| `BB_VK_SEGMENT_KB` | integer KiB ≥1 (64) | Size of command-stream segments handed to the recording threads (~60 draws per 64 KiB). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:127` |
|
||||
| `BB_VMA_BLOCK_MB` | integer MiB ≥1 (64; VMA's own default is 256) | Size of Vulkan memory allocator blocks. Smaller blocks are returned to the driver sooner when freed. | `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:826` |
|
||||
| `BB_VMA_DEDICATED_MB` | MiB; 0 = never (4) | Images of this size or more get dedicated memory, returned to the driver when freed (shared VMA blocks left ~0.9 GB of holes after texture evictions). | `gpu/shadps4/video_core/texture_cache/image.cpp:107-113` |
|
||||
| `BB_VRAM_IDLE_SECONDS` | seconds (60) | Mirrored (VRAM-copied) blocks not bound for this long go back in place. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:1785-1790`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:412` |
|
||||
| `BB_VMA_BLOCK_MB` | integer MiB ≥1 (64; VMA's own default is 256) | Size of Vulkan memory allocator blocks. Smaller blocks are returned to the driver sooner when freed. | `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:906` |
|
||||
| `BB_VMA_DEDICATED_MB` | MiB; 0 = never (4) | Images of this size or more get dedicated memory, returned to the driver when freed (shared VMA blocks left ~0.9 GB of holes after texture evictions). | `gpu/shadps4/video_core/texture_cache/image.cpp:109-115` |
|
||||
| `BB_VRAM_IDLE_SECONDS` | seconds (60) | Mirrored (VRAM-copied) blocks not bound for this long go back in place. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:1823`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:412` |
|
||||
| `BB_VRAM_PRESSURE_IDLE_SECONDS` | seconds (3) | Near the critical VRAM mark, blocks no binding used directly for this long (an area left behind) go back in place first, longest unused first. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:903-913` |
|
||||
| `BB_VRAM_PRESSURE_PERCENT` | percent, max 95; 0 = off (85) | How full VRAM is against the driver's budget: "tight" from this percent, "full" 5 points above (fewer VRAM copies then). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2005-2011` |
|
||||
| `BB_VRAM_PROMOTE_PERCENT` | percent (90) | Past this share of the driver's VRAM budget no new VRAM copies are made; candidates wait and are read in place meanwhile (issue #32: 8 GB cards). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3836-3846` |
|
||||
| `BB_VRAM_PRESSURE_PERCENT` | percent, max 95; 0 = off (85) | How full VRAM is against the driver's budget: "tight" from this percent, "full" 5 points above (fewer VRAM copies then). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2045` |
|
||||
| `BB_VRAM_PROMOTE_PERCENT` | percent (90) | Past this share of the driver's VRAM budget no new VRAM copies are made; candidates wait and are read in place meanwhile (issue #32: 8 GB cards). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3950` |
|
||||
| `MESA_SHADER_CACHE_MAX_SIZE` | size (run.sh default 4G) | Mesa's shader cache size, larger than Mesa's default. | `run.sh:31` |
|
||||
| `__GL_SHADER_DISK_CACHE` | run.sh default 1 | NVIDIA's shader disk cache on (run.sh default; keeps compiled pipelines between sessions). | `run.sh:25-30` |
|
||||
| `__GL_SHADER_DISK_CACHE_SIZE` | bytes (run.sh default 10737418240) | NVIDIA's shader cache size (10 GiB by default here). | `run.sh:30` |
|
||||
@@ -265,26 +274,28 @@ behaviour when the variable is unset. In the tables:
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_GAMEPAD` | SDL GUID, part of the name, or the device path (`/dev/hidrawN`, `/dev/input/eventN`; the log's `gamepad connected` line shows it) (default: first controller) | **Launcher:** Controls → Gamepad → "Controller". Preferred controller, so wheels and other always-connected devices do not take over. The path tells two pads of the same model apart (local co-op). | `src/runtime_pad.c` (preferred_gamepad), `launcher/bbport_launcher.py:513` |
|
||||
| `BB_GAMEPAD` | SDL GUID, part of the name, or the device path (`/dev/hidrawN`, `/dev/input/eventN`; the log's `gamepad connected` line shows it) (default: first controller) | **Launcher:** Controls → Gamepad → "Controller". Preferred controller, so wheels and other always-connected devices do not take over. The path tells two pads of the same model apart (local co-op). | `src/runtime_pad.c` (preferred_gamepad), `launcher/bbport_launcher.py:655` |
|
||||
| `BB_GAMEPAD_STRICT` | 1 (default off) | Local co-op: only the BB_GAMEPAD controller, never the first one or the one in use instead; with no BB_GAMEPAD, no controller at all. | `src/runtime_pad.c` (gamepad_strict) |
|
||||
| `BB_KEYBOARD` | 0 (default on) | Local co-op: this game ignores the keyboard and the mouse, and never takes the mouse. | `src/runtime_pad.c` (keyboard_enabled) |
|
||||
| `BB_PAD_BACKGROUND` | 1 (default off) | Local co-op: the controller keeps driving the game while another window has the focus, and the game keeps its sound then (bbport.ini's mute_unfocused is ignored). **Launcher:** the Multiplayer → Local tab sets it, with BB_WINDOW_RECT, BB_GAMEPAD, BB_GAMEPAD_STRICT, BB_KEYBOARD, BB_AUDIO and BB_DATA_DIR for each player (`launcher/bbport_coop.py`). | `gpu/shim/window.cpp`, `gpu/shim/bbgpu.cpp` (bbgpu_audio_muted) |
|
||||
| `BB_MOUSE_DEADZONE` | 0–100 of 127 (default 16) | Minimum right-stick deflection used for mouse look, so slow mouse movement still turns the camera past the game's dead zone. | `src/runtime_pad.c:630-656` |
|
||||
| `BB_MOUSE_DEADZONE` | 0–100 of 127 (default 16) | Minimum right-stick deflection used for mouse look, so slow mouse movement still turns the camera past the game's dead zone. | `src/runtime_pad.c:761` |
|
||||
| `BB_PAD_CENTER` | lx,ly,rx,ry (default: measured) | Overrides the measured neutral position of each stick axis. | `src/runtime_pad.c:27`, `src/runtime_pad.c:113-118` |
|
||||
| `BB_PAD_CENTER_CAL` | 0 = off (default on) | Disables measuring each axis' neutral and travel (for third-party pads whose axes are biased). | `src/runtime_pad.c:28`, `src/runtime_pad.c:106` |
|
||||
| `BB_PAD_DEADZONE` | 0–126 (default 5) | Inner stick dead zone, as shadPS4's analog_deadzone. | `src/runtime_pad.c:26`, `src/runtime_pad.c:109` |
|
||||
| `BB_PAD_DEADZONE_OUTER` | 1–127 (default 127) | Outer stick dead zone: travel up to this value maps to full deflection (helps pads that never reach full). | `src/runtime_pad.c:26`, `src/runtime_pad.c:111` |
|
||||
| `BB_PROMPTS` | playstation \| xbox \| switch \| keyboard (default playstation) | **Launcher:** Controls → Gamepad → "Button prompts" (Automatic follows the controller). Button prompts of the game's menus, made from the game's files at start. | `run.sh:67-70`, `scripts/menu_prompts.py:57`, `launcher/bbport_launcher.py:502` |
|
||||
| `BB_PAD_CENTER_CAL` | 0 = off (default on) | Disables measuring each axis' neutral and travel (for third-party pads whose axes are biased). | `src/runtime_pad.c:28`, `src/runtime_pad.c:110` |
|
||||
| `BB_PAD_DEADZONE` | 0–126 (default 5) | **Launcher:** Controls → "Stick dead zone (%)" (percent × 127 / 100; written only when not 4 %). Inner stick dead zone, as shadPS4's analog_deadzone. | `src/runtime_pad.c:113`, `launcher/bbport_launcher.py:658` |
|
||||
| `BB_PAD_DEADZONE_OUTER` | 1–127 (default 127) | Outer stick dead zone: travel up to this value maps to full deflection (helps pads that never reach full). | `src/runtime_pad.c:26`, `src/runtime_pad.c:115` |
|
||||
| `BB_PAD_LIGHTBAR` | RRGGBB hex colour, an optional leading # (unset: SDL's own, the player's colour) | **Launcher:** Controls → "Controller light bar colour" (set only when on). Every controller with a light bar (DualShock 4, DualSense) shows this colour, set when it is opened. | `src/runtime_pad.c:297`, `launcher/bbport_launcher.py:662`, `launcher/bbport_launcher.py:1475` |
|
||||
| `BB_PROMPTS` | playstation \| xbox \| switch \| keyboard (default playstation) | **Launcher:** Controls → Gamepad → "Button prompts" (Automatic follows the controller). Button prompts of the game's menus, made from the game's files at start. | `run.sh:136`, `scripts/menu_prompts.py:57`, `launcher/bbport_launcher.py:644` |
|
||||
| `BB_STEAM_INPUT_X11` | 0 = keep Wayland (default on) | Launched from Steam with Steam Input on a Wayland desktop, the game window goes through Xwayland (`SDL_VIDEO_DRIVER=x11`): Steam sends its virtual controller's input only to a focused window it can see, and it sees only X11 windows (issue #12). Not when `SDL_VIDEO_DRIVER` is set. | `run.sh` |
|
||||
| `BB_STEAM_OVERLAY` | 1 = on (default 0) | **Launcher:** Setup → Game → "Steam overlay", under "Add to Steam" (saved as soon as it changes). Started from Steam, the game gets Steam's 64-bit `gameoverlayrenderer.so` in `LD_PRELOAD`, read from the nearest process above that still has it (the AppImage's sharun clears `LD_PRELOAD` in every program it runs), and nothing else Steam preloads. On a Wayland desktop the window goes through Xwayland (`SDL_VIDEO_DRIVER=x11`), since Steam draws its overlay only on X11 windows; not when `SDL_VIDEO_DRIVER` is set (issue #28). | `run.sh:370`, `launcher/bbport_launcher.py:700`, `launcher/bbport_launcher.py:1274` |
|
||||
|
||||
## Audio
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_AUDIO` | none, or unset (sound on) | `none` keeps the game silent: the runtime uses a silent timer instead of an SDL audio device, and the trophy chime is off too. | `src/runtime_audio.c:104`, `gpu/shim/bbport_overlay.cpp:776` |
|
||||
| `BB_AUDIO_DUMP` | file path prefix (default off) | Writes the raw converted PCM of each audio port to <prefix>.port<N>.<ch>ch.<f32\|s16>. | `src/runtime_audio.c:167` |
|
||||
| `BB_AUDIO_GAIN` | float >0 and ≤8 (default 1.0) | Master gain applied to the game's audio output. | `src/runtime_audio.c:88` |
|
||||
| `BB_AUDIO_STATS` | any value = on (default off) | Per-port audio timing statistics (gaps between submissions) in the log. | `src/runtime_audio.c:166` |
|
||||
| `BB_AUDIO` | none, or unset (sound on) | `none` keeps the game silent: the runtime uses a silent timer instead of an SDL audio device, and the trophy chime is off too. | `src/runtime_audio.c:83`, `gpu/shim/bbport_overlay.cpp:832` |
|
||||
| `BB_AUDIO_DUMP` | file path prefix (default off) | Writes the raw converted PCM of each audio port to <prefix>.port<N>.<ch>ch.<f32\|s16>. | `src/runtime_audio.c:252` |
|
||||
| `BB_AUDIO_GAIN` | float >0 and ≤8 (default 1.0) | Master gain applied to the game's audio output. | `src/runtime_audio.c:173` |
|
||||
| `BB_AUDIO_STATS` | any value = on (default off) | Per-port audio timing statistics (gaps between submissions) in the log. | `src/runtime_audio.c:251` |
|
||||
| `BB_AUDIO_TRACE` | 1 = on (default off) | Logs AJM decoder configurations and failing jobs, plus sound file opens and failed reads (missing game sounds). | `src/runtime_ajm.c:77`, `src/runtime_file.c:222` |
|
||||
| `BB_SOUND_HACK` | 0 = off (default on) | Bloodborne "sound hack": sets the flag at 0x204E of userdata0010 in each save before the game reads it, otherwise some sounds (e.g. weapon sounds) never play; 0 leaves saves untouched. | `src/runtime_savedata.c:72-79` |
|
||||
|
||||
@@ -292,110 +303,134 @@ behaviour when the variable is unset. In the tables:
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_FPS` | uncap \| 60 \| 90 \| 30 (default uncap) | **Launcher:** Graphics → Frame rate → "Mode". Frame-rate patch: uncap = delta-time patch with 480 Hz vblank and sprint fix; 60/90 = fixed-timestep patches (90 adds the sprint fix); 30 = unpatched. Also picks BB_VBLANK_HZ when that is unset. | `run.sh:101-104`, `run.sh:228-230`, `scripts/patches.py:17-21` |
|
||||
| `BB_GAME_MENU` | 0, or unset (on) | 0 leaves the game's System menu as it is: no port pages (Display, Game effects, Game patches) built into the game's own menu. | `gpu/shim/bbport_game_menu.cpp:585`, `gpu/shim/bbport_game_menu.h:5` |
|
||||
| `BB_GESTURES_NO_MOTION` | 0 \| 1 (default 0) | **Launcher:** Controls → Gamepad → "Gestures menu without motion-control icons". Dommo's stripped-down gestures menu (no motion-control icons), made from the game's files at start. | `run.sh:67-70`, `scripts/menu_prompts.py:15`, `launcher/bbport_launcher.py:503` |
|
||||
| `BB_FPS` | uncap \| 60 \| 90 \| 30 (default uncap) | **Launcher:** Graphics → Frame rate → "Mode". Frame-rate patch: uncap = delta-time patch with 480 Hz vblank and sprint fix; 60/90 = fixed-timestep patches (90 adds the sprint fix); 30 = unpatched. Also picks BB_VBLANK_HZ when that is unset. | `run.sh:77`, `run.sh:77`, `scripts/patches.py:17-21` |
|
||||
| `BB_GAME_MENU` | 0, or unset (on) | 0 leaves the game's System menu as it is: no port pages (Display, Game effects, Game patches) built into the game's own menu. | `gpu/shim/bbport_game_menu.cpp:596`, `gpu/shim/bbport_game_menu.h:5` |
|
||||
| `BB_GESTURES_NO_MOTION` | 0 \| 1 (default 0) | **Launcher:** Controls → Gamepad → "Gestures menu without motion-control icons". Dommo's stripped-down gestures menu (no motion-control icons), made from the game's files at start. | `run.sh:137`, `scripts/menu_prompts.py:15`, `launcher/bbport_launcher.py:645` |
|
||||
| `BB_GX_COMMAND_HEAP` | 0 = off (default 1) | Patch that enlarges the game's two per-frame GX command heaps to 1 MiB (crashes when a frame fills the 256 KiB heap); 0 keeps the game's size. | `scripts/patches.py:83-91` |
|
||||
| `BB_LANGUAGE` | PS4 system language number: 0 Japanese, 1 English, 2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian, ... (runtime default 1) | **Launcher:** Setup > Game language (Auto: from the desktop locale). The system language the runtime reports to the game; also picks the trophy names' language and the in-game menu's default language. | `src/runtime_services.c:70`, `gpu/shim/bbport_game_menu.cpp:104`, `launcher/bbport_launcher.py:506` |
|
||||
| `BB_MODS_ENABLED` | 0 \| 1 (default 1) | **Launcher:** Mods → Mods → "Load mods". Whether mods are merged into the game's files for this launch. | `run.sh:73`, `launcher/bbport_launcher.py:500` |
|
||||
| `BB_PATCHES` | patch names separated by ";" (default none) | Extra patch names (from patches/Bloodborne.xml) applied at start. | `run.sh:103`, `run.sh:149` |
|
||||
| `BB_REGION` | eu \| na \| empty (default: the dump's param.sfo) | **Launcher:** Setup → "Game region". The game's region for this launch; picks the regional text (e.g. Spanish from Spain or Latin American). | `run.sh:94-96`, `scripts/content_profile.py:3-5`, `launcher/bbport_launcher.py:507` |
|
||||
| `BB_LANGUAGE` | PS4 system language number: 0 Japanese, 1 English, 2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian, ... (runtime default 1) | **Launcher:** Setup > Game language (Auto: from the desktop locale). The system language the runtime reports to the game; also picks the trophy names' language and the in-game menu's default language. | `src/runtime_services.c:70`, `gpu/shim/bbport_game_menu.cpp:104`, `launcher/bbport_launcher.py:648` |
|
||||
| `BB_MODS_ENABLED` | 0 \| 1 (default 1) | **Launcher:** Mods → Mods → "Load mods". Whether mods are merged into the game's files for this launch. | `run.sh:166`, `launcher/bbport_launcher.py:642` |
|
||||
| `BB_PATCHES` | patch names separated by ";" (default none) | Extra patch names (from patches/Bloodborne.xml) applied at start. | `run.sh:190`, `run.sh:190` |
|
||||
| `BB_REGION` | eu \| na \| empty (default: the dump's param.sfo) | **Launcher:** Setup → "Game region". The game's region for this launch; picks the regional text (e.g. Spanish from Spain or Latin American). | `run.sh:189`, `scripts/content_profile.py:3-5`, `launcher/bbport_launcher.py:649` |
|
||||
| `BB_SKIP_GAME_CHECK` | 1 = skip (default check) | Skips the check that the game files are the supported 1.09 dump (wrong eboot, missing update, broken extraction). | `scripts/game_check.py:11`, `scripts/game_check.py:72`, `scripts/prepare.py:288` |
|
||||
| `BB_SKIP_NETWORK_CHOICE` | 0 \| 1 (default 1) | **Launcher:** Multiplayer → Online → "Skip the "play online / offline" choice" (default on). Patch: the title screen goes straight to the main menu, online when online play is on (BB_ONLINE=1), as PLAY ONLINE on a PS4, offline otherwise; 0 shows the online/offline dialog. | `scripts/patches.py:73-80`, `launcher/bbport_launcher.py:516` |
|
||||
| `BB_TROPHY_KEY` | 32 hex characters (default none) | **Launcher:** Trophies → "Trophy key". The PS4 trophy key; decrypts trophy names and descriptions in the game's language (without it they are in English). | `run.sh:298`, `scripts/trophies.py:366`, `launcher/bbport_launcher.py:554` |
|
||||
| `BB_SKIP_NETWORK_CHOICE` | 0 \| 1 (default 1) | **Launcher:** Multiplayer → Online → "Skip the "play online / offline" choice" (default on). Patch: the title screen goes straight to the main menu, online when online play is on (BB_ONLINE=1), as PLAY ONLINE on a PS4, offline otherwise; 0 shows the online/offline dialog. | `scripts/patches.py:73-80`, `launcher/bbport_launcher.py:663` |
|
||||
| `BB_TROPHY_KEY` | 32 hex characters (default none) | **Launcher:** Trophies → "Trophy key". The PS4 trophy key; decrypts trophy names and descriptions in the game's language (without it they are in English). | `run.sh:351`, `scripts/trophies.py:495`, `launcher/bbport_launcher.py:702` |
|
||||
| `BB_USER_NAME` | text (Hunter) | The PS4 user name reported to the game and the online module; also the default answer to a text-input dialog when the on-screen keyboard is unavailable. | `src/runtime_services.c:55`, `gpu/bbnet/include/core/user_manager.h:38` |
|
||||
|
||||
## Saves & files
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BBSC_SAVE_DIR` | folder path; default unset | Folder holding userdata0000.. on hosts that keep saves elsewhere (bbhost); BBSC counts its slots as the active save and copies it in its save backups. On the port BBSC looks in ./user (working folder) otherwise. | `bbsc/emulator/src/core/bloodborne_re.cpp:5580-5590`, `bbsc/emulator/src/core/bloodborne_re.cpp:12484-12490` |
|
||||
| `BB_CONFIG` | path (run.sh: <data dir>/bbport.ini; the GPU library alone: ./bbport.ini) | **Internal** (run.sh default $BB_DATA_DIR/bbport.ini). Path of bbport.ini, the settings file read by the GPU library (in-game menu settings), the controls (key./pad. lines) and the launcher. | `run.sh:21`, `gpu/shim/bbport_settings.cpp:21`, `src/runtime_pad.c:465` |
|
||||
| `BB_DATA_DIR` | directory (default: the port directory; AppImage/Nix: $XDG_DATA_HOME/bbport) | **Internal.** Writable directory for generated files (out/), saves (user/), bbport.ini, mods, patches and logs. | `run.sh:16-18`, `launcher/bbport_launcher.py:51`, `packaging/appimage-sharun.sh:71` |
|
||||
| `BB_DLC_DIR` | directory (default none) | **Launcher:** Setup → "DLC folder". Folder with the dumped add-ons (one folder each, e.g. SPEXPANSIONDLC03); linked as <user>/addcont/<title id> so the runtime reports them installed. | `run.sh:82-93`, `launcher/bbport_launcher.py:497` |
|
||||
| `BB_GAME_DIR` | directory (default ../CUSA03173) | **Launcher:** Setup → "Game folder". The game's folder (eboot.bin, sce_module, …). | `run.sh:63-65`, `launcher/bbport_launcher.py:493`, `scripts/patches.py:409` |
|
||||
| `BBSC_SAVE_DIR` | folder path; default unset | Folder holding userdata0000.. on hosts that keep saves elsewhere (bbhost); BBSC counts its slots as the active save and copies it in its save backups. On the port BBSC looks in ./user (working folder) otherwise. | `bbsc/emulator/src/core/bloodborne_re.cpp:5931`, `bbsc/emulator/src/core/bloodborne_re.cpp:14468` |
|
||||
| `BB_CONFIG` | path (run.sh: <data dir>/bbport.ini; the GPU library alone: ./bbport.ini) | **Internal** (run.sh default $BB_DATA_DIR/bbport.ini). Path of bbport.ini, the settings file read by the GPU library (in-game menu settings), the controls (key./pad. lines) and the launcher. | `run.sh:21`, `gpu/shim/bbport_settings.cpp:21`, `src/runtime_pad.c:520` |
|
||||
| `BB_DATA_DIR` | directory (default: the port directory; AppImage/Nix: $XDG_DATA_HOME/bbport) | **Internal.** Writable directory for generated files (out/), saves (user/), bbport.ini, mods, patches and logs. | `run.sh:16-18`, `launcher/bbport_launcher.py:56`, `packaging/appimage-sharun.sh:74` |
|
||||
| `BB_DLC_DIR` | directory (default none) | **Launcher:** Setup → "DLC folder". Folder with the dumped add-ons (one folder each, e.g. SPEXPANSIONDLC03); linked as <user>/addcont/<title id> so the runtime reports them installed. | `run.sh:178`, `launcher/bbport_launcher.py:639` |
|
||||
| `BB_GAME_DIR` | directory (default ../CUSA03173) | **Launcher:** Setup → "Game folder". The game's folder (eboot.bin, sce_module, …). | `run.sh:63-65`, `launcher/bbport_launcher.py:635`, `scripts/patches.py:494` |
|
||||
| `BB_GPU_USER_DIR` | directory | **Internal** (run.sh: defaults to BB_USER_DIR, else <data>/user). Where the GPU library keeps its caches (pipeline cache etc.) and the player's DLSS folder (<dir>/dlss). Set by run.sh before the GPU library loads. | `gpu/shadps4/common/path_util.cpp:88`, `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.cpp:95`, `run.sh:24` |
|
||||
| `BB_MODS_CONFIG` | file (default <data>/mods.json) | **Launcher:** always set to <data>/mods.json. The mods list/configuration used by scripts/mods.py. | `run.sh:72`, `launcher/bbport_launcher.py:499` |
|
||||
| `BB_MODS_DIR` | directory (default <data>/mods) | **Launcher:** Mods → Mods → "Mods folder". Folder the mods are loaded from (also used by BB_MOD_TRACE). | `run.sh:72`, `launcher/bbport_launcher.py:498`, `src/runtime_file.c:390` |
|
||||
| `BB_PATCHES_CONFIG` | file (default <data>/patches.json) | **Launcher:** always set to <data>/patches.json. Configuration of the external patches. | `run.sh:150`, `launcher/bbport_launcher.py:506` |
|
||||
| `BB_PATCHES_DIR` | directory (default <data>/patches) | **Launcher:** Mods → Third-party patches → "Patches folder". Folder of external patch files. | `run.sh:150`, `launcher/bbport_launcher.py:505` |
|
||||
| `BB_USER_DIR` | folder path; default <data dir>/user (run.sh); BBSC falls back to ./user | **Launcher:** Setup > Saves > "Saves folder". User folder: saves, trophies, DLC links, host_overrides.json, bbsc/ logs. The launcher sets it only when a saves folder is chosen; also the default of BB_GPU_USER_DIR. | `launcher/bbport_launcher.py:494-495`, `run.sh:24`, `88`, `313`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:109` |
|
||||
| `BB_MODS_CONFIG` | file (default <data>/mods.json) | **Launcher:** always set to <data>/mods.json. The mods list/configuration used by scripts/mods.py. | `run.sh:165`, `launcher/bbport_launcher.py:641` |
|
||||
| `BB_MODS_DIR` | directory (default <data>/mods) | **Launcher:** Mods → Mods → "Mods folder". Folder the mods are loaded from (also used by BB_MOD_TRACE). | `run.sh:165`, `launcher/bbport_launcher.py:640`, `src/runtime_file.c:390` |
|
||||
| `BB_PATCHES_CONFIG` | file (default <data>/patches.json) | **Launcher:** always set to <data>/patches.json. Configuration of the external patches. | `run.sh:192`, `launcher/bbport_launcher.py:647` |
|
||||
| `BB_PATCHES_DIR` | directory (default <data>/patches) | **Launcher:** Mods → Third-party patches → "Patches folder". Folder of external patch files. | `run.sh:192`, `launcher/bbport_launcher.py:646` |
|
||||
| `BB_USER_DIR` | folder path; default <data dir>/user (run.sh); BBSC falls back to ./user | **Launcher:** Setup > Saves > "Saves folder". User folder: saves, trophies, DLC links, host_overrides.json, bbsc/ logs. The launcher sets it only when a saves folder is chosen; also the default of BB_GPU_USER_DIR. | `launcher/bbport_launcher.py:637`, `run.sh:24`, `88`, `313`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:114` |
|
||||
|
||||
## Hunter's Atlas (map)
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_ATLAS_ENABLED` | 1 or 0 (unset: the map's own `settings.ini`) | **Launcher:** Mods → Map (Hunter's Atlas) → "In-game map". The in-game map on or off; while set, the map's own switch in the game is greyed out. 0: none of its code runs. | `src/probe.c:626`, `gpu/shim/bbport_map.cpp:304` |
|
||||
| `BB_MAP_DIR` | folder (`navigation-map/maps`) | **Launcher:** the maps Hunter's Atlas generated. | `gpu/shim/bbport_map.cpp:294` |
|
||||
| `BB_MAP_ART_DIR` | folder (`<maps>/../art`) | **Launcher:** the map's pictures. | `gpu/shim/bbport_map_art.cpp:79` |
|
||||
| `BB_MAP_FONT` | file | **Launcher:** the font the map's names are drawn with. | `gpu/shim/bbport_overlay.cpp:1232` |
|
||||
| `BB_MAP_CATALOG` | file (`<maps>/../catalog.json`) | **Launcher:** names of places and items. | `gpu/shim/bbport_map.cpp:174` |
|
||||
| `BB_MAP_CONFIG` | file (`<maps>/../settings.ini`) | **Launcher:** the map's settings file. | `gpu/shim/bbport_map.cpp:295` |
|
||||
| `BB_MAP_TRAVEL_ENABLED` | 1 = on (off) | **Launcher:** "Travel to lamps from the map". | `gpu/shim/bbport_map_features.cpp:70`, `src/runtime_atlas_state.c:162` |
|
||||
| `BB_MAP_ITEMS_ENABLED` | 1 = on (off) | **Launcher:** "Items on the map". | `gpu/shim/bbport_map_features.cpp:70` |
|
||||
| `BB_MAP_FOG_ENABLED` | 1 = on (off; the launcher writes 0) | Areas not visited yet stay hidden. | `gpu/shim/bbport_map_features.cpp:70` |
|
||||
| `BB_MAP_NPC_MODE` | all \| new_dialogue \| hidden (all) | Which characters the map shows. | `gpu/shim/bbport_map_features.cpp:71` |
|
||||
| `BB_MAP_ITEM_CATEGORIES` | comma list (healing,insight,equipment,echoes) | Which items the map shows. | `gpu/shim/bbport_map_features.cpp:73` |
|
||||
| `BB_MAP_SOUND_GAIN` | 0–2 (0.024) | Volume of the map's sounds. | `src/runtime_audio.c:75` |
|
||||
| `BB_ATLAS_DEBUG` | 1 = on (off) | Logs what the map reads from the game's quick menu. | `src/runtime_map.c:107`, `gpu/shim/bbport_atlas_menu.cpp:35` |
|
||||
| `BB_ATLAS_QA` / `BB_ATLAS_QA_DEVICE` / `BB_MAP_TEST_CONTROL` / `BB_MAP_TEST_TRACE` | test hooks | Automated map tests. | `gpu/shim/bbport_map.cpp:388`, `gpu/shim/bbport_map.cpp:412`, `gpu/shim/bbport_map.cpp:412`, `gpu/shim/bbport_atlas_menu.cpp:70` |
|
||||
|
||||
## Online & co-op
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_BRIDGE` | 1 = on (default 0) | **Launcher:** Multiplayer → Online → shadNet server "The Hunter's Dream". Uses bbbridge on this PC as the shadNet server: run.sh starts bbbridge-cli for the launch (or reuses one already listening on 127.0.0.1:31313) and stops it when the game ends. | `run.sh:255-280`, `scripts/online.py:120` |
|
||||
| `BB_SEAMLESSHUNT_TOMBS` | 1 = keep (0) | **Launcher:** Multiplayer → Online → "Bloodstains on Seamless Hunt". Keeps bloodstains and death visions on Seamless Hunt's game server. Off by default: they crash the game with "Transaction failed" there (#10), so those requests are left unanswered and the game goes on without them. | `scripts/online.py:177`, `scripts/online.py:508` |
|
||||
| `BB_BRIDGE` | 1 = on (default 0) | **Launcher:** Multiplayer → Online → shadNet server "The Hunter's Dream". Uses bbbridge on this PC as the shadNet server: run.sh starts bbbridge-cli for the launch (or reuses one already listening on 127.0.0.1:31313) and stops it when the game ends. | `run.sh:308`, `scripts/online.py:159` |
|
||||
| `BB_CLIENT_VERSION` | text (shadPS4 bbport) | Client version string reported to the shadNet server, for a server whose clientversions.cfg expects another format. | `gpu/bbnet/shadps4/shadnet/client.cpp:542` |
|
||||
| `BB_COMMUNITY_SERVER` | host[:port] (launcher sets it when online) | **Launcher:** Multiplayer → Online → "Game server". The community game server (messages, bloodstains, phantoms) the game's own server traffic is redirected to. | `scripts/online.py:136`, `scripts/online.py:425` |
|
||||
| `BB_COMMUNITY_SERVER` | host[:port] (launcher sets it when online) | **Launcher:** Multiplayer → Online → "Game server". The community game server (messages, bloodstains, phantoms) the game's own server traffic is redirected to. | `scripts/online.py:500`, `scripts/online.py:500` |
|
||||
| `BB_NP_COUNTRY` | country code (us) | Country reported for the PSN user by the online module. | `gpu/bbnet/include/core/user_manager.h:45` |
|
||||
| `BB_NP_LANGUAGE` | language code (en) | Language reported for the PSN user by the online module. | `gpu/bbnet/include/core/user_manager.h:46` |
|
||||
| `BB_ONLINE` | 1 or 0/unset (off) | **Launcher:** Multiplayer → Online (online play on). Online play: the runtime loads the online module (gpu/bbnet) and sends the game's network and PSN imports to it; run.sh then sets up the shadNet server, overrides, bbbridge and BBSC. | `src/runtime_net.c:26`, `gpu/bbnet/include/core/emulator_settings.h:83`, `run.sh:245` |
|
||||
| `BB_SHADNET_NPID` | online ID (empty: shadNet off) | **Launcher:** Multiplayer → Online (online ID; The Hunter's Dream: the Discord sign-in's). Online ID used to log in to the shadNet server; empty disables the shadNet user. | `gpu/bbnet/include/core/user_manager.h:41`, `scripts/online.py:133` |
|
||||
| `BB_SHADNET_PASSWORD` | text (empty) | **Launcher:** Multiplayer → Online (password). Password for the shadNet login. | `gpu/bbnet/include/core/user_manager.h:42`, `scripts/online.py:134` |
|
||||
| `BB_SHADNET_SERVER` | host:port (code fallback 127.0.0.1:31313) | **Launcher:** Multiplayer → Online (server); run.sh default play.seamlesshunt.com:31313 when online. shadNet server the online module connects to. | `gpu/bbnet/include/core/emulator_settings.h:85`, `run.sh:246`, `scripts/online.py:132` |
|
||||
| `BB_ONLINE` | 1 or 0/unset (off) | **Launcher:** Multiplayer → Online (online play on). Online play: the runtime loads the online module (gpu/bbnet) and sends the game's network and PSN imports to it; run.sh then sets up the shadNet server, overrides, bbbridge and BBSC. | `src/runtime_net.c:26`, `gpu/bbnet/include/core/emulator_settings.h:83`, `run.sh:294` |
|
||||
| `BB_SHADNET_NPID` | online ID (empty: shadNet off) | **Launcher:** Multiplayer → Online (online ID; The Hunter's Dream: the Discord sign-in's). Online ID used to log in to the shadNet server; empty disables the shadNet user. | `gpu/bbnet/include/core/user_manager.h:41`, `scripts/online.py:162` |
|
||||
| `BB_SHADNET_PASSWORD` | text (empty) | **Launcher:** Multiplayer → Online (password). Password for the shadNet login. | `gpu/bbnet/include/core/user_manager.h:42`, `scripts/online.py:163` |
|
||||
| `BB_SHADNET_SERVER` | host:port (code fallback 127.0.0.1:31313) | **Launcher:** Multiplayer → Online (server); run.sh default play.seamlesshunt.com:31313 when online. shadNet server the online module connects to. | `gpu/bbnet/include/core/emulator_settings.h:85`, `run.sh:299`, `scripts/online.py:499` |
|
||||
| `BB_SHADNET_TOKEN` | text (empty) | Token passed with the shadNet login (np_handler); the launcher never sets it. | `gpu/bbnet/include/core/user_manager.h:43`, `gpu/bbnet/shadps4/core/libraries/np/np_handler.cpp:85` |
|
||||
| `BB_SHADNET_WEBAPI` | URL (code fallback http://127.0.0.1:31315) | **Launcher:** Multiplayer → Online (WebAPI); run.sh default http://<server host>:31315. shadNet WebAPI used for summon signs and the HTTP host overrides. | `gpu/bbnet/include/core/emulator_settings.h:86`, `run.sh:254`, `scripts/online.py:426` |
|
||||
| `BB_UPNP` | 1/y/t or 0 (1) | **Launcher:** Multiplayer → Online (UPnP); The Hunter's Dream forces 0. Opens the P2P UDP port on the router through UPnP (issue #121). | `gpu/bbnet/include/core/emulator_settings.h:88`, `gpu/bbnet/shadps4/core/libraries/network/p2p_sockets.cpp:239`, `scripts/online.py:135` |
|
||||
| `SHADPS4_BLOODBORNE_SEAMLESS_COOP` | non-empty and not "0" = on; default off | **Launcher:** Multiplayer → Online (own server + bbport.json online_seamless, no UI row). Online module: routes summon-messenger HTTP requests to the shadNet WebAPI (the server needs BloodborneSeamlessCoop=true). Inside libbbsc the same name is redirected to BBSC_MOD_SEAMLESS and ignored. | `gpu/bbnet/shadps4/core/libraries/network/http.cpp:676-679`, `805-808`, `scripts/online.py:141-143`, `bbsc/emulator/src/core/bloodborne_re.cpp:942-955` |
|
||||
| `SHADPS4_P2P_PORT` | 1-65535 (unset: a random port each time) | **Launcher:** Multiplayer → Online (P2P port; 0 = not set); The Hunter's Dream sets the bridge's port. Pins the local UDP port of the P2P transport (useful for a manual port forward); an invalid value stops the transport. | `gpu/bbnet/shadps4/core/libraries/network/p2p_sockets.cpp:201`, `scripts/online.py:140` |
|
||||
| `SHADPS4_P2P_RELAY` | 0/off, auto/direct, a.b.c.d:port, or unset (relay forced) | **Launcher:** set to 0 when seamless co-op is off or for The Hunter's Dream. UDP relay on the shadNet server's address (port 3478): unset sends every peer through it from the first frame; auto/direct = direct first, relay after 2 s; 0/off disables it; ip:port is a test override. | `gpu/bbnet/shadps4/core/libraries/network/p2p_sockets.cpp:586`, `scripts/online.py:117`, `scripts/online.py:149` |
|
||||
| `BB_SHADNET_WEBAPI` | URL (code fallback http://127.0.0.1:31315) | **Launcher:** Multiplayer → Online (WebAPI); run.sh default http://<server host>:31315. shadNet WebAPI used for summon signs and the HTTP host overrides. | `gpu/bbnet/include/core/emulator_settings.h:86`, `run.sh:307`, `scripts/online.py:501` |
|
||||
| `BB_UPNP` | 1/y/t or 0 (1) | **Launcher:** Multiplayer → Online (UPnP); The Hunter's Dream forces 0. Opens the P2P UDP port on the router through UPnP (issue #121). | `gpu/bbnet/include/core/emulator_settings.h:88`, `gpu/bbnet/shadps4/core/libraries/network/p2p_sockets.cpp:239`, `scripts/online.py:164` |
|
||||
| `SHADPS4_BLOODBORNE_SEAMLESS_COOP` | non-empty and not "0" = on; default off | **Launcher:** Multiplayer → Online (own server + bbport.json online_seamless, no UI row). Online module: routes summon-messenger HTTP requests to the shadNet WebAPI (the server needs BloodborneSeamlessCoop=true). Inside libbbsc the same name is redirected to BBSC_MOD_SEAMLESS and ignored. | `gpu/bbnet/shadps4/core/libraries/network/http.cpp:814`, `805-808`, `scripts/online.py:184`, `bbsc/emulator/src/core/bloodborne_re.cpp:959` |
|
||||
| `SHADPS4_P2P_PORT` | 1-65535 (unset: a random port each time) | **Launcher:** Multiplayer → Online (P2P port; 0 = not set); The Hunter's Dream sets the bridge's port. Pins the local UDP port of the P2P transport (useful for a manual port forward); an invalid value stops the transport. | `gpu/bbnet/shadps4/core/libraries/network/p2p_sockets.cpp:201`, `scripts/online.py:166` |
|
||||
| `SHADPS4_P2P_RELAY` | 0/off, auto/direct, a.b.c.d:port, or unset (relay forced) | **Launcher:** set to 0 when seamless co-op is off or for The Hunter's Dream. UDP relay on the shadNet server's address (port 3478): unset sends every peer through it from the first frame; auto/direct = direct first, relay after 2 s; 0/off disables it; ip:port is a test override. | `gpu/bbnet/shadps4/core/libraries/network/p2p_sockets.cpp:586`, `scripts/online.py:157`, `scripts/online.py:157` |
|
||||
| `SHADPS4_P2P_RELAY_PORT` | 1-65535 (3478) | Port of the P2P UDP relay. | `gpu/bbnet/shadps4/core/libraries/network/p2p_sockets.cpp:594` |
|
||||
| `SSL_CERT_FILE` | CA bundle path (default: first found) | CA certificates for HTTPS when online; run.sh picks the system bundle if unset or unreadable. | `run.sh:247-253` |
|
||||
| `SSL_CERT_FILE` | CA bundle path (default: first found) | CA certificates for HTTPS when online; run.sh picks the system bundle if unset or unreadable. | `run.sh:302` |
|
||||
|
||||
## Seamless co-op (BBSC)
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BBSC_MOD_HOOKS` | "0" off, anything else or unset on (mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Co-op hooks". Master switch that installs the BBSC co-op hooks (warps, world sync, Hunter's Dream, messages...); most other BBSC features need it. In libbbsc it stands in for SHADPS4_BLOODBORNE_RE_TRACE. | `bbsc/emulator/mod/mod.json:12`, `bbsc/emulator/src/core/bloodborne_re.cpp:942-955`, `bbsc/emulator/src/core/bloodborne_re.cpp:15205` |
|
||||
| `BBSC_MOD_SEAMLESS` | "0" off, anything else or unset on (mod.json default true). Forced to 0 by libbbsc on a public game server. | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Seamless summon". Master switch for Wozzardman's seamless summon (summon without the vanilla signs and waits). In libbbsc it stands in for SHADPS4_BLOODBORNE_SEAMLESS_COOP. | `bbsc/emulator/mod/mod.json:11`, `bbsc/emulator/src/core/bloodborne_re.cpp:942-955`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:305-307` |
|
||||
| `BBSC_MOD_HOOKS` | "0" off, anything else or unset on (mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Co-op hooks". Master switch that installs the BBSC co-op hooks (warps, world sync, Hunter's Dream, messages...); most other BBSC features need it. In libbbsc it stands in for SHADPS4_BLOODBORNE_RE_TRACE. | `bbsc/emulator/mod/mod.json:12`, `bbsc/emulator/src/core/bloodborne_re.cpp:962`, `bbsc/emulator/src/core/bloodborne_re.cpp:962` |
|
||||
| `BBSC_MOD_SEAMLESS` | "0" off, anything else or unset on (mod.json default true). Forced to 0 by libbbsc on a public game server. | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Seamless summon". Master switch for Wozzardman's seamless summon (summon without the vanilla signs and waits). In libbbsc it stands in for SHADPS4_BLOODBORNE_SEAMLESS_COOP. | `bbsc/emulator/mod/mod.json:11`, `bbsc/emulator/src/core/bloodborne_re.cpp:958`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:370` |
|
||||
| `BBSC_RELAY` | host[:port] (port default 20444); empty/unset: the game server address from the host_overrides file | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Relay address". BBSC relay server for warps and world sync. Unset: taken from SHADPS4_HTTP_HOST_OVERRIDES_JSON, else <user>/host_overrides.custom.json, then host_overrides.json. Launcher passes it only when non-empty. | `bbsc/emulator/mod/mod.json:41`, `bbsc/emulator/mod/bbsc_relay.cpp:123-160` |
|
||||
| `BBSC_SCALE_BOSS_HP` | integer percent; default 100 (code and mod.json); launcher range 0-300, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Boss HP per guest (%)". Extra health for bosses, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:34`, `bbsc/emulator/src/core/bloodborne_re.cpp:11688-11694`, `bbsc/emulator/src/core/bloodborne_re.cpp:11712` |
|
||||
| `BBSC_SCALE_BOSS_STAGGER` | integer percent; default 20 (code and mod.json); launcher range 0-200, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Boss stagger resistance per guest (%)". Bosses are harder to stagger, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:37`, `bbsc/emulator/src/core/bloodborne_re.cpp:11688-11694`, `bbsc/emulator/src/core/bloodborne_re.cpp:11715` |
|
||||
| `BBSC_SCALE_DAMAGE` | integer percent; default 15 (code and mod.json); launcher range 0-200, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Enemy damage per guest (%)". Extra damage for enemies and bosses, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:35`, `bbsc/emulator/src/core/bloodborne_re.cpp:11688-11694`, `bbsc/emulator/src/core/bloodborne_re.cpp:11713` |
|
||||
| `BBSC_SCALE_ENEMY_HP` | integer percent; default 40 (code and mod.json); launcher range 0-300, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Enemy HP per guest (%)". Extra health for normal enemies, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:33`, `bbsc/emulator/src/core/bloodborne_re.cpp:11688-11694`, `bbsc/emulator/src/core/bloodborne_re.cpp:11711` |
|
||||
| `BBSC_SCALE_ENEMY_STAGGER` | integer percent; default 15 (code and mod.json); launcher range 0-200, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Enemy stagger resistance per guest (%)". Normal enemies are harder to stagger, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:36`, `bbsc/emulator/src/core/bloodborne_re.cpp:11688-11694`, `bbsc/emulator/src/core/bloodborne_re.cpp:11714` |
|
||||
| `BBSC_SCALE_NPC_HUNTERS` | only "1" turns it on (default off; mod.json default false) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Scale NPC hunters". Henryk, Djura and the other hostile NPC hunters also get the enemy scaling (off = vanilla). | `bbsc/emulator/mod/mod.json:32`, `bbsc/emulator/src/core/bloodborne_re.cpp:11819-11820` |
|
||||
| `BB_BBSC` | 1 = on (default 0) | **Launcher:** Multiplayer → Online → "Seamless co-op (BBSC)". Loads BBSC (libbbsc.so) into the game; only takes effect together with BB_ONLINE=1. | `run.sh:242`, `run.sh:287`, `scripts/bbsc.py:60` |
|
||||
| `SHADPS4_BBSC_CINEMATIC` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | F1 toggles a free camera for filming (keyboard + mouse). | `bbsc/emulator/src/core/bbsc_persistence.h:46`, `bbsc/emulator/src/core/bbsc_cinematic.cpp:28`, `bbsc/emulator/src/core/bloodborne_re.cpp:10461` |
|
||||
| `SHADPS4_BBSC_CONNECT_FIRST` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Cross-map summon". Cross-map summon: join first, then one load into the host's map. | `bbsc/emulator/mod/mod.json:13`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:14332` |
|
||||
| `SHADPS4_BBSC_DEATH_PENALTY` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Bloodstain on death". A kept death (session not lost) still drops a bloodstain and the echoes. | `bbsc/emulator/mod/mod.json:25`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:7499` |
|
||||
| `SHADPS4_BBSC_DREAM` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Hunter's Dream together". A guest uses the Doll, workshop and messengers in the host's Hunter's Dream. | `bbsc/emulator/mod/mod.json:16`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:9037` |
|
||||
| `SHADPS4_BBSC_FIVE_HUNTERS` | "0" off, any other value on (default on; mod.json default true) Forced to 0 by libbbsc on a public game server. | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Five hunters". The host may summon up to 4 guests (5 hunters). | `bbsc/emulator/mod/mod.json:27`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:442`, `bbsc/emulator/src/core/bloodborne_re.cpp:13299`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:308` |
|
||||
| `SHADPS4_BBSC_FLOOR_GUARD` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Floor guard". No crash when a character's floor was unloaded. | `bbsc/emulator/mod/mod.json:30`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:10891` |
|
||||
| `SHADPS4_BBSC_FREE_BELL` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Free Beckoning Bell". The Beckoning Bell costs no Insight. | `bbsc/emulator/mod/mod.json:26`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:13562` |
|
||||
| `SHADPS4_BBSC_FULL_ECHOES` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Full echoes". A summoned guest is paid the host's rate of Blood Echoes, not half. | `bbsc/emulator/mod/mod.json:21`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:430` |
|
||||
| `SHADPS4_BBSC_GUEST_ITEMS` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Guest items". A guest picks up and keeps items in the host's world. | `bbsc/emulator/mod/mod.json:23`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:432`, `bbsc/emulator/src/core/bloodborne_re.cpp:6071` |
|
||||
| `SHADPS4_BBSC_INVADE_ANYWHERE` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Every world open to invaders (Bell Maiden SpEffect 9020). | `bbsc/emulator/src/core/bbsc_persistence.h:48`, `bbsc/emulator/src/core/bloodborne_re.cpp:12674` |
|
||||
| `SHADPS4_BBSC_LAMP` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Lamps in co-op". The host can use lamps during a session (lamp prompt works in co-op; only while hosting). | `bbsc/emulator/mod/mod.json:18`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:418` |
|
||||
| `SHADPS4_BBSC_LOAD_COVER` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Black fade on joins". The guest's summon/follow loads happen behind a black fade. | `bbsc/emulator/mod/mod.json:29`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:6544` |
|
||||
| `SHADPS4_BBSC_MESSAGES` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Co-op texts". Co-op texts that fit seamless play (no "Returning to your world"). | `bbsc/emulator/mod/mod.json:28`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:10949` |
|
||||
| `SHADPS4_BBSC_NO_FOG_WALLS` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "No co-op fog walls". Co-op area fog walls hidden; leaving the area is allowed. | `bbsc/emulator/mod/mod.json:17`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:424`, `bbsc/emulator/src/core/bloodborne_re.cpp:8772` |
|
||||
| `SHADPS4_BBSC_NO_HP_PENALTY` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "No guest HP cut". Removes the summoned guest's 30% max HP cut (effect 9006). | `bbsc/emulator/mod/mod.json:22`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:8658` |
|
||||
| `BBSC_SCALE_BOSS_HP` | integer percent; default 100 (code and mod.json); launcher range 0-300, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Boss HP per guest (%)". Extra health for bosses, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:34`, `bbsc/emulator/src/core/bloodborne_re.cpp:13515`, `bbsc/emulator/src/core/bloodborne_re.cpp:13515` |
|
||||
| `BBSC_SCALE_BOSS_STAGGER` | integer percent; default 20 (code and mod.json); launcher range 0-200, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Boss stagger resistance per guest (%)". Bosses are harder to stagger, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:37`, `bbsc/emulator/src/core/bloodborne_re.cpp:13516`, `bbsc/emulator/src/core/bloodborne_re.cpp:13516` |
|
||||
| `BBSC_SCALE_DAMAGE` | integer percent; default 15 (code and mod.json); launcher range 0-200, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Enemy damage per guest (%)". Extra damage for enemies and bosses, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:35`, `bbsc/emulator/src/core/bloodborne_re.cpp:13516`, `bbsc/emulator/src/core/bloodborne_re.cpp:13516` |
|
||||
| `BBSC_SCALE_ENEMY_HP` | integer percent; default 40 (code and mod.json); launcher range 0-300, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Enemy HP per guest (%)". Extra health for normal enemies, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:33`, `bbsc/emulator/src/core/bloodborne_re.cpp:13515`, `bbsc/emulator/src/core/bloodborne_re.cpp:13515` |
|
||||
| `BBSC_SCALE_ENEMY_STAGGER` | integer percent; default 15 (code and mod.json); launcher range 0-200, the code clamps to 0-1000 | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Enemy stagger resistance per guest (%)". Normal enemies are harder to stagger, per guest. Needs SHADPS4_BBSC_SCALING; a summoned guest uses the host's numbers (sent over the relay). | `bbsc/emulator/mod/mod.json:36`, `bbsc/emulator/src/core/bloodborne_re.cpp:13516`, `bbsc/emulator/src/core/bloodborne_re.cpp:13516` |
|
||||
| `BBSC_SCALE_NPC_HUNTERS` | only "1" turns it on (default off; mod.json default false) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Scale NPC hunters". Henryk, Djura and the other hostile NPC hunters also get the enemy scaling (off = vanilla). | `bbsc/emulator/mod/mod.json:32`, `bbsc/emulator/src/core/bloodborne_re.cpp:13727` |
|
||||
| `BB_BBSC` | 1 = on (default 0) | **Launcher:** Multiplayer → Online → "Seamless co-op (BBSC)". Loads BBSC (libbbsc.so) into the game; only takes effect together with BB_ONLINE=1. | `run.sh:294`, `run.sh:294`, `scripts/bbsc.py:60` |
|
||||
| `SHADPS4_BBSC_AREA_LIST_CAP` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Asks the server for fewer messages and bloodstains per area: long lists crashed the game ("Panic 265"). | `bbsc/emulator/src/core/bbsc_persistence.h:49`, `bbsc/emulator/src/core/bloodborne_re.cpp` (CapBbscAreaListRequest) |
|
||||
| `SHADPS4_BBSC_BELLS_ANYWHERE` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Bells anywhere". The kit's bells ring anywhere, the Hunter's Dream included; role, Insight and being online still count. | `bbsc/emulator/mod/mod.json`, `bbsc/emulator/src/core/bbsc_persistence.h:52` |
|
||||
| `SHADPS4_BBSC_CINEMATIC` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | The camera that follows a co-op partner while you are dead (needs SHADPS4_BBSC_SPECTATE): Tab, E or left click the next hunter, Shift+Tab or Q the previous one, F a free camera and back, F1 off until the next death; R1/L1 and the sticks on a gamepad. On Linux the free filming camera (F1 at any time) is left out: every key and mouse button also reaches the game through bbport's pad. | `bbsc/emulator/src/core/bbsc_persistence.h:46`, `bbsc/emulator/src/core/bbsc_cinematic.cpp`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:751` |
|
||||
| `SHADPS4_BBSC_CONNECT_FIRST` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Cross-map summon". Cross-map summon: join first, then one load into the host's map. | `bbsc/emulator/mod/mod.json:13`, `bbsc/emulator/src/core/bbsc_persistence.h:33`, `bbsc/emulator/src/core/bloodborne_re.cpp:16788` |
|
||||
| `SHADPS4_BBSC_DEATH_PENALTY` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Bloodstain on death". A kept death (session not lost) still drops a bloodstain and the echoes. | `bbsc/emulator/mod/mod.json:25`, `bbsc/emulator/src/core/bbsc_persistence.h:27`, `bbsc/emulator/src/core/bloodborne_re.cpp:8171` |
|
||||
| `SHADPS4_BBSC_DREAM` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Hunter's Dream together". A guest uses the Doll, workshop and messengers in the host's Hunter's Dream. | `bbsc/emulator/mod/mod.json:16`, `bbsc/emulator/src/core/bbsc_persistence.h:26`, `bbsc/emulator/src/core/bloodborne_re.cpp:10086` |
|
||||
| `SHADPS4_BBSC_FIVE_HUNTERS` | "0" off, any other value on (default on; mod.json default true) Forced to 0 by libbbsc on a public game server. | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Five hunters". The host may summon up to 4 guests (5 hunters). | `bbsc/emulator/mod/mod.json:27`, `bbsc/emulator/src/core/bbsc_persistence.h:45`, `bbsc/emulator/src/core/bbsc_patch_tables.h:442`, `bbsc/emulator/src/core/bloodborne_re.cpp:15425`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:371` |
|
||||
| `SHADPS4_BBSC_FLOOR_GUARD` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Floor guard". No crash when a character's floor was unloaded. | `bbsc/emulator/mod/mod.json:30`, `bbsc/emulator/src/core/bbsc_persistence.h:25`, `bbsc/emulator/src/core/bloodborne_re.cpp:12704` |
|
||||
| `SHADPS4_BBSC_FREE_BELL` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Free Beckoning Bell". The Beckoning Bell costs no Insight. | `bbsc/emulator/mod/mod.json:26`, `bbsc/emulator/src/core/bbsc_persistence.h:44`, `bbsc/emulator/src/core/bloodborne_re.cpp:15751` |
|
||||
| `SHADPS4_BBSC_FULL_ECHOES` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Full echoes". A summoned guest is paid the host's rate of Blood Echoes, not half. | `bbsc/emulator/mod/mod.json:21`, `bbsc/emulator/src/core/bbsc_persistence.h:22`, `bbsc/emulator/src/core/bbsc_patch_tables.h:430` |
|
||||
| `SHADPS4_BBSC_GUEST_ITEMS` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Guest items". A guest picks up and keeps items in the host's world. | `bbsc/emulator/mod/mod.json:23`, `bbsc/emulator/src/core/bbsc_persistence.h:23`, `bbsc/emulator/src/core/bbsc_patch_tables.h:432`, `bbsc/emulator/src/core/bloodborne_re.cpp:6542` |
|
||||
| `SHADPS4_BBSC_INVADE_ANYWHERE` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Every world open to invaders (Bell Maiden SpEffect 9020). | `bbsc/emulator/src/core/bbsc_persistence.h:48`, `bbsc/emulator/src/core/bloodborne_re.cpp:14727` |
|
||||
| `SHADPS4_BBSC_LAMP` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Lamps in co-op". The host can use lamps during a session (lamp prompt works in co-op; only while hosting). | `bbsc/emulator/mod/mod.json:18`, `bbsc/emulator/src/core/bbsc_persistence.h:29`, `bbsc/emulator/src/core/bbsc_patch_tables.h:418` |
|
||||
| `SHADPS4_BBSC_LOAD_COVER` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Black fade on joins". The guest's summon/follow loads happen behind a black fade. | `bbsc/emulator/mod/mod.json:29`, `bbsc/emulator/src/core/bbsc_persistence.h:21`, `bbsc/emulator/src/core/bloodborne_re.cpp:7106` |
|
||||
| `SHADPS4_BBSC_MESSAGES` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Co-op texts". Co-op texts that fit seamless play (no "Returning to your world"). | `bbsc/emulator/mod/mod.json:28`, `bbsc/emulator/src/core/bbsc_persistence.h:32`, `bbsc/emulator/src/core/bloodborne_re.cpp:12762` |
|
||||
| `SHADPS4_BBSC_NO_FOG_WALLS` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "No co-op fog walls". Co-op area fog walls hidden; leaving the area is allowed. | `bbsc/emulator/mod/mod.json:17`, `bbsc/emulator/src/core/bbsc_persistence.h:31`, `bbsc/emulator/src/core/bbsc_patch_tables.h:424`, `bbsc/emulator/src/core/bloodborne_re.cpp:9821` |
|
||||
| `SHADPS4_BBSC_NO_HP_PENALTY` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "No guest HP cut". Removes the summoned guest's 30% max HP cut (effect 9006). | `bbsc/emulator/mod/mod.json:22`, `bbsc/emulator/src/core/bbsc_persistence.h:30`, `bbsc/emulator/src/core/bloodborne_re.cpp:9707` |
|
||||
| `SHADPS4_BBSC_P2P_CHANNEL` | a value starting with 0, f or F turns it off; else on (mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "P2P channel". Sends BBSC's own messages (follow, world sync, scaling, leaving) straight to the other players over the game's own connection when there is no BBSC relay; needed on public servers (The Hunter's Dream). | `bbsc/emulator/mod/mod.json:40`, `bbsc/emulator/mod/bbsc_channel.cpp:348-352` |
|
||||
| `SHADPS4_BBSC_PEERS` | npid=host:port,npid=host:port (unset) | Static peer map ("direct connection" mode, e.g. on Tailscale/Radmin/ZeroTier): a listed player's address wins over the server lookup. The port is that player's SHADPS4_P2P_PORT. | `gpu/bbnet/shadps4/core/libraries/np/np_matching2/np_matching2_signaling.cpp:112` |
|
||||
| `SHADPS4_BBSC_PEERS_JSON` | file path (<user dir>/bbsc_peers.json) | JSON file of static peers ({"NPID": "host:port"}); entries in SHADPS4_BBSC_PEERS win. | `gpu/bbnet/shadps4/core/libraries/np/np_matching2/np_matching2_signaling.cpp:113` |
|
||||
| `SHADPS4_BBSC_PERSIST` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Keeps the co-op session alive through loads and deaths (persistent-session patches). 0 disables them. | `bbsc/emulator/src/core/bbsc_persistence.h:38`, `bbsc/emulator/src/core/bbsc_persistence.cpp:125` |
|
||||
| `SHADPS4_BBSC_PERSIST_BOSS` | "0" off, any other non-empty value on; default off (not a default-on switch) | Persistent-session patch group "Boss": keeps the session through a boss clear. | `bbsc/emulator/src/core/bbsc_patch_tables.h:414`, `bbsc/emulator/src/core/bbsc_persistence.h:84-86` |
|
||||
| `SHADPS4_BBSC_PERSIST_HOSTDEAD` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Stay when the host dies". The guest keeps the session if the host dies. | `bbsc/emulator/mod/mod.json:19`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:422` |
|
||||
| `SHADPS4_BBSC_PERSIST_HOSTDEAD` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Stay when the host dies". The guest keeps the session if the host dies. | `bbsc/emulator/mod/mod.json:19`, `bbsc/emulator/src/core/bbsc_persistence.h:42`, `bbsc/emulator/src/core/bbsc_patch_tables.h:422` |
|
||||
| `SHADPS4_BBSC_PERSIST_MATCHCHECK` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Ignores the post-load matching-check kick. | `bbsc/emulator/src/core/bbsc_persistence.h:39`, `bbsc/emulator/src/core/bbsc_patch_tables.h:416` |
|
||||
| `SHADPS4_BBSC_PERSIST_TASKS` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Keeps member tasks across a kept load. | `bbsc/emulator/src/core/bbsc_persistence.h:41`, `bbsc/emulator/src/core/bbsc_patch_tables.h:420` |
|
||||
| `SHADPS4_BBSC_REJOIN` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Guest rejoin / reinsert after a kept death. | `bbsc/emulator/src/core/bbsc_persistence.h:40`, `bbsc/emulator/src/core/bloodborne_re.cpp:3318` |
|
||||
| `SHADPS4_BBSC_SCALING` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Enemy scaling". Enemies and bosses get stronger (HP, damage, stagger) for every guest; replaces the game's own boss buff. Amounts: BBSC_SCALE_*. | `bbsc/emulator/mod/mod.json:31`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:436`, `bbsc/emulator/src/core/bloodborne_re.cpp:11684` |
|
||||
| `SHADPS4_BBSC_SHARED_PROGRESS` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Shared progress". When a guest goes home, bosses, shortcuts, doors and lamps the host has done are added to the guest's own world (adds only; NPC quests and the character untouched; saves backed up first). | `bbsc/emulator/mod/mod.json:38`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:9565` |
|
||||
| `SHADPS4_BBSC_REJOIN` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Guest rejoin / reinsert after a kept death. | `bbsc/emulator/src/core/bbsc_persistence.h:40`, `bbsc/emulator/src/core/bloodborne_re.cpp:3470` |
|
||||
| `SHADPS4_BBSC_SCALING` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Enemy scaling". Enemies and bosses get stronger (HP, damage, stagger) for every guest; replaces the game's own boss buff. Amounts: BBSC_SCALE_*. | `bbsc/emulator/mod/mod.json:31`, `bbsc/emulator/src/core/bbsc_persistence.h:47`, `bbsc/emulator/src/core/bbsc_patch_tables.h:436`, `bbsc/emulator/src/core/bloodborne_re.cpp:13515` |
|
||||
| `SHADPS4_BBSC_SHARED_PROGRESS` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Shared progress". When a guest goes home, bosses, shortcuts, doors and lamps the host has done are added to the guest's own world (adds only; NPC quests and the character untouched; saves backed up first). | `bbsc/emulator/mod/mod.json:38`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:10791` |
|
||||
| `SHADPS4_BBSC_SKIP_LEAVE_LIMIT` | "0" off, any other non-empty value on; default off | Persistent-session patch group "LeaveLimit"; exact effect not described in code, see bbsc/emulator/src/core/bbsc_patch_tables.h:426. | `bbsc/emulator/src/core/bbsc_patch_tables.h:426` |
|
||||
| `SHADPS4_BBSC_SOLID_NPCS` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Solid NPCs". A guest sees NPCs solid, not as grey wandering ghosts. | `bbsc/emulator/mod/mod.json:24`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:434`, `bbsc/emulator/src/core/bloodborne_re.cpp:12021` |
|
||||
| `SHADPS4_BBSC_SPECTATE` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Respawn in place". A dead player respawns in place / spectates instead of leaving the session. | `bbsc/emulator/mod/mod.json:20`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bbsc_patch_tables.h:428`, `bbsc/emulator/src/core/bloodborne_re.cpp:3843` |
|
||||
| `SHADPS4_BBSC_WARP_JOIN` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | After a cross-map join, one more real reload (fixes lighting). | `bbsc/emulator/src/core/bbsc_persistence.h:34`, `bbsc/emulator/src/core/bloodborne_re.cpp:4284` |
|
||||
| `SHADPS4_BBSC_WARP_TOGETHER` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Warp together". The guest follows the host through lamp warps. | `bbsc/emulator/mod/mod.json:14`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:4510` |
|
||||
| `SHADPS4_BBSC_WORLD_SYNC` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "World sync". The guest loads the host's map with the host's world state (doors, bosses, lifts). | `bbsc/emulator/mod/mod.json:15`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:4955` |
|
||||
| `SHADPS4_BLOODBORNE_RE_TRACE` | ignored by libbbsc (aliased to BBSC_MOD_HOOKS) | **No effect in this port.** In the BBSC emulator build this installed the BBSC hooks; in the port's libbbsc (BBSC_MOD) the name is redirected to BBSC_MOD_HOOKS, so setting it has no effect. | `bbsc/emulator/src/core/bloodborne_re.cpp:942-955`, `bbsc/emulator/src/core/bbsc_persistence.h:36` |
|
||||
| `SHADPS4_BBSC_SOLID_NPCS` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Solid NPCs". A guest sees NPCs solid, not as grey wandering ghosts. | `bbsc/emulator/mod/mod.json:24`, `bbsc/emulator/src/core/bbsc_persistence.h:24`, `bbsc/emulator/src/core/bbsc_patch_tables.h:434`, `bbsc/emulator/src/core/bloodborne_re.cpp:13934` |
|
||||
| `SHADPS4_BBSC_SPECTATE` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Respawn in place". A dead player respawns in place / spectates instead of leaving the session. | `bbsc/emulator/mod/mod.json:20`, `bbsc/emulator/src/core/bbsc_persistence.h:43`, `bbsc/emulator/src/core/bbsc_patch_tables.h:428`, `bbsc/emulator/src/core/bloodborne_re.cpp:3996` |
|
||||
| `SHADPS4_BBSC_TIMELINE` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | A short `bbsc-timeline-*.txt` of what happened in each game (joins, hosts, deaths, bosses) in `<user dir>/bbsc/logs`, next to `bbsc-mod-*.log`; the last 5 are kept. | `bbsc/emulator/src/core/bbsc_persistence.h:51`, `bbsc/emulator/src/core/bloodborne_re.cpp:1028`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp` |
|
||||
| `SHADPS4_BBSC_WARP_JOIN` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | After a cross-map join, one more real reload (fixes lighting). | `bbsc/emulator/src/core/bbsc_persistence.h:34`, `bbsc/emulator/src/core/bloodborne_re.cpp:4503` |
|
||||
| `SHADPS4_BBSC_WARP_TOGETHER` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Warp together". The guest follows the host through lamp warps. | `bbsc/emulator/mod/mod.json:14`, `bbsc/emulator/src/core/bbsc_persistence.h:17`, `bbsc/emulator/src/core/bloodborne_re.cpp:4733` |
|
||||
| `SHADPS4_BBSC_WORLD_SYNC` | "0" off, any other value on (default on; mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "World sync". The guest loads the host's map with the host's world state (doors, bosses, lifts). | `bbsc/emulator/mod/mod.json:15`, `bbsc/emulator/src/core/bbsc_persistence.h:20`, `bbsc/emulator/src/core/bloodborne_re.cpp:5191` |
|
||||
| `SHADPS4_BBSC_WORLD_SYNC_ALL` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | With SHADPS4_BBSC_WORLD_SYNC, the guest takes the host's world flags for every area, not only the one the host is in. | `bbsc/emulator/src/core/bbsc_persistence.h:50`, `bbsc/emulator/src/core/bloodborne_re.cpp:5214` |
|
||||
| `SHADPS4_BLOODBORNE_RE_TRACE` | ignored by libbbsc (aliased to BBSC_MOD_HOOKS) | **No effect in this port.** In the BBSC emulator build this installed the BBSC hooks; in the port's libbbsc (BBSC_MOD) the name is redirected to BBSC_MOD_HOOKS, so setting it has no effect. | `bbsc/emulator/src/core/bloodborne_re.cpp:962`, `bbsc/emulator/src/core/bbsc_persistence.h:36` |
|
||||
|
||||
## Discord
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_DISCORD` | 0 = off (default 1) | **Launcher:** Setup → "Discord status" (default on). Discord Rich Presence helper (scripts/discord_presence.py) that shows the area and co-op partners; 0 turns it off. | `run.sh:303-312`, `launcher/bbport_launcher.py:552` |
|
||||
| `BB_DISCORD` | 0 = off (default 1) | **Launcher:** Setup → "Discord status" (default on). Discord Rich Presence helper (scripts/discord_presence.py) that shows the area and co-op partners; 0 turns it off. | `run.sh:359`, `launcher/bbport_launcher.py:699` |
|
||||
| `SHADPS4_BBSC_DISCORD` | "0" or "false" off, else on (mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Discord status". BBSC's own Discord status (area, boss fights, number of hunters). Read only by bbsc_discord.cpp, which the Linux libbbsc does not compile (bbsc/CMakeLists.txt), so it has no effect in this port; bbport's presence is BB_DISCORD. | `bbsc/emulator/mod/bbsc_discord.cpp:31-35`, `bbsc/CMakeLists.txt:22-29`, `bbsc/emulator/mod/mod.json:39` |
|
||||
|
||||
## Diagnostics & logs
|
||||
@@ -403,133 +438,134 @@ behaviour when the variable is unset. In the tables:
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_BREADCRUMBS` | 0 = off; unset/other = on | GPU breadcrumbs (VK_AMD_buffer_marker): on device lost the log says which draw/dispatch never finished. 0 turns them off. | `gpu/shadps4/video_core/renderer_vulkan/vk_breadcrumbs.cpp:59`, `gpu/shadps4/video_core/renderer_vulkan/vk_breadcrumbs.h:7` |
|
||||
| `BB_BUFFER_STATS` | 1 = on (off) | Prints every 5 s how buffer bindings reach the GPU by guest region (stream copies, arena bindings, re-uploaded bytes) and how many frames the GPU lags behind. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:984`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:4724` |
|
||||
| `BB_CAMERA_LOG` | any value = on (off) | Prints the camera position every 100 ms (for scripted movement tests at different frame rates). | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:295` |
|
||||
| `BB_BUFFER_STATS` | 1 = on (off) | Prints every 5 s how buffer bindings reach the GPU by guest region (stream copies, arena bindings, re-uploaded bytes) and how many frames the GPU lags behind. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:984`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:4810` |
|
||||
| `BB_CAMERA_LOG` | any value = on (off) | Prints the camera position every 100 ms (for scripted movement tests at different frame rates). | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:296` |
|
||||
| `BB_CAPTURE_DIR` | directory (unset = current directory) | Where the frame analyzer (BB_CAPTURE_TRIGGER) writes frame_<time>.txt. | `gpu/shadps4/video_core/renderer_vulkan/vk_frame_capture.cpp:87` |
|
||||
| `BB_CAPTURE_TRIGGER` | file path (unset = off) | Frame analyzer: creating this file records the next full frame (every render pass with its targets, depth, draw count, shaders, textures, and compute dispatches) to a text file. | `gpu/shadps4/video_core/renderer_vulkan/vk_frame_capture.cpp:133`, `gpu/shadps4/video_core/renderer_vulkan/vk_frame_capture.h:2` |
|
||||
| `BB_CE_STATS` | 1 or unset (off) | Statistics of the constant engine's dumps (DumpConstRam per second, bytes, pages) and how often the translator reads memory a dump wrote in the last 100 ms. | `gpu/shim/bbport_ce_stats.h:1` |
|
||||
| `BB_COPY_SHADER_TRACE` | any value = on (off) | Logs the copy shader's buffers every 2 s. | `gpu/shadps4/video_core/renderer_vulkan/vk_shader_hle.cpp:161` |
|
||||
| `BB_DCB_STATS` | first char 1/y/t (off) | Structure of graphics command buffers (read-only scan, printed every 5 s): how many buffers, draws per buffer, state set before the first draw. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:1024`, `1383` |
|
||||
| `BB_DEBUG_MOTION` | 1 = on (off) | Blends the motion vectors as colors into the frame before it is shown. | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:77`, `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.h:3` |
|
||||
| `BB_DCB_STATS` | first char 1/y/t (off) | Structure of graphics command buffers (read-only scan, printed every 5 s): how many buffers, draws per buffer, state set before the first draw. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:1071`, `1383` |
|
||||
| `BB_DEBUG_MOTION` | 1 = on (off) | Blends the motion vectors as colors into the frame before it is shown. | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:78`, `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.h:3` |
|
||||
| `BB_DUMP_DIR` | directory (out/dump) | Where the upscaler/present/final frame dumps (BB_DUMP_TRIGGER, BB_PRESENT_DUMP_TRIGGER, BB_FINAL_DUMP_TRIGGER) are written. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:91` |
|
||||
| `BB_DUMP_FRAMES` | integer ≥1 (8) | How many frames one BB_DUMP_TRIGGER dump records. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:70` |
|
||||
| `BB_DUMP_SHADERS` | 1/y/t or 0 (off) | shadPS4's shader dump: writes the translated shaders to disk. | `gpu/shim/core/emulator_settings.h:58`, `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:1328` |
|
||||
| `BB_DUMP_SHADERS` | 1/y/t or 0 (off) | shadPS4's shader dump: writes the translated shaders to disk. | `gpu/shim/core/emulator_settings.h:58`, `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:1332` |
|
||||
| `BB_DUMP_TRIGGER` | file path (unset = off) | Creating this file dumps the upscaler's images of the next BB_DUMP_FRAMES frames as raw files in BB_DUMP_DIR (temporal stability checks; tools/dump_view.py converts them). | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:56` |
|
||||
| `BB_FAULT_WAIT` | 1 or unset (off) | A fatal fault or a GPU-library assertion leaves the process alive and stopped in its handler, so a debugger can attach (bash tools/debug_attach.sh <pid>); a desktop notice says so. | `src/probe.c:142`, `gpu/shim/common/assert.cpp:12` |
|
||||
| `BB_FAULT_WAIT` | 1 or unset (off) | A fatal fault or a GPU-library assertion leaves the process alive and stopped in its handler, so a debugger can attach (bash tools/debug_attach.sh <pid>); a desktop notice says so. | `src/probe.c:150`, `gpu/shim/common/assert.cpp:12` |
|
||||
| `BB_FILE_TRACE` | 1 = on (default off) | Logs every guest path the game opens, stats or checks (mods, missing files). | `src/runtime_file.c:127-129` |
|
||||
| `BB_FINAL_DUMP_TRIGGER` | file path (unset = off) | Creating this file saves the next frame as presented (after FSR and post-processing) as final_<w>x<h> in BB_DUMP_DIR (used by tools/frame.sh KIND=final). | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:164`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.h:41` |
|
||||
| `BB_FRAME_LOG` | file path (unset = off) | With BB_FRAME_STATS: one CSV line per flip (frame time, waits, stage times, allocations…), to find what long frames (stutter) have in common. BB_SAVE_LOG=1 points it at logs/<time>.frames.csv. | `gpu/shadps4/core/libraries/videoout/driver.cpp:526`, `run.sh:47` |
|
||||
| `BB_FRAME_STATS` | Flag(): first char 1/y/t = on (off) | **Launcher:** Advanced > Developer > "Frame statistics in the log" (run.sh also sets it with BB_SAVE_LOG=1). Prints flip rate and frame-time spread (plus many internal counters) every 5 s. Without it a short summary line is printed every 30 s. | `gpu/shadps4/core/libraries/videoout/driver.cpp:380`, `launcher/bbport_launcher.py:533` |
|
||||
| `BB_FREE_CHECK` | 1 = on (off) | **Launcher:** Advanced > Developer > "Crash diagnostics" (together with BB_WRITE_LOG and BB_PRODUCER_CHECK). Diagnostic for the guest heap corruption at guest offset 0x263b8e7: every write the GPU emulation makes to guest memory is checked against the free list of the game's label pool, and a watcher thread reports corrupted free lists. Slightly slower. | `gpu/shim/bbport_free_check.h:2`, `gpu/shim/bbport_free_check.cpp:488-494`, `launcher/bbport_launcher.py:544-547` |
|
||||
| `BB_FRAME_STATS` | Flag(): first char 1/y/t = on (off) | **Launcher:** Advanced > Developer > "Frame statistics in the log" (run.sh also sets it with BB_SAVE_LOG=1). Prints flip rate and frame-time spread (plus many internal counters) every 5 s. Without it a short summary line is printed every 30 s. | `gpu/shadps4/core/libraries/videoout/driver.cpp:380`, `launcher/bbport_launcher.py:678` |
|
||||
| `BB_FREE_CHECK` | 1 = on (off) | **Launcher:** Advanced > Developer > "Crash diagnostics" (together with BB_WRITE_LOG and BB_PRODUCER_CHECK). Diagnostic for the guest heap corruption at guest offset 0x263b8e7: every write the GPU emulation makes to guest memory is checked against the free list of the game's label pool, and a watcher thread reports corrupted free lists. Slightly slower. | `gpu/shim/bbport_free_check.h:2`, `gpu/shim/bbport_free_check.cpp:488-494`, `launcher/bbport_launcher.py:692` |
|
||||
| `BB_FSR4_PROFILE` | 1 = on (off) | FSR 4 / 4.1.1: GPU time per pass, printed every 300 frames. fsr4-bench sets it itself. | `gpu/shadps4/video_core/renderer_vulkan/fsr411/fsr411.cpp:280`, `gpu/tools/fsr4_bench.cpp:305` |
|
||||
| `BB_FSR4_STATS` | 1 or unset (off) | Prints the driver's per-pass statistics of the FSR 4 pipelines (registers, spills, instruction counts) when VK_KHR_pipeline_executable_properties is available; read by the patched FSR 4 provider, set by fsr4_bench --stats. | `gpu/patches/fsr-vulkan/0001-FSR-4-v07-provider-BB_FSR4_PROFILE-pass-timings-BB_F.patch:50`, `gpu/tools/fsr4_bench.cpp:307` |
|
||||
| `BB_GAME_MENU_TRACE` | 1 or unset (off) | How often the game asks for the port's menu texts (per 2 s). | `gpu/shim/bbport_game_menu.cpp:461` |
|
||||
| `BB_GARLIC_VRAM_RANGE` | begin,end (C integers, hex with 0x) (unset = whole address space) | Keeps VRAM copies only for guest addresses in [begin, end): for finding the block whose copy goes stale by halving the range. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2149-2156` |
|
||||
| `BB_GAME_MENU_TRACE` | 1 or unset (off) | How often the game asks for the port's menu texts (per 2 s). | `gpu/shim/bbport_game_menu.cpp:474` |
|
||||
| `BB_GARLIC_VRAM_RANGE` | begin,end (C integers, hex with 0x) (unset = whole address space) | Keeps VRAM copies only for guest addresses in [begin, end): for finding the block whose copy goes stale by halving the range. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2188` |
|
||||
| `BB_GNM_CENSUS` | 1 or unset (off) | Hooks the eboot's libGnm entry points, counts the calls per function and prints the busiest every 5 s. | `gpu/shim/bbport_gnm_hooks.h:4`, `gpu/shim/bbport_gnm_hooks.cpp:513` |
|
||||
| `BB_GNM_CENSUS_FILE` | file path (unset: no file) | With the Gnm census: writes every function's total calls so far (offset, calls, cb method, PM4) to this file. | `gpu/shim/bbport_gnm_hooks.cpp:437` |
|
||||
| `BB_GNM_CENSUS_RANGE` | first:last table indices (all) | Hooks only that range of the libGnm table (for bisecting). | `gpu/shim/bbport_gnm_hooks.cpp:543` |
|
||||
| `BB_GNM_OBSERVE` | 1 or unset (off) | Command buffer methods run through an observing stub; submissions are checked against the packets known API calls wrote (statistics in the census report). Also enables the hooks without BB_GNM_CENSUS. | `gpu/shim/bbport_gnm_hooks.cpp:69`, `gpu/shim/bbport_gnm_hooks.cpp:514` |
|
||||
| `BB_GNM_TRACE` | hex offset[:count] (count 40) | With the census or observer: prints the arguments, the caller and the packets written by the first `count` calls of the libGnm function at that offset. | `gpu/shim/bbport_gnm_hooks.cpp:84`, `gpu/shim/bbport_gnm_hooks.cpp:524` |
|
||||
| `BB_GPU_LOG` | trace, debug, info, warning, error, critical (warning) | Log level of the GPU library (shadPS4 log); unknown values mean warning. | `gpu/shim/common/logging/log.cpp:11`, `gpu/shim/common/logging/log.h:2` |
|
||||
| `BB_GPU_PROFILE` | 1 = on (off) | **Launcher:** Advanced > Developer > "GPU profile in the log". GPU time per render pass, dispatch and upscaler run, printed every 5 s (GPU ms per frame by label). | `gpu/shadps4/video_core/renderer_vulkan/vk_gpu_profiler.cpp:13`, `gpu/shadps4/video_core/renderer_vulkan/vk_gpu_profiler.h:2`, `launcher/bbport_launcher.py:549` |
|
||||
| `BB_GPU_PROFILE` | 1 = on (off) | **Launcher:** Advanced > Developer > "GPU profile in the log". GPU time per render pass, dispatch and upscaler run, printed every 5 s (GPU ms per frame by label). | `gpu/shadps4/video_core/renderer_vulkan/vk_gpu_profiler.cpp:13`, `gpu/shadps4/video_core/renderer_vulkan/vk_gpu_profiler.h:2`, `launcher/bbport_launcher.py:696` |
|
||||
| `BB_HEAP_SITES` | 1 or unset (off) | Wraps the game's malloc replacement and counts live allocations per call site; prints the sites whose live bytes grew (what leaks when the guest heap runs out). Also patches the deferred object release to count failed registry checks. | `gpu/shim/bbport_heap_sites.cpp:2`, `gpu/shim/bbport_guest_hooks.cpp:45` |
|
||||
| `BB_HLE_STATS` | any value = on (off) | Logs how often the compute-shader substitutions (HLE) fire. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2354` |
|
||||
| `BB_IMAGE_COPY_LOG` | any value = on (off) | Logs (first 40) the compute image copies the port recognizes and replaces. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2874` |
|
||||
| `BB_IMAGE_SYNC_LOG` | set (any value) = on (off) | Logs image refreshes from memory the GPU wrote, and the images refreshed most per 5 s with their flags. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:257`, `910-921` |
|
||||
| `BB_IMAGE_WATCH` | guest address (hex with 0x) (unset = off) | Prints how the image at that guest address is used (first 300 events). | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:236-251` |
|
||||
| `BB_HLE_STATS` | any value = on (off) | Logs how often the compute-shader substitutions (HLE) fire. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2385` |
|
||||
| `BB_IMAGE_COPY_LOG` | any value = on (off) | Logs (first 40) the compute image copies the port recognizes and replaces. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2950` |
|
||||
| `BB_IMAGE_SYNC_LOG` | set (any value) = on (off) | Logs image refreshes from memory the GPU wrote, and the images refreshed most per 5 s with their flags. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:352`, `910-921` |
|
||||
| `BB_IMAGE_WATCH` | guest address (hex with 0x) (unset = off) | Prints how the image at that guest address is used (first 300 events). | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:334` |
|
||||
| `BB_LABEL_TRAP` | 1, 2, or unset (off); needs BB_FREE_CHECK=1 | Label trap of the free check: 1 makes pool pages with pending fence labels read-only (slow), 2 puts breakpoints on the guest allocator's release and the collector's done branch; a label block released before its fence landed is reported. | `gpu/shim/bbport_free_check.cpp:657`, `gpu/shim/bbport_free_check.h:49` |
|
||||
| `BB_LAYER_PAGED_STATS` | any value = on (off) | Prints paged buffer bindings per shader every 5 s (layer memory module). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3109` |
|
||||
| `BB_LAYER_PAGED_STATS` | any value = on (off) | Prints paged buffer bindings per shader every 5 s (layer memory module). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3193` |
|
||||
| `BB_LAYER_WRITE_LOG` | set (any value) = on (off) | Layer memory module: logs large (1 MiB+) GPU writes kept in place, and the images near them. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:1234-1236` |
|
||||
| `BB_MOD_TRACE` | 1 = on (default off) | Logs the first 32 files the game opens from the mods folder. | `src/runtime_file.c:390-398` |
|
||||
| `BB_MOTION_SELECT_LOG` | 1 = on (off) | Logs each G-buffer vertex shader once with its buffer sizes and whether object motion selected it. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:892` |
|
||||
| `BB_MOTION_SELECT_LOG` | 1 = on (off) | Logs each G-buffer vertex shader once with its buffer sizes and whether object motion selected it. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:900` |
|
||||
| `BB_OCCLUSION_READ_TRACE` | 1 = on (off) | Pages with occlusion counters become no-access after an event until the game's first access, to report whether results were on the CPU by then. | `gpu/shadps4/video_core/renderer_vulkan/vk_occlusion.cpp:360`, `gpu/shadps4/video_core/renderer_vulkan/vk_occlusion.h:60` |
|
||||
| `BB_OCCLUSION_TRACE` | integer N (0) | Logs the first N occlusion events with their frame and render pass. | `gpu/shadps4/video_core/renderer_vulkan/vk_occlusion.cpp:274` |
|
||||
| `BB_PASS_BREAK_TRACE` | 1 = on (off) | Every 5 s: who ended a render pass that began again with the same state (attachments stored and loaded again). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:267`, `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:780` |
|
||||
| `BB_PRESENT_DUMP_COUNT` | integer ≥1 (1) | How many consecutive frames one BB_PRESENT_DUMP_TRIGGER saves. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:2199` |
|
||||
| `BB_PRESENT_DUMP_TRIGGER` | file path (unset = off) | Creating this file saves the completed display buffer (with the game's UI) as present_<w>x<h> in BB_DUMP_DIR; used by tools/frame.sh. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:2200`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:407` |
|
||||
| `BB_PRODUCER_CHECK` | 1 = on (off) | **Launcher:** Advanced > Developer > "Crash diagnostics" (also set by run.sh with BB_AS_0_3=1). Checks that only one thread at a time records into the scheduler (costs an atomic exchange per recorded command). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:832`, `launcher/bbport_launcher.py:547`, `run.sh:197` |
|
||||
| `BB_PASS_BREAK_TRACE` | 1 = on (off) | Every 5 s: who ended a render pass that began again with the same state (attachments stored and loaded again). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:267`, `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:789` |
|
||||
| `BB_PRESENT_DUMP_COUNT` | integer ≥1 (1) | How many consecutive frames one BB_PRESENT_DUMP_TRIGGER saves. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:2229` |
|
||||
| `BB_PRESENT_DUMP_TRIGGER` | file path (unset = off) | Creating this file saves the completed display buffer (with the game's UI) as present_<w>x<h> in BB_DUMP_DIR; used by tools/frame.sh. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:2227`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:407` |
|
||||
| `BB_PRODUCER_CHECK` | 1 = on (off) | **Launcher:** Advanced > Developer > "Crash diagnostics" (also set by run.sh with BB_AS_0_3=1). Checks that only one thread at a time records into the scheduler (costs an atomic exchange per recorded command). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:845`, `launcher/bbport_launcher.py:694`, `run.sh:239` |
|
||||
| `BB_READBACK_LOG` | file path (unset = off) | **Internal** (set by run.sh when BB_SAVE_LOG=1 to logs/<time>.readbacks.csv). CSV of every download of GPU-written buffer memory back to guest memory: who asked, where, how much, how long it took, how old the GPU's write was, whether the readback queue served it. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:138-160`, `run.sh:48` |
|
||||
| `BB_RECORDER_TRACE` | 1 = on (off) | Prints the most frequent direct command-buffer callers every 2000 calls. | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:315`, `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:813` |
|
||||
| `BB_RECORDER_TRACE` | 1 = on (off) | Prints the most frequent direct command-buffer callers every 2000 calls. | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:315`, `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:822` |
|
||||
| `BB_RESIDENCY_MIX` | 1 or 2 (off) | Prints bindings per kind (none, in place, mirror, mixed) and written, every 2 s; 2 also lists the in-place or mixed bindings with the most bytes (layer module). | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:106-110`, `162-164`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:459-465` |
|
||||
| `BB_RESIDENCY_REPORT` | trigger file path (unset = off) | When the file appears (checked once a second), reports the blocks bound in place in the last seconds and why they have no VRAM copy. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3041-3050` |
|
||||
| `BB_RES_STATS` | 1 = on (off) | Every 5 s: whether bound shaders read their resource descriptors from user-data registers or from tables in memory, per draw and dispatch. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2260` |
|
||||
| `BB_SAMPLE_THREAD` | thread name (default off) | With frame stats: samples where that guest thread runs with SIGPROF and reports it. | `src/runtime_kernel.c:392`, `src/runtime_kernel.c:450` |
|
||||
| `BB_SAVE_LOG` | 1 = on (default off) | **Launcher:** Advanced → Developer → "Save the log and statistics to a file". Also writes this run's output to <data>/logs/<time>.log and turns on BB_FRAME_STATS with .frames.csv and .readbacks.csv next to it. | `run.sh:41-53`, `launcher/bbport_launcher.py:533` |
|
||||
| `BB_RESIDENCY_REPORT` | trigger file path (unset = off) | When the file appears (checked once a second), reports the blocks bound in place in the last seconds and why they have no VRAM copy. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3108` |
|
||||
| `BB_RES_STATS` | 1 = on (off) | Every 5 s: whether bound shaders read their resource descriptors from user-data registers or from tables in memory, per draw and dispatch. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2317` |
|
||||
| `BB_SAMPLE_THREAD` | thread name (default off) | With frame stats: samples where that guest thread runs with SIGPROF and reports it. | `src/runtime_kernel.c:464`, `src/runtime_kernel.c:464` |
|
||||
| `BB_SAVE_LOG` | 1 = on (default off) | **Launcher:** Advanced → Developer → "Save the log and statistics to a file". Also writes this run's output to <data>/logs/<time>.log and turns on BB_FRAME_STATS with .frames.csv and .readbacks.csv next to it. | `run.sh:41-53`, `launcher/bbport_launcher.py:680` |
|
||||
| `BB_SAVE_TRACE` | 1 = on (default off) | Logs every operation on save files (/savedataN). | `src/runtime_file.c:224-225` |
|
||||
| `BB_SCENE_DEBUG` | file path (unset = off) | Once the file exists (it is consumed), prints for one frame why passes keep the native size and which scene targets are read at native size. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1627`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3756` |
|
||||
| `BB_SECTIONS` | 1 = on (off) | Time the draw recording thread spends per part of a draw (and ordered tasks by function), printed with the draw-pipe statistics. | `gpu/shim/bbport_sections.h:51`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1283` |
|
||||
| `BB_SYNC_TRACE` | set (any value) = on (off) | Prints each SyncForCpuRead (command-processor read of memory: drain and finish of the GPU work). | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:1316` |
|
||||
| `BB_TAA_DIAGNOSTICS` | integer 1–3 (unset = off) | TAA upscaler: selects a diagnostic output of the TAA shader, and with BB_DUMP_TRIGGER also dumps the TAA history images. Meaning of 1–3: see gpu/shadps4/video_core/host_shaders/taa.comp:16. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:797`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1956` |
|
||||
| `BB_TILE_BACK_LOG` | set (any value) = on (off) | Logs each image written back into the game's memory because a texel buffer reads it, with the age of its last GPU write (first 400). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3549-3555` |
|
||||
| `BB_SCENE_DEBUG` | file path (unset = off) | Once the file exists (it is consumed), prints for one frame why passes keep the native size and which scene targets are read at native size. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1644`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1644` |
|
||||
| `BB_SECTIONS` | 1 = on (off) | Time the draw recording thread spends per part of a draw (and ordered tasks by function), printed with the draw-pipe statistics. | `gpu/shim/bbport_sections.h:51`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1300` |
|
||||
| `BB_SYNC_TRACE` | set (any value) = on (off) | Prints each SyncForCpuRead (command-processor read of memory: drain and finish of the GPU work). | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:1363` |
|
||||
| `BB_TAA_DIAGNOSTICS` | integer 1–3 (unset = off) | TAA upscaler: selects a diagnostic output of the TAA shader, and with BB_DUMP_TRIGGER also dumps the TAA history images. Meaning of 1–3: see gpu/shadps4/video_core/host_shaders/taa.comp:16. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:797`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1983` |
|
||||
| `BB_TAA_RESPONSIVE` | 1 = on (off) | TAA upscaler: history weights 0.88/0.75 instead of 0.98/0.85, so fewer trails behind moving things and a little more shimmer. Off by default until compared in game; from Mrsuss60's Windows port. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:814`, `gpu/shadps4/video_core/host_shaders/taa.comp:204` |
|
||||
| `BB_TILE_BACK_LOG` | set (any value) = on (off) | Logs each image written back into the game's memory because a texel buffer reads it, with the age of its last GPU write (first 400). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3657` |
|
||||
| `BB_TIMELINE` | output file path (unset = off) | Records a timeline of the frame's way to the GPU (guest submissions and waits, decoding, Vulkan submissions, GPU completion); starts BB_TIMELINE_START seconds in, written once when the buffer is full (~15 s of play). | `gpu/shim/bbport_timeline.h:2-5`, `42` |
|
||||
| `BB_TIMELINE_START` | seconds (30) | With BB_TIMELINE=<file>: recording of the frame timeline starts this many seconds after the first event (and stops when the buffer is full, ~15 s). | `gpu/shim/bbport_timeline.h:4` |
|
||||
| `BB_TRACE_ADDR` | addr,size (C integers; size default 4096) (unset = off) | That guest range is traced from the start by the binding trace; only has an effect when BB_TRACE_SIZE is also set (non-zero). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2576-2588` |
|
||||
| `BB_TRACE_ADDR` | addr,size (C integers; size default 4096) (unset = off) | That guest range is traced from the start by the binding trace; only has an effect when BB_TRACE_SIZE is also set (non-zero). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2643` |
|
||||
| `BB_TRACE_SEMA` | any value = on (default off) | Logs every blocking semaphore wait (deadlock diagnosis). | `src/runtime_sema.c:69` |
|
||||
| `BB_TRACE_SHADER` | hex hash[,hash…] (unset = off) | For those shaders: the buffers they bind (address, size, read/written, whether in place or in VRAM, how often), printed every 5 s. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2955` |
|
||||
| `BB_TRACE_SIZE` | bytes (C integer) (0 = off) | Bindings over GPU-written buffers of that size are counted by the path they take and reported every 5 s. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:318-320`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2571-2575` |
|
||||
| `BB_TRACE_SHADER` | hex hash[,hash…] (unset = off) | For those shaders: the buffers they bind (address, size, read/written, whether in place or in VRAM, how often), printed every 5 s. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3045` |
|
||||
| `BB_TRACE_SIZE` | bytes (C integer) (0 = off) | Bindings over GPU-written buffers of that size are counted by the path they take and reported every 5 s. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:318-320`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2638` |
|
||||
| `BB_UI_TRACE` | any value = on (off) | Logs the draws of frames that ended without the output-size UI (shown at the game's own size), and of one that had it. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.h:229`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:393` |
|
||||
| `BB_VK_MARKERS` | 1/y/t or 0 (off) | Vulkan debug markers for guest and host passes (for RenderDoc and similar tools). | `gpu/shim/core/emulator_settings.h:71`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:65` |
|
||||
| `BB_VK_VALIDATION` | 1/y/t or 0 (off) | **Launcher:** Advanced > Developer > Vulkan validation layers. Enables the Vulkan validation layers (much slower). | `gpu/shim/core/emulator_settings.h:74`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:155`, `launcher/bbport_launcher.py:552` |
|
||||
| `BB_VK_VALIDATION` | 1/y/t or 0 (off) | **Launcher:** Advanced > Developer > Vulkan validation layers. Enables the Vulkan validation layers (much slower). | `gpu/shim/core/emulator_settings.h:74`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:155`, `launcher/bbport_launcher.py:698` |
|
||||
| `BB_VK_VALIDATION_SYNC` | 1/y/t or 0 (off) | Synchronisation validation in the Vulkan validation layer (with BB_VK_VALIDATION). | `gpu/shim/core/emulator_settings.h:76`, `gpu/shadps4/video_core/renderer_vulkan/vk_platform.cpp:311` |
|
||||
| `BB_VRAM_ACCESS_TRAP` | set (any value) = on (off) | Blocks moved to VRAM are closed to the CPU (PROT_NONE); the first CPU access to each is recorded (code site, read or write) and the block opened again. Finds CPU readers of data the GPU writes in VRAM. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2896-2930` |
|
||||
| `BB_VRAM_TRAP_RANGE` | addr,size (C integers) (unset = all blocks) | With BB_VRAM_ACCESS_TRAP: only blocks over that range, re-armed for every packet, every access counted by site. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2908-2918` |
|
||||
| `BB_WAIT_LOG` | file (default off) | With BB_SAMPLE_THREAD/frame stats: writes per-frame wait sites (cond/mutex/sema/sleep, counts and ms) to this file. | `src/runtime_kernel.c:375` |
|
||||
| `BB_VRAM_ACCESS_TRAP` | set (any value) = on (off) | Blocks moved to VRAM are closed to the CPU (PROT_NONE); the first CPU access to each is recorded (code site, read or write) and the block opened again. Finds CPU readers of data the GPU writes in VRAM. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2994` |
|
||||
| `BB_VRAM_TRAP_RANGE` | addr,size (C integers) (unset = all blocks) | With BB_VRAM_ACCESS_TRAP: only blocks over that range, re-armed for every packet, every access counted by site. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2977` |
|
||||
| `BB_WAIT_LOG` | file (default off) | With BB_SAMPLE_THREAD/frame stats: writes per-frame wait sites (cond/mutex/sema/sleep, counts and ms) to this file. | `src/runtime_kernel.c:389` |
|
||||
| `BB_WAIT_TRACE` | 1 or unset (off) | Attributes each wait for GPU ticks (Scheduler::Wait) to its thread and call stack; the frame stats print the stacks that waited longest. | `gpu/shim/bbport_wait_trace.h:2` |
|
||||
| `BB_WARN_META_TEXTURE` | any value = on (off) | Logs a warning when a shader reads texture metadata as a texture. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3264` |
|
||||
| `BB_WATCH` | addr,size (unset = off) | What the shader translator reads on the CPU from that guest range (user data pointing into it, constants copied from it), printed every 2 s. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1056` |
|
||||
| `BB_WRITE_LOG` | 1, 2, or unset/0 (off) | **Launcher:** Advanced > Developer > Crash diagnostics (with BB_FREE_CHECK, BB_PRODUCER_CHECK). Ring of the writes the GPU emulation makes to guest memory (fences, WriteData, DMA, downloads), dumped when the guest faults. 1 logs at the write itself (changes timing), 2 where the command thread decodes the fence or WriteData. | `gpu/shim/bbport_write_log.h:2`, `gpu/shim/bbport_write_log.cpp:37`, `launcher/bbport_launcher.py:546` |
|
||||
| `BB_WARN_META_TEXTURE` | any value = on (off) | Logs a warning when a shader reads texture metadata as a texture. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3347` |
|
||||
| `BB_WATCH` | addr,size (unset = off) | What the shader translator reads on the CPU from that guest range (user data pointing into it, constants copied from it), printed every 2 s. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1076` |
|
||||
| `BB_WRITE_LOG` | 1, 2, or unset/0 (off) | **Launcher:** Advanced > Developer > Crash diagnostics (with BB_FREE_CHECK, BB_PRODUCER_CHECK). Ring of the writes the GPU emulation makes to guest memory (fences, WriteData, DMA, downloads), dumped when the guest faults. 1 logs at the write itself (changes timing), 2 where the command thread decodes the fence or WriteData. | `gpu/shim/bbport_write_log.h:2`, `gpu/shim/bbport_write_log.cpp:37`, `launcher/bbport_launcher.py:693` |
|
||||
| `BB_WRITE_VERIFY` | 1 = on (off; only without write tracking) | Pages the GPU side watches are write-protected anyway to verify that every CPU write to them is announced. | `gpu/shadps4/video_core/buffer_cache/buffer.h:53`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:497-503` |
|
||||
| `MANGOHUD` | 1 = on (default off) | **Launcher:** Advanced → Developer → "MangoHud". MangoHud overlay. run.sh keeps it off if Steam's performance overlay (mangoapp) runs, and uses the system MangoHud instead of the bundled one when installed. | `run.sh:200-215`, `launcher/bbport_launcher.py:529` |
|
||||
| `SHADPS4_BBSC_TRACE_MAPS` | "0" off, any other non-empty value on; default off | Logs which maps are resident (and which one the player is in) whenever the set changes. | `bbsc/emulator/src/core/bloodborne_re.cpp:6230-6245` |
|
||||
| `SHADPS4_BBSC_TRACE_WALLS` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Logs which map objects get shown/made solid (fog walls). | `bbsc/emulator/src/core/bbsc_persistence.h:28`, `bbsc/emulator/src/core/bloodborne_re.cpp:8777` |
|
||||
| `SHADPS4_CAPTURE_BLOODBORNE_SUMMON` | non-empty and not "0" = on; default off | Online module writes capture files of summon-messenger HTTP requests (for collect-logs). Listed as default-on in BBSC but only the online module reads it. | `gpu/bbnet/shadps4/core/libraries/network/http.cpp:725-727`, `bbsc/emulator/src/core/bbsc_persistence.h:37` |
|
||||
| `VK_LOADER_LAYERS_DISABLE` | layer list | **Internal.** run.sh --software disables all implicit layers; with MANGOHUD=1 and a system MangoHud, run.sh adds the bundled MangoHud layer so only one overlay draws. | `run.sh:12`, `run.sh:209-214` |
|
||||
| `MANGOHUD` | 1 = on (default off) | **Launcher:** Advanced → Developer → "MangoHud". MangoHud overlay. run.sh keeps it off if Steam's performance overlay (mangoapp) runs, and uses the system MangoHud instead of the bundled one when installed. | `run.sh:246`, `launcher/bbport_launcher.py:676` |
|
||||
| `SHADPS4_BBSC_TRACE_MAPS` | "0" off, any other non-empty value on; default off | Logs which maps are resident (and which one the player is in) whenever the set changes. | `bbsc/emulator/src/core/bloodborne_re.cpp:6305` |
|
||||
| `SHADPS4_BBSC_TRACE_WALLS` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Logs which map objects get shown/made solid (fog walls). | `bbsc/emulator/src/core/bbsc_persistence.h:28`, `bbsc/emulator/src/core/bloodborne_re.cpp:8840` |
|
||||
| `SHADPS4_CAPTURE_BLOODBORNE_SUMMON` | non-empty and not "0" = on; default off | Online module writes capture files of summon-messenger HTTP requests (for collect-logs). Listed as default-on in BBSC but only the online module reads it. | `gpu/bbnet/shadps4/core/libraries/network/http.cpp:734`, `bbsc/emulator/src/core/bbsc_persistence.h:37` |
|
||||
| `VK_LOADER_LAYERS_DISABLE` | layer list | **Internal.** run.sh --software disables all implicit layers; with MANGOHUD=1 and a system MangoHud, run.sh adds the bundled MangoHud layer so only one overlay draws. | `run.sh:12`, `run.sh:255` |
|
||||
|
||||
## Developer/tests
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BBSC_EMULATOR` | path | **No effect in this port.** Windows bbsc-launch only: another program to start instead of the emulator. Not built for the Linux port. | `bbsc/emulator/mod/bbsc_launch.cpp:263-268` |
|
||||
| `BBSC_URL_OVERRIDES` | 0 = off | **No effect in this port.** Windows bbsc.dll only (bbhost): turns off its own URL overrides. The Linux build leaves URL overrides to the online module and never reads it. | `bbsc/emulator/mod/bbsc_http.cpp:139-145`, `176-181` |
|
||||
| `BBSC_URL_OVERRIDES` | 0 = off | **No effect in this port.** Windows bbsc.dll only (bbhost): turns off its own URL overrides. The Linux build leaves URL overrides to the online module and never reads it. | `bbsc/emulator/mod/bbsc_http.cpp:158`, `176-181` |
|
||||
| `BB_ASYNC_FENCES` | 1 = on; anything else off (off) | Experiment: an end-of-pipe fence is written once the guest memory copies on the copy threads are done instead of waiting for them. Measured slower (79 vs 83 FPS), so off. Ignored when honest labels are in use. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:476` |
|
||||
| `BB_AS_0_3` | 1 = on (default off) | **Launcher:** Advanced → Developer → "Synchronisation as in 0.3". Reverts what changed since the 0.3 release, for comparisons: run.sh forces BB_GUEST_IN_PLACE=0, BB_HOST_COPY_WAITS=all, BB_PRODUCER_CHECK=1 (overriding any value you set for those). | `run.sh:196`, `launcher/bbport_launcher.py:543` |
|
||||
| `BB_CAMERA_INVERSE` | own (unset = the game's stored inverse) | own: camera motion vectors use the inverse of the view matrix computed by the port instead of the one the game stores (A/B test; they agree to float precision). | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:280` |
|
||||
| `BB_CAMERA_Y` | up / down (unset = from the G-buffer viewport) | Forces the screen y sign used by camera motion vectors (for tests); normally the frame's G-buffer viewport decides. | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:313` |
|
||||
| `BB_AS_0_3` | 1 = on (default off) | **Launcher:** Advanced → Developer → "Synchronisation as in 0.3". Reverts what changed since the 0.3 release, for comparisons: run.sh forces BB_GUEST_IN_PLACE=0, BB_HOST_COPY_WAITS=all, BB_PRODUCER_CHECK=1 (overriding any value you set for those). | `run.sh:238`, `launcher/bbport_launcher.py:688` |
|
||||
| `BB_CAMERA_INVERSE` | own (unset = the game's stored inverse) | own: camera motion vectors use the inverse of the view matrix computed by the port instead of the one the game stores (A/B test; they agree to float precision). | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:281` |
|
||||
| `BB_CAMERA_Y` | up / down (unset = from the G-buffer viewport) | Forces the screen y sign used by camera motion vectors (for tests); normally the frame's G-buffer viewport decides. | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:314` |
|
||||
| `BB_CHUNK_TEXTURES` | 0 = off; else on (on) | A/B switch for the PC memory model (BB_GUEST_IN_PLACE): texture data is read by the GPU straight from the guest memory chunk it is in (no CPU copy, no VRAM buffer copy) when the range is contiguous. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:419`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:209` |
|
||||
| `BB_CHUNK_UPLOADS` | 0 = off; else on (on) | A/B switch for the PC memory model: VRAM copies are uploaded by GPU copies from the guest chunks; 0 = CPU staging instead. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:410` |
|
||||
| `BB_CONTENT_SKU` | full \| trial (default full) | AppContent SKU reported to the game (a probe setting, not a licence check). | `run.sh:96`, `scripts/content_profile.py:33` |
|
||||
| `BB_CP_DECODER` | 1 = on (off) | Opt-in: graphics command buffers are decoded by the translator's own decoder (gpu/cp) instead of shadPS4's packet walk. Off by default: the GPU thread spends ~7 % more per draw. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:855`, `gpu/shadps4/video_core/amdgpu/liverpool.h:111` |
|
||||
| `BB_CONTENT_SKU` | full \| trial (default full) | AppContent SKU reported to the game (a probe setting, not a licence check). | `run.sh:189`, `scripts/content_profile.py:33` |
|
||||
| `BB_CP_DECODER` | 1 = on (off) | Opt-in: graphics command buffers are decoded by the translator's own decoder (gpu/cp) instead of shadPS4's packet walk. Off by default: the GPU thread spends ~7 % more per draw. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:906`, `gpu/shadps4/video_core/amdgpu/liverpool.h:111` |
|
||||
| `BB_CP_SHADOW` | 1 = on (off) | Self-check: the translator's own decoder (gpu/cp) also decodes every top-level graphics buffer in the shadow and the register files are compared; the first differences are printed. Off with the PM4 self-test or BB_CP_DECODER=1. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:1059-1108` |
|
||||
| `BB_DEBUG_KEEP` | 1 (set by tools/debug_attach.sh) | tools/debug_attach.py: keep the process stopped/attached after the dump; see tools/debug_attach.py:74. | `tools/debug_attach.py:74`, `tools/debug_attach.sh:18` |
|
||||
| `BB_DEBUG_OUT` | file (default stdout) | Output file of tools/debug_attach.py (gdb attach helper). | `tools/debug_attach.py:5` |
|
||||
| `BB_DIRECT_MEMORY_ACCESS` | 1/y/t or 0 (off) | shadPS4's direct memory access in shaders: when off, shaders that read memory themselves (uses_dma) get no DMA buffer. | `gpu/shim/core/emulator_settings.h:57`, `gpu/shadps4/shader_recompiler/ir/passes/shader_info_collection_pass.cpp:179` |
|
||||
| `BB_DUMP_GOT` | guest GOT offset (hex/dec) | With BB_SAMPLE_THREAD sampling: prints which host function a guest GOT slot calls. | `src/runtime_kernel.c:466` |
|
||||
| `BB_DUMP_GOT` | guest GOT offset (hex/dec) | With BB_SAMPLE_THREAD sampling: prints which host function a guest GOT slot calls. | `src/runtime_kernel.c:480` |
|
||||
| `BB_FENCE_DELAY_TEST` | 1 = on (off) | Experiment: holds every 200th fence back 80 ms, to test whether late labels corrupt the guest heap (the sequence numbers come from BB_FREE_CHECK=1). | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:407-414` |
|
||||
| `BB_FIXED_ARENA` | 1/0 forces on/off (unset = on when guest memory is host-imported, i.e. NVIDIA) | NVIDIA workaround for the sparse arena: the arena keeps the first binding of every block, so game data is read in place and never moved to VRAM. Not used with the layer memory module (BB_LAYER_MEMORY); only with BB_GUEST_IN_PLACE. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:435-457` |
|
||||
| `BB_FSR411_OUT` | directory (default fsr4_411 in the tree; launcher sets it) | **Internal.** Where tools/fsr4cap/build_assets.sh writes the FSR 4.1.1 asset set. | `tools/fsr4cap/build_assets.sh:37`, `launcher/bbport_launcher.py:374` |
|
||||
| `BB_FSR4CAP_CLEAN` | 1 = remove work folder (launcher sets it when packaged) | **Internal.** build_assets.sh removes its work folder after building FSR 4.1.1 assets. | `tools/fsr4cap/build_assets.sh:142`, `launcher/bbport_launcher.py:370` |
|
||||
| `BB_FSR4CAP_FP8` | auto \| 1 \| 0 (default auto) | FSR 4.1.1 capture: also record the DLL's FP8 matrix variant outside RDNA4 via GE-Proton's FP16 emulation (1), or never (0); auto records it only where the DLL uses it (RDNA4). | `tools/fsr4cap/capture_all.sh:10-15`, `tools/fsr4cap/capture_all.sh:161` |
|
||||
| `BB_FSR411_OUT` | directory (default fsr4_411 in the tree; launcher sets it) | **Internal.** Where tools/fsr4cap/build_assets.sh writes the FSR 4.1.1 asset set. | `tools/fsr4cap/build_assets.sh:37`, `launcher/bbport_launcher.py:479` |
|
||||
| `BB_FSR4CAP_CLEAN` | 1 = remove work folder (launcher sets it when packaged) | **Internal.** build_assets.sh removes its work folder after building FSR 4.1.1 assets. | `tools/fsr4cap/build_assets.sh:142`, `launcher/bbport_launcher.py:475` |
|
||||
| `BB_FSR4CAP_FP8` | auto \| 1 \| 0 (default auto) | FSR 4.1.1 capture: also record the DLL's FP8 matrix variant outside RDNA4 via GE-Proton's FP16 emulation (1), or never (0); auto records it only where the DLL uses it (RDNA4). | `tools/fsr4cap/capture_all.sh:10-15`, `tools/fsr4cap/capture_all.sh:176` |
|
||||
| `BB_FSR4CAP_RUNNER` | steam \| direct (default automatic) | FSR 4.1.1 capture: steam = use Steam's runtime even where umu-run would be used; direct = run Proton directly from the start. | `tools/fsr4cap/capture_all.sh:37`, `tools/fsr4cap/proton.sh:118-135` |
|
||||
| `BB_FSR4CAP_SPOOF` | 0 = off (default 1) | FSR 4.1.1 capture: when AMD's DLL offers only FSR 3/2 on this GPU, retry with the GPU reported as an RX 7800 XT. | `tools/fsr4cap/capture_all.sh:16-19`, `tools/fsr4cap/capture_all.sh:120` |
|
||||
| `BB_FSR4CAP_WORK` | directory (default out/fsr4cap; launcher sets <data>/fsr4cap when packaged) | **Internal.** Work folder of build_assets.sh (must be under $HOME for Steam's runtime). | `tools/fsr4cap/build_assets.sh:19-20`, `tools/fsr4cap/build_assets.sh:36`, `launcher/bbport_launcher.py:369` |
|
||||
| `BB_FSR4CAP_SPOOF` | 0 = off (default 1) | FSR 4.1.1 capture: when AMD's DLL offers only FSR 3/2 on this GPU, retry with the GPU reported as an RX 7800 XT. | `tools/fsr4cap/capture_all.sh:16-19`, `tools/fsr4cap/capture_all.sh:131` |
|
||||
| `BB_FSR4CAP_WORK` | directory (default out/fsr4cap; launcher sets <data>/fsr4cap when packaged) | **Internal.** Work folder of build_assets.sh (must be under $HOME for Steam's runtime). | `tools/fsr4cap/build_assets.sh:19-20`, `tools/fsr4cap/build_assets.sh:36`, `launcher/bbport_launcher.py:474` |
|
||||
| `BB_FSR4_TIERS` | space-separated list (default "1080 2160") | Which FSR 4 asset tiers tools/fetch_fsr4_assets.sh downloads. | `tools/fetch_fsr4_assets.sh:6`, `tools/fetch_fsr4_assets.sh:18` |
|
||||
| `BB_GAME_PROFILE` | none, or unset (by TITLE_ID) | `none` runs the game as an unknown one, without the Bloodborne profile (the code notes Bloodborne then crashes). | `gpu/games/game_profile.cpp:22`, `gpu/games/bloodborne.cpp:17` |
|
||||
| `BB_GC_BUDGET_MB` | MiB (unset = 0 = driver budget, integrated GPUs only) | Texture collector: applies the integrated-GPU rule (marks at 70/85/95 % of the budget) with this fixed budget on any GPU. For tests on a desktop. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1322-1333` |
|
||||
| `BB_GC_BUDGET_MB` | MiB (unset = 0 = driver budget, integrated GPUs only) | Texture collector: applies the integrated-GPU rule (marks at 70/85/95 % of the budget) with this fixed budget on any GPU. For tests on a desktop. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1375-1386` |
|
||||
| `BB_GPU_DATA_CHECK` | 0 = off; else on (on) | With the PC memory model, GPU writes not finished yet and data the GPU wrote into a VRAM copy count as GPU-modified for CPU access checks (issue #122). 0 = not counted. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:1366-1374` |
|
||||
| `BB_GPU_LABELS_PORTABLE` | anything but 0 = on (off) | Writes GPU labels with portable Vulkan buffer updates even when VK_AMD_buffer_marker is available (testing the non-AMD path). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:309` |
|
||||
| `BB_GPU_LABELS_PORTABLE` | anything but 0 = on (off) | Writes GPU labels with portable Vulkan buffer updates even when VK_AMD_buffer_marker is available (testing the non-AMD path). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:326` |
|
||||
| `BB_GPU_WRITES_IN_PLACE` | 1 = on (off) | With the PC memory model and no write tracking, GPU-written buffers are kept in the game's memory (in place) instead of VRAM; costs ~9 ms a frame over PCIe on an RX 7800 XT. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:1118-1127` |
|
||||
| `BB_GUEST_HOOKS` | 0 = off; else on (on) | Hooks in the game's own code from its profile (gpu/games): the memory hooks with the PC memory model, the fixes of the game's own bugs (BB_JOB_SLOT_FIX, BB_KERNEL_CLAMP) with either model. 0 = none, as for a game the port knows nothing about. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:572-583`, `gpu/shim/bbgpu.cpp:381-390`, `gpu/games/bloodborne.cpp:38-39` |
|
||||
| `BB_GUEST_HOOKS` | 0 = off; else on (on) | Hooks in the game's own code from its profile (gpu/games): the memory hooks with the PC memory model, the fixes of the game's own bugs (BB_JOB_SLOT_FIX, BB_KERNEL_CLAMP) with either model. 0 = none, as for a game the port knows nothing about. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:572-583`, `gpu/shim/bbgpu.cpp:384-393`, `gpu/games/bloodborne.cpp:17` |
|
||||
| `BB_JOB_SLOT_FIX` | 0 = off (default on) | A render job that fails gives its pool slot back (the game kept it: Laurence's cinematic above 30 FPS emptied the pool and hung); also when a runner is destroyed holding one. | `gpu/shim/bbport_guest_hooks.cpp:443-470` |
|
||||
| `BB_KERNEL_CLAMP` | 0 = off (default on) | Raises the blur kernel's base sample count to 3 at the entry of 0xbbc3e0 (game data now and then gives 2: exception 0xa0020007). | `gpu/shim/bbport_guest_hooks.cpp:472-491` |
|
||||
| `BB_GUEST_HOOK_TRAPS` | 1 or unset (off) | Keeps the guest hooks as int3 traps instead of native detours. | `gpu/shim/bbport_guest_hooks.cpp:72`, `gpu/shim/bbport_guest_hooks.cpp:398` |
|
||||
| `BB_GUEST_HOOK_TRAPS` | 1 or unset (off) | Keeps the guest hooks as int3 traps instead of native detours. | `gpu/shim/bbport_guest_hooks.cpp:72`, `gpu/shim/bbport_guest_hooks.cpp:529` |
|
||||
| `BB_GUEST_WHOLE_CHUNKS` | 1 or unset (off) | Acts as if mapping a dma-buf at an offset failed (tests the NVIDIA path). | `gpu/shim/bbport_guest_memory.cpp:608` |
|
||||
| `BB_HEAP_HOOKS` | 0, or unset (on) | 0 (bisecting): the game heap's ranges are not reported to the GPU side. | `gpu/shim/bbport_guest_hooks.cpp:381` |
|
||||
| `BB_HLE_OFF` | bit mask, decimal or 0x hex (0): 1 = image copy, 2 = metadata clear, 4 = image clear | Experiment: the selected compute-shader substitutions (HLE) run as the game's own shaders instead. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2356` |
|
||||
| `BB_HEAP_HOOKS` | 0, or unset (on) | 0 (bisecting): the game heap's ranges are not reported to the GPU side. | `gpu/shim/bbport_guest_hooks.cpp:513` |
|
||||
| `BB_HLE_OFF` | bit mask, decimal or 0x hex (0): 1 = image copy, 2 = metadata clear, 4 = image clear | Experiment: the selected compute-shader substitutions (HLE) run as the game's own shaders instead. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2389` |
|
||||
| `BB_HONEST_FLIPS` | 1 = on (off) | Experiment: a flip completes once the GPU has finished the frame (as on the PS4) instead of when the frame is queued. Did not help, so off. | `gpu/shadps4/core/libraries/videoout/driver.cpp:745-755` |
|
||||
| `BB_HONEST_IDLE_US` | microseconds (200) | With honest labels (BB_HONEST_LABELS): how long the GPU command thread waits idle before flushing unsubmitted work that fences may be waiting for. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:136-146` |
|
||||
| `BB_HOT_PAGES` | 1 = on (off) | Opt-in: pages the guest writes again and again stay writable and are uploaded on every use instead of faulting. Off by default: in Hunter's Nightmare the set grew to ~14k pages and cost far more (33 FPS). | `gpu/shadps4/video_core/buffer_cache/region_manager.h:259-273` |
|
||||
| `BB_IMAGE_TRAPS` | 0 = off; else on (on, only with the PC memory model and no write tracking) | Pages under images are trapped for writes, so a write nobody announced still reaches the texture cache. 0 = announced writes only, as before. | `gpu/shadps4/video_core/page_manager.cpp:225-240` |
|
||||
| `BB_IME_TEXT` | text (default unset) | Answers every on-screen keyboard (IME) dialog at once with this text (automated runs). | `src/runtime_services.c:247-248` |
|
||||
| `BB_IME_TEXT` | text (default unset) | Answers every on-screen keyboard (IME) dialog at once with this text (automated runs). | `src/runtime_services.c:298` |
|
||||
| `BB_INPLACE_TEXTURES` | 1 = on (off) | A/B switch for the PC memory model: texture data bound in place whatever its memory type. Off by default: sampled data looked smeared and ghosted with it (2026-10-04). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:411`, `426-433` |
|
||||
| `BB_INTERNAL_HEIGHT` | pixels (1080) | Height of the video-out driver's internal screen (shadPS4 setting). See BB_RENDER_RES for the render size. | `gpu/shim/core/emulator_settings.h:24`, `gpu/shadps4/core/libraries/videoout/video_out.cpp:494` |
|
||||
| `BB_INTERNAL_WIDTH` | pixels (1920) | Width of the video-out driver's internal screen (shadPS4 setting). | `gpu/shim/core/emulator_settings.h:23`, `gpu/shadps4/core/libraries/videoout/video_out.cpp:493` |
|
||||
@@ -540,73 +576,76 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_LAYER_WATCH_HOT` | 1 = on (off) | Experiment (layer memory module): blocks the CPU writes often are watched too and uploaded again after every write, as the 0.3 model does. Off: they go back in place (uploading them ended render passes). | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:428-438` |
|
||||
| `BB_LAYER_WRITE_TRAPS` | 0 = off; else on (on) | Layer memory module: blocks with a VRAM copy are write-protected so a write the game does not announce sends the block back in place before the next binding (else stale copies give exploding vertices and garbage textures). BB_LAYER_WATCH needs it. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:1386-1406` |
|
||||
| `BB_MEM_SEMAPHORE_ORDER` | 0 = off; else on (on, only with honest labels) | A MEM_SEMAPHORE signal is given once the GPU has finished the work before it, in stream order. 0 = signalled when decoded (as before). | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:497-507` |
|
||||
| `BB_MENU_KEYS_FILE` | file path (unset: off) | Scripted menu tests: tokens in the file (toggle up down left right enter back l1 r1) are pressed one per frame when the file appears; it is then removed. | `gpu/shim/bbport_overlay.cpp:1019` |
|
||||
| `BB_MENU_KEYS_FILE` | file path (unset: off) | Scripted menu tests: tokens in the file (toggle up down left right enter back l1 r1) are pressed one per frame when the file appears; it is then removed. | `gpu/shim/bbport_overlay.cpp:1087` |
|
||||
| `BB_NULL_GPU` | 1/y/t or 0 (off) | shadPS4's null GPU: the command processor is not bound to the rasterizer, so nothing is drawn. | `gpu/shim/core/emulator_settings.h:61`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:75` |
|
||||
| `BB_OBJECT_MOTION_ALL` | 1 = on (off) | Keeps the old broad object-motion path (every candidate draw, without the bone-palette gate) for visual A/B tests. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:916`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1688` |
|
||||
| `BB_OBJECT_MOTION_ALL` | 1 = on (off) | Keeps the old broad object-motion path (every candidate draw, without the bone-palette gate) for visual A/B tests. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:921`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1705` |
|
||||
| `BB_STABLE_MOTION_HASH` | 1 = on (off) | Object motion: a draw's vertex streams are identified without their buffers' addresses, so streams the game rewrites into a ring buffer at a new address each frame keep one identity (steadier motion vectors); two objects laid out alike can share one. Off by default until compared; from Mrsuss60's Windows port. | `gpu/shadps4/video_core/renderer_vulkan/vk_draw_prep.h:44` |
|
||||
| `BB_OCCLUSION_LABEL_ORDER` | 0 = off; unset/other = on | With occlusion queries: results recorded before a label are written before it, as on the PS4. 0: labels may go ahead (statistics only). | `gpu/shadps4/video_core/renderer_vulkan/vk_occlusion.cpp:228` |
|
||||
| `BB_PACKAGE_FSR411` | 1 = include (build-time, default off) | Nix build: package the FSR 4.1.1 models (extracted from AMD's DLLs; never in a public package). | `packaging/default.nix:12-14` |
|
||||
| `BB_PAD_FILE` | file (default off) | Scripted input for automated runs: button names held while listed, lx=/ly=/rx=/ry= stick overrides, re-read when the file changes; token "replay" starts BB_PAD_REPLAY. | `src/runtime_pad.c:749-760` |
|
||||
| `BB_CAM_TRACE` | file (default off) | Writes "ms yaw pitch" (radians) to this file whenever the camera turns, to measure how the game turns it for a stick input (with BB_PAD_FILE). Not with mouse_camera=direct. | `gpu/shim/bloodborne_cam.cpp` (TraceTick) |
|
||||
| `BB_PAD_RECORD` | file (default off) | F9 starts/stops recording the pad state (gamepad or keyboard) to this file. | `src/runtime_pad.c:794-804` |
|
||||
| `BB_PAD_REPLAY` | file (default off) | Plays a BB_PAD_RECORD recording back, started by the token "replay" in BB_PAD_FILE. | `src/runtime_pad.c:795`, `src/runtime_pad.c:836` |
|
||||
| `BB_PIPE_VERIFY` | integer N (0 = off) | Every Nth draw-pipe packet also carries the full register state, for verification. Exact check: see gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1177. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:805` |
|
||||
| `BB_PAD_FILE` | file (default off) | Scripted input for automated runs: button names held while listed, lx=/ly=/rx=/ry= stick overrides, re-read when the file changes; token "replay" starts BB_PAD_REPLAY. | `src/runtime_pad.c:899` |
|
||||
| `BB_CAM_TRACE` | file (default off) | Writes "ms yaw pitch" (radians) to this file whenever the camera turns, to measure how the game turns it for a stick input (with BB_PAD_FILE). Only with mouse_camera=stick in bbport.ini (the default is direct). | `gpu/shim/bloodborne_cam.cpp` (TraceTick) |
|
||||
| `BB_PAD_RECORD` | file (default off) | F9 starts/stops recording the pad state (gamepad or keyboard) to this file. | `src/runtime_pad.c:944` |
|
||||
| `BB_PAD_REPLAY` | file (default off) | Plays a BB_PAD_RECORD recording back, started by the token "replay" in BB_PAD_FILE. | `src/runtime_pad.c:976`, `src/runtime_pad.c:976` |
|
||||
| `BB_PIPE_VERIFY` | integer N (0 = off) | Every Nth draw-pipe packet also carries the full register state, for verification. Exact check: see gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1177. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:822` |
|
||||
| `BB_PM4_SELFTEST` | 1 = on (off) | Self-test of command-processor packets Bloodborne never sends (occlusion queries, COPY_DATA, COND_EXEC, predication, MEM_SEMAPHORE, timestamps): injected ~40 s into the game and the results printed. Turns on BB_OCCLUSION_QUERIES. | `gpu/shadps4/video_core/amdgpu/pm4_selftest.cpp:221`, `gpu/shadps4/video_core/amdgpu/pm4_selftest.h:2` |
|
||||
| `BB_PM4_SELFTEST_STEPS` | bit mask (C integer, 0x allowed) (all steps) | PM4 self-test (BB_PM4_SELFTEST): which steps run (bit n = step n); others skipped. | `gpu/shadps4/video_core/amdgpu/pm4_selftest.cpp:250-254` |
|
||||
| `BB_PRESET_FILE` | file path holding a preset number (unset = off) | Read about once a second; switches the upscaler preset like the menu does (scripted tests of live preset changes). | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:323` |
|
||||
| `BB_PROBE` | path to a program; default bin/bb-probe (packaged) or out/bb-probe | Runs this instead of bb-probe, e.g. a gdb wrapper; bb-gpu-capabilities is then taken from its folder. | `run.sh:114`, `236-239` |
|
||||
| `BB_PROBE` | path to a program; default bin/bb-probe (packaged) or out/bb-probe | Runs this instead of bb-probe, e.g. a gdb wrapper; bb-gpu-capabilities is then taken from its folder. | `run.sh:90`, `236-239` |
|
||||
| `BB_RESIDENCY_VRAM_LIMIT_MB` | MiB (unset = 0 = no limit) | Tests: VRAM counts as "full" after this many MiB of residency blocks, so blocks fall back to system memory read over the bus. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:1643-1648` |
|
||||
| `BB_SCENE_STENCIL_BITS` | 1 = on (off) | Forces reduced-size depth/stencil scene targets on GPUs without shader stencil export (e.g. Pascal), where they normally stay native (tests). | `gpu/shadps4/video_core/renderer_vulkan/vk_scene_resolution.cpp:25` |
|
||||
| `BB_SHADOWS` | 1 = on (off) | With the PC memory model on a discrete GPU, a read-only binding of data the CPU writes in place is copied once per submission into VRAM and bound there. Off by default: cost more than reading in place (48 -> 44 FPS). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:305-309`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2715-2720` |
|
||||
| `BB_DEPTH_MIN_FILTER` | 1 = on (off) | Reduced-size scene targets: depth-only targets are resampled by the shader, which keeps the nearest of each 2x2 when it downscales instead of one sample. Off by default until compared; from Mrsuss60's Windows port. | `gpu/shadps4/video_core/renderer_vulkan/vk_scene_resolution.cpp:63` |
|
||||
| `BB_SHADOWS` | 1 = on (off) | With the PC memory model on a discrete GPU, a read-only binding of data the CPU writes in place is copied once per submission into VRAM and bound there. Off by default: cost more than reading in place (48 -> 44 FPS). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:305-309`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2782` |
|
||||
| `BB_SKIP_CPU_CHECK` | 1 = skip (default check) | Skips the start-up check for the CPU features the game's code needs (AVX, BMI1, MOVBE, LZCNT, POPCNT); without them the game crashes with SIGILL. | `src/probe.c:45-52` |
|
||||
| `BB_SUBMIT_LOCK` | gpu \| ahead \| anything else (unset = frame/decode behaviour) | Submission lock mode with honest labels: unset/other = submissions are handed to the GPU first and return once the previous frame finished; gpu = the lock as before (wait for GPU idle); ahead = experiment, submissions never wait and sceGnmSubmitDone(F) waits for frame F-1. Only "gpu" and "ahead" are compared in code. | `gpu/shadps4/core/libraries/gnmdriver/gnmdriver.cpp:92-130`, `gpu/shadps4/core/libraries/gnmdriver/gnmdriver.h:229` |
|
||||
| `BB_TEST_STAGE` | integer | **Test only.** Used only inside tests/test_run_settings.py's fake probe. | `tests/test_run_settings.py:45` |
|
||||
| `BB_TIMEOUT` | seconds, 0 = none (run.sh default 0) | Watchdog: run.sh passes it as bb-probe --timeout; the process is stopped after that many seconds. | `run.sh:313`, `src/probe.c:381-437` |
|
||||
| `BB_TOGGLE_FILE` | file path (unset = off) | The file holds "N [M]", re-read every 250 ms: N is a bit mask of optimizations switched off while the game runs, M temporary experiment bits (A/B without restarting). Bits listed in src/runtime_memory.c and gpu/shim/bbport_toggles.h. | `src/runtime_memory.c:681`, `src/runtime_memory.c:728`, `gpu/shim/bbport_toggles.h:1` |
|
||||
| `BB_TROPHY_PREVIEW` | trophy id (default off) | At the game's first trophy call, shows that trophy's notice without recording it, to check how it looks. | `src/runtime_services.c:339-356` |
|
||||
| `BB_TIMEOUT` | seconds, 0 = none (run.sh default 0) | Watchdog: run.sh passes it as bb-probe --timeout; the process is stopped after that many seconds. | `run.sh:404`, `src/probe.c:381-437` |
|
||||
| `BB_TOGGLE_FILE` | file path (unset = off) | The file holds "N [M]", re-read every 250 ms: N is a bit mask of optimizations switched off while the game runs, M temporary experiment bits (A/B without restarting). Bits listed in src/runtime_memory.c and gpu/shim/bbport_toggles.h. | `src/runtime_memory.c:774`, `src/runtime_memory.c:774`, `gpu/shim/bbport_toggles.h:1` |
|
||||
| `BB_TROPHY_PREVIEW` | trophy id (default off) | At the game's first trophy call, shows that trophy's notice without recording it, to check how it looks. | `src/runtime_services.c:399` |
|
||||
| `BB_UI_TRIGGER_VS` | hex shader hash (0x34e8a281) | Vertex shader hash that marks the UI/movie phase of a frame for the upscaler. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:196`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.h:225` |
|
||||
| `BB_UPSCALE_BEFORE_CS` | hex shader hash (0x9a9cf8a9) | Compute shader hash before which the upscaler runs on the scene color (the post-processing combine pass). | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:193`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.h:207` |
|
||||
| `BB_VRAM_LIMIT_MB` | integer MiB (0 = no limit) | Tests: the driver's VRAM budget is taken as at most N MiB, so the texture collector and VRAM copies behave as on a smaller card. Allocations themselves are not limited. | `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:39` |
|
||||
| `BB_WORK_RETIRED` | 0 = off; else on (on) | GPU idle (sceGnmSubmitDone not blocking) also requires the draw recording thread and its deferred fences to be done; 0 = only submissions counted (Steam Deck crashes noted). | `gpu/shadps4/video_core/amdgpu/liverpool.h:141-150`, `gpu/shadps4/core/libraries/gnmdriver/gnmdriver.cpp:94` |
|
||||
| `BB_WRITE_TRACKING` | 1 = on (off; on anyway without the PC memory model) | Forces page write tracking (protection) back on with the PC memory model (BB_GUEST_IN_PLACE), where it is otherwise off. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:487-495`, `gpu/shadps4/video_core/buffer_cache/buffer.h:48` |
|
||||
| `SHADPS4_BBSC_NO_FAST_SIGN` | — | **No effect in this port.** Named only in the vanilla-emulator stub of SwitchEnabled; never read by libbbsc. | `bbsc/emulator/src/core/bbsc_persistence.h:59-67` |
|
||||
| `SHADPS4_BBSC_NO_GUEST_HOOKS` | "0" off, any other non-empty value on; default off | Isolation switch: skips the per-tick guest pre-match warp, deferred reload and responder re-ring (byte patches stay installed). | `bbsc/emulator/src/core/bloodborne_re.cpp:13718-13720` |
|
||||
| `SHADPS4_BBSC_NO_GUEST_HOOKS` | "0" off, any other non-empty value on; default off | Isolation switch: skips the per-tick guest pre-match warp, deferred reload and responder re-ring (byte patches stay installed). | `bbsc/emulator/src/core/bloodborne_re.cpp:16170` |
|
||||
| `SHADPS4_BBSC_NO_PLACEMENT_HEADER` | — | **No effect in this port.** Named only in the vanilla-emulator stub of SwitchEnabled (counts as set when BBSC is not built in); never read by libbbsc. | `bbsc/emulator/src/core/bbsc_persistence.h:59-67` |
|
||||
| `SHADPS4_BBSC_PHANTOM_FLAGS` | "0" off, any other non-empty value on; default off | Re-enables an experiment writing the phantom creation flags onto the guest (made the guest invisible and immune to damage). | `bbsc/emulator/src/core/bloodborne_re.cpp:3658-3664` |
|
||||
| `SHADPS4_BBSC_REINSERT` | joined \| leftjoined; unset, empty or 0: off (default) | Experimental: after a kept session end, replays the room members once the stage load is consumed; "leftjoined" also announces them as left first. | `bbsc/emulator/src/core/bloodborne_re.cpp:3228-3300` |
|
||||
| `SHADPS4_BBSC_REINSERT_DELAY_MS` | milliseconds; default 6000 | Delay before the experimental SHADPS4_BBSC_REINSERT replay runs. | `bbsc/emulator/src/core/bloodborne_re.cpp:3274-3277` |
|
||||
| `SHADPS4_BBSC_REJOIN_V6` | "0" off, any other non-empty value on; default off | Restores the older v6 deferred rejoin path instead of the default v7. | `bbsc/emulator/src/core/bloodborne_re.cpp:3322-3332` |
|
||||
| `SHADPS4_BBSC_PHANTOM_FLAGS` | "0" off, any other non-empty value on; default off | Re-enables an experiment writing the phantom creation flags onto the guest (made the guest invisible and immune to damage). | `bbsc/emulator/src/core/bloodborne_re.cpp:3813` |
|
||||
| `SHADPS4_BBSC_REINSERT` | joined \| leftjoined; unset, empty or 0: off (default) | Experimental: after a kept session end, replays the room members once the stage load is consumed; "leftjoined" also announces them as left first. | `bbsc/emulator/src/core/bloodborne_re.cpp:3396` |
|
||||
| `SHADPS4_BBSC_REINSERT_DELAY_MS` | milliseconds; default 6000 | Delay before the experimental SHADPS4_BBSC_REINSERT replay runs. | `bbsc/emulator/src/core/bloodborne_re.cpp:3427` |
|
||||
| `SHADPS4_BBSC_REJOIN_V6` | "0" off, any other non-empty value on; default off | Restores the older v6 deferred rejoin path instead of the default v7. | `bbsc/emulator/src/core/bloodborne_re.cpp:3474` |
|
||||
| `SHADPS4_P2P_SAME_HOST` | any value but 0 (off) | Two instances on one machine: the peer given as this network's own public address is reached through loopback. | `gpu/bbnet/shadps4/core/libraries/network/p2p_sockets.cpp:65` |
|
||||
|
||||
## Internal
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_AUTO_RENDER_RES` | 1 or unset | **Internal** (run.sh sets it with BB_RENDER_RES/BB_OUTPUT_RES). Marks BB_RENDER_RES/BB_OUTPUT_RES as chosen by run.sh from bbport.ini output_res (startup patch); run.sh drops them (and BB_UI_RES) on an in-game restart so they are recomputed, and the menu shows the matching hint. | `run.sh:68-70`, `run.sh:116`, `gpu/shim/bbport_overlay.cpp:409` |
|
||||
| `BB_BBSC_LIB` | path to libbbsc.so; default out/bbsc/libbbsc.so or BB_BBSC_PACKAGED_LIB | **Internal.** Library bb-probe loads into the game for seamless co-op. run.sh unsets it unless BB_BBSC=1 and BB_ONLINE=1, then resolves it (an explicit value is kept as an override). | `src/probe.c:302-316`, `run.sh:241-242`, `287-295` |
|
||||
| `BB_BBSC_PACKAGED_LIB` | path (set by the AppImage entry) | **Internal.** Location of the packaged libbbsc.so; run.sh falls back to it when BB_BBSC_LIB is unset, the launcher uses it to know BBSC is available. | `packaging/appimage-sharun.sh:70`, `run.sh:288`, `launcher/bbport_launcher.py:462` |
|
||||
| `BB_BRIDGE_CLI` | path (default out/bbbridge/bbbridge-cli; AppImage sets its copy) | **Internal.** The bbbridge-cli binary used for The Hunter's Dream. | `run.sh:260`, `launcher/bbport_launcher.py:469`, `packaging/appimage-sharun.sh:68` |
|
||||
| `BB_SAVEDATA_TITLE` | title id | **Set by the runtime**, not by you: the save folder's title id, for BBSC's save path (`BBSC_SAVE_DIR`). | `src/runtime_savedata.c:106` |
|
||||
| `BB_AUTO_RENDER_RES` | 1 or unset | **Internal** (run.sh sets it with BB_RENDER_RES/BB_OUTPUT_RES). Marks BB_RENDER_RES/BB_OUTPUT_RES as chosen by run.sh from bbport.ini output_res (startup patch); run.sh drops them (and BB_UI_RES) on an in-game restart so they are recomputed, and the menu shows the matching hint. | `run.sh:68-70`, `run.sh:119`, `gpu/shim/bbport_overlay.cpp:442` |
|
||||
| `BB_BBSC_LIB` | path to libbbsc.so; default out/bbsc/libbbsc.so or BB_BBSC_PACKAGED_LIB | **Internal.** Library bb-probe loads into the game for seamless co-op. run.sh unsets it unless BB_BBSC=1 and BB_ONLINE=1, then resolves it (an explicit value is kept as an override). | `src/probe.c:340`, `run.sh:341`, `287-295` |
|
||||
| `BB_BBSC_PACKAGED_LIB` | path (set by the AppImage entry) | **Internal.** Location of the packaged libbbsc.so; run.sh falls back to it when BB_BBSC_LIB is unset, the launcher uses it to know BBSC is available. | `packaging/appimage-sharun.sh:73`, `run.sh:341`, `launcher/bbport_launcher.py:572` |
|
||||
| `BB_BRIDGE_CLI` | path (default out/bbbridge/bbbridge-cli; AppImage sets its copy) | **Internal.** The bbbridge-cli binary used for The Hunter's Dream. | `run.sh:313`, `launcher/bbport_launcher.py:579`, `packaging/appimage-sharun.sh:71` |
|
||||
| `BB_BUNDLED_VK_DRIVER_FILES` | colon-separated ICD list (set by the Nix wrapper) | **Internal.** The package's own Mesa ICDs; bbport_vulkan.py puts them in VK_DRIVER_FILES (after the host NVIDIA ICD if one is found). | `launcher/bbport_vulkan.py:71`, `packaging/default.nix:215` |
|
||||
| `BB_DLSS_DIR` | directory (unset = next to bb-probe) | **Internal.** Where the DLSS bridge (libbbport_dlss.so) and NVIDIA's libnvidia-ngx-dlss are looked up. The AppImage entry sets it to its own dlss folder. | `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.cpp:61`, `packaging/appimage-sharun.sh:66`, `launcher/bbport_launcher.py:340` |
|
||||
| `BB_DLSS_DIR` | directory (unset = next to bb-probe) | **Internal.** Where the DLSS bridge (libbbport_dlss.so) and NVIDIA's libnvidia-ngx-dlss are looked up. The AppImage entry sets it to its own dlss folder. | `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.cpp:61`, `packaging/appimage-sharun.sh:69`, `launcher/bbport_launcher.py:447` |
|
||||
| `BB_FSR4CAP_CHECKED` | 1 (set by the script itself) | **Internal.** build_assets.sh marks that the DLL check already ran (re-exec guard). | `tools/fsr4cap/build_assets.sh:49-51` |
|
||||
| `BB_FSR4CAP_ON_HOST` | 1 (set by the script itself) | **Internal.** capture_all.sh re-runs itself on the host through systemd-run; this marks the re-run. | `tools/fsr4cap/capture_all.sh:47`, `tools/fsr4cap/capture_all.sh:62` |
|
||||
| `BB_FSR4CAP_SHELL` | 1 (set by the script itself) | **Internal.** build_assets.sh re-exec guard inside nix-shell (missing build tools). | `tools/fsr4cap/build_assets.sh:65-68` |
|
||||
| `BB_FSR4CAP_TOOLS` | directory (launcher sets it when bundled) | **Internal.** Folder holding prebuilt dxil-spirv and fsr4cap.exe, so build_assets.sh needs no MinGW toolchain. | `tools/fsr4cap/build_assets.sh:13`, `tools/fsr4cap/build_assets.sh:38`, `launcher/bbport_launcher.py:373` |
|
||||
| `BB_FSR4CAP_TOOLS` | directory (launcher sets it when bundled) | **Internal.** Folder holding prebuilt dxil-spirv and fsr4cap.exe, so build_assets.sh needs no MinGW toolchain. | `tools/fsr4cap/build_assets.sh:13`, `tools/fsr4cap/build_assets.sh:38`, `launcher/bbport_launcher.py:478` |
|
||||
| `BB_FSR4CAP_UMU_SHELL` | 1 (set by the script itself) | **Internal.** capture_all.sh re-exec guard inside a nix-shell with umu-launcher (NixOS). | `tools/fsr4cap/capture_all.sh:40-44` |
|
||||
| `BB_HOST_<NAME>` | set by the packaged entry | **Internal.** Copies of host variables the packaged entry dropped (LD_PRELOAD, LD_LIBRARY_PATH, GTK_*, pressure-vessel VK_* …), kept only for the log. | `packaging/bbport-entry.c:19-28`, `packaging/appimage-sharun.sh:73-79` |
|
||||
| `BB_IN_NIX_SHELL` | 1 (set by run.sh) | **Internal.** run.sh re-exec guard when it re-runs itself inside nix-shell. | `run.sh:37-39` |
|
||||
| `BB_NET_MODULE` | path (AppImage sets its copy; default gpu/libbbnet.so beside bb-probe) | **Internal.** The online module (libbbnet.so) the runtime dlopens. | `src/runtime_net.c:43-50`, `packaging/appimage-sharun.sh:67`, `launcher/bbport_launcher.py:485` |
|
||||
| `BB_PKGEXTRACT` | path (AppImage sets its copy) | **Internal.** pkgextract binary for "Install from PKG". | `scripts/pkg_install.py:50-51`, `launcher/bbport_launcher.py:476`, `packaging/appimage-sharun.sh:69` |
|
||||
| `BB_PREBUILT` | 1 (AppImage/Nix wrapper) | **Internal.** Packaged build: binaries are installed next to run.sh, nothing is built and no nix-shell is used; the launcher stores data in BB_DATA_DIR. | `run.sh:14`, `run.sh:235`, `launcher/bbport_launcher.py:50`, `packaging/appimage-sharun.sh:63` |
|
||||
| `BB_PRESENCE_DIR` | directory (run.sh: out/presence, new each start) | **Internal** (run.sh, when Discord is on). Where the game writes the current area (`area`) and co-op partners (`coop`) for the Discord Rich Presence helper. | `run.sh:307`, `src/runtime_file.c:317`, `gpu/bbnet/shadps4/core/libraries/np/np_signaling/np_signaling_state.cpp:248` |
|
||||
| `BB_TROPHY_DIR` | directory (run.sh sets it) | **Internal.** Trophy list and icons prepared by scripts/trophies.py; trophies are recorded there and shown in the overlay. | `run.sh:297-302`, `src/runtime_services.c:270-284` |
|
||||
| `BB_VULKANINFO` | path (default vulkaninfo) | **Internal.** vulkaninfo binary used by bbport_vulkan.py --vulkan-info (packaged builds set their copy). | `launcher/bbport_vulkan.py:192`, `packaging/appimage-sharun.sh:65` |
|
||||
| `DISABLE_GAMESCOPE_WSI` | 1 (set by the packaged entry) | **Internal.** Set when Steam's pressure-vessel Vulkan variables were dropped, turning off the host's gamescope WSI layer. | `packaging/bbport-entry.c:61-63`, `packaging/appimage-sharun.sh:80` |
|
||||
| `DISABLE_MANGOHUD` | 1 (set by run.sh) | **Internal.** Set when Steam's performance overlay already draws MangoHud. | `run.sh:208` |
|
||||
| `BB_NET_MODULE` | path (AppImage sets its copy; default gpu/libbbnet.so beside bb-probe) | **Internal.** The online module (libbbnet.so) the runtime dlopens. | `src/runtime_net.c:43-50`, `packaging/appimage-sharun.sh:70`, `launcher/bbport_launcher.py:595` |
|
||||
| `BB_PKGEXTRACT` | path (AppImage sets its copy) | **Internal.** pkgextract binary for "Install from PKG". | `scripts/pkg_install.py:50-51`, `launcher/bbport_launcher.py:586`, `packaging/appimage-sharun.sh:72` |
|
||||
| `BB_PREBUILT` | 1 (AppImage/Nix wrapper) | **Internal.** Packaged build: binaries are installed next to run.sh, nothing is built and no nix-shell is used; the launcher stores data in BB_DATA_DIR. | `run.sh:14`, `run.sh:284`, `launcher/bbport_launcher.py:55`, `packaging/appimage-sharun.sh:66` |
|
||||
| `BB_PRESENCE_DIR` | directory (run.sh: out/presence, new each start) | **Internal** (run.sh, when Discord is on). Where the game writes the current area (`area`) and co-op partners (`coop`) for the Discord Rich Presence helper. | `run.sh:360`, `src/runtime_file.c:317`, `gpu/bbnet/shadps4/core/libraries/np/np_signaling/np_signaling_state.cpp:248` |
|
||||
| `BB_TROPHY_DIR` | directory (run.sh sets it) | **Internal.** Trophy list and icons prepared by scripts/trophies.py; trophies are recorded there and shown in the overlay. | `run.sh:354`, `src/runtime_services.c:335` |
|
||||
| `BB_VULKANINFO` | path (default vulkaninfo) | **Internal.** vulkaninfo binary used by bbport_vulkan.py --vulkan-info (packaged builds set their copy). | `launcher/bbport_vulkan.py:199`, `packaging/appimage-sharun.sh:68` |
|
||||
| `DISABLE_GAMESCOPE_WSI` | 1 (set by the packaged entry) | **Internal.** Set when Steam's pressure-vessel Vulkan variables were dropped, turning off the host's gamescope WSI layer. | `packaging/bbport-entry.c:61-63`, `packaging/appimage-sharun.sh:87` |
|
||||
| `DISABLE_MANGOHUD` | 1 (set by run.sh) | **Internal.** Set when Steam's performance overlay already draws MangoHud. | `run.sh:250` |
|
||||
| `DISABLE_VK_LAYER_VALVE_steam_fossilize_1` | 1 (set by the Nix entry) | **Internal.** Disables Steam's fossilize Vulkan layer in the Nix package. | `packaging/bbport-entry.c:65` |
|
||||
| `DISABLE_VK_LAYER_VALVE_steam_overlay_1` | 1 (set by the Nix entry) | **Internal.** Disables Steam's overlay Vulkan layer (a host library) in the Nix package. | `packaging/bbport-entry.c:64` |
|
||||
| `GSK_RENDERER` | launcher default cairo | **Internal.** GTK renderer of the launcher window (setdefault: your value wins). | `launcher/bbport_launcher.py:33` |
|
||||
| `PYTHON` | path (default python3 on PATH) | **Internal.** Python used by run.sh for its scripts (the AppImage/Nix set their own). | `run.sh:54-62`, `packaging/appimage-sharun.sh:64` |
|
||||
| `SHADPS4_HTTP_HOST_OVERRIDES_JSON` | path to a JSON file; unset: <user>/host_overrides.json | **Internal.** Host-overrides file (game URL rewrites) for the online module and BBSC's relay lookup. run.sh writes it from scripts/online.py whenever Online is on, overwriting any value given. | `run.sh:261-283`, `gpu/bbnet/shadps4/core/libraries/network/http.cpp:336-343`, `bbsc/emulator/mod/bbsc_relay.cpp:150-156` |
|
||||
| `GSK_RENDERER` | launcher default cairo | **Internal.** GTK renderer of the launcher window (setdefault: your value wins). | `launcher/bbport_launcher.py:38` |
|
||||
| `PYTHON` | path (default python3 on PATH) | **Internal.** Python used by run.sh for its scripts (the AppImage/Nix set their own). | `run.sh:54-62`, `packaging/appimage-sharun.sh:67` |
|
||||
| `SHADPS4_HTTP_HOST_OVERRIDES_JSON` | path to a JSON file; unset: <user>/host_overrides.json | **Internal.** Host-overrides file (game URL rewrites) for the online module and BBSC's relay lookup. run.sh writes it from scripts/online.py whenever Online is on, overwriting any value given. | `run.sh:315`, `gpu/bbnet/shadps4/core/libraries/network/http.cpp:336-343`, `bbsc/emulator/mod/bbsc_relay.cpp:150-156` |
|
||||
|
||||
## Not read by the game
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Layer memory module: PS4 GPU addresses without sparse (plan, 2026-10-09)
|
||||
|
||||
## Why
|
||||
|
||||
The new memory model keeps game memory in a single sparse arena and moves blocks between game
|
||||
memory and VRAM by rebinding (`vkQueueBindSparse`). On NVIDIA (Linux, drivers ≥ 555), the bind
|
||||
time grows with the number of pages already bound, and the call holds a global lock: each
|
||||
move to VRAM cost seconds, and the game and the desktop froze (GTX 1660 Ti, 615.71). Without moves
|
||||
(`BB_FIXED_ARENA=1`) it runs at 48.6 FPS versus ~205 on an RX 7800 XT. We need a model that works the
|
||||
same on AMD, NVIDIA and Intel and is not tied to the shadPS4 buffer cache: part of a future layer
|
||||
for running PS4 games.
|
||||
|
||||
## Idea
|
||||
|
||||
The PS4 GPU has its own virtual memory (GPUVM) on top of shared memory; every V#, T# and pointer in
|
||||
user data is a virtual address. On PC we do the same with Vulkan 1.2 means (BDA):
|
||||
|
||||
- **Memory sources**: ordinary (non-sparse) Vulkan buffers:
|
||||
- chunks of game memory: dma-buf (AMD) or host memory import (NVIDIA, Intel), one chunk per
|
||||
game direct memory allocation (allocations are mapped whole, so a range inside one
|
||||
mapping is always contiguous within one chunk);
|
||||
- VRAM mirrors: segments aligned to game addresses (game page X sits in the segment at the
|
||||
same offset), allocated when the first VRAM page appears in the segment.
|
||||
- **Page table** (a buffer on the GPU): page of a PS4 address → device address (BDA) of its
|
||||
current source. It is changed by a copy in the command stream, in order with draws, without semaphores.
|
||||
- **Direct path**: the whole V# range is in one source (almost all bindings) → an ordinary
|
||||
descriptor (source buffer, offset, size). The shader translator does not change.
|
||||
- **Paged path**: the range is mixed (≈85 huge bindings per frame: shaders bound
|
||||
to all of memory) → the descriptor points to a record {PS4 address, size}, and the shader
|
||||
translates each address through the table. A pipeline variant only for such bindings
|
||||
(specialization key).
|
||||
- **Vertices, indices, indirect arguments, copies, texture uploads**: the direct path; otherwise,
|
||||
assembly into a contiguous temporary buffer (read-only) or per-page copies (the CPU knows the table).
|
||||
- **Moving to VRAM and back**: the policy of the current new model (data whose writes we
|
||||
know about: loaders, the GPU; unknown writers stay in place), but the execution is a copy + a
|
||||
table write + choosing the source at bind time. Bindings recorded before a move read the old
|
||||
source, which is still valid; VRAM is freed once the GPU has passed those commands.
|
||||
|
||||
## Order of work
|
||||
|
||||
1. **Module core** (`gpu/layer/`, no dependencies on shadPS4): sources, pages, table,
|
||||
`Resolve(address, size)` → a direct source or "mixed", walking over sources for copies.
|
||||
The command recording interface is abstract (copy, table write), so it does not pull in the scheduler.
|
||||
2. **Hook-up without VRAM**: the new model returns ranges in place through chunk buffers, not
|
||||
through the arena; the new model does not create the sparse arena. Expected: ~48 FPS on AMD (like
|
||||
`BB_FIXED_ARENA`), no `vkQueueBindSparse` at all. Check the image, then make a build for NVIDIA.
|
||||
3. **VRAM mirrors + moves**: the policy from `BufferCache`, the page table, the paged path in
|
||||
shaders for mixed bindings. Goal: parity with the sparse arena on AMD (~205 FPS).
|
||||
4. **NVIDIA/Intel**: a test build, a tester log; then remove sparse from the new model entirely.
|
||||
|
||||
Measurements: RX 7800 XT, standing still (`tools/bench.sh`), `BB_PC_MODEL=1`; diagnostics `BB_RESIDENCY_MIX=1`.
|
||||
|
||||
## Status (2026-10-09, morning)
|
||||
|
||||
- Steps 1–3 are done. On the RX 7800 XT, standing still: module ~205 FPS, sparse arena ~205–238, old model
|
||||
~220 (at the lamp in the Hunter's Nightmare); in another spot (1170 draw calls): 167.5 / 164.5 / 146.
|
||||
`vkQueueBindSparse` is never called in module mode (counter in the `Layer memory:` line).
|
||||
- Mixed bindings are solved without the paged path: "volatile" blocks (`layer_volatile`). A block with
|
||||
unknown bytes (the edge of a range, a gap between images) goes into the mirror together with
|
||||
the rest of the range, is refreshed from game memory before each binding, and whatever a dispatch
|
||||
wrote into it is copied back after the dispatch (`LayerAfterBindings`). The paged path remains for
|
||||
bindings of 64 MiB and larger. `BB_LAYER_VOLATILE=0` turns it off (for comparison).
|
||||
- Default: AMD uses the sparse arena, other GPUs use the module (`BB_LAYER_MEMORY=1/0` to
|
||||
choose manually). Step 4 (NVIDIA): needs a tester log; the host memory import path cannot be
|
||||
checked on AMD (RADV does not import memfd).
|
||||
|
||||
## Addendum: original copying and portable labels (2026-10-09)
|
||||
|
||||
In the new module, the paged path of the original copy shader is fixed: writes update game memory and the current mirror; the native shader is now selected by default. For GPU labels, a standard Vulkan path without AMD buffer marker was added. The implementation, switches and verification limits are described in [TRANSLATION_GPU_COPY_LABELS.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/TRANSLATION_GPU_COPY_LABELS.ru.md). This removes the dependency of label writes on the AMD extension, but does not replace hardware testing on NVIDIA.
|
||||
@@ -1,68 +0,0 @@
|
||||
# Модуль памяти прослойки: адреса видеочипа PS4 без sparse (план, 2026-10-09)
|
||||
|
||||
## Зачем
|
||||
|
||||
Новая модель памяти держит память игры в одной sparse-арене и переносит блоки между памятью
|
||||
игры и VRAM перепривязкой (`vkQueueBindSparse`). На NVIDIA (Linux, драйверы ≥ 555) время
|
||||
привязки растёт с числом уже привязанных страниц, вызов держит глобальную блокировку: каждый
|
||||
перенос в VRAM стоил секунды, игра и рабочий стол вставали (GTX 1660 Ti, 615.71). Без переносов
|
||||
(`BB_FIXED_ARENA=1`) — 48,6 FPS против ~205 на RX 7800 XT. Нужна модель, работающая одинаково на
|
||||
AMD, NVIDIA и Intel, и не привязанная к буферному кэшу shadPS4: часть будущей прослойки запуска
|
||||
игр PS4.
|
||||
|
||||
## Идея
|
||||
|
||||
У видеочипа PS4 своя виртуальная память (GPUVM) над общей памятью; любой V#, T#, указатель в
|
||||
user data — виртуальный адрес. На ПК то же самое делаем средствами Vulkan 1.2 (BDA):
|
||||
|
||||
- **Источники памяти** — обычные (не sparse) буферы Vulkan:
|
||||
- куски памяти игры: dma-buf (AMD) или импорт памяти хоста (NVIDIA, Intel), по куску на
|
||||
выделение прямой памяти игры (выделения отображаются целиком — диапазон внутри одного
|
||||
отображения всегда непрерывен в одном куске);
|
||||
- зеркала в VRAM: сегменты, выровненные по адресам игры (страница игры X лежит в сегменте по
|
||||
тому же смещению), выделяются, когда в сегменте появляется первая страница в VRAM.
|
||||
- **Таблица страниц** (буфер на видеочипе): страница адреса PS4 → адрес устройства (BDA) её
|
||||
текущего источника. Меняется копией в потоке команд — в порядке с отрисовками, без семафоров.
|
||||
- **Прямой путь**: диапазон V# целиком в одном источнике (почти все привязки) → обычный
|
||||
дескриптор (буфер источника, смещение, размер). Транслятор шейдеров не меняется.
|
||||
- **Страничный путь**: диапазон смешанный (≈85 огромных привязок за кадр: шейдеры, привязанные
|
||||
ко всей памяти) → дескриптор указывает на запись {адрес PS4, размер}, шейдер переводит каждый
|
||||
адрес через таблицу. Вариант пайплайна только для таких привязок (ключ специализации).
|
||||
- **Вершины, индексы, аргументы indirect, копии, загрузка текстур**: прямой путь, иначе сборка
|
||||
в непрерывный временный буфер (только чтение) или копии по страницам (процессор знает таблицу).
|
||||
- **Перенос в VRAM и обратно** — политика нынешней новой модели (данные, о записи которых мы
|
||||
знаем: загрузчики, видеочип; неизвестные писатели — на месте), но исполнение — копия + запись в
|
||||
таблицу + выбор источника при привязке. Привязки, записанные до переноса, читают старый
|
||||
источник — он ещё верен; освобождение VRAM — когда видеочип прошёл эти команды.
|
||||
|
||||
## Порядок работ
|
||||
|
||||
1. **Ядро модуля** (`gpu/layer/`, без зависимостей от shadPS4): источники, страницы, таблица,
|
||||
`Resolve(адрес, размер)` → прямой источник или «смешанный», обход по источникам для копий.
|
||||
Интерфейс записи команд — абстрактный (копия, запись таблицы), чтобы не тянуть планировщик.
|
||||
2. **Подключение без VRAM**: новая модель отдаёт диапазоны на месте через буферы кусков, а не
|
||||
через арену; sparse-арена в новой модели не создаётся. Ожидание: ~48 FPS на AMD (как
|
||||
`BB_FIXED_ARENA`), никаких `vkQueueBindSparse`. Проверка картинки, затем сборка для NVIDIA.
|
||||
3. **Зеркала в VRAM + перенос**: политика из `BufferCache`, таблица страниц, страничный путь в
|
||||
шейдерах для смешанных привязок. Цель — паритет с sparse-ареной на AMD (~205 FPS).
|
||||
4. **NVIDIA/Intel**: тестовая сборка, лог тестера; затем убрать sparse из новой модели совсем.
|
||||
|
||||
Замеры — RX 7800 XT, стоя (`tools/bench.sh`), `BB_PC_MODEL=1`; диагностика `BB_RESIDENCY_MIX=1`.
|
||||
|
||||
## Состояние (2026-10-09, утро)
|
||||
|
||||
- Шаги 1–3 сделаны. На RX 7800 XT стоя: модуль ~205 FPS, sparse-арена ~205–238, старая модель
|
||||
~220 (у лампы в Кошмаре охотника); в другом месте (1170 вызовов отрисовки) — 167,5 / 164,5 / 146.
|
||||
`vkQueueBindSparse` в режиме модуля не вызывается ни разу (счётчик в строке `Layer memory:`).
|
||||
- Смешанные привязки решены без страничного пути: «летучие» блоки (`layer_volatile`) — блок с
|
||||
неизвестными байтами (край диапазона, промежуток между образами) кладётся в зеркало вместе с
|
||||
остальным диапазоном, обновляется из памяти игры перед каждой привязкой, а то, что запуск в него
|
||||
записал, копируется обратно после запуска (`LayerAfterBindings`). Страничный путь остаётся для
|
||||
привязок от 64 МиБ. `BB_LAYER_VOLATILE=0` — выключить (для сравнения).
|
||||
- По умолчанию: AMD — sparse-арена, остальные видеокарты — модуль (`BB_LAYER_MEMORY=1/0` —
|
||||
вручную). Шаг 4 (NVIDIA): нужен лог тестера; путь импорта памяти хоста на AMD не проверить
|
||||
(RADV не импортирует memfd).
|
||||
|
||||
## Дополнение: оригинальное копирование и переносимые метки (2026-10-09)
|
||||
|
||||
В новом модуле исправлен paged-путь оригинального copy shader: записи обновляют память игры и актуальное зеркало; нативный шейдер теперь выбран по умолчанию. Для GPU-меток добавлен стандартный Vulkan-путь без AMD buffer marker. Реализация, переключатели и границы проверки описаны в [TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md). Это закрывает зависимость записи меток от AMD-расширения, но не заменяет аппаратную проверку NVIDIA.
|
||||
+1
-1
@@ -114,4 +114,4 @@ the launcher.
|
||||
|
||||
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.
|
||||
This guide is based on bbport's [MODS.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/MODS.md) (Russian), by deadinside28.
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
# Файловые моды
|
||||
|
||||
В лаунчере, в разделе **Модификации**, есть группа **Моды**: выбор папки, общий переключатель,
|
||||
включение каждого мода и стрелки порядка загрузки. Изменения применяются при
|
||||
следующем запуске. Нижний включённый мод имеет больший приоритет; при совпадении
|
||||
файлов в журнале выводится, какой мод заменил предыдущий.
|
||||
|
||||
По умолчанию используется `bbport/mods/`, а в AppImage —
|
||||
`~/.local/share/bbport/mods/`. Распакуйте скачанный мод так:
|
||||
|
||||
```text
|
||||
mods/
|
||||
Название мода/
|
||||
dvdroot_ps4/
|
||||
chr/...
|
||||
param/...
|
||||
menu/...
|
||||
```
|
||||
|
||||
Также принимаются:
|
||||
|
||||
- `Название мода/app0/dvdroot_ps4/...` и `Название мода/CUSA03173/dvdroot_ps4/...`;
|
||||
- до трёх лишних папок-обёрток, как после распаковки архива в папку с его именем:
|
||||
`Название мода/Название мода v1.2/dvdroot_ps4/...` (рядом могут лежать заметки, картинки и
|
||||
ссылки: `README`, `.txt`, `.pdf`, `.png`, `.webp`, `.url`, ...);
|
||||
- мод без `dvdroot_ps4`, в котором сразу лежат папки игры: `Название мода/chr/...`,
|
||||
`Название мода/parts/...` (`chr`, `parts`, `map`, `menu`, `msg`, `param`, `sfx`,
|
||||
`sound`, `font` и другие папки `dvdroot_ps4`), в том числе рядом с другими папками вроде
|
||||
`Screenshots` или `Optional`, которые в игру не попадают.
|
||||
|
||||
Папка в папке модов, которая не является модом, показывается в лаунчере под модами с причиной:
|
||||
это ещё архив, несколько модов в одной папке, нет файлов игры или папка пуста.
|
||||
|
||||
Регистр имён в моде не важен: `DVDROOT_PS4/Chr/C0000.chrbnd.dcx` из мода, собранного
|
||||
под Windows, заменяет игровой `dvdroot_ps4/chr/c0000.chrbnd.dcx`. Раньше такой файл
|
||||
добавлялся рядом под своим именем, и игра его не видела. В журнале запуска видно, сколько
|
||||
файлов игры мод заменил и сколько добавил: `Mods: 12 game files replaced, 0 added`.
|
||||
Если мод должен заменять файлы, а пишет `0 game files replaced`, проверьте пути внутри мода.
|
||||
Архивы ZIP/7z нужно предварительно распаковать. Поддерживаются готовые замены
|
||||
файлов `dvdroot_ps4`: текстуры, модели, параметры, интерфейс, звук и шрифты.
|
||||
Менеджер не объединяет содержимое двух одинаковых `.dcx`/`.bnd` файлов: последняя
|
||||
замена выигрывает целиком.
|
||||
|
||||
Для уже подготовленной папки shadPS4 работает соседний каталог
|
||||
`CUSA03173-mods/dvdroot_ps4/...` рядом с `CUSA03173/`. Он подключается автоматически
|
||||
перед модами из списка. Общий переключатель выключает и его. Можно также выбрать
|
||||
папку, непосредственно содержащую `dvdroot_ps4`, как папку модов; такая папка
|
||||
подключается целиком, без списка отдельных модов.
|
||||
Соглашение `-mods` проверено по
|
||||
[исходникам shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/file_sys/fs.cpp).
|
||||
|
||||
Оригиналы игры не перезаписываются. На время запуска создаётся отдельное
|
||||
объединённое дерево ссылок в каталоге данных `out/mod-game-*`; чтение, stat и
|
||||
перечисление каталогов видят выбранные замены и остальные оригинальные файлы.
|
||||
Игровые `/app0` и `/hostapp` доступны только для чтения. Сохранения остаются
|
||||
в прежней папке и доступны для записи. При обычном завершении или остановке
|
||||
лаунчером временное дерево удаляется. После SIGKILL/аварийного отключения оно
|
||||
может остаться; такие каталоги не используются в следующем запуске.
|
||||
|
||||
Моды с заменой `eboot.bin`, `sce_module` или `sce_sys`, инжекторы DLL и скриптовые
|
||||
плагины этим загрузчиком не поддерживаются. Патчи исполняемого кода остаются
|
||||
в существующей системе `patches/` / `BB_PATCHES`. Символьные ссылки внутри мода
|
||||
не принимаются. Совместимость конкретного мода с CUSA03173 1.09 определяется
|
||||
его автором.
|
||||
|
||||
Для запуска из терминала:
|
||||
|
||||
```sh
|
||||
BB_GAME_DIR=/path/to/CUSA03173 BB_MODS_DIR=/path/to/mods bash run.sh
|
||||
BB_GAME_DIR=/path/to/CUSA03173 BB_MODS_ENABLED=0 bash run.sh
|
||||
```
|
||||
|
||||
Порядок и отключения сохраняются в каталоге данных `mods.json`:
|
||||
|
||||
```json
|
||||
{"order": ["Первый", "Второй"], "disabled": ["Первый"]}
|
||||
```
|
||||
|
||||
Новые папки включаются автоматически и добавляются в конец по алфавиту.
|
||||
`BB_MODS_CONFIG` меняет путь профиля. `BB_MOD_TRACE=1` с `BB_MODS_DIR` выводит
|
||||
первые чтения реально подключённых файлов для диагностики.
|
||||
|
||||
# Сторонние патчи (XML shadPS4)
|
||||
|
||||
Папка `patches/` в каталоге данных (в AppImage — `~/.local/share/bbport/patches/`, в лаунчере
|
||||
группа **Сторонние патчи**, там же можно выбрать другую папку). Подходят XML-файлы патчей в
|
||||
формате shadPS4/GoldHEN: берутся `Metadata` для `AppVer="01.09"` и `eboot.bin`, файлы с
|
||||
`TitleID` других игр пропускаются. Включение по умолчанию — из `isEnabled` файла, выбор в
|
||||
лаунчере сохраняется в `patches.json` каталога данных:
|
||||
|
||||
```json
|
||||
{"enabled": ["файл.xml/Имя патча"], "disabled": ["файл.xml/Другой патч"]}
|
||||
```
|
||||
|
||||
Поддерживаются строки `bytes`, `bytes16/32/64`, `float32/64`, `utf8`, `utf16`.
|
||||
Патч со строками `mask` (поиск по шаблону) или с адресом вне eboot пропускается целиком,
|
||||
с сообщением в журнале. Сторонние патчи применяются после встроенных
|
||||
(`patches/Bloodborne.xml`): при пересечении побеждает сторонний. Из терминала:
|
||||
`BB_PATCHES_DIR=/путь BB_PATCHES_CONFIG=/путь/patches.json bash run.sh`.
|
||||
|
||||
Патчи, которые заменяют указатели в таблицах (как `60 FPS++` и `90 FPS++`), загрузчик
|
||||
переносит на реальный адрес образа. Раньше такие патчи останавливали запуск ошибкой
|
||||
`patch overlaps a relocation`.
|
||||
|
||||
# Резкость TAA
|
||||
|
||||
Настройки `sharpen` и `sharpness` теперь работают также для TAA, в лаунчере и
|
||||
меню игры. Диапазон — **0…1**; при 0 или выключенном переключателе дополнительного
|
||||
прохода нет. RCAS применяется к готовой сцене перед HUD. В историю TAA сохраняется
|
||||
цвет до повышения резкости, чтобы она не усиливалась при повторном накоплении.
|
||||
Ровные участки сохраняют исходный цвет, включая HDR; дополнительный буфер не нужен.
|
||||
@@ -1,232 +0,0 @@
|
||||
# Новая модель памяти на NVIDIA: итог работы (2026-10-09)
|
||||
|
||||
Замеры ниже относятся к этапу разработки модуля. Актуальные изменения `0.5-pre2` и результаты
|
||||
проверки оригинального copy shader: [CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md) и
|
||||
[TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md).
|
||||
|
||||
## Задача
|
||||
|
||||
Новая модель памяти и трансляции (`BB_PC_MODEL=1`) держала память игры в одном sparse-буфере
|
||||
(«арене») и переносила горячие блоки в VRAM перепривязкой страниц (`vkQueueBindSparse`). На
|
||||
проприетарном драйвере NVIDIA для Linux это не работает: начиная с версии 555 время
|
||||
`vkQueueBindSparse` растёт с числом уже привязанных страниц, а сам вызов держит глобальную
|
||||
блокировку. У тестера (GTX 1660 Ti, драйвер 615.71) каждый перенос держал видеочип 10 секунд и
|
||||
дольше, и вместе с игрой вставал рабочий стол. Поэтому новая модель была доступна только на AMD.
|
||||
|
||||
Цель: новая модель памяти на всех видеокартах.
|
||||
|
||||
## Решение: модуль памяти прослойки
|
||||
|
||||
Вместо sparse-арены работает собственный модуль памяти (`gpu/layer/bblayer_gpu_memory.*`,
|
||||
`gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp`, план — `MEMORY_MODULE_PLAN.ru.md`):
|
||||
|
||||
- память игры привязывается **на месте**, через буферы её кусков (dma-buf на AMD, импорт памяти
|
||||
хоста `VK_EXT_external_memory_host` на NVIDIA);
|
||||
- часто читаемое копируется в VRAM в отдельные буферы-«зеркала». Перенос сводится к копии и
|
||||
учёту в модуле, без перепривязки страниц;
|
||||
- привязка берёт зеркало, если диапазон целиком в VRAM, иначе память игры на месте. Диапазоны от
|
||||
64 МиБ идут через таблицу страниц в транслированных шейдерах.
|
||||
|
||||
**`vkQueueBindSparse` в этом режиме не вызывается ни разу**: счётчик выводится в строке журнала
|
||||
`Layer memory: ... vkQueueBindSparse: 0 calls`.
|
||||
|
||||
### Когда включается
|
||||
|
||||
| Видеокарта | По умолчанию |
|
||||
|---|---|
|
||||
| AMD | sparse-арена, как раньше |
|
||||
| NVIDIA, Intel и остальные | модуль памяти, автоматически |
|
||||
|
||||
`BB_LAYER_MEMORY=1` / `=0` выбирает вручную. Лаунчер больше не прячет переключатель «Новая модель
|
||||
памяти и трансляции» на видеокартах не от AMD. Стартовая проверка импорта памяти хоста в режиме
|
||||
модуля проверяет запись видеочипа и процессора через сам импортированный буфер, без sparse-буфера.
|
||||
|
||||
## Что пришлось исправить, чтобы модуль заработал правильно и быстро
|
||||
|
||||
### Скорость: с 48–60 FPS до ~205 FPS на AMD (стоя у лампы)
|
||||
|
||||
1. **Подмена копий и очисток образов не срабатывала.** Детекторы `IsComputeImageCopy` и
|
||||
`IsComputeImageClear` ждали у шейдера ровно два буфера, а модуль добавлял каждому шейдеру
|
||||
третий — таблицу страниц. Копия целей рендеринга (`cs 3d5ebf4e`) и очистка (`cs 8b355b5a`,
|
||||
31 раз за кадр) выполнялись настоящими шейдерами через память, а образы 1080p
|
||||
перезагружались каждый кадр. Теперь таблица страниц не считается (`ShaderBufferCount`):
|
||||
60 → 136 FPS.
|
||||
2. **Смешанные диапазоны.** Если у диапазона все блоки в VRAM, кроме одного-двух крайних с
|
||||
«неизвестными» байтами (промежуток между образами, чужие данные), весь диапазон читался через
|
||||
шину. Такие блоки теперь «летучие»: лежат в зеркале и обновляются из памяти игры перед каждой
|
||||
привязкой на чтение. 139 → 207 FPS. Для привязок на запись летучие блоки не используются.
|
||||
3. **Перенос в VRAM при первой привязке** диапазона известных данных (как делала арена). Он
|
||||
соблюдает `BB_GARLIC_VRAM=0` и бюджет VRAM (`BB_VRAM_PROMOTE_PERCENT`, важно для карт на 6 ГБ).
|
||||
4. **Нагрузка на процессор.** Ответы модуля кэшируются по поколению размещения; повторные
|
||||
проверки «можно ли перенести» — не чаще раза за кадр на диапазон; проверка «есть ли у
|
||||
диапазона копия в VRAM» — без блокировок, по счётчику версий модуля. Это сняло около 20%
|
||||
времени привязок и почти все блокировки в потоке командного процессора.
|
||||
|
||||
### Правильность: взрывы вершин и мусорные текстуры
|
||||
|
||||
Причина была одна: **копия в VRAM расходилась с памятью игры**. Найдено и закрыто несколько
|
||||
путей:
|
||||
|
||||
1. **Код игры пишет в память без нашего ведома**: заголовки её кучи (`0x2085a60` / `0x20858a0`),
|
||||
часть загрузчика ресурсов (`0x216d420`). Блоки с копией в VRAM теперь защищены от записи
|
||||
собственными ловушками модуля. Запись проходит сразу, а блок возвращается на место перед
|
||||
следующей привязкой. Часто записываемые блоки остаются на месте навсегда (`NoteCpuWrite`).
|
||||
2. **Чтения мимо модуля.** Мелкие привязки, кольцо констант и копирования командного процессора
|
||||
брали данные из памяти игры, хотя новые данные видеочипа лежали в VRAM: в новой модели
|
||||
`IsRegionGpuModified` знает только о незавершённых записях. Теперь всё, что задевает блок с
|
||||
копией в VRAM, идёт через модуль (`LayerMirrored`).
|
||||
3. **Данные видеочипа затирали новые данные процессора.** При возврате блока на место и при
|
||||
повторной выдаче памяти игре модуль копировал «изменённое видеочипом» обратно, в том числе
|
||||
поверх того, что процессор успел записать позже: пометки ставились слишком широко. Теперь
|
||||
заявленные записи процессора снимают пометки со своих байтов (`LayerForgetGpuData`), выдача
|
||||
памяти заново снимает пометки со своего диапазона, а после незаявленной записи данные видеочипа
|
||||
в блоке считаются мёртвыми и назад не копируются.
|
||||
4. **Куда видеочипу можно писать в VRAM** (`BB_LAYER_GPU_WRITES`): по умолчанию (`1`) — только в
|
||||
блоки, которые процессор не трогал с момента выделения: цели рендеринга и другие данные одного
|
||||
видеочипа. `0` — никогда (правильно, но 60–70 FPS: записи идут через шину), `2` — везде (давало
|
||||
взрывы).
|
||||
5. **Шейдер копирования игры** в `0.5-pre2` по умолчанию транслируется в SPIR-V и выполняется
|
||||
на GPU. Таблица страниц содержит актуальное GPU-представление и адреса памяти игры;
|
||||
copy shader записывает результат в оба представления. Это устраняет расхождение зеркала
|
||||
VRAM и памяти игры без подмены оригинального списка копий на CPU.
|
||||
`BB_COPY_SHADER_NATIVE=0` оставляет HLE для сравнения.
|
||||
|
||||
На предыдущем этапе проверено в игре на AMD (модуль включён вручную, шейдер копирования через HLE): с
|
||||
`BB_LAYER_GPU_WRITES=0` и с правилом «видеочип пишет в VRAM только внутри образов» — Сон
|
||||
Охотника, собаки и трупы в Ярнаме чистые. На последней сборке (правило «блоки, которые процессор
|
||||
не трогал») артефактов при игре не замечено, но отдельной проверки всех мест ещё не было.
|
||||
|
||||
## Замеры (RX 7800 XT, AMD, модуль включён вручную)
|
||||
|
||||
| Место | Модуль | Sparse-арена | Старая модель |
|
||||
|---|---|---|---|
|
||||
| У лампы, утром (до исправлений правильности) | ~205 FPS | ~205–238 | ~220 |
|
||||
| Сон Охотника, сейчас | 130–144 FPS | — | — |
|
||||
|
||||
В Сне Охотника упираемся то в поток команд видеочипа на процессоре (~3,9 мкс на вызов при
|
||||
~1500 вызовах за кадр), то в сам видеочип: проходы G-буфера 960×540, FSR ~1,07 мс.
|
||||
|
||||
**Открытый вопрос:** в части запусков на том же месте видеочип работает заметно медленнее
|
||||
(70–95 FPS, FSR до 3,2 мс вместо 1,07). Причина не найдена; версии с компиляцией шейдеров,
|
||||
поучастковым профилем `BB_SECTIONS` и обновлением летучих блоков не подтвердились. Сравнение
|
||||
запусков осложняет то, что игра рисует разные проходы в зависимости от ракурса и сцены.
|
||||
|
||||
## Что не проверено
|
||||
|
||||
- **Сама NVIDIA.** На AMD путь NVIDIA (импорт памяти хоста + модуль) не воспроизвести: RADV не
|
||||
импортирует memfd. Нужен лог тестера.
|
||||
- Карты с малым объёмом VRAM (6 ГБ): поведение при заполненной VRAM проверено только
|
||||
искусственно (`BB_VRAM_PROMOTE_PERCENT=1`: копий нет, ~38 FPS, стабильно).
|
||||
- Intel.
|
||||
|
||||
## Что попросить у тестера на NVIDIA
|
||||
|
||||
Сборку с включённой в лаунчере «Новой моделью памяти и трансляции». В журнале должны быть строки:
|
||||
|
||||
- `Guest memory: host memory imported by the GPU (VK_EXT_external_memory_host ...)`;
|
||||
- `Guest memory: the new memory model through the layer's memory module on this GPU (vendor 0x10de)`;
|
||||
- `Layer memory: ... vkQueueBindSparse: 0 calls, 0 ranges; write traps hit N`;
|
||||
- `Frame stats: ... FPS`.
|
||||
|
||||
Если видна строка `vkQueueBindSparse of N ranges took X ms`, значит, sparse где-то всё же
|
||||
используется — это ошибка.
|
||||
|
||||
## Первые логи с NVIDIA и исправления (вечер 2026-10-09, `0.5-pre3`)
|
||||
|
||||
Тестер: GTX 1660 Ti (ноутбук, 6 ГБ, PCIe 3.0), драйвер 615.71, сборка 0.5-pre2. Импорт памяти
|
||||
хоста и модуль включились, `vkQueueBindSparse: 0`.
|
||||
|
||||
1. **Зеркал не было вовсе** (10–15 FPS, мерцание фона на титульном экране): `FixedArena()` по
|
||||
умолчанию включалась при импорте памяти хоста и через `GarlicInVramForLayer()` запрещала модулю
|
||||
переносы. Теперь в режиме модуля она всегда выключена. До новой сборки обход — `BB_FIXED_ARENA=0`:
|
||||
с ним у тестера 30–35 FPS (видео), против 60–70 в старой модели.
|
||||
2. **Кэш текстур и зеркала делили VRAM без общего бюджета.** Сборщик кэша текстур сравнивает с
|
||||
порогами всю VRAM процесса, зеркала тоже, а пороги на картах меньше ~9 ГБ — 40 и 80 %
|
||||
стартового бюджета (у тестера давление 1963 МиБ, критическая отметка 3927 МиБ). Переносы же
|
||||
вставали только на 90 % бюджета драйвера. Итог — 3,9–4,0 ГБ, сборщик постоянно за критической
|
||||
отметкой: 155–1255 выселенных образов за 5 с против 0–29 на AMD. Теперь:
|
||||
- переносы в VRAM встают за 384 МиБ до критической отметки (`VramPromotionsPaused`, строка
|
||||
`copies of loaded blocks wait`);
|
||||
- за 128 МиБ до неё модуль сам освобождает место (`LayerYieldToImages`): сначала обрезает запас
|
||||
роста зеркал, затем возвращает на место целые зеркала, не использовавшиеся 3 с
|
||||
(`BB_VRAM_PRESSURE_IDLE_SECONDS`).
|
||||
3. **Перестройка зеркал.** Участок, примыкающий к зеркалу, создавал новое зеркало целиком с
|
||||
копией старого. При загрузке уровня это 15–27 ГБ выделений на 1,3 ГБ данных и всплеск VRAM до
|
||||
7,4 ГБ (у тестера в логе 5,6 ГБ при физических 6). Во время игры, при подгрузке районов,
|
||||
то же самое. Теперь растущее вверх зеркало получает запас под рост, равный своему размеру, но
|
||||
не больше 64 МиБ (`BB_LAYER_MIRROR_GROWTH_MB`): 5–10 ГБ выделений, худший кадр загрузки 555 →
|
||||
87 мс. Заменённые зеркала освобождаются на каждой отправке, а не раз в секунду.
|
||||
4. **Записи `WRITE_DATA` в зеркало.** Командный процессор пишет на видеочипе в память игры; блок
|
||||
с зеркалом раньше уходил на место. Если диапазон целиком в одном зеркале, запись идёт в оба
|
||||
представления (`CommandWriteMirror`).
|
||||
5. **Наблюдаемые блоки** (`BB_LAYER_WATCH`, по умолчанию включено). Блоки, в которых процессор
|
||||
когда-то писал без предупреждения, раньше всегда читались на месте, а значит на NVIDIA через
|
||||
шину. Теперь для читающих привязок они тоже получают зеркало под ловушкой записи. Запись
|
||||
процессора не возвращает блок на место: он перезаливается перед следующей привязкой, а
|
||||
ловушка взводится снова. Видеочип в такие блоки пишет на месте; блок, куда он записал,
|
||||
больше не наблюдается. Наблюдение почти полностью заменило летучие блоки (RX 7800 XT:
|
||||
1,3 ГБ/с перезаливок → 36 МБ/с; у тестера летучих было 455 МБ/с). Блоки, которые процессор
|
||||
пишет часто (3 кадра из 60), по-прежнему уходят на место: перезаливка их перед привязками
|
||||
завершала render pass (видеочип 4,8 → 7,2 мс за кадр). `BB_LAYER_WATCH_HOT=1` — эксперимент
|
||||
с ними.
|
||||
6. Чтения файлов рантайма (`read`/`pread`) повторяются, если ядро вернуло `EFAULT`: ловушку могли
|
||||
взвести снова между касанием страниц и чтением.
|
||||
|
||||
Проверка на RX 7800 XT с `BB_VRAM_LIMIT_MB=5600` (пороги сборщика как у тестера: 1960/3920):
|
||||
старая модель ~195 FPS; модуль ~200–213 FPS, видеочип 4,6–5,2 мс; VRAM держится ниже
|
||||
критической отметки; маршрут `docs/route2.txt` без кадров дольше 25 мс; 3 минуты прогулки без
|
||||
ошибок; картинка в Сне Охотника и в конце маршрута как без наблюдения. Сама NVIDIA этими
|
||||
замерами не проверена: на AMD чтения на месте почти бесплатны.
|
||||
|
||||
## Отзывы на 0.5-pre3 и исправления (`0.5-pre4`)
|
||||
|
||||
- **GTX 1060 6 ГБ:** новая модель вровень со старой (41–48 FPS при ~1500 вызовах за кадр в обеих,
|
||||
видеочип 21–24 мс). Ранние просадки — компиляция шейдеров в первой сессии (160–241 компиляция
|
||||
за 5 с, рывки 1–3,7 с): теперь новые конвейеры проходов, рисуемых каждый кадр, собираются в
|
||||
фоне (`BB_ASYNC_PIPELINES`), а их отрисовки кадр-другой пропускаются.
|
||||
- **Карта на 4 ГБ:** через ~4,5 минуты 5 FPS — регрессия pre3. Переносы в VRAM вставали за
|
||||
384 МиБ до критической отметки сборщика текстур (у карты 4 ГБ — нижние значения порогов,
|
||||
1536/3072 МиБ), а текстуры и служебные выделения уже держали 2808 МиБ: копий стало 124 МиБ на
|
||||
1,9 ГБ данных, видеочип ~200 мс на кадр. Теперь у зеркал своя доля (25 % бюджета драйвера,
|
||||
`BB_LAYER_MIRROR_FLOOR_PERCENT`): ниже неё пауза из-за сборщика не действует.
|
||||
- **После телепорта ~10 FPS меньше:** большие привязки на всю память (через таблицу страниц)
|
||||
помечали каждые 2 МиБ как используемые, поэтому в игре ничего не простаивало, и зеркала
|
||||
покинутой локации держали VRAM. Теперь учитываются только прямые привязки
|
||||
(`layer_group_use`): при нехватке VRAM сначала уходят данные, к которым напрямую не
|
||||
привязывались 3 с, а зеркала с памятью без действительных блоков пересоздаются только поверх
|
||||
действительных (`LayerCompactMirrors`) — иначе частично вернувшееся на место зеркало держало
|
||||
всю свою память. На RX 7800 XT с `BB_VRAM_LIMIT_MB=4200–4600` это освобождало 160–490 МиБ за
|
||||
раз, картинка не менялась. На NVIDIA не проверено.
|
||||
|
||||
## Переключатели и диагностика
|
||||
|
||||
| Переменная | Что делает |
|
||||
|---|---|
|
||||
| `BB_LAYER_MEMORY=1/0` | модуль памяти вкл/выкл (по умолчанию: на не-AMD вкл) |
|
||||
| `BB_LAYER_GPU_WRITES=0/1/2` | где видеочипу можно писать в VRAM (см. выше), по умолчанию 1 |
|
||||
| `BB_LAYER_WRITE_TRAPS=0` | без ловушек записи (только для сравнения: вернутся взрывы) |
|
||||
| `BB_LAYER_VOLATILE=1` | с летучими блоками (с 2026-10-10 выключены по умолчанию: их копии разрывали проходы рендера) |
|
||||
| `BB_LAYER_PAGED=0` | без таблицы страниц для привязок от 64 МиБ |
|
||||
| `BB_COPY_SHADER_NATIVE=0/1` | HLE / нативный шейдер копирования игры; в новой модели по умолчанию нативный |
|
||||
| `BB_RESIDENCY_MIX=1/2` | привязки на месте / в VRAM / смешанные; `2` — ещё и самые тяжёлые с разбором блоков |
|
||||
| `BB_LAYER_WRITE_LOG=1` | крупные записи видеочипа, оставленные на месте, и образы рядом |
|
||||
| `BB_TOGGLE_FILE`, бит эксперимента 8 | чтение на месте вместо копий в VRAM (проверка устаревших копий) |
|
||||
| `BB_TOGGLE_FILE`, бит эксперимента 16 | без обновлений летучих блоков |
|
||||
| `BB_LAYER_WATCH=0` | без наблюдаемых блоков (блоки, где писал процессор, читаются на месте) |
|
||||
| `BB_LAYER_WATCH_HOT=1` | наблюдать и часто записываемые процессором блоки (эксперимент) |
|
||||
| `BB_LAYER_MIRROR_GROWTH_MB=N` | запас роста зеркала, по умолчанию 64; `0` — без запаса |
|
||||
| `BB_VRAM_PRESSURE_IDLE_SECONDS=N` | при нехватке VRAM на место уходят данные, к которым напрямую не привязывались N с (3; `0` — нет) |
|
||||
| `BB_LAYER_MIRROR_FLOOR_PERCENT=N` | доля бюджета VRAM, которую сборщик текстур не забирает у зеркал (25) |
|
||||
| `BB_ASYNC_PIPELINES=0` | новые конвейеры компилируются сразу, игра ждёт (по умолчанию — в фоне для проходов, рисуемых каждый кадр) |
|
||||
| `BB_ASYNC_PIPELINE_THREADS=N` | число потоков фоновой компиляции (по умолчанию четверть потоков процессора, 1–4) |
|
||||
| `BB_VRAM_LIMIT_MB=N` | тест: бюджет VRAM драйвера считается не больше N МиБ (имитация карты на 6 ГБ: 5600) |
|
||||
| `BB_RESIDENCY_MIX=1` | строка `Layer in place` теперь также говорит, почему блоки не наблюдаются |
|
||||
|
||||
## Что дальше
|
||||
|
||||
1. Лог с NVIDIA.
|
||||
2. Найти причину медленных запусков (или доказать, что это разная сцена).
|
||||
3. Летучие блоки обновляются на 0,9–1,6 ГБ/с — дешевле было бы обновлять их только после
|
||||
записи процессора (ловушки уже есть).
|
||||
4. Если модуль на NVIDIA подтвердится, перевести на него и AMD и убрать sparse-арену из новой
|
||||
модели.
|
||||
+25
-28
@@ -1,13 +1,28 @@
|
||||
# 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 co-op pool,
|
||||
Online play is a beta. Messages, bloodstains and ghosts come from a community game server:
|
||||
Seamless Hunt's (the default) or [The Hunter's Dream](https://thehuntersdream.com). Bells and summons go through a co-op pool,
|
||||
and then the players' games connect to each other. Linux and Windows players can play together:
|
||||
everyone needs the game at version 1.09 and the same pool.
|
||||
|
||||
> **shadPS4's public shadNet server no longer accepts bbport builds**: it signs the game out
|
||||
> right after the login ("This shadNet server doesn't accept your shadPS4 build" in the log), and
|
||||
> the launcher says so. The pools for bbport are The Hunter's Dream's, Seamless Hunt and a server of your own.
|
||||
> the launcher says so. The pools for bbport are Seamless Hunt (the default), The Hunter's Dream's and a server of your own.
|
||||
|
||||
## Seamless Hunt
|
||||
|
||||
[Seamless Hunt](https://install.seamlesshunt.com) is a community server for Seamless Co-op (BBSC),
|
||||
where Windows players join with BBSC's package for the Windows port.
|
||||
|
||||
1. In the launcher's **Multiplayer → Online** tab, turn on **Play online** and choose **Seamless Hunt**: its
|
||||
shadNet server, WebAPI and game server come with it.
|
||||
2. Type a name and a password under **Account** and press **Create** (once). The server wants an
|
||||
email for each account; the launcher makes one from the name, as Seamless Hunt's own tool does.
|
||||
3. Turn on BBSC in the **Seamless co-op (BBSC)** group and press **Test** in the **Status** row.
|
||||
|
||||
Its bloodstains and death visions crash the game with "Transaction failed", on Windows too: they
|
||||
are off by default, as in Seamless Hunt's package (**Bloodstains on Seamless Hunt** brings them
|
||||
back). Messages, ghosts and summon signs are not affected.
|
||||
|
||||
## The Hunter's Dream (with bbhost's players)
|
||||
|
||||
@@ -28,24 +43,9 @@ show you.
|
||||
do not support bbport: report problems here ([issues](https://github.com/ayozetr/bloodborne_linux/issues)),
|
||||
not to The Hunter's Dream's support.
|
||||
|
||||
## Seamless Hunt
|
||||
|
||||
[Seamless Hunt](https://install.seamlesshunt.com) is a community server for Seamless Co-op (BBSC),
|
||||
where Windows players join with BBSC's package for the Windows port.
|
||||
|
||||
1. In the launcher's **Multiplayer → Online** tab, turn on **Play online** and choose **Seamless Hunt**: its
|
||||
shadNet server, WebAPI and game server come with it.
|
||||
2. Type a name and a password under **Account** and press **Create** (once). The server wants an
|
||||
email for each account; the launcher makes one from the name, as Seamless Hunt's own tool does.
|
||||
3. Turn on **Seamless co-op (BBSC)** and press **Test** in the **Status** row.
|
||||
|
||||
Its bloodstains and death visions crash the game with "Transaction failed", on Windows too: they
|
||||
are off by default, as in Seamless Hunt's package (**Bloodstains on Seamless Hunt** brings them
|
||||
back). Messages, ghosts and summon signs are not affected.
|
||||
|
||||
## Your own server
|
||||
|
||||
Its owner creates the account ([SERVER.md](SERVER.md)), or you make it with **Create** under
|
||||
Its owner creates the account ([hosting your own server](SERVER.md)), or you make it with **Create** under
|
||||
**Account** when the server lets anyone register.
|
||||
|
||||
1. In the launcher's **Multiplayer → Online** tab, turn on **Play online**, choose **Your own server** with
|
||||
@@ -67,14 +67,14 @@ the guest follows the host through lamps, sees the host's world (doors, bosses,
|
||||
items and echoes it gets, and enemies get stronger for every guest. It runs inside the game
|
||||
(`bbsc/`, loaded as `libbbsc.so`). It is new, and its own author marks much of it as untested.
|
||||
|
||||
1. In the launcher's **Multiplayer → Online** tab, turn on **Seamless co-op (BBSC)**. **BBSC settings** has its
|
||||
1. In the launcher's **Multiplayer → Online** tab, turn on BBSC in the **Seamless co-op (BBSC)** group. **BBSC settings** has its
|
||||
options (the same as in BBSC's Windows launcher); the defaults are BBSC's.
|
||||
2. Every player of the session needs BBSC, the same version: on Linux this port, on Windows
|
||||
BBSC's package for the Windows port (its *BBport* release). A player without it plays the
|
||||
normal game.
|
||||
3. The server decides how much of it works:
|
||||
- **BBSC's game server** (your group's, [SERVER.md](SERVER.md#seamless-co-op-bbsc), or BBSC's
|
||||
public one): everything, summons across areas and 5 hunters included. Set it as the
|
||||
- **BBSC's game server** (Seamless Hunt's, which comes with it, or your group's,
|
||||
[hosted with it](SERVER.md#seamless-co-op-bbsc)): everything, summons across areas and 5 hunters included. Set it as the
|
||||
**Game server** of the Online tab.
|
||||
- **The Hunter's Dream**: the session stays together (deaths, lamps, bosses), but summons are
|
||||
the normal game's: BBSC turns its own summon changes and the 5 hunters off on public servers.
|
||||
@@ -114,7 +114,7 @@ the game's log has its warnings (`BBSC:` lines). Not in the Linux build: BBSC's
|
||||
## 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
|
||||
Tailscale ([hosting your own server](SERVER.md)) it goes through Tailscale: turn **UPnP** off and skip this
|
||||
section. Otherwise each player has to be reachable from the Internet:
|
||||
|
||||
1. **The router.** With **UPnP** on (the default), the game opens its port on the router itself:
|
||||
@@ -130,10 +130,7 @@ section. Otherwise each player has to be reachable from the Internet:
|
||||
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
|
||||
```
|
||||
Windows players open the same UDP port in their own firewall.
|
||||
|
||||
A router whose public address is shared by many customers (CG-NAT) cannot forward ports: the
|
||||
log's `UPNP: IGD found - external IP=...` should be your real public address. Tailscale works
|
||||
@@ -149,5 +146,5 @@ Run with `BB_GPU_LOG=info` in the extra variables for the online details. In a s
|
||||
| `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. |
|
||||
| `Online: the shadNet server ... does not accept this build` | The server refuses bbport (shadPS4's public one): use The Hunter's Dream or your own server. |
|
||||
| `Online: the shadNet server ... does not accept this build` | The server refuses bbport (shadPS4's public one): use Seamless Hunt, The Hunter's Dream or your own server. |
|
||||
| `event=0(DEAD)` | The connection gave up. |
|
||||
+26
-27
@@ -2,39 +2,38 @@
|
||||
|
||||
## For players
|
||||
|
||||
The same documents, with a few pages written for players, make up the
|
||||
[players' guide](https://ayozetr.github.io/bloodborne_linux/) (built from this folder: `mkdocs.yml`; published with `tools/publish_guide.sh`).
|
||||
|
||||
- [getting-started.md](getting-started.md): what you need, the first launch, saves and updates.
|
||||
- [steam-deck.md](steam-deck.md): Add to Steam, the Steam Deck's Game Mode, Steam Input and the overlay.
|
||||
- [logs.md](logs.md): getting a log, also from Game Mode, and sending it.
|
||||
- [faq.md](faq.md): the questions players ask most.
|
||||
- [COMPATIBILITY.md](COMPATIBILITY.md): which releases of Bloodborne run, and which have been
|
||||
checked.
|
||||
- [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.
|
||||
- [ONLINE.md](ONLINE.md): online play and co-op: the account, the bells, the ports.
|
||||
- [SERVER.md](SERVER.md): hosting your own co-op server for a group of friends.
|
||||
- [MODS.en.md](MODS.en.md): file mods and third-party patches.
|
||||
|
||||
## For this fork
|
||||
## For developers
|
||||
|
||||
- [ENVIRONMENT.md](ENVIRONMENT.md): every environment variable the port reads, with its default
|
||||
and where in the code it is read.
|
||||
- [DEBUGGING.md](DEBUGGING.md): catching a crash with a debugger (`BB_FAULT_WAIT`).
|
||||
- [RELEASE_TEMPLATE.md](RELEASE_TEMPLATE.md): the format of every release's notes.
|
||||
- [CREDITS.md](CREDITS.md): whose work is in the port, and its licenses.
|
||||
|
||||
## bbport's development notes
|
||||
## Design notes from upstream bbport
|
||||
|
||||
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).
|
||||
The port is built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28. These
|
||||
notes explain parts of its engine that this port still runs, and the code points to them. Some
|
||||
describe plans or measurements at the date in their title.
|
||||
|
||||
- **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/).
|
||||
- **GPU commands in parallel:** [parallel_gpu.md](parallel_gpu.md).
|
||||
- **Upscaling, TAA and frame generation:** [upscaler.md](upscaler.md).
|
||||
- **The translator that replaces emulation:** [TRANSLATOR_ARCHITECTURE.md](TRANSLATOR_ARCHITECTURE.md).
|
||||
- **The GPU layer's memory module:** [MEMORY_MODULE_PLAN.md](MEMORY_MODULE_PLAN.md).
|
||||
|
||||
Upstream's release notes, roadmap, session logs and its other notes (most in Russian) stay in
|
||||
[its docs folder](https://github.com/deadinside28/bloodborne_pc/tree/master/docs).
|
||||
@@ -43,4 +43,5 @@ SHA-256: `<sha256sum of the AppImage>`
|
||||
|
||||
**Full changes:** https://github.com/ayozetr/bloodborne_linux/compare/<previous tag>...<tag>
|
||||
**Credits and licenses:** https://github.com/ayozetr/bloodborne_linux#credits
|
||||
Includes Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine).
|
||||
```
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
# Нативный порт Bloodborne: что сделано и что дальше
|
||||
|
||||
Основной текст — состояние на 2026-09-26; ниже первым разделом — что изменилось к 30.09.
|
||||
Самый подробный срез: [CURRENT_STATUS_2026-09-29.md](history/CURRENT_STATUS_2026-09-29.md).
|
||||
Отчёт о работе 30.09–01.10 (конвейер GPU, апскейлер, падение, лаунчер, AppImage):
|
||||
[SESSION_2026-09-30.md](history/SESSION_2026-09-30.md).
|
||||
Подробности по отдельным темам:
|
||||
[parallel_gpu.md](parallel_gpu.md) (многопоточность GPU, подтормаживания при подгрузке),
|
||||
[upscaler.md](upscaler.md) (анализ кадра игры, константы сцены).
|
||||
|
||||
## Обновление 30.09
|
||||
|
||||
- Сделано после 26.09: FSR 4 INT8 (v07), векторы движения объектов, смена пресета без
|
||||
перезапуска (уменьшенные цели сцены; глубина D32S8 копируется шейдером), LTO и PGO,
|
||||
масштабирование подготовки draw под число потоков (Steam Deck тоже).
|
||||
- Проблемы из раздела «Известные проблемы» ниже: пункт 3 (перезапуск при смене пресета)
|
||||
решён; пункт 1 (векторы объектов) реализован, у FSR 4 остаётся гостинг на персонаже при
|
||||
Ultra Performance; пункт 2 (мыльный интерфейс) — UI рисуется в 1920×1080.
|
||||
- Ограничение: уменьшенный рендер сцены (`SceneTargets::Eligible`) требует цель 1920×1080,
|
||||
то есть работает только при выводе 1080p.
|
||||
- Распараллеливание потока GPU-команд (шаг 2 [parallel_gpu.md](parallel_gpu.md)): текстуры на
|
||||
отдельном потоке параллельно с буферами работают корректно, но прироста не дают —
|
||||
данные каждого draw переезжают между ядрами. Узкое место — поток GPU-команд: 91% на CPU,
|
||||
~9% ожидания мелких копий гостевой памяти перед fence. Дальше — двухстадийный конвейер
|
||||
(второй поток владеет текстурным кешем и записью draw, копия регистров из дельт сканера)
|
||||
или мемоизация привязок текстур между кадрами; см. «What would scale» в parallel_gpu.md.
|
||||
- **Двухстадийный конвейер draw** (`vk_draw_pipe.h`): поток GPU-команд декодирует PM4 и
|
||||
выбирает пайплайн, второй поток (`bb:DrawRec`) со своей копией регистров привязывает ресурсы
|
||||
и записывает draw. +19% FPS на 16 потоках, +18% на 8 (как Steam Deck). Включён по умолчанию
|
||||
при 8+ потоках (`BB_DRAW_PIPE=0/1`). Узкое место теперь — вторая стадия; подробности и план —
|
||||
в parallel_gpu.md.
|
||||
- Пресеты и нагрузка GPU: сцена на RX 7800 XT стоит мало, поэтому пресет меняет время GPU
|
||||
лишь на ~0.5 мс, а сам FSR 4 при выводе 1080p — ~1.4 мс (FSR 4 Ultra Performance дороже
|
||||
нативного рендера без апскейлера). Исправлено: проходы освещения оставались в 1920×1080 при
|
||||
любом пресете (проверка смотрела на чужие слоты целей); уменьшенные цели теперь сэмплируются
|
||||
напрямую, без ресэмплинга в натив (кэш шейдеров пересобирается один раз). Следующее для
|
||||
Steam Deck — гостевая копия памяти целей рендеринга `3d5ebf4e` (см. parallel_gpu.md).
|
||||
- **Исправлено падение с конвейером** (порча гостевой кучи, guest offset `0x263b8e7`, через
|
||||
13–17 минут): прерывание «GPU простаивает» уходило раньше меток EOP, отложенных в поток записи
|
||||
Vulkan, и игра освобождала объекты с этими метками. Теперь перед ним конвейер и отложенные
|
||||
метки дожидаются; 2 прогона по 30 минут без падений (до исправления — 7 из 7 падений).
|
||||
- **Разрешение вывода 1440p/2160p** (меню «Разрешение вывода», bbport.ini `output_res`, после
|
||||
перезапуска): сцена рисуется в разрешении пресета от выхода, апскейлер (FSR 3 или FSR 4)
|
||||
дорисовывает кадр, интерфейс растеризуется в выходном разрешении. Эффекты игры (аберрация,
|
||||
DoF, размытие, SSAO, сглаживание игры, динамические тени, SSR, заставки, LOD) — там же.
|
||||
- **Лаунчер** (`launcher/bb-launcher.sh`, GTK4/libadwaita) и **AppImage** для Steam Deck
|
||||
(`bash packaging/appimage.sh` → `dist/`; данные в `~/.local/share/bbport`, `--play` —
|
||||
запуск без окна лаунчера).
|
||||
- Инструменты: `BB_PAD_FILE` (скриптовый ввод геймпада), `ab.sh` (A/B битов
|
||||
`BB_TOGGLE_FILE` в запущенной игре), в `Frame stats` — простой и блокировки потока GPU.
|
||||
|
||||
## Как устроен порт
|
||||
|
||||
- Оригинальный eboot (CUSA03173 v01.09) исполняется напрямую на x86-64. Системные библиотеки
|
||||
PS4 заменены своим HLE-рантаймом на C (`runtime_*.c`, `probe.c`).
|
||||
- Графика: видеоядро и рекомпилятор шейдеров shadPS4 (GPL), собранные в `libbbgpu.so`
|
||||
(`gpu/`), с собственными доработками. Эмулятор shadPS4 целиком не используется.
|
||||
- Патчи игры (FPS, разрешение и др.) — XML сообщества (`patches/Bloodborne.xml`),
|
||||
компилируются `patches.py` и накладываются загрузчиком при старте.
|
||||
|
||||
## Сделано
|
||||
|
||||
### Производительность и плавность
|
||||
|
||||
| Что | Результат |
|
||||
|---|---|
|
||||
| Запись команд Vulkan в отдельном потоке, кеши текстур/буферов, rwlock вместо мьютекса | 26 → 64 FPS в одной сцене |
|
||||
| Подготовка draw-вызовов на 4 потоках (`bb:DrawPrep`), проверка на потоке GPU | +11.5% |
|
||||
| Окно снятия защиты 256 КиБ при записи игры в память (меньше page fault) | +22% |
|
||||
| Кеш описаний текстур, быстрый путь для тех же render target | до ~94 FPS на улице |
|
||||
| Копирования гостевой памяти на пуле потоков, пул staging-буферов | фризы при подгрузке 60–170 мс → 40–50 мс |
|
||||
| Ожидание копий перед видимыми игре fence | убрано мерцание интерфейса |
|
||||
| Режим «Readbacks Relaxed» | нет «взрывов вершин» (FaceGen) |
|
||||
| Vblank 480 Гц + лимит кадров до min(монитор, 120), вывод сразу по флипу | ровная подача кадров; бег не замедляется выше 120 FPS |
|
||||
|
||||
Всё переключается во время игры через `BB_TOGGLE_FILE` (битовая маска) для A/B-сравнений.
|
||||
Статистика: `BB_FRAME_STATS=1`.
|
||||
|
||||
### Звук, ввод
|
||||
|
||||
- Вывод звука с правильным темпом (раньше щелчки каждые 10.7 мс).
|
||||
- Геймпад через SDL3, запасная раскладка клавиатуры.
|
||||
|
||||
### Анализ кадра
|
||||
|
||||
- `BB_CAPTURE_TRIGGER=<файл>` записывает один кадр: проходы, цели, шейдеры, текстуры,
|
||||
константы, режимы смешивания.
|
||||
- Разобрано устройство кадра Bloodborne (G-buffer, свет, цвет сцены RGBA16F, туман,
|
||||
пост-обработка, тонмаппинг, сглаживание игры, интерфейс) и 864-байтные константы сцены
|
||||
(матрицы вида и проекции).
|
||||
|
||||
### Временной апскейлер (FSR 3.1)
|
||||
|
||||
- **Векторы движения камеры**: из глубины сцены и матриц текущего и прошлого кадра
|
||||
(`vk_camera_motion.cpp`, `camera_motion.comp`). Проверены перепроецированием прошлого
|
||||
кадра: статичная геометрия совпадает.
|
||||
- **Jitter**: субпиксельный сдвиг геометрии сцены через viewport, последовательность
|
||||
Halton(2,3); число фаз 8·(вывод/рендер)². Интерфейс и пост-обработка не сдвигаются.
|
||||
- **FSR 3.1** (FireBurn/FSR-Vulkan, нативный Vulkan, подмодуль `gpu/third_party/fsr-vulkan`):
|
||||
- *Native AA*: на HDR-цвете сцены перед пост-обработкой, 1:1.
|
||||
- *Пресеты Quality/Balanced/Performance/Ultra Performance* (x1.5/1.7/2/3): игра рендерит в
|
||||
пониженном разрешении (патч разрешения, который `patches.py` выводит из патча 720p
|
||||
сообщества), FSR увеличивает готовый кадр до 1920x1080 перед первым проходом интерфейса.
|
||||
Интерфейс и проход вывода перенаправлены в изображения полного размера, презентер
|
||||
показывает их вместо буфера игры.
|
||||
- **Маска реактивности** (снимок сцены до прозрачных отрисовок, сравнение перед наложением
|
||||
тумана): работает, но на дымке Bloodborne даёт дрожание — по умолчанию выключена.
|
||||
- **Внутриигровое меню** (Dear ImGui, Insert или L3+R3): апскейлер, пресет, резкость,
|
||||
jitter, маска, счётчик FPS. Настройки в `bbport.ini`. Смена пресета — кнопка
|
||||
«Применить и перезапустить игру».
|
||||
|
||||
## Известные проблемы
|
||||
|
||||
Состояние последнего коммита. В рабочем дереве подготовлен перенос пресетов на уменьшенные
|
||||
цели рендера при сохранении гостевых буферов, постобработки и UI в 1920×1080. Переходы
|
||||
между пресетами и копирование цвета/глубины/stencil прошли Vulkan-тест на Lavapipe.
|
||||
RX 7800 XT не поддерживает blit D32S8, поэтому новый путь оставлял FSR вход 1920×1080
|
||||
и снижал FPS. Для таких GPU запуск теперь автоматически выбирает проверенный путь с патчем
|
||||
разрешения игры и UI 1920×1080; смена пресета снова требует перезапуска. На GPU с нужной
|
||||
поддержкой остаётся переключение без перезапуска. Пункты 2–3 ниже относятся к последнему
|
||||
коммиту, до этих изменений рабочего дерева.
|
||||
|
||||
Общий проход векторов движения объектов был слишком дорогим: Performance давал около
|
||||
49 FPS с ним и 77–78 FPS без него. Теперь дополнительная цель и шейдеры включаются только
|
||||
для G-buffer отрисовок с буфером позы/костей; остальные используют векторы камеры.
|
||||
В пробном запуске выборочный путь держал около 81–87 FPS, без пропусков из-за ёмкости
|
||||
истории. Векторы объектов включены по умолчанию; `BB_OBJECT_MOTION=0` отключает их, а
|
||||
`BB_OBJECT_MOTION_ALL=1` возвращает старый общий путь для сравнения. Выбор по размеру
|
||||
буфера пока эвристический: качество оружия и одежды в движении требует проверки в игре.
|
||||
|
||||
1. **Движущиеся объекты «рассыпаются»** (оружие на спине, персонаж, ткань, враги): есть
|
||||
только векторы движения камеры. Камера от третьего лица следует за персонажем, поэтому
|
||||
векторы камеры для него неверны, и апскейлер берёт историю не из того места. Это касается
|
||||
любого временного апскейлера (FSR 3/4, DLSS, XeSS).
|
||||
2. **Интерфейс в пресетах мыльный**: с патчем разрешения игра считает себя 540p-игрой и,
|
||||
судя по всему, готовит шрифты и элементы интерфейса под это разрешение. Меню
|
||||
(титульное, пауза) вообще не проходят через апскейлер.
|
||||
3. **Смена пресета требует перезапуска**: разрешение задаётся патчем кода при старте, а
|
||||
буферы игра выделяет один раз.
|
||||
|
||||
## План
|
||||
|
||||
### 1. Векторы движения объектов
|
||||
|
||||
Нужны всем апскейлерам и генерации кадров. Игра их не считает, поэтому считаем сами.
|
||||
|
||||
Как:
|
||||
- Для каждой отрисовки в G-buffer (проходы с 5+ целями) рекомпилятор делает второй вариант
|
||||
вершинного шейдера, который выдаёт позицию дважды: с текущими константами и с константами
|
||||
прошлого кадра. Разница проекций — вектор движения, пишется во вспомогательную цель
|
||||
RG16F как дополнительный выход пиксельного шейдера.
|
||||
- Константы прошлого кадра: для каждого draw запоминаем содержимое константных буферов
|
||||
(матрица объекта, кости скелета) по ключу «шейдер + номер draw в проходе» или по адресу
|
||||
данных. Нужно выяснить, как игра хранит матрицы скелета: в буферах, которые перезаписываются
|
||||
каждый кадр (тогда копируем прошлую версию), или в кольцевом буфере (тогда достаточно
|
||||
адреса прошлого кадра).
|
||||
- Где объект не найден (первый кадр, новый объект) — векторы камеры, как сейчас.
|
||||
- Проверка: отладочный вид векторов (`BB_DEBUG_MOTION`), перепроецирование прошлого кадра
|
||||
по полным векторам, визуально — пила на спине при беге.
|
||||
|
||||
### 2. Масштабирование на уровне рендерера (вместо патча разрешения)
|
||||
|
||||
Игра работает в 1920x1080 (или в разрешении монитора), а в пониженном разрешении рисуются
|
||||
только проходы 3D-сцены. Интерфейс и меню остаются чёткими, пресет меняется на лету.
|
||||
|
||||
Как:
|
||||
- Кеш текстур выделяет для целей сцены (G-buffer, глубина, свет, цвет сцены, эффекты) копию
|
||||
уменьшенного размера; гостевая память и описания игры не меняются.
|
||||
- Viewport и scissor проходов в эти цели масштабируются (как уже сделано для интерфейса,
|
||||
включая проходы с отключённым отсечением).
|
||||
- Шейдеры, которые считают по пиксельным координатам (`gl_FragCoord`, `texelFetch`,
|
||||
compute-шейдеры по пикселям), получают коэффициент масштаба через push-константы
|
||||
рекомпилятора. Выборки по нормированным координатам работают без изменений.
|
||||
- FSR ставится туда же, где Native AA: цвет сцены перед пост-обработкой (HDR, лучшее
|
||||
качество); пост-обработка и интерфейс идут в полном разрешении.
|
||||
- Риски: проходы, читающие цели сцены по пиксельным координатам в пост-обработке, и
|
||||
чтения из гостевой памяти этих целей (их нужно запретить или масштабировать обратно).
|
||||
|
||||
### 3. Другие апскейлеры
|
||||
|
||||
Все используют одни и те же входы (цвет, глубина, векторы движения, jitter, маски).
|
||||
|
||||
- **FSR 4 (INT8)**: в FSR-Vulkan есть экспериментальный провайдер FSR 4 v07 INT8 — добавить
|
||||
как вариант в меню и сравнить с FSR 3.1 после шага 1.
|
||||
- **DLSS**: нативный Linux Vulkan SDK (NGX). На RX 7800 XT не работает, но нужен для
|
||||
пользователей NVIDIA.
|
||||
- **XeSS / XeFG и OptiScaler**: Intel поставляет их только как Windows DLL. Варианты:
|
||||
загрузчик Windows DLL (PE) внутри процесса либо запуск всей сборки под Proton с
|
||||
OptiScaler, которому отдаём входы через интерфейс DLSS.
|
||||
Проверено 29.09: libxess.dll (одинаковая у DOOM: The Dark Ages, PRAGMATA и др.) имеет
|
||||
Vulkan-путь (`xessVKCreateContext`, `xessVKExecute`), но импортирует ~300 функций:
|
||||
MSVCP140 (iostream), VCRUNTIME140 (C++-исключения), UCRT, kernel32, setupapi, dxgi.
|
||||
Решено отложить; самый реалистичный путь — хелпер под Wine с общей памятью Vulkan.
|
||||
|
||||
### 4. Генерация кадров
|
||||
|
||||
- FSR 3.1 Frame Generation (есть в FSR-Vulkan): промежуточный кадр между двумя готовыми,
|
||||
по тем же векторам движения; интерфейс нужно рисовать поверх отдельно (есть перенаправление
|
||||
интерфейса — его можно рендерить в отдельный слой).
|
||||
- XeFG — после решения с Windows DLL (см. выше).
|
||||
- Требование: подача кадров через презентер (очередь и VRR уже работают).
|
||||
|
||||
### 5. Прочее
|
||||
|
||||
- Steam Deck: портативная сборка (patchelf, без Nix), проверка пресетов на 1280x800.
|
||||
- Дальнейшая разгрузка потока GPU (шаг 2 из [parallel_gpu.md](parallel_gpu.md)).
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
# Hosting your own co-op server
|
||||
|
||||
Bells and summons go through a shadNet server. shadPS4's public one (`srv.shadps4.net`) no longer
|
||||
accepts bbport builds, and the public pool for bbport is now The Hunter's Dream's, through
|
||||
bbbridge (the launcher's **Multiplayer → Online** tab; [README](../README.md#online-play)). A group of friends
|
||||
accepts bbport builds, and the public pools for bbport are Seamless Hunt (the default) and The
|
||||
Hunter's Dream's, through bbbridge (the launcher's **Multiplayer → Online** tab; [README](../README.md#online-play)). A group of friends
|
||||
can also run their own server:
|
||||
[shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) by Wozzardman, the shadNet server with
|
||||
that UDP port working and a summon service of its own.
|
||||
|
||||
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
|
||||
With your own shadNet server, messages, bloodstains and ghosts come from the **Game server** field
|
||||
of the Online tab: The Hunter's Dream by default, or BBSC's game server ([below](#seamless-co-op-bbsc)). Windows players of [Supermedo's port](https://github.com/Supermedo/bloodborne_pc) can join
|
||||
the same server.
|
||||
|
||||
Tested with two players, one on this Linux port and one on Supermedo's Windows port, over
|
||||
@@ -187,7 +187,7 @@ place of The Hunter's Dream for messages, bloodstains and the server list.
|
||||
| 3478 | UDP | The relay BBSC players fall back to when they cannot reach each other directly |
|
||||
|
||||
Each player then sets **Game server** to `http://100.x.y.z:20443` in the launcher's
|
||||
**Multiplayer → Online** tab, keeps the same shadNet server and account, and turns on **Seamless co-op (BBSC)**.
|
||||
**Multiplayer → Online** tab, keeps the same shadNet server and account, and turns on BBSC in the **Seamless co-op (BBSC)** group.
|
||||
BBSC finds its relay at the game server's address (or `BBSC_RELAY`, the relay address in BBSC's
|
||||
settings).
|
||||
|
||||
@@ -203,4 +203,4 @@ settings).
|
||||
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.
|
||||
See also [Online play and co-op](ONLINE.md) for the bells, passwords and log lines of a summon.
|
||||
@@ -1,134 +0,0 @@
|
||||
# Проверка TAA и входов временных апскейлеров — 2026-10-01
|
||||
|
||||
## Состояние проекта
|
||||
|
||||
bbport — специализированный Linux-рантайм для оригинального x86-64 кода Bloodborne
|
||||
CUSA03173 1.09 с переводом GPU-команд в Vulkan на основе shadPS4. Это экспериментальный
|
||||
порт: загрузчик, HLE системных вызовов, звук, ввод, сохранения, игровой интерфейс настроек
|
||||
и temporal upscaling уже реализованы. В этой рабочей копии доступны исходники, собранная
|
||||
библиотека, игровой дамп и сохранение. Git-метаданных проекта нет, поэтому историю
|
||||
изменений и чистоту рабочего дерева проверить нельзя.
|
||||
|
||||
Полное прохождение, переносимость на другие драйверы и Steam Deck этой проверкой не
|
||||
подтверждаются. В документации остаются ограничения: искусственные occlusion query,
|
||||
неполный predication, прозрачные эффекты без собственных векторов движения и гонки
|
||||
в модели FSR 4.1.1 для некоторых нестандартных размеров вывода.
|
||||
|
||||
## Видео и причины
|
||||
|
||||
Исходная запись `video_2026-10-01_21-21-02.mp4`: 94.89 с, 2560×1080, 60 FPS.
|
||||
В ней переключаются TAA, FSR 4.1.1, FSR 4 и FSR 3.1. На архитектуре, тонких контурах
|
||||
и движущемся оружии видна временная нестабильность. В меню включён игровой motion blur;
|
||||
его вклад нельзя приписывать TAA. Файл `video_fixed_taa.mp4` имеет ту же длительность,
|
||||
но другой размер кадра (2560×1082); сама эта запись не подтверждает исправление рендерера.
|
||||
Кадры и последовательности для просмотра лежат в `out/dev/temporal-review/`.
|
||||
|
||||
Выявлены ошибки в коде, независимо от субъективной оценки видео:
|
||||
|
||||
1. **FP16 depth в TAA history.** Шаг half около 1 составляет примерно 0.000488.
|
||||
При near ≈ 0.05 дальние поверхности становятся неразличимыми. Порог проверки истории
|
||||
был меньше этого шага, поэтому история могла как ошибочно отбрасываться, так и смешиваться
|
||||
с другой поверхностью. История теперь RGBA32F.
|
||||
2. **Несопоставимые камеры.** Старая проверка сравнивала глубину прошлого кадра с глубиной
|
||||
текущего. Теперь текущая точка переводится в систему предыдущей камеры, и сравнение
|
||||
выполняется с ожидаемой глубиной там. Допуск масштабируется по перспективной глубине.
|
||||
3. **Обнуление дальних векторов.** `depth >= 0.99999` считалось небом и получало нулевое
|
||||
движение. Для проекции с near ≈ 0.05, far 3000 этот порог затрагивает геометрию примерно
|
||||
от 1875 единиц. Теперь только clear depth 1 считается бесконечным фоном; фон получает
|
||||
вращательное движение камеры без переноса. Дальняя геометрия получает обычную репроекцию.
|
||||
4. **FP16 depth в object motion.** В RGBA16F терялась глубина, используемая для проверки,
|
||||
не перекрыт ли вектор более поздним draw. Буфер и формат графического pipeline теперь
|
||||
RGBA32F; проверка глубины больше не принимает другую дальнюю поверхность с широким
|
||||
фиксированным допуском. Финальные векторы для FSR по-прежнему RG16F.
|
||||
5. **Проверка смешанной глубины на краях.** TAA интерполировал глубину истории между
|
||||
разными поверхностями. Теперь четыре bilinear tap проверяются независимо, цвет собирается
|
||||
только из согласованных tap; при частичном покрытии вес истории уменьшается.
|
||||
6. **Слишком слабое накопление вдали.** Прежнее depth-adaptive снижение веса до 0.8/0.5
|
||||
заменено весом 0.98…0.85 в зависимости от движения при валидной истории (нижний
|
||||
вес достигается при 8 px/frame). Диагностический прогон с весом 0.94 показал
|
||||
остаточные колебания цвета между фазами jitter даже с принятой историей. RGB истории
|
||||
ограничивается диапазоном соседей текущего кадра.
|
||||
7. **Jitter глубины на склонах и тонких деталях.** Глубина теперь переносится в
|
||||
стабильную позицию пикселя по локальному одностороннему градиенту, с защитой от
|
||||
разрывов поверхности. Для motion/depth выбирается ближайшая поверхность в 3×3;
|
||||
цвет и диапазон clipping берутся вокруг исходного цветового пикселя. Это позволяет
|
||||
тонким контурам сохранять историю при чередовании foreground/background под jitter.
|
||||
8. **Нестабильная адресация истории при почти нулевом движении — основная причина
|
||||
обнаруженного дребезга TAA.** Раньше целая и дробная части bilinear-координаты
|
||||
вычислялись после сложения большого номера пикселя с очень маленьким motion.
|
||||
На GPU это давало несогласованные части и выбор соседнего texel. Теперь сначала
|
||||
разделяется сам motion: `base = p + ivec2(floor(motion))`,
|
||||
`fraction = motion - floor(motion)`. Регрессионная проверка на пикселе x=2010
|
||||
с движением около −0.00003 px воспроизводит ошибку старой формулы:
|
||||
цвет 0.250000 вместо ожидаемого 0.739985. Исправленная формула проходит проверку.
|
||||
|
||||
Исправления 3–4 относятся к общим входам TAA/FSR 3.1/FSR 4/FSR 4.1.1. Алгоритмы и
|
||||
ML-ассеты AMD не изменялись. Проверка ABI shader/pipeline и диагностический dump object
|
||||
motion обновлены вместе с форматом ресурса.
|
||||
|
||||
## Проверки и ограничения
|
||||
|
||||
Сборка `bbgpu`, `taa-shader-test`, `camera-motion-test` через `shell.nix`.
|
||||
На Vulkan GPU прошли 18 случаев production TAA shader: reset с NaN, unjitter, накопление,
|
||||
clipping, раскрытие поверхности, выход за кадр, NaN history, точная дальняя глубина,
|
||||
отказ другой дальней поверхности, перенос камеры, край с частично валидной историей,
|
||||
небо и граница небо/геометрия, наклонная глубина при обоих знаках jitter, тонкий контур,
|
||||
снижение persistence при движении и очень малое отрицательное движение на большой координате.
|
||||
Прошли 6 проверок production camera-motion shader:
|
||||
дальняя геометрия, вращение неба, отсутствие переноса неба, исключение jitter,
|
||||
валидный и устаревший object vector. Лог: `out/dev/temporal-review/tests.log`.
|
||||
Дополнительная проверка unjitter: `out/dev/temporal-review/taa-final-tests.log`.
|
||||
Воспроизведение регрессии старой формулы и 18 успешных проверок новой:
|
||||
`out/dev/temporal-review/regression-proof.log`.
|
||||
Финальная проверка игры: `out/dev/temporal-review/subpixel-validation.log`.
|
||||
Все 64 существующих Python-теста прошли (`out/dev/temporal-review/python-tests.log`).
|
||||
|
||||
Live smoke: копия сохранения загружена в Сне Охотника. В одном процессе проверены
|
||||
TAA → Off → FSR 3.1 → FSR 4 v07 → FSR 4.1.1 → TAA, вывод 2560×1440,
|
||||
по шесть финальных кадров на режим и движение камеры между переключениями.
|
||||
FSR работают с входом 1706×960; TAA — с нативным 2560×1440. Контексты AMD
|
||||
подтверждены логом, подмены на FSR 3.1 не было. Процесс завершился штатно;
|
||||
после компиляции pipeline выдерживал лимит 60 FPS. Логи и raw-кадры:
|
||||
`out/dev/temporal-review/subpixel-validation.log`, `out/dev/temporal-review/far-refined/`.
|
||||
Игровые motion blur и AA отключены в тестовой конфигурации.
|
||||
|
||||
При неподвижной камере на участке дальней башни (x=2010…2099, y=400…779)
|
||||
среднее абсолютное изменение RGB между шестью кадрами TAA снизилось с 1.892 до
|
||||
0.299 по шкале 0…255, примерно в 6.3 раза. После возврата к TAA — 0.279.
|
||||
В финальном прогоне FSR 3.1: 0.328, FSR 4: 0.278, FSR 4.1.1: 0.364.
|
||||
Это результат выбранной сцены, а не универсальная оценка качества: разрешение входа
|
||||
FSR отличается от TAA, а до/после здесь сравнивается именно последняя правка
|
||||
адресации истории. Данные: `out/dev/temporal-review/far-refined/stability.json`;
|
||||
предыдущий прогон: `out/dev/temporal-review/final-validation.log`.
|
||||
|
||||
Для диагностики добавлен `BB_TAA_DIAGNOSTICS=1`: альфа выхода TAA содержит вес принятой
|
||||
истории вместо 1. Его можно прочитать в `BB_DUMP_TRIGGER` raw RGBA16F. RGB не меняется,
|
||||
альфа истории по-прежнему содержит depth. Обычный режим сохраняет непрозрачный выход.
|
||||
Дополнительные режимы 2 и 3 выводят текущий цвет или ограниченную историю;
|
||||
диагностический образ истории позволяет сравнить прямое чтение с репроекцией.
|
||||
Эти сравнения выявили ошибку адресации даже при принятой истории и почти нулевом motion.
|
||||
|
||||
Исходники и `out/gpu/libbbgpu.so` обновлены. Пересобран
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage` (832 MiB); пакетный `--vulkan-info`
|
||||
успешно определил RX 7800 XT через RADV. Логи: `appimage-build.log` и
|
||||
`appimage-vulkan.log` в `out/dev/temporal-review/`. Полный игровой прогон выше
|
||||
выполнен с локальной библиотекой; отдельный полный игровой прогон AppImage не выполнялся.
|
||||
|
||||
Эти тесты подтверждают конкретные исправления, но не доказывают исчезновение любого
|
||||
мерцания в игре. Движение объектов по истории вершин, прозрачности, анимированные
|
||||
материалы, выбор LOD и субпиксельная геометрия при сильном уменьшении разрешения
|
||||
по-прежнему требуют проверки в отдельных сценах.
|
||||
|
||||
Цена полной точности: две истории TAA занимают 253 MiB вместо 127 MiB в 4K;
|
||||
object motion — 127 MiB вместо 63 MiB при 4K render. Это также повышает расход
|
||||
пропускной способности памяти. Возможная следующая оптимизация — отдельный R32F depth
|
||||
при сохранении цвета и motion в FP16, с теми же тестами корректности.
|
||||
|
||||
Для регрессий:
|
||||
|
||||
```sh
|
||||
nix-shell shell.nix --run 'ninja -C out/gpu taa-shader-test camera-motion-test && out/gpu/taa-shader-test && out/gpu/camera-motion-test'
|
||||
```
|
||||
|
||||
Подход с движением для всех входных пикселей согласуется с рекомендациями
|
||||
[AMD FSR 3.1 integration](https://gpuopen.com/presentations/2024/FidelityFX_Super_Resolution_3-1_Release-Overview_and_Integration.pdf).
|
||||
@@ -1,42 +0,0 @@
|
||||
# Оригинальное GPU-копирование и переносимые метки
|
||||
|
||||
Срез 09.10.2026. Область проверки: `BB_PC_MODEL=1 BB_LAYER_MEMORY=1`; новая модель, без оценки старой модели памяти.
|
||||
|
||||
## Оригинальный copy shader
|
||||
|
||||
В режиме памяти игры на месте шейдер `fefebf9f` теперь по умолчанию транслируется в SPIR-V и исполняется на GPU. Его список копий читает сам шейдер при исполнении; CPU не заменяет его списком Vulkan-копий. `BB_COPY_SHADER_NATIVE=0` сохраняет HLE для сравнения. Эксперимент 2 по-прежнему инвертирует выбор.
|
||||
|
||||
Для больших привязок таблица BDA содержит два представления: в первой половине — актуальные адреса (зеркало или память игры), во второй — адреса памяти игры. Существующая адресация первой половины сохранена для DMA/ReadConst. Описание привязки содержит флаг записи насквозь и смещение второй таблицы.
|
||||
|
||||
Оригинальный copy shader записывает результат в актуальный GPU-адрес и в память игры. Поэтому CPU-потребители видят результат после синхронизации, а GPU-потребители не читают устаревшее зеркало. Когда обе записи адресуют память игры, они дают тот же результат. Остальные вычислительные шейдеры сохраняют свой учёт GPU-владения зеркалами. Записи насквозь не помечают всё зеркало огромной привязки как требующее обратного копирования; ранее накопленные GPU-записи при этом сохраняются в учёте.
|
||||
|
||||
Проверка диапазона не допускает переполнения `offset + size`; отвергнутый доступ не индексирует произвольную страницу таблицы. Версия shader binary cache поднята до 9. Профиль кэша теперь содержит версии binary/meta/pipeline: при их смене весь кэш перестраивается до загрузки модулей. Старые метаданные и SPIR-V с совпадающими именами индексов вариаций не смешиваются с новыми.
|
||||
|
||||
`run.sh` задаёт `BB_GPU_USER_DIR` до запуска процесса, из `BB_USER_DIR` или `$BB_DATA_DIR/user`. GPU-каталоги создаются при загрузке библиотеки, поэтому установка этого значения внутри `bbgpu_init` была слишком поздней. Это обеспечивает правильный выбор кэша для AppImage и отдельных профилей сохранений; явный `BB_GPU_USER_DIR` сохраняется.
|
||||
|
||||
## EOP/EOS и RELEASE_MEM
|
||||
|
||||
Если `VK_AMD_buffer_marker` доступно, сохраняется быстрый путь AMD. Без расширения метку пишет GPU через стандартный `vkCmdUpdateBuffer`. `BB_GPU_LABELS_PORTABLE=1` принудительно выбирает этот путь и позволяет проверить его на AMD. `BB_GPU_LABELS=0` оставляет прежний callback CPU после завершения GPU.
|
||||
|
||||
Переносимый путь завершает текущий render pass, публикует предшествующие GPU-записи для CPU, записывает старшую половину 64-битной метки, ставит зависимость и записывает младшую половину. Следующая зависимость обеспечивает видимость метки для GPU и CPU. Уведомление планировщику и обработка WAIT_REG_MEM сохраняются. Неподходящие адреса и метки с прерыванием сохраняют соответствующие запасные пути.
|
||||
|
||||
Стандартная команда разрешена вне render pass: [vkCmdUpdateBuffer](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdUpdateBuffer.html). Маски стадий и доступа соответствуют [правилам Vulkan synchronization](https://docs.vulkan.org/spec/latest/chapters/synchronization.html). Завершение проходов и барьеры имеют стоимость; принудительный переносимый путь на AMD не выбран по умолчанию.
|
||||
|
||||
При проверке также исправлены две зависимости соседних путей: update/inline data учитываются на стадии CLEAR, а диапазон index buffer учитывается после draw, чтобы последующая загрузка в зеркало ожидала чтения индексов.
|
||||
|
||||
## Проверки
|
||||
|
||||
`translation-memory-test` исполняет SPIR-V производственного backend и производственную функцию переносимых меток на Vulkan GPU. Проверяются чтение актуального источника, запись в зеркало и память игры, граница страниц, unmapped/out-of-range store, отключённая запись насквозь, 32/64-битные метки, порядок половин, видимость предшествующего GPU-результата при опросе CPU, последующий GPU-потребитель и нулевое значение. Диагностические AMD breadcrumbs отключены, чтобы тест не записывал AMD-маркеры.
|
||||
|
||||
```sh
|
||||
nix-shell shell.nix --run 'ninja -C out/gpu translation-memory-test'
|
||||
# VK_LAYER_PATH при необходимости указывает каталог установленного Khronos validation layer.
|
||||
BB_VK_VALIDATION_SYNC=1 out/gpu/translation-memory-test out/translation-shaders
|
||||
spirv-val --target-env vulkan1.3 out/translation-shaders/paged-copy.spv
|
||||
```
|
||||
|
||||
PM4 self-test измеряет положительный occlusion-интервал через несколько отправок. Большая отдельная отправка могла содержать только compute/state и давать ложный FAIL. Требования positive samples, valid bits, нулевой интервал и predication сохранены.
|
||||
|
||||
Аппаратная проверка выполнена на RX 7800 XT / RADV. Переносимый путь проверен принудительно на AMD; выполнение на настоящей NVIDIA, импорт её host memory и длительное прохождение игры требуют отдельного аппаратного прогона.
|
||||
|
||||
Локальные журналы, копии исходников перед изменениями, замеры и снимки: `out/dev/translation-fixes-20261009/`. Игровые проверки используют копии сохранений и настроек. Исходные сохранения и настройки не заменяются. Журналы и игровые данные в релиз не включены.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Our own translator instead of shadPS4: structure and order of work (2026-10-10)
|
||||
|
||||
Goal: move away from emulation and from shadPS4 code. We want our own PS4 command translator and
|
||||
our own graphics renderer that scale to other games (everything specific to one game lives in its
|
||||
profile, `gpu/games/`) and behave the same on AMD and NVIDIA. After that comes shadnet (online play).
|
||||
|
||||
## Current state
|
||||
|
||||
Borrowed from shadPS4 (`gpu/VENDOR.txt`, commit b4e7ae7 from 2026-09-24): about 116k lines.
|
||||
|
||||
| Part | Lines | What it does | Our work in it |
|
||||
|---|---|---|---|
|
||||
| `video_core/amdgpu` | 8.3k | PM4 parsing (command processor `liverpool.cpp`), registers, tiling | GPU-side labels, occlusion, predication, PM4 self-test |
|
||||
| `video_core/renderer_vulkan` | 28.6k | Vulkan renderer: pipelines, rasterizer, scheduler, presenter | draw pipeline (stages A/B), recording threads, upscalers, background compilation |
|
||||
| `video_core/buffer_cache` | 7.4k | game memory for the GPU | almost all rewritten (memory module, VRAM copies, traps) |
|
||||
| `video_core/texture_cache` | 4.8k | textures by address, detiling | time-based collector, uploads |
|
||||
| `shader_recompiler` | 38.6k | GCN → IR → SPIR-V | paged buffers (BDA), fixes |
|
||||
| `core`, `common` | 21.8k | VideoOut, GnmDriver, event queues, utilities | vblank, labels |
|
||||
|
||||
Our own code, outside shadPS4: `gpu/layer/` (memory module `BbLayer::GpuMemory`), `gpu/games/` (game
|
||||
profiles), `gpu/shim/` (overlay, settings, hooks), the runtime in `src/` (kernel, libc, files, saves).
|
||||
|
||||
## Target structure
|
||||
|
||||
Modules with narrow interfaces. None of them knows addresses in the game's code (that belongs to the
|
||||
profile), and none does the GPU's work on the CPU (that is emulation):
|
||||
|
||||
1. **memory**: game memory in place, VRAM copies as a cache, shared tracking of page protection
|
||||
(write traps for copies and textures). Exists: `layer/` + the module in `buffer_cache_layer.cpp`.
|
||||
2. **cp**: command processor. Parses PM4 into the translator's command stream (state, draws,
|
||||
dispatches, synchronization) in the order the GPU executes it. Replaces `liverpool.cpp`.
|
||||
3. **state**: context registers → pipeline keys and Vulkan dynamic state. Replaces
|
||||
`liverpool_to_vk` and the key building in `vk_pipeline_cache`.
|
||||
4. **resources**: V#/T#/S# descriptors → buffers, images, samplers (caches, detiling). Replaces
|
||||
`buffer_cache`, `texture_cache` (the buffer part is already almost ours).
|
||||
5. **render**: Vulkan. Command recording per thread, barriers, submission, presentation. Replaces
|
||||
`renderer_vulkan` (the scheduler and stages A/B are already ours).
|
||||
6. **shaders**: GCN → SPIR-V. The largest part; it is replaced last, stage by stage (decoder,
|
||||
IR, passes, code generation).
|
||||
7. **platform**: VideoOut, GnmDriver, kernel events (`core/libraries`), next to the runtime.
|
||||
8. **games**: game profiles (exists).
|
||||
|
||||
## Order
|
||||
|
||||
One module is replaced at a time. First an interface in our code with the shadPS4 code behind it
|
||||
as is, then our own implementation behind the same interface. After each step: the route (a route
|
||||
benchmark script (not in this repository), frames and snapshots), the PM4 self-test
|
||||
(`BB_PM4_SELFTEST=1`), unit tests, the game on AMD and (from tester logs) on NVIDIA.
|
||||
|
||||
| Step | What | Why in this order |
|
||||
|---|---|---|
|
||||
| 1 | memory: shared tracking of page protection; write traps under textures by default; model 0.3 and the sparse arena are removed | removes the rest of memory emulation and the dependency on game hooks |
|
||||
| 2 | cp: the translator's command stream; `MEM_SEMAPHORE`, GPU time, compute queues done by the GPU | the last emulation left is in the command processor |
|
||||
| 3 | state + resources | they build on 1 and 2; the buffer part is already ours |
|
||||
| 4 | render | the scheduler and stages are already ours; the rasterizer and pipelines remain |
|
||||
| 5 | shaders | the largest and riskiest; by then everything else is already ours |
|
||||
| 6 | shadnet | networking, once the graphics are our own |
|
||||
|
||||
Steps 1 and 2 are the plan to move away from emulation
|
||||
([`EMULATION_REMOVAL_PLAN.ru.md`](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/EMULATION_REMOVAL_PLAN.ru.md),
|
||||
section dated 2026-10-10).
|
||||
|
||||
## Progress
|
||||
|
||||
### 2026-10-10: our own PM4 decoder (step 2, start)
|
||||
|
||||
- `gpu/cp/`: our own code, without shadPS4. Packet format and opcodes (`cp_pm4.h`), register file and
|
||||
decoder (`cp_decoder.{h,cpp}`). The decoder walks the buffer in stream order, applies register
|
||||
packets (`SET_*_REG`, `CLEAR_STATE`, registers written by draw packets), descends into
|
||||
nested buffers and hands each packet to a "sink" (`BbCp::Sink`) with the state already applied:
|
||||
draws and dispatches as typed calls, the rest as packets. The sink decides `COND_EXEC`. The
|
||||
decoder knows nothing about Vulkan, the game or where execution happens.
|
||||
- Unit test `tests/test_cp_decoder.cpp` (target `cp-decoder-test`): registers, partial writes,
|
||||
draws, dispatches, nested buffers, `COND_EXEC`, `CLEAR_STATE`, corrupt buffers.
|
||||
- Register addresses are checked against the shadPS4 layout at build time (`static_assert`). This
|
||||
found a bug: on CIK, `CP_STRMOUT_CNTL` is in uconfig (`0xC03F`), not in config.
|
||||
- Shadow check in the game (`BB_CP_SHADOW=1`): the new decoder parses every top-level buffer
|
||||
from the same state, and the register files are compared at the end of the buffer. On the route,
|
||||
all buffers matched (~800k buffers, ~200 million packets, 0 mismatches, 0 parse errors).
|
||||
Bloodborne does not use nested buffers (they are covered by the unit test).
|
||||
- Next: the decoder as a cursor (packet by packet, so the command processor loop can yield to
|
||||
other queues while waiting), the register file as shared state storage (its layout matches
|
||||
shadPS4 `Regs` word for word), then moving packet handling from `liverpool.cpp` into the
|
||||
translator's sink one packet type at a time, each time with the shadow check and the route.
|
||||
|
||||
### 2026-10-10: the new decoder is the real parsing path
|
||||
|
||||
- The decoder became a cursor (`BbCp::Cursor`: packet by packet, nested buffers at the
|
||||
caller's choice). The register file can live in the owner's storage (for the command processor,
|
||||
its `regs`, with the same word layout). A write observer (`RegisterObserver`, one call per
|
||||
packet) provides what draw preparation needs: the written blocks and the packet checksum.
|
||||
- `ProcessGraphics` takes packets and register writes from the cursor (`BB_CP_DECODER=0` selects
|
||||
the old walk). What each packet does is still the old code. PM4 self-test: 11 of 11. The share of
|
||||
prepared draws is the same (747k used, 19k not, versus 755k/24k), so the checksums
|
||||
match. The image is the same.
|
||||
@@ -1,92 +0,0 @@
|
||||
# Свой транслятор вместо shadPS4: устройство и порядок работ (2026-10-10)
|
||||
|
||||
Цель: уйти от эмуляции и от кода shadPS4 — собственный транслятор команд PS4 и собственный
|
||||
графический рендер, которые масштабируются на другие игры (всё, что про одну игру, — в её
|
||||
профиле, `gpu/games/`), одинаковые для AMD и NVIDIA. Потом — shadnet (сетевая игра).
|
||||
|
||||
## Что сейчас
|
||||
|
||||
Заимствовано из shadPS4 (`gpu/VENDOR.txt`, коммит b4e7ae7 от 2026-09-24) около 116 тыс. строк:
|
||||
|
||||
| Часть | Строк | Что делает | Наше в ней |
|
||||
|---|---|---|---|
|
||||
| `video_core/amdgpu` | 8,3 тыс. | разбор PM4 (командный процессор `liverpool.cpp`), регистры, тайлинг | метки видеочипом, occlusion, предикация, самотест PM4 |
|
||||
| `video_core/renderer_vulkan` | 28,6 тыс. | рендер Vulkan: конвейеры, растеризатор, планировщик, презентер | конвейер отрисовок (стадии A/B), потоки записи, апскейлеры, фоновая компиляция |
|
||||
| `video_core/buffer_cache` | 7,4 тыс. | память игры для видеочипа | почти всё переписано (модуль памяти, копии в VRAM, ловушки) |
|
||||
| `video_core/texture_cache` | 4,8 тыс. | текстуры по адресу, детайлинг | сборщик по времени, загрузки |
|
||||
| `shader_recompiler` | 38,6 тыс. | GCN → IR → SPIR-V | постраничные буферы (BDA), исправления |
|
||||
| `core`, `common` | 21,8 тыс. | VideoOut, GnmDriver, очереди событий, утилиты | vblank, метки |
|
||||
|
||||
Своё, вне кода shadPS4: `gpu/layer/` (модуль памяти `BbLayer::GpuMemory`), `gpu/games/` (профили
|
||||
игр), `gpu/shim/` (оверлей, настройки, перехваты), рантайм `src/` (ядро, libc, файлы, сохранения).
|
||||
|
||||
## Устройство, к которому идём
|
||||
|
||||
Модули с узкими интерфейсами; ни один не знает адресов кода игры (это профиль) и не делает
|
||||
работу видеочипа на процессоре (это эмуляция):
|
||||
|
||||
1. **memory** — память игры на месте, копии в VRAM как кэш, общий учёт защиты страниц (ловушки
|
||||
записи для копий и текстур). Есть: `layer/` + модуль в `buffer_cache_layer.cpp`.
|
||||
2. **cp** — командный процессор: разбор PM4 в поток команд транслятора (состояние, отрисовки,
|
||||
запуски, синхронизация) в порядке исполнения видеочипом. Заменяет `liverpool.cpp`.
|
||||
3. **state** — регистры контекста → ключи конвейеров и динамическое состояние Vulkan. Заменяет
|
||||
`liverpool_to_vk`, сборку ключей в `vk_pipeline_cache`.
|
||||
4. **resources** — описатели V#/T#/S# → буферы, образы, сэмплеры (кэши, детайлинг). Заменяет
|
||||
`buffer_cache`, `texture_cache` (буферная часть уже почти наша).
|
||||
5. **render** — Vulkan: запись команд по потокам, барьеры, отправка, вывод. Заменяет
|
||||
`renderer_vulkan` (планировщик и стадии A/B уже наши).
|
||||
6. **shaders** — GCN → SPIR-V. Самая большая часть; заменяется последней, по стадиям (декодер,
|
||||
IR, проходы, генерация).
|
||||
7. **platform** — VideoOut, GnmDriver, события ядра (`core/libraries`), рядом с рантаймом.
|
||||
8. **games** — профили игр (есть).
|
||||
|
||||
## Порядок
|
||||
|
||||
Замена по одному модулю: сначала интерфейс в нашем коде и за ним — код shadPS4 как есть, потом
|
||||
наша реализация за тем же интерфейсом. После каждого шага: маршрут (`route_bench.sh`, кадры и
|
||||
снимки), самотест PM4 (`BB_PM4_SELFTEST=1`), юнит-тесты, игра на AMD и (по логам тестеров) NVIDIA.
|
||||
|
||||
| Шаг | Что | Почему в этом порядке |
|
||||
|---|---|---|
|
||||
| 1 | memory: общий учёт защиты страниц; ловушки под текстурами по умолчанию; модель 0.3 и sparse-арена удаляются | убирает остаток эмуляции памяти и привязку к перехватам игры |
|
||||
| 2 | cp: поток команд транслятора; `MEM_SEMAPHORE`, время видеочипа, очереди compute — видеочипом | в командном процессоре осталась последняя эмуляция |
|
||||
| 3 | state + resources | опираются на 1 и 2; буферная часть уже наша |
|
||||
| 4 | render | планировщик и стадии уже наши; остаются растеризатор и конвейеры |
|
||||
| 5 | shaders | самый большой и рискованный; к этому времени остальное уже наше |
|
||||
| 6 | shadnet | сеть, когда графика своя |
|
||||
|
||||
Шаги 1 и 2 — это план ухода от эмуляции (`EMULATION_REMOVAL_PLAN.ru.md`, раздел от 2026-10-10).
|
||||
|
||||
## Ход работ
|
||||
|
||||
### 2026-10-10: свой декодер PM4 (шаг 2, начало)
|
||||
|
||||
- `gpu/cp/` — свой код, без shadPS4: формат пакетов и опкоды (`cp_pm4.h`), файл регистров и
|
||||
декодер (`cp_decoder.{h,cpp}`). Декодер идёт по буферу в порядке потока, применяет пакеты
|
||||
регистров (`SET_*_REG`, `CLEAR_STATE`, регистры, которые пишут пакеты отрисовки), спускается во
|
||||
вложенные буферы и отдаёт каждый пакет «приёмнику» (`BbCp::Sink`) с уже применённым состоянием:
|
||||
отрисовки и запуски — типизированно, остальное — пакетами. `COND_EXEC` решает приёмник. Про
|
||||
Vulkan, игру и место исполнения декодер не знает.
|
||||
- Юнит-тест `tests/test_cp_decoder.cpp` (цель `cp-decoder-test`): регистры, частичные записи,
|
||||
отрисовки, запуски, вложенные буферы, `COND_EXEC`, `CLEAR_STATE`, битые буферы.
|
||||
- Адреса регистров сверены с раскладкой shadPS4 на этапе сборки (`static_assert`): так нашлась
|
||||
ошибка — на CIK `CP_STRMOUT_CNTL` в uconfig (`0xC03F`), не в config.
|
||||
- Теневая проверка в игре (`BB_CP_SHADOW=1`): новый декодер разбирает каждый буфер верхнего уровня
|
||||
с того же состояния, в конце буфера файлы регистров сравниваются. На маршруте — все буферы
|
||||
совпали (~800 тыс. буферов, ~200 млн пакетов, 0 расхождений, 0 ошибок разбора). Вложенных
|
||||
буферов Bloodborne не использует (проверены юнит-тестом).
|
||||
- Дальше: декодер как курсор (пакет за пакетом, чтобы цикл командного процессора мог уступать
|
||||
другим очередям на ожиданиях), файл регистров — общее хранилище состояния (раскладка совпадает
|
||||
с `Regs` shadPS4 по словам), затем перенос обработки пакетов из `liverpool.cpp` в приёмник
|
||||
транслятора по одному виду пакетов, каждый раз с теневой проверкой и маршрутом.
|
||||
|
||||
### 2026-10-10: новый декодер — настоящий путь разбора
|
||||
|
||||
- Декодер стал курсором (`BbCp::Cursor`: пакет за пакетом, вложенные буферы — по желанию
|
||||
вызывающего), файл регистров может жить в хранилище владельца (у командного процессора — его
|
||||
`regs`, раскладка по словам та же), наблюдатель записей (`RegisterObserver`, один вызов на
|
||||
пакет) даёт то, что нужно подготовке отрисовок: записанные блоки и контрольную сумму пакетов.
|
||||
- `ProcessGraphics` берёт пакеты и запись регистров у курсора (`BB_CP_DECODER=0` — старый обход);
|
||||
что делает каждый пакет — пока прежний код. PM4-самопроверка — 11 из 11; доля подготовленных
|
||||
отрисовок та же (747 тыс. использовано, 19 тыс. нет, против 755/24 тыс.) — контрольные суммы
|
||||
сходятся; картинка та же.
|
||||
+11
-3
@@ -12,7 +12,7 @@ The launcher checks the game folder before starting. What it says, and what to d
|
||||
| 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). |
|
||||
| This is not a retail release of Bloodborne | Alpha tests and other games do not run. See [supported releases](COMPATIBILITY.md). |
|
||||
| eboot.bin could not be read as a decrypted PS4 executable | Dump the game and the update again. |
|
||||
| The game files are damaged | The extraction tool broke compressed files (LibOrbisPkg PkgTool on .NET 6 or newer); the game would hang while loading. Extract the game and the update again with a fixed tool. |
|
||||
|
||||
@@ -91,6 +91,14 @@ log says `Window: X11 (Xwayland), so Steam Input reaches the game`). On older re
|
||||
`SDL_VIDEO_DRIVER=x11 %command% --play` in the shortcut's launch options (keep `--play` after
|
||||
`%command%`), or turn Steam Input off for the shortcut: the game reads the controller itself.
|
||||
|
||||
## The Steam overlay does not open in the game
|
||||
|
||||
Steam's overlay (Shift+Tab, its FPS counter, F12 screenshots) is off by default: turn on **Steam
|
||||
overlay** under **Add to Steam** in the launcher, then start the game from Steam. The log then says
|
||||
`Steam overlay: …/ubuntu12_64/gameoverlayrenderer.so`, and on a Wayland desktop also
|
||||
`Window: X11 (Xwayland), so Steam's overlay shows on it`: Steam draws its overlay only on X11
|
||||
windows. If it says the library was not found, the game was not started by Steam.
|
||||
|
||||
## The camera turns much faster on diagonals
|
||||
|
||||
With the right stick pushed all the way, the game switches to its fast turn (about twice the
|
||||
@@ -104,9 +112,9 @@ camera can feel heavier: the game's own *Camera speed* setting makes up for it.
|
||||
Press **Test** in the **Status** row of **Multiplayer → Online**. An error mark on the shadNet server's UDP line means
|
||||
the server never learns your address, and no summon can connect: check that the server is running
|
||||
and that its log lists the UDP listener, and that no firewall blocks UDP 31314 on its side
|
||||
([SERVER.md](SERVER.md)). shadPS4's public server does not accept bbport builds at all: use The
|
||||
([hosting your own server](SERVER.md)). shadPS4's public server does not accept bbport builds at all: use The
|
||||
Hunter's Dream, Seamless Hunt or your own server.
|
||||
|
||||
## Other online problems
|
||||
|
||||
See [ONLINE.md](ONLINE.md): the account, the bells, the ports and the log lines of a summon.
|
||||
See [Online play and co-op](ONLINE.md): the account, the bells, the ports and the log lines of a summon.
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# FAQ
|
||||
|
||||
## Is this an emulator?
|
||||
|
||||
No. The game's own code runs directly on the CPU, and its graphics are translated to Vulkan. It is
|
||||
built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28.
|
||||
|
||||
## Where do I get the game?
|
||||
|
||||
From your own PS4: a dump of your copy with update 1.09. No game files are included, and none can
|
||||
be shared here.
|
||||
|
||||
## Can I play with Windows players?
|
||||
|
||||
Yes. On Seamless Hunt or The Hunter's Dream, Linux and Windows players play together; for seamless
|
||||
co-op everyone needs the same BBSC version (0.0.2). [Online play](ONLINE.md).
|
||||
|
||||
## Summons take a long time
|
||||
|
||||
Some waiting is normal: the game checks for summon signs about every 35 seconds, as the PS4's
|
||||
servers had it, so a sign can take over half a minute to show up.
|
||||
|
||||
## My friend never gets summoned with the Kin Bell
|
||||
|
||||
It depends on the server:
|
||||
|
||||
- On **Seamless Hunt** (or your own server with BBSC's game server), seamless co-op works fully:
|
||||
summons across areas and up to 5 hunters.
|
||||
- On **The Hunter's Dream**, BBSC keeps the session together, but summons are the normal game's:
|
||||
both hunters must be in the **same area**, the area's boss must still be alive in the host's world,
|
||||
and your levels must be within range.
|
||||
|
||||
[Seamless co-op on each server](ONLINE.md#seamless-co-op-bbsc).
|
||||
|
||||
## Do I need to open ports on my router?
|
||||
|
||||
No. With seamless co-op (BBSC) on, Seamless Hunt relays players who cannot reach each other
|
||||
directly, and The Hunter's Dream's bridge relays by itself. Only [your own server](SERVER.md) needs a few ports, through Tailscale.
|
||||
|
||||
## How do I get a log on the Steam Deck?
|
||||
|
||||
Turn on *Advanced → Developer → Save the log and statistics to a file* once in Desktop Mode; Game
|
||||
Mode sessions then save theirs too. [Getting a log](logs.md).
|
||||
|
||||
## The game updated and my Steam shortcut stopped working
|
||||
|
||||
Press **Add to Steam** again in the launcher: it updates the shortcut.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Getting started
|
||||
|
||||
## What you need
|
||||
|
||||
- **Your own dump of Bloodborne with update 1.09**, from your PS4: any retail release (US, Europe,
|
||||
UK, Japan, Asia, and the editions with The Old Hunters). No game files are included, and other
|
||||
versions of the game's executable start and then crash. [Which releases work](COMPATIBILITY.md).
|
||||
- **Linux x86-64** and a GPU with **Vulkan 1.3**: AMD or Intel with Mesa, or NVIDIA with NVIDIA's
|
||||
driver. The Steam Deck works too.
|
||||
- About **6 GB of free memory**, 10 GB for 1440p or 4K.
|
||||
|
||||
## First launch
|
||||
|
||||
1. Download `Bloodborne-bbport-x86_64.AppImage` from the
|
||||
[latest release](https://github.com/ayozetr/bloodborne_linux/releases/latest).
|
||||
2. Make it executable (in a file manager: *Properties → Permissions → Allow executing*, or
|
||||
`chmod +x Bloodborne-bbport-x86_64.AppImage`) and run it.
|
||||
3. In the launcher, choose your **game folder**: the one with `eboot.bin` in it.
|
||||
4. Press **Play**.
|
||||
|
||||
!!! tip "The first minutes"
|
||||
The first time, the game builds its shaders as it meets them, so it can stutter for a few
|
||||
minutes. The next sessions start with them ready.
|
||||
|
||||
## Saves, settings and the DLC
|
||||
|
||||
- Saves and settings go to `~/.local/share/bbport`.
|
||||
- **Setup → Saves** exports your saves into a `.zip` and imports them back, also from shadPS4.
|
||||
- The Game of the Year edition already has *The Old Hunters*. With the base game, put its DLC
|
||||
folder in **Setup → DLC folder**.
|
||||
|
||||
## Updating
|
||||
|
||||
The launcher's **About** page says when a new release is out and installs it; it can also go back
|
||||
to an earlier one. Your saves and settings stay as they are.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Play online](ONLINE.md), alone or in seamless co-op.
|
||||
- [Add the game to Steam](steam-deck.md), or play on the Steam Deck.
|
||||
- Something wrong? [Troubleshooting](TROUBLESHOOTING.md) and [getting a log](logs.md).
|
||||
@@ -1,67 +0,0 @@
|
||||
# Исправления — 2026-09-27
|
||||
|
||||
## ✅ Что исправлено
|
||||
|
||||
### 1. Маска реактивности не сохранялась
|
||||
- **Файл:** `gpu/shim/bbport_settings.cpp`
|
||||
- **Проблема:** Чекбокс "Показать маску (отладка)" не сохранялся в `bbport.ini`
|
||||
- **Решение:** Добавлена загрузка/сохранение параметра `debug_view`
|
||||
|
||||
### 2. 🔥 ДИНАМИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ПРЕСЕТОВ на RX 7800 XT
|
||||
- **Файлы:**
|
||||
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp`
|
||||
- `gpu/shim/bbport_overlay.cpp`
|
||||
- **Проблема:** RX 7800 XT не поддерживает `blit D32S8`, поэтому требовался перезапуск
|
||||
- **Решение:**
|
||||
- Fallback: UI depth очищается вместо копирования (работает корректно)
|
||||
- Убрана блокировка `BB_LIVE_SCALING_UNSUPPORTED`
|
||||
- Обновлено меню — убрано требование перезапуска для пресетов
|
||||
|
||||
### 3. Правильная конфигурация
|
||||
- **Файл:** `bbport.ini`
|
||||
- **Критично:** `jitter=1` (БЫЛО ВЫКЛЮЧЕНО!)
|
||||
- Оптимизированы параметры резкости и маски
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Что теперь работает БЕЗ перезапуска
|
||||
|
||||
- ✅ **Смена пресетов** (Native AA / Quality / Balanced / Performance / Ultra Performance)
|
||||
- ✅ Включение/выключение апскейлера
|
||||
- ✅ Резкость
|
||||
- ✅ Jitter
|
||||
- ✅ Маска реактивности
|
||||
- ✅ Отладочные режимы
|
||||
|
||||
**Перезапуск нужен ТОЛЬКО для:** векторов движения объектов (`object_motion`)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### Сборка
|
||||
✅ Успешно
|
||||
|
||||
### Проверить в игре:
|
||||
|
||||
1. **Динамическое переключение:**
|
||||
- Открыть меню (Insert или L3+R3)
|
||||
- Сменить пресет
|
||||
- **Должно применится СРАЗУ**
|
||||
|
||||
2. **Шлейфы на оружии:**
|
||||
- С `jitter=1` должны стать НАМНОГО меньше
|
||||
- Сравнить с `object_motion=0`
|
||||
|
||||
3. **Маска реактивности:**
|
||||
- Должна отображаться (красный цвет)
|
||||
- Помечены прозрачные эффекты
|
||||
|
||||
---
|
||||
|
||||
## 📝 Изменённые файлы
|
||||
|
||||
- `gpu/shim/bbport_settings.cpp`
|
||||
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp`
|
||||
- `gpu/shim/bbport_overlay.cpp`
|
||||
- `bbport.ini`
|
||||
@@ -1,248 +0,0 @@
|
||||
# Bloodborne на ПК: фактическое состояние порта и апскейлера
|
||||
|
||||
Срез на **29 сентября 2026 года**. Источник — файлы в `game_files`, история Git
|
||||
`native_probe` до `ac9fe90`, текущие незакоммиченные изменения, сохранённые логи
|
||||
запусков и локальные тесты. В документе «есть в коде», «запускалось» и «проверено
|
||||
по изображению» не считаются одним и тем же.
|
||||
|
||||
## Краткий итог
|
||||
|
||||
Это работающий экспериментальный **нативный Linux-порт**, а не запуск через
|
||||
полный эмулятор: исходный x86-64 `CUSA03173/eboot.bin` исполняется на CPU ПК,
|
||||
системные вызовы PS4 обрабатывает собственный HLE-рантайм, графику выводит
|
||||
Vulkan-ядро на основе shadPS4. Игра доходит до сцены с персонажем, принимает
|
||||
ввод, работает со звуком и сохранениями; это подтверждают логи запуска,
|
||||
снимки и видео в корне `native_probe`. Прохождение всей игры, все локации и
|
||||
отсутствие сбоев этими материалами **не подтверждены**.
|
||||
|
||||
**FSR 3.1 temporal upscaling реально работает по проверенному пути с разрешением
|
||||
сцены, заданным при старте**: лог `логи.md` показывает `960×540 → 1920×1080`,
|
||||
компоновку UI в 1920×1080 и запуск на RX 7800 XT. Текущий путь смены пресета
|
||||
без перезапуска остаётся проблемным на этой видеокарте: последний
|
||||
`last_run.log` показывает переключение запрошенных размеров сцены, но FSR-контекст
|
||||
`1920×1080 → 1920×1080`. По коду уменьшенные цели здесь не выбираются, когда
|
||||
`D32S8` не поддерживает blit. Следовательно, лог переключения пресетов **не
|
||||
доказывает реальное снижение разрешения 3D и выигрыш FPS**.
|
||||
|
||||
## Состав проекта и развитие
|
||||
|
||||
| Путь | Роль |
|
||||
|---|---|
|
||||
| `../CUSA03173/` | Локальные файлы игры, `eboot.bin`, ресурсы и модули PS4. |
|
||||
| `../np_base/` | Снимок ранней базы порта; рабочая разработка идёт в `native_probe`. |
|
||||
| `prepare.py`, `link_libc.py`, `link_modules.py`, `probe.c`, `runtime_*.c` | Подготовка исполняемого образа, привязка модулей, загрузчик и HLE. |
|
||||
| `gpu/` | Vulkan-видеоядро, рекомпилятор шейдеров, доработки для игры, FSR и ImGui. Заимствованное ядро shadPS4 описано в `gpu/VENDOR.txt`. |
|
||||
| `patches/Bloodborne.xml`, `patches.py` | Патчи FPS и разрешения, применяемые к гостевому коду при старте. |
|
||||
| `bbport.ini`, `run.sh`, `build.sh` | Настройки, полный запуск и сборка. |
|
||||
| `out/`, `out/dev/`, `last_run.log`, `логи.md` | Собранные бинарники и следы проверок; часть логов относится к более ранним версиям кода. |
|
||||
|
||||
Развитие по Git: 25 сентября добавлены загрузчик, HLE, графика, звук и
|
||||
оптимизации CPU/GPU; 26 сентября — анализ кадра, векторы камеры, jitter,
|
||||
FSR 3.1, пресеты и меню; 27 сентября последний коммит `ac9fe90` исправил
|
||||
переключение пресетов и отображение маски. После него в рабочем дереве есть
|
||||
незакоммиченные доработки векторов объектов, уменьшенных целей рендера,
|
||||
разделения разрешений сцены/UI, скриптов и тестов. Это **текущее рабочее
|
||||
дерево**, не готовый релиз. Старые разделы `README.md`, `docs/upscaler.md`
|
||||
и `docs/ROADMAP.md` описывают разные этапы и местами уже устарели.
|
||||
|
||||
## Что реализовано
|
||||
|
||||
### Запуск и производительность
|
||||
|
||||
- Гостевой `eboot` и часть оригинальных PRX запускаются вместе с реализациями
|
||||
памяти, потоков, файлов, времени, контроллера, аудио, сохранений и сервисов.
|
||||
Сеть работает в офлайн-режиме; PSN не реализован.
|
||||
- SDL3 обслуживает геймпад и запасное управление с клавиатуры. Звук выводится
|
||||
через host-аудиосистему; в коде есть исправление темпа `AudioOut` и флаг
|
||||
сохранения для отсутствовавших звуков.
|
||||
- Vulkan-рендерер получил отдельный поток записи команд, подготовку draw на
|
||||
четырёх потоках, кеши текстур и буферов, копирование гостевой памяти и пул
|
||||
staging-буферов. `docs/parallel_gpu.md` и `docs/ROADMAP.md` содержат
|
||||
замеры в отдельных сценах; их нельзя переносить на все конфигурации.
|
||||
- `BB_FPS=uncap` по умолчанию применяет патч с шагом симуляции по времени
|
||||
кадра; доступны `30`, `60`, `90`. Частота вывода ограничивается монитором
|
||||
и максимумом 120 FPS. Для диагностики есть `BB_FRAME_STATS=1`,
|
||||
`BB_TOGGLE_FILE` и захват структуры кадра через `BB_CAPTURE_TRIGGER`.
|
||||
|
||||
### Апскейлер
|
||||
|
||||
- В игре нет готового буфера скорости. Порт вычисляет векторы камеры из
|
||||
глубины и матриц двух кадров; для геометрии сцены делает субпиксельный
|
||||
jitter Halton. Перепроекция статичной геометрии проверялась ранее
|
||||
(`docs/upscaler.md`).
|
||||
- Интегрирован FSR 3.1 через `gpu/third_party/fsr-vulkan`. Native AA работает
|
||||
1:1 на HDR-цвете сцены перед постобработкой. Проверенный режим с пониженным
|
||||
разрешением использует патч гостевого разрешения, FSR после тонмаппинга и
|
||||
отдельную компоновку интерфейса в 1920×1080. Патч UI в текущем дереве
|
||||
оставляет его координаты нативными. Явный `BB_RENDER_RES=ШxВ` включает
|
||||
этот путь; например, лог `логи.md` подтверждает `960x540`.
|
||||
- Пресеты в коде: Native AA `1920×1080`, Quality `1280×720`, Balanced
|
||||
`1130×636`, Performance `960×540`, Ultra Performance `640×360` для вывода
|
||||
1920×1080. Меню ImGui открывается `Insert` или `L3+R3`. В `bbport.ini`
|
||||
сохраняются FSR, пресет, резкость, jitter, маска реактивности, режим
|
||||
отладки, векторы объектов и счётчик FPS. Текущий конфиг содержит
|
||||
`upscaler=fsr3`, `preset=4`, `jitter=1`, `reactive=1`,
|
||||
`object_motion=1`, `debug_view=0`.
|
||||
- Маска реактивности формируется сравнением цвета до и после прозрачных
|
||||
проходов. Она опциональна: прежние наблюдения отмечают дрожание тонкой
|
||||
дымки при её включении. Настройка по умолчанию в коде выключена, но
|
||||
в текущем `bbport.ini` включена.
|
||||
- В рабочем дереве есть экспериментальные векторы анимированных объектов:
|
||||
прошлые позиции вершин, дополнительная цель G-buffer, сопоставление
|
||||
отрисовок между кадрами и ограничение на подходящие буферы позы/костей.
|
||||
Изменение `object_motion` требует перезапуска. По
|
||||
`out/dev/motion-selective.log` история в выбранных draws часто находится,
|
||||
без пропусков по ёмкости в показанных интервалах. Это метрика draw, **не
|
||||
оценка покрытия пикселей или визуального качества**.
|
||||
|
||||
## Текущий дефект динамических пресетов
|
||||
|
||||
В `vk_scene_resolution.cpp` функция `SceneTargets::Eligible` требует
|
||||
`BlitSrc|BlitDst` у формата каждого кандидата. В `vk_rasterizer.cpp`
|
||||
уменьшенный проход отключается, если неподходящая глубина; от неё же зависит
|
||||
выбор уменьшенного входа FSR в `vk_temporal_upscaler.cpp`. Сохранённый лог
|
||||
для RX 7800 XT сообщает об отсутствии `blit` у `D32S8`. Позднее добавлен
|
||||
fallback, который **очищает глубину UI** вместо её копирования, но он не
|
||||
устраняет требование `blit` у глубины **3D-сцены**.
|
||||
|
||||
Итог на этой машине: в `last_run.log` 28 сентября перечислены переключения
|
||||
`640×360`, `1280×720`, `960×540` и других пресетов, однако созданный
|
||||
FSR-контекст остался `1920×1080 → 1920×1080 (HDR scene color)`. Сообщение
|
||||
`Scene resolution: raster ...` отражает запрошенный размер proxy-целей;
|
||||
оно не измеряет фактически использованный размер каждого draw. Заявления
|
||||
`README_FIXES.md` и подсказки меню о работающем на всех GPU динамическом
|
||||
масштабировании сильнее, чем позволяют код и последний лог. `логи.md`
|
||||
относится к более раннему запуску с автоматическим выбором стартового патча,
|
||||
который затем изменили в `run.sh`.
|
||||
|
||||
## Проверки и границы уверенности
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Логи игрового запуска и кадры от 27–28 сентября | Загрузка в игровую сцену и вывод изображения подтверждены. Сквозной тест прохождения не проводился. |
|
||||
| `логи.md` на RX 7800 XT | Подтверждены патч сцены `960×540`, UI `1920×1080`, FSR-контекст `960×540 → 1920×1080`. |
|
||||
| `last_run.log` | Меню меняло пресеты без перезапуска, но FSR был 1:1; FPS-статистики этого запуска нет. |
|
||||
| `out/dev/motion-selective.log` | Для одной серии сцен с выборочными векторами объектов видны интервалы примерно 72–100 FPS, без `capacity skips` в приведённых сводках; это не контролируемое сравнение пресетов. |
|
||||
| `out/gpu/motion-history-test` и `out/gpu/ui-composition-test` | Повторно выполнены 29 сентября: оба `PASS`. |
|
||||
| `nix-shell shell.nix --run 'python3 -m unittest -v test_patches'` | Повторно выполнено 29 сентября: 5 тестов `OK`. |
|
||||
| `out/dev/scene-validation.log` | Vulkan-тест уменьшенных целей на Lavapipe **завершился assertion** из-за несовпадения версии заголовков Vulkan и диспетчера. Полной успешной проверки этого пути лог не даёт. |
|
||||
|
||||
Векторы объектов в игре и их эффект на шлейфы оружия и одежды визуально не
|
||||
подтверждены систематическим A/B-тестом. Ранние замеры около 49 FPS со
|
||||
всеми draws против 77–78 FPS без них и около 81–87 FPS на более узкой
|
||||
выборке относятся к разным запускам; это ориентир для дальнейшего теста,
|
||||
а не гарантированная производительность текущего конфига. Снимок
|
||||
`image_2026-09-28_00-44-14.png` показывает игровую сцену и оверлей около
|
||||
59 FPS, но не раскрывает условия замера.
|
||||
|
||||
## Что ещё не сделано / что проверять следующим
|
||||
|
||||
1. Исправить и подтвердить реальный уменьшенный 3D-рендер в динамическом
|
||||
режиме на RX 7800 XT: проверить фактический размер color/depth attachments
|
||||
и FSR-вход на каждом пресете. Если путь остаётся несовместимым с `D32S8`,
|
||||
явно выбирать стартовый патч разрешения и показывать в меню необходимость
|
||||
перезапуска.
|
||||
2. После исправления провести одинаковую игровую сцену с Native AA,
|
||||
Quality и Performance: зафиксировать вход/выход FSR, FPS, время кадра,
|
||||
чёткость UI и изображение в движении.
|
||||
3. Визуально сравнить векторы объектов включено/выключено при беге, повороте
|
||||
камеры и в бою; проверить шлейфы, покрытие истории и стоимость GPU.
|
||||
4. Разобраться с assertion в `scene-resolution-test`; остальные тесты не
|
||||
заменяют игровой Vulkan-тест. Генерации кадров, FSR 4, DLSS и XeSS в
|
||||
текущем порте нет — они перечислены в `docs/ROADMAP.md` как планы.
|
||||
|
||||
## Как воспроизвести подтверждённый путь
|
||||
|
||||
Из `game_files` на этой конфигурации:
|
||||
|
||||
```bash
|
||||
BB_RENDER_RES=960x540 BB_FRAME_STATS=1 bash native_probe/run.sh
|
||||
```
|
||||
|
||||
`BB_RENDER_RES` выбирает патч стартового разрешения сцены, а текущий
|
||||
`patches.py` сохраняет UI в 1920×1080. Смена разрешения таким способом
|
||||
требует нового запуска. Без `BB_RENDER_RES` включается экспериментальный
|
||||
динамический путь, описанный выше. Запуск пересобирает порт и использует
|
||||
локальные файлы `CUSA03173`; наличие исходников и бинарника само по себе
|
||||
не подтверждает воспроизводимость на другом ПК.
|
||||
|
||||
## Исправление 29 сентября: живая смена пресета
|
||||
|
||||
Причин, по которым смена пресета в меню не меняла картинку, было две:
|
||||
|
||||
1. `SceneTargets` копировал цели между нативным и уменьшенным размером только
|
||||
через `vkCmdBlitImage`, а RADV не даёт blit для `D32S8`. Поэтому глубина
|
||||
сцены считалась неподходящей и уменьшенный рендер не включался. Теперь для
|
||||
depth/stencil-форматов без blit копирование идёт полноэкранной отрисовкой
|
||||
(`host_shaders/depth_resample.frag`): глубина пишется через `gl_FragDepth`,
|
||||
трафарет через `VK_EXT_shader_stencil_export`.
|
||||
2. Маска реактивности (`reactive=1`) снимала цвет в 1920×1080 и пересоздавала
|
||||
FSR-контекст 1920→1920, а основной проход сразу пересоздавал его под
|
||||
уменьшенный вход. Контекст пересоздавался каждый кадр. Теперь маска
|
||||
читает уменьшенную цель сцены.
|
||||
|
||||
Проверка на RX 7800 XT (`out/dyn-res-test.log`): переключения в меню дают
|
||||
`FSR 3 context 1280x720 / 1130x636 / 960x540 / 640x360 -> 1920x1080`,
|
||||
по одному пересозданию на смену пресета, без сбоев. FPS в этой сцене около
|
||||
54–56 на всех пресетах, то есть узкое место здесь не в разрешении 3D.
|
||||
|
||||
## 30 сентября: FSR 4 INT8 и производительность
|
||||
|
||||
### FSR 4 (v07 INT8/DOT4)
|
||||
|
||||
- `vk_fsr4.cpp/.h`: провайдер FSR-Vulkan `ffxFsr4V07*` с моделью под пресет (native,
|
||||
quality, balanced, performance, ultraperf), тир 1080. Входы те же, что у FSR 3.1.
|
||||
В меню «Апскейлер → FSR 4 (INT8)», в ini `upscaler=fsr4`, env `BB_UPSCALER=fsr4`.
|
||||
Если нет ассетов или возможностей GPU, автоматически включается FSR 3.1.
|
||||
- Ассеты: `fetch_fsr4_assets.sh` скачивает набор, собранный Q2RTX из MIT-исходников
|
||||
FidelityFX SDK (`fsr4_shaders/`, в `.gitignore`, 98 SHA-256 сверены с манифестами).
|
||||
`BB_FSR4_DIR` задаёт другой каталог.
|
||||
- Устройство: включены `shaderIntegerDotProduct` и `VK_KHR_compute_shader_derivatives`
|
||||
(`Instance::IsFsr4Int8Supported`).
|
||||
- Проверено на RX 7800 XT: модели quality/ultraperf создаются, при смене пресета
|
||||
фриз ~6,5 с (компиляция). Пользователь: картинка чётче FSR 3.1, но сильный гостинг
|
||||
на персонаже и оружии. Векторы проверены отладочным видом, они корректны:
|
||||
персонаж получает векторы объекта. В SPIR-V pre-прохода глубина выбирается
|
||||
по минимуму (как у нас), векторы трактуются как «прошлая − текущая». Для A/B
|
||||
в меню: «FSR 4: авто-экспозиция», «FSR 4: обратный знак jitter».
|
||||
|
||||
### Производительность (perf, поток GPU-команд)
|
||||
|
||||
- `SceneTargets::Eligible` спрашивал у драйвера свойства формата на каждую отрисовку:
|
||||
~13–15% времени потока GPU. Теперь кеш по формату.
|
||||
- `ObjectMotion::Attach`, снимок цвета и маска реактивности вызывали
|
||||
`Scheduler::CommandBuffer()`. Это переводило запись всего остатка сабмита (почти
|
||||
весь G-buffer) на поток GPU. Теперь они идут через `Scheduler::Record`.
|
||||
Остались два прямых вызова в конце кадра (`TemporalUpscaler::Run`, презентер).
|
||||
- Итог: поток GPU ~74% вместо ~94% одного ядра. В игровых сценах 61–98 FPS против
|
||||
53–87 до исправлений (сцены не совпадают один в один, это ориентир).
|
||||
|
||||
### Профиль с DWARF-стеком (perf, поток GPU-команд, 30.09)
|
||||
|
||||
- `Motion::IndexedRange` (скан и хеш индексов для векторов объектов) занимал ~10% потока
|
||||
GPU. Добавлен `Motion::IndexRangeCache`: результат переиспользуется и полностью
|
||||
перепроверяется раз в 32 кадра. В игре ~98% диапазонов из кеша (260 записей).
|
||||
- `SceneTargets`: плоская таблица свойств форматов и память последних восьми целей;
|
||||
вместе с Attachment/Get/View/Eligible ~4% вместо ~7%.
|
||||
- После этого поток GPU загружен работой без заметных ожиданий. Поток записи Vulkan
|
||||
~40% работы и ~57% ожидания. GPU (RX 7800 XT) в среднем ~65% занятости.
|
||||
Ограничивает кадр поток GPU-команд. Главные статьи — базовые затраты видеоядра:
|
||||
FindView ~5,6%, BindTextures ~5,6%, memmove ~4,6%, SmallGuestCopy ~3,5%,
|
||||
CachedImageDescEntry ~3,2%, BindBuffers/BindVertexBuffers ~3% каждая.
|
||||
Следующий шаг — перенос этих вычислений на рабочие потоки (шаг 2 parallel_gpu.md).
|
||||
|
||||
### FSR 4: гостинг на персонаже и оружии (30.09, повторная проверка)
|
||||
|
||||
Видео `video_2026-09-30_03-43-12.mp4`: FSR 4, Ultra Performance (640×360 → 1920×1080). За
|
||||
движущимся диском пилы остаётся тёмный отпечаток на открывшемся фоне. Проверено:
|
||||
|
||||
- отладочный вид векторов (FSR 4): персонаж и оружие получают векторы объекта;
|
||||
- кеш диапазонов индексов не устаревает (0 изменений при перепроверках);
|
||||
- последовательность jitter совпадает с провайдером (Halton 2/3, ceil(8·ratio²) фаз);
|
||||
- A/B в одном запуске с выключенными оптимизациями после 29.09 (биты 4096, 8192, 1<<30):
|
||||
шлейфы одинаковые в обеих фазах, то есть это не регрессия от них. Попутно: эти
|
||||
оптимизации вместе дают 84,4 против 70,8 FPS при беге (+19%).
|
||||
|
||||
Модель v07 определяет открывшийся фон по цвету (глубина используется только для расширения
|
||||
векторов), и при входе 640×360 ей мало информации. FSR 3.1 отбрасывает такую историю по
|
||||
глубине, поэтому на Ultra Performance шлейфов у него меньше.
|
||||
@@ -1,248 +0,0 @@
|
||||
# Исправления и анализ проблем
|
||||
|
||||
## Дата: 2026-09-27
|
||||
|
||||
### Проблемы из тестирования
|
||||
|
||||
1. **Маска реактивности не отображается в отладочном режиме**
|
||||
2. **Шлейфы на оружии при движении**
|
||||
3. **Изменение настроек требует перезапуск игры**
|
||||
|
||||
---
|
||||
|
||||
## 1. Маска реактивности не сохраняется ✅ ИСПРАВЛЕНО
|
||||
|
||||
### Проблема
|
||||
Чекбокс "Показать маску (отладка)" в меню работал, но настройка `debug_view` не сохранялась в `bbport.ini`, поэтому после перезапуска маска не отображалась.
|
||||
|
||||
### Причина
|
||||
В `bbport_settings.cpp`:
|
||||
- Функция `Set()` не обрабатывала ключ `debug_view`
|
||||
- Функция `Save()` не записывала значение `debug_view` в файл
|
||||
|
||||
### Исправление
|
||||
**Файл:** `native_probe/gpu/shim/bbport_settings.cpp`
|
||||
|
||||
1. Добавлена обработка в `Set()`:
|
||||
```cpp
|
||||
} else if (key == "debug_view") {
|
||||
v.debug_view = std::clamp(i, 0, DebugViewCount - 1);
|
||||
}
|
||||
```
|
||||
|
||||
2. Добавлена запись в `Save()`:
|
||||
```cpp
|
||||
"debug_view=%d\nshow_fps=%d\n",
|
||||
// ...
|
||||
v.debug_view.load(), int(v.show_fps.load())
|
||||
```
|
||||
|
||||
### Как проверить
|
||||
1. Запустить игру
|
||||
2. Открыть меню (Insert или L3+R3)
|
||||
3. Включить "Маска реактивности" → "Показать маску (отладка)"
|
||||
4. Закрыть меню
|
||||
5. Маска должна отображаться красным цветом поверх затемнённого кадра
|
||||
|
||||
---
|
||||
|
||||
## 2. Шлейфы на оружии
|
||||
|
||||
### Анализ
|
||||
Из логов видно:
|
||||
```
|
||||
Object motion: on (4194304 vertices per frame)
|
||||
```
|
||||
|
||||
Векторы движения объектов включены и работают. Шлейфы могут быть из-за:
|
||||
|
||||
#### Возможные причины:
|
||||
|
||||
**A. Проблема FSR 3.1**
|
||||
- FSR 3.1 может неправильно обрабатывать быстро движущиеся объекты
|
||||
- Нужно сравнить с FSR 4 (когда будет реализован)
|
||||
|
||||
**B. Векторы движения объектов неточные**
|
||||
- Выборочный путь включает векторы только для отрисовок с буферами поз/костей
|
||||
- Возможно, оружие на спине использует другой тип анимации
|
||||
|
||||
**C. Маска реактивности не работает для оружия**
|
||||
- Маска помечает только прозрачные эффекты (частицы, дымка)
|
||||
- Оружие — непрозрачный объект, поэтому маска на него не влияет
|
||||
|
||||
**D. Jitter выключен**
|
||||
Из `bbport.ini`:
|
||||
```
|
||||
jitter=0
|
||||
```
|
||||
Субпиксельный сдвиг **выключен**, что ухудшает качество темпорального апскейлера!
|
||||
|
||||
### Рекомендации для тестирования
|
||||
|
||||
1. **Включить jitter:**
|
||||
```ini
|
||||
jitter=1
|
||||
```
|
||||
Jitter критически важен для FSR 3.1!
|
||||
|
||||
2. **Проверить маску реактивности:**
|
||||
- Включить `debug_view=1` и `reactive=1`
|
||||
- Посмотреть, какие области помечены как реактивные
|
||||
- Оружие НЕ должно быть в маске (оно непрозрачное)
|
||||
|
||||
3. **Попробовать увеличить reactive_scale:**
|
||||
```ini
|
||||
reactive_scale=2.00
|
||||
```
|
||||
(хотя это для прозрачности, не для оружия)
|
||||
|
||||
4. **Отключить векторы объектов для сравнения:**
|
||||
```ini
|
||||
object_motion=0
|
||||
```
|
||||
Перезапустить и сравнить — станет хуже или лучше?
|
||||
|
||||
5. **Проверить разные пресеты:**
|
||||
- Native AA (preset=0) — без масштабирования
|
||||
- Quality (preset=1) — x1.5
|
||||
- Performance (preset=3) — x2.0
|
||||
|
||||
Шлейфы должны быть меньше на более высоких разрешениях рендера.
|
||||
|
||||
### Логи для отладки
|
||||
|
||||
Включить отладку векторов движения:
|
||||
```bash
|
||||
BB_DEBUG_MOTION=1 ./run.sh
|
||||
```
|
||||
|
||||
Должны появиться опции:
|
||||
- Toggle 1<<22: перепроецирование прошлого кадра
|
||||
- Toggle 1<<23: карта ошибок
|
||||
|
||||
---
|
||||
|
||||
## 3. Перезапуск для применения настроек
|
||||
|
||||
### Текущая ситуация
|
||||
|
||||
Из логов:
|
||||
```
|
||||
GPU scene scaling: AMD Radeon RX 7800 XT (RADV NAVI32) lacks blit support for D32S8
|
||||
GPU scene scaling: using startup resolution 960x540 for this driver
|
||||
```
|
||||
|
||||
Твоя RX 7800 XT **не поддерживает blit D32S8** (копирование depth+stencil буфера), поэтому используется старый путь:
|
||||
- Разрешение задаётся патчем при запуске
|
||||
- Смена пресета требует перезапуска
|
||||
|
||||
### Новый путь (в рабочем дереве, не закоммичен)
|
||||
|
||||
Согласно ROADMAP.md, в рабочем дереве есть код для:
|
||||
- Динамического масштабирования на уровне рендерера
|
||||
- Переключения пресетов без перезапуска
|
||||
- Автоматического fallback для GPU без blit D32S8
|
||||
|
||||
Но этот код **ещё не закоммичен** — автор ждёт тестирования на реальном GPU.
|
||||
|
||||
### Что работает без перезапуска
|
||||
|
||||
Эти настройки применяются **сразу**:
|
||||
- ✅ Включение/выключение апскейлера
|
||||
- ✅ Резкость (sharpness)
|
||||
- ✅ Jitter (если не включён BB_LIVE_SCALING_UNSUPPORTED)
|
||||
- ✅ Маска реактивности и её параметры
|
||||
- ✅ Отладочные режимы
|
||||
|
||||
### Что требует перезапуск
|
||||
|
||||
На твоём GPU:
|
||||
- ❌ Смена пресета (Native AA ↔ Quality ↔ Performance)
|
||||
- ❌ Включение/выключение векторов движения объектов
|
||||
|
||||
### Обходной путь
|
||||
|
||||
Для тестирования разных пресетов без перезапуска игры:
|
||||
```bash
|
||||
# Редактировать bbport.ini
|
||||
preset=1 # Quality
|
||||
# Перезапустить только процесс (Ctrl+C, затем ./run.sh)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Дополнительные находки
|
||||
|
||||
### Конфигурация из bbport.ini
|
||||
|
||||
```ini
|
||||
upscaler=fsr3
|
||||
preset=3 # Performance (960x540 → 1920x1080, x2.0)
|
||||
sharpen=1
|
||||
sharpness=0.81 # Высокая резкость
|
||||
jitter=0 # ⚠️ ВЫКЛЮЧЕН — это плохо для FSR!
|
||||
reactive=1
|
||||
object_motion=1
|
||||
reactive_scale=1.00
|
||||
reactive_threshold=0.20
|
||||
reactive_max=0.90
|
||||
show_fps=1
|
||||
```
|
||||
|
||||
### Рекомендованные настройки для теста
|
||||
|
||||
```ini
|
||||
upscaler=fsr3
|
||||
preset=3 # Performance
|
||||
sharpen=1
|
||||
sharpness=0.50 # Немного снизить
|
||||
jitter=1 # ⚠️ ВКЛЮЧИТЬ обязательно!
|
||||
reactive=1
|
||||
object_motion=1
|
||||
reactive_scale=1.50 # Увеличить для теста
|
||||
reactive_threshold=0.15
|
||||
reactive_max=0.90
|
||||
debug_view=1 # Показывать маску
|
||||
show_fps=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
### Для пользователя (тестирование)
|
||||
|
||||
1. ✅ Перезапустить игру с исправлениями
|
||||
2. Включить `jitter=1` и `debug_view=1`
|
||||
3. Протестировать отображение маски
|
||||
4. Записать видео с маской и без неё
|
||||
5. Сравнить шлейфы с `object_motion=0` и `object_motion=1`
|
||||
|
||||
### Для разработчика (следующие фичи)
|
||||
|
||||
1. **Закоммитить код динамического масштабирования**
|
||||
- Уже работает в рабочем дереве
|
||||
- Нужно тестирование на RX 7800 XT
|
||||
|
||||
2. **Улучшить векторы движения объектов**
|
||||
- Проверить, почему оружие оставляет шлейфы
|
||||
- Возможно, нужен более широкий критерий для "движущихся отрисовок"
|
||||
|
||||
3. **Добавить FSR 4 INT8**
|
||||
- Уже есть в FSR-Vulkan как experimental
|
||||
- Сравнить качество с FSR 3.1
|
||||
|
||||
4. **Добавить DLSS**
|
||||
- Нативный Linux Vulkan SDK (NGX)
|
||||
- Не будет работать на AMD, но нужен для пользователей NVIDIA
|
||||
|
||||
---
|
||||
|
||||
## Коммит
|
||||
|
||||
Изменённые файлы:
|
||||
- `native_probe/gpu/shim/bbport_settings.cpp` — добавлено сохранение/загрузка `debug_view`
|
||||
|
||||
Тестирование:
|
||||
- Сборка: ✅ успешно
|
||||
- Требуется проверка в игре
|
||||
@@ -1,147 +0,0 @@
|
||||
# 🎮 Исправления для Bloodborne Native Port
|
||||
|
||||
## ✅ Что сделано
|
||||
|
||||
### 1. 🔧 Исправлена маска реактивности
|
||||
- Параметр `debug_view` теперь сохраняется в `bbport.ini`
|
||||
- Маска отображается после перезапуска
|
||||
|
||||
### 2. 🚀 ДИНАМИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ПРЕСЕТОВ на RX 7800 XT
|
||||
**Главное исправление!** Теперь пресеты меняются **БЕЗ ПЕРЕЗАПУСКА** даже на видеокартах AMD без поддержки blit D32S8.
|
||||
|
||||
**Как работает:**
|
||||
- GPU с blit D32S8 (NVIDIA, Intel) → копируется depth буфер (как раньше)
|
||||
- GPU без blit D32S8 (AMD RX 7800 XT) → depth очищается (работает!)
|
||||
- UI и глубина работают корректно в обоих случаях
|
||||
|
||||
### 3. ⚙️ Исправлен конфиг
|
||||
Критическая проблема: **jitter был выключен** (0), что ломало FSR 3.1!
|
||||
- `jitter=1` — включён (обязательно для темпорального апскейлера)
|
||||
- `debug_view=1` — включена маска
|
||||
- Оптимизированы параметры резкости и маски
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Что теперь работает БЕЗ перезапуска
|
||||
|
||||
- ✅ **Смена пресетов** (Native AA / Quality / Balanced / Performance / Ultra Performance)
|
||||
- ✅ Включение/выключение апскейлера
|
||||
- ✅ Резкость (sharpen/sharpness)
|
||||
- ✅ Jitter (субпиксельный сдвиг)
|
||||
- ✅ Маска реактивности (reactive) и её параметры
|
||||
- ✅ Отладочные режимы (debug_view)
|
||||
|
||||
**Перезапуск нужен ТОЛЬКО для:** векторов движения объектов (`object_motion`)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Как запустить
|
||||
|
||||
### Вариант 1: Быстрый старт
|
||||
```bash
|
||||
./start_fixed.sh
|
||||
```
|
||||
|
||||
### Вариант 2: Обычный запуск
|
||||
```bash
|
||||
bash run.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Что проверить
|
||||
|
||||
### 1. Динамическое переключение пресетов
|
||||
1. Запустить игру
|
||||
2. Открыть меню: **Insert** или **L3+R3** на геймпаде
|
||||
3. Сменить пресет (например, с Performance на Quality)
|
||||
4. Закрыть меню
|
||||
5. ✅ **Разрешение должно измениться СРАЗУ**
|
||||
6. Проверить FPS и качество
|
||||
|
||||
### 2. Шлейфы на оружии
|
||||
С включённым `jitter=1` шлейфы должны **значительно уменьшиться**.
|
||||
|
||||
**Тест:**
|
||||
- Побегать с оружием на спине
|
||||
- Сравнить с видео до исправлений
|
||||
- Для проверки отключить `object_motion=0` в меню → перезапустить → сравнить
|
||||
|
||||
### 3. Маска реактивности
|
||||
- Должна отображаться (красный цвет на затемнённом кадре)
|
||||
- Помечены прозрачные эффекты (дымка, частицы)
|
||||
- Оружие НЕ должно быть в маске (оно непрозрачное)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Производительность
|
||||
|
||||
Проверь разные пресеты:
|
||||
|
||||
| Пресет | Разрешение рендера | Масштаб | Качество | FPS |
|
||||
|--------|-------------------|---------|----------|-----|
|
||||
| Native AA | 1920×1080 | x1.0 | Максимум | ~60-70 |
|
||||
| Quality | 1280×720 | x1.5 | Высокое | ~80-90 |
|
||||
| Balanced | ~1130×635 | x1.7 | Среднее | ~90-100 |
|
||||
| **Performance** | **960×540** | **x2.0** | **Хорошее** | **~100-110** |
|
||||
| Ultra Perf | 640×360 | x3.0 | Базовое | ~120+ |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Технические детали
|
||||
|
||||
### Fallback для UI depth на AMD
|
||||
```cpp
|
||||
// Если GPU поддерживает blit D32S8:
|
||||
if (depth_blit) {
|
||||
// Копируем depth сцены в UI depth
|
||||
cmd.blitImage(scene_depth, ui_depth, ...);
|
||||
}
|
||||
// Иначе (RX 7800 XT):
|
||||
else {
|
||||
// Просто очищаем UI depth
|
||||
cmd.clearDepthStencilImage(ui_depth, ...);
|
||||
}
|
||||
// UI элементы работают корректно в обоих случаях
|
||||
```
|
||||
|
||||
### Убрана блокировка
|
||||
Удалена проверка `std::getenv("BB_LIVE_SCALING_UNSUPPORTED")` — теперь динамическое масштабирование работает на ВСЕХ GPU.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Изменённые файлы
|
||||
|
||||
- `gpu/shim/bbport_settings.cpp` — сохранение `debug_view`
|
||||
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp` — fallback UI depth, убрана блокировка
|
||||
- `gpu/shim/bbport_overlay.cpp` — обновлено меню
|
||||
- `bbport.ini` — правильная конфигурация
|
||||
- `CHANGELOG.md` — описание изменений
|
||||
- `start_fixed.sh` — скрипт быстрого запуска
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Известные проблемы
|
||||
|
||||
1. **Шлейфы на оружии** — может быть проблема FSR 3.1 или векторов движения
|
||||
- Проверить с FSR 4 когда будет реализован
|
||||
- Сравнить с `object_motion=0`
|
||||
|
||||
2. **Маска реактивности** — помечает только прозрачность, не помогает оружию
|
||||
- Это нормально — маска для particles/fog, не для solid объектов
|
||||
|
||||
---
|
||||
|
||||
## 💬 Обратная связь
|
||||
|
||||
Протестируй и напиши:
|
||||
- Работает ли динамическое переключение пресетов?
|
||||
- Стали ли меньше шлейфы с `jitter=1`?
|
||||
- Отображается ли маска реактивности?
|
||||
- Какой FPS на разных пресетах?
|
||||
|
||||
---
|
||||
|
||||
✅ **Сборка протестирована:** успешно
|
||||
✅ **Коммит:** `ac9fe90`
|
||||
📅 **Дата:** 2026-09-27
|
||||
@@ -1,240 +0,0 @@
|
||||
# Работа 30.09–01.10.2026: многопоточный GPU, апскейлер, стабильность, лаунчер, AppImage
|
||||
|
||||
Отчёт о большой сессии разработки нативного порта. Коммиты — от `37c8eac` до `9d0cf7f`
|
||||
(35 штук, `git log 37c8eac^..HEAD`). Технические подробности на английском — в
|
||||
[parallel_gpu.md](parallel_gpu.md) (разделы «Two-stage draw pipeline», «Second round»,
|
||||
«Third round», «Fixed: guest heap corruption»); краткая сводка — в [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
Все замеры — RX 7800 XT, 16 потоков, вход в локацию после загрузки, персонаж стоит, FSR 4
|
||||
Ultra Performance, если не сказано иное. Между перезапусками FPS гуляет на ±2–5, поэтому
|
||||
решения о каждой оптимизации принимались по A/B в одном процессе (`ab.sh` и биты
|
||||
`BB_TOGGLE_FILE`).
|
||||
|
||||
## Итог в цифрах
|
||||
|
||||
| Что | Было | Стало |
|
||||
|---|---|---|
|
||||
| FPS на входе в локацию | ~86 | ~145–150 |
|
||||
| Время GPU на кадр, FSR 4 Ultra Performance | 5.11 мс | 4.82 мс |
|
||||
| Разброс времени GPU между пресетами | ~0.5 мс | ~0.9 мс |
|
||||
| Падение через 13–17 минут игры | в 7 из 7 прогонов | 0 из 2 прогонов по 30 минут |
|
||||
| Разрешение вывода | только 1080p | 1080p / 1440p / 2160p |
|
||||
| Запуск | `run.sh` из терминала | GTK4-лаунчер и AppImage |
|
||||
|
||||
## 1. Двухстадийный конвейер обработки GPU-команд
|
||||
|
||||
**Проблема.** Поток GPU-команд (`shadPS4:GpuComm`) делал всё сам: разбирал PM4, выбирал
|
||||
пайплайны, привязывал ресурсы и записывал draw. Остальные ядра простаивали.
|
||||
|
||||
**Решение** (`vk_draw_pipe.h`, `vk_rasterizer.cpp`, `liverpool.cpp`):
|
||||
- **Stage A** (`GpuComm`) разбирает PM4, ведёт регистры, выбирает пайплайн, копирует мелкие
|
||||
константы в кольцевой буфер и кладёт пакет в очередь.
|
||||
- **Stage B** (`bb:DrawRec`) держит копию регистров (по 32-словным блокам изменений), привязывает
|
||||
ресурсы и записывает draw и dispatch.
|
||||
- Между стадиями работает очередь без блокировок на 16 МБ.
|
||||
- Операции с памятью (EOP/EOS, WriteData, DmaData, сигнал flip) выполняются на stage B строго
|
||||
по порядку с draw.
|
||||
- Дренажи (stage A ждёт, пока B опустеет) остались только там, где без них нельзя.
|
||||
|
||||
Включается автоматически при 8+ потоках (`BB_DRAW_PIPE=0/1`). На 16 потоках: 110.9 против
|
||||
83.9 FPS (+32%); на 8 потоках (как Steam Deck): 83.8 против 71.0.
|
||||
|
||||
**Дальнейшие оптимизации:**
|
||||
|
||||
| Оптимизация | Тогл-бит | Эффект |
|
||||
|---|---|---|
|
||||
| Кольцо констант на stage A | 36 | +11% |
|
||||
| Memo состояния рендер-прохода (90% draw продолжают открытый проход) | 44 | +9% |
|
||||
| Memo наборов текстур по хешам T# (32768 слотов) | 45 | +5.6%, затем ещё +1.3% |
|
||||
| Конвейерные indirect-draw и indirect-dispatch | 46 | +1.4% |
|
||||
| Дешёвая проверка отложенных GPU-записей (слияние диапазонов, редкая чистка) | — | ~+4% |
|
||||
| Copy-shader HLE одним compute-диспатчем, слияние в пределах 64 КБ | 43 | GPU: 2.1 → 0.6 мс |
|
||||
| Таблицы истории движения без аллокаций | — | без изменения FPS |
|
||||
|
||||
Что выяснилось:
|
||||
- Узкое место теперь stage B: она занята ~85% времени, около 2.8 мкс на пакет, профиль плоский.
|
||||
- Stage A ждёт ~25% времени — на первом дренаже после длинной серии draw. В статистике кадров
|
||||
теперь видно ожидание по каждому месту дренажа.
|
||||
- Векторы движения объектов стоят около 10% FPS.
|
||||
- Не дали выигрыша и откачены:
|
||||
- ослабление барьера при передаче пакетов (+0.4%, риск пропущенного пробуждения);
|
||||
- memo динамического состояния: context-регистры меняются почти на каждом draw.
|
||||
- PGO-профиль пересобран под новый код.
|
||||
|
||||
## 2. Почему пресеты апскейлера мало меняли нагрузку на GPU
|
||||
|
||||
Выяснилось, что на RX 7800 XT отрисовка самой сцены стоит дёшево. Нативный 1080p без
|
||||
апскейлера — 3.67 мс GPU, а сам FSR 4 при выводе 1080p — около 1.4 мс. Поэтому FSR 4 Ultra
|
||||
Performance нагружает GPU сильнее, чем нативный рендер. При ~145 FPS упор в CPU, и GPU около
|
||||
четверти времени простаивает.
|
||||
|
||||
Найдены и исправлены две реальные проблемы:
|
||||
1. **Проходы освещения всегда рисовались в 1920×1080.** Проверка «можно ли рисовать проход в
|
||||
уменьшенном разрешении» смотрела на все восемь слотов целей, а не только на цели самого
|
||||
прохода. В лишних слотах лежали G-buffer-текстуры, которые проход читает. Тогл 47.
|
||||
2. **Лишний ресэмплинг.** Каждый проход, читавший уменьшенную цель, сначала растягивал её до
|
||||
1080p — около 25 раз за кадр, 0.4 мс GPU. Теперь рекомпилятор шейдеров помечает образы,
|
||||
которые читаются только нормализованным сэмплированием, и такие проходы читают уменьшенную
|
||||
копию напрямую. Тогл 48. Из-за изменения формата метаданных кэш шейдеров один раз
|
||||
пересобирается при первом запуске.
|
||||
|
||||
Время GPU по пресетам после исправлений (один процесс, `BB_PRESET_FILE`):
|
||||
|
||||
| Пресет | GPU, мс/кадр |
|
||||
|---|---|
|
||||
| Native AA | 5.72 |
|
||||
| Quality | 5.35 |
|
||||
| Balanced | 5.21 |
|
||||
| Performance | 4.99 |
|
||||
| Ultra Performance | 4.82 |
|
||||
|
||||
Следующий кандидат для Steam Deck — гостевая компьют-копия `3d5ebf4e`. Она копирует память
|
||||
целей рендеринга (глубина 1080p — 12 МБ) и тянет за собой выгрузки и загрузки образов. Её
|
||||
замена на копирование между образами описана в parallel_gpu.md.
|
||||
|
||||
## 3. Падение через 13–17 минут (испорченная гостевая куча)
|
||||
|
||||
**Симптом.** `Guest fault … at guest offset 0x263b8e7, address 0x5300000000`: в free-list
|
||||
гостевого аллокатора лежал испорченный указатель. Падало только с конвейером.
|
||||
|
||||
**Как искали.**
|
||||
- Стресс-тесты по 15–40 минут (`tools/soak.sh`) и бисекция тоглами конвейера.
|
||||
- Журнал записей в гостевую память `BB_WRITE_LOG=1` гонку маскировал: даже наносекунды
|
||||
задержки на записи меток её прятали.
|
||||
- Помог режим `BB_WRITE_LOG=2`: он записывает адреса меток в момент разбора команд, то есть вне
|
||||
гонки. Дамп при падении показал, что метки конца кадра лежат внутри освобождённых
|
||||
48-байтовых объектов кучи.
|
||||
|
||||
**Причина.** Stage A посылал прерывание «GPU простаивает», как только разобрал все командные
|
||||
буферы. По нему `sceGnmSubmitDone` отпускает игру, и та считает работу кадра завершённой. Но
|
||||
метки конца кадра (EOP) в этот момент ещё ждали в потоке записи Vulkan (режим
|
||||
RecorderFences). Игра освобождала объекты с этими метками, а другой её поток продолжал их
|
||||
обновлять.
|
||||
|
||||
**Исправление.** Перед этим прерыванием stage A дренирует конвейер и ждёт все отложенные
|
||||
метки. То же сделано перед WriteData/ReleaseMem вычислительных очередей и перед редкими
|
||||
неконвейерными командами графики. Результат: до исправления — 7 падений из 7 прогонов, после —
|
||||
2 прогона по 30 минут без падений, FPS не изменился.
|
||||
|
||||
## 4. Разрешение вывода 1440p/2160p и эффекты игры
|
||||
|
||||
Новый раздел в меню игры (Insert или L3+R3) — «Разрешение и эффекты (после перезапуска)».
|
||||
Кнопка «Применить и перезапустить игру» перезапускает игру через run.sh.
|
||||
|
||||
**Разрешение вывода** (`bbport.ini`: `output_res=2560x1440` или `3840x2160`):
|
||||
- Игра патчем рисует сцену в разрешении пресета от выхода. Например, 4K Performance — это
|
||||
1916×1078: ровно 1920×1080 нельзя, такой размер порт принимает за пространство UI.
|
||||
- FSR 3 или FSR 4 дорисовывает кадр до выходного размера. FSR 4 добавлен в этот режим: его
|
||||
HDR-выход копируется в образ UI.
|
||||
- Интерфейс остаётся в координатах 1920×1080 игры, а порт растеризует его сразу в выходном
|
||||
разрешении, поэтому он чёткий. Первая попытка патчить UI до размера вывода растягивала его в
|
||||
1.5 раза — исправлено.
|
||||
- Для сцены больше 1080p добавляется патч кучи сообщества и увеличивается бюджет direct memory
|
||||
(`BB_DMEM_MB`, по умолчанию 5056 МБ, run.sh поднимает до 9152).
|
||||
- Проверено:
|
||||
- 1440p FSR 3 и FSR 4 Quality — около 150 FPS;
|
||||
- 4K FSR 4 Performance — около 230 FPS.
|
||||
|
||||
HUD в обоих случаях на месте.
|
||||
- Для FSR 4 при выводе 1440p и 2160p нужны модели уровня «2160». `fetch_fsr4_assets.sh`
|
||||
теперь скачивает их по умолчанию.
|
||||
|
||||
**Эффекты.** Включаются и выключаются патчами сообщества из `patches/Bloodborne.xml`:
|
||||
- хроматическая аберрация;
|
||||
- глубина резкости (DoF);
|
||||
- размытие в движении;
|
||||
- SSAO;
|
||||
- собственное сглаживание игры;
|
||||
- тени от динамических источников;
|
||||
- отражения SSR (по умолчанию выключены);
|
||||
- пропуск заставок;
|
||||
- детализация моделей (LOD -2…2).
|
||||
|
||||
Ключи в bbport.ini: `effect_*`, `skip_intro`, `model_lod`.
|
||||
|
||||
## 5. Лаунчер (GTK4 / libadwaita)
|
||||
|
||||
Запуск: `launcher/bb-launcher.sh` (при отсутствии PyGObject/GTK4 поднимается через
|
||||
`nix-shell launcher/shell.nix`).
|
||||
|
||||
Что в нём есть:
|
||||
- **Игра:** путь к папке игры (проверяется наличие `eboot.bin`), папка сохранений, язык системы.
|
||||
- **Экран:** разрешение вывода, полный экран, режим показа кадров, HDR.
|
||||
- **Апскейлер:** выбор, пресет, резкость, векторы движения объектов, счётчик FPS — это те же
|
||||
ключи bbport.ini, что и в меню игры.
|
||||
- **Эффекты игры** и детализация моделей.
|
||||
- **Частота кадров:** патч (uncap/60/90/30) и ограничение FPS.
|
||||
- **Производительность:** конвейер GPU (авто/вкл/выкл), режим readbacks.
|
||||
- **Для разработчика:** MangoHud, статистика кадров, профиль GPU, валидация Vulkan,
|
||||
произвольные переменные окружения.
|
||||
- Вкладка «Журнал» с выводом игры, кнопки «Запустить» и «Остановить» (игра и её помощники
|
||||
останавливаются как одна группа процессов).
|
||||
- `--play` запускает игру с сохранёнными настройками без окна — для игрового режима Steam Deck.
|
||||
|
||||
Настройки самого лаунчера хранятся в `~/.config/bbport-launcher/settings.json`.
|
||||
|
||||
## 6. AppImage для Steam Deck
|
||||
|
||||
Сборка: `bash build.sh && bash packaging/appimage.sh`. Результат —
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`, около 700 МБ.
|
||||
|
||||
Как устроено:
|
||||
- `packaging/default.nix` собирает пакет: готовые бинарники, скрипты подготовки, патчи, модели
|
||||
FSR 4, лаунчер с Python, GTK4, libadwaita, темами значков и Vulkan-драйверами Mesa.
|
||||
- Бандлер nix-appimage монтирует внутренний `/nix/store` через user namespaces — они есть в
|
||||
SteamOS. Поэтому библиотеки SteamOS не конфликтуют с glibc пакета.
|
||||
- Драйвер хоста в процесс игры не загрузить, поэтому используется radv из пакета
|
||||
(`VK_DRIVER_FILES`).
|
||||
- Данные хранятся в `~/.local/share/bbport`: сгенерированные файлы, `user/` (сохранения и кэш
|
||||
шейдеров) и `bbport.ini`. Сохранения отдельные от `native_probe/user`, их нужно скопировать.
|
||||
- run.sh получил режимы `BB_PREBUILT=1` (готовые бинарники, без сборки и nix-shell),
|
||||
`BB_DATA_DIR` (каталог данных) и `BB_GAME_DIR` (папка игры).
|
||||
|
||||
Ошибки, найденные при первом запуске на живой системе:
|
||||
1. «no version information available / undefined symbol: , version». patchelf в фазе fixup nix
|
||||
испортил версии символов не-PIE бинарника игры. Теперь бинарник ищет свою библиотеку через
|
||||
`$ORIGIN/gpu`, а patchelf к нему не применяется.
|
||||
2. «libSDL3.so.0: cannot open». Nix находит зависимости только среди входов деривации, а
|
||||
бинарники собраны снаружи. Теперь `appimage.sh` выписывает store-пути из RUNPATH в
|
||||
`packaging/runtime-paths.nix`, и пакет берёт их как входы.
|
||||
3. «FSR 4 недоступен: missing …_2160_pre.spv»: не хватало моделей уровня «2160» (см. раздел 4).
|
||||
|
||||
После этих исправлений игра из AppImage запускается (`--play`), FSR 4 при выводе 4K работает.
|
||||
|
||||
## 7. Инструменты для разработки
|
||||
|
||||
| Инструмент | Назначение |
|
||||
|---|---|
|
||||
| `tools/restart.sh` | перезапуск игры со статистикой и вход в локацию (крест, крест) |
|
||||
| `tools/press.sh` | ввод геймпада из скрипта через `BB_PAD_FILE` |
|
||||
| `tools/soak.sh <сек>` | стресс-тест с поворотами камеры, сообщает о падении |
|
||||
| `ab.sh` | A/B бита `BB_TOGGLE_FILE` в запущенной игре |
|
||||
| `BB_GPU_PROFILE=1` | время GPU по проходам, диспатчам, апскейлеру, загрузкам и выгрузкам образов, ресэмплингу прокси |
|
||||
| `BB_FRAME_STATS=1` | статистика кадров: ожидание stage A по причинам и местам дренажа, memo, чтение прокси |
|
||||
| `BB_SCENE_DEBUG=<файл>` | после создания файла печатает, почему проходы кадра остались в полном разрешении |
|
||||
| `BB_PRESET_FILE=<файл>` | смена пресета из файла (раз в секунду) |
|
||||
| `BB_WRITE_LOG=1/2` | журнал записей эмуляции GPU в гостевую память и дамп при гостевом падении |
|
||||
| `BB_DMEM_MB` | бюджет direct memory |
|
||||
|
||||
Биты `BB_TOGGLE_FILE` (выключают оптимизацию при установке бита):
|
||||
|
||||
| Бит | Что выключает | Бит | Что выключает |
|
||||
|---|---|---|---|
|
||||
| 36 | кольцо констант | 43 | multi-copy шейдер |
|
||||
| 37 | конвейер целиком | 44 | memo состояния прохода |
|
||||
| 38 | задачи в конвейере | 45 | memo наборов текстур |
|
||||
| 39 | ожидание отложенных меток | 46 | конвейерные indirect |
|
||||
| 40 | конвейерные dispatch | 47 | только свои слоты целей |
|
||||
| 41 | метки через поток записи | 48 | чтение уменьшенных копий |
|
||||
| 42 | записи в память на stage B | 49 | порядок гостевых записей |
|
||||
|
||||
## 8. Что дальше
|
||||
|
||||
- **Stage B — узкое место CPU.** Дальнейший рост требует разделить её работу: кэши текстур и
|
||||
буферов против подготовки дескрипторов и записи.
|
||||
- **Steam Deck (GPU):** HLE компьют-копии `3d5ebf4e` как копирования между образами;
|
||||
проходы в половинном разрешении (960×540) тоже уменьшать.
|
||||
- **Масштабируемый режим (1440p/2160p):** пресет меняется только с перезапуском; объединить его
|
||||
с живым режимом уменьшенных целей.
|
||||
- Проверить AppImage на самом Steam Deck: radv из пакета на Van Gogh, звук через PipeWire хоста,
|
||||
игровой режим с `--play`.
|
||||
@@ -1,10 +0,0 @@
|
||||
Первая версия векторов движения объектов собрана, игра запущена, ошибок при старте нет. Но в самой игре я её ещё не проверял: шейдеры G-buffer с новым кодом компилируются только при загрузке уровня, так что главная проверка впереди.
|
||||
|
||||
Как это работает:
|
||||
- Вершинный шейдер каждой отрисовки в G-buffer сохраняет итоговые позиции вершин и читает позиции тех же вершин из прошлого кадра.
|
||||
- Пиксельный шейдер пишет разницу в дополнительную цель G-buffer.
|
||||
- Где объект двигался, FSR получает этот вектор, в остальных местах — вектор камеры.
|
||||
- Позиции сохраняются только для движущихся отрисовок: тех, у которых изменились константы вершинного шейдера (матрица объекта, кости). Статичная геометрия памяти не тратит.
|
||||
- Это учитывает любую анимацию, которую считает вершинный шейдер: скиннинг, оружие, ткань, врагов.
|
||||
|
||||
Загрузись, пожалуйста, в игру и напиши «готов». Я проверю лог: сколько отрисовок движется и у скольких нашлись позиции прошлого кадра. Потом побегай с пилой на спине. Для сравнения я смогу выключать векторы объектов на ходу, остаются только векторы камеры.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
template: home.html
|
||||
title: Home
|
||||
hide:
|
||||
- navigation
|
||||
- toc
|
||||
---
|
||||
|
||||
# Where to begin
|
||||
|
||||
The PlayStation 4 version of *Bloodborne*, running natively on Linux and the Steam Deck: the
|
||||
game's own code runs on the CPU and its graphics are translated to Vulkan. You need your own dump
|
||||
of the game with update 1.09; no game files are included.
|
||||
|
||||
<div class="hd-cards">
|
||||
<a class="hd-card" href="getting-started/"><strong>Getting started</strong><span>Your dump, the AppImage and the first launch.</span></a>
|
||||
<a class="hd-card" href="ONLINE/"><strong>Playing online</strong><span>Seamless Hunt, The Hunter's Dream or your own server, and seamless co-op.</span></a>
|
||||
<a class="hd-card" href="steam-deck/"><strong>Steam and the Steam Deck</strong><span>Add to Steam, Game Mode, controllers and the Steam overlay.</span></a>
|
||||
<a class="hd-card" href="TROUBLESHOOTING/"><strong>When something breaks</strong><span>Common problems, and how to send a log.</span></a>
|
||||
</div>
|
||||
|
||||
## What you get
|
||||
|
||||
- **Online and co-op**: messages, bloodstains, bells and summons, and **seamless co-op** that
|
||||
lasts through deaths, lamps and areas, with Linux and Windows players together.
|
||||
- **Local co-op**: 2 to 4 players on one PC, each with their own controller.
|
||||
- **Hunter's Atlas**: a fullscreen in-game map with fast travel to the lamps you have found.
|
||||
- **Frame rate unlocked** or capped at 30, 60 or 90, from **720p to 4K**, and ultrawide screens.
|
||||
- **FSR 3.1, FSR 4 and DLSS** upscaling.
|
||||
- **Any controller**, with its own button prompts in the game, and **keyboard and mouse**.
|
||||
- **Trophies**, **Discord** status and **Add to Steam** in one click.
|
||||
- An **AppImage** for any distribution and the Steam Deck, which **updates itself**.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Getting a log
|
||||
|
||||
A log is the first thing needed to fix a problem: it says what the game and the GPU were doing.
|
||||
|
||||
## From the launcher
|
||||
|
||||
1. Turn on *Advanced → Developer → Save the log and statistics to a file*. It stays on.
|
||||
2. Play until the problem happens, then close the game.
|
||||
3. The log is in `~/.local/share/bbport/logs`, named after the time the game started
|
||||
(`20261026_183438.log`). The launcher's **Log** page shows the last one too.
|
||||
|
||||
This also works when the game is started from Steam or from the Steam Deck's Game Mode: turn the
|
||||
option on once in the launcher, and every session saves its log.
|
||||
|
||||
The log's first line says which version ran, and for a test build which one
|
||||
(`bbport Linux 0.51-linux.18, build 626ec908 fix/issue1-lamp-blur 2026-10-29`).
|
||||
|
||||
## Sending it
|
||||
|
||||
Open an [issue](https://github.com/ayozetr/bloodborne_linux/issues/new/choose) and attach the
|
||||
`.log` file. It helps a lot to say:
|
||||
|
||||
- what happened, and roughly when (an area, a boss, a menu, after a lamp travel);
|
||||
- your distribution, GPU and driver;
|
||||
- the upscaler, preset and output resolution;
|
||||
- for online problems, which server and whether seamless co-op (BBSC) was on, and the other
|
||||
player's log too if you can.
|
||||
|
||||
!!! note "Crashes"
|
||||
*Advanced → Developer → Crash diagnostics* adds checks that point at what went wrong in some
|
||||
crashes. The game runs a bit slower with it on: turn it on when asked in an issue.
|
||||
@@ -1,131 +0,0 @@
|
||||
# Векторы движения: доработка после видео 2026-09-26
|
||||
|
||||
Исходный материал: `video_2026-09-26_04-19-53.mp4`, FSR 3.1 Performance,
|
||||
рендер 960×540 → вывод 1920×1080, и `out/dev/t71.log`.
|
||||
Это описание текущих изменений рабочего дерева, а не подтверждение качества в игре.
|
||||
|
||||
Общий проход для всех G-buffer draws оказался слишком дорогим: Performance работал примерно
|
||||
при 49 FPS с ним и 77–78 FPS без него. Теперь векторы объектов включены по умолчанию только
|
||||
для шейдеров с буфером позы/костей (размер 640–16384 байт, кроме общего буфера сцены
|
||||
864 байта; шаг 16 байт). В пробном запуске с более узким порогом 1024 байта
|
||||
это сократило число обработанных draws примерно до 140 за кадр, устранило переполнение
|
||||
истории и дало около 81–87 FPS. Остальные draws сохраняют дешёвые векторы камеры.
|
||||
`BB_OBJECT_MOTION=0` выключает всю функцию, `BB_OBJECT_MOTION_ALL=1` возвращает общий
|
||||
проход для A/B. Альфа-канал вспомогательной цели хранит глубину: если поздний обычный
|
||||
draw перекрыл объект, его вектор не используется. Качество персонажа и оружия в движении
|
||||
после выборки ещё нужно проверить визуально.
|
||||
|
||||
## Что обнаружено
|
||||
|
||||
- В конце исходного лога за 600 кадров: 257400 G-buffer draws, 256200 объявлены
|
||||
движущимися, только 30000 получили предыдущие позиции (~11,7% всех draws).
|
||||
Это покрытие отрисовок, не процент пикселей. Причина каждого пропуска старым
|
||||
счётчиком не различалась.
|
||||
- Под историю резервировался размер целого vertex buffer, умноженный на число
|
||||
экземпляров **вместе с firstInstance**. Для подмешей общих буферов это лишние
|
||||
позиции; бюджет составлял 2097152 позиции на кадр.
|
||||
- История не имела отдельного барьера между записями/чтениями vertex shaders;
|
||||
переиспользование кольца host-параметров не ожидало GPU.
|
||||
- Выключенные записи всех вершин направлялись в один scratch-элемент.
|
||||
- `CameraMotion::OnDisplayPass` сбрасывал Depth() перед расчётом jitter phases:
|
||||
Performance получал 8 фаз вместо 32. В восстановлении позиции из глубины
|
||||
также не учитывался сдвиг viewport.
|
||||
|
||||
## Изменения
|
||||
|
||||
- Диапазон истории определяется min/max фактических индексов с учётом baseVertex
|
||||
и primitive restart. FirstInstance вычитается в шейдере, а не расходует память.
|
||||
- Сохраняются все подходящие прямые G-buffer draws, начиная с первого появления.
|
||||
Эвристика изменения констант удалена: она считала движущимися почти все draws
|
||||
и пропускала начало движения. Чтение возможно только при совпадении с прошлым кадром.
|
||||
- Ключ включает все описания vertex streams, шейдер, адрес и хеш содержимого
|
||||
индексов, диапазон вершин, экземпляры и номер среди одинаковых draws.
|
||||
Смена топологии или пропуск кадра сбрасывает соответствующую историю.
|
||||
- Бюджет — 4194304 позиции/кадр; два массива вместе занимают 128 МиБ вместо 64 МиБ.
|
||||
При нехватке памяти остаются векторы камеры. Лог отдельно показывает сохранения,
|
||||
совпадения, отсутствие истории, нехватку памяти и недопустимые диапазоны.
|
||||
- Добавлены vertex-to-vertex barriers, ожидание GPU перед повторным использованием
|
||||
host-параметров, coherent host memory и независимый учёт layout изображения движения.
|
||||
Повторное чтение изображения не стирает признак наличия векторов.
|
||||
- Выключенные обращения обходятся ветвлением. Для повторных invocation одной
|
||||
индексированной вершины используются атомарные записи компонентов позиции.
|
||||
Позиции за камерой и нечисловые/бесконечные векторы не используются.
|
||||
- Performance получает 32 фазы jitter после создания FSR context. Векторы камеры
|
||||
восстанавливаются из unjittered координат; векторы объектов уже получались
|
||||
из clip positions до сдвига viewport.
|
||||
|
||||
## Проверки
|
||||
|
||||
Сборка и CPU-тесты:
|
||||
|
||||
```bash
|
||||
cd native_probe
|
||||
nix-shell shell.nix --run 'bash build.sh && ninja -C out/gpu motion-history-test motion-shader-test && out/gpu/motion-history-test && out/gpu/motion-shader-test out/motion-shaders'
|
||||
```
|
||||
|
||||
`test_motion_history.cpp` проверяет диапазоны u16/u32, restart, отрицательный baseVertex,
|
||||
firstInstance, повторные draws, смену топологии, пропущенный кадр, исчерпание бюджета,
|
||||
переполнение и число фаз. Также пройден запуск с AddressSanitizer/UBSan.
|
||||
|
||||
`test_motion_shaders.cpp` использует настоящий SPIR-V backend и создаёт VS/PS с
|
||||
векторами и без них. Все четыре результата прошли `spirv-val --target-env vulkan1.3`.
|
||||
Для повторения нужен `spirv-val` из SPIRV-Tools:
|
||||
|
||||
```bash
|
||||
for shader in out/motion-shaders/*.spv; do
|
||||
spirv-val --target-env vulkan1.3 "$shader" || exit
|
||||
done
|
||||
```
|
||||
|
||||
Логи сборки/тестов: `out/dev/motion-validation.log`. Доступа к `/dev/dri` в среде
|
||||
проверки нет: качество на RX 7800 XT, FPS и процент покрытия после изменений
|
||||
ещё не проверены. Успешная валидация SPIR-V не доказывает правильность картинки.
|
||||
|
||||
## Следующий запуск в игре
|
||||
|
||||
Из `game_files`:
|
||||
|
||||
```bash
|
||||
BB_FRAME_STATS=1 BB_TOGGLE_FILE=out/dev/motion-toggles bash native_probe/run.sh 2>&1 | tee native_probe/out/dev/motion-run.log
|
||||
```
|
||||
|
||||
Бит 29 во временном toggle-файле выключает только запись истории. Для корректного
|
||||
сравнения производительности надо запускать игру с `BB_OBJECT_MOTION=0` и без него:
|
||||
выбор пайплайна и дополнительная цель задаются при старте процесса.
|
||||
После переключения дать FSR несколько кадров на обновление истории.
|
||||
Сравнивать одну сцену: персонаж/оружие при беге, поворот камеры, бой, неподвижная камера.
|
||||
Через ~600 кадров проверить `Object motion:`: `with history`, `capacity skips`, `unmatched`.
|
||||
|
||||
Оставшиеся ограничения: одинаковые меши сопоставляются по порядку draws, который
|
||||
может меняться при отсечении объектов; смена адресов динамических vertex buffers
|
||||
теряет совпадение. Indirect draws, tessellation/geometry shaders, MSAA и проходы
|
||||
вне G-buffer остаются на векторах камеры. Прозрачность/частицы, мыльный UI и перенос
|
||||
масштабирования на уровень рендерера этим изменением не решены. Стоимость сканирования
|
||||
индексов и атомарных записей нужно измерить на реальном запуске.
|
||||
|
||||
## 29 сентября: оружие и маленькие скелеты (гостинг пилы-вертушки)
|
||||
|
||||
Видео `video_2026-09-29_23-08-26.mp4`: персонаж стоит, диск пилы вращается и оставляет
|
||||
шлейф. Лог `BB_MOTION_SELECT_LOG=1` показал причину: второй буфер вершинного шейдера —
|
||||
палитра костей по 48 байт (3x4), и у оружия/реквизита она 96–384 байта (2–8 костей).
|
||||
Правило «от 640 байт» такие отрисовки не включало, диск получал только векторы камеры,
|
||||
то есть для FSR был неподвижным.
|
||||
|
||||
Изменения:
|
||||
|
||||
- `Motion::ClassifyBuffer` (`motion_history.h`): 864 — сцена, ≥640 — скелет персонажа,
|
||||
96..639 кратно 48 — маленький скелет. Пайплайны с любым скелетом получают вариант
|
||||
с векторами.
|
||||
- Маленьких скелетов около тысячи на кадр, большинство — статичные части карты.
|
||||
Поэтому они «гейтятся»: хеш палитры сравнивается с прошлым кадром по дешёвому
|
||||
ключу (`History::Moving`) до сканирования индексов. Совпала — вектор камеры точен,
|
||||
ничего не пишется. Изменилась — позиции сохраняются, со следующего кадра вектор
|
||||
объекта. Хеш с буфером 416 байт не подходит: он меняется вместе с камерой почти у всех
|
||||
(проверено: переполнение истории и падение FPS до ~40).
|
||||
- Меню: «Показать векторы движения (отладка)». Красный/зелёный — |x|/|y|, синий —
|
||||
пиксель получил вектор объекта. `BB_MOTION_SELECT_LOG=1` печатает шейдеры G-buffer
|
||||
с размерами буферов и решением.
|
||||
|
||||
Замер `out/motion-gated.log`: ~225 сохранённых отрисовок и ~600 «неподвижных» на кадр,
|
||||
без переполнения, 58–80 FPS в игровых сценах. Визуально исчезновение шлейфа
|
||||
на пиле ещё нужно подтвердить.
|
||||
@@ -268,10 +268,10 @@ copies queued in the recording stream, i.e. until the recording thread has recor
|
||||
command before them, plus ~4% for the copy threads (`BbCopy::WaitAsync`). The recording thread
|
||||
itself works ~40% (the rest is its spin); it is not the limit.
|
||||
|
||||
### Texture binding on a helper thread (opt-in: `BB_TEXTURE_HELPER=1`, toggle 1 << 22)
|
||||
### Texture binding on a helper thread (opt-in: `BB_TEXTURE_HELPER=1`, toggle 1 << 34)
|
||||
|
||||
`bb:TexBind` binds a draw's textures while the GPU thread binds its buffers and resolves its
|
||||
vertex/index buffers (split into Resolve/Emit, toggle 1 << 23); the GPU thread joins before
|
||||
vertex/index buffers (split into Resolve/Emit, toggle 1 << 35); the GPU thread joins before
|
||||
`BeginRendering`. The helper takes only the memoized path (FindImage memo valid, image clean,
|
||||
no scene-target proxy, no upscaler redirect, no storage images, no mip arrays, no storage
|
||||
buffers in the pipeline); anything else is bound after the join. The GPU thread joins before
|
||||
@@ -291,7 +291,7 @@ not pay on this CPU; the helper stays opt-in for other CPUs.
|
||||
|
||||
| change | A/B | kept |
|
||||
|---|---|---|
|
||||
| FindView memo in the image description cache (1 << 21) + write prefetch in record chunks (1 << 20) | 81.5 vs 80.8 FPS | yes |
|
||||
| FindView memo in the image description cache (1 << 33) + write prefetch in record chunks (1 << 32) | 81.5 vs 80.8 FPS | yes |
|
||||
| format check by table instead of `magic_enum::enum_contains` (a linear scan per texture per draw); hot `Image` fields (flags, binding, tracking range, backing, ticks) moved in front of `ImageInfo` | within noise | yes (no downside) |
|
||||
| host copy queue: small copies in a lock-free MPMC queue, run by the idle recording thread, the rest by the GPU thread at the wait | blocked 9.5% → 4.8%, but 86.5 vs 87.2 FPS: the copying moved onto the GPU thread | no |
|
||||
| no barrier tracking for read-only stream buffer ranges | 85.9 vs 87.4 FPS, both phase orders (cause not found) | no |
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Steam and the Steam Deck
|
||||
|
||||
## Add to Steam
|
||||
|
||||
**Setup → Game → Add to Steam** puts *Bloodborne* in your Steam library as a non-Steam game. It
|
||||
starts the game straight away, with the launcher's settings, without opening the launcher. Steam
|
||||
has to close for it, and the launcher offers to do that.
|
||||
|
||||
After updating from an older release, press **Add to Steam** once more so the shortcut points to
|
||||
the right place.
|
||||
|
||||
## On the Steam Deck
|
||||
|
||||
1. In Desktop Mode, download the AppImage and set the game up in the launcher, as in
|
||||
[Getting started](getting-started.md). A resolution of **720p** with an upscaler (FSR) suits the
|
||||
Deck's screen.
|
||||
2. Press **Add to Steam**.
|
||||
3. Back in Game Mode, start *Bloodborne* from your library.
|
||||
|
||||
To change a setting later, go back to Desktop Mode and open the launcher: what you change there is
|
||||
what Game Mode uses.
|
||||
|
||||
!!! tip "Logs in Game Mode"
|
||||
Turn on *Advanced → Developer → Save the log and statistics to a file* once in Desktop Mode:
|
||||
every session played from Game Mode then saves its log too. [Getting a log](logs.md).
|
||||
|
||||
## Controllers and Steam Input
|
||||
|
||||
With Steam Input on, the game gets Steam's virtual controller. On a Wayland desktop the game then
|
||||
opens its window through Xwayland, so Steam's input reaches it. With Steam Input off, the game reads
|
||||
the controller itself. [More](TROUBLESHOOTING.md#the-controller-does-nothing-when-the-game-is-started-from-steam).
|
||||
|
||||
## The Steam overlay
|
||||
|
||||
Shift+Tab, Steam's FPS counter and F12 screenshots need **Steam overlay**, under *Add to Steam* in
|
||||
the launcher (off by default), and the game started from Steam.
|
||||
[More](TROUBLESHOOTING.md#the-steam-overlay-does-not-open-in-the-game).
|
||||
@@ -0,0 +1,148 @@
|
||||
/* The players' guide: "Hunter's Dream" theme over MkDocs Material's dark scheme. Moonlit
|
||||
blue-grey; IM Fell English for headings and menus, EB Garamond for reading (IM Fell has only
|
||||
old-style figures: 1 and 0 read as ı and o in versions and values). No artwork or typeface from
|
||||
the game. */
|
||||
@import url("https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=IM+Fell+English:ital@0;1&family=IM+Fell+English+SC&family=JetBrains+Mono:wght@400&display=swap");
|
||||
|
||||
[data-md-color-scheme="slate"] {
|
||||
--hd-bg: #0f1218;
|
||||
--hd-bg-raised: #12161e;
|
||||
--hd-line: #2a3140;
|
||||
--hd-text: #d4d9e2;
|
||||
--hd-muted: #a9b3c4;
|
||||
--hd-bright: #f2f4f8;
|
||||
--hd-moon: #e4eaf5;
|
||||
--hd-blood: #e0a0a0;
|
||||
|
||||
--md-default-bg-color: var(--hd-bg);
|
||||
--md-default-fg-color: var(--hd-text);
|
||||
--md-default-fg-color--light: var(--hd-muted);
|
||||
--md-default-fg-color--lighter: #6f7a8c;
|
||||
--md-default-fg-color--lightest: var(--hd-line);
|
||||
--md-primary-fg-color: #141924;
|
||||
--md-primary-fg-color--dark: #1a2030;
|
||||
--md-primary-fg-color--light: #2a3140;
|
||||
--md-primary-bg-color: var(--hd-bright);
|
||||
--md-accent-fg-color: #ffffff;
|
||||
--md-typeset-a-color: #b9c8e6;
|
||||
--md-code-bg-color: #0a0d12;
|
||||
--md-code-fg-color: #c8d3e6;
|
||||
--md-footer-bg-color: #0b0d12;
|
||||
--md-footer-bg-color--dark: #08090d;
|
||||
--md-text-font-family: "EB Garamond", Georgia, serif;
|
||||
--md-code-font-family: "JetBrains Mono", monospace;
|
||||
}
|
||||
|
||||
body, input { font-family: "EB Garamond", Georgia, serif; font-variant-numeric: lining-nums; }
|
||||
.md-typeset { font-size: .92rem; line-height: 1.65; }
|
||||
.md-typeset code, .md-typeset pre { font-family: "JetBrains Mono", monospace; }
|
||||
|
||||
/* Headings, the header and the navigation in the small-capitals cut. */
|
||||
.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4,
|
||||
.md-header__title, .md-nav__title, .md-tabs__link, .md-nav__item--section > .md-nav__link {
|
||||
font-family: "IM Fell English SC", Georgia, serif;
|
||||
font-weight: 400;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.md-typeset h1 { color: var(--hd-bright); font-family: "IM Fell English", Georgia, serif; }
|
||||
.md-typeset h2 { color: var(--hd-bright); border-bottom: 1px solid var(--hd-line); padding-bottom: .2em; }
|
||||
.md-typeset h3 { color: #c4cfe2; }
|
||||
|
||||
.md-header { background: rgba(15, 18, 24, .85); backdrop-filter: blur(6px); border-bottom: 1px solid var(--hd-line); box-shadow: none; }
|
||||
.md-sidebar--primary .md-nav__link--active { color: #fff; }
|
||||
/* The phone drawer: its repository strip and title in the theme's colours, not Material's blue. */
|
||||
.md-nav__source { background-color: #1a2030; }
|
||||
.md-nav--primary .md-nav__title[for="__drawer"] { background-color: #141924; }
|
||||
.md-sidebar--primary .md-nav__link--active::before { content: "✦ "; color: #9fb0d0; }
|
||||
.md-nav__link:hover, .md-typeset a:hover { color: #fff; }
|
||||
|
||||
/* Admonitions: quiet frames instead of Material's bright ones. */
|
||||
.md-typeset .admonition, .md-typeset details {
|
||||
background: rgba(160, 180, 215, .06);
|
||||
border: 1px solid #2f3a4f !important;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
font-size: .85rem;
|
||||
}
|
||||
.md-typeset .admonition-title, .md-typeset summary {
|
||||
background: transparent !important;
|
||||
font-family: "IM Fell English SC", Georgia, serif;
|
||||
color: #c4cfe2;
|
||||
}
|
||||
.md-typeset .admonition-title::before, .md-typeset summary::before { background-color: #9fb0d0 !important; }
|
||||
.md-typeset .warning, .md-typeset .danger { border-color: #5a3a3a !important; background: rgba(170, 80, 80, .08); }
|
||||
.md-typeset .warning > .admonition-title, .md-typeset .danger > .admonition-title { color: var(--hd-blood); }
|
||||
.md-typeset .warning > .admonition-title::before, .md-typeset .danger > .admonition-title::before { background-color: var(--hd-blood) !important; }
|
||||
|
||||
.md-typeset table:not([class]) { border: 1px solid var(--hd-line); font-size: .8rem; }
|
||||
/* Reference tables (ENVIRONMENT.md): names whole, and every column wide enough to read. */
|
||||
.md-typeset table:not([class]) td:first-child code { white-space: nowrap; word-break: normal; }
|
||||
.md-typeset table:not([class]) td:nth-child(2) { min-width: 6rem; }
|
||||
.md-typeset table:not([class]) td:nth-child(3) { min-width: 15rem; }
|
||||
.md-typeset table:not([class]) td:nth-child(4) { min-width: 7rem; }
|
||||
/* Wider pages than Material's 61rem: the reference tables have four columns. */
|
||||
.md-grid { max-width: 90rem; }
|
||||
.md-typeset table:not([class]) td code { word-break: break-word; }
|
||||
.md-typeset table:not([class]) th { background: var(--hd-bg-raised); font-family: "IM Fell English SC", Georgia, serif; font-weight: 400; }
|
||||
|
||||
/* Home page: the moon and the mist (overrides/home.html). */
|
||||
.hd-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
padding: 7rem 1rem 6.5rem;
|
||||
background:
|
||||
radial-gradient(circle at 78% 22%, #eef2f9 0 42px, rgba(220, 228, 244, .35) 43px, rgba(200, 212, 236, .10) 110px, transparent 210px),
|
||||
radial-gradient(ellipse 70% 40% at 50% 100%, rgba(160, 175, 205, .22), transparent 70%),
|
||||
linear-gradient(#1c2335, var(--hd-bg) 75%);
|
||||
}
|
||||
.hd-hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
height: 160px;
|
||||
background: linear-gradient(transparent, rgba(190, 200, 220, .10) 60%, var(--hd-bg));
|
||||
}
|
||||
.hd-hero__content { position: relative; z-index: 1; }
|
||||
.hd-hero h1 { margin: 0; font: 400 clamp(2.4rem, 6vw, 4.4rem) "IM Fell English", Georgia, serif; color: var(--hd-bright); }
|
||||
.hd-hero p { margin: .9rem 0 0; color: #b8c2d3; font-style: italic; font-size: 1.3rem; }
|
||||
.hd-buttons { display: flex; gap: .9rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem; }
|
||||
.hd-button {
|
||||
padding: .75rem 1.75rem;
|
||||
font: 1.1rem "IM Fell English SC", Georgia, serif;
|
||||
color: #e9edf3 !important;
|
||||
border: 1px solid rgba(220, 228, 240, .45);
|
||||
border-radius: 2px;
|
||||
background: rgba(15, 18, 24, .55);
|
||||
transition: background .2s, border-color .2s;
|
||||
}
|
||||
.hd-button:hover { background: rgba(255, 255, 255, .1); border-color: rgba(240, 244, 250, .8); }
|
||||
.hd-button--primary { background: rgba(228, 234, 245, .92); color: #141822 !important; border-color: transparent; }
|
||||
.hd-button--primary:hover { background: #fff; }
|
||||
|
||||
.hd-home { max-width: 54rem; margin: 0 auto; }
|
||||
.hd-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin: 1.2rem 0 2rem; }
|
||||
.hd-card {
|
||||
display: block;
|
||||
border: 1px solid var(--hd-line);
|
||||
background: rgba(160, 180, 215, .05);
|
||||
padding: 1.1rem 1.2rem;
|
||||
border-radius: 4px;
|
||||
color: var(--hd-text) !important;
|
||||
transition: border-color .2s, background .2s;
|
||||
}
|
||||
.hd-card:hover { border-color: #5b6a86; background: rgba(160, 180, 215, .09); }
|
||||
.hd-card strong { display: block; font: 1.25rem "IM Fell English SC", Georgia, serif; font-weight: 400; color: #dfe5ef; margin-bottom: .3rem; }
|
||||
.hd-card span { color: var(--hd-muted); font-size: .85rem; }
|
||||
|
||||
@media screen and (max-width: 44.9375em) {
|
||||
/* A smaller moon in the corner: in the middle it covered the title. */
|
||||
.hd-hero {
|
||||
padding: 4.5rem 1rem 4rem;
|
||||
background:
|
||||
radial-gradient(circle at 86% 11%, #eef2f9 0 22px, rgba(220, 228, 244, .35) 23px, rgba(200, 212, 236, .10) 60px, transparent 120px),
|
||||
radial-gradient(ellipse 70% 40% at 50% 100%, rgba(160, 175, 205, .22), transparent 70%),
|
||||
linear-gradient(#1c2335, var(--hd-bg) 75%);
|
||||
}
|
||||
.hd-cards { grid-template-columns: 1fr; }
|
||||
}
|
||||
+133
-131
@@ -140,162 +140,164 @@ The previous frame's matrices are not there; the port keeps them itself.
|
||||
- Missing: motion of animated objects (characters, cloth, foliage), reactive/transparency
|
||||
masks for particles and fog, render-resolution scaling, frame generation.
|
||||
|
||||
## 2026-10-01: FSR 4 при выводе 1440p/2160p не работал (мерцание и дрожание)
|
||||
## 2026-10-01: FSR 4 did not work at 1440p/2160p output (flicker and shimmer)
|
||||
|
||||
Видео `video_2026-10-01_03-58-59.mp4` (вывод 3840x2160, FSR 4 Performance): мерцание и дрожание
|
||||
всех объектов. Причина — апскейлер в этом режиме вообще не выполнялся, а jitter оставался
|
||||
включённым: на экран шёл растянутый кадр сцены, каждый кадр сдвинутый на свою фазу Halton.
|
||||
Video `video_2026-10-01_03-58-59.mp4` (3840x2160 output, FSR 4 Performance): flicker and shimmer
|
||||
on all objects. Cause: the upscaler did not run at all in this mode, while jitter stayed
|
||||
enabled. The screen showed a stretched scene frame, each frame shifted by its own Halton phase.
|
||||
|
||||
1. Проход UI опознавался по точному размеру цели `BB_RENDER_RES` (1916x1078), а игра выделяет
|
||||
цели с выровненной высотой (1916x1080; в константах сцены тоже 1916x1080). `RunScaled` не
|
||||
вызывался ни разу (в логе не было `UI: native composition`). Теперь допускается выравнивание
|
||||
до 8 пикселей (`RenderTarget`), а размер сцены для FSR берётся из констант сцены
|
||||
(`CameraMotion::RenderSize`, `SceneSize`).
|
||||
2. После этого FSR 4 падал с `external image registration failed (-1000069000)`: `RunScaled`
|
||||
создавал новые image view каждый кадр, а реестр FSR 4 вмещает восемь. Теперь те же
|
||||
`CachedView`, что и в пути Native AA.
|
||||
1. The UI pass was identified by the exact `BB_RENDER_RES` target size (1916x1078), but the game
|
||||
allocates targets with an aligned height (1916x1080; the scene constants also say 1916x1080).
|
||||
`RunScaled` was never called (the log had no `UI: native composition`). Alignment up to
|
||||
8 pixels is now accepted (`RenderTarget`), and the scene size for FSR comes from the scene
|
||||
constants (`CameraMotion::RenderSize`, `SceneSize`).
|
||||
2. After that FSR 4 failed with `external image registration failed (-1000069000)`: `RunScaled`
|
||||
created new image views every frame, and the FSR 4 registry holds eight. It now uses the same
|
||||
`CachedView` as the Native AA path.
|
||||
|
||||
Проверка дампом (`BB_DUMP_TRIGGER=<файл> BB_DUMP_DIR=<каталог>`, `BB_DUMP_FRAMES`, по умолчанию 8:
|
||||
вход FSR, векторы движения и выход, raw): при неподвижной камере PSNR соседних кадров выхода
|
||||
~45 дБ против ~28 дБ у входа с jitter; при повороте камеры и ходьбе шлейфов нет. FPS в этом
|
||||
режиме ~107 вместо ~220 — раньше FSR 4 просто не выполнялся.
|
||||
Dump check (`BB_DUMP_TRIGGER=<file> BB_DUMP_DIR=<dir>`, `BB_DUMP_FRAMES`, default 8:
|
||||
FSR input, motion vectors and output, raw): with a static camera the PSNR of adjacent output frames
|
||||
is ~45 dB against ~28 dB for the jittered input; no trails when turning the camera or walking. FPS in
|
||||
this mode is ~107 instead of ~220; before, FSR 4 simply did not run.
|
||||
|
||||
Осталось: спрайты (по 4 индекса) в цвет сцены со сценической глубиной — свечения, огоньки —
|
||||
по-прежнему не сдвигаются jitter (правило «≤ 6 индексов = полноэкранный проход»).
|
||||
Remaining: sprites (4 indices each) drawn into scene color with scene depth (glows, small lights)
|
||||
are still not shifted by jitter (the rule "≤ 6 indices = full-screen pass").
|
||||
|
||||
## 2026-10-01: FSR 4 быстрее — проход post (2.9 → 0.8 мс в 4K)
|
||||
## 2026-10-01: Faster FSR 4 — the post pass (2.9 → 0.8 ms at 4K)
|
||||
|
||||
Замеры: `BB_FSR4_PROFILE=1` (время каждого прохода FSR 4, патч провайдера в субмодуле,
|
||||
см. `gpu/patches/fsr-vulkan`), бенчмарк вне игры `out/gpu/fsr4-bench` (`ninja -C out/gpu
|
||||
fsr4-bench`; `--stats` — регистры и инструкции от RADV). 4K Balanced (2260x1272 → 3840x2160),
|
||||
RX 7800 XT: весь FSR 4 — 5.8 мс, из них **post 2.5–2.9 мс** (не нейросеть: последние слои,
|
||||
pixel shuffle 2x2 и смешивание с историей), 12 проходов модели — 2.4 мс, pre 0.55.
|
||||
Measurements: `BB_FSR4_PROFILE=1` (time of each FSR 4 pass, a provider patch in the submodule,
|
||||
see `gpu/patches/fsr-vulkan`), the out-of-game benchmark `out/gpu/fsr4-bench` (`ninja -C out/gpu
|
||||
fsr4-bench`; `--stats` prints registers and instructions from RADV). 4K Balanced (2260x1272 → 3840x2160),
|
||||
RX 7800 XT: all of FSR 4 takes 5.8 ms, of which **post is 2.5–2.9 ms** (not the neural network: the last layers,
|
||||
2x2 pixel shuffle and blending with history), the 12 model passes 2.4 ms, pre 0.55.
|
||||
|
||||
Причина: каждый поток считает блок 2x2 выходных пикселей и пишет их по одному в три образа
|
||||
(рекуррентное состояние, история, выход) — каждая инструкция записи волны пишет пиксели через
|
||||
один. Убрав любую из трёх записей, проход ускорялся в 1.3–4 раза при том же коде.
|
||||
Cause: each thread computes a 2x2 block of output pixels and writes them one by one into three images
|
||||
(recurrent state, history, output), so each wave store instruction writes every other
|
||||
pixel. Removing any one of the three stores made the pass 1.3–4 times faster with the same code.
|
||||
|
||||
Решение: `tools/fsr4_optimize.sh` декомпилирует post (spirv-cross), `tools/fsr4_post_lds.pl`
|
||||
собирает блок 16x16 рабочей группы в shared memory и пишет сплошными строками, glslang
|
||||
компилирует обратно в `fsr4_shaders/opt/`; `vk_fsr4.cpp` берёт его оттуда (`BB_FSR4_OPT=0` —
|
||||
оригинал). Две тонкости, найденные сравнением выходов:
|
||||
- spirv-cross переводит знаковую распаковку int8 (`OpBitcast` в `i8vec4`) как `unpack8(uint)`
|
||||
(беззнаковую) — без исправления результат совсем другой (PSNR 17 дБ);
|
||||
- значения в shared memory должны оставаться float: half из shared memory компилятор
|
||||
превращает в 16-битную запись, а она иначе округляет в unorm8.
|
||||
Fix: `tools/fsr4_optimize.sh` decompiles post (spirv-cross), `tools/fsr4_post_lds.pl`
|
||||
gathers the workgroup's 16x16 block in shared memory and writes contiguous rows, and glslang
|
||||
compiles it back into `fsr4_shaders/opt/`; `vk_fsr4.cpp` loads it from there (`BB_FSR4_OPT=0` selects the
|
||||
original). Two subtleties, found by comparing outputs:
|
||||
- spirv-cross translates the signed int8 unpack (`OpBitcast` to `i8vec4`) as `unpack8(uint)`
|
||||
(unsigned); without a fix the result is completely different (PSNR 17 dB);
|
||||
- values in shared memory must stay float: the compiler turns a half from shared memory
|
||||
into a 16-bit store, which rounds differently to unorm8.
|
||||
|
||||
`tools/fsr4_verify.sh` сравнивает оригинал и оптимизированный post для всех пресетов при выводе
|
||||
1080p/1440p/4K на псевдослучайных входах: 1440p и 4K — побитово одинаково; post 4K 2.1–3.5 →
|
||||
0.81–0.87 мс, 1440p 1.2–1.5 → 0.36–0.38 мс, 1080p 0.37–0.69 → 0.20–0.22 мс. В игре (4K
|
||||
Balanced): FSR 4 5.8 → 4.0 мс, кадр GPU 13.1 → ~11.6 мс; дальше FPS упирается в CPU (поток
|
||||
GPU-команд ждёт копии гостевой памяти, «host copies» ~20%).
|
||||
`tools/fsr4_verify.sh` compares the original and the optimized post for all presets at
|
||||
1080p/1440p/4K output on pseudo-random inputs: 1440p and 4K are bit-identical; post at 4K 2.1–3.5 →
|
||||
0.81–0.87 ms, 1440p 1.2–1.5 → 0.36–0.38 ms, 1080p 0.37–0.69 → 0.20–0.22 ms. In game (4K
|
||||
Balanced): FSR 4 5.8 → 4.0 ms, GPU frame 13.1 → ~11.6 ms; beyond that FPS is CPU-bound (the
|
||||
GPU command thread waits for guest memory copies, "host copies" ~20%).
|
||||
|
||||
Найдено попутно: **оригинальный FSR 4 на уровне 1080 (вывод 1920x1080) недетерминирован** — от
|
||||
запуска к запуску на одинаковых входах меняется полоса у левого края (столбцы 0–132), то есть
|
||||
где-то гонка или чтение неинициализированной памяти в модели/провайдере. При выводе 1080p это
|
||||
может давать мерцание у левого края кадра. Не исследовано.
|
||||
Found along the way: **the original FSR 4 at the 1080 tier (1920x1080 output) is nondeterministic**: from
|
||||
run to run on identical inputs a strip at the left edge changes (columns 0–132), so there is
|
||||
a race or a read of uninitialized memory somewhere in the model/provider. At 1080p output this
|
||||
can cause flicker at the left edge of the frame. Not investigated.
|
||||
|
||||
Маска реактивности при FSR 4 больше не считается (FSR 4 её не принимает).
|
||||
The reactive mask is no longer computed with FSR 4 (FSR 4 does not accept it).
|
||||
|
||||
### Что ещё проверено (там же, 4K Balanced)
|
||||
### What else was checked (same setup, 4K Balanced)
|
||||
|
||||
- **A/B в одном процессе** (`ab.sh`, бит 24 — FSR выключен, UI-копия без апскейла): 86.5 FPS с
|
||||
FSR 4 против 87.7 без него. После оптимизации post кадр упирается в CPU (поток GPU-команд,
|
||||
ожидание копий гостевой памяти), а не в GPU. Async compute для FSR (перекрыть его с началом
|
||||
следующего кадра) дал бы не больше этого ~1% — отложено до ускорения CPU-части; к тому же он
|
||||
требует переставлять команды UI и вывода кадра N после работы кадра N+1.
|
||||
- **WMMA (`VK_KHR_cooperative_matrix`, RADV на RDNA3 поддерживает)**: прототип прохода 1
|
||||
модели (остаточный блок 16 каналов: 3x3 16→16, 1x1 16→32 ReLU, 1x1 32→16) — 0.85 мс в первом
|
||||
варианте, 0.49 мс со словной раскладкой в shared memory, против 0.30 мс у исходного dot4.
|
||||
Арифметика на WMMA заняла бы ~0.1 мс, но при 16 каналах выкладка тайлов, эпилоги через shared
|
||||
memory и занятость (6 волн/SIMD, предел по LDS) съедают выигрыш. Потолок — около 1 мс на
|
||||
все 12 проходов при тонкой ручной переработке каждого; не начато.
|
||||
- `RADV_PERFTEST=cswave32` (wave32 для compute): FSR 4 медленнее, 4.2 → 5.4 мс.
|
||||
- Проход pre (0.54 мс) запись не ограничивает (0.50 мс без записи).
|
||||
- **A/B in one process** (`ab.sh`, bit 24 turns FSR off, UI copy without upscaling): 86.5 FPS with
|
||||
FSR 4 against 87.7 without it. After the post optimization the frame is CPU-bound (GPU command thread,
|
||||
waiting for guest memory copies), not GPU-bound. Async compute for FSR (overlapping it with the start
|
||||
of the next frame) would gain no more than this ~1%; postponed until the CPU side is faster. It also
|
||||
requires reordering the UI and present commands of frame N after the work of frame N+1.
|
||||
- **WMMA (`VK_KHR_cooperative_matrix`, supported by RADV on RDNA3)**: a prototype of model pass 1
|
||||
(16-channel residual block: 3x3 16→16, 1x1 16→32 ReLU, 1x1 32→16): 0.85 ms in the first
|
||||
version, 0.49 ms with a word layout in shared memory, against 0.30 ms for the original dot4.
|
||||
The arithmetic on WMMA would take ~0.1 ms, but with 16 channels the tile layout, epilogues through shared
|
||||
memory and occupancy (6 waves/SIMD, limited by LDS) eat the gain. The ceiling is about 1 ms for
|
||||
all 12 passes with careful manual rework of each; not started.
|
||||
- `RADV_PERFTEST=cswave32` (wave32 for compute): FSR 4 is slower, 4.2 → 5.4 ms.
|
||||
- The pre pass (0.54 ms) is not limited by stores (0.50 ms without stores).
|
||||
|
||||
## 2026-10-01: мерцание FSR 4 у левого края при выводе 1080p — гонка в проходе 11 модели
|
||||
## 2026-10-01: FSR 4 flicker at the left edge at 1080p output — a race in model pass 11
|
||||
|
||||
Недетерминизм уровня 1080 (см. выше) — ошибка в шейдере модели v07, проход 11 (декодер,
|
||||
1/4 → 1/2 разрешения). Каждый поток — пиксель входа 1/4 разрешения и пишет блок 2x2 выхода
|
||||
1/2 разрешения. Диспатч округляет ширину до 64 потоков, а потоки за шириной входа не
|
||||
останавливаются: на уровне 1080 (вход 480 в ширину) потоки 480..511 пишут пиксели выхода
|
||||
960..1023, то есть первые пиксели следующей строки (строка любого тензора — 15360 байт), и
|
||||
гоняются с их настоящими авторами. При выводе 4K ширина 960 делится на 64, ошибки нет.
|
||||
The 1080-tier nondeterminism (see above) is a bug in the v07 model shader, pass 11 (decoder,
|
||||
1/4 → 1/2 resolution). Each thread is one pixel of the 1/4-resolution input and writes a 2x2 block of the
|
||||
1/2-resolution output. The dispatch rounds the width up to 64 threads, and threads beyond the input width do not
|
||||
exit: at the 1080 tier (input 480 wide) threads 480..511 write output pixels
|
||||
960..1023, which are the first pixels of the next row (a row of any tensor is 15360 bytes), and
|
||||
race with their real writers. At 4K output the width 960 is divisible by 64, so there is no bug.
|
||||
|
||||
`tools/fsr4_pass11_guard.pl` добавляет ранний выход для потоков за границей (размер берётся из
|
||||
проверки соседей самого прохода); `tools/fsr4_optimize.sh` собирает его в `fsr4_shaders/opt`
|
||||
для всех пресетов вместе с post; `vk_fsr4.cpp` и бенчмарк берут из `opt/` любой проход.
|
||||
`tools/fsr4_verify.sh`: 1440p/4K — побитово как оригинал (защита там не срабатывает); 1080p —
|
||||
одинаковый результат от запуска к запуску, отличия от оригинала после первого кадра только в
|
||||
бывшей полосе у левого края.
|
||||
`tools/fsr4_pass11_guard.pl` adds an early exit for out-of-bounds threads (the size comes from
|
||||
the pass's own neighbor check); `tools/fsr4_optimize.sh` builds it into `fsr4_shaders/opt`
|
||||
for all presets together with post; `vk_fsr4.cpp` and the benchmark load any pass from `opt/`.
|
||||
`tools/fsr4_verify.sh`: 1440p/4K are bit-identical to the original (the guard does not trigger there); 1080p gives
|
||||
the same result from run to run, and after the first frame it differs from the original only in
|
||||
the former strip at the left edge.
|
||||
|
||||
Найдено при этом: spirv-cross переводит все знаковые распаковки int8 (`OpBitcast` в `v4char`)
|
||||
как беззнаковый `unpack8(uint)`; в pass 11 их девять разных форм. Исправление общее для
|
||||
скриптов — `tools/Fsr4SpirvCrossFixes.pm` (все `unpack8` через знаковый помощник).
|
||||
Found in the process: spirv-cross translates all signed int8 unpacks (`OpBitcast` to `v4char`)
|
||||
as unsigned `unpack8(uint)`; pass 11 has nine different forms of them. The fix is shared by the
|
||||
scripts: `tools/Fsr4SpirvCrossFixes.pm` (all `unpack8` go through a signed helper).
|
||||
|
||||
## 2026-10-01: FSR 4.1.1 — что внутри и как его запустить в Vulkan (разведка)
|
||||
## 2026-10-01: FSR 4.1.1 — what is inside and how to run it in Vulkan (investigation)
|
||||
|
||||
Источник: `amd_fidelityfx_upscaler_dx12.dll` 4.1.1.2740 из OptiScaler (`FSR4_LATEST`).
|
||||
- В DLL есть провайдер `ffxProvider_FSR4_Int8` / `Fsr4Int8UpscalerModel` (версия «FSR4-i8 4.1.1»).
|
||||
1028 DXIL-контейнеров (25.7 МБ): проходы модели `fsr4_model_v07_fp8_no_scale_pass1..12` (+ `_post`),
|
||||
270 вариантов `prepass`, 144 `postpass`, `fsr_rcas_pass` и запасной FSR 3.1/2. Сжатых данных нет.
|
||||
- Несмотря на «fp8» в имени, проходы модели — обычный DXIL с `dot4AddPacked` (int8 dot4), без
|
||||
расширений AMD (WMMA). Архитектура та же v07 (12 проходов), но веса и смещения не зашиты в
|
||||
шейдер, а читаются из `InitializerBuffer` (t18, typed buffer), размеры тензоров — из cbuffer
|
||||
`CsTensorSizes`, scratch — `u11`. У каждого прохода 9 вариантов: сетка зашита (уровни 1080 / 4K /
|
||||
8K: 480x270, 960x540, 1920x1080 для прохода 5) и, видимо, пресеты.
|
||||
- `amdxcffx64.dll` (загрузчик 2.3.0.2913, его подгружает Proton) — те же FP8-проходы FSR 4 и ML-
|
||||
генерация кадров (`mlfi_*`), отдельной INT8-модели нет. `FSR4_INT8` 4.0.2 — это `v07_i8`, то есть
|
||||
то, что порт уже использует.
|
||||
- dxil-spirv (HansKristian-Work/dxil-spirv, собирается из исходников) переводит проходы в
|
||||
корректный SPIR-V: `dxil-spirv <pass> --enable-shader-i8-dot --ssbo-uav --use-reflection-names`.
|
||||
Source: `amd_fidelityfx_upscaler_dx12.dll` 4.1.1.2740 from OptiScaler (`FSR4_LATEST`).
|
||||
- The DLL contains the provider `ffxProvider_FSR4_Int8` / `Fsr4Int8UpscalerModel` (version "FSR4-i8 4.1.1").
|
||||
1028 DXIL containers (25.7 MB): model passes `fsr4_model_v07_fp8_no_scale_pass1..12` (+ `_post`),
|
||||
270 `prepass` variants, 144 `postpass`, `fsr_rcas_pass` and the FSR 3.1/2 fallback. There is no compressed data.
|
||||
- Despite "fp8" in the name, the model passes are plain DXIL with `dot4AddPacked` (int8 dot4), without
|
||||
AMD extensions (WMMA). The architecture is the same v07 (12 passes), but weights and biases are not baked into
|
||||
the shader; they are read from `InitializerBuffer` (t18, typed buffer), tensor sizes from the cbuffer
|
||||
`CsTensorSizes`, scratch is `u11`. Each pass has 9 variants: the grid is baked in (1080 / 4K /
|
||||
8K tiers: 480x270, 960x540, 1920x1080 for pass 5) and, apparently, the presets.
|
||||
- `amdxcffx64.dll` (loader 2.3.0.2913, loaded by Proton) has the same FP8 FSR 4 passes and ML
|
||||
frame generation (`mlfi_*`), with no separate INT8 model. `FSR4_INT8` 4.0.2 is `v07_i8`, that is,
|
||||
what the port already uses.
|
||||
- dxil-spirv (HansKristian-Work/dxil-spirv, built from source) translates the passes into
|
||||
correct SPIR-V: `dxil-spirv <pass> --enable-shader-i8-dot --ssbo-uav --use-reflection-names`.
|
||||
|
||||
Чего не хватает для своего провайдера 4.1.1: соответствие контейнеров (пресет, уровень, проход),
|
||||
содержимое `InitializerBuffer`, значения `CsTensorSizes`, перестановки prepass/postpass, константы и
|
||||
размеры диспатчей. Самый надёжный путь — не разбирать x86-код провайдера, а записать один кадр:
|
||||
маленькая тестовая программа на FFX API (DX12, MinGW) под Wine/Proton с vkd3d-proton, явный выбор
|
||||
провайдера «FSR4-i8» через override версии, и Vulkan-слой, который пишет пайплайны (SPIR-V от
|
||||
dxil-spirv), содержимое буферов и размеры диспатчей. Затем — воспроизведение своим провайдером
|
||||
через тот же бенчмарк (`fsr4-bench`), с проверкой против записанного выхода.
|
||||
What is missing for an own 4.1.1 provider: the container mapping (preset, tier, pass),
|
||||
the contents of `InitializerBuffer`, the `CsTensorSizes` values, the prepass/postpass permutations, constants and
|
||||
dispatch sizes. The most reliable way is not to reverse the provider's x86 code but to record one frame:
|
||||
a small FFX API (DX12, MinGW) test program under Wine/Proton with vkd3d-proton, explicit selection of
|
||||
the "FSR4-i8" provider through a version override, and a Vulkan layer that writes the pipelines (SPIR-V from
|
||||
dxil-spirv), buffer contents and dispatch sizes. Then replay with an own provider
|
||||
through the same benchmark (`fsr4-bench`), checked against the recorded output.
|
||||
|
||||
## 2026-10-01: FSR 4.1.1 в порту (upscaler=fsr411)
|
||||
## 2026-10-01: FSR 4.1.1 in the port (upscaler=fsr411)
|
||||
|
||||
Модель AMD 4.1.1 (INT8) работает нативно в Vulkan и совпадает с DLL до бита.
|
||||
The AMD 4.1.1 (INT8) model runs natively in Vulkan and matches the DLL bit for bit.
|
||||
|
||||
**Как получено.** `tools/fsr4cap/fsr4cap.exe` (C, MinGW) вызывает FidelityFX API 2.3 из
|
||||
`amd_fidelityfx_loader_dx12.dll` с явной версией 4.1.1 и через подмену таблиц методов D3D12
|
||||
записывает всё, что делает DLL под vkd3d-proton: пайплайны (DXIL), корневые сигнатуры,
|
||||
дескрипторы, загрузки, константы, диспатчи. Кадр — 29 диспатчей: SPD авто-экспозиции, prepass,
|
||||
pass0_post, проходы модели 1..12 (за каждым `_post`, обнуление рамки тензора), postpass, RCAS.
|
||||
- Моделей две: m0 (Native..Performance) и m1 (Ultra Performance, соотношение 3.0), веса — 128 КБ
|
||||
(`InitializerBuffer`). Уровни: t1080 (вывод до 1920x1080), t2160 (больше, вкл. 2560x1080).
|
||||
- Тензоры считаются от вывода, выровненного до 8; таблица `CsTensorSizes` (17 размеров), число
|
||||
групп и константы (MLSR как у v07, но width/height не выровнены и `inv_scale = 1 / scale`;
|
||||
SPD; RCAS) проверяются `extract.py` на 20 записях.
|
||||
- dxil-spirv переводит DXIL так же, как vkd3d-proton: `--mixed-float-dot-product`
|
||||
(`VK_VALVE_shader_mixed_float_dot_product`, dot2 по half с накоплением во float) — без него
|
||||
PSNR 40 дБ вместо точного совпадения. `--class-bindings` (патч) раскладывает регистры SRV/UAV/
|
||||
CBV/сэмплеров по биндингам +0/+32/+64/+96, рантайм привязывает ресурсы по именам.
|
||||
- `fsr411/fsr411.cpp` — независимое ядро на Vulkan (игра и `fsr4-bench --fsr411`).
|
||||
- `verify.sh`: DLL и воспроизведение на одинаковых входах, 8 кадров — побитово одинаково для
|
||||
1080p (обе модели), 1440p, 2560x1080, 4K (обе модели). 1600x900 расходится только у правого
|
||||
края, где сама DLL недетерминирована (гонка в её шейдерах при ширине тензора не кратной 64).
|
||||
**How it was obtained.** `tools/fsr4cap/fsr4cap.exe` (C, MinGW) calls the FidelityFX API 2.3 from
|
||||
`amd_fidelityfx_loader_dx12.dll` with an explicit version 4.1.1 and, by replacing D3D12 method tables,
|
||||
records everything the DLL does under vkd3d-proton: pipelines (DXIL), root signatures,
|
||||
descriptors, uploads, constants, dispatches. (Since then fsr4cap.exe loads AMD's upscaler DLL directly,
|
||||
which exports the API itself; the loader is only needed for a DLL that does not.) A frame is 29 dispatches: auto-exposure SPD, prepass,
|
||||
pass0_post, model passes 1..12 (each followed by `_post`, which clears the tensor border), postpass, RCAS.
|
||||
- There are two models: m0 (Native..Performance) and m1 (Ultra Performance, ratio 3.0); weights are 128 KB
|
||||
(`InitializerBuffer`). Tiers: t1080 (output up to 1920x1080), t2160 (larger, incl. 2560x1080).
|
||||
- Tensors are computed from the output aligned to 8; the `CsTensorSizes` table (17 sizes), the group
|
||||
counts and the constants (MLSR as in v07, but width/height are not aligned and `inv_scale = 1 / scale`;
|
||||
SPD; RCAS) are checked by `extract.py` on 20 recordings.
|
||||
- dxil-spirv translates DXIL the same way as vkd3d-proton: `--mixed-float-dot-product`
|
||||
(`VK_VALVE_shader_mixed_float_dot_product`, half dot2 with float accumulation); without it
|
||||
PSNR is 40 dB instead of an exact match. `--class-bindings` (a patch) lays out SRV/UAV/
|
||||
CBV/sampler registers at bindings +0/+32/+64/+96; the runtime binds resources by name.
|
||||
- `fsr411/fsr411.cpp` is an independent Vulkan core (the game and `fsr4-bench --fsr411`).
|
||||
- `verify.sh`: DLL and replay on identical inputs, 8 frames, bit-identical for
|
||||
1080p (both models), 1440p, 2560x1080, 4K (both models). 1600x900 differs only at the right
|
||||
edge, where the DLL itself is nondeterministic (a race in its shaders when the tensor width is not a multiple of 64).
|
||||
|
||||
**Ускорение.** postpass переписан через workgroup memory на уровне SPIR-V-ассемблера
|
||||
(`postpass_lds.py`; перевод через GLSL неточен: теряются `DenormPreserve` и точная расстановка
|
||||
`RelaxedPrecision`, и ещё что-то меняет результат): 2.18 → 0.95 мс в 4K, побитово как DLL.
|
||||
Весь FSR 4.1.1: 1.09 мс при 1080p (1280x720), 4.2 мс при 4K Balanced. В игре (4K Balanced)
|
||||
апскейлер 4.12 мс против 4.32 мс у ускоренного v07.
|
||||
**Speedup.** postpass is rewritten to use workgroup memory at the SPIR-V assembly level
|
||||
(`postpass_lds.py`; going through GLSL is inexact: `DenormPreserve` and the exact placement of
|
||||
`RelaxedPrecision` are lost, and something else changes the result too): 2.18 → 0.95 ms at 4K, bit-identical to the DLL.
|
||||
All of FSR 4.1.1: 1.09 ms at 1080p (1280x720), 4.2 ms at 4K Balanced. In game (4K Balanced)
|
||||
the upscaler takes 4.12 ms against 4.32 ms for the accelerated v07.
|
||||
|
||||
**Сборка ассетов** (пользователем, из своих DLL; в репозитории ничего от AMD нет):
|
||||
**Building the assets** (by the user, from their own DLLs; the repository contains nothing from AMD):
|
||||
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll 4.1.x> <amd_fidelityfx_loader_dx12.dll 2.3.x>
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll 4.1.x> [<amd_fidelityfx_loader_dx12.dll 2.3.x>]
|
||||
|
||||
Нужны nix-shell (или MinGW, cmake, ninja, python3, SPIRV-Tools, umu-run), сеть для dxil-spirv и
|
||||
заголовков FidelityFX SDK и Proton (GE-Proton). DLL есть, например, у OptiScaler
|
||||
(`FSR4_LATEST/`) и в играх с FSR 3.1/4. Сборка с нуля даёт те же файлы; `VERIFY=1` в конце
|
||||
сравнивает с DLL.
|
||||
Needs nix-shell (or MinGW, cmake, ninja, python3, SPIRV-Tools, git), network access for dxil-spirv, and
|
||||
Proton (PROTONPATH, or GE-Proton 10+ / Proton-CachyOS / Proton Experimental; umu-run or Steam's runtime).
|
||||
In the AppImage the tools are prebuilt (`BB_FSR4CAP_TOOLS`). The DLLs are available, for example, from OptiScaler
|
||||
(`FSR4_LATEST/`) and in games with FSR 3.1/4. A build from scratch produces the same files; `VERIFY=1` at the end
|
||||
compares against the DLL.
|
||||
|
||||
**Поправка к A/B выше («кадр упирается в CPU»).** Те замеры шли, пока 12 зависших процессов
|
||||
shadPS4 занимали по ядру. На свободной системе: «host copies» 1.6%, ожидание GPU ~41% — кадр
|
||||
упирается в GPU, так что async compute для FSR снова имеет смысл.
|
||||
**Correction to the A/B above ("the frame is CPU-bound").** Those measurements ran while 12 hung shadPS4
|
||||
processes each occupied a core. On an idle system: "host copies" 1.6%, waiting for the GPU ~41%; the frame
|
||||
is GPU-bound, so async compute for FSR makes sense again.
|
||||
+1
-1
@@ -127,7 +127,7 @@ file(GLOB SHIM_SOURCES CONFIGURE_DEPENDS shim/*.cpp shim/*/*.cpp)
|
||||
file(GLOB LAYER_SOURCES CONFIGURE_DEPENDS layer/*.cpp)
|
||||
# Game profiles (what the translator knows about one game, out of its core): games/.
|
||||
file(GLOB GAME_SOURCES CONFIGURE_DEPENDS games/*.cpp)
|
||||
# The translator's own command processor (docs/TRANSLATOR_ARCHITECTURE.ru.md, step 2): cp/.
|
||||
# The translator's own command processor (docs/TRANSLATOR_ARCHITECTURE.md, step 2): cp/.
|
||||
file(GLOB CP_SOURCES CONFIGURE_DEPENDS cp/*.cpp)
|
||||
add_library(bbgpu SHARED ${VIDEO_CORE} ${RECOMPILER} ${COMMON} ${SHIM_SOURCES} ${LAYER_SOURCES} ${GAME_SOURCES} ${CP_SOURCES}
|
||||
shadps4/core/libraries/gnmdriver/gnmdriver.cpp
|
||||
|
||||
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user