0.6-pre1: new memory model on every GPU, seamless co-op, aspect ratios and frame rate by hooks, own renderer

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
deadinsideandClaude Opus 5.5 committed 2026-10-15 12:18:28 +03:00
1 parent 3c57da4198
commit 72e5c8ce22
825 files changed
+107541 -102523

No files matched your search

+47 -37
View File
@@ -11,15 +11,15 @@ directly on the PC:
- **as in Wine**, the game's x86-64 code runs on the CPU directly, and a runtime written for
this one game replaces the PS4 system libraries;
- **as in DXVK**, the game's graphics are translated to Vulkan — by a renderer derived from
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1;
- memory and the GPU's work follow the PC model on AMD GPUs by default (the new memory and
translation model); NVIDIA and other GPUs keep the old memory model of 0.3 for now. The
launcher's *Memory model* switches either way.
- **as in DXVK**, the game's graphics are translated to Vulkan — by the port's own renderer and
shader translator (they started from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s and were
rewritten), including temporal upscaling with AMD FSR 3.1, FSR 4, FSR 4.1.1 and NVIDIA DLSS;
- memory and the GPU's work follow the PC model on every GPU by default (the new memory and
translation model, since 0.6); the old memory model of 0.3 stays as a launcher option
(*Memory model → Old*).
One step remains to the full Wine + DXVK model: the new model on NVIDIA as well, then removing the
old memory model (see below).
What remains to the full Wine + DXVK model: removing the old memory model once the new one is
confirmed on NVIDIA and Intel (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
@@ -37,11 +37,8 @@ Mesa/RADV) has been tested thoroughly.
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
translation: the game code runs at full speed.
- **Two memory models** (launcher → *Mode → Memory and translation model*; *Auto*, the default,
picks by GPU):
- **The old memory model, as in 0.3 — the default on NVIDIA, Intel and others.** VRAM copies
of the game's memory, writes tracked through page protection. With every fix made since 0.3
(motion vectors and the upscaler, flicker with DoF on, a damaged shader cache).
- **The new memory and translation model — the default on AMD.** The game's memory lives
is the new one on every GPU):
- **The new memory and translation model — the default on every GPU (0.6).** 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;
@@ -49,20 +46,31 @@ Mesa/RADV) has been tested thoroughly.
is translated into Vulkan commands rather than emulated on the CPU: memory writes
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written by the
GPU itself at the end of the pipeline, at the same point of the stream as the PS4's command
processor writes them. On AMD the game's memory is kept in a sparse buffer (the arena); on
NVIDIA, Intel and others it goes through the layer's memory module: bound in place, VRAM
copies as separate buffers, no sparse rebinding (NVIDIA's sparse binding froze the GPU for
seconds); `BB_LAYER_MEMORY=1/0` chooses by hand. Tested on an RX 7800 XT. On NVIDIA it can be chosen by hand (*New*); testers' results differ by scene,
some scenes are slower than with the old model, so it is not the default there yet. Not
tested on Intel. Without the launcher: `BB_PC_MODEL=1/0` (unset: by GPU, as *Auto*).
processor writes them. On every GPU the game's memory goes through the layer's memory module:
bound in place, VRAM copies as separate buffers, no sparse binding (NVIDIA's sparse binding
froze the GPU for seconds; on AMD the module is faster than the former sparse arena too);
`BB_LAYER_MEMORY=0` gives AMD's sparse arena. Tested thoroughly on an RX 7800 XT; on NVIDIA
by testers. A driver that fails the startup checks falls back to VRAM copies by itself.
Without the launcher: `BB_PC_MODEL=1/0` (unset: the new one, as *Auto*).
- **The old memory model, as in 0.3** (*Old*). VRAM copies of the game's memory, writes
tracked through page protection, with every fix made since 0.3.
[Changes in 0.5](docs/CHANGES_0.5.md); the prereleases: [pre4](docs/CHANGES_0.5-pre4.md),
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
[Changes in 0.6-pre1](docs/CHANGES_0.6-pre1.md), [0.51](docs/CHANGES_0.51.md),
[0.5](docs/CHANGES_0.5.md).
Unused textures are freed in both modes, so VRAM no longer grows with every area visited.
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
- **Unlocked frame rate.** Hooks in the game's code make the simulation use the real frame time
(the community patches in `patches/Bloodborne.xml` remain for comparison, `BB_FRAME_HOOKS=0`);
~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at 1440p with FSR 4 Quality.
Also 30/60/90 FPS modes.
- **Any aspect ratio, live.** 4:3, 16:10, 16:9, 21:9 and 32:9 with the HUD at the screen's edges,
through hooks (the game's code is not patched).
- **Keyboard and mouse.** Mouse look turns the game's camera directly; a mouse pointer in the
game's menus; a layout close to Dark Souls III's; button prompts show the bound key, or the
buttons of the gamepad in use (Xbox and others), PlayStation icons only with a PlayStation pad.
- **Online play and seamless co-op.** Messages, bloodstains, bells and summons through a shadNet
server; seamless co-op (BBSC). Seamless Hunt's servers by default (`play.seamlesshunt.com`,
an account from seamlesshunt.com).
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
computes motion vectors itself: camera motion from depth and the scene matrices, and object
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
@@ -97,16 +105,16 @@ Mesa/RADV) has been tested thoroughly.
|---|---|---|
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | By default the same model (as in 0.3); in the *New memory and translation model* mode: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | By default the same; in the new mode translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | 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 (the 0.3 model as an option) |
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | Translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished (the old model emulates, as shadPS4) |
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
| GPU | shadPS4 video core and shader recompiler | Its own command processor, renderer, caches and shader translator (they started from shadPS4's, `gpu/VENDOR.txt`): two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, DLSS, frame capture and GPU profiler |
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
the graphics side.
Without shadPS4 there would be no bbport: its renderer and shader recompiler were the starting
point of the graphics side.
### Wine + DXVK for one game
@@ -115,8 +123,8 @@ the graphics side.
- **System libraries.** As Wine replaces the Windows API, the bbport runtime (`src/runtime_*.c`)
implements exactly the PS4 OS functions Bloodborne calls: memory, threads, files, audio, pad,
saves.
- **Memory.** In the new mode, as in a PC game: the game's data in system RAM, VRAM used the way
a PC game uses it. The mode is still experimental.
- **Memory.** As in a PC game (the default model): the game's data in system RAM, VRAM used the
way a PC game uses it.
- **Graphics.** The PS4 GPU's command stream (PM4) is a recording of the game's graphics API
calls: it is written by 99 functions of the statically linked libGnm, so decoding the stream
and translating the calls themselves come to the same thing. GCN shaders are translated to
@@ -364,7 +372,7 @@ build without it plays offline only.
|---|---|
| `src/` | Loader (`probe.c`) and the HLE runtime |
| `scripts/` | Offline preparation of the game image, module linking, patch compiler |
| `gpu/` | Renderer library: vendored shadPS4 video core with this port's changes (`gpu/VENDOR.txt`), shims, ImGui menu, FSR 4.1.1 runtime (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
| `gpu/` | Renderer library, the port's own (`gpu/VENDOR.txt`): command processor, shader translator, renderer, caches, ImGui menu, FSR 4.1.1 runtime (`gpu/upscale/fsr411`); the online module (`gpu/bbnet`) |
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
| `patches/` | Community patches for Bloodborne |
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
@@ -376,8 +384,8 @@ Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
## Roadmap
- The new memory and translation model on by default on NVIDIA too, once it is as fast there;
the old model is removed after that.
- The old memory model removed, once the new one (the default on every GPU since 0.6) is
confirmed on NVIDIA and Intel.
- 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.
@@ -386,7 +394,8 @@ Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
- Async compute for the upscaler (the frame is GPU-bound at 4K).
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
memory (a memory-bridge prototype is in `tools/bridge_helper`); inputs exposed so that OptiScaler-style mapping works.
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
- Frame generation (FSR 3.1 FG: in the code, hidden until its artifacts with upscaling are
fixed), reactive and transparency masks for particles and fog.
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
- Steam Deck validation of the AppImage; HDR output.
@@ -395,11 +404,12 @@ Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
from shadPS4 (GPL-2.0-or-later). Third-party components keep their licenses:
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
[shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+: the renderer and shader translator were derived from it and rewritten; the online module keeps its network and NP code),
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts, BBSC seamless co-op
(GPL-2.0+, in the online module),
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
+47 -37
View File
@@ -10,15 +10,15 @@ bbport — это аналог Wine + DXVK, только для одной иг
- **как в 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.
В лаунчере *Модель памяти* переключается в обе стороны.
- **как в DXVK**, графика игры переводится в Vulkan — собственным рендером и транслятором шейдеров
порта (начинались с кода [shadPS4](https://github.com/shadps4-emu/shadPS4) и переписаны), вплоть
до временного апскейлинга AMD FSR 3.1, FSR 4, FSR 4.1.1 и NVIDIA DLSS;
- память и работа видеочипа по умолчанию на любой видеокарте устроены как у игры для ПК (новая
модель памяти и трансляции, с 0.6); старая модель памяти, как в 0.3, остаётся выбором в лаунчере
(*Модель памяти → Старая*).
До полной модели Wine + DXVK остался один шаг: новая модель и на NVIDIA, затем удаление старой
модели памяти (см. ниже).
До полной модели Wine + DXVK осталось удалить старую модель памяти — когда новая подтвердится на
NVIDIA и Intel (см. ниже).
> **Файлов игры в репозитории нет.** Нужен свой дамп Bloodborne с обновлением 1.09: любое издание
> из магазина (CUSA00900 в США, CUSA03173 — европейское Game of the Year, ...): у них один и тот же
@@ -36,11 +36,8 @@ bbport — это аналог Wine + DXVK, только для одной иг
нативный код прилинкованы libc и libSceFios2 из PS4. Никакой эмуляции процессора и
трансляции инструкций: код игры работает на полной скорости.
- **Две модели памяти** (лаунчер → *Режим работы → Модель памяти и трансляции*; *Авто*, по
умолчанию, выбирает по видеокарте):
- **Старая модель памяти, как в 0.3, — по умолчанию на NVIDIA, Intel и других.** Копии памяти
игры в VRAM, записи отслеживаются защитой страниц. Со всеми исправлениями после 0.3
(векторы движения и апскейлер, мерцание при включённом DoF, повреждённый кэш шейдеров).
- **Новая модель памяти и трансляции — по умолчанию на AMD.** Память
умолчанию, — новая на любой видеокарте):
- **Новая модель памяти и трансляции — по умолчанию на любой видеокарте (0.6).** Память
игры находится в оперативной памяти, и GPU читает её на месте, как буферы игры для ПК; то,
что он читает часто, держится в видеопамяти и освобождается, когда перестаёт использоваться
(текстуры — через 20 с, буферы — через 60 с). Модуль памяти прослойки отслеживает записи CPU
@@ -49,22 +46,32 @@ bbport — это аналог Wine + DXVK, только для одной иг
эмулируется на процессоре: записи в память (`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` (не задано — по видеокарте, как *Авто*).
На любой видеокарте память игры идёт через модуль памяти прослойки: привязывается на месте,
копии в VRAM — отдельные буферы, без sparse-привязок (на NVIDIA sparse binding держал видеочип
секундами; на AMD модуль быстрее прежней sparse-арены); `BB_LAYER_MEMORY=0` — sparse-арена
AMD. Основательно проверено на RX 7800 XT, на NVIDIA — тестерами. Если драйвер не проходит
проверки при запуске, порт сам переходит на копии в VRAM. Без лаунчера — `BB_PC_MODEL=1/0`
(не задано — новая, как *Авто*).
- **Старая модель памяти, как в 0.3** (*Старая*). Копии памяти игры в VRAM, записи
отслеживаются защитой страниц, со всеми исправлениями после 0.3.
[Изменения в 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).
[Изменения в 0.6-pre1](docs/CHANGES_0.6-pre1.md), [0.51](docs/CHANGES_0.51.md),
[0.5](docs/CHANGES_0.5.md).
Неиспользуемые текстуры освобождаются в обоих режимах, поэтому видеопамять больше не растёт
с каждой локацией.
- **Разблокированная частота кадров.** Патчи сообщества (`patches/Bloodborne.xml`) переводят
симуляцию на реальное время кадра; ~90 FPS в 4K с FSR 4 Balanced на RX 7800 XT, ~150 FPS в
1440p с FSR 4 Quality. Есть режимы 30/60/90 FPS.
- **Разблокированная частота кадров.** Хуки в коде игры переводят симуляцию на реальное время
кадра (патчи сообщества из `patches/Bloodborne.xml` остались для сравнения, `BB_FRAME_HOOKS=0`);
~90 FPS в 4K с FSR 4 Balanced на RX 7800 XT, ~150 FPS в 1440p с FSR 4 Quality. Есть режимы
30/60/90 FPS.
- **Любое соотношение сторон, на лету.** 4:3, 16:10, 16:9, 21:9 и 32:9, HUD у краёв экрана — хуками
(код игры не патчится).
- **Клавиатура и мышь.** Камера мышью поворачивается напрямую; указатель мыши в меню игры;
раскладка близка к Dark Souls III; подсказки кнопок показывают назначенную клавишу или кнопки
используемого геймпада (Xbox и других), значки PlayStation — только с геймпадом PlayStation.
- **Игра по сети и бесшовный кооператив.** Сообщения, пятна крови, колокола и призывы — через
сервер shadNet; бесшовный кооператив (BBSC). По умолчанию серверы Seamless Hunt
(`play.seamlesshunt.com`, учётная запись — на seamlesshunt.com).
- **Временной апскейлинг под эту игру.** Буфера скоростей в Bloodborne нет, поэтому порт
считает векторы движения сам: движение камеры — по глубине и матрицам сцены, движение
объектов (персонажи, ткань, оружие) — по позициям вершин прошлого кадра. Сцена сдвигается
@@ -99,15 +106,16 @@ bbport — это аналог Wine + DXVK, только для одной иг
|---|---|---|
| Охват | Универсальный эмулятор PS4, много игр | Одна игра: Bloodborne v1.09 |
| Загрузка | Свой ELF-загрузчик и эмуляция ядра во время работы | eboot заранее превращается (`scripts/`) в образ с прилинкованными libc/Fios2 PS4; загрузчик на C отображает его и передаёт управление игре (загрузчик и рантайм — ~5 тыс. строк) |
| Память | Вид памяти PS4 со стороны GPU хранится копиями в VRAM, синхронизация через отслеживание записей защитой страниц | По умолчанию та же модель (как в 0.3); в режиме *Новая модель памяти и трансляции*: память игры в оперативной памяти, GPU использует её на месте, часто читаемое — в VRAM, освобождается, когда не нужно |
| Командный процессор | Эмулируется: записи в память, DMA и метки выполняет процессор во время разбора команд | По умолчанию так же; в новом режиме переводится в команды Vulkan, их выполняет видеочип в порядке потока, метки — после реального завершения работы |
| Память | Вид памяти PS4 со стороны GPU хранится копиями в VRAM, синхронизация через отслеживание записей защитой страниц | По умолчанию новая модель памяти и трансляции: память игры в оперативной памяти, GPU использует её на месте, часто читаемое — в VRAM, освобождается, когда не нужно (модель 0.3 — выбором) |
| Командный процессор | Эмулируется: записи в память, DMA и метки выполняет процессор во время разбора команд | Переводится в команды Vulkan, их выполняет видеочип в порядке потока, метки — после реального завершения работы (старая модель эмулирует, как shadPS4) |
| Системные библиотеки | Широкий HLE ОС PS4 | Небольшой рантайм (`src/runtime_*.c`) ровно под то, что вызывает Bloodborne: память, потоки, синхронизация, файлы, звук (включая ATRAC9), геймпад, сохранения, AppContent |
| GPU | Видеоядро и рекомпилятор шейдеров shadPS4 | То же ядро (вложено, GPL) с ~200 помеченными правками (`bbport:`) и новыми модулями: двухстадийный конвейер draw, мемоизация состояния прохода и наборов текстур, уменьшенные цели сцены, векторы движения, FSR 3.1/4/4.1.1, захват кадра, профайлер GPU |
| GPU | Видеоядро и рекомпилятор шейдеров shadPS4 | Свои командный процессор, рендер, кэши и транслятор шейдеров (начинались с кода shadPS4, `gpu/VENDOR.txt`): двухстадийный конвейер draw, мемоизация состояния прохода и наборов текстур, уменьшенные цели сцены, векторы движения, FSR 3.1/4/4.1.1, DLSS, захват кадра, профайлер GPU |
| Поток GPU | Один поток обрабатывает весь поток команд (узкое место в Bloodborne) | Разбор и запись draw идут в разных потоках; работа масштабируется под число потоков (в том числе Steam Deck) |
| Апскейлинг | — | Временной (FSR 3.1, FSR 4, FSR 4.1.1) на собственных векторах движения и jitter игры |
| Патчи игры | Патч-файлы применяет эмулятор | Те же патчи сообщества компилируются при запуске (`scripts/patches.py`); разрешение рендера, эффекты и FPS — из лаунчера |
Без shadPS4 bbport бы не было: его рендерер и рекомпилятор шейдеров — основа графической части.
Без shadPS4 bbport бы не было: его рендерер и рекомпилятор шейдеров — отправная точка графической
части.
### Wine + DXVK для одной игры
@@ -116,8 +124,8 @@ bbport — это аналог Wine + DXVK, только для одной иг
- **Системные библиотеки.** Как Wine подменяет Windows API, так рантайм bbport (`src/runtime_*.c`)
реализует ровно те функции ОС PS4, которые вызывает Bloodborne: память, потоки, файлы, звук,
геймпад, сохранения.
- **Память.** В новом режиме — как у игры для ПК: данные игры в оперативной памяти, видеопамять
используется так, как её использует игра для ПК. Пока режим экспериментальный.
- **Память.** Как у игры для ПК (модель по умолчанию): данные игры в оперативной памяти,
видеопамять используется так, как её использует игра для ПК.
- **Графика.** Поток команд видеочипа PS4 (PM4) — это записанные вызовы графического API игры:
его пишут 99 функций статически слинкованной libGnm, так что разбор потока и перевод самих
вызовов дают одно и то же. Шейдеры GCN переводятся в SPIR-V. В новом режиме работа командного
@@ -353,7 +361,7 @@ OpenSSL, zlib и miniupnpc (`shell.nix`), входит в AppImage. Включа
|---|---|
| `src/` | Загрузчик (`probe.c`) и HLE-рантайм |
| `scripts/` | Подготовка образа игры, линковка модулей, компилятор патчей |
| `gpu/` | Библиотека рендерера: видеоядро shadPS4 с правками порта (`gpu/VENDOR.txt`), прослойки, меню ImGui, рантайм FSR 4.1.1 (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
| `gpu/` | Библиотека рендерера, собственный код порта (`gpu/VENDOR.txt`): командный процессор, транслятор шейдеров, рендер, кэши, меню ImGui, рантайм FSR 4.1.1 (`gpu/upscale/fsr411`); онлайн-модуль (`gpu/bbnet`) |
| `launcher/`, `packaging/` | Лаунчер GTK4; пакет Nix и AppImage |
| `patches/` | Патчи сообщества для Bloodborne |
| `tools/` | Утилиты разработчика: скриптовые запуски, A/B-переключатели, бенчмарк FSR, переписывание шейдеров FSR 4, `fsr4cap` (запись и извлечение FSR 4.1.1) |
@@ -365,8 +373,8 @@ OpenSSL, zlib и miniupnpc (`shell.nix`), входит в AppImage. Включа
## Что будет дальше
- Новая модель памяти и трансляции по умолчанию и на NVIDIA, когда она станет там не медленнее;
после этого старая модель удаляется.
- Удалить старую модель памяти, когда новая (по умолчанию на любой видеокарте с 0.6)
подтвердится на NVIDIA и Intel.
- Текстуры — объекты, которые создаются при загрузке (меньше угадывания по адресам, загрузка
вне кадра).
- Шейдеры переводятся заранее, при установке, а не во время игры.
@@ -376,7 +384,8 @@ OpenSSL, zlib и miniupnpc (`shell.nix`), входит в AppImage. Включа
- XeSS (суперразрешение) и генерация кадров XeFG через помощник под Wine с общей памятью Vulkan
(прототип моста памяти — `tools/bridge_helper`); входы апскейлера
в виде, совместимом с подходом OptiScaler.
- Генерация кадров (сначала FSR 3.1 FG), маски реактивности и прозрачности для частиц и тумана.
- Генерация кадров (FSR 3.1 FG: в коде есть, скрыта, пока не исправлены артефакты с
апскейлером), маски реактивности и прозрачности для частиц и тумана.
- Исправление гонок в шейдерах FSR 4.1.1 от AMD при ширине вывода, не кратной 64 (например,
1600×900) — так же, как уже исправлена гонка у левого края в FSR 4 v07 при 1080p.
- Проверка AppImage на Steam Deck; вывод HDR.
@@ -385,11 +394,12 @@ OpenSSL, zlib и miniupnpc (`shell.nix`), входит в AppImage. Включа
bbport распространяется по **GNU GPL v2 или новее** ([LICENSE](LICENSE)) — в нём код shadPS4
(GPL-2.0-or-later). Сторонние компоненты сохраняют свои лицензии:
видеоядро и рекомпилятор шейдеров [shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+),
[shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+: рендер и транслятор шейдеров выросли из него и переписаны; онлайн-модуль сохраняет его сетевой код и NP),
[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,
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), шрифты DejaVu, бесшовный кооператив
BBSC (GPL-2.0+, в онлайн-модуле),
[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) и загрузчик взяты из
+3 -2
View File
@@ -50,7 +50,8 @@ if ! fsr_patches_apply --check 2>/dev/null; then
fi
fsr_patches_apply
cmake -S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBB_PGO="$pgo" \
-DBB_LTO="${BB_LTO:-ON}" -DBB_PGO_DIR="$PWD/pgo" >/dev/null
-DBB_LTO="${BB_LTO:-ON}" -DBB_PGO_DIR="$PWD/pgo" > out/gpu-cmake.log
grep '^-- Online module' out/gpu-cmake.log || true
echo "GPU library: PGO $pgo, LTO ${BB_LTO:-ON}"
# A failed GPU build must stop here: an older libbbgpu.so would otherwise be used silently.
if ! ninja -C out/gpu bbgpu $(grep -q "^build bbnet:" out/gpu/build.ninja && echo bbnet) > out/gpu-build.log 2>&1; then
@@ -84,7 +85,7 @@ echo "Built $PWD/out/bb-probe"
# GPU check for run.sh (live_resolution=auto) and the launcher's gamepad list (--gamepads).
"$CC" -std=c11 -O2 -Wall -Wextra -Werror "${includes[@]}" tools/gpu_capabilities.c "${libraries[@]}" -o out/bb-gpu-capabilities
if [[ ${1:-} == --test ]]; then
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -o out/pad-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -lm -o out/pad-test
out/pad-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_runtime.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/runtime-test
out/runtime-test
+73
View File
@@ -0,0 +1,73 @@
# BBSC: Bloodborne seamless co-op in the port
BBSC (seamless co-op for Bloodborne, GPL-2.0-or-later; https://seamlesshunt.com) keeps a co-op
session alive through deaths, lamps, area changes, bosses and the Hunter's Dream, for up to five
hunters. It is written for shadp2p (a shadPS4 fork); the port carries it in its online module.
## Switching it on
Launcher, Online page: "Play online" and "Seamless co-op (BBSC)". Every player needs it on and the
same server: a shadNet server with BBSC's game server next to it. Without the switch nothing of
BBSC is installed and the online module behaves as before.
The launcher's defaults are BBSC's public servers, Seamless Hunt (0.6): shadNet
`play.seamlesshunt.com:31313`, WebAPI `http://play.seamlesshunt.com:31315`, game server
`http://play.seamlesshunt.com` (plain http: BBSC's game server listens on 20443 without TLS). The
account (online id and password) is made on https://seamlesshunt.com; the launcher only takes it.
shadPS4's public shadNet server (`srv.shadps4.net`) accepts only shadPS4's own builds since
2026-10-12: it signs the port out (notification 19) and then refuses its logins for a while.
BBSC's game server on this PC: when the server's own game server does not answer (its
`:20443/bb-eu/ss.info` timed out for players while the rest of the public server worked,
2026-10-14), `run.sh` starts BBSC's `bbsc_gameserver.py` (`scripts/bbsc_gameserver`, from
BBSC-Server-Linux) on 127.0.0.1:20443 and points the game's server there; it sends the summon
calls to the shadNet server's WebAPI, accounts and bells go to the shadNet server as before.
Messages and bloodstains are then this PC's only. It stops with the game; its logs and data are in
`<user>/bbsc/`. `BB_BBSC_LOCAL_SERVER=1` always, `=0` never, `auto` (default) when the server's
does not answer within 5 s. (BBSC's own `BBSC_RELAY` is for the TCP relay in its Windows DLL; the
port's relay is the UDP one on port 3478 of the shadNet server.)
Environment: `BB_ONLINE=1 BB_BBSC=1` (what the launcher sets). BBSC's own switches
(`SHADPS4_BBSC_*`, `SHADPS4_BLOODBORNE_*`, see `gpu/bbnet/shadps4/core/bbsc_persistence.h`) work as
in BBSC: most features are on by default, `=0` turns one off.
## Where it lives
| Part | Files |
|---|---|
| Game code (hooks, byte patches, session policy, world sync) | `gpu/bbnet/shadps4/core/bloodborne_re.cpp`, `bbsc_*.{h,cpp}`, `bbsc_messages.inc` (BBSC's, unchanged) |
| Network (signaling packets, relay fallback, room replay, placement header) | BBSC's changes merged into `gpu/bbnet/shadps4/core/libraries/network` and `np/` (three-way merge against shadp2p 6e97b40, the base of both); the relay as in BBSC's BBport build of 2026-10-14 (below) |
| NP lookup (`sceNpLookupNpId`, a third and later player) | `gpu/bbnet/shadps4/core/libraries/np/np_utility.{h,cpp}` (BBSC's), registered in `bbnet_register` |
| Hooks in the game's code (any game) | `gpu/shim/bbport_guest_code_hooks.{h,cpp}`: shadPS4's `Core::InstallGuestCodeHook` interface, the port's trampolines |
| What BBSC asks of shadPS4 | `gpu/bbnet/include/` (`core/cpu_patches.h`, `core/memory.h`, `core/file_format/psf.h`, `common/memory_patcher.h`), `gpu/bbnet/bbsc_glue.cpp` |
| Local game server | `scripts/bbsc_gameserver/` (BBSC's, unchanged; NoDreamForHunter format, MIT), chosen by `scripts/online.py bbsc-local`, started by `run.sh` |
| Start | `bbnet_image_ready` (`gpu/bbnet/bbnet_glue.cpp`), called by the runtime once the image is mapped and patched (`src/probe.c` -> `runtime_net_image_ready`) |
Differences from BBSC's own builds:
- The game's code stays writable while BBSC is on (`Core::KeepGuestCodeWritable`): BBSC writes
byte patches into it and switches some while the game runs; the port's own code patches leave it
so too.
- Any retail Bloodborne 1.09 (every region runs the same executable) counts as CUSA03173; every
BBSC site is still byte-checked.
- Saves: `BBSC_SAVE_DIR` points at `<user>/savedata/<id>/CUSA00207/SPRJ0005` (the port's layout).
- Its loading screen is a line in the port's overlay (`bbgpu_overlay_banner`); the cinematic
camera (F1, keyboard and mouse) gets the window's events through `bbgpu_add_event_filter`.
- Not ported: the animated title card (off by default in BBSC too), Discord status, its launcher's
account creation and server cards (the port's launcher takes the server address and account).
- The "bell check" trace hook does not install (BBSC's own byte patch is at the same address); it
only logs.
## Updates taken
- 2026-10-14, BBSC 0.0.1 "bug fixes" (its BBport build, `bbport-1.7.2-bbsc.patch`): the relay goes
through the server for every known peer from the first frame (`SHADPS4_P2P_RELAY=auto` or
`direct` = the old "direct first, relay after 2 s", `=0` off), peers are kept by online id with
every endpoint as an alias (the one-way relay behind CGNAT that died after 30 s); the NP lookup
above. Its signaling and matching files were already the same as here. The fixes inside
`bbsc.dll` of that release (death and respawn, sign targeting, relay TCP framing) have no
published source yet; they come in when BBSC publishes it.
## Checks
`out/dev/verify/verify.sh NAME BB_ONLINE=1 BB_BBSC=1`: loads the save offline-capable, 14 self-tests
pass, no faults (2026-10-14). Real co-op needs two players on a BBSC server.
+226
View File
@@ -0,0 +1,226 @@
**English** · [Русский](#русский)
# 0.6-pre1 — pre-release for testing
bbport is the counterpart of Wine + DXVK for a single game: Bloodborne (v1.09) on Linux.
First 0.6 pre-release, built from the `testing` branch. Changes since
[0.51](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.51); details and measurements:
[CHANGES_after_0.5.md](https://github.com/deadinside28/bloodborne_pc/blob/testing/docs/CHANGES_after_0.5.md)
(Russian).
Questions about this project: Discord https://discord.gg/KYZRKk9CB — not the shadPS4 server.
### The new memory model is the default on every GPU
- *Auto* (the launcher's default) is now the new memory and translation model on **every** GPU,
NVIDIA and Intel included, not only on AMD. The game's memory goes through the layer's memory
module everywhere: no sparse binding of the game's memory (NVIDIA's sparse binding froze the
GPU for seconds), so nothing vendor-specific stands in the way. On AMD the module is faster
than the former sparse arena too (RX 7800 XT: a route in Yharnam 4.30 vs 4.60 ms a frame,
Oedon Chapel 234 vs 213 FPS), and only with it a release without the game's profile is right.
- The old model (as in 0.3) stays in the launcher: *Memory model → Old* (`BB_PC_MODEL=0`). A
driver that fails the startup checks still falls back to VRAM copies by itself.
- **Please report** how the new model runs on NVIDIA and Intel (FPS, stutter, VRAM), with the
log.
### New
- **Seamless co-op (BBSC)**: the session survives deaths, lamps, area changes and bosses, up to
five hunters (BBSC, GPL-2.0+, carried in the online module). Launcher → *Online → Seamless
co-op (BBSC)*, on by default; every player needs it and the same server.
- **Online servers: Seamless Hunt by default** (BBSC's public servers): shadNet
`play.seamlesshunt.com:31313`, WebAPI `http://play.seamlesshunt.com:31315`, game server
`http://play.seamlesshunt.com`. Create the account at https://seamlesshunt.com and enter it in
the launcher. Settings saved with the old defaults switch over by themselves.
shadPS4's public server (`srv.shadps4.net`) accepts only shadPS4's own builds since
2026-10-12: it signed the port out 20–40 s after login (in game: "Network connection lost")
and then refused logins for a while. The port now reports its version to the server and, if
refused, stays offline from the start with the server's reason in the log, without the popup
in the middle of play and without retrying.
- **Any aspect ratio, live, through hooks** (the game's code is not patched): 4:3, 16:10, 16:9,
21:9, 32:9; aspect ratio and resolution are separate settings (game menu *Display*, overlay,
launcher). The camera shows more at the sides (wider) or above and below (taller), the HUD is
anchored to the screen's edges, full-screen fills and menu backdrops cover the whole output,
markers over characters stay on them (#183, #151, #76, #35, #82). The removed variant patched
the game's frame size.
- **Frame rate through hooks, not patches**: above 30 FPS the game's main loop and 48 places tied
to the 30 FPS frame use the real frame time (found anew: timers of physics-driven body parts ran
6–7× faster at 200 FPS; running fell to walking above ~90 FPS). The community patches remain
for comparison (`BB_FRAME_HOOKS=0`).
- **Keyboard and mouse:**
- mouse look turns the game's camera directly (no stick dead zone, acceleration or speed
limit); where the camera does not take it (lock-on, menus) the mouse drives the right stick
as before;
- a **mouse pointer in the game's menus**: hover selects, left click confirms, right click goes
back, the wheel scrolls;
- **a layout close to Dark Souls III**: WASD move, Space dodge/run, F/Enter interact, LMB
attack, Shift+LMB strong attack, RMB shoot, Q blood vial, E weapon transform, Tab/R quick
item, 1/2 left/right weapon, 3/4 up/down d-pad (quick items), C lock-on, Esc menu, G gestures,
Backspace personal items. Bindings may be chords ("Left Shift+Mouse Left"). Your own
bindings are kept;
- **button prompts** for the device in use, also in the HUD ("Examine" at lamps, gravestones,
items): the bound key or mouse button, or the buttons of an Xbox or another gamepad;
PlayStation icons only with a PlayStation pad.
- **The game's occlusion queries are real by default** in the new model, counted at the game's
resolution whatever the render scale (the lamp glow no longer flashes grey with the upscaler).
- **The port's own code instead of shadPS4** (except the online module): command processor, GCN
definitions, shader translator, renderer, caches and platform libraries
(`gpu/VENDOR.txt`). The translator gives byte-identical SPIR-V on the game's shader corpus.
### Fixes
- NVIDIA: 0.51 did not start (`CreateDevice: ErrorInitializationFailed`, code 134/-1): the
launcher now provides `libcuda`, and the device is created again without NGX extensions if
needed (DLSS then off, FSR 3.1) (#135, #147, #152, #187, #189).
- Crashes at RIP=0 on NVIDIA and Intel: an extension was enabled but its commands were missing
(#161, #177, #181).
- New model at 4K (scene 2560×1440): a frame-sized buffer was written over the bus; 95 → 132 FPS
on an RX 7800 XT (#149).
- New model: FPS fell ~28 % after 15–20 minutes of play with lamp travel; now stays.
- New model: stale VRAM copies after a race in the write-trap handler (stretched "exploding"
vertices, possibly some GPU hangs).
- Loading hang at lamp travel: thread handles above 40 bits never matched the owner stored by
the game's lock (analysis by the author of PR #193; reimplemented).
- Crash at `0x2199310` (the game's per-frame GX heap full) (#162, #192).
- Old model: VRAM copies accumulated until "VRAM is full" after an hour or two on 4 GB cards
(#142).
- Empty character-creator preview (#178).
- Decoder and format asserts during draw preparation (#144, #165, #166).
- In-game menu language with `--play` / stuck in bbport.ini (#134).
- Steam Deck Game Mode: pressure-vessel's Vulkan variables hid the bundled drivers (#172).
- FSR 4.1.1 assets on RX 9000: the FP8 passes are now recorded (#186).
- `AppImage --play` failed at once ("Software caused connection abort").
- HUD: white streaks at 21:9.
- Exit after the game stops itself: a crash in the online module (`libbbnet.so`, "terminate
called without an active exception"); the log now names the game code that stopped.
- Pipelines from the disk cache: geometry-shader pipelines got an empty fetch shader; on NVIDIA
the clip-distance discard shader was not saved; a draw without a pixel shader took the previous
draw's fragment settings.
- Mouse: the camera no longer jumps when the mouse is captured.
- Building: Debian without a miniz package (#136); the build names the packages missing for the
online module (#145).
### Faster
- The draw-recording stage spends 6 % less on buffer bindings through the memory module; it
matters on slower CPUs (on the test machine the frame is GPU-bound).
**Known limits:** real co-op through Seamless Hunt was not tested by the author yet (one
account); the new model on NVIDIA and Intel needs your reports. Frame generation is in the code
but hidden until its artifacts with upscaling are fixed.
The AppImage contains no game files. AMD's FSR 4 model assets are not bundled. `SHA256SUMS`
accompanies the download.
## Русский
# 0.6-pre1 — пререлиз для тестирования
bbport — аналог Wine + DXVK для одной игры: Bloodborne (v1.09) на Linux. Первый пререлиз 0.6,
из ветки `testing`. Изменения после
[0.51](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.51); подробности и замеры —
[CHANGES_after_0.5.md](https://github.com/deadinside28/bloodborne_pc/blob/testing/docs/CHANGES_after_0.5.md).
Вопросы по проекту: Discord https://discord.gg/KYZRKk9CB — не сервер shadPS4.
### Новая модель памяти — по умолчанию на любой видеокарте
- *Авто* (по умолчанию в лаунчере) — теперь новая модель памяти и трансляции на **любой**
видеокарте, включая NVIDIA и Intel, а не только на AMD. Память игры везде идёт через модуль
памяти прослойки: без sparse-привязки памяти игры (на NVIDIA sparse binding держал видеочип
секундами), поэтому ничего зависящего от производителя на пути нет. На AMD модуль быстрее
прежней sparse-арены (RX 7800 XT: маршрут по Ярнаму 4,30 против 4,60 мс на кадр, часовня Одеона
234 против 213 FPS), и только с ним верно идёт издание без профиля игры.
- Старая модель (как в 0.3) остаётся в лаунчере: *Модель памяти → Старая* (`BB_PC_MODEL=0`).
Если драйвер не проходит проверки при запуске, порт сам переходит на копии в VRAM.
- **Пожалуйста, сообщайте**, как новая модель работает на NVIDIA и Intel (FPS, рывки, VRAM), с
журналом.
### Новое
- **Бесшовный кооператив (BBSC)**: сессия переживает смерти, лампы, смену локаций и боссов, до
пяти охотников (BBSC, GPL-2.0+, в онлайн-модуле). Лаунчер → *Онлайн → Бесшовный кооператив
(BBSC)*, по умолчанию включён; нужен у всех игроков и один сервер.
- **Серверы по умолчанию — Seamless Hunt** (публичные серверы BBSC): shadNet
`play.seamlesshunt.com:31313`, WebAPI `http://play.seamlesshunt.com:31315`, игровой сервер
`http://play.seamlesshunt.com`. Учётную запись создайте на https://seamlesshunt.com и введите
в лаунчере. Сохранённые старые адреса по умолчанию меняются сами.
Публичный сервер shadPS4 (`srv.shadps4.net`) с 12.10.2026 принимает только сборки shadPS4: он
выкидывал порт через 20–40 с после входа (в игре — «Потеряно соединение с сетью»), а потом
какое-то время отклонял вход. Теперь порт сообщает серверу свою версию и при отказе с самого
начала остаётся офлайн — с причиной от сервера в журнале, без окна посреди игры и без повторных
попыток.
- **Любое соотношение сторон, на лету, хуками** (код игры не патчится): 4:3, 16:10, 16:9, 21:9,
32:9; соотношение и разрешение — отдельные настройки (меню игры *Изображение*, оверлей,
лаунчер). Камера видит больше по бокам (шире) или сверху и снизу (выше), HUD прижат к краям
экрана, полноэкранные заливки и подложки меню — на весь вывод, метки над персонажами остаются на
них (#183, #151, #76, #35, #82). Прежний вариант с патчем размера кадра игры убран.
- **Частота кадров — хуками, а не патчами**: выше 30 FPS главный цикл игры и 48 мест, привязанных к
кадру 30 FPS, идут по реальному времени кадра (найдено заново: таймеры частей тела, которые
двигает физика, на 200 FPS шли в 6–7 раз быстрее; выше ~90 FPS бег падал до шага). Патчи
сообщества остались для сравнения (`BB_FRAME_HOOKS=0`).
- **Клавиатура и мышь:**
- камера мышью поворачивает камеру игры напрямую (без мёртвой зоны стика, разгона и предела
скорости); где камера поворот не берёт (захват цели, меню) — мышь, как раньше, ведёт правый
стик;
- **указатель мыши в меню игры**: наведение выбирает, левая кнопка — подтвердить, правая —
назад, колесо — прокрутка;
- **раскладка как в Dark Souls III**: WASD — движение, Space — уклонение/бег, F/Enter —
взаимодействие, ЛКМ — атака, Shift+ЛКМ — сильная атака, ПКМ — выстрел, Q — флакон крови, E —
трансформация оружия, Tab/R — быстрый предмет, 1/2 — оружие левой/правой руки, 3/4 —
крестовина вверх/вниз (быстрые предметы), C — захват цели, Esc — меню, G — жесты, Backspace —
личные вещи. Назначения могут быть сочетаниями («Left Shift+Mouse Left»). Свои назначения
сохраняются;
- **подсказки кнопок** для используемого устройства, и в HUD тоже («Осмотреть» у ламп,
надгробий, предметов): назначенная клавиша или кнопка мыши либо кнопки Xbox или другого
геймпада; значки PlayStation — только с геймпадом PlayStation.
- **Occlusion-запросы игры по умолчанию настоящие** в новой модели, со счётом в разрешении игры
при любом масштабе рендера (свечение лампы больше не вспыхивает серым с апскейлером).
- **Свой код вместо shadPS4** (кроме онлайн-модуля): командный процессор, описания GCN, транслятор
шейдеров, рендер, кэши и платформенные библиотеки (`gpu/VENDOR.txt`). Транслятор даёт побайтно
тот же SPIR-V на корпусе шейдеров игры.
### Исправления
- NVIDIA: 0.51 не запускалась (`CreateDevice: ErrorInitializationFailed`, код 134/-1): лаунчер
теперь даёт `libcuda`, а устройство при нужде создаётся заново без расширений NGX (тогда DLSS
выключен, FSR 3.1) (#135, #147, #152, #187, #189).
- Падения с RIP=0 на NVIDIA и Intel: расширение включено, а его команд нет (#161, #177, #181).
- Новая модель в 4K (сцена 2560×1440): буфер размером с кадр писался через шину; 95 → 132 FPS на
RX 7800 XT (#149).
- Новая модель: FPS падал на ~28 % после 15–20 минут игры с перемещениями по лампам; теперь нет.
- Новая модель: устаревшие копии в VRAM после гонки в обработчике ловушек записи («взрывающиеся»
растянутые вершины, возможно, часть зависаний видеочипа).
- Зависание загрузки при перемещении по лампам: дескрипторы потоков выше 40 бит не совпадали с
владельцем, которого хранит блокировка игры (анализ автора PR #193; сделано заново).
- Падение на `0x2199310` (переполнена покадровая куча GX игры) (#162, #192).
- Старая модель: копии в VRAM копились до «VRAM is full» через час-два на картах 4 ГБ (#142).
- Пустой предпросмотр в редакторе персонажа (#178).
- Остановки декодера и форматов при подготовке отрисовок (#144, #165, #166).
- Язык меню игры с `--play` / застревал в bbport.ini (#134).
- Steam Deck, игровой режим: переменные Vulkan от pressure-vessel скрывали драйверы из пакета
(#172).
- Ассеты FSR 4.1.1 на RX 9000: теперь записываются и проходы FP8 (#186).
- `AppImage --play` падал сразу («Software caused connection abort»).
- HUD: белые полосы при 21:9.
- Выход после того, как игра сама остановилась: падение в онлайн-модуле (`libbbnet.so`,
«terminate called without an active exception»); журнал теперь называет код игры, который её
остановил.
- Конвейеры из кэша на диске: с геометрическим шейдером получали пустой fetch-шейдер; на NVIDIA
не сохранялся шейдер отбрасывания по clip distances; отрисовка без пиксельного шейдера брала
настройки фрагментной стадии предыдущей.
- Мышь: камера больше не дёргается при захвате мыши.
- Сборка: Debian без пакета miniz (#136); сборка называет пакеты, которых не хватает онлайн-модулю
(#145).
### Быстрее
- Стадия записи отрисовок тратит на 6 % меньше на привязки буферов через модуль памяти — важно
на процессорах слабее (на тестовой машине кадр упирается в видеокарту).
**Известные ограничения:** настоящий кооператив через Seamless Hunt автор ещё не проверял (одна
учётная запись); новая модель на NVIDIA и Intel ждёт ваших отчётов. Генерация кадров в коде есть,
но скрыта, пока не исправлены её артефакты с апскейлером.
AppImage не содержит файлов игры. Ассеты модели FSR 4 от AMD не входят. К загрузке приложен
`SHA256SUMS`.
+302
View File
@@ -157,3 +157,305 @@
что рецепт nixpkgs ждёт его выход `spirv2dxil`; в пакет он не попадает.
- Итог: AppImage 925 → 399 МБ, пакет без сжатия 2,85 → 1,06 ГБ. Проверено: лаунчер открывается,
игра из пакета стартует на встроенном RADV до титульного экрана.
# После 0.51: уход от эмуляции
## Occlusion-запросы игры — по умолчанию настоящие (новая модель)
- Были выключены (заглушка «всё видно»): с апскейлером свечение лампы однажды вспыхивало серым.
Найдено: проход, нарисованный в другом размере, чем цель игры (уменьшенная сцена; интерфейс в
разрешении вывода), считал пиксели в своём размере — игра получала 1/4 или 1/9 счёта PS4 (лампа
в часовне Одеона: 84 286 при 960×540 и 37 455 при 640×360 вместо 337 280 при 1920×1080).
- Теперь у каждого отрезка запроса — коэффициент его прохода (`target_scale` растеризатора,
8.8 в фиксированной точке), шейдер сложения (`occlusion_resolve.comp`) умножает на него: при
пресетах 0, 3, 4 игра читает одинаково — 337 249 / 337 149 / 337 122 и 43 814 / 43 761 / 43 750.
- Вспышки не воспроизвелись: 8 направлений с неподвижной камерой по 25 кадров (разброс яркости
≤ 1 %, лампа в кадре в 6 из 8) и поворот камеры (80 кадров) при 640×360, вывод 3840×2160 с
FSR 4.1.1. FPS тот же (100–104 против 99–101 с заглушкой). Самотест PM4 — 11 из 11.
`BB_OCCLUSION_QUERIES=0` — заглушка. `BB_OCCLUSION_SAMPLES=N` — диагностика: сколько пикселей
прочла игра.
## Модуль памяти слоя — по умолчанию на всех видеокартах (новая модель)
- На AMD новая модель держала память игры в sparse-арене, на остальных — в модуле памяти слоя (без
sparse-привязки). Теперь модуль и на AMD: на RX 7800 XT он быстрее арены — маршрут по Ярнаму
4,30 против 4,60 мс на кадр (p99 5,8–6,0 против 6,6), Сон охотника с поворотом камеры 4,8–5,0
против 5,2–5,3 мс, часовня Одеона 234 против 213 FPS.
- И только с ним игра верна без профиля игры: с ареной в такой игре (так шло американское издание
до 0.51, #119) чтение 512 КБ окна из устаревшей VRAM-копии затирало учёт аллокатора, записанный
процессором, — падение на входе в мир (гость `0x208596a`). С модулем без профиля — 196 FPS, без
падения.
- `BB_LAYER_MEMORY=0` — sparse-арена (только AMD), как раньше.
## Нагрузка потоков: где упор (новая модель, RX 7800 XT, Ryzen 7 5700X3D)
Сон охотника (горящий), стоя, ~860 draw за кадр, FSR 4.1.1, пресет 3 (сцена 960×540 → 1080p).
- **Упор — видеокарта**: занята ~97 % (sysfs). Из 5,05 мс кадра FSR 4.1.1 — 1,09 мс, шейдер
копирования игры — 0,31 мс (58 dispatch), полноразмерные копии сцены для игры — 0,22 мс,
промежутки между отправками — 0,6 мс.
- Процессор: поток записи draw (`bb:DrawRec`, стадия B) — 92–95 % ядра, из них 12–15 % —
ожидание пакетов; декодер PM4 (`GpuComm`) — 66 %; главный поток игры — 53 %, её `GXRenderThread`
— 27–30 %. Игра ждёт предыдущий кадр 2,7–2,9 мс на кадр: запас у неё есть.
- С лёгкой нагрузкой на GPU (FSR 3, пресет 4) потолок процессора — ~225 FPS: тогда упор —
стадия B (занята ~90 %), один поток. На процессорах слабее упор в неё наступает раньше.
- Игра отправляет ~62 командных буфера за кадр, тремя пачками. Глубина перекрытия кадров
(`BB_SUBMIT_AHEAD` 2–6) FPS не меняет. Интервал отправок (`BB_HONEST_FLUSH_US`) 500–1000 мкс —
+0,5 %, в пределах разброса, ценой +5 % процессора на потоках записи Vulkan; 4000–8000 —
хуже (188–196 FPS). Оставлено как было.
- Статистика (`BB_FRAME_STATS=1`) стоит ~2 % FPS: её таймеры стоят в пути каждой привязки.
**Ускорено — стадия B, привязки буферов через модуль памяти** (было 17 % её времени, стало 12 %):
- Очередь переноса блоков в VRAM (`LayerQueuePromotion`) запоминала диапазоны в 256 ячейках по
первому блоку: диапазоны из одного блока (потоки вершин одного буфера) вытесняли друг друга,
каждый восьмой вызов заново обходил ~18 блоков под блокировкой модуля. Теперь — хеш по всему
диапазону (4096 ячеек) и ещё память по блокам (данные, которые игра пишет в кольца, каждый
кадр по новым адресам); раз в секунду всё смотрится заново. 4,2 % → 0,6 % времени стадии B.
- `LayerInPlace` (буфер куска памяти игры и смещение) запоминается до следующего снятия
отображения игрой: 2,0 % → 1,1 %. Память `Resolve` — тот же хеш, 2048 ячеек.
- Таблицы — в куче: `thread_local`-массивы на сотни килобайт ложатся в статический TLS каждого
потока, и потоки игры с маленьким стеком не создавались (`pthread_create failed: 22`).
- Итог: работа стадии B −6 % (её время на пакеты 86 → 81 %, ожидание пакетов 8 → 13 %); FPS на
этой машине не изменился — A/B поочерёдно, с одного замороженного сохранения: FSR 4.1.1
200,1 → 199,7, FSR 3 пресет 4 226,0 → 225,8 (разброс ±1). Ни один ресурс не упирается в 100 %:
GPU ~90 %, стадия B ~85 % работы, игра ждёт кадр 2,7 мс; время кадра задаёт цепочка «игра →
декодер → запись → GPU», а игре можно быть впереди GPU лишь на кадр (её правило с PS4: дальше
её куча течёт). Запас стадии B пригодится на процессорах слабее. Прогулка 2 минуты, кадры, игра
без профиля, самотест PM4 (11 из 11) — без ошибок.
- Ещё мелочи стадии B: `GetDrawOffsets` принимал тип описания выборки вершин без `const` — каждый
draw копировал его со списком атрибутов (malloc); масштаб occlusion-прохода брал мьютекс на
каждом draw; метка времени записи GPU по огромному диапазону шейдера копирования (~60 раз за
кадр, по записи на каждый МиБ) — теперь раз за отправку.
- Найдено, но не сделано: между соседними dispatch шейдера копирования игра ставит `ACQUIRE_MEM`
(сброс кэшей), но не `CS_PARTIAL_FLUSH` — на PS4 они перекрываются, у нас между каждыми двумя
полный барьер (0,31 мс GPU за кадр на 58 dispatch). Снять его — до ~3 % GPU, но риск гонки в
данных игры; без проверки глазами не трогал.
Инструменты: `BB_FPS_PRINT=1` — частота кадров раз в 5 с без статистики
(`tools/restart.sh` с `BB_FRAME_STATS=0` включает её сам); в `BB_TIMELINE` — простои стадии B;
в `BB_GPU_PROFILE` — начало каждой отправки (видно, что промежуток между отправками — не
копирования в её начале: 0,006 мс на кадр).
## Камера мышью — нативно, без правого стика
- Было: движение мыши превращалось в отклонение правого стика, а дальше — путь стика в игре: мёртвая
зона, кривая отклика, разгон и предельная скорость поворота. Мышь ощущалась как стик.
- Найдено (обратная разработка): камера игры (`ChrExFollowCam`, обновление `0x143ac60`, `rdi` —
камера) каждый кадр выводит углы из положения камеры вокруг персонажа — наклон в `+0x140`
(ограничен `+0x1f0`…`+0x1ec`), поворот в `+0x144` (в пределах ±π); в обычном режиме
`0x143cde0`…`0x143cdf0` считает приращение «стик камеры × скорость × время кадра» и прибавляет
его к углам, по ним камера ставится заново. Путь стика к ней: устройство пада (`+0x1e4`…),
виртуальные входы 62/66 (X) и 63/67 (Y), действия `0x48`/`0x49`, манипулятор (`+0x40`).
- Теперь: перехват в `0x143cde0` прибавляет к этому приращению поворот мыши с прошлого обновления
(радианы, забирается атомарно) и отмечает ввод активным, как делает стик (иначе игра сразу
возвращает наклон к углу покоя). Мышь поворачивает камеру ровно на столько, на сколько
сдвинулась, без мёртвой зоны, разгона и предела скорости, независимо от FPS; ограничение наклона
и обход препятствий — игровые. Чувствительность 1 — 0,0015 рад на пиксель (1000 пикселей —
86°). Где камера поворот не берёт (захват цели, меню, ролики: её обычное обновление не идёт,
поворот не забран 100 мс), мышь, как раньше, ведёт правый стик — захват цели переключается
движением мыши. `BB_NATIVE_MOUSE=0` — только стик.
- Клавиатура: в игре (сборка PS4) нет своего устройства клавиатуры — библиотека ввода
(`DLUserInput*`) знает только пад, а действия игры привязаны к его кнопкам. Клавиши остаются
назначениями на эти действия, как и было: «нативнее» их подать в игру нечем.
- Проверено: поворот мышью влево/вправо и вверх/вниз (направления — как у стика), минута ходьбы
с поворотами мышью, без ошибок. Для тестов `BB_PAD_FILE` понимает `mdx=`/`mdy=` (пиксели за
чтение пада).
## Ультраширокие и другие соотношения сторон — на лету, хуками
- Отдельно **соотношение сторон** (Авто — по монитору, 4:3, 16:10, 16:9, 21:9, 32:9) и
**разрешение** из списка этого соотношения, от 360–480 строк (4:3: 640×480 … 2880×2160; 16:10:
640×400 … 3840×2400; 16:9: 640×360, 854×480, 960×540, 1280×720 … 3840×2160; 21:9: 860×360,
1280×540, 1720×720, 2560×1080, 3440×1440, 3840×1600, 5120×2160; 32:9: 1280×360 … 7680×2160;
«Авто» добавляет размер самого монитора). Смена соотношения сохраняет высоту. Меняются на лету
(`live_resolution=1`; при патче разрешения на старте — после перезапуска). В bbport.ini:
`aspect_ratio=auto|4:3|16:10|16:9|21:9|32:9`, `output_res=WxH`. Меню игры, оверлей и лаунчер.
- Кадр игры остаётся 1920×1080 — код игры не патчится. Вместо этого:
- камера (хуки на 5 копированиях параметров проекции в менеджере камеры, `0x1436c2c` …):
соотношение сторон вывода; шире — больше видно по бокам (вертикальный угол прежний), выше
(16:10) — угол по вертикали расширяется (`2·atan(tan(fov/2)·(16/9)/A)`, x87 в заглушке),
ширина обзора прежняя;
- сцена рисуется в прокси-целях с соотношением вывода (масштаб по осям разный, `FragCoord`
пересчитывается по осям), апскейлер выдаёт кадр вывода;
- интерфейс (Scaleform): игра каждый кадр задаёт вьюпорт каждому фильму (`0x1f59100` →
`MovieImpl::SetViewport` `0x1f510`, режим ShowAll); хук подставляет пиксельное соотношение
сторон вывода (`Viewport::AspectRatio` = (ширина/высота)/(16/9)) — фильм сам показывает свой
экран 16:9 посередине более широкой (высокой) видимой области, сжатой в кадр 1920×1080, а вывод
растягивает её обратно; то, что за краями экрана игры, рисуется, а не отбрасывается;
- HUD прижат к краям вывода и следует за сменой соотношения: группы `fe.gfx` двигаются так же,
как игра двигает свои клипы — `SetMember("x"/"y")` объектов Scaleform (ObjectInterface игры:
GetMember `+0x28`, SetMember `+0x30`) в ежекадровом обновлении игрового экрана (`0x1bf9e10`;
HUD — `экран+0xa80`, значения берутся и отпускаются в том же кадре: фильм HUD выгружается с
областями): полосы, предметы, эффекты — к левому краю, отголоски крови, озарение и значок
автосохранения — к правому; на 4:3/16:10 верхние — вверх, полоса босса — вниз
(`BB_HUD_ANCHOR=0` — в середине 16:9);
- полноэкранные заливки и подложки интерфейса (затемнение меню — игра рисует его чуть больше
своего экрана, — «Вы погибли», затухания) растягиваются на весь вывод: вершинный шейдер
переносит в углы вывода вершины, лежащие в углах экрана игры или за ними (сразу по x и по
y); то, что HUD вынес за край только по одной оси, маски Scaleform и всё под ними (тест
трафарета: например, лента иконок предметов, скользящая в слоте) остаются на месте
(`motion_param` этих отрисовок; кэш шейдеров пересобирается один раз: версия 18); большие
картинки (титульная, 2048×1024) и текст — без растяжения; `BB_UI_DRAWS=<файл>` — отрисовки
интерфейса одного кадра в лог;
- кадры без сцены (титульный экран, загрузки): поля за экраном игры чёрные (фильмы заливают
свою расширенную область цветом фона, серым у титульного);
- метки над персонажами (захват цели, полосы здоровья врагов, урон): хуки на трёх местах их
перевода в координаты интерфейса (`0x1644379`, `0x1644c45`, `0x16452b7`) умножают на вектор
порта — метка встаёт над своим персонажем; за краями области 16:9 их скрывает сама игра.
- Меню игры: страница «Изображение» — соотношение сторон, разрешение, апскейлер, пресет, резкость,
детализация; выбор — строкой игры со всплывающим списком (та же, что у её языковых настроек,
`0x1b29370`) вместо полосок 0–10. Сквозь список были видны строки ниже (в макете
ControllSetting строки сложены «вверх»): рантайм отдаёт игре копию `menu/optionsetting.gfx`, где
у этого макета глубины строк развёрнуты, как у её LanguageSetting (меняются только поля глубины;
файл на диске не трогается; `BB_GAME_MENU=0` — как есть). После смены соотношения строка
«Разрешение» сразу показывает размер в новом соотношении (та же высота), а её список — ровно
размеры этого соотношения: строка игры держит свои значения (`строка+0x3d8`, по 0x48 байт, число
`+0xce0`), их тексты берутся из буферов порта при показе; хук ввода строк страницы (`0x1b26b00`,
поток меню игры) переписывает значения и число, и строка показывает их заново (`0x1b2b7c0` —
текст значения, `0x1ad06e0` — тексты списка). Счётчик FPS — в оверлее и лаунчере.
- Также: «Авто» знает монитор окна (и его смену); FSR 4 после неудачи на неподдерживаемом выводе
(7680×2160 без ассетов 4320) снова пробуется при смене вывода, а не остаётся на FSR 3.1.
- Ограничения: FSR 4.1.1 — вывод до 3840×2160 (5120×2160, 5120×1440, 7680×2160 — FSR 3.1);
метки над персонажами за краем области 16:9 скрывает сама игра (её проверка границ экрана).
- Убрано: прежний вариант через патч кода при загрузке (`bloodborne_display.cpp`, `BB_GAME_FRAME`,
кадр игры шире 1920×1080).
## NVIDIA: 0.51 не запускалась (CreateDevice: ErrorInitializationFailed) (#135, #147, #152)
- С DLSS (библиотека NVIDIA рядом) устройство Vulkan создаётся с расширениями NGX; драйвер их
объявляет, но для них грузит `libcuda.so.1` (и нужен модуль `nvidia-uvm`). Лаунчер AppImage
выкладывал в свою папку библиотек NVIDIA только `libnvidia-*`, `libGLX_nvidia`,
`libEGL_nvidia` — `libcuda` не находилась, `vkCreateDevice` давал ErrorInitializationFailed, а
игра шла дальше и падала на «Invalid device» (код 134/-1); в т. ч. на картах без DLSS (GTX
1660), раз библиотека DLSS лежит в AppImage.
- Теперь: лаунчер кладёт туда и `libcuda.so*` (`launcher/bbport_vulkan.py`); если устройство с
расширениями NGX всё же не создаётся — второе создание без них, DLSS выключен (в журнале
причина), игра на FSR 3.1, выбор DLSS в настройках остаётся; без устройства вообще — понятная
остановка, а не падение дальше. (Те же исправления предлагали PR #137 и #140.)
## Новая модель: буферы размером с кадр снова в VRAM (#149)
- RX 7900 XT, вывод 3840×2160 с патчем разрешения на старте (сцена 2560×1440): 0.4 — 136 FPS,
0.51 — 58 (старая модель — 128). Здесь (RX 7800 XT, та же конфигурация): новая модель 95 FPS,
старая 130; GPU 10.5 мс на кадр против 7.85. Попиксельный проход (`cs 3d5ebf4e`, 58880 групп)
писал буфер размером с кадр (19.9 МБ) в память игры через шину, 1.8 ГБ/с, и соседние проходы
его там же читали.
- Причина — модуль памяти слоя (с 0.5 по умолчанию): привязка, которую GPU пишет, должна целиком
лежать в одном зеркале VRAM, иначе весь её диапазон уходит в память игры, а со второго раза —
навсегда (`dynamic_blocks`). У этого буфера 303 блока из 305 были в зеркале; первый и последний
блок он покрывает лишь частично (начало не выровнено на 64 КБ), остальные их байты модуль не
знает (ни загрузчик, ни GPU их не писал) — в зеркало их не брали. С разрешением 1080p буферы
так не ложились.
- Теперь такие крайние блоки записываемой привязки идут в зеркало с ней (если процессорную запись
туда ловит ловушка — `layer_edge_blocks`): процессор, записав рядом, отправляет блок на место, и
байты GPU в нём сначала копируются обратно (только записанные GPU диапазоны). Здесь: 95 → 132
FPS (старая модель — 130); живая смена разрешения и обычная конфигурация — без изменений
(120 и ~194 FPS); кадры те же. Диагностика: `BB_RESIDENCY_MIX=2` (привязки «на месте» с
наибольшим объёмом и почему).
## Старая модель памяти: VRAM больше не копится до падения (#142, 4 ГБ)
- Старая модель (по умолчанию на NVIDIA) держала в VRAM каждую копию памяти игры, которую хоть
раз копировала: область за областью «guest blocks in VRAM» росли (1.6 ГБ на 4-гиговой карте) и
через час-два «Failed to allocate .. MiB for guest memory copies: VRAM is full» останавливал
игру (у NVIDIA разреженная арена берёт только VRAM).
- Теперь: на картах до 8 ГБ копии, которые GPU не использовал минуту и в которых нет записанного
им, уходят из VRAM (при следующем использовании загружаются снова), их места занимают новые;
при нехватке VRAM — после 5 с, на любой карте; если память на новый блок всё же не выделилась —
сначала уходят копии, не использованные секунду, и новые блоки встают на их места; опустевшие
куски возвращаются драйверу. (`BB_VRAM_IDLE_SECONDS=N` — своё время и на любой карте;
`BB_ARENA_VRAM_ONLY=1` с `BB_RESIDENCY_VRAM_LIMIT_MB` — проверка, как на такой карте.)
- Если рабочему набору области не хватает VRAM целиком, игра по-прежнему остановится — но
накопления за часы больше нет.
## Частота кадров — хуками, без патчей; привязанное к кадру — по времени кадра
- Выше 30 FPS («Без ограничения», 60, 90) частоту кадров ведут хуки в коде игры
(`gpu/shim/bbport_frame_rate.cpp`), а не патчи сообщества Uncap FPS++ / 60 FPS++ / 90 FPS++ и
Sprint Fix (`BB_FRAME_HOOKS=0` — снова патчи, для сравнения). Ставятся при загрузке образа, до
запуска игры; каждое место сверяется побайтно.
- Главный цикл (`0x2018d20`): пауза кадра (`0x2034770`) ждёт кадра 1/30 с и измеряет кадр в
`+0x264`, затем игра шагает на фиксированные 1/30 с (`0x2018e36`). Хуки держат режим паузы
на одном vblank с целью кадра 1/240 (60/90 и `BB_FPS_LIMIT` — их кадр: ограничивает сама
игра, до симуляции кадра, а не задержкой готовых кадров), а шаг — измеренное время кадра
(1/240…1/30 с; ниже 30 FPS игра замедляется, как всегда). Частота смены кадров, которую
просит игра (`sceVideoOutSetFlipRate`, каждый второй vblank), не действует.
- То, что игра привязывает к кадру 30 FPS, читает значение текущего кадра: инструкции берут
константу со страницы рядом с образом (меняется только адрес операнда), значение
пересчитывается каждый кадр. Шаг 1/30 с → время кадра; доля f за кадр → `1−(1−f)^n`,
затухание k за кадр → `k^n`, приращение за кадр → `·n` (n — длина кадра в кадрах 30 FPS).
Всего 48 мест и 2 фиксированных шага подсистем (`0x17f9b71`, `0x1572a7d`): «один кадр» в
эффектах и таймерах, шаги подсистем (в т. ч. шаг, который игра в своём режиме 60 Гц делает
1/60), сглаживание камеры (1/30 и 0.1 за кадр), гашение движения персонажа (0.95 за кадр),
рампа ±0.1 за кадр (`0x19084b0`; в Uncap FPS++ её не было), жёсткость ткани (1.0 при 30 FPS,
0.5 от 60 FPS — как у патчей 60 FPS).
- Найдено заново (не было ни в одном патче): таймеры частей тела, которые двигает физика
(`0x1515ec0` → `0x180df90`, `0x1811650`, каждая часть каждый кадр) шли на 1/30 с за кадр — на
200 FPS в 6–7 раз быстрее; и гашение «упора в стену» у бега (×0.8 за кадр).
- Бег: проверка «упора в стену» (`0x1514c24`) сравнивала расстояние за кадр ×30 — делится на
время кадра (скорость при любой частоте; иначе выше ~90 FPS бег падал до шага).
- Перевод длительностей из кадров 30 FPS в секунды (затухания эффектов, звука, анимаций)
остаётся 1/30: то, что их отсчитывает, идёт по реальному времени (патчи укорачивали их в
2–8 раз). Остальные места с 1/30 в коде (67 непосредственных значений, ~100 функций с 1/30,
30, 1/60, 60) проверены по частоте вызовов (`BB_RE_ENTRY=адреса` — сколько раз и с каким
xmm0 вызывается функция) — в игре каждый кадр их не зовут, либо это пересчёт единиц.
- Проверка: из одного сохранения 2 с шага и 3 с бега — при 30 FPS, 60 и ~200 FPS персонаж в
одних и тех же местах.
- Интерфейс (Scaleform) продвигается каждый кадр на реальное время (`MovieImpl::Advance`
`0x20f00`, `BB_RE_ADVANCE=1`), но его ролики сделаны на 30 кадров в секунду (все 102 в
`menu/`): анимации их шкал шагают по 1/30 с.
## AppImage: `--play` снова запускает игру
- `Bloodborne-bbport-x86_64.AppImage --play` (игровой режим Steam Deck, ярлыки) падал сразу:
«python3: Software caused connection abort». Среда AppImage держит смонтированный образ, пока
программа держит унаследованный от неё канал — первый свободный дескриптор, 3. С `--play`
`run.sh` заменяет собой лаунчер, а сохранение журнала (`exec 3>…`) закрывало этот канал: образ
отмонтировался под работающим скриптом. Через кнопку «Играть» канал держал сам лаунчер, поэтому
там ошибки не было. Теперь журнал пишется в дескриптор, который выбирает bash (10 и выше).
## Новая модель: FPS больше не падает за долгую игру с телепортами
- Пользователь: «просел кадров на 10». Замер в одной точке (Сон охотника, у Куклы, ~885
отрисовок, видеочип загружен на 99 %): свежий запуск 76,6 FPS, после 17 минут игры с
телепортами по лампам — 55,3 (−28 %); выход в меню и повторная загрузка в том же процессе не
помогали (54,2). Доля загруженных данных игры с копией в VRAM за это время падала с 84 % до 50 %.
- Причина (`BB_RESIDENCY_MIX=2`: самые тяжёлые привязки «на месте» и их блоки): модуль переносит
читаемый диапазон в VRAM только целиком, и один блок, который видеочип однажды писал «на месте»
(`dynamic_blocks`), оставлял весь диапазон в памяти игры — 7-мегабайтная привязка читалась по
шине из-за одного такого блока из 110. Такие пометки не снимались никогда: при загрузке новой
области её ассеты ложились в память, где прежние буферы видеочипа оставили пометки (164 → 1117 МиБ
«динамических» блоков за сессию).
- Теперь блок, который загрузчики игры переписали целиком (все его байты — данные процессора),
снова может получить копию в VRAM; запись видеочипа в нём остаётся «на месте»
(`layer_cpu_blocks`), чтобы процессор (транслятор читает там код шейдеров и описатели) никогда не
видел устаревших данных. Часто записываемые процессором блоки остаются «динамическими».
`BB_LAYER_RELOAD_RESET=0` — как раньше.
- Проверка: 16 минут игры с телепортами — «динамических» 366–771 МиБ без роста (3 ГБ блоков
возвращено), смешанные чтения ~300 МиБ за 2 с вместо ~8 ГБ; в точке конца после игры 66–72 FPS,
свежий запуск там же 67–73 (разброс между запусками ~8 %). Копирование в VRAM 16 → 23 МБ/с.
- Отвергнуто: снимать такие пометки по времени (через 30 с без повода). Через минуту транслятор
прочитал из памяти игры мусор вместо кода шейдера (рекомпилятор остановился на неизвестной
инструкции): часть пометок защищает данные, которые видеочип пишет, а процессор читает.
- Снятие отображения памяти игрой теперь сбрасывает все пометки целиком снятых блоков
(`BB_LAYER_UNMAP_RESET=0` — как раньше). Bloodborne во время игры память не снимает — на неё не
влияет.
- Диагностика: строка `Layer memory: held in place` раз в 10 с — объём «динамических» блоков, с
проваленным наблюдением, записанных процессором (и сколько записей видеочипа из-за них осталось
«на месте» за 10 с), возвращённых после перезагрузки и загруженных данных без копии в VRAM.
## Свой код вместо shadPS4 (кроме онлайн-модуля)
- `gpu/shadps4` удалён: командный процессор, описания GCN, транслятор шейдеров, рендер, кэши и
платформенные библиотеки — свой код (`gpu/VENDOR.txt`, `docs/SHADPS4_EXIT_PLAN.ru.md`).
Транслятор проверен на корпусе шейдеров игры: SPIR-V побайтно совпадает с прежним.
- Кэш конвейеров и его хранение на диске — `gpu/render/pipeline_cache.*`, `pipeline_store.*`;
растеризатор разбит на модули `gpu/render/raster_*.cpp`.
- NVIDIA: конвейеры из кэша на диске теряли шейдер отбрасывания по clip distances (его не
сохраняли): теперь он сохраняется; старые записи кэша читаются как раньше, без пересборки.
- Конвейеры с геометрическим шейдером из кэша на диске получали пустой fetch-шейдер (его
затирала стадия без него).
- Отрисовка без пиксельного шейдера брала настройки фрагментной стадии от предыдущей отрисовки
(NVIDIA: наличие шейдера отбрасывания зависело от порядка отрисовок).
- Отладочные метки Vulkan (`BB_VK_MARKERS=1`) больше не читают за концом строки игры.
+210
View File
@@ -0,0 +1,210 @@
# Анализ CPU-бинарника Bloodborne и начало статической рекомпиляции
Дата анализа: 2026-10-09. Целевые платформы: Linux x86-64 и Linux ARM64 (AArch64).
Этот этап дал воспроизводимый анализ и AOT-прототип отдельных функций.
Полная рекомпиляция игры и ARM64-лаунчер пока не реализованы.
## Основной результат
Существующий `bbport` уже выполняет оригинальный процессорный код напрямую на
x86-64: `src/probe.c` отображает образ игры и вызывает его entry point, а
`src/runtime_*.c` подменяют системные API PS4. Для этого варианта CPU-рекомпиляция
не обязательна. Для отдельной сборки ARM64 нужен перевод всех исполняемых
участков игры и используемых PS4-библиотек с сохранением их поведения.
Создан конвейер `recompiler/`: plaintext SELF → проверенный ELF → диапазоны
unwind и граф переходов → выбранные целочисленные функции → LLVM IR → нативные
объекты x86-64 и AArch64. На стадии исполнения этих AOT-функций нет декодера
инструкций, интерпретатора или JIT. QEMU используется только в тестах ARM64.
## Какой именно бинарник разобран
- Источник: `/home/deadinside/Games/bloodborne/game_files/CUSA03173/eboot.bin`.
- Издание: `CUSA03173`, APP_VER `01.09`.
- SHA-256 исходного SELF: `d65f0b4f01d59166aed16f8604196d8b7dd805abbf0758b356e8f1354c9429f9`.
- SHA-256 загружаемого образа: `071df19c8880086d97182dbc057bc8cb37badaca57d9112683836b24a0444c0a`.
- Размер файла: 93 825 704 байт; образа: 91 042 036 байт.
- ELF64 little-endian, x86-64, тип `0xfe10`, FreeBSD OSABI.
- Entry point: `0xa0`; обычных ELF section headers нет.
- Читается plaintext SELF; дешифрование не выполнялось.
Хеш образа совпадает с поддерживаемым 1.09 в `scripts/game_check.py`.
Исходные файлы игры читаются; все извлечённые образы и рекомпилированный код
записываются в `out/recompiler/`.
## Восстановленная структура
| Измерение | Результат |
|---|---:|
| Диапазоны FDE из `.eh_frame_hdr` | 162 959 |
| Суммарный размер этих диапазонов | 41 719 719 байт |
| Размер RX-сегмента, включая данные и unwind | 84 776 668 байт |
| Пересечения диапазонов FDE | 0 |
| FDE с указателем LSDA | 38 |
| Релокации | 235 000 |
| Импортированные символы | 701 |
| Экспортированные символы основного бинарника | 0 |
| Relative-релокации на начала известных FDE | 149 926 |
FDE даёт диапазон unwind, а не доказанную границу каждой исходной C++-функции.
RX-сегмент содержит константы и метаданные; его размер не равен размеру кода.
Динамические имена представлены PS4 NID с идентификаторами библиотеки и модуля.
Сам бинарник не предоставляет имён для всех 162 959 диапазонов.
Типы релокаций: 7: 661, 8: 231 478, 1: 2 838, 6: 23.
Много указателей на код находится в данных: при ARM-рекомпиляции недостаточно
перевести прямые `call`; требуется согласованное представление vtable, callbacks
и указателей на функции.
## Обход инструкций и переходов
Zydis декодирует инструкции от известных входов. Условные переходы обходятся
по обеим веткам внутри диапазона. Прямые вызовы регистрируются как рёбра;
косвенные вызовы не раскрываются; косвенный jump завершает текущую ветку.
Поиск отдельных байтов `E8`/`E9` не используется.
Дополнительно разобраны 606 входов прямых вызовов без FDE,
в том числе 604 импортных PLT-thunks. Их RIP-relative
слоты сопоставлены с реальными релокациями и полными идентификаторами импортов.
Учтены `exit`, `abort`, `__stack_chk_fail`, которые не возвращаются.
Первый проход ошибочно заходил в inline-таблицы после этих вызовов: после
исправления число недекодируемых адресов сократилось с 843 до 22.
Финальные числа ниже относятся к обходу FDE, без добавления PLT-прохода:
| Измерение | Результат |
|---|---:|
| Декодированные инструкции | 9 358 972 |
| Декодированные байты | 38 345 547 |
| Прямые вызовы | 407 756 |
| Косвенные вызовы | 158 095 |
| Косвенные jumps | 15 073 |
| Обращения через FS/GS | 16 733 |
| Инструкции с LOCK | 11 946 |
| Инструкции, читающие CPU flags | 820 619 |
| Вызовы известных noreturn-thunks | 32 482 |
| Недекодируемые адреса | 22 |
| Конфликты границ инструкций | 3 |
| Ветки, дошедшие до конца FDE без завершения | 5 635 |
Это неполное восстановление CFG. Косвенные jump tables, некоторые входы без
unwind, landing pads исключений и продолжения за границей FDE требуют отдельного
разбора. Все оставшиеся проблемы записаны в `cfg-issues.json`; это проблемы
восстановления графа, а не доказательство повреждения игры.
Классификация Zydis по расширениям ISA:
| ISA extension | Инструкции |
|---|---:|
| AVX | 852 131 |
| BASE | 8 452 210 |
| BMI1 | 3 910 |
| LONGMODE | 46 441 |
| LZCNT | 325 |
| MOVBE | 868 |
| PAUSE | 1 570 |
| RDTSCP | 1 |
| SSE | 1 338 |
| SSE2 | 60 |
| SSE4 | 118 |
Видимые SIMD-регистры: XMM — 844 157, YMM — 8 427,
ZMM — 0 инструкций. AVX-код не обязательно 256-битный:
VEX-инструкции используют также 128-битные XMM. Это существенно для переноса
на ARM NEON. Классификация касается восстановленного кода основного бинарника.
## Проверенный AOT-прототип
Из 26 651 предварительных кандидатов строгий
lifter принял 9 613. В тестовый корпус выбраны 140
функций с различными последовательностями инструкций.
Контракт ограничен: шесть сырых целочисленных входов из RDI/RSI/RDX/RCX/R8/R9,
определённый результат в RAX, без обращений к памяти, вызовов и ветвлений.
Допускается проверенная пара frame prologue/epilogue. Реализованы целочисленные
операции, частичные регистры, zero/sign extension, маскирование shift count,
LEA от входных регистров, CMP/TEST, SETcc/CMOVcc и необходимые flags.
Неопределённые входы, неподдержанная семантика и неизвестные flags отклоняются.
Имена и сигнатуры исходных функций не восстанавливались и не угадывались.
| Проверка | Результат |
|---|---|
| Оригинальные x86-64 байты против AOT x86-64 | 308 420 сравнений; 0 расхождений |
| AOT AArch64 против результатов оригинального x86-64 | 46 340 сравнений; 0 расхождений в QEMU; физический ARM не проверялся |
| Формат объектов | ELF64 x86-64 / ELF64 AArch64 |
| Неопределённые символы в AOT-объектах | 0 |
Векторы включают граничные значения 8/16/32/64-битных целых, shift counts,
нулевые входы и случайные 64-битные аргументы; seed `0xbb109`.
Сравнивался RAX; полный набор регистров, состояние FP и ABI всей игры не проверены.
ARM64-тест — freestanding Linux ELF без libc и динамического интерпретатора.
На физическом ARM-процессоре проверки не проводились.
Артефакты: `leaves.ll`, `leaves-x86_64.o`, `leaves-aarch64.o`,
`leaves-x86_64.so`, `arm-check`, `lift.json`, `verification.json`, дизассемблирование
обоих объектов. Они содержат производный код из локального дампа и остаются в `out/`.
## Что нужно для двух вариантов запуска всей игры
1. Завершить восстановление CFG: устранить 22 decode failures,
3 конфликта, 5 635 выходов
за границу диапазонов; раскрыть switch tables, callbacks и exception landing pads.
Собрать тесты для новых типов функций на оригинальном x86-64.
2. Добавить полный lifter для памяти, стеков, ветвлений, SSE/AVX/BMI и FP.
Сохранить x86 flags, MXCSR/FP rounding, NaN-поведение, порядок памяти и LOCK.
На ARM нельзя заменить x86 атомарные операции и TSO обычными C-записями.
3. Ввести согласованное отображение guest адресов кода в AOT entry points.
Прямые вызовы, vtable, сохранённые callbacks, создание потоков и обратные вызовы
рантайма должны использовать одну модель. Адреса данных и layout PS4 сохраняются.
4. Перевести или заменить используемые PS4 PRX. `bbport` сейчас прилинковывает
`libc.prx` и `libSceFios2.prx` как оригинальный x86-код; они также нужны на ARM:
| Модуль | RX-байты | FDE | Импорты |
|---|---:|---:|---:|
| libc.prx | 751 908 | 2 164 | 79 |
| libSceFios2.prx | 309 956 | 650 | 83 |
5. Перенести архитектурные части рантайма: запрет ARM в `src/probe.c`, x86 asm
входа/импортных trampoline, FS→GS/`arch_prctl` в `runtime_thread.c`, обработчики
`REG_RIP`/`ucontext`, guest `setjmp`/unwind и SysV→AAPCS64-мосты.
Проверить архитектурные зависимости GPU-ядра: SIMD headers, trap handlers,
Xbyak в `flatten_extended_userdata_pass.cpp`: его не-x86 ветка сейчас вызывает
`UNREACHABLE_MSG` и не реализует проход. Vulkan-рендерер остаётся необходимым.
6. Получить отдельный ARM64 executable и пройти последовательные smoke-проверки:
entry → initialization → title screen → loading save → gameplay/audio/saving.
После этого добавлять выбор CPU-архитектуры и упаковку двух сборок.
Прототип подтверждает практический перевод конкретных функций в обе ISA.
Он не подтверждает, что вся игра уже переносится автоматически или будет иметь
ту же производительность. x86-64 direct execution можно сохранять как рабочий
вариант и как эталон для проверки ARM64-рекомпиляции.
## Воспроизведение
Из `bbport/`:
```bash
nix-shell recompiler/shell.nix --run 'bash recompiler/run.sh ../game_files/CUSA03173'
```
Нужны GCC, CMake, Ninja, Python 3, Zydis 4, LLVM 21, LLD и QEMU user-mode.
Текущий прогон: GCC 15.3, Zydis 4.1.1, LLVM 21.1.8, LLD 22.0 (ROCm), QEMU 11.1.1.
Также можно запускать `bash recompiler/run.sh GAME_DIR` в готовом окружении.
`BB_RECOMP_OUT` задаёт каталог артефактов; `BB_LLVM_BIN`, `BB_LLD`,
`BB_QEMU_AARCH64` позволяют выбрать установленные инструменты.
Конвейер проверяет хеш исходного бинарника и не запускает игру.
## Источники для следующего этапа
Для расширения lifter рассмотрен [Remill](https://github.com/lifting-bits/remill):
это библиотека семантики машинных инструкций и подъёма в LLVM IR, включая amd64.
Она не решает автоматически PS4 runtime/CFG и не встроена в текущий прототип.
[Ограничения McSema](https://github.com/lifting-bits/mcsema/blob/master/docs/Limitations.md)
отдельно описывают code/data recovery, self-modifying code и исключения;
рассматривать этот инструмент как готовый конвертер всей игры нельзя.
Мост вызовов ARM должен соответствовать
[AAPCS64](https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst);
перенос исключений требует отдельного разбора
[C++ Exception Handling ABI](https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html).
Все численные результаты выше получены локальными инструментами из указанного SELF.
+5 -5
View File
@@ -22,7 +22,7 @@
### 1. Depth-Adaptive Disocclusion Threshold
**Файл:** `gpu/shadps4/video_core/host_shaders/taa.comp`
**Файл:** `gpu/upscale/shaders/taa.comp`
```glsl
float depthThreshold(float depth) {
@@ -75,7 +75,7 @@ result = mix(current, clipped, weight);
### 3. Motion Vector Validation (camera_motion.comp)
**Файл:** `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
**Файл:** `gpu/upscale/shaders/camera_motion.comp`
```glsl
// Depth-adaptive validation: distant surfaces need tighter depth matching.
@@ -93,7 +93,7 @@ if (object_motion.b > 0.99 && abs(object_motion.a - depth) < depth_threshold &&
### 4. Улучшенная визуализация debug режимов
**Файл:** `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
**Файл:** `gpu/upscale/shaders/camera_motion.comp`
Режимы `BB_DEBUG_MOTION=1` теперь структурированы:
```glsl
@@ -184,5 +184,5 @@ echo "0" > /tmp/bb_toggle
- [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`
- TAA shader: `gpu/upscale/shaders/taa.comp`
- Camera motion: `gpu/upscale/shaders/camera_motion.comp`
+1 -1
View File
@@ -186,7 +186,7 @@ DXVK), синтетический vblank (HLE VideoOut), заглушки для
| Запись процессором в память, видимую видеочипу, «объявляется» (обёртки `memcpy`, перехваты загрузчиков, кучи, аллокатора, чтение файлов) | да — для копий в VRAM и текстур; `BB_WRITE_VERIFY` на маршруте не нашёл необъявленных записей, кроме ожидаемых |
| Шейдер копирования игры — подмена (список читается процессором) | да; теперь можно транслировать (`BB_COPY_SHADER_NATIVE=1`) |
Строки «нет» нужны только при переносе на другие игры (Driveclub, inFAMOUS: там асинхронный
Строки «нет» нужны только при переносе на другие игры (где асинхронный
compute и occlusion есть) — проверить их на Bloodborne нельзя.
Шейдер копирования (`cs fefebf9f`): подмену можно заменить трансляцией самого шейдера. Она нашла
+1 -1
View File
@@ -18,7 +18,7 @@
## Решение: модуль памяти прослойки
Вместо sparse-арены работает собственный модуль памяти (`gpu/layer/bblayer_gpu_memory.*`,
`gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp`, план — `MEMORY_MODULE_PLAN.ru.md`):
`gpu/buffers/buffer_cache.cpp`, план — `MEMORY_MODULE_PLAN.ru.md`):
- память игры привязывается **на месте**, через буферы её кусков (dma-buf на AMD, импорт памяти
хоста `VK_EXT_external_memory_host` на NVIDIA);
+178
View File
@@ -0,0 +1,178 @@
# Уход от shadPS4: где он стоит, что мешает другим играм, порядок замены (2026-10-12)
> **Итог (2026-10-13): `gpu/shadps4` удалён.** Всё, что в нём было, заменено своим кодом:
> командный процессор (`gpu/cp`), описания GCN (`gpu/gcn`), транслятор шейдеров (`gpu/shader`,
> проверен на корпусе шейдеров игры — SPIR-V побайтно совпадает с прежним), рендер
> (`gpu/render`: конвейеры, кэш конвейеров и его хранение на диске, растеризатор в модулях
> `raster_*.cpp`), кэши буферов и текстур, платформенные библиотеки. От shadPS4 остался только
> онлайн-модуль (`gpu/bbnet/shadps4`: сеть и NP), его не трогали. Дальше — оптимизация с
> сохранением асинхронности (раздел 6) и документация по встраиванию апскейлеров.
> Ниже — план в том виде, в каком он писался.
Требования: минимум накладных расходов (на отрисовку на процессоре, на кадр на видеочипе) и
ничего, что знает одну игру, вне её профиля (`gpu/games/`). Общность проверяется на играх с другим
движком, где, например, частицы целиком на асинхронных очередях compute.
Замеры — RX 7800 XT / RADV, Ryzen 7 5700X3D, сохранение пользователя.
## 1. Сколько shadPS4 осталось
Сравнение `gpu/shadps4` с исходным коммитом b4e7ae7: 118 тыс. строк, из них 88,8 тыс. (75 %) —
строки оригинала.
| Часть | Строк | Своего | На горячем пути отрисовки |
|---|---|---|---|
| `shader_recompiler` | 38,6 тыс. | ~2 % | нет (только компиляция конвейеров) |
| `renderer_vulkan` | 29,5 тыс. | 60 % | да: растеризатор, кэш конвейеров, привязки |
| `amdgpu` (командный процессор, регистры, тайлинг) | 8,7 тыс. | 28 % | да: разбор PM4 (`liverpool.cpp`) |
| `buffer_cache` | 7,8 тыс. | 75 % | да: `ObtainBuffer` (своё) |
| `texture_cache` | 4,8 тыс. | 13 % | да: поиск образов по адресу |
| `core/libraries` (gnmdriver, videoout, avplayer) | 11,8 тыс. | ~8 % | раз в кадр / раз в отправку |
| `common` | 10 тыс. | 1 % | утилиты |
Своё вне shadPS4: модуль памяти (`gpu/layer`), декодер PM4 (`gpu/cp`, включается вручную), профили
игр (`gpu/games`), конвейер стадий A/B, потоки записи Vulkan, апскейлеры, вся процессорная часть
(загрузчик, рантайм `src/`).
## 2. Куда уходит кадр сейчас (замер 2026-10-13)
Одна точка (Ярнам, ~885 отрисовок), RX 7800 XT + Ryzen 7 5700X3D.
**Видеочип, настройки пользователя** (вывод 3840×1600, FSR 4.1.1 Native AA): 14,9 мс на кадр,
67 FPS, видеочип занят на 99 % (`BB_GPU_PROFILE=1`):
| Что | мс/кадр | Доля | Чьё |
|---|---|---|---|
| проходы и запуски самой игры | ~7,5 | ~50 % | игра (перевод — рекомпилятор shadPS4) |
| апскейлер FSR 4.1.1 | 4,18 | 28 % | наше |
| прокси сцены (уменьшение/заливка для проходов, ждущих 1920×1080) | 1,51 | 10 % | наше, плата за перевод разрешения |
| видеочип простаивает между отправками (7 отправок за кадр) | 1,44 | 10 % | наше: граница отправки, синхронизация |
| шейдер копирования игры (60 запусков с полным барьером между ними) | 0,35 | 2 % | перевод барьеров |
**Процессор, лёгкая нагрузка на видеочип** (FSR 3 Ultra Performance): 175 FPS, видеочип всё равно
занят на 99 %; стадия B занята на 72 % (`BB_SECTIONS=1`, мкс на пакет):
| Часть стадии B | мкс | Чьё |
|---|---|---|
| всего на пакет | 2,54 | |
| привязка ресурсов (буферы 0,36, текстуры 0,27) | 0,75 | shadPS4 (`BindBuffers`/`BindTextures`) + наши кэши |
| `ObtainBuffer` (в т. ч. модуль памяти 0,39) | 0,50 | наше |
| вершины и индексы | 0,36 | shadPS4 + наше |
| начало прохода | 0,20 | shadPS4 + наше |
| упорядоченные задачи (метки, DMA) | 0,24 | командный процессор |
| запуски compute | 0,20 | shadPS4 |
| динамическое состояние, дескрипторы, передача потокам записи | 0,36 | смешано |
Вывод: на этой машине упор всегда в видеочип; процессорной части shadPS4 хватает с запасом
(потолок стадии B ~240 FPS при таком числе отрисовок). Время, которое можно вернуть, лежит на
шкале видеочипа, и почти всё оно — в нашем коде и в том, как переводятся синхронизация и барьеры.
## 3. Что в коде shadPS4 мешает «быстро» и «для любой игры»
1. **Командный процессор (`liverpool.cpp`).** Корутины, большой `switch`, CE (`DUMP_CONST_RAM`)
и DMA/`WRITE_DATA` вне памяти «на месте» исполняет процессор при разборе. Очереди compute
(ASC) — корутины в том же потоке, **каждый их пакет сбрасывает конвейер стадий A/B**
(`DrainDrawPipe(ReasonCompute)`). Bloodborne ASC не использует (0 отправок за кадр); в
играх с асинхронным compute это обычный кадр — параллельность записи отрисовок пропадёт, асинхронность compute
(перекрытие с графикой) тоже.
2. **Привязка ресурсов.** Каждая отрисовка заново ищет буферы и образы по адресам; SRT walker
(обход таблиц описателей кодом шейдера на процессоре) выполняется воркером и ещё раз на
стадии B для проверки. Кэши (`texture set memo` 98–99 % попаданий, `render state memo` ~80 %)
срезают часть, но строятся поверх того же поиска.
3. **Кэш текстур по адресу и хешу.** Образ угадывается по адресу/формату из T#, наложения и
«цель рендера как текстура» — эвристики `FindImage`. Для другой игры с другими раскладками
памяти это главный источник артефактов.
4. **Ключ конвейера собирается из регистров на каждой отрисовке** (`RefreshGraphicsKey`);
динамического состояния мало — много вариантов конвейеров и компиляций.
5. **Модель памяти** (уже своя) несёт три пути: 0.3, sparse-арена, модуль. Сегодня найдено, что у
модуля «пожизненные» правила (блок, который видеочип однажды писал «на месте», — навсегда вне
VRAM) копились при загрузке областей и выводили из VRAM целые диапазоны: −26 % FPS за 17 минут
с телепортами. Исправлено сбросом при полной перезаписи блока загрузчиком; общий урок — у
каждого состояния блока нужен явный выход по событию, а не «навсегда».
## 4. Что в видеочасти знает только Bloodborne
| Где | Что | Для другой игры |
|---|---|---|
| `gpu/games/bloodborne.cpp` | перехваты загрузчиков, аллокатора, кучи; писатели «каждый кадр»; хеш шейдера копирования | профиль (уже так) |
| `renderer_vulkan/ui_composition.h` | хеши шейдеров Scaleform (интерфейс в разрешении вывода) | в профиль; без профиля — без раздельного UI |
| `vk_camera_motion.cpp` | распознаёт константы сцены Bloodborne (дальняя плоскость 3000, размер) | в профиль; у игр с размытием в движении может быть свой буфер скоростей, который апскейлер может взять напрямую |
| `vk_scene_resolution.cpp` | «сцена» = цели 1920×1080 | параметр профиля |
| `gpu/shim` | частота кадров, соотношение сторон, меню, мышь, значки кнопок (`KG_*`) — хуки по адресам 1.09 | особенности порта конкретной игры |
## 5. Что нужно другим играм, чего нет или что не проверено
- **ASC как настоящие очереди Vulkan**: своя очередь compute на каждую очередь игры, метки и
`WAIT_REG_MEM` между очередями — timeline-семафоры; разбор очередей без сброса конвейера A/B.
- **GDS** (ordered append, счётчики, GWS): сейчас частично (DMA/EOS в GDS); нужна память GDS как
буфер видеочипа с атомарными операциями.
- **MSAA/EQAA с FMASK** (декомпрессия FMASK сейчас пропускается), быстрые очистки CMASK, HTILE.
- **Тесселяция и геометрические шейдеры** — в рекомпиляторе shadPS4 частично; Bloodborne их не
проверяет.
- **Память без перехватов игры**: корректность только на ловушках записи (64 КиБ); у игры, много
пишущей в память видеочипа с процессора, это поток срабатываний. Модуль должен работать без
профиля (сейчас проверено: Bloodborne без профиля — 196 FPS).
- **Рантайм** (`src/`) реализует ровно то, что зовёт Bloodborne; для другой игры — больше
системных библиотек. Это не shadPS4, но без этого вторая игра не запустится.
## 6. Устройство, к которому идём, и правила низких накладных расходов
1. **Стадия B только записывает.** Всё, что можно решить по снимку регистров и описателей, —
на воркерах стадии A (простаивающие ядра): конвейер, образы, буферы, сэмплеры, готовый блок
дескрипторов. Стадия B сверяет поколение (ресурсы не менялись) и копирует блок
(`VK_EXT_descriptor_buffer`) — цель ~1 мкс на пакет вместо 2,5.
2. **Идентичность вместо поиска.** Образы и буферы — в реестре по идентичности описателя
(адрес, формат, размеры), недействительны по поколению памяти (ловушки записи, записи
видеочипа), без хеширования данных игры при использовании.
3. **Состояние по изменениям.** Декодер (`gpu/cp`) отдаёт записанные блоки регистров; группы
состояния (растеризация, смешивание, глубина, цели) пересчитываются только при их записи.
Больше динамического состояния (`VK_EXT_extended_dynamic_state3`, динамический ввод вершин) —
меньше конвейеров.
4. **Командный процессор — транслятор, не эмулятор.** Ничего «вместо видеочипа» на процессоре;
очереди compute — очереди Vulkan; метки — записи видеочипа (уже так).
5. **Профиль игры — единственное место знаний об игре**: адреса кода, хеши шейдеров, раскладки
констант для векторов движения, размер сцены, UI.
## 6а. Асинхронность: сколько можно вернуть на видеочипе
| Что | Сейчас | Цель | Как |
|---|---|---|---|
| простои между отправками | 1,44 мс | ~0,3 мс | меньше отправок за кадр (7 → 2–3), запись следующей отправки, пока видеочип занят текущей; ожидания меток — на видеочипе (timeline-семафоры), не на процессоре |
| FSR на отдельной очереди compute | 4,18 мс последовательно | скрыть 30–50 % | апскейл кадра N идёт вместе с растеризацией кадра N+1 (тени, G-буфер — упор в растеризацию, вычислительные блоки свободны); нужны две копии целей сцены (прокси — наши образы: +~150 МБ при 3840×1600) |
| прокси сцены | 1,51 мс | ~0,5–1 мс | часть проходов рисовать сразу в размере сцены (пересчёт `FragCoord` уже есть), уменьшения — на очереди compute |
| барьеры из сбросов кэшей игры | полный барьер на `ACQUIRE_MEM` | точные барьеры | переводить, что игра реально просит (`ACQUIRE_MEM` без `CS_PARTIAL_FLUSH` — без ожидания предыдущего запуска): шейдер копирования 0,35 → ~0,1 мс, плюс остальные проходы |
| async compute самой игры (ASC) | корутины в одном потоке + сброс конвейера A/B на каждом пакете | очереди Vulkan | для Bloodborne 0 (не использует), для игр с асинхронным compute — обязательно |
Оценка для настроек пользователя: −3…−5 мс из 14,9 → 85–100 FPS вместо 67 (+25–45 %); реально
ожидать +25–30 %. Самое дешёвое — простои между отправками и точные барьеры, самое дорогое —
FSR на отдельной очереди (двойные цели сцены, синхронизация с интерфейсом).
На процессоре асинхронность — это перенос решения о ресурсах на воркеры стадии A (стадия B только
записывает) и несколько потоков стадии B по проходам. Здесь это даёт только запас (72 % → ~30 %
занятости стадии B); на Steam Deck и слабых процессорах — разницу между упором в процессор и его
отсутствием.
## 7. Порядок замены
| Шаг | Что | Выигрыш | Риск | Проверка |
|---|---|---|---|---|
| 0 | Память: старение модуля (сделано, проверяется); удалить модель 0.3 и арену после логов NVIDIA | меньше ветвлений в `ObtainBuffer`; один путь | средний (NVIDIA) | маршрут, долгая игра с телепортами, логи тестеров |
| 1 | Командный процессор: `gpu/cp` по умолчанию (найти +7 %), обработчики пакетов из `liverpool.cpp`; ASC → очереди Vulkan + timeline-семафоры; GDS | декодер дешевле; асинхронный compute; нужно играм с асинхронным compute | средний | самотест PM4 + новые тесты ASC/GDS; Bloodborne |
| 2 | Привязки: решение на стадии A, descriptor buffers, реестр образов по идентичности | стадия B ~1 мкс на пакет вместо 2,5 (выигрыш на слабых процессорах и высоких FPS) | высокий (наложения образов) | кадры маршрута 1:1, `BB_SECTIONS` |
| 3 | Состояние по изменениям + динамическое состояние | меньше конвейеров, дешевле ключ | средний | число конвейеров, компиляции в первой сессии |
| 4 | Профили: UI, камера, размер сцены, значки — в `gpu/games` | вторая игра без правок ядра | низкий | Bloodborne без изменений |
| 5 | Платформа: gnmdriver/videoout/avplayer — своё, рядом с рантаймом | меньше кода shadPS4, API PS4 под контролем | низкий | Bloodborne; затем вторая игра |
| 6 | Рекомпилятор шейдеров: оставить (исправления из upstream переносятся), менять точечно (меньше специализаций по описателям) | — | высокий при замене | — |
Рекомпилятор на отрисовку ничего не стоит, а его замена — самый большой риск для картинки.
Уходить от него стоит последним и только если понадобится (например, описатели на видеочипе).
Остальное заменяется за интерфейсами по одному модулю, как в
[TRANSLATOR_ARCHITECTURE.ru.md](TRANSLATOR_ARCHITECTURE.ru.md): сначала интерфейс с кодом
shadPS4 за ним, потом своя реализация, после каждого шага — маршрут, самотест PM4, кадры.
## 8. Чем мерить
`Frame stats` (мкс на отрисовку стадии B, простой), `BB_SECTIONS=1` (разбивка стадии B),
`Draw pipe` (готовые отрисовки, сбросы конвейера по причинам), FPS с лёгкой нагрузкой на видеочип
(FSR 3, пресет 4: упор в процессор), p99 времени кадра, время видеочипа (`BB_GPU_PROFILE=1`),
доля данных в VRAM (`Layer memory: held in place`) на долгой игре.
+160 -69
View File
@@ -43,18 +43,50 @@ endif()
find_package(Vulkan REQUIRED)
find_package(fmt REQUIRED)
find_package(Boost REQUIRED)
find_package(magic_enum REQUIRED)
find_package(magic_enum QUIET)
if(NOT magic_enum_FOUND)
# Header-only; packaged on few systems (the Windows cross build's MSYS2 has none).
include(FetchContent)
FetchContent_Declare(magic_enum GIT_REPOSITORY https://github.com/Neargye/magic_enum.git GIT_TAG v0.9.7)
FetchContent_MakeAvailable(magic_enum)
endif()
find_package(tsl-robin-map REQUIRED)
find_package(VulkanMemoryAllocator CONFIG REQUIRED)
find_package(VulkanMemoryAllocator CONFIG QUIET)
if (NOT TARGET GPUOpen::VulkanMemoryAllocator)
# The header alone, without its CMake package (MSYS2: include/vma/vk_mem_alloc.h).
find_path(VMA_INCLUDE_DIR vk_mem_alloc.h PATH_SUFFIXES vma REQUIRED)
add_library(GPUOpen::VulkanMemoryAllocator INTERFACE IMPORTED)
target_include_directories(GPUOpen::VulkanMemoryAllocator INTERFACE ${VMA_INCLUDE_DIR})
endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
find_package(miniz REQUIRED)
if (NOT WIN32)
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
endif()
find_package(miniz QUIET)
if(NOT miniz_FOUND)
# Distributions without a miniz package (Debian, Ubuntu: #136): built from its release.
include(FetchContent)
FetchContent_Declare(miniz GIT_REPOSITORY https://github.com/richgel999/miniz.git GIT_TAG 3.1.2)
FetchContent_MakeAvailable(miniz)
if(NOT TARGET miniz::miniz)
add_library(miniz::miniz ALIAS miniz)
endif()
endif()
find_package(Zydis REQUIRED)
find_program(GLSLANG_COMPILER NAMES glslang glslangValidator REQUIRED)
set(SIRIT_USE_SYSTEM_SPIRV_HEADERS OFF CACHE BOOL "" FORCE)
if (WIN32)
# Every target, the third-party ones too: what glibc and MSVC have and MinGW does not.
include_directories(BEFORE platform/win32/include)
enable_language(RC)
# Bit-fields laid out as on Linux (GCC's way), not MSVC's: the GPU registers and the PS4's
# structures are declared with them.
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-mno-ms-bitfields>)
add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/platform/win32/include/bbport_win32.h>")
endif()
add_subdirectory(third_party/sirit EXCLUDE_FROM_ALL)
# bbport: FSR 3.1 temporal upscaling / frame generation, native Vulkan (MIT, FireBurn/FSR-Vulkan).
@@ -62,64 +94,66 @@ set(FFX_VK_PORTABLE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FFX_VK_PORTABLE_BUILD_PROBE OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/fsr-vulkan EXCLUDE_FROM_ALL)
# Host shaders -> headers (same scheme as shadPS4's host_shaders/CMakeLists.txt).
set(SHADER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/shadps4/video_core/host_shaders)
# Host shaders -> headers: SPIR-V arrays from glslang, in bbgpu/shaders/.
set(SHADER_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/include)
set(SHADER_DIR ${SHADER_INCLUDE}/video_core/host_shaders)
set(SHADER_HEADERS "")
function(add_host_shader FILENAME SHADER_NAME)
set(OUT ${SHADER_DIR}/${SHADER_NAME}.h)
function(add_shader_header SOURCE OUT_DIR SHADER_NAME)
set(OUT ${OUT_DIR}/${SHADER_NAME}.h)
string(TOUPPER ${SHADER_NAME} SYMBOL)
set(DEFINES ${ARGN})
list(TRANSFORM DEFINES PREPEND "-D")
add_custom_command(OUTPUT ${OUT}
COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADER_DIR}
COMMAND ${GLSLANG_COMPILER} --target-env vulkan1.3 -o ${OUT} --vn ${SYMBOL} ${DEFINES} ${SHADER_SRC}/${FILENAME}
COMMAND ${CMAKE_COMMAND} "-DHEADER_FILE=${OUT}" -P ${SHADER_SRC}/AddShaderHeaderInclude.cmake
DEPENDS ${SHADER_SRC}/${FILENAME} VERBATIM)
COMMAND ${CMAKE_COMMAND} -E make_directory ${OUT_DIR}
COMMAND ${GLSLANG_COMPILER} --target-env vulkan1.3 -o ${OUT} --vn ${SYMBOL} ${DEFINES}
-I${CMAKE_CURRENT_SOURCE_DIR}/third_party/amd-fsr1 ${SOURCE}
COMMAND ${CMAKE_COMMAND} "-DHEADER_FILE=${OUT}" -P ${CMAKE_CURRENT_SOURCE_DIR}/render/shaders/header.cmake
DEPENDS ${SOURCE} VERBATIM)
set(SHADER_HEADERS ${SHADER_HEADERS} ${OUT} PARENT_SCOPE)
endfunction()
add_host_shader(color_to_ms_depth.frag color_to_ms_depth_frag)
add_host_shader(ms_image_blit.frag ms_image_blit_frag)
add_host_shader(ms_image_blit.frag ms_image_blit_msaa_frag SRC_MSAA=1)
add_host_shader(fault_buffer_process.comp fault_buffer_process_comp)
add_host_shader(fs_tri.vert fs_tri_vert)
add_host_shader(fsr.comp fsr_easu_comp SAMPLE_EASU=1)
add_host_shader(fsr.comp fsr_rcas_comp SAMPLE_RCAS=1)
add_host_shader(post_process.frag post_process_frag)
add_host_shader(camera_motion.comp camera_motion_comp)
add_host_shader(upscale_merge.comp upscale_merge_comp)
add_host_shader(upscale_reactive.comp upscale_reactive_comp)
add_host_shader(taa.comp taa_comp)
add_host_shader(taa_sharpen.comp taa_sharpen_comp)
add_host_shader(taa_sharpen.comp taa_sharpen_ldr_comp LDR_OUTPUT=1)
add_host_shader(fsr4_color.comp fsr4_decode_comp)
add_host_shader(fsr4_color.comp fsr4_encode_comp ENCODE=1)
add_host_shader(fsr4_reactive.comp fsr4_reactive_comp)
add_host_shader(buffer_multi_copy.comp buffer_multi_copy_comp)
add_host_shader(occlusion_resolve.comp occlusion_resolve_comp)
add_host_shader(gpu_timestamp.comp gpu_timestamp_comp)
add_host_shader(indirect_guard.comp indirect_guard_comp)
add_host_shader(depth_resample.frag depth_resample_frag)
add_host_shader(depth_resample.frag depth_stencil_resample_frag HAS_STENCIL=1)
add_host_shader(depth_resample.frag depth_stencil_bits_frag STENCIL_BITS=1)
add_host_shader(camera_motion.comp camera_motion_debug_comp DEBUG_OVERLAY=1)
set(OWN_SHADER_DIR ${SHADER_INCLUDE}/bbgpu/shaders)
function(add_own_shader FILENAME SHADER_NAME)
add_shader_header(${CMAKE_CURRENT_SOURCE_DIR}/${FILENAME} ${OWN_SHADER_DIR} ${SHADER_NAME} ${ARGN})
set(SHADER_HEADERS ${SHADER_HEADERS} PARENT_SCOPE)
endfunction()
add_own_shader(render/shaders/depth_from_color.frag depth_from_color_frag)
add_own_shader(render/shaders/texel_copy.frag texel_copy_frag)
add_own_shader(render/shaders/texel_copy.frag texel_copy_msaa_frag MSAA_SOURCE=1)
add_own_shader(buffers/shaders/fault_scan.comp fault_scan_comp)
add_own_shader(render/shaders/fullscreen.vert fullscreen_vert)
add_own_shader(render/shaders/present_encode.frag present_encode_frag)
add_own_shader(upscale/shaders/camera_motion.comp camera_motion_comp)
add_own_shader(upscale/shaders/upscale_merge.comp upscale_merge_comp)
add_own_shader(upscale/shaders/upscale_reactive.comp upscale_reactive_comp)
add_own_shader(upscale/shaders/taa.comp taa_comp)
add_own_shader(upscale/shaders/taa_sharpen.comp taa_sharpen_comp)
add_own_shader(upscale/shaders/taa_sharpen.comp taa_sharpen_ldr_comp LDR_OUTPUT=1)
add_own_shader(upscale/shaders/fsr4_color.comp fsr4_decode_comp)
add_own_shader(upscale/shaders/fsr4_color.comp fsr4_encode_comp ENCODE=1)
add_own_shader(upscale/shaders/fsr4_reactive.comp fsr4_reactive_comp)
add_own_shader(render/shaders/buffer_multi_copy.comp buffer_multi_copy_comp)
add_own_shader(render/shaders/occlusion_resolve.comp occlusion_resolve_comp)
add_own_shader(render/shaders/gpu_timestamp.comp gpu_timestamp_comp)
add_own_shader(render/shaders/indirect_guard.comp indirect_guard_comp)
add_own_shader(render/shaders/indirect_draw_guard.comp indirect_draw_guard_comp)
add_own_shader(upscale/shaders/depth_resample.frag depth_resample_frag)
add_own_shader(upscale/shaders/depth_resample.frag depth_stencil_resample_frag HAS_STENCIL=1)
add_own_shader(upscale/shaders/depth_resample.frag depth_stencil_bits_frag STENCIL_BITS=1)
add_own_shader(upscale/shaders/camera_motion.comp camera_motion_debug_comp DEBUG_OVERLAY=1)
foreach(BPP IN ITEMS 8 16 32 64 96 128)
add_host_shader(tiling.comp tiling_micro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=0)
add_host_shader(tiling.comp tiling_macro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=1)
add_own_shader(textures/shaders/detile.comp detile_micro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=0)
add_own_shader(textures/shaders/detile.comp detile_macro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=1)
endforeach()
add_custom_target(host_shaders DEPENDS ${SHADER_HEADERS})
file(GLOB_RECURSE VIDEO_CORE CONFIGURE_DEPENDS shadps4/video_core/*.cpp)
file(GLOB_RECURSE RECOMPILER CONFIGURE_DEPENDS shadps4/shader_recompiler/*.cpp)
list(FILTER VIDEO_CORE EXCLUDE REGEX "renderdoc\\.cpp$")
set(COMMON
shadps4/common/decoder.cpp shadps4/common/error.cpp
shadps4/common/io_file.cpp shadps4/common/native_clock.cpp shadps4/common/number_utils.cpp
shadps4/common/path_util.cpp shadps4/common/rdtsc.cpp shadps4/common/spin_lock.cpp shadps4/common/recursive_lock.cpp
shadps4/common/string_util.cpp shadps4/common/thread.cpp shadps4/common/signal_context.cpp
shim/common/logging/log.cpp)
file(GLOB AVPLAYER CONFIGURE_DEPENDS shadps4/core/libraries/avplayer/*.cpp)
file(GLOB_RECURSE RECOMPILER CONFIGURE_DEPENDS shader/*.cpp)
# Own utilities (types, logging, paths, locks, containers): common/.
file(GLOB COMMON CONFIGURE_DEPENDS common/*.cpp common/logging/*.cpp)
# Own system libraries (Gnm, VideoOut, AvPlayer, event queues, interrupts): platform/.
file(GLOB_RECURSE PLATFORM_SOURCES CONFIGURE_DEPENDS platform/*.cpp)
# platform/win32: the POSIX subset over Win32 (with its headers in platform/win32/include).
if (NOT WIN32)
list(FILTER PLATFORM_SOURCES EXCLUDE REGEX "/platform/win32/")
endif()
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET libavformat libavcodec libavutil libswscale libswresample)
file(GLOB SHIM_SOURCES CONFIGURE_DEPENDS shim/*.cpp shim/*/*.cpp)
# The layer's own modules (independent of the shadPS4 code): layer/.
@@ -127,12 +161,20 @@ 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/.
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
shadps4/core/libraries/videoout/driver.cpp shadps4/core/libraries/videoout/video_out.cpp
shadps4/core/libraries/kernel/equeue.cpp ${AVPLAYER}
shadps4/core/libraries/videodec/video_utils.cpp)
file(GLOB CP_SOURCES CONFIGURE_DEPENDS cp/*.cpp memory/*.cpp gcn/*.cpp textures/*.cpp buffers/*.cpp vulkan/*.cpp)
# The translator's rendering pieces (labels, timestamps, occlusion, draw preparation, profiling,
# frame capture, prompts): render/. Temporal upscaling, motion vectors, scene resolution: upscale/.
file(GLOB RENDER_SOURCES CONFIGURE_DEPENDS render/*.cpp)
file(GLOB UPSCALE_SOURCES CONFIGURE_DEPENDS upscale/*.cpp upscale/fsr411/*.cpp)
# Windows: linked into bb-probe.exe (a DLL cannot leave the runtime's symbols to the executable
# that loads it, as -Wl,--allow-shlib-undefined does on Linux).
if (WIN32)
set(BBGPU_LIBRARY_TYPE STATIC)
else()
set(BBGPU_LIBRARY_TYPE SHARED)
endif()
add_library(bbgpu ${BBGPU_LIBRARY_TYPE} ${RECOMPILER} ${COMMON} ${PLATFORM_SOURCES} ${SHIM_SOURCES} ${LAYER_SOURCES} ${GAME_SOURCES} ${CP_SOURCES}
${RENDER_SOURCES} ${UPSCALE_SOURCES})
add_dependencies(bbgpu host_shaders)
# In-game settings menu: Dear ImGui (third_party/imgui, MIT) with its Vulkan backend, and an
# embedded Cyrillic font (third_party/fonts).
@@ -152,24 +194,55 @@ target_compile_definitions(bbgpu PRIVATE IMGUI_IMPL_VULKAN_NO_PROTOTYPES
BB_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf")
set_source_files_properties(shim/bbport_overlay.cpp PROPERTIES
OBJECT_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf")
target_include_directories(bbgpu PRIVATE shim layer games cp shadps4 ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
target_include_directories(bbgpu PRIVATE . shim layer games cp ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
if (WIN32)
target_include_directories(bbgpu PUBLIC platform/win32/include)
endif()
target_compile_definitions(bbgpu PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
target_compile_options(bbgpu PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
# Loaded with bb-probe at startup (DT_NEEDED, never dlopen): thread_local access without a
# TLS descriptor call (_dl_tlsdesc_return was ~2% of the draw recording thread).
target_compile_options(bbgpu PRIVATE -ftls-model=initial-exec)
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against bb-probe.
target_link_options(bbgpu PRIVATE -Wl,--allow-shlib-undefined)
if (NOT WIN32)
target_compile_options(bbgpu PRIVATE -ftls-model=initial-exec)
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against bb-probe.
target_link_options(bbgpu PRIVATE -Wl,--allow-shlib-undefined)
endif()
target_link_libraries(bbgpu PRIVATE ffx-vulkan::portable ffx-vulkan::fsr4-v07-vulkan)
target_link_libraries(bbgpu PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::X11 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit)
tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit)
if (NOT WIN32)
target_link_libraries(bbgpu PUBLIC PkgConfig::X11)
endif()
# Windows: the runtime (src/, C) and this library in one bb-probe.exe (on Linux build.sh links the
# runtime against libbbgpu.so). Below 1 TiB stay the host heap and thread stacks the game packs
# into 40-bit fields: no high-entropy address randomization.
if (WIN32)
file(GLOB BB_RUNTIME_SOURCES CONFIGURE_DEPENDS ../src/runtime*.c)
file(GLOB BB_ATRAC9_SOURCES CONFIGURE_DEPENDS ../third_party/LibAtrac9/C/src/*.c)
add_library(bbatrac9 STATIC ${BB_ATRAC9_SOURCES})
target_compile_options(bbatrac9 PRIVATE -w)
add_executable(bb-probe ../src/probe.c ../src/vulkan_smoke.c ${BB_RUNTIME_SOURCES}
platform/win32/bb-probe.rc)
set_target_properties(bb-probe PROPERTIES C_STANDARD 11 C_EXTENSIONS ON)
target_include_directories(bb-probe PRIVATE .. ../src)
target_compile_options(bb-probe PRIVATE $<$<COMPILE_LANGUAGE:C>:-Wall -Wextra>)
target_link_libraries(bb-probe PRIVATE bbgpu bbatrac9 pthread ws2_32 bcrypt dbghelp winmm onecore
${Vulkan_LIBRARIES})
# Plain -l names in the packages' CMake files (Zydis: Zycore) are found in the sysroot.
foreach(root ${CMAKE_FIND_ROOT_PATH})
target_link_directories(bb-probe PRIVATE ${root}/lib)
endforeach()
target_link_options(bb-probe PRIVATE -Wl,--disable-high-entropy-va
-Wl,--stack,16777216 -Wl,--error-limit=0)
endif()
# Explicit test targets; neither launches the game nor needs a Vulkan device.
add_executable(motion-history-test EXCLUDE_FROM_ALL ../tests/test_motion_history.cpp)
target_compile_options(motion-history-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(motion-history-test PRIVATE ..)
add_executable(motion-shader-test EXCLUDE_FROM_ALL ../tests/test_motion_shaders.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(motion-shader-test PRIVATE shim shadps4 third_party/gcn/include)
target_include_directories(motion-shader-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
target_link_libraries(motion-shader-test PRIVATE bbgpu)
add_executable(ui-composition-test EXCLUDE_FROM_ALL ../tests/test_ui_composition.cpp)
target_compile_options(ui-composition-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
@@ -182,7 +255,7 @@ target_compile_options(settings-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(settings-test PRIVATE .. shim)
add_executable(scene-resolution-test EXCLUDE_FROM_ALL ../tests/test_scene_resolution.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(scene-resolution-test PRIVATE shim shadps4 third_party/gcn/include)
target_include_directories(scene-resolution-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
# The test's Vulkan-Hpp inline methods have a different set of platform extensions
# from vk_platform.cpp. Keep them local and use an explicit dispatcher in the test.
target_compile_options(scene-resolution-test PRIVATE -UNDEBUG -fPIC)
@@ -190,7 +263,7 @@ set_source_files_properties(../tests/test_scene_resolution.cpp PROPERTIES
COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
target_link_libraries(scene-resolution-test PRIVATE bbgpu)
add_executable(taa-shader-test EXCLUDE_FROM_ALL ../tests/test_taa_shader.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(taa-shader-test PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include)
target_include_directories(taa-shader-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
target_compile_options(taa-shader-test PRIVATE -UNDEBUG -fPIC)
set_source_files_properties(../tests/test_taa_shader.cpp PROPERTIES
COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
@@ -198,14 +271,14 @@ target_link_libraries(taa-shader-test PRIVATE bbgpu)
add_dependencies(taa-shader-test host_shaders)
add_executable(camera-motion-test EXCLUDE_FROM_ALL ../tests/test_camera_motion.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(camera-motion-test PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include)
target_include_directories(camera-motion-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
target_compile_options(camera-motion-test PRIVATE -UNDEBUG -fPIC)
target_link_libraries(camera-motion-test PRIVATE bbgpu)
add_dependencies(camera-motion-test host_shaders)
add_executable(translation-memory-test EXCLUDE_FROM_ALL ../tests/test_translation_memory.cpp
../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(translation-memory-test PRIVATE shim shadps4 third_party/gcn/include)
target_include_directories(translation-memory-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
target_compile_options(translation-memory-test PRIVATE -UNDEBUG -fPIC)
set_source_files_properties(../tests/test_translation_memory.cpp PROPERTIES
COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
@@ -218,8 +291,8 @@ target_include_directories(upscaler-support-test PRIVATE shim)
target_compile_options(upscaler-support-test PRIVATE -UNDEBUG)
add_executable(fsr4-bench EXCLUDE_FROM_ALL tools/fsr4_bench.cpp
shadps4/video_core/renderer_vulkan/fsr411/fsr411.cpp)
target_include_directories(fsr4-bench PRIVATE shadps4/video_core/renderer_vulkan/fsr411)
upscale/fsr411/fsr411.cpp)
target_include_directories(fsr4-bench PRIVATE upscale/fsr411)
target_link_libraries(fsr4-bench PRIVATE ffx-vulkan::fsr4-v07-vulkan ffx-vulkan::fsr4-v07-assets Vulkan::Vulkan)
# bbport online module (gpu/bbnet, libbbnet.so): built when its libraries are there (shell.nix);
@@ -237,6 +310,24 @@ if (BB_NET)
add_subdirectory(bbnet)
message(STATUS "Online module: on (libbbnet.so)")
else()
message(STATUS "Online module: off (protobuf, nlohmann_json, OpenSSL, zlib or miniupnpc missing)")
# Named one by one (#145: "the build has no online module" with no hint why).
set(missing "")
foreach(dep Protobuf nlohmann_json OpenSSL ZLIB)
if (NOT ${dep}_FOUND)
list(APPEND missing ${dep})
endif()
endforeach()
if (NOT MINIUPNPC_INCLUDE OR NOT MINIUPNPC_LIBRARY)
list(APPEND missing miniupnpc)
endif()
list(JOIN missing ", " missing)
message(STATUS "Online module: off (missing: ${missing}; shell.nix lists the packages)")
endif()
endif()
# bbport: the shader corpus replay (render/shader_capture.h): ninja -C out/gpu shader-corpus-test.
add_executable(shader-corpus-test EXCLUDE_FROM_ALL ../tests/test_shader_corpus.cpp
../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(shader-corpus-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
target_compile_options(shader-corpus-test PRIVATE -UNDEBUG -fPIC)
target_link_libraries(shader-corpus-test PRIVATE bbgpu)
+13 -10
View File
@@ -1,10 +1,13 @@
Vendored from https://github.com/shadps4-emu/shadPS4 commit b4e7ae776071ceb1dd35004577a8719481e1d258 (GPL-2.0-or-later).
Directories: src/video_core, src/shader_recompiler, src/common, src/core/libraries/{gnmdriver,videoout}, externals/gcn, externals/sirit, externals/half.
Also: src/core/libraries/kernel/{orbis_error,posix_error,time,process,equeue}.h, equeue.cpp, src/core/libraries/error_codes.h, src/core/platform.h, src/core/libraries/system/{userservice,systemservice}.h.
Also: src/core/libraries/avplayer (without the handle streamer), src/core/libraries/videodec/video_utils.{h,cpp}.
Local changes are marked with "bbport:" comments.
Backported from upstream after b4e7ae7 (2026-10-02), recompiler correctness fixes only:
233562929 V_INTERP_MOV_F32 P10/P20 selectors (#5087), d96278e1e compute shader floating-point
mode (#5126), e518a6515 S_SUBB_U32 SCC wrap (#5031), 282e5c792 S_ADDC_U32 SCC carry-out (#5030),
fb6367d29 32-bit literals in the high dword of 64-bit floats (#5114), 94e217781 V_BFM_B32 (#5185),
d1cb90dbb ADDC (#5200), dd43d1cac V_CVT_PK_U8_F32 (#5194), ef08b1e1f V_ALIGNBIT/ALIGNBYTE (#5186).
The GPU layer (gpu/) is the port's own code: command processor (cp/), GCN hardware definitions
(gcn/), shader translator GCN -> SPIR-V (shader/), renderer (render/), buffer and texture caches
(buffers/, textures/), memory tracking (memory/), platform libraries (platform/), upscalers
(upscale/). It started from shadPS4 (https://github.com/shadps4-emu/shadPS4, commit
b4e7ae776071ceb1dd35004577a8719481e1d258, GPL-2.0-or-later) and was rewritten part by part;
gpu/shadps4 was removed on 2026-10-13. The shader translator was checked against the shadPS4
translator it replaced on a corpus of the game's shaders (byte-identical SPIR-V,
tests/test_shader_corpus.cpp).
The online module (gpu/bbnet) keeps its shadPS4 network and NP code (bbnet/shadps4).
Third-party libraries in third_party/: sirit (SPIR-V emitter), gcn (AMD GCN headers), half,
httplib, imgui, fonts, amd-fsr1 (FidelityFX headers used by shaders), fsr-vulkan.
+29 -1
View File
@@ -1,4 +1,4 @@
/* C interface of the GPU library (gpu/): shadPS4's Liverpool/Vulkan video core,
/* C interface of the GPU library (gpu/): the command processor and Vulkan renderer,
* GnmDriver, VideoOut and kernel event queues, adapted to the native loader. */
#ifndef BBGPU_H
#define BBGPU_H
@@ -36,7 +36,35 @@ int bbgpu_overlay_captures_input(void);
* bbport.ini keeps the window from taking it. */
void bbgpu_mouse_take(double *dx, double *dy, int *wheel_up, int *wheel_down);
int bbgpu_mouse_captured(void);
/* Asks the window to take the mouse (relative mode, as a click in the game does); the window
* thread does it. */
void bbgpu_mouse_capture(void);
void bbgpu_mouse_look_enable(int enabled);
/* A module's look at the window's events first (an SDL_Event and the SDL_Window, on the window
* thread): nonzero takes the event (the port and the game do not see it). Up to 4. */
typedef int (*BbGpuEventFilter)(const void *sdl_event, void *sdl_window);
void bbgpu_add_event_filter(BbGpuEventFilter filter);
/* A module's menu pointer: nonzero while the game shows a menu the mouse points into. The window
* then lets the mouse go and shows the cursor, and takes the mouse back after (as it had it).
* Up to 4; pointer_wanted: any of them says so. */
typedef int (*BbGpuPointerQuery)(void);
void bbgpu_add_pointer_query(BbGpuPointerQuery query);
int bbgpu_pointer_wanted(void);
/* A module's changes to the pad state the game reads (runtime_pad.c, after the host's input and
* the scripted one): the buttons (PS4 bits); the sticks (lx ly rx ry) as they are. Up to 4. */
typedef void (*BbGpuPadFilter)(uint32_t *buttons, const uint8_t *sticks);
void bbgpu_add_pad_filter(BbGpuPadFilter filter);
void bbgpu_filter_pad(uint32_t *buttons, const uint8_t *sticks);
/* The shown frame's width / height (the display buffer's; the window letterboxes it). */
float bbgpu_frame_aspect(void);
/* A line of text over the game, near the bottom, until called with NULL (any thread). */
void bbgpu_overlay_banner(const char *utf8);
/* Native mouse look: turns the game's camera (radians, applied at its next update); 0 when the
* camera hook is not in (another game version, BB_NATIVE_MOUSE=0) or the camera does not take the
* turns now (lock-on, menus): drive the right stick then. */
int bbgpu_camera_look(float yaw, float pitch);
/* The game's current camera state (its projection parameters at +0x50), 0 until known. */
uint64_t bbgpu_camera_state(void);
/* Patches the loaded image before the game runs (image still writable): libGnm entry hooks. */
void bbgpu_patch_image(unsigned char *image, uint64_t size);
/* Number of symbols registered by the vendored libraries (diagnostics). */
+9 -3
View File
@@ -15,17 +15,23 @@ add_custom_command(
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/shadps4/shadnet/shadnet.proto")
file(GLOB_RECURSE BBNET_SOURCES CONFIGURE_DEPENDS shadps4/core/libraries/network/*.cpp
shadps4/core/libraries/np/*.cpp shadps4/shadnet/*.cpp)
# BBSC, Bloodborne seamless co-op (GPL-2.0+): its game code and the
# port's stand-ins for shadPS4's UI (bbsc_glue.cpp). Installed only for Bloodborne with BB_BBSC=1.
list(APPEND BBNET_SOURCES shadps4/core/bloodborne_re.cpp shadps4/core/bbsc_persistence.cpp
shadps4/core/bbsc_memscan.cpp shadps4/core/bbsc_cinematic.cpp bbsc_glue.cpp)
# Trophies and the system dialogs stay in the C runtime; their shadPS4 versions need the
# emulator's UI layers.
list(FILTER BBNET_SOURCES EXCLUDE REGEX
"np_trophy|trophy_ui|np_sns_facebook_dialog|np_profile_dialog/|np_commerce/")
add_library(bbnet SHARED ${BBNET_SOURCES} "${SHADNET_PROTO_OUT}/shadnet.pb.cc" bbnet_glue.cpp)
# The module's own headers first, then the port's shim and shadPS4 (common/, logging).
# The module's own headers first, then the port's utilities (common/, logging) and shim.
target_include_directories(bbnet PRIVATE include shadps4 shadps4/shadnet "${SHADNET_PROTO_OUT}"
${CMAKE_CURRENT_SOURCE_DIR} ../shim ../shadps4 ../third_party/imgui ../third_party/httplib)
${CMAKE_CURRENT_SOURCE_DIR} .. ../shim ../third_party/imgui ../third_party/httplib)
# The version the shadNet client reports to the server (ShadNetClient::BuildVersionString).
set(BBPORT_VERSION "0.6-pre1")
target_compile_definitions(bbnet PRIVATE BOOST_ASIO_STANDALONE NDEBUG
CPPHTTPLIB_OPENSSL_SUPPORT CPPHTTPLIB_ZLIB_SUPPORT)
CPPHTTPLIB_OPENSSL_SUPPORT CPPHTTPLIB_ZLIB_SUPPORT BBNET_PORT_VERSION="${BBPORT_VERSION}")
target_compile_options(bbnet PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden
-Wno-deprecated-declarations -Wno-missing-field-initializers)
target_link_options(bbnet PRIVATE -Wl,-Bsymbolic -Wl,--exclude-libs,ALL)
+5
View File
@@ -30,6 +30,11 @@ typedef struct BbNetHost {
typedef int (*BbNetInitFn)(const BbNetHost *host);
int bbnet_init(const BbNetHost *host);
/* Optional: the game's image is mapped and patched, before its first instruction runs (game
* modules of the online module install their hooks here: BBSC for Bloodborne). */
typedef void (*BbNetImageReadyFn)(uint64_t image_base, uint64_t image_size);
void bbnet_image_ready(uint64_t image_base, uint64_t image_size);
/* The module's function for an imported NID ("NID#lib#mod"), or 0. */
typedef uintptr_t (*BbNetResolveFn)(const char *scoped_nid);
uintptr_t bbnet_resolve(const char *scoped_nid);
+108 -23
View File
@@ -4,6 +4,7 @@
// module's C interface (bbnet.h). From the Linux co-op fork of bbport, which took it from the
// Windows port; the libraries are shadp2p's (a shadPS4 fork, GPL-2.0).
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstdio>
@@ -24,6 +25,7 @@
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#include <unistd.h>
#endif
@@ -31,6 +33,15 @@
#include "common/types.h"
#include "core/emulator_settings.h"
#include "core/bloodborne_re.h"
#include "core/bbsc_cinematic.h"
#include "bbgpu.h"
#include <SDL3/SDL_events.h>
#include "core/cpu_patches.h"
#include "common/memory_patcher.h"
#include "common/singleton.h"
#include "core/bbsc_persistence.h"
#include "core/file_format/psf.h"
#include "games/game_profile.h"
#include "core/debugger.h"
#include "core/libraries/kernel/kernel.h"
#include "core/libraries/kernel/memory.h"
@@ -58,6 +69,7 @@
#include "core/libraries/np/np_score/np_score.h"
#include "core/libraries/np/np_signaling/np_signaling.h"
#include "core/libraries/np/np_tus.h"
#include "core/libraries/np/np_utility.h" // BBSC: sceNpLookupNpId online (3rd player and on)
#include "core/libraries/np/np_web_api/np_web_api.h"
#include "core/libraries/np/np_web_api2/np_web_api2.h"
@@ -318,29 +330,7 @@ s32 GetCurrentPid() {
}
} // namespace Core::Debugger
namespace Core::Bloodborne {
namespace {
std::mutex placement_mutex;
std::optional<std::string> placement;
} // namespace
std::optional<std::string> GetSeamlessHostPlacementHeader() {
std::scoped_lock lock{placement_mutex};
return placement;
}
bool SetSeamlessHostPlacementHeader(std::string_view value) {
if (value.empty() || value.size() > 4096) {
return false;
}
std::scoped_lock lock{placement_mutex};
placement = std::string{value};
return true;
}
void ClearSeamlessHostPlacementHeader() {
std::scoped_lock lock{placement_mutex};
placement.reset();
}
void TraceMatching2LeaveRoom(std::uintptr_t, std::uint64_t) {}
} // namespace Core::Bloodborne
// Core::Bloodborne: BBSC's seamless co-op (shadps4/core/bloodborne_re.cpp).
// ---- Notifications (log only for now) ------------------------------------------------------------
@@ -448,6 +438,12 @@ private:
namespace {
void Configure(const char* app0, const char* serial, const char* title, const char* app_ver) {
Core::Emulator::SetGame(app0, serial, title, app_ver);
// What BBSC reads of the game (param.sfo, the serial: shadPS4's PSF and memory patcher).
auto* psf = Common::Singleton<PSF>::Instance();
psf->app_ver = app_ver ? app_ver : "";
psf->title_id = serial ? serial : "";
psf->title = title ? title : "";
MemoryPatcher::g_game_serial = serial ? serial : "";
const auto& ids = Common::ElfInfo::Instance().GetNpCommIds();
std::printf("Co-op: %s, NP communication id %s%s\n", serial ? serial : "?",
ids.empty() ? "missing" : ids.front().c_str(),
@@ -484,6 +480,89 @@ extern "C" __attribute__((visibility("default"))) uintptr_t bbnet_resolve(const
return 0;
}
extern "C" __attribute__((visibility("default"))) void bbnet_image_ready(std::uint64_t base,
std::uint64_t size) {
MemoryPatcher::g_eboot_address = base;
MemoryPatcher::g_eboot_image_size = size;
// BBSC (Bloodborne seamless co-op): BB_BBSC=1 (the launcher's
// co-op switch). Its game code checks for CUSA03173 01.09; every retail Bloodborne runs the
// same 1.09 executable (gpu/games/bloodborne.cpp), and each of its sites is byte-checked.
const char* bbsc = std::getenv("BB_BBSC");
if (!bbsc || bbsc[0] != '1') {
return;
}
const Game::Profile* profile = Game::Active();
if (!profile || std::string_view{profile->name} != "Bloodborne") {
std::printf("BBSC: not Bloodborne: seamless co-op off\n");
return;
}
MemoryPatcher::g_game_serial = "CUSA03173";
// BBSC finds the save it backs up and journals in shadPS4's folders; the port's are
// <user>/savedata/<user id>/CUSA00207/SPRJ0005 (BBSC_SAVE_DIR: the folder of userdata0000..).
if (!std::getenv("BBSC_SAVE_DIR")) {
const char* user = std::getenv("BB_GPU_USER_DIR");
std::error_code error;
for (const auto& id : std::filesystem::directory_iterator(
std::filesystem::path(user ? user : "user") / "savedata", error)) {
const auto folder = id.path() / "CUSA00207" / "SPRJ0005";
if (std::filesystem::is_directory(folder, error)) {
setenv("BBSC_SAVE_DIR", folder.c_str(), 0);
std::printf("BBSC: save folder %s\n", folder.c_str());
break;
}
}
}
// BBSC writes its byte patches straight into the game's code, and turns some on and off
// while the game runs (session teardown, the host's lamp): its code pages stay writable
// (shadPS4 maps them so; the port maps them read and execute).
unsigned writable = 0;
if (FILE* maps = std::fopen("/proc/self/maps", "r")) {
char line[512];
while (std::fgets(line, sizeof(line), maps)) {
unsigned long long start = 0, end = 0;
char perms[5] = {};
if (std::sscanf(line, "%llx-%llx %4s", &start, &end, perms) != 3 || perms[2] != 'x') {
continue;
}
start = std::max<unsigned long long>(start, base);
end = std::min<unsigned long long>(end, base + size);
if (start < end) {
writable += mprotect(reinterpret_cast<void*>(start), end - start,
PROT_READ | PROT_WRITE | PROT_EXEC) == 0;
std::printf("BBSC: code %#llx-%#llx writable\n", start, end);
}
}
std::fclose(maps);
}
std::printf("BBSC: seamless co-op hooks for the image at %#llx (%u code mappings writable)\n",
static_cast<unsigned long long>(base), writable);
if (const char* probe = std::getenv("BB_BBSC_PROBE")) {
const unsigned long long at = std::strtoull(probe, nullptr, 0);
if (FILE* maps = std::fopen("/proc/self/maps", "r")) {
char line[512];
while (std::fgets(line, sizeof(line), maps)) {
unsigned long long a = 0, b = 0;
if (std::sscanf(line, "%llx-%llx", &a, &b) == 2 && at >= a && at < b) {
std::printf("BBSC probe: %s", line);
}
}
std::fclose(maps);
}
std::fflush(stdout);
std::printf("BBSC probe: byte %#x\n", unsigned(*reinterpret_cast<volatile u8*>(at)));
}
// The cinematic camera (F1, keyboard and mouse): BBSC sees the window's events first.
if (Core::Bbsc::SwitchEnabled("SHADPS4_BBSC_CINEMATIC")) {
bbgpu_add_event_filter([](const void* event, void* window) -> int {
return Core::Bbsc::CinematicHandleEvent(*static_cast<const SDL_Event*>(event),
static_cast<SDL_Window*>(window));
});
}
Core::Bloodborne::InstallSeamlessCoopPatches();
Core::Bbsc::InstallPersistentSessionPatches();
Core::Bloodborne::InstallReverseEngineeringTrace();
}
extern "C" __attribute__((visibility("default"))) int bbnet_init(const BbNetHost* host) {
if (!host || host->abi != BBNET_ABI_VERSION || !host->thread_spawn || !host->thread_join) {
std::printf("Online: the runtime and the online module are from different versions\n");
@@ -491,6 +570,11 @@ extern "C" __attribute__((visibility("default"))) int bbnet_init(const BbNetHost
}
g_host = *host;
Configure(host->app0, host->serial, host->title, host->app_ver);
// BBSC patches the game's code, some of it while the game runs: from now on the port's own
// code patches (the GPU side's hooks go in meanwhile) leave it writable (bbnet_image_ready).
if (const char* bbsc = std::getenv("BB_BBSC"); bbsc && bbsc[0] == '1') {
Core::KeepGuestCodeWritable();
}
Core::Loader::SymbolsResolver resolver;
Core::Loader::SymbolsResolver* sym = &resolver;
Libraries::Net::RegisterLib(sym);
@@ -508,6 +592,7 @@ extern "C" __attribute__((visibility("default"))) int bbnet_init(const BbNetHost
Libraries::Np::NpScore::RegisterLib(sym);
Libraries::Np::NpSignaling::RegisterLib(sym);
Libraries::Np::NpTus::RegisterLib(sym);
Libraries::Np::NpUtility::RegisterLib(sym);
Libraries::Np::NpWebApi::RegisterLib(sym);
Libraries::Np::NpWebApi2::RegisterLib(sym);
std::printf("Online: %zu network and PSN functions\n", g_symbols.size());
+27
View File
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport online module: what BBSC (shadps4/core/bloodborne_re.cpp, GPL-2.0+)
// needs from shadPS4's UI. Its loading screen is a line in the port's overlay; its animated title
// card (off by default in BBSC too) is not ported.
#include <cstdio>
#include <string_view>
#include "bbgpu.h"
#include "imgui/bbsc_intro_layer.h"
#include "imgui/bbsc_loading_layer.h"
namespace Core::Bbsc {
void ShowLoadingScreen(LoadingScreenKind kind) {
// The port's overlay line (BBSC draws its own loading screen in shadPS4's ImGui layer).
bbgpu_overlay_banner(kind == LoadingScreenKind::Summon ? "Joining the host's world..."
: "Following the host...");
}
void HideLoadingScreen() {
bbgpu_overlay_banner(nullptr);
}
void StartIntro(std::string_view) {}
} // namespace Core::Bbsc
+11
View File
@@ -0,0 +1,11 @@
// bbport online module: what BBSC (bloodborne_re.cpp, bbsc_persistence.cpp) reads of shadPS4's
// memory patcher: where the game's image is and which game it is. Set by bbnet_init (bbnet.h).
#pragma once
#include <cstdint>
#include <string>
namespace MemoryPatcher {
inline std::uint64_t g_eboot_address = 0;
inline std::uint64_t g_eboot_image_size = 0;
inline std::string g_game_serial;
} // namespace MemoryPatcher
+48 -2
View File
@@ -1,14 +1,60 @@
// bbport co-op: the Bloodborne summon helpers from shadp2p's bloodborne_re.h. Seamless
// co-op (host placement) is not ported yet; these keep the header value between requests.
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
namespace Core::Bloodborne {
std::optional<std::string> GetSeamlessHostPlacementHeader();
bool SetSeamlessHostPlacementHeader(std::string_view value);
void ClearSeamlessHostPlacementHeader();
// BBSC connect-first cross-map summon: the host map a pending sign should advertise, if any.
std::optional<std::uint32_t> GetBbscConnectFirstAreaId();
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.
void OnBbscHostFollowPacket(std::uint32_t packed_map, float x, float y, float z, float heading,
std::uint32_t sequence, std::int32_t lamp_point);
// BBSC world state: one chunk of the host's event flags for its map block arrived (a whole
// SignalingBbscWorldFlags packet). Signaling receive thread; only stores it.
void OnBbscWorldFlagsPacket(const void* packet, std::uint32_t size);
// BBSC life state: another player says whether it is dead (spectating) or alive. Relay receive
// thread; only stores it.
void OnBbscLifePacket(std::uint32_t player, std::uint8_t dead, std::uint32_t sequence);
// BBSC scaling: the host's five scaling percentages (enemy HP, boss HP, damage, enemy stagger, boss
// stagger). Relay receive thread; only stores them.
void OnBbscScalePacket(const std::uint16_t (&percent)[5]);
// BBSC PvP (Duel Bell): kind 0 = the host's PvP state, kind 1 = a guest asks the host for `on`.
// Relay receive thread; only stores it.
void OnBbscPvpPacket(std::uint8_t kind, std::uint8_t on);
// BBSC relay sessions (mod): a hosting player's follow packet (relay thread; only stored), the host
// a guest takes packets from (0 = none known yet), and the session state as of the last game tick.
// BBSC shared progress: the host's boss-defeat flags that are on (relay "SHAD" 0x26). Relay thread.
void OnBbscProgressPacket(const std::uint32_t* flags, std::size_t count);
// BBSC boss line: the boss on the host's boss bar (name id, -1 none; relay "SHAD" 0x27).
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();
std::int32_t GetBbscRelaySessionState();
void InstallSeamlessCoopPatches();
void InstallReverseEngineeringTrace();
// BBSC Discord status: what the game is doing, gathered once a second on the game tick.
struct BbscPresence {
bool in_game = false;
std::string area; // "Central Yharnam", "Chalice Dungeon", ...
std::string boss; // boss bar name during a boss fight, else empty
int role = 0; // 0 solo, 1 host, 2 guest
int hunters = 1; // players in the session
int max_hunters = 3;
std::int64_t boss_since_s = 0; // unix time the boss bar appeared
};
BbscPresence GetBbscPresence();
} // namespace Core::Bloodborne
+5
View File
@@ -0,0 +1,5 @@
// bbport online module: shadPS4's core/cpu_patches.h for BBSC (bloodborne_re.cpp): the hook
// interface is the port's own, in libbbgpu (bbport_guest_code_hooks).
#pragma once
#include "bbport_guest_code_hooks.h"
#include "common/types.h"
+17
View File
@@ -0,0 +1,17 @@
// bbport online module: shadPS4's param.sfo reader as BBSC uses it (GetString("APP_VER")),
// answered from what the runtime passed to bbnet_init.
#pragma once
#include <optional>
#include <string>
#include <string_view>
class PSF {
public:
std::optional<std::string_view> GetString(std::string_view key) const {
if (key == "APP_VER" && !app_ver.empty()) return std::string_view{app_ver};
if (key == "TITLE_ID" && !title_id.empty()) return std::string_view{title_id};
if (key == "TITLE" && !title.empty()) return std::string_view{title};
return std::nullopt;
}
std::string app_ver, title_id, title;
};
+74
View File
@@ -0,0 +1,74 @@
// bbport online module: shadPS4's memory manager as BBSC asks it (is this address mapped, with
// which protection), answered from the runtime's own map (runtime_memory_vma_info). Each thread
// remembers the last few mappings for a moment: BBSC checks every read it makes, and the map is
// behind a lock the game's own mappings take.
#pragma once
#include <array>
#include <chrono>
#include <cstdint>
#include "common/singleton.h"
#include "common/types.h"
extern "C" int runtime_memory_vma_info(uintptr_t address, int* prot, int* type, uintptr_t* end);
namespace Core {
enum class MemoryProt : u32 {
NoAccess = 0,
CpuRead = 1,
CpuWrite = 2,
CpuReadWrite = 3,
CpuExec = 4,
GpuRead = 16,
GpuWrite = 32,
GpuReadWrite = 48,
};
class MemoryManager {
public:
bool IsValidMapping(u64 address, u64 size) {
u32 prot = 0;
void* start = nullptr;
void* end = nullptr;
return QueryProtection(address, &start, &end, &prot) == 0 &&
address + size <= reinterpret_cast<u64>(end);
}
/// 0 and the mapping's range and protection (MemoryProt bits), or -1 when unmapped.
int QueryProtection(u64 address, void** start, void** end, u32* prot) {
struct Cached {
u64 start = 0, end = 0;
u32 prot = 0;
std::chrono::steady_clock::time_point at{};
};
thread_local std::array<Cached, 8> cache{};
thread_local unsigned next = 0;
const auto now = std::chrono::steady_clock::now();
for (const auto& c : cache) {
if (address >= c.start && address < c.end &&
now - c.at < std::chrono::milliseconds(250)) {
*start = reinterpret_cast<void*>(c.start);
*end = reinterpret_cast<void*>(c.end);
*prot = c.prot;
return 0;
}
}
int p = 0, type = -1;
uintptr_t mapping_end = 0;
if (!runtime_memory_vma_info(address, &p, &type, &mapping_end)) {
return -1;
}
// The runtime's map gives the end of the mapping, not its start: the address itself
// stands for it (callers check address >= start).
auto& slot = cache[next++ % cache.size()];
slot = {address & ~u64(0xfff), mapping_end, u32(p), now};
*start = reinterpret_cast<void*>(slot.start);
*end = reinterpret_cast<void*>(slot.end);
*prot = slot.prot;
return 0;
}
};
using Memory = Common::Singleton<MemoryManager>;
} // namespace Core
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string_view>
namespace Core::Bbsc {
// BBSC's animated title card, drawn by the ImGui overlay over the game's own boot logos (so it
// adds no loading time). Bloodborne (CUSA03173) only; switch SHADPS4_BBSC_INTRO, default on.
void StartIntro(std::string_view serial);
} // namespace Core::Bbsc
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
namespace Core::Bbsc {
// What the guest is loading into, for the loading screen's line.
enum class LoadingScreenKind { Summon, Follow };
// BBSC's own loading screen, drawn by the ImGui overlay over the black load cover while a guest's
// summon / cross-map / follow load runs (the game shows nothing there). Any thread.
void ShowLoadingScreen(LoadingScreenKind kind);
void HideLoadingScreen();
} // namespace Core::Bbsc
@@ -9,10 +9,10 @@
#include <vector>
#include <map>
#include "assert.h"
#include "bit_field.h"
#include "singleton.h"
#include "types.h"
#include "common/assert.h"
#include "common/bit_field.h"
#include "common/singleton.h"
#include "common/types.h"
namespace Core {
class Emulator;
File renamed without changes.
+158
View File
@@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
// Input side of the BBSC cinematic camera. While it is on, the keyboard and mouse fly the camera
// and never reach the game (shadPS4 would otherwise map them to the pad and move the hunter too);
// the controller is untouched. Keys:
// F1 on/off | hold right mouse: look | WASD move | E / Space up, Q / C down
// Shift fast, Alt slow | wheel: speed | Z / X zoom, V reset zoom | , . roll, / reset roll
// arrow keys: look | [ ] smoothing (glide) less / more
#include <mutex>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_mouse.h>
#include "common/logging/log.h"
#include "core/bbsc_cinematic.h"
#include "core/bbsc_persistence.h"
namespace Core::Bbsc {
namespace {
std::mutex cine_mutex;
CinematicInput cine_state;
bool looking = false;
bool CinematicEnabledBySwitch() {
static const bool enabled = SwitchEnabled("SHADPS4_BBSC_CINEMATIC");
return enabled;
}
void SetKey(SDL_Keycode key, bool down) {
switch (key) {
case SDLK_W: cine_state.forward = down; break;
case SDLK_S: cine_state.back = down; break;
case SDLK_A: cine_state.left = down; break;
case SDLK_D: cine_state.right = down; break;
case SDLK_E:
case SDLK_SPACE: cine_state.up = down; break;
case SDLK_Q:
case SDLK_C: cine_state.down = down; break;
case SDLK_LSHIFT:
case SDLK_RSHIFT: cine_state.fast = down; break;
case SDLK_LALT:
case SDLK_RALT: cine_state.slow = down; break;
case SDLK_Z: cine_state.zoom_in = down; break;
case SDLK_X: cine_state.zoom_out = down; break;
case SDLK_V: cine_state.zoom_reset = cine_state.zoom_reset || down; break;
case SDLK_COMMA: cine_state.roll_left = down; break;
case SDLK_PERIOD: cine_state.roll_right = down; break;
case SDLK_SLASH: cine_state.roll_reset = cine_state.roll_reset || down; break;
case SDLK_UP: cine_state.look_up = down; break;
case SDLK_DOWN: cine_state.look_down = down; break;
case SDLK_LEFT: cine_state.look_left = down; break;
case SDLK_RIGHT: cine_state.look_right = down; break;
case SDLK_LEFTBRACKET:
if (down) --cine_state.smooth_steps;
break;
case SDLK_RIGHTBRACKET:
if (down) ++cine_state.smooth_steps;
break;
default: break;
}
}
void ReleaseAll(SDL_Window* window) {
const bool enabled = cine_state.enabled;
cine_state = CinematicInput{};
cine_state.enabled = enabled;
if (looking && window != nullptr) {
SDL_SetWindowRelativeMouseMode(window, false);
}
looking = false;
}
} // namespace
bool CinematicHandleEvent(const SDL_Event& event, SDL_Window* window) {
if (!CinematicEnabledBySwitch()) {
return false;
}
std::scoped_lock lock{cine_mutex};
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_F1) {
if (!event.key.repeat) {
cine_state.enabled = !cine_state.enabled;
ReleaseAll(window);
LOG_INFO(Debug, "BBSC cinematic camera: {} (F1)", cine_state.enabled ? "on" : "off");
}
return true;
}
if (event.type == SDL_EVENT_KEY_UP && event.key.key == SDLK_F1) {
return true;
}
if (!cine_state.enabled) {
return false;
}
switch (event.type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: {
const SDL_Keycode key = event.key.key;
if (key >= SDLK_F2 && key <= SDLK_F12) {
return false; // the emulator's own hotkeys (fullscreen, fps, pause...) keep working
}
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_RIGHT) {
looking = event.type == SDL_EVENT_MOUSE_BUTTON_DOWN;
if (window != nullptr) {
SDL_SetWindowRelativeMouseMode(window, looking);
}
}
return true;
case SDL_EVENT_MOUSE_MOTION:
if (looking) {
cine_state.mouse_dx += event.motion.xrel;
cine_state.mouse_dy += event.motion.yrel;
}
return true;
case SDL_EVENT_MOUSE_WHEEL:
cine_state.wheel += event.wheel.y > 0.0f ? 1 : (event.wheel.y < 0.0f ? -1 : 0);
return true;
case SDL_EVENT_WINDOW_FOCUS_LOST:
ReleaseAll(window);
return false;
default:
return false;
}
}
CinematicInput TakeCinematicInput() {
std::scoped_lock lock{cine_mutex};
CinematicInput out = cine_state;
cine_state.mouse_dx = 0.0f;
cine_state.mouse_dy = 0.0f;
cine_state.wheel = 0;
cine_state.smooth_steps = 0;
cine_state.zoom_reset = false;
cine_state.roll_reset = false;
return out;
}
void CinematicForceOff() {
std::scoped_lock lock{cine_mutex};
cine_state = CinematicInput{};
}
void CinematicForceOn() {
std::scoped_lock lock{cine_mutex};
if (!cine_state.enabled) {
cine_state = CinematicInput{};
cine_state.enabled = true;
}
}
} // namespace Core::Bbsc
+45
View File
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// BBSC cinematic camera (2026-09-28): a free camera for filming, flown with keyboard and mouse while
// the controller keeps playing the hunter. The window side (sdl_window.cpp) feeds it input; the
// game side (bloodborne_re.cpp, BbscCinematicCameraHook) turns the game's own debug free camera on
// and moves it. Switch SHADPS4_BBSC_CINEMATIC (default on). F1 toggles it.
union SDL_Event;
struct SDL_Window;
namespace Core::Bbsc {
// Window thread. True when the event belongs to the cinematic camera and must not reach the game.
bool CinematicHandleEvent(const SDL_Event& event, SDL_Window* window);
struct CinematicInput {
bool enabled = false;
bool forward = false, back = false, left = false, right = false, up = false, down = false;
bool fast = false, slow = false;
bool zoom_in = false, zoom_out = false, zoom_reset = false;
bool roll_left = false, roll_right = false, roll_reset = false;
bool look_up = false, look_down = false, look_left = false, look_right = false;
float mouse_dx = 0.0f; // accumulated since the last TakeCinematicInput
float mouse_dy = 0.0f;
int wheel = 0; // speed steps, accumulated
int smooth_steps = 0; // smoothing steps, accumulated
// Controller (mod only, -1..1, added to the keys): left stick move, right stick look, L1/R1
// down/up, L2/R2 zoom out/in.
float pad_move_x = 0.0f, pad_move_y = 0.0f, pad_rise = 0.0f;
float pad_look_x = 0.0f, pad_look_y = 0.0f, pad_zoom = 0.0f;
};
// Game thread: the current keys plus everything accumulated since the last call.
CinematicInput TakeCinematicInput();
// Game thread: the camera could not be switched on (logged); the toggle goes back to off.
void CinematicForceOff();
// Game thread: switches the camera on (you died in co-op); nothing when it is already on.
void CinematicForceOn();
} // namespace Core::Bbsc
+126
View File
@@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/bbsc_memscan.h"
#ifdef _WIN32
#include <windows.h>
#endif
#include <algorithm>
#include <cstdio>
#include <cstring>
#ifndef _WIN32
#include <sys/uio.h>
#include <unistd.h>
#endif
namespace Core::Bbsc {
namespace {
#ifdef _WIN32
// One region. The emulator changes page protection all the time (GPU tracking), so a page that was
// readable a moment ago may fault now: the search runs under SEH and a fault just ends the region.
void SearchRegion(const std::uint8_t* start, std::size_t size, const std::uint8_t* needle,
std::size_t length, std::vector<std::uint64_t>& hits, std::size_t max_hits) {
__try {
const std::uint8_t* end = start + size;
const std::uint8_t* at = start;
while (hits.size() < max_hits && end - at >= static_cast<std::ptrdiff_t>(length)) {
at = static_cast<const std::uint8_t*>(
std::memchr(at, needle[0], static_cast<std::size_t>(end - at) - length + 1));
if (at == nullptr) {
break;
}
if (std::memcmp(at, needle, length) == 0) {
hits.push_back(reinterpret_cast<std::uint64_t>(at));
}
++at;
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
}
}
#endif
} // namespace
std::vector<std::uint64_t> FindInMemory(std::span<const std::uint8_t> needle, std::uint64_t lo,
std::uint64_t hi, std::size_t max_hits) {
std::vector<std::uint64_t> hits;
#ifdef _WIN32
if (needle.empty()) {
return hits;
}
constexpr DWORD Readable = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ |
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
std::uint64_t at = lo;
while (at < hi && hits.size() < max_hits) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(reinterpret_cast<void*>(at), &info, sizeof(info)) == 0) {
break;
}
const auto start = reinterpret_cast<std::uint64_t>(info.BaseAddress);
const std::uint64_t end = start + info.RegionSize;
if (info.State == MEM_COMMIT && (info.Protect & Readable) != 0 &&
(info.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0) {
SearchRegion(reinterpret_cast<const std::uint8_t*>(start), info.RegionSize,
needle.data(), needle.size(), hits, max_hits);
}
if (end <= at) {
break;
}
at = end;
}
#else
// bbport (Linux): readable mappings from /proc/self/maps, copied in pieces with
// process_vm_readv on this process: a page the port protects meanwhile (its write and read
// traps) only fails that copy instead of faulting.
if (needle.empty()) {
return hits;
}
FILE* maps = std::fopen("/proc/self/maps", "r");
if (!maps) {
return hits;
}
constexpr std::size_t Piece = 1u << 20;
std::vector<std::uint8_t> buffer(Piece + needle.size());
char line[512];
while (hits.size() < max_hits && std::fgets(line, sizeof(line), maps)) {
unsigned long long start = 0, end = 0;
char perms[5] = {};
if (std::sscanf(line, "%llx-%llx %4s", &start, &end, perms) != 3 || perms[0] != 'r') {
continue;
}
start = std::max<unsigned long long>(start, lo);
end = std::min<unsigned long long>(end, hi);
for (std::uint64_t at = start; at < end && hits.size() < max_hits; at += Piece) {
const std::size_t want =
std::size_t(std::min<std::uint64_t>(end - at, Piece + needle.size() - 1));
iovec local{buffer.data(), want};
iovec remote{reinterpret_cast<void*>(at), want};
const ssize_t got = process_vm_readv(getpid(), &local, 1, &remote, 1, 0);
if (got < ssize_t(needle.size())) {
continue;
}
const std::uint8_t* data = buffer.data();
const std::uint8_t* last = data + got - needle.size();
for (const std::uint8_t* p = data; p <= last && hits.size() < max_hits; ++p) {
p = static_cast<const std::uint8_t*>(
std::memchr(p, needle[0], std::size_t(last - p) + 1));
if (!p) {
break;
}
// A match starting past this piece's own bytes is found by the next piece.
if (std::size_t(p - data) < Piece && std::memcmp(p, needle.data(), needle.size()) == 0) {
hits.push_back(at + std::uint64_t(p - data));
}
}
}
}
std::fclose(maps);
#endif
return hits;
}
} // namespace Core::Bbsc
+21
View File
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
// BBSC: find bytes in the game's memory (the param tables are found by their header text). Kept
// out of bloodborne_re.cpp so <windows.h> stays out of it. Shared by the emulator and the mod DLL.
#pragma once
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace Core::Bbsc {
// Every address in [lo, hi) where `needle` sits in committed, readable memory (at most max_hits).
// Slow (a full walk of the address range): call it once, off the game thread.
std::vector<std::uint64_t> FindInMemory(std::span<const std::uint8_t> needle, std::uint64_t lo,
std::uint64_t hi, std::size_t max_hits);
} // namespace Core::Bbsc
+212
View File
@@ -0,0 +1,212 @@
// Generated by tools/gamedata/gen_messages.py from the game's English text - do not edit.
// {category, message id, BBSC text} // original text
{30, 102001, u"<?joinName?> joined your game."}, // The bell's resonance has beckoned <?joinName?>.
{30, 102003, u"<?joinName?> joined your game."}, // <?joinName?> has arrived by the bell's resonance
{30, 102004, u"<?joinName?> joined your game."}, // <?joinName?> has arrived by the bell's resonance
{30, 102005, u"<?joinName?> joined your game."}, // <?joinName?> has arrived by the bell's resonance
{30, 102006, u"<?joinName?> joined your game."}, // Confederate <?joinName?> has arrived by the bell's resonance.
{30, 102007, u"<?joinName?> joined your game."}, // Confederate <?joinName?> has arrived by the bell's resonance.
{30, 102056, u"<?joinName?> joined your game."}, // Confederate <?joinName?> has arrived by the bell's resonance.
{30, 102057, u"<?joinName?> joined your game."}, // Confederate <?joinName?> has arrived by the bell's resonance.
{30, 102051, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102052, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102053, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102054, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102055, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102100, u"You joined <?hostName?>'s game."}, // Entered the world of <?hostName?>, who beckoned you here.
{30, 102200, u"You joined <?hostName?>'s game."}, // Entered the world of <?hostName?>.
{30, 102600, u"You joined <?hostName?>'s game."}, // You are now in the world of the beckoner, <?hostName?>.
{30, 102700, u"You joined <?hostName?>'s game."}, // You are now in the world of the beckoner, <?hostName?>.
{30, 103100, u"You died."}, // You died. Returning to your world.
{30, 103200, u"You died."}, // You died. Returning to your world.
{30, 103300, u"You died."}, // You died. Returning to your world.
{30, 103400, u"You died."}, // You died. Returning to your world.
{30, 103500, u"You died."}, // You died. Returning to your world.
{30, 103101, u"The host died.\nYou stay in their game."}, // Your beckoner has died. Returning to own world.
{30, 103001, u"<?deadName?> died.\nThey will respawn."}, // Cooperator <?deadName?> has died.
{30, 103006, u"<?deadName?> died.\nThey will respawn."}, // Confederate <?deadName?> has died.
{30, 103007, u"<?deadName?> died.\nThey will respawn."}, // Confederate <?deadName?> has died.
{30, 103056, u"<?deadName?> died.\nThey will respawn."}, // Confederate <?deadName?> has died.
{30, 103057, u"<?deadName?> died.\nThey will respawn."}, // Confederate <?deadName?> has died.
{30, 103051, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 103052, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 103053, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 103054, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 103055, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 106200, u"The host <?hostName?> died.\nYou stay in their game."}, // Beckoner <?hostName?> has fallen. It is time to bid farewell.
{30, 105200, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 105300, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 105400, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 105500, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 141010, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 141020, u"The host started the battle against\n<?npcNameId@106000?>."}, // Battle with <?npcNameId@106000?> has commenced. Returning to your world.
{30, 141030, u"The host started the battle against\n<?npcNameId@127000?>."}, // Battle with <?npcNameId@127000?> has commenced. Returning to your world.
{30, 141040, u"The host started the battle against\n<?npcNameId@209000?>."}, // Battle with <?npcNameId@209000?> has commenced. Returning to your world.
{30, 141050, u"The host started the battle against\n<?npcNameId@210000?>."}, // Battle with <?npcNameId@210000?> has commenced. Returning to your world.
{30, 141060, u"The host started the battle against\n<?npcNameId@212010?>."}, // Battle with <?npcNameId@212010?> has commenced. Returning to your world.
{30, 141070, u"The host started the battle against\n<?npcNameId@212020?>."}, // Battle with <?npcNameId@212020?> has commenced. Returning to your world.
{30, 141080, u"The host started the battle against\n<?npcNameId@212030?>."}, // Battle with <?npcNameId@212030?> has commenced. Returning to your world.
{30, 141090, u"The host started the battle against\n<?npcNameId@216000?>."}, // Battle with <?npcNameId@216000?> has commenced. Returning to your world.
{30, 141100, u"The host started the battle against\n<?npcNameId@232000?>."}, // Battle with <?npcNameId@232000?> has commenced. Returning to your world.
{30, 141110, u"The host started the battle against\n<?npcNameId@251000?>."}, // Battle with <?npcNameId@251000?> has commenced. Returning to your world.
{30, 141120, u"The host started the battle against\n<?npcNameId@257000?>."}, // Battle with <?npcNameId@257000?> has commenced. Returning to your world.
{30, 141130, u"The host started the battle against\n<?npcNameId@271000?>."}, // Battle with <?npcNameId@271000?> has commenced. Returning to your world.
{30, 141140, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 141150, u"The host started the battle against\n<?npcNameId@272000?>."}, // Battle with <?npcNameId@272000?> has commenced. Returning to your world.
{30, 141160, u"The host started the battle against\n<?npcNameId@305000?>."}, // Battle with <?npcNameId@305000?> has commenced. Returning to your world.
{30, 141170, u"The host started the battle against\n<?npcNameId@306000?>."}, // Battle with <?npcNameId@306000?> has commenced. Returning to your world.
{30, 141180, u"The host started the battle against\n<?npcNameId@313000?>."}, // Battle with <?npcNameId@313000?> has commenced. Returning to your world.
{30, 141190, u"The host started the battle against\n<?npcNameId@500000?>."}, // Battle with <?npcNameId@500000?> has commenced. Returning to your world.
{30, 141200, u"The host started the battle against\n<?npcNameId@501000?>."}, // Battle with <?npcNameId@501000?> has commenced. Returning to your world.
{30, 141210, u"The host started the battle against\n<?npcNameId@502000?>."}, // Battle with <?npcNameId@502000?> has commenced. Returning to your world.
{30, 141220, u"The host started the battle against\n<?npcNameId@503000?>."}, // Battle with <?npcNameId@503000?> has commenced. Returning to your world.
{30, 141230, u"The host started the battle against\n<?npcNameId@504000?>."}, // Battle with <?npcNameId@504000?> has commenced. Returning to your world.
{30, 141240, u"The host started the battle against\n<?npcNameId@505000?>."}, // Battle with <?npcNameId@505000?> has commenced. Returning to your world.
{30, 141250, u"The host started the battle against\n<?npcNameId@506000?>."}, // Battle with <?npcNameId@506000?> has commenced. Returning to your world.
{30, 141260, u"The host started the battle against\n<?npcNameId@507000?>."}, // Battle with <?npcNameId@507000?> has commenced. Returning to your world.
{30, 141270, u"The host started the battle against\n<?npcNameId@508000?>."}, // Battle with <?npcNameId@508000?> has commenced. Returning to your world.
{30, 141280, u"The host started the battle against\n<?npcNameId@509000?>."}, // Battle with <?npcNameId@509000?> has commenced. Returning to your world.
{30, 141290, u"The host started the battle against\n<?npcNameId@510000?>."}, // Battle with <?npcNameId@510000?> has commenced. Returning to your world.
{30, 141300, u"The host started the battle against\n<?npcNameId@511000?>."}, // Battle with <?npcNameId@511000?> has commenced. Returning to your world.
{30, 141310, u"The host started the battle against\n<?npcNameId@512000?>."}, // Battle with <?npcNameId@512000?> has commenced. Returning to your world.
{30, 141320, u"The host started the battle against\n<?npcNameId@513000?>."}, // Battle with <?npcNameId@513000?> has commenced. Returning to your world.
{30, 141330, u"The host started the battle against\n<?npcNameId@540000?>."}, // Battle with <?npcNameId@540000?> has commenced. Returning to your world.
{30, 141340, u"The host started the battle against\n<?npcNameId@551000?>."}, // Battle with <?npcNameId@551000?> has commenced. Returning to your world.
{30, 141350, u"The host started the battle against\n<?npcNameId@750000?>."}, // Battle with <?npcNameId@750000?> has commenced. Returning to your world.
{30, 141360, u"The host started the battle against\n<?npcNameId@804000?>."}, // Battle with <?npcNameId@804000?> has commenced. Returning to your world.
{30, 141361, u"The host started the battle against\n<?npcNameId@805000?>."}, // Battle with <?npcNameId@805000?> has commenced. Returning to your world.
{30, 141380, u"The host started the battle against\n<?npcNameId@899000?>."}, // Battle with <?npcNameId@899000?> has commenced. Returning to your world.
{30, 105100, u"The host started the battle against\n<?npcNameId@500000?>."}, // Beckoner has challenged <?npcNameId@500000?>.
{30, 105199, u"The host started a boss battle."}, // The beckoner has begun the final battle.
{30, 105299, u"The host started a boss battle."}, // The beckoner has begun the final battle. Returning to own world.
{30, 105399, u"The host started a boss battle."}, // The Executioner has begun the final battle. Returning to own world.
{30, 105499, u"The host started a boss battle."}, // The Vileblood has begun the final battle. Returning to own world.
{30, 105599, u"The host started a boss battle."}, // The blood-addled hunter has begun the final battle. Returning to own world.
{30, 106100, u"<?npcNameId@1000?> defeated."}, // <?npcNameId@1000?> was defeated, ending the hunt. Now, say farewell.
{30, 142010, u"<?npcNameId@1010?> defeated."}, // <?npcNameId@1010?> was defeated, ending the hunt. Now, say farewell.
{30, 142020, u"<?npcNameId@106000?> defeated."}, // <?npcNameId@106000?> was defeated, ending the hunt. Now, say farewell.
{30, 142030, u"<?npcNameId@127000?> defeated."}, // <?npcNameId@127000?> was defeated, ending the hunt. Now, say farewell.
{30, 142040, u"<?npcNameId@209000?> defeated."}, // <?npcNameId@209000?> was defeated, ending the hunt. Now, say farewell.
{30, 142050, u"<?npcNameId@210000?> defeated."}, // <?npcNameId@210000?> was defeated, ending the hunt. Now, say farewell.
{30, 142060, u"<?npcNameId@212010?> defeated."}, // <?npcNameId@212010?> was defeated, ending the hunt. Now, say farewell.
{30, 142070, u"<?npcNameId@212020?> defeated."}, // <?npcNameId@212020?> was defeated, ending the hunt. Now, say farewell.
{30, 142080, u"<?npcNameId@212030?> defeated."}, // <?npcNameId@212030?> was defeated, ending the hunt. Now, say farewell.
{30, 142090, u"<?npcNameId@216000?> defeated."}, // <?npcNameId@216000?> was defeated, ending the hunt. Now, say farewell.
{30, 142100, u"<?npcNameId@232000?> defeated."}, // <?npcNameId@232000?> was defeated, ending the hunt. Now, say farewell.
{30, 142110, u"<?npcNameId@251000?> defeated."}, // <?npcNameId@251000?> was defeated, ending the hunt. Now, say farewell.
{30, 142120, u"<?npcNameId@257000?> defeated."}, // <?npcNameId@257000?> was defeated, ending the hunt. Now, say farewell.
{30, 142130, u"<?npcNameId@271000?> defeated."}, // <?npcNameId@271000?> was defeated, ending the hunt. Now, say farewell.
{30, 142140, u"<?npcNameId@272000?> defeated."}, // <?npcNameId@272000?> was defeated, ending the hunt. Now, say farewell.
{30, 142150, u"<?npcNameId@304000?> defeated."}, // <?npcNameId@304000?> was defeated, ending the hunt. Now, say farewell.
{30, 142160, u"<?npcNameId@305000?> defeated."}, // <?npcNameId@305000?> was defeated, ending the hunt. Now, say farewell.
{30, 142170, u"<?npcNameId@306000?> defeated."}, // <?npcNameId@306000?> was defeated, ending the hunt. Now, say farewell.
{30, 142180, u"<?npcNameId@313000?> defeated."}, // <?npcNameId@313000?> was defeated, ending the hunt. Now, say farewell.
{30, 142190, u"<?npcNameId@500000?> defeated."}, // <?npcNameId@500000?> was defeated, ending the hunt. Now, say farewell.
{30, 142200, u"<?npcNameId@501000?> defeated."}, // <?npcNameId@501000?> was defeated, ending the hunt. Now, say farewell.
{30, 142210, u"<?npcNameId@502000?> defeated."}, // <?npcNameId@502000?> was defeated, ending the hunt. Now, say farewell.
{30, 142220, u"<?npcNameId@503000?> defeated."}, // <?npcNameId@503000?> was defeated, ending the hunt. Now, say farewell.
{30, 142230, u"<?npcNameId@504000?> defeated."}, // <?npcNameId@504000?> was defeated, ending the hunt. Now, say farewell.
{30, 142240, u"<?npcNameId@505000?> defeated."}, // <?npcNameId@505000?> was defeated, ending the hunt. Now, say farewell.
{30, 142250, u"<?npcNameId@506000?> defeated."}, // <?npcNameId@506000?> was defeated, ending the hunt. Now, say farewell.
{30, 142260, u"<?npcNameId@507000?> defeated."}, // <?npcNameId@507000?> was defeated, ending the hunt. Now, say farewell.
{30, 142270, u"<?npcNameId@508000?> defeated."}, // <?npcNameId@508000?> was defeated, ending the hunt. Now, say farewell.
{30, 142280, u"<?npcNameId@509000?> defeated."}, // <?npcNameId@509000?> was defeated, ending the hunt. Now, say farewell.
{30, 142290, u"<?npcNameId@510000?> defeated."}, // <?npcNameId@510000?> was defeated, ending the hunt. Now, say farewell.
{30, 142300, u"<?npcNameId@511000?> defeated."}, // <?npcNameId@511000?> was defeated, ending the hunt. Now, say farewell.
{30, 142310, u"<?npcNameId@512000?> defeated."}, // <?npcNameId@512000?> was defeated, ending the hunt. Now, say farewell.
{30, 142320, u"<?npcNameId@513000?> defeated."}, // <?npcNameId@513000?> was defeated, ending the hunt. Now, say farewell.
{30, 142330, u"<?npcNameId@540000?> defeated."}, // <?npcNameId@540000?> was defeated, ending the hunt. Now, say farewell.
{30, 142340, u"<?npcNameId@551000?> defeated."}, // <?npcNameId@551000?> was defeated, ending the hunt. Now, say farewell.
{30, 142350, u"<?npcNameId@750000?> defeated."}, // <?npcNameId@750000?> was defeated, ending the hunt. Now, say farewell.
{30, 142360, u"<?npcNameId@804000?> defeated."}, // <?npcNameId@804000?> was defeated, ending the hunt. Now, say farewell.
{30, 142370, u"<?npcNameId@805000?> defeated."}, // <?npcNameId@805000?> was defeated, ending the hunt. Now, say farewell.
{30, 142380, u"<?npcNameId@899000?> defeated."}, // <?npcNameId@899000?> was defeated, ending the hunt. Now, say farewell.
{30, 106199, u"Boss defeated."}, // The final battle has ended. It is time to bid farewell.
{30, 102002, u"Invader <?joinName?> entered your world!"}, // Adversary <?joinName?> has arrived.
{30, 109000, u"A bell-ringing woman appeared.\nAn invader may come."}, // Drawn by the bell's timbre, a bell-ringing woman has appeared somewhere.
{30, 103002, u"Invader <?deadName?> was killed."}, // Adversary <?deadName?> has died.
{30, 103003, u"Invader <?deadName?> was killed."}, // Vileblood <?deadName?> has died.
{30, 103004, u"Invader <?deadName?> was killed."}, // Executioner <?deadName?> has died.
{30, 103005, u"Invader <?deadName?> was killed."}, // Hunter of Hunters, <?deadName?>, has died.
{30, 106300, u"The host <?hostName?> died.\nYou stay in their game."}, // Executioner <?hostName?> has fallen. It is time to bid farewell.
{30, 106400, u"The host <?hostName?> died.\nYou stay in their game."}, // Vileblood <?hostName?> has fallen. It is time to bid farewell.
{30, 106500, u"The host <?hostName?> died.\nYou stay in their game."}, // Blood-addled hunter <?hostName?> has fallen. It is time to bid farewell.
{30, 20000407, u"Invader <?leaveName?> left\nand went back to their world."}, // Adversary <?leaveName?> returned to their own world.
{30, 20000408, u"Invader <?leaveName?> left\nand went back to their world."}, // Adversary <?leaveName?> returned to their own world.
{30, 20000409, u"Invader <?leaveName?> left\nand went back to their world."}, // Vileblood <?leaveName?> returned to their own world.
{30, 20000410, u"Invader <?leaveName?> left\nand went back to their world."}, // Vileblood <?leaveName?> returned to their own world.
{30, 20000411, u"Invader <?leaveName?> left\nand went back to their world."}, // Executioner <?leaveName?> returned to their own world.
{30, 20000412, u"Invader <?leaveName?> left\nand went back to their world."}, // Executioner <?leaveName?> returned to their own world.
{30, 20000413, u"Invader <?leaveName?> left\nand went back to their world."}, // Hunter of Hunters <?leaveName?> returned to their own world.
{30, 20000414, u"Invader <?leaveName?> left\nand went back to their world."}, // Hunter of Hunters <?leaveName?> returned to their own world.
{30, 102300, u"You invaded <?hostName?>'s world."}, // You are now in the world of the beckoner, Executioner <?hostName?>
{30, 102400, u"You invaded <?hostName?>'s world."}, // You are now in the world of the beckoner, Vileblood <?hostName?>
{30, 102500, u"You invaded <?hostName?>'s world."}, // You are now in the world of the beckoner, the blood-addled hunter <?hostName?>.
{30, 101001, u"A player answered your bell.\nJoining..."}, // Discovered the chime of a Small Resonant Bell.
{30, 101003, u"A player answered your bell.\nJoining..."}, // Discovered the chime of a Small Resonant Bell.
{30, 101004, u"A player answered your bell.\nJoining..."}, // Discovered the chime of a Small Resonant Bell.
{30, 101005, u"A player answered your bell.\nJoining..."}, // Discovered the chime of a Small Resonant Bell.
{30, 101100, u"Found a host.\nJoining their game..."}, // Discovered the chime of a Beckoning Bell.
{30, 14000, u"Ring the Hunt Bell\nto call other players?"}, // Expend Insight and seek the assistance of a hunter from another world?
{30, 14003, u"Stop the Hunt Bell"}, // Silence the Beckoning Bell
{30, 14004, u"Stop the Kin Bell"}, // Silence the Small Resonant Bell
{30, 14005, u"Stop the Bloodlust Bell"}, // Silence the Sinister Resonant Bell
{30, 100001, u"Hunt Bell rung.\nWaiting for players to join..."}, // Searching for the chime of a Small Resonant Bell...
{30, 100100, u"Kin Bell rung.\nLooking for a host to join..."}, // Searching for the chime of a Beckoning Bell...
{30, 100200, u"Bloodlust Bell rung.\nLooking for a world to invade..."}, // Searching for the chime of a Sinister Bell...
{30, 100300, u"Nobody found.\nRing the bell again to keep looking."}, // Could not discover the chime of a Small Resonant Bell. No insight was consumed.
{30, 101002, u"Someone is about to invade your world."}, // Sinister Bell resonates with another...
{30, 101200, u"Found a world to invade.\nEntering..."}, // Discovered the chime of a Sinister Bell.
{30, 100011, u"Hunt Bell rung (password).\nWaiting for players to join..."}, // Searching for the chime of a Small Resonant Bell with matching password.
{30, 100110, u"Kin Bell rung (password).\nLooking for a host to join..."}, // Searching for the chime of a Beckoning Bell with matching password.
{30, 101300, u"Found a host,\nbut the beckoner is an Executioner, and thus an adversary."}, // Discovered the chime of a Beckoning Bell, but the beckoner is an Executioner, and thus an adversary.
{30, 101400, u"Found a host,\nbut the beckoner is a Vileblood, and thus an adversary."}, // Discovered the chime of a Beckoning Bell, but the beckoner is a Vileblood, and thus an adversary.
{30, 101500, u"Found a host,\nbut the beckoner is a blood-addled hunter, and thus an adversary."}, // Discovered the chime of a Beckoning Bell, but the beckoner is a blood-addled hunter, and thus an adversary.
{30, 10011270, u"Ring the Hunt Bell to call\nother players into your game."}, // Consume Insight and ring the Beckoning Bell to enlist the cooperation of hunters from other worlds.
{30, 10011280, u"Use the Parting Blank to end co-op."}, // Silencing Blank ends such forms of cooperation.
{200, 266302, u"Bloodlust Bell"}, // Sinister Bell
{200, 280140, u"Bloodlust Bell"}, // Sinister Bell
{200, 280240, u"Bloodlust Bell"}, // Sinister Bell
{200, 302000, u"Parting Blank"}, // Silencing Blank
{200, 302100, u"Parting Blank"}, // Silencing Blank
{200, 302110, u"Stop the Hunt Bell"}, // Silence Beckoning Bell
{200, 302111, u"Stop the Kin Bell"}, // Silence Small Resonant Bell
{200, 302112, u"Stop the Bloodlust Bell"}, // Silence Sinister Resonant Bell
{200, 302120, u"End co-op"}, // Disrupt Resonance
{200, 302123, u"End co-op"}, // Disrupt Resonance
{200, 302124, u"End co-op and go home"}, // Disrupt Resonance and Return Home
{201, 269002, u"Quick search: find a host and join their game."}, // Quick-search a Beckoning Bell chime to begin co-op.
{201, 269003, u"Quick search: find a world to invade."}, // Quick-search a Sinister Bell chime to become an adversary.
{201, 302110, u"Stop calling other players."}, // Silence Bell and cease requesting co-op.
{201, 302111, u"Stop looking for a host."}, // Silence Bell and stop helping other Hunters.
{201, 302112, u"Stop looking for a world to invade."}, // Silence Bell and cease threat.
{204, 269001, u"Find a host and join their game?"}, // Quick-search a Beckoning Bell chime and begin co-op?
{204, 269002, u"Find a world to invade?"}, // Quick-search a Sinister Bell chime and become an adversary?
{204, 302200, u"Kin Bell rung.\nLooking for a host to join..."}, // Searching for the chime of a Beckoning Bell...
{204, 302201, u"Bloodlust Bell rung.\nLooking for a world to invade..."}, // Searching for the chime of a Sinister Bell...
{204, 302210, u"Kin Bell rung (password).\nLooking for a host to join..."}, // Searching for the chime of a Beckoning Bell with matching password.
{2, 2000, u"Need help? Ring the Hunt Bell."}, // Ring the Beckoning Bell if you require assistance.
{2, 1121, u"Call other players into your game\nwith the Hunt Bell."}, // Receive assistance from the hunters of other worlds by using the Beckoning Bell.
{2, 1122, u"Join another player's game\nwith the Kin Bell."}, // Assist the hunters of other worlds by using the Small Resonant Bell.
{30, 15000011, u"You caught up with <?hostName?>."}, // *
{30, 15000012, u"<?joinName?> caught up with you."}, // *
{30, 15000035, u"You are in the host's Hunter's Dream."}, // *
{10, 4016, u"Hunt Bell"}, // (empty)
{20, 4016, u"Ring to call other players into your game.\nCosts no Insight."}, // (empty)
{24, 4016, u"An old bell carried by the hunters of the\nworkshop, its toll heard across every world.\n\nRing it, and hunters who answer with a\nKin Bell may join your hunt. Together you\nwalk the night, through every lamp,\ndeath and dream, until the hunt is done."}, // (empty)
{10, 4121, u"Kin Bell"}, // (empty)
{20, 4121, u"Ring to join another players game.\nCosts no Insight."}, // (empty)
{24, 4121, u"A small bell of the choir, its chime\nfaint, but true.\n\nHunters who hear a Hunt Bell may answer\nwith it, crossing into another world to\nstand at the side of kin. What is slain\nthere is slain for both."}, // (empty)
{10, 11201, u"Bloodlust Bell"}, // (empty)
{20, 11201, u"Ring to invade another players world.\nCosts no Insight."}, // (empty)
{24, 11201, u"A bell that sounds only for the blood-drunk.\n\nNo maiden need ring for its toll to be\nheard: every hunter of the night is prey,\nalone or in company, wherever they walk.\n\nThe hunt for hunters never ends."}, // (empty)
{10, 11251, u"Parting Blank"}, // (empty)
{20, 11251, u"Use to end co-op.\nGuests go home; the host sends everyone home."}, // (empty)
{24, 11251, u"A blank of silent powder.\n\nIts inaudible burst breaks the resonance\nbinding hunters of different worlds, and\neach returns to their own night."}, // (empty)
{20, 200, u"No longer used. Use the Hunt Bell."}, // Use Insight to ring and call to other worlds for help.
{20, 205, u"No longer used. Use the Kin Bell."}, // Ring this bell to help hunters in other worlds.
{20, 225, u"No longer used. Use the Bloodlust Bell."}, // Ring to become the enemy of a hunter in another world.
{20, 111, u"No longer used. Use the Parting Blank."}, // Fire to end ongoing, and prevent further, cooperation.
{10, 4313, u"Duel Bell"}, // (empty)
{20, 4313, u"Ring in co-op to turn on PvP between players.\nRing again to turn it off."}, // (empty)
{24, 4313, u"A cracked bell with a sour, discordant chime.\n\nRing it while hunting together, and the\nresonance binding the hunters turns to\nrivalry: they may strike one another as foes.\n\nRing it again to restore the old bond.\nOnly the beckoner decides; a cooperator\nwho rings it merely asks."}, // (empty)
+493
View File
@@ -0,0 +1,493 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
// BBSC byte patches for Bloodborne CUSA03173 01.09: the sites, their expected bytes and what they
// become. Shared by the emulator build (bbsc_persistence.cpp) and the injected mod DLL
// (mod/bbsc_mod.cpp), so both apply exactly the same list. Standard library only.
#pragma once
#include <array>
#include <cstdint>
#include <cstring>
#include "core/bbsc_persistence.h"
namespace Core::Bbsc::PatchTables {
using u8 = std::uint8_t;
using u64 = std::uint64_t;
// CUSA03173 01.09 addresses (eboot sha256 d65f0b4f...). See docs/SESSION-LIFECYCLE.md.
constexpr u64 MultiPlayStopOffset = 0x01ED07A0; // CSMultiPlayMan::Stop
constexpr u64 MatchingExistsOffset = 0x01ED00A0; // CSMultiPlayMan "matching controller exists"
constexpr u64 RemoteCleanupOffset = 0x015BED10; // WorldChrMan remote-player cleanup
constexpr u64 ClearInsertTasksOffset = 0x01E54830; // CSMultiPlayMan: destroy every member task
constexpr u64 RemoveMemberOffset = 0x01E54DB0; // CSMultiPlayMan::RemoveMember(id)
constexpr u64 RemoveMemberTwinOffset = 0x01E55050; // second member list removal
constexpr u64 MarkSlotsLeavingOffset = 0x014EB860; // marks every remote slot leaving
constexpr u64 DeathReloadRequestOffset = 0x013CDE30; // StageTransition request at the end of death
enum class Group : u8 {
Stage, // map loads: StageTransition, stage helper, stage-UID policy, stage flow, destructor
Revive, // OnReviveMagic_1 reload
Boss, // BlockClearAnim / boss-area leave
MatchCheck, // OnMatchingCheck (missing remote character) — weakens disconnect detection
Lamp, // lamp prompt 4th availability byte (+0x4B) from 0x0191B6E0
Tasks, // keep the per-member CSMultiPlayerIns tasks across a kept load (host death)
HostDead, // guest ignores the host's death instead of being sent home through a load
NoFogWalls, // co-op area fog walls never appear
LeaveLimit, // leaving the co-op area does not send a guest home
Spectate, // the death steps run in full except for the reload they request at the end
Echoes, // a summoned guest keeps the whole blood echo reward, not half of it
GuestItems, // a summoned guest sees, picks up and keeps the items of the host's world
SolidNpcs, // a guest sees the host world's NPCs as they are, not as grey wandering ghosts
Scaling, // vanilla's boss co-op buff (Lua_MultiDoping) off - BBSC enemy scaling instead
KitBells, // the BBSC kit's Hunt Bell gets the Beckoning Bell's special checks (Dream use)
SendBacklog, // one member that never answers does not end the session for everyone
Hunters, // the host keeps looking for co-op signs past vanilla's two guests
};
// Lamp action update 0x012F5C40 hides the lamp prompt unless bytes +0x48..+0x4B are all zero.
// +0x49 (multiplayer) is cleared for the host by the shadp2p seamless hook; +0x4B is written
// later from `call 0x0191B6E0` (WorldChrMan scan of characters bound to the lamp entity), which
// the shadp2p hook runs too early to affect. Retargeting that single call to a native
// `xor eax,eax; ret` clears the fourth gate at this call site only.
constexpr u64 LampCharacterGateOffset = 0x0191B6E0;
constexpr u64 ReturnZeroOffset = 0x01300AA0;
struct CallRetarget {
const char* name;
u64 site; // offset of the 5-byte call/jmp rel32
u8 opcode; // 0xE8 call or 0xE9 jmp
u64 original; // expected current target
u64 target; // replacement target
Group group;
};
// Every Stop site below is followed by `test al,al; je` and stores a session field only on
// success; MatchingExists returns true exactly when a room is active, so callers keep their
// native success branch without the session being torn down. The paired cleanup call is
// redirected to a return-only function.
constexpr std::array<CallRetarget, 27> Retargets{{
// SoloPlayDeath_2 (0x01381DE0) and PartyGhostDeath_2 (0x013824C0) are the last step of the
// death script. Both end with `0x01332BC0()` (pick the reload destination) and
// `0x013CDE30()` (request the StageTransition that rebuilds the world). BBSC used to skip the
// whole step, which left the step's own bookkeeping undone: the death UI objects at
// global+0x1690/+0x1698/+0x16B0/+0x16B8 were never released, so a SECOND death never reached
// the step again and the player stayed dead with only camera control (run 2026-09-19, host
// log: death armed, no "death held", room timed out 60 s later). Dropping only the reload
// call lets the step finish - including its own blood echo drop - while the world stays up.
{"SoloPlayDeath_2.Reload", 0x01381EDE, 0xE8, DeathReloadRequestOffset, PersistNoOpOffset,
Group::Spectate},
{"PartyGhostDeath_2.Reload", 0x01382663, 0xE8, DeathReloadRequestOffset, PersistNoOpOffset,
Group::Spectate},
{"Lamp.CharacterGate", 0x012F83BC, 0xE8, LampCharacterGateOffset, ReturnZeroOffset, Group::Lamp},
{"StageTransition.Stop", 0x013CDEF6, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
{"StageTransition.Cleanup", 0x013CDF17, 0xE8, RemoteCleanupOffset, PersistNoOpOffset, Group::Stage},
{"StageHelper.Stop", 0x013CDDF5, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
{"StageHelper.Cleanup", 0x013CDE26, 0xE9, RemoteCleanupOffset, PersistNoOpOffset, Group::Stage},
{"StageUidPolicy.Stop", 0x0193C657, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
{"StageUidPolicy.Cleanup", 0x0193C689, 0xE8, RemoteCleanupOffset, PersistNoOpOffset, Group::Stage},
{"StageFlow.Stop", 0x01939876, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
// Also retargeted by the shadp2p seamless patches; accepted in either state.
{"StageTaskDestructor.Stop", 0x019471B1, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
{"Revive.Stop", 0x01389DDD, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Revive},
{"Revive.Cleanup", 0x01389DFE, 0xE8, RemoteCleanupOffset, PersistNoOpOffset, Group::Revive},
{"BossArea.Stop", 0x0138B410, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Boss},
{"BossArea.Cleanup", 0x0138B437, 0xE8, RemoteCleanupOffset, PersistNoOpOffset, Group::Boss},
{"MatchingCheck.Stop", 0x013809B7, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::MatchCheck},
{"MatchingCheck.Cleanup", 0x013809E8, 0xE9, RemoteCleanupOffset, PersistNoOpOffset, Group::MatchCheck},
// Stage task destructor (0x019470F0) destroys every member task when the summon reload phase
// is not 3. A host death is exactly that case, so the host loses the task that recreates the
// guest's character and can no longer see the guest (observed 2026-09-16: host shape ins=[]).
{"StageTaskDestructor.ClearIns", 0x01947207, 0xE8, ClearInsertTasksOffset, PersistNoOpOffset,
Group::Tasks},
// PlayerIns death handler 0x018C49F0: when the LOCAL player dies and the session state is 3
// (host), it calls RemoveMember for every member task, the twin removal for the second list,
// and marks every remote slot as leaving. That is the host-death kick (trace 2026-09-16:
// RemoveMember x2 from 0x018C4D82 right at the host's death). Only the host branch is patched.
{"HostOnDeath.RemoveMember", 0x018C4D7D, 0xE8, RemoveMemberOffset, PersistNoOpOffset,
Group::HostDead},
{"HostOnDeath.RemoveMemberTwin", 0x018C4DAC, 0xE8, RemoveMemberTwinOffset, PersistNoOpOffset,
Group::HostDead},
{"HostOnDeath.MarkSlotsLeaving", 0x018C4DBA, 0xE8, MarkSlotsLeavingOffset, PersistNoOpOffset,
Group::HostDead},
// Guest item pickup, part 6 (2026-10-04, "items cannot be picked up"). Guest log 2026-10-02:
// the map's items were there right after every load (list +0x28: 132 items, 119 pickable) and
// the whole list was gone 2-3 s after each "reinsert v8: restarted ... FirstSyncWait". The item
// manager's network update 0x017D2F30 reads packet 0x2C; one message kind is "drop your items"
// with a list selector ([rbp-0x19C] + 1, jump table 0x017D4C2C): -1 = all three lists
// (0x017D3498/34A0/34A8), or one list (0x017D4089/409A/40AB). 0x017D59F0 / 5B40 / 5C40 free the
// list's items with +0x48 bit 0x80 and then zero the list head, so every map item vanished
// while its glow could stay. In vanilla the guest never owns map items, so the host's join sync
// clears them; with BBSC the guest makes its own (GuestItems.SpawnOwn), so the clear is dropped.
// Nothing else calls these three.
{"GuestItems.KeepOnSyncAll1", 0x017D3498, 0xE8, 0x017D59F0, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSyncAll2", 0x017D34A0, 0xE8, 0x017D5B40, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSyncAll3", 0x017D34A8, 0xE8, 0x017D5C40, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSync1", 0x017D4089, 0xE8, 0x017D59F0, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSync3", 0x017D409A, 0xE8, 0x017D5C40, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSync2", 0x017D40AB, 0xE8, 0x017D5B40, ReturnZeroOffset, Group::GuestItems},
}};
// Functions made to return immediately. Only for Lua-registered C functions whose whole job is a
// session-ending action, so skipping all of it leaves nothing half-applied.
struct EntryReturn {
const char* name;
u64 site;
std::array<u8, 6> prologue; // expected first bytes
Group group;
};
constexpr std::array<EntryReturn, 3> EntryReturns{{
// OnLeave_Limit (Lua, 0x0138A4A0): a living guest who leaves the co-op area is sent home
// (event state 2, forced warp global+0x1520, home respawn point 0x01332BC0(-1), StageTransition).
// With the fog walls gone the guest may walk past their old spot, so the step is skipped.
// Skipping it also removes the only way a guest gets sent home, so it is off by default:
// with it on the players reported not being able to leave the world (run 2026-09-19).
{"OnLeave_Limit", 0x0138A4A0, {0x55, 0x48, 0x89, 0xE5, 0x53, 0x50}, Group::LeaveLimit},
// HostDead (Lua, 0x01381870): the first step on the guest when the host dies (returns at once
// on the host itself, state 3). It sets event flag 4047 (0x0132E5A0), shows the host-dead
// message and sets death-pending [0x0553B130]+0x70 (0x01386960), and schedules HostDead_1 and
// EventMenuBrake (menu brake MenuMan+0x2DC). With the guest staying in the world none of that
// is wanted; after it ran the guest could not use ladders (run 2026-09-16).
{"HostDead", 0x01381870, {0x55, 0x48, 0x89, 0xE5, 0x41, 0x57}, Group::HostDead},
// HostDead_1 (Lua, registered at 0x0138DADB): runs on the guest when the host dies. It sets the
// forced-warp byte (global+0x1520), selects the home respawn point (0x01332BC0(-1)), starts a
// StageTransition (0x013CDE30) and queues the original player type (+0x8C). Skipping it keeps
// the guest standing where he is, with no loading screen, while the host respawns.
{"HostDead_1", 0x01381A60, {0x55, 0x48, 0x89, 0xE5, 0x53, 0x50}, Group::HostDead},
}};
// Two-byte instruction rewrites, verified against the bytes that must be there.
struct ByteEdit {
const char* name;
u64 site;
std::array<u8, 2> original;
std::array<u8, 2> replacement;
Group group;
};
constexpr std::array<ByteEdit, 17> ByteEdits{{
// The lamp prompt in 0x012F5C40 needs all four of +0x48, +0x49, +0x4A and +0x4B to be zero.
// +0x49 is the "I am in a multiplayer session" flag, built right here:
// CMP dword ptr [RBX + 0x14], 0x1
// SETG AL ; > 1 members
// TEST ECX, ECX
// SETG CL ; SprjSessionManager member vector is non-empty
// OR CL, AL <- 0x012F8321, the two bytes we replace
// MOV byte ptr [R13 + 0x49], CL
// "OR CL,AL" (08 C1) becomes "XOR CL,CL" (30 C9), so the flag stores 0 and the prompt appears
// in co-op. Same length, same register, nothing after it moves.
//
// This replaces what Lamp.CharacterGate was meant to do. That entry neutralises 0x0191B6E0,
// but the decompile shows that call only runs when +0x49 is already zero - so in a session it
// never executed and the patch did nothing. Reported 2026-09-22: no lamp prompt at all, with
// lamp=true in the log.
{"Lamp.MultiplayerGate", 0x012F8321, {0x08, 0xC1}, {0x30, 0xC9}, Group::Lamp},
// The "Travel" prompt itself comes from the lamp's talk script, not from 0x012F5C40. The talk
// condition dispatcher 0x0203AD60 answers the script function "is multiplayer in progress"
// (case 0x34) with the same expression as +0x49 above:
// CMP dword ptr [RBX + 0x14], 0x1 / SETG AL / TEST ECX, ECX / SETG CL
// OR CL, AL <- 0x0203C564
// MOVZX EAX, CL ; the script's answer
// Clearing it makes every talk script on the HOST see a solo game (lamp prompt, and anything
// else the scripts gate on co-op). The guest keeps the vanilla answer: Group::Lamp is a role
// group that is only switched on while hosting.
{"Lamp.TalkMultiplayerCheck", 0x0203C564, {0x08, 0xC1}, {0x30, 0xC9}, Group::Lamp},
// Guest item pickup, part 2 (part 1 is below in WideEdits). Picking up a map treasure is
// 0x017D8570(item manager [0x0553D6E0], &item id). For a treasure (item +0x48 bit 0x80) it asks
// the session state first:
// CMP dword ptr [RAX + 0x124], 0x6 ; SprjSessionManager: summoned guest
// JNZ 0x017D8768 <- 0x017D85FB: not a guest -> take it locally
// ... ; a guest only sends packet 0x2C to the host
// and later, on that local path:
// CMP dword ptr [RAX + 0x124], 0x6
// JZ 0x017D88D3 <- 0x017D88C2: a guest skips 0x017D89F0 (give items)
// So a guest takes the host's path: the item is marked taken in the guest's game and its lot is
// given to the guest. The inventory is saved live (the guest's save writer 0x0156AA50 only swaps
// world snapshots), so the guest keeps it; the host's world is not touched and the host can pick
// the same item up too. 0F 85 -> 90 E9 keeps the displacement and the target (0x017D8768).
{"GuestItems.PickupAsHost", 0x017D85FB, {0x0F, 0x85}, {0x90, 0xE9}, Group::GuestItems},
{"GuestItems.GiveToGuest", 0x017D88C2, {0x74, 0x0F}, {0x90, 0x90}, Group::GuestItems},
// Guest item pickup, part 3 (2026-09-26, "guest can't see items", NG+ guest that took nothing):
// the guest's map items were never created. Spawning an item (0x017D8370, called for the map's
// treasures and for drops) does, for a summoned guest,
// CMP dword ptr [RAX + 0x124], 0x6
// JNZ 0x017D8511 <- 0x017D83D2: host/solo -> create it here (0x017DE870)
// and otherwise only sends packet 0x2C type 3 to the host - nothing ever comes back as an item
// the guest can see. 0F 85 -> 90 E9: the guest creates its own, from its own map load and its
// own pickup flags, so each player sees the items they have not taken (personal loot).
{"GuestItems.SpawnOwn", 0x017D83D2, {0x0F, 0x85}, {0x90, 0xE9}, Group::GuestItems},
// Part 4: marking a picked-up item taken (its pickup flag +0x28 and count flag +0xB8, in
// 0x017D6780) is skipped for a guest outside its own world:
// CMP dword ptr [RAX + 0x124], 0x6 / MOV R14, RBX
// JZ 0x017D6AE8 <- 0x017D6ADC: guest -> "own world?" -> skip
// CMP dword ptr [R15 + 0x98], -1 / JZ 0x017D6AFC (set the flags)
// Without the flag a guest's item came back after every load. 74 0A -> EB 1E jumps straight
// to 0x017D6AFC. The flag lives in the live table of the host's world only; the guest's save
// keeps its own world (see bloodborne_re.cpp, world state).
{"GuestItems.MarkTaken", 0x017D6ADC, {0x74, 0x0A}, {0xEB, 0x1E}, Group::GuestItems},
// Part 5 (2026-09-29, "items cannot be seen by the guest", still on 5B726D9D): a guest's log
// showed 0x017D8370 and 0x017DE870 never run at all, host or guest, so parts 3 and 3b patched
// functions map treasures do not go through. Map treasures are placed by 0x017DC300(item man,
// entity, MSB event, ...), called per MSB event of type 4 (treasure) at map load, which for a
// summoned guest or anyone in another world skips the whole thing (see WideEdits, part 5).
// It then hands each treasure to 0x017D4C40 / 0x017D4FB0 / 0x017D5320 by lot kind, and each
// of those numbers the new item only when hosting or solo:
// CMP dword ptr [RAX + 0x124], 0x6 ; JZ skip <- guest
// CMP dword ptr [R15 + 0x98], -1 ; JNZ skip <- in another world
// MOV EAX, [RBX + 8] / INC / store: item number = counter << 4
// An item left unnumbered cannot be looked up by its pickup, so both jumps become NOPs: the
// guest numbers its own items from its own counter, as its game does in its own world.
{"GuestItems.NumberKind1", 0x017D4F62, {0x74, 0x26}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind1Away", 0x017D4F6F, {0x75, 0x1C}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind3", 0x017D52D9, {0x74, 0x26}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind3Away", 0x017D52E6, {0x75, 0x1C}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind2", 0x017D5649, {0x74, 0x26}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind2Away", 0x017D5656, {0x75, 0x1C}, {0x90, 0x90}, Group::GuestItems},
// Grey NPCs (2026-09-29, guest, outside the Dream): the NPC constructor 0x018E4870 gives an
// NpcParam row with isChangeWanderGhost (+0x145 bit 7) its ghost model (+0x10C) and type 4
// whenever the local player is in someone else's world:
// CMP dword ptr [RAX + 0x124], 0x6 ; JZ ... / CMP [RBX + 0x98], -1 ; JZ 0x018E4FDC
// MOV ECX, [RAX + 0x145] / TEST CL, 0x80
// JZ 0x018E4FDC <- 0x018E4FBF: not a ghost NPC -> keep its own look
// ... ghost model, R12D = 4
// 74 1B -> EB 1B: every NPC keeps its own model and type for a guest, as the host sees it.
// Host and solo never reach this test. The Dream-only row edit (Doll, Gehrman) stays as well.
{"SolidNpcs.NoWanderGhost", 0x018E4FBF, {0x74, 0x1B}, {0xEB, 0x1B}, Group::SolidNpcs},
// Lua_MultiDoping (0x0138BC70): CMP EAX,2 / JNZ (2 guests -> SpEffect 7501) and CMP EAX,1 / JNZ
// (1 guest -> 7500). Both JNZ -> JMP: the boss never gets the vanilla buff, the game still
// sets its event flag 4070 after it. BBSC's own scaling (bloodborne_re.cpp) replaces it.
{"Scaling.NoVanillaTwoGuests", 0x0138BCC2, {0x75, 0x32}, {0xEB, 0x32}, Group::Scaling},
{"Scaling.NoVanillaOneGuest", 0x0138BCF9, {0x75, 0x54}, {0xEB, 0x54}, Group::Scaling},
// Boss insight for the guest (2026-10-08, testers: "guest only gets insight for discovering the
// boss, not for killing it"). The kill reward 0x018C5070, run in every player's game for every
// death, gives a boss's insight ("HumanityDrop", boss row via 0x01F1B4C0, then 0x01F1B760 /
// 0x01F1B830 / 0x028986B0) only behind
// CMP byte ptr [global + 0x1592], 0 ; in our own world?
// JZ 0x018C59A6 <- 0x018C52F9 (0F 84 A7 06 00 00): no -> nothing
// A guest is never in its own world. EB 04 jumps over the rest of the JZ to the next
// instruction (0x018C52FF), so the guest's copy gives the guest's insight too; host and solo
// never took the jump anyway.
{"Echoes.GuestBossInsight", 0x018C52F9, {0x0F, 0x84}, {0xEB, 0x04}, Group::Echoes},
// Five hunters (test 2026-10-08, host with 2 guests: a third player's co-op sign got
// 18 empty sign lists in a row, the same player's invader sign was found at once). The host's
// sign search 0x01872360 asks the server for co-op signs (type 7) only while one of vanilla's
// two co-op slots is free - the slot table 0x0186FE40 fills [rbp-0x9C] / [rbp-0x90] with the
// first and second guest:
// CMP [rbp-0x9C], 0 ; JZ 0x018723B1 ; first slot free -> search
// MOV EAX, [rbp-0x90] ; TEST EAX, EAX
// JNZ 0x018723FF <- 0x018723AF (75 4E): both taken -> no co-op search
// 75 4E -> 90 90: the co-op search goes on; the summon builder's own cap (4 cooperators,
// ApplyBbscHunterCap) still decides who is let in.
{"Hunters.KeepSearchingCoop", 0x018723AF, {0x75, 0x4E}, {0x90, 0x90}, Group::Hunters},
}};
// Four-byte rewrites (a RIP-relative displacement), verified against the bytes that must be there.
struct WideEdit {
const char* name;
u64 site;
std::array<u8, 4> original;
std::array<u8, 4> replacement;
Group group;
};
constexpr std::array<WideEdit, 11> WideEdits{{
// Every blood echo reward the local player receives is computed by 0x01335400 and left in a
// float at [[0x0553B130]+8]+0xF8. Halfway through:
// MOV EAX, dword ptr [R15 + 0x78] ; the local player's character type
// OR EAX, 0x8 / CMP EAX, 0x8
// JZ 0x01335596 ; host (type 0) and type 8 skip the next three
// VMOVSS XMM0, [RBP + -0x44]
// VMULSS XMM0, XMM0, [RIP + 0x035F058B] <- 0x01335589, disp32 at 0x0133558D
// VMOVSS [RBP + -0x44], XMM0 ; that address is 0x04925B1C = 0.5
// Skipping the multiply (JZ -> JMP, builds up to 39A08659) did not help: a guest still got
// exactly half the host's echoes for the same kill (+671 against +1341, both players NG+,
// 2026-09-24). That is because 0x01335400 is only the EVENT reward (registered as a Lua/event
// function at 0x013421AC); ordinary kills go through 0x018FC860, below. Doubling here
// (build 43B41805) changed nothing for kills either, and would have paid a guest 2x for event
// rewards, so the multiply now reads 1.0 - the float at 0x04925B20, right after the 0.5.
// New displacement: 0x04925B20 - 0x01335591 = 0x035F058F.
{"Echoes.GuestEventFull", 0x0133558D, {0x8B, 0x05, 0x5F, 0x03}, {0x8F, 0x05, 0x5F, 0x03},
Group::Echoes},
// The kill reward: 0x018FC860(rate, chr, enemy, bonus, guest_share), called by 0x018C5070
// with guest_share = [[0x05556678]+0x16F8]+0x98 != -1 || SprjSessionManager+0x124 == 6 (a
// summoned guest), multiplies by 0.5 when guest_share is set:
// 0x018FC947 VMOVSS XMM3, [RIP + 0x0302B571] ; 0x04927EC0 = 0.5
// and pays it through chr->vtable[0x3D0]. Pointing the load at 0x04927EBC (1.0, the float
// just before) pays a guest what the host gets: +671 against +1341 in every 2026-09-24 test.
// New displacement: 0x04927EBC - 0x018FC94F = 0x0302B56D.
{"Echoes.GuestKillFull", 0x018FC94B, {0x71, 0xB5, 0x02, 0x03}, {0x6D, 0xB5, 0x02, 0x03},
Group::Echoes},
// Guest item pickup, part 1. 0x017D9E70(item manager, &item id, on) makes an item pickable
// (+0x48 bit 0x40, which the pickup prompt search 0x017D9FB0 requires). For map treasures
// (bit 0x80) it refuses a summoned guest with the same two tests as the echo halving:
// CMP dword ptr [RCX + 0x124], 0x6 ; JZ return 0 (0F 84 B6000000 at 0x017D9EE7)
// CMP dword ptr [R15 + 0x98], -1 ; JNZ return 0 (0F 85 A8000000 at 0x017D9EF5)
// Zeroing both displacements makes each jump land on the next instruction, so the treasure is
// enabled for the guest exactly as for the host.
{"GuestItems.ShowTreasure", 0x017D9EE9, {0xB6, 0x00, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00},
Group::GuestItems},
{"GuestItems.ShowTreasureShare", 0x017D9EF7, {0xA8, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00}, Group::GuestItems},
// Part 3, second half: the create function 0x017DE870 itself starts with
// CMP dword ptr [RAX + 0x124], 0x6 ; JZ 0x017DEB34 (return) (0F 84 5C020000 at 0x017DE8D2)
// A zero displacement lands the jump on the next instruction, so the guest's item is made.
// It then only broadcasts the new item when hosting (+0x124 == 3), so a guest sends nothing.
{"GuestItems.CreateOwn", 0x017DE8D4, {0x5C, 0x02, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00},
Group::GuestItems},
// Part 5: placing the map's treasures, 0x017DC300 (see ByteEdits, part 5):
// CMP dword ptr [RAX + 0x124], 0x6 ; JZ 0x017DD081 (0F 84 A50C0000 at 0x017DC3D6)
// CMP dword ptr [RDX + 0x98], -1 ; JNZ 0x017DD081 (0F 85 980C0000 at 0x017DC3E3)
// 0x017DD081 is the end of the function: a guest placed no treasure at all. Zero
// displacements fall through, so the guest's game places them from its own map load. Which
// are shown is decided by the pickup flags (guest loot view in bloodborne_re.cpp); the
// character type table 0x047348A0 used right after allows type 1 (co-op phantom).
{"GuestItems.PlaceTreasures", 0x017DC3D8, {0xA5, 0x0C, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00},
Group::GuestItems},
{"GuestItems.PlaceTreasuresAway", 0x017DC3E5, {0x98, 0x0C, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00}, Group::GuestItems},
// Report 2026-10-07: "cannot use the bells in the Dream". The item availability check 0x0157F200
// gives the bells their own branches by goods id: CMP R12D, 0xCD / 0xE1 (Small Resonant /
// Sinister Bell) and at 0x0157F855 CMP EBX, 0xC8 (Beckoning Bell, 81 FB C8000000) - the branch
// that holds the seamless patches letting a host ring it where vanilla says no (0x0157F8C8,
// 0x0157F960). The kit's Hunt Bell is goods 4016 (a copy of 200), so it never got there; the
// vanilla bell itself is disabled by the kit. Its id goes into the compare: 4016 = 0x00000FB0.
// (The Kin Bell already works in the Dream through the general path - left as it is.)
{"KitBells.HuntBellChecks", 0x0157F857, {0xC8, 0x00, 0x00, 0x00}, {0xB0, 0x0F, 0x00, 0x00},
Group::KitBells},
// Three players, 2026-10-08 (host, guest cls_amg; a tester's game had crashed but his
// sign was still listed): the host summoned the dead sign and sat in CreateWait for 6 minutes,
// then its own game raised "OnIrregularLeaveSession" (0x017897DA) and ended the session for the
// guest too. That is the reliable-packet send 0x017894E0: a packet it cannot deliver to a member
// is queued (list [SprjSessionManager]+0x188, counts at +0x30, plus +0x1B0), and once
// CMP EAX, 0x3E9 <- 0x017897AD (3D E9 03 00 00)
// 1001 are waiting it leaves. A member that never connects fills it at ~3 a second. The limit
// goes to 30000 (~2-3 hours); "BBSC send backlog:" lines log a queue that keeps growing.
{"SendBacklog.Limit", 0x017897AE, {0xE9, 0x03, 0x00, 0x00}, {0x30, 0x75, 0x00, 0x00},
Group::SendBacklog},
// Five hunters, part 2 (test 2026-10-08 on mod 37: with Hunters.KeepSearchingCoop the host's
// sign lists DID hold a third co-op sign - 3 signs, 2742-2745 bytes - but no summon was ever
// built for it). After the search, the same slot table decides which sign types may be taken:
// both co-op slots taken -> MOV ESI, 0xFFF7FF7F (co-op types masked out), else 0xFFFFFFFF
// at 0x0187304A (sign search 0x01872360) and 0x01879382 (0x01878D90). The mask stays all-ones;
// the summon builder's own cap (4 cooperators) still decides who gets in.
{"Hunters.CoopMaskSearch", 0x0187304B, {0x7F, 0xFF, 0xF7, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
Group::Hunters},
{"Hunters.CoopMaskSelect", 0x01879383, {0x7F, 0xFF, 0xF7, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
Group::Hunters},
}};
// Function entries replaced by a jmp to a sibling function with the same arguments.
struct EntryJump {
const char* name;
u64 site;
std::array<u8, 5> prologue; // expected first 5 bytes (whole instructions)
u64 target;
Group group;
};
constexpr std::array<EntryJump, 1> EntryJumps{{
// Lua_MultiWall(id) (0x0138B720) shows the co-op fog wall entity: 0x013CABD0(id, 1) draw on,
// 0x013CA580(id, 1) collision on, 0x01315FA0(id + 1) its effect. Lua_InvalidMultiWall (0x0138B890)
// is the same function with 0 / 0x01315E50, i.e. hides the wall. Jumping there keeps every wall
// hidden and walkable (CONFIRMED statically from both listings, 2026-09-17).
{"Lua_MultiWall", 0x0138B720, {0x55, 0x48, 0x89, 0xE5, 0x53}, 0x0138B890, Group::NoFogWalls},
}};
// ---- Which groups are on (same switches in the emulator and the mod DLL) ----
inline bool EnvFlag(const char* name) {
return SwitchEnabled(name);
}
inline bool GroupEnabled(Group group) {
switch (group) {
case Group::Stage:
case Group::Revive:
return true;
case Group::Boss:
return EnvFlag("SHADPS4_BBSC_PERSIST_BOSS");
case Group::MatchCheck:
return EnvFlag("SHADPS4_BBSC_PERSIST_MATCHCHECK");
case Group::Lamp:
return EnvFlag("SHADPS4_BBSC_LAMP");
case Group::Tasks:
return EnvFlag("SHADPS4_BBSC_PERSIST_TASKS");
case Group::HostDead:
return EnvFlag("SHADPS4_BBSC_PERSIST_HOSTDEAD");
case Group::NoFogWalls:
return EnvFlag("SHADPS4_BBSC_NO_FOG_WALLS");
case Group::LeaveLimit:
return EnvFlag("SHADPS4_BBSC_SKIP_LEAVE_LIMIT");
case Group::Spectate:
return EnvFlag("SHADPS4_BBSC_SPECTATE");
case Group::Echoes:
return EnvFlag("SHADPS4_BBSC_FULL_ECHOES");
case Group::GuestItems:
return EnvFlag("SHADPS4_BBSC_GUEST_ITEMS");
case Group::SolidNpcs:
return EnvFlag("SHADPS4_BBSC_SOLID_NPCS");
case Group::Scaling:
return EnvFlag("SHADPS4_BBSC_SCALING");
case Group::KitBells:
return true; // the kit is always given (bloodborne_re.cpp, PrepareBbscKitRows)
case Group::SendBacklog:
return true;
case Group::Hunters:
return EnvFlag("SHADPS4_BBSC_FIVE_HUNTERS");
}
return false;
}
// Groups a suspension (quit to title, partner gone) switches off so the vanilla teardown can run.
// Only the ones that keep a session alive: the echo fix, the lamp fix, the fog walls, the death
// reload retarget and host-death handling have nothing to do with ending a session, and switching
// them off for 20 s halved echoes, blocked lamps and let fog walls appear (code review B2).
inline bool IsSessionGroup(Group group) {
switch (group) {
case Group::Stage:
case Group::Revive:
case Group::Boss:
case Group::MatchCheck:
case Group::Tasks:
return true;
default:
return false;
}
}
// The lamp edits follow the player's role at runtime instead of being on all the time: a guest
// must never get the lamp prompt (vanilla never allows it, and a guest's lamp travel starts a load
// in the host's world), and single player must keep the vanilla gates (code review B3/B4).
inline bool IsRoleGroup(Group group) {
return group == Group::Lamp;
}
enum class Scope : u8 { Install, Session, Role };
inline bool InScope(Group group, Scope scope) {
switch (scope) {
case Scope::Install:
return !IsRoleGroup(group); // role groups start in their vanilla state
case Scope::Session:
return IsSessionGroup(group);
case Scope::Role:
return IsRoleGroup(group);
}
return false;
}
inline std::array<u8, 5> EncodeRel32(u8 opcode, u64 site, u64 target) {
const std::int64_t displacement = static_cast<std::int64_t>(target) - static_cast<std::int64_t>(site + 5);
const std::int32_t rel = static_cast<std::int32_t>(displacement);
std::array<u8, 5> bytes{opcode};
std::memcpy(bytes.data() + 1, &rel, sizeof(rel));
return bytes;
}
} // namespace Core::Bbsc::PatchTables
+263
View File
@@ -0,0 +1,263 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/bbsc_persistence.h"
#include "core/bbsc_patch_tables.h"
#include <array>
#include <cstdlib>
#include <cstring>
#include <string_view>
#include "common/logging/log.h"
#include "common/memory_patcher.h"
#include "common/singleton.h"
#include "common/types.h"
#include "core/file_format/psf.h"
namespace Core::Bbsc {
namespace {
using namespace PatchTables;
// Set once every enabled site has been byte-verified, so the runtime toggle can never write into
// an image the patches were rejected for.
bool patches_verified = false;
bool session_patches_on = false;
bool role_patches_on = false;
// Writes every enabled site in `scope` in its patched or its original form. Returns how many were
// written.
int WritePatches(bool enable, Scope scope) {
const u64 base = MemoryPatcher::g_eboot_address;
if (!patches_verified || base == 0) {
return 0;
}
if (scope == Scope::Session && session_patches_on == enable) {
return 0;
}
if (scope == Scope::Role && role_patches_on == enable) {
return 0;
}
int count = 0;
for (const auto& entry : EntryJumps) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
auto* site = reinterpret_cast<u8*>(base + entry.site);
if (enable) {
const auto patched = EncodeRel32(0xE9, entry.site, entry.target);
std::memcpy(site, patched.data(), patched.size());
} else {
std::memcpy(site, entry.prologue.data(), entry.prologue.size());
}
++count;
}
for (const auto& entry : Retargets) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
const auto bytes = EncodeRel32(entry.opcode, entry.site,
enable ? entry.target : entry.original);
std::memcpy(reinterpret_cast<u8*>(base + entry.site), bytes.data(), bytes.size());
++count;
}
for (const auto& entry : EntryReturns) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
// Only the first byte changes: 0xC3 (ret) or the function's real first instruction byte.
*reinterpret_cast<u8*>(base + entry.site) = enable ? 0xC3 : entry.prologue[0];
++count;
}
for (const auto& entry : ByteEdits) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
const auto& bytes = enable ? entry.replacement : entry.original;
std::memcpy(reinterpret_cast<u8*>(base + entry.site), bytes.data(), bytes.size());
++count;
}
for (const auto& entry : WideEdits) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
const auto& bytes = enable ? entry.replacement : entry.original;
std::memcpy(reinterpret_cast<u8*>(base + entry.site), bytes.data(), bytes.size());
++count;
}
if (scope == Scope::Install) {
session_patches_on = enable;
} else if (scope == Scope::Session) {
session_patches_on = enable;
} else {
role_patches_on = enable;
}
return count;
}
} // namespace
void SetPersistentSessionPatches(bool enabled) {
const int count = WritePatches(enabled, Scope::Session);
if (count != 0) {
LOG_INFO(Debug, "BBSC persistence {} ({} session sites; echoes, lamp, fog walls and "
"deaths untouched)",
enabled ? "re-applied" : "suspended", count);
}
}
void SetHostLampPatches(bool hosting) {
const int count = WritePatches(hosting, Scope::Role);
if (count != 0) {
LOG_INFO(Debug, "BBSC lamp: {} ({} sites)",
hosting ? "prompt allowed (hosting a session)"
: "vanilla gates (solo or guest - a guest never gets the lamp)",
count);
}
}
void InstallPersistentSessionPatches() {
static bool attempted = false;
if (attempted) {
return;
}
if (!EnvFlag("SHADPS4_BBSC_PERSIST")) {
LOG_INFO(Debug, "BBSC persistence declined: SHADPS4_BBSC_PERSIST is off");
return;
}
attempted = true;
if (MemoryPatcher::g_game_serial != "CUSA03173") {
LOG_INFO(Debug, "BBSC persistence declined: this is {}, not CUSA03173",
MemoryPatcher::g_game_serial.empty() ? "an unknown title"
: MemoryPatcher::g_game_serial);
return;
}
const auto* param_sfo = Common::Singleton<PSF>::Instance();
const std::string_view app_version = param_sfo->GetString("APP_VER").value_or("Unknown");
if (app_version != "01.09") {
LOG_ERROR(Debug, "BBSC persistence requires CUSA03173 01.09; loaded version is {}",
app_version);
return;
}
const u64 base = MemoryPatcher::g_eboot_address;
const u64 image_size = MemoryPatcher::g_eboot_image_size;
if (base == 0) {
LOG_ERROR(Debug, "BBSC persistence declined: the eboot image base is not known yet");
return;
}
// Verify every enabled site before writing any, so a mismatch leaves the game untouched.
for (const auto& entry : Retargets) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + 5 > image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto original = EncodeRel32(entry.opcode, entry.site, entry.original);
const auto patched = EncodeRel32(entry.opcode, entry.site, entry.target);
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (std::memcmp(actual, original.data(), 5) != 0 &&
std::memcmp(actual, patched.data(), 5) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
for (const auto& entry : EntryReturns) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + entry.prologue.size() > image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (actual[0] != 0xC3 &&
std::memcmp(actual, entry.prologue.data(), entry.prologue.size()) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
for (const auto& entry : EntryJumps) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + entry.prologue.size() > image_size || entry.target >= image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto patched = EncodeRel32(0xE9, entry.site, entry.target);
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (std::memcmp(actual, patched.data(), patched.size()) != 0 &&
std::memcmp(actual, entry.prologue.data(), entry.prologue.size()) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
for (const auto& entry : ByteEdits) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + entry.original.size() > image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (std::memcmp(actual, entry.original.data(), entry.original.size()) != 0 &&
std::memcmp(actual, entry.replacement.data(), entry.replacement.size()) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
for (const auto& entry : WideEdits) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + entry.original.size() > image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (std::memcmp(actual, entry.original.data(), entry.original.size()) != 0 &&
std::memcmp(actual, entry.replacement.data(), entry.replacement.size()) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
patches_verified = true;
const int applied = WritePatches(true, Scope::Install);
LOG_INFO(Debug,
"BBSC persistence enabled ({} sites; boss={}, matching-check={}, lamp={}, "
"keep-tasks={}, ignore-host-death={}, no-fog-walls={}, skip-leave-limit={}, "
"death-without-reload={}, guest-items={}, solid-npcs={})",
applied, GroupEnabled(Group::Boss), GroupEnabled(Group::MatchCheck),
GroupEnabled(Group::Lamp), GroupEnabled(Group::Tasks),
GroupEnabled(Group::HostDead), GroupEnabled(Group::NoFogWalls),
GroupEnabled(Group::LeaveLimit), GroupEnabled(Group::Spectate),
GroupEnabled(Group::GuestItems), GroupEnabled(Group::SolidNpcs));
}
} // namespace Core::Bbsc
+102
View File
@@ -0,0 +1,102 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstdint>
#include <cstdlib>
#include <string_view>
namespace Core::Bbsc {
// BBSC build defaults: the co-op features confirmed working in game (2026-09-16) are on without
// any environment variable, so the build runs straight from a launcher. Everything still only
// applies to CUSA03173 01.09 (checked where each feature installs). Experimental switches stay
// off, and so is the boot intro (SHADPS4_BBSC_INTRO=1 brings it back). Setting a variable always wins: "0" turns a default off, any other value turns it on.
inline constexpr std::array<std::string_view, 31> DefaultOnSwitches{
"SHADPS4_BBSC_SHARED_PROGRESS", // merge the host's progress into the guest's world (adds only)
"SHADPS4_BBSC_WARP_TOGETHER", // the guest follows the host through a lamp warp
"SHADPS4_BBSC_WORLD_SYNC", // ...and loads the host's map with the host's world flags
"SHADPS4_BBSC_LOAD_COVER", // the guest's summon/follow loads behind a smooth black fade
"SHADPS4_BBSC_FULL_ECHOES", // a summoned guest is paid the host's rate, not half
"SHADPS4_BBSC_GUEST_ITEMS", // a guest picks up and keeps items in the host's world
"SHADPS4_BBSC_SOLID_NPCS", // a guest sees NPCs solid, not as grey wandering ghosts
"SHADPS4_BBSC_FLOOR_GUARD", // no crash when a character's floor was unloaded
"SHADPS4_BBSC_DREAM", // a guest uses the Doll, workshop and messengers in the host's Dream
"SHADPS4_BBSC_DEATH_PENALTY", // a kept death still drops a bloodstain and the echoes
"SHADPS4_BBSC_TRACE_WALLS", // log which map objects get shown/made solid (fog walls)
"SHADPS4_BBSC_LAMP", // the lamp prompt works in co-op (clears the +0x4B gate)
"SHADPS4_BBSC_NO_HP_PENALTY", // clears the summoned guest's 30% max HP cut (effect 9006)
"SHADPS4_BBSC_NO_FOG_WALLS", // co-op area fog walls hidden, leaving the area allowed
"SHADPS4_BBSC_MESSAGES", // co-op texts that fit seamless play (no "Returning to your world")
"SHADPS4_BBSC_CONNECT_FIRST", // cross-map summon: join first, one load into host map
"SHADPS4_BBSC_WARP_JOIN", // after a cross-map join, one more real reload (lighting)
"SHADPS4_BLOODBORNE_SEAMLESS_COOP", // Wozzardman's seamless summon
"SHADPS4_BLOODBORNE_RE_TRACE", // installs the BBSC hooks (and the trace logs)
"SHADPS4_CAPTURE_BLOODBORNE_SUMMON", // summon capture files for collect-logs
"SHADPS4_BBSC_PERSIST", // keep the session through loads and deaths
"SHADPS4_BBSC_PERSIST_MATCHCHECK", // ignore the post-load matching check kick
"SHADPS4_BBSC_REJOIN", // guest rejoin / reinsert after a kept death
"SHADPS4_BBSC_PERSIST_TASKS", // keep member tasks across a kept load
"SHADPS4_BBSC_PERSIST_HOSTDEAD", // the guest stays when the host dies
"SHADPS4_BBSC_SPECTATE", // respawn in place / spectate, join teleport
"SHADPS4_BBSC_FREE_BELL", // the Beckoning Bell costs no Insight
"SHADPS4_BBSC_FIVE_HUNTERS", // the host may summon up to 4 guests (5 hunters)
"SHADPS4_BBSC_CINEMATIC", // F1: free camera for filming (keyboard + mouse)
"SHADPS4_BBSC_SCALING", // enemy/boss HP, damage, stagger per guest (like ERSC)
"SHADPS4_BBSC_INVADE_ANYWHERE", // every world open to invaders (Bell Maiden SpEffect 9020)
};
// False in the vanilla build (CMake BBSC_VANILLA): the emulator carries no BBSC of its own and
// BBSC runs as the bbsc.dll mod, which compiles this header itself (BBSC_MOD) with BuiltIn true.
#if defined(BBSC_VANILLA) && !defined(BBSC_MOD)
inline constexpr bool BuiltIn = false;
#else
inline constexpr bool BuiltIn = true;
#endif
inline bool SwitchEnabled(const char* name) {
if constexpr (!BuiltIn) {
// Every BBSC feature is off, whatever the environment says. The three real opt-out
// switches count as set, so the emulator's own seamless-summon helpers (fast sign,
// placement header) stay out of the way of the mod's.
const std::string_view key{name};
return key == "SHADPS4_BBSC_NO_PLACEMENT_HEADER" || key == "SHADPS4_BBSC_NO_FAST_SIGN" ||
key == "SHADPS4_BBSC_NO_GUEST_HOOKS";
}
const char* value = std::getenv(name);
if (value == nullptr || value[0] == '\0') {
for (const std::string_view on : DefaultOnSwitches) {
if (on == name) {
return true;
}
}
return false;
}
return std::string_view{value} != "0";
}
// Return-only guest function used as the target for skipped remote-player cleanup calls.
// Observed by the RE trace as "BBSC.Persist.SkippedCleanup" to record every persistence decision.
inline constexpr std::uint64_t PersistNoOpOffset = 0x013001E0;
// Phase 1 (docs/PHASE1-DESIGN.md, Option A): keep Bloodborne's co-op session alive across
// structural session ends (stage loads, revive, optionally boss clear and OnMatchingCheck) by
// retargeting the call sites of CSMultiPlayMan::Stop and WorldChrMan remote cleanup.
// On by default (SHADPS4_BBSC_PERSIST=0 disables). CUSA03173 01.09 only; every site is byte-verified.
void InstallPersistentSessionPatches();
// Turns the applied patches off and on again at runtime. A deliberate quit to the title has to
// tear the session down the way the game intends - otherwise the other player keeps a phantom
// partner and has to close the game (reported 2026-09-20). Reverting the patches for a few
// seconds lets the vanilla teardown run, then they go back on. Safe to call repeatedly; does
// nothing when the patches were never applied.
void SetPersistentSessionPatches(bool enabled);
// The lamp edits (prompt gate +0x49 and the character gate) are only on while this player hosts a
// session. Called from the game tick whenever the role changes; the lamp stays vanilla in single
// player and for a guest.
void SetHostLampPatches(bool hosting);
} // namespace Core::Bbsc
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+249 -241
View File
@@ -1,242 +1,250 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifdef _WIN32
#define _WINSOCK_DEPRECATED_NO_WARNINGS
// clang-format off
#include <winsock2.h>
#include <ws2tcpip.h>
#include <afunix.h>
#include <iphlpapi.h>
#include <mstcpip.h>
// clang-format on
typedef SOCKET net_socket;
typedef int socklen_t;
#ifndef LPFN_WSASENDMSG
typedef INT(PASCAL* LPFN_WSASENDMSG)(SOCKET s, LPWSAMSG lpMsg, DWORD dwFlags,
LPDWORD lpNumberOfBytesSent, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
#endif
#ifndef WSAID_WSASENDMSG
static const GUID WSAID_WSASENDMSG = {
0xa441e712, 0x754f, 0x43ca, {0x84, 0xa7, 0x0d, 0xee, 0x44, 0xcf, 0x60, 0x6d}};
#endif
#ifndef LPFN_WSARECVMSG
typedef INT(PASCAL* LPFN_WSARECVMSG)(SOCKET s, LPWSAMSG lpMsg, LPDWORD lpdwNumberOfBytesRecvd,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
#endif
#ifndef WSAID_WSARECVMSG
static const GUID WSAID_WSARECVMSG = {
0xf689d7c8, 0x6f1f, 0x436b, {0x8a, 0x53, 0xe5, 0x4f, 0xe3, 0x51, 0xc3, 0x22}};
#endif
#else
#include <cerrno>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
typedef int net_socket;
#endif
#include <condition_variable>
#include <deque>
#include <map>
#include <memory>
#include <mutex>
#include <vector>
#include "net.h"
namespace Libraries::Kernel {
struct OrbisKernelStat;
}
namespace Libraries::Net {
struct Socket;
typedef std::shared_ptr<Socket> SocketPtr;
struct OrbisNetLinger {
s32 l_onoff;
s32 l_linger;
};
struct Socket {
explicit Socket(int domain, int type, int protocol) : socket_type(type) {}
virtual ~Socket() = default;
virtual bool IsValid() const = 0;
virtual int Close() = 0;
virtual int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) = 0;
virtual int GetSocketOptions(int level, int optname, void* optval, u32* optlen) = 0;
virtual int Bind(const OrbisNetSockaddr* addr, u32 addrlen) = 0;
virtual int Listen(int backlog) = 0;
virtual int SendMessage(const OrbisNetMsghdr* msg, int flags) = 0;
virtual int SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to,
u32 tolen) = 0;
virtual SocketPtr Accept(OrbisNetSockaddr* addr, u32* addrlen) = 0;
virtual int ReceiveMessage(OrbisNetMsghdr* msg, int flags) = 0;
virtual int ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from,
u32* fromlen) = 0;
virtual int Connect(const OrbisNetSockaddr* addr, u32 namelen) = 0;
virtual int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) = 0;
virtual int GetPeerName(OrbisNetSockaddr* addr, u32* namelen) = 0;
virtual int fstat(Libraries::Kernel::OrbisKernelStat* stat) = 0;
virtual std::optional<net_socket> Native() = 0;
std::mutex m_mutex;
std::mutex receive_mutex;
int socket_type;
};
struct PosixSocket : public Socket {
net_socket sock;
int sockopt_so_connecttimeo = 0;
int sockopt_so_reuseport = 0;
int sockopt_so_onesbcast = 0;
int sockopt_so_usecrypto = 0;
int sockopt_so_usesignature = 0;
int sockopt_so_nbio = 0;
int sockopt_ip_ttlchk = 0;
int sockopt_ip_maxttl = 0;
int sockopt_tcp_mss_to_advertise = 0;
int socket_type;
explicit PosixSocket(int domain, int type, int protocol)
: Socket(domain, type, protocol), sock(socket(domain, type, protocol)) {
socket_type = type;
}
explicit PosixSocket(net_socket sock) : Socket(0, 0, 0), sock(sock) {}
bool IsValid() const override;
int Close() override;
int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) override;
int GetSocketOptions(int level, int optname, void* optval, u32* optlen) override;
int Bind(const OrbisNetSockaddr* addr, u32 addrlen) override;
int Listen(int backlog) override;
int SendMessage(const OrbisNetMsghdr* msg, int flags) override;
int SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to,
u32 tolen) override;
int ReceiveMessage(OrbisNetMsghdr* msg, int flags) override;
int ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from, u32* fromlen) override;
SocketPtr Accept(OrbisNetSockaddr* addr, u32* addrlen) override;
int Connect(const OrbisNetSockaddr* addr, u32 namelen) override;
int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) override;
int GetPeerName(OrbisNetSockaddr* addr, u32* namelen) override;
int fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
std::optional<net_socket> Native() override {
return sock;
}
};
struct P2PDatagram {
u32 source_addr{};
u16 source_port{};
u16 source_vport{};
std::vector<u8> payload;
};
struct P2PSocket : public Socket {
explicit P2PSocket(int domain, int type, int protocol);
~P2PSocket() override;
bool IsValid() const override {
return valid;
}
int Close() override;
int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) override;
int GetSocketOptions(int level, int optname, void* optval, u32* optlen) override;
int Bind(const OrbisNetSockaddr* addr, u32 addrlen) override;
int Listen(int backlog) override;
int SendMessage(const OrbisNetMsghdr* msg, int flags) override;
int SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to,
u32 tolen) override;
int ReceiveMessage(OrbisNetMsghdr* msg, int flags) override;
int ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from, u32* fromlen) override;
SocketPtr Accept(OrbisNetSockaddr* addr, u32* addrlen) override;
int Connect(const OrbisNetSockaddr* addr, u32 namelen) override;
int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) override;
int GetPeerName(OrbisNetSockaddr* addr, u32* namelen) override;
int fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
std::optional<net_socket> Native() override {
#ifdef _WIN32
if (readiness_fd != INVALID_SOCKET) {
return readiness_fd;
}
#elif defined(__linux__)
if (readiness_fd >= 0) {
return readiness_fd;
}
#endif
return {};
}
// Called by the shared UDP transport after it demultiplexes a virtual port.
void EnqueuePacket(P2PDatagram packet);
private:
bool valid{true};
bool bound{false};
bool connected{false};
OrbisNetSockaddrIn local_addr{};
OrbisNetSockaddrIn peer_addr{};
int sockopt_so_nbio{0};
int sockopt_so_usecrypto{0};
int sockopt_so_usesignature{0};
std::map<int, int> integer_socket_options;
std::condition_variable receive_cv;
std::deque<P2PDatagram> receive_queue;
#ifdef _WIN32
// wepoll requires a Winsock handle. A private loopback pair carries readiness bytes only.
net_socket readiness_fd{INVALID_SOCKET};
net_socket readiness_signal_fd{INVALID_SOCKET};
#elif defined(__linux__)
// eventfd carries readiness only; network data stays on the shared UDP transport.
int readiness_fd{-1};
#endif
};
struct UnixSocket : public Socket {
net_socket sock;
int socket_type;
explicit UnixSocket(int domain, int type, int protocol)
: Socket(domain, type, protocol), sock(socket(domain, type, protocol)) {
socket_type = type;
}
explicit UnixSocket(net_socket sock) : Socket(0, 0, 0), sock(sock) {}
bool IsValid() const override;
int Close() override;
int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) override;
int GetSocketOptions(int level, int optname, void* optval, u32* optlen) override;
int Bind(const OrbisNetSockaddr* addr, u32 addrlen) override;
int Listen(int backlog) override;
int SendMessage(const OrbisNetMsghdr* msg, int flags) override;
int SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to,
u32 tolen) override;
int ReceiveMessage(OrbisNetMsghdr* msg, int flags) override;
int ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from, u32* fromlen) override;
SocketPtr Accept(OrbisNetSockaddr* addr, u32* addrlen) override;
int Connect(const OrbisNetSockaddr* addr, u32 namelen) override;
int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) override;
int GetPeerName(OrbisNetSockaddr* addr, u32* namelen) override;
int fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
std::optional<net_socket> Native() override {
return sock;
}
};
u16 GetP2PConfiguredPort();
u32 GetP2PAdvertisedAddr();
bool EnsureP2PTransport();
bool P2PTransportIsReady();
int P2PSignalingSendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port);
int P2PSignalingRecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port);
int P2PControlSendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port);
int P2PControlRecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port);
int P2PMatching2SendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port);
int P2PMatching2RecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port);
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifdef _WIN32
#define _WINSOCK_DEPRECATED_NO_WARNINGS
// clang-format off
#include <winsock2.h>
#include <ws2tcpip.h>
#include <afunix.h>
#include <iphlpapi.h>
#include <mstcpip.h>
// clang-format on
typedef SOCKET net_socket;
typedef int socklen_t;
#ifndef LPFN_WSASENDMSG
typedef INT(PASCAL* LPFN_WSASENDMSG)(SOCKET s, LPWSAMSG lpMsg, DWORD dwFlags,
LPDWORD lpNumberOfBytesSent, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
#endif
#ifndef WSAID_WSASENDMSG
static const GUID WSAID_WSASENDMSG = {
0xa441e712, 0x754f, 0x43ca, {0x84, 0xa7, 0x0d, 0xee, 0x44, 0xcf, 0x60, 0x6d}};
#endif
#ifndef LPFN_WSARECVMSG
typedef INT(PASCAL* LPFN_WSARECVMSG)(SOCKET s, LPWSAMSG lpMsg, LPDWORD lpdwNumberOfBytesRecvd,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
#endif
#ifndef WSAID_WSARECVMSG
static const GUID WSAID_WSARECVMSG = {
0xf689d7c8, 0x6f1f, 0x436b, {0x8a, 0x53, 0xe5, 0x4f, 0xe3, 0x51, 0xc3, 0x22}};
#endif
#else
#include <cerrno>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
typedef int net_socket;
#endif
#include <condition_variable>
#include <deque>
#include <string_view>
#include <map>
#include <memory>
#include <mutex>
#include <vector>
#include "net.h"
namespace Libraries::Kernel {
struct OrbisKernelStat;
}
namespace Libraries::Net {
struct Socket;
typedef std::shared_ptr<Socket> SocketPtr;
struct OrbisNetLinger {
s32 l_onoff;
s32 l_linger;
};
struct Socket {
explicit Socket(int domain, int type, int protocol) : socket_type(type) {}
virtual ~Socket() = default;
virtual bool IsValid() const = 0;
virtual int Close() = 0;
virtual int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) = 0;
virtual int GetSocketOptions(int level, int optname, void* optval, u32* optlen) = 0;
virtual int Bind(const OrbisNetSockaddr* addr, u32 addrlen) = 0;
virtual int Listen(int backlog) = 0;
virtual int SendMessage(const OrbisNetMsghdr* msg, int flags) = 0;
virtual int SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to,
u32 tolen) = 0;
virtual SocketPtr Accept(OrbisNetSockaddr* addr, u32* addrlen) = 0;
virtual int ReceiveMessage(OrbisNetMsghdr* msg, int flags) = 0;
virtual int ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from,
u32* fromlen) = 0;
virtual int Connect(const OrbisNetSockaddr* addr, u32 namelen) = 0;
virtual int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) = 0;
virtual int GetPeerName(OrbisNetSockaddr* addr, u32* namelen) = 0;
virtual int fstat(Libraries::Kernel::OrbisKernelStat* stat) = 0;
virtual std::optional<net_socket> Native() = 0;
std::mutex m_mutex;
std::mutex receive_mutex;
int socket_type;
};
struct PosixSocket : public Socket {
net_socket sock;
int sockopt_so_connecttimeo = 0;
int sockopt_so_reuseport = 0;
int sockopt_so_onesbcast = 0;
int sockopt_so_usecrypto = 0;
int sockopt_so_usesignature = 0;
int sockopt_so_nbio = 0;
int sockopt_ip_ttlchk = 0;
int sockopt_ip_maxttl = 0;
int sockopt_tcp_mss_to_advertise = 0;
int socket_type;
explicit PosixSocket(int domain, int type, int protocol)
: Socket(domain, type, protocol), sock(socket(domain, type, protocol)) {
socket_type = type;
}
explicit PosixSocket(net_socket sock) : Socket(0, 0, 0), sock(sock) {}
bool IsValid() const override;
int Close() override;
int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) override;
int GetSocketOptions(int level, int optname, void* optval, u32* optlen) override;
int Bind(const OrbisNetSockaddr* addr, u32 addrlen) override;
int Listen(int backlog) override;
int SendMessage(const OrbisNetMsghdr* msg, int flags) override;
int SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to,
u32 tolen) override;
int ReceiveMessage(OrbisNetMsghdr* msg, int flags) override;
int ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from, u32* fromlen) override;
SocketPtr Accept(OrbisNetSockaddr* addr, u32* addrlen) override;
int Connect(const OrbisNetSockaddr* addr, u32 namelen) override;
int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) override;
int GetPeerName(OrbisNetSockaddr* addr, u32* namelen) override;
int fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
std::optional<net_socket> Native() override {
return sock;
}
};
struct P2PDatagram {
u32 source_addr{};
u16 source_port{};
u16 source_vport{};
std::vector<u8> payload;
};
struct P2PSocket : public Socket {
explicit P2PSocket(int domain, int type, int protocol);
~P2PSocket() override;
bool IsValid() const override {
return valid;
}
int Close() override;
int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) override;
int GetSocketOptions(int level, int optname, void* optval, u32* optlen) override;
int Bind(const OrbisNetSockaddr* addr, u32 addrlen) override;
int Listen(int backlog) override;
int SendMessage(const OrbisNetMsghdr* msg, int flags) override;
int SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to,
u32 tolen) override;
int ReceiveMessage(OrbisNetMsghdr* msg, int flags) override;
int ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from, u32* fromlen) override;
SocketPtr Accept(OrbisNetSockaddr* addr, u32* addrlen) override;
int Connect(const OrbisNetSockaddr* addr, u32 namelen) override;
int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) override;
int GetPeerName(OrbisNetSockaddr* addr, u32* namelen) override;
int fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
std::optional<net_socket> Native() override {
#ifdef _WIN32
if (readiness_fd != INVALID_SOCKET) {
return readiness_fd;
}
#elif defined(__linux__)
if (readiness_fd >= 0) {
return readiness_fd;
}
#endif
return {};
}
// Called by the shared UDP transport after it demultiplexes a virtual port.
void EnqueuePacket(P2PDatagram packet);
private:
bool valid{true};
bool bound{false};
bool connected{false};
OrbisNetSockaddrIn local_addr{};
OrbisNetSockaddrIn peer_addr{};
int sockopt_so_nbio{0};
int sockopt_so_usecrypto{0};
int sockopt_so_usesignature{0};
std::map<int, int> integer_socket_options;
std::condition_variable receive_cv;
std::deque<P2PDatagram> receive_queue;
#ifdef _WIN32
// wepoll requires a Winsock handle. A private loopback pair carries readiness bytes only.
net_socket readiness_fd{INVALID_SOCKET};
net_socket readiness_signal_fd{INVALID_SOCKET};
#elif defined(__linux__)
// eventfd carries readiness only; network data stays on the shared UDP transport.
int readiness_fd{-1};
#endif
};
struct UnixSocket : public Socket {
net_socket sock;
int socket_type;
explicit UnixSocket(int domain, int type, int protocol)
: Socket(domain, type, protocol), sock(socket(domain, type, protocol)) {
socket_type = type;
}
explicit UnixSocket(net_socket sock) : Socket(0, 0, 0), sock(sock) {}
bool IsValid() const override;
int Close() override;
int SetSocketOptions(int level, int optname, const void* optval, u32 optlen) override;
int GetSocketOptions(int level, int optname, void* optval, u32* optlen) override;
int Bind(const OrbisNetSockaddr* addr, u32 addrlen) override;
int Listen(int backlog) override;
int SendMessage(const OrbisNetMsghdr* msg, int flags) override;
int SendPacket(const void* msg, u32 len, int flags, const OrbisNetSockaddr* to,
u32 tolen) override;
int ReceiveMessage(OrbisNetMsghdr* msg, int flags) override;
int ReceivePacket(void* buf, u32 len, int flags, OrbisNetSockaddr* from, u32* fromlen) override;
SocketPtr Accept(OrbisNetSockaddr* addr, u32* addrlen) override;
int Connect(const OrbisNetSockaddr* addr, u32 namelen) override;
int GetSocketAddress(OrbisNetSockaddr* name, u32* namelen) override;
int GetPeerName(OrbisNetSockaddr* addr, u32* namelen) override;
int fstat(Libraries::Kernel::OrbisKernelStat* stat) override;
std::optional<net_socket> Native() override {
return sock;
}
};
u16 GetP2PConfiguredPort();
u32 GetP2PAdvertisedAddr();
bool EnsureP2PTransport();
bool P2PTransportIsReady();
int P2PSignalingSendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port);
int P2PSignalingRecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port);
int P2PControlSendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port);
int P2PControlRecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port);
int P2PMatching2SendTo(const void* data, u32 len, u32 dest_addr, u16 dest_port);
int P2PMatching2RecvFrom(void* buf, u32 len, u32* from_addr, u16* from_port);
// BBSC relay fallback (2026-10-12): a peer that sends nothing back directly within 2 s is reached
// through the game server's UDP relay (port 3478 on the shadNet server's address), by online id.
void P2PRelaySetServer(u32 addr_nbo);
void P2PRelaySetLocalId(std::string_view online_id);
void P2PRelaySetSelfEndpoint(u32 addr_nbo, u16 port_nbo); // our public endpoint (STUN echo)
void P2PRelayRememberPeer(std::string_view online_id, u32 addr_nbo, u16 port_nbo);
} // namespace Libraries::Net
File diff suppressed because it is too large. Load diff
+329 -324
View File
@@ -1,325 +1,330 @@
// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <chrono>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include "common/types.h"
#include "core/libraries/np/np_manager.h"
#include "core/libraries/np/np_score/np_score.h"
#include "core/libraries/np/np_score/np_score_ctx.h"
#include "core/libraries/rtc/rtc.h"
#include "core/libraries/system/userservice.h"
#include "shadnet/client.h"
namespace Libraries::Np {
class NpHandler {
public:
static NpHandler& GetInstance();
NpHandler(const NpHandler&) = delete;
NpHandler& operator=(const NpHandler&) = delete;
// Connect every currently-logged-in user that has shadNet credentials.
void Initialize();
// Disconnect all clients, stop worker threads, fire SignedOut for each.
void Shutdown();
// Connect/disconnect a single user in response to a user service login/logout event.
void OnUserLoggedIn(s32 user_id);
void OnUserLoggedOut(s32 user_id);
// True if this specific user is authenticated to the shadNet server.
bool IsPsnSignedIn(s32 user_id) const;
/// True if any user is currently signed in
bool IsAnySignedIn() const;
// Set the Appear-Offline preference for all signed-in users (and future logins). While
// enabled, shadNet handles the user as offline for everyone else. Call from the UI/config.
void SetAppearOffline(bool enable);
bool IsAppearOffline() const {
return m_appear_offline.load();
}
/// Full NP ID for this user, built once from shadnet_npid after login.
OrbisNpId GetNpId(s32 user_id) const;
/// The Online ID embedded in the NP ID (npid.handle).
OrbisNpOnlineId GetOnlineId(s32 user_id) const;
// Avatar URL returned by the server.
std::string GetAvatarUrl(s32 user_id) const;
// 64-bit account ID assigned by the server.
OrbisNpAccountId GetAccountId(s32 user_id) const;
// WebAPI bearer token
std::string GetBearerToken(s32 user_id) const;
// Sends a session invitation via the WebAPI (POST /v1/sessions/<id>/invitations). Each 'to'
// entry is an online ID (npid) or a decimal account ID
bool SendSessionInvitation(s32 user_id, const std::string& session_id,
const std::vector<std::string>& to, const std::string& message);
// Raises ORBIS_SYSTEM_SERVICE_EVENT_SESSION_INVITATION (0x10000002) so titles that watch the
// system-service event learn about the invite.
void PostSessionInvitationEvent(const std::string& session_id, const std::string& invitation_id,
const std::string& accepter_online_id);
// A session invitation surfaced from a shadNet push, stashed until the user acts on it.
struct PendingInvitation {
std::string session_id;
std::string invitation_id;
std::string from_npid; // sender (for display)
std::string to_npid; // local recipient / accepter
int64_t valid_until = 0; // ms since epoch; 0 = never expires
};
// Pending invitations stashed for a local user (newest last), populated on arrival.
std::vector<PendingInvitation> GetPendingInvitations(s32 user_id) const;
// Consumes an invitation server-side (PUT usedFlag=true) and drops it from the stash. Returns
// true if the invite was found and the consume succeeded.
bool AcceptSessionInvitation(s32 user_id, const std::string& invitation_id);
// Dismisses an invitation locally by dropping it from the stash. No server call (a declined
// invite is left unconsumed and simply ages out server-side).
void DeclineSessionInvitation(s32 user_id, const std::string& invitation_id);
// Local IP address (network byte order) as seen at connect time.
u32 GetLocalIpAddr(s32 user_id) const;
// Reverse lookup: server account_id to local user_id.
// Returns -1 if no connected user owns that account_id.
s32 GetUserIdByAccountId(u64 account_id) const;
// Reverse lookup: OrbisNpOnlineId to local user_id.
// Scans m_np_ids for a matching handle.data string.
// Returns -1 if no connected user has that Online ID.
s32 GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const;
// Friend list
u32 GetNumFriends(s32 user_id) const;
std::optional<std::string> GetFriendNpid(s32 user_id, u32 index) const;
// ---- Friend list / requests / blocked (shadNet) ----
struct FriendInfo {
std::string npid;
bool online = false;
};
struct FriendListSnapshot {
std::vector<FriendInfo> friends;
std::vector<std::string> requests_received;
std::vector<std::string> requests_sent;
std::vector<std::string> blocked;
};
// Local users that currently have an authenticated shadNet session.
std::vector<s32> GetConnectedUsers() const;
// user's friend state for UI display.
FriendListSnapshot GetFriendList(s32 user_id) const;
// Friend/block actions
s32 SendFriendRequest(s32 user_id, const std::string& npid);
s32 RemoveFriend(s32 user_id, const std::string& npid);
s32 BlockUser(s32 user_id, const std::string& npid);
s32 UnblockUser(s32 user_id, const std::string& npid);
// Submit a RecordScore request to the shadNet server.
s32 RecordScore(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score,
const char* comment, size_t commentLen, const u8* gameInfoData,
size_t gameInfoSize, std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 RecordGameData(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score,
const u8* data, size_t size, std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetGameData(s32 user_id, s32 service_label, u32 boardId, const std::string& npId, s32 pcId,
void* dataOut, u64 recvSize, u64* totalSizeOut,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetGameDataByAccountId(s32 user_id, s32 service_label, u32 boardId, u64 accountId, s32 pcId,
void* dataOut, u64 recvSize, u64* totalSizeOut,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetBoardInfo(s32 user_id, s32 service_label, u32 boardId,
NpScore::OrbisNpScoreBoardInfo* boardInfo,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// Submit a GetRankingByNpId request to the shadNet server.
s32 GetRankingByNpId(s32 user_id, s32 service_label, u32 boardId,
const std::vector<std::string>& npIds, const std::vector<s32>& pcIds,
NpScore::OrbisNpScorePlayerRankData* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// Submit a GetRankingByRange request to the shadNet server.
s32 GetRankingByRange(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank,
u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// A-variant of GetRankingByRange.
s32 GetRankingByRangeA(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank,
u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetRankingByAccountId(s32 user_id, s32 service_label, u32 boardId,
const std::vector<u64>& accountIds, const std::vector<s32>& pcIds,
NpScore::OrbisNpScorePlayerRankDataA* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// Submit a GetFriendsRanking request to the shadNet server.
s32 GetFriendsRanking(s32 user_id, s32 service_label, u32 boardId, bool includeSelf,
u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// A-variant of GetFriendsRanking
s32 GetFriendsRankingA(s32 user_id, s32 service_label, u32 boardId, bool includeSelf,
u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// State callbacks
using StateCallback = std::function<void(Libraries::UserService::OrbisUserServiceUserId user_id,
NpManager::OrbisNpState state)>;
s32 RegisterStateCallback(StateCallback cb, void* userdata);
void UnregisterStateCallback(s32 handle);
private:
NpHandler() = default;
~NpHandler() = default;
/// Connect one user. Blocks until connected+authenticated or failed.
bool ConnectUser(s32 user_id, const std::string& host, u16 port, const std::string& npid,
const std::string& password, const std::string& token);
// Connect a single logged-in user by id (looks up credentials, parses server).
bool ConnectUserById(s32 user_id);
// Parse the configured shadNet server "host:port" (default port 31313).
std::pair<std::string, u16> ParseServerAddress() const;
// Start the health-monitor worker thread if not already running (idempotent).
void StartWorker();
// Disconnect and remove one user's client.
void DisconnectUser(s32 user_id);
void WorkerThread();
// Transparent reconnect after a network drop
void MarkForReconnect(s32 user_id);
void TryReconnect();
void FireStateCallback(s32 user_id, NpManager::OrbisNpState state);
// Notification forwarders wired into each client
void OnFriendQuery(s32 user_id, const ShadNet::NotifyFriendQuery& n);
void OnFriendNew(s32 user_id, const ShadNet::NotifyFriendNew& n);
void OnFriendLost(s32 user_id, const ShadNet::NotifyFriendLost& n);
void OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n);
void OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n);
void OnLoginResult(s32 user_id, const ShadNet::LoginResult& res);
// General async reply dispatch. Routes a reply to the owning subsystem by
// command. Called from the per-user ShadNetClient on the reader thread.
void OnAsyncReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);
void OnScoreReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);
// 12-byte NP Communication ID
std::string GetNpCommId(s32 service_label) const;
// Appear-Offline preference, applied to every client at login and on change.
std::atomic<bool> m_appear_offline{false};
// Per-user client map
mutable std::mutex m_mutex_clients;
std::map<s32, std::shared_ptr<ShadNet::ShadNetClient>> m_clients;
// Per-user NP ID built once from shadnet_npid after login.
std::map<s32, OrbisNpId> m_np_ids;
// Score requests awaiting a reply, keyed by the submit packet id.
struct PendingScoreRequest {
std::shared_ptr<NpScore::ScoreRequestCtx> req;
ShadNet::CommandType cmd;
std::vector<std::string> requestedNpIds;
NpScore::OrbisNpScorePlayerRankData* rankArray = nullptr;
NpScore::OrbisNpScoreRankData* plainRankArray = nullptr;
NpScore::OrbisNpScoreRankDataA* aRankArray = nullptr;
NpScore::OrbisNpScorePlayerRankDataA* aPlayerRankArray = nullptr;
NpScore::OrbisNpScoreComment* commentArray = nullptr;
NpScore::OrbisNpScoreGameInfo* infoArray = nullptr;
NpScore::OrbisNpScoreBoardInfo* boardInfo = nullptr;
// GetGameData / GetGameDataByAccountId output buffers.
void* dataOut = nullptr;
u64 recvSize = 0;
u64* totalSizeOut = nullptr;
Libraries::Rtc::OrbisRtcTick* lastSortDate = nullptr;
u32* totalRecord = nullptr;
u64 arrayNum = 0;
};
mutable std::mutex m_mutex_pending_score;
std::map<u64, PendingScoreRequest> m_pending_score;
// Worker thread
std::atomic<bool> m_initialized{false};
std::atomic<bool> m_worker_running{false};
std::thread m_worker_thread;
// Users dropped by a network error and awaiting transparent reconnect
struct ReconnectState {
std::chrono::steady_clock::time_point next_attempt{};
std::chrono::milliseconds backoff{0};
};
std::unordered_map<s32, ReconnectState> m_reconnect;
mutable std::mutex m_mutex_pending_invites;
std::unordered_map<s32, std::vector<PendingInvitation>> m_pending_invites;
// State callbacks
struct CbEntry {
s32 handle;
StateCallback cb;
void* userdata;
};
mutable std::mutex m_mutex_cbs;
std::vector<CbEntry> m_state_cbs;
s32 m_next_handle{1};
// Friend state per user, seeded from LoginReply and updated by notifications/actions.
mutable std::mutex m_mutex_friend_state;
std::map<s32, FriendListSnapshot> m_friend_state;
};
// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <chrono>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include "common/types.h"
#include "core/libraries/np/np_manager.h"
#include "core/libraries/np/np_score/np_score.h"
#include "core/libraries/np/np_score/np_score_ctx.h"
#include "core/libraries/rtc/rtc.h"
#include "core/libraries/system/userservice.h"
#include "shadnet/client.h"
namespace Libraries::Np {
class NpHandler {
public:
static NpHandler& GetInstance();
NpHandler(const NpHandler&) = delete;
NpHandler& operator=(const NpHandler&) = delete;
// Connect every currently-logged-in user that has shadNet credentials.
void Initialize();
// Disconnect all clients, stop worker threads, fire SignedOut for each.
void Shutdown();
// Connect/disconnect a single user in response to a user service login/logout event.
void OnUserLoggedIn(s32 user_id);
void OnUserLoggedOut(s32 user_id);
// True if this specific user is authenticated to the shadNet server.
bool IsPsnSignedIn(s32 user_id) const;
/// True if any user is currently signed in
bool IsAnySignedIn() const;
// Set the Appear-Offline preference for all signed-in users (and future logins). While
// enabled, shadNet handles the user as offline for everyone else. Call from the UI/config.
void SetAppearOffline(bool enable);
bool IsAppearOffline() const {
return m_appear_offline.load();
}
/// Full NP ID for this user, built once from shadnet_npid after login.
OrbisNpId GetNpId(s32 user_id) const;
/// The Online ID embedded in the NP ID (npid.handle).
OrbisNpOnlineId GetOnlineId(s32 user_id) const;
// Avatar URL returned by the server.
std::string GetAvatarUrl(s32 user_id) const;
// 64-bit account ID assigned by the server.
OrbisNpAccountId GetAccountId(s32 user_id) const;
// WebAPI bearer token
std::string GetBearerToken(s32 user_id) const;
// Sends a session invitation via the WebAPI (POST /v1/sessions/<id>/invitations). Each 'to'
// entry is an online ID (npid) or a decimal account ID
bool SendSessionInvitation(s32 user_id, const std::string& session_id,
const std::vector<std::string>& to, const std::string& message);
// Raises ORBIS_SYSTEM_SERVICE_EVENT_SESSION_INVITATION (0x10000002) so titles that watch the
// system-service event learn about the invite.
void PostSessionInvitationEvent(const std::string& session_id, const std::string& invitation_id,
const std::string& accepter_online_id);
// A session invitation surfaced from a shadNet push, stashed until the user acts on it.
struct PendingInvitation {
std::string session_id;
std::string invitation_id;
std::string from_npid; // sender (for display)
std::string to_npid; // local recipient / accepter
int64_t valid_until = 0; // ms since epoch; 0 = never expires
};
// Pending invitations stashed for a local user (newest last), populated on arrival.
std::vector<PendingInvitation> GetPendingInvitations(s32 user_id) const;
// Consumes an invitation server-side (PUT usedFlag=true) and drops it from the stash. Returns
// true if the invite was found and the consume succeeded.
bool AcceptSessionInvitation(s32 user_id, const std::string& invitation_id);
// Dismisses an invitation locally by dropping it from the stash. No server call (a declined
// invite is left unconsumed and simply ages out server-side).
void DeclineSessionInvitation(s32 user_id, const std::string& invitation_id);
// Local IP address (network byte order) as seen at connect time.
u32 GetLocalIpAddr(s32 user_id) const;
// Reverse lookup: server account_id to local user_id.
// Returns -1 if no connected user owns that account_id.
s32 GetUserIdByAccountId(u64 account_id) const;
// Reverse lookup: OrbisNpOnlineId to local user_id.
// Scans m_np_ids for a matching handle.data string.
// Returns -1 if no connected user has that Online ID.
s32 GetUserIdByOnlineId(const OrbisNpOnlineId& online_id) const;
// Friend list
u32 GetNumFriends(s32 user_id) const;
std::optional<std::string> GetFriendNpid(s32 user_id, u32 index) const;
// ---- Friend list / requests / blocked (shadNet) ----
struct FriendInfo {
std::string npid;
bool online = false;
};
struct FriendListSnapshot {
std::vector<FriendInfo> friends;
std::vector<std::string> requests_received;
std::vector<std::string> requests_sent;
std::vector<std::string> blocked;
};
// Local users that currently have an authenticated shadNet session.
std::vector<s32> GetConnectedUsers() const;
// user's friend state for UI display.
FriendListSnapshot GetFriendList(s32 user_id) const;
// Friend/block actions
s32 SendFriendRequest(s32 user_id, const std::string& npid);
s32 RemoveFriend(s32 user_id, const std::string& npid);
s32 BlockUser(s32 user_id, const std::string& npid);
s32 UnblockUser(s32 user_id, const std::string& npid);
// Submit a RecordScore request to the shadNet server.
s32 RecordScore(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score,
const char* comment, size_t commentLen, const u8* gameInfoData,
size_t gameInfoSize, std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 RecordGameData(s32 user_id, s32 service_label, u32 boardId, s32 pcId, s64 score,
const u8* data, size_t size, std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetGameData(s32 user_id, s32 service_label, u32 boardId, const std::string& npId, s32 pcId,
void* dataOut, u64 recvSize, u64* totalSizeOut,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetGameDataByAccountId(s32 user_id, s32 service_label, u32 boardId, u64 accountId, s32 pcId,
void* dataOut, u64 recvSize, u64* totalSizeOut,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetBoardInfo(s32 user_id, s32 service_label, u32 boardId,
NpScore::OrbisNpScoreBoardInfo* boardInfo,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// Submit a GetRankingByNpId request to the shadNet server.
s32 GetRankingByNpId(s32 user_id, s32 service_label, u32 boardId,
const std::vector<std::string>& npIds, const std::vector<s32>& pcIds,
NpScore::OrbisNpScorePlayerRankData* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// Submit a GetRankingByRange request to the shadNet server.
s32 GetRankingByRange(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank,
u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// A-variant of GetRankingByRange.
s32 GetRankingByRangeA(s32 user_id, s32 service_label, u32 boardId, u32 startSerialRank,
u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
s32 GetRankingByAccountId(s32 user_id, s32 service_label, u32 boardId,
const std::vector<u64>& accountIds, const std::vector<s32>& pcIds,
NpScore::OrbisNpScorePlayerRankDataA* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// Submit a GetFriendsRanking request to the shadNet server.
s32 GetFriendsRanking(s32 user_id, s32 service_label, u32 boardId, bool includeSelf,
u32 arrayNum, NpScore::OrbisNpScoreRankData* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// A-variant of GetFriendsRanking
s32 GetFriendsRankingA(s32 user_id, s32 service_label, u32 boardId, bool includeSelf,
u32 arrayNum, NpScore::OrbisNpScoreRankDataA* rankArray,
NpScore::OrbisNpScoreComment* commentArray,
NpScore::OrbisNpScoreGameInfo* infoArray,
Libraries::Rtc::OrbisRtcTick* lastSortDate, u32* totalRecord,
std::shared_ptr<NpScore::ScoreRequestCtx> req);
// State callbacks
using StateCallback = std::function<void(Libraries::UserService::OrbisUserServiceUserId user_id,
NpManager::OrbisNpState state)>;
s32 RegisterStateCallback(StateCallback cb, void* userdata);
void UnregisterStateCallback(s32 handle);
private:
NpHandler() = default;
~NpHandler() = default;
/// Connect one user. Blocks until connected+authenticated or failed.
bool ConnectUser(s32 user_id, const std::string& host, u16 port, const std::string& npid,
const std::string& password, const std::string& token);
// Connect a single logged-in user by id (looks up credentials, parses server).
bool ConnectUserById(s32 user_id);
// Parse the configured shadNet server "host:port" (default port 31313).
std::pair<std::string, u16> ParseServerAddress() const;
// Start the health-monitor worker thread if not already running (idempotent).
void StartWorker();
// Disconnect and remove one user's client.
void DisconnectUser(s32 user_id);
void WorkerThread();
// Transparent reconnect after a network drop
void MarkForReconnect(s32 user_id);
void TryReconnect();
void FireStateCallback(s32 user_id, NpManager::OrbisNpState state);
// Notification forwarders wired into each client
void OnFriendQuery(s32 user_id, const ShadNet::NotifyFriendQuery& n);
void OnFriendNew(s32 user_id, const ShadNet::NotifyFriendNew& n);
void OnFriendLost(s32 user_id, const ShadNet::NotifyFriendLost& n);
void OnFriendStatus(s32 user_id, const ShadNet::NotifyFriendStatus& n);
void OnWebApiPushEvent(s32 user_id, const ShadNet::NotifyWebApiPushEvent& n);
// The server refused a service (matchmaking full, not allowed, disabled): tell the player.
void OnServiceUnavailable(s32 user_id, const ShadNet::NotifyServiceUnavailable& n);
// The server signed this client out (e.g. version not accepted): tell the player and stay
// offline for this run instead of reconnecting into the same refusal.
void OnClientRejected(s32 user_id, const ShadNet::NotifyClientRejected& n);
void OnLoginResult(s32 user_id, const ShadNet::LoginResult& res);
// General async reply dispatch. Routes a reply to the owning subsystem by
// command. Called from the per-user ShadNetClient on the reader thread.
void OnAsyncReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);
void OnScoreReply(s32 user_id, ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);
// 12-byte NP Communication ID
std::string GetNpCommId(s32 service_label) const;
// Appear-Offline preference, applied to every client at login and on change.
std::atomic<bool> m_appear_offline{false};
// Per-user client map
mutable std::mutex m_mutex_clients;
std::map<s32, std::shared_ptr<ShadNet::ShadNetClient>> m_clients;
// Per-user NP ID built once from shadnet_npid after login.
std::map<s32, OrbisNpId> m_np_ids;
// Score requests awaiting a reply, keyed by the submit packet id.
struct PendingScoreRequest {
std::shared_ptr<NpScore::ScoreRequestCtx> req;
ShadNet::CommandType cmd;
std::vector<std::string> requestedNpIds;
NpScore::OrbisNpScorePlayerRankData* rankArray = nullptr;
NpScore::OrbisNpScoreRankData* plainRankArray = nullptr;
NpScore::OrbisNpScoreRankDataA* aRankArray = nullptr;
NpScore::OrbisNpScorePlayerRankDataA* aPlayerRankArray = nullptr;
NpScore::OrbisNpScoreComment* commentArray = nullptr;
NpScore::OrbisNpScoreGameInfo* infoArray = nullptr;
NpScore::OrbisNpScoreBoardInfo* boardInfo = nullptr;
// GetGameData / GetGameDataByAccountId output buffers.
void* dataOut = nullptr;
u64 recvSize = 0;
u64* totalSizeOut = nullptr;
Libraries::Rtc::OrbisRtcTick* lastSortDate = nullptr;
u32* totalRecord = nullptr;
u64 arrayNum = 0;
};
mutable std::mutex m_mutex_pending_score;
std::map<u64, PendingScoreRequest> m_pending_score;
// Worker thread
std::atomic<bool> m_initialized{false};
std::atomic<bool> m_worker_running{false};
std::thread m_worker_thread;
// Users dropped by a network error and awaiting transparent reconnect
struct ReconnectState {
std::chrono::steady_clock::time_point next_attempt{};
std::chrono::milliseconds backoff{0};
};
std::unordered_map<s32, ReconnectState> m_reconnect;
mutable std::mutex m_mutex_pending_invites;
std::unordered_map<s32, std::vector<PendingInvitation>> m_pending_invites;
// State callbacks
struct CbEntry {
s32 handle;
StateCallback cb;
void* userdata;
};
mutable std::mutex m_mutex_cbs;
std::vector<CbEntry> m_state_cbs;
s32 m_next_handle{1};
// Friend state per user, seeded from LoginReply and updated by notifications/actions.
mutable std::mutex m_mutex_friend_state;
std::map<s32, FriendListSnapshot> m_friend_state;
};
} // namespace Libraries::Np
File diff suppressed because it is too large. Load diff
@@ -1,94 +1,98 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <string_view>
#include <vector>
#include "common/types.h"
#include "core/libraries/np/np_matching2/np_matching2.h"
namespace ShadNet {
class ShadNetClient;
enum class CommandType : u16;
enum class ErrorType : u8;
} // namespace ShadNet
namespace Libraries::Np::NpMatching2 {
enum class MmCommand : u16 {
ContextStart = 100,
CreateRoom = 101,
JoinRoom = 102,
LeaveRoom = 103,
SearchRoom = 104,
RequestSignalingInfos = 105,
ContextStop = 106,
SetUserInfo = 107,
SetRoomDataInternal = 108,
SetRoomDataExternal = 109,
KickoutRoomMember = 110,
GetWorldInfoList = 111,
GetRoomDataExternalList = 112,
GetUserInfoList = 113,
GetRoomMemberDataExternalList = 114,
SendRoomMessage = 115,
};
void SetMmShadNetClient(std::shared_ptr<ShadNet::ShadNetClient> client,
std::string_view server_host, u16 tcp_port);
void ClearMmShadNetClient();
bool IsMmClientRunning();
void OnMatchingReply(ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);
void MmContextStart(OrbisNpMatching2ContextId ctx_id);
s32 MmContextStop(OrbisNpMatching2ContextId ctx_id);
s32 MmSubmitRequest(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
OrbisNpMatching2Event req_event, MmCommand cmd, const std::vector<u8>& payload,
bool a_variant = false);
s32 MmCreateJoinRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2CreateJoinRoomRequest& request);
s32 MmCreateJoinRoomA(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2CreateJoinRoomRequestA& request);
s32 MmJoinRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2JoinRoomRequest& request);
s32 MmJoinRoomA(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2JoinRoomRequestA& request);
s32 MmLeaveRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2LeaveRoomRequest& request);
s32 MmGetWorldInfoList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetWorldInfoListRequest& request);
s32 MmSearchRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SearchRoomRequest& request, bool a_variant = false);
s32 MmGetRoomDataExternalList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetRoomDataExternalListRequest& request,
bool a_variant = false);
s32 MmGetRoomMemberDataExternalList(
OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetRoomMemberDataExternalListRequest& request, bool a_variant = false);
s32 MmGetUserInfoList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetUserInfoListRequest& request,
bool a_variant = false);
s32 MmSetUserInfo(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetUserInfoRequest& request);
s32 MmSendRoomMessage(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SendRoomMessageRequest& request);
s32 MmSetRoomDataInternal(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetRoomDataInternalRequest& request);
s32 MmSetRoomDataExternal(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetRoomDataExternalRequest& request);
s32 MmKickoutRoomMember(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2KickoutRoomMemberRequest& request);
u32 GetMmServerAddr();
u16 GetMmServerUdpPort();
bool RequestSignalingInfos(std::string_view target_online_id, u32* out_addr, u16* out_port);
} // namespace Libraries::Np::NpMatching2
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <string_view>
#include <vector>
#include "common/types.h"
#include "core/libraries/np/np_matching2/np_matching2.h"
namespace ShadNet {
class ShadNetClient;
enum class CommandType : u16;
enum class ErrorType : u8;
} // namespace ShadNet
namespace Libraries::Np::NpMatching2 {
enum class MmCommand : u16 {
ContextStart = 100,
CreateRoom = 101,
JoinRoom = 102,
LeaveRoom = 103,
SearchRoom = 104,
RequestSignalingInfos = 105,
ContextStop = 106,
SetUserInfo = 107,
SetRoomDataInternal = 108,
SetRoomDataExternal = 109,
KickoutRoomMember = 110,
GetWorldInfoList = 111,
GetRoomDataExternalList = 112,
GetUserInfoList = 113,
GetRoomMemberDataExternalList = 114,
SendRoomMessage = 115,
};
void SetMmShadNetClient(std::shared_ptr<ShadNet::ShadNetClient> client,
std::string_view server_host, u16 tcp_port);
void ClearMmShadNetClient();
bool IsMmClientRunning();
void OnMatchingReply(ShadNet::CommandType cmd, u64 pkt_id, ShadNet::ErrorType error,
const std::vector<u8>& body);
void MmContextStart(OrbisNpMatching2ContextId ctx_id);
s32 MmContextStop(OrbisNpMatching2ContextId ctx_id);
s32 MmSubmitRequest(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
OrbisNpMatching2Event req_event, MmCommand cmd, const std::vector<u8>& payload,
bool a_variant = false);
s32 MmCreateJoinRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2CreateJoinRoomRequest& request);
s32 MmCreateJoinRoomA(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2CreateJoinRoomRequestA& request);
s32 MmJoinRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2JoinRoomRequest& request);
s32 MmJoinRoomA(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2JoinRoomRequestA& request);
s32 MmLeaveRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2LeaveRoomRequest& request);
s32 MmGetWorldInfoList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetWorldInfoListRequest& request);
s32 MmSearchRoom(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SearchRoomRequest& request, bool a_variant = false);
s32 MmGetRoomDataExternalList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetRoomDataExternalListRequest& request,
bool a_variant = false);
s32 MmGetRoomMemberDataExternalList(
OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetRoomMemberDataExternalListRequest& request, bool a_variant = false);
s32 MmGetUserInfoList(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2GetUserInfoListRequest& request,
bool a_variant = false);
s32 MmSetUserInfo(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetUserInfoRequest& request);
s32 MmSendRoomMessage(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SendRoomMessageRequest& request);
s32 MmSetRoomDataInternal(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetRoomDataInternalRequest& request);
s32 MmSetRoomDataExternal(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2SetRoomDataExternalRequest& request);
s32 MmKickoutRoomMember(OrbisNpMatching2ContextId ctx_id, OrbisNpMatching2RequestId req_id,
const OrbisNpMatching2KickoutRoomMemberRequest& request);
u32 GetMmServerAddr();
u16 GetMmServerUdpPort();
bool RequestSignalingInfos(std::string_view target_online_id, u32* out_addr, u16* out_port);
// BBSC experimental re-insertion: re-delivers member-joined (optionally member-left first) and
// signaling-established callbacks for every other member of the current room. Returns the count.
int BbscReplayRoomMembers(bool announce_left);
} // namespace Libraries::Np::NpMatching2
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// BBSC packets on the np_signaling socket that the Bloodborne code also builds and reads. Kept out
// of np_signaling_state.h, which pulls in the platform headers (their `near`/`far` macros break
// bloodborne_re.cpp).
#include <cstddef>
#include "common/types.h"
namespace Libraries::Np::NpSignaling {
inline constexpr u8 kSignalingBbscWorldFlagsType = 0x22;
// 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.
// ~1 KB, below the 1280-byte tunnel MTU (Tailscale) with the UDP and v-port headers.
inline constexpr size_t kBbscWorldFlagGroupBytes = 125;
inline constexpr size_t kBbscWorldFlagGroupsPerChunk = 8;
#pragma pack(push, 1)
struct SignalingBbscWorldFlags {
u8 magic[4] = {'S', 'H', 'A', 'D'};
u8 type = kSignalingBbscWorldFlagsType;
u8 version = 1;
u32 packed_map = 0;
u32 snapshot = 0; // the host's snapshot counter: chunks of one snapshot belong together
u8 chunk = 0; // index of this chunk
u8 chunk_count = 0; // chunks in the snapshot
u8 present = 0; // bit n: group n of this chunk exists in the host's flag table
u8 group_count = 0; // groups carried (the last chunk may carry fewer than 8)
u8 groups[kBbscWorldFlagGroupsPerChunk][kBbscWorldFlagGroupBytes]{};
};
static_assert(sizeof(SignalingBbscWorldFlags) == 18 + 8 * 125);
#pragma pack(pop)
} // namespace Libraries::Np::NpSignaling
File diff suppressed because it is too large. Load diff
@@ -1,34 +1,43 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
namespace Libraries::Np::NpSignaling::Helpers {
struct SignalingRuntimeHooks {
void (*start_dispatch)(s32 priority, u64 affinity_mask, u64 stack_size);
void (*start_receive)(s32 priority, u64 affinity_mask, u64 stack_size);
void (*start_ping)(s32 priority, u64 affinity_mask, u64 stack_size);
void (*stop_ping)();
void (*stop_receive)();
void (*stop_dispatch)();
};
void SetRuntimeHooks(const SignalingRuntimeHooks& hooks);
s32 CheckInitializeAppType(u32* is_app_type_4);
s32 InitSignalingHeap(s64 pool_size);
void ShutdownSignalingHeap();
s32 CheckAppType();
s32 StartMainRuntime(s32 thread_priority, s32 cpu_affinity_mask, s64 thread_stack_size);
s32 StartEchoRuntime(s32 thread_priority, s32 cpu_affinity_mask);
void ShutdownRuntime();
} // namespace Libraries::Np::NpSignaling::Helpers
namespace Libraries::Np::NpSignaling {
void RegisterRuntimeHooks();
}
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
namespace Libraries::Np::NpSignaling::Helpers {
struct SignalingRuntimeHooks {
void (*start_dispatch)(s32 priority, u64 affinity_mask, u64 stack_size);
void (*start_receive)(s32 priority, u64 affinity_mask, u64 stack_size);
void (*start_ping)(s32 priority, u64 affinity_mask, u64 stack_size);
void (*stop_ping)();
void (*stop_receive)();
void (*stop_dispatch)();
};
void SetRuntimeHooks(const SignalingRuntimeHooks& hooks);
s32 CheckInitializeAppType(u32* is_app_type_4);
s32 InitSignalingHeap(s64 pool_size);
void ShutdownSignalingHeap();
s32 CheckAppType();
s32 StartMainRuntime(s32 thread_priority, s32 cpu_affinity_mask, s64 thread_stack_size);
s32 StartEchoRuntime(s32 thread_priority, s32 cpu_affinity_mask);
void ShutdownRuntime();
// BBSC: send one "the host is in this map" packet to every established signaling peer. Called
// from the game thread; takes the signaling mutex only long enough to copy the peer addresses.
// Returns how many peers it went to.
u32 SendBbscFollowToPeers(u32 packed_map, float x, float y, float z, float heading, u32 sequence,
s32 lamp_point);
// BBSC: send one world-flag chunk (SignalingBbscWorldFlags, filled in by the caller) to every
// established signaling peer. Same threading as SendBbscFollowToPeers.
u32 SendBbscWorldFlagsToPeers(const void* packet, u32 size);
} // namespace Libraries::Np::NpSignaling::Helpers
namespace Libraries::Np::NpSignaling {
void RegisterRuntimeHooks();
}
File diff suppressed because it is too large. Load diff
@@ -1,357 +1,399 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <map>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "common/types.h"
#include "core/libraries/kernel/threads.h"
#include "core/libraries/np/np_common.h"
#include "core/libraries/np/np_signaling/np_signaling.h"
#include "core/libraries/np/np_types.h"
namespace Libraries::Np::NpSignaling {
extern NpCommon::OrbisNpCalloutContext g_callout_ctx;
extern bool g_callout_ctx_active;
constexpr s64 kActivateCooldownUs = 6'000'000;
constexpr s64 kActivateBudgetMaxUs = 600'000'000;
constexpr u32 kSigRetryMs = 500;
constexpr u32 kSigPingMs = 5'000;
#pragma pack(push, 1)
struct StunPing {
u8 cmd = 0x01;
u8 online_id[16]{};
u32 local_ip = 0;
};
static_assert(sizeof(StunPing) == 21, "StunPing must be exactly 21 bytes");
#pragma pack(pop)
#pragma pack(push, 1)
struct StunEcho {
u32 ext_ip = 0;
u16 ext_port = 0;
};
static_assert(sizeof(StunEcho) == 6, "StunEcho must be exactly 6 bytes");
#pragma pack(pop)
inline constexpr u8 kSignalingMagic[4] = {'S', 'H', 'A', 'D'};
enum class SignalingPacketType : u8 {
EchoPing = 0x05,
EchoPong = 0x06,
Handshake = 0x07,
Control = 0x10,
};
#pragma pack(push, 1)
struct SignalingEchoPing {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::EchoPing);
u32 conn_id = 0;
u64 send_ts_us = 0;
};
static_assert(sizeof(SignalingEchoPing) == 17);
struct SignalingEchoPong {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::EchoPong);
u32 conn_id = 0;
u64 orig_ts_us = 0;
};
static_assert(sizeof(SignalingEchoPong) == 17);
#pragma pack(pop)
#pragma pack(push, 1)
struct SignalingHandshake {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::Handshake);
u8 kind = 0;
u8 online_id_from[16]{};
u8 online_id_to[16]{};
u32 from_conn_id = 0;
u32 to_conn_id = 0;
u32 mapped_addr = 0;
u16 mapped_port = 0;
u16 reserved = 0;
u64 nonce = 0;
};
static_assert(sizeof(SignalingHandshake) == 0x3e);
#pragma pack(pop)
enum class HandshakeKind : u8 {
Offer = 1,
Accept = 2,
Check = 3,
CheckAck = 4,
};
#pragma pack(push, 1)
struct SignalingControl {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::Control);
u8 kind = 0;
u8 online_id_from[16]{};
u8 online_id_to[16]{};
u32 from_conn_id = 0;
u32 mapped_addr = 0;
u16 mapped_port = 0;
u16 reason = 0;
};
static_assert(sizeof(SignalingControl) == 0x32);
#pragma pack(pop)
enum class ControlKind : u8 {
ActivationRequest = 1,
ActivationAck = 2,
Close = 3,
Established = 4,
};
enum class ControlReason : u16 {
Terminate = 0,
Deactivate = 1,
};
enum class ConnState : s32 {
Inactive = 0,
SendingOffer = 5,
WaitOffer = 6,
ConnCheck = 7,
SendingAccept = 8,
WaitAccept = 9,
Established = 10,
};
struct ConnectionInfo {
OrbisNpSignalingConnectionId conn_id = 0;
OrbisNpSignalingContextId ctx_id = 0;
u32 addr = 0;
u16 port = 0;
u32 local_addr = 0;
u16 local_port = 0;
s32 status = ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE;
OrbisNpId npid{};
OrbisNpOnlineId online_id{};
s32 activation_mode = 0;
static constexpr u32 kProbeSampleCount = 6;
s32 probe_rtt_samples[kProbeSampleCount] = {-1, -1, -1, -1, -1, -1};
u32 probe_sample_write_index = 0;
s64 last_echo_ping_us = 0;
s64 last_peer_rx_us = 0;
ConnState state = ConnState::Inactive;
bool established_fired = false;
bool established_event_fired = false;
bool peer_activated = false;
bool locally_activated = false;
bool peer_activated_fired = false;
bool peer_established = false;
bool mutual_fired = false;
bool dead_fired = false;
bool is_initiator = false;
s64 last_handshake_send_us = 0;
NpCommon::OrbisNpCalloutEntry timeout_callout{};
bool timeout_callout_armed = false;
NpCommon::OrbisNpCalloutEntry step_callout{};
bool step_callout_armed = false;
bool deactivate_lingering = false;
u32 echo_derived_value = 0;
};
struct NpSignalingContext {
OrbisNpSignalingHandler callback{nullptr};
void* callback_arg{nullptr};
bool active{false};
u32 flags{0};
u32 compiled_sdk_version{0};
u64 account_id{0};
u32 platform_type{0};
OrbisNpId owner_npid{};
OrbisNpOnlineId owner_online_id{};
u16 bound_port{0};
std::atomic<u32> ext_addr{0};
std::atomic<u16> ext_port{0};
std::mutex stun_mutex{};
std::condition_variable stun_cv{};
s64 activate_budget_us{0};
s64 activate_last_update_us{0};
};
struct CtxNpIdKey {
OrbisNpSignalingContextId ctx_id = 0;
OrbisNpId npid{};
};
struct CtxNpIdKeyHash {
size_t operator()(const CtxNpIdKey& key) const noexcept {
size_t hash = static_cast<size_t>(key.ctx_id);
const auto* bytes = reinterpret_cast<const u8*>(&key.npid);
for (size_t i = 0; i < sizeof(key.npid); ++i) {
hash ^= static_cast<size_t>(bytes[i]) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
}
return hash;
}
};
inline bool operator==(const CtxNpIdKey& lhs, const CtxNpIdKey& rhs) {
return lhs.ctx_id == rhs.ctx_id && std::memcmp(&lhs.npid, &rhs.npid, sizeof(lhs.npid)) == 0;
}
struct QueuedDispatch {
std::chrono::steady_clock::time_point fire_at{};
u32 delay_ms = 0;
OrbisNpSignalingContextId ctx_id = 0;
OrbisNpSignalingConnectionId conn_id = 0;
u32 event_type = 0;
u32 error_code = 0;
OrbisNpSignalingHandler callback = nullptr;
void* callback_arg = nullptr;
};
constexpr u32 kPeerNetInfoRequestIdPrefix = 0x21000000;
struct PeerNetInfoResult {
OrbisNpSignalingContextId ctx_id = 0;
OrbisNpSignalingConnectionId conn_id = 0;
u32 local_ipv4 = 0;
u32 external_ipv4 = 0;
u32 nat_route_kind = 0;
};
extern std::unordered_map<s32, NpSignalingContext> g_contexts;
extern std::unordered_map<s32, ConnectionInfo> g_connections;
extern std::unordered_map<CtxNpIdKey, s32, CtxNpIdKeyHash> g_npid_to_conn;
extern std::unordered_map<s32, PeerNetInfoResult> g_peer_netinfo_results;
struct PendingActivation {
OrbisNpSignalingConnectionId conn_id = 0;
std::string peer_online_id;
bool start_handshake = true;
};
extern std::vector<PendingActivation> g_pending_activations;
extern u32 g_peer_netinfo_next_id;
extern u32 g_peak_connection_count;
extern u32 g_last_assigned_context_id;
extern u32 g_connection_id_seed;
extern bool g_initialized;
extern Libraries::Kernel::PthreadMutexT g_mutex_storage;
struct SignalingMutexGuard {
SignalingMutexGuard();
~SignalingMutexGuard();
SignalingMutexGuard(const SignalingMutexGuard&) = delete;
SignalingMutexGuard& operator=(const SignalingMutexGuard&) = delete;
};
void InitSignalingMutex();
void DestroySignalingMutex();
extern std::multimap<std::chrono::steady_clock::time_point, QueuedDispatch> g_dispatch_queue;
extern std::mutex g_dispatch_mutex;
extern std::condition_variable g_dispatch_cv;
extern bool g_dispatch_stop;
long long NowMs();
s64 NowUs();
std::string OnlineIdToString(const OrbisNpOnlineId& online_id_in);
std::string OnlineIdFromNpId(const OrbisNpId& np_id);
OrbisNpId NpIdFromOnlineId(const OrbisNpOnlineId& online_id);
bool OnlineIdEqualsString(const OrbisNpOnlineId& online_id, std::string_view value);
bool IsValidNpId(const OrbisNpId& np_id);
s32 NormalizeNpId(const void* np_id, OrbisNpId* out_npid, OrbisNpOnlineId* out_online_id = nullptr);
CtxNpIdKey MakeCtxNpIdKey(OrbisNpSignalingContextId ctx_id, const OrbisNpId& npid);
bool IsContextValidLocked(OrbisNpSignalingContextId ctx_id);
OrbisNpSignalingContextId AllocateContextIdLocked();
OrbisNpSignalingConnectionId AllocateConnectionIdLocked();
void RemoveConnectionLocked(OrbisNpSignalingConnectionId conn_id);
void RemoveContextConnectionsLocked(OrbisNpSignalingContextId ctx_id);
s32 ConnectionStateFromStatus(s32 status);
u32 CaptureCompiledSdkVersion();
u32 ParseIpv4Nbo(const std::string& dotted);
s32 GetConnectionInfoInternal(OrbisNpSignalingContextId ctx_id,
OrbisNpSignalingConnectionId conn_id, s32 info_code, void* out_a,
void* out_b);
bool ConsumeActivationBudgetGatedLocked(NpSignalingContext& ctx);
void SnapshotConnectionStatisticsLocked(u32* out_peak, u32* out_active, u32* out_transient,
u32* out_established);
OrbisNpSignalingRequestId StagePeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id);
bool TakePeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id, s32 req_or_conn_id,
PeerNetInfoResult* out);
bool DropPeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id, s32 req_or_conn_id);
void RecordRttSampleLocked(OrbisNpSignalingConnectionId conn_id, s32 rtt_us);
void SendEchoPings();
void DispatchConnectionEvent(OrbisNpSignalingConnectionId conn_id, s32 event_type, s32 event_data);
void DispatchPeerActivatedEvent(OrbisNpSignalingConnectionId conn_id);
void CloseConnectionAndDispatchDead(OrbisNpSignalingConnectionId conn_id, s32 error_code);
void EstablishConnection(OrbisNpSignalingConnectionId conn_id, bool peer_activated_hint);
void DeactivateConnectionFaithful(OrbisNpSignalingConnectionId conn_id);
void TerminateConnectionFaithful(OrbisNpSignalingConnectionId conn_id);
void StartHandshakeInitiator(OrbisNpSignalingConnectionId conn_id);
void QueueActivationLocked(OrbisNpSignalingConnectionId conn_id, std::string_view peer_online_id,
bool start_handshake = true);
void ProcessPendingActivations();
void HandleHandshakePacket(u32 from_addr, u16 from_port, const SignalingHandshake& pkt);
void ArmConnectTimeoutLocked(OrbisNpSignalingConnectionId conn_id);
void ClearLingerAndTimeoutLocked(OrbisNpSignalingConnectionId conn_id);
void SendActivationRequestLocked(const ConnectionInfo& ci);
void SendControlCloseLocked(const ConnectionInfo& ci, ControlReason reason);
void HandleControlPacket(u32 from_addr, u16 from_port, const SignalingControl& pkt);
const char* SignalingEventName(u32 event_type);
bool ConsumeActivationBudgetLocked(NpSignalingContext& ctx);
void SendStunPing(OrbisNpSignalingContextId ctx_id);
void ClearActiveDeadlines();
void ClearConnectionDeadlineForPeer(std::string_view npid);
void SetRoomLeft();
} // namespace Libraries::Np::NpSignaling
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <map>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "common/types.h"
#include "core/libraries/kernel/threads.h"
#include "core/libraries/np/np_common.h"
#include "core/libraries/np/np_signaling/np_signaling.h"
#include "core/libraries/np/np_signaling/np_signaling_bbsc.h"
#include "core/libraries/np/np_types.h"
namespace Libraries::Np::NpSignaling {
extern NpCommon::OrbisNpCalloutContext g_callout_ctx;
extern bool g_callout_ctx_active;
constexpr s64 kActivateCooldownUs = 6'000'000;
constexpr s64 kActivateBudgetMaxUs = 600'000'000;
constexpr u32 kSigRetryMs = 500;
constexpr u32 kSigPingMs = 5'000;
#pragma pack(push, 1)
struct StunPing {
u8 cmd = 0x01;
u8 online_id[16]{};
u32 local_ip = 0;
};
static_assert(sizeof(StunPing) == 21, "StunPing must be exactly 21 bytes");
#pragma pack(pop)
#pragma pack(push, 1)
struct StunEcho {
u32 ext_ip = 0;
u16 ext_port = 0;
};
static_assert(sizeof(StunEcho) == 6, "StunEcho must be exactly 6 bytes");
#pragma pack(pop)
inline constexpr u8 kSignalingMagic[4] = {'S', 'H', 'A', 'D'};
enum class SignalingPacketType : u8 {
EchoPing = 0x05,
EchoPong = 0x06,
Handshake = 0x07,
Control = 0x10,
// BBSC, not part of the PS4 protocol: the host tells its co-op partners which map it is in,
// so a guest can follow it through a lamp warp. Bloodborne itself never carries this - when a
// host changes stage the game simply ends the session - so there is nothing to piggyback on.
// Only BBSC builds send it, and any other build drops it as an unknown packet.
BbscFollow = 0x20,
// BBSC: the host's event flags for the map block it is in, so a guest following it there loads
// the host's world state (bosses, doors, shortcuts) instead of whatever its own flags say.
// 0x21 is taken (Matching2 frames, see P2PTransport::ClassifyInternal).
BbscWorldFlags = 0x22,
};
static_assert(static_cast<u8>(SignalingPacketType::BbscWorldFlags) == kSignalingBbscWorldFlagsType);
#pragma pack(push, 1)
struct SignalingEchoPing {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::EchoPing);
u32 conn_id = 0;
u64 send_ts_us = 0;
};
static_assert(sizeof(SignalingEchoPing) == 17);
struct SignalingEchoPong {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::EchoPong);
u32 conn_id = 0;
u64 orig_ts_us = 0;
};
static_assert(sizeof(SignalingEchoPong) == 17);
#pragma pack(pop)
#pragma pack(push, 1)
struct SignalingHandshake {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::Handshake);
u8 kind = 0;
u8 online_id_from[16]{};
u8 online_id_to[16]{};
u32 from_conn_id = 0;
u32 to_conn_id = 0;
u32 mapped_addr = 0;
u16 mapped_port = 0;
u16 reserved = 0;
u64 nonce = 0;
};
static_assert(sizeof(SignalingHandshake) == 0x3e);
#pragma pack(pop)
enum class HandshakeKind : u8 {
Offer = 1,
Accept = 2,
Check = 3,
CheckAck = 4,
};
#pragma pack(push, 1)
struct SignalingControl {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::Control);
u8 kind = 0;
u8 online_id_from[16]{};
u8 online_id_to[16]{};
u32 from_conn_id = 0;
u32 mapped_addr = 0;
u16 mapped_port = 0;
u16 reason = 0;
};
static_assert(sizeof(SignalingControl) == 0x32);
#pragma pack(pop)
#pragma pack(push, 1)
struct SignalingBbscFollow {
u8 magic[4] = {kSignalingMagic[0], kSignalingMagic[1], kSignalingMagic[2], kSignalingMagic[3]};
u8 type = static_cast<u8>(SignalingPacketType::BbscFollow);
u8 kind = 0; // 0: "I am the host, this is where I am"
u32 packed_map = 0;
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float heading = 0.0f;
u32 sequence = 0;
// The host's last lamp respawn point (global+0x1528), -1 if none. Added 2026-09-24: a dead
// guest respawns at the host's lamp. Older builds sent the 0x1E-byte packet without it.
s32 lamp_point = -1;
};
static_assert(sizeof(SignalingBbscFollow) == 0x22);
inline constexpr size_t kSignalingBbscFollowV1Size = 0x1E;
#pragma pack(pop)
enum class ControlKind : u8 {
ActivationRequest = 1,
ActivationAck = 2,
Close = 3,
Established = 4,
};
enum class ControlReason : u16 {
Terminate = 0,
Deactivate = 1,
};
enum class ConnState : s32 {
Inactive = 0,
SendingOffer = 5,
WaitOffer = 6,
ConnCheck = 7,
SendingAccept = 8,
WaitAccept = 9,
Established = 10,
};
struct ConnectionInfo {
OrbisNpSignalingConnectionId conn_id = 0;
OrbisNpSignalingContextId ctx_id = 0;
u32 addr = 0;
u16 port = 0;
u32 local_addr = 0;
u16 local_port = 0;
s32 status = ORBIS_NP_SIGNALING_CONN_STATUS_INACTIVE;
OrbisNpId npid{};
OrbisNpOnlineId online_id{};
s32 activation_mode = 0;
static constexpr u32 kProbeSampleCount = 6;
s32 probe_rtt_samples[kProbeSampleCount] = {-1, -1, -1, -1, -1, -1};
u32 probe_sample_write_index = 0;
s64 last_echo_ping_us = 0;
s64 last_peer_rx_us = 0;
ConnState state = ConnState::Inactive;
bool established_fired = false;
bool established_event_fired = false;
bool peer_activated = false;
bool locally_activated = false;
bool peer_activated_fired = false;
bool peer_established = false;
bool mutual_fired = false;
bool dead_fired = false;
bool is_initiator = false;
s64 last_handshake_send_us = 0;
NpCommon::OrbisNpCalloutEntry timeout_callout{};
bool timeout_callout_armed = false;
NpCommon::OrbisNpCalloutEntry step_callout{};
bool step_callout_armed = false;
bool deactivate_lingering = false;
// BBSC rejoin fix (2026-10-12): the peer's connection id from its handshake packets - a new
// one on an established connection means the peer started over (left and came back).
u32 peer_conn_id = 0;
// Re-activated after a deactivate: established again only when the peer answers a Check
// (10 s, else DEAD) - a peer that had left must not keep the connection "active" forever.
bool revalidating = false;
s64 revalidate_since_us = 0;
u32 echo_derived_value = 0;
};
struct NpSignalingContext {
OrbisNpSignalingHandler callback{nullptr};
void* callback_arg{nullptr};
bool active{false};
u32 flags{0};
u32 compiled_sdk_version{0};
u64 account_id{0};
u32 platform_type{0};
OrbisNpId owner_npid{};
OrbisNpOnlineId owner_online_id{};
u16 bound_port{0};
std::atomic<u32> ext_addr{0};
std::atomic<u16> ext_port{0};
std::mutex stun_mutex{};
std::condition_variable stun_cv{};
s64 activate_budget_us{0};
s64 activate_last_update_us{0};
};
struct CtxNpIdKey {
OrbisNpSignalingContextId ctx_id = 0;
OrbisNpId npid{};
};
struct CtxNpIdKeyHash {
size_t operator()(const CtxNpIdKey& key) const noexcept {
size_t hash = static_cast<size_t>(key.ctx_id);
const auto* bytes = reinterpret_cast<const u8*>(&key.npid);
for (size_t i = 0; i < sizeof(key.npid); ++i) {
hash ^= static_cast<size_t>(bytes[i]) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
}
return hash;
}
};
inline bool operator==(const CtxNpIdKey& lhs, const CtxNpIdKey& rhs) {
return lhs.ctx_id == rhs.ctx_id && std::memcmp(&lhs.npid, &rhs.npid, sizeof(lhs.npid)) == 0;
}
struct QueuedDispatch {
std::chrono::steady_clock::time_point fire_at{};
u32 delay_ms = 0;
OrbisNpSignalingContextId ctx_id = 0;
OrbisNpSignalingConnectionId conn_id = 0;
u32 event_type = 0;
u32 error_code = 0;
OrbisNpSignalingHandler callback = nullptr;
void* callback_arg = nullptr;
};
constexpr u32 kPeerNetInfoRequestIdPrefix = 0x21000000;
struct PeerNetInfoResult {
OrbisNpSignalingContextId ctx_id = 0;
OrbisNpSignalingConnectionId conn_id = 0;
u32 local_ipv4 = 0;
u32 external_ipv4 = 0;
u32 nat_route_kind = 0;
};
extern std::unordered_map<s32, NpSignalingContext> g_contexts;
extern std::unordered_map<s32, ConnectionInfo> g_connections;
extern std::unordered_map<CtxNpIdKey, s32, CtxNpIdKeyHash> g_npid_to_conn;
extern std::unordered_map<s32, PeerNetInfoResult> g_peer_netinfo_results;
struct PendingActivation {
OrbisNpSignalingConnectionId conn_id = 0;
std::string peer_online_id;
bool start_handshake = true;
};
extern std::vector<PendingActivation> g_pending_activations;
extern u32 g_peer_netinfo_next_id;
extern u32 g_peak_connection_count;
extern u32 g_last_assigned_context_id;
extern u32 g_connection_id_seed;
extern bool g_initialized;
extern Libraries::Kernel::PthreadMutexT g_mutex_storage;
struct SignalingMutexGuard {
SignalingMutexGuard();
~SignalingMutexGuard();
SignalingMutexGuard(const SignalingMutexGuard&) = delete;
SignalingMutexGuard& operator=(const SignalingMutexGuard&) = delete;
};
void InitSignalingMutex();
void DestroySignalingMutex();
extern std::multimap<std::chrono::steady_clock::time_point, QueuedDispatch> g_dispatch_queue;
extern std::mutex g_dispatch_mutex;
extern std::condition_variable g_dispatch_cv;
extern bool g_dispatch_stop;
long long NowMs();
s64 NowUs();
std::string OnlineIdToString(const OrbisNpOnlineId& online_id_in);
std::string OnlineIdFromNpId(const OrbisNpId& np_id);
OrbisNpId NpIdFromOnlineId(const OrbisNpOnlineId& online_id);
bool OnlineIdEqualsString(const OrbisNpOnlineId& online_id, std::string_view value);
bool IsValidNpId(const OrbisNpId& np_id);
s32 NormalizeNpId(const void* np_id, OrbisNpId* out_npid, OrbisNpOnlineId* out_online_id = nullptr);
CtxNpIdKey MakeCtxNpIdKey(OrbisNpSignalingContextId ctx_id, const OrbisNpId& npid);
bool IsContextValidLocked(OrbisNpSignalingContextId ctx_id);
OrbisNpSignalingContextId AllocateContextIdLocked();
OrbisNpSignalingConnectionId AllocateConnectionIdLocked();
void RemoveConnectionLocked(OrbisNpSignalingConnectionId conn_id);
void RemoveContextConnectionsLocked(OrbisNpSignalingContextId ctx_id);
s32 ConnectionStateFromStatus(s32 status);
u32 CaptureCompiledSdkVersion();
u32 ParseIpv4Nbo(const std::string& dotted);
s32 GetConnectionInfoInternal(OrbisNpSignalingContextId ctx_id,
OrbisNpSignalingConnectionId conn_id, s32 info_code, void* out_a,
void* out_b);
bool ConsumeActivationBudgetGatedLocked(NpSignalingContext& ctx);
void SnapshotConnectionStatisticsLocked(u32* out_peak, u32* out_active, u32* out_transient,
u32* out_established);
OrbisNpSignalingRequestId StagePeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id);
bool TakePeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id, s32 req_or_conn_id,
PeerNetInfoResult* out);
bool DropPeerNetInfoResultLocked(OrbisNpSignalingContextId ctx_id, s32 req_or_conn_id);
void RecordRttSampleLocked(OrbisNpSignalingConnectionId conn_id, s32 rtt_us);
void SendEchoPings();
void DispatchConnectionEvent(OrbisNpSignalingConnectionId conn_id, s32 event_type, s32 event_data);
void DispatchPeerActivatedEvent(OrbisNpSignalingConnectionId conn_id);
void CloseConnectionAndDispatchDead(OrbisNpSignalingConnectionId conn_id, s32 error_code);
void EstablishConnection(OrbisNpSignalingConnectionId conn_id, bool peer_activated_hint);
void DeactivateConnectionFaithful(OrbisNpSignalingConnectionId conn_id);
void TerminateConnectionFaithful(OrbisNpSignalingConnectionId conn_id);
void StartHandshakeInitiator(OrbisNpSignalingConnectionId conn_id);
void QueueActivationLocked(OrbisNpSignalingConnectionId conn_id, std::string_view peer_online_id,
bool start_handshake = true);
void ProcessPendingActivations();
void HandleHandshakePacket(u32 from_addr, u16 from_port, const SignalingHandshake& pkt);
void ArmConnectTimeoutLocked(OrbisNpSignalingConnectionId conn_id);
void ClearLingerAndTimeoutLocked(OrbisNpSignalingConnectionId conn_id);
// BBSC rejoin fix: the game activates a connection it deactivated before - check the peer is
// still there before it counts as established again.
void BeginRevalidationLocked(OrbisNpSignalingConnectionId conn_id);
void SendActivationRequestLocked(const ConnectionInfo& ci);
void SendControlCloseLocked(const ConnectionInfo& ci, ControlReason reason);
void HandleControlPacket(u32 from_addr, u16 from_port, const SignalingControl& pkt);
const char* SignalingEventName(u32 event_type);
bool ConsumeActivationBudgetLocked(NpSignalingContext& ctx);
void SendStunPing(OrbisNpSignalingContextId ctx_id);
void ClearActiveDeadlines();
void ClearConnectionDeadlineForPeer(std::string_view npid);
void SetRoomLeft();
} // namespace Libraries::Np::NpSignaling
@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
// libSceNpUtility's NP lookup (online id -> NpId).
//
// 2026-10-06 (4-player test): every guest that joined a room which already held another guest
// called sceNpLookupNpId for that other player, got nothing back (the call was an unresolved stub),
// passed the empty NpId to sceNpSignalingActivateConnection ("peer NpId is not a valid online id
// (first bytes 00 ...)"), and left the room. With one guest there is nobody to look up, which is
// why two players always worked. On shadnet an NpId is just the online id in its handle, so the
// lookup answers at once; the async request reports "finished" on the first poll.
#include <atomic>
#include <cstring>
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/libs.h"
#include "core/libraries/np/np_types.h"
#include "core/libraries/np/np_utility.h"
namespace Libraries::Np::NpUtility {
constexpr s32 ORBIS_NP_LOOKUP_ERROR_INVALID_ARGUMENT = static_cast<s32>(0x80550702);
constexpr s32 PollAsyncFinished = 0;
std::atomic<s32> g_next_id{1};
s32 PS4_SYSV_ABI sceNpLookupCreateTitleCtx(const void* /*title_id*/, const OrbisNpId* /*self*/) {
const s32 id = g_next_id++;
LOG_INFO(Lib_NpManager, "lookup title ctx {}", id);
return id;
}
s32 PS4_SYSV_ABI sceNpLookupDeleteTitleCtx(s32 /*ctx_id*/) {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNpLookupCreateAsyncRequest(s32 /*ctx_id*/) {
return g_next_id++;
}
s32 PS4_SYSV_ABI sceNpLookupNpId(s32 req_id, const OrbisNpOnlineId* online_id, OrbisNpId* np_id,
void* /*option*/) {
if (online_id == nullptr || np_id == nullptr) {
return ORBIS_NP_LOOKUP_ERROR_INVALID_ARGUMENT;
}
std::memset(np_id, 0, sizeof(*np_id));
std::memcpy(np_id->handle.data, online_id->data, sizeof(np_id->handle.data));
np_id->handle.term = 0;
LOG_INFO(Lib_NpManager, "lookup {}: NpId for '{}'", req_id,
std::string(np_id->handle.data, strnlen(np_id->handle.data,
sizeof(np_id->handle.data))));
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNpLookupPollAsync(s32 /*req_id*/, s32* result) {
if (result != nullptr) {
*result = ORBIS_OK;
}
return PollAsyncFinished;
}
s32 PS4_SYSV_ABI sceNpLookupAbortRequest(s32 /*req_id*/) {
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNpLookupDeleteRequest(s32 /*req_id*/) {
return ORBIS_OK;
}
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("8533Q+LU7EQ", "libSceNpUtility", 1, "libSceNpUtility",
sceNpLookupCreateTitleCtx);
LIB_FUNCTION("mtqDK9zkoIE", "libSceNpUtility", 1, "libSceNpUtility",
sceNpLookupDeleteTitleCtx);
LIB_FUNCTION("JA4+sS39GMs", "libSceNpUtility", 1, "libSceNpUtility",
sceNpLookupCreateAsyncRequest);
LIB_FUNCTION("T6tnM1Uti4g", "libSceNpUtility", 1, "libSceNpUtility", sceNpLookupNpId);
LIB_FUNCTION("V4EVrruHuy8", "libSceNpUtility", 1, "libSceNpUtility", sceNpLookupPollAsync);
LIB_FUNCTION("eYz4v5Uek9U", "libSceNpUtility", 1, "libSceNpUtility", sceNpLookupAbortRequest);
LIB_FUNCTION("wLaxchvEEnk", "libSceNpUtility", 1, "libSceNpUtility",
sceNpLookupDeleteRequest);
};
} // namespace Libraries::Np::NpUtility
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/types.h"
namespace Core::Loader {
class SymbolsResolver;
}
namespace Libraries::Np::NpUtility {
void RegisterLib(Core::Loader::SymbolsResolver* sym);
} // namespace Libraries::Np::NpUtility
File diff suppressed because it is too large. Load diff
+416 -385
View File
@@ -1,385 +1,416 @@
// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <semaphore>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "common/types.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using ShadSocketHandle = SOCKET;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = INVALID_SOCKET;
#define SHAD_CLOSE(s) ::closesocket(s)
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
using ShadSocketHandle = int;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = -1;
#define SHAD_CLOSE(s) ::close(s)
#endif
namespace ShadNet {
// Protocol constants
static constexpr u32 SHAD_HEADER_SIZE = 15;
static constexpr u32 SHAD_CONNECT_TIMEOUT_MS = 10000; // 10 second connect/handshake timeout
// MAX_ATTEMPTS * CONNECT_TIMEOUT + sum(backoffs).
static constexpr u32 SHAD_CONNECT_MAX_ATTEMPTS = 4;
static constexpr u32 SHAD_CONNECT_RETRY_BACKOFF_MS =
1000; // base backoff, doubled per retry (cap 8s)
static constexpr u32 SHAD_PROTOCOL_VERSION = 1;
static constexpr u32 SHAD_MAX_PACKET_SIZE = 0x800000; // 8 MiB
// Protocol enumerations (must match shadnet server protocol.h)
enum class PacketType : u8 {
Request = 0,
Reply = 1,
Notification = 2,
ServerInfo = 3,
};
enum class CommandType : u16 {
Login = 0,
Terminate = 1,
Create = 2,
Delete = 3,
SendToken = 4,
SendResetToken = 5,
ResetPassword = 6,
ResetState = 7,
AddFriend = 8,
RemoveFriend = 9,
AddBlock = 10,
RemoveBlock = 11,
GetServerFeatures = 12,
GetBoardInfos = 30,
RecordScore = 31,
RecordScoreData = 32,
GetScoreData = 33,
GetScoreRange = 34,
GetScoreFriends = 35,
GetScoreNpid = 36,
GetScoreAccountId = 37,
GetScoreGameDataByAccId = 38,
GetToken = 39,
SetAppearOffline = 40,
// Matchmaking
ContextStart = 100,
CreateRoom = 101,
JoinRoom = 102,
LeaveRoom = 103,
SearchRoom = 104,
RequestSignalingInfos = 105,
ContextStop = 106,
SetUserInfo = 107,
SetRoomDataInternal = 108,
SetRoomDataExternal = 109,
KickoutRoomMember = 110,
GetWorldInfoList = 111,
GetRoomDataExternalList = 112,
GetUserInfoList = 113,
GetRoomMemberDataExternalList = 114,
SendRoomMessage = 115,
};
enum class NotificationType : u16 {
FriendQuery = 5,
FriendNew = 6,
FriendLost = 7,
FriendStatus = 8,
RoomEvent = 10,
RoomMessage = 11,
WebApiPushEvent = 17, // Generic NP WebApi push event
};
enum class ErrorType : uint8_t {
NoError = 0,
Malformed = 1,
Invalid = 2,
InvalidInput = 3,
TooSoon = 4,
LoginError = 5,
LoginAlreadyLoggedIn = 6,
LoginInvalidUsername = 7,
LoginInvalidPassword = 8,
LoginInvalidToken = 9,
CreationError = 10,
CreationExistingUsername = 11,
CreationBannedEmailProvider = 12,
CreationExistingEmail = 13,
RoomMissing = 14,
RoomAlreadyJoined = 15,
RoomFull = 16,
RoomPasswordMismatch = 17,
RoomPasswordMissing = 18,
RoomGroupNoJoinLabel = 19,
RoomGroupFull = 20,
RoomGroupJoinLabelNotFound = 21,
RoomGroupMaxSlotMismatch = 22,
Unauthorized = 23,
DbFail = 24,
EmailFail = 25,
NotFound = 26,
Blocked = 27,
AlreadyFriend = 28,
ScoreNotBest = 29,
ScoreInvalid = 30,
ScoreHasData = 31,
CondFail = 32,
Unsupported = 33,
};
enum class ShadNetState {
Ok,
FailureInput,
FailureResolve,
FailureConnect,
FailureServerInfo,
FailureAuth,
FailureAlreadyIn,
FailureUsername,
FailurePassword,
FailureToken,
FailureProtocol,
FailureOther,
};
// Callback data structures
struct FriendEntry {
std::string npid;
bool online = false;
};
struct LoginResult {
ErrorType error = ErrorType::Malformed;
std::string avatarUrl;
u64 userId = 0;
std::vector<FriendEntry> friends;
std::vector<std::string> requestsSent;
std::vector<std::string> requestsReceived;
std::vector<std::string> blocked;
};
struct NotifyFriendQuery {
std::string fromNpid;
};
struct NotifyFriendNew {
std::string npid;
bool online = false;
};
struct NotifyFriendLost {
std::string npid;
};
struct NotifyFriendStatus {
std::string npid;
bool online = false;
u64 timestamp = 0;
};
struct NotifyWebApiPushEvent {
std::string npServiceName; // may be empty (catch-all listeners match any)
u32 npServiceLabel = 0;
std::string dataType; // e.g. "np:service:..."
std::string data; // raw event body (typically PSN-format JSON)
std::string fromNpid; // may be empty
std::string toNpid; // may be empty
// Optional extended-data (key,value) pairs (e.g. friendlist trigger/additionalTrigger,
// presence gameStatus/gameData). Empty when the server sends none / is older.
std::vector<std::pair<std::string, std::string>> extdData;
};
struct MatchingBinAttr {
u32 attr_id = 0;
u64 update_date = 0;
u32 update_member_id = 0;
std::vector<u8> data;
};
struct NotifyRoomEvent {
u32 ctx_id = 0;
u64 room_id = 0;
u32 event = 0;
u32 event_cause = 0;
s32 error_code = 0;
u32 flags = 0;
bool has_passwd_mask = false;
u64 passwd_slot_mask = 0;
std::string member_npid;
u64 member_account_id = 0;
u32 member_platform = 0;
u32 member_id = 0;
u32 member_team_id = 0;
bool member_is_owner = false;
u64 member_join_date = 0;
u32 member_nat_type = 0;
u32 member_flag_attr = 0;
u32 member_group_id = 0;
std::string member_addr;
u32 member_port = 0;
std::vector<MatchingBinAttr> member_bin_attrs;
std::vector<MatchingBinAttr> bin_attrs;
};
struct NotifyRoomMessage {
u32 ctx_id = 0;
u64 room_id = 0;
u32 src_member_id = 0;
u32 event = 0;
u32 cast_type = 0;
std::vector<u32> dst_member_ids;
std::string src_npid;
u64 src_account_id = 0;
u32 src_platform = 0;
std::vector<u8> msg;
};
// ShadNetClient
class ShadNetClient {
public:
ShadNetClient();
~ShadNetClient();
ShadNetClient(const ShadNetClient&) = delete;
ShadNetClient& operator=(const ShadNetClient&) = delete;
void Start(const std::string& host, u16 port, const std::string& npid,
const std::string& password, const std::string& token = {});
void Stop();
ShadNetState WaitForConnection();
ShadNetState WaitForAuthenticated();
bool IsConnected() const;
bool IsAuthenticated() const;
ShadNetState GetState() const;
const std::string& GetAvatarUrl() const;
u64 GetUserId() const;
u32 GetServerProtocolVersion() const {
return m_server_protocol_version.load();
}
u32 GetAddrLocal() const;
u32 GetAddrServer() const;
bool IsMatching2Enabled() const;
u32 GetNumFriends() const;
std::optional<std::string> GetFriendNpid(u32 index) const;
std::string GetBearerToken() const;
// Callbacks
std::function<void(const LoginResult&)> onLoginResult;
std::function<void(const NotifyFriendQuery&)> onFriendQuery;
std::function<void(const NotifyFriendNew&)> onFriendNew;
std::function<void(const NotifyFriendLost&)> onFriendLost;
std::function<void(const NotifyFriendStatus&)> onFriendStatus;
std::function<void(const NotifyRoomEvent&)> onRoomEvent;
std::function<void(const NotifyRoomMessage&)> onRoomMessage;
std::function<void(const NotifyWebApiPushEvent&)> onWebApiPushEvent;
// Async reply callback.
// cmd —command this reply is for (matches the request's cmd)
// pkt_id —packet id echoed back from the original request header
// error —ErrorType byte that prefixes every reply body
// body —reply payload AFTER the error byte (may be empty)
std::function<void(CommandType cmd, u64 pkt_id, ErrorType error, const std::vector<u8>& body)>
onAsyncReply;
// Submit a Request packet for async processing.
// Allocates a packet id, builds the packet, pushes it to the writer queue.
// Returns the packet id so callers can correlate the eventual reply.
u64 SubmitRequest(CommandType cmd, const std::vector<u8>& payload);
// Friend / block commands.
u64 AddFriend(const std::string& npid);
u64 RemoveFriend(const std::string& npid);
u64 AddBlock(const std::string& npid);
u64 RemoveBlock(const std::string& npid);
// Set the Appear-Offline preference mid-session (server handles us as offline while set).
u64 SetAppearOffline(bool enable);
private:
void ConnectThread();
void ReaderThread();
void WriterThread();
bool DoConnect();
void DoDisconnect();
bool RecvN(u8* buf, u32 n);
bool SendAll(const std::vector<u8>& data);
std::vector<u8> BuildPacket(CommandType cmd, u64 id, const std::vector<u8>& payload) const;
void DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id, const std::vector<u8>& payload);
void HandleLoginReply(const std::vector<u8>& payload);
void HandleGetTokenReply(const std::vector<u8>& payload);
void HandleServerFeaturesReply(const std::vector<u8>& payload);
void HandleNotification(u16 cmd_raw, const std::vector<u8>& payload);
bool RequestServerFeatures();
// Helper: read a u32-LE-prefixed proto blob from a byte vector at pos.
static std::string ExtractBlob(const std::vector<u8>& p, int pos);
static void PutLE16(std::vector<u8>& b, size_t off, u16 v);
static void PutLE32(std::vector<u8>& b, size_t off, u32 v);
static void PutLE64(std::vector<u8>& b, size_t off, u64 v);
static u16 GetLE16(const u8* p);
static u32 GetLE32(const u8* p);
static u64 GetLE64(const u8* p);
ShadSocketHandle m_sock = SHAD_INVALID_SOCK;
std::string m_host;
u16 m_port = 31313;
std::string m_npid;
std::string m_password;
std::string m_token;
bool m_appear_offline = false; // user's Appear-Offline preference (sent at login)
std::atomic<bool> m_terminate{false};
std::atomic<bool> m_connected{false};
std::atomic<bool> m_authenticated{false};
std::atomic<ShadNetState> m_state{ShadNetState::Ok};
std::binary_semaphore m_sem_connected{0};
std::binary_semaphore m_sem_authenticated{0};
std::mutex m_mutex_connected;
std::mutex m_mutex_authenticated;
std::thread m_thread_connect;
std::thread m_thread_reader;
std::thread m_thread_writer;
std::mutex m_mutex_send_direct;
std::mutex m_mutex_send_queue;
std::condition_variable m_cv_send_queue;
std::vector<std::vector<u8>> m_send_queue;
std::string m_avatar_url;
u64 m_user_id = 0;
mutable std::mutex m_mutex_bearer;
std::string m_bearer_token;
std::atomic<u32> m_addr_local{0};
std::atomic<u32> m_addr_server{0};
std::atomic<u32> m_server_protocol_version{0};
std::atomic<bool> m_matching2_enabled{false};
std::atomic<bool> m_server_features_received{false};
mutable std::mutex m_mutex_friends;
std::vector<FriendEntry> m_friends;
std::atomic<u64> m_pkt_counter{1};
};
} // namespace ShadNet
// SPDX-FileCopyrightText: Copyright 2019-2026 rpcs3 Project
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <semaphore>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include "common/types.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using ShadSocketHandle = SOCKET;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = INVALID_SOCKET;
#define SHAD_CLOSE(s) ::closesocket(s)
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
using ShadSocketHandle = int;
static constexpr ShadSocketHandle SHAD_INVALID_SOCK = -1;
#define SHAD_CLOSE(s) ::close(s)
#endif
namespace ShadNet {
// Protocol constants
static constexpr u32 SHAD_HEADER_SIZE = 15;
static constexpr u32 SHAD_CONNECT_TIMEOUT_MS = 10000; // 10 second connect/handshake timeout
// MAX_ATTEMPTS * CONNECT_TIMEOUT + sum(backoffs).
static constexpr u32 SHAD_CONNECT_MAX_ATTEMPTS = 4;
static constexpr u32 SHAD_CONNECT_RETRY_BACKOFF_MS =
1000; // base backoff, doubled per retry (cap 8s)
static constexpr u32 SHAD_PROTOCOL_VERSION = 1;
static constexpr u32 SHAD_MAX_PACKET_SIZE = 0x800000; // 8 MiB
// Protocol enumerations (must match shadnet server protocol.h)
enum class PacketType : u8 {
Request = 0,
Reply = 1,
Notification = 2,
ServerInfo = 3,
};
enum class CommandType : u16 {
Login = 0,
Terminate = 1,
Create = 2,
Delete = 3,
SendToken = 4,
SendResetToken = 5,
ResetPassword = 6,
ResetState = 7,
AddFriend = 8,
RemoveFriend = 9,
AddBlock = 10,
RemoveBlock = 11,
GetServerFeatures = 12,
SetClientVersion = 13,
GetBoardInfos = 30,
RecordScore = 31,
RecordScoreData = 32,
GetScoreData = 33,
GetScoreRange = 34,
GetScoreFriends = 35,
GetScoreNpid = 36,
GetScoreAccountId = 37,
GetScoreGameDataByAccId = 38,
GetToken = 39,
SetAppearOffline = 40,
// Matchmaking
ContextStart = 100,
CreateRoom = 101,
JoinRoom = 102,
LeaveRoom = 103,
SearchRoom = 104,
RequestSignalingInfos = 105,
ContextStop = 106,
SetUserInfo = 107,
SetRoomDataInternal = 108,
SetRoomDataExternal = 109,
KickoutRoomMember = 110,
GetWorldInfoList = 111,
GetRoomDataExternalList = 112,
GetUserInfoList = 113,
GetRoomMemberDataExternalList = 114,
SendRoomMessage = 115,
};
enum class NotificationType : u16 {
FriendQuery = 5,
FriendNew = 6,
FriendLost = 7,
FriendStatus = 8,
RoomEvent = 10,
RoomMessage = 11,
WebApiPushEvent = 17, // Generic NP WebApi push event
ServiceUnavailable = 18, // Server refused a service (e.g. matchmaking full)
ClientRejected = 19, // Server is signing this client out (e.g. version not accepted)
};
enum class ErrorType : uint8_t {
NoError = 0,
Malformed = 1,
Invalid = 2,
InvalidInput = 3,
TooSoon = 4,
LoginError = 5,
LoginAlreadyLoggedIn = 6,
LoginInvalidUsername = 7,
LoginInvalidPassword = 8,
LoginInvalidToken = 9,
CreationError = 10,
CreationExistingUsername = 11,
CreationBannedEmailProvider = 12,
CreationExistingEmail = 13,
RoomMissing = 14,
RoomAlreadyJoined = 15,
RoomFull = 16,
RoomPasswordMismatch = 17,
RoomPasswordMissing = 18,
RoomGroupNoJoinLabel = 19,
RoomGroupFull = 20,
RoomGroupJoinLabelNotFound = 21,
RoomGroupMaxSlotMismatch = 22,
Unauthorized = 23,
DbFail = 24,
EmailFail = 25,
NotFound = 26,
Blocked = 27,
AlreadyFriend = 28,
ScoreNotBest = 29,
ScoreInvalid = 30,
ScoreHasData = 31,
CondFail = 32,
Unsupported = 33,
};
enum class ShadNetState {
Ok,
FailureInput,
FailureResolve,
FailureConnect,
FailureServerInfo,
FailureAuth,
FailureAlreadyIn,
FailureUsername,
FailurePassword,
FailureToken,
FailureProtocol,
FailureRejected, // the server does not accept this client (its version)
FailureOther,
};
// Callback data structures
struct FriendEntry {
std::string npid;
bool online = false;
};
struct LoginResult {
ErrorType error = ErrorType::Malformed;
std::string avatarUrl;
u64 userId = 0;
std::vector<FriendEntry> friends;
std::vector<std::string> requestsSent;
std::vector<std::string> requestsReceived;
std::vector<std::string> blocked;
};
struct NotifyFriendQuery {
std::string fromNpid;
};
struct NotifyFriendNew {
std::string npid;
bool online = false;
};
struct NotifyFriendLost {
std::string npid;
};
struct NotifyFriendStatus {
std::string npid;
bool online = false;
u64 timestamp = 0;
};
// Notification 18 (NotifyServiceUnavailable): why the server refused a service.
struct NotifyServiceUnavailable {
enum : u32 { ServiceMatching2 = 1 };
enum : u32 { ReasonServerFull = 1, ReasonNotAllowed = 2, ReasonDisabled = 3 };
u32 service = 0;
u32 reason = 0;
u32 inUse = 0; // set for ReasonServerFull
u32 limit = 0; // set for ReasonServerFull
};
// Notification 19 (NotifyClientRejected): the server is signing this client out and will
// disconnect right after.
struct NotifyClientRejected {
enum : u32 { ReasonVersion = 1 };
u32 reason = 0;
std::string message; // for the player, from the server's rule
std::string version; // the version the server checked ("" = none reported)
};
struct NotifyWebApiPushEvent {
std::string npServiceName; // may be empty (catch-all listeners match any)
u32 npServiceLabel = 0;
std::string dataType; // e.g. "np:service:..."
std::string data; // raw event body (typically PSN-format JSON)
std::string fromNpid; // may be empty
std::string toNpid; // may be empty
// Optional extended-data (key,value) pairs (e.g. friendlist trigger/additionalTrigger,
// presence gameStatus/gameData). Empty when the server sends none / is older.
std::vector<std::pair<std::string, std::string>> extdData;
};
struct MatchingBinAttr {
u32 attr_id = 0;
u64 update_date = 0;
u32 update_member_id = 0;
std::vector<u8> data;
};
struct NotifyRoomEvent {
u32 ctx_id = 0;
u64 room_id = 0;
u32 event = 0;
u32 event_cause = 0;
s32 error_code = 0;
u32 flags = 0;
bool has_passwd_mask = false;
u64 passwd_slot_mask = 0;
std::string member_npid;
u64 member_account_id = 0;
u32 member_platform = 0;
u32 member_id = 0;
u32 member_team_id = 0;
bool member_is_owner = false;
u64 member_join_date = 0;
u32 member_nat_type = 0;
u32 member_flag_attr = 0;
u32 member_group_id = 0;
std::string member_addr;
u32 member_port = 0;
std::vector<MatchingBinAttr> member_bin_attrs;
std::vector<MatchingBinAttr> bin_attrs;
};
struct NotifyRoomMessage {
u32 ctx_id = 0;
u64 room_id = 0;
u32 src_member_id = 0;
u32 event = 0;
u32 cast_type = 0;
std::vector<u32> dst_member_ids;
std::string src_npid;
u64 src_account_id = 0;
u32 src_platform = 0;
std::vector<u8> msg;
};
// ShadNetClient
class ShadNetClient {
public:
ShadNetClient();
~ShadNetClient();
ShadNetClient(const ShadNetClient&) = delete;
ShadNetClient& operator=(const ShadNetClient&) = delete;
void Start(const std::string& host, u16 port, const std::string& npid,
const std::string& password, const std::string& token = {});
void Stop();
ShadNetState WaitForConnection();
ShadNetState WaitForAuthenticated();
bool IsConnected() const;
bool IsAuthenticated() const;
ShadNetState GetState() const;
const std::string& GetAvatarUrl() const;
u64 GetUserId() const;
u32 GetServerProtocolVersion() const {
return m_server_protocol_version.load();
}
u32 GetAddrLocal() const;
u32 GetAddrServer() const;
bool IsMatching2Enabled() const;
u32 GetNumFriends() const;
std::optional<std::string> GetFriendNpid(u32 index) const;
std::string GetBearerToken() const;
// Callbacks
std::function<void(const LoginResult&)> onLoginResult;
std::function<void(const NotifyFriendQuery&)> onFriendQuery;
std::function<void(const NotifyFriendNew&)> onFriendNew;
std::function<void(const NotifyFriendLost&)> onFriendLost;
std::function<void(const NotifyFriendStatus&)> onFriendStatus;
std::function<void(const NotifyRoomEvent&)> onRoomEvent;
std::function<void(const NotifyRoomMessage&)> onRoomMessage;
std::function<void(const NotifyWebApiPushEvent&)> onWebApiPushEvent;
std::function<void(const NotifyServiceUnavailable&)> onServiceUnavailable;
std::function<void(const NotifyClientRejected&)> onClientRejected;
// Async reply callback.
// cmd —command this reply is for (matches the request's cmd)
// pkt_id —packet id echoed back from the original request header
// error —ErrorType byte that prefixes every reply body
// body —reply payload AFTER the error byte (may be empty)
std::function<void(CommandType cmd, u64 pkt_id, ErrorType error, const std::vector<u8>& body)>
onAsyncReply;
// Submit a Request packet for async processing.
// Allocates a packet id, builds the packet, pushes it to the writer queue.
// Returns the packet id so callers can correlate the eventual reply.
u64 SubmitRequest(CommandType cmd, const std::vector<u8>& payload);
// Friend / block commands.
u64 AddFriend(const std::string& npid);
u64 RemoveFriend(const std::string& npid);
u64 AddBlock(const std::string& npid);
u64 RemoveBlock(const std::string& npid);
// Set the Appear-Offline preference mid-session (server handles us as offline while set).
u64 SetAppearOffline(bool enable);
// The version this client reports after login (SetClientVersion): the port's own name and
// version (BB_SHADNET_CLIENT_VERSION overrides it, for a server of one's own).
static std::string BuildVersionString();
u64 ReportClientVersion();
private:
void ConnectThread();
void ReaderThread();
void WriterThread();
bool DoConnect();
void DoDisconnect();
bool RecvN(u8* buf, u32 n);
bool SendAll(const std::vector<u8>& data);
std::vector<u8> BuildPacket(CommandType cmd, u64 id, const std::vector<u8>& payload) const;
void DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id, const std::vector<u8>& payload);
void HandleLoginReply(const std::vector<u8>& payload);
void HandleGetTokenReply(const std::vector<u8>& payload);
void HandleServerFeaturesReply(const std::vector<u8>& payload);
void HandleClientVersionReply(const std::vector<u8>& payload);
void HandleNotification(u16 cmd_raw, const std::vector<u8>& payload);
bool RequestServerFeatures();
// Helper: read a u32-LE-prefixed proto blob from a byte vector at pos.
static std::string ExtractBlob(const std::vector<u8>& p, int pos);
static void PutLE16(std::vector<u8>& b, size_t off, u16 v);
static void PutLE32(std::vector<u8>& b, size_t off, u32 v);
static void PutLE64(std::vector<u8>& b, size_t off, u64 v);
static u16 GetLE16(const u8* p);
static u32 GetLE32(const u8* p);
static u64 GetLE64(const u8* p);
ShadSocketHandle m_sock = SHAD_INVALID_SOCK;
std::string m_host;
u16 m_port = 31313;
std::string m_npid;
std::string m_password;
std::string m_token;
bool m_appear_offline = false; // user's Appear-Offline preference (sent at login)
std::atomic<bool> m_terminate{false};
std::atomic<bool> m_connected{false};
std::atomic<bool> m_authenticated{false};
// Signed in and the server took the reported version (or answered it): the login is over.
std::atomic<bool> m_ready{false};
std::atomic<bool> m_rejected{false};
std::atomic<ShadNetState> m_state{ShadNetState::Ok};
std::binary_semaphore m_sem_connected{0};
std::binary_semaphore m_sem_authenticated{0};
std::mutex m_mutex_connected;
std::mutex m_mutex_authenticated;
std::thread m_thread_connect;
std::thread m_thread_reader;
std::thread m_thread_writer;
std::mutex m_mutex_send_direct;
std::mutex m_mutex_send_queue;
std::condition_variable m_cv_send_queue;
std::vector<std::vector<u8>> m_send_queue;
std::string m_avatar_url;
u64 m_user_id = 0;
mutable std::mutex m_mutex_bearer;
std::string m_bearer_token;
std::atomic<u32> m_addr_local{0};
std::atomic<u32> m_addr_server{0};
std::atomic<u32> m_server_protocol_version{0};
std::atomic<bool> m_matching2_enabled{false};
std::atomic<bool> m_server_features_received{false};
mutable std::mutex m_mutex_friends;
std::vector<FriendEntry> m_friends;
std::atomic<u64> m_pkt_counter{1};
};
} // namespace ShadNet
File diff suppressed because it is too large. Load diff
+327
View File
@@ -0,0 +1,327 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "buffers/buffer.h"
#include <array>
#include <cstdio>
#include <numeric>
#include <vk_mem_alloc.h>
#include "bbport_guest_memory.h"
#include "common/alignment.h"
#include "common/assert.h"
#include "vulkan/instance.h"
#include "vulkan/platform.h"
#include "render/scheduler.h"
namespace VideoCore {
namespace {
const char* TypeName(MemoryType type) {
constexpr const char* names[] = {"DeviceLocal", "HostUncached", "HostCached", "Stream",
"Sparse"};
return u32(type) < std::size(names) ? names[u32(type)] : "Invalid";
}
/// VMA's view of a memory type: where it should live and how the host touches it.
VmaAllocationCreateInfo AllocationInfo(MemoryType type, bool device_address) {
VmaAllocationCreateInfo info{
.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT,
.usage = VMA_MEMORY_USAGE_UNKNOWN,
.preferredFlags = type != MemoryType::DeviceLocal ? VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
: VkMemoryPropertyFlags{},
};
if (device_address) {
info.flags |= VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT;
}
switch (type) {
case MemoryType::DeviceLocal:
info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
break;
case MemoryType::Stream:
info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
info.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT |
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
break;
case MemoryType::HostUncached:
info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
info.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT |
VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
break;
case MemoryType::HostCached:
info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
info.flags |=
VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT;
break;
case MemoryType::Sparse:
break;
}
return info;
}
/// Sparse arenas take exported guest memory chunks (in place) as well as VRAM blocks: created
/// for external memory when the driver lets such a buffer take VRAM too, else in place is off.
vk::Buffer CreateArena(vk::Device device, VmaAllocator allocator,
vk::BufferCreateInfo& create_info) {
create_info.flags |=
vk::BufferCreateFlagBits::eSparseBinding | vk::BufferCreateFlagBits::eSparseResidency;
if (GuestInPlace()) {
const vk::ExternalMemoryBufferCreateInfo external{
.handleTypes = BbGuestMemory::HandleType(),
};
create_info.pNext = &external;
const auto [result, buffer] = device.createBuffer(create_info);
create_info.pNext = nullptr;
if (result != vk::Result::eSuccess) {
std::printf("Guest memory: the arena cannot take exported memory (%s); the GPU "
"uses copies in VRAM as before\n",
vk::to_string(result).c_str());
DisableGuestInPlace();
} else {
const auto requirements = device.getBufferMemoryRequirements(buffer);
const VkPhysicalDeviceMemoryProperties* properties = nullptr;
vmaGetMemoryProperties(allocator, &properties);
bool takes_vram = false;
for (u32 i = 0; i < properties->memoryTypeCount; ++i) {
takes_vram |= (requirements.memoryTypeBits >> i & 1) &&
(properties->memoryTypes[i].propertyFlags &
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
}
if (takes_vram) {
return buffer;
}
std::printf("Guest memory: the arena for guest memory cannot take VRAM; the GPU "
"uses copies in VRAM as before\n");
device.destroyBuffer(buffer);
DisableGuestInPlace();
}
}
return Vulkan::Check(device.createBuffer(create_info));
}
} // namespace
BufferAllocation::BufferAllocation(vk::Device device_, VmaAllocator allocator_)
: device{device_}, allocator{allocator_} {}
BufferAllocation::~BufferAllocation() {
Destroy();
}
BufferAllocation::BufferAllocation(BufferAllocation&& other) noexcept
: device{other.device}, buffer{std::exchange(other.buffer, VK_NULL_HANDLE)},
bda_addr{std::exchange(other.bda_addr, 0)},
allocator{std::exchange(other.allocator, VK_NULL_HANDLE)},
allocation{std::exchange(other.allocation, VK_NULL_HANDLE)} {}
BufferAllocation& BufferAllocation::operator=(BufferAllocation&& other) noexcept {
if (this != &other) {
Destroy();
device = other.device;
buffer = std::exchange(other.buffer, VK_NULL_HANDLE);
bda_addr = std::exchange(other.bda_addr, 0);
allocator = std::exchange(other.allocator, VK_NULL_HANDLE);
allocation = std::exchange(other.allocation, VK_NULL_HANDLE);
}
return *this;
}
void BufferAllocation::Destroy() {
if (allocation) {
vmaDestroyBuffer(allocator, buffer, allocation);
} else if (buffer) {
device.destroyBuffer(buffer);
}
buffer = VK_NULL_HANDLE;
allocation = VK_NULL_HANDLE;
bda_addr = 0;
}
void BufferAllocation::Create(vk::BufferCreateInfo& create_info, MemoryType type,
VmaAllocationInfo* out_info) {
const bool device_address =
bool(create_info.usage & vk::BufferUsageFlagBits::eShaderDeviceAddress);
if (type == MemoryType::Sparse) {
buffer = CreateArena(device, allocator, create_info);
} else {
const VmaAllocationCreateInfo alloc_info = AllocationInfo(type, device_address);
const VkBufferCreateInfo vk_info = create_info;
VkBuffer handle{};
const VkResult result =
vmaCreateBuffer(allocator, &vk_info, &alloc_info, &handle, &allocation, out_info);
ASSERT_MSG(result == VK_SUCCESS, "buffer allocation: {}",
vk::to_string(vk::Result{result}));
buffer = handle;
}
if (device_address) {
bda_addr = device.getBufferAddress({.buffer = buffer});
ASSERT_MSG(bda_addr != 0, "no device address");
}
}
Buffer::Buffer(const Vulkan::Instance& instance, VAddr cpu_addr_, u64 size_bytes_,
MemoryType type, std::string_view debug_name)
: cpu_addr{cpu_addr_}, size_bytes{size_bytes_}, mem_type{type},
buffer{instance.GetDevice(), instance.GetAllocator()} {
vk::BufferCreateInfo create_info{
.size = size_bytes,
.usage = AllFlags,
.sharingMode = vk::SharingMode::eExclusive,
};
// Arenas are read by the readback queue too.
const std::array<u32, 2> families = {instance.GetGraphicsQueueFamilyIndex(),
instance.GetReadbackQueueFamilyIndex()};
if (type == MemoryType::Sparse && instance.GetReadbackQueue()) {
create_info.sharingMode = vk::SharingMode::eConcurrent;
create_info.queueFamilyIndexCount = u32(families.size());
create_info.pQueueFamilyIndices = families.data();
}
VmaAllocationInfo alloc_info{};
buffer.Create(create_info, type, &alloc_info);
const auto device = instance.GetDevice();
if (debug_name.empty()) {
Vulkan::SetObjectName(device, Handle(), "Buffer {:#x}:{:#x}", cpu_addr, size_bytes);
} else {
Vulkan::SetObjectName(device, Handle(), debug_name);
}
if (type != MemoryType::Sparse) {
VkMemoryPropertyFlags flags{};
vmaGetAllocationMemoryProperties(instance.GetAllocator(), buffer.allocation, &flags);
if (alloc_info.pMappedData) {
mapped_data = {static_cast<u8*>(alloc_info.pMappedData), size_bytes};
}
is_coherent = flags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
}
}
Buffer::Buffer(const Vulkan::Instance& instance, u64 size_bytes_, vk::DeviceMemory memory,
std::string_view debug_name)
: size_bytes{size_bytes_}, mem_type{MemoryType::HostCached},
buffer{instance.GetDevice(), instance.GetAllocator()} {
const vk::ExternalMemoryBufferCreateInfo external{
.handleTypes = BbGuestMemory::HandleType(),
};
const bool layer = BbGuestMemory::LayerMemory();
vk::BufferUsageFlags usage =
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst |
vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eUniformBuffer |
vk::BufferUsageFlagBits::eUniformTexelBuffer | vk::BufferUsageFlagBits::eStorageTexelBuffer;
if (layer) {
usage |= vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eIndexBuffer |
vk::BufferUsageFlagBits::eIndirectBuffer |
vk::BufferUsageFlagBits::eShaderDeviceAddress;
}
const auto device = instance.GetDevice();
buffer.buffer = Vulkan::Check(device.createBuffer({
.pNext = &external,
.size = size_bytes,
.usage = usage,
.sharingMode = vk::SharingMode::eExclusive,
}));
const auto result = device.bindBufferMemory(buffer.buffer, memory, 0);
ASSERT_MSG(result == vk::Result::eSuccess, "binding a guest memory chunk: {}",
vk::to_string(result));
if (layer) {
buffer.bda_addr = device.getBufferAddress({.buffer = buffer.buffer});
}
Vulkan::SetObjectName(device, Handle(), debug_name);
is_coherent = true;
}
void Buffer::Flush(u64 offset, u64 size) {
if (!mapped_data.empty() && !is_coherent) {
vmaFlushAllocation(buffer.allocator, buffer.allocation, offset, size);
}
}
void Buffer::Invalidate(u64 offset, u64 size) {
if (!mapped_data.empty() && !is_coherent) {
vmaInvalidateAllocation(buffer.allocator, buffer.allocation, offset, size);
}
}
StreamBuffer::StreamBuffer(const Vulkan::Instance& instance, Vulkan::Scheduler& scheduler_,
MemoryType type, u64 size)
: Buffer{instance, 0, size, type}, scheduler{scheduler_},
atom_size{instance.NonCoherentAtomSize()}, section_bytes{std::max<u64>(
(size + Sections - 1) / Sections, 1)} {
Vulkan::SetObjectName(instance.GetDevice(), Handle(), "StreamBuffer({}):{:#x}", TypeName(type),
size);
}
bool StreamBuffer::Prepare(u64 size, u64 alignment, bool allow_wait) {
// Host writes to non-coherent memory are flushed in whole atoms: pieces never share one.
if (!mapped_data.empty() && !is_coherent) {
size = Common::AlignUp(size, atom_size);
alignment = alignment ? std::lcm(alignment, atom_size) : atom_size;
}
if (size > size_bytes) {
return false;
}
u64 offset = alignment ? Common::AlignUp(cursor, alignment) : cursor;
const bool wraps = offset + size > size_bytes;
if (wraps) {
offset = 0; // what does not fit before the end starts the next lap
}
// The sections this piece enters that the lap has not claimed yet: what they held one lap
// earlier must be done. Checked before anything changes (a refusal leaves the ring as it was).
const u32 first = wraps ? 0 : claimed;
const u32 last = size ? u32((offset + size - 1) / section_bytes) : first;
for (u32 section = first; section <= last && section < Sections; ++section) {
const u64 tick = section_ticks[section];
if (tick != 0 && !scheduler.IsFree(tick)) {
if (!allow_wait) {
return false;
}
scheduler.Wait(tick);
}
}
if (wraps) {
claimed = 0;
}
claimed = std::max<u32>(claimed, std::min<u32>(last + 1, Sections));
pending_offset = offset;
pending_size = size;
return true;
}
void StreamBuffer::Advance() {
last_tick = scheduler.CurrentTick();
if (pending_size != 0) {
const u32 first = u32(pending_offset / section_bytes);
const u32 last = u32((pending_offset + pending_size - 1) / section_bytes);
for (u32 section = first; section <= last && section < Sections; ++section) {
section_ticks[section] = last_tick;
}
}
cursor = pending_offset + pending_size;
}
std::pair<u8*, u64> StreamBuffer::Map(u64 size, u64 alignment, bool allow_wait) {
if (!Prepare(size, alignment, allow_wait)) {
return {nullptr, 0};
}
return {mapped_data.empty() ? nullptr : mapped_data.data() + pending_offset, pending_offset};
}
void StreamBuffer::Commit() {
if (mem_type == MemoryType::HostCached) {
Invalidate(pending_offset, pending_size);
} else {
Flush(pending_offset, pending_size);
}
Advance();
}
std::optional<u64> StreamBuffer::Reserve(u64 size, u64 alignment, bool allow_wait) {
if (!Prepare(size, alignment, allow_wait)) {
return std::nullopt;
}
const u64 offset = pending_offset;
Advance();
return offset;
}
} // namespace VideoCore
+160
View File
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport buffers: Vulkan buffers with their memory: VMA allocations (device local, upload,
// readback, stream), sparse arenas (blocks bound on demand), and buffers over guest memory chunks
// owned elsewhere. StreamBuffer: a ring of host-visible memory for per-draw data.
#pragma once
#include <cstring>
#include <array>
#include <optional>
#include <span>
#include <string_view>
#include <utility>
#include "buffers/memory_model.h"
#include "common/assert.h"
#include "common/types.h"
#include "vulkan/vk_common.h"
namespace Vulkan {
class Instance;
class Scheduler;
} // namespace Vulkan
VK_DEFINE_HANDLE(VmaAllocation)
VK_DEFINE_HANDLE(VmaAllocator)
struct VmaAllocationInfo;
namespace VideoCore {
enum class MemoryType : u8 {
DeviceLocal, ///< VRAM
HostUncached, ///< host visible, for uploads (write-combined)
HostCached, ///< host visible and cached, for readbacks
Stream, ///< device local and host visible if there is such memory, else host
Sparse, ///< no memory: blocks bound later (arenas)
};
constexpr vk::BufferUsageFlags ReadFlags =
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eUniformBuffer |
vk::BufferUsageFlagBits::eIndexBuffer | vk::BufferUsageFlagBits::eVertexBuffer |
vk::BufferUsageFlagBits::eIndirectBuffer | vk::BufferUsageFlagBits::eShaderDeviceAddress;
constexpr vk::BufferUsageFlags AllFlags =
ReadFlags | vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eStorageBuffer;
/// A buffer handle with its VMA allocation (none for sparse and external buffers).
struct BufferAllocation {
BufferAllocation(vk::Device device, VmaAllocator allocator);
~BufferAllocation();
BufferAllocation(const BufferAllocation&) = delete;
BufferAllocation& operator=(const BufferAllocation&) = delete;
BufferAllocation(BufferAllocation&& other) noexcept;
BufferAllocation& operator=(BufferAllocation&& other) noexcept;
/// Creates the buffer (sparse ones bound to nothing yet); fills `out_info` for allocations.
void Create(vk::BufferCreateInfo& create_info, MemoryType type, VmaAllocationInfo* out_info);
void Destroy();
vk::Device device;
vk::Buffer buffer{};
vk::DeviceAddress bda_addr{};
VmaAllocator allocator;
VmaAllocation allocation{};
};
struct Buffer {
Buffer(const Vulkan::Instance& instance, VAddr cpu_addr, u64 size_bytes, MemoryType type,
std::string_view debug_name = "");
/// Over memory owned elsewhere (a guest memory chunk, exported): transfers and texel and
/// storage reads; with the layer's memory module vertex, index, indirect data and device
/// addresses too.
Buffer(const Vulkan::Instance& instance, u64 size_bytes, vk::DeviceMemory memory,
std::string_view debug_name);
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
Buffer(Buffer&&) = default;
Buffer& operator=(Buffer&&) = default;
[[nodiscard]] bool IsInBounds(VAddr addr, u64 size) const noexcept {
return addr >= cpu_addr && addr + size <= cpu_addr + size_bytes;
}
[[nodiscard]] VAddr CpuAddr() const noexcept {
return cpu_addr;
}
[[nodiscard]] u64 Offset(VAddr addr) const noexcept {
return addr - cpu_addr;
}
size_t SizeBytes() const {
return size_bytes;
}
vk::Buffer Handle() const noexcept {
return buffer.buffer;
}
vk::DeviceAddress BufferDeviceAddress() const noexcept {
ASSERT_MSG(buffer.bda_addr != 0, "buffer has no device address");
return buffer.bda_addr;
}
/// Host writes in [offset, offset + size) made visible to the device (non-coherent memory).
void Flush(u64 offset, u64 size);
/// Device writes made visible to the host.
void Invalidate(u64 offset, u64 size);
VAddr cpu_addr = 0;
size_t size_bytes = 0;
std::span<u8> mapped_data{};
bool is_coherent{};
MemoryType mem_type{MemoryType::DeviceLocal};
BufferAllocation buffer;
};
/// A ring of memory handed out front to back. The ring is cut into Sections; each remembers the
/// newest submission (scheduler tick) that used it. Space within the section in use is a bump of
/// the cursor; entering the next section is the only point where the GPU must be done with what
/// that section held one lap earlier. Nothing is kept per request.
struct StreamBuffer : public Buffer {
StreamBuffer(const Vulkan::Instance& instance, Vulkan::Scheduler& scheduler,
MemoryType mem_type, u64 size_bytes);
/// Space for `size` bytes (to be followed by Commit): its pointer (null for memory not
/// mapped) and offset; {null, 0} when it would have to wait and `allow_wait` is false.
std::pair<u8*, u64> Map(u64 size, u64 alignment = 0, bool allow_wait = true);
/// The mapped space is written: visible to the GPU, used by the current submission.
void Commit();
/// Map and Commit without a write.
std::optional<u64> Reserve(u64 size, u64 alignment = 0, bool allow_wait = true);
/// The submission tick of the last commit (0: never used).
[[nodiscard]] u64 LastTick() const noexcept {
return last_tick;
}
/// Copies `size` bytes from `src` into new space; its offset.
u64 Copy(auto src, size_t size, size_t alignment = 0) {
const auto [data, offset] = Map(size, alignment);
ASSERT_MSG(data, "stream buffer: no room for {:#x} bytes", size);
std::memcpy(data, reinterpret_cast<const void*>(src), size);
Commit();
return offset;
}
private:
static constexpr u32 Sections = 64;
/// Finds room for the next `size` bytes (pending_offset, pending_size); false when it would
/// have to wait for the GPU and may not.
bool Prepare(u64 size, u64 alignment, bool allow_wait);
/// The pending space is used by the current submission.
void Advance();
Vulkan::Scheduler& scheduler;
vk::DeviceSize atom_size{};
u64 section_bytes = 1;
std::array<u64, Sections> section_ticks{}; ///< newest submission in each (0: none)
u32 claimed = 0; ///< sections [0, claimed) belong to the current lap
u64 cursor = 0; ///< the next free byte of the current lap
u64 pending_offset = 0;
u64 pending_size = 0; ///< the mapped space
u64 last_tick = 0;
};
} // namespace VideoCore
@@ -1,4 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <chrono>
@@ -20,16 +19,16 @@
#include "common/alignment.h"
#include "core/emulator_settings.h"
#include "core/memory.h"
#include "video_core/amdgpu/liverpool.h"
#include "video_core/buffer_cache/buffer.h"
#include "video_core/buffer_cache/buffer_cache.h"
#include "video_core/buffer_cache/memory_tracker.h"
#include "video_core/renderer_vulkan/vk_graphics_pipeline.h"
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_platform.h"
#include "video_core/renderer_vulkan/vk_runtime.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/texture_cache/texture_cache.h"
#include "cp/command_processor.h"
#include "buffers/buffer.h"
#include "buffers/buffer_cache.h"
#include "memory/dirty_tracker.h"
#include "render/pipeline.h"
#include "vulkan/instance.h"
#include "vulkan/platform.h"
#include "render/runtime.h"
#include "render/scheduler.h"
#include "textures/texture_cache.h"
#include <mutex>
#include <pthread.h>
@@ -479,53 +478,6 @@ bool GarlicInVram() {
return on && !FixedArena();
}
void DisableGuestInPlace() {
Detail::guest_in_place.store(2, std::memory_order_relaxed);
Detail::write_tracking.store(0, std::memory_order_relaxed); // recomputed: now on
}
bool Detail::ComputeWriteTracking() {
static const bool forced = [] {
const char* env = std::getenv("BB_WRITE_TRACKING");
return env && env[0] == '1';
}();
const bool on = forced || !GuestInPlace();
write_tracking.store(on ? 1 : 2, std::memory_order_relaxed);
return on;
}
bool WriteVerify() {
static const bool on = [] {
const char* env = std::getenv("BB_WRITE_VERIFY");
return env && env[0] == '1';
}();
return on && !WriteTracking();
}
bool ConstantsInPlace() {
static const bool on = [] {
const char* env = std::getenv("BB_CONSTANTS_IN_PLACE");
return env && env[0] == '1';
}();
return GuestInPlace() && on != BbToggle::Experiment(1);
}
bool Detail::ComputeGuestInPlace() {
static const bool on = [] {
const char* env = std::getenv("BB_GUEST_IN_PLACE");
const bool enabled = env && env[0] == '1';
if (enabled) {
std::printf("Guest memory: the GPU uses the game's direct memory in place "
"(BB_GUEST_IN_PLACE=1)\n");
}
return enabled;
}();
// DisableGuestInPlace may have run first: it stays off then.
u8 expected = 0;
guest_in_place.compare_exchange_strong(expected, on ? 1 : 2, std::memory_order_relaxed);
return guest_in_place.load(std::memory_order_relaxed) == 1;
}
static constexpr size_t GDS_BUFFER_SIZE = 64_KB;
static constexpr size_t STREAM_BUFFER_SIZE = 128_MB;
@@ -550,12 +502,12 @@ std::optional<u32> FindMemoryType(const vk::PhysicalDeviceMemoryProperties& prop
}
BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& scheduler_,
Vulkan::Runtime& runtime_, AmdGpu::Liverpool* liverpool_,
TextureCache& texture_cache_, PageManager& tracker)
Vulkan::Runtime& runtime_, BbCp::CommandProcessor* liverpool_,
TextureCache& texture_cache_, BbMem::PageWatch& tracker)
: instance{instance_}, scheduler{scheduler_}, runtime{runtime_},
staging_pool{runtime_.GetStagingPool()}, liverpool{liverpool_},
memory{Core::Memory::Instance()}, texture_cache{texture_cache_},
memory_tracker{std::make_unique<MemoryTracker>(tracker)},
memory_tracker{std::make_unique<BbMem::DirtyTracker>(tracker)},
stream_buffer{instance, scheduler, MemoryType::Stream, STREAM_BUFFER_SIZE},
gds_buffer{instance, 0, GDS_BUFFER_SIZE, MemoryType::Stream, "GDS Buffer"},
memory_semaphore{instance} {
@@ -618,7 +570,11 @@ BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& s
reqs.memoryTypeBits)
.value();
arena_memory_type_bits = reqs.memoryTypeBits;
{
// BB_ARENA_VRAM_ONLY=1 (tests): the arena takes VRAM only, as NVIDIA's sparse buffers do
// (with BB_RESIDENCY_VRAM_LIMIT_MB: VRAM running out on such a card).
if (const char* env = std::getenv("BB_ARENA_VRAM_ONLY"); env && env[0] == '1') {
arena_memory_type_bits = 1u << arena_memory_type_index;
} else {
const auto& properties = instance.GetMemoryProperties();
for (u32 i = 0; i < properties.memoryTypeCount; ++i) {
const auto flags = properties.memoryTypes[i].propertyFlags;
@@ -633,7 +589,7 @@ BufferCache::BufferCache(const Vulkan::Instance& instance_, Vulkan::Scheduler& s
const u64 bda_pagetable_size =
(blocks_per_arena_page * NUM_ARENA_PAGES) * sizeof(vk::DeviceAddress) *
(LayerActive() ? 2 : 1);
fault_manager = std::make_unique<FaultManager>(instance, scheduler, *this, block_shift,
fault_manager = std::make_unique<FaultBuffer>(instance, scheduler, *this, block_shift,
blocks_per_arena_page * NUM_ARENA_PAGES);
bda_pagetable_buffer = std::make_unique<Buffer>(
instance, 0, bda_pagetable_size, MemoryType::DeviceLocal, "BDA Page Table Buffer");
@@ -694,7 +650,7 @@ void BufferCache::ExtendWriteFault(VAddr device_addr, u64 guest_rip) {
const u64 kib = env ? std::strtoull(env, nullptr, 10) : 256;
return std::bit_ceil(std::clamp<u64>(kib, 4, 1024)) * 1024;
}();
if (window <= TRACKER_BYTES_PER_PAGE || BbToggle::Disabled(BbToggle::FaultWindow)) {
if (window <= BbMem::PageSize || BbToggle::Disabled(BbToggle::FaultWindow)) {
return;
}
memory_tracker->ExtendWriteFault(Common::AlignDown(device_addr, window), window);
@@ -724,8 +680,8 @@ void BufferCache::ReadMemory(VAddr device_addr, u64 size, bool is_write, bool as
// bbport (BB_GPU_WRITE_TWINS): bytes the GPU is still writing on the faulting page are
// twinned instead of waited for (see TwinsEnabled).
if (is_write && TwinsEnabled()) {
const VAddr page_begin = Common::AlignDown(device_addr, TRACKER_BYTES_PER_PAGE);
const VAddr page_end = Common::AlignUp(device_addr + size, TRACKER_BYTES_PER_PAGE);
const VAddr page_begin = Common::AlignDown(device_addr, BbMem::PageSize);
const VAddr page_end = Common::AlignUp(device_addr + size, BbMem::PageSize);
u64 tick = 0, gpu_bytes = 0;
gpu_modified_ranges.ForEachInRange(page_begin, page_end - page_begin,
[&](VAddr start, VAddr end) {
@@ -768,8 +724,8 @@ void BufferCache::ReadMemory(VAddr device_addr, u64 size, bool is_write, bool as
window_tick = std::max(window_tick, WriteTicks().Newest(start, end - start));
});
if (window_tick != 0 && !scheduler.IsFree(window_tick)) {
download_start = Common::AlignDown(device_addr, TRACKER_BYTES_PER_PAGE);
download_end = Common::AlignUp(device_addr + size, TRACKER_BYTES_PER_PAGE);
download_start = Common::AlignDown(device_addr, BbMem::PageSize);
download_end = Common::AlignUp(device_addr + size, BbMem::PageSize);
}
}
DownloadMemory(arena, download_start, download_end - download_start);
@@ -784,20 +740,27 @@ void BufferCache::ReadMemory(VAddr device_addr, u64 size, bool is_write, bool as
}
}
bool BufferCache::LayerReadTrapHit(VAddr address, bool assume_locks) {
bool BufferCache::LayerReadTrapHit(VAddr address, TrapCaller caller) {
if (!(BbLayer::WriteTraps::Reasons(address) & BbLayer::WriteTraps::VramData)) {
return false;
}
layer_read_trap_hits.fetch_add(1, std::memory_order_relaxed);
if (assume_locks) {
LayerCopyBackForCpu(address);
} else {
liverpool->SendCommand<true>([this, address] { LayerCopyBackForCpu(address); });
switch (caller) {
case TrapCaller::Guest:
// Commands run with the draw pipe drained (RunCommands): flushes and copies are safe.
liverpool->SendCommand<true>([this, address] { LayerCopyBackForCpu(address, true); });
break;
case TrapCaller::CommandThread:
LayerCopyBackForCpu(address, true); // the caller drained the draw pipe
break;
case TrapCaller::RecordingThread:
LayerCopyBackForCpu(address, false);
break;
}
return true;
}
void BufferCache::LayerCopyBackForCpu(VAddr address) {
void BufferCache::LayerCopyBackForCpu(VAddr address, bool may_record) {
if (!(BbLayer::WriteTraps::Reasons(address) & BbLayer::WriteTraps::VramData)) {
return; // copied back meanwhile
}
@@ -818,9 +781,23 @@ void BufferCache::LayerCopyBackForCpu(VAddr address) {
});
}
bool copied = copies.empty();
if (!copies.empty() && !may_record && tick != 0 && !scheduler.IsSubmitted(tick)) {
// The recording thread itself, before its own GPU writes are submitted: they cannot be
// waited for here. The page is read as it is in the game's memory (as without read
// traps); the GPU's data stays marked for later copies back.
layer_read_trap_stale.fetch_add(1, std::memory_order_relaxed);
std::scoped_lock lk{layer_read_trap_mutex};
BbLayer::WriteTraps::Set(from, block_size, BbLayer::WriteTraps::VramData, false);
layer_read_trapped.Subtract(from, block_size);
return;
}
if (!copies.empty()) {
if (tick != 0 && !scheduler.IsFree(tick)) {
scheduler.Wait(tick); // submitted, and done
if (may_record) {
scheduler.Wait(tick); // submitted, and done
} else {
scheduler.GetWorkSemaphore()->Wait(tick); // submitted already: done
}
}
auto& readback = SharedReadback(instance);
if (readback.Available() && !BbToggle::Disabled(BbToggle::ReadbackQueue)) {
@@ -836,14 +813,23 @@ void BufferCache::LayerCopyBackForCpu(VAddr address) {
}
if (copied) {
gpu_modified_ranges.Subtract(from, block_size);
} else {
} else if (may_record) {
// No readback queue: a copy in stream order, waited for.
LayerCopyBack(from >> block_shift, (from >> block_shift) + 1);
scheduler.Finish();
} else {
layer_read_trap_stale.fetch_add(1, std::memory_order_relaxed);
}
layer_read_trap_bytes.fetch_add(total, std::memory_order_relaxed);
} else if (layer_copy_back_tick != 0 && !scheduler.IsFree(layer_copy_back_tick)) {
scheduler.Wait(layer_copy_back_tick); // a copy back recorded is not done yet
// A copy back recorded is not done yet.
if (may_record) {
scheduler.Wait(layer_copy_back_tick);
} else if (scheduler.IsSubmitted(layer_copy_back_tick)) {
scheduler.GetWorkSemaphore()->Wait(layer_copy_back_tick);
} else {
layer_read_trap_stale.fetch_add(1, std::memory_order_relaxed);
}
}
std::scoped_lock lk{layer_read_trap_mutex};
BbLayer::WriteTraps::Set(from, block_size, BbLayer::WriteTraps::VramData, false);
@@ -1087,6 +1073,22 @@ std::pair<const Buffer*, u64> BufferCache::ObtainBuffer(VAddr device_addr, u32 s
};
// bbport BB_LAYER_MEMORY: the game's memory in place through its chunk buffer (the module
// says which); what is not whole in one chunk takes the paths below (VRAM copies).
if (LayerMode() && !is_written && !is_texel_buffer && !stats && BindMemoOn()) {
// A reading binding resolved before, with nothing changed since (BindMemo).
if (maintained_epoch != packet_epoch) {
Maintain();
}
LayerProcessTraps();
const BindMemo& memo = BindMemoSlot(device_addr, size);
if (memo.address == device_addr && memo.size == size &&
memo.generation == layer_generation &&
memo.second == BbStats::coarse_second.load(std::memory_order_relaxed) &&
memo.epoch == BbMem::cpu_change_epoch.load(std::memory_order_acquire)) {
(memo.vram ? BbStats::bound_vram_bytes : BbStats::bound_in_place_bytes)
.fetch_add(size, std::memory_order_relaxed);
return {memo.buffer, memo.offset};
}
}
if (LayerMode() && (is_written || size > STREAM_THRESHOLD || ConstantsInPlace() ||
IsRegionGpuModified(device_addr, size) || LayerMirrored(device_addr, size))) {
if (const auto bound = LayerBind(device_addr, size, is_written, is_texel_buffer)) {
@@ -1408,57 +1410,52 @@ void BufferCache::SynchronizeDmaBuffers() {
}
const Buffer* BufferCache::GetArena(u64 first_block, u64 last_block) {
// Arenas cover whole 4 GiB pages of the address space; a binding lies in at most two pages.
const u64 first_page = first_block >> blocks_per_arena_page_shift;
const u64 last_page = last_block >> blocks_per_arena_page_shift;
ASSERT_MSG(last_page - first_page <= 1,
"Buffer request cannot span more than two VA arena pages");
const auto* first_arena = address_space[first_page];
const auto* last_arena = address_space[last_page];
if (first_arena == last_arena) {
if (!first_arena) {
const u64 base_block = Common::AlignDownPow2<u64>(first_block, blocks_per_arena_page);
const u64 num_pages = last_page - first_page + 1;
const auto* new_arena =
&arenas.emplace_back(instance, base_block << block_shift,
num_pages << ARENA_PAGE_BITS, MemoryType::Sparse);
address_space[first_page] = new_arena;
address_space[last_page] = new_arena;
ASSERT_MSG(last_page - first_page <= 1, "a binding over more than two arena pages");
const Buffer* first = address_space[first_page];
const Buffer* last = address_space[last_page];
if (first == last) {
if (!first) {
first = &arenas.emplace_back(instance, first_page << ARENA_PAGE_BITS,
(last_page - first_page + 1) << ARENA_PAGE_BITS,
MemoryType::Sparse);
address_space[first_page] = first;
address_space[last_page] = first;
}
return address_space[first_page];
return first;
}
// Different arenas (or one and none): one arena over both, their resident blocks bound into
// it again.
LOG_WARNING(Render, "Migrating arena");
const u64 first_addr = first_arena ? first_arena->cpu_addr : (first_page << ARENA_PAGE_BITS);
const u64 first_size = first_arena ? first_arena->size_bytes : ARENA_PAGE_SIZE;
const u64 last_size = last_arena ? last_arena->size_bytes : ARENA_PAGE_SIZE;
const u64 base_block = first_addr >> block_shift;
const u64 total_size = first_size + last_size;
const u64 end_block = (first_addr + total_size) >> block_shift;
auto* new_arena = &arenas.emplace_back(instance, first_addr, total_size, MemoryType::Sparse);
auto* bind = BindsForArena(new_arena);
const VAddr base = first ? first->cpu_addr : first_page << ARENA_PAGE_BITS;
const u64 first_size = first ? first->size_bytes : ARENA_PAGE_SIZE;
const u64 last_size = last ? last->size_bytes : ARENA_PAGE_SIZE;
const u64 base_block = base >> block_shift;
const u64 end_block = (base + first_size + last_size) >> block_shift;
const Buffer* merged =
&arenas.emplace_back(instance, base, first_size + last_size, MemoryType::Sparse);
auto* binds = BindsForArena(merged);
resident_ranges.ForEachInRange(base_block, end_block, [&](const Backing& backing) {
const u64 start = std::max(base_block, backing.start);
const u64 end = std::min(end_block, backing.end);
bind->binds.push_back(vk::SparseMemoryBind{
binds->binds.push_back(vk::SparseMemoryBind{
.resourceOffset = (start - base_block) << block_shift,
.size = (end - start) << block_shift,
.memory = backing.memory,
.memoryOffset = (backing.offset + (start - backing.start)) << block_shift,
});
});
u64 base_page = first_addr >> ARENA_PAGE_BITS;
for (u32 page = 0; page < (first_size >> ARENA_PAGE_BITS); ++page) {
address_space[base_page + page] = new_arena;
}
base_page = last_page;
for (u32 page = 0; page < (last_size >> ARENA_PAGE_BITS); ++page) {
address_space[base_page + page] = new_arena;
}
return new_arena;
const auto assign = [&](u64 page, u64 bytes) {
for (u64 i = 0; i < (bytes >> ARENA_PAGE_BITS); ++i) {
address_space[page + i] = merged;
}
};
assign(base >> ARENA_PAGE_BITS, first_size);
assign(last_page, last_size);
return merged;
}
void BufferCache::EnsureResident(const Buffer* arena, u64 first_block, u64 last_block,
@@ -1500,30 +1497,49 @@ void BufferCache::EnsureResident(const Buffer* arena, u64 first_block, u64 last_
vram_blocks += range.end - range.start;
}
// bbport: the copies model with VRAM short: copies unused for 2 s make room first (their
// slots, block by block below) rather than a new chunk the driver may not have.
if (vram_blocks > free_slot_count && !GuestInPlace() && TextureCache::VramShort()) {
EvictIdleCopies(2, vram_blocks - free_slot_count);
}
if (vram_blocks != 0) {
// bbport: the slots of blocks that left VRAM first, block by block. A request of several
// blocks only ever took new space, and the free slots piled up while new 64 MiB chunks
// were allocated.
IntervalList rest_ranges;
for (const auto& range : bind_ranges) {
u64 block = range.start;
for (; block < range.end && free_slot_count != 0; ++block) {
const auto [memory, memory_offset] = AllocateResidency(block_size);
auto* last = binds.empty() ? nullptr : &binds.back();
if (last && last->end == block && last->memory == memory &&
last->offset + ((block - last->start) << block_shift) == memory_offset) {
last->end = block + 1;
} else {
binds.push_back({block, block + 1, memory, memory_offset});
const auto into_slots = [&](const auto& ranges) {
std::remove_cvref_t<decltype(ranges)> rest;
for (const auto& range : ranges) {
u64 block = range.start;
for (; block < range.end && free_slot_count != 0; ++block) {
const auto [memory, memory_offset] = AllocateResidency(block_size);
auto* last = binds.empty() ? nullptr : &binds.back();
if (last && last->end == block && last->memory == memory &&
last->offset + ((block - last->start) << block_shift) == memory_offset) {
last->end = block + 1;
} else {
binds.push_back({block, block + 1, memory, memory_offset});
}
--vram_blocks;
}
if (block < range.end) {
rest.Add({block, range.end});
}
--vram_blocks;
}
if (block < range.end) {
rest_ranges.Add({block, range.end});
}
}
return rest;
};
auto rest_ranges = into_slots(bind_ranges);
if (vram_blocks != 0) {
auto [memory, memory_offset] = AllocateResidency(vram_blocks << block_shift);
auto [memory, memory_offset] = AllocateResidency(vram_blocks << block_shift, !GuestInPlace());
if (!memory) {
// bbport: VRAM full and no other memory the arena takes (the copies model on
// NVIDIA: the game stopped, #142): copies unused for a second make room, the
// blocks go into their slots; only what is left after that needs new memory.
EvictIdleCopies(1, vram_blocks);
rest_ranges = into_slots(rest_ranges);
if (vram_blocks != 0) {
std::tie(memory, memory_offset) = AllocateResidency(vram_blocks << block_shift);
}
}
for (const auto& range : rest_ranges) {
binds.push_back({range.start, range.end, memory, memory_offset});
memory_offset += (range.end - range.start) << block_shift;
@@ -1567,7 +1583,7 @@ void BufferCache::EnsureResident(const Buffer* arena, u64 first_block, u64 last_
runtime.CopyBuffer(staging.buffer, bda_pagetable_buffer.get(), copies);
}
std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes) {
std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes, bool may_fail) {
if (bytes == block_size && free_slot_count != 0) {
// bbport: a slot of the fullest chunk: the emptier ones drain and go back to the driver.
auto best = residency_chunks.end();
@@ -1672,6 +1688,9 @@ std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes) {
}
}
}
if (!memory && may_fail) {
return {vk::DeviceMemory{}, 0}; // the caller makes room (EnsureResident)
}
ASSERT_MSG(memory, "Failed to allocate {} MiB for guest memory copies: VRAM is full", size >> 20);
residency_memory = memory;
residency_size = size;
@@ -1776,6 +1795,20 @@ void BufferCache::NoteResidencyMix(u64 first_block, u64 last_block, u64 size, bo
void BufferCache::NoteUse(VAddr address, u64 size) {
const u32 now = BbStats::coarse_second.load(std::memory_order_relaxed);
// The same range noted this second already: its groups are marked (a group demoted since
// is looked at again next second). Paged bindings span nearly all memory: thousands of
// groups each, for every draw.
struct Noted {
VAddr address = 0;
u64 size = 0;
u32 second = ~0u;
};
thread_local std::array<Noted, 256> noted{};
auto& memo = noted[((address >> 12) ^ (size * 0x9E3779B97F4A7C15ull >> 40)) & 255];
if (memo.address == address && memo.size == size && memo.second == now) {
return;
}
memo = {address, size, now};
const u64 last = std::min<u64>((address + size - 1) >> USE_GROUP_BITS, group_use.size() - 1);
for (u64 group = address >> USE_GROUP_BITS; group <= last; ++group) {
if (group_use[group] != now) {
@@ -1833,15 +1866,57 @@ void BufferCache::EvictVramBlock(u64 block) {
ReleaseResidencySlot(memory, offset);
// After the work submitted so far: it may still read the block.
bind_wait_tick = std::max(bind_wait_tick, scheduler.CurrentTick() - 1);
BbStats::vram_unmapped_bytes.fetch_add(block_size, std::memory_order_relaxed);
}
u64 BufferCache::EvictIdleCopies(u32 idle_seconds, u64 limit) {
const u32 now = BbStats::coarse_second.load(std::memory_order_relaxed);
std::vector<u64> idle;
for (const auto& range : resident_ranges) {
if (!residency_chunks.contains(static_cast<VkDeviceMemory>(range.memory))) {
continue;
}
for (u64 block = range.start; block < range.end && idle.size() < limit; ++block) {
if (now - group_use[(block << block_shift) >> USE_GROUP_BITS] >= idle_seconds &&
!IsRegionGpuModified(block << block_shift, block_size)) {
idle.push_back(block);
}
}
if (idle.size() >= limit) {
break;
}
}
for (const u64 block : idle) {
EvictVramBlock(block);
}
if (!idle.empty()) {
BbStats::vram_idle_bytes.fetch_add(idle.size() * block_size, std::memory_order_relaxed);
}
return idle.size();
}
void BufferCache::ProcessIdleBlocks() {
const u32 now = BbStats::coarse_second.load(std::memory_order_relaxed);
if (now == idle_scan_second || !GuestInPlace() || VramIdleSeconds() == 0) {
if (now == idle_scan_second || VramIdleSeconds() == 0) {
return;
}
idle_scan_second = now;
if (!GuestInPlace()) {
// bbport: the copies model kept every block it ever copied to VRAM: on 4 GB cards VRAM
// filled up area by area and the game stopped after an hour or two ("Failed to allocate
// .. for guest memory copies", #142). On cards up to 8 GB copies unused for a minute
// leave VRAM (their slots take the next ones), on any card for 5 s while it is short;
// chunks emptied so go back to the driver. Larger cards keep them (no uploads again).
// BB_VRAM_IDLE_SECONDS set: on any card.
static const bool small_vram =
instance.GetTotalMemoryBudget() <= (8ull << 30) || std::getenv("BB_VRAM_IDLE_SECONDS");
if (TextureCache::VramShort()) {
EvictIdleCopies(5, ~u64{0});
} else if (small_vram) {
EvictIdleCopies(VramIdleSeconds(), ~u64{0});
}
FreeEmptyChunks(now);
return;
}
if (LayerMode()) {
LayerMaintain();
LayerProcessIdle();
@@ -1895,6 +1970,10 @@ void BufferCache::ProcessIdleBlocks() {
}
}
}
FreeEmptyChunks(now);
}
void BufferCache::FreeEmptyChunks(u32 now) {
// Chunks without blocks for 5 s, the rebinds away from them long done: back to the driver.
for (auto it = residency_chunks.begin(); it != residency_chunks.end();) {
const ResidencyChunk& chunk = it->second;
@@ -2173,6 +2252,32 @@ std::optional<std::pair<const Buffer*, u64>> BufferCache::GuestChunkSource(VAddr
std::optional<std::pair<const Buffer*, u64>> BufferCache::LayerInPlace(VAddr address, u64 size,
bool clamp_huge) {
// Where a range is in place changes only when the game unmaps memory (counted when queued,
// UnmapInPlace); each answer took a walk of its mappings (~2% of the draw recording thread).
// Only ranges found in place: a mapping made later at a range not mapped before counts.
struct Memo {
VAddr address = 0;
u64 size = 0, unmaps = 0;
bool clamp_huge = false;
std::pair<const Buffer*, u64> result{};
};
thread_local auto memo = std::make_unique<std::array<Memo, 1024>>(); // heap: see LayerBind
auto& m = (*memo)[((address * 0x9E3779B97F4A7C15ull) ^ (size * 0xC2B2AE3D27D4EB4Full)) >> 54];
const u64 unmaps = unmaps_queued.load(std::memory_order_acquire) + 1;
if (m.address == address && m.size == size && m.unmaps == unmaps &&
m.clamp_huge == clamp_huge) {
return m.result;
}
const auto result = LayerInPlaceUncached(address, size, clamp_huge);
if (result) {
m = {address, size, unmaps, clamp_huge, *result};
}
return result;
}
std::optional<std::pair<const Buffer*, u64>> BufferCache::LayerInPlaceUncached(VAddr address,
u64 size,
bool clamp_huge) {
auto span = BbLayer::GpuMemory::Get().ResolvePrefix(address, size);
if (!span) {
return std::nullopt;
@@ -2320,6 +2425,36 @@ void BufferCache::ProcessPendingAssets() {
continue;
}
asset_bytes.Add({addr, addr + size});
// bbport: a block the GPU once wrote in place (dynamic for good) that the loaders have now
// written whole holds the CPU's data only: it may have a VRAM copy again. Kept, such
// blocks piled up as areas were loaded over memory the last one's GPU buffers had used
// (164 -> 1117 MiB of dynamic blocks after a few teleports), and one of them in a range
// kept the whole range in place (a 7 MiB binding read over the bus for one such block of
// 110): the same spot went from 76.6 to 55.3 FPS. The GPU's writes there stay in place
// from now on (layer_cpu_blocks), so the CPU's view (the translator reads shader code and
// descriptors there) is never left behind a VRAM copy. Blocks the CPU writes often
// (layer_cpu_hot) stay dynamic. BB_LAYER_RELOAD_RESET=0: as before.
static const bool reload_reset = [] {
const char* env = std::getenv("BB_LAYER_RELOAD_RESET");
return !(env && env[0] == '0');
}();
if (LayerMode() && reload_reset) {
for (u64 block = first; block < end; ++block) {
if (!dynamic_blocks.Contains(block) || layer_cpu_hot.Contains(block, block + 1)) {
continue;
}
bool whole = true;
asset_bytes.ForEachGap(block << block_shift, (block + 1) << block_shift,
[&](u64, u64) { whole = false; });
if (!whole) {
continue;
}
layer_cpu_blocks.Add({block, block + 1});
dynamic_blocks.Subtract(block, block + 1);
layer_write_bounces.Subtract(block, block + 1);
BbStats::dynamic_reloaded_bytes.fetch_add(block_size, std::memory_order_relaxed);
}
}
for (u64 block = first; block < end; ++block) {
const bool in_place =
LayerMode() ? !BbLayer::GpuMemory::Get().AnyValid(block, block + 1)
@@ -2968,6 +3103,7 @@ void BufferCache::UnmapInPlace(VAddr addr, u64 size) {
if (!GuestInPlace() || size == 0) {
return;
}
unmaps_queued.fetch_add(1, std::memory_order_release);
std::scoped_lock lk{pending_unmaps_mutex};
pending_unmaps.emplace_back(addr, size);
unmaps_pending.store(true, std::memory_order_release);
@@ -3008,6 +3144,41 @@ void BufferCache::ProcessPendingUnmaps() {
}
asset_bytes.Subtract(first << block_shift, end << block_shift);
gpu_written_bytes.Subtract(first << block_shift, end << block_shift);
// bbport: blocks wholly unmapped hold nobody's data: whatever kept them in place (the
// CPU wrote them, the GPU wrote them in place, their watch failed) is over. Memory mapped
// there again starts as new. BB_LAYER_UNMAP_RESET=0: as before.
static const bool unmap_reset = [] {
const char* env = std::getenv("BB_LAYER_UNMAP_RESET");
return !(env && env[0] == '0');
}();
static std::atomic<u64> unmap_reset_bytes{0};
if (const u64 whole_first = (addr + block_size - 1) >> block_shift,
whole_end = (addr + size) >> block_shift;
unmap_reset && whole_first < whole_end) {
const u64 total = unmap_reset_bytes.fetch_add((whole_end - whole_first) << block_shift) +
((whole_end - whole_first) << block_shift);
if ((total >> 26) != ((total - ((whole_end - whole_first) << block_shift)) >> 26)) {
std::printf("Guest memory: %llu MiB of unmapped blocks started over\n",
(unsigned long long)(total >> 20));
}
dynamic_blocks.Subtract(whole_first, whole_end);
if (LayerMode()) {
layer_cpu_blocks.Subtract(whole_first, whole_end);
layer_cpu_hot.Subtract(whole_first, whole_end);
layer_watch_failed.Subtract(whole_first, whole_end);
layer_write_bounces.Subtract(whole_first, whole_end);
layer_watched.Subtract(whole_first, whole_end);
layer_edge_blocks.Subtract(whole_first, whole_end);
layer_volatile.Subtract(whole_first, whole_end);
for (u64 block = whole_first; block < whole_end; ++block) {
layer_watch_strikes.erase(block);
}
}
std::scoped_lock dynamic_lock{dynamic_mutex};
for (u64 block = whole_first; block < whole_end; ++block) {
cpu_write_frames.erase(block);
}
}
copy_back_blocks.Subtract(first, end);
promote_candidates.Subtract(first, end);
idle_demoted.Subtract(first, end);
@@ -3038,6 +3209,9 @@ void BufferCache::ProcessPendingUnmaps() {
// to the next blocks moved there; uploaded again if mapped and used).
for (u64 block = (addr + block_size - 1) >> block_shift; block < (addr + size) >> block_shift;
++block) {
if (resident_ranges.Contains(block)) {
BbStats::vram_unmapped_bytes.fetch_add(block_size, std::memory_order_relaxed);
}
EvictVramBlock(block);
}
}
@@ -3442,8 +3616,8 @@ void BufferCache::Preupload(u64 budget) {
at = start;
}
while (at < end && visited < MaxRegions) {
const VAddr region = at & ~(TRACKER_HIGHER_PAGE_SIZE - 1);
const VAddr stop = std::min(end, region + TRACKER_HIGHER_PAGE_SIZE);
const VAddr region = at & ~(BbMem::RegionSize - 1);
const VAddr stop = std::min(end, region + BbMem::RegionSize);
++visited;
preupload_cursor = stop;
if (memory_tracker->PreuploadCandidate(region, quiet_since, !full)) {
@@ -3583,7 +3757,18 @@ void BufferCache::ReportArenaBinds() {
namespace VideoCore {
void BufferCache::NoteWriteTick(VAddr address, u64 size) {
WriteTicks().Note(address, size, scheduler.CurrentTick());
// The same range again in the same submission changes nothing: the copy shader's paged
// bindings over nearly all memory (~60 a frame, ~10 a submission) were a store per MiB each.
thread_local VAddr last_address = 0;
thread_local u64 last_size = 0, last_tick = 0;
const u64 tick = scheduler.CurrentTick();
if (address == last_address && size == last_size && tick == last_tick) {
return;
}
last_address = address;
last_size = size;
last_tick = tick;
WriteTicks().Note(address, size, tick);
}
} // namespace VideoCore
@@ -1,8 +1,20 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport buffers: the buffer cache: guest memory bound for the GPU. Ranges lie in sparse arenas
// (one per 4 GiB of the address space, blocks bound on demand: VRAM copies uploaded from the
// guest, or the game's own memory in place), in the layer's memory module (chunk buffers and VRAM
// mirrors), or are copied per draw into the stream buffer (small constants). See
// buffer_cache.cpp and layer/buffer_cache_layer.cpp.
#pragma once
namespace BbCp {
class CommandProcessor;
}
namespace BbMem {
class PageWatch;
class DirtyTracker;
} // namespace BbMem
#include "bbport_copy.h"
#include <atomic>
@@ -18,14 +30,11 @@
#include "common/interval_set.h"
#include "common/types.h"
#include "video_core/buffer_cache/buffer.h"
#include "video_core/buffer_cache/fault_manager.h"
#include "video_core/buffer_cache/range_set.h"
#include "video_core/renderer_vulkan/vk_semaphore.h"
#include "buffers/buffer.h"
#include "buffers/fault_buffer.h"
#include "buffers/range_set.h"
#include "vulkan/timeline.h"
namespace AmdGpu {
struct Liverpool;
}
namespace Core {
class MemoryManager;
@@ -41,8 +50,8 @@ class StagingBufferPool;
namespace VideoCore {
class TextureCache;
class MemoryTracker;
class PageManager;
/// bbport: an IntervalList whose every change advances `generation`: lookups of the buffer
/// cache's residency remember their answers for as long as nothing changed (ResidencyMemo).
@@ -77,8 +86,8 @@ public:
static constexpr u64 STREAM_THRESHOLD = 16_KB;
explicit BufferCache(const Vulkan::Instance& instance, Vulkan::Scheduler& scheduler,
Vulkan::Runtime& runtime, AmdGpu::Liverpool* liverpool,
TextureCache& texture_cache, PageManager& tracker);
Vulkan::Runtime& runtime, BbCp::CommandProcessor* liverpool,
TextureCache& texture_cache, BbMem::PageWatch& tracker);
~BufferCache();
/// Returns a pointer to GDS device local buffer.
@@ -115,10 +124,15 @@ public:
/// Flushes any GPU modified buffer in the logical page range back to CPU memory.
void ReadMemory(VAddr device_addr, u64 size, bool is_write = false, bool assume_locks = false);
/// BB_LAYER_PROBE (diagnostics): a CPU access to a probed page, counted; the trap goes.
bool LayerProbeHit(VAddr address, u64 rip, bool write, bool gpu_thread);
/// Who hit a read trap: a guest (or runtime) thread, the GPU command thread (stage A, the
/// draw pipe drained) or the draw recording thread (stage B: it may not record or flush).
enum class TrapCaller { Guest, CommandThread, RecordingThread };
/// bbport BB_LAYER_READ_TRAPS: a CPU access hit a page whose newest data the GPU wrote into a
/// VRAM copy only: the block's GPU data is copied back into the game's memory first (on the
/// GPU command thread unless `assume_locks`). False: not such a page.
bool LayerReadTrapHit(VAddr address, bool assume_locks);
/// VRAM copy only: the block's GPU data is copied back into the game's memory first (a guest's
/// access on the GPU command thread). False: not such a page.
bool LayerReadTrapHit(VAddr address, TrapCaller caller);
/// bbport: a new draw or dispatch is being recorded: the per-binding housekeeping of
/// EnsureResident (unmaps, demotions, assets, late writes) runs once for it, not per binding.
@@ -235,6 +249,8 @@ public:
/// the part in place (the descriptor range is clamped to the buffer).
std::optional<std::pair<const Buffer*, u64>> LayerInPlace(VAddr address, u64 size,
bool clamp_huge = false);
std::optional<std::pair<const Buffer*, u64>> LayerInPlaceUncached(VAddr address, u64 size,
bool clamp_huge);
/// Whether any part of the range is.
[[nodiscard]] bool IsAnyInPlace(VAddr addr, u64 size) const {
if (LayerActive() && size != 0) {
@@ -344,7 +360,8 @@ private:
void QueuePromotions(u64 submitted);
void ReportInPlaceBlocks(); ///< bbport BB_RESIDENCY_REPORT (diagnostics)
/// Arena residency memory from the 64 MiB blocks: the memory and the byte offset in it.
std::pair<vk::DeviceMemory, u64> AllocateResidency(u64 bytes);
/// `may_fail`: an empty memory when nothing can be allocated, instead of stopping.
std::pair<vk::DeviceMemory, u64> AllocateResidency(u64 bytes, bool may_fail = false);
/// bbport: BbStats::residency_unused_bytes after the free list or the current block changed.
void NoteResidencyUnused();
/// bbport: idle VRAM blocks (see group_use).
@@ -356,6 +373,12 @@ private:
void ReleaseResidencySlot(vk::DeviceMemory memory, u64 offset);
/// Unbinds a VRAM block of memory the game unmapped (uploaded again if mapped and used).
void EvictVramBlock(u64 block);
/// bbport: the copies model (not in place): VRAM copies the GPU has not used for `idle_seconds`
/// and holds no GPU-written data in leave VRAM (at most `limit` blocks; uploaded again when
/// used). Returns how many left.
u64 EvictIdleCopies(u32 idle_seconds, u64 limit);
/// bbport: chunks without blocks for 5 s, their rebinds long done: back to the driver.
void FreeEmptyChunks(u32 now);
[[nodiscard]] static u32 VramIdleSeconds();
void DownloadMemory(const Buffer* arena, VAddr device_addr, u64 size);
@@ -376,6 +399,21 @@ private:
/// bbport BB_LAYER_MEMORY (buffer_cache_layer.cpp): a binding through the layer's memory
/// module: a mirror in VRAM, or in place (empty: neither; the arena takes it). may_promote
/// false (image uploads): a mirror only where the range is in VRAM already, none made for it.
/// bbport: reading bindings LayerBind resolved to a whole mirror or in place, reused while
/// nothing moved (layer_generation), the CPU marked nothing modified anywhere
/// (BbMem::cpu_change_epoch: no upload can be due) and within the same second (the idle
/// tracking's use marks are per second). BB_BIND_MEMO=0: every binding takes the full path.
struct BindMemo {
VAddr address = 0;
u64 size = 0, generation = 0, epoch = 0, offset = 0;
const Buffer* buffer = nullptr;
u32 second = 0;
bool vram = false;
};
static BindMemo& BindMemoSlot(VAddr address, u64 size);
static bool BindMemoOn();
void StoreBindMemo(VAddr address, u64 size, u64 epoch, const Buffer* buffer, u64 offset,
bool vram);
std::optional<std::pair<const Buffer*, u64>> LayerBind(VAddr address, u64 size, bool is_written,
bool is_texel_buffer,
bool may_promote = true);
@@ -455,16 +493,16 @@ private:
std::array<ImageMiss, 256> image_miss_cache{};
Vulkan::Runtime& runtime;
Vulkan::StagingBufferPool& staging_pool;
AmdGpu::Liverpool* liverpool;
BbCp::CommandProcessor* liverpool;
Core::MemoryManager* memory;
TextureCache& texture_cache;
std::unique_ptr<MemoryTracker> memory_tracker;
std::unique_ptr<BbMem::DirtyTracker> memory_tracker;
StreamBuffer stream_buffer;
Buffer gds_buffer;
RangeSet gpu_modified_ranges;
std::unique_ptr<FaultManager> fault_manager;
std::unique_ptr<FaultBuffer> fault_manager;
std::unique_ptr<Buffer> bda_pagetable_buffer;
std::array<const Buffer*, NUM_ARENA_PAGES> address_space{};
@@ -502,6 +540,11 @@ private:
/// in VRAM (layer_cpu_blocks). On NVIDIA shaders reading the game's memory over the bus were
/// slow where the model of 0.3 had copies in VRAM.
IntervalList<> layer_watched;
/// Blocks a range the GPU writes covers only in part (its first and last, the rest of them
/// unannounced bytes), moved to VRAM with it all the same (LayerBind): a trap catches the CPU
/// writing beside it, and the GPU's bytes there are copied back before the block goes in place.
/// A frame-sized buffer of 305 blocks went in place for two such blocks (#149).
IntervalList<> layer_edge_blocks;
/// Watched blocks that went back in place for writes (LayerDemote): not watched again.
IntervalList<> layer_watch_failed;
std::unordered_map<u64, u8> layer_watch_strikes; ///< watched blocks sent back for requests
@@ -526,7 +569,20 @@ private:
std::atomic<u64> layer_read_trap_hits{0}, layer_read_trap_bytes{0};
static bool LayerReadTrapsOn();
void LayerArmReadTraps(VAddr address, u64 size);
void LayerCopyBackForCpu(VAddr address);
/// BB_LAYER_PROBE=1 (diagnostics): which CPU code touches blocks the GPU writes in place for
/// the CPU's sake (layer_cpu_blocks), and how soon. Nothing moves; see LayerArmProbe.
static bool LayerProbeOn();
void LayerArmProbe(VAddr address, u64 size);
void LayerPrintProbe();
/// BB_LAYER_VERIFY=1 (diagnostics): a VRAM copy read while the game's bytes changed and no
/// trap or announced write said so (a stale copy) is reported with what is known of the block.
static bool LayerVerifyOn();
void LayerVerifyMirror(VAddr address, u64 size, bool is_texel_buffer);
void LayerVerifyForget(u64 first, u64 end);
/// `may_record`: the scheduler may be flushed and copies recorded (not on the recording
/// thread, which may be inside a render pass with its own commands half recorded).
void LayerCopyBackForCpu(VAddr address, bool may_record);
std::atomic<u64> layer_read_trap_stale{0}; ///< recording thread hits on unsubmitted GPU data
u64 layer_watch_refreshes = 0; ///< statistics: watched blocks uploaded again
/// Statistics: trap hits announced; by the game's code on watched / other blocks; by other
/// code on watched / other blocks.
@@ -550,6 +606,7 @@ private:
std::mutex pending_unmaps_mutex;
std::vector<std::pair<VAddr, u64>> pending_unmaps;
std::atomic<bool> unmaps_pending{false};
std::atomic<u64> unmaps_queued{0}; ///< every UnmapInPlace (LayerInPlace's memo)
std::mutex dynamic_mutex;
std::unordered_map<u64, std::pair<u32, u32>> cpu_write_frames; ///< block -> last frame, frames
std::unordered_map<u64, std::pair<u32, u32>> gpu_write_frames; ///< written range -> last frame, frames
+159
View File
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "buffers/fault_buffer.h"
#include <cstring>
#include <limits>
#include "common/div_ceil.h"
#include "common/logging/log.h"
#include "buffers/buffer_cache.h"
#include "vulkan/instance.h"
#include "vulkan/platform.h"
#include "render/scheduler.h"
#include "vulkan/shader_module.h"
#include "bbgpu/shaders/fault_scan_comp.h"
namespace VideoCore {
namespace {
constexpr u32 MaxFaults = 1024;
constexpr u64 AreaSize = MaxFaults * sizeof(u64);
} // namespace
FaultBuffer::FaultBuffer(const Vulkan::Instance& instance, Vulkan::Scheduler& scheduler_,
BufferCache& buffer_cache_, u32 page_bits, u64 num_pages_)
: scheduler{scheduler_}, buffer_cache{buffer_cache_}, page_size{1ull << page_bits},
num_pages{num_pages_}, bits_size{num_pages_ / 8},
bits{instance, 0, bits_size, MemoryType::DeviceLocal},
readback{instance, 0, MaxPendingReads * AreaSize, MemoryType::HostCached} {
const auto device = instance.GetDevice();
Vulkan::SetObjectName(device, bits.Handle(), "Fault Buffer");
const auto binding = [](u32 index) {
return vk::DescriptorSetLayoutBinding{
.binding = index,
.descriptorType = vk::DescriptorType::eStorageBuffer,
.descriptorCount = 1,
.stageFlags = vk::ShaderStageFlagBits::eCompute,
};
};
const std::array bindings = {binding(0), binding(1)};
set_layout = Vulkan::Check(device.createDescriptorSetLayoutUnique({
.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR,
.bindingCount = u32(bindings.size()),
.pBindings = bindings.data(),
}));
const vk::DescriptorSetLayout set = *set_layout;
pipeline_layout =
Vulkan::Check(device.createPipelineLayoutUnique({.setLayoutCount = 1, .pSetLayouts = &set}));
const std::array<u32, 2> constants{page_bits, MaxFaults};
const std::array<vk::SpecializationMapEntry, 2> entries{{{0, 0, 4}, {1, 4, 4}}};
const vk::SpecializationInfo specialization{
.mapEntryCount = u32(entries.size()),
.pMapEntries = entries.data(),
.dataSize = sizeof(constants),
.pData = constants.data(),
};
const auto module = Vulkan::CompileSPV(FAULT_SCAN_COMP, device);
Vulkan::SetObjectName(device, module, "Fault Buffer Parser");
pipeline = Vulkan::Check(device.createComputePipelineUnique(
{}, vk::ComputePipelineCreateInfo{
.stage{
.stage = vk::ShaderStageFlagBits::eCompute,
.module = module,
.pName = "main",
.pSpecializationInfo = &specialization,
},
.layout = *pipeline_layout,
}));
Vulkan::SetObjectName(device, *pipeline, "Fault Buffer Parser Pipeline");
device.destroyShaderModule(module);
}
void FaultBuffer::ProcessFaultBuffer() {
// The area is free once the GPU is done with its last scan and the results were read.
if (const u64 tick = area_ticks[next_area]) {
scheduler.Wait(tick);
scheduler.PopPendingOperations();
}
const u32 area = next_area;
const u64 offset = area * AreaSize;
u8* results = readback.mapped_data.data() + offset;
std::memset(results, 0, AreaSize);
const auto barrier = [&](vk::PipelineStageFlags2 src_stage, vk::AccessFlags2 src_access,
vk::PipelineStageFlags2 dst_stage, vk::AccessFlags2 dst_access) {
return vk::BufferMemoryBarrier2{
.srcStageMask = src_stage,
.srcAccessMask = src_access,
.dstStageMask = dst_stage,
.dstAccessMask = dst_access,
.buffer = bits.Handle(),
.offset = 0,
.size = bits_size,
};
};
const auto before = barrier(vk::PipelineStageFlagBits2::eAllCommands,
vk::AccessFlagBits2::eShaderWrite,
vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderRead);
const auto after = barrier(vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderWrite,
vk::PipelineStageFlagBits2::eAllCommands,
vk::AccessFlagBits2::eShaderWrite);
const vk::DescriptorBufferInfo bits_info{bits.Handle(), 0, bits_size};
const vk::DescriptorBufferInfo results_info{readback.Handle(), offset, AreaSize};
const std::array writes = {
vk::WriteDescriptorSet{
.dstBinding = 0,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageBuffer,
.pBufferInfo = &bits_info,
},
vk::WriteDescriptorSet{
.dstBinding = 1,
.descriptorCount = 1,
.descriptorType = vk::DescriptorType::eStorageBuffer,
.pBufferInfo = &results_info,
},
};
scheduler.EndRendering();
const auto cmd = scheduler.CommandBuffer();
const Vulkan::Breadcrumbs::Scope crumb{cmd, scheduler.CrumbStream(), "fault buffer"};
cmd.pipelineBarrier2({
.dependencyFlags = vk::DependencyFlagBits::eByRegion,
.bufferMemoryBarrierCount = 1,
.pBufferMemoryBarriers = &before,
});
cmd.bindPipeline(vk::PipelineBindPoint::eCompute, *pipeline);
cmd.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, *pipeline_layout, 0, writes);
// A word (32 pages) per invocation, 64 per workgroup.
cmd.dispatch(Common::DivCeil(u32(num_pages / 32), 64u), 1, 1);
cmd.pipelineBarrier2({
.dependencyFlags = vk::DependencyFlagBits::eByRegion,
.bufferMemoryBarrierCount = 1,
.pBufferMemoryBarriers = &after,
});
scheduler.DeferOperation([this, results, area] {
faulted.Clear();
const auto* entries = reinterpret_cast<const u64*>(results);
const u32 count = u32(entries[0]);
for (u32 i = 1; i <= count && i < MaxFaults; ++i) {
faulted.Add(entries[i], page_size);
LOG_INFO(Render_Vulkan, "Accessed non-GPU cached memory at {:#x}", entries[i]);
}
faulted.ForEach([&](VAddr start, VAddr end) {
ASSERT_MSG(end - start <= std::numeric_limits<u32>::max(), "fault range too large");
(void)buffer_cache.ObtainBuffer(start, u32(end - start), false);
});
area_ticks[area] = 0;
});
area_ticks[next_area] = scheduler.CurrentTick();
next_area = (next_area + 1) % MaxPendingReads;
}
} // namespace VideoCore
+46
View File
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport buffers: shaders that reach memory through device addresses mark the pages the buffer
// cache does not hold in a bit buffer; once per submission the bits are collected on the GPU
// (buffers/shaders/fault_scan.comp) into a host buffer, and the pages are brought in when the GPU
// is done with it.
#pragma once
#include <array>
#include "buffers/buffer.h"
#include "buffers/range_set.h"
namespace VideoCore {
class BufferCache;
class FaultBuffer {
public:
FaultBuffer(const Vulkan::Instance& instance, Vulkan::Scheduler& scheduler,
BufferCache& buffer_cache, u32 page_bits, u64 num_pages);
[[nodiscard]] Buffer* GetFaultBuffer() noexcept {
return &bits;
}
/// Collects the marked pages (results read back when the GPU finishes them).
void ProcessFaultBuffer();
private:
static constexpr size_t MaxPendingReads = 8;
Vulkan::Scheduler& scheduler;
BufferCache& buffer_cache;
u64 page_size;
u64 num_pages;
u64 bits_size;
Buffer bits; ///< one bit per page
Buffer readback; ///< MaxPendingReads areas of a count and page addresses
std::array<u64, MaxPendingReads> area_ticks{};
u32 next_area = 0;
RangeSet faulted;
vk::UniqueDescriptorSetLayout set_layout;
vk::UniquePipelineLayout pipeline_layout;
vk::UniquePipeline pipeline;
};
} // namespace VideoCore
+57
View File
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "buffers/memory_model.h"
#include <cstdio>
#include <cstdlib>
#include "bbport_toggles.h"
namespace VideoCore {
namespace {
bool EnvFlag(const char* name) {
const char* env = std::getenv(name);
return env && env[0] == '1';
}
} // namespace
void DisableGuestInPlace() {
Detail::guest_in_place.store(2, std::memory_order_relaxed);
Detail::write_tracking.store(0, std::memory_order_relaxed); // recomputed: on now
}
bool Detail::ComputeGuestInPlace() {
static const bool requested = [] {
const bool on = EnvFlag("BB_GUEST_IN_PLACE");
if (on) {
std::printf("Guest memory: the GPU uses the game's direct memory in place "
"(BB_GUEST_IN_PLACE=1)\n");
}
return on;
}();
// DisableGuestInPlace may have come first: it stays off then.
u8 unknown = 0;
guest_in_place.compare_exchange_strong(unknown, requested ? 1 : 2, std::memory_order_relaxed);
return guest_in_place.load(std::memory_order_relaxed) == 1;
}
bool Detail::ComputeWriteTracking() {
static const bool forced = EnvFlag("BB_WRITE_TRACKING");
const bool on = forced || !GuestInPlace();
write_tracking.store(on ? 1 : 2, std::memory_order_relaxed);
return on;
}
bool WriteVerify() {
static const bool on = EnvFlag("BB_WRITE_VERIFY");
return on && !WriteTracking();
}
bool ConstantsInPlace() {
static const bool on = EnvFlag("BB_CONSTANTS_IN_PLACE");
return GuestInPlace() && on != BbToggle::Experiment(1);
}
} // namespace VideoCore
+42
View File
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport buffers: how the GPU side reaches the game's memory. In place (BB_GUEST_IN_PLACE=1): the
// GPU uses the game's direct memory where it is (its chunks are exported to Vulkan); else copies
// in VRAM. Asked by every buffer binding: the answers are latched in atomics.
#pragma once
#include <atomic>
#include "common/types.h"
namespace VideoCore {
namespace Detail {
/// 0 not known yet, 1 on, 2 off.
inline std::atomic<u8> guest_in_place{0}, write_tracking{0};
bool ComputeGuestInPlace();
bool ComputeWriteTracking();
} // namespace Detail
/// The GPU uses the game's memory in place (bound to the chunks it lives in).
inline bool GuestInPlace() {
const u8 state = Detail::guest_in_place.load(std::memory_order_relaxed);
return state != 0 ? state == 1 : Detail::ComputeGuestInPlace();
}
/// The driver cannot bind the game's memory: in place stays off from now on.
void DisableGuestInPlace();
/// CPU writes to GPU memory caught by page protection. Off in place: the GPU side hears of writes
/// from the writers (file reads, the game's loaders, its DMA and command writes);
/// BB_WRITE_TRACKING=1 brings the protection back.
inline bool WriteTracking() {
const u8 state = Detail::write_tracking.load(std::memory_order_relaxed);
return state != 0 ? state == 1 : Detail::ComputeWriteTracking();
}
/// BB_WRITE_VERIFY=1 (diagnostics, in place): watched pages are protected as well, so writes
/// nothing announced show up as write faults.
bool WriteVerify();
/// BB_CONSTANTS_IN_PLACE=1 (in place): small read-only buffers (constants) are read where the game
/// wrote them, when the draw executes, instead of being copied when the draw is decoded or
/// recorded. Experiment bit 1 of BB_TOGGLE_FILE inverts it (A/B while the game runs).
bool ConstantsInPlace();
} // namespace VideoCore
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport buffers: a set of address ranges (half-open, touching ones joined), by address and size.
#pragma once
#include <algorithm>
#include "common/interval_set.h"
#include "common/types.h"
namespace VideoCore {
class RangeSet {
public:
void Add(VAddr address, u64 size) {
ranges.Add({address, address + size});
}
void Subtract(VAddr address, u64 size) {
ranges.Subtract(address, address + size);
}
void Clear() {
ranges.Clear();
}
/// All of [address, address + size) is in the set (an empty range always is).
bool Contains(VAddr address, u64 size) const {
return size == 0 || ranges.Contains(address, address + size);
}
bool Intersects(VAddr address, u64 size) const {
return ranges.Overlaps(address, address + size);
}
/// func(start, end) for each range.
void ForEach(auto&& func) const {
for (const Interval& range : ranges) {
func(range.start, range.end);
}
}
/// func(start, end) for each part of [address, address + size) in the set.
void ForEachInRange(VAddr address, u64 size, auto&& func) const {
const VAddr end = address + size;
ranges.ForEachInRange(address, end, [&](const Interval& range) {
func(std::max(range.start, address), std::min(range.end, end));
});
}
/// func(start, size) for each part of [address, address + size) not in the set.
void ForEachNotInRange(VAddr address, u64 size, auto&& func) const {
ranges.ForEachGap(address, address + size,
[&](u64 start, u64 end) { func(start, end - start); });
}
private:
IntervalList<> ranges;
};
} // namespace VideoCore
+28
View File
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport buffers: lists the pages shaders reached through device addresses the buffer cache does
// not hold (their bits set in the fault buffer, one per page) and clears the bits. Output: a count
// in the first entry, then up to MAX_FAULTS page addresses.
#version 450
#extension GL_ARB_gpu_shader_int64 : enable
layout(constant_id = 0) const uint PAGE_BITS = 12;
layout(constant_id = 1) const uint MAX_FAULTS = 1024;
layout(local_size_x = 64) in;
layout(std430, binding = 0) buffer Faults { uint fault_bits[]; };
layout(std430, binding = 1) buffer Pages { uint64_t pages[]; };
layout(std430, binding = 1) buffer Count { uint count; };
void main() {
const uint word_index = gl_GlobalInvocationID.x;
uint word = fault_bits[word_index];
fault_bits[word_index] = 0u;
for (; word != 0u; word &= word - 1u) {
const uint slot = atomicAdd(count, 1u) + 1u;
if (slot >= MAX_FAULTS) {
return;
}
pages[slot] = uint64_t(word_index * 32u + findLSB(word)) << PAGE_BITS;
}
}
@@ -1,11 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: glibc's adaptive mutex: spins a little before sleeping, for locks held for a moment.
#pragma once
#if __unix__
#include <pthread.h>
#endif
namespace Common {
@@ -32,6 +29,6 @@ public:
private:
pthread_mutex_t mutex = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
};
#endif // PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP
#endif
} // namespace Common
@@ -1,6 +1,5 @@
// SPDX-FileCopyrightText: 2014 Jannik Vogel <[email protected]>
// SPDX-License-Identifier: CC0-1.0
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: rounding to multiples.
#pragma once
#include <cstddef>
@@ -8,52 +7,56 @@
namespace Common {
/// `value` rounded up to a multiple of `size` (any size).
template <typename T>
[[nodiscard]] constexpr T AlignUp(T value, std::size_t size) {
static_assert(std::is_unsigned_v<T>, "T must be an unsigned value.");
auto mod{static_cast<T>(value % size)};
value -= mod;
return static_cast<T>(mod == T{0} ? value : value + size);
static_assert(std::is_unsigned_v<T>, "AlignUp needs an unsigned type");
const T rest = static_cast<T>(value % size);
return rest == 0 ? value : static_cast<T>(value - rest + size);
}
/// `value` rounded down to a multiple of `size` (any size).
template <typename T>
[[nodiscard]] constexpr T AlignDown(T value, std::size_t size) {
static_assert(std::is_unsigned_v<T>, "T must be an unsigned value.");
static_assert(std::is_unsigned_v<T>, "AlignDown needs an unsigned type");
return static_cast<T>(value - value % size);
}
/// As AlignUp, `size` a power of two.
template <typename T>
[[nodiscard]] constexpr T AlignUpPow2(T value, T size) {
return static_cast<T>(value + (size - 1) & ~(size - 1));
return static_cast<T>((value + (size - 1)) & ~(size - 1));
}
/// As AlignDown, `size` a power of two.
template <typename T>
[[nodiscard]] constexpr T AlignDownPow2(T value, T size) {
return static_cast<T>(value & ~(size - 1));
}
/// Whether `value` is a multiple of `alignment` (a power of two).
template <typename T>
requires std::is_integral_v<T>
[[nodiscard]] constexpr bool IsAligned(T value, std::size_t alignment) {
return (value & (alignment - 1)) == 0;
return (value & static_cast<T>(alignment - 1)) == 0;
}
template <typename T>
requires std::is_integral_v<T>
[[nodiscard]] constexpr bool Is16KBAligned(T value) {
return (value & 0x3FFF) == 0;
return IsAligned(value, 16u << 10);
}
template <typename T>
requires std::is_integral_v<T>
[[nodiscard]] constexpr bool Is64KBAligned(T value) {
return (value & 0xFFFF) == 0;
return IsAligned(value, 64u << 10);
}
template <typename T>
requires std::is_integral_v<T>
[[nodiscard]] constexpr bool Is2MBAligned(T value) {
return (value & 0x1FFFFF) == 0;
return IsAligned(value, 2u << 20);
}
} // namespace Common
+9
View File
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: the host architecture (the port runs on x86-64).
#pragma once
#if defined(__x86_64__)
#define ARCH_X86_64 1
#elif defined(__aarch64__)
#define ARCH_ARM64 1
#endif
@@ -1,9 +1,11 @@
// bbport: GPU-side assertion failures stop the port with exit code 23.
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: GPU-side check failures stop the port with exit code 23.
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include "common/assert.h"
#include "common/logging/log.h"
#include <execinfo.h>
#include <unistd.h>
// bb-probe (probe.c): set while the port restarts itself through run.sh. The device fd is closed
@@ -17,6 +19,11 @@ void assert_fail_impl() {
}
}
std::fflush(stdout);
// The calls that led here (module+offset: addr2line -e out/gpu/libbbgpu.so).
void* frames[48];
const int depth = backtrace(frames, 48);
std::fputs("Assertion call stack:\n", stderr);
backtrace_symbols_fd(frames, depth, STDERR_FILENO);
std::fputs("STOP: GPU library assertion failed (see GPU log above)\n", stderr);
std::_Exit(23);
}
@@ -1,36 +1,24 @@
// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: checks that stop the port (exit code 23) with a message in the GPU log.
#pragma once
#include "common/logging/log.h"
// Sometimes we want to try to continue even after hitting an assert.
// However touching this file yields a global recompilation as this header is included almost
// everywhere. So let's just move the handling of the failed assert to a single cpp file.
/// Stops the port; noreturn in practice (assert.cpp).
void assert_fail_impl();
[[noreturn]] void unreachable_impl();
#ifdef _MSC_VER
#define SHAD_NO_INLINE __declspec(noinline)
#else
#define SHAD_NO_INLINE __attribute__((noinline))
#endif
#define ASSERT(_a_) \
#define ASSERT(cond) \
do { \
if (!(_a_)) [[unlikely]] { \
LOG_CRITICAL(Debug, "Assertion Failed!"); \
if (!(cond)) [[unlikely]] { \
LOG_CRITICAL(Debug, "Assertion Failed! ({})", #cond); \
assert_fail_impl(); \
} \
} while (false)
#define ASSERT_MSG(_a_, ...) \
#define ASSERT_MSG(cond, ...) \
do { \
if (!(_a_)) [[unlikely]] { \
if (!(cond)) [[unlikely]] { \
LOG_CRITICAL(Debug, "Assertion Failed!\n" __VA_ARGS__); \
assert_fail_impl(); \
} \
@@ -40,46 +28,40 @@ void assert_fail_impl();
do { \
LOG_CRITICAL(Debug, "Unreachable code!"); \
unreachable_impl(); \
} while (0)
} while (false)
#define UNREACHABLE_MSG(...) \
do { \
LOG_CRITICAL(Debug, "Unreachable code!\n" __VA_ARGS__); \
unreachable_impl(); \
} while (0)
} while (false)
#ifdef _DEBUG
#define DEBUG_ASSERT(_a_) ASSERT(_a_)
#define DEBUG_ASSERT_MSG(_a_, ...) ASSERT_MSG(_a_, __VA_ARGS__)
#else // not debug
#define DEBUG_ASSERT(_a_) \
// Checks that cost something on hot paths: compiled out (as in release builds before).
#define DEBUG_ASSERT(cond) \
do { \
} while (0)
#define DEBUG_ASSERT_MSG(_a_, _desc_, ...) \
} while (false)
#define DEBUG_ASSERT_MSG(cond, ...) \
do { \
} while (0)
#endif
} while (false)
#define UNIMPLEMENTED() ASSERT_MSG(false, "Unimplemented code!")
#define UNIMPLEMENTED_MSG(...) ASSERT_MSG(false, __VA_ARGS__)
#define UNIMPLEMENTED_IF(cond) ASSERT_MSG(!(cond), "Unimplemented code!")
#define UNIMPLEMENTED_IF_MSG(cond, ...) ASSERT_MSG(!(cond), __VA_ARGS__)
// If the assert is ignored, execute _b_
#define ASSERT_OR_EXECUTE(_a_, _b_) \
/// ASSERT, then `otherwise` if execution continued past a failed check.
#define ASSERT_OR_EXECUTE(cond, otherwise) \
do { \
ASSERT(_a_); \
if (!(_a_)) [[unlikely]] { \
_b_ \
ASSERT(cond); \
if (!(cond)) [[unlikely]] { \
otherwise \
} \
} while (0)
} while (false)
// If the assert is ignored, execute _b_
#define ASSERT_OR_EXECUTE_MSG(_a_, _b_, ...) \
#define ASSERT_OR_EXECUTE_MSG(cond, otherwise, ...) \
do { \
ASSERT_MSG(_a_, __VA_ARGS__); \
if (!(_a_)) [[unlikely]] { \
_b_ \
ASSERT_MSG(cond, __VA_ARGS__); \
if (!(cond)) [[unlikely]] { \
otherwise \
} \
} while (0)
} while (false)
+79
View File
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: a bit range of an integer, for unions describing hardware registers and packets.
//
// union Reg {
// u32 raw;
// BitField<0, 4, u32> low; // bits 0..3
// BitField<4, 12, s32> offset; // bits 4..15, sign-extended when read
// };
//
// Reading converts to T (enums too); writing goes through Assign. The member is the whole
// integer, so the union stays its size, and byte-packed (the packets put fields at any offset).
#pragma once
#include <cstddef>
#include <type_traits>
#pragma pack(push, 1)
template <std::size_t Position, std::size_t Bits, typename T>
struct BitField {
using Type = T;
using UnderlyingType =
typename std::conditional_t<std::is_enum_v<T>, std::underlying_type<T>, std::type_identity<T>>::type;
using StorageType = std::make_unsigned_t<UnderlyingType>;
static constexpr std::size_t position = Position;
static constexpr std::size_t bits = Bits;
static constexpr std::size_t width = 8 * sizeof(StorageType);
static_assert(Bits > 0 && Position < width && Position + Bits <= width, "field outside the integer");
static_assert(std::is_trivially_copyable_v<T>, "a BitField's type must be trivially copyable");
/// The field's bits within the integer.
static constexpr StorageType mask =
static_cast<StorageType>((Bits == width ? ~StorageType{0} : (StorageType{1} << Bits) - 1)
<< Position);
/// `value` placed at the field's bits (others zero).
[[nodiscard]] static constexpr StorageType FormatValue(const T& value) {
return static_cast<StorageType>(static_cast<StorageType>(value) << Position) & mask;
}
/// The field's value in `storage`; signed types are sign-extended from the field's top bit.
[[nodiscard]] static constexpr T ExtractValue(const StorageType& storage) {
if constexpr (std::is_signed_v<UnderlyingType>) {
// Move the field to the top, then shift arithmetically back down.
constexpr std::size_t up = width - Position - Bits;
const auto top = static_cast<UnderlyingType>(static_cast<StorageType>(storage << up));
return static_cast<T>(static_cast<UnderlyingType>(top >> (width - Bits)));
} else {
return static_cast<T>((storage & mask) >> Position);
}
}
constexpr BitField() noexcept = default;
constexpr BitField(const BitField&) noexcept = default;
constexpr BitField(BitField&&) noexcept = default;
constexpr BitField& operator=(const BitField&) noexcept = default;
constexpr BitField& operator=(BitField&&) noexcept = default;
// A value would set only this field of the union, or all of it: neither is meant. Assign.
BitField(T) = delete;
BitField& operator=(T) = delete;
[[nodiscard]] constexpr T Value() const {
return ExtractValue(storage);
}
[[nodiscard]] constexpr operator T() const {
return Value();
}
[[nodiscard]] constexpr explicit operator bool() const {
return (storage & mask) != 0;
}
constexpr void Assign(const T& value) {
storage = static_cast<StorageType>((storage & ~mask) | FormatValue(value));
}
private:
StorageType storage;
};
#pragma pack(pop)
@@ -1,6 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: small concepts.
#pragma once
#include <concepts>
@@ -9,19 +8,15 @@
namespace Common {
// Check if type satisfies the ContiguousContainer named requirement.
template <typename T>
concept IsContiguousContainer = std::contiguous_iterator<typename T::iterator>;
template <typename Derived, typename Base>
concept DerivedFrom = std::derived_from<Derived, Base>;
// TODO: Replace with std::convertible_to when libc++ implements it.
template <typename From, typename To>
concept ConvertibleTo = std::is_convertible_v<From, To>;
// No equivalents in the stdlib
template <typename T>
concept IsArithmetic = std::is_arithmetic_v<T>;
+24
View File
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/decoder.h"
namespace Common {
DecoderImpl::DecoderImpl() {
ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
ZydisFormatterInit(&formatter, ZYDIS_FORMATTER_STYLE_INTEL);
}
std::string DecoderImpl::disassembleInst(ZydisDecodedInstruction& inst,
ZydisDecodedOperand* operands, u64 address) {
char text[256];
ZydisFormatterFormatInstruction(&formatter, &inst, operands, inst.operand_count_visible, text,
sizeof(text), address, ZYAN_NULL);
return text;
}
ZyanStatus DecoderImpl::decodeInstruction(ZydisDecodedInstruction& inst,
ZydisDecodedOperand* operands, void* data, u64 size) {
return ZydisDecoderDecodeFull(&decoder, data, size, &inst, operands);
}
} // namespace Common
@@ -1,9 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: x86-64 instruction decoding (Zydis) for fault handlers that need to know what a
// faulting instruction did.
#pragma once
#include <string>
#include <Zydis/Zydis.h>
#include "common/singleton.h"
#include "common/types.h"
@@ -12,7 +15,6 @@ namespace Common {
class DecoderImpl {
public:
DecoderImpl();
~DecoderImpl();
std::string disassembleInst(ZydisDecodedInstruction& inst, ZydisDecodedOperand* operands,
u64 address);
@@ -20,10 +22,10 @@ public:
void* data, u64 size = 15);
private:
ZydisDecoder m_decoder;
ZydisFormatter m_formatter;
ZydisDecoder decoder;
ZydisFormatter formatter;
};
using Decoder = Common::Singleton<DecoderImpl>;
using Decoder = Singleton<DecoderImpl>;
} // namespace Common
+6 -7
View File
@@ -1,25 +1,24 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: division rounding up.
#pragma once
#include <cstddef>
#include <type_traits>
namespace Common {
/// Ceiled integer division.
/// number / divisor, rounded up.
template <typename N, typename D>
requires std::is_integral_v<N> && std::is_unsigned_v<D>
[[nodiscard]] constexpr N DivCeil(N number, D divisor) {
return static_cast<N>((static_cast<D>(number) + divisor - 1) / divisor);
}
/// Ceiled integer division with logarithmic divisor in base 2
/// value / 2^shift, rounded up.
template <typename N, typename D>
requires std::is_integral_v<N> && std::is_unsigned_v<D>
[[nodiscard]] constexpr N DivCeilLog2(N value, D alignment_log2) {
return static_cast<N>((static_cast<D>(value) + (D(1) << alignment_log2) - 1) >> alignment_log2);
[[nodiscard]] constexpr N DivCeilLog2(N value, D shift) {
const D one = 1;
return static_cast<N>((static_cast<D>(value) + (one << shift) - 1) >> shift);
}
} // namespace Common
+84
View File
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: what the GPU library knows about the running game, set once at start (BbGpuConfig):
// its serial (the pipeline cache's name), title, SDK version and param.sfo attributes.
#pragma once
#include <string>
#include <string_view>
#include "common/bit_field.h"
#include "common/types.h"
namespace Common {
/// param.sfo ATTRIBUTE.
union PSFAttributes {
BitField<0, 1, u32> support_initial_user_logout;
BitField<1, 1, u32> enter_button_cross;
BitField<2, 1, u32> ps_move_warning;
BitField<3, 1, u32> support_stereoscopic_3d;
BitField<4, 1, u32> ps_button_suspend;
BitField<5, 1, u32> enter_button_system;
BitField<6, 1, u32> override_share_menu;
BitField<8, 1, u32> special_res_ps_button_suspend;
BitField<9, 1, u32> enable_hdcp;
BitField<10, 1, u32> disable_hdcp_non_game;
BitField<14, 1, u32> support_ps_vr;
BitField<15, 1, u32> six_cpu_mode;
BitField<16, 1, u32> seven_cpu_mode;
BitField<23, 1, u32> support_neo_mode;
BitField<26, 1, u32> require_ps_vr;
BitField<29, 1, u32> support_hdr;
BitField<31, 1, u32> display_location;
u32 raw{};
};
static_assert(sizeof(PSFAttributes) == 4);
class ElfInfo {
public:
// SDK versions as games report them (sceKernelGetCompiledSdkVersion).
static constexpr u32 FW_100 = 0x1000000;
static constexpr u32 FW_150 = 0x1500000;
static constexpr u32 FW_170 = 0x1700000;
static constexpr u32 FW_200 = 0x2000000;
static constexpr u32 FW_250 = 0x2500000;
static constexpr u32 FW_300 = 0x3000000;
static constexpr u32 FW_350 = 0x3500000;
static constexpr u32 FW_400 = 0x4000000;
static constexpr u32 FW_450 = 0x4500000;
static constexpr u32 FW_500 = 0x5000000;
static constexpr u32 FW_550 = 0x5500000;
static ElfInfo& Instance() {
static ElfInfo info;
return info;
}
void Set(std::string serial, std::string game_title, u32 sdk, u32 attributes) {
game_serial = std::move(serial);
title = std::move(game_title);
sdk_ver = sdk;
psf_attributes.raw = attributes;
}
[[nodiscard]] std::string_view GameSerial() const {
return game_serial;
}
[[nodiscard]] std::string_view Title() const {
return title;
}
[[nodiscard]] u32 CompiledSdkVer() const {
return sdk_ver;
}
[[nodiscard]] const PSFAttributes& GetPSFAttributes() const {
return psf_attributes;
}
private:
std::string game_serial{"UNKNOWN"};
std::string title;
u32 sdk_ver = 0;
PSFAttributes psf_attributes{};
};
} // namespace Common
+119
View File
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: bit-flag operators for enums, and a small flag set.
#pragma once
#include <type_traits>
#include <utility>
#include "common/types.h"
/// Bitwise operators for an enum class used as flags, and True/False tests.
#define DECLARE_ENUM_FLAG_OPERATORS(E) \
[[nodiscard]] constexpr E operator|(E a, E b) noexcept { \
return static_cast<E>(std::to_underlying(a) | std::to_underlying(b)); \
} \
[[nodiscard]] constexpr E operator&(E a, E b) noexcept { \
return static_cast<E>(std::to_underlying(a) & std::to_underlying(b)); \
} \
[[nodiscard]] constexpr E operator^(E a, E b) noexcept { \
return static_cast<E>(std::to_underlying(a) ^ std::to_underlying(b)); \
} \
[[nodiscard]] constexpr E operator<<(E a, E b) noexcept { \
return static_cast<E>(std::to_underlying(a) << std::to_underlying(b)); \
} \
[[nodiscard]] constexpr E operator>>(E a, E b) noexcept { \
return static_cast<E>(std::to_underlying(a) >> std::to_underlying(b)); \
} \
[[nodiscard]] constexpr E operator~(E a) noexcept { \
return static_cast<E>(~std::to_underlying(a)); \
} \
constexpr E& operator|=(E& a, E b) noexcept { \
return a = a | b; \
} \
constexpr E& operator&=(E& a, E b) noexcept { \
return a = a & b; \
} \
constexpr E& operator^=(E& a, E b) noexcept { \
return a = a ^ b; \
} \
constexpr E& operator<<=(E& a, E b) noexcept { \
return a = a << b; \
} \
constexpr E& operator>>=(E& a, E b) noexcept { \
return a = a >> b; \
} \
[[nodiscard]] constexpr bool True(E a) noexcept { \
return std::to_underlying(a) != 0; \
} \
[[nodiscard]] constexpr bool False(E a) noexcept { \
return std::to_underlying(a) == 0; \
}
namespace Common {
/// A set of flags, each enum value a bit position.
template <typename T>
class Flags {
public:
using IntType = std::underlying_type_t<T>;
Flags() = default;
Flags(IntType raw) : bits_{raw} {}
template <typename... More>
Flags(T first, More... more) : bits_{Mask(first, more...)} {}
template <typename... All>
void set(All... flags) {
bits_ |= Mask(flags...);
}
void set(Flags other) {
bits_ |= other.bits_;
}
template <typename... All>
void clr(All... flags) {
bits_ &= ~Mask(flags...);
}
void clr(Flags other) {
bits_ &= ~other.bits_;
}
template <typename... All>
bool any(All... flags) const {
return (bits_ & Mask(flags...)) != 0;
}
template <typename... All>
bool all(All... flags) const {
const IntType mask = Mask(flags...);
return (bits_ & mask) == mask;
}
bool test(T flag) const {
return any(flag);
}
bool isClear() const {
return bits_ == 0;
}
void clrAll() {
bits_ = 0;
}
u32 raw() const {
return bits_;
}
Flags operator&(const Flags& o) const {
return Flags(IntType(bits_ & o.bits_));
}
Flags operator|(const Flags& o) const {
return Flags(IntType(bits_ | o.bits_));
}
Flags operator^(const Flags& o) const {
return Flags(IntType(bits_ ^ o.bits_));
}
bool operator==(const Flags&) const = default;
private:
template <typename... All>
static IntType Mask(All... flags) {
return IntType((IntType{0} | ... | IntType(IntType{1} << static_cast<IntType>(flags))));
}
IntType bits_ = 0;
};
} // namespace Common
+19
View File
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cerrno>
#include <cstring>
#include "common/error.h"
namespace Common {
std::string NativeErrorToString(int e) {
char buffer[256];
// GNU strerror_r: returns the message, which may or may not be in `buffer`.
return std::string{strerror_r(e, buffer, sizeof(buffer))};
}
std::string GetLastErrorMsg() {
return NativeErrorToString(errno);
}
} // namespace Common
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: system error texts.
#pragma once
#include <string>
namespace Common {
/// The text of errno as it is now (call right after the failing call).
[[nodiscard]] std::string GetLastErrorMsg();
/// The text of error number `e`.
[[nodiscard]] std::string NativeErrorToString(int e);
} // namespace Common
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: return and argument types of functions and lambdas.
#pragma once
#include <cstddef>
#include <tuple>
#include <type_traits>
namespace Common {
template <class Func>
struct FuncTraits {};
template <class Result, class... Args>
struct FuncTraits<Result (*)(Args...)> {
using ReturnType = Result;
static constexpr std::size_t NUM_ARGS = sizeof...(Args);
template <std::size_t I>
using ArgType = std::tuple_element_t<I, std::tuple<Args...>>;
};
template <typename Func>
struct LambdaTraits : LambdaTraits<decltype(&std::remove_reference_t<Func>::operator())> {};
template <typename Result, typename Lambda, typename... Args>
struct LambdaTraits<Result (Lambda::*)(Args...) const> {
static constexpr std::size_t NUM_ARGS = sizeof...(Args);
template <std::size_t I>
using ArgType = std::tuple_element_t<I, std::tuple<Args...>>;
};
} // namespace Common
@@ -1,6 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: combining hashes. The arithmetic is the usual golden-ratio mix (as boost's
// hash_combine); it must not change: pipeline cache keys on disk are made with it.
#pragma once
#include "common/types.h"
@@ -11,4 +11,4 @@
[[nodiscard]] inline u32 HashCombine(const u32 seed, const u32 hash) {
return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2));
}
}
+20
View File
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: thread-safe increasing ids, starting at 1.
#pragma once
#include <atomic>
namespace Common {
template <typename Counter>
class IncrementalIdProvider {
public:
Counter Next() {
return next.fetch_add(1, std::memory_order_relaxed);
}
private:
std::atomic<Counter> next{1};
};
} // namespace Common
+214
View File
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: a set of disjoint [start, end) ranges kept sorted in one array. An interval type may
// carry data (Backing: memory and offset); it says which neighbours may merge into one interval
// (CanMergeWith) and how to cut a piece out of itself (SubRange). Adding a range over existing
// ones replaces what it covers; touching ranges that may merge are merged.
#pragma once
#include <algorithm>
#include <concepts>
#include <cstddef>
#include <iterator>
#include <type_traits>
#include <vector>
#include "common/types.h"
struct Interval {
u64 start;
u64 end;
constexpr bool CanMergeWith(const Interval&) const noexcept {
return true;
}
constexpr Interval SubRange(u64 a, u64 b) const noexcept {
return {a, b};
}
};
template <class IV>
concept IsInterval = std::derived_from<IV, Interval> && std::is_trivially_copyable_v<IV> &&
requires(const IV& a, const IV& b, u64 x) {
{ a.CanMergeWith(b) } -> std::same_as<bool>;
{ a.SubRange(x, x) } -> std::same_as<IV>;
};
template <class IV = Interval>
requires IsInterval<IV>
class IntervalList {
public:
using List = std::vector<IV>;
using const_iterator = typename List::const_iterator;
using iterator = typename List::iterator;
const_iterator begin() const {
return intervals.begin();
}
const_iterator end() const {
return intervals.end();
}
void Clear() {
intervals.clear();
}
bool Empty() const {
return intervals.empty();
}
std::size_t Size() const {
return intervals.size();
}
void Reserve(std::size_t n) {
intervals.reserve(n);
}
/// Puts `value` in, over whatever it covers.
void Add(IV value) {
if (value.start >= value.end) [[unlikely]] {
return;
}
// Intervals that overlap it, and the one right after it if it touches.
const auto first = FirstEndingAfter(value.start);
const auto last = std::upper_bound(first, intervals.end(), value.end,
[](u64 at, const IV& iv) { return at < iv.start; });
IV parts[3];
std::size_t count = 0;
if (first != last && first->start < value.start) {
parts[count++] = first->SubRange(first->start, value.start);
}
parts[count++] = value;
if (first != last) {
const IV& tail = *std::prev(last);
if (tail.end > value.end) {
parts[count++] = tail.SubRange(value.end, tail.end);
}
}
const std::size_t at = std::size_t(first - intervals.begin());
Replace(first, last, parts, count);
MergeAround(at, count);
}
/// Takes [start, end) out (the parts of intervals outside it stay).
void Subtract(u64 start, u64 end) {
if (start >= end) [[unlikely]] {
return;
}
const auto first = FirstEndingAfter(start);
const auto last = std::lower_bound(first, intervals.end(), end,
[](const IV& iv, u64 at) { return iv.start < at; });
if (first == last) {
return;
}
IV parts[2];
std::size_t count = 0;
if (first->start < start) {
parts[count++] = first->SubRange(first->start, start);
}
const IV& tail = *std::prev(last);
if (tail.end > end) {
parts[count++] = tail.SubRange(end, tail.end);
}
Replace(first, last, parts, count);
}
/// The interval holding `addr`, or end().
const_iterator Find(u64 addr) const {
const auto it = FirstEndingAfter(addr);
return it != intervals.end() && it->start <= addr ? it : intervals.end();
}
bool Contains(u64 addr) const {
return Find(addr) != intervals.end();
}
/// [start, end) is covered completely (by one interval or touching ones).
bool Contains(u64 start, u64 end) const {
if (start >= end) [[unlikely]] {
return false;
}
u64 covered = start;
for (auto it = FirstEndingAfter(start); it != intervals.end() && it->start <= covered;
++it) {
covered = it->end;
if (covered >= end) {
return true;
}
}
return false;
}
/// Some of [start, end) is in the set.
bool Overlaps(u64 start, u64 end) const {
if (start >= end) [[unlikely]] {
return false;
}
const auto it = FirstEndingAfter(start);
return it != intervals.end() && it->start < end;
}
/// Calls func(gap_start, gap_end) for each part of [start, end) not in the set.
void ForEachGap(u64 start, u64 end, auto&& func) const {
if (start >= end) [[unlikely]] {
return;
}
u64 at = start;
for (auto it = FirstEndingAfter(start); it != intervals.end() && it->start < end; ++it) {
if (it->start > at) {
func(at, it->start);
}
at = std::max(at, it->end);
}
if (at < end) {
func(at, end);
}
}
/// Calls func(interval) for each interval overlapping [start, end).
void ForEachInRange(u64 start, u64 end, auto&& func) const {
if (start >= end) [[unlikely]] {
return;
}
for (auto it = FirstEndingAfter(start); it != intervals.end() && it->start < end; ++it) {
func(*it);
}
}
protected:
iterator FirstEndingAfter(u64 addr) {
return std::upper_bound(intervals.begin(), intervals.end(), addr,
[](u64 at, const IV& iv) { return at < iv.end; });
}
const_iterator FirstEndingAfter(u64 addr) const {
return std::upper_bound(intervals.begin(), intervals.end(), addr,
[](u64 at, const IV& iv) { return at < iv.end; });
}
/// [first, last) becomes parts[0, count).
void Replace(iterator first, iterator last, const IV* parts, std::size_t count) {
const std::size_t slots = std::size_t(last - first);
const std::size_t reused = std::min(slots, count);
std::copy_n(parts, reused, first);
if (count < slots) {
intervals.erase(first + count, last);
} else if (count > slots) {
intervals.insert(first + slots, parts + slots, parts + count);
}
}
/// Merges touching, mergeable intervals among [at - 1, at + count].
void MergeAround(std::size_t at, std::size_t count) {
std::size_t i = at ? at - 1 : 0;
std::size_t stop = std::min(at + count + 1, intervals.size());
while (i + 1 < stop) {
IV& a = intervals[i];
const IV& b = intervals[i + 1];
if (a.end == b.start && a.CanMergeWith(b)) {
a.end = b.end;
intervals.erase(intervals.begin() + std::ptrdiff_t(i + 1));
--stop;
} else {
++i;
}
}
}
List intervals;
};
+151
View File
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: a file opened for the life of the object (stdio underneath): pipeline cache entries,
// shader dumps and patches.
#pragma once
#include <cstdio>
#include <filesystem>
#include <span>
#include <string_view>
#include <type_traits>
#include <utility>
#include <sys/stat.h>
#include "common/types.h"
namespace Common::FS {
enum class FileAccessMode {
Read = 1 << 0,
Write = 1 << 1,
ReadWrite = Read | Write,
Append = 1 << 2,
ReadAppend = Read | Append,
Create = 1 << 3, ///< write, emptying the file or making a new one
};
enum class FileType {
BinaryFile,
TextFile,
};
class IOFile final {
public:
IOFile() = default;
IOFile(const std::filesystem::path& path, FileAccessMode mode,
FileType type = FileType::BinaryFile) {
Open(path, mode, type);
}
~IOFile() {
Close();
}
IOFile(const IOFile&) = delete;
IOFile& operator=(const IOFile&) = delete;
IOFile(IOFile&& other) noexcept : file{std::exchange(other.file, nullptr)} {}
IOFile& operator=(IOFile&& other) noexcept {
if (this != &other) {
Close();
file = std::exchange(other.file, nullptr);
}
return *this;
}
void Open(const std::filesystem::path& path, FileAccessMode mode,
FileType type = FileType::BinaryFile) {
Close();
#ifdef _WIN32
const char* narrow = ModeString(mode, type);
wchar_t wide[8] = {};
for (int i = 0; i < 7 && narrow[i]; ++i) {
wide[i] = wchar_t(narrow[i]);
}
file = _wfopen(path.c_str(), wide);
#else
file = std::fopen(path.c_str(), ModeString(mode, type));
#endif
}
void Close() {
if (file) {
std::fclose(file);
file = nullptr;
}
}
[[nodiscard]] bool IsOpen() const {
return file != nullptr;
}
[[nodiscard]] u64 GetSize() const {
struct stat info {};
return file && fstat(fileno(file), &info) == 0 ? u64(info.st_size) : 0;
}
/// Fills a contiguous container (count of elements read) or one object (1 or 0).
template <typename T>
std::size_t Read(T& data) const {
if constexpr (requires { std::span{data}; }) {
const auto span = std::span{data};
return ReadRaw<typename decltype(span)::value_type>(span.data(), span.size());
} else {
static_assert(std::is_trivially_copyable_v<T>);
return ReadRaw<T>(&data, 1);
}
}
/// Writes a contiguous container (count of elements written) or one object (1 or 0).
template <typename T>
std::size_t Write(const T& data) const {
if constexpr (requires { std::span{data}; }) {
const auto span = std::span{data};
return WriteRaw<typename decltype(span)::element_type>(span.data(), span.size());
} else {
static_assert(std::is_trivially_copyable_v<T>);
return WriteRaw<T>(&data, 1);
}
}
template <typename T>
std::size_t WriteSpan(std::span<const T> data) const {
return WriteRaw<T>(data.data(), data.size());
}
std::size_t WriteString(std::string_view text) const {
return WriteRaw<char>(text.data(), text.size());
}
/// `count` objects of T.
template <typename T>
std::size_t ReadRaw(void* data, std::size_t count) const {
return file ? std::fread(data, sizeof(T), count, file) : 0;
}
template <typename T>
std::size_t WriteRaw(const void* data, std::size_t count) const {
if (!file) {
return 0;
}
const std::size_t written = std::fwrite(data, sizeof(T), count, file);
return std::fflush(file) == 0 ? written : 0;
}
private:
static const char* ModeString(FileAccessMode mode, FileType type) {
const bool text = type == FileType::TextFile;
switch (mode) {
case FileAccessMode::Read:
return text ? "r" : "rb";
case FileAccessMode::Write:
case FileAccessMode::Create:
return text ? "w" : "wb";
case FileAccessMode::ReadWrite:
return text ? "r+" : "r+b";
case FileAccessMode::Append:
return text ? "a" : "ab";
case FileAccessMode::ReadAppend:
return text ? "a+" : "a+b";
}
return "rb";
}
std::FILE* file = nullptr;
};
} // namespace Common::FS
+58
View File
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: log classes (the part of the GPU library a message comes from).
#pragma once
namespace Common::Log::Class {
constexpr auto Common = "Common";
constexpr auto Common_Filesystem = "Common.Filesystem";
constexpr auto Common_Memory = "Common.Memory";
constexpr auto Config = "Config";
constexpr auto Core = "Core";
constexpr auto Debug = "Debug";
constexpr auto Frontend = "Frontend";
constexpr auto ImGui = "ImGui";
constexpr auto Input = "Input";
constexpr auto Kernel = "Kernel";
constexpr auto Kernel_Event = "Kernel.Event";
constexpr auto KeyManager = "KeyManager";
constexpr auto Lib = "Lib";
constexpr auto Lib_AvPlayer = "Lib.AvPlayer";
constexpr auto Lib_GnmDriver = "Lib.GnmDriver";
constexpr auto Lib_Kernel = "Lib.Kernel";
constexpr auto Lib_VideoOut = "Lib.VideoOut";
constexpr auto Lib_Videodec = "Lib.Videodec";
constexpr auto Loader = "Loader";
constexpr auto Log = "Log";
constexpr auto Render = "Render";
constexpr auto Render_Recompiler = "Render.Recompiler";
constexpr auto Render_Vulkan = "Render.Vulkan";
constexpr auto Tty = "Tty";
// The online module (gpu/bbnet).
constexpr auto Lib_Http = "Lib.Http";
constexpr auto Lib_Http2 = "Lib.Http2";
constexpr auto Lib_Net = "Lib.Net";
constexpr auto Lib_NetCtl = "Lib.NetCtl";
constexpr auto Lib_NpAuth = "Lib.NpAuth";
constexpr auto Lib_NpCommerce = "Lib.NpCommerce";
constexpr auto Lib_NpCommon = "Lib.NpCommon";
constexpr auto Lib_NpManager = "Lib.NpManager";
constexpr auto Lib_NpMatching = "Lib.NpMatching";
constexpr auto Lib_NpMatching2 = "Lib.NpMatching2";
constexpr auto Lib_NpPartner = "Lib.NpPartner";
constexpr auto Lib_NpParty = "Lib.NpParty";
constexpr auto Lib_NpProfileDialog = "Lib.NpProfileDialog";
constexpr auto Lib_NpScore = "Lib.NpScore";
constexpr auto Lib_NpSignaling = "Lib.NpSignaling";
constexpr auto Lib_NpSnsFacebookDialog = "Lib.NpSnsFacebookDialog";
constexpr auto Lib_NpTrophy = "Lib.NpTrophy";
constexpr auto Lib_NpTus = "Lib.NpTus";
constexpr auto Lib_NpWebApi = "Lib.NpWebApi";
constexpr auto Lib_NpWebApi2 = "Lib.NpWebApi2";
constexpr auto Lib_Ssl = "Lib.Ssl";
constexpr auto Lib_Ssl2 = "Lib.Ssl2";
constexpr auto NpHandler = "NpHandler";
constexpr auto ShadNet = "ShadNet";
} // namespace Common::Log::Class
+38
View File
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: fmt support for enums (printed as their numbers) and UTF-8 strings.
#pragma once
#include <string>
#include <string_view>
#include <type_traits>
#include <fmt/format.h>
template <typename E>
struct fmt::formatter<E, std::enable_if_t<std::is_enum_v<E>, char>>
: fmt::formatter<std::underlying_type_t<E>> {
template <typename Context>
auto format(const E& value, Context& ctx) const -> decltype(ctx.out()) {
return fmt::formatter<std::underlying_type_t<E>>::format(
static_cast<std::underlying_type_t<E>>(value), ctx);
}
};
namespace fmt {
/// A u8 string printed as a char string.
template <typename T = std::string_view>
struct UTF {
T data;
explicit UTF(std::u8string_view text)
: data{reinterpret_cast<const char*>(text.data()), text.size()} {}
explicit UTF(const std::u8string& text) : UTF(std::u8string_view{text}) {}
};
} // namespace fmt
template <>
struct fmt::formatter<fmt::UTF<std::string_view>, char> : fmt::formatter<std::string_view> {
template <typename Context>
auto format(const fmt::UTF<std::string_view>& text, Context& ctx) const {
return fmt::formatter<std::string_view>::format(text.data, ctx);
}
};
+46
View File
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: see log.h.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include "common/logging/log.h"
namespace Common::Log {
namespace {
constexpr const char* LevelNames[] = {"trace", "debug", "info", "warning", "error", "critical"};
Level Threshold() {
static const Level threshold = [] {
const char* env = std::getenv("BB_GPU_LOG");
if (env) {
for (int i = 0; i < 6; ++i) {
if (std::strcmp(env, LevelNames[i]) == 0) {
return Level(i);
}
}
}
return Level::warn;
}();
return threshold;
}
} // namespace
bool ShouldLog(Level level) {
return level >= Threshold();
}
void Write(std::string_view log_class, Level level, const char* file, int line,
std::string&& message) {
static std::mutex mutex;
const char* slash = std::strrchr(file, '/');
const int index = int(level) >= 0 && int(level) < 6 ? int(level) : 5;
std::scoped_lock lock{mutex};
std::fprintf(stderr, "gpu %s (%.*s): %s [%s:%d]\n", LevelNames[index], int(log_class.size()),
log_class.data(), message.c_str(), slash ? slash + 1 : file, line);
}
} // namespace Common::Log
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: the GPU library's log: one line per message on stderr,
// "gpu <level> (<class>): <message> [<file>:<line>]". BB_GPU_LOG=trace|debug|info|warning|error|
// critical sets the threshold (default warning). Formatting happens only for messages that pass.
#pragma once
#include <algorithm>
#include <array>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <fmt/std.h>
#include "common/logging/classes.h"
#include "common/logging/formatter.h"
// Callers have used these through the log header.
#include "common/path_util.h"
#include "common/thread.h"
namespace Common::Log {
enum class Level : int { trace, debug, info, warn, err, critical, off };
[[nodiscard]] bool ShouldLog(Level level);
void Write(std::string_view log_class, Level level, const char* file, int line,
std::string&& message);
inline void Flush() {}
} // namespace Common::Log
// The level's old spelling (vendored callers pick a level at run time).
namespace spdlog {
using level = Common::Log::Level;
}
#define LOG_GENERIC(log_class, log_level, fmt_str, ...) \
do { \
if (::Common::Log::ShouldLog(log_level)) { \
::Common::Log::Write(log_class, log_level, __FILE__, __LINE__, \
fmt::format(fmt_str __VA_OPT__(, ) __VA_ARGS__)); \
} \
} while (false)
#define LOG_TRACE(cls, ...) \
LOG_GENERIC(::Common::Log::Class::cls, ::Common::Log::Level::trace, __VA_ARGS__)
#define LOG_DEBUG(cls, ...) \
LOG_GENERIC(::Common::Log::Class::cls, ::Common::Log::Level::debug, __VA_ARGS__)
#define LOG_INFO(cls, ...) \
LOG_GENERIC(::Common::Log::Class::cls, ::Common::Log::Level::info, __VA_ARGS__)
#define LOG_WARNING(cls, ...) \
LOG_GENERIC(::Common::Log::Class::cls, ::Common::Log::Level::warn, __VA_ARGS__)
#define LOG_ERROR(cls, ...) \
LOG_GENERIC(::Common::Log::Class::cls, ::Common::Log::Level::err, __VA_ARGS__)
#define LOG_CRITICAL(cls, ...) \
LOG_GENERIC(::Common::Log::Class::cls, ::Common::Log::Level::critical, __VA_ARGS__)
+109
View File
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: objects ordered by the tick they were last used at (oldest first), for the texture
// cache's garbage collection. Items live in one array and link to each other by index; freed
// slots are reused.
#pragma once
#include <cstddef>
#include <type_traits>
#include <vector>
#include "common/types.h"
namespace Common {
template <typename ObjectType, typename TickType>
class LeastRecentlyUsedCache {
static constexpr u32 None = ~0u;
struct Item {
ObjectType obj{};
TickType tick{};
u32 prev = None;
u32 next = None;
};
public:
/// Adds `obj` as the most recently used; returns its id.
std::size_t Insert(ObjectType obj, TickType tick) {
u32 id;
if (free_ids.empty()) {
id = u32(items.size());
items.emplace_back();
} else {
id = free_ids.back();
free_ids.pop_back();
}
Item& item = items[id];
item.obj = obj;
item.tick = tick;
Append(id);
return id;
}
/// Marks `id` used at `tick` (an older tick changes nothing).
void Touch(std::size_t id, TickType tick) {
Item& item = items[id];
if (item.tick >= tick) {
return;
}
item.tick = tick;
if (u32(id) != last) {
Unlink(u32(id));
Append(u32(id));
}
}
void Free(std::size_t id) {
Unlink(u32(id));
free_ids.push_back(u32(id));
}
/// Calls `func(obj)` for the items last used at `tick` or before, oldest first. `func` may
/// free the item it is given; one that returns bool ends the walk by returning true.
template <typename Func>
void ForEachItemBelow(TickType tick, Func&& func) {
for (u32 id = first; id != None;) {
const Item& item = items[id];
if (static_cast<s64>(tick) - static_cast<s64>(item.tick) < 0) {
return;
}
const u32 next = item.next;
if constexpr (std::is_same_v<std::invoke_result_t<Func, ObjectType>, bool>) {
if (func(item.obj)) {
return;
}
} else {
func(item.obj);
}
id = next;
}
}
private:
void Append(u32 id) {
Item& item = items[id];
item.prev = last;
item.next = None;
if (last != None) {
items[last].next = id;
} else {
first = id;
}
last = id;
}
void Unlink(u32 id) {
Item& item = items[id];
(item.prev != None ? items[item.prev].next : first) = item.next;
(item.next != None ? items[item.next].prev : last) = item.prev;
item.prev = item.next = None;
}
std::vector<Item> items;
std::vector<u32> free_ids;
u32 first = None;
u32 last = None;
};
} // namespace Common
+100
View File
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <bit>
#include <cmath>
#include "common/number_utils.h"
namespace NumberUtils {
namespace {
/// An unsigned small float: 5-bit exponent (bias 15) over a `mantissa_bits` mantissa.
float SmallUnsignedFloat(u32 value, u32 mantissa_bits) {
const u32 mantissa = value & ((1u << mantissa_bits) - 1);
const u32 exponent = (value >> mantissa_bits) & 0x1f;
if (exponent == 0) {
// Denormal: mantissa * 2^(1 - 15 - mantissa_bits).
return std::ldexp(float(mantissa), 1 - 15 - int(mantissa_bits));
}
if (exponent == 31) {
return std::bit_cast<float>(0x7f800000u | mantissa); // infinity, or NaN with a payload
}
const float fraction = 1.0f + float(mantissa) / float(1u << mantissa_bits);
return std::ldexp(fraction, int(exponent) - 15);
}
/// value times 1 / (2^bits - 1) (the reciprocal in single precision, as the GPU tables are).
template <u32 Max, typename T>
float Normalized(T value) {
constexpr float scale = 1.0f / float(Max);
return float(value * scale);
}
} // namespace
float Uf11ToF32(u16 value) {
return SmallUnsignedFloat(value, 6);
}
float Uf10ToF32(u16 value) {
return SmallUnsignedFloat(value, 5);
}
float Uf16ToF32(u16 value) {
// IEEE binary16 -> binary32.
const u32 sign = u32(value & 0x8000) << 16;
const u32 exponent = (value >> 10) & 0x1f;
const u32 mantissa = value & 0x3ff;
if (exponent == 0) {
const float magnitude = std::ldexp(float(mantissa), -24);
return sign ? -magnitude : magnitude;
}
if (exponent == 31) {
return std::bit_cast<float>(sign | 0x7f800000u | (mantissa << 13));
}
return std::bit_cast<float>(sign | ((exponent + 112) << 23) | (mantissa << 13));
}
float U2ToUnorm(u8 value) {
return Normalized<3>(value);
}
float S2ToSnorm(s8 value) {
return Normalized<1>(value);
}
float U4ToUnorm(u8 value) {
return Normalized<15>(value);
}
float S4ToSnorm(s8 value) {
return Normalized<7>(value);
}
float U5ToUnorm(u8 value) {
return Normalized<31>(value);
}
float S5ToSnorm(s8 value) {
return Normalized<15>(value);
}
float U6ToUnorm(u8 value) {
return Normalized<63>(value);
}
float S6ToSnorm(s8 value) {
return Normalized<31>(value);
}
float U8ToUnorm(u8 value) {
return Normalized<255>(value);
}
float S8ToSnorm(s8 value) {
return Normalized<127>(value);
}
float U10ToUnorm(u16 value) {
return Normalized<1023>(value);
}
float S10ToSnorm(s16 value) {
return Normalized<511>(value);
}
float U16ToUnorm(u16 value) {
return Normalized<65535>(value);
}
float S16ToSnorm(s16 value) {
return Normalized<32767>(value);
}
} // namespace NumberUtils
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: small float and normalized-integer formats to float (border colors and the like).
#pragma once
#include "common/types.h"
namespace NumberUtils {
/// Unsigned 11-bit float (5-bit exponent, 6-bit mantissa), as in R11G11B10.
float Uf11ToF32(u16 value);
/// Unsigned 10-bit float (5-bit exponent, 5-bit mantissa).
float Uf10ToF32(u16 value);
/// IEEE half float.
float Uf16ToF32(u16 value);
/// n-bit unsigned normalized: value / (2^n - 1). Signed: value / (2^(n-1) - 1).
float U2ToUnorm(u8 value);
float S2ToSnorm(s8 value);
float U4ToUnorm(u8 value);
float S4ToSnorm(s8 value);
float U5ToUnorm(u8 value);
float S5ToSnorm(s8 value);
float U6ToUnorm(u8 value);
float S6ToSnorm(s8 value);
float U8ToUnorm(u8 value);
float S8ToSnorm(s8 value);
float U10ToUnorm(u16 value);
float S10ToSnorm(s16 value);
float U16ToUnorm(u16 value);
float S16ToSnorm(s16 value);
} // namespace NumberUtils
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: an arena for objects of one type that are all released at once (the translator's
// instructions and blocks, page table pages). Objects are placed one after another in chunks;
// ReleaseContents destroys them and keeps the chunks, so the next shader reuses the memory.
#pragma once
#include <cstddef>
#include <memory>
#include <new>
#include <type_traits>
#include <utility>
#include <vector>
namespace Common {
template <typename T>
requires std::is_destructible_v<T>
class ObjectPool {
public:
explicit ObjectPool(std::size_t chunk_size = 1024) : chunk_objects{chunk_size ? chunk_size : 1} {}
ObjectPool(const ObjectPool&) = delete;
ObjectPool& operator=(const ObjectPool&) = delete;
~ObjectPool() {
ReleaseContents();
}
template <typename... Args>
requires std::is_constructible_v<T, Args...>
[[nodiscard]] T* Create(Args&&... args) {
if (used == chunk_objects || chunks.empty()) {
NextChunk();
}
T* const slot = std::launder(reinterpret_cast<T*>(chunks[current].get()) + used);
std::construct_at(slot, std::forward<Args>(args)...);
++used;
return slot;
}
/// Destroys every object; the memory stays for the next ones.
void ReleaseContents() {
if constexpr (!std::is_trivially_destructible_v<T>) {
for (std::size_t chunk = 0; chunk < chunks.size() && chunk <= current; ++chunk) {
T* const objects = std::launder(reinterpret_cast<T*>(chunks[chunk].get()));
const std::size_t count = chunk == current ? used : chunk_objects;
for (std::size_t i = 0; i < count; ++i) {
std::destroy_at(objects + i);
}
}
}
current = 0;
used = chunks.empty() ? chunk_objects : 0;
}
private:
struct alignas(T) Slot {
std::byte bytes[sizeof(T)];
};
void NextChunk() {
if (!chunks.empty() && used == chunk_objects && current + 1 < chunks.size()) {
++current; // a chunk kept from before
} else {
chunks.push_back(std::make_unique_for_overwrite<Slot[]>(chunk_objects));
current = chunks.size() - 1;
}
used = 0;
}
std::vector<std::unique_ptr<Slot[]>> chunks;
std::size_t chunk_objects;
std::size_t current = 0; ///< chunk being filled
std::size_t used = 0; ///< objects in it
};
} // namespace Common
+123
View File
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <cstdlib>
#include <mutex>
#include <limits.h>
#include "common/logging/log.h"
#include "common/path_util.h"
namespace Common::FS {
namespace fs = std::filesystem;
namespace {
constexpr std::size_t NumTypes = std::size_t(PathType::CustomModulesDir) + 1;
/// Sub-folder of the user folder for each type ("" = the user folder itself).
constexpr std::array<const char*, NumTypes> SubFolders = {
"", "log", "screenshots", "shader", "temp",
"data", "sys_modules", "download", "captures", "cheats",
"patches", "game_data", "custom_trophy", "custom_configs", "cache",
"fonts", "trophy", "home", "custom_modules",
};
struct Paths {
std::mutex mutex;
std::array<fs::path, NumTypes> paths;
std::array<bool, NumTypes> ready{};
Paths() {
const char* env = std::getenv("BB_GPU_USER_DIR");
const fs::path user = env && *env ? fs::path{env} : fs::current_path() / "user";
for (std::size_t i = 0; i < NumTypes; ++i) {
paths[i] = *SubFolders[i] ? user / SubFolders[i] : user;
}
}
};
Paths& Instance() {
static Paths paths;
return paths;
}
} // namespace
bool ValidatePath(const fs::path& path) {
if (path.empty()) {
LOG_ERROR(Common_Filesystem, "empty path");
return false;
}
if (path.native().size() >= PATH_MAX) {
LOG_ERROR(Common_Filesystem, "path too long: {}", path.string());
return false;
}
return true;
}
std::string PathToUTF8String(const fs::path& path) {
const auto text = path.u8string();
return std::string{text.begin(), text.end()};
}
const fs::path& GetUserPath(PathType type) {
auto& state = Instance();
const auto index = std::size_t(type);
std::scoped_lock lock{state.mutex};
if (!state.ready[index]) {
std::error_code ec;
fs::create_directories(state.paths[index], ec);
state.ready[index] = true;
}
return state.paths[index];
}
std::string GetUserPathString(PathType type) {
return PathToUTF8String(GetUserPath(type));
}
void SetUserPath(PathType type, const fs::path& path) {
if (!fs::is_directory(path)) {
LOG_ERROR(Common_Filesystem, "not a directory: {}", path.string());
return;
}
auto& state = Instance();
std::scoped_lock lock{state.mutex};
state.paths[std::size_t(type)] = path;
state.ready[std::size_t(type)] = true;
}
std::optional<fs::path> FindGameByID(const fs::path& dir, const std::string& game_id,
int max_depth) {
if (max_depth < 0) {
return std::nullopt;
}
const auto eboot_in = [](const fs::path& root) -> std::optional<fs::path> {
std::error_code ec;
if (fs::is_directory(root, ec) && fs::exists(root / "sce_sys" / "param.sfo", ec) &&
fs::exists(root / "eboot.bin", ec)) {
return root / "eboot.bin";
}
return std::nullopt;
};
if (dir.filename() == game_id) {
if (auto found = eboot_in(dir)) {
return found;
}
}
if (auto found = eboot_in(dir / game_id)) {
return found;
}
std::error_code ec;
for (const auto& entry : fs::directory_iterator(dir, ec)) {
std::error_code entry_ec;
if (entry.is_directory(entry_ec)) {
if (auto found = FindGameByID(entry.path(), game_id, max_depth - 1)) {
return found;
}
}
}
return std::nullopt;
}
} // namespace Common::FS
+49
View File
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: where the GPU library keeps its files: BB_GPU_USER_DIR (run.sh points it at the
// port's user folder), else ./user. A folder is created the first time it is asked for.
#pragma once
#include <filesystem>
#include <optional>
#include <string>
namespace Common::FS {
enum class PathType {
UserDir,
LogDir,
ScreenshotsDir,
ShaderDir,
TempDataDir,
GameDataDir,
SysModuleDir,
DownloadDir,
CapturesDir,
CheatsDir,
PatchesDir,
MetaDataDir,
CustomTrophy,
CustomConfigs,
CacheDir,
FontsDir,
TrophyDir,
HomeDir,
CustomModulesDir,
};
/// Not empty and not longer than the system allows.
[[nodiscard]] bool ValidatePath(const std::filesystem::path& path);
[[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path);
/// The folder of `type` (created now if it is not there).
[[nodiscard]] const std::filesystem::path& GetUserPath(PathType type);
[[nodiscard]] std::string GetUserPathString(PathType type);
/// Points `type` at another existing folder.
void SetUserPath(PathType type, const std::filesystem::path& path);
/// The eboot.bin of an unpacked game `game_id` (folder name) under `dir`, `max_depth` levels down.
[[nodiscard]] std::optional<std::filesystem::path> FindGameByID(const std::filesystem::path& dir,
const std::string& game_id,
int max_depth);
} // namespace Common::FS
Loaded 100 of 825 files, more files were not shown because too many files have changed in this diff. Show more