mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-08 17:03:48 -10:00
Compare commits
136
Commits
0.51-linux.6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98750d0687 | ||
|
|
0b3dac11d7 | ||
|
|
5041586f9a | ||
|
|
e77bce746f | ||
|
|
d1dea1b8e4 | ||
|
|
6796fbbd13 | ||
|
|
badd460277 | ||
|
|
1d0bbec978 | ||
|
|
843424de13 | ||
|
|
21e1ce4224 | ||
|
|
498a37d24b | ||
|
|
e8c7eaaf9c | ||
|
|
74b1a30870 | ||
|
|
62dbeefa6b | ||
|
|
7c6b25a560 | ||
|
|
43ffa68201 | ||
|
|
0f90125d4a | ||
|
|
02dc648c96 | ||
|
|
c16b6d3d58 | ||
|
|
b4f75ec15c | ||
|
|
c8373033ad | ||
|
|
42e8b4c606 | ||
|
|
afac1a4f39 | ||
|
|
4c1e2e3061 | ||
|
|
ab3088469a | ||
|
|
dbc374fcec | ||
|
|
8e9dbc20e1 | ||
|
|
027e834a79 | ||
|
|
0219c34163 | ||
|
|
f0306a62a1 | ||
|
|
5fd9e2ccb5 | ||
|
|
311aeb36ff | ||
|
|
3963108624 | ||
|
|
afef07c90f | ||
|
|
aae3f0566f | ||
|
|
dff4079ef6 | ||
|
|
e0543751fb | ||
|
|
76ebf406f3 | ||
|
|
c27d8fb36b | ||
|
|
2719bce35f | ||
|
|
8cfcdc1f76 | ||
|
|
a68b6a75c8 | ||
|
|
2bdee3185a | ||
|
|
6621a1f0a7 | ||
|
|
e2ac43b725 | ||
|
|
614602cf36 | ||
|
|
f0068ecb34 | ||
|
|
1f98a632b5 | ||
|
|
4e0aa911d2 | ||
|
|
312d119b5a | ||
|
|
0d34da1577 | ||
|
|
28eac08195 | ||
|
|
f0aa0fa2ae | ||
|
|
983c89fa9a | ||
|
|
da3ff0ecb0 | ||
|
|
d698ad290f | ||
|
|
e5fde7ed25 | ||
|
|
7c4a740338 | ||
|
|
91f63ff553 | ||
|
|
7331bb9410 | ||
|
|
2399b7a708 | ||
|
|
cefd12304c | ||
|
|
e1239378d6 | ||
|
|
7f8cbf12a4 | ||
|
|
7f69b82128 | ||
|
|
edfb811965 | ||
|
|
4a6177cf06 | ||
|
|
a238c6937b | ||
|
|
1441367174 | ||
|
|
8c6aeeb1c5 | ||
|
|
ae6c8728f0 | ||
|
|
5c8bf041d7 | ||
|
|
c93a1f915c | ||
|
|
b2a3990de2 | ||
|
|
4e715f48d5 | ||
|
|
5b8ac8efa5 | ||
|
|
d96d384538 | ||
|
|
683b593f45 | ||
|
|
a0ec0efaf3 | ||
|
|
90ba06f7fc | ||
|
|
456532bfa7 | ||
|
|
59da2f9a6f | ||
|
|
47085f688c | ||
|
|
10732fb349 | ||
|
|
e4d879ef3c | ||
|
|
c8bfa7f54c | ||
|
|
61824c2b2a | ||
|
|
a9dcce1eac | ||
|
|
96bd219320 | ||
|
|
c0cde157e3 | ||
|
|
fcca90e6fa | ||
|
|
0fd636c7f0 | ||
|
|
4bac966821 | ||
|
|
32383ee5d6 | ||
|
|
8cf70f6492 | ||
|
|
1bd6314b69 | ||
|
|
39a2174f47 | ||
|
|
a5e89fd8ac | ||
|
|
e64655f7f6 | ||
|
|
570ee9b579 | ||
|
|
fc17a2d7f4 | ||
|
|
afadddfaf9 | ||
|
|
63be02c67f | ||
|
|
3ed9330a6a | ||
|
|
f47ef822ee | ||
|
|
2824ea7ab2 | ||
|
|
a48a131590 | ||
|
|
8cff10a374 | ||
|
|
b902cbc5ff | ||
|
|
cddb964ebd | ||
|
|
cfc63a6766 | ||
|
|
64f7962cac | ||
|
|
1cb0fd24ed | ||
|
|
b2a6130b42 | ||
|
|
ed9d812b85 | ||
|
|
bdd7994cca | ||
|
|
4247abab8e | ||
|
|
fa0d7232d9 | ||
|
|
b2f516f0e0 | ||
|
|
773368a150 | ||
|
|
0d091c9335 | ||
|
|
95c548e6f1 | ||
|
|
55eb72e933 | ||
|
|
40031dd622 | ||
|
|
1eadf67c8c | ||
|
|
b8bb54a791 | ||
|
|
f760e22ebf | ||
|
|
c61dc69ae7 | ||
|
|
05807030a2 | ||
|
|
64660056b4 | ||
|
|
c2a45f36e6 | ||
|
|
47b276b389 | ||
|
|
71e6cc9592 | ||
|
+24 |
0fc2921c45 | ||
|
|
8185b88d80 | ||
|
|
72e5c8ce22 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
# GitHub's language bar: this C++ table of game texts (its <?name?> placeholders) is not PHP.
|
||||
gpu/bbnet20/bbsc/core/bbsc_messages.inc linguist-language=C++
|
||||
gpu/bbnet20/bbsc/core/bbsc_translations.inc linguist-language=C++
|
||||
@@ -12,7 +12,7 @@ body:
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: The launcher's **About** page shows it.
|
||||
description: The launcher's **About** page shows it, with the build of a test build; so does the first line of the log.
|
||||
placeholder: 0.51-linux.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
+8
-2
@@ -23,8 +23,6 @@ perf.data
|
||||
/bloodborne_upscalers_*.md
|
||||
mods.json
|
||||
patches.json
|
||||
# The mods folder of runs from source (the launcher's default, DATA_DIR/mods)
|
||||
/mods/
|
||||
# Local: game logs, messages and archives from users, internal plans
|
||||
/logs/
|
||||
/message*.txt
|
||||
@@ -32,6 +30,10 @@ patches.json
|
||||
/docs/ISSUE_ACTION_PLAN_*.md
|
||||
# AMD FidelityFX DLLs (FSR 4.1.1 is built from the user's own): never in the repository
|
||||
*.dll
|
||||
# The mods folder of runs from source (the launcher's default, DATA_DIR/mods), and the list of the
|
||||
# last run's mods
|
||||
/mods/
|
||||
/mods.last
|
||||
# Game files and dumps: never in the repository
|
||||
eboot.bin
|
||||
param.sfo
|
||||
@@ -39,3 +41,7 @@ param.sfo
|
||||
*.dcx
|
||||
*.7z
|
||||
/CUSA*/
|
||||
# The launcher's icon (shipped)
|
||||
!launcher/bloodborne.png
|
||||
# The players' guide, built by mkdocs (mkdocs.yml)
|
||||
_site/
|
||||
@@ -1,28 +1,23 @@
|
||||
THIS PROJECT IS NOT RELATED TO SHADPS4. ALL QUESTIONS RELATED TO THIS PROJECT SHOULD BE SENT TO THE DISCORD SERVER https://discord.gg/KYZRKk9CB, NOT TO THE SHADPS4 SERVER.
|
||||
|
||||
|
||||
# bbport — a native Linux port of Bloodborne
|
||||
# bbport — a native Linux and Windows port of Bloodborne
|
||||
|
||||
> This is the README of the original project (deadinside28/bloodborne_pc), kept for its
|
||||
> technical details. The fork's own README is [README.md](README.md).
|
||||
|
||||
**English** · [Русский](BBPORT.ru.md)
|
||||
> This is the README of the original project (deadinside28/bloodborne_pc, version 0.7), kept for
|
||||
> its technical details. The Linux fork's own README is [README.md](README.md).
|
||||
|
||||
bbport is the counterpart of Wine + DXVK for a single game: *Bloodborne* for PlayStation 4
|
||||
(CUSA03173, game version 1.09) on an x86-64 Linux PC. The game's original executable runs
|
||||
directly on the PC:
|
||||
(CUSA03173, game version 1.09) on an x86-64 PC with Linux or Windows 10/11 (since 0.7, see
|
||||
[Windows](#windows)). The game's original executable runs directly on the PC:
|
||||
|
||||
- **as in Wine**, the game's x86-64 code runs on the CPU directly, and a runtime written for
|
||||
this one game replaces the PS4 system libraries;
|
||||
- **as in DXVK**, the game's graphics are translated to Vulkan — by a renderer derived from
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
|
||||
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1;
|
||||
- memory and the GPU's work are moving to the PC model: the launcher has an experimental
|
||||
*New memory and translation model* mode; without it the game runs on the old
|
||||
memory model, as in 0.3.
|
||||
|
||||
One step remains to the full Wine + DXVK model: make the new mode the default and remove the old
|
||||
memory model (see below).
|
||||
- **as in DXVK**, the game's graphics are translated to Vulkan — by the port's own renderer and
|
||||
shader translator (they started from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s and were
|
||||
rewritten), including temporal upscaling with AMD FSR 3.1, FSR 4, FSR 4.1.1 and NVIDIA DLSS,
|
||||
and DLSS Frame Generation;
|
||||
- memory and the GPU's work follow the PC model on every GPU: the new memory and translation
|
||||
model is the only one (the default since 0.6; the old model of 0.3 was removed in 0.7).
|
||||
|
||||
> **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
|
||||
@@ -31,19 +26,17 @@ memory model (see below).
|
||||
|
||||
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
|
||||
and several areas of Yharnam were played with it) with sound, gamepad and saving.
|
||||
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
|
||||
Mesa/RADV) has been tested thoroughly.
|
||||
A full play-through has not been verified. Tested thoroughly: AMD Radeon RX 7800 XT (Linux with
|
||||
Mesa/RADV, and Windows 11), the Steam Deck; on Windows also two laptops with NVIDIA GTX 1050 and
|
||||
GeForce 930MX; other NVIDIA and Intel GPUs by players.
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
|
||||
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
|
||||
translation: the game code runs at full speed.
|
||||
- **Two modes** (launcher → *Mode*):
|
||||
- **Switch off (the default) — as in 0.3.** The old memory model: VRAM copies of the game's
|
||||
memory, writes tracked through page protection. With every fix made since 0.3 (motion
|
||||
vectors and the upscaler, flicker with DoF on, a damaged shader cache).
|
||||
- **New memory and translation model — experimental.** The game's memory lives
|
||||
- **One memory and translation model** (the model of 0.3 is gone):
|
||||
- The game's memory lives
|
||||
in system RAM and the GPU reads it where it is, as a PC game's buffers; data it reads often
|
||||
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). The
|
||||
layer's memory module tracks CPU writes to mirrored blocks to keep those copies coherent;
|
||||
@@ -51,25 +44,37 @@ 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. **It may crash**; hardware testing has been performed on an
|
||||
RX 7800 XT, and performance depends on the scene and settings. On AMD the game's memory is kept
|
||||
in a sparse buffer (the arena). **On NVIDIA, Intel and others** it goes through the layer's
|
||||
memory module: the game's memory is bound in place and VRAM copies are separate buffers, with
|
||||
no sparse rebinding. This avoids the sparse-binding path associated with reported freezes
|
||||
on a GTX 1660 Ti. On NVIDIA the module runs on a tester's GTX 1660 Ti, its speed is still
|
||||
being checked; not tested on Intel. Without the launcher: `BB_PC_MODEL=1`; to choose the memory
|
||||
path: `BB_GUEST_MEMORY=host|dmabuf`; the memory module: `BB_LAYER_MEMORY=1/0`.
|
||||
processor writes them. On every GPU the game's memory goes through the layer's memory module:
|
||||
bound in place, VRAM copies as separate buffers, no sparse binding (NVIDIA's sparse binding
|
||||
froze the GPU for seconds; on AMD the module is faster than the former sparse arena too);
|
||||
`BB_LAYER_MEMORY=0` gives AMD's sparse arena. Tested thoroughly on an RX 7800 XT; on NVIDIA
|
||||
by testers. A driver that fails the startup checks stops the game with a line beginning
|
||||
`STOP: the GPU driver cannot use the game's memory in place`.
|
||||
|
||||
[0.5-pre4 changes](docs/CHANGES_0.5-pre4.md): no stutter of the memory module (NVIDIA) while areas
|
||||
stream in, shader compilation in the background, VRAM on 4–6 GB cards, damaged game extractions
|
||||
reported, the settings menu as in 0.3. [0.5-pre3](docs/CHANGES_0.5-pre3.md): the memory module on
|
||||
NVIDIA — VRAM copies, one VRAM budget with the texture cache, watched blocks. [0.5-pre2](docs/CHANGES_0.5-pre2.md): the memory
|
||||
module, native GPU copy shader, portable GPU completion labels, and shader-cache migration.
|
||||
The graphics heaps of the game grow as it takes memory and give it back when freed.
|
||||
|
||||
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.
|
||||
[Changes in 0.7](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.7.md), [0.6](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6.md), [0.6-pre1](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6-pre1.md),
|
||||
[0.51](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.51.md), [0.5](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.5.md).
|
||||
|
||||
Unused textures are freed, so VRAM no longer grows with every area visited; *Texture quality*
|
||||
(full, half, quarter, auto = half on cards with up to 3 GB) skips the top levels of big
|
||||
textures on small cards.
|
||||
- **Unlocked frame rate.** Hooks in the game's code make the simulation use the real frame time
|
||||
(the community patches in `patches/Bloodborne.xml` remain for comparison, `BB_FRAME_HOOKS=0`);
|
||||
~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at 1440p with FSR 4 Quality.
|
||||
Also 30/60/90 FPS modes; **F3** switches a 30 FPS limit on and off while playing (some
|
||||
cinematics, e.g. Laurence's, are safest at 30).
|
||||
- **Any aspect ratio, live.** 4:3, 16:10, 16:9, 21:9 and 32:9 with the HUD at the screen's edges,
|
||||
through hooks (the game's code is not patched); cinematics are wide too.
|
||||
- **Keyboard and mouse.** Mouse look turns the game's camera directly; a mouse pointer in the
|
||||
game's menus (F1 releases the mouse); a layout close to Dark Souls III's (Space dodge, Left
|
||||
Shift run, Shift+Space jump); movement keys ramp up like a stick; button prompts show the bound
|
||||
key, or the buttons of the gamepad in use (Xbox and others), PlayStation icons only with a
|
||||
PlayStation pad.
|
||||
- **Online play and seamless co-op.** Two pools on shadPS4 0.20's network stack: **Seamless
|
||||
Hunt** (default; bells, summons, messages, bloodstains and seamless co-op, BBSC, up to five
|
||||
hunters; an account from seamlesshunt.com) and **The Hunter's Dream** (sign-in with Discord; no
|
||||
BBSC).
|
||||
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
|
||||
computes motion vectors itself: camera motion from depth and the scene matrices, and object
|
||||
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
|
||||
@@ -84,6 +89,18 @@ Mesa/RADV) has been tested thoroughly.
|
||||
9000; picked automatically). The assets are built on your machine from one DLL of your own
|
||||
(4.1.x): the launcher's *FSR 4.1.1 from your own AMD DLL → Choose DLL…* button (2–5 minutes,
|
||||
twice that on RDNA4; needs a recent Proton), or `tools/fsr4cap`.
|
||||
- **DLSS Super Resolution and DLSS Frame Generation** (NVIDIA GeForce RTX) through an
|
||||
optional bridge built against NVIDIA's SDK (see Building). Frame generation makes the frames
|
||||
between two of the game's (2x on RTX 40, up to 4x on RTX 50) from the finished frame, the
|
||||
frame before its UI, the scene depth, the motion vectors and the game's camera matrices;
|
||||
*Overlay menu → Frame generation*, or the launcher.
|
||||
- **FSR 4.1.2's models from the AMD driver** (INT8 on RDNA2/3 and the *Lite* model), taken
|
||||
from the driver's `amdxcffx64.dll` with the upscaler DLL (*FSR 4 model* in the menus).
|
||||
- **FSR 1** (spatial, no motion vectors) for weak GPUs, and **FSR 3.1 frame generation**
|
||||
(experimental, every GPU; the port's own optical-flow and interpolation shaders).
|
||||
- **Anti-aliasing** separate from the upscalers: *auto* (the game's AA only without an
|
||||
upscaler), the game's, FXAA (Kyo's *Force FXAA2*), SMAA 1x (the port's own, before the UI), off;
|
||||
sharpening (RCAS) also without an upscaler. **GT-VBAO** can replace the game's SSAO.
|
||||
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
|
||||
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
|
||||
4K on an RX 7800 XT instead of ~6 ms.
|
||||
@@ -91,12 +108,19 @@ Mesa/RADV) has been tested thoroughly.
|
||||
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
|
||||
thread and helper threads for memory copies. Early on the single GPU thread capped the game
|
||||
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
|
||||
- **Settings inside the game's own menu:** *System → Display / Game effects / Game patches*
|
||||
(after *Screen/Sound*), built and drawn by the game itself — output resolution, upscaler,
|
||||
preset, sharpness, FPS counter, model detail and the effect patches, in the game's language.
|
||||
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
|
||||
- **Settings inside the game's own menu:** *System → Display / Effects* (after *Screen/Sound*),
|
||||
built and drawn by the game itself — output resolution, upscaler, preset, sharpness, FPS
|
||||
counter, model detail and the game's effects, in the game's language. Chromatic aberration,
|
||||
depth of field, motion blur, SSAO, the game's AA and dynamic-light shadows switch at once;
|
||||
screen-space reflections, the intro, free camera and the debug menu need a restart.
|
||||
- **Overlay menu** (Insert, or hold L3+R3), styled like the game's dialogs: everything above plus the
|
||||
advanced settings, with gamepad, keyboard and mouse.
|
||||
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
|
||||
- **Shaders before you play.** A pack of shader recipes from players' caches (655 programs, 1149
|
||||
pipelines) is compiled in the background at start, and the cache survives updates: a repeat
|
||||
session compiles nothing.
|
||||
- **Launchers:** GTK4 on Linux, WPF in the Windows 11 style on Windows (both with sections and
|
||||
descriptions); an **AppImage** for Linux and the Steam Deck, the same as a **folder**
|
||||
(`tar.zst`, `Bloodborne.sh`), a **zip** for Windows.
|
||||
|
||||
## How it differs from shadPS4
|
||||
|
||||
@@ -104,16 +128,17 @@ Mesa/RADV) has been tested thoroughly.
|
||||
|---|---|---|
|
||||
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
|
||||
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
|
||||
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | By default the same model (as in 0.3); in the *New memory and translation model* mode: the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
|
||||
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | By default the same; in the new mode translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
|
||||
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | The game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
|
||||
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | Translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
|
||||
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
|
||||
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
|
||||
| GPU | shadPS4 video core and shader recompiler | Its own command processor, renderer, caches and shader translator (they started from shadPS4's, `gpu/VENDOR.txt`): two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, DLSS, frame capture and GPU profiler |
|
||||
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
|
||||
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
|
||||
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1/4.1.2, DLSS) with motion vectors computed for the game and jitter; frame generation (DLSS, FSR 3.1) |
|
||||
| Platforms | Linux, Windows, macOS | Linux and Windows (x86-64) |
|
||||
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
|
||||
|
||||
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
|
||||
the graphics side.
|
||||
Without shadPS4 there would be no bbport: its renderer and shader recompiler were the starting
|
||||
point of the graphics side.
|
||||
|
||||
### Wine + DXVK for one game
|
||||
|
||||
@@ -122,8 +147,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
|
||||
@@ -136,22 +161,20 @@ the graphics side.
|
||||
performs them. The game's occlusion queries (~60 a frame) are
|
||||
Vulkan occlusion queries; predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
|
||||
`BB_PM4_SELFTEST=1`). Details:
|
||||
[docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), "Шаг 4" (Russian).
|
||||
[docs/EMULATION_REMOVAL_PLAN.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/EMULATION_REMOVAL_PLAN.ru.md), "Шаг 4" (Russian).
|
||||
|
||||
"Port" here means a build for this one game, not a rewrite of its source code, which the project
|
||||
neither has nor includes.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26 (RADV).
|
||||
The *New memory and translation model* mode is not tested on NVIDIA yet.
|
||||
- Linux x86-64 or Windows 10/11 x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26
|
||||
(RADV) and with AMD's Windows driver; NVIDIA and Intel by players (please report how they run).
|
||||
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
|
||||
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
|
||||
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
|
||||
before the first frame and are disabled in the in-game menu.
|
||||
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
|
||||
The US release (`CUSA00900`) runs too: its 1.09 executable is the same as this European
|
||||
Game of the Year edition's.
|
||||
A dumped update is a separate folder: copy it over the base game, replacing files. The base
|
||||
game alone (1.00) crashes at start (guest offset 0x20348b8); the launcher and `run.sh` check
|
||||
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). The other
|
||||
@@ -187,22 +210,13 @@ its main menu offline (a game patch; the launcher's *Game effects → Skip the
|
||||
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Screen → Monitor* in the launcher
|
||||
(`BB_DISPLAY=<number or part of the name>`) picks the one for the game.
|
||||
|
||||
Add-ons: `BB_DLC_DIR=/path/to/dlc` (the launcher's *DLC folder*) holds the dumped add-ons of
|
||||
the game, one folder each named by its entitlement label (`SPEXPANSIONDLC03/sce_sys/...`).
|
||||
Their data ships with the 1.09 update, so a folder with only `sce_sys` is enough. `run.sh` links
|
||||
it as `user/addcont/<title id>`, where the runtime finds add-ons (see **DLC** below); a real
|
||||
folder already there is kept. The GOTY edition (CUSA03173) needs none.
|
||||
|
||||
**Resolution and preset changes:** for outputs other than 1080p (720p on the Steam Deck,
|
||||
1440p, 4K) the whole game renders at the preset's resolution, set by a patch at start — the
|
||||
fastest path. Changing the output or the preset in the in-game menu then needs *Apply and
|
||||
restart the game*. The *Live resolution changes* setting (launcher, in-game menu,
|
||||
`bbport.ini` `live_resolution=0|1|auto`; **off by default**) instead keeps the game at
|
||||
1080p internally and scales its render targets at run time, so 720p/1080p/1440p/4K and the
|
||||
presets switch without a restart. It costs more: the game then believes it renders 1080p
|
||||
and draws more (e.g. ~8× more small lights), and some targets are copied between sizes —
|
||||
use it on strong desktop GPUs only (`auto` turns it on for discrete GPUs with 8+ GB that are
|
||||
not pre-Turing NVIDIA). 1080p output and TAA always use the live path.
|
||||
**Resolution and preset changes:** the game starts at the scene size of the chosen output
|
||||
and preset (a patch at start, the fastest path); the output is the monitor's resolution by
|
||||
default (*Auto*). With *Live resolution changes* (on by default; `bbport.ini`
|
||||
`live_resolution=0|1`) the preset, the output resolution and the aspect ratio then switch while
|
||||
playing, to smaller and to larger sizes, at the same FPS as a start at that size; only another
|
||||
upscaler kind needs a restart. GPUs that cannot give the game's depth a proxy (no D32S8 blits
|
||||
and no shader stencil export, e.g. GTX 10xx) apply a change at the next start instead.
|
||||
|
||||
**TAA:** a separate native-resolution temporal AA mode in the launcher and overlay,
|
||||
switchable live without an FSR model. The saved FSR preset is restored when returning
|
||||
@@ -214,27 +228,24 @@ temporal accumulation and leaves its history and HUD unchanged.
|
||||
**Mods:** the launcher accepts separate loose-file mod folders (with `dvdroot_ps4/`, an extra
|
||||
wrapper folder, or the game folders such as `chr/` directly; file name case does not matter),
|
||||
with enable switches and load order. A sibling `CUSA03173-mods/` overlay also works.
|
||||
The original game is preserved; later mods override conflicting files.
|
||||
The original game is preserved; later mods override conflicting files. Texture packs with
|
||||
upscaled map textures (from the mods folder or already in the game folder) get larger graphics
|
||||
heaps by themselves (`BB_LARGE_HEAPS=1/0` forces it on or off).
|
||||
**Third-party patches:** shadPS4-format XML patch files in the data directory's `patches/`,
|
||||
switched on and off in the launcher. See [mods and patches](docs/MODS.md).
|
||||
switched on and off in the launcher. See [mods and patches](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/MODS.md).
|
||||
**DLC:** put your add-on dumps in `user/addcont/<title id>/<entitlement label>/` (shadPS4's
|
||||
layout, e.g. `user/addcont/CUSA03173/SPEXPANSIONDLC03` for The Old Hunters): each folder there is
|
||||
reported to the game as an installed add-on.
|
||||
|
||||
**Launcher language:** Russian, English, Brazilian Portuguese, Spanish, Italian, French, German or Simplified Chinese (follows the system language by default).
|
||||
**Game language:** the PS4 system language, *System language* in the launcher; by default it
|
||||
follows the desktop's locale (Japanese, English, French, Spanish, German, Italian or Russian,
|
||||
English for the rest). `run.sh` takes it as `BB_LANGUAGE` (PS4 code: 0 Japanese, 1 English,
|
||||
2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian; without it, the desktop's locale as above).
|
||||
**Game region:** *Game region* in the launcher, `BB_REGION=eu|na` for `run.sh`: the regional text
|
||||
of a language, such as Spanish from Spain (`eu`) or Latin American Spanish (`na`). It sets the
|
||||
game's `USER_DEFINED_PARAM_1` (13 Europe, 2 the Americas) for the launch; by default the dump's
|
||||
`param.sfo` decides.
|
||||
**Launcher language:** Russian, English, Brazilian Portuguese, Simplified Chinese or French (follows the system language by default).
|
||||
The game's own text language is chosen separately, from all 21 languages the game has.
|
||||
**GPU choice:** *Mode → GPU* picks the graphics card on machines with two (*Auto* prefers the
|
||||
discrete one; stored by name; `BB_GPU_ID=N` from the shell wins).
|
||||
|
||||
**Free camera and game debug menu** (v1.09): enable the corresponding switches in the
|
||||
launcher or in-game menu and restart. Free camera uses Lance McDonald's
|
||||
[GoldHEN patch](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
|
||||
hold Cross and press L3 to cycle modes (keyboard: hold Space and press Z). It needs no fonts
|
||||
hold Cross and press L3 to cycle modes (keyboard: hold F and press Left Ctrl). It needs no fonts
|
||||
and conflicts with *Enemy Control*.
|
||||
For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) into the game's
|
||||
@@ -242,11 +253,11 @@ For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
|
||||
the unsafe patch. Open it with the left touchpad / Tab (with the debug menu on, the left half
|
||||
no longer opens the gestures); Backspace is the right touchpad.
|
||||
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
|
||||
pads without a touch surface. The port's settings menu remains Insert / L3+R3.
|
||||
pads without a touch surface. The port's settings menu remains Insert / holding L3+R3.
|
||||
|
||||
GPU occlusion queries still use synthetic pixel counters (`PixelPipeStatDump`), and
|
||||
`IT_SET_PREDICATION` is unimplemented. Free camera allows visual investigation; it does
|
||||
not implement GPU occlusion culling.
|
||||
With the *Restore Debug Camera* patch the mouse turns the free camera. The game's occlusion
|
||||
queries are Vulkan occlusion queries in the default model (`BB_OCCLUSION_QUERIES=0`: everything
|
||||
counts as visible, as in the old model).
|
||||
|
||||
**Upscaler assets** (not included; FSR 3.1 needs none):
|
||||
|
||||
@@ -267,7 +278,8 @@ builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pas
|
||||
the DLL runs under Proton and is recorded at 20 sizes (progress in the row and in the *Log* tab;
|
||||
on RDNA4 a second time, for the FP8 variant),
|
||||
the passes are translated to SPIR-V, and FSR 4.1.1 is selected. The Proton build is picked
|
||||
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer,
|
||||
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer (also the
|
||||
AUR's `proton-ge-custom` in `/usr/share/steam/compatibilitytools.d`),
|
||||
Proton-CachyOS, Proton Experimental (tested: GE-Proton 11, Proton-CachyOS 11, Experimental of
|
||||
October 2026; Steam's Proton 11.0 and GE-Proton 9 do not). It runs in the Steam runtime it
|
||||
requires (Steam installs it the first time any game runs with that Proton), or through
|
||||
@@ -287,13 +299,6 @@ DLSS gets the inputs FSR 3.1 gets (scene color, depth, motion vectors, jitter).
|
||||
libraries, on other GPUs or with `BB_DLSS=0` it is listed as unavailable, and a DLSS setting falls
|
||||
back to FSR 3.1.
|
||||
|
||||
On Linux, DLSS also needs NVIDIA's CUDA/UVM runtime. If device creation fails with the DLSS
|
||||
extensions, the renderer retries without them and uses FSR 3.1. If `/dev/nvidia-uvm` is missing,
|
||||
load the installed driver module with `sudo modprobe nvidia_uvm` and restart the game. Installing
|
||||
the distribution's `nvidia-modprobe` package lets NVIDIA's libraries load the module when needed.
|
||||
If `modprobe` reports `Key was rejected by service` with Secure Boot enabled, enroll the
|
||||
module's signing key using the distribution's [MOK procedure](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
|
||||
|
||||
The AppImage built with `DLSS_SDK_ROOT` carries the bridge and NVIDIA's library (NVIDIA's RTX SDK
|
||||
license allows that in an application; its text is `NVIDIA-DLSS-LICENSE.txt` in the package). A
|
||||
newer `libnvidia-ngx-dlss.so.<version>` (from the SDK's `lib/Linux_x86_64/rel`) is chosen in the
|
||||
@@ -301,6 +306,31 @@ launcher: *Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR
|
||||
copied to `<user>/dlss/`, which the game prefers. A game's `nvngx_dlss.dll` is the Windows
|
||||
library: NVIDIA's Linux driver does not load it.
|
||||
|
||||
**DLSS Frame Generation (GeForce RTX 40 and newer).** The same SDK (310.7 or newer) has NVIDIA's
|
||||
Linux frame generation library, `lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.<version>`;
|
||||
`build.sh` copies it next to `bb-probe` with the Super Resolution one, and the launcher's
|
||||
*DLSS: NVIDIA's library → Choose a file…* takes it too (into `<user>/dlss/`, beside or instead of
|
||||
the package's). Then *Frame generation → DLSS* in the overlay menu (Insert, or hold L3+R3) or the
|
||||
launcher: between each two of the game's frames one generated frame is shown (RTX 40), or up to
|
||||
three (*Multiplier*, RTX 50); the display needs the refresh rate for them. It works with any
|
||||
upscaler, DLSS or FSR; the inputs are the frame as shown, the frame before its UI, the scene depth,
|
||||
the motion vectors bbport computes and the game's cameras as clip-space matrices. Without the
|
||||
library, on an RTX 30 or older or with `BB_DLSS=0` the entry says why it is unavailable, and the
|
||||
game runs without generated frames. `BB_FRAME_GEN=dlss` and `BB_FRAME_GEN_MULTIPLIER=2..4` set it
|
||||
from the command line. FSR 3.1 frame generation is offered on every GPU as experimental
|
||||
(*Frame generation → FSR 3.1*).
|
||||
|
||||
**Low latency (NVIDIA Reflex).** On NVIDIA's driver (550 or newer) the port uses the Vulkan
|
||||
form of Reflex, `VK_NV_low_latency2`, the same mechanism Proton gives Windows games: the game's
|
||||
thread waits for the driver before each frame so that frames do not queue up in front of the GPU,
|
||||
and the frame's simulation, submissions and present are marked for the driver. On by default where
|
||||
the driver has it: the overlay menu (*Output → Low latency (NVIDIA Reflex)*) and the launcher
|
||||
offer Off, On and On + Boost (`low_latency=off|on|boost` in `bbport.ini`, `BB_LOW_LATENCY`);
|
||||
on AMD the same setting uses **Anti-Lag 2** (`VK_AMD_anti_lag`: AMD's Windows driver, or Mesa's
|
||||
anti-lag layer on Linux; `BB_ANTI_LAG=0` leaves it out). With DLSS frame generation it is always
|
||||
at least On, as NVIDIA requires of games: the generated frames add latency that Reflex takes back.
|
||||
Reflex and DLSS frame generation came from PR #260 by adds39939.
|
||||
|
||||
From source the script takes its tools from the system (MinGW GCC, CMake, Ninja, Python 3,
|
||||
SPIRV-Tools, Git) or from Nix; the AppImage has them prebuilt.
|
||||
|
||||
@@ -320,8 +350,9 @@ the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
|
||||
game is 16:9; on the 1280×800 screen it gets thin bars).
|
||||
|
||||
**Adding the AppImage to Steam** (*Add a Non-Steam Game*) needs no options; the compatibility tool
|
||||
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage removes it
|
||||
before its own programs start, so the Steam overlay is not shown in the game.) Where Steam
|
||||
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage keeps it out
|
||||
of the launcher and gives it to the game only, so it can open in the game with Shift+Tab; `BB_STEAM_OVERLAY=0`
|
||||
leaves it out.) Where Steam
|
||||
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
|
||||
AppImage*), set the launch options to
|
||||
|
||||
@@ -340,7 +371,9 @@ NVIDIA userspace driver matched to the host kernel module. Standard Linux distri
|
||||
these libraries under `/usr/lib*`; on NixOS the AppImage's internal `/nix/store` may hide them.
|
||||
In that case copy the NVIDIA libraries into an accessible directory and set
|
||||
`BB_NVIDIA_LIB_DIR=/path/to/libraries` (the NVIDIA manifest must also be accessible).
|
||||
Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
|
||||
With the host's NVIDIA driver the AppImage also passes the host's EGL vendor files, NVIDIA's
|
||||
first (`__EGL_VENDOR_LIBRARY_FILENAMES`; without them NVIDIA's ICD gave no `vkCreateInstance` on
|
||||
some hosts, #107; `BB_NVIDIA_EGL=0`: off). Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
|
||||
the package with:
|
||||
|
||||
```bash
|
||||
@@ -357,8 +390,7 @@ When running from source, install MangoHud separately. A diagnostic launch with
|
||||
`VK_LOADER_LAYERS_DISABLE=~implicit~` also disables MangoHud.
|
||||
|
||||
Useful variables: `BB_FRAME_STATS=1` (frame statistics, including a `Memory:` line: VRAM, GTT,
|
||||
RSS, images and guest blocks in VRAM), `BB_PC_MODEL=1` (the new memory and translation model,
|
||||
0, the old model as in 0.3, is the default), `BB_ANISO=N` (anisotropic filtering of scene textures; 16 by
|
||||
RSS, images and guest blocks in VRAM), `BB_ANISO=N` (anisotropic filtering of scene textures; 16 by
|
||||
default, 0 = the game's own),
|
||||
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (how long unused textures / buffers stay in VRAM;
|
||||
20 / 60), `BB_BREADCRUMBS=0` (no GPU breadcrumbs; with them a GPU hang names the draw or dispatch
|
||||
@@ -374,17 +406,72 @@ budget taken as at most N MiB: checks the behaviour of cards with little VRAM),
|
||||
with `BB_PRESENT_DUMP_COUNT=N` (dump N consecutive presented frames),
|
||||
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (FSR 4.1.1 variant; by default FP8 where the GPU has FP8
|
||||
matrices), `BB_READBACKS=0|1|2` (reads of GPU-written memory by the game: 1 by default, 2
|
||||
precise and slow, 0 off).
|
||||
More in [docs/](docs); recent changes: [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md) (in Russian),
|
||||
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
|
||||
precise and slow, 0 off), `BB_RENDER_RESOURCE_CACHE_MB=160` (the game's render resource cache
|
||||
at its own size instead of 512 MiB of the port's memory; `BB_DMEM_EXTRA_MB`, 512 by default, is
|
||||
the direct memory the port keeps past the game's own). All of them, each with what it does:
|
||||
[docs/ENVIRONMENT.md](docs/ENVIRONMENT.md); the log's `Environment:` line names those set and any
|
||||
unknown `BB_*` name (a typo). More in [docs/](docs); recent changes: [docs/CHANGES_0.7.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.7.md),
|
||||
[docs/CHANGES_0.6.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6.md).
|
||||
|
||||
**Online play (shadNet).** A separate module, `out/gpu/libbbnet.so` (`gpu/bbnet`: shadPS4's network
|
||||
and PSN libraries from shadp2p, a shadPS4 fork, with the shadNet client), built when protobuf,
|
||||
nlohmann_json, OpenSSL, zlib and miniupnpc are there (`shell.nix`) and included in the AppImage.
|
||||
The launcher's *Online* page turns it on (`BB_ONLINE=1`): messages, bloodstains and ghosts come
|
||||
from The Hunter's Dream, bells and summons go through a shadNet server (`srv.shadps4.net:31313` by
|
||||
default) with a shadNet account (Online ID and password). Offline the module is never loaded; a
|
||||
build without it plays offline only.
|
||||
**Online play.** A separate module, `out/gpu/libbbnet20.so` (`gpu/bbnet20`: shadPS4 v0.20's
|
||||
network, NP and shadNet code, the port's BBSC and The Hunter's Dream backend), built when
|
||||
protobuf, nlohmann_json, OpenSSL, zlib and miniupnpc are there (`shell.nix`) and included in the
|
||||
AppImage. The launcher's *Online* page turns it on (`BB_ONLINE=1`) and offers two pools:
|
||||
- **Seamless Hunt** (default): a shadNet server with BBSC's game server
|
||||
(`play.seamlesshunt.com:31313`, WebAPI `:31315`); bells, summons, messages, bloodstains and
|
||||
**seamless co-op (BBSC)**: the session survives deaths, lamps, area changes, bosses and the
|
||||
Hunter's Dream, up to five hunters (*Seamless co-op (BBSC)*, on by default; every player needs
|
||||
it and the same server). The account (online id and password) is made on
|
||||
https://seamlesshunt.com. Details: [docs/BBSC.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/BBSC.md).
|
||||
- **The Hunter's Dream** (thehuntersdream.com): sign in with Discord from the launcher (a code is
|
||||
confirmed on the site); no BBSC.
|
||||
|
||||
On Windows the module is `online\bbnet20.dll` (Seamless Hunt with BBSC; the launcher's *Online*
|
||||
section). The Linux folder package keeps it open as `online/libbbnet20.so`, and `BB_NET_MODULE`
|
||||
points the port at any other build of it (a newer BBSC, say).
|
||||
|
||||
The former module on shadp2p (`libbbnet.so`) was removed (0.7). shadPS4's
|
||||
public server (`srv.shadps4.net`) accepts only shadPS4's own builds. Offline the module is never
|
||||
loaded; a build without it plays offline only.
|
||||
|
||||
## Windows
|
||||
|
||||
Since 0.7 bbport runs natively on Windows 10/11 (x86-64, a Vulkan 1.3 GPU with a current driver:
|
||||
AMD Adrenalin, NVIDIA GeForce, Intel Arc). The same code as on Linux: the game's x86-64 code runs
|
||||
directly, the runtime's POSIX layer is implemented on Win32 (`gpu/platform/win32`), and the
|
||||
game's memory is shared with Vulkan through `VK_EXT_external_memory_host`.
|
||||
|
||||
**Package:** `Bloodborne-bbport-<version>-windows-x64.zip`. Unpack it anywhere (not into *Program Files*), start
|
||||
`Bloodborne.exe`, choose the game folder (the one with `eboot.bin`), press *Play*. Insert (or
|
||||
holding L3+R3) opens the port's menu in the game. Everything the port writes stays in that folder:
|
||||
`bbport.ini` (settings), `user\` (saves, shader cache), `out\` (prepared game files), `logs\` (one
|
||||
log per run: for a report send the newest one). `Bloodborne.exe --play` starts the game without
|
||||
the launcher window (through `bbport-run.exe`).
|
||||
|
||||
**Launcher:** WPF in the Windows 11 style, following the system's light or dark theme, in five
|
||||
languages; the same settings as on Linux (sections with descriptions), monitor, controller and
|
||||
GPU choice, the log of the running game, *Stop*.
|
||||
|
||||
**Upscalers:** FSR 3.1 and FSR 4 as on Linux. FSR 4.1.1: choose *Build from an AMD DLL…* and pick
|
||||
`amd_fidelityfx_upscaler_dx12.dll` 4.1.x; the recording runs on vkd3d-proton and DXVK from the
|
||||
package (`tools\fsr4cap\bin`), so any GPU with Vulkan can build the set (a minute or two, into
|
||||
`fsr4_411\`). FSR 4.1.2's models (INT8 for RDNA2/3, *Lite*): pick the driver's `amdxcffx64.dll`
|
||||
(Adrenalin 26.10.1 or newer, `C:\Windows\System32\DriverStore\FileRepository\u0*\`) together with
|
||||
the upscaler DLL. DLSS and DLSS Frame Generation (GeForce RTX): with `bin\bbport_dlss.dll` and
|
||||
NVIDIA's `nvngx_dlss.dll` / `nvngx_dlssg.dll` in the package.
|
||||
|
||||
**NVIDIA:** at start the port adds a *prefer maximum performance* power profile for
|
||||
`bb-probe.exe` to the driver, unless you chose a mode in NVIDIA's Control Panel yourself
|
||||
(`BB_NV_POWER=0`: leave it alone); presentation uses FIFO (`BB_NVIDIA_MAILBOX=1`: Mailbox).
|
||||
|
||||
**Online play:** the launcher's *Online* section, the same two pools as on Linux (Seamless Hunt
|
||||
with BBSC, The Hunter's Dream with Discord sign-in), UPnP and a connection check; the module is
|
||||
`online\bbnet20.dll`. **Mods:** the launcher's *Mods and patches* section, as on Linux.
|
||||
|
||||
**Building:** in MSYS2's CLANG64 shell (`mingw-w64-clang-x86_64` toolchain, CMake, Ninja, Python,
|
||||
SDL3, glslang), or cross-built from Linux with llvm-mingw; the package
|
||||
is made by `packaging/windows/package.sh` (the launcher needs the .NET 9 SDK). DLSS:
|
||||
`DLSS_SDK_ROOT=<NVIDIA's DLSS SDK for Windows>` (`sdk/DLSS-windows`).
|
||||
|
||||
## Repository layout
|
||||
|
||||
@@ -392,45 +479,71 @@ 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`) |
|
||||
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
|
||||
| `gpu/` | Renderer library, the port's own (`gpu/VENDOR.txt`): command processor, shader translator, renderer, caches, ImGui menu, FSR 4.1.1 runtime (`gpu/upscale/fsr411`); the online module (`gpu/bbnet20`) |
|
||||
| `launcher/`, `packaging/` | GTK4 launcher (Linux), WPF launcher (`launcher/windows`), the shared settings schema; Nix package, AppImage, Windows package |
|
||||
| `patches/` | Community patches for Bloodborne |
|
||||
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
|
||||
| `tests/` | Loader, runtime, patch and renderer tests |
|
||||
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](docs/motion_vectors.md), [roadmap](docs/ROADMAP.md)) |
|
||||
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/motion_vectors.md), [roadmap](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/ROADMAP.md)) |
|
||||
|
||||
Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test camera-matrices-test`.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- The new memory and translation model on by default once it is stable and tested on NVIDIA;
|
||||
the old model is removed after that.
|
||||
- NVIDIA on Windows: more reports from RTX cards in the heaviest views.
|
||||
- Textures as objects created at load time (less guessing by address, uploads outside the
|
||||
frame).
|
||||
- Shaders translated ahead of time, at install, not during play.
|
||||
- All of the game's shaders translated ahead of time (today: recipes from players' caches).
|
||||
- More CPU parallelism in GPU command processing (split the draw-recording stage further),
|
||||
scaling to all hardware threads — most important for the Steam Deck.
|
||||
- Async compute for the upscaler (the frame is GPU-bound at 4K).
|
||||
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
|
||||
memory (a memory-bridge prototype is in `tools/bridge_helper`); inputs exposed so that OptiScaler-style mapping works.
|
||||
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
|
||||
- Reactive and transparency masks for particles and fog.
|
||||
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
|
||||
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
|
||||
- Steam Deck validation of the AppImage; HDR output.
|
||||
|
||||
## Related projects
|
||||
|
||||
- [ducvd89/Bloodborne-Android](https://github.com/ducvd89/Bloodborne-Android): Bloodborne on
|
||||
Android, a fork of bbport (GSR2 / FSR 3.1 and LSFG, an on-screen controller; tested on
|
||||
Snapdragon 8 Gen 2 and 8 Elite).
|
||||
- [bloodborne-native/bloodborne-mac](https://github.com/bloodborne-native/bloodborne-mac):
|
||||
Bloodborne running natively on Apple Silicon, a macOS app built on bbport and shadPS4.
|
||||
|
||||
## Credits and licenses
|
||||
|
||||
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
|
||||
from shadPS4 (GPL-2.0-or-later). Third-party components keep their licenses:
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
|
||||
from shadPS4 (GPL-2.0-or-later). Reusing bbport's code: keep its copyright and license notices
|
||||
(`SPDX-FileCopyrightText: Copyright 2026 bbport contributors` at the top of its files) and
|
||||
[NOTICE](NOTICE), and say where the code comes from, with a link to this repository. Third-party components keep their licenses:
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+: the renderer and shader translator were derived from it and rewritten; the online module keeps its network and NP code),
|
||||
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
|
||||
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
|
||||
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
|
||||
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
|
||||
[BBSC](https://github.com/CLSAMG22/BBSC) seamless co-op (GPL-2.0+, in the online module), the
|
||||
online module's network code from [shadPS4 v0.20](https://github.com/shadps4-emu/shadPS4/tree/v.0.20.0),
|
||||
The Hunter's Dream backend from
|
||||
[droogie/bbhost](https://github.com/droogie/bbhost) (GPL-3.0-or-later, so `libbbnet20.so` as a
|
||||
whole is GPL-3.0-or-later),
|
||||
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
|
||||
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
|
||||
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
|
||||
loader come from [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc), adapted
|
||||
FSR 4.1.1 assets), [wepoll](https://github.com/piscisaureus/wepoll) (BSD-2-Clause, the Windows
|
||||
online module). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
|
||||
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) is adapted
|
||||
from [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
|
||||
AMD's FSR 4 DLLs and model data and NVIDIA's DLSS SDK and libraries are not distributed here.
|
||||
|
||||
Contributions merged into bbport (thank you; the full list with each release is in
|
||||
[docs/CONTRIBUTORS_0.7.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CONTRIBUTORS_0.7.md)): SrDeTs (#10), IgorWarzocha (#36), itachi-re
|
||||
(#43), Bainianzzz (#71), hideoneric (#78), guitaripod (#80), unTanana (#83, #84, #89), andrR89
|
||||
(#93), SaeedShow (#108), AndiMajore (#112, #113), ayozetr (#117), JuanraCM (#120), Supermedo and Awaken (the online
|
||||
module's network stack, from Supermedo's Windows fork), wheat32 (#360 larger heaps for texture packs),
|
||||
Sarxzer (#347 French), whp199 (#210, #211, #214, #342, #343), DominickCobb-rs (#346), maiconjsv
|
||||
(#339), iannitram (#293), adds39939 (#260 DLSS Frame Generation and Reflex), AndiMajore (#166,
|
||||
#224, #225), Qiming-Liu (#75, #157 Chinese), AyrtonSanabio (#153, #154), axpico (#206), msisovic (#193),
|
||||
noelmiller (#235), Obrikash (#140), Aryxa (#137), ArenaMMORPG (#352/#353), guutol (trophies),
|
||||
redditor69 (GT-VBAO), SrShadowy and Teefpyre (engine and BBSC fixes from SrShadowy's fork), and
|
||||
the players who sent their shader caches and logs.
|
||||
-410
@@ -1,410 +0,0 @@
|
||||
ДАННЫЙ ПРОЕКТ НЕ ИМЕЕТ ОТНОШЕНИЯ К SHADPS4. ВСЕ ВОПРОСЫ, СВЯЗАННЫЕ С ЭТИМ ПРОЕКТОМ - ПИШИТЕ НА ДИСКОРД СЕРВЕР https://discord.gg/KYZRKk9CB, А НЕ НА СЕРВЕР SHADPS4
|
||||
|
||||
# bbport — нативный порт Bloodborne для Linux
|
||||
|
||||
> README оригинального проекта (deadinside28/bloodborne_pc), сохранён ради технических
|
||||
> подробностей. README этого форка — [README.md](README.md) (на английском).
|
||||
|
||||
[English](BBPORT.md) · **Русский**
|
||||
|
||||
bbport — это аналог Wine + DXVK, только для одной игры: *Bloodborne* для PlayStation 4
|
||||
(CUSA03173, версия игры 1.09) на ПК с Linux x86-64. Оригинальный исполняемый файл игры
|
||||
запускается прямо на ПК:
|
||||
|
||||
- **как в Wine**, x86-64-код игры выполняется на процессоре напрямую, а системные библиотеки PS4
|
||||
заменяет собственный рантайм, написанный под эту игру;
|
||||
- **как в DXVK**, графика игры переводится в Vulkan — рендером на основе
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4), сильно доработанным под Bloodborne, вплоть
|
||||
до временного апскейлинга AMD FSR 3.1, FSR 4 и FSR 4.1.1;
|
||||
- память и работа видеочипа на AMD по умолчанию устроены как у игры для ПК (новая модель памяти
|
||||
и трансляции); NVIDIA и другие видеокарты пока работают со старой моделью памяти, как в 0.3.
|
||||
В лаунчере *Модель памяти* переключается в обе стороны.
|
||||
|
||||
До полной модели Wine + DXVK остался один шаг: новая модель и на NVIDIA, затем удаление старой
|
||||
модели памяти (см. ниже).
|
||||
|
||||
> **Файлов игры в репозитории нет.** Нужен свой дамп Bloodborne с обновлением 1.09: любое издание
|
||||
> из магазина (CUSA00900 в США, CUSA03173 — европейское Game of the Year, ...): у них один и тот же
|
||||
> исполняемый файл.
|
||||
> Проект не связан с Sony Interactive Entertainment, FromSoftware и AMD.
|
||||
|
||||
**Состояние: экспериментальный, играбельный.** Игра запускается, загружает сохранения и
|
||||
проходится (Сон Охотника и несколько локаций Ярнама) со звуком,
|
||||
геймпадом и сохранением. Полное прохождение не проверено; основательно проверена одна машина
|
||||
(Linux, AMD Radeon RX 7800 XT, Mesa/RADV).
|
||||
|
||||
## Что сделано
|
||||
|
||||
- **Нативное исполнение.** eboot заранее превращается в плоский образ памяти, в который как
|
||||
нативный код прилинкованы libc и libSceFios2 из PS4. Никакой эмуляции процессора и
|
||||
трансляции инструкций: код игры работает на полной скорости.
|
||||
- **Две модели памяти** (лаунчер → *Режим работы → Модель памяти и трансляции*; *Авто*, по
|
||||
умолчанию, выбирает по видеокарте):
|
||||
- **Старая модель памяти, как в 0.3, — по умолчанию на NVIDIA, Intel и других.** Копии памяти
|
||||
игры в VRAM, записи отслеживаются защитой страниц. Со всеми исправлениями после 0.3
|
||||
(векторы движения и апскейлер, мерцание при включённом DoF, повреждённый кэш шейдеров).
|
||||
- **Новая модель памяти и трансляции — по умолчанию на AMD.** Память
|
||||
игры находится в оперативной памяти, и GPU читает её на месте, как буферы игры для ПК; то,
|
||||
что он читает часто, держится в видеопамяти и освобождается, когда перестаёт использоваться
|
||||
(текстуры — через 20 с, буферы — через 60 с). Модуль памяти прослойки отслеживает записи CPU
|
||||
в блоки с зеркалами для согласования данных; вся память, видимая GPU, не копируется.
|
||||
Работа командного процессора PS4 переводится в команды Vulkan, а не
|
||||
эмулируется на процессоре: записи в память (`WRITE_DATA`, DMA) выполняет видеочип в порядке
|
||||
потока команд, метки (fences) пишет сам видеочип в конце конвейера, в той же точке потока,
|
||||
что и командный процессор PS4.
|
||||
На AMD память игры держится в sparse-буфере (арене); на NVIDIA, Intel и других — через модуль
|
||||
памяти прослойки: привязывается на месте, копии в VRAM — отдельные буферы, без
|
||||
sparse-перепривязок (на NVIDIA sparse binding держал видеочип секундами); `BB_LAYER_MEMORY=1/0`
|
||||
— выбрать вручную. Проверено на RX 7800 XT. На NVIDIA её можно выбрать
|
||||
вручную (*Новая*); у тестеров результат зависит от сцены, в части сцен медленнее старой
|
||||
модели, поэтому там она пока не по умолчанию. На Intel не проверялась. Без лаунчера —
|
||||
`BB_PC_MODEL=1/0` (не задано — по видеокарте, как *Авто*).
|
||||
|
||||
[Изменения в 0.5](docs/CHANGES_0.5.md); пререлизы: [pre4](docs/CHANGES_0.5-pre4.md),
|
||||
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
|
||||
|
||||
Неиспользуемые текстуры освобождаются в обоих режимах, поэтому видеопамять больше не растёт
|
||||
с каждой локацией.
|
||||
- **Разблокированная частота кадров.** Патчи сообщества (`patches/Bloodborne.xml`) переводят
|
||||
симуляцию на реальное время кадра; ~90 FPS в 4K с FSR 4 Balanced на RX 7800 XT, ~150 FPS в
|
||||
1440p с FSR 4 Quality. Есть режимы 30/60/90 FPS.
|
||||
- **Временной апскейлинг под эту игру.** Буфера скоростей в Bloodborne нет, поэтому порт
|
||||
считает векторы движения сам: движение камеры — по глубине и матрицам сцены, движение
|
||||
объектов (персонажи, ткань, оружие) — по позициям вершин прошлого кадра. Сцена сдвигается
|
||||
субпиксельным jitter (Halton) и рисуется в уменьшенном разрешении, апскейлер достраивает
|
||||
вывод (720p для Steam Deck, 1080p, 1440p или 2160p), интерфейс рисуется сразу в выходном разрешении.
|
||||
- **FSR 3.1** (FireBurn/FSR-Vulkan).
|
||||
- **FSR 4 (INT8, модель v07)** на видеокартах с нужными возможностями шейдеров Vulkan,
|
||||
включая RDNA2/3 (см. требования).
|
||||
- **FSR 4.1.1**: DLL AMD 4.1.1 один раз записывается под vkd3d-proton, а её проходы
|
||||
воспроизводятся нативно в Vulkan; результат **побитово совпадает** с DLL. Два варианта, как
|
||||
в самой DLL: INT8 на любой видеокарте с нужными возможностями шейдеров и матрицы FP8 на
|
||||
RDNA4 (RX 9000; выбирается сам). Ассеты собираются на твоей машине из одной твоей DLL
|
||||
4.1.x: в лаунчере кнопка *FSR 4.1.1 из своей DLL AMD → Выбрать DLL…* (2–5 минут, на RDNA4
|
||||
вдвое дольше; нужен свежий Proton), или `tools/fsr4cap`.
|
||||
- Быстрее шейдеров самой AMD на RDNA3: финальные проходы FSR 4 и 4.1.1 переписаны с записью
|
||||
через shared-память (в 3.5 и 2.3 раза быстрее, побитово так же); FSR 4 в 4K на RX 7800 XT
|
||||
стоит ~4 мс вместо ~6.
|
||||
- **Многопоточная обработка GPU-команд.** Поток команд PS4 разбирается в одном потоке, а
|
||||
привязка ресурсов и запись draw — в другом (двухстадийный конвейер), плюс отдельный поток
|
||||
записи команд Vulkan и вспомогательные потоки для копий памяти. В начале проекта единственный
|
||||
поток GPU ограничивал игру ~26 FPS; сейчас 90–150 FPS в зависимости от разрешения и сцены.
|
||||
- **Настройки в меню самой игры:** *Система → Изображение / Эффекты* (после *Экран/звук*) — их
|
||||
строит и рисует сама игра: разрешение вывода, апскейлер, пресет, резкость, счётчик FPS,
|
||||
детализация моделей и эффекты игры, на языке игры.
|
||||
- **Меню поверх игры** (Insert или L3+R3) в стиле диалогов игры: всё то же и расширенные
|
||||
настройки; геймпад, клавиатура и мышь.
|
||||
- **Лаунчер на GTK4** и **AppImage** для Steam Deck.
|
||||
|
||||
## Чем отличается от shadPS4
|
||||
|
||||
| | shadPS4 | bbport |
|
||||
|---|---|---|
|
||||
| Охват | Универсальный эмулятор PS4, много игр | Одна игра: Bloodborne v1.09 |
|
||||
| Загрузка | Свой ELF-загрузчик и эмуляция ядра во время работы | eboot заранее превращается (`scripts/`) в образ с прилинкованными libc/Fios2 PS4; загрузчик на C отображает его и передаёт управление игре (загрузчик и рантайм — ~5 тыс. строк) |
|
||||
| Память | Вид памяти PS4 со стороны GPU хранится копиями в VRAM, синхронизация через отслеживание записей защитой страниц | По умолчанию та же модель (как в 0.3); в режиме *Новая модель памяти и трансляции*: память игры в оперативной памяти, GPU использует её на месте, часто читаемое — в VRAM, освобождается, когда не нужно |
|
||||
| Командный процессор | Эмулируется: записи в память, DMA и метки выполняет процессор во время разбора команд | По умолчанию так же; в новом режиме переводится в команды Vulkan, их выполняет видеочип в порядке потока, метки — после реального завершения работы |
|
||||
| Системные библиотеки | Широкий HLE ОС PS4 | Небольшой рантайм (`src/runtime_*.c`) ровно под то, что вызывает Bloodborne: память, потоки, синхронизация, файлы, звук (включая ATRAC9), геймпад, сохранения, AppContent |
|
||||
| GPU | Видеоядро и рекомпилятор шейдеров shadPS4 | То же ядро (вложено, GPL) с ~200 помеченными правками (`bbport:`) и новыми модулями: двухстадийный конвейер draw, мемоизация состояния прохода и наборов текстур, уменьшенные цели сцены, векторы движения, FSR 3.1/4/4.1.1, захват кадра, профайлер GPU |
|
||||
| Поток GPU | Один поток обрабатывает весь поток команд (узкое место в Bloodborne) | Разбор и запись draw идут в разных потоках; работа масштабируется под число потоков (в том числе Steam Deck) |
|
||||
| Апскейлинг | — | Временной (FSR 3.1, FSR 4, FSR 4.1.1) на собственных векторах движения и jitter игры |
|
||||
| Патчи игры | Патч-файлы применяет эмулятор | Те же патчи сообщества компилируются при запуске (`scripts/patches.py`); разрешение рендера, эффекты и FPS — из лаунчера |
|
||||
|
||||
Без shadPS4 bbport бы не было: его рендерер и рекомпилятор шейдеров — основа графической части.
|
||||
|
||||
### Wine + DXVK для одной игры
|
||||
|
||||
- **Процессор.** Процессор PS4 — x86-64, поэтому код игры выполняется прямо на процессоре ПК, без
|
||||
эмуляции и перевода инструкций.
|
||||
- **Системные библиотеки.** Как Wine подменяет Windows API, так рантайм bbport (`src/runtime_*.c`)
|
||||
реализует ровно те функции ОС PS4, которые вызывает Bloodborne: память, потоки, файлы, звук,
|
||||
геймпад, сохранения.
|
||||
- **Память.** В новом режиме — как у игры для ПК: данные игры в оперативной памяти, видеопамять
|
||||
используется так, как её использует игра для ПК. Пока режим экспериментальный.
|
||||
- **Графика.** Поток команд видеочипа PS4 (PM4) — это записанные вызовы графического API игры:
|
||||
его пишут 99 функций статически слинкованной libGnm, так что разбор потока и перевод самих
|
||||
вызовов дают одно и то же. Шейдеры GCN переводятся в SPIR-V. В новом режиме работа командного
|
||||
процессора (записи в память, DMA, метки) тоже переводится в команды Vulkan; метки пишет сам
|
||||
видеочип. Дескрипторы ресурсов (текстур, буферов) транслятор читает на процессоре, как DXVK
|
||||
читает привязки ресурсов: текстуру PS4 нужно перевести из раскладки PS4 в образ Vulkan до
|
||||
отрисовки, поэтому процессор должен знать её дескриптор заранее — это трансляция, а не
|
||||
эмуляция. Таблицы дескрипторов (дампы движка констант) читает только транслятор, поэтому их
|
||||
выполняет процессор. Occlusion-запросы игры (~60 за кадр) — настоящие запросы Vulkan; предикация, `COPY_DATA`,
|
||||
`COND_EXEC` тоже переводятся (проверка — `BB_PM4_SELFTEST=1`).
|
||||
Подробно — [docs/EMULATION_REMOVAL_PLAN.ru.md](docs/EMULATION_REMOVAL_PLAN.ru.md), «Шаг 4».
|
||||
|
||||
«Порт» здесь — сборка под одну игру, а не переписанный исходный код: исходного кода игры у
|
||||
проекта нет, и он его не содержит.
|
||||
|
||||
## Требования
|
||||
|
||||
- Linux x86-64, видеокарта с Vulkan 1.3. Проверено: AMD RX 7800 XT, Mesa 26 (RADV).
|
||||
Режим *Новая модель памяти и трансляции* на NVIDIA пока не проверен.
|
||||
FSR 4 / 4.1.1 требуют shader Float16, Int8/Int16, целочисленных dot-произведений,
|
||||
linear compute derivatives и extended storage image formats; FSR 4.1.1 дополнительно
|
||||
требует `VK_VALVE_shader_mixed_float_dot_product`. Неподдерживаемый вариант заменяется
|
||||
на FSR 3.1 до первого кадра и недоступен в игровом меню.
|
||||
- Свой расшифрованный дамп игры: папка `CUSA03173` (eboot.bin, sce_module, ...), версия 1.09.
|
||||
Дамп обновления — отдельная папка: скопируйте её поверх базовой игры с заменой файлов. Одна
|
||||
базовая игра (1.00) падает при запуске (guest offset 0x20348b8); лаунчер и `run.sh` проверяют
|
||||
исполняемый файл и пишут, чего не хватает (`BB_SKIP_GAME_CHECK=1` отключает проверку).
|
||||
Другие издания (американское `CUSA00900`, азиатское `CUSA03023`, ...) запускают тот же
|
||||
исполняемый файл 1.09. В некоторых дампах в него уже вшит патч 60 FPS от Lance McDonald, из-за которого игра
|
||||
падает при открытии меню жестов: проверка тогда просит чистый eboot.bin (у bbport свои 60 FPS).
|
||||
- Для сборки: GCC, CMake, Ninja, Python 3, glslang, SDL3, заголовки Vulkan и библиотеки из
|
||||
`shell.nix`. С [Nix](https://nixos.org) всё берётся из `shell.nix` автоматически.
|
||||
|
||||
## Сборка и запуск
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
|
||||
bash build.sh # собирает out/bb-probe и out/gpu/libbbgpu.so
|
||||
BB_GAME_DIR=/путь/к/CUSA03173 bash run.sh
|
||||
```
|
||||
|
||||
или через лаунчер (выбрать папку игры, настройки, «Запустить»):
|
||||
|
||||
```bash
|
||||
bash launcher/bb-launcher.sh # launcher/install-desktop.sh добавит его в меню приложений
|
||||
```
|
||||
|
||||
По умолчанию папка игры ищется рядом с репозиторием (`../CUSA03173`). Сохранения и кэш шейдеров
|
||||
— в `user/` (в лаунчере можно выбрать другую папку), настройки — в `bbport.ini`. Геймпад через
|
||||
SDL3 (если их несколько, нужный выбирается в лаунчере: *Управление → Контроллер*;
|
||||
`BB_GAMEPAD=<GUID или часть имени>`). Клавиатура тоже работает, и при подключённом геймпаде
|
||||
(на Steam Deck он есть всегда); обе раскладки меняются в лаунчере (*Управление*). Имя персонажа
|
||||
вводится с клавиатуры в окне поверх игры. Тачпад: левая половина (Tab, Back/Select) открывает
|
||||
жесты, правая (Backspace) — личные вещи. PSN нет: экран «Играть по сети / вне сети» пропускается,
|
||||
игра сразу открывает главное меню вне сети (патч игры; в лаунчере *Эффекты игры → Пропускать
|
||||
выбор «по сети / вне сети»*, `BB_SKIP_NETWORK_CHOICE=0` — показывать). При нескольких мониторах нужный выбирается в
|
||||
лаунчере: *Экран → Монитор* (`BB_DISPLAY=<номер или часть имени>`).
|
||||
|
||||
**Смена разрешения и пресетов:** для вывода не 1080p (720p на Steam Deck, 1440p, 4K) вся игра
|
||||
рисуется в разрешении пресета, которое задаёт патч при запуске, — это самый быстрый путь.
|
||||
Смена вывода или пресета в игровом меню тогда требует «Применить и перезапустить игру».
|
||||
Настройка «Смена разрешения на лету» (лаунчер, игровое меню, `bbport.ini`
|
||||
`live_resolution=0|1|auto`; **по умолчанию выключена**) вместо этого оставляет игре 1080p и
|
||||
масштабирует её цели на ходу: 720p/1080p/1440p/4K и пресеты меняются без перезапуска. Это
|
||||
дороже: игра считает, что рисует 1080p, и рисует больше (например, мелких источников света
|
||||
примерно в 8 раз больше), часть целей копируется между размерами — включай только на мощных
|
||||
настольных видеокартах (`auto` включает её на дискретных GPU с 8+ ГБ, кроме NVIDIA старше
|
||||
Turing). Вывод 1080p и TAA всегда идут живым путём.
|
||||
|
||||
**TAA:** отдельный пункт сглаживания в лаунчере и игровом меню, переключается на ходу.
|
||||
Работает в разрешении вывода без модели FSR; пресет FSR сохраняется для обратного
|
||||
переключения. Native AA у FSR выполняет реконструкцию поверх полного разрешения и
|
||||
может быть тяжелее выключенного сглаживания. TAA тоже добавляет обработку кадра.
|
||||
|
||||
**Моды:** лаунчер подключает распакованные моды из отдельных папок (с `dvdroot_ps4/`, с лишней
|
||||
папкой-обёрткой или сразу с папками игры вроде `chr/`; регистр имён не важен), с переключателями
|
||||
и порядком загрузки. **Сторонние патчи:** XML-файлы в формате shadPS4 в папке `patches/` каталога
|
||||
данных, включаются в лаунчере. Подробнее — [моды и патчи](docs/MODS.md).
|
||||
**DLC:** дампы дополнений клади в `user/addcont/<title id>/<метка>/` (как в shadPS4, например
|
||||
`user/addcont/CUSA03173/SPEXPANSIONDLC03` для The Old Hunters): каждая такая папка сообщается
|
||||
игре как установленное дополнение.
|
||||
|
||||
**Язык лаунчера:** русский, английский, бразильский португальский или упрощённый китайский (по умолчанию — как в системе).
|
||||
|
||||
**Свободная камера и debug menu игры** (v1.09): включи соответствующие галочки в лаунчере
|
||||
или игровом меню и перезапусти игру. Для камеры используется
|
||||
[патч Lance McDonald из GoldHEN](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
|
||||
удерживай Cross и нажимай L3 для переключения режимов (клавиатура: Space + Z).
|
||||
Камере шрифты не нужны; с патчем Enemy Control она несовместима.
|
||||
Для debug menu сначала установи `DbgFont14h.ccm` и `DbgFont14h.tpf` из
|
||||
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) в
|
||||
`dvdroot_ps4/font/` внутри папки игры. Если файлов нет или они пустые, запуск остановится
|
||||
с объяснением. Меню открывается левым touchpad / Tab (с включённым debug menu левая половина
|
||||
больше не открывает жесты); правый touchpad — Backspace.
|
||||
Координаты касания передаются с SDL-геймпадов; Back/Select заменяет левый клик на геймпадах
|
||||
без сенсорной поверхности. Меню настроек порта по-прежнему открывается Insert / L3+R3.
|
||||
|
||||
GPU-запросы occlusion пока возвращают искусственные счётчики пикселей
|
||||
(`PixelPipeStatDump`), а `IT_SET_PREDICATION` не реализован. Свободная камера позволяет
|
||||
проверять видимость, но не добавляет GPU occlusion culling.
|
||||
|
||||
**Ассеты апскейлеров** (в репозитории их нет; FSR 3.1 они не нужны):
|
||||
|
||||
```bash
|
||||
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, собраны Q2RTX из исходников AMD)
|
||||
# FSR 4.1.1 — из своих DLL AMD (например, FSR4_LATEST у OptiScaler), нужен GE-Proton 10 или новее:
|
||||
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
|
||||
```
|
||||
|
||||
**FSR 4.1.1 из своей DLL.** Проще всего — в лаунчере: *Апскейлер → FSR 4.1.1 из своей DLL AMD →
|
||||
Выбрать DLL…* и указать `amd_fidelityfx_upscaler_dx12.dll` версии 4.1.x: из папки `FSR4_LATEST`
|
||||
OptiScaler или из игры с FSR 4.1. Этого одного файла достаточно: DLL AMD сама экспортирует
|
||||
FidelityFX API, загрузчик не нужен (через `amd_fidelityfx_loader_dx12.dll` записывалась бы только
|
||||
DLL без этих функций, и тогда лаунчер его спросит). DLL проверяется сразу, без долгой записи, и не подходят: официальная FSR 4.0.x
|
||||
(например, 4.0.3 из Pragmata — AMD включает её только на RDNA4, под Proton на других видеокартах
|
||||
она не запускается) и сборки сообщества (4.0.2b, 4.1.1b и подобные INT8-сборки OptiScaler:
|
||||
другая модель и число проходов). Дальше DLL запускается под Proton и записывается в 20
|
||||
разрешениях (ход виден в строке и во вкладке «Журнал»; на RDNA4 — второй раз, для варианта FP8), проходы переводятся в SPIR-V, и FSR 4.1.1
|
||||
сразу выбирается. Proton выбирается сам — первый, под которым DLL включает FSR 4.1: GE-Proton 10
|
||||
или новее, Proton-CachyOS, Proton Experimental (проверено: GE-Proton 11, Proton-CachyOS 11,
|
||||
Experimental от октября 2026; Proton 11.0 из Steam и GE-Proton 9 — нет). Он запускается в
|
||||
рантайме Steam, который требует (Steam ставит его при первом запуске любой игры с этим Proton),
|
||||
или через umu-launcher, если он установлен. На NixOS umu-launcher берётся из `nix-shell` (в первый
|
||||
раз скачивается, ~1,7 ГБ); AppImage запускает запись на самой системе, через systemd пользователя
|
||||
(`systemd-run --user`), так как системный `/nix` ей не виден. Запись на Steam Deck пока не проверена; если DLL не включит
|
||||
там FSR 4.1, собери на ПК и скопируй папку `fsr4_411`. То же из командной строки:
|
||||
`--build-fsr411 <DLL>` у лаунчера (и у AppImage).
|
||||
|
||||
**DLSS (NVIDIA GeForce RTX).** SDK DLSS от NVIDIA не входит в репозиторий: скачай
|
||||
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (достаточно `include/` и
|
||||
`lib/Linux_x86_64`) и собери с `DLSS_SDK_ROOT=/путь/к/DLSS bash build.sh`. Тогда собирается и мост
|
||||
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, единственный код, использующий SDK), а
|
||||
`libnvidia-ngx-dlss.so.<версия>` от NVIDIA копируется рядом с `out/bb-probe`; остальное даёт
|
||||
драйвер. Затем выбери *DLSS* в лаунчере или во внутриигровом меню: пресет задаёт разрешение
|
||||
рендера, Native AA — это DLAA. DLSS получает те же входы, что и FSR 3.1 (цвет сцены, глубину,
|
||||
векторы движения, jitter). Без библиотек, на других видеокартах или с `BB_DLSS=0` он недоступен,
|
||||
а выбранный DLSS заменяется на FSR 3.1.
|
||||
|
||||
На Linux DLSS также нужен CUDA/UVM из драйвера NVIDIA. Если устройство не создаётся с
|
||||
расширениями DLSS, рендерер повторяет попытку без них и использует FSR 3.1. Если отсутствует
|
||||
`/dev/nvidia-uvm`, загрузи установленный модуль командой `sudo modprobe nvidia_uvm` и перезапусти
|
||||
игру. Пакет `nvidia-modprobe` из репозитория дистрибутива позволяет библиотекам NVIDIA загружать
|
||||
модуль по мере необходимости.
|
||||
Если при включённом Secure Boot команда `modprobe` сообщает `Key was rejected by service`,
|
||||
зарегистрируй ключ подписи модуля по [процедуре MOK своего дистрибутива](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
|
||||
|
||||
AppImage, собранный с `DLSS_SDK_ROOT`, содержит мост и библиотеку NVIDIA (лицензия RTX SDK от NVIDIA
|
||||
это разрешает в составе приложения; её текст — `NVIDIA-DLSS-LICENSE.txt` в пакете). Более новая
|
||||
`libnvidia-ngx-dlss.so.<версия>` (из `lib/Linux_x86_64/rel` в SDK) выбирается в лаунчере:
|
||||
*Апскейлер → DLSS: библиотека NVIDIA → Выбрать файл…*, как DLL для FSR 4.1.1; она копируется в
|
||||
`<user>/dlss/`, и игра берёт её первой. `nvngx_dlss.dll` из игр — библиотека для Windows: драйвер
|
||||
NVIDIA на Linux её не загружает.
|
||||
|
||||
Скрипт из исходников берёт инструменты из системы (MinGW GCC, CMake, Ninja, Python 3,
|
||||
SPIRV-Tools, Git) или из Nix; в AppImage они уже собраны.
|
||||
|
||||
**FP8 на RDNA4.** Когда vkd3d-proton даёт кооперативные матрицы FP8 (RDNA4), DLL запускает
|
||||
другой вариант своих проходов: другие шейдеры и веса, другие размеры диспатчей. Сборка
|
||||
записывает оба: INT8 в `fsr4_411/`, FP8 в `fsr4_411/fp8/`, а игра сама выбирает FP8, если у
|
||||
видеокарты есть матрицы FP8 (`VK_EXT_shader_float8`); в журнале — `Upscaler: FSR 4.1.1 replay,
|
||||
FP8 …`. `BB_FSR411_VARIANT=int8` — принудительно INT8. Проверено на RX 7800 XT через эмуляцию FP8
|
||||
на FP16 в vkd3d-proton (`BB_FSR4CAP_FP8=1` записывает его и там, `fp8emu/`): побитово как DLL;
|
||||
**на самой RDNA4 ещё не запускалось.**
|
||||
|
||||
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`; данные в `~/.local/share/bbport`, `--play` запускает
|
||||
игру без окна лаунчера (игровой режим). Модели FSR 4.1.1 в пакет не входят: собери их кнопкой
|
||||
лаунчера (см. выше), они лягут в `~/.local/share/bbport/fsr4_411` (`BB_PACKAGE_FSR411=1` кладёт
|
||||
локальную `fsr4_411` в AppImage для своих устройств). На Steam Deck выбирай вывод 1280×720 (игра 16:9, на
|
||||
экране 1280×800 будут узкие полосы).
|
||||
|
||||
**Добавление AppImage в Steam** («Добавить стороннюю игру»): ничего настраивать не нужно,
|
||||
инструмент совместимости не важен. (Steam подгружает свой оверлей во все сторонние игры; AppImage
|
||||
убирает его до запуска своих программ, поэтому оверлея Steam в игре нет.) Если Steam запускает игры без FUSE (NixOS: FHS-песочница Steam; AppImage тогда завершается с
|
||||
*Cannot mount AppImage*), в параметрах запуска укажи
|
||||
|
||||
```
|
||||
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
|
||||
```
|
||||
|
||||
Тогда AppImage при первом запуске распакуется (~2 ГБ, `~/.cache/appimage_extracted_*`, ~10 с),
|
||||
а дальше будет запускать эту копию; новая версия AppImage распакуется заново, старую копию можно
|
||||
удалить. Без `TMPDIR` распаковка шла бы в `/tmp` Steam, а он там в оперативной памяти.
|
||||
` --play` после `%command%` — запуск без лаунчера.
|
||||
|
||||
**NVIDIA в AppImage:** пакет автоматически ищет установленный на машине 64-битный NVIDIA
|
||||
Vulkan-драйвер и подключает его вместе с комплектными AMD/Intel-драйверами. Используются
|
||||
библиотеки NVIDIA с этой машины, чтобы их версия совпадала с модулем ядра. На обычных Linux
|
||||
дистрибутивах они доступны в `/usr/lib*`; на NixOS внутренний `/nix/store` AppImage может скрывать
|
||||
драйвер хоста. В этом случае библиотеки NVIDIA нужно скопировать в доступную папку и указать
|
||||
`BB_NVIDIA_LIB_DIR=/путь/к/библиотекам` (манифест NVIDIA тоже должен быть доступен).
|
||||
С драйвером NVIDIA хоста AppImage передаёт и файлы EGL-вендоров хоста, NVIDIA первым
|
||||
(`__EGL_VENDOR_LIBRARY_FILENAMES`; без них ICD NVIDIA на некоторых системах не отдавал
|
||||
`vkCreateInstance`, #107; `BB_NVIDIA_EGL=0` — выключить). Явные `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` сохраняются. Проверка драйвера внутри пакета:
|
||||
|
||||
```bash
|
||||
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
|
||||
```
|
||||
|
||||
Пользователь подтвердил запуск с FSR 3 на GTX 1060 6GB (Fedora 44, NVIDIA 580.178.04);
|
||||
выбор FSR 4 приводил к чёрному окну. На этой конфигурации используй FSR 3.
|
||||
|
||||
MangoHud включён в AppImage; включи его галочку в лаунчере. Если MangoHud установлен ещё и в
|
||||
системе или включён оверлей производительности Steam (игровой режим Steam Deck), рисуется только
|
||||
один оверлей (два давали двоящийся текст со сдвигом).
|
||||
При запуске из исходников MangoHud нужно установить отдельно. Диагностический запуск
|
||||
с `VK_LOADER_LAYERS_DISABLE=~implicit~` отключает и MangoHud.
|
||||
|
||||
Полезные переменные: `BB_FRAME_STATS=1` (статистика кадров, в том числе строка `Memory:` —
|
||||
VRAM, GTT, RSS, текстуры и блоки памяти игры в VRAM), `BB_PC_MODEL=1` (новая модель памяти и трансляции;
|
||||
по умолчанию 0 — старая модель, как в 0.3), `BB_ANISO=N` (анизотропная фильтрация текстур сцены; по умолчанию
|
||||
16, 0 — как в игре),
|
||||
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (сколько неиспользуемые текстуры / буферы
|
||||
остаются в VRAM; 20 / 60), `BB_BREADCRUMBS=0` (без меток GPU; с ними зависание видеокарты
|
||||
называет отрисовку или dispatch, на которой она встала), `BB_GPU_PROFILE=1` (время GPU по
|
||||
проходам), `BB_FSR4_PROFILE=1` (время каждого прохода FSR 4), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
|
||||
`BB_FRAMES_AHEAD=N` (на сколько кадров поток команд может опережать GPU; по умолчанию 1, 0 — без
|
||||
ограничения), `BB_PRESENT_THREAD=0` (показ в потоке vblank, как раньше), `BB_LIVE_RES=1` (смена
|
||||
разрешения без перезапуска вместо патча при запуске для вывода не 1080p),
|
||||
`BB_PAD_RECORD=файл` / `BB_PAD_REPLAY=файл` (запись маршрута по F9 и его повтор в тестах),
|
||||
`BB_GC_BUDGET_MB=N` (бюджет кэша текстур, как на встроенных GPU), `BB_VRAM_LIMIT_MB=N` (бюджет
|
||||
VRAM считается не больше N МиБ: проверка поведения карт с малой VRAM), `BB_ASYNC_PIPELINES=0`
|
||||
(новые конвейеры компилируются сразу, игра ждёт; по умолчанию — в фоне), `BB_PRESENT_DUMP_TRIGGER=файл`
|
||||
с `BB_PRESENT_DUMP_COUNT=N` (снимок N показанных кадров подряд),
|
||||
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (вариант FSR 4.1.1; по умолчанию FP8, где у видеокарты есть
|
||||
матрицы FP8), `BB_READBACKS=0|1|2` (чтение игрой памяти, записанной GPU: по умолчанию 1, 2 —
|
||||
точно и медленно, 0 — выключено).
|
||||
Подробнее — в [docs/](docs); последние изменения — [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md),
|
||||
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
|
||||
|
||||
**Игра по сети (shadNet).** Отдельный модуль `out/gpu/libbbnet.so` (`gpu/bbnet`: сетевые и PSN-библиотеки
|
||||
shadPS4 из shadp2p, форка shadPS4, с клиентом shadNet); собирается, когда есть protobuf, nlohmann_json,
|
||||
OpenSSL, zlib и miniupnpc (`shell.nix`), входит в AppImage. Включается на странице лаунчера *Онлайн*
|
||||
(`BB_ONLINE=1`): сообщения, пятна крови и призраки — с The Hunter's Dream, колокола и призывы — через
|
||||
сервер shadNet (по умолчанию `srv.shadps4.net:31313`) с учётной записью shadNet (Online ID и пароль).
|
||||
Офлайн модуль не загружается; сборка без него играет только офлайн.
|
||||
|
||||
## Устройство репозитория
|
||||
|
||||
| Путь | Содержимое |
|
||||
|---|---|
|
||||
| `src/` | Загрузчик (`probe.c`) и HLE-рантайм |
|
||||
| `scripts/` | Подготовка образа игры, линковка модулей, компилятор патчей |
|
||||
| `gpu/` | Библиотека рендерера: видеоядро shadPS4 с правками порта (`gpu/VENDOR.txt`), прослойки, меню ImGui, рантайм FSR 4.1.1 (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
|
||||
| `launcher/`, `packaging/` | Лаунчер GTK4; пакет Nix и AppImage |
|
||||
| `patches/` | Патчи сообщества для Bloodborne |
|
||||
| `tools/` | Утилиты разработчика: скриптовые запуски, A/B-переключатели, бенчмарк FSR, переписывание шейдеров FSR 4, `fsr4cap` (запись и извлечение FSR 4.1.1) |
|
||||
| `tests/` | Тесты загрузчика, рантайма, патчей и рендерера |
|
||||
| `docs/` | Заметки и замеры ([апскейлер](docs/upscaler.md), [многопоточный GPU](docs/parallel_gpu.md), [векторы движения](docs/motion_vectors.md), [план](docs/ROADMAP.md), [журнал разработки](docs/DEVELOPMENT_LOG.ru.md)) |
|
||||
|
||||
Тесты: `bash build.sh --test`, `python3 -m unittest discover -s tests` и
|
||||
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
|
||||
|
||||
## Что будет дальше
|
||||
|
||||
- Новая модель памяти и трансляции по умолчанию и на NVIDIA, когда она станет там не медленнее;
|
||||
после этого старая модель удаляется.
|
||||
- Текстуры — объекты, которые создаются при загрузке (меньше угадывания по адресам, загрузка
|
||||
вне кадра).
|
||||
- Шейдеры переводятся заранее, при установке, а не во время игры.
|
||||
- Больше параллельности CPU в обработке GPU-команд (дальнейшее разделение стадии записи draw)
|
||||
с масштабированием на все потоки — важнее всего для Steam Deck.
|
||||
- Async compute для апскейлера (в 4K кадр упирается в GPU).
|
||||
- XeSS (суперразрешение) и генерация кадров XeFG через помощник под Wine с общей памятью Vulkan
|
||||
(прототип моста памяти — `tools/bridge_helper`); входы апскейлера
|
||||
в виде, совместимом с подходом OptiScaler.
|
||||
- Генерация кадров (сначала FSR 3.1 FG), маски реактивности и прозрачности для частиц и тумана.
|
||||
- Исправление гонок в шейдерах FSR 4.1.1 от AMD при ширине вывода, не кратной 64 (например,
|
||||
1600×900) — так же, как уже исправлена гонка у левого края в FSR 4 v07 при 1080p.
|
||||
- Проверка AppImage на Steam Deck; вывод HDR.
|
||||
|
||||
## Благодарности и лицензии
|
||||
|
||||
bbport распространяется по **GNU GPL v2 или новее** ([LICENSE](LICENSE)) — в нём код shadPS4
|
||||
(GPL-2.0-or-later). Сторонние компоненты сохраняют свои лицензии:
|
||||
видеоядро и рекомпилятор шейдеров [shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+),
|
||||
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
|
||||
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) от FireBurn (MIT; FSR 3.1 на Vulkan и
|
||||
провайдер FSR 4 v07), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
|
||||
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), шрифты DejaVu,
|
||||
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, для сборки ассетов FSR 4.1.1).
|
||||
Патчи игры — Kyo, Lance McDonald, auser1337, illusion, emoose и другие участники сообщества
|
||||
(`patches/Bloodborne.xml`). Мост DLSS (`gpu/dlss_bridge`, MIT) и загрузчик взяты из
|
||||
[Windows-порта Supermedo](https://github.com/Supermedo/bloodborne_pc), адаптированного из
|
||||
[IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
|
||||
DLL и данные модели FSR 4 от AMD, а также SDK и библиотеки DLSS от NVIDIA здесь не
|
||||
распространяются.
|
||||
@@ -1,281 +1,622 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
<https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
0. Definitions.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
1. Source Code.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
2. Basic Permissions.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
14. Revised Versions of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
NO WARRANTY
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
@@ -287,15 +628,15 @@ free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
@@ -303,36 +644,31 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Moe Ghoul>, 1 April 1989
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,36 @@
|
||||
bbport — a native PC port of Bloodborne (CUSA03173, 1.09)
|
||||
https://github.com/deadinside28/bloodborne_pc
|
||||
|
||||
This copy is the Linux fork, https://github.com/ayozetr/bloodborne_linux. bbport's own files stay
|
||||
GPL-2.0-or-later, as below. The fork as a whole is distributed under the GNU General Public
|
||||
License, version 3 or (at your option) any later version (LICENSE has its text): it includes code
|
||||
under GPL-3.0-or-later (the online module's BBSC and The Hunter's Dream parts), and everything
|
||||
else in it allows that. Files from other projects keep their own notices.
|
||||
|
||||
Copyright 2026 bbport contributors (the authors of this repository's commits and merged pull
|
||||
requests). Licensed under the GNU General Public License, version 2 or (at your option) any later
|
||||
version (the fork's LICENSE has version 3's text). No game code or data is part of this project.
|
||||
|
||||
Files of this project carry, in their first lines,
|
||||
SPDX-FileCopyrightText: Copyright 2026 bbport contributors
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
(or the license and copyright holders of the code they came from). The GPL requires these notices
|
||||
to be kept intact in copies and in works based on them (GPL-2.0, sections 1 and 2): if you reuse
|
||||
bbport's code — the runtime in src/, the GPU layer in gpu/, the scripts, the launchers — keep
|
||||
them, keep this file, and say in your own documentation that the code comes from bbport, with a
|
||||
link to the repository above. Code reused from a fork of bbport is bbport's code too.
|
||||
|
||||
Code bbport is built on, with its own notices kept in its files:
|
||||
|
||||
- shadPS4 (https://github.com/shadps4-emu/shadPS4, GPL-2.0-or-later, Copyright shadPS4 Emulator
|
||||
Project): the GPU layer and the shader translator started from its video core and shader
|
||||
recompiler and were rewritten part by part (gpu/VENDOR.txt). The online module keeps shadPS4
|
||||
v0.20.0's network, NP and shadNet code (gpu/bbnet20/vendor/shadps4, gpu/bbnet20/SHADNET-PORT.md).
|
||||
- The online module's other parts (gpu/bbnet20: BBSC, The Hunter's Dream pool; GPL-3.0-or-later
|
||||
where their files say so) keep their authors' copyright (gpu/bbnet20/SHADNET-PORT.md).
|
||||
- The DLSS bridge (gpu/dlss_bridge, MIT), adapted from IFreemz's shadPS4-Bloodborne-DLSS-FSR
|
||||
(https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
|
||||
- AMD FidelityFX shaders and headers (MIT) in gpu/upscale/shaders/ffx, gpu/third_party and
|
||||
tools/fsr4cap/ffx, with AMD's notices.
|
||||
- The libraries in gpu/third_party (sirit, imgui, half, httplib, fsr-vulkan and others), each under
|
||||
its own license in its folder.
|
||||
@@ -2,67 +2,103 @@
|
||||
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
[](#features)
|
||||
[](https://ko-fi.com/ayozetr)
|
||||
|
||||
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own x86-64 code
|
||||
runs directly on the CPU and its graphics are translated to Vulkan. It is not an emulator.
|
||||
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own code runs
|
||||
directly on the CPU and its graphics are translated to Vulkan: it is not an emulator.
|
||||
|
||||
This fork builds on [bbport](https://github.com/deadinside28/bloodborne_pc), the native Linux port
|
||||
by deadinside28. It adds the features of [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc)
|
||||
(online play, DLSS, cheats) brought to Linux, plus a few of its own.
|
||||
This fork is built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28. It
|
||||
adds improvements of its own, and others from the community, taken from pull requests and other
|
||||
projects ([credits](docs/CREDITS.md)).
|
||||
|
||||
> **No game files are included.** You need your own decrypted dump of Bloodborne with update
|
||||
> 1.09, from a retail release: [which ones, and which have been checked](docs/COMPATIBILITY.md).
|
||||
> This project is not affiliated with Sony Interactive Entertainment or FromSoftware.
|
||||
> 1.09, from a retail release ([which ones](docs/COMPATIBILITY.md)). This project is not
|
||||
> affiliated with Sony Interactive Entertainment or FromSoftware.
|
||||
|
||||
**[The players' guide](https://ayozetr.github.io/bloodborne_linux/)**: installing, playing online and
|
||||
seamless co-op, the Steam Deck, and what to do when something goes wrong.
|
||||
|
||||
## Features
|
||||
|
||||
- **Online play (beta)** through [The Hunter's Dream](https://thehuntersdream.com): messages,
|
||||
bloodstains, ghosts, bells and summons, with bbhost's players and Windows players of bbport, or
|
||||
with your group on [a server of your own](docs/SERVER.md).
|
||||
- **Upscaling:** AMD FSR 3.1, FSR 4 and FSR 4.1.1, **NVIDIA DLSS** on GeForce RTX, and
|
||||
native-resolution TAA.
|
||||
- **Unlocked frame rate**, capped at your display's refresh rate up to 120 FPS (above that the
|
||||
game's own movement timing breaks), or 30/60/90 FPS. Output from 720p to 4K.
|
||||
- **Launcher** with every setting in one window, in English, Spanish, Italian, French, German,
|
||||
Portuguese, Russian and Simplified Chinese, like the in-game menu (Insert or L3+R3).
|
||||
- **Settings in the game's own System menu** as well (Display and Effects pages), in the game's
|
||||
language.
|
||||
- **Two memory models:** the new one, where the GPU works with the game's memory as in a PC game
|
||||
(the default on AMD), and the one of 0.3 (the default elsewhere); the launcher switches them.
|
||||
- **Xbox, Switch and keyboard button prompts**, picked by the controller in use, and a gestures
|
||||
menu without the motion-control icons.
|
||||
- **Cheats and gameplay tweaks:** never die, stealth, silent steps, enemy control, camera
|
||||
distance, ragdoll physics and more.
|
||||
- **Trophies:** a notice in the game, in the style of the PS4's, and a Trophies page in the
|
||||
launcher with every trophy, from your own dump. Their names and descriptions need the PS4's
|
||||
trophy key (shadPS4's TrophyKey), which you enter in the launcher; without it, only the icons.
|
||||
- **Game effects** on and off, **mods** and community **patches**, loaded without changing the
|
||||
game files ([how](docs/MODS.en.md)).
|
||||
- **Add to Steam** in one click (native or Flatpak Steam), and **Discord Rich Presence** with the
|
||||
area you are in and your co-op partners.
|
||||
- **AppImage:** one file with everything inside, for any distribution and the Steam Deck. It
|
||||
updates itself: the launcher's **About** page checks this repository's releases and installs
|
||||
a new AppImage over the old one.
|
||||
- **Multiplayer**: online messages, bloodstains, ghosts, bells and summons, on
|
||||
[The Hunter's Dream](https://thehuntersdream.com) or a server of your own; **Seamless Co-op**
|
||||
(experimental), co-op that lasts through deaths, lamps and areas, for up to 5 hunters; and
|
||||
**local co-op** (split screen), 2 to 4 players on one PC, each with their own controller.
|
||||
- **In-game map** (Hunter's Atlas): a fullscreen map with where you are, lamps and items, and fast
|
||||
travel to the lamps you've found.
|
||||
- **Frame rate unlocked** (up to 120 FPS recommended; the game's movement can break above it)
|
||||
or capped at **30, 60 or 90**, and changed while you play from the in-game menu. Resolutions from
|
||||
**720p to 4K**, and any **aspect ratio** (4:3 to 32:9 ultrawide, [how](docs/TROUBLESHOOTING.md#the-picture-is-stretched-on-an-ultrawide-screen)).
|
||||
- **Upscaling:** AMD **FSR 3.1** and **FSR 4**, NVIDIA **DLSS** (GeForce RTX) and TAA, and
|
||||
**frame generation** (FSR 3.1, or DLSS on GeForce RTX 40 and newer).
|
||||
- **Any controller:** PlayStation, Xbox and Switch pads with their own **button prompts in the
|
||||
game**, and **keyboard and mouse**, with keys you can rebind (bbport 0.7's Dark Souls
|
||||
III-style layout, or the classic one of earlier Linux versions), movement keys that keep a
|
||||
sprint through turns (the last of two opposite keys wins; `keyboard_movement=0.7` in
|
||||
`bbport.ini` for bbport 0.7's ramped stick) and the mouse as a pointer in the game's menus.
|
||||
- **Exit to desktop** from the game's title and System menus.
|
||||
- **Cheats and gameplay tweaks:** never die, stealth, camera distance, ragdoll physics and more.
|
||||
- **Trophies and the platinum**, with a notice in the game like on the PS4.
|
||||
- **Add to Steam** in one click, and **Discord Rich Presence** with your area, your co-op
|
||||
partners and a button to this repository.
|
||||
- **Launcher and in-game menu** (Insert, or hold L3 + R3) in English, Spanish, French, German, Italian,
|
||||
Portuguese, Russian and Simplified Chinese.
|
||||
- **AppImage** ready for any distribution and the Steam Deck, which **updates itself** from the
|
||||
launcher (and can go back to an earlier release).
|
||||
- **Mods and community patches**, without touching the game files ([how](docs/MODS.en.md)).
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux x86-64 and a GPU with Vulkan 1.3: AMD or Intel with Mesa, or NVIDIA with NVIDIA's driver
|
||||
(open kernel modules included). Mesa's NVK has not been tested.
|
||||
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's driver; it does not work with NVK.
|
||||
- About 6 GB of free memory, 10 GB for 1440p or 4K output.
|
||||
- Linux x86-64 and a GPU with Vulkan 1.3: AMD or Intel with Mesa, or NVIDIA with NVIDIA's driver.
|
||||
- About 6 GB of free memory, 10 GB for 1440p or 4K.
|
||||
|
||||
## Getting started
|
||||
|
||||
### AppImage
|
||||
1. Download `Bloodborne-bbport-x86_64.AppImage` from the
|
||||
[latest release](https://github.com/ayozetr/bloodborne_linux/releases/latest).
|
||||
2. Make it executable and run it.
|
||||
3. Choose your game folder (the one with `eboot.bin`) and press **Play**.
|
||||
|
||||
Build it with `bash build.sh && bash packaging/appimage-sharun.sh`, then run
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`. Choose your game folder (the one with `eboot.bin`) and
|
||||
press **Play**. Saves and settings go to `~/.local/share/bbport`.
|
||||
Saves and settings go to `~/.local/share/bbport`; **Setup → Saves** exports your saves into a
|
||||
.zip and imports them back, or from shadPS4, and can keep a copy of them from before each game
|
||||
(off by default). The Game of the Year edition already has The Old
|
||||
Hunters; with the base game, its DLC goes in **Setup → DLC folder** ([how](docs/COMPATIBILITY.md)).
|
||||
|
||||
### From source
|
||||
## Online play
|
||||
|
||||
In the launcher's **Multiplayer → Online** tab, turn on **Play online**, choose a server and press **Test**
|
||||
in the **Status** row (every line should get a check mark):
|
||||
|
||||
- **Seamless Hunt** (the default): the community server of Seamless Co-op (BBSC), with players on
|
||||
the Windows port. Make an account from the launcher with **Create** ([docs/ONLINE.md](docs/ONLINE.md#seamless-hunt)).
|
||||
- **The Hunter's Dream**: sign in with Discord. You play with bbhost's players
|
||||
([docs/ONLINE.md](docs/ONLINE.md#the-hunters-dream-with-bbhosts-players)).
|
||||
- **Your own server**: for your group, with Docker and Tailscale ([docs/SERVER.md](docs/SERVER.md)).
|
||||
- **Seamless Co-op (BBSC)**, in the same tab: every player needs it, and summons across areas need
|
||||
its game server, which your own server can run ([docs/ONLINE.md](docs/ONLINE.md#seamless-co-op-bbsc)).
|
||||
|
||||
With online play on, the game starts online by itself, as PLAY ONLINE on a PS4. If you turned off
|
||||
*Skip the “play online / offline” choice* and pick offline, choose **Log In** on the title screen later
|
||||
for online play; until then the co-op bells are greyed out.
|
||||
|
||||
Everyone in a session needs update 1.09. Turn cheats off when you play with others. Ports,
|
||||
firewalls and the rest: [docs/ONLINE.md](docs/ONLINE.md).
|
||||
|
||||
## Local co-op (experimental)
|
||||
|
||||
Several games on one PC, one per player, each in its part of the screen with its own controller
|
||||
(the launcher's **Multiplayer → Local** tab): choose the players (2 to 4), the layout and each player's
|
||||
controller, then press **Play together**. Player 1 plays with the launcher's settings and saves;
|
||||
players 2 to 4 get their own saves and settings in `~/.local/share/bbport/coop/player<N>` (copied
|
||||
from player 1's the first time, about 400 MB each), without online play or Discord. One player can
|
||||
use the keyboard and mouse. Only player 1 has sound unless you turn that off. The windows can go
|
||||
to a monitor of their own (**Monitor**), and **Copy player 1's saves** gives another player the
|
||||
same characters (their own saves are backed up first). The windows are placed through X11, which
|
||||
works in a Wayland session too (XWayland).
|
||||
|
||||
## Building from source
|
||||
|
||||
On Arch Linux (or CachyOS, Manjaro…):
|
||||
|
||||
@@ -75,105 +111,42 @@ paru -S magic_enum vulkan-memory-allocator xbyak # from the AUR
|
||||
git clone --recursive https://github.com/ayozetr/bloodborne_linux
|
||||
cd bloodborne_linux
|
||||
bash build.sh
|
||||
python3 launcher/bbport_launcher.py # or launcher/install-desktop.sh for a menu entry
|
||||
python3 launcher/bbport_launcher.py
|
||||
```
|
||||
|
||||
With [Nix](https://nixos.org), `bash build.sh` gets every dependency from `shell.nix` by itself.
|
||||
With [Nix](https://nixos.org), `bash build.sh` gets every dependency by itself. DLSS needs
|
||||
`bash tools/build_dlss.sh` once; the AppImage is `bash packaging/appimage-sharun.sh`, which bundles
|
||||
the libraries of the PC it runs on. Releases are built with `bash packaging/appimage-in-arch.sh`
|
||||
(Docker, plain Arch Linux): on distributions built for newer processors (CachyOS's x86-64-v3
|
||||
repositories, for example) the AppImage would otherwise not start on CPUs without AVX2. Environment
|
||||
variables for the launcher's **Advanced → Developer → Extra variables**: [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md).
|
||||
Problems: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
|
||||
|
||||
### Online play
|
||||
## On Windows
|
||||
|
||||
1. In the launcher, open **Online**, turn on **Play online** and choose the co-op server:
|
||||
- **The Hunter's Dream (with bbhost players)**: sign in with Discord, or create an account
|
||||
with just a name and keep the recovery code it shows (it brings the account back on
|
||||
another PC). [bbbridge](bbbridge/README.md) by MaskZeus runs with the game and talks to
|
||||
The Hunter's Dream the way bbhost does, so you play with bbhost's players and with Windows
|
||||
players of bbport on the same pool.
|
||||
- **Your own server**: your group's shadNet server and your account on it
|
||||
([docs/SERVER.md](docs/SERVER.md), Docker and Tailscale; tested with a Linux and a Windows
|
||||
player).
|
||||
2. **Test the connection**: every line should be green.
|
||||
3. Start the game and choose online at the title screen.
|
||||
|
||||
Everyone in a session needs game version 1.09 and the same pool. Turn cheats off when you play
|
||||
with others. shadPS4's public shadNet server no longer accepts bbport builds: it signs them out
|
||||
right after the login, and the launcher says so.
|
||||
|
||||
With your own server, co-op connects the players directly over UDP. UPnP opens the port on the router by itself; with
|
||||
ufw or firewalld on, the launcher's **Online** page says so and gives the commands: pin the
|
||||
**Co-op port** (for example 3658), open it (`sudo ufw allow 3658/udp`), and for UPnP let the
|
||||
router's answers in (`sudo ufw allow from <router> proto udp`). [More in docs/ONLINE.md](docs/ONLINE.md).
|
||||
|
||||
### DLSS
|
||||
|
||||
On a PC with a GeForce RTX, run `bash tools/build_dlss.sh` once after `build.sh`. It downloads
|
||||
what it needs from the NVIDIA DLSS SDK and builds the bridge. Then choose **DLSS** as the
|
||||
upscaler.
|
||||
|
||||
### DLC
|
||||
|
||||
The Game of the Year edition (`CUSA03173`) already includes The Old Hunters. With the base game
|
||||
(such as the US `CUSA00900`), dump your add-ons too: each one is a folder (`SPEXPANSIONDLC03`,
|
||||
`SPDLCMESSENGER00`…) with only `sce_sys` inside, because their data ships with the 1.09 update.
|
||||
Put them in one folder and pick it in the launcher (**Game → DLC folder**), or set
|
||||
`BB_DLC_DIR=/path/to/dlc` for `run.sh`; the launcher shows how many it found. Each folder must
|
||||
keep the add-on's name (its entitlement label, such as `SPEXPANSIONDLC03`). Copying them to
|
||||
`user/addcont/<title id>/`, shadPS4's layout, works too.
|
||||
|
||||
### Add to Steam
|
||||
|
||||
**Game → Add to Steam** puts Bloodborne in your Steam library; it starts with the launcher's
|
||||
settings. Steam has to be closed while the game is added, and the launcher offers to do that.
|
||||
Flatpak Steam runs games inside its sandbox, which has neither the game nor your drivers, so
|
||||
the shortcut starts the game outside it with `flatpak-spawn --host`. That needs the
|
||||
`org.freedesktop.Flatpak` permission for Steam, which the launcher asks for once: it lets Steam
|
||||
start programs on your system.
|
||||
|
||||
### Button prompts
|
||||
|
||||
The game shows PlayStation buttons. In the launcher, **Controls → Button prompts** switches them to
|
||||
Xbox, Nintendo Switch or keyboard ones; *Automatic* follows the controller in use, and the
|
||||
keyboard's when none is connected. Keyboard prompts show the keys bound in **Controls →
|
||||
Keyboard**. **Gestures menu without motion-control icons** cleans up the gestures menu. The
|
||||
prompts are put into your own game's menu files at each start, and nothing in the game folder
|
||||
changes.
|
||||
If you need a Windows version, I recommend
|
||||
[Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc) and
|
||||
[Awakenchan's LanternEngine](https://github.com/Awakenchan/LanternEngine).
|
||||
|
||||
## Support
|
||||
|
||||
If this fork is useful to you and you would like to support it, you can on
|
||||
[Ko-fi](https://ko-fi.com/ayozetr). The port itself is deadinside28's work (see below).
|
||||
This fork is built on the port deadinside28 created, but it has a lot of hours of work behind it,
|
||||
and above all hours and hours of testing things and hunting down fixes so it runs as well as it
|
||||
can. I always try to fix every report as soon as possible, because I know how much this game means
|
||||
to some people. If it's useful to you and you'd like to support it, you can on
|
||||
[Ko-fi](https://ko-fi.com/ayozetr).
|
||||
|
||||
## Credits
|
||||
|
||||
- [deadinside28/bloodborne_pc](https://github.com/deadinside28/bloodborne_pc): **bbport**, the
|
||||
native Linux port this fork is built on, and almost all of the work behind it. Its README, with
|
||||
the technical details, is in [BBPORT.md](BBPORT.md).
|
||||
- [Supermedo/bloodborne_pc](https://github.com/Supermedo/bloodborne_pc): the Windows port, whose
|
||||
online play, DLSS support, cheats page, Add to Steam and icon were brought to Linux here.
|
||||
- [shadPS4](https://github.com/shadps4-emu/shadPS4): the renderer and shader recompiler.
|
||||
- [shadp2p](https://github.com/Wozzardman/shadp2p) by Wozzardman: the network and PSN libraries
|
||||
for online play. Servers by the [shadPS4](https://shadps4.net) team and
|
||||
[The Hunter's Dream](https://thehuntersdream.com) by droogie and its team.
|
||||
- [bbbridge](https://github.com/MaskZeus/bbbridge) by MaskZeus (GPL-3.0-or-later): The Hunter's
|
||||
Dream's co-op pool for bbport, built for Linux here ([bbbridge/](bbbridge/)).
|
||||
- [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR):
|
||||
the DLSS bridge, and the [NVIDIA DLSS SDK](https://github.com/NVIDIA/DLSS).
|
||||
- [FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn and the AMD FidelityFX SDK.
|
||||
- **Dommo**: the Xbox and Switch button prompts (*DS3 Style - Modern Xbox prompts*, *DS3 Style -
|
||||
Nintendo Switch prompts*) and the *Stripped down gesture menu*, from
|
||||
[Nexus Mods (Bloodborne, mod 30)](https://www.nexusmods.com/bloodborne/mods/30), uploaded by
|
||||
goomab. Only their drawn prompts are in this repository ([assets/menu_prompts](assets/menu_prompts)).
|
||||
- **Abken**: the [KBM Icons Generator](https://www.nexusmods.com/bloodborne/mods/537), which showed
|
||||
which textures keyboard prompts need (after the idea of
|
||||
[mod 460](https://www.nexusmods.com/bloodborne/mods/460)); bbport draws its own.
|
||||
- [quick-sharun](https://github.com/pkgforge-dev/Anylinux-AppImages) for the AppImage.
|
||||
- [ps4-pkg-extractor](https://github.com/paulomanrique/ps4-pkg-extractor) by paulomanrique
|
||||
(Unlicense), on [orbis-pkg-util](https://crates.io/crates/orbis-pkg-util): the launcher's
|
||||
*Install from PKG*.
|
||||
- Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and the Bloodborne community.
|
||||
|
||||
Full list of components and licenses: [BBPORT.md](BBPORT.md#credits-and-licenses).
|
||||
Built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28, with
|
||||
[shadPS4](https://github.com/shadps4-emu/shadPS4)'s renderer, and with work from many people and
|
||||
projects: everyone is in [docs/CREDITS.md](docs/CREDITS.md). It includes code from
|
||||
Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine). Components and their licenses:
|
||||
[BBPORT.md](BBPORT.md#credits-and-licenses).
|
||||
|
||||
## License
|
||||
|
||||
GNU GPL v2 or later ([LICENSE](LICENSE)). Third-party components keep their own licenses. NVIDIA
|
||||
GNU GPL v3 or later ([LICENSE](LICENSE)). Files taken from other projects keep their own notices:
|
||||
most are GPL-2.0-or-later (bbport, shadPS4), which allows them to be distributed under
|
||||
GPL-3.0-or-later. Third-party components keep their own licenses. NVIDIA
|
||||
and DLSS are trademarks of NVIDIA Corporation.
|
||||
@@ -11,6 +11,8 @@ Only what the mods drew is here, never the game's own files:
|
||||
of big prompts from the shared UI atlas (`atlas.dds`, scaled down to the game's atlas size).
|
||||
- `gesturetop.json`: the edits that turn the game's `menu/gesturetop.gfx` into the mod's.
|
||||
|
||||
`scripts/menu_prompts.py` puts them into the player's own `menu/common.tpf.dcx` and
|
||||
`menu/gesturetop.gfx` at each start; `tools/extract_prompt_assets.py` rebuilds this folder from
|
||||
the mods' ZIPs. Keyboard prompts are drawn by `scripts/menu_prompts.py` itself.
|
||||
The port draws its button prompts live (`gpu/shim/bbport_button_prompts.cpp`): `scripts/prompt_art.py`
|
||||
turns the 20 icons into the raw images it uses for the Xbox and Switch styles, and the keyboard's
|
||||
are its own. `scripts/gestures_menu.py` applies `gesturetop.json` to the player's own
|
||||
`menu/gesturetop.gfx` when the launcher's *Gestures menu without motion-control icons* is on.
|
||||
The Linux fork's `tools/extract_prompt_assets.py` rebuilt this folder from the mods' ZIPs.
|
||||
@@ -1,20 +0,0 @@
|
||||
# Your account (token + recovery code): never commit or share it.
|
||||
bbbridge_state.json
|
||||
bbbridge_state.json.*
|
||||
# Per-PC settings (the chosen bbport folder, ...)
|
||||
bbbridge_config.json
|
||||
|
||||
# Logs and trace captures (they can hold session ids)
|
||||
*.log
|
||||
captures/
|
||||
|
||||
# Build output
|
||||
build/
|
||||
bin/
|
||||
bbbridge.exe
|
||||
.vs/
|
||||
out/
|
||||
__pycache__/
|
||||
|
||||
# Release packages (tools\make_release.ps1)
|
||||
dist/
|
||||
@@ -1,40 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.2 - 2026-10-12
|
||||
|
||||
- **Fix: "Failed to acquire Bloodborne server information" after closing
|
||||
bbbridge.** The file that sends the game's server traffic to bbbridge
|
||||
(`host_overrides.custom.json` in bbport's user folder) now exists only while
|
||||
bbbridge runs with the switch on The Hunter's Dream: it goes when bbbridge
|
||||
closes, when the bridge stops, or when bbport is set back to shadNet - also
|
||||
in the bbport launcher itself. One left behind by an older bbbridge
|
||||
(including the Python 0.2.0) is cleaned up the next time bbbridge starts.
|
||||
- Troubleshooting notes in the README.
|
||||
|
||||
## 0.3.1 - 2026-10-12
|
||||
|
||||
- **Online switch** in place of the "Route bbport through bbbridge" box:
|
||||
shadNet on the left, The Hunter's Dream (bbhost) on the right. A line under
|
||||
it says which pool and account PLAY uses.
|
||||
- **shadNet comes back whole**: switching to shadNet restores bbport's own
|
||||
server *and* shadNet account (name, password). The Hunter's Dream account is
|
||||
bbport's online id only while switched to it. PLAY follows the switch; on
|
||||
shadNet no Discord sign-in is needed.
|
||||
- **Set up by default**: once signed in with Discord, the switch goes to The
|
||||
Hunter's Dream and the live map patch is added; turning either off sticks.
|
||||
- **bbport folder**: shows where it was found; bbbridge.exe right next to
|
||||
Bloodborne.exe works too; asks when it had to guess, when a chosen folder is
|
||||
gone, or when bbport is not found.
|
||||
- **Clear errors**: PLAY and "bbport settings..." say what is missing (sign-in,
|
||||
game folder, shadNet account, launcher open, missing exe, Windows' error).
|
||||
"bbport settings..." also works before the launcher has any settings.
|
||||
- **Lighter**: the window draws at full rate only while used (idle CPU about a
|
||||
quarter of before).
|
||||
- **Steadier event poll**: it no longer stalls ~8 s about once a minute, so a
|
||||
summon reaches the game without that delay.
|
||||
- Verified live: joining a bbhost host's world as a guest (Small Resonant Bell).
|
||||
|
||||
## 0.3.0 - 2026-10-12
|
||||
|
||||
- First C++ release: one window for the bridge, Discord sign-in, routing bbport,
|
||||
the live map patch and starting the game.
|
||||
@@ -1,81 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(bbbridge VERSION 0.3.2 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
# One self-contained exe: the C++ runtime is linked in (no VC++ redistributable).
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
|
||||
if(MSVC)
|
||||
add_compile_options(/utf-8 /permissive- /EHsc)
|
||||
add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00 UNICODE _UNICODE _CRT_SECURE_NO_WARNINGS)
|
||||
endif()
|
||||
|
||||
# The bridge itself: everything but the front ends.
|
||||
add_library(bbbridge_core STATIC
|
||||
src/util.cpp
|
||||
src/proto3.cpp
|
||||
src/http.cpp
|
||||
src/bbhost_client.cpp
|
||||
src/p2p.cpp
|
||||
src/bridge.cpp
|
||||
)
|
||||
target_include_directories(bbbridge_core PUBLIC src third_party)
|
||||
target_sources(bbbridge_core PRIVATE src/account_link.cpp)
|
||||
if(WIN32)
|
||||
target_link_libraries(bbbridge_core PUBLIC ws2_32 winhttp bcrypt)
|
||||
# Pointing bbport for Windows at the bridge (its launcher's settings, host overrides).
|
||||
target_sources(bbbridge_core PRIVATE src/setup.cpp)
|
||||
else()
|
||||
# Linux: BSD sockets, libcurl for HTTPS, OpenSSL for SHA-256. The bbport launcher points the
|
||||
# game at the bridge itself, so there is no setup.cpp, and no window (bbbridge-cli only).
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(CURL REQUIRED)
|
||||
find_package(OpenSSL REQUIRED COMPONENTS Crypto)
|
||||
target_link_libraries(bbbridge_core PUBLIC Threads::Threads CURL::libcurl OpenSSL::Crypto)
|
||||
target_compile_options(bbbridge_core PRIVATE -Wall -Wextra)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# Dear ImGui (third_party/imgui, v1.91.5) with the Win32 + DirectX 11 backends.
|
||||
add_library(imgui STATIC
|
||||
third_party/imgui/imgui.cpp
|
||||
third_party/imgui/imgui_draw.cpp
|
||||
third_party/imgui/imgui_tables.cpp
|
||||
third_party/imgui/imgui_widgets.cpp
|
||||
third_party/imgui/imgui_impl_win32.cpp
|
||||
third_party/imgui/imgui_impl_dx11.cpp
|
||||
)
|
||||
target_include_directories(imgui PUBLIC third_party/imgui)
|
||||
target_compile_definitions(imgui PUBLIC IMGUI_DISABLE_DEMO_WINDOWS)
|
||||
|
||||
|
||||
# The app: bbbridge.exe, next to this CMakeLists (inside bbport's folder).
|
||||
add_executable(bbbridge WIN32 src/app.cpp src/app.rc)
|
||||
target_link_libraries(bbbridge PRIVATE bbbridge_core imgui d3d11 dxgi)
|
||||
add_custom_command(TARGET bbbridge POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:bbbridge> ${CMAKE_SOURCE_DIR}/bbbridge.exe)
|
||||
endif()
|
||||
|
||||
# Headless bridge: tests, servers, troubleshooting.
|
||||
add_executable(bbbridge-cli src/main_cli.cpp)
|
||||
target_link_libraries(bbbridge-cli PRIVATE bbbridge_core)
|
||||
|
||||
# Unit tests for the byte-level parts (framing, signaling, STUN, protobuf).
|
||||
add_executable(bbbridge-unit tests/unit.cpp)
|
||||
target_link_libraries(bbbridge-unit PRIVATE bbbridge_core)
|
||||
|
||||
# Warnings at /W4 for our code (not for third_party).
|
||||
if(MSVC)
|
||||
foreach(target bbbridge_core bbbridge bbbridge-cli bbbridge-unit)
|
||||
target_compile_options(${target} PRIVATE /W4)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# The CLI and the unit tests land in bin\ (on Windows; elsewhere they stay in the build folder).
|
||||
if(WIN32)
|
||||
foreach(target bbbridge-cli bbbridge-unit)
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:${target}> ${CMAKE_SOURCE_DIR}/bin/$<TARGET_FILE_NAME:${target}>)
|
||||
endforeach()
|
||||
endif()
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "x64-release",
|
||||
"displayName": "x64 Release",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"architecture": { "value": "x64", "strategy": "external" },
|
||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_CXX_COMPILER": "cl" }
|
||||
},
|
||||
{
|
||||
"name": "x64-debug",
|
||||
"displayName": "x64 Debug",
|
||||
"inherits": "x64-release",
|
||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{ "name": "x64-release", "configurePreset": "x64-release" },
|
||||
{ "name": "x64-debug", "configurePreset": "x64-debug" }
|
||||
]
|
||||
}
|
||||
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -1,92 +0,0 @@
|
||||
# bbbridge (C++)
|
||||
|
||||
Play **co-op with bbhost players** on **The Hunter's Dream** while using
|
||||
**bbport** (Supermedo's `bloodborne_pc` for Windows, DLSS / FSR 3.1 / FSR 4).
|
||||
|
||||
bbbridge looks like a shadNet server to bbport and talks to The Hunter's Dream
|
||||
the way bbhost does: matching (rooms, bells, summons), the P2P traffic between
|
||||
the two clients (different packet framing, bbport's signaling handshake,
|
||||
The Hunter's Dream's STUN and relay), and the game's own server traffic
|
||||
(account token, play log for the live map). bbport itself is not modified.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
E:\bloodborne\ bbport
|
||||
└─ bbbridge\ this folder
|
||||
├─ bbbridge.exe the app (built)
|
||||
├─ bbbridge_config.json settings (optional)
|
||||
├─ bbbridge_state.json your account: token + recovery code - never share it
|
||||
├─ bbbridge.log log of the last run (previous: bbbridge.prev.log)
|
||||
├─ src\ C++ sources
|
||||
├─ tests\ unit tests (C++) and end-to-end tests (Python)
|
||||
├─ third_party\ nlohmann/json 3.11.3, Dear ImGui 1.91.5
|
||||
└─ bin\ bbbridge-cli.exe (headless), bbbridge-unit.exe
|
||||
```
|
||||
<img width="798" height="718" alt="resim" src="https://github.com/user-attachments/assets/672a0038-4ead-4572-9293-b6c3936ed128" />
|
||||
|
||||
|
||||
## Using it
|
||||
|
||||
1. Put the `bbbridge` folder inside bbport's folder (next to `Bloodborne.exe`)
|
||||
and start `bbbridge.exe` (close the bbport launcher first). bbbridge uses
|
||||
the bbport it sits in; anywhere else it asks which bbport to use
|
||||
(**Change...** picks another one later).
|
||||
2. **Sign in with Discord**: the app shows a code and opens The Hunter's Dream;
|
||||
sign in there with your Discord account and approve the code. Your account
|
||||
there (the same one bbhost uses) becomes bbport's online id. No password is
|
||||
stored; the account's token goes to `bbbridge_state.json`.
|
||||
3. The **Online** switch goes to **The Hunter's Dream (bbhost)** by itself
|
||||
once you are signed in (and the live map patch is added; untick it if you
|
||||
like).
|
||||
4. Press **PLAY**. The app starts the bridge if needed and launches the game
|
||||
with bbport's saved settings. You can also start the game from the bbport
|
||||
launcher - as long as bbbridge is open. Graphics, mods and cheats stay in
|
||||
the bbport launcher (**bbport settings...**) or the in-game menu (Insert).
|
||||
|
||||
Move the **Online** switch to **shadNet** to go back to bbport's own shadNet
|
||||
co-op pool: bbport gets its own server and shadNet account back (the ones it
|
||||
had before; a shadNet account is registered at
|
||||
[shadnet.shadps4.net](https://shadnet.shadps4.net) and entered in the bbport
|
||||
launcher's *Online co-op*). PLAY follows the switch: right, the bbhost pool
|
||||
with your Discord sign-in; left, bbport's own shadNet - no sign-in needed.
|
||||
Running bbhost and bbport with the same account at the same time is not
|
||||
supported.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Failed to acquire Bloodborne server information" with bbbridge closed**:
|
||||
bbbridge 0.3.1 and older (and the Python 0.2.0) left
|
||||
`user\host_overrides.custom.json` in bbport's folder (bbport's user folder),
|
||||
which sends the game's server traffic to bbbridge even when it is not
|
||||
running. Start bbbridge 0.3.2 or newer once (it removes the file when it is
|
||||
not needed), or delete that file by hand. From 0.3.2 the file exists only
|
||||
while bbbridge runs with the switch on The Hunter's Dream.
|
||||
- **Nobody comes when I ring a bell**: a Small Resonant Bell is answered only
|
||||
by a host ringing the Beckoning Bell in the same area, near your level (and
|
||||
the other way round). The live map at
|
||||
[thehuntersdream.com/map](https://thehuntersdream.com/map) shows who is
|
||||
ringing which bell where.
|
||||
|
||||
## Building (Visual Studio 2022/2026)
|
||||
|
||||
Workload **Desktop development with C++** (MSVC, Windows SDK, CMake tools).
|
||||
|
||||
- Visual Studio: *File > Open > Folder...* this folder, pick the **x64 Release**
|
||||
preset, *Build > Build All*. `bbbridge.exe` is copied here, the CLI and the
|
||||
unit tests to `bin\`.
|
||||
- Command line (Developer PowerShell):
|
||||
`cmake --preset x64-release` then `cmake --build --preset x64-release`
|
||||
|
||||
## Tests
|
||||
|
||||
- `bin\bbbridge-unit.exe` - framing, signaling, STUN, protobuf, account id.
|
||||
- `python tests\e2e\test_e2e.py` - the bridge exe against a fake bbhost
|
||||
server, a fake STUN/relay server and fake game sockets (no internet).
|
||||
|
||||
## Credits and license
|
||||
|
||||
The protocol comes from **bbhost** and The Hunter's Dream by droogie (GPL-3.0);
|
||||
**bloodborne_pc / bbport** by Supermedo on deadinside28's port and shadPS4.
|
||||
bbbridge is GPL-3.0-or-later. nlohmann/json and Dear ImGui are MIT-licensed.
|
||||
Not affiliated with Sony, FromSoftware, Supermedo or The Hunter's Dream.
|
||||
@@ -1,51 +0,0 @@
|
||||
bbbridge includes the following third-party software.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Dear ImGui 1.91.5 - https://github.com/ocornut/imgui
|
||||
------------------------------------------------------------------------------
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2024 Omar Cornut
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
JSON for Modern C++ 3.11.3 - https://github.com/nlohmann/json
|
||||
------------------------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2013-2023 Niels Lohmann <https://nlohmann.me>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,248 +0,0 @@
|
||||
#include "account_link.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
#include "http.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
namespace {
|
||||
|
||||
// POST to the account server; the reply is read whatever the status (a limit
|
||||
// is a 429 whose Message the player should see), as bbhost's auth_call does.
|
||||
bool auth_call(const std::string& base, const std::string& path, const json& body, json& reply, std::string& error) {
|
||||
http::ClientRequest req;
|
||||
req.url = base + path;
|
||||
req.body = body.dump();
|
||||
req.headers = {{"Content-Type", "application/json"}, {"Accept", "application/json"},
|
||||
{"User-Agent", std::string("bbbridge/") + kVersion}};
|
||||
const http::ClientResponse res = http::send(req);
|
||||
if (!res.ok()) {
|
||||
error = res.error;
|
||||
return false;
|
||||
}
|
||||
reply = json::parse(res.body, nullptr, false);
|
||||
if (reply.is_discarded() || !reply.is_object() || !reply.contains("ResKind")) {
|
||||
error = fmt("%s has no account service (HTTP %d)", base.c_str(), res.status);
|
||||
return false;
|
||||
}
|
||||
if (jint(reply, "ResKind", 1) != 0) {
|
||||
error = jstr(reply, "Message", "refused");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
json read_state(const std::wstring& path) {
|
||||
std::ifstream in(fspath(path));
|
||||
if (!in) return json::object();
|
||||
json j = json::parse(in, nullptr, false);
|
||||
return j.is_object() ? j : json::object();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string account_kind(const std::wstring& state_file, const std::string& npid) {
|
||||
const json state = read_state(state_file);
|
||||
if (!state.contains("accounts") || !state["accounts"].contains(npid)) return "";
|
||||
const json& e = state["accounts"][npid];
|
||||
if (jstr(e, "token").empty() && jstr(e, "recovery_code").empty()) return "";
|
||||
return jstr(e, "linked") == "discord" ? "discord" : "bbbridge";
|
||||
}
|
||||
|
||||
std::string website_code(const std::string& auth_server, const std::wstring& state_file, const std::string& npid,
|
||||
std::string& error) {
|
||||
const json state = read_state(state_file);
|
||||
const std::string token =
|
||||
state.contains("accounts") && state["accounts"].contains(npid) ? jstr(state["accounts"][npid], "token") : "";
|
||||
if (token.empty()) {
|
||||
error = "no account for '" + npid + "' yet - play once (or sign in with Discord) first";
|
||||
return "";
|
||||
}
|
||||
json reply;
|
||||
if (!auth_call(auth_server, "/auth/web_code", {{"Token", token}}, reply, error)) return "";
|
||||
const std::string code = jstr(reply, "Code");
|
||||
if (code.empty()) error = "no code in the reply";
|
||||
return code;
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool write_state(const std::wstring& path, const json& state) {
|
||||
const std::wstring tmp = path + L".tmp";
|
||||
{
|
||||
std::ofstream out(fspath(tmp), std::ios::trunc);
|
||||
if (!out) return false;
|
||||
out << state.dump(2);
|
||||
}
|
||||
return replace_file(tmp, path);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string recovery_code_of(const std::wstring& state_file, const std::string& name) {
|
||||
const json state = read_state(state_file);
|
||||
return state.contains("accounts") && state["accounts"].contains(name)
|
||||
? jstr(state["accounts"][name], "recovery_code")
|
||||
: "";
|
||||
}
|
||||
|
||||
std::string account_create_now(const std::string& auth_server, const std::wstring& state_file, const std::string& name) {
|
||||
json reply;
|
||||
std::string error;
|
||||
if (!auth_call(auth_server, "/auth/account/create", {{"Name", name}, {"DeviceName", device_name()}}, reply, error))
|
||||
return error;
|
||||
json state = read_state(state_file);
|
||||
if (!state["accounts"].is_object()) state["accounts"] = json::object();
|
||||
state["accounts"][name] = {{"bbhost_name", name}, {"recovery_code", jstr(reply, "RecoveryCode")},
|
||||
{"token", jstr(reply, "Token")}, {"online_id", jstr(reply, "OnlineId", name)}};
|
||||
if (!write_state(state_file, state)) return "could not write the account file";
|
||||
log("account: created '" + name + "' on this PC (recovery code stored in bbbridge_state.json)");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string account_recover_now(const std::string& auth_server, const std::wstring& state_file, const std::string& name,
|
||||
const std::string& recovery_code) {
|
||||
json reply;
|
||||
std::string error;
|
||||
if (!auth_call(auth_server, "/auth/recover", {{"Name", name}, {"RecoveryCode", recovery_code}}, reply, error))
|
||||
return error;
|
||||
json state = read_state(state_file);
|
||||
if (!state["accounts"].is_object()) state["accounts"] = json::object();
|
||||
const std::string fresh = jstr(reply, "RecoveryCode");
|
||||
state["accounts"][name] = {{"bbhost_name", name}, {"recovery_code", fresh.empty() ? recovery_code : fresh},
|
||||
{"token", jstr(reply, "Token")}, {"online_id", jstr(reply, "OnlineId", name)}};
|
||||
if (!write_state(state_file, state)) return "could not write the account file";
|
||||
log("account: recovered '" + name + "' (its new recovery code is in bbbridge_state.json)");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string account_sign_out(const std::string& auth_server, const std::wstring& state_file, const std::string& name) {
|
||||
json state = read_state(state_file);
|
||||
if (!state.contains("accounts") || !state["accounts"].contains(name)) return "no account for '" + name + "' here";
|
||||
json& e = state["accounts"][name];
|
||||
const std::string token = jstr(e, "token");
|
||||
if (!token.empty()) {
|
||||
json reply;
|
||||
std::string error;
|
||||
auth_call(auth_server, "/auth/logout", {{"Token", token}}, reply, error); // best effort, as bbhost
|
||||
}
|
||||
if (jstr(e, "recovery_code").empty()) state["accounts"].erase(name); // Discord: sign in again to come back
|
||||
else e.erase("token");
|
||||
if (!write_state(state_file, state)) return "could not write the account file";
|
||||
log("account: signed out of '" + name + "' on this PC");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string device_name() {
|
||||
char host[256] = {};
|
||||
#ifdef _WIN32
|
||||
size_t n = 0;
|
||||
getenv_s(&n, host, sizeof(host), "COMPUTERNAME");
|
||||
#else
|
||||
const size_t n = gethostname(host, sizeof(host) - 1) == 0 ? std::strlen(host) + 1 : 0;
|
||||
#endif
|
||||
return n > 1 ? "bbbridge on " + std::string(host).substr(0, 48) : "bbbridge";
|
||||
}
|
||||
|
||||
double DiscordLink::seconds_left() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return expires_at_ > 0 ? std::max(0.0, expires_at_ - now()) : 0.0;
|
||||
}
|
||||
|
||||
void DiscordLink::start() {
|
||||
cancel();
|
||||
cancel_ = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
user_code_.clear();
|
||||
uri_.clear();
|
||||
online_id_.clear();
|
||||
message_.clear();
|
||||
expires_at_ = 0;
|
||||
}
|
||||
state_ = State::Starting;
|
||||
worker_ = std::thread([this] { run(); });
|
||||
}
|
||||
|
||||
void DiscordLink::cancel() {
|
||||
cancel_ = true;
|
||||
if (worker_.joinable()) worker_.join();
|
||||
if (state_ == State::Starting || state_ == State::Waiting) state_ = State::Idle;
|
||||
}
|
||||
|
||||
void DiscordLink::fail(const std::string& why) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
message_ = why;
|
||||
}
|
||||
log("discord sign-in: " + why);
|
||||
state_ = State::Failed;
|
||||
}
|
||||
|
||||
void DiscordLink::run() {
|
||||
json reply;
|
||||
std::string error;
|
||||
if (!auth_call(auth_, "/auth/device/start", {{"DeviceName", device_name()}}, reply, error)) return fail(error);
|
||||
const std::string code = jstr(reply, "DeviceCode");
|
||||
int interval = static_cast<int>(jint(reply, "Interval", 5));
|
||||
if (interval < 1) interval = 5;
|
||||
const int expires = static_cast<int>(jint(reply, "ExpiresIn", 600));
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
user_code_ = jstr(reply, "UserCode");
|
||||
uri_ = jstr(reply, "VerificationUri");
|
||||
expires_at_ = now() + expires;
|
||||
if (code.empty() || user_code_.empty()) return fail("no code in the reply");
|
||||
}
|
||||
log("discord sign-in: approve code " + user_code() + " at " + uri());
|
||||
state_ = State::Waiting;
|
||||
while (!cancel_) {
|
||||
for (int i = 0; i < interval * 10 && !cancel_; ++i) Sleep(100);
|
||||
if (cancel_) return;
|
||||
if (seconds_left() <= 0) return fail("the code expired - start again");
|
||||
if (!auth_call(auth_, "/auth/device/poll", {{"DeviceCode", code}}, reply, error)) return fail(error);
|
||||
const std::string status = jstr(reply, "Status");
|
||||
if (status == "approved") {
|
||||
const std::string token = jstr(reply, "Token"), id = jstr(reply, "OnlineId");
|
||||
if (token.empty() || id.empty()) return fail("no token in the reply");
|
||||
if (!save(id, token)) return fail("could not write the account file");
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
online_id_ = id;
|
||||
}
|
||||
log("discord sign-in: linked, signed in as '" + id + "'");
|
||||
state_ = State::Approved;
|
||||
return;
|
||||
}
|
||||
if (status != "pending" && !status.empty()) return fail("the server says: " + status);
|
||||
}
|
||||
}
|
||||
|
||||
bool DiscordLink::save(const std::string& online_id, const std::string& token) {
|
||||
json state = json::object();
|
||||
{
|
||||
std::ifstream in(fspath(state_file_));
|
||||
if (in) {
|
||||
state = json::parse(in, nullptr, false);
|
||||
if (!state.is_object()) state = json::object();
|
||||
}
|
||||
}
|
||||
if (!state["accounts"].is_object()) state["accounts"] = json::object();
|
||||
// Keyed by the account's name, which becomes the NPID: the game signs in to
|
||||
// the game server under it, and the token's account matches.
|
||||
state["accounts"][online_id] = {{"bbhost_name", online_id}, {"token", token}, {"online_id", online_id},
|
||||
{"linked", "discord"}};
|
||||
const std::wstring tmp = state_file_ + L".tmp";
|
||||
{
|
||||
std::ofstream out(fspath(tmp), std::ios::trunc);
|
||||
if (!out) return false;
|
||||
out << state.dump(2);
|
||||
}
|
||||
return replace_file(tmp, state_file_);
|
||||
}
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,73 +0,0 @@
|
||||
// Sign in with Discord: the OAuth device flow bbhost uses (src/net/account.cpp).
|
||||
// POST /auth/device/start {DeviceName} -> DeviceCode, UserCode, VerificationUri, Interval, ExpiresIn
|
||||
// POST /auth/device/poll {DeviceCode} -> Status pending | approved (+ Token, OnlineId) | ...
|
||||
// The player approves the code on The Hunter's Dream with their Discord account;
|
||||
// the account and its token then go into bbbridge_state.json under its name, and
|
||||
// the bridge signs in with that token when the game connects with that NPID.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
class DiscordLink {
|
||||
public:
|
||||
enum class State { Idle, Starting, Waiting, Approved, Failed };
|
||||
|
||||
DiscordLink(std::string auth_server, std::wstring state_file) : auth_(std::move(auth_server)), state_file_(std::move(state_file)) {}
|
||||
~DiscordLink() { cancel(); }
|
||||
|
||||
void start(); // runs in its own thread; read progress with the getters
|
||||
void cancel();
|
||||
|
||||
State state() const { return state_; }
|
||||
std::string user_code() const { std::lock_guard<std::mutex> lk(mu_); return user_code_; }
|
||||
std::string uri() const { std::lock_guard<std::mutex> lk(mu_); return uri_; }
|
||||
std::string online_id() const { std::lock_guard<std::mutex> lk(mu_); return online_id_; }
|
||||
std::string message() const { std::lock_guard<std::mutex> lk(mu_); return message_; }
|
||||
double seconds_left() const;
|
||||
|
||||
private:
|
||||
void run();
|
||||
void fail(const std::string& why);
|
||||
bool save(const std::string& online_id, const std::string& token);
|
||||
|
||||
std::string auth_;
|
||||
std::wstring state_file_;
|
||||
std::atomic<State> state_{State::Idle};
|
||||
std::atomic<bool> cancel_{false};
|
||||
mutable std::mutex mu_;
|
||||
std::string user_code_, uri_, online_id_, message_;
|
||||
double expires_at_ = 0;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
std::string device_name(); // "bbbridge on <COMPUTERNAME>"
|
||||
|
||||
// The website sign-in code for an account the bridge holds a token for
|
||||
// (thehuntersdream.com/account, "Sign in with a code from the game"):
|
||||
// POST /auth/web_code {Token} -> Code. Returns "" and sets error on failure.
|
||||
std::string website_code(const std::string& auth_server, const std::wstring& state_file, const std::string& npid,
|
||||
std::string& error);
|
||||
|
||||
// What the state file holds for this NPID: "discord", "bbbridge", or "" (none yet).
|
||||
std::string account_kind(const std::wstring& state_file, const std::string& npid);
|
||||
|
||||
// bbhost's other account buttons (src/net/account.cpp), on the same server:
|
||||
// create an account named `name` on this PC; recover one with its recovery code
|
||||
// (the server answers with a new code, which replaces the old one); sign out
|
||||
// (the token is ended on the server and removed here; a recovery code stays).
|
||||
// Each returns "" on success, else the reason.
|
||||
std::string account_create_now(const std::string& auth_server, const std::wstring& state_file, const std::string& name);
|
||||
std::string account_recover_now(const std::string& auth_server, const std::wstring& state_file, const std::string& name,
|
||||
const std::string& recovery_code);
|
||||
std::string account_sign_out(const std::string& auth_server, const std::wstring& state_file, const std::string& name);
|
||||
std::string recovery_code_of(const std::wstring& state_file, const std::string& name);
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,976 +0,0 @@
|
||||
// bbbridge.exe: the bridge with a window. It sits in a folder inside bbport's
|
||||
// (E:\bloodborne\bbbridge\), keeps its config, account and log there, points
|
||||
// bbport at itself, and starts the game.
|
||||
#include "bridge.hpp" // first: winsock2.h before anything pulls in windows.h's winsock.h
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <shobjidl.h>
|
||||
|
||||
#include <deque>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
#include "account_link.hpp"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_dx11.h"
|
||||
#include "imgui_impl_win32.h"
|
||||
#include "setup.hpp"
|
||||
|
||||
#pragma comment(lib, "d3d11.lib")
|
||||
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND, UINT, WPARAM, LPARAM);
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace bb;
|
||||
|
||||
// ---- D3D11 boilerplate (from Dear ImGui's example_win32_directx11) ----
|
||||
ID3D11Device* g_device = nullptr;
|
||||
ID3D11DeviceContext* g_context = nullptr;
|
||||
IDXGISwapChain* g_swap = nullptr;
|
||||
ID3D11RenderTargetView* g_rtv = nullptr;
|
||||
UINT g_resize_w = 0, g_resize_h = 0;
|
||||
bool g_close_requested = false;
|
||||
|
||||
void create_rtv() {
|
||||
ID3D11Texture2D* back = nullptr;
|
||||
g_swap->GetBuffer(0, IID_PPV_ARGS(&back));
|
||||
g_device->CreateRenderTargetView(back, nullptr, &g_rtv);
|
||||
back->Release();
|
||||
}
|
||||
|
||||
void release_rtv() {
|
||||
if (g_rtv) g_rtv->Release();
|
||||
g_rtv = nullptr;
|
||||
}
|
||||
|
||||
bool create_device(HWND hwnd) {
|
||||
DXGI_SWAP_CHAIN_DESC sd{};
|
||||
sd.BufferCount = 2;
|
||||
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
sd.BufferDesc.RefreshRate.Numerator = 60;
|
||||
sd.BufferDesc.RefreshRate.Denominator = 1;
|
||||
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
|
||||
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
|
||||
sd.OutputWindow = hwnd;
|
||||
sd.SampleDesc.Count = 1;
|
||||
sd.Windowed = TRUE;
|
||||
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
|
||||
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0};
|
||||
D3D_FEATURE_LEVEL got;
|
||||
HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, levels, 2,
|
||||
D3D11_SDK_VERSION, &sd, &g_swap, &g_device, &got, &g_context);
|
||||
if (hr == DXGI_ERROR_UNSUPPORTED) // no GPU driver: the software rasterizer
|
||||
hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, levels, 2, D3D11_SDK_VERSION,
|
||||
&sd, &g_swap, &g_device, &got, &g_context);
|
||||
if (FAILED(hr)) return false;
|
||||
create_rtv();
|
||||
return true;
|
||||
}
|
||||
|
||||
void destroy_device() {
|
||||
release_rtv();
|
||||
if (g_swap) g_swap->Release();
|
||||
if (g_context) g_context->Release();
|
||||
if (g_device) g_device->Release();
|
||||
}
|
||||
|
||||
LRESULT WINAPI wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wp, lp)) return TRUE;
|
||||
switch (msg) {
|
||||
case WM_SIZE:
|
||||
if (wp != SIZE_MINIMIZED) {
|
||||
g_resize_w = LOWORD(lp);
|
||||
g_resize_h = HIWORD(lp);
|
||||
}
|
||||
return 0;
|
||||
case WM_SYSCOMMAND:
|
||||
if ((wp & 0xfff0) == SC_KEYMENU) return 0;
|
||||
break;
|
||||
case WM_CLOSE:
|
||||
g_close_requested = true; // the app decides (the game may still be running)
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
// ---- the app ----
|
||||
|
||||
fs::path exe_dir() {
|
||||
wchar_t buf[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, buf, MAX_PATH);
|
||||
return fs::path(buf).parent_path();
|
||||
}
|
||||
|
||||
// "" when started, else why not
|
||||
std::string launch(const fs::path& exe, const fs::path& cwd) {
|
||||
std::error_code ec;
|
||||
if (!fs::is_regular_file(exe, ec)) return narrow(exe.wstring()) + " is missing.";
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi{};
|
||||
std::wstring cmd = L"\"" + exe.wstring() + L"\"";
|
||||
if (!CreateProcessW(exe.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, cwd.c_str(), &si, &pi)) {
|
||||
const DWORD err = GetLastError();
|
||||
wchar_t* text = nullptr;
|
||||
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, err, 0, reinterpret_cast<wchar_t*>(&text), 0, nullptr);
|
||||
std::string why = text ? narrow(text) : fmt("error %lu", err);
|
||||
if (text) LocalFree(text);
|
||||
while (!why.empty() && (why.back() == '\n' || why.back() == '\r' || why.back() == '.')) why.pop_back();
|
||||
return "Could not start " + narrow(exe.wstring()) + ": " + why + ".";
|
||||
}
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
return "";
|
||||
}
|
||||
|
||||
// A two-way switch: "left" label, the switch, "right" label; *right is the
|
||||
// state. The switch and either label change it. True when changed.
|
||||
bool network_switch(const char* id, const char* left, const char* right, bool* on) {
|
||||
const ImVec4 lit(0.95f, 0.95f, 0.95f, 1), dim(0.5f, 0.5f, 0.5f, 1);
|
||||
const ImVec4 left_track(0.22f, 0.42f, 0.62f, 1), right_track(0.62f, 0.16f, 0.16f, 1);
|
||||
bool changed = false;
|
||||
ImGui::PushID(id);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextColored(*on ? dim : lit, "%s", left);
|
||||
if (ImGui::IsItemClicked() && *on) *on = false, changed = true;
|
||||
ImGui::SameLine();
|
||||
const float h = ImGui::GetFrameHeight(), w = h * 2.1f;
|
||||
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
if (ImGui::InvisibleButton("switch", ImVec2(w, h))) *on = !*on, changed = true;
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
// slide the knob over ~0.12 s
|
||||
ImGuiStorage* store = ImGui::GetStateStorage();
|
||||
const ImGuiID key = ImGui::GetID("t");
|
||||
float t = store->GetFloat(key, *on ? 1.0f : 0.0f);
|
||||
const float step = ImGui::GetIO().DeltaTime / 0.12f;
|
||||
t = *on ? (t + step > 1 ? 1 : t + step) : (t - step < 0 ? 0 : t - step);
|
||||
store->SetFloat(key, t);
|
||||
ImVec4 track(left_track.x + (right_track.x - left_track.x) * t, left_track.y + (right_track.y - left_track.y) * t,
|
||||
left_track.z + (right_track.z - left_track.z) * t, 1);
|
||||
if (hovered) track = ImVec4(track.x * 1.15f, track.y * 1.15f, track.z * 1.15f, 1);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(p, ImVec2(p.x + w, p.y + h), ImGui::GetColorU32(track), h * 0.5f);
|
||||
const float r = h * 0.5f - 3;
|
||||
dl->AddCircleFilled(ImVec2(p.x + r + 3 + t * (w - 2 * r - 6), p.y + h * 0.5f), r, ImGui::GetColorU32(lit));
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(*on ? lit : dim, "%s", right);
|
||||
if (ImGui::IsItemClicked() && !*on) *on = true, changed = true;
|
||||
ImGui::PopID();
|
||||
return changed;
|
||||
}
|
||||
|
||||
std::optional<fs::path> pick_folder(HWND owner) {
|
||||
std::optional<fs::path> out;
|
||||
IFileOpenDialog* dlg = nullptr;
|
||||
if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&dlg)))) return out;
|
||||
DWORD opts = 0;
|
||||
dlg->GetOptions(&opts);
|
||||
dlg->SetOptions(opts | FOS_PICKFOLDERS);
|
||||
dlg->SetTitle(L"bbport folder (the one with Bloodborne.exe)");
|
||||
if (SUCCEEDED(dlg->Show(owner))) {
|
||||
IShellItem* item = nullptr;
|
||||
if (SUCCEEDED(dlg->GetResult(&item))) {
|
||||
PWSTR path = nullptr;
|
||||
if (SUCCEEDED(item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
|
||||
out = fs::path(path);
|
||||
CoTaskMemFree(path);
|
||||
}
|
||||
item->Release();
|
||||
}
|
||||
}
|
||||
dlg->Release();
|
||||
return out;
|
||||
}
|
||||
|
||||
class App {
|
||||
public:
|
||||
explicit App(HWND hwnd) : hwnd_(hwnd), dir_(exe_dir()) {
|
||||
set_log_file((dir_ / L"bbbridge.log").wstring());
|
||||
set_log_sink([this](const std::string& line) {
|
||||
std::lock_guard<std::mutex> lk(log_mu_);
|
||||
lines_.push_back(line);
|
||||
while (lines_.size() > 3000) lines_.pop_front();
|
||||
scroll_ = true;
|
||||
});
|
||||
load_config();
|
||||
reload_setup();
|
||||
log(fmt("bbbridge %s - bbport: %s (%s)", kVersion, bbport_dir_.empty() ? "-" : narrow(bbport_dir_.wstring()).c_str(),
|
||||
describe(bbport_source_).c_str()));
|
||||
if (!setup_error_.empty()) log(setup_error_);
|
||||
check_folder_on_start();
|
||||
if (setup_ && setup_->routed()) start_bridge(); // routed already: be ready when the game starts
|
||||
}
|
||||
|
||||
~App() {
|
||||
stop_bridge(); // and the game's server traffic goes back to bbport's own addresses
|
||||
}
|
||||
|
||||
void frame();
|
||||
bool wants_close() {
|
||||
if (!g_close_requested) return false;
|
||||
if (game_running_ && bridge_) {
|
||||
confirm_close_ = true;
|
||||
g_close_requested = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
void load_config();
|
||||
void save_config();
|
||||
void try_setup();
|
||||
void reload_setup();
|
||||
void refresh();
|
||||
void check_folder_on_start();
|
||||
void choose_folder();
|
||||
void start_bridge();
|
||||
void stop_bridge();
|
||||
void play();
|
||||
void open_launcher();
|
||||
void fail(const std::string& message);
|
||||
void status_panel();
|
||||
void setup_panel();
|
||||
void account_row();
|
||||
void account_popups();
|
||||
void message_popups();
|
||||
void log_panel();
|
||||
void use_npid(const std::string& npid);
|
||||
void apply_defaults();
|
||||
void sync_overrides();
|
||||
bool live_map_wanted() const { return jint(config_, "live_map", 1) != 0; }
|
||||
bool route_wanted() const { return jint(config_, "route", 1) != 0; } // the box, as last ticked; bbhost by default
|
||||
bool signed_in() const { return !account_.empty() && !account_kind(state_path(), account_).empty(); }
|
||||
std::wstring state_path() const { return (dir_ / L"bbbridge_state.json").wstring(); }
|
||||
std::string auth_server() const { return jstr(config_, "auth_server", "https://thehuntersdream.com"); }
|
||||
|
||||
HWND hwnd_;
|
||||
fs::path dir_;
|
||||
json config_ = json::object();
|
||||
std::unique_ptr<Bridge> bridge_;
|
||||
std::optional<BbportSetup> setup_;
|
||||
std::string setup_error_;
|
||||
fs::path chosen_bbport_;
|
||||
fs::path bbport_dir_; // known even when setup_ is not (no launcher settings yet)
|
||||
BbportSource bbport_source_ = BbportSource::None;
|
||||
// The Hunter's Dream account (bbbridge_config.json "account"). It is bbport's
|
||||
// online id only while routed: unrouted, bbport keeps its own shadNet account.
|
||||
std::string account_;
|
||||
bool launcher_running_ = false, game_running_ = false, routed_ = false, patched_ = false;
|
||||
double last_refresh_ = 0;
|
||||
std::string error_, folder_question_;
|
||||
bool open_error_ = false, open_folder_ = false;
|
||||
bool confirm_close_ = false;
|
||||
std::unique_ptr<DiscordLink> link_;
|
||||
bool open_link_ = false, open_web_ = false;
|
||||
std::string web_code_, web_error_;
|
||||
std::mutex log_mu_;
|
||||
std::deque<std::string> lines_;
|
||||
bool scroll_ = false;
|
||||
};
|
||||
|
||||
void App::load_config() {
|
||||
std::ifstream in(dir_ / L"bbbridge_config.json");
|
||||
if (in) {
|
||||
json j = json::parse(in, nullptr, false);
|
||||
if (j.is_object()) config_ = j;
|
||||
}
|
||||
if (jhas(config_, "bbport_dir")) chosen_bbport_ = fs::path(widen(jstr(config_, "bbport_dir")));
|
||||
account_ = jstr(config_, "account");
|
||||
}
|
||||
|
||||
void App::save_config() {
|
||||
std::ofstream(dir_ / L"bbbridge_config.json", std::ios::trunc) << config_.dump(2) << "\n";
|
||||
}
|
||||
|
||||
void App::try_setup() {
|
||||
try {
|
||||
setup_.emplace(dir_, chosen_bbport_);
|
||||
setup_error_.clear();
|
||||
bbport_dir_ = setup_->bbport;
|
||||
bbport_source_ = setup_->source;
|
||||
// older bbbridge kept the account only as bbport's online id
|
||||
if (account_.empty() && !account_kind(state_path(), setup_->npid()).empty()) {
|
||||
account_ = setup_->npid();
|
||||
config_["account"] = account_;
|
||||
save_config();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
setup_.reset();
|
||||
setup_error_ = e.what();
|
||||
bbport_dir_ = find_bbport(dir_, chosen_bbport_, "", &bbport_source_); // still lets "bbport settings..." work
|
||||
}
|
||||
}
|
||||
|
||||
void App::reload_setup() {
|
||||
try_setup();
|
||||
refresh();
|
||||
}
|
||||
|
||||
// The first look: say where bbport is unless bbbridge sits inside it (the
|
||||
// normal install), and ask when it had to guess or a chosen folder is gone.
|
||||
void App::check_folder_on_start() {
|
||||
const std::string here = narrow(bbport_dir_.wstring());
|
||||
if (bbport_source_ == BbportSource::None) {
|
||||
folder_question_ = "bbport was not found.\n\nPut the bbbridge folder inside bbport's folder (the one with "
|
||||
"Bloodborne.exe), e.g. bloodborne\\bbbridge\\bbbridge.exe, or choose the bbport folder.";
|
||||
} else if (!chosen_bbport_.empty() && bbport_source_ != BbportSource::Chosen) {
|
||||
folder_question_ = "The bbport folder you chose before has no Bloodborne.exe any more:\n " +
|
||||
narrow(chosen_bbport_.wstring()) + "\n\nbbbridge found bbport here instead (" +
|
||||
describe(bbport_source_) + "):\n " + here + "\n\nUse this folder?";
|
||||
} else if (bbport_source_ == BbportSource::Launcher) {
|
||||
folder_question_ = "bbbridge is not inside a bbport folder. The bbport launcher points to this one:\n " + here +
|
||||
"\n\nUse this folder?";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
open_folder_ = true;
|
||||
}
|
||||
|
||||
void App::choose_folder() {
|
||||
auto p = pick_folder(hwnd_);
|
||||
if (!p) return;
|
||||
if (!is_bbport_dir(*p)) {
|
||||
fail(narrow(p->wstring()) + " is not a bbport folder: it has no Bloodborne.exe or Play Bloodborne.exe.\n\n"
|
||||
"Choose the folder that has Bloodborne.exe in it.");
|
||||
return;
|
||||
}
|
||||
chosen_bbport_ = *p;
|
||||
config_["bbport_dir"] = narrow(p->wstring());
|
||||
save_config();
|
||||
reload_setup();
|
||||
log("bbport folder: " + narrow(p->wstring()));
|
||||
if (!setup_error_.empty()) fail(setup_error_);
|
||||
}
|
||||
|
||||
void App::fail(const std::string& message) {
|
||||
log("error: " + message);
|
||||
error_ = message;
|
||||
open_error_ = true;
|
||||
}
|
||||
|
||||
void App::refresh() {
|
||||
launcher_running_ = launcher_running();
|
||||
game_running_ = game_running();
|
||||
if (!setup_ && !launcher_running_ && bbport_source_ != BbportSource::None) try_setup(); // e.g. the launcher just made its settings
|
||||
if (setup_) {
|
||||
try {
|
||||
if (!launcher_running_) setup_->reload(); // the launcher may have saved new settings
|
||||
} catch (...) {
|
||||
}
|
||||
routed_ = setup_->routed();
|
||||
patched_ = setup_->patched();
|
||||
if (!jint(config_, "defaults_applied", 0) && !launcher_running_ && !game_running_ && signed_in())
|
||||
apply_defaults();
|
||||
sync_overrides();
|
||||
}
|
||||
last_refresh_ = now();
|
||||
}
|
||||
|
||||
// The game's server traffic goes to bbbridge (host_overrides.custom.json) exactly
|
||||
// while bbport is switched to The Hunter's Dream and the bridge runs: with
|
||||
// bbbridge closed, or bbport set back to shadNet in its own launcher, the file
|
||||
// would leave the game unable to reach its server.
|
||||
void App::sync_overrides() {
|
||||
if (!setup_) return;
|
||||
const bool want = setup_->routed() && bridge_; // live: PLAY routes and starts the bridge in one go
|
||||
if (want == setup_->overrides_written()) return;
|
||||
try {
|
||||
setup_->set_overrides(want);
|
||||
log(want ? "the game's server traffic goes through bbbridge"
|
||||
: "the game's server traffic goes to bbport's own addresses again");
|
||||
} catch (const std::exception& e) {
|
||||
log(std::string("host_overrides.custom.json: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// Once, when there is an account to play with: route bbport through bbbridge
|
||||
// and add the live map patch, so starting from the bbport launcher works too
|
||||
// and nobody has to remember them. Switching them off later sticks.
|
||||
void App::apply_defaults() {
|
||||
try {
|
||||
if (!routed_) setup_->enable();
|
||||
if (setup_->npid() != account_) setup_->set_npid(account_);
|
||||
if (live_map_wanted() && !patched_) setup_->set_patch(true);
|
||||
} catch (const std::exception& e) {
|
||||
fail(std::string("Could not set bbport up: ") + e.what());
|
||||
return;
|
||||
}
|
||||
config_["defaults_applied"] = true;
|
||||
config_["route"] = true;
|
||||
save_config();
|
||||
routed_ = setup_->routed();
|
||||
patched_ = setup_->patched();
|
||||
log("bbport now uses bbbridge" + std::string(patched_ ? ", with the live map patch" : ""));
|
||||
if (routed_) start_bridge();
|
||||
}
|
||||
|
||||
void App::start_bridge() {
|
||||
if (bridge_) return;
|
||||
Config cfg = Config::from_json(config_);
|
||||
cfg.state_file = narrow((dir_ / L"bbbridge_state.json").wstring()); // the account, next to the exe
|
||||
auto b = std::make_unique<Bridge>(cfg);
|
||||
std::string error;
|
||||
if (!b->start(&error)) {
|
||||
fail("The bridge could not start: " + error);
|
||||
return;
|
||||
}
|
||||
bridge_ = std::move(b);
|
||||
sync_overrides();
|
||||
}
|
||||
|
||||
void App::stop_bridge() {
|
||||
if (!bridge_) return;
|
||||
bridge_->stop();
|
||||
bridge_.reset();
|
||||
log("bridge stopped");
|
||||
sync_overrides();
|
||||
}
|
||||
|
||||
void App::play() {
|
||||
refresh();
|
||||
if (!setup_) {
|
||||
fail(setup_error_);
|
||||
return;
|
||||
}
|
||||
const bool via_bridge = route_wanted();
|
||||
if (via_bridge && !signed_in()) {
|
||||
fail("Not signed in. Press 'Sign in with Discord...' first - The Hunter's Dream needs your own account.\n\n"
|
||||
"(To play in bbport's own shadNet pool instead, move the Online switch to shadNet.)");
|
||||
return;
|
||||
}
|
||||
if (launcher_running_) {
|
||||
fail("Close the bbport launcher first - it would undo bbbridge's settings when it saves.");
|
||||
return;
|
||||
}
|
||||
if (!setup_->game_ready()) {
|
||||
fail("bbport has no game folder yet. Press 'bbport settings...', choose your Bloodborne folder "
|
||||
"(CUSA03173, the one with eboot.bin), close the launcher, then press PLAY.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (via_bridge) {
|
||||
if (!routed_) setup_->enable();
|
||||
if (setup_->npid() != account_) setup_->set_npid(account_);
|
||||
if (live_map_wanted() && !patched_) setup_->set_patch(true);
|
||||
} else if (routed_) {
|
||||
setup_->disable(); // e.g. the launcher was open when the switch moved
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fail(std::string("Could not set bbport up: ") + e.what());
|
||||
return;
|
||||
}
|
||||
if (!via_bridge && setup_->online() && !setup_->shadnet_account_set()) {
|
||||
fail("bbport's online play is on but it has no shadNet account.\n\nRegister one at "
|
||||
"https://shadnet.shadps4.net, then press 'bbport settings...' > Online co-op and enter the account "
|
||||
"name (not the email) and password. Or move the Online switch to The Hunter's Dream "
|
||||
"to play with bbhost players using your Discord sign-in.");
|
||||
return;
|
||||
}
|
||||
if (via_bridge) {
|
||||
start_bridge();
|
||||
if (!bridge_) return;
|
||||
}
|
||||
if (const std::string err = launch(setup_->play_exe(), setup_->bbport); !err.empty()) {
|
||||
fail(err);
|
||||
return;
|
||||
}
|
||||
log(fmt("starting the game (%s): %s", via_bridge ? "bbhost pool through bbbridge" : "bbport's own shadNet",
|
||||
narrow(setup_->play_exe().wstring()).c_str()));
|
||||
refresh();
|
||||
}
|
||||
|
||||
void App::open_launcher() {
|
||||
refresh();
|
||||
if (bbport_dir_.empty()) {
|
||||
fail("bbport was not found. Press 'Choose bbport folder...' and pick the folder with Bloodborne.exe.");
|
||||
return;
|
||||
}
|
||||
if (launcher_running_) {
|
||||
fail("The bbport launcher is already open.");
|
||||
return;
|
||||
}
|
||||
if (game_running_) {
|
||||
fail("The game is running - close it before changing bbport's settings.");
|
||||
return;
|
||||
}
|
||||
if (const std::string err = launch(bbport_dir_ / L"Bloodborne.exe", bbport_dir_); !err.empty()) fail(err);
|
||||
}
|
||||
|
||||
void App::status_panel() {
|
||||
const BridgeStatus st = bridge_ ? bridge_->status() : BridgeStatus{};
|
||||
const ImVec4 green(0.35f, 0.85f, 0.45f, 1), grey(0.6f, 0.6f, 0.6f, 1), amber(0.95f, 0.75f, 0.3f, 1);
|
||||
ImGui::TextColored(bridge_ ? green : grey, bridge_ ? " Bridge running" : " Bridge stopped");
|
||||
if (bridge_) {
|
||||
ImGui::SameLine();
|
||||
if (st.relay_port)
|
||||
ImGui::TextDisabled(" relay port %d%s", st.relay_port,
|
||||
st.mapped ? (" - seen as " + st.mapped->str()).c_str() : "");
|
||||
else
|
||||
ImGui::TextDisabled(" waiting for The Hunter's Dream's STUN answer");
|
||||
}
|
||||
if (bridge_) {
|
||||
if (st.logged_in) ImGui::Text("Signed in as %s", st.npid.c_str());
|
||||
else if (!st.npid.empty()) ImGui::TextColored(amber, "Signing in as %s ...", st.npid.c_str());
|
||||
else ImGui::TextDisabled("Waiting for the game to connect");
|
||||
if (st.room_id) ImGui::Text("Room %llu - %d member(s)", static_cast<unsigned long long>(st.room_id), st.room_members);
|
||||
if (!st.peers.empty() && ImGui::BeginTable("peers", 4, ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("Player");
|
||||
ImGui::TableSetupColumn("Member");
|
||||
ImGui::TableSetupColumn("Packets out");
|
||||
ImGui::TableSetupColumn("Packets in");
|
||||
ImGui::TableHeadersRow();
|
||||
for (const auto& p : st.peers) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(p.online_id.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%d", p.member_id);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", static_cast<unsigned long long>(p.to_peer));
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::Text("%llu", static_cast<unsigned long long>(p.to_port));
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void App::setup_panel() {
|
||||
ImGui::SeparatorText("Setup");
|
||||
if (!bbport_dir_.empty()) {
|
||||
ImGui::Text("bbport:");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(narrow(bbport_dir_.wstring()).c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(%s)", describe(bbport_source_).c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Change...")) choose_folder();
|
||||
}
|
||||
if (!setup_) {
|
||||
ImGui::PushTextWrapPos(0);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.5f, 0.4f, 1), "%s", setup_error_.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
if (bbport_dir_.empty() && ImGui::Button("Choose bbport folder...")) choose_folder();
|
||||
return;
|
||||
}
|
||||
if (!setup_->game_ready())
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.3f, 1),
|
||||
"Choose your game folder (CUSA03173, with eboot.bin) in the bbport launcher first.");
|
||||
account_row();
|
||||
ImGui::BeginDisabled(launcher_running_);
|
||||
bool routed = routed_;
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Online:");
|
||||
ImGui::SameLine();
|
||||
if (network_switch("network", "shadNet", "The Hunter's Dream (bbhost)", &routed)) {
|
||||
try {
|
||||
if (routed) {
|
||||
setup_->enable();
|
||||
if (signed_in() && setup_->npid() != account_) setup_->set_npid(account_);
|
||||
start_bridge(); // the game's server traffic needs it running
|
||||
} else {
|
||||
setup_->disable(); // bbport's own shadNet server and account come back
|
||||
}
|
||||
config_["route"] = routed;
|
||||
save_config();
|
||||
log(routed ? "bbport now uses bbbridge" : "bbport back on its own shadNet server and account");
|
||||
} catch (const std::exception& e) {
|
||||
fail(e.what());
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Right: bbbridge connects bbport to The Hunter's Dream (bbhost players, Discord sign-in).\n"
|
||||
"Left: bbport's own shadNet server and account, without bbbridge.");
|
||||
ImGui::Indent();
|
||||
if (routed_)
|
||||
ImGui::TextDisabled(signed_in() ? "Co-op with bbhost players through bbbridge, as '%s'."
|
||||
: "Co-op with bbhost players through bbbridge - sign in with Discord first.",
|
||||
account_.c_str());
|
||||
else if (!setup_->online())
|
||||
ImGui::TextDisabled("bbport's own settings - online play is off there.");
|
||||
else if (setup_->shadnet_account_set())
|
||||
ImGui::TextDisabled("bbport's own shadNet pool (%s), as '%s'.", setup_->server().c_str(),
|
||||
setup_->npid().c_str());
|
||||
else
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.3f, 1),
|
||||
"shadNet needs a shadNet account (shadnet.shadps4.net), entered in\n"
|
||||
"bbport settings > Online co-op.");
|
||||
ImGui::Unindent();
|
||||
bool patched = patched_;
|
||||
if (ImGui::Checkbox("Live map patch (show me on thehuntersdream.com's map)", &patched)) {
|
||||
setup_->set_patch(patched);
|
||||
config_["live_map"] = patched; // PLAY keeps to it
|
||||
save_config();
|
||||
refresh();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (launcher_running_)
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.3f, 1), "The bbport launcher is open - close it to change these.");
|
||||
bool trace = jint(config_, "trace", 0) != 0;
|
||||
if (ImGui::Checkbox("Detailed log (for bug reports; applies when the bridge restarts)", &trace)) {
|
||||
config_["trace"] = trace;
|
||||
save_config();
|
||||
}
|
||||
}
|
||||
|
||||
void App::use_npid(const std::string& npid) {
|
||||
account_ = npid;
|
||||
config_["account"] = npid;
|
||||
save_config();
|
||||
// bbport's online id changes only while it plays through bbbridge (PLAY sets it too)
|
||||
if (!setup_ || !routed_ || launcher_running_) return;
|
||||
try {
|
||||
setup_->set_npid(npid);
|
||||
log(fmt("online id set to '%s'", npid.c_str()));
|
||||
} catch (const std::exception& e) {
|
||||
fail(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void App::account_row() {
|
||||
// Everyone signs in with their own Discord account on The Hunter's Dream;
|
||||
// the account's name is bbport's online id (NPID) while routed.
|
||||
const std::string kind = account_.empty() ? "" : account_kind(state_path(), account_);
|
||||
if (!kind.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.55f, 0.65f, 1.0f, 1), "Signed in as '%s'%s", account_.c_str(),
|
||||
kind == "discord" ? " (Discord)" : " (bbbridge account)");
|
||||
ImGui::BeginDisabled(launcher_running_);
|
||||
if (ImGui::Button("Website sign-in code")) {
|
||||
web_code_ = website_code(auth_server(), state_path(), account_, web_error_);
|
||||
open_web_ = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("A code for thehuntersdream.com/account, 'Sign in with a code from the game'.");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Switch account...")) {
|
||||
link_ = std::make_unique<DiscordLink>(auth_server(), state_path());
|
||||
link_->start();
|
||||
open_link_ = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Sign out")) {
|
||||
const std::string err = account_sign_out(auth_server(), state_path(), account_);
|
||||
if (!err.empty()) {
|
||||
fail("Sign out failed: " + err);
|
||||
} else {
|
||||
account_.clear();
|
||||
config_.erase("account");
|
||||
save_config();
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
return;
|
||||
}
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.3f, 1), "Not signed in. Sign in with your Discord account to play online.");
|
||||
ImGui::BeginDisabled(launcher_running_);
|
||||
if (ImGui::Button("Sign in with Discord...", ImVec2(220, 0))) {
|
||||
link_ = std::make_unique<DiscordLink>(auth_server(), state_path());
|
||||
link_->start();
|
||||
open_link_ = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("opens The Hunter's Dream: sign in there with Discord and approve the code");
|
||||
}
|
||||
|
||||
void App::account_popups() {
|
||||
if (open_link_) {
|
||||
ImGui::OpenPopup("Sign in with Discord");
|
||||
open_link_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Sign in with Discord", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
const DiscordLink::State st = link_ ? link_->state() : DiscordLink::State::Idle;
|
||||
if (st == DiscordLink::State::Starting) {
|
||||
ImGui::TextUnformatted("Asking The Hunter's Dream for a code ...");
|
||||
} else if (st == DiscordLink::State::Waiting) {
|
||||
ImGui::TextUnformatted("1. Open the sign-in page and sign in with Discord.");
|
||||
ImGui::TextUnformatted("2. Enter this code there:");
|
||||
ImGui::SetWindowFontScale(2.2f);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.85f, 0.5f, 1), " %s", link_->user_code().c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::TextDisabled("%s", link_->uri().c_str());
|
||||
if (ImGui::Button("Open sign-in page"))
|
||||
ShellExecuteW(nullptr, L"open", widen(link_->uri()).c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Copy code")) ImGui::SetClipboardText(link_->user_code().c_str());
|
||||
ImGui::TextDisabled("Waiting for the approval ... (%d s left)", static_cast<int>(link_->seconds_left()));
|
||||
} else if (st == DiscordLink::State::Approved) {
|
||||
ImGui::Text("Signed in as '%s'.", link_->online_id().c_str());
|
||||
ImGui::TextUnformatted("bbport will use this account as its online id.");
|
||||
} else if (st == DiscordLink::State::Failed) {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.5f, 0.4f, 1), "Sign-in failed: %s", link_->message().c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (st == DiscordLink::State::Approved) {
|
||||
if (ImGui::Button("Done")) {
|
||||
use_npid(link_->online_id());
|
||||
link_.reset();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
} else {
|
||||
if (st == DiscordLink::State::Failed && ImGui::Button("Try again")) link_->start();
|
||||
if (st == DiscordLink::State::Failed) ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
link_.reset();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (open_web_) {
|
||||
ImGui::OpenPopup("Website sign-in code");
|
||||
open_web_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("Website sign-in code", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
if (web_code_.empty()) {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.5f, 0.4f, 1), "No code: %s", web_error_.c_str());
|
||||
} else {
|
||||
ImGui::Text("For thehuntersdream.com/account, 'Sign in with a code from the game' (account '%s'):",
|
||||
account_.c_str());
|
||||
ImGui::SetWindowFontScale(2.2f);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.85f, 0.5f, 1), " %s", web_code_.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::TextDisabled("Valid for a few minutes.");
|
||||
if (ImGui::Button("Copy code")) ImGui::SetClipboardText(web_code_.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Open the account page"))
|
||||
ShellExecuteW(nullptr, L"open", widen(auth_server() + "/account").c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void App::message_popups() {
|
||||
const float wrap = ImGui::GetFontSize() * 34;
|
||||
if (open_folder_) {
|
||||
ImGui::OpenPopup("bbport folder");
|
||||
open_folder_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("bbport folder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::PushTextWrapPos(wrap);
|
||||
ImGui::TextUnformatted(folder_question_.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Separator();
|
||||
if (!bbport_dir_.empty() && ImGui::Button("Use this folder")) {
|
||||
if (bbport_source_ == BbportSource::ExeFolder) { // the normal place: nothing to remember
|
||||
chosen_bbport_.clear();
|
||||
config_.erase("bbport_dir");
|
||||
} else {
|
||||
chosen_bbport_ = bbport_dir_;
|
||||
config_["bbport_dir"] = narrow(bbport_dir_.wstring());
|
||||
}
|
||||
save_config();
|
||||
reload_setup();
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
if (!bbport_dir_.empty()) ImGui::SameLine();
|
||||
if (ImGui::Button("Choose another folder...")) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
choose_folder();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close")) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (open_error_) {
|
||||
ImGui::OpenPopup("bbbridge###error");
|
||||
open_error_ = false;
|
||||
}
|
||||
if (ImGui::BeginPopupModal("bbbridge###error", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::PushTextWrapPos(wrap);
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.6f, 0.5f, 1), "%s", error_.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("OK", ImVec2(100, 0)) || ImGui::IsKeyPressed(ImGuiKey_Enter)) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void App::log_panel() {
|
||||
ImGui::SeparatorText("Log");
|
||||
if (ImGui::SmallButton("Copy log")) {
|
||||
std::string all;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(log_mu_);
|
||||
for (const auto& l : lines_) all += l + "\n";
|
||||
}
|
||||
ImGui::SetClipboardText(all.c_str());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Open folder")) ShellExecuteW(nullptr, L"open", dir_.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
ImGui::BeginChild("log", ImVec2(0, 0), ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(log_mu_);
|
||||
ImGuiListClipper clip;
|
||||
clip.Begin(static_cast<int>(lines_.size()));
|
||||
while (clip.Step())
|
||||
for (int i = clip.DisplayStart; i < clip.DisplayEnd; ++i) ImGui::TextUnformatted(lines_[i].c_str());
|
||||
if (scroll_ && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 40) ImGui::SetScrollHereY(1.0f);
|
||||
scroll_ = false;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void App::frame() {
|
||||
if (now() - last_refresh_ > 1.5) refresh();
|
||||
const ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(vp->WorkPos);
|
||||
ImGui::SetNextWindowSize(vp->WorkSize);
|
||||
ImGui::Begin("bbbridge", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::Text("bbbridge %s", kVersion);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("- co-op with bbhost players on The Hunter's Dream");
|
||||
ImGui::Spacing();
|
||||
status_panel();
|
||||
ImGui::Spacing();
|
||||
setup_panel();
|
||||
ImGui::Spacing();
|
||||
|
||||
// PLAY and bbport settings stay clickable: when something is missing they say what.
|
||||
ImGui::BeginDisabled(game_running_);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.55f, 0.12f, 0.12f, 1));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.16f, 0.16f, 1));
|
||||
if (ImGui::Button(game_running_ ? " PLAYING " : " PLAY ", ImVec2(160, 40))) play();
|
||||
ImGui::PopStyleColor(2);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(bridge_ ? "Stop bridge" : "Start bridge", ImVec2(130, 40))) {
|
||||
if (bridge_) stop_bridge();
|
||||
else start_bridge();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("bbport settings...", ImVec2(160, 40))) open_launcher();
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Opens the bbport launcher for graphics, mods and cheats. Close it before pressing PLAY here.");
|
||||
ImGui::Spacing();
|
||||
log_panel();
|
||||
|
||||
account_popups();
|
||||
message_popups();
|
||||
if (confirm_close_) ImGui::OpenPopup("Close bbbridge?");
|
||||
if (ImGui::BeginPopupModal("Close bbbridge?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
ImGui::TextUnformatted("The game is still running. Closing bbbridge takes it offline.");
|
||||
if (ImGui::Button("Close anyway")) {
|
||||
confirm_close_ = false;
|
||||
g_close_requested = true;
|
||||
game_running_ = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel")) {
|
||||
confirm_close_ = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR, int) {
|
||||
// One bbbridge at a time: a second would fight the first over the ports.
|
||||
HANDLE single = CreateMutexW(nullptr, TRUE, L"Local\\bbbridge-app");
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
if (HWND other = FindWindowW(L"bbbridge", nullptr)) SetForegroundWindow(other);
|
||||
return 0;
|
||||
}
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
net_init();
|
||||
ImGui_ImplWin32_EnableDpiAwareness();
|
||||
WNDCLASSEXW wc{sizeof(wc), CS_CLASSDC, wnd_proc, 0, 0, inst, LoadIconW(inst, MAKEINTRESOURCEW(1)), nullptr,
|
||||
nullptr, nullptr, L"bbbridge", nullptr};
|
||||
RegisterClassExW(&wc);
|
||||
const float scale = ImGui_ImplWin32_GetDpiScaleForMonitor(MonitorFromPoint(POINT{0, 0}, MONITOR_DEFAULTTOPRIMARY));
|
||||
HWND hwnd = CreateWindowW(wc.lpszClassName, L"bbbridge", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
static_cast<int>(820 * scale), static_cast<int>(760 * scale), nullptr, nullptr, inst, nullptr);
|
||||
if (!create_device(hwnd)) {
|
||||
MessageBoxW(nullptr, L"Direct3D 11 is not available.", L"bbbridge", MB_ICONERROR);
|
||||
return 1;
|
||||
}
|
||||
ShowWindow(hwnd, SW_SHOWDEFAULT);
|
||||
UpdateWindow(hwnd);
|
||||
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.IniFilename = nullptr;
|
||||
ImGui::StyleColorsDark();
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(14, 12);
|
||||
style.FrameRounding = 4;
|
||||
style.ScaleAllSizes(scale);
|
||||
static const ImWchar ranges[] = {0x0020, 0x00FF, 0x0100, 0x017F, 0x2013, 0x2026, 0}; // Latin-1 + Ext-A
|
||||
if (GetFileAttributesW(L"C:\\Windows\\Fonts\\segoeui.ttf") != INVALID_FILE_ATTRIBUTES)
|
||||
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", 17.0f * scale, nullptr, ranges);
|
||||
ImGui_ImplWin32_Init(hwnd);
|
||||
ImGui_ImplDX11_Init(g_device, g_context);
|
||||
|
||||
{
|
||||
App app(hwnd);
|
||||
bool done = false;
|
||||
double last_input = now();
|
||||
while (!done) {
|
||||
// Full rate (vsync) only while the player is using the window; idle
|
||||
// ~10 frames a second (the log and status still move), minimized 4 -
|
||||
// the game is the one that should have the CPU and GPU.
|
||||
const bool minimized = IsIconic(hwnd) != 0;
|
||||
if (now() - last_input > 0.5)
|
||||
MsgWaitForMultipleObjects(0, nullptr, FALSE, minimized ? 250 : 100, QS_ALLINPUT);
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
if (msg.message == WM_QUIT) done = true;
|
||||
if (!minimized && msg.message != WM_TIMER && msg.message != WM_PAINT) last_input = now();
|
||||
}
|
||||
if (done) break;
|
||||
if (app.wants_close()) {
|
||||
DestroyWindow(hwnd);
|
||||
continue;
|
||||
}
|
||||
if (g_resize_w) {
|
||||
release_rtv();
|
||||
g_swap->ResizeBuffers(0, g_resize_w, g_resize_h, DXGI_FORMAT_UNKNOWN, 0);
|
||||
g_resize_w = g_resize_h = 0;
|
||||
create_rtv();
|
||||
}
|
||||
ImGui_ImplDX11_NewFrame();
|
||||
ImGui_ImplWin32_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
app.frame();
|
||||
ImGui::Render();
|
||||
if (minimized) continue; // the app still runs (status, close); nothing to show
|
||||
const float clear[4] = {0.07f, 0.07f, 0.08f, 1};
|
||||
g_context->OMSetRenderTargets(1, &g_rtv, nullptr);
|
||||
g_context->ClearRenderTargetView(g_rtv, clear);
|
||||
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
||||
g_swap->Present(1, 0);
|
||||
}
|
||||
} // the App stops the bridge here
|
||||
ImGui_ImplDX11_Shutdown();
|
||||
ImGui_ImplWin32_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
destroy_device();
|
||||
UnregisterClassW(wc.lpszClassName, wc.hInstance);
|
||||
CoUninitialize();
|
||||
if (single) CloseHandle(single);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#include <winver.h>
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 0,3,2,0
|
||||
PRODUCTVERSION 0,3,2,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "FileDescription", "bbbridge - co-op with bbhost players for bbport"
|
||||
VALUE "FileVersion", "0.3.2"
|
||||
VALUE "ProductName", "bbbridge"
|
||||
VALUE "ProductVersion", "0.3.2"
|
||||
VALUE "LegalCopyright", "GPL-3.0-or-later"
|
||||
VALUE "OriginalFilename", "bbbridge.exe"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
@@ -1,141 +0,0 @@
|
||||
#include "bbhost_client.hpp"
|
||||
|
||||
#include "http.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
BBHostClient::Reply BBHostClient::post(const std::string& path, const json& body, int timeout_ms, http::Pool* pool) {
|
||||
http::ClientRequest req;
|
||||
req.method = "POST";
|
||||
req.url = base_ + path;
|
||||
req.body = body.dump();
|
||||
req.timeout_ms = timeout_ms;
|
||||
req.headers = {{"Content-Type", "application/json"}, {"Accept", "application/json"}, {"User-Agent", user_agent}};
|
||||
const std::string tok = token();
|
||||
if (!tok.empty()) req.headers.emplace_back("X-BB-Token", tok);
|
||||
if (!ruleset.empty()) req.headers.emplace_back("X-BBHost-Ruleset", ruleset);
|
||||
http::ClientResponse res = pool ? pool->send(req) : http::send(req);
|
||||
if (!res.ok()) throw BridgeError(path + ": " + res.error);
|
||||
Reply r;
|
||||
r.status = res.status;
|
||||
r.body = json::parse(res.body, nullptr, false);
|
||||
if (r.body.is_discarded()) r.body = json::object();
|
||||
return r;
|
||||
}
|
||||
|
||||
json BBHostClient::call(const std::string& path, const json& body, int timeout_ms) {
|
||||
Reply r = post(path, body, timeout_ms);
|
||||
if (r.status < 200 || r.status >= 300)
|
||||
throw BridgeError(fmt("%s: HTTP %d %s", path.c_str(), r.status, jstr(r.body, "Message").c_str()));
|
||||
if (!r.body.is_object() || !r.body.contains("ResKind"))
|
||||
throw BridgeError(fmt("%s: no account/matching service (HTTP %d)", path.c_str(), r.status));
|
||||
if (jint(r.body, "ResKind", -1) != 0)
|
||||
throw BridgeError(path + ": refused: " + jstr(r.body, "Message", "unknown"));
|
||||
return r.body;
|
||||
}
|
||||
|
||||
std::string BBHostClient::account_create(const std::string& name) {
|
||||
json r = call("/auth/account/create", {{"Name", name}, {"DeviceName", "bbbridge"}});
|
||||
set_identity(jstr(r, "Token"), jstr(r, "OnlineId", name));
|
||||
return jstr(r, "RecoveryCode");
|
||||
}
|
||||
|
||||
std::string BBHostClient::account_recover(const std::string& name, const std::string& code) {
|
||||
json r = call("/auth/recover", {{"Name", name}, {"RecoveryCode", code}});
|
||||
set_identity(jstr(r, "Token"), jstr(r, "OnlineId", name));
|
||||
return jstr(r, "RecoveryCode");
|
||||
}
|
||||
|
||||
std::optional<bool> BBHostClient::token_valid(const std::string& online_id) {
|
||||
try {
|
||||
Reply r = post("/np/signaling/resolve", {{"OnlineId", online_id}}, 6000);
|
||||
if (r.status == 401) return false;
|
||||
if (r.status >= 200 && r.status < 500) return true;
|
||||
} catch (const BridgeError&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
json BBHostClient::context_start(const std::string& online_id, const Endpoint& signaling, const Endpoint& mapped) {
|
||||
return call("/mp/matching2/context_start", {{"OnlineId", online_id},
|
||||
{"SignalingAddr", signaling.ip},
|
||||
{"SignalingPort", signaling.port},
|
||||
{"MappedAddr", mapped.ip},
|
||||
{"MappedPort", mapped.port}});
|
||||
}
|
||||
|
||||
json BBHostClient::create_room(const std::string& online_id, const Endpoint& local, const Endpoint& mapped,
|
||||
int max_members, const json& extra) {
|
||||
json body = {{"OnlineId", online_id}, {"LocalAddr", local.ip}, {"LocalPort", local.port},
|
||||
{"PublicAddr", local.ip}, {"PublicPort", local.port}, {"MappedAddr", mapped.ip},
|
||||
{"MappedPort", mapped.port}, {"MaxMembers", max_members}};
|
||||
if (extra.is_object())
|
||||
for (auto it = extra.begin(); it != extra.end(); ++it) body[it.key()] = it.value();
|
||||
return call("/mp/matching2/create_room", body);
|
||||
}
|
||||
|
||||
json BBHostClient::join_room(const std::string& online_id, uint64_t room_id, const Endpoint& local,
|
||||
const Endpoint& mapped) {
|
||||
return call("/mp/matching2/join_room", {{"OnlineId", online_id}, {"RoomId", room_id},
|
||||
{"LocalAddr", local.ip}, {"LocalPort", local.port},
|
||||
{"PublicAddr", local.ip}, {"PublicPort", local.port},
|
||||
{"MappedAddr", mapped.ip}, {"MappedPort", mapped.port}});
|
||||
}
|
||||
|
||||
void BBHostClient::leave_room(const std::string& session_id, int member_id) {
|
||||
call("/mp/matching2/leave_room", {{"SessionId", session_id}, {"MemberId", member_id}});
|
||||
}
|
||||
|
||||
int BBHostClient::heartbeat(const std::string& session_id, int member_id) {
|
||||
try {
|
||||
json r = call("/mp/matching2/heartbeat", {{"SessionId", session_id}, {"MemberId", member_id}}, 4000);
|
||||
return static_cast<int>(jint(r, "InRoom", 1));
|
||||
} catch (const BridgeError&) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void BBHostClient::kick_member(const std::string& session_id, int member_id, int kicker_id) {
|
||||
call("/mp/matching2/kick_member",
|
||||
{{"SessionId", session_id}, {"MemberId", member_id}, {"KickerMemberId", kicker_id}, {"OptData", ""}});
|
||||
}
|
||||
|
||||
void BBHostClient::signaling_update(const std::string& online_id, const Endpoint& mapped,
|
||||
const std::string& session_id, int member_id) {
|
||||
try {
|
||||
call("/mp/matching2/signaling_update", {{"OnlineId", online_id}, {"MappedAddr", mapped.ip},
|
||||
{"MappedPort", mapped.port}, {"SessionId", session_id},
|
||||
{"MemberId", member_id}});
|
||||
} catch (const BridgeError& e) {
|
||||
log(std::string("signaling_update failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Endpoint> BBHostClient::resolve_peer(const std::string& online_id) {
|
||||
try {
|
||||
json r = call("/np/signaling/resolve", {{"OnlineId", online_id}});
|
||||
Endpoint e{jstr(r, "Addr"), static_cast<int>(jint(r, "Port"))};
|
||||
if (e.valid()) return e;
|
||||
} catch (const BridgeError&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uint64_t BBHostClient::poll_events(const std::string& online_id, uint64_t cursor, json& event) {
|
||||
Reply r = post("/np/events/poll", {{"OnlineId", online_id}, {"Cursor", cursor}, {"Categories", {"matching2"}}},
|
||||
5000, &poll_pool_);
|
||||
if (r.status == 401) throw BridgeError("server refused our token (HTTP 401)");
|
||||
if (r.status < 200 || r.status >= 300) throw BridgeError(fmt("/np/events/poll: HTTP %d", r.status));
|
||||
event = jint(r.body, "HasEvent", 0) ? r.body : json();
|
||||
return static_cast<uint64_t>(jint(r.body, "NextCursor", static_cast<int64_t>(cursor)));
|
||||
}
|
||||
|
||||
void BBHostClient::ack_event(const std::string& online_id, uint64_t cursor) {
|
||||
try {
|
||||
post("/np/events/ack", {{"OnlineId", online_id}, {"Cursor", cursor}}, 4000);
|
||||
} catch (const BridgeError& e) {
|
||||
log(std::string("event ack failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,75 +0,0 @@
|
||||
// The Hunter's Dream (bbhost server) REST client. The contract is bbhost's own
|
||||
// client code (GPL-3.0): src/net/http.cpp (headers, ResKind), src/net/account.cpp,
|
||||
// src/net/session.cpp, src/hle/np_matching2.cpp.
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "http.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
struct BridgeError : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class BBHostClient {
|
||||
public:
|
||||
explicit BBHostClient(std::string base) : base_(std::move(base)) {
|
||||
while (!base_.empty() && base_.back() == '/') base_.pop_back();
|
||||
}
|
||||
|
||||
std::string user_agent = "bbbridge";
|
||||
std::string ruleset = "vanilla";
|
||||
|
||||
std::string token() const { std::lock_guard<std::mutex> lk(mu_); return token_; }
|
||||
std::string online_id() const { std::lock_guard<std::mutex> lk(mu_); return online_id_; }
|
||||
void set_identity(const std::string& token, const std::string& online_id) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
token_ = token;
|
||||
online_id_ = online_id;
|
||||
}
|
||||
|
||||
// account; each returns the recovery code the server sent ("" if none)
|
||||
std::string account_create(const std::string& name);
|
||||
std::string account_recover(const std::string& name, const std::string& code);
|
||||
// true accepted, false refused (401), nullopt when it could not be asked
|
||||
std::optional<bool> token_valid(const std::string& online_id);
|
||||
|
||||
// matching
|
||||
json context_start(const std::string& online_id, const Endpoint& signaling, const Endpoint& mapped);
|
||||
json create_room(const std::string& online_id, const Endpoint& local, const Endpoint& mapped, int max_members,
|
||||
const json& extra);
|
||||
json join_room(const std::string& online_id, uint64_t room_id, const Endpoint& local, const Endpoint& mapped);
|
||||
void leave_room(const std::string& session_id, int member_id);
|
||||
int heartbeat(const std::string& session_id, int member_id); // InRoom, -1 when not asked
|
||||
void kick_member(const std::string& session_id, int member_id, int kicker_id);
|
||||
void signaling_update(const std::string& online_id, const Endpoint& mapped, const std::string& session_id,
|
||||
int member_id);
|
||||
std::optional<Endpoint> resolve_peer(const std::string& online_id);
|
||||
|
||||
// events: returns the next cursor; `event` is null when there was none
|
||||
uint64_t poll_events(const std::string& online_id, uint64_t cursor, json& event);
|
||||
void ack_event(const std::string& online_id, uint64_t cursor);
|
||||
|
||||
private:
|
||||
struct Reply {
|
||||
int status = 0;
|
||||
json body;
|
||||
};
|
||||
// throws on transport failure; pool: a connection of its own (else the shared one)
|
||||
Reply post(const std::string& path, const json& body, int timeout_ms = 8000, http::Pool* pool = nullptr);
|
||||
json call(const std::string& path, const json& body, int timeout_ms = 8000); // + ResKind == 0
|
||||
|
||||
http::Pool poll_pool_{45.0}; // the event poll, every 0.4 s: redialled before it is a minute old
|
||||
std::string base_;
|
||||
mutable std::mutex mu_;
|
||||
std::string token_;
|
||||
std::string online_id_;
|
||||
};
|
||||
|
||||
} // namespace bb
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,195 +0,0 @@
|
||||
// bbbridge core: a local shadNet server for bbport that speaks The Hunter's
|
||||
// Dream's REST API (bbhost's protocol) behind it, plus the P2P gateway, the
|
||||
// WebAPI the game's summon calls go to, and the game-server proxy.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "bbhost_client.hpp"
|
||||
#include "http.hpp"
|
||||
#include "p2p.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
uint64_t account_id_for(const std::string& npid); // the PSN-style id bbport hands the game
|
||||
|
||||
struct Config {
|
||||
std::string bbhost_server = "https://thehuntersdream.com:18671";
|
||||
std::string auth_server = "https://thehuntersdream.com"; // Discord sign-in, website codes (bbhost's auth_server)
|
||||
std::string community_server = "https://thehuntersdream.com:18671";
|
||||
std::string bind_host = "127.0.0.1";
|
||||
int tcp_port = 31313, udp_port = 31314, webapi_port = 31315;
|
||||
bool p2p_gateway = true;
|
||||
int p2p_port = 0; // outer UDP port, 0 = any
|
||||
int port_p2p_port = 3658; // bbport's P2P UDP port (SHADPS4_P2P_PORT)
|
||||
std::string stun_server; // "host:port", "" = the bbhost host on 3478, "off"
|
||||
std::string local_addr; // LAN address to register, "" = detected
|
||||
double poll_interval = 0.4;
|
||||
std::string state_file = "bbbridge_state.json";
|
||||
bool trace = false;
|
||||
bool debug_endpoints = false; // /debug/* on the WebAPI (tests only)
|
||||
bool create_accounts = false; // make an account for an unknown NPID (tests); players sign in with Discord
|
||||
double stun_refresh = 15.0; // seconds between STUN refreshes (tests shorten it)
|
||||
std::map<int, std::string> game_proxy_routes = {{31316, "https://thehuntersdream.com:20443"},
|
||||
{31317, "https://thehuntersdream.com:18671"}};
|
||||
std::string user_agent = std::string("bbbridge/") + kVersion;
|
||||
|
||||
static Config from_json(const json& j);
|
||||
std::optional<std::pair<std::string, int>> stun_endpoint() const;
|
||||
};
|
||||
|
||||
class Bridge;
|
||||
|
||||
class TcpConn {
|
||||
public:
|
||||
explicit TcpConn(SOCKET s) : sock(s) {}
|
||||
bool send(const std::string& data);
|
||||
void close();
|
||||
SOCKET sock;
|
||||
std::atomic<bool> alive{true};
|
||||
|
||||
private:
|
||||
std::mutex send_mu_;
|
||||
};
|
||||
|
||||
struct RoomState {
|
||||
uint64_t room_id = 0;
|
||||
std::string session_id;
|
||||
int member_id = 0, owner_id = 0, max_slot = 5;
|
||||
std::map<int, json> members; // member_id -> {OnlineId, Addr, Port, LocalAddr, LocalPort, MappedAddr, MappedPort}
|
||||
void clear() { *this = RoomState{}; }
|
||||
};
|
||||
|
||||
class Session {
|
||||
public:
|
||||
Session(Bridge& bridge, std::shared_ptr<TcpConn> conn);
|
||||
~Session();
|
||||
std::pair<uint8_t, std::string> handle(uint16_t command, uint64_t packet_id, const std::string& payload);
|
||||
void shutdown();
|
||||
|
||||
bool logged_in() const { return logged_in_; }
|
||||
std::string npid() const;
|
||||
BBHostClient bb;
|
||||
std::atomic<bool> terminate_requested{false};
|
||||
std::shared_ptr<TcpConn> conn;
|
||||
|
||||
// for the bridge (mapped-address change, summon advertisement, app status)
|
||||
void on_mapped_change(const Endpoint& mapped);
|
||||
void set_advertisement(const json& adv);
|
||||
uint64_t room_id() const;
|
||||
int room_members() const;
|
||||
|
||||
private:
|
||||
using Reply = std::pair<uint8_t, std::string>;
|
||||
Reply on_login(const std::string& payload);
|
||||
Reply on_get_token();
|
||||
Reply on_context_start(const std::string& payload);
|
||||
Reply on_context_stop();
|
||||
Reply on_get_world_info_list();
|
||||
Reply on_create_room(const std::string& payload);
|
||||
Reply on_join_room(const std::string& payload);
|
||||
Reply on_leave_room(const std::string& payload);
|
||||
Reply on_request_signaling_infos(const std::string& payload);
|
||||
Reply on_room_id_echo(const std::string& payload); // SetRoomData{Internal,External}
|
||||
Reply on_kickout(const std::string& payload);
|
||||
|
||||
bool bbhost_account();
|
||||
std::pair<Endpoint, Endpoint> p2p_addresses();
|
||||
json host_extra_from_advertisement() const;
|
||||
static Endpoint member_endpoint(const json& member);
|
||||
Endpoint port_endpoint_for(const json& member);
|
||||
std::optional<Endpoint> port_p2p_endpoint() const;
|
||||
void forget_peers(std::optional<int> member_id = std::nullopt);
|
||||
void ensure_self_member();
|
||||
void absorb_members(const json& members);
|
||||
std::string build_details(int me_id, int owner_id);
|
||||
std::string member_data(const json& member, int owner_id);
|
||||
void leave_room_quietly();
|
||||
void send_room_event(uint32_t event, uint32_t cause, const json& member, int64_t error_code = 0);
|
||||
void start_pump();
|
||||
void pump_main();
|
||||
void handle_event(const json& event);
|
||||
|
||||
Bridge& bridge_;
|
||||
mutable std::recursive_mutex mu_; // the session's state; the TCP and pump threads share it
|
||||
std::string npid_, title_id_;
|
||||
uint64_t ctx_id_ = 0;
|
||||
std::atomic<bool> logged_in_{false};
|
||||
uint64_t account_id_ = 0;
|
||||
RoomState room_;
|
||||
json advertisement_;
|
||||
std::vector<json> pending_guest_invites_;
|
||||
std::thread pump_;
|
||||
std::atomic<bool> pump_running_{false};
|
||||
std::mutex join_mu_; // shutdown() can come from the TCP thread and Bridge::stop at once
|
||||
std::mutex pump_mu_;
|
||||
std::condition_variable pump_cv_;
|
||||
bool pump_stop_ = false;
|
||||
};
|
||||
|
||||
struct BridgeStatus {
|
||||
bool running = false;
|
||||
std::string npid;
|
||||
bool logged_in = false;
|
||||
uint64_t room_id = 0;
|
||||
int room_members = 0;
|
||||
int relay_port = 0;
|
||||
std::optional<Endpoint> mapped;
|
||||
std::vector<p2p::PeerInfo> peers;
|
||||
};
|
||||
|
||||
class Bridge {
|
||||
public:
|
||||
explicit Bridge(Config cfg) : cfg(std::move(cfg)) {}
|
||||
~Bridge() { stop(); }
|
||||
bool start(std::string* error);
|
||||
void stop();
|
||||
BridgeStatus status() const;
|
||||
|
||||
const Config cfg;
|
||||
std::unique_ptr<p2p::Gateway> gateway;
|
||||
|
||||
// shared with sessions
|
||||
json load_state();
|
||||
void save_state(const json& state);
|
||||
std::optional<Endpoint> registry_get(const std::string& npid) const;
|
||||
std::pair<std::string, std::string> identity() const; // (token, ruleset) of the logged-in player
|
||||
json last_advertisement() const;
|
||||
|
||||
private:
|
||||
void tcp_accept_loop();
|
||||
void tcp_serve(std::shared_ptr<TcpConn> conn);
|
||||
void stun_loop();
|
||||
http::ServerResponse webapi(const http::ServerRequest& req);
|
||||
http::ServerResponse summon(const http::ServerRequest& req);
|
||||
http::ServerResponse game_proxy(const std::string& upstream, const http::ServerRequest& req);
|
||||
void capture(const std::string& name, const std::string& request, int status, const std::string& response,
|
||||
const std::string& note);
|
||||
void on_mapped_change(const Endpoint& mapped);
|
||||
|
||||
SOCKET tcp_ = INVALID_SOCKET, stun_ = INVALID_SOCKET;
|
||||
std::thread tcp_thread_, stun_thread_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<int> tcp_active_{0};
|
||||
http::Server webapi_server_;
|
||||
std::vector<std::unique_ptr<http::Server>> proxies_;
|
||||
|
||||
mutable std::mutex sessions_mu_;
|
||||
std::vector<std::shared_ptr<Session>> sessions_;
|
||||
mutable std::mutex registry_mu_;
|
||||
std::map<std::string, Endpoint> registry_; // npid -> the port's endpoint, from its STUN pings
|
||||
json last_advertisement_;
|
||||
std::mutex state_mu_;
|
||||
};
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,437 +0,0 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winhttp.h>
|
||||
#else
|
||||
#include <curl/curl.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma comment(lib, "winhttp.lib")
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
#endif
|
||||
|
||||
namespace http {
|
||||
|
||||
namespace {
|
||||
|
||||
bool iequals(const std::string& a, const std::string& b) {
|
||||
return a.size() == b.size() &&
|
||||
std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) {
|
||||
return std::tolower(static_cast<unsigned char>(x)) == std::tolower(static_cast<unsigned char>(y));
|
||||
});
|
||||
}
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t a = 0, b = s.size();
|
||||
while (a < b && (s[a] == ' ' || s[a] == '\t')) ++a;
|
||||
while (b > a && (s[b - 1] == ' ' || s[b - 1] == '\t' || s[b - 1] == '\r')) --b;
|
||||
return s.substr(a, b - a);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
HINTERNET open_session() {
|
||||
return WinHttpOpen(L"bbbridge", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME,
|
||||
WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
}
|
||||
|
||||
HINTERNET session() {
|
||||
static HINTERNET h = open_session();
|
||||
return h;
|
||||
}
|
||||
|
||||
ClientResponse send_on(HINTERNET session, const ClientRequest& req);
|
||||
|
||||
struct Handle {
|
||||
HINTERNET h = nullptr;
|
||||
~Handle() { if (h) WinHttpCloseHandle(h); }
|
||||
};
|
||||
#endif
|
||||
|
||||
const char* reason(int status) {
|
||||
switch (status) {
|
||||
case 200: return "OK";
|
||||
case 201: return "Created";
|
||||
case 204: return "No Content";
|
||||
case 400: return "Bad Request";
|
||||
case 401: return "Unauthorized";
|
||||
case 404: return "Not Found";
|
||||
case 500: return "Internal Server Error";
|
||||
case 502: return "Bad Gateway";
|
||||
default: return "Status";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string header_value(const Headers& headers, const std::string& name) {
|
||||
for (const auto& [k, v] : headers)
|
||||
if (iequals(k, name)) return v;
|
||||
return "";
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
ClientResponse send(const ClientRequest& req) { return send_on(session(), req); }
|
||||
|
||||
Pool::~Pool() {
|
||||
if (session_) WinHttpCloseHandle(session_);
|
||||
}
|
||||
|
||||
ClientResponse Pool::send(const ClientRequest& req) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
if (session_ && bb::now() - opened_ >= max_age_) { // closing the session closes its connection
|
||||
WinHttpCloseHandle(session_);
|
||||
session_ = nullptr;
|
||||
}
|
||||
if (!session_) {
|
||||
session_ = open_session();
|
||||
opened_ = bb::now();
|
||||
}
|
||||
return send_on(session_, req);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
ClientResponse send_on(HINTERNET session, const ClientRequest& req) {
|
||||
ClientResponse res;
|
||||
if (!session) {
|
||||
res.error = "WinHttpOpen failed";
|
||||
return res;
|
||||
}
|
||||
const std::wstring wurl = bb::widen(req.url);
|
||||
URL_COMPONENTS uc{};
|
||||
uc.dwStructSize = sizeof(uc);
|
||||
wchar_t host[256] = {}, path[4096] = {};
|
||||
uc.lpszHostName = host;
|
||||
uc.dwHostNameLength = 256;
|
||||
uc.lpszUrlPath = path;
|
||||
uc.dwUrlPathLength = 4096;
|
||||
wchar_t extra[4096] = {};
|
||||
uc.lpszExtraInfo = extra;
|
||||
uc.dwExtraInfoLength = 4096;
|
||||
if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &uc)) {
|
||||
res.error = "bad url " + req.url;
|
||||
return res;
|
||||
}
|
||||
const bool secure = uc.nScheme == INTERNET_SCHEME_HTTPS;
|
||||
Handle conn{WinHttpConnect(session, host, uc.nPort, 0)};
|
||||
if (!conn.h) {
|
||||
res.error = bb::fmt("connect failed (%lu)", GetLastError());
|
||||
return res;
|
||||
}
|
||||
const std::wstring object = std::wstring(path) + extra;
|
||||
Handle request{WinHttpOpenRequest(conn.h, bb::widen(req.method).c_str(), object.c_str(), nullptr,
|
||||
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES,
|
||||
secure ? WINHTTP_FLAG_SECURE : 0)};
|
||||
if (!request.h) {
|
||||
res.error = bb::fmt("open request failed (%lu)", GetLastError());
|
||||
return res;
|
||||
}
|
||||
DWORD policy = WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
|
||||
WinHttpSetOption(request.h, WINHTTP_OPTION_REDIRECT_POLICY, &policy, sizeof(policy));
|
||||
WinHttpSetTimeouts(request.h, req.timeout_ms, req.timeout_ms, req.timeout_ms, req.timeout_ms);
|
||||
std::wstring headers;
|
||||
for (const auto& [k, v] : req.headers) headers += bb::widen(k + ": " + v + "\r\n");
|
||||
if (!headers.empty())
|
||||
WinHttpAddRequestHeaders(request.h, headers.c_str(), static_cast<DWORD>(-1L),
|
||||
WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE);
|
||||
const DWORD len = static_cast<DWORD>(req.body.size());
|
||||
if (!WinHttpSendRequest(request.h, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||
len ? const_cast<char*>(req.body.data()) : WINHTTP_NO_REQUEST_DATA, len, len, 0) ||
|
||||
!WinHttpReceiveResponse(request.h, nullptr)) {
|
||||
const DWORD e = GetLastError();
|
||||
res.error = e == ERROR_WINHTTP_TIMEOUT ? "The read operation timed out" : bb::fmt("request failed (%lu)", e);
|
||||
return res;
|
||||
}
|
||||
DWORD status = 0, size = sizeof(status);
|
||||
WinHttpQueryHeaders(request.h, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &status, &size, WINHTTP_NO_HEADER_INDEX);
|
||||
res.status = static_cast<int>(status);
|
||||
size = 0;
|
||||
WinHttpQueryHeaders(request.h, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, nullptr,
|
||||
&size, WINHTTP_NO_HEADER_INDEX);
|
||||
if (size) {
|
||||
std::wstring raw(size / sizeof(wchar_t), L'\0');
|
||||
if (WinHttpQueryHeaders(request.h, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
raw.data(), &size, WINHTTP_NO_HEADER_INDEX)) {
|
||||
std::istringstream lines(bb::narrow(raw));
|
||||
std::string line;
|
||||
bool first = true;
|
||||
while (std::getline(lines, line)) {
|
||||
if (first) { first = false; continue; } // status line
|
||||
const size_t c = line.find(':');
|
||||
if (c != std::string::npos) res.headers.emplace_back(trim(line.substr(0, c)), trim(line.substr(c + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
DWORD avail = 0;
|
||||
if (!WinHttpQueryDataAvailable(request.h, &avail)) {
|
||||
res.error = bb::fmt("read failed (%lu)", GetLastError());
|
||||
return res;
|
||||
}
|
||||
if (!avail) break;
|
||||
std::string chunk(avail, '\0');
|
||||
DWORD read = 0;
|
||||
if (!WinHttpReadData(request.h, chunk.data(), avail, &read)) {
|
||||
res.error = bb::fmt("read failed (%lu)", GetLastError());
|
||||
return res;
|
||||
}
|
||||
res.body.append(chunk.data(), read);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#else
|
||||
// Linux: libcurl, as WinHTTP above: certificates checked, no redirects followed, the request's
|
||||
// timeout for connecting and again for the answer, and a Pool keeping one connection alive.
|
||||
namespace {
|
||||
|
||||
// The system's CA bundle: libcurl's built-in path is the one of the distribution it was built
|
||||
// on, and the AppImage carries it to others.
|
||||
const char* ca_bundle() {
|
||||
static const char* found = [] {
|
||||
for (const char* p : {"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/ssl/ca-bundle.pem", "/etc/pki/tls/cacert.pem",
|
||||
"/etc/ssl/cert.pem"})
|
||||
if (access(p, R_OK) == 0) return p;
|
||||
return static_cast<const char*>(nullptr);
|
||||
}();
|
||||
return found;
|
||||
}
|
||||
|
||||
size_t on_body(char* data, size_t size, size_t count, void* user) {
|
||||
static_cast<std::string*>(user)->append(data, size * count);
|
||||
return size * count;
|
||||
}
|
||||
|
||||
size_t on_header(char* data, size_t size, size_t count, void* user) {
|
||||
auto* headers = static_cast<Headers*>(user);
|
||||
std::string line(data, size * count);
|
||||
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) line.pop_back(); // trim() keeps '\n'
|
||||
line = trim(line);
|
||||
if (line.rfind("HTTP/", 0) == 0) headers->clear(); // a new response (after a 100 Continue)
|
||||
else if (const size_t c = line.find(':'); c != std::string::npos)
|
||||
headers->emplace_back(trim(line.substr(0, c)), trim(line.substr(c + 1)));
|
||||
return size * count;
|
||||
}
|
||||
|
||||
ClientResponse send_on(CURL* curl, const ClientRequest& req) {
|
||||
ClientResponse res;
|
||||
if (!curl) {
|
||||
res.error = "curl_easy_init failed";
|
||||
return res;
|
||||
}
|
||||
curl_easy_reset(curl);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, req.url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "bbbridge");
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
if (const char* ca = ca_bundle()) curl_easy_setopt(curl, CURLOPT_CAINFO, ca);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, static_cast<long>(req.timeout_ms));
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, static_cast<long>(req.timeout_ms) * 2);
|
||||
if (req.method == "GET") {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
} else if (req.method == "HEAD") {
|
||||
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req.method.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req.body.data());
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(req.body.size()));
|
||||
}
|
||||
curl_slist* headers = curl_slist_append(nullptr, "Expect:"); // no 100-continue round trip
|
||||
for (const auto& [k, v] : req.headers) headers = curl_slist_append(headers, (k + ": " + v).c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, on_body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res.body);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, on_header);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &res.headers);
|
||||
const CURLcode code = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
if (code != CURLE_OK) {
|
||||
res.error = code == CURLE_OPERATION_TIMEDOUT ? "The read operation timed out"
|
||||
: bb::fmt("request failed (%s)", curl_easy_strerror(code));
|
||||
res.body.clear();
|
||||
res.headers.clear();
|
||||
return res;
|
||||
}
|
||||
long status = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
res.status = static_cast<int>(status);
|
||||
return res;
|
||||
}
|
||||
|
||||
CURL* new_handle() {
|
||||
static const bool initialised = curl_global_init(CURL_GLOBAL_DEFAULT) == CURLE_OK;
|
||||
return initialised ? curl_easy_init() : nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ClientResponse send(const ClientRequest& req) {
|
||||
// One connection per request, as WinHTTP's shared session gives no keep-alive guarantee.
|
||||
CURL* curl = new_handle();
|
||||
ClientResponse res = send_on(curl, req);
|
||||
if (curl) curl_easy_cleanup(curl);
|
||||
return res;
|
||||
}
|
||||
|
||||
Pool::~Pool() {
|
||||
if (session_) curl_easy_cleanup(static_cast<CURL*>(session_));
|
||||
}
|
||||
|
||||
ClientResponse Pool::send(const ClientRequest& req) {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
if (session_ && bb::now() - opened_ >= max_age_) { // a new handle dials a new connection
|
||||
curl_easy_cleanup(static_cast<CURL*>(session_));
|
||||
session_ = nullptr;
|
||||
}
|
||||
if (!session_) {
|
||||
session_ = new_handle();
|
||||
opened_ = bb::now();
|
||||
}
|
||||
return send_on(static_cast<CURL*>(session_), req);
|
||||
}
|
||||
#endif
|
||||
|
||||
ServerResponse ServerResponse::json(const bb::json& j, int status) {
|
||||
ServerResponse r;
|
||||
r.status = status;
|
||||
r.body = j.dump();
|
||||
r.headers.emplace_back("Content-Type", "application/json");
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- server ----
|
||||
|
||||
bool Server::start(const std::string& host, int port, Handler handler, std::string* error) {
|
||||
bb::net_init();
|
||||
handler_ = std::move(handler);
|
||||
listen_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
#ifdef _WIN32
|
||||
BOOL exclusive = TRUE;
|
||||
setsockopt(listen_, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast<const char*>(&exclusive), sizeof(exclusive));
|
||||
#else
|
||||
// Linux keeps a port that just closed (TIME_WAIT) from being bound again without SO_REUSEADDR,
|
||||
// which Windows allows; it still never lets a second program listen on it.
|
||||
int reuse = 1;
|
||||
setsockopt(listen_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
||||
#endif
|
||||
sockaddr_in addr = bb::to_sockaddr(bb::Endpoint{host, port});
|
||||
if (bind(listen_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0 || ::listen(listen_, 16) != 0) {
|
||||
if (error) *error = bb::fmt("cannot listen on %s:%d (error %d)", host.c_str(), port, WSAGetLastError());
|
||||
closesocket(listen_);
|
||||
listen_ = INVALID_SOCKET;
|
||||
return false;
|
||||
}
|
||||
socklen_t len = sizeof(addr);
|
||||
getsockname(listen_, reinterpret_cast<sockaddr*>(&addr), &len);
|
||||
port_ = ntohs(addr.sin_port);
|
||||
running_ = true;
|
||||
acceptor_ = std::thread([this] { accept_loop(); });
|
||||
return true;
|
||||
}
|
||||
|
||||
void Server::stop() {
|
||||
if (!running_.exchange(false)) return;
|
||||
closesocket(listen_);
|
||||
listen_ = INVALID_SOCKET;
|
||||
if (acceptor_.joinable()) acceptor_.join();
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (SOCKET s : clients_) shutdown(s, SD_BOTH);
|
||||
}
|
||||
// Connection threads are detached (the game opens one per request); wait
|
||||
// for the ones still running, which a shutdown socket ends quickly.
|
||||
for (int i = 0; i < 300 && active_.load() > 0; ++i) Sleep(10);
|
||||
}
|
||||
|
||||
void Server::accept_loop() {
|
||||
while (running_) {
|
||||
SOCKET s = accept(listen_, nullptr, nullptr);
|
||||
if (s == INVALID_SOCKET) break;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
clients_.insert(s);
|
||||
}
|
||||
++active_;
|
||||
std::thread([this, s] {
|
||||
serve(s);
|
||||
--active_;
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
void Server::serve(SOCKET s) {
|
||||
std::string buf;
|
||||
char chunk[8192];
|
||||
bool keep = true;
|
||||
while (keep && running_) {
|
||||
size_t end;
|
||||
while ((end = buf.find("\r\n\r\n")) == std::string::npos) {
|
||||
const int n = recv(s, chunk, sizeof(chunk), 0);
|
||||
if (n <= 0) { keep = false; break; }
|
||||
buf.append(chunk, n);
|
||||
if (buf.size() > (1 << 20)) { keep = false; break; }
|
||||
}
|
||||
if (!keep) break;
|
||||
ServerRequest req;
|
||||
std::istringstream head(buf.substr(0, end));
|
||||
std::string line, version;
|
||||
std::getline(head, line);
|
||||
{
|
||||
std::istringstream rl(line);
|
||||
rl >> req.method >> req.target >> version;
|
||||
}
|
||||
while (std::getline(head, line)) {
|
||||
const size_t c = line.find(':');
|
||||
if (c != std::string::npos) req.headers.emplace_back(trim(line.substr(0, c)), trim(line.substr(c + 1)));
|
||||
}
|
||||
buf.erase(0, end + 4);
|
||||
size_t want = 0;
|
||||
const std::string cl = header_value(req.headers, "Content-Length");
|
||||
if (!cl.empty()) {
|
||||
try { want = static_cast<size_t>(std::stoull(cl)); } catch (...) { want = 0; }
|
||||
}
|
||||
while (buf.size() < want) {
|
||||
const int n = recv(s, chunk, sizeof(chunk), 0);
|
||||
if (n <= 0) { keep = false; break; }
|
||||
buf.append(chunk, n);
|
||||
}
|
||||
if (!keep) break;
|
||||
req.body = buf.substr(0, want);
|
||||
buf.erase(0, want);
|
||||
if (iequals(header_value(req.headers, "Connection"), "close") || version == "HTTP/1.0") keep = false;
|
||||
|
||||
ServerResponse res;
|
||||
try {
|
||||
res = handler_(req);
|
||||
} catch (const std::exception& e) {
|
||||
res = ServerResponse::json({{"ResKind", 1}, {"Message", std::string("bridge error: ") + e.what()}}, 500);
|
||||
}
|
||||
std::string out = bb::fmt("HTTP/1.1 %d %s\r\n", res.status, reason(res.status));
|
||||
for (const auto& [k, v] : res.headers)
|
||||
if (!iequals(k, "Content-Length") && !iequals(k, "Connection")) out += k + ": " + v + "\r\n";
|
||||
out += "Content-Length: " + std::to_string(res.body.size()) + "\r\n";
|
||||
out += keep ? "Connection: keep-alive\r\n\r\n" : "Connection: close\r\n\r\n";
|
||||
if (req.method != "HEAD") out += res.body;
|
||||
size_t sent = 0;
|
||||
while (sent < out.size()) {
|
||||
const int n = ::send(s, out.data() + sent, static_cast<int>(out.size() - sent), 0);
|
||||
if (n <= 0) { keep = false; break; }
|
||||
sent += static_cast<size_t>(n);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
clients_.erase(s);
|
||||
}
|
||||
closesocket(s);
|
||||
}
|
||||
|
||||
} // namespace http
|
||||
@@ -1,101 +0,0 @@
|
||||
// HTTP: a WinHTTP client (certificates checked by Windows, no redirects
|
||||
// followed) and a small threaded HTTP/1.1 server for the bridge's local
|
||||
// listeners (WebAPI, game-server proxy).
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
namespace http {
|
||||
|
||||
using Headers = std::vector<std::pair<std::string, std::string>>;
|
||||
|
||||
std::string header_value(const Headers& headers, const std::string& name); // case-insensitive, "" if absent
|
||||
|
||||
struct ClientRequest {
|
||||
std::string method = "POST";
|
||||
std::string url;
|
||||
Headers headers;
|
||||
std::string body;
|
||||
int timeout_ms = 8000;
|
||||
};
|
||||
|
||||
struct ClientResponse {
|
||||
int status = 0;
|
||||
Headers headers;
|
||||
std::string body;
|
||||
std::string error; // transport failure (no HTTP answer)
|
||||
bool ok() const { return error.empty(); }
|
||||
};
|
||||
|
||||
ClientResponse send(const ClientRequest& req);
|
||||
|
||||
// A WinHTTP session of its own for one caller's requests in a row: its kept-
|
||||
// alive connection is closed and dialled again once it is max_age_s old, before
|
||||
// something on the way drops it silently. (The Hunter's Dream's event poll, on
|
||||
// the shared pool, hung for its whole timeout about once a minute.)
|
||||
class Pool {
|
||||
public:
|
||||
explicit Pool(double max_age_s) : max_age_(max_age_s) {}
|
||||
~Pool();
|
||||
Pool(const Pool&) = delete;
|
||||
Pool& operator=(const Pool&) = delete;
|
||||
ClientResponse send(const ClientRequest& req);
|
||||
|
||||
private:
|
||||
std::mutex mu_;
|
||||
void* session_ = nullptr; // HINTERNET
|
||||
double opened_ = 0, max_age_;
|
||||
};
|
||||
|
||||
struct ServerRequest {
|
||||
std::string method;
|
||||
std::string target; // path with the query
|
||||
Headers headers;
|
||||
std::string body;
|
||||
std::string path() const { return target.substr(0, target.find('?')); }
|
||||
std::string query() const {
|
||||
const size_t q = target.find('?');
|
||||
return q == std::string::npos ? "" : target.substr(q + 1);
|
||||
}
|
||||
};
|
||||
|
||||
struct ServerResponse {
|
||||
int status = 200;
|
||||
Headers headers; // Content-Length is added
|
||||
std::string body;
|
||||
static ServerResponse json(const bb::json& j, int status = 200);
|
||||
};
|
||||
|
||||
using Handler = std::function<ServerResponse(const ServerRequest&)>;
|
||||
|
||||
class Server {
|
||||
public:
|
||||
~Server() { stop(); }
|
||||
bool start(const std::string& host, int port, Handler handler, std::string* error);
|
||||
void stop();
|
||||
int port() const { return port_; }
|
||||
|
||||
private:
|
||||
void accept_loop();
|
||||
void serve(SOCKET s);
|
||||
|
||||
SOCKET listen_ = INVALID_SOCKET;
|
||||
int port_ = 0;
|
||||
Handler handler_;
|
||||
std::thread acceptor_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::mutex mu_;
|
||||
std::set<SOCKET> clients_;
|
||||
std::atomic<int> active_{0}; // detached connection threads still running
|
||||
};
|
||||
|
||||
} // namespace http
|
||||
@@ -1,152 +0,0 @@
|
||||
// bbbridge-cli: the bridge without a window (tests, servers, troubleshooting).
|
||||
// bbbridge-cli[.exe] [--config bbbridge_config.json] [--log bbbridge.log]
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "account_link.hpp"
|
||||
#include "bridge.hpp"
|
||||
#ifdef _WIN32
|
||||
#include "setup.hpp"
|
||||
#else
|
||||
#include <csignal>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
std::atomic<bool> g_quit{false};
|
||||
#ifdef _WIN32
|
||||
BOOL WINAPI on_ctrl(DWORD) {
|
||||
g_quit = true;
|
||||
return TRUE;
|
||||
}
|
||||
#else
|
||||
void on_signal(int) { g_quit = true; }
|
||||
#endif
|
||||
|
||||
int run(const std::vector<std::wstring>& argv) {
|
||||
const int argc = static_cast<int>(argv.size());
|
||||
std::wstring config_path = L"bbbridge_config.json", log_path, web_code_npid;
|
||||
bool link = false;
|
||||
std::wstring route, bbport_dir;
|
||||
struct { std::wstring op, name, code; } account_op;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::wstring a = argv[i];
|
||||
if (a == L"--config" && i + 1 < argc) config_path = argv[++i];
|
||||
else if (a == L"--log" && i + 1 < argc) log_path = argv[++i];
|
||||
else if (a == L"--link-discord") link = true;
|
||||
else if (a == L"--route" && i + 1 < argc) route = argv[++i];
|
||||
else if (a == L"--bbport" && i + 1 < argc) bbport_dir = argv[++i];
|
||||
else if (a == L"--web-code" && i + 1 < argc) web_code_npid = argv[++i];
|
||||
else if (a == L"--create" && i + 1 < argc) account_op = {L"create", argv[++i], L""};
|
||||
else if (a == L"--recover" && i + 2 < argc) {
|
||||
account_op = {L"recover", argv[i + 1], argv[i + 2]};
|
||||
i += 2;
|
||||
} else if (a == L"--sign-out" && i + 1 < argc) account_op = {L"sign-out", argv[++i], L""};
|
||||
else if (a == L"--version") {
|
||||
std::printf("bbbridge %s\n", bb::kVersion);
|
||||
return 0;
|
||||
} else {
|
||||
std::printf("usage: bbbridge-cli [--config file.json] [--log file.log] [--version]\n"
|
||||
" [--link-discord] [--web-code NPID] [--create NAME]\n"
|
||||
" [--recover NAME CODE] [--sign-out NAME]\n"
|
||||
" [--route on|off [--bbport DIR]]\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (!log_path.empty()) bb::set_log_file(log_path);
|
||||
bb::json j = bb::json::object();
|
||||
std::ifstream in(bb::fspath(config_path));
|
||||
if (in) {
|
||||
j = bb::json::parse(in, nullptr, false);
|
||||
if (j.is_discarded()) {
|
||||
std::printf("config %s is not valid JSON\n", bb::narrow(config_path).c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const bb::Config cfg = bb::Config::from_json(j);
|
||||
if (link) { // sign in with Discord, store the account, exit
|
||||
bb::DiscordLink dl(cfg.auth_server, bb::widen(cfg.state_file));
|
||||
dl.start();
|
||||
bool shown = false;
|
||||
while (dl.state() == bb::DiscordLink::State::Starting || dl.state() == bb::DiscordLink::State::Waiting) {
|
||||
if (!shown && dl.state() == bb::DiscordLink::State::Waiting) {
|
||||
std::printf("Open %s and enter the code %s\n", dl.uri().c_str(), dl.user_code().c_str());
|
||||
std::fflush(stdout);
|
||||
shown = true;
|
||||
}
|
||||
Sleep(200);
|
||||
}
|
||||
if (dl.state() != bb::DiscordLink::State::Approved) {
|
||||
std::printf("failed: %s\n", dl.message().c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("signed in as %s\n", dl.online_id().c_str());
|
||||
return 0;
|
||||
}
|
||||
if (!route.empty()) { // point bbport at the bridge (on) or back (off), as the app's checkbox
|
||||
#ifndef _WIN32
|
||||
std::printf("failed: --route is for bbport for Windows; on Linux the bbport launcher routes the game\n");
|
||||
return 1;
|
||||
#else
|
||||
try {
|
||||
wchar_t self[MAX_PATH];
|
||||
GetModuleFileNameW(nullptr, self, MAX_PATH);
|
||||
bb::BbportSetup setup(bb::fs::path(self).parent_path(), bbport_dir);
|
||||
if (route == L"on") setup.enable();
|
||||
else setup.disable();
|
||||
std::printf("ok: bbport %s, routed=%d\n", bb::narrow(setup.bbport.wstring()).c_str(), setup.routed() ? 1 : 0);
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::printf("failed: %s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (!account_op.op.empty()) {
|
||||
const std::wstring state = bb::widen(cfg.state_file);
|
||||
const std::string name = bb::narrow(account_op.name);
|
||||
const std::string err =
|
||||
account_op.op == L"create" ? bb::account_create_now(cfg.auth_server, state, name)
|
||||
: account_op.op == L"recover" ? bb::account_recover_now(cfg.auth_server, state, name, bb::narrow(account_op.code))
|
||||
: bb::account_sign_out(cfg.auth_server, state, name);
|
||||
std::printf("%s\n", err.empty() ? "ok" : ("failed: " + err).c_str());
|
||||
return err.empty() ? 0 : 1;
|
||||
}
|
||||
if (!web_code_npid.empty()) {
|
||||
std::string error;
|
||||
const std::string code = bb::website_code(cfg.auth_server, bb::widen(cfg.state_file), bb::narrow(web_code_npid), error);
|
||||
if (code.empty()) {
|
||||
std::printf("failed: %s\n", error.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::printf("%s\n", code.c_str());
|
||||
return 0;
|
||||
}
|
||||
bb::log(bb::fmt("bbbridge %s (cli)", bb::kVersion));
|
||||
bb::Bridge bridge(cfg);
|
||||
std::string error;
|
||||
if (!bridge.start(&error)) return 1;
|
||||
#ifdef _WIN32
|
||||
SetConsoleCtrlHandler(on_ctrl, TRUE);
|
||||
#else
|
||||
std::signal(SIGINT, on_signal);
|
||||
std::signal(SIGTERM, on_signal);
|
||||
#endif
|
||||
while (!g_quit) Sleep(200);
|
||||
bb::log("stopping");
|
||||
bridge.stop();
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t** argv) { return run(std::vector<std::wstring>(argv, argv + argc)); }
|
||||
#else
|
||||
int main(int argc, char** argv) {
|
||||
std::vector<std::wstring> args;
|
||||
for (int i = 0; i < argc; ++i) args.push_back(bb::widen(argv[i]));
|
||||
return run(args);
|
||||
}
|
||||
#endif
|
||||
@@ -1,735 +0,0 @@
|
||||
#include "p2p.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <mstcpip.h>
|
||||
|
||||
#ifndef SIO_UDP_CONNRESET
|
||||
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
|
||||
namespace bb::p2p {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t kPortSignalingVport = 0xFFFF;
|
||||
constexpr uint8_t kPs4Magic = 0xFF, kFlagP2p = 0x80, kFlagByteVports = 0x40, kFlagComid = 0x20, kFlagType = 0x03;
|
||||
constexpr uint8_t kRelayMagic = 0xFB;
|
||||
constexpr uint32_t kStunCookie = 0x2112A442;
|
||||
constexpr uint16_t kStunBindingRequest = 0x0001, kStunBindingResponse = 0x0101, kStunMapped = 0x0001,
|
||||
kStunXorMapped = 0x0020, kStunXorMappedOld = 0x8020, kBbhostHello = 0x8100,
|
||||
kBbhostRelay = 0x8101;
|
||||
const std::string kRelayVersion = "bbr1";
|
||||
const std::string kShad = "SHAD";
|
||||
|
||||
uint16_t be16(const std::string& s, size_t at) {
|
||||
return static_cast<uint16_t>((static_cast<uint8_t>(s[at]) << 8) | static_cast<uint8_t>(s[at + 1]));
|
||||
}
|
||||
uint32_t be32(const std::string& s, size_t at) {
|
||||
return (static_cast<uint32_t>(be16(s, at)) << 16) | be16(s, at + 2);
|
||||
}
|
||||
void put_be16(std::string& s, uint16_t v) {
|
||||
s.push_back(static_cast<char>(v >> 8));
|
||||
s.push_back(static_cast<char>(v & 0xFF));
|
||||
}
|
||||
template <typename T>
|
||||
void put_le(std::string& s, T v) {
|
||||
for (size_t i = 0; i < sizeof(T); ++i) s.push_back(static_cast<char>((static_cast<uint64_t>(v) >> (8 * i)) & 0xFF));
|
||||
}
|
||||
template <typename T>
|
||||
T get_le(const std::string& s, size_t at) {
|
||||
uint64_t v = 0;
|
||||
for (size_t i = 0; i < sizeof(T); ++i) v |= static_cast<uint64_t>(static_cast<uint8_t>(s[at + i])) << (8 * i);
|
||||
return static_cast<T>(v);
|
||||
}
|
||||
std::string id16(const std::string& name) {
|
||||
std::string out = name.substr(0, 16);
|
||||
out.resize(16, '\0');
|
||||
return out;
|
||||
}
|
||||
std::string ip_from_be32(uint32_t v) {
|
||||
return fmt("%u.%u.%u.%u", (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF);
|
||||
}
|
||||
uint64_t wall_micros() {
|
||||
using namespace std::chrono;
|
||||
return static_cast<uint64_t>(duration_cast<microseconds>(system_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
void no_udp_connreset(SOCKET s) {
|
||||
#ifdef _WIN32
|
||||
// Without this, an ICMP "port unreachable" for an earlier send makes the
|
||||
// next recvfrom fail with WSAECONNRESET (10054).
|
||||
BOOL off = FALSE;
|
||||
DWORD ret = 0;
|
||||
WSAIoctl(s, SIO_UDP_CONNRESET, &off, sizeof(off), nullptr, 0, &ret, nullptr, nullptr);
|
||||
#else
|
||||
(void)s; // Linux reports ICMP errors only on connected UDP sockets; these are not
|
||||
#endif
|
||||
}
|
||||
SOCKET udp_bind(const std::string& ip, int port, Endpoint* bound) {
|
||||
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s == INVALID_SOCKET) return s;
|
||||
sockaddr_in a = to_sockaddr(Endpoint{ip, port});
|
||||
if (bind(s, reinterpret_cast<sockaddr*>(&a), sizeof(a)) != 0) {
|
||||
closesocket(s);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
no_udp_connreset(s);
|
||||
socklen_t len = sizeof(a);
|
||||
getsockname(s, reinterpret_cast<sockaddr*>(&a), &len);
|
||||
if (bound) *bound = from_sockaddr(a);
|
||||
return s;
|
||||
}
|
||||
void udp_send(SOCKET s, const std::string& data, const Endpoint& to) {
|
||||
sockaddr_in a = to_sockaddr(to);
|
||||
sendto(s, data.data(), static_cast<int>(data.size()), 0, reinterpret_cast<sockaddr*>(&a), sizeof(a));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::string kProbe("\xfe" "bbhp\0\0\0", 8);
|
||||
|
||||
// ---- framing ----
|
||||
|
||||
Frame port_to_ps4(const std::string& frame, std::string& out) {
|
||||
if (frame.size() < 4) return Frame::None;
|
||||
const uint16_t src = be16(frame, 0), dst = be16(frame, 2);
|
||||
const std::string payload = frame.substr(4);
|
||||
if (dst == kPortSignalingVport) {
|
||||
out = payload;
|
||||
return Frame::Signaling;
|
||||
}
|
||||
out.clear();
|
||||
out.push_back(static_cast<char>(kPs4Magic));
|
||||
if (src < 256 && dst < 256) {
|
||||
out.push_back(static_cast<char>(kFlagP2p | kFlagByteVports | kFlagType));
|
||||
out.push_back(static_cast<char>(src));
|
||||
out.push_back(static_cast<char>(dst));
|
||||
} else {
|
||||
out.push_back(static_cast<char>(kFlagP2p | kFlagType));
|
||||
put_be16(out, src);
|
||||
put_be16(out, dst);
|
||||
}
|
||||
out += payload;
|
||||
return Frame::Game;
|
||||
}
|
||||
|
||||
bool ps4_to_port(const std::string& d, std::string& out) {
|
||||
if (d.size() < 4 || static_cast<uint8_t>(d[0]) != kPs4Magic) return false;
|
||||
const uint8_t flags = static_cast<uint8_t>(d[1]);
|
||||
size_t at = 2 + ((flags & kFlagComid) ? 4 : 0);
|
||||
uint16_t src, dst;
|
||||
if (flags & kFlagByteVports) {
|
||||
if (at + 2 > d.size()) return false;
|
||||
src = static_cast<uint8_t>(d[at]);
|
||||
dst = static_cast<uint8_t>(d[at + 1]);
|
||||
at += 2;
|
||||
} else {
|
||||
if (at + 4 > d.size()) return false;
|
||||
src = be16(d, at);
|
||||
dst = be16(d, at + 2);
|
||||
at += 4;
|
||||
}
|
||||
out.clear();
|
||||
put_be16(out, src);
|
||||
put_be16(out, dst);
|
||||
out += d.substr(at);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string port_signaling_frame(const std::string& payload) {
|
||||
std::string out;
|
||||
put_be16(out, kPortSignalingVport);
|
||||
put_be16(out, kPortSignalingVport);
|
||||
return out + payload;
|
||||
}
|
||||
|
||||
std::string relay_wrap(const std::string& token, int port, const std::string& datagram) {
|
||||
std::string out;
|
||||
out.push_back(static_cast<char>(kRelayMagic));
|
||||
out.push_back('R');
|
||||
out += token;
|
||||
put_be16(out, static_cast<uint16_t>(port));
|
||||
return out + datagram;
|
||||
}
|
||||
|
||||
bool relay_unwrap(const std::string& data, int& port, std::string& datagram) {
|
||||
if (data.size() < 4 || static_cast<uint8_t>(data[0]) != kRelayMagic || data[1] != 'r') return false;
|
||||
port = be16(data, 2);
|
||||
datagram = data.substr(4);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string stun_request(const std::string& token, std::string& txid) {
|
||||
static std::mt19937_64 rng{std::random_device{}()};
|
||||
txid.clear();
|
||||
txid.push_back(static_cast<char>(kStunCookie >> 24));
|
||||
txid.push_back(static_cast<char>((kStunCookie >> 16) & 0xFF));
|
||||
txid.push_back(static_cast<char>((kStunCookie >> 8) & 0xFF));
|
||||
txid.push_back(static_cast<char>(kStunCookie & 0xFF));
|
||||
for (int i = 0; i < 12; ++i) txid.push_back(static_cast<char>(rng() & 0xFF));
|
||||
std::string attr;
|
||||
put_be16(attr, kBbhostHello);
|
||||
put_be16(attr, 12);
|
||||
attr += kRelayVersion;
|
||||
attr += token.size() == 8 ? token : std::string(8, '\0');
|
||||
std::string out;
|
||||
put_be16(out, kStunBindingRequest);
|
||||
put_be16(out, static_cast<uint16_t>(attr.size()));
|
||||
return out + txid + attr;
|
||||
}
|
||||
|
||||
bool stun_parse(const std::string& d, const std::string& txid, StunAnswer& out) {
|
||||
if (d.size() < 20 || be16(d, 0) != kStunBindingResponse || d.compare(4, 16, txid) != 0) return false;
|
||||
const size_t length = be16(d, 2);
|
||||
if (20 + length > d.size()) return false;
|
||||
const uint32_t xor_addr = be32(d, 4);
|
||||
const uint16_t xor_port = be16(d, 4);
|
||||
std::optional<std::pair<uint32_t, uint16_t>> mapped, xored;
|
||||
out = StunAnswer{};
|
||||
size_t at = 20;
|
||||
const size_t end = 20 + length;
|
||||
while (at + 4 <= end) {
|
||||
const uint16_t type = be16(d, at), alen = be16(d, at + 2);
|
||||
if (at + 4 + alen > end) break;
|
||||
const std::string v = d.substr(at + 4, alen);
|
||||
if (type == kBbhostRelay && alen >= 20 && v.compare(0, 4, kRelayVersion) == 0) {
|
||||
out.relay = true;
|
||||
out.relay_token = v.substr(4, 8);
|
||||
out.relay_port = be16(v, 12);
|
||||
} else if (alen >= 8 && static_cast<uint8_t>(v[1]) == 0x01) {
|
||||
const uint16_t port = be16(v, 2);
|
||||
const uint32_t addr = be32(v, 4);
|
||||
if (type == kStunMapped) mapped = std::make_pair(addr, port);
|
||||
else if (type == kStunXorMapped || type == kStunXorMappedOld)
|
||||
xored = std::make_pair(addr ^ xor_addr, static_cast<uint16_t>(port ^ xor_port));
|
||||
}
|
||||
at += 4 + ((alen + 3u) & ~3u);
|
||||
}
|
||||
const auto chosen = mapped ? mapped : xored;
|
||||
if (!chosen || !chosen->first || !chosen->second) return false;
|
||||
out.mapped = Endpoint{ip_from_be32(chosen->first), chosen->second};
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- signaling ----
|
||||
|
||||
PeerSignaling::PeerSignaling(const std::string& peer_online_id, int peer_member, const std::string& port_online_id,
|
||||
int port_member, uint64_t room)
|
||||
: peer_id(id16(peer_online_id)),
|
||||
port_id(id16(port_online_id)),
|
||||
peer_member_id(peer_member > 0 ? peer_member : 0),
|
||||
port_member_id(port_member),
|
||||
room_id(room),
|
||||
conn_id(0x4242u + static_cast<uint32_t>(peer_member > 0 ? peer_member : 0)) {}
|
||||
|
||||
std::string PeerSignaling::handshake(uint8_t kind, uint32_t to_conn, uint64_t nonce) const {
|
||||
std::string p = kShad;
|
||||
p.push_back(static_cast<char>(T_HANDSHAKE));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
p += peer_id + port_id;
|
||||
put_le<uint32_t>(p, conn_id);
|
||||
put_le<uint32_t>(p, to_conn);
|
||||
put_le<uint32_t>(p, 0); // mapped addr: 0 makes the port keep the inner socket's address
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint64_t>(p, nonce);
|
||||
return p; // 62 bytes
|
||||
}
|
||||
|
||||
std::string PeerSignaling::control(uint8_t kind) const {
|
||||
std::string p = kShad;
|
||||
p.push_back(static_cast<char>(T_CONTROL));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
p += peer_id + port_id;
|
||||
put_le<uint32_t>(p, conn_id);
|
||||
put_le<uint32_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
return p; // 50 bytes
|
||||
}
|
||||
|
||||
std::string PeerSignaling::m2(uint8_t kind, uint64_t nonce) const {
|
||||
std::string p = kShad;
|
||||
p.push_back(static_cast<char>(T_M2));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
put_le<uint64_t>(p, room_id);
|
||||
put_le<uint16_t>(p, static_cast<uint16_t>(peer_member_id));
|
||||
put_le<uint16_t>(p, static_cast<uint16_t>(port_member_id));
|
||||
p += peer_id;
|
||||
put_le<uint32_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint16_t>(p, 0);
|
||||
put_le<uint64_t>(p, nonce);
|
||||
return p; // 50 bytes
|
||||
}
|
||||
|
||||
std::vector<std::string> PeerSignaling::handle(const std::string& p) {
|
||||
if (p.size() < 5 || p.compare(0, 4, kShad) != 0) return {};
|
||||
const uint8_t type = static_cast<uint8_t>(p[4]);
|
||||
if (type == T_ECHO_PING && p.size() == 17) {
|
||||
heard = true;
|
||||
std::string pong = kShad;
|
||||
pong.push_back(static_cast<char>(T_ECHO_PONG));
|
||||
pong += p.substr(5, 12); // conn u32 + timestamp u64, echoed
|
||||
return {pong};
|
||||
}
|
||||
if (type == T_HANDSHAKE && p.size() == 0x3E) {
|
||||
heard = true;
|
||||
const uint8_t kind = static_cast<uint8_t>(p[5]);
|
||||
const uint32_t from_conn = get_le<uint32_t>(p, 38);
|
||||
const uint64_t nonce = get_le<uint64_t>(p, 54);
|
||||
if (kind == HS_OFFER) return {handshake(HS_ACCEPT, from_conn, 0)};
|
||||
if (kind == HS_ACCEPT) return {handshake(HS_CHECK, from_conn, wall_micros())};
|
||||
if (kind == HS_CHECK) return {handshake(HS_CHECK_ACK, from_conn, nonce)};
|
||||
return {};
|
||||
}
|
||||
if (type == T_CONTROL && p.size() == 0x32) {
|
||||
heard = true;
|
||||
const uint8_t kind = static_cast<uint8_t>(p[5]);
|
||||
if (kind == CTL_ACTIVATION_REQUEST) return {control(CTL_ACTIVATION_ACK)};
|
||||
if (kind == CTL_ESTABLISHED && !sent_established) {
|
||||
sent_established = true;
|
||||
return {control(CTL_ESTABLISHED)};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (type == T_M2 && p.size() == 0x32) {
|
||||
heard = true;
|
||||
const uint8_t kind = static_cast<uint8_t>(p[5]);
|
||||
const uint64_t room = get_le<uint64_t>(p, 6);
|
||||
const uint16_t from_member = get_le<uint16_t>(p, 14), to_member = get_le<uint16_t>(p, 16);
|
||||
const uint64_t nonce = get_le<uint64_t>(p, 42);
|
||||
if (room) room_id = room;
|
||||
// the port addresses the member it means: learn the ids from it
|
||||
if (to_member && !peer_member_id) peer_member_id = to_member;
|
||||
if (from_member && !port_member_id) port_member_id = from_member;
|
||||
if (kind == HS_OFFER) return {m2(HS_ACCEPT, 0)};
|
||||
if (kind == HS_ACCEPT) return {m2(HS_CHECK, 0)};
|
||||
if (kind == HS_CHECK) return {m2(HS_CHECK_ACK, nonce)};
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::string> PeerSignaling::opening() const {
|
||||
std::vector<std::string> out{control(CTL_ACTIVATION_REQUEST)};
|
||||
if (room_id && port_member_id && peer_member_id) out.push_back(m2(HS_OFFER, 0));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- gateway ----
|
||||
|
||||
Gateway::Gateway(int outer_port, std::optional<std::pair<std::string, int>> stun_server, std::string lan_addr,
|
||||
std::function<void(const Endpoint&)> on_mapped_change)
|
||||
: outer_port_(outer_port),
|
||||
stun_server_(std::move(stun_server)),
|
||||
lan_addr_(std::move(lan_addr)),
|
||||
on_mapped_change_(std::move(on_mapped_change)) {}
|
||||
|
||||
Gateway::~Gateway() { stop(); }
|
||||
|
||||
bool Gateway::start(std::string* error) {
|
||||
net_init();
|
||||
Endpoint bound;
|
||||
outer_ = udp_bind("0.0.0.0", outer_port_, &bound);
|
||||
if (outer_ == INVALID_SOCKET) {
|
||||
if (error) *error = fmt("p2p: cannot bind udp :%d (error %d)", outer_port_, WSAGetLastError());
|
||||
return false;
|
||||
}
|
||||
outer_port_ = bound.port;
|
||||
stop_ = false;
|
||||
threads_.emplace_back([this] { outer_main(); });
|
||||
threads_.emplace_back([this] { inner_main(); });
|
||||
threads_.emplace_back([this] { tick_main(); });
|
||||
log(fmt("p2p: gateway on udp :%d (bbhost framing)", outer_port_));
|
||||
return true;
|
||||
}
|
||||
|
||||
void Gateway::stop() {
|
||||
if (stop_.exchange(true)) return;
|
||||
if (outer_ != INVALID_SOCKET) closesocket(outer_);
|
||||
outer_ = INVALID_SOCKET;
|
||||
for (auto& t : threads_)
|
||||
if (t.joinable()) t.join();
|
||||
threads_.clear();
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (auto& [id, link] : peers_) close_link(*link);
|
||||
peers_.clear();
|
||||
}
|
||||
|
||||
void Gateway::close_link(Link& link) {
|
||||
if (link.inner != INVALID_SOCKET) closesocket(link.inner);
|
||||
link.inner = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
std::pair<Endpoint, Endpoint> Gateway::endpoints() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
const Endpoint local{lan_addr_, outer_port_};
|
||||
return {local, mapped_ ? *mapped_ : local};
|
||||
}
|
||||
|
||||
std::optional<Endpoint> Gateway::mapped() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return mapped_;
|
||||
}
|
||||
|
||||
int Gateway::relay_port() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
return relay_ ? relay_->port : 0;
|
||||
}
|
||||
|
||||
Endpoint Gateway::add_peer(int member_id, const std::string& online_id, const Endpoint& endpoint,
|
||||
const Endpoint& local_endpoint, const std::string& port_online_id, int port_member_id,
|
||||
uint64_t room_id, const std::optional<Endpoint>& port_endpoint) {
|
||||
LinkPtr link;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
// A peer the port asked for before it joined (a host signaling the
|
||||
// guest it summons) sits under a provisional key: keep its inner socket
|
||||
// - the port knows it there - and file it under the real id.
|
||||
for (auto it = peers_.begin(); it != peers_.end(); ++it) {
|
||||
if (!online_id.empty() && it->second->online_id == online_id && it->first != member_id) {
|
||||
LinkPtr other = it->second;
|
||||
peers_.erase(it);
|
||||
auto old = peers_.find(member_id);
|
||||
if (old != peers_.end()) {
|
||||
if (old->second != other) close_link(*old->second);
|
||||
peers_.erase(old);
|
||||
}
|
||||
other->member_id = member_id;
|
||||
other->signaling.peer_member_id = member_id > 0 ? member_id : 0;
|
||||
peers_[member_id] = other;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto it = peers_.find(member_id);
|
||||
if (it != peers_.end() && it->second->online_id == online_id) {
|
||||
Link& l = *it->second;
|
||||
if (endpoint.valid()) l.endpoint = endpoint;
|
||||
if (local_endpoint.valid()) l.local_endpoint = local_endpoint;
|
||||
if (port_endpoint && !l.port_endpoint) l.port_endpoint = port_endpoint;
|
||||
// a guest invite seeds the host before the port has joined: the room
|
||||
// and the port's member id arrive with the join
|
||||
if (port_member_id) l.signaling.port_member_id = port_member_id;
|
||||
if (room_id) l.signaling.room_id = room_id;
|
||||
return l.inner_endpoint;
|
||||
}
|
||||
if (it != peers_.end()) {
|
||||
close_link(*it->second);
|
||||
peers_.erase(it);
|
||||
}
|
||||
link = std::make_shared<Link>(PeerSignaling(online_id, member_id, port_online_id, port_member_id, room_id));
|
||||
link->member_id = member_id;
|
||||
link->online_id = online_id;
|
||||
link->endpoint = endpoint;
|
||||
link->local_endpoint = local_endpoint;
|
||||
link->port_endpoint = port_endpoint;
|
||||
link->created = now();
|
||||
link->inner = udp_bind("127.0.0.1", 0, &link->inner_endpoint);
|
||||
peers_[member_id] = link;
|
||||
}
|
||||
log(fmt("p2p: member %d (%s) at %s -> port sees 127.0.0.1:%d", member_id, online_id.c_str(),
|
||||
endpoint.valid() ? endpoint.str().c_str() : "?:0", link->inner_endpoint.port));
|
||||
return link->inner_endpoint;
|
||||
}
|
||||
|
||||
void Gateway::remove_peer(int member_id) {
|
||||
LinkPtr link;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
auto it = peers_.find(member_id);
|
||||
if (it == peers_.end()) return;
|
||||
link = it->second;
|
||||
peers_.erase(it);
|
||||
close_link(*link);
|
||||
}
|
||||
log(fmt("p2p: member %d removed (to peer %llu, to port %llu, signaling %llu)", member_id,
|
||||
static_cast<unsigned long long>(link->to_peer), static_cast<unsigned long long>(link->to_port),
|
||||
static_cast<unsigned long long>(link->signaling_count)));
|
||||
}
|
||||
|
||||
void Gateway::clear_peers() {
|
||||
std::vector<int> ids;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (const auto& [id, link] : peers_) ids.push_back(id);
|
||||
}
|
||||
for (int id : ids) remove_peer(id);
|
||||
}
|
||||
|
||||
void Gateway::remove_peers_except(const std::set<std::string>& online_ids) {
|
||||
std::vector<int> ids;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (const auto& [id, link] : peers_)
|
||||
if (!online_ids.count(link->online_id)) ids.push_back(id);
|
||||
}
|
||||
for (int id : ids) remove_peer(id);
|
||||
}
|
||||
|
||||
std::optional<Endpoint> Gateway::inner_endpoint_for(const std::string& online_id) const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (const auto& [id, link] : peers_)
|
||||
if (link->online_id == online_id) return link->inner_endpoint;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<PeerInfo> Gateway::peers() const {
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
std::vector<PeerInfo> out;
|
||||
for (const auto& [id, l] : peers_) {
|
||||
PeerInfo p;
|
||||
p.member_id = id;
|
||||
p.online_id = l->online_id;
|
||||
p.endpoint = l->endpoint;
|
||||
p.inner = l->inner_endpoint;
|
||||
p.peer_member_id = l->signaling.peer_member_id;
|
||||
p.port_member_id = l->signaling.port_member_id;
|
||||
p.room_id = l->signaling.room_id;
|
||||
p.to_peer = l->to_peer;
|
||||
p.to_port = l->to_port;
|
||||
p.signaling = l->signaling_count;
|
||||
out.push_back(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool Gateway::relay_target(const Endpoint& e) const {
|
||||
return relay_ && e.valid() && e.ip == relay_->server.ip && e.port != relay_->server.port;
|
||||
}
|
||||
|
||||
void Gateway::send_outer(const std::string& datagram, const Endpoint& to) {
|
||||
if (!to.valid()) return;
|
||||
std::optional<Relay> relay;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
relay = relay_;
|
||||
}
|
||||
if (relay && to.ip == relay->server.ip && to.port != relay->server.port)
|
||||
udp_send(outer_, relay_wrap(relay->token, to.port, datagram), relay->server);
|
||||
else
|
||||
udp_send(outer_, datagram, to);
|
||||
}
|
||||
|
||||
void Gateway::to_port(const LinkPtr& link, const std::string& frame) {
|
||||
SOCKET s;
|
||||
std::optional<Endpoint> dst;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
s = link->inner;
|
||||
dst = link->port_endpoint;
|
||||
}
|
||||
if (s != INVALID_SOCKET && dst) udp_send(s, frame, *dst);
|
||||
}
|
||||
|
||||
Gateway::LinkPtr Gateway::link_for(const Endpoint& src) {
|
||||
for (auto& [id, link] : peers_) {
|
||||
if (src == link->endpoint || src == link->local_endpoint || link->aliases.count(src)) {
|
||||
if (src != link->endpoint && link->aliases.count(src)) link->endpoint = src; // answer where it last spoke
|
||||
return link;
|
||||
}
|
||||
}
|
||||
// A NAT may hand the peer's datagrams a new port, and a relayed bbhost peer
|
||||
// can reach us from two relay ports (the one the room lists and the one
|
||||
// signaling resolved). With one peer from that address, take the new port
|
||||
// as another of its endpoints.
|
||||
LinkPtr only;
|
||||
int same = 0;
|
||||
for (auto& [id, link] : peers_)
|
||||
if (link->endpoint.valid() && link->endpoint.ip == src.ip) {
|
||||
only = link;
|
||||
++same;
|
||||
}
|
||||
if (same != 1) return nullptr;
|
||||
only->aliases.insert(only->endpoint);
|
||||
only->aliases.insert(src);
|
||||
log(fmt("p2p: member %d (%s) also at %s", only->member_id, only->online_id.c_str(), src.str().c_str()));
|
||||
only->endpoint = src;
|
||||
return only;
|
||||
}
|
||||
|
||||
void Gateway::outer_main() {
|
||||
std::string buf(65536, '\0');
|
||||
while (!stop_) {
|
||||
sockaddr_in from{};
|
||||
socklen_t flen = sizeof(from);
|
||||
const int n = recvfrom(outer_, buf.data(), static_cast<int>(buf.size()), 0,
|
||||
reinterpret_cast<sockaddr*>(&from), &flen);
|
||||
if (n < 0) {
|
||||
if (stop_) return;
|
||||
continue;
|
||||
}
|
||||
Endpoint src = from_sockaddr(from);
|
||||
std::string data(buf.data(), static_cast<size_t>(n));
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
int port;
|
||||
std::string inner;
|
||||
if (relay_ && src == relay_->server && relay_unwrap(data, port, inner)) {
|
||||
src = Endpoint{relay_->server.ip, port};
|
||||
data = std::move(inner);
|
||||
}
|
||||
}
|
||||
if (data.size() >= 20 && data[0] == 0x01 && data[1] == 0x01) {
|
||||
on_stun(data);
|
||||
continue;
|
||||
}
|
||||
LinkPtr link;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
link = link_for(src);
|
||||
if (data.compare(0, 5, kProbe, 0, 5) == 0) {
|
||||
if (link) link->heard_peer = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
std::string frame;
|
||||
if (!link || !ps4_to_port(data, frame)) continue;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
link->heard_peer = true;
|
||||
++link->to_port;
|
||||
}
|
||||
to_port(link, frame);
|
||||
}
|
||||
}
|
||||
|
||||
void Gateway::inner_main() {
|
||||
std::string buf(65536, '\0');
|
||||
while (!stop_) {
|
||||
std::map<SOCKET, LinkPtr> links;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (auto& [id, link] : peers_)
|
||||
if (link->inner != INVALID_SOCKET) links[link->inner] = link;
|
||||
}
|
||||
if (links.empty()) {
|
||||
Sleep(50);
|
||||
continue;
|
||||
}
|
||||
fd_set rd;
|
||||
FD_ZERO(&rd);
|
||||
SOCKET highest = 0; // select()'s first argument: ignored on Windows, the highest fd + 1 elsewhere
|
||||
for (auto& [s, l] : links) {
|
||||
FD_SET(s, &rd);
|
||||
highest = std::max(highest, s);
|
||||
}
|
||||
timeval tv{0, 100000};
|
||||
if (select(static_cast<int>(highest) + 1, &rd, nullptr, nullptr, &tv) <= 0) continue; // a socket closed under us: rebuild
|
||||
for (auto& [s, link] : links) {
|
||||
if (!FD_ISSET(s, &rd)) continue;
|
||||
sockaddr_in from{};
|
||||
socklen_t flen = sizeof(from);
|
||||
const int n = recvfrom(s, buf.data(), static_cast<int>(buf.size()), 0,
|
||||
reinterpret_cast<sockaddr*>(&from), &flen);
|
||||
if (n < 0) continue;
|
||||
const std::string data(buf.data(), static_cast<size_t>(n));
|
||||
std::string out;
|
||||
const Frame kind = port_to_ps4(data, out);
|
||||
Endpoint peer;
|
||||
std::vector<std::string> replies;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
link->port_endpoint = from_sockaddr(from);
|
||||
if (kind == Frame::Game) {
|
||||
++link->to_peer;
|
||||
peer = link->endpoint;
|
||||
} else if (kind == Frame::Signaling) {
|
||||
++link->signaling_count;
|
||||
replies = link->signaling.handle(out);
|
||||
}
|
||||
}
|
||||
if (kind == Frame::Game) send_outer(out, peer);
|
||||
for (const auto& r : replies) to_port(link, port_signaling_frame(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Gateway::tick_main() {
|
||||
double last_stun = -1e9, last_punch = 0, last_opening = 0;
|
||||
while (!stop_) {
|
||||
Sleep(100);
|
||||
const double t = now();
|
||||
if (stun_server_ && t - last_stun >= stun_refresh) {
|
||||
last_stun = t;
|
||||
send_stun();
|
||||
}
|
||||
if (t - last_punch >= punch_interval) {
|
||||
last_punch = t;
|
||||
std::vector<Endpoint> targets;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (auto& [id, l] : peers_) {
|
||||
if (l->heard_peer || t - l->created > punch_for || relay_target(l->endpoint)) continue;
|
||||
if (l->endpoint.valid()) targets.push_back(l->endpoint);
|
||||
if (l->local_endpoint.valid()) targets.push_back(l->local_endpoint);
|
||||
}
|
||||
}
|
||||
for (const auto& e : targets) udp_send(outer_, kProbe, e);
|
||||
}
|
||||
if (t - last_opening >= opening_interval) {
|
||||
last_opening = t;
|
||||
std::vector<std::pair<LinkPtr, std::vector<std::string>>> openings;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (auto& [id, l] : peers_)
|
||||
if (!l->signaling.heard && t - l->created <= opening_for)
|
||||
openings.emplace_back(l, l->signaling.opening());
|
||||
}
|
||||
for (auto& [l, payloads] : openings)
|
||||
for (const auto& p : payloads) to_port(l, port_signaling_frame(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Gateway::send_stun() {
|
||||
const std::string ip = resolve_ipv4(stun_server_->first);
|
||||
if (ip.empty()) {
|
||||
log(fmt("p2p: cannot resolve STUN server %s", stun_server_->first.c_str()));
|
||||
return;
|
||||
}
|
||||
std::string request;
|
||||
Endpoint server{ip, stun_server_->second};
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
const std::string token = relay_ && relay_->server.ip == ip ? relay_->token : "";
|
||||
request = stun_request(token, stun_txid_);
|
||||
stun_server_ep_ = server;
|
||||
}
|
||||
udp_send(outer_, request, server);
|
||||
}
|
||||
|
||||
void Gateway::on_stun(const std::string& data) {
|
||||
std::optional<Endpoint> changed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
StunAnswer a;
|
||||
if (stun_txid_.empty() || !stun_parse(data, stun_txid_, a)) return;
|
||||
if (a.relay && stun_server_ep_.valid()) {
|
||||
if (!relay_ || relay_->port != a.relay_port) log(fmt("p2p: relay on, our port on it is %d", a.relay_port));
|
||||
relay_ = Relay{stun_server_ep_, a.relay_token, a.relay_port};
|
||||
} else if (relay_) {
|
||||
log("p2p: relay off");
|
||||
relay_.reset();
|
||||
}
|
||||
if (!mapped_ || *mapped_ != a.mapped) {
|
||||
log("p2p: STUN sees us as " + a.mapped.str());
|
||||
mapped_ = a.mapped;
|
||||
changed = a.mapped;
|
||||
}
|
||||
}
|
||||
if (changed && on_mapped_change_) {
|
||||
try {
|
||||
on_mapped_change_(*changed);
|
||||
} catch (const std::exception& e) {
|
||||
log(std::string("p2p: mapped-change callback failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bb::p2p
|
||||
@@ -1,163 +0,0 @@
|
||||
// P2P gateway: carries the game's UDP traffic between bbport and bbhost players.
|
||||
//
|
||||
// bbport (gpu/shadps4/core/libraries/network/p2p_sockets.cpp)
|
||||
// game datagram: [src vport u16 be][dst vport u16 be][payload]
|
||||
// dst vport 0xffff: internal "SHAD" packets - 0x05/0x06 echo (17 B),
|
||||
// 0x07 handshake (62 B), 0x10 control (50 B), 0x21 matching2 handshake (50 B)
|
||||
// bbhost (src/hle/net.cpp, src/net/stun.cpp)
|
||||
// game datagram: [0xff][flags][src][dst][payload] - flags 0xC3 with one-byte
|
||||
// vports (Bloodborne's 40 and 30), 0x83 with two-byte, +4 bytes with 0x20
|
||||
// hole-punch probe "\xfebbhp\0\0\0"; STUN Binding + BBHOST-HELLO (0x8100);
|
||||
// relay: to the STUN port [0xfb]['R'][token 8][port u16][datagram],
|
||||
// from it [0xfb]['r'][source port u16][datagram]
|
||||
//
|
||||
// One OUTER socket faces the internet and speaks bbhost's format; each remote
|
||||
// member gets an INNER socket on 127.0.0.1, which is where the port is told the
|
||||
// member lives. bbport's signaling handshakes are answered on the bbhost
|
||||
// peer's behalf (bbhost has none: its connection state comes from the server).
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb::p2p {
|
||||
|
||||
// ---- framing (pure, unit-tested) ----
|
||||
enum class Frame { None, Game, Signaling };
|
||||
Frame port_to_ps4(const std::string& frame, std::string& out); // Game: bbhost datagram; Signaling: payload
|
||||
bool ps4_to_port(const std::string& datagram, std::string& out);
|
||||
std::string port_signaling_frame(const std::string& payload);
|
||||
std::string relay_wrap(const std::string& token, int port, const std::string& datagram);
|
||||
bool relay_unwrap(const std::string& data, int& port, std::string& datagram);
|
||||
extern const std::string kProbe;
|
||||
|
||||
struct StunAnswer {
|
||||
Endpoint mapped;
|
||||
bool relay = false;
|
||||
std::string relay_token; // 8 bytes
|
||||
int relay_port = 0;
|
||||
};
|
||||
std::string stun_request(const std::string& token, std::string& txid); // token "" = zeros
|
||||
bool stun_parse(const std::string& data, const std::string& txid, StunAnswer& out);
|
||||
|
||||
// ---- the port's signaling, answered for one bbhost peer ----
|
||||
enum : uint8_t { T_ECHO_PING = 0x05, T_ECHO_PONG = 0x06, T_HANDSHAKE = 0x07, T_CONTROL = 0x10, T_M2 = 0x21 };
|
||||
enum : uint8_t { HS_OFFER = 1, HS_ACCEPT = 2, HS_CHECK = 3, HS_CHECK_ACK = 4 };
|
||||
enum : uint8_t { CTL_ACTIVATION_REQUEST = 1, CTL_ACTIVATION_ACK = 2, CTL_CLOSE = 3, CTL_ESTABLISHED = 4 };
|
||||
|
||||
class PeerSignaling {
|
||||
public:
|
||||
PeerSignaling(const std::string& peer_online_id, int peer_member_id, const std::string& port_online_id,
|
||||
int port_member_id, uint64_t room_id);
|
||||
std::vector<std::string> handle(const std::string& payload); // replies (payloads, no frame header)
|
||||
std::vector<std::string> opening() const; // what a real peer sends first
|
||||
|
||||
std::string peer_id, port_id; // 16 bytes each, NUL padded
|
||||
int peer_member_id = 0; // 0 until the peer is a room member
|
||||
int port_member_id = 0;
|
||||
uint64_t room_id = 0;
|
||||
uint32_t conn_id = 0;
|
||||
bool sent_established = false;
|
||||
bool heard = false; // any signaling from the port for this peer
|
||||
|
||||
private:
|
||||
std::string handshake(uint8_t kind, uint32_t to_conn, uint64_t nonce) const;
|
||||
std::string control(uint8_t kind) const;
|
||||
std::string m2(uint8_t kind, uint64_t nonce) const;
|
||||
};
|
||||
|
||||
// ---- the gateway ----
|
||||
struct PeerInfo { // a snapshot, for the app and the tests
|
||||
int member_id = 0;
|
||||
std::string online_id;
|
||||
Endpoint endpoint, inner;
|
||||
int peer_member_id = 0, port_member_id = 0;
|
||||
uint64_t room_id = 0;
|
||||
uint64_t to_peer = 0, to_port = 0, signaling = 0;
|
||||
};
|
||||
|
||||
class Gateway {
|
||||
public:
|
||||
Gateway(int outer_port, std::optional<std::pair<std::string, int>> stun_server, std::string lan_addr,
|
||||
std::function<void(const Endpoint&)> on_mapped_change);
|
||||
~Gateway();
|
||||
bool start(std::string* error);
|
||||
void stop();
|
||||
|
||||
std::pair<Endpoint, Endpoint> endpoints() const; // (local, mapped) as bbhost registers them
|
||||
std::optional<Endpoint> mapped() const;
|
||||
int relay_port() const;
|
||||
|
||||
// Returns the 127.0.0.1 endpoint the port should use for this member.
|
||||
Endpoint add_peer(int member_id, const std::string& online_id, const Endpoint& endpoint,
|
||||
const Endpoint& local_endpoint, const std::string& port_online_id, int port_member_id,
|
||||
uint64_t room_id, const std::optional<Endpoint>& port_endpoint);
|
||||
void remove_peer(int member_id);
|
||||
void clear_peers();
|
||||
void remove_peers_except(const std::set<std::string>& online_ids);
|
||||
std::optional<Endpoint> inner_endpoint_for(const std::string& online_id) const;
|
||||
std::vector<PeerInfo> peers() const;
|
||||
|
||||
double punch_interval = 0.5, punch_for = 20.0, opening_interval = 1.0, opening_for = 30.0,
|
||||
stun_refresh = 15.0;
|
||||
|
||||
private:
|
||||
struct Link {
|
||||
int member_id = 0;
|
||||
std::string online_id;
|
||||
Endpoint endpoint, local_endpoint;
|
||||
std::set<Endpoint> aliases;
|
||||
std::optional<Endpoint> port_endpoint;
|
||||
PeerSignaling signaling;
|
||||
SOCKET inner = INVALID_SOCKET;
|
||||
Endpoint inner_endpoint;
|
||||
bool heard_peer = false;
|
||||
double created = 0;
|
||||
uint64_t to_peer = 0, to_port = 0, signaling_count = 0;
|
||||
Link(PeerSignaling s) : signaling(std::move(s)) {}
|
||||
};
|
||||
using LinkPtr = std::shared_ptr<Link>;
|
||||
|
||||
void outer_main();
|
||||
void inner_main();
|
||||
void tick_main();
|
||||
void send_outer(const std::string& datagram, const Endpoint& to);
|
||||
void to_port(const LinkPtr& link, const std::string& frame);
|
||||
LinkPtr link_for(const Endpoint& src); // under mu_
|
||||
bool relay_target(const Endpoint& e) const; // under mu_
|
||||
void send_stun();
|
||||
void on_stun(const std::string& data);
|
||||
static void close_link(Link& link);
|
||||
|
||||
int outer_port_ = 0;
|
||||
std::optional<std::pair<std::string, int>> stun_server_;
|
||||
std::string lan_addr_;
|
||||
std::function<void(const Endpoint&)> on_mapped_change_;
|
||||
SOCKET outer_ = INVALID_SOCKET;
|
||||
mutable std::mutex mu_;
|
||||
std::map<int, LinkPtr> peers_;
|
||||
std::optional<Endpoint> mapped_;
|
||||
struct Relay {
|
||||
Endpoint server; // its STUN endpoint
|
||||
std::string token;
|
||||
int port = 0; // ours on it
|
||||
};
|
||||
std::optional<Relay> relay_;
|
||||
std::string stun_txid_;
|
||||
Endpoint stun_server_ep_;
|
||||
std::atomic<bool> stop_{false};
|
||||
std::vector<std::thread> threads_;
|
||||
};
|
||||
|
||||
} // namespace bb::p2p
|
||||
@@ -1,85 +0,0 @@
|
||||
#include "proto3.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace pb {
|
||||
|
||||
namespace {
|
||||
bool read_varint(const std::string& b, size_t& pos, uint64_t& out) {
|
||||
out = 0;
|
||||
int shift = 0;
|
||||
while (true) {
|
||||
if (pos >= b.size() || shift > 63) return false;
|
||||
const uint8_t c = static_cast<uint8_t>(b[pos++]);
|
||||
out |= static_cast<uint64_t>(c & 0x7F) << shift;
|
||||
if (!(c & 0x80)) return true;
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool parse(const std::string& b, std::vector<Field>& out) {
|
||||
size_t pos = 0;
|
||||
while (pos < b.size()) {
|
||||
uint64_t tag = 0;
|
||||
if (!read_varint(b, pos, tag)) return false;
|
||||
Field f;
|
||||
f.number = static_cast<int>(tag >> 3);
|
||||
f.wire = static_cast<int>(tag & 7);
|
||||
switch (f.wire) {
|
||||
case 0:
|
||||
if (!read_varint(b, pos, f.value)) return false;
|
||||
break;
|
||||
case 2: {
|
||||
uint64_t len = 0;
|
||||
if (!read_varint(b, pos, len) || len > b.size() - pos) return false;
|
||||
f.data.assign(b, pos, static_cast<size_t>(len));
|
||||
pos += static_cast<size_t>(len);
|
||||
break;
|
||||
}
|
||||
case 5:
|
||||
if (b.size() - pos < 4) return false;
|
||||
{ uint32_t v; std::memcpy(&v, b.data() + pos, 4); f.value = v; }
|
||||
pos += 4;
|
||||
break;
|
||||
case 1:
|
||||
if (b.size() - pos < 8) return false;
|
||||
std::memcpy(&f.value, b.data() + pos, 8);
|
||||
pos += 8;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
out.push_back(std::move(f));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t Message::u(int field, uint64_t def) const {
|
||||
uint64_t v = def;
|
||||
for (const auto& f : fields_)
|
||||
if (f.number == field && f.wire != 2) v = f.value;
|
||||
return v;
|
||||
}
|
||||
|
||||
std::string Message::s(int field) const {
|
||||
std::string v;
|
||||
for (const auto& f : fields_)
|
||||
if (f.number == field && f.wire == 2) v = f.data;
|
||||
return v;
|
||||
}
|
||||
|
||||
bool Message::has(int field) const {
|
||||
for (const auto& f : fields_)
|
||||
if (f.number == field) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> Message::all(int field) const {
|
||||
std::vector<std::string> v;
|
||||
for (const auto& f : fields_)
|
||||
if (f.number == field && f.wire == 2) v.push_back(f.data);
|
||||
return v;
|
||||
}
|
||||
|
||||
} // namespace pb
|
||||
@@ -1,73 +0,0 @@
|
||||
// Minimal protobuf3 wire format: varint and length-delimited fields, which is
|
||||
// all the shadNet messages use. proto3 semantics: fields at their default value
|
||||
// are omitted on encode; decode keeps what is on the wire.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace pb {
|
||||
|
||||
class Writer {
|
||||
public:
|
||||
void varint(uint64_t v) {
|
||||
while (v >= 0x80) {
|
||||
buf_.push_back(static_cast<char>((v & 0x7F) | 0x80));
|
||||
v >>= 7;
|
||||
}
|
||||
buf_.push_back(static_cast<char>(v));
|
||||
}
|
||||
void tag(int field, int wire) { varint((static_cast<uint64_t>(field) << 3) | static_cast<uint64_t>(wire)); }
|
||||
|
||||
Writer& u(int field, uint64_t v) { // uint / int (0 omitted)
|
||||
if (v) { tag(field, 0); varint(v); }
|
||||
return *this;
|
||||
}
|
||||
Writer& i(int field, int64_t v) { return u(field, static_cast<uint64_t>(v)); }
|
||||
Writer& flag(int field, bool v) { // bool (false omitted)
|
||||
if (v) { tag(field, 0); varint(1); }
|
||||
return *this;
|
||||
}
|
||||
Writer& s(int field, const std::string& v) { // string / bytes ("" omitted)
|
||||
if (!v.empty()) { tag(field, 2); varint(v.size()); buf_ += v; }
|
||||
return *this;
|
||||
}
|
||||
Writer& msg(int field, const std::string& encoded) { // embedded message, always written
|
||||
tag(field, 2);
|
||||
varint(encoded.size());
|
||||
buf_ += encoded;
|
||||
return *this;
|
||||
}
|
||||
const std::string& str() const { return buf_; }
|
||||
|
||||
private:
|
||||
std::string buf_;
|
||||
};
|
||||
|
||||
struct Field {
|
||||
int number = 0;
|
||||
int wire = 0;
|
||||
uint64_t value = 0; // varint / fixed
|
||||
std::string data; // length-delimited
|
||||
};
|
||||
|
||||
bool parse(const std::string& buf, std::vector<Field>& out);
|
||||
|
||||
// Read access by field number; scalars keep the last occurrence.
|
||||
class Message {
|
||||
public:
|
||||
Message() = default;
|
||||
explicit Message(const std::string& buf) { ok_ = parse(buf, fields_); }
|
||||
bool ok() const { return ok_; }
|
||||
uint64_t u(int field, uint64_t def = 0) const;
|
||||
std::string s(int field) const;
|
||||
bool has(int field) const;
|
||||
std::vector<std::string> all(int field) const;
|
||||
|
||||
private:
|
||||
std::vector<Field> fields_;
|
||||
bool ok_ = false;
|
||||
};
|
||||
|
||||
} // namespace pb
|
||||
@@ -1,262 +0,0 @@
|
||||
#include "setup.hpp"
|
||||
|
||||
#include <tlhelp32.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace bb {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kBackup = ".before-bbbridge";
|
||||
constexpr const char* kMarker = "_bbbridge";
|
||||
constexpr const char* kP2pVar = "SHADPS4_P2P_PORT";
|
||||
constexpr int kP2pPort = 3658;
|
||||
constexpr const char* kBridgeServer = "127.0.0.1:31313";
|
||||
constexpr const char* kBridgeWebapi = "http://127.0.0.1:31315";
|
||||
constexpr const char* kPublicServer = "srv.shadps4.net:31313";
|
||||
constexpr const char* kPlaceholderPassword = "bbbridge"; // see enable()
|
||||
|
||||
// bbhost's play-log patch (bbhost src/engine/playlog.cpp) in bbport's patch
|
||||
// format: the float immediates of `mov dword [rip+disp], imm32` in
|
||||
// SprjPlaylogSystem's static initializer - 0x23F9B86 stores 300.0f (upload
|
||||
// period), 0x23F9B68 stores 1.5f (position period); the float is at +6.
|
||||
// Verified against eboot.bin 1.09.
|
||||
const char* kPatchXml = R"xml(<?xml version="1.0"?>
|
||||
<Patch>
|
||||
<TitleID>
|
||||
<ID>CUSA03173</ID>
|
||||
</TitleID>
|
||||
<Metadata Title="Bloodborne" Name="Live map: play log every 5 s, position every 0.5 s" Note="Uploads the play log every 5 s instead of 300 s and samples the position every 0.5 s instead of 1.5 s, as bbhost does, so the player shows on thehuntersdream.com's live map." Author="bbbridge (from bbhost)" PatchVer="1.0" AppVer="01.09" AppElf="eboot.bin" isEnabled="true">
|
||||
<PatchList>
|
||||
<Line Type="float32" Address="0x023F9B8C" Value="5.0"/>
|
||||
<Line Type="float32" Address="0x023F9B6E" Value="0.5"/>
|
||||
</PatchList>
|
||||
</Metadata>
|
||||
</Patch>
|
||||
)xml";
|
||||
|
||||
nlohmann::ordered_json overrides() {
|
||||
return {{kMarker, "Written by bbbridge: the game's server traffic goes through bbbridge. Move bbbridge's "
|
||||
"Online switch to shadNet to remove."},
|
||||
{"https://ss4.scej-network.jp:20443", "http://127.0.0.1:31316"},
|
||||
{"https://thehuntersdream.com:20443", "http://127.0.0.1:31316"},
|
||||
{"https://thehuntersdream.com:18671", "http://127.0.0.1:31317"},
|
||||
{"http://thehuntersdream.com:18671", "http://127.0.0.1:31317"},
|
||||
{"https://bb-playlog-test.s3.amazonaws.com:443", "http://127.0.0.1:31317"},
|
||||
{"https://bb-playlog-prod.s3.amazonaws.com:443", "http://127.0.0.1:31317"},
|
||||
{"https://thehuntersdream.com:18671/summon_messenger", "http://127.0.0.1:31315"},
|
||||
{"http://thehuntersdream.com:18671/summon_messenger", "http://127.0.0.1:31315"}};
|
||||
}
|
||||
|
||||
using ojson = nlohmann::ordered_json;
|
||||
|
||||
ojson read_json(const fs::path& p) {
|
||||
std::ifstream in(p);
|
||||
if (!in) return ojson();
|
||||
ojson j = ojson::parse(in, nullptr, false);
|
||||
return j.is_discarded() ? ojson() : j;
|
||||
}
|
||||
|
||||
void write_json(const fs::path& p, const ojson& j) {
|
||||
const fs::path tmp = p.wstring() + L".tmp";
|
||||
{
|
||||
std::ofstream out(tmp, std::ios::trunc);
|
||||
out << j.dump(2, ' ', false) << "\n";
|
||||
}
|
||||
MoveFileExW(tmp.c_str(), p.c_str(), MOVEFILE_REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
void backup(const fs::path& p) {
|
||||
const fs::path target = p.wstring() + widen(kBackup);
|
||||
std::error_code ec;
|
||||
if (fs::exists(p, ec) && !fs::exists(target, ec)) fs::copy_file(p, target, ec);
|
||||
}
|
||||
|
||||
std::string merge_env(const std::string& extra, const std::string& key, const std::string* value) {
|
||||
std::istringstream in(extra);
|
||||
std::string item, out;
|
||||
while (in >> item)
|
||||
if (item.rfind(key + "=", 0) != 0) out += (out.empty() ? "" : " ") + item;
|
||||
if (value) out += (out.empty() ? "" : " ") + key + "=" + *value;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Ours, or the Python bbbridge's (0.2.0: "_note": "bbbridge: ...").
|
||||
bool ours(const fs::path& overrides_file) {
|
||||
const ojson j = read_json(overrides_file);
|
||||
return j.is_object() && (j.contains(kMarker) || jstr(j, "_note").rfind("bbbridge:", 0) == 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool process_running(const wchar_t* image) {
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return false;
|
||||
PROCESSENTRY32W e{};
|
||||
e.dwSize = sizeof(e);
|
||||
bool found = false;
|
||||
for (BOOL ok = Process32FirstW(snap, &e); ok && !found; ok = Process32NextW(snap, &e))
|
||||
found = _wcsicmp(e.szExeFile, image) == 0;
|
||||
CloseHandle(snap);
|
||||
return found;
|
||||
}
|
||||
|
||||
bool launcher_running() { return process_running(L"Bloodborne.exe"); }
|
||||
bool game_running() { return process_running(L"Play Bloodborne.exe") || process_running(L"bb-probe.exe"); }
|
||||
|
||||
bool is_bbport_dir(const fs::path& dir) {
|
||||
std::error_code ec;
|
||||
return fs::is_regular_file(dir / L"Bloodborne.exe", ec) || fs::is_regular_file(dir / L"Play Bloodborne.exe", ec);
|
||||
}
|
||||
|
||||
fs::path find_bbport(const fs::path& exe_dir, const fs::path& chosen, const std::string& game_dir,
|
||||
BbportSource* source) {
|
||||
const std::pair<fs::path, BbportSource> candidates[] = {
|
||||
{chosen, BbportSource::Chosen},
|
||||
{exe_dir.parent_path(), BbportSource::ExeFolder}, // bbbridge lives in a folder inside bbport's
|
||||
{exe_dir, BbportSource::ExeFolder}, // or right next to Bloodborne.exe
|
||||
{game_dir.empty() ? fs::path() : fs::path(widen(game_dir)).parent_path(), BbportSource::Launcher}};
|
||||
for (const auto& [dir, src] : candidates)
|
||||
if (!dir.empty() && is_bbport_dir(dir)) {
|
||||
if (source) *source = src;
|
||||
return dir;
|
||||
}
|
||||
if (source) *source = BbportSource::None;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string describe(BbportSource source) {
|
||||
switch (source) {
|
||||
case BbportSource::Chosen: return "the folder you chose";
|
||||
case BbportSource::ExeFolder: return "the folder bbbridge is in";
|
||||
case BbportSource::Launcher: return "found from the bbport launcher's game folder";
|
||||
default: return "not found";
|
||||
}
|
||||
}
|
||||
|
||||
BbportSetup::BbportSetup(const fs::path& exe_dir, const fs::path& bbport_dir) : exe_dir_(exe_dir), chosen_(bbport_dir) {
|
||||
reload();
|
||||
}
|
||||
|
||||
void BbportSetup::reload() {
|
||||
wchar_t appdata[MAX_PATH] = {};
|
||||
if (!GetEnvironmentVariableW(L"APPDATA", appdata, MAX_PATH)) throw SetupError("APPDATA is not set");
|
||||
settings_file.clear();
|
||||
for (const wchar_t* name : {L"bbport-launcher", L"bbport-launcher-coop"}) {
|
||||
const fs::path p = fs::path(appdata) / name / L"settings.json";
|
||||
std::error_code ec;
|
||||
if (fs::is_regular_file(p, ec)) {
|
||||
settings_file = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (settings_file.empty())
|
||||
throw SetupError("bbport launcher settings not found. Start the bbport launcher once and close it.");
|
||||
settings = read_json(settings_file);
|
||||
if (!settings.is_object()) throw SetupError("the bbport launcher's settings.json cannot be read");
|
||||
bbport = find_bbport(exe_dir_, chosen_, jstr(settings, "game_dir"), &source);
|
||||
if (bbport.empty()) throw SetupError("bbport folder not found - choose the folder with Bloodborne.exe.");
|
||||
const std::string user = jstr(settings, "user_dir"), patches = jstr(settings, "patches_dir");
|
||||
user_dir = user.empty() ? bbport / L"user" : fs::path(widen(user));
|
||||
patches_dir = patches.empty() ? bbport / L"patches" : fs::path(widen(patches));
|
||||
}
|
||||
|
||||
bool BbportSetup::routed() const {
|
||||
return jint(settings, "online", 0) && jstr(settings, "online_server") == kBridgeServer &&
|
||||
jstr(settings, "online_webapi") == kBridgeWebapi && !jstr(settings, "online_password").empty();
|
||||
}
|
||||
|
||||
bool BbportSetup::overrides_written() const { return ours(overrides_file()); }
|
||||
|
||||
// bbport's run.py uses host_overrides.custom.json, when there is one, instead of
|
||||
// its own addresses: ours sends the game's server traffic to bbbridge, so it may
|
||||
// only be there while bbbridge runs - else the game cannot reach its server
|
||||
// ("Failed to acquire Bloodborne server information").
|
||||
void BbportSetup::set_overrides(bool on) {
|
||||
std::error_code ec;
|
||||
if (on) {
|
||||
fs::create_directories(user_dir, ec);
|
||||
if (fs::exists(overrides_file(), ec) && !ours(overrides_file())) backup(overrides_file());
|
||||
write_json(overrides_file(), overrides());
|
||||
} else if (ours(overrides_file())) {
|
||||
fs::remove(overrides_file(), ec);
|
||||
const fs::path prev = overrides_file().wstring() + widen(kBackup);
|
||||
if (fs::exists(prev, ec)) fs::rename(prev, overrides_file(), ec);
|
||||
}
|
||||
}
|
||||
|
||||
bool BbportSetup::game_ready() const {
|
||||
const std::string dir = jstr(settings, "game_dir");
|
||||
std::error_code ec;
|
||||
return !dir.empty() && fs::is_regular_file(fs::path(widen(dir)) / L"eboot.bin", ec);
|
||||
}
|
||||
|
||||
bool BbportSetup::patched() const {
|
||||
std::error_code ec;
|
||||
return fs::is_regular_file(patch_file(), ec);
|
||||
}
|
||||
|
||||
void BbportSetup::save_settings() {
|
||||
backup(settings_file);
|
||||
write_json(settings_file, settings);
|
||||
}
|
||||
|
||||
void BbportSetup::set_npid(const std::string& npid) {
|
||||
settings["online_npid"] = npid;
|
||||
save_settings();
|
||||
}
|
||||
|
||||
bool BbportSetup::shadnet_account_set() const {
|
||||
const std::string pw = jstr(settings, "online_password");
|
||||
return !npid().empty() && !pw.empty() && pw != kPlaceholderPassword;
|
||||
}
|
||||
|
||||
void BbportSetup::enable() {
|
||||
// On bbport's own server now: keep that setup (server, shadNet account) for disable(),
|
||||
// also when the player changed it since the last time.
|
||||
if (jstr(settings, "online_server") != kBridgeServer)
|
||||
write_json(settings_file.wstring() + widen(kBackup), settings);
|
||||
settings["online"] = true;
|
||||
settings["online_server"] = kBridgeServer;
|
||||
settings["online_webapi"] = kBridgeWebapi;
|
||||
// bbport does not go online without a password (np_handler.cpp: "shadNet
|
||||
// enabled but credentials missing, skipping"). The bridge never reads it; a
|
||||
// player without a shadNet account just needs something there.
|
||||
if (jstr(settings, "online_password").empty()) settings["online_password"] = kPlaceholderPassword;
|
||||
const std::string port = std::to_string(kP2pPort);
|
||||
settings["extra_env"] = merge_env(jstr(settings, "extra_env"), kP2pVar, &port);
|
||||
save_settings();
|
||||
set_overrides(true);
|
||||
}
|
||||
|
||||
void BbportSetup::disable() {
|
||||
const ojson old = read_json(settings_file.wstring() + widen(kBackup));
|
||||
std::string server = jstr(old, "online_server", kPublicServer);
|
||||
if (server.empty() || server == kBridgeServer) server = kPublicServer;
|
||||
std::string webapi = jstr(old, "online_webapi", "");
|
||||
if (webapi == kBridgeWebapi) webapi.clear();
|
||||
settings["online_server"] = server;
|
||||
settings["online_webapi"] = webapi;
|
||||
if (jhas(old, "online_npid")) settings["online_npid"] = jstr(old, "online_npid"); // the shadNet account, not The Hunter's Dream's
|
||||
if (jstr(settings, "online_password") == kPlaceholderPassword) { // ours: back to what it was
|
||||
const std::string before = jstr(old, "online_password");
|
||||
settings["online_password"] = before == kPlaceholderPassword ? "" : before;
|
||||
}
|
||||
settings["extra_env"] = merge_env(jstr(settings, "extra_env"), kP2pVar, nullptr);
|
||||
save_settings();
|
||||
set_overrides(false);
|
||||
}
|
||||
|
||||
void BbportSetup::set_patch(bool on) {
|
||||
std::error_code ec;
|
||||
if (on) {
|
||||
fs::create_directories(patches_dir, ec);
|
||||
std::ofstream(patch_file(), std::ios::trunc) << kPatchXml;
|
||||
} else {
|
||||
fs::remove(patch_file(), ec);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,80 +0,0 @@
|
||||
// Points an installed bbport (bloodborne_pc for Windows) at the bridge, or back.
|
||||
// Turning it on keeps bbport's own shadNet setup (server, account) in
|
||||
// settings.json.before-bbbridge; turning it off puts that back.
|
||||
// "Routed" means (a backup of every file is kept as *.before-bbbridge):
|
||||
// 1. %APPDATA%\bbport-launcher\settings.json: online on; co-op server
|
||||
// 127.0.0.1:31313; WebAPI http://127.0.0.1:31315; extra variable
|
||||
// SHADPS4_P2P_PORT=3658 (the bridge must know the port's P2P UDP port).
|
||||
// 2. <bbport user dir>\host_overrides.custom.json: the game's own server
|
||||
// traffic goes through the bridge (the account token, and the play log to
|
||||
// The Hunter's Dream instead of FromSoftware's closed upload bucket). Only
|
||||
// while bbbridge runs: the app removes it when it closes.
|
||||
// 3. <bbport>\patches\bbbridge-live-map.xml (optional): play log every 5 s,
|
||||
// position every 0.5 s - bbhost's own patch - for the site's live map.
|
||||
// The bbport launcher must be closed while settings.json changes: it rewrites
|
||||
// the file when it saves.
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "util.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct SetupError : std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
bool process_running(const wchar_t* image);
|
||||
bool launcher_running(); // Bloodborne.exe
|
||||
bool game_running(); // Play Bloodborne.exe / bb-probe.exe
|
||||
bool is_bbport_dir(const fs::path& dir);
|
||||
|
||||
// Where bbport was found, in this order: the folder the player chose, the
|
||||
// folder bbbridge's own folder is in (the normal install), bbbridge's own folder
|
||||
// (bbbridge.exe next to Bloodborne.exe), the launcher's game folder.
|
||||
enum class BbportSource { None, Chosen, ExeFolder, Launcher };
|
||||
fs::path find_bbport(const fs::path& exe_dir, const fs::path& chosen, const std::string& game_dir,
|
||||
BbportSource* source);
|
||||
std::string describe(BbportSource source);
|
||||
|
||||
class BbportSetup {
|
||||
public:
|
||||
// bbport_dir: "" = the folder above this exe, or from the launcher's settings.
|
||||
explicit BbportSetup(const fs::path& exe_dir, const fs::path& bbport_dir = {});
|
||||
void reload();
|
||||
|
||||
fs::path bbport, settings_file, user_dir, patches_dir;
|
||||
BbportSource source = BbportSource::None;
|
||||
nlohmann::ordered_json settings; // the launcher's file, key order kept
|
||||
|
||||
std::string npid() const { return jstr(settings, "online_npid"); }
|
||||
std::string server() const { return jstr(settings, "online_server"); }
|
||||
bool online() const { return jint(settings, "online", 0) != 0; }
|
||||
bool shadnet_account_set() const; // a name and a real (not bbbridge's placeholder) password
|
||||
bool routed() const; // bbport's settings point at bbbridge
|
||||
bool overrides_written() const; // our host_overrides.custom.json is there
|
||||
void set_overrides(bool on); // only while bbbridge runs (see setup.cpp)
|
||||
bool patched() const;
|
||||
bool game_ready() const; // the launcher has a game folder with eboot.bin
|
||||
|
||||
// the caller makes sure the launcher is closed
|
||||
void set_npid(const std::string& npid);
|
||||
void enable();
|
||||
void disable();
|
||||
void set_patch(bool on);
|
||||
|
||||
fs::path play_exe() const { return bbport / L"Play Bloodborne.exe"; }
|
||||
fs::path launcher_exe() const { return bbport / L"Bloodborne.exe"; }
|
||||
|
||||
private:
|
||||
void save_settings();
|
||||
fs::path overrides_file() const { return user_dir / L"host_overrides.custom.json"; }
|
||||
fs::path patch_file() const { return patches_dir / L"bbbridge-live-map.xml"; }
|
||||
fs::path exe_dir_, chosen_;
|
||||
};
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,103 +0,0 @@
|
||||
// shadNet wire protocol: 15-byte packet framing, command / error / notification
|
||||
// numbers, and the field numbers of the messages the bridge reads or writes.
|
||||
// Sources: shadnet-p2p's protocol.h and shadnet.proto, the port's client.cpp.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace shadnet {
|
||||
|
||||
constexpr size_t kHeaderSize = 15;
|
||||
constexpr uint32_t kMaxPacketSize = 0x800000;
|
||||
constexpr uint32_t kProtocolVersion = 1;
|
||||
|
||||
enum PacketType : uint8_t { Request = 0, Reply = 1, Notification = 2, ServerInfo = 3 };
|
||||
|
||||
namespace cmd {
|
||||
constexpr uint16_t Login = 0, Terminate = 1, AddFriend = 8, RemoveFriend = 9, AddBlock = 10,
|
||||
RemoveBlock = 11, GetServerFeatures = 12, GetToken = 39, SetAppearOffline = 40,
|
||||
ContextStart = 100, CreateRoom = 101, JoinRoom = 102, LeaveRoom = 103,
|
||||
SearchRoom = 104, RequestSignalingInfos = 105, ContextStop = 106,
|
||||
SetRoomDataInternal = 108, SetRoomDataExternal = 109, KickoutRoomMember = 110,
|
||||
GetWorldInfoList = 111;
|
||||
}
|
||||
|
||||
namespace err {
|
||||
constexpr uint8_t NoError = 0, Malformed = 1, Invalid = 2, InvalidInput = 3, LoginError = 5,
|
||||
LoginInvalidPassword = 8, RoomMissing = 14, NotFound = 26, Unsupported = 33;
|
||||
}
|
||||
|
||||
namespace notify {
|
||||
constexpr uint16_t RoomEvent = 10;
|
||||
}
|
||||
|
||||
namespace room_event {
|
||||
constexpr uint32_t MemberJoined = 0x1101, MemberLeft = 0x1102, KickedOut = 0x1103;
|
||||
}
|
||||
|
||||
namespace cause {
|
||||
constexpr uint32_t Leave = 1, Kickout = 2, ServerOperation = 4, MemberDisappeared = 5;
|
||||
}
|
||||
|
||||
constexpr uint32_t kOwnerFlag = 0x80000000; // SCE_NP_MATCHING2_ROOMMEMBER_FLAG_ATTR_OWNER
|
||||
|
||||
struct Header {
|
||||
uint8_t type = 0;
|
||||
uint16_t command = 0;
|
||||
uint32_t size = 0;
|
||||
uint64_t packet_id = 0;
|
||||
};
|
||||
|
||||
inline bool parse_header(const std::string& buf, Header& h) {
|
||||
if (buf.size() < kHeaderSize) return false;
|
||||
const char* p = buf.data();
|
||||
h.type = static_cast<uint8_t>(p[0]);
|
||||
std::memcpy(&h.command, p + 1, 2);
|
||||
std::memcpy(&h.size, p + 3, 4);
|
||||
std::memcpy(&h.packet_id, p + 7, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string build_packet(uint8_t type, uint16_t command, uint64_t packet_id, const std::string& payload) {
|
||||
std::string out(kHeaderSize, '\0');
|
||||
const uint32_t size = static_cast<uint32_t>(kHeaderSize + payload.size());
|
||||
out[0] = static_cast<char>(type);
|
||||
std::memcpy(&out[1], &command, 2);
|
||||
std::memcpy(&out[3], &size, 4);
|
||||
std::memcpy(&out[7], &packet_id, 8);
|
||||
return out + payload;
|
||||
}
|
||||
|
||||
inline std::string build_server_info() {
|
||||
std::string v(4, '\0');
|
||||
std::memcpy(v.data(), &kProtocolVersion, 4);
|
||||
return build_packet(ServerInfo, 0, 0, v);
|
||||
}
|
||||
|
||||
inline std::string build_reply(uint16_t command, uint64_t packet_id, uint8_t error, const std::string& payload) {
|
||||
return build_packet(Reply, command, packet_id, std::string(1, static_cast<char>(error)) + payload);
|
||||
}
|
||||
|
||||
// Every proto payload is framed as a u32-LE length + the message.
|
||||
inline std::string proto_blob(const std::string& msg) {
|
||||
std::string out(4, '\0');
|
||||
const uint32_t n = static_cast<uint32_t>(msg.size());
|
||||
std::memcpy(out.data(), &n, 4);
|
||||
return out + msg;
|
||||
}
|
||||
|
||||
inline bool parse_proto_blob(const std::string& payload, std::string& msg) {
|
||||
if (payload.size() < 4) return false;
|
||||
uint32_t n;
|
||||
std::memcpy(&n, payload.data(), 4);
|
||||
msg = payload.substr(4, n);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string build_notification(uint16_t type, const std::string& msg) {
|
||||
return build_packet(Notification, type, 0, proto_blob(msg));
|
||||
}
|
||||
|
||||
} // namespace shadnet
|
||||
@@ -1,197 +0,0 @@
|
||||
#include "util.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <csignal>
|
||||
#endif
|
||||
|
||||
namespace bb {
|
||||
|
||||
namespace {
|
||||
std::mutex g_log_mu;
|
||||
std::function<void(const std::string&)> g_sink;
|
||||
std::unique_ptr<std::ofstream> g_file;
|
||||
} // namespace
|
||||
|
||||
std::string fmt(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
va_list copy;
|
||||
va_copy(copy, args);
|
||||
const int n = std::vsnprintf(nullptr, 0, format, copy);
|
||||
va_end(copy);
|
||||
std::string out(n > 0 ? static_cast<size_t>(n) : 0, '\0');
|
||||
if (n > 0) std::vsnprintf(out.data(), out.size() + 1, format, args);
|
||||
va_end(args);
|
||||
return out;
|
||||
}
|
||||
|
||||
void log(const std::string& msg) {
|
||||
std::time_t t = std::time(nullptr);
|
||||
std::tm tm{};
|
||||
localtime_s(&tm, &t);
|
||||
char stamp[16];
|
||||
std::strftime(stamp, sizeof(stamp), "%H:%M:%S", &tm);
|
||||
const std::string line = std::string(stamp) + " [bbbridge] " + msg;
|
||||
std::lock_guard<std::mutex> lk(g_log_mu);
|
||||
std::fwrite(line.data(), 1, line.size(), stdout);
|
||||
std::fputc('\n', stdout);
|
||||
std::fflush(stdout);
|
||||
if (g_file && *g_file) {
|
||||
*g_file << line << '\n';
|
||||
g_file->flush();
|
||||
}
|
||||
if (g_sink) g_sink(line);
|
||||
}
|
||||
|
||||
void set_log_sink(std::function<void(const std::string&)> sink) {
|
||||
std::lock_guard<std::mutex> lk(g_log_mu);
|
||||
g_sink = std::move(sink);
|
||||
}
|
||||
|
||||
void set_log_file(const std::wstring& path) {
|
||||
std::lock_guard<std::mutex> lk(g_log_mu);
|
||||
std::wstring prev = path;
|
||||
const size_t dot = prev.rfind(L".log");
|
||||
prev = (dot == std::wstring::npos ? prev : prev.substr(0, dot)) + L".prev.log";
|
||||
replace_file(path, prev);
|
||||
g_file = std::make_unique<std::ofstream>(fspath(path), std::ios::out | std::ios::trunc);
|
||||
}
|
||||
|
||||
double now() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<duration<double>>(steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
bool net_init() {
|
||||
static bool ok = [] {
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa;
|
||||
return WSAStartup(MAKEWORD(2, 2), &wsa) == 0;
|
||||
#else
|
||||
// A send to a peer that already closed raises SIGPIPE on Linux, which ends the process;
|
||||
// Windows returns an error, which the bridge handles.
|
||||
std::signal(SIGPIPE, SIG_IGN);
|
||||
return true;
|
||||
#endif
|
||||
}();
|
||||
return ok;
|
||||
}
|
||||
|
||||
sockaddr_in to_sockaddr(const Endpoint& e) {
|
||||
sockaddr_in a{};
|
||||
a.sin_family = AF_INET;
|
||||
a.sin_port = htons(static_cast<u_short>(e.port));
|
||||
inet_pton(AF_INET, e.ip.c_str(), &a.sin_addr);
|
||||
return a;
|
||||
}
|
||||
|
||||
Endpoint from_sockaddr(const sockaddr_in& a) {
|
||||
char ip[INET_ADDRSTRLEN] = {};
|
||||
inet_ntop(AF_INET, &a.sin_addr, ip, sizeof(ip));
|
||||
return Endpoint{ip, ntohs(a.sin_port)};
|
||||
}
|
||||
|
||||
std::string detect_lan_addr() {
|
||||
// The interface the OS would route to the internet through (no packet is sent).
|
||||
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s == INVALID_SOCKET) return "127.0.0.1";
|
||||
sockaddr_in target = to_sockaddr(Endpoint{"8.8.8.8", 80});
|
||||
std::string out = "127.0.0.1";
|
||||
if (connect(s, reinterpret_cast<sockaddr*>(&target), sizeof(target)) == 0) {
|
||||
sockaddr_in local{};
|
||||
socklen_t len = sizeof(local);
|
||||
if (getsockname(s, reinterpret_cast<sockaddr*>(&local), &len) == 0) out = from_sockaddr(local).ip;
|
||||
}
|
||||
closesocket(s);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string resolve_ipv4(const std::string& host) {
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
addrinfo* res = nullptr;
|
||||
if (getaddrinfo(host.c_str(), nullptr, &hints, &res) != 0 || !res) return "";
|
||||
const std::string ip = from_sockaddr(*reinterpret_cast<sockaddr_in*>(res->ai_addr)).ip;
|
||||
freeaddrinfo(res);
|
||||
return ip;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string narrow(const std::wstring& w) {
|
||||
if (w.empty()) return {};
|
||||
const int n = WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), nullptr, 0, nullptr, nullptr);
|
||||
std::string out(static_cast<size_t>(n), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast<int>(w.size()), out.data(), n, nullptr, nullptr);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::wstring widen(const std::string& s) {
|
||||
if (s.empty()) return {};
|
||||
const int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), nullptr, 0);
|
||||
std::wstring out(static_cast<size_t>(n), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), out.data(), n);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::filesystem::path fspath(const std::wstring& w) { return std::filesystem::path(w); }
|
||||
|
||||
bool replace_file(const std::wstring& from, const std::wstring& to) {
|
||||
return MoveFileExW(from.c_str(), to.c_str(), MOVEFILE_REPLACE_EXISTING) != 0;
|
||||
}
|
||||
#else
|
||||
// wchar_t holds a whole code point (UTF-32) on Linux.
|
||||
std::string narrow(const std::wstring& w) {
|
||||
std::string out;
|
||||
for (const wchar_t wc : w) {
|
||||
const uint32_t c = static_cast<uint32_t>(wc);
|
||||
if (c < 0x80) {
|
||||
out += static_cast<char>(c);
|
||||
} else if (c < 0x800) {
|
||||
out += static_cast<char>(0xC0 | (c >> 6));
|
||||
out += static_cast<char>(0x80 | (c & 0x3F));
|
||||
} else if (c < 0x10000) {
|
||||
out += static_cast<char>(0xE0 | (c >> 12));
|
||||
out += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
out += static_cast<char>(0xF0 | (c >> 18));
|
||||
out += static_cast<char>(0x80 | ((c >> 12) & 0x3F));
|
||||
out += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::wstring widen(const std::string& s) {
|
||||
std::wstring out;
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
const unsigned char b = static_cast<unsigned char>(s[i]);
|
||||
const int extra = b < 0x80 ? 0 : b < 0xE0 ? 1 : b < 0xF0 ? 2 : 3;
|
||||
uint32_t c = extra == 0 ? b : extra == 1 ? (b & 0x1F) : extra == 2 ? (b & 0x0F) : (b & 0x07);
|
||||
size_t j = 1;
|
||||
for (; j <= static_cast<size_t>(extra) && i + j < s.size(); ++j)
|
||||
c = (c << 6) | (static_cast<unsigned char>(s[i + j]) & 0x3F);
|
||||
out += static_cast<wchar_t>(c);
|
||||
i += j;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::filesystem::path fspath(const std::wstring& w) { return std::filesystem::path(narrow(w)); }
|
||||
|
||||
bool replace_file(const std::wstring& from, const std::wstring& to) {
|
||||
return std::rename(narrow(from).c_str(), narrow(to).c_str()) == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,126 +0,0 @@
|
||||
// Shared helpers: logging, endpoints, Winsock, tolerant JSON field access.
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
// Linux: BSD sockets under Winsock's names, so the bridge's code is the same on both.
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <thread>
|
||||
|
||||
using SOCKET = int;
|
||||
constexpr SOCKET INVALID_SOCKET = -1;
|
||||
constexpr int SD_BOTH = SHUT_RDWR;
|
||||
// Closing a socket does not wake a thread blocked in accept() or recvfrom() on it on Linux
|
||||
// (Windows does): shutdown() first, which does, also for listening and unconnected UDP sockets.
|
||||
inline int closesocket(SOCKET s) {
|
||||
::shutdown(s, SHUT_RDWR);
|
||||
return ::close(s);
|
||||
}
|
||||
inline int WSAGetLastError() { return errno; }
|
||||
inline void Sleep(unsigned ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
|
||||
inline void localtime_s(std::tm* out, const std::time_t* t) { localtime_r(t, out); }
|
||||
#endif
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
namespace bb {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
constexpr const char* kVersion = "0.3.2";
|
||||
|
||||
// ---- logging: "HH:MM:SS [bbbridge] msg" to stdout, the log file and the app ----
|
||||
void log(const std::string& msg);
|
||||
std::string fmt(const char* format, ...);
|
||||
void set_log_sink(std::function<void(const std::string&)> sink); // the app's log panel
|
||||
void set_log_file(const std::wstring& path); // keeps the previous as *.prev.log
|
||||
|
||||
double now(); // seconds, monotonic
|
||||
|
||||
// ---- endpoints ----
|
||||
struct Endpoint {
|
||||
std::string ip;
|
||||
int port = 0;
|
||||
bool valid() const { return !ip.empty() && port > 0 && port <= 65535; }
|
||||
bool operator==(const Endpoint& o) const { return ip == o.ip && port == o.port; }
|
||||
bool operator!=(const Endpoint& o) const { return !(*this == o); }
|
||||
bool operator<(const Endpoint& o) const { return ip != o.ip ? ip < o.ip : port < o.port; }
|
||||
std::string str() const { return ip + ":" + std::to_string(port); }
|
||||
};
|
||||
|
||||
bool net_init();
|
||||
sockaddr_in to_sockaddr(const Endpoint& e);
|
||||
Endpoint from_sockaddr(const sockaddr_in& a);
|
||||
std::string detect_lan_addr();
|
||||
std::string resolve_ipv4(const std::string& host); // "" when it fails
|
||||
|
||||
// ---- JSON as Python's dict.get(): tolerant of missing keys, null, numbers as strings ----
|
||||
// Templates, so they serve json and ordered_json (settings files keep their key order).
|
||||
template <typename J>
|
||||
bool jhas(const J& j, const char* key) { // present and not null
|
||||
if (!j.is_object()) return false;
|
||||
auto it = j.find(key);
|
||||
return it != j.end() && !it->is_null();
|
||||
}
|
||||
|
||||
template <typename J>
|
||||
int64_t jint(const J& j, const char* key, int64_t def = 0) {
|
||||
if (!jhas(j, key)) return def;
|
||||
const auto& v = j.at(key);
|
||||
try {
|
||||
if (v.is_number_integer()) return v.template get<int64_t>();
|
||||
if (v.is_number_unsigned()) return static_cast<int64_t>(v.template get<uint64_t>());
|
||||
if (v.is_number_float()) return static_cast<int64_t>(v.template get<double>());
|
||||
if (v.is_boolean()) return v.template get<bool>() ? 1 : 0;
|
||||
if (v.is_string()) {
|
||||
const std::string s = v.template get<std::string>();
|
||||
return s.empty() ? def : std::stoll(s);
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
template <typename J>
|
||||
std::string jstr(const J& j, const char* key, const std::string& def = "") {
|
||||
if (!jhas(j, key)) return def;
|
||||
const auto& v = j.at(key);
|
||||
if (v.is_string()) return v.template get<std::string>();
|
||||
if (v.is_number_integer()) return std::to_string(v.template get<int64_t>());
|
||||
if (v.is_number_unsigned()) return std::to_string(v.template get<uint64_t>());
|
||||
return v.dump();
|
||||
}
|
||||
|
||||
std::string narrow(const std::wstring& w);
|
||||
std::wstring widen(const std::string& s);
|
||||
// A file name kept as UTF-16/32 text (std::wstring) as a path to open: on Linux file names are bytes,
|
||||
// UTF-8 here.
|
||||
std::filesystem::path fspath(const std::wstring& w);
|
||||
// Replaces `to` with `from` in one step (MoveFileExW with MOVEFILE_REPLACE_EXISTING, rename()).
|
||||
bool replace_file(const std::wstring& from, const std::wstring& to);
|
||||
|
||||
} // namespace bb
|
||||
@@ -1,147 +0,0 @@
|
||||
"""Minimal protobuf3 wire-format codec (no dependencies).
|
||||
|
||||
Only what shadnet_proto.py needs: varint and length-delimited fields.
|
||||
proto3 semantics: fields at their default value are omitted on encode;
|
||||
decode returns only the fields present on the wire.
|
||||
|
||||
Schema entry: field_number -> (name, kind)
|
||||
kind: "varint" (int), "bool", "string", "bytes", or ("message", schema).
|
||||
A wire field may repeat; decoded values are then lists (never for our
|
||||
scalars in practice, but decode keeps the last occurrence for scalars and
|
||||
extends a list for messages, which is what the shadnet messages use).
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
|
||||
def encode_varint(value):
|
||||
if value < 0:
|
||||
# int32/int64 negatives are encoded as 10-byte two's complement
|
||||
value &= (1 << 64) - 1
|
||||
out = bytearray()
|
||||
while True:
|
||||
b = value & 0x7F
|
||||
value >>= 7
|
||||
if value:
|
||||
out.append(b | 0x80)
|
||||
else:
|
||||
out.append(b)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decode_varint(buf, pos):
|
||||
result = 0
|
||||
shift = 0
|
||||
while True:
|
||||
if pos >= len(buf):
|
||||
raise ValueError("varint runs past end of buffer")
|
||||
b = buf[pos]
|
||||
pos += 1
|
||||
result |= (b & 0x7F) << shift
|
||||
if not (b & 0x80):
|
||||
return result, pos
|
||||
shift += 7
|
||||
if shift > 70:
|
||||
raise ValueError("varint too long")
|
||||
|
||||
|
||||
def encode_tag(field_number, wire_type):
|
||||
return encode_varint((field_number << 3) | wire_type)
|
||||
|
||||
|
||||
def encode_field(field_number, kind, value):
|
||||
"""Encode one field occurrence. Returns bytes."""
|
||||
if kind == "varint":
|
||||
if value in (0, None):
|
||||
return b""
|
||||
return encode_tag(field_number, 0) + encode_varint(int(value))
|
||||
if kind == "bool":
|
||||
if not value:
|
||||
return b""
|
||||
return encode_tag(field_number, 0) + encode_varint(1)
|
||||
if kind in ("string", "bytes"):
|
||||
if not value:
|
||||
return b""
|
||||
data = value.encode("utf-8") if kind == "string" else bytes(value)
|
||||
return encode_tag(field_number, 2) + encode_varint(len(data)) + data
|
||||
if isinstance(kind, tuple) and kind[0] == "message":
|
||||
if value is None:
|
||||
return b""
|
||||
data = encode_message(kind[1], value)
|
||||
return encode_tag(field_number, 2) + encode_varint(len(data)) + data
|
||||
raise ValueError("unknown kind %r" % (kind,))
|
||||
|
||||
|
||||
def encode_message(schema, values):
|
||||
"""values: {field_number: value}. Repeated messages take a list."""
|
||||
out = bytearray()
|
||||
for field_number in sorted(values):
|
||||
name, kind = schema[field_number]
|
||||
value = values[field_number]
|
||||
if isinstance(kind, tuple) and kind[0] == "repeated":
|
||||
_, inner = kind
|
||||
for item in (value or []):
|
||||
out += encode_field(field_number, inner, item)
|
||||
else:
|
||||
out += encode_field(field_number, kind, value)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def decode_raw_fields(buf):
|
||||
"""Yield (field_number, wire_type, value) for every field on the wire.
|
||||
Length-delimited values are bytes; varints are ints."""
|
||||
pos = 0
|
||||
n = len(buf)
|
||||
while pos < n:
|
||||
tag, pos = decode_varint(buf, pos)
|
||||
field_number = tag >> 3
|
||||
wire_type = tag & 7
|
||||
if wire_type == 0:
|
||||
value, pos = decode_varint(buf, pos)
|
||||
elif wire_type == 2:
|
||||
length, pos = decode_varint(buf, pos)
|
||||
value = buf[pos:pos + length]
|
||||
if len(value) != length:
|
||||
raise ValueError("length-delimited field runs past end")
|
||||
pos += length
|
||||
elif wire_type == 5: # fixed32
|
||||
value = struct.unpack_from("<I", buf, pos)[0]
|
||||
pos += 4
|
||||
elif wire_type == 1: # fixed64
|
||||
value = struct.unpack_from("<Q", buf, pos)[0]
|
||||
pos += 8
|
||||
else:
|
||||
raise ValueError("unsupported wire type %d" % wire_type)
|
||||
yield field_number, wire_type, value
|
||||
|
||||
|
||||
def decode_message(schema, buf):
|
||||
"""Decode into {name: value}. Scalars keep the last occurrence; repeated
|
||||
messages accumulate into lists; nested messages are decoded recursively.
|
||||
Unknown fields are kept under name "<field:N>" as raw bytes/int."""
|
||||
out = {}
|
||||
for field_number, wire_type, value in decode_raw_fields(buf):
|
||||
entry = schema.get(field_number)
|
||||
if entry is None:
|
||||
name = "<field:%d>" % field_number
|
||||
out[name] = value
|
||||
continue
|
||||
name, kind = entry
|
||||
if isinstance(kind, tuple) and kind[0] == "repeated":
|
||||
_, inner = kind
|
||||
if isinstance(inner, tuple):
|
||||
item = decode_message(inner[1], value)
|
||||
elif inner == "string":
|
||||
item = value.decode("utf-8", "replace")
|
||||
else:
|
||||
item = value
|
||||
out.setdefault(name, []).append(item)
|
||||
elif isinstance(kind, tuple) and kind[0] == "message":
|
||||
out[name] = decode_message(kind[1], value)
|
||||
elif kind == "string":
|
||||
out[name] = value.decode("utf-8", "replace") if isinstance(value, (bytes, bytearray)) else value
|
||||
elif kind == "bool":
|
||||
out[name] = bool(value)
|
||||
else: # varint
|
||||
out[name] = value
|
||||
return out
|
||||
@@ -1,394 +0,0 @@
|
||||
"""shadNet wire protocol: 15-byte packet framing + message schemas.
|
||||
|
||||
Sources (read from the actual code, not the stale docs):
|
||||
- Packet layout: shadnet-p2p-main/documentation/protocol.md
|
||||
- Command IDs: shadnet-p2p-main/src/protocol.h (== the port's client.h;
|
||||
matching commands live at 100-115, NOT 12-23)
|
||||
- Message fields: the port's gpu/shadps4/shadnet/shadnet.proto (subset used
|
||||
by the client) cross-checked against shadnet-p2p's fuller proto.
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
import proto3
|
||||
|
||||
HEADER_SIZE = 15
|
||||
MAX_PACKET_SIZE = 0x800000
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
PACKET_REQUEST = 0
|
||||
PACKET_REPLY = 1
|
||||
PACKET_NOTIFICATION = 2
|
||||
PACKET_SERVER_INFO = 3
|
||||
|
||||
|
||||
class Command:
|
||||
Login = 0
|
||||
Terminate = 1
|
||||
Create = 2
|
||||
Delete = 3
|
||||
AddFriend = 8
|
||||
RemoveFriend = 9
|
||||
AddBlock = 10
|
||||
RemoveBlock = 11
|
||||
GetServerFeatures = 12
|
||||
GetToken = 39
|
||||
SetAppearOffline = 40
|
||||
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
|
||||
GetBoardInfos = 30
|
||||
RecordScore = 31
|
||||
RecordScoreData = 32
|
||||
GetScoreData = 33
|
||||
GetScoreRange = 34
|
||||
GetScoreFriends = 35
|
||||
GetScoreNpid = 36
|
||||
|
||||
|
||||
class Error:
|
||||
NoError = 0
|
||||
Malformed = 1
|
||||
Invalid = 2
|
||||
InvalidInput = 3
|
||||
TooSoon = 4
|
||||
LoginError = 5
|
||||
LoginAlreadyLoggedIn = 6
|
||||
LoginInvalidPassword = 8
|
||||
LoginInvalidToken = 9
|
||||
RoomMissing = 14
|
||||
RoomAlreadyJoined = 15
|
||||
RoomFull = 16
|
||||
Unauthorized = 23
|
||||
NotFound = 26
|
||||
Unsupported = 33
|
||||
|
||||
|
||||
class Notify:
|
||||
FriendQuery = 5
|
||||
FriendNew = 6
|
||||
FriendLost = 7
|
||||
FriendStatus = 8
|
||||
RoomEvent = 10
|
||||
RoomMessage = 11
|
||||
|
||||
|
||||
# Orbis room event ids (bbhost np_matching2.cpp + shadnet-p2p matching.md)
|
||||
class RoomEvent:
|
||||
MemberJoined = 0x1101
|
||||
MemberLeft = 0x1102
|
||||
KickedOut = 0x1103
|
||||
RoomDestroyed = 0x1104
|
||||
RoomDataUpdatedInternal = 0x1106
|
||||
|
||||
|
||||
class EventCause:
|
||||
Leave = 1
|
||||
Kickout = 2
|
||||
ServerOperation = 4
|
||||
MemberDisappeared = 5
|
||||
|
||||
|
||||
# ---- protobuf message schemas (field number -> (name, kind)) ----------------
|
||||
|
||||
MATCHING_BIN_ATTR = {
|
||||
1: ("attr_id", "varint"),
|
||||
2: ("data", "bytes"),
|
||||
3: ("update_date", "varint"),
|
||||
4: ("update_member_id", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_INT_ATTR = {
|
||||
1: ("attr_id", "varint"),
|
||||
2: ("attr_value", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_GROUP = {
|
||||
1: ("group_id", "varint"),
|
||||
2: ("has_passwd", "bool"),
|
||||
3: ("has_label", "bool"),
|
||||
4: ("label", "bytes"),
|
||||
5: ("slot_count", "varint"),
|
||||
6: ("num_members", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_MEMBER_DATA = {
|
||||
1: ("npid", "string"),
|
||||
2: ("member_id", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("is_owner", "bool"),
|
||||
5: ("group", ("message", MATCHING_ROOM_GROUP)),
|
||||
6: ("bin_attrs_internal", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
7: ("join_date", "varint"),
|
||||
8: ("nat_type", "varint"),
|
||||
9: ("flag_attr", "varint"),
|
||||
10: ("addr", "string"),
|
||||
11: ("port", "varint"),
|
||||
12: ("group_id", "varint"),
|
||||
13: ("account_id", "varint"),
|
||||
14: ("platform", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_DATA_INTERNAL = {
|
||||
1: ("public_slots", "varint"),
|
||||
2: ("private_slots", "varint"),
|
||||
3: ("open_public_slots", "varint"),
|
||||
4: ("open_private_slots", "varint"),
|
||||
5: ("max_slot", "varint"),
|
||||
6: ("server_id", "varint"),
|
||||
7: ("world_id", "varint"),
|
||||
8: ("lobby_id", "varint"),
|
||||
9: ("room_id", "varint"),
|
||||
10: ("passwd_slot_mask", "varint"),
|
||||
11: ("joined_slot_mask", "varint"),
|
||||
12: ("groups", ("repeated", ("message", MATCHING_ROOM_GROUP))),
|
||||
13: ("flags", "varint"),
|
||||
14: ("bin_attrs_internal", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
}
|
||||
|
||||
CREATE_JOIN_ROOM_RESPONSE = {
|
||||
1: ("room_data", ("message", MATCHING_ROOM_DATA_INTERNAL)),
|
||||
2: ("members", ("repeated", ("message", MATCHING_ROOM_MEMBER_DATA))),
|
||||
3: ("me_member_id", "varint"),
|
||||
4: ("owner_member_id", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_WORLD = {
|
||||
1: ("world_id", "varint"),
|
||||
2: ("lobbies_num", "varint"),
|
||||
3: ("max_lobby_members", "varint"),
|
||||
4: ("lobby_members_num", "varint"),
|
||||
5: ("rooms_num", "varint"),
|
||||
6: ("room_members_num", "varint"),
|
||||
}
|
||||
|
||||
LOGIN_REQUEST = {
|
||||
1: ("npid", "string"),
|
||||
2: ("password", "string"),
|
||||
3: ("token", "string"),
|
||||
4: ("title_id", "string"),
|
||||
5: ("title_name", "string"),
|
||||
6: ("appear_offline", "bool"),
|
||||
}
|
||||
|
||||
LOGIN_REPLY = {
|
||||
1: ("avatar_url", "string"),
|
||||
2: ("user_id", "varint"),
|
||||
3: ("friends", ("repeated", ("message", {
|
||||
1: ("npid", "string"),
|
||||
2: ("online", "bool"),
|
||||
3: ("presence", "bytes"),
|
||||
}))),
|
||||
4: ("friend_requests_sent", ("repeated", "string")),
|
||||
5: ("friend_requests_received", ("repeated", "string")),
|
||||
6: ("blocked", ("repeated", "string")),
|
||||
}
|
||||
|
||||
SERVER_FEATURES_REPLY = {1: ("matching2_enabled", "bool")}
|
||||
|
||||
GET_TOKEN_REPLY = {
|
||||
1: ("token", "string"),
|
||||
2: ("user_id", "varint"),
|
||||
3: ("npid", "string"),
|
||||
}
|
||||
|
||||
CONTEXT_START_REQUEST = {1: ("ctx_id", "varint")}
|
||||
CONTEXT_STOP_REQUEST = {1: ("ctx_id", "varint")}
|
||||
|
||||
CREATE_ROOM_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("max_slots", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("world_id", "varint"),
|
||||
5: ("lobby_id", "varint"),
|
||||
6: ("flags", "varint"),
|
||||
7: ("group_config_count", "varint"),
|
||||
8: ("allowed_user_count", "varint"),
|
||||
9: ("blocked_user_count", "varint"),
|
||||
10: ("internal_bin_attr_count", "varint"),
|
||||
11: ("external_search_int_attrs", ("repeated", ("message", MATCHING_INT_ATTR))),
|
||||
12: ("external_search_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
13: ("external_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
14: ("member_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
15: ("join_group_label_present", "bool"),
|
||||
16: ("room_password_present", "bool"),
|
||||
17: ("sig_type", "varint"),
|
||||
18: ("sig_flag", "varint"),
|
||||
19: ("sig_main_member", "varint"),
|
||||
20: ("internal_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
21: ("user_id_kind", "varint"),
|
||||
22: ("allowed_online_ids", ("repeated", "string")),
|
||||
23: ("blocked_online_ids", ("repeated", "string")),
|
||||
24: ("allowed_account_ids", ("repeated", "varint")),
|
||||
25: ("blocked_account_ids", ("repeated", "varint")),
|
||||
26: ("group_configs", ("repeated", ("message", {
|
||||
1: ("slot_count", "varint"),
|
||||
2: ("has_label", "bool"),
|
||||
3: ("label", "bytes"),
|
||||
4: ("has_passwd", "bool"),
|
||||
}))),
|
||||
27: ("room_password", "bytes"),
|
||||
28: ("has_passwd_slot_mask", "bool"),
|
||||
29: ("passwd_slot_mask", "varint"),
|
||||
30: ("join_group_label", "bytes"),
|
||||
}
|
||||
|
||||
CREATE_ROOM_REPLY = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("server_id", "varint"),
|
||||
3: ("world_id", "varint"),
|
||||
4: ("lobby_id", "varint"),
|
||||
5: ("member_id", "varint"),
|
||||
6: ("max_slots", "varint"),
|
||||
7: ("flags", "varint"),
|
||||
8: ("cur_members", "varint"),
|
||||
9: ("details", ("message", CREATE_JOIN_ROOM_RESPONSE)),
|
||||
}
|
||||
|
||||
JOIN_ROOM_REQUEST = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("req_id", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("join_flags", "varint"),
|
||||
5: ("blocked_user_count", "varint"),
|
||||
6: ("member_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
7: ("room_password_present", "bool"),
|
||||
8: ("join_group_label_present", "bool"),
|
||||
9: ("user_id_kind", "varint"),
|
||||
10: ("blocked_online_ids", ("repeated", "string")),
|
||||
11: ("blocked_account_ids", ("repeated", "varint")),
|
||||
}
|
||||
|
||||
JOIN_ROOM_REPLY = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("member_id", "varint"),
|
||||
3: ("max_slots", "varint"),
|
||||
4: ("flags", "varint"),
|
||||
5: ("cur_members", "varint"),
|
||||
6: ("details", ("message", CREATE_JOIN_ROOM_RESPONSE)),
|
||||
}
|
||||
|
||||
LEAVE_ROOM_REQUEST = {1: ("room_id", "varint"), 2: ("req_id", "varint")}
|
||||
LEAVE_ROOM_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
REQUEST_SIGNALING_INFOS_REQUEST = {1: ("target_npid", "string")}
|
||||
REQUEST_SIGNALING_INFOS_REPLY = {
|
||||
1: ("target_npid", "string"),
|
||||
2: ("target_ip", "string"),
|
||||
3: ("target_port", "varint"),
|
||||
4: ("target_member_id", "varint"),
|
||||
}
|
||||
|
||||
SET_ROOM_DATA_INTERNAL_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("flag_filter", "varint"),
|
||||
4: ("flag_attr", "varint"),
|
||||
5: ("bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
6: ("has_passwd_mask", "bool"),
|
||||
7: ("passwd_slot_mask", "varint"),
|
||||
}
|
||||
SET_ROOM_DATA_INTERNAL_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
SET_ROOM_DATA_EXTERNAL_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("search_int_attrs", ("repeated", ("message", MATCHING_INT_ATTR))),
|
||||
4: ("search_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
5: ("ext_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
}
|
||||
SET_ROOM_DATA_EXTERNAL_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
KICKOUT_ROOM_MEMBER_REQUEST = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("req_id", "varint"),
|
||||
3: ("target_member_id", "varint"),
|
||||
4: ("block_kick_flag", "varint"),
|
||||
}
|
||||
KICKOUT_ROOM_MEMBER_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
SEARCH_ROOM_REPLY = {
|
||||
1: ("rooms", ("repeated", ("message", {
|
||||
1: ("max_slot", "varint"),
|
||||
2: ("cur_members", "varint"),
|
||||
3: ("flags", "varint"),
|
||||
4: ("server_id", "varint"),
|
||||
5: ("world_id", "varint"),
|
||||
6: ("lobby_id", "varint"),
|
||||
7: ("room_id", "varint"),
|
||||
}))),
|
||||
2: ("range_start", "varint"),
|
||||
3: ("range_total", "varint"),
|
||||
4: ("range_result", "varint"),
|
||||
}
|
||||
|
||||
GET_WORLD_INFO_LIST_REQUEST = {1: ("server_id", "varint")}
|
||||
GET_WORLD_INFO_LIST_REPLY = {1: ("worlds", ("repeated", ("message", MATCHING_WORLD)))}
|
||||
|
||||
FRIEND_COMMAND_REQUEST = {1: ("npid", "string")}
|
||||
|
||||
NOTIFY_ROOM_EVENT = {
|
||||
1: ("ctx_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("event", "varint"),
|
||||
4: ("event_cause", "varint"),
|
||||
5: ("error_code", "varint"),
|
||||
6: ("member", ("message", MATCHING_ROOM_MEMBER_DATA)),
|
||||
7: ("bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
8: ("flags", "varint"),
|
||||
9: ("has_passwd_mask", "bool"),
|
||||
10: ("passwd_slot_mask", "varint"),
|
||||
}
|
||||
|
||||
SET_APPEAR_OFFLINE_REQUEST = {1: ("appear_offline", "bool")}
|
||||
|
||||
|
||||
# ---- packet framing ---------------------------------------------------------
|
||||
|
||||
def build_packet(packet_type, command, packet_id, payload=b""):
|
||||
size = HEADER_SIZE + len(payload)
|
||||
return struct.pack("<BHIQ", packet_type, command, size, packet_id) + payload
|
||||
|
||||
|
||||
def build_server_info():
|
||||
return build_packet(PACKET_SERVER_INFO, 0, 0, struct.pack("<I", PROTOCOL_VERSION))
|
||||
|
||||
|
||||
def build_reply(command, packet_id, error, payload=b""):
|
||||
return build_packet(PACKET_REPLY, command, packet_id, bytes([error]) + payload)
|
||||
|
||||
|
||||
def build_notification(notification_type, payload):
|
||||
return build_packet(PACKET_NOTIFICATION, notification_type, 0, payload)
|
||||
|
||||
|
||||
def proto_blob(message_bytes):
|
||||
"""The u32-LE length prefix + message framing every proto payload uses."""
|
||||
return struct.pack("<I", len(message_bytes)) + message_bytes
|
||||
|
||||
|
||||
def parse_proto_blob(payload):
|
||||
if len(payload) < 4:
|
||||
return None
|
||||
(size,) = struct.unpack_from("<I", payload, 0)
|
||||
return payload[4:4 + size]
|
||||
|
||||
|
||||
def parse_header(buf):
|
||||
if len(buf) < HEADER_SIZE:
|
||||
return None
|
||||
packet_type, command, size, packet_id = struct.unpack_from("<BHIQ", buf, 0)
|
||||
return packet_type, command, size, packet_id
|
||||
@@ -1,763 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end tests for the C++ bridge (bin/bbbridge-cli.exe).
|
||||
|
||||
The same checks the Python bridge passed (its test_offline.py and test_p2p.py),
|
||||
run against the C++ build over real sockets: a fake bbhost server, a fake
|
||||
STUN/relay server, a shadNet client, fake port and peer UDP sockets. The
|
||||
protobuf / shadNet codecs used to read the bridge's answers are the Python
|
||||
bridge's own (proto3.py, shadnet_proto.py) - an independent implementation.
|
||||
|
||||
python tests/e2e/test_e2e.py [path\\to\\bbbridge-cli.exe] (Linux: the path to bbbridge-cli)
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import proto3 # noqa: E402
|
||||
import shadnet_proto as sp # noqa: E402
|
||||
|
||||
EXE = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "..", "bin", "bbbridge-cli.exe")
|
||||
HOST = "127.0.0.1"
|
||||
NPID, HOST_NPID = "tester01", "hostplayer"
|
||||
TIMEOUT = 5.0
|
||||
PASSED, FAILED = [], []
|
||||
|
||||
|
||||
def check(name, cond, detail=""):
|
||||
(PASSED if cond else FAILED).append((name, detail))
|
||||
print(" [%s] %s%s" % ("PASS" if cond else "FAIL", name, "" if cond else " " + str(detail)))
|
||||
|
||||
|
||||
def account_id_for(npid):
|
||||
d = hashlib.sha256(("bbbridge:" + npid).encode()).digest()
|
||||
return (1 << 52) | (int.from_bytes(d[:8], "big") & ((1 << 48) - 1))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- fake bbhost
|
||||
|
||||
class FakeBBHost(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def _json(self, obj, status=200):
|
||||
body = json.dumps(obj).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
n = int(self.headers.get("Content-Length", 0) or 0)
|
||||
try:
|
||||
body = json.loads(self.rfile.read(n).decode()) if n else {}
|
||||
except ValueError:
|
||||
body = {}
|
||||
path = self.path.split("?")[0]
|
||||
st = self.server.state
|
||||
with st["lock"]:
|
||||
st["calls"].append(path)
|
||||
emit = st["emit_event"]
|
||||
event = st.get("event")
|
||||
r = {"ResKind": 0}
|
||||
if path == "/auth/account/create":
|
||||
r.update(Token="tok-123", OnlineId=body.get("Name"), RecoveryCode="RC-42")
|
||||
elif path == "/auth/recover":
|
||||
r.update(Token="tok-456", OnlineId=body.get("Name"), RecoveryCode="RC-43")
|
||||
elif path == "/mp/matching2/create_room":
|
||||
r.update(RoomId=777, SessionId="sess-A", MemberId=1, OwnerMemberId=1, MaxMembers=5, Members=[])
|
||||
elif path == "/mp/matching2/join_room":
|
||||
r.update(SessionId="sess-B", MemberId=2, OwnerMemberId=1, MaxMembers=5, Members=[
|
||||
{"MemberId": 1, "OnlineId": HOST_NPID, "LocalAddr": "10.0.0.9", "LocalPort": 5551,
|
||||
"Addr": "9.9.9.8", "Port": 5552, "MappedAddr": "9.9.9.8", "MappedPort": 5552}])
|
||||
elif path == "/mp/matching2/heartbeat":
|
||||
r.update(InRoom=1)
|
||||
elif path == "/np/signaling/resolve":
|
||||
if self.headers.get("X-BB-Token") == "tok-expired":
|
||||
self._json({"ResKind": 1, "Message": "bad or expired token"}, 401)
|
||||
return
|
||||
r.update(Addr="9.9.9.9", Port=5555)
|
||||
elif path == "/auth/device/start":
|
||||
st["device_polls"] = 0
|
||||
r.update(DeviceCode="dev-1", UserCode="ABCD-EFGH", VerificationUri="http://example.test/link",
|
||||
Interval=1, ExpiresIn=60, Device=body.get("DeviceName"))
|
||||
elif path == "/auth/device/poll":
|
||||
st["device_polls"] = st.get("device_polls", 0) + 1
|
||||
if body.get("DeviceCode") == "dev-1" and st["device_polls"] >= 2:
|
||||
r.update(Status="approved", Token="tok-discord", OnlineId="discorduser")
|
||||
else:
|
||||
r.update(Status="pending")
|
||||
elif path == "/auth/web_code":
|
||||
if body.get("Token") != "tok-discord":
|
||||
self._json({"ResKind": 1, "Message": "unknown token"})
|
||||
return
|
||||
r.update(Code="WXYZ-1234")
|
||||
elif path == "/np/events/poll":
|
||||
r.update(NextCursor=body.get("Cursor", 0) + 1, HasEvent=bool(emit))
|
||||
if emit:
|
||||
r.update(event or {"Name": "room_member_joined", "MemberId": 2, "OnlineId": HOST_NPID,
|
||||
"RoomId": 777, "LocalAddr": "10.0.0.9", "LocalPort": 5551,
|
||||
"Addr": "9.9.9.8", "Port": 5552, "MappedAddr": "9.9.9.8",
|
||||
"MappedPort": 5552})
|
||||
elif path == "/summon_messenger/create":
|
||||
r.update(MessageId="SummonDataCreateResponse")
|
||||
st["summon_token"] = self.headers.get("X-BB-Token", "")
|
||||
elif path == "/gs/echo":
|
||||
r.update(Token=self.headers.get("X-BB-Token", ""), UA=self.headers.get("User-Agent", ""), Body=body)
|
||||
elif path not in ("/mp/matching2/context_start", "/mp/matching2/leave_room", "/auth/logout",
|
||||
"/mp/matching2/signaling_update", "/np/events/ack"):
|
||||
self._json({"ResKind": 1, "Message": "unknown path"}, 404)
|
||||
return
|
||||
self._json(r)
|
||||
|
||||
|
||||
class QuietServer(ThreadingHTTPServer):
|
||||
def handle_error(self, request, client_address):
|
||||
pass # a bridge we terminate drops its keep-alive connections; that is expected
|
||||
|
||||
|
||||
def fake_bbhost(port):
|
||||
srv = QuietServer((HOST, port), FakeBBHost)
|
||||
srv.state = {"lock": threading.Lock(), "calls": [], "emit_event": False, "event": None}
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
return srv
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- bridge process
|
||||
|
||||
class ExeBridge:
|
||||
def __init__(self, cfg):
|
||||
self.dir = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
self.cfg = dict(cfg, state_file=os.path.join(self.dir, "state.json"), debug_endpoints=True)
|
||||
path = os.path.join(self.dir, "config.json")
|
||||
with open(path, "w") as fh:
|
||||
json.dump(self.cfg, fh)
|
||||
self.log = os.path.join(self.dir, "bridge.log")
|
||||
self.proc = subprocess.Popen([EXE, "--config", path, "--log", self.log],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
urllib.request.urlopen("http://%s:%d/status" % (HOST, self.cfg["webapi_port"]), timeout=1).close()
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
raise RuntimeError("bridge did not start; log: %s" % self.log)
|
||||
|
||||
def _url(self, path):
|
||||
return "http://%s:%d%s" % (HOST, self.cfg["webapi_port"], path)
|
||||
|
||||
def debug(self):
|
||||
with urllib.request.urlopen(self._url("/debug/state"), timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def post(self, path, obj):
|
||||
req = urllib.request.Request(self._url(path), data=json.dumps(obj).encode(), method="POST",
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def peer(self, online_id):
|
||||
return next((p for p in self.debug()["peers"] if p["online_id"] == online_id), None)
|
||||
|
||||
def stop(self):
|
||||
self.proc.terminate()
|
||||
self.proc.wait(10)
|
||||
|
||||
|
||||
class ShadNetClient:
|
||||
def __init__(self, port):
|
||||
self.sock = socket.create_connection((HOST, port), timeout=TIMEOUT)
|
||||
self.next_id = 1
|
||||
self.queue = []
|
||||
ptype, _, _, payload = self._read()
|
||||
assert ptype == sp.PACKET_SERVER_INFO and struct.unpack("<I", payload)[0] == sp.PROTOCOL_VERSION
|
||||
|
||||
def _exact(self, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
c = self.sock.recv(n - len(buf))
|
||||
if not c:
|
||||
raise ConnectionError("closed")
|
||||
buf += c
|
||||
return buf
|
||||
|
||||
def _read(self):
|
||||
ptype, cmd, size, pid = sp.parse_header(self._exact(sp.HEADER_SIZE))
|
||||
return ptype, cmd, pid, self._exact(size - sp.HEADER_SIZE) if size > sp.HEADER_SIZE else b""
|
||||
|
||||
def request(self, command, msg=None):
|
||||
payload = sp.proto_blob(msg) if msg is not None else b""
|
||||
self.sock.sendall(sp.build_packet(sp.PACKET_REQUEST, command, self.next_id, payload))
|
||||
self.next_id += 1
|
||||
deadline = time.time() + TIMEOUT * 2
|
||||
while time.time() < deadline:
|
||||
ptype, cmd, _, raw = self._read()
|
||||
if ptype == sp.PACKET_REPLY and cmd == command:
|
||||
return (raw[0] if raw else 255), sp.parse_proto_blob(raw[1:])
|
||||
self.queue.append((ptype, cmd, raw))
|
||||
raise TimeoutError(command)
|
||||
|
||||
def notification(self, kind, timeout=3.0):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
for i, (ptype, cmd, raw) in enumerate(self.queue):
|
||||
if ptype == sp.PACKET_NOTIFICATION and cmd == kind:
|
||||
return self.queue.pop(i)[2]
|
||||
try:
|
||||
self.sock.settimeout(0.5)
|
||||
ptype, cmd, _, raw = self._read()
|
||||
except (socket.timeout, TimeoutError):
|
||||
continue
|
||||
finally:
|
||||
self.sock.settimeout(TIMEOUT)
|
||||
self.queue.append((ptype, cmd, raw))
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
|
||||
|
||||
def stun_ping(npid, udp_port):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(TIMEOUT)
|
||||
s.sendto(b"\x01" + npid.encode()[:16].ljust(16, b"\0") + socket.inet_aton(HOST), (HOST, udp_port))
|
||||
data, _ = s.recvfrom(64)
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return socket.inet_ntoa(data[:4]), struct.unpack(">H", data[4:6])[0], port
|
||||
|
||||
|
||||
def get_json(url):
|
||||
with urllib.request.urlopen(url, timeout=TIMEOUT) as r:
|
||||
return r.status, json.loads(r.read())
|
||||
|
||||
|
||||
def post_json(url, obj, headers=None):
|
||||
req = urllib.request.Request(url, data=json.dumps(obj).encode(), method="POST",
|
||||
headers=dict({"Content-Type": "application/json"}, **(headers or {})))
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- A: server path
|
||||
|
||||
def part_a():
|
||||
print("-- A: server path (gateway off)")
|
||||
ports = {"tcp": 42313, "udp": 42314, "webapi": 42315, "bbhost": 42671, "gs": 42316}
|
||||
fake = fake_bbhost(ports["bbhost"])
|
||||
up = "http://%s:%d" % (HOST, ports["bbhost"])
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "bind_host": HOST,
|
||||
"tcp_port": ports["tcp"], "udp_port": ports["udp"], "webapi_port": ports["webapi"],
|
||||
"p2p_gateway": False, "local_addr": HOST, "poll_interval": 0.05,
|
||||
"game_proxy_routes": {str(ports["gs"]): up}, "create_accounts": True})
|
||||
c = None
|
||||
try:
|
||||
c = ShadNetClient(ports["tcp"])
|
||||
err, blob = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID, 4: "CUSA03173"}))
|
||||
reply = proto3.decode_message(sp.LOGIN_REPLY, blob) if blob else {}
|
||||
check("login accepted with the per-player account id",
|
||||
err == 0 and reply.get("user_id") == account_id_for(NPID), (err, reply))
|
||||
check("bbhost account created", "/auth/account/create" in fake.state["calls"])
|
||||
err, blob = c.request(sp.Command.GetServerFeatures)
|
||||
check("matching2 enabled", err == 0 and proto3.decode_message(sp.SERVER_FEATURES_REPLY, blob).get("matching2_enabled") is True)
|
||||
err, blob = c.request(sp.Command.GetToken)
|
||||
tok = proto3.decode_message(sp.GET_TOKEN_REPLY, blob)
|
||||
check("token relayed", err == 0 and tok.get("token") == "tok-123" and tok.get("npid") == NPID, tok)
|
||||
|
||||
ip, port, local = stun_ping(NPID, ports["udp"])
|
||||
check("stun reply", ip == HOST and port == local, (ip, port, local))
|
||||
time.sleep(0.2)
|
||||
check("stun registry", br.debug()["registry"].get(NPID) == [HOST, local], br.debug()["registry"])
|
||||
|
||||
err, _ = c.request(sp.Command.ContextStart, proto3.encode_message(sp.CONTEXT_START_REQUEST, {1: 7}))
|
||||
check("context_start", err == 0 and "/mp/matching2/context_start" in fake.state["calls"], err)
|
||||
|
||||
err, blob = c.request(sp.Command.CreateRoom, proto3.encode_message(sp.CREATE_ROOM_REQUEST, {1: 1, 2: 5}))
|
||||
reply = proto3.decode_message(sp.CREATE_ROOM_REPLY, blob) if blob else {}
|
||||
check("create_room accepted", err == 0 and reply.get("room_id") == 777 and reply.get("member_id") == 1
|
||||
and reply.get("max_slots") == 5, (err, reply))
|
||||
details = reply.get("details") or {}
|
||||
check("create_room details", details.get("me_member_id") == 1 and details.get("owner_member_id") == 1
|
||||
and (details.get("room_data") or {}).get("room_id") == 777, details)
|
||||
me = [m for m in details.get("members") or [] if m.get("member_id") == 1]
|
||||
check("create_room lists ourselves (port needs members.me/owner)",
|
||||
len(me) == 1 and me[0].get("npid") == NPID and me[0].get("is_owner") is True, details.get("members"))
|
||||
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = True
|
||||
raw = c.notification(sp.Notify.RoomEvent)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
ev = proto3.decode_message(sp.NOTIFY_ROOM_EVENT, sp.parse_proto_blob(raw)) if raw else {}
|
||||
check("room_member_joined notification", ev.get("event") == sp.RoomEvent.MemberJoined
|
||||
and ev.get("room_id") == 777 and (ev.get("member") or {}).get("npid") == HOST_NPID
|
||||
and (ev.get("member") or {}).get("member_id") == 2, ev)
|
||||
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: HOST_NPID}))
|
||||
sig = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
check("signaling resolve", err == 0 and sig.get("target_ip") == "9.9.9.9" and sig.get("target_port") == 5555, sig)
|
||||
|
||||
webapi = "http://%s:%d" % (HOST, ports["webapi"])
|
||||
check("webapi /status", get_json(webapi + "/status")[1].get("ok") is True)
|
||||
ok = True
|
||||
for name in ("friendList", "blockList"):
|
||||
status, body = get_json("%s/v1/users/%s/%s?fields=npId&limit=50" % (webapi, NPID, name))
|
||||
ok = ok and status == 200 and body.get(name) == [] and body.get("totalResults") == 0
|
||||
check("webapi friendList/blockList answer empty lists", ok)
|
||||
ad = {"SessionId": "sess-A", "UserId": 1, "AreaId": 3, "SummonData": "QUJD", "MatchingLevel": 44}
|
||||
r = post_json(webapi + "/summon_messenger/create", ad)
|
||||
check("summon create proxied", r.get("ResKind") == 0 and r.get("MessageId") == "SummonDataCreateResponse", r)
|
||||
check("summon proxy carries the account token", fake.state.get("summon_token") == "tok-123",
|
||||
fake.state.get("summon_token"))
|
||||
echo = post_json("http://%s:%d/gs/echo" % (HOST, ports["gs"]), {"x": 1}, {"User-Agent": "BloodborneGame"})
|
||||
check("game proxy forwards with token and own UA", echo.get("Token") == "tok-123" and echo.get("Body") == {"x": 1}
|
||||
and echo.get("UA", "").startswith("BloodborneGame bbbridge/"), echo)
|
||||
check("advertisement captured", (br.debug().get("last_advertisement") or {}).get("AreaId") == 3)
|
||||
|
||||
err, blob = c.request(sp.Command.LeaveRoom, proto3.encode_message(sp.LEAVE_ROOM_REQUEST, {1: 777, 2: 2}))
|
||||
check("leave_room accepted", err == 0 and proto3.decode_message(sp.LEAVE_ROOM_REPLY, blob).get("room_id") == 777
|
||||
and "/mp/matching2/leave_room" in fake.state["calls"])
|
||||
err, _ = c.request(sp.Command.Terminate)
|
||||
closed = False
|
||||
try:
|
||||
c.sock.settimeout(2)
|
||||
closed = not c.sock.recv(16)
|
||||
except OSError:
|
||||
closed = True
|
||||
check("terminate closes the connection", err == 0 and closed)
|
||||
|
||||
c2 = ShadNetClient(ports["tcp"])
|
||||
err, _ = c2.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID}))
|
||||
_, blob = c2.request(sp.Command.GetToken)
|
||||
c2.close()
|
||||
check("second login uses the stored token, no recover",
|
||||
err == 0 and fake.state["calls"].count("/auth/recover") == 0
|
||||
and proto3.decode_message(sp.GET_TOKEN_REPLY, blob).get("token") == "tok-123")
|
||||
finally:
|
||||
if c:
|
||||
c.close()
|
||||
br.stop()
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- B: gateway
|
||||
|
||||
class FakeStun(threading.Thread):
|
||||
def __init__(self):
|
||||
super().__init__(daemon=True)
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.bind((HOST, 0))
|
||||
self.sock.settimeout(0.2)
|
||||
self.port = self.sock.getsockname()[1]
|
||||
self.relay_port = 41000
|
||||
self.token = b"RELAYTOK"
|
||||
self.requests, self.relayed = [], []
|
||||
self.stop = False
|
||||
|
||||
def run(self):
|
||||
while not self.stop:
|
||||
try:
|
||||
data, src = self.sock.recvfrom(65536)
|
||||
except (socket.timeout, OSError):
|
||||
continue
|
||||
if data[:2] == b"\xfbR":
|
||||
self.relayed.append(data)
|
||||
continue
|
||||
if len(data) < 20 or struct.unpack(">H", data[:2])[0] != 1:
|
||||
continue
|
||||
self.requests.append(data)
|
||||
attrs = struct.pack(">HHBBH4s", 0x0001, 8, 0, 1, 5678, socket.inet_aton("1.2.3.4"))
|
||||
if self.relay_port and len(data) >= 36 and data[20:22] == b"\x81\x00":
|
||||
v = b"bbr1" + self.token + struct.pack(">H", self.relay_port) + socket.inet_aton(src[0]) + struct.pack(">H", src[1])
|
||||
attrs += struct.pack(">HH", 0x8101, len(v)) + v
|
||||
self.sock.sendto(struct.pack(">HH", 0x0101, len(attrs)) + data[4:20] + attrs, src)
|
||||
|
||||
|
||||
def udp(timeout=2.0):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.bind((HOST, 0))
|
||||
s.settimeout(timeout)
|
||||
return s
|
||||
|
||||
|
||||
def recv_until(sock, pred, timeout=2.0):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
sock.settimeout(max(0.05, deadline - time.time()))
|
||||
try:
|
||||
data, src = sock.recvfrom(65536)
|
||||
except socket.timeout:
|
||||
return None
|
||||
if pred(data):
|
||||
return data, src
|
||||
return None
|
||||
|
||||
|
||||
def handshake(kind, frm, to, from_conn=7, nonce=0):
|
||||
return b"SHAD" + bytes([0x07, kind]) + frm.encode().ljust(16, b"\0") + to.encode().ljust(16, b"\0") + \
|
||||
struct.pack("<IIIHHQ", from_conn, 0, 0, 0, 0, nonce)
|
||||
|
||||
|
||||
def m2(kind, room, frm_member, to_member, frm_id, nonce=0):
|
||||
return b"SHAD" + bytes([0x21, kind]) + struct.pack("<QHH", room, frm_member, to_member) + \
|
||||
frm_id.encode().ljust(16, b"\0") + struct.pack("<IHHQ", 0, 0, 0, nonce)
|
||||
|
||||
|
||||
def part_b():
|
||||
print("-- B: gateway over real sockets")
|
||||
stun = FakeStun()
|
||||
stun.start()
|
||||
ports = {"tcp": 43313, "udp": 43314, "webapi": 43315}
|
||||
br = ExeBridge({"bbhost_server": "http://%s:1" % HOST, "bind_host": HOST, "tcp_port": ports["tcp"],
|
||||
"udp_port": ports["udp"], "webapi_port": ports["webapi"], "local_addr": HOST,
|
||||
"stun_server": "%s:%d" % (HOST, stun.port), "stun_refresh": 0.3, "game_proxy_routes": {}})
|
||||
port_sock, peer_sock = udp(), udp()
|
||||
try:
|
||||
deadline = time.time() + 4
|
||||
while time.time() < deadline and not (br.debug().get("mapped") and br.debug().get("relay_port")):
|
||||
time.sleep(0.1)
|
||||
st = br.debug()
|
||||
check("STUN request carries BBHOST-HELLO", bool(stun.requests) and stun.requests[0][20:24] == b"\x81\x00\x00\x0c")
|
||||
check("STUN mapping learned", st.get("mapped") == ["1.2.3.4", 5678], st.get("mapped"))
|
||||
check("relay turned on", st.get("relay_port") == 41000, st.get("relay_port"))
|
||||
|
||||
stun.relay_port = 0 # direct part: the next refresh turns the relay off
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and br.debug().get("relay_port"):
|
||||
time.sleep(0.1)
|
||||
inner = tuple(br.post("/debug/add_peer", {
|
||||
"member_id": 2, "online_id": "hunter", "endpoint": list(peer_sock.getsockname()),
|
||||
"port_online_id": "porthost", "port_member_id": 1, "room_id": 777,
|
||||
"port_endpoint": list(port_sock.getsockname())})["inner"])
|
||||
check("inner endpoint on loopback", inner[0] == HOST and inner[1] > 0, inner)
|
||||
opening = recv_until(port_sock, lambda d: d[4:9] == b"SHAD\x10")
|
||||
check("peer opens with ActivationRequest to the port",
|
||||
opening is not None and opening[0][9] == 1 and opening[1] == inner, opening)
|
||||
|
||||
port_sock.sendto(struct.pack(">HH", 40, 30) + b"hello-peer", inner)
|
||||
got = recv_until(peer_sock, lambda d: d[:1] == b"\xff")
|
||||
check("port game data reaches the peer in bbhost framing",
|
||||
got is not None and got[0] == b"\xff\xc3\x28\x1ehello-peer", got)
|
||||
outer_port = got[1][1] if got else 0
|
||||
peer_sock.sendto(b"\xff\xc3\x1e\x28hello-port", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d.endswith(b"hello-port"))
|
||||
check("peer game data reaches the port in port framing",
|
||||
got is not None and got[0] == struct.pack(">HH", 30, 40) + b"hello-port" and got[1] == inner, got)
|
||||
peer_sock.sendto(b"\xfebbhp\0\0\0", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d[:4] != b"\xff\xff\xff\xff", timeout=0.4)
|
||||
check("hole-punch probe not delivered to the port", got is None, got)
|
||||
|
||||
sig = struct.pack(">HH", 0xFFFF, 0xFFFF)
|
||||
port_sock.sendto(sig + handshake(1, "porthost", "hunter"), inner)
|
||||
check("port's handshake Offer answered with Accept",
|
||||
recv_until(port_sock, lambda d: d[4:10] == b"SHAD\x07\x02") is not None)
|
||||
port_sock.sendto(sig + m2(3, 777, 1, 2, "porthost", nonce=42), inner)
|
||||
got = recv_until(port_sock, lambda d: d[4:10] == b"SHAD\x21\x04")
|
||||
check("port's matching2 Check answered with CheckAck",
|
||||
got is not None and struct.unpack("<Q", got[0][4 + 42:4 + 50])[0] == 42)
|
||||
|
||||
stun.relay_port = 41000
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and not br.debug().get("relay_port"):
|
||||
time.sleep(0.1)
|
||||
inner3 = tuple(br.post("/debug/add_peer", {
|
||||
"member_id": 3, "online_id": "relayed", "endpoint": [HOST, 41001], "port_online_id": "porthost",
|
||||
"port_member_id": 1, "room_id": 777, "port_endpoint": list(port_sock.getsockname())})["inner"])
|
||||
port_sock.sendto(struct.pack(">HH", 40, 30) + b"via-relay", inner3)
|
||||
deadline = time.time() + 2
|
||||
while time.time() < deadline and not stun.relayed:
|
||||
time.sleep(0.05)
|
||||
expect = b"\xfbR" + stun.token + struct.pack(">H", 41001) + b"\xff\xc3\x28\x1evia-relay"
|
||||
check("datagram for a relayed peer goes framed to the STUN port", stun.relayed[:1] == [expect], stun.relayed[:1])
|
||||
stun.sock.sendto(b"\xfbr" + struct.pack(">H", 41001) + b"\xff\xc3\x1e\x28back", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d.endswith(b"back"))
|
||||
check("relay delivery reaches the port from the right member", got is not None and got[1] == inner3, got)
|
||||
|
||||
br.post("/debug/remove_peer", {"member_id": 2})
|
||||
check("removed peer forgotten", br.peer("hunter") is None)
|
||||
finally:
|
||||
stun.stop = True
|
||||
br.stop()
|
||||
port_sock.close()
|
||||
peer_sock.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- C: bridge + gateway
|
||||
|
||||
def part_c():
|
||||
print("-- C: bridge flows with the gateway on")
|
||||
ports = {"tcp": 44313, "udp": 44314, "webapi": 44315, "bbhost": 44671}
|
||||
fake = fake_bbhost(ports["bbhost"])
|
||||
up = "http://%s:%d" % (HOST, ports["bbhost"])
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "bind_host": HOST, "tcp_port": ports["tcp"],
|
||||
"udp_port": ports["udp"], "webapi_port": ports["webapi"], "local_addr": HOST,
|
||||
"poll_interval": 0.05, "stun_server": "off", "game_proxy_routes": {}, "create_accounts": True})
|
||||
c = None
|
||||
try:
|
||||
c = ShadNetClient(ports["tcp"])
|
||||
c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID}))
|
||||
stun_ping(NPID, ports["udp"])
|
||||
c.request(sp.Command.ContextStart, proto3.encode_message(sp.CONTEXT_START_REQUEST, {1: 7}))
|
||||
with fake.state["lock"]:
|
||||
fake.state["event"] = {"Name": "guest_invite", "RoomId": 777, "HostOnlineId": HOST_NPID,
|
||||
"HostAddr": "9.9.9.8", "HostPort": 5552, "HostLocalAddr": "10.0.0.9",
|
||||
"HostLocalPort": 5551, "HostMappedAddr": "9.9.9.8", "HostMappedPort": 5552,
|
||||
"HostArea": 3, "HostLevel": 26}
|
||||
fake.state["emit_event"] = True
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and br.peer(HOST_NPID) is None:
|
||||
time.sleep(0.05)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
fake.state["event"] = None
|
||||
seeded = br.peer(HOST_NPID)
|
||||
check("guest_invite seeds the host in the gateway", seeded is not None)
|
||||
|
||||
err, blob = c.request(sp.Command.JoinRoom, proto3.encode_message(sp.JOIN_ROOM_REQUEST, {1: 777, 2: 1}))
|
||||
reply = proto3.decode_message(sp.JOIN_ROOM_REPLY, blob) if blob else {}
|
||||
host = next((m for m in (reply.get("details") or {}).get("members") or [] if m.get("npid") == HOST_NPID), {})
|
||||
link = br.peer(HOST_NPID)
|
||||
check("joining the invited room keeps the host's inner endpoint",
|
||||
seeded is not None and link is not None and link["inner"] == seeded["inner"], (seeded, link))
|
||||
check("the join fills in the port's member id for the handshakes",
|
||||
link is not None and link["port_member_id"] == 2 and link["room_id"] == 777, link)
|
||||
check("joined room member points at the gateway",
|
||||
err == 0 and link is not None and host.get("addr") == HOST and host.get("port") == link["inner"][1], host)
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: HOST_NPID}))
|
||||
sig = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
check("signaling infos point at the gateway",
|
||||
err == 0 and link is not None and sig.get("target_ip") == HOST and sig.get("target_port") == link["inner"][1], sig)
|
||||
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: "summonedguest"}))
|
||||
sig2 = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
early = br.peer("summonedguest")
|
||||
check("signaling infos for a non-member resolve through the server",
|
||||
err == 0 and early is not None and sig2.get("target_port") == early["inner"][1], (sig2, early))
|
||||
with fake.state["lock"]:
|
||||
fake.state["event"] = {"Name": "room_member_joined", "MemberId": 3, "RoomId": 777,
|
||||
"OnlineId": "summonedguest", "Addr": "9.9.9.9", "Port": 5555}
|
||||
fake.state["emit_event"] = True
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and (br.peer("summonedguest") or {}).get("member_id") != 3:
|
||||
time.sleep(0.05)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
fake.state["event"] = None
|
||||
joined = br.peer("summonedguest")
|
||||
check("the guest joining keeps the inner endpoint the port already uses",
|
||||
joined is not None and early is not None and joined["member_id"] == 3 and joined["inner"] == early["inner"]
|
||||
and joined["peer_member_id"] == 3 and all(p["member_id"] > 0 for p in br.debug()["peers"]), joined)
|
||||
|
||||
time.sleep(5.6)
|
||||
check("heartbeat inside the server's 15 s window", fake.state["calls"].count("/mp/matching2/heartbeat") >= 1)
|
||||
c.request(sp.Command.LeaveRoom, proto3.encode_message(sp.LEAVE_ROOM_REQUEST, {1: 777}))
|
||||
check("leaving the room drops the peers", br.debug()["peers"] == [], br.debug()["peers"])
|
||||
finally:
|
||||
if c:
|
||||
c.close()
|
||||
br.stop()
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- D: Discord sign-in
|
||||
|
||||
def part_d():
|
||||
print("-- D: sign in with Discord, website code, expired token")
|
||||
port = 45671
|
||||
fake = fake_bbhost(port)
|
||||
up = "http://%s:%d" % (HOST, port)
|
||||
d = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
state = os.path.join(d, "state.json")
|
||||
cfg_path = os.path.join(d, "config.json")
|
||||
with open(cfg_path, "w") as fh:
|
||||
json.dump({"auth_server": up, "bbhost_server": up, "state_file": state}, fh)
|
||||
try:
|
||||
r = subprocess.run([EXE, "--config", cfg_path, "--link-discord"], capture_output=True, text=True, timeout=30)
|
||||
check("device code shown to the player", "ABCD-EFGH" in r.stdout and "http://example.test/link" in r.stdout,
|
||||
r.stdout)
|
||||
check("approval signs in", r.returncode == 0 and "signed in as discorduser" in r.stdout, r.stdout + r.stderr)
|
||||
with open(state) as fh:
|
||||
acc = json.load(fh)["accounts"].get("discorduser", {})
|
||||
check("Discord account stored under its name",
|
||||
acc.get("token") == "tok-discord" and acc.get("linked") == "discord" and not acc.get("recovery_code"), acc)
|
||||
r = subprocess.run([EXE, "--config", cfg_path, "--web-code", "discorduser"], capture_output=True, text=True,
|
||||
timeout=30)
|
||||
check("website sign-in code", r.returncode == 0 and r.stdout.strip() == "WXYZ-1234", r.stdout + r.stderr)
|
||||
|
||||
def cli(*args):
|
||||
return subprocess.run([EXE, "--config", cfg_path, *args], capture_output=True, text=True, timeout=30)
|
||||
|
||||
def accounts():
|
||||
with open(state) as fh:
|
||||
return json.load(fh)["accounts"]
|
||||
|
||||
r = cli("--create", "pcplayer")
|
||||
check("create account on this PC", r.returncode == 0 and accounts().get("pcplayer", {}).get("recovery_code") == "RC-42"
|
||||
and accounts()["pcplayer"].get("token") == "tok-123", r.stdout + str(accounts().get("pcplayer")))
|
||||
r = cli("--recover", "pcplayer", "RC-42")
|
||||
check("recover account keeps the server's new code", r.returncode == 0
|
||||
and accounts()["pcplayer"].get("recovery_code") == "RC-43" and accounts()["pcplayer"].get("token") == "tok-456",
|
||||
accounts().get("pcplayer"))
|
||||
r = cli("--sign-out", "pcplayer")
|
||||
check("sign out ends the token, keeps the recovery code", r.returncode == 0 and "/auth/logout" in fake.state["calls"]
|
||||
and "token" not in accounts()["pcplayer"] and accounts()["pcplayer"].get("recovery_code") == "RC-43",
|
||||
accounts().get("pcplayer"))
|
||||
r = cli("--sign-out", "discorduser")
|
||||
check("signing out of a Discord account forgets it here", r.returncode == 0 and "discorduser" not in accounts())
|
||||
acc = dict(acc) # the bridge part below uses the Discord account as linked earlier
|
||||
creates = fake.state["calls"].count("/auth/account/create") # the bridge must add none below
|
||||
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "auth_server": up, "bind_host": HOST,
|
||||
"tcp_port": 45313, "udp_port": 45314, "webapi_port": 45315, "p2p_gateway": False,
|
||||
"local_addr": HOST, "game_proxy_routes": {}})
|
||||
try:
|
||||
with open(br.cfg["state_file"], "w") as fh:
|
||||
json.dump({"accounts": {"discorduser": acc, "expired": {
|
||||
"bbhost_name": "expired", "token": "tok-expired", "online_id": "expired", "linked": "discord"}}}, fh)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "discorduser"}))
|
||||
_, blob = c.request(sp.Command.GetToken)
|
||||
c.close()
|
||||
check("the bridge plays as the Discord account",
|
||||
err == 0 and proto3.decode_message(sp.GET_TOKEN_REPLY, blob).get("token") == "tok-discord"
|
||||
and fake.state["calls"].count("/auth/account/create") == creates)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "expired"}))
|
||||
c.close()
|
||||
with open(br.log) as fh:
|
||||
said = "has expired" in fh.read()
|
||||
check("an expired Discord sign-in is refused, not replaced by a new account",
|
||||
err == sp.Error.LoginError and said and fake.state["calls"].count("/auth/account/create") == creates, err)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "stranger"}))
|
||||
c.close()
|
||||
with open(br.log) as fh:
|
||||
said = "use 'Sign in with Discord' in bbbridge first" in fh.read()
|
||||
check("no account on this PC: sign in with Discord first, nothing created",
|
||||
err == sp.Error.LoginError and said and fake.state["calls"].count("/auth/account/create") == creates, err)
|
||||
finally:
|
||||
br.stop()
|
||||
finally:
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- E: bbport setup
|
||||
|
||||
def part_e():
|
||||
print("-- E: routing a fresh bbport (sandboxed APPDATA)")
|
||||
tmp = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
appdata, bbport = os.path.join(tmp, "AppData"), os.path.join(tmp, "bloodborne")
|
||||
user = os.path.join(bbport, "user")
|
||||
os.makedirs(os.path.join(appdata, "bbport-launcher"))
|
||||
os.makedirs(user)
|
||||
open(os.path.join(bbport, "Bloodborne.exe"), "wb").close()
|
||||
mine = {"https://example.test:1": "http://127.0.0.1:1"}
|
||||
with open(os.path.join(user, "host_overrides.custom.json"), "w") as fh:
|
||||
json.dump(mine, fh)
|
||||
sfile = os.path.join(appdata, "bbport-launcher", "settings.json")
|
||||
fresh = {"ui_language": "", "game_dir": os.path.join(bbport, "CUSA03173"), "user_dir": "", "online": False,
|
||||
"online_server": "srv.shadps4.net:31313", "online_webapi": "", "online_npid": "", "online_password": "",
|
||||
"online_upnp": True, "extra_env": "BB_GPU_LOG=info SHADPS4_P2P_PORT=1"}
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(fresh, fh)
|
||||
env = dict(os.environ, APPDATA=appdata)
|
||||
|
||||
def route(on):
|
||||
return subprocess.run([EXE, "--route", "on" if on else "off", "--bbport", bbport], env=env,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
|
||||
def settings():
|
||||
with open(sfile) as fh:
|
||||
return json.load(fh)
|
||||
|
||||
r = route(True)
|
||||
s = settings()
|
||||
check("route on: bbport points at the bridge", r.returncode == 0 and "routed=1" in r.stdout and s["online"] is True
|
||||
and s["online_server"] == "127.0.0.1:31313" and s["online_webapi"] == "http://127.0.0.1:31315", r.stdout + str(s))
|
||||
check("route on: an empty password gets a placeholder (bbport skips online without one)",
|
||||
s["online_password"] == "bbbridge", s["online_password"])
|
||||
check("route on: extra variables merged, P2P port pinned once",
|
||||
s["extra_env"].split() == ["BB_GPU_LOG=info", "SHADPS4_P2P_PORT=3658"], s["extra_env"])
|
||||
check("route on: npid untouched, key order kept", s["online_npid"] == "" and list(s)[:4] == list(fresh)[:4], list(s))
|
||||
ov = json.load(open(os.path.join(user, "host_overrides.custom.json")))
|
||||
check("route on: overrides written, the player's own backed up",
|
||||
"_bbbridge" in ov and json.load(open(os.path.join(user, "host_overrides.custom.json.before-bbbridge"))) == mine)
|
||||
r = route(False)
|
||||
s = settings()
|
||||
check("route off: back to shadNet, password and variables restored",
|
||||
r.returncode == 0 and s["online_server"] == "srv.shadps4.net:31313" and s["online_webapi"] == ""
|
||||
and s["online_password"] == "" and s["extra_env"] == "BB_GPU_LOG=info", s)
|
||||
check("route off: the player's own overrides restored",
|
||||
json.load(open(os.path.join(user, "host_overrides.custom.json"))) == mine)
|
||||
s["online_password"] = "secret"
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(s, fh)
|
||||
route(True)
|
||||
on_pw = settings()["online_password"]
|
||||
route(False)
|
||||
check("a real shadNet password is left alone", on_pw == "secret" and settings()["online_password"] == "secret",
|
||||
(on_pw, settings()["online_password"]))
|
||||
|
||||
def set_settings(**kv):
|
||||
s = settings()
|
||||
s.update(kv)
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(s, fh)
|
||||
|
||||
# the Python bbbridge's file is ours too: replaced, then removed - not kept as the player's
|
||||
ovfile = os.path.join(user, "host_overrides.custom.json")
|
||||
with open(ovfile, "w") as fh:
|
||||
json.dump({"_note": "bbbridge: the game's own server traffic goes through the bridge",
|
||||
"https://thehuntersdream.com:20443": "http://127.0.0.1:31316"}, fh)
|
||||
route(True)
|
||||
route(False)
|
||||
check("route off: an old (Python) bbbridge overrides file is removed, not restored",
|
||||
not os.path.exists(ovfile) and not os.path.exists(ovfile + ".before-bbbridge"),
|
||||
os.listdir(user))
|
||||
|
||||
# the app puts the Hunter's Dream account in while routed; off brings the shadNet one back
|
||||
for name, pw in (("hunter_sn", "secret"), ("renamed_sn", "secret2")): # also when changed in between
|
||||
set_settings(online_npid=name, online_password=pw)
|
||||
route(True)
|
||||
set_settings(online_npid="discord_name")
|
||||
route(False)
|
||||
s = settings()
|
||||
check("route off: the shadNet account comes back (%s)" % name,
|
||||
s["online_npid"] == name and s["online_password"] == pw and s["online_server"] == "srv.shadps4.net:31313",
|
||||
(s["online_npid"], s["online_server"]))
|
||||
|
||||
|
||||
def main():
|
||||
print("== bbbridge C++ end-to-end (%s) ==" % os.path.abspath(EXE))
|
||||
part_a()
|
||||
part_b()
|
||||
part_c()
|
||||
part_d()
|
||||
if os.name == "nt": # routing bbport for Windows (setup.cpp); on Linux the bbport launcher does it
|
||||
part_e()
|
||||
print("\n%d passed, %d failed" % (len(PASSED), len(FAILED)))
|
||||
sys.exit(1 if FAILED else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,209 +0,0 @@
|
||||
// Byte-level unit tests: the same checks as the Python bridge's test_p2p.py
|
||||
// part 1, plus protobuf and the account id (values the Python bridge produced).
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "bridge.hpp"
|
||||
#include "p2p.hpp"
|
||||
#include "proto3.hpp"
|
||||
#ifdef _WIN32
|
||||
#include "setup.hpp"
|
||||
#endif
|
||||
#include "shadnet.hpp"
|
||||
|
||||
using namespace bb;
|
||||
using namespace bb::p2p;
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
|
||||
static void check(const char* name, bool ok, const std::string& detail = "") {
|
||||
if (ok) {
|
||||
++g_pass;
|
||||
std::printf(" [PASS] %s\n", name);
|
||||
} else {
|
||||
++g_fail;
|
||||
std::printf(" [FAIL] %s %s\n", name, detail.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static std::string hex(const std::string& s) {
|
||||
std::string out;
|
||||
for (unsigned char c : s) out += fmt("%02x", c);
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string be16s(uint16_t v) { return std::string{static_cast<char>(v >> 8), static_cast<char>(v & 0xFF)}; }
|
||||
|
||||
static std::string handshake(uint8_t kind, const std::string& from, const std::string& to, uint32_t from_conn,
|
||||
uint64_t nonce) {
|
||||
std::string p = "SHAD";
|
||||
p.push_back(static_cast<char>(T_HANDSHAKE));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
std::string f = from, t = to;
|
||||
f.resize(16, '\0');
|
||||
t.resize(16, '\0');
|
||||
p += f + t;
|
||||
for (int i = 0; i < 4; ++i) p.push_back(static_cast<char>((from_conn >> (8 * i)) & 0xFF));
|
||||
p += std::string(12, '\0');
|
||||
for (int i = 0; i < 8; ++i) p.push_back(static_cast<char>((nonce >> (8 * i)) & 0xFF));
|
||||
return p;
|
||||
}
|
||||
|
||||
static std::string m2(uint8_t kind, uint64_t room, uint16_t from_member, uint16_t to_member, const std::string& from,
|
||||
uint64_t nonce) {
|
||||
std::string p = "SHAD";
|
||||
p.push_back(static_cast<char>(T_M2));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
for (int i = 0; i < 8; ++i) p.push_back(static_cast<char>((room >> (8 * i)) & 0xFF));
|
||||
p.push_back(static_cast<char>(from_member & 0xFF));
|
||||
p.push_back(static_cast<char>(from_member >> 8));
|
||||
p.push_back(static_cast<char>(to_member & 0xFF));
|
||||
p.push_back(static_cast<char>(to_member >> 8));
|
||||
std::string f = from;
|
||||
f.resize(16, '\0');
|
||||
p += f + std::string(8, '\0');
|
||||
for (int i = 0; i < 8; ++i) p.push_back(static_cast<char>((nonce >> (8 * i)) & 0xFF));
|
||||
return p;
|
||||
}
|
||||
|
||||
static std::string control(uint8_t kind, const std::string& from, const std::string& to) {
|
||||
std::string p = "SHAD";
|
||||
p.push_back(static_cast<char>(T_CONTROL));
|
||||
p.push_back(static_cast<char>(kind));
|
||||
std::string f = from, t = to;
|
||||
f.resize(16, '\0');
|
||||
t.resize(16, '\0');
|
||||
p += f + t + std::string(12, '\0');
|
||||
return p;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static T le(const std::string& s, size_t at) {
|
||||
uint64_t v = 0;
|
||||
for (size_t i = 0; i < sizeof(T); ++i) v |= static_cast<uint64_t>(static_cast<uint8_t>(s[at + i])) << (8 * i);
|
||||
return static_cast<T>(v);
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::printf("== bbbridge unit ==\n");
|
||||
std::string out;
|
||||
|
||||
// framing
|
||||
check("port->bbhost one-byte vports",
|
||||
port_to_ps4(be16s(40) + be16s(30) + "game", out) == Frame::Game && out == std::string("\xff\xc3\x28\x1e") + "game",
|
||||
hex(out));
|
||||
check("bbhost->port one-byte vports",
|
||||
ps4_to_port(std::string("\xff\xc3\x1e\x28") + "data", out) && out == be16s(30) + be16s(40) + "data", hex(out));
|
||||
std::string two;
|
||||
port_to_ps4(be16s(3000) + be16s(40) + "x", two);
|
||||
check("two-byte vports round trip", ps4_to_port(two, out) && out == be16s(3000) + be16s(40) + "x", hex(two));
|
||||
check("comid flag skipped",
|
||||
ps4_to_port(std::string("\xff\xe3") + "ABCD" + "\x28\x1e" + "z", out) && out == be16s(40) + be16s(30) + "z");
|
||||
check("probe is not game data", !ps4_to_port(kProbe, out));
|
||||
check("port signaling channel detected",
|
||||
port_to_ps4(be16s(0xFFFF) + be16s(0xFFFF) + "SHAD\x05", out) == Frame::Signaling && out == "SHAD\x05");
|
||||
const std::string token = "T0K3N!!!";
|
||||
const std::string wrapped = relay_wrap(token, 41000, std::string("\xff\xc3\x28\x1e"));
|
||||
check("relay frame to server", wrapped.substr(0, 2) == "\xfbR" && wrapped.substr(2, 8) == token &&
|
||||
wrapped.substr(10, 2) == be16s(41000) && wrapped.size() == 16);
|
||||
int port = 0;
|
||||
check("relay frame from server",
|
||||
relay_unwrap(std::string("\xfbr") + be16s(41001) + "dg", port, out) && port == 41001 && out == "dg");
|
||||
|
||||
// STUN
|
||||
std::string txid;
|
||||
const std::string req = stun_request("", txid);
|
||||
check("stun request shape", req.substr(0, 4) == be16s(1) + be16s(16) && req.substr(4, 4) == "\x21\x12\xa4\x42" &&
|
||||
req.substr(20, 4) == std::string("\x81\x00\x00\x0c", 4) && req.substr(24, 4) == "bbr1");
|
||||
const uint32_t addr = (5u << 24) | (6u << 16) | (7u << 8) | 8u;
|
||||
const uint32_t xaddr = addr ^ ((static_cast<uint8_t>(txid[0]) << 24) | (static_cast<uint8_t>(txid[1]) << 16) |
|
||||
(static_cast<uint8_t>(txid[2]) << 8) | static_cast<uint8_t>(txid[3]));
|
||||
const uint16_t xport = static_cast<uint16_t>(4321 ^ ((static_cast<uint8_t>(txid[0]) << 8) | static_cast<uint8_t>(txid[1])));
|
||||
std::string attr = be16s(0x0020) + be16s(8) + std::string("\x00\x01", 2) + be16s(xport) + be16s(xaddr >> 16) +
|
||||
be16s(xaddr & 0xFFFF);
|
||||
const std::string resp = be16s(0x0101) + be16s(static_cast<uint16_t>(attr.size())) + txid + attr;
|
||||
StunAnswer a;
|
||||
check("stun xor-mapped parsed", stun_parse(resp, txid, a) && a.mapped.ip == "5.6.7.8" && a.mapped.port == 4321,
|
||||
a.mapped.str());
|
||||
check("stun wrong txid rejected", !stun_parse(resp, std::string(16, '\0'), a));
|
||||
|
||||
// signaling, answered for the bbhost peer
|
||||
PeerSignaling ps("hunter", 2, "porthost", 1, 777);
|
||||
auto r = ps.handle(handshake(HS_OFFER, "porthost", "hunter", 5, 0));
|
||||
check("Offer -> Accept from the peer",
|
||||
r.size() == 1 && r[0].size() == 62 && static_cast<uint8_t>(r[0][5]) == HS_ACCEPT &&
|
||||
r[0].compare(6, 6, "hunter") == 0 && r[0].compare(22, 8, "porthost") == 0 && le<uint32_t>(r[0], 42) == 5 &&
|
||||
le<uint32_t>(r[0], 46) == 0);
|
||||
r = ps.handle(handshake(HS_CHECK, "porthost", "hunter", 7, 123456));
|
||||
check("Check -> CheckAck echoes nonce",
|
||||
r.size() == 1 && static_cast<uint8_t>(r[0][5]) == HS_CHECK_ACK && le<uint64_t>(r[0], 54) == 123456);
|
||||
r = ps.handle(control(CTL_ACTIVATION_REQUEST, "porthost", "hunter"));
|
||||
check("ActivationRequest -> ActivationAck", r.size() == 1 && r[0].size() == 50 && static_cast<uint8_t>(r[0][5]) == CTL_ACTIVATION_ACK);
|
||||
const auto first = ps.handle(control(CTL_ESTABLISHED, "porthost", "hunter"));
|
||||
const auto second = ps.handle(control(CTL_ESTABLISHED, "porthost", "hunter"));
|
||||
check("Established answered once", first.size() == 1 && second.empty());
|
||||
r = ps.handle(m2(HS_OFFER, 777, 1, 2, "porthost", 0));
|
||||
check("matching2 Offer -> Accept, member ids swapped",
|
||||
r.size() == 1 && r[0].size() == 50 && static_cast<uint8_t>(r[0][5]) == HS_ACCEPT && le<uint64_t>(r[0], 6) == 777 &&
|
||||
le<uint16_t>(r[0], 14) == 2 && le<uint16_t>(r[0], 16) == 1 && r[0].compare(18, 6, "hunter") == 0);
|
||||
r = ps.handle(m2(HS_CHECK, 777, 1, 2, "porthost", 99));
|
||||
check("matching2 Check -> CheckAck", r.size() == 1 && static_cast<uint8_t>(r[0][5]) == HS_CHECK_ACK && le<uint64_t>(r[0], 42) == 99);
|
||||
PeerSignaling provisional("guest", -29999, "porthost", 1, 777);
|
||||
const auto opening = provisional.opening();
|
||||
check("a provisional (negative) key opens without matching2",
|
||||
opening.size() == 1 && static_cast<uint8_t>(opening[0][5]) == CTL_ACTIVATION_REQUEST &&
|
||||
provisional.conn_id == 0x4242);
|
||||
r = ps.handle(std::string("SHAD\x05", 5) + std::string("\x03\x00\x00\x00", 4) + std::string("\x2b\x02\x00\x00\x00\x00\x00\x00", 8));
|
||||
check("echo ping -> pong", r.size() == 1 && r[0].size() == 17 && static_cast<uint8_t>(r[0][4]) == T_ECHO_PONG &&
|
||||
le<uint32_t>(r[0], 5) == 3 && le<uint64_t>(r[0], 9) == 555);
|
||||
|
||||
// protobuf + shadNet framing
|
||||
pb::Writer w;
|
||||
w.s(1, "npid").u(2, 300).flag(4, true).u(5, 0).msg(6, "");
|
||||
pb::Message m(w.str());
|
||||
check("protobuf round trip", m.ok() && m.s(1) == "npid" && m.u(2) == 300 && m.u(4) == 1 && !m.has(5) && m.has(6));
|
||||
std::string blob;
|
||||
check("proto blob framing", shadnet::parse_proto_blob(shadnet::proto_blob("abc"), blob) && blob == "abc");
|
||||
shadnet::Header h;
|
||||
check("packet header", shadnet::parse_header(shadnet::build_reply(101, 7, 0, "xy"), h) && h.type == shadnet::Reply &&
|
||||
h.command == 101 && h.size == 18 && h.packet_id == 7);
|
||||
|
||||
// account id: the values the Python bridge gave these names
|
||||
check("account id matches the Python bridge (porthost)", account_id_for("porthost") == 4562636682122493ull,
|
||||
std::to_string(account_id_for("porthost")));
|
||||
check("account id matches the Python bridge (tester01)", account_id_for("tester01") == 4533768973479740ull,
|
||||
std::to_string(account_id_for("tester01")));
|
||||
|
||||
#ifdef _WIN32 // bbport for Windows' folder (setup.cpp); on Linux the launcher routes the game
|
||||
// where bbport is found: chosen > the folder bbbridge is in > the launcher's game folder
|
||||
{
|
||||
const fs::path root = fs::temp_directory_path() / L"bbbridge-unit-find";
|
||||
std::error_code ec;
|
||||
fs::remove_all(root, ec);
|
||||
const fs::path bbport_dir = root / L"bloodborne", other = root / L"other", games = root / L"games";
|
||||
for (const auto& d : {bbport_dir / L"bbbridge", other, games / L"CUSA03173"}) fs::create_directories(d);
|
||||
std::ofstream(bbport_dir / L"Bloodborne.exe") << "x";
|
||||
std::ofstream(games / L"Play Bloodborne.exe") << "x";
|
||||
const std::string game_dir = narrow((games / L"CUSA03173").wstring());
|
||||
BbportSource src{};
|
||||
check("bbport: the folder bbbridge is in",
|
||||
find_bbport(bbport_dir / L"bbbridge", {}, game_dir, &src) == bbport_dir && src == BbportSource::ExeFolder);
|
||||
check("bbport: bbbridge.exe right next to Bloodborne.exe",
|
||||
find_bbport(bbport_dir, {}, "", &src) == bbport_dir && src == BbportSource::ExeFolder);
|
||||
check("bbport: a chosen folder that is gone falls back",
|
||||
find_bbport(bbport_dir / L"bbbridge", other, game_dir, &src) == bbport_dir && src == BbportSource::ExeFolder);
|
||||
check("bbport: the chosen folder wins",
|
||||
find_bbport(bbport_dir / L"bbbridge", games, "", &src) == games && src == BbportSource::Chosen);
|
||||
check("bbport: from the launcher's game folder",
|
||||
find_bbport(other / L"bbbridge", {}, game_dir, &src) == games && src == BbportSource::Launcher);
|
||||
check("bbport: not found",
|
||||
find_bbport(other / L"bbbridge", {}, "", &src).empty() && src == BbportSource::None);
|
||||
fs::remove_all(root, ec);
|
||||
}
|
||||
#endif
|
||||
|
||||
std::printf("\n%d passed, %d failed\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// DEAR IMGUI COMPILE-TIME OPTIONS
|
||||
// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure.
|
||||
// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions.
|
||||
//-----------------------------------------------------------------------------
|
||||
// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it)
|
||||
// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template.
|
||||
//-----------------------------------------------------------------------------
|
||||
// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp
|
||||
// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures.
|
||||
// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts.
|
||||
// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#pragma once
|
||||
|
||||
//---- Define assertion handler. Defaults to calling assert().
|
||||
// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement.
|
||||
//#define IM_ASSERT(_EXPR) MyAssert(_EXPR)
|
||||
//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts
|
||||
|
||||
//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows
|
||||
// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility.
|
||||
// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions()
|
||||
// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details.
|
||||
//#define IMGUI_API __declspec(dllexport) // MSVC Windows: DLL export
|
||||
//#define IMGUI_API __declspec(dllimport) // MSVC Windows: DLL import
|
||||
//#define IMGUI_API __attribute__((visibility("default"))) // GCC/Clang: override visibility when set is hidden
|
||||
|
||||
//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names.
|
||||
//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
|
||||
//---- Disable all of Dear ImGui or don't implement standard windows/tools.
|
||||
// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp.
|
||||
//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty.
|
||||
//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty.
|
||||
//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty.
|
||||
|
||||
//---- Don't implement some functions to reduce linkage requirements.
|
||||
//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a)
|
||||
//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW)
|
||||
//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a)
|
||||
//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME).
|
||||
//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default).
|
||||
//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")).
|
||||
//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf)
|
||||
//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself.
|
||||
//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies)
|
||||
//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function.
|
||||
//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions().
|
||||
//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available
|
||||
|
||||
//---- Enable Test Engine / Automation features.
|
||||
//#define IMGUI_ENABLE_TEST_ENGINE // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details.
|
||||
|
||||
//---- Include imgui_user.h at the end of imgui.h as a convenience
|
||||
// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included.
|
||||
//#define IMGUI_INCLUDE_IMGUI_USER_H
|
||||
//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h"
|
||||
|
||||
//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another)
|
||||
//#define IMGUI_USE_BGRA_PACKED_COLOR
|
||||
|
||||
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
||||
//#define IMGUI_USE_WCHAR32
|
||||
|
||||
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
|
||||
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
|
||||
//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h"
|
||||
//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h"
|
||||
//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined.
|
||||
//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION
|
||||
//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION
|
||||
//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined.
|
||||
|
||||
//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined)
|
||||
// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h.
|
||||
//#define IMGUI_USE_STB_SPRINTF
|
||||
|
||||
//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui)
|
||||
// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided).
|
||||
// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'.
|
||||
//#define IMGUI_ENABLE_FREETYPE
|
||||
|
||||
//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT)
|
||||
// Only works in combination with IMGUI_ENABLE_FREETYPE.
|
||||
// - lunasvg is currently easier to acquire/install, as e.g. it is part of vcpkg.
|
||||
// - plutosvg will support more fonts and may load them faster. It currently requires to be built manually but it is fairly easy. See misc/freetype/README for instructions.
|
||||
// - Both require headers to be available in the include path + program to be linked with the library code (not provided).
|
||||
// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement)
|
||||
//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
//#define IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
//---- Use stb_truetype to build and rasterize the font atlas (default)
|
||||
// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend.
|
||||
//#define IMGUI_ENABLE_STB_TRUETYPE
|
||||
|
||||
//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4.
|
||||
// This will be inlined as part of ImVec2 and ImVec4 class declarations.
|
||||
/*
|
||||
#define IM_VEC2_CLASS_EXTRA \
|
||||
constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \
|
||||
operator MyVec2() const { return MyVec2(x,y); }
|
||||
|
||||
#define IM_VEC4_CLASS_EXTRA \
|
||||
constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \
|
||||
operator MyVec4() const { return MyVec4(x,y,z,w); }
|
||||
*/
|
||||
//---- ...Or use Dear ImGui's own very basic math operators.
|
||||
//#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
|
||||
//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices.
|
||||
// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices).
|
||||
// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer.
|
||||
// Read about ImGuiBackendFlags_RendererHasVtxOffset for details.
|
||||
//#define ImDrawIdx unsigned int
|
||||
|
||||
//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly)
|
||||
//struct ImDrawList;
|
||||
//struct ImDrawCmd;
|
||||
//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data);
|
||||
//#define ImDrawCallback MyImDrawCallback
|
||||
|
||||
//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase)
|
||||
// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.)
|
||||
//#define IM_DEBUG_BREAK IM_ASSERT(0)
|
||||
//#define IM_DEBUG_BREAK __debugbreak()
|
||||
|
||||
//---- Debug Tools: Enable slower asserts
|
||||
//#define IMGUI_DEBUG_PARANOID
|
||||
|
||||
//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files)
|
||||
/*
|
||||
namespace ImGui
|
||||
{
|
||||
void MyFunction(const char* name, MyMatrix44* mtx);
|
||||
}
|
||||
*/
|
||||
Vendored
-16789
File diff suppressed because it is too large.
Load diff
Vendored
-3714
File diff suppressed because it is too large.
Load diff
-4674
File diff suppressed because it is too large.
Load diff
-618
@@ -1,618 +0,0 @@
|
||||
// dear imgui: Renderer Backend for DirectX11
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as ImTextureID. Read the FAQ about ImTextureID!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) with 16-bit indices.
|
||||
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2024-10-07: DirectX11: Changed default texture sampler to Clamp instead of Repeat/Wrap.
|
||||
// 2024-10-07: DirectX11: Expose selected render state in ImGui_ImplDX11_RenderState, which you can access in 'void* platform_io.Renderer_RenderState' during draw callbacks.
|
||||
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
|
||||
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
|
||||
// 2021-05-19: DirectX11: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
|
||||
// 2021-02-18: DirectX11: Change blending equation to preserve alpha in output buffer.
|
||||
// 2019-08-01: DirectX11: Fixed code querying the Geometry Shader state (would generally error with Debug layer enabled).
|
||||
// 2019-07-21: DirectX11: Backup, clear and restore Geometry Shader is any is bound when calling ImGui_ImplDX11_RenderDrawData. Clearing Hull/Domain/Compute shaders without backup/restore.
|
||||
// 2019-05-29: DirectX11: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
|
||||
// 2019-04-30: DirectX11: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
|
||||
// 2018-12-03: Misc: Added #pragma comment statement to automatically link with d3dcompiler.lib when using D3DCompile().
|
||||
// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
|
||||
// 2018-08-01: DirectX11: Querying for IDXGIFactory instead of IDXGIFactory1 to increase compatibility.
|
||||
// 2018-07-13: DirectX11: Fixed unreleased resources in Init and Shutdown functions.
|
||||
// 2018-06-08: Misc: Extracted imgui_impl_dx11.cpp/.h away from the old combined DX11+Win32 example.
|
||||
// 2018-06-08: DirectX11: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle.
|
||||
// 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplDX11_RenderDrawData() in the .h file so you can call it yourself.
|
||||
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
|
||||
// 2016-05-07: DirectX11: Disabling depth-write.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_impl_dx11.h"
|
||||
|
||||
// DirectX
|
||||
#include <stdio.h>
|
||||
#include <d3d11.h>
|
||||
#include <d3dcompiler.h>
|
||||
#ifdef _MSC_VER
|
||||
#pragma comment(lib, "d3dcompiler") // Automatically link with d3dcompiler.lib as we are using D3DCompile() below.
|
||||
#endif
|
||||
|
||||
// DirectX11 data
|
||||
struct ImGui_ImplDX11_Data
|
||||
{
|
||||
ID3D11Device* pd3dDevice;
|
||||
ID3D11DeviceContext* pd3dDeviceContext;
|
||||
IDXGIFactory* pFactory;
|
||||
ID3D11Buffer* pVB;
|
||||
ID3D11Buffer* pIB;
|
||||
ID3D11VertexShader* pVertexShader;
|
||||
ID3D11InputLayout* pInputLayout;
|
||||
ID3D11Buffer* pVertexConstantBuffer;
|
||||
ID3D11PixelShader* pPixelShader;
|
||||
ID3D11SamplerState* pFontSampler;
|
||||
ID3D11ShaderResourceView* pFontTextureView;
|
||||
ID3D11RasterizerState* pRasterizerState;
|
||||
ID3D11BlendState* pBlendState;
|
||||
ID3D11DepthStencilState* pDepthStencilState;
|
||||
int VertexBufferSize;
|
||||
int IndexBufferSize;
|
||||
|
||||
ImGui_ImplDX11_Data() { memset((void*)this, 0, sizeof(*this)); VertexBufferSize = 5000; IndexBufferSize = 10000; }
|
||||
};
|
||||
|
||||
struct VERTEX_CONSTANT_BUFFER_DX11
|
||||
{
|
||||
float mvp[4][4];
|
||||
};
|
||||
|
||||
// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts
|
||||
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
|
||||
static ImGui_ImplDX11_Data* ImGui_ImplDX11_GetBackendData()
|
||||
{
|
||||
return ImGui::GetCurrentContext() ? (ImGui_ImplDX11_Data*)ImGui::GetIO().BackendRendererUserData : nullptr;
|
||||
}
|
||||
|
||||
// Functions
|
||||
static void ImGui_ImplDX11_SetupRenderState(ImDrawData* draw_data, ID3D11DeviceContext* device_ctx)
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
|
||||
// Setup viewport
|
||||
D3D11_VIEWPORT vp;
|
||||
memset(&vp, 0, sizeof(D3D11_VIEWPORT));
|
||||
vp.Width = draw_data->DisplaySize.x;
|
||||
vp.Height = draw_data->DisplaySize.y;
|
||||
vp.MinDepth = 0.0f;
|
||||
vp.MaxDepth = 1.0f;
|
||||
vp.TopLeftX = vp.TopLeftY = 0;
|
||||
device_ctx->RSSetViewports(1, &vp);
|
||||
|
||||
// Setup shader and vertex buffers
|
||||
unsigned int stride = sizeof(ImDrawVert);
|
||||
unsigned int offset = 0;
|
||||
device_ctx->IASetInputLayout(bd->pInputLayout);
|
||||
device_ctx->IASetVertexBuffers(0, 1, &bd->pVB, &stride, &offset);
|
||||
device_ctx->IASetIndexBuffer(bd->pIB, sizeof(ImDrawIdx) == 2 ? DXGI_FORMAT_R16_UINT : DXGI_FORMAT_R32_UINT, 0);
|
||||
device_ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||
device_ctx->VSSetShader(bd->pVertexShader, nullptr, 0);
|
||||
device_ctx->VSSetConstantBuffers(0, 1, &bd->pVertexConstantBuffer);
|
||||
device_ctx->PSSetShader(bd->pPixelShader, nullptr, 0);
|
||||
device_ctx->PSSetSamplers(0, 1, &bd->pFontSampler);
|
||||
device_ctx->GSSetShader(nullptr, nullptr, 0);
|
||||
device_ctx->HSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
device_ctx->DSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
device_ctx->CSSetShader(nullptr, nullptr, 0); // In theory we should backup and restore this as well.. very infrequently used..
|
||||
|
||||
// Setup blend state
|
||||
const float blend_factor[4] = { 0.f, 0.f, 0.f, 0.f };
|
||||
device_ctx->OMSetBlendState(bd->pBlendState, blend_factor, 0xffffffff);
|
||||
device_ctx->OMSetDepthStencilState(bd->pDepthStencilState, 0);
|
||||
device_ctx->RSSetState(bd->pRasterizerState);
|
||||
}
|
||||
|
||||
// Render function
|
||||
void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data)
|
||||
{
|
||||
// Avoid rendering when minimized
|
||||
if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f)
|
||||
return;
|
||||
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
ID3D11DeviceContext* device = bd->pd3dDeviceContext;
|
||||
|
||||
// Create and grow vertex/index buffers if needed
|
||||
if (!bd->pVB || bd->VertexBufferSize < draw_data->TotalVtxCount)
|
||||
{
|
||||
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
|
||||
bd->VertexBufferSize = draw_data->TotalVtxCount + 5000;
|
||||
D3D11_BUFFER_DESC desc;
|
||||
memset(&desc, 0, sizeof(D3D11_BUFFER_DESC));
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.ByteWidth = bd->VertexBufferSize * sizeof(ImDrawVert);
|
||||
desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
desc.MiscFlags = 0;
|
||||
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVB) < 0)
|
||||
return;
|
||||
}
|
||||
if (!bd->pIB || bd->IndexBufferSize < draw_data->TotalIdxCount)
|
||||
{
|
||||
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
|
||||
bd->IndexBufferSize = draw_data->TotalIdxCount + 10000;
|
||||
D3D11_BUFFER_DESC desc;
|
||||
memset(&desc, 0, sizeof(D3D11_BUFFER_DESC));
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.ByteWidth = bd->IndexBufferSize * sizeof(ImDrawIdx);
|
||||
desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
if (bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pIB) < 0)
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload vertex/index data into a single contiguous GPU buffer
|
||||
D3D11_MAPPED_SUBRESOURCE vtx_resource, idx_resource;
|
||||
if (device->Map(bd->pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &vtx_resource) != S_OK)
|
||||
return;
|
||||
if (device->Map(bd->pIB, 0, D3D11_MAP_WRITE_DISCARD, 0, &idx_resource) != S_OK)
|
||||
return;
|
||||
ImDrawVert* vtx_dst = (ImDrawVert*)vtx_resource.pData;
|
||||
ImDrawIdx* idx_dst = (ImDrawIdx*)idx_resource.pData;
|
||||
for (int n = 0; n < draw_data->CmdListsCount; n++)
|
||||
{
|
||||
const ImDrawList* draw_list = draw_data->CmdLists[n];
|
||||
memcpy(vtx_dst, draw_list->VtxBuffer.Data, draw_list->VtxBuffer.Size * sizeof(ImDrawVert));
|
||||
memcpy(idx_dst, draw_list->IdxBuffer.Data, draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
|
||||
vtx_dst += draw_list->VtxBuffer.Size;
|
||||
idx_dst += draw_list->IdxBuffer.Size;
|
||||
}
|
||||
device->Unmap(bd->pVB, 0);
|
||||
device->Unmap(bd->pIB, 0);
|
||||
|
||||
// Setup orthographic projection matrix into our constant buffer
|
||||
// Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps.
|
||||
{
|
||||
D3D11_MAPPED_SUBRESOURCE mapped_resource;
|
||||
if (device->Map(bd->pVertexConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource) != S_OK)
|
||||
return;
|
||||
VERTEX_CONSTANT_BUFFER_DX11* constant_buffer = (VERTEX_CONSTANT_BUFFER_DX11*)mapped_resource.pData;
|
||||
float L = draw_data->DisplayPos.x;
|
||||
float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
|
||||
float T = draw_data->DisplayPos.y;
|
||||
float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
|
||||
float mvp[4][4] =
|
||||
{
|
||||
{ 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
|
||||
{ 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
|
||||
{ 0.0f, 0.0f, 0.5f, 0.0f },
|
||||
{ (R+L)/(L-R), (T+B)/(B-T), 0.5f, 1.0f },
|
||||
};
|
||||
memcpy(&constant_buffer->mvp, mvp, sizeof(mvp));
|
||||
device->Unmap(bd->pVertexConstantBuffer, 0);
|
||||
}
|
||||
|
||||
// Backup DX state that will be modified to restore it afterwards (unfortunately this is very ugly looking and verbose. Close your eyes!)
|
||||
struct BACKUP_DX11_STATE
|
||||
{
|
||||
UINT ScissorRectsCount, ViewportsCount;
|
||||
D3D11_RECT ScissorRects[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
|
||||
D3D11_VIEWPORT Viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
|
||||
ID3D11RasterizerState* RS;
|
||||
ID3D11BlendState* BlendState;
|
||||
FLOAT BlendFactor[4];
|
||||
UINT SampleMask;
|
||||
UINT StencilRef;
|
||||
ID3D11DepthStencilState* DepthStencilState;
|
||||
ID3D11ShaderResourceView* PSShaderResource;
|
||||
ID3D11SamplerState* PSSampler;
|
||||
ID3D11PixelShader* PS;
|
||||
ID3D11VertexShader* VS;
|
||||
ID3D11GeometryShader* GS;
|
||||
UINT PSInstancesCount, VSInstancesCount, GSInstancesCount;
|
||||
ID3D11ClassInstance *PSInstances[256], *VSInstances[256], *GSInstances[256]; // 256 is max according to PSSetShader documentation
|
||||
D3D11_PRIMITIVE_TOPOLOGY PrimitiveTopology;
|
||||
ID3D11Buffer* IndexBuffer, *VertexBuffer, *VSConstantBuffer;
|
||||
UINT IndexBufferOffset, VertexBufferStride, VertexBufferOffset;
|
||||
DXGI_FORMAT IndexBufferFormat;
|
||||
ID3D11InputLayout* InputLayout;
|
||||
};
|
||||
BACKUP_DX11_STATE old = {};
|
||||
old.ScissorRectsCount = old.ViewportsCount = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE;
|
||||
device->RSGetScissorRects(&old.ScissorRectsCount, old.ScissorRects);
|
||||
device->RSGetViewports(&old.ViewportsCount, old.Viewports);
|
||||
device->RSGetState(&old.RS);
|
||||
device->OMGetBlendState(&old.BlendState, old.BlendFactor, &old.SampleMask);
|
||||
device->OMGetDepthStencilState(&old.DepthStencilState, &old.StencilRef);
|
||||
device->PSGetShaderResources(0, 1, &old.PSShaderResource);
|
||||
device->PSGetSamplers(0, 1, &old.PSSampler);
|
||||
old.PSInstancesCount = old.VSInstancesCount = old.GSInstancesCount = 256;
|
||||
device->PSGetShader(&old.PS, old.PSInstances, &old.PSInstancesCount);
|
||||
device->VSGetShader(&old.VS, old.VSInstances, &old.VSInstancesCount);
|
||||
device->VSGetConstantBuffers(0, 1, &old.VSConstantBuffer);
|
||||
device->GSGetShader(&old.GS, old.GSInstances, &old.GSInstancesCount);
|
||||
|
||||
device->IAGetPrimitiveTopology(&old.PrimitiveTopology);
|
||||
device->IAGetIndexBuffer(&old.IndexBuffer, &old.IndexBufferFormat, &old.IndexBufferOffset);
|
||||
device->IAGetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset);
|
||||
device->IAGetInputLayout(&old.InputLayout);
|
||||
|
||||
// Setup desired DX state
|
||||
ImGui_ImplDX11_SetupRenderState(draw_data, device);
|
||||
|
||||
// Setup render state structure (for callbacks and custom texture bindings)
|
||||
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
|
||||
ImGui_ImplDX11_RenderState render_state;
|
||||
render_state.Device = bd->pd3dDevice;
|
||||
render_state.DeviceContext = bd->pd3dDeviceContext;
|
||||
render_state.SamplerDefault = bd->pFontSampler;
|
||||
platform_io.Renderer_RenderState = &render_state;
|
||||
|
||||
// Render command lists
|
||||
// (Because we merged all buffers into a single one, we maintain our own offset into them)
|
||||
int global_idx_offset = 0;
|
||||
int global_vtx_offset = 0;
|
||||
ImVec2 clip_off = draw_data->DisplayPos;
|
||||
for (int n = 0; n < draw_data->CmdListsCount; n++)
|
||||
{
|
||||
const ImDrawList* draw_list = draw_data->CmdLists[n];
|
||||
for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
|
||||
{
|
||||
const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
|
||||
if (pcmd->UserCallback != nullptr)
|
||||
{
|
||||
// User callback, registered via ImDrawList::AddCallback()
|
||||
// (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.)
|
||||
if (pcmd->UserCallback == ImDrawCallback_ResetRenderState)
|
||||
ImGui_ImplDX11_SetupRenderState(draw_data, device);
|
||||
else
|
||||
pcmd->UserCallback(draw_list, pcmd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Project scissor/clipping rectangles into framebuffer space
|
||||
ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y);
|
||||
ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y);
|
||||
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
|
||||
continue;
|
||||
|
||||
// Apply scissor/clipping rectangle
|
||||
const D3D11_RECT r = { (LONG)clip_min.x, (LONG)clip_min.y, (LONG)clip_max.x, (LONG)clip_max.y };
|
||||
device->RSSetScissorRects(1, &r);
|
||||
|
||||
// Bind texture, Draw
|
||||
ID3D11ShaderResourceView* texture_srv = (ID3D11ShaderResourceView*)pcmd->GetTexID();
|
||||
device->PSSetShaderResources(0, 1, &texture_srv);
|
||||
device->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset + global_idx_offset, pcmd->VtxOffset + global_vtx_offset);
|
||||
}
|
||||
}
|
||||
global_idx_offset += draw_list->IdxBuffer.Size;
|
||||
global_vtx_offset += draw_list->VtxBuffer.Size;
|
||||
}
|
||||
platform_io.Renderer_RenderState = NULL;
|
||||
|
||||
// Restore modified DX state
|
||||
device->RSSetScissorRects(old.ScissorRectsCount, old.ScissorRects);
|
||||
device->RSSetViewports(old.ViewportsCount, old.Viewports);
|
||||
device->RSSetState(old.RS); if (old.RS) old.RS->Release();
|
||||
device->OMSetBlendState(old.BlendState, old.BlendFactor, old.SampleMask); if (old.BlendState) old.BlendState->Release();
|
||||
device->OMSetDepthStencilState(old.DepthStencilState, old.StencilRef); if (old.DepthStencilState) old.DepthStencilState->Release();
|
||||
device->PSSetShaderResources(0, 1, &old.PSShaderResource); if (old.PSShaderResource) old.PSShaderResource->Release();
|
||||
device->PSSetSamplers(0, 1, &old.PSSampler); if (old.PSSampler) old.PSSampler->Release();
|
||||
device->PSSetShader(old.PS, old.PSInstances, old.PSInstancesCount); if (old.PS) old.PS->Release();
|
||||
for (UINT i = 0; i < old.PSInstancesCount; i++) if (old.PSInstances[i]) old.PSInstances[i]->Release();
|
||||
device->VSSetShader(old.VS, old.VSInstances, old.VSInstancesCount); if (old.VS) old.VS->Release();
|
||||
device->VSSetConstantBuffers(0, 1, &old.VSConstantBuffer); if (old.VSConstantBuffer) old.VSConstantBuffer->Release();
|
||||
device->GSSetShader(old.GS, old.GSInstances, old.GSInstancesCount); if (old.GS) old.GS->Release();
|
||||
for (UINT i = 0; i < old.VSInstancesCount; i++) if (old.VSInstances[i]) old.VSInstances[i]->Release();
|
||||
device->IASetPrimitiveTopology(old.PrimitiveTopology);
|
||||
device->IASetIndexBuffer(old.IndexBuffer, old.IndexBufferFormat, old.IndexBufferOffset); if (old.IndexBuffer) old.IndexBuffer->Release();
|
||||
device->IASetVertexBuffers(0, 1, &old.VertexBuffer, &old.VertexBufferStride, &old.VertexBufferOffset); if (old.VertexBuffer) old.VertexBuffer->Release();
|
||||
device->IASetInputLayout(old.InputLayout); if (old.InputLayout) old.InputLayout->Release();
|
||||
}
|
||||
|
||||
static void ImGui_ImplDX11_CreateFontsTexture()
|
||||
{
|
||||
// Build texture atlas
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
unsigned char* pixels;
|
||||
int width, height;
|
||||
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
|
||||
|
||||
// Upload texture to graphics system
|
||||
{
|
||||
D3D11_TEXTURE2D_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.Width = width;
|
||||
desc.Height = height;
|
||||
desc.MipLevels = 1;
|
||||
desc.ArraySize = 1;
|
||||
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
desc.SampleDesc.Count = 1;
|
||||
desc.Usage = D3D11_USAGE_DEFAULT;
|
||||
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
desc.CPUAccessFlags = 0;
|
||||
|
||||
ID3D11Texture2D* pTexture = nullptr;
|
||||
D3D11_SUBRESOURCE_DATA subResource;
|
||||
subResource.pSysMem = pixels;
|
||||
subResource.SysMemPitch = desc.Width * 4;
|
||||
subResource.SysMemSlicePitch = 0;
|
||||
bd->pd3dDevice->CreateTexture2D(&desc, &subResource, &pTexture);
|
||||
IM_ASSERT(pTexture != nullptr);
|
||||
|
||||
// Create texture view
|
||||
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
|
||||
ZeroMemory(&srvDesc, sizeof(srvDesc));
|
||||
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
||||
srvDesc.Texture2D.MipLevels = desc.MipLevels;
|
||||
srvDesc.Texture2D.MostDetailedMip = 0;
|
||||
bd->pd3dDevice->CreateShaderResourceView(pTexture, &srvDesc, &bd->pFontTextureView);
|
||||
pTexture->Release();
|
||||
}
|
||||
|
||||
// Store our identifier
|
||||
io.Fonts->SetTexID((ImTextureID)bd->pFontTextureView);
|
||||
|
||||
// Create texture sampler
|
||||
// (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling)
|
||||
{
|
||||
D3D11_SAMPLER_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
||||
desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
desc.MipLODBias = 0.f;
|
||||
desc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.MinLOD = 0.f;
|
||||
desc.MaxLOD = 0.f;
|
||||
bd->pd3dDevice->CreateSamplerState(&desc, &bd->pFontSampler);
|
||||
}
|
||||
}
|
||||
|
||||
bool ImGui_ImplDX11_CreateDeviceObjects()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
if (!bd->pd3dDevice)
|
||||
return false;
|
||||
if (bd->pFontSampler)
|
||||
ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
|
||||
// By using D3DCompile() from <d3dcompiler.h> / d3dcompiler.lib, we introduce a dependency to a given version of d3dcompiler_XX.dll (see D3DCOMPILER_DLL_A)
|
||||
// If you would like to use this DX11 sample code but remove this dependency you can:
|
||||
// 1) compile once, save the compiled shader blobs into a file or source code and pass them to CreateVertexShader()/CreatePixelShader() [preferred solution]
|
||||
// 2) use code to detect any version of the DLL and grab a pointer to D3DCompile from the DLL.
|
||||
// See https://github.com/ocornut/imgui/pull/638 for sources and details.
|
||||
|
||||
// Create the vertex shader
|
||||
{
|
||||
static const char* vertexShader =
|
||||
"cbuffer vertexBuffer : register(b0) \
|
||||
{\
|
||||
float4x4 ProjectionMatrix; \
|
||||
};\
|
||||
struct VS_INPUT\
|
||||
{\
|
||||
float2 pos : POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
\
|
||||
struct PS_INPUT\
|
||||
{\
|
||||
float4 pos : SV_POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
\
|
||||
PS_INPUT main(VS_INPUT input)\
|
||||
{\
|
||||
PS_INPUT output;\
|
||||
output.pos = mul( ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));\
|
||||
output.col = input.col;\
|
||||
output.uv = input.uv;\
|
||||
return output;\
|
||||
}";
|
||||
|
||||
ID3DBlob* vertexShaderBlob;
|
||||
if (FAILED(D3DCompile(vertexShader, strlen(vertexShader), nullptr, nullptr, nullptr, "main", "vs_4_0", 0, 0, &vertexShaderBlob, nullptr)))
|
||||
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
|
||||
if (bd->pd3dDevice->CreateVertexShader(vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), nullptr, &bd->pVertexShader) != S_OK)
|
||||
{
|
||||
vertexShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the input layout
|
||||
D3D11_INPUT_ELEMENT_DESC local_layout[] =
|
||||
{
|
||||
{ "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, pos), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, (UINT)offsetof(ImDrawVert, uv), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
{ "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, (UINT)offsetof(ImDrawVert, col), D3D11_INPUT_PER_VERTEX_DATA, 0 },
|
||||
};
|
||||
if (bd->pd3dDevice->CreateInputLayout(local_layout, 3, vertexShaderBlob->GetBufferPointer(), vertexShaderBlob->GetBufferSize(), &bd->pInputLayout) != S_OK)
|
||||
{
|
||||
vertexShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
vertexShaderBlob->Release();
|
||||
|
||||
// Create the constant buffer
|
||||
{
|
||||
D3D11_BUFFER_DESC desc;
|
||||
desc.ByteWidth = sizeof(VERTEX_CONSTANT_BUFFER_DX11);
|
||||
desc.Usage = D3D11_USAGE_DYNAMIC;
|
||||
desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
|
||||
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
||||
desc.MiscFlags = 0;
|
||||
bd->pd3dDevice->CreateBuffer(&desc, nullptr, &bd->pVertexConstantBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the pixel shader
|
||||
{
|
||||
static const char* pixelShader =
|
||||
"struct PS_INPUT\
|
||||
{\
|
||||
float4 pos : SV_POSITION;\
|
||||
float4 col : COLOR0;\
|
||||
float2 uv : TEXCOORD0;\
|
||||
};\
|
||||
sampler sampler0;\
|
||||
Texture2D texture0;\
|
||||
\
|
||||
float4 main(PS_INPUT input) : SV_Target\
|
||||
{\
|
||||
float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \
|
||||
return out_col; \
|
||||
}";
|
||||
|
||||
ID3DBlob* pixelShaderBlob;
|
||||
if (FAILED(D3DCompile(pixelShader, strlen(pixelShader), nullptr, nullptr, nullptr, "main", "ps_4_0", 0, 0, &pixelShaderBlob, nullptr)))
|
||||
return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob!
|
||||
if (bd->pd3dDevice->CreatePixelShader(pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize(), nullptr, &bd->pPixelShader) != S_OK)
|
||||
{
|
||||
pixelShaderBlob->Release();
|
||||
return false;
|
||||
}
|
||||
pixelShaderBlob->Release();
|
||||
}
|
||||
|
||||
// Create the blending setup
|
||||
{
|
||||
D3D11_BLEND_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.AlphaToCoverageEnable = false;
|
||||
desc.RenderTarget[0].BlendEnable = true;
|
||||
desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
|
||||
desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
|
||||
desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
|
||||
desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
|
||||
desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA;
|
||||
desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
|
||||
desc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
|
||||
bd->pd3dDevice->CreateBlendState(&desc, &bd->pBlendState);
|
||||
}
|
||||
|
||||
// Create the rasterizer state
|
||||
{
|
||||
D3D11_RASTERIZER_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.FillMode = D3D11_FILL_SOLID;
|
||||
desc.CullMode = D3D11_CULL_NONE;
|
||||
desc.ScissorEnable = true;
|
||||
desc.DepthClipEnable = true;
|
||||
bd->pd3dDevice->CreateRasterizerState(&desc, &bd->pRasterizerState);
|
||||
}
|
||||
|
||||
// Create depth-stencil State
|
||||
{
|
||||
D3D11_DEPTH_STENCIL_DESC desc;
|
||||
ZeroMemory(&desc, sizeof(desc));
|
||||
desc.DepthEnable = false;
|
||||
desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
|
||||
desc.DepthFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.StencilEnable = false;
|
||||
desc.FrontFace.StencilFailOp = desc.FrontFace.StencilDepthFailOp = desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
|
||||
desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
|
||||
desc.BackFace = desc.FrontFace;
|
||||
bd->pd3dDevice->CreateDepthStencilState(&desc, &bd->pDepthStencilState);
|
||||
}
|
||||
|
||||
ImGui_ImplDX11_CreateFontsTexture();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_InvalidateDeviceObjects()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
if (!bd->pd3dDevice)
|
||||
return;
|
||||
|
||||
if (bd->pFontSampler) { bd->pFontSampler->Release(); bd->pFontSampler = nullptr; }
|
||||
if (bd->pFontTextureView) { bd->pFontTextureView->Release(); bd->pFontTextureView = nullptr; ImGui::GetIO().Fonts->SetTexID(0); } // We copied data->pFontTextureView to io.Fonts->TexID so let's clear that as well.
|
||||
if (bd->pIB) { bd->pIB->Release(); bd->pIB = nullptr; }
|
||||
if (bd->pVB) { bd->pVB->Release(); bd->pVB = nullptr; }
|
||||
if (bd->pBlendState) { bd->pBlendState->Release(); bd->pBlendState = nullptr; }
|
||||
if (bd->pDepthStencilState) { bd->pDepthStencilState->Release(); bd->pDepthStencilState = nullptr; }
|
||||
if (bd->pRasterizerState) { bd->pRasterizerState->Release(); bd->pRasterizerState = nullptr; }
|
||||
if (bd->pPixelShader) { bd->pPixelShader->Release(); bd->pPixelShader = nullptr; }
|
||||
if (bd->pVertexConstantBuffer) { bd->pVertexConstantBuffer->Release(); bd->pVertexConstantBuffer = nullptr; }
|
||||
if (bd->pInputLayout) { bd->pInputLayout->Release(); bd->pInputLayout = nullptr; }
|
||||
if (bd->pVertexShader) { bd->pVertexShader->Release(); bd->pVertexShader = nullptr; }
|
||||
}
|
||||
|
||||
bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
IMGUI_CHECKVERSION();
|
||||
IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
|
||||
|
||||
// Setup backend capabilities flags
|
||||
ImGui_ImplDX11_Data* bd = IM_NEW(ImGui_ImplDX11_Data)();
|
||||
io.BackendRendererUserData = (void*)bd;
|
||||
io.BackendRendererName = "imgui_impl_dx11";
|
||||
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
|
||||
|
||||
// Get factory from device
|
||||
IDXGIDevice* pDXGIDevice = nullptr;
|
||||
IDXGIAdapter* pDXGIAdapter = nullptr;
|
||||
IDXGIFactory* pFactory = nullptr;
|
||||
|
||||
if (device->QueryInterface(IID_PPV_ARGS(&pDXGIDevice)) == S_OK)
|
||||
if (pDXGIDevice->GetParent(IID_PPV_ARGS(&pDXGIAdapter)) == S_OK)
|
||||
if (pDXGIAdapter->GetParent(IID_PPV_ARGS(&pFactory)) == S_OK)
|
||||
{
|
||||
bd->pd3dDevice = device;
|
||||
bd->pd3dDeviceContext = device_context;
|
||||
bd->pFactory = pFactory;
|
||||
}
|
||||
if (pDXGIDevice) pDXGIDevice->Release();
|
||||
if (pDXGIAdapter) pDXGIAdapter->Release();
|
||||
bd->pd3dDevice->AddRef();
|
||||
bd->pd3dDeviceContext->AddRef();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_Shutdown()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
if (bd->pFactory) { bd->pFactory->Release(); }
|
||||
if (bd->pd3dDevice) { bd->pd3dDevice->Release(); }
|
||||
if (bd->pd3dDeviceContext) { bd->pd3dDeviceContext->Release(); }
|
||||
io.BackendRendererName = nullptr;
|
||||
io.BackendRendererUserData = nullptr;
|
||||
io.BackendFlags &= ~ImGuiBackendFlags_RendererHasVtxOffset;
|
||||
IM_DELETE(bd);
|
||||
}
|
||||
|
||||
void ImGui_ImplDX11_NewFrame()
|
||||
{
|
||||
ImGui_ImplDX11_Data* bd = ImGui_ImplDX11_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplDX11_Init()?");
|
||||
|
||||
if (!bd->pFontSampler)
|
||||
ImGui_ImplDX11_CreateDeviceObjects();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
// dear imgui: Renderer Backend for DirectX11
|
||||
// This needs to be used along with a Platform Backend (e.g. Win32)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Renderer: User texture binding. Use 'ID3D11ShaderResourceView*' as ImTextureID. Read the FAQ about ImTextureID!
|
||||
// [X] Renderer: Large meshes support (64k+ vertices) with 16-bit indices.
|
||||
// [X] Renderer: Expose selected render state for draw callbacks to use. Access in '(ImGui_ImplXXXX_RenderState*)GetPlatformIO().Renderer_RenderState'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
struct ID3D11Device;
|
||||
struct ID3D11DeviceContext;
|
||||
struct ID3D11SamplerState;
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context);
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_NewFrame();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data);
|
||||
|
||||
// Use if you want to reset your rendering device without losing Dear ImGui state.
|
||||
IMGUI_IMPL_API bool ImGui_ImplDX11_CreateDeviceObjects();
|
||||
IMGUI_IMPL_API void ImGui_ImplDX11_InvalidateDeviceObjects();
|
||||
|
||||
// [BETA] Selected render state data shared with callbacks.
|
||||
// This is temporarily stored in GetPlatformIO().Renderer_RenderState during the ImGui_ImplDX11_RenderDrawData() call.
|
||||
// (Please open an issue if you feel you need access to more data)
|
||||
struct ImGui_ImplDX11_RenderState
|
||||
{
|
||||
ID3D11Device* Device;
|
||||
ID3D11DeviceContext* DeviceContext;
|
||||
ID3D11SamplerState* SamplerDefault;
|
||||
};
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
-940
@@ -1,940 +0,0 @@
|
||||
// dear imgui: Platform Backend for Windows (standard windows API for 32-bits AND 64-bits applications)
|
||||
// This needs to be used along with a Renderer (e.g. DirectX11, OpenGL3, Vulkan..)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui)
|
||||
// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen.
|
||||
// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy VK_* values are obsolete since 1.87 and not supported since 1.91.5]
|
||||
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
|
||||
// [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
// Configuration flags to add in your imconfig file:
|
||||
//#define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD // Disable gamepad support. This was meaningful before <1.81 but we now load XInput dynamically so the option is now less relevant.
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2024-07-08: Inputs: Fixed ImGuiMod_Super being mapped to VK_APPS instead of VK_LWIN||VK_RWIN. (#7768)
|
||||
// 2023-10-05: Inputs: Added support for extra ImGuiKey values: F13 to F24 function keys, app back/forward keys.
|
||||
// 2023-09-25: Inputs: Synthesize key-down event on key-up for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit it (same behavior as GLFW/SDL).
|
||||
// 2023-09-07: Inputs: Added support for keyboard codepage conversion for when application is compiled in MBCS mode and using a non-Unicode window.
|
||||
// 2023-04-19: Added ImGui_ImplWin32_InitForOpenGL() to facilitate combining raw Win32/Winapi with OpenGL. (#3218)
|
||||
// 2023-04-04: Inputs: Added support for io.AddMouseSourceEvent() to discriminate ImGuiMouseSource_Mouse/ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen. (#2702)
|
||||
// 2023-02-15: Inputs: Use WM_NCMOUSEMOVE / WM_NCMOUSELEAVE to track mouse position over non-client area (e.g. OS decorations) when app is not focused. (#6045, #6162)
|
||||
// 2023-02-02: Inputs: Flipping WM_MOUSEHWHEEL (horizontal mouse-wheel) value to match other backends and offer consistent horizontal scrolling direction. (#4019, #6096, #1463)
|
||||
// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11.
|
||||
// 2022-09-28: Inputs: Convert WM_CHAR values with MultiByteToWideChar() when window class was registered as MBCS (not Unicode).
|
||||
// 2022-09-26: Inputs: Renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported).
|
||||
// 2022-01-26: Inputs: replaced short-lived io.AddKeyModsEvent() (added two weeks ago) with io.AddKeyEvent() using ImGuiKey_ModXXX flags. Sorry for the confusion.
|
||||
// 2021-01-20: Inputs: calling new io.AddKeyAnalogEvent() for gamepad support, instead of writing directly to io.NavInputs[].
|
||||
// 2022-01-17: Inputs: calling new io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() API (1.87+).
|
||||
// 2022-01-17: Inputs: always update key mods next and before a key event (not in NewFrame) to fix input queue with very low framerates.
|
||||
// 2022-01-12: Inputs: Update mouse inputs using WM_MOUSEMOVE/WM_MOUSELEAVE + fallback to provide it when focused but not hovered/captured. More standard and will allow us to pass it to future input queue API.
|
||||
// 2022-01-12: Inputs: Maintain our own copy of MouseButtonsDown mask instead of using ImGui::IsAnyMouseDown() which will be obsoleted.
|
||||
// 2022-01-10: Inputs: calling new io.AddKeyEvent(), io.AddKeyModsEvent() + io.SetKeyEventNativeData() API (1.87+). Support for full ImGuiKey range.
|
||||
// 2021-12-16: Inputs: Fill VK_LCONTROL/VK_RCONTROL/VK_LSHIFT/VK_RSHIFT/VK_LMENU/VK_RMENU for completeness.
|
||||
// 2021-08-17: Calling io.AddFocusEvent() on WM_SETFOCUS/WM_KILLFOCUS messages.
|
||||
// 2021-08-02: Inputs: Fixed keyboard modifiers being reported when host window doesn't have focus.
|
||||
// 2021-07-29: Inputs: MousePos is correctly reported when the host platform window is hovered but not focused (using TrackMouseEvent() to receive WM_MOUSELEAVE events).
|
||||
// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX).
|
||||
// 2021-06-08: Fixed ImGui_ImplWin32_EnableDpiAwareness() and ImGui_ImplWin32_GetDpiScaleForMonitor() to handle Windows 8.1/10 features without a manifest (per-monitor DPI, and properly calls SetProcessDpiAwareness() on 8.1).
|
||||
// 2021-03-23: Inputs: Clearing keyboard down array when losing focus (WM_KILLFOCUS).
|
||||
// 2021-02-18: Added ImGui_ImplWin32_EnableAlphaCompositing(). Non Visual Studio users will need to link with dwmapi.lib (MinGW/gcc: use -ldwmapi).
|
||||
// 2021-02-17: Fixed ImGui_ImplWin32_EnableDpiAwareness() attempting to get SetProcessDpiAwareness from shcore.dll on Windows 8 whereas it is only supported on Windows 8.1.
|
||||
// 2021-01-25: Inputs: Dynamically loading XInput DLL.
|
||||
// 2020-12-04: Misc: Fixed setting of io.DisplaySize to invalid/uninitialized data when after hwnd has been closed.
|
||||
// 2020-03-03: Inputs: Calling AddInputCharacterUTF16() to support surrogate pairs leading to codepoint >= 0x10000 (for more complete CJK inputs)
|
||||
// 2020-02-17: Added ImGui_ImplWin32_EnableDpiAwareness(), ImGui_ImplWin32_GetDpiScaleForHwnd(), ImGui_ImplWin32_GetDpiScaleForMonitor() helper functions.
|
||||
// 2020-01-14: Inputs: Added support for #define IMGUI_IMPL_WIN32_DISABLE_GAMEPAD/IMGUI_IMPL_WIN32_DISABLE_LINKING_XINPUT.
|
||||
// 2019-12-05: Inputs: Added support for ImGuiMouseCursor_NotAllowed mouse cursor.
|
||||
// 2019-05-11: Inputs: Don't filter value from WM_CHAR before calling AddInputCharacter().
|
||||
// 2019-01-17: Misc: Using GetForegroundWindow()+IsChild() instead of GetActiveWindow() to be compatible with windows created in a different thread or parent.
|
||||
// 2019-01-17: Inputs: Added support for mouse buttons 4 and 5 via WM_XBUTTON* messages.
|
||||
// 2019-01-15: Inputs: Added support for XInput gamepads (if ImGuiConfigFlags_NavEnableGamepad is set by user application).
|
||||
// 2018-11-30: Misc: Setting up io.BackendPlatformName so it can be displayed in the About Window.
|
||||
// 2018-06-29: Inputs: Added support for the ImGuiMouseCursor_Hand cursor.
|
||||
// 2018-06-10: Inputs: Fixed handling of mouse wheel messages to support fine position messages (typically sent by track-pads).
|
||||
// 2018-06-08: Misc: Extracted imgui_impl_win32.cpp/.h away from the old combined DX9/DX10/DX11/DX12 examples.
|
||||
// 2018-03-20: Misc: Setup io.BackendFlags ImGuiBackendFlags_HasMouseCursors and ImGuiBackendFlags_HasSetMousePos flags + honor ImGuiConfigFlags_NoMouseCursorChange flag.
|
||||
// 2018-02-20: Inputs: Added support for mouse cursors (ImGui::GetMouseCursor() value and WM_SETCURSOR message handling).
|
||||
// 2018-02-06: Inputs: Added mapping for ImGuiKey_Space.
|
||||
// 2018-02-06: Inputs: Honoring the io.WantSetMousePos by repositioning the mouse (when using navigation and ImGuiConfigFlags_NavMoveMouse is set).
|
||||
// 2018-02-06: Misc: Removed call to ImGui::Shutdown() which is not available from 1.60 WIP, user needs to call CreateContext/DestroyContext themselves.
|
||||
// 2018-01-20: Inputs: Added Horizontal Mouse Wheel support.
|
||||
// 2018-01-08: Inputs: Added mapping for ImGuiKey_Insert.
|
||||
// 2018-01-05: Inputs: Added WM_LBUTTONDBLCLK double-click handlers for window classes with the CS_DBLCLKS flag.
|
||||
// 2017-10-23: Inputs: Added WM_SYSKEYDOWN / WM_SYSKEYUP handlers so e.g. the VK_MENU key can be read.
|
||||
// 2017-10-23: Inputs: Using Win32 ::SetCapture/::GetCapture() to retrieve mouse positions outside the client area when dragging.
|
||||
// 2016-11-12: Inputs: Only call Win32 ::SetCursor(nullptr) when io.MouseDrawCursor is set.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_impl_win32.h"
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <windowsx.h> // GET_X_LPARAM(), GET_Y_LPARAM()
|
||||
#include <tchar.h>
|
||||
#include <dwmapi.h>
|
||||
|
||||
// Using XInput for gamepad (will load DLL dynamically)
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
#include <xinput.h>
|
||||
typedef DWORD(WINAPI* PFN_XInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*);
|
||||
typedef DWORD(WINAPI* PFN_XInputGetState)(DWORD, XINPUT_STATE*);
|
||||
#endif
|
||||
|
||||
// Clang/GCC warnings with -Weverything
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader)
|
||||
#endif
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
|
||||
#pragma GCC diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader)
|
||||
#endif
|
||||
|
||||
struct ImGui_ImplWin32_Data
|
||||
{
|
||||
HWND hWnd;
|
||||
HWND MouseHwnd;
|
||||
int MouseTrackedArea; // 0: not tracked, 1: client area, 2: non-client area
|
||||
int MouseButtonsDown;
|
||||
INT64 Time;
|
||||
INT64 TicksPerSecond;
|
||||
ImGuiMouseCursor LastMouseCursor;
|
||||
UINT32 KeyboardCodePage;
|
||||
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
bool HasGamepad;
|
||||
bool WantUpdateHasGamepad;
|
||||
HMODULE XInputDLL;
|
||||
PFN_XInputGetCapabilities XInputGetCapabilities;
|
||||
PFN_XInputGetState XInputGetState;
|
||||
#endif
|
||||
|
||||
ImGui_ImplWin32_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
// Backend data stored in io.BackendPlatformUserData to allow support for multiple Dear ImGui contexts
|
||||
// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts.
|
||||
// FIXME: multi-context support is not well tested and probably dysfunctional in this backend.
|
||||
// FIXME: some shared resources (mouse cursor shape, gamepad) are mishandled when using multi-context.
|
||||
static ImGui_ImplWin32_Data* ImGui_ImplWin32_GetBackendData()
|
||||
{
|
||||
return ImGui::GetCurrentContext() ? (ImGui_ImplWin32_Data*)ImGui::GetIO().BackendPlatformUserData : nullptr;
|
||||
}
|
||||
static ImGui_ImplWin32_Data* ImGui_ImplWin32_GetBackendData(ImGuiIO& io)
|
||||
{
|
||||
return (ImGui_ImplWin32_Data*)io.BackendPlatformUserData;
|
||||
}
|
||||
|
||||
// Functions
|
||||
static void ImGui_ImplWin32_UpdateKeyboardCodePage(ImGuiIO& io)
|
||||
{
|
||||
// Retrieve keyboard code page, required for handling of non-Unicode Windows.
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
HKL keyboard_layout = ::GetKeyboardLayout(0);
|
||||
LCID keyboard_lcid = MAKELCID(HIWORD(keyboard_layout), SORT_DEFAULT);
|
||||
if (::GetLocaleInfoA(keyboard_lcid, (LOCALE_RETURN_NUMBER | LOCALE_IDEFAULTANSICODEPAGE), (LPSTR)&bd->KeyboardCodePage, sizeof(bd->KeyboardCodePage)) == 0)
|
||||
bd->KeyboardCodePage = CP_ACP; // Fallback to default ANSI code page when fails.
|
||||
}
|
||||
|
||||
static bool ImGui_ImplWin32_InitEx(void* hwnd, bool platform_has_own_dc)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
IMGUI_CHECKVERSION();
|
||||
IM_ASSERT(io.BackendPlatformUserData == nullptr && "Already initialized a platform backend!");
|
||||
|
||||
INT64 perf_frequency, perf_counter;
|
||||
if (!::QueryPerformanceFrequency((LARGE_INTEGER*)&perf_frequency))
|
||||
return false;
|
||||
if (!::QueryPerformanceCounter((LARGE_INTEGER*)&perf_counter))
|
||||
return false;
|
||||
|
||||
// Setup backend capabilities flags
|
||||
ImGui_ImplWin32_Data* bd = IM_NEW(ImGui_ImplWin32_Data)();
|
||||
io.BackendPlatformUserData = (void*)bd;
|
||||
io.BackendPlatformName = "imgui_impl_win32";
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional)
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used)
|
||||
|
||||
bd->hWnd = (HWND)hwnd;
|
||||
bd->TicksPerSecond = perf_frequency;
|
||||
bd->Time = perf_counter;
|
||||
bd->LastMouseCursor = ImGuiMouseCursor_COUNT;
|
||||
ImGui_ImplWin32_UpdateKeyboardCodePage(io);
|
||||
|
||||
// Set platform dependent data in viewport
|
||||
ImGuiViewport* main_viewport = ImGui::GetMainViewport();
|
||||
main_viewport->PlatformHandle = main_viewport->PlatformHandleRaw = (void*)bd->hWnd;
|
||||
IM_UNUSED(platform_has_own_dc); // Used in 'docking' branch
|
||||
|
||||
// Dynamically load XInput library
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
bd->WantUpdateHasGamepad = true;
|
||||
const char* xinput_dll_names[] =
|
||||
{
|
||||
"xinput1_4.dll", // Windows 8+
|
||||
"xinput1_3.dll", // DirectX SDK
|
||||
"xinput9_1_0.dll", // Windows Vista, Windows 7
|
||||
"xinput1_2.dll", // DirectX SDK
|
||||
"xinput1_1.dll" // DirectX SDK
|
||||
};
|
||||
for (int n = 0; n < IM_ARRAYSIZE(xinput_dll_names); n++)
|
||||
if (HMODULE dll = ::LoadLibraryA(xinput_dll_names[n]))
|
||||
{
|
||||
bd->XInputDLL = dll;
|
||||
bd->XInputGetCapabilities = (PFN_XInputGetCapabilities)::GetProcAddress(dll, "XInputGetCapabilities");
|
||||
bd->XInputGetState = (PFN_XInputGetState)::GetProcAddress(dll, "XInputGetState");
|
||||
break;
|
||||
}
|
||||
#endif // IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd)
|
||||
{
|
||||
return ImGui_ImplWin32_InitEx(hwnd, false);
|
||||
}
|
||||
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd)
|
||||
{
|
||||
// OpenGL needs CS_OWNDC
|
||||
return ImGui_ImplWin32_InitEx(hwnd, true);
|
||||
}
|
||||
|
||||
void ImGui_ImplWin32_Shutdown()
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "No platform backend to shutdown, or already shutdown?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Unload XInput library
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
if (bd->XInputDLL)
|
||||
::FreeLibrary(bd->XInputDLL);
|
||||
#endif // IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
|
||||
io.BackendPlatformName = nullptr;
|
||||
io.BackendPlatformUserData = nullptr;
|
||||
io.BackendFlags &= ~(ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_HasSetMousePos | ImGuiBackendFlags_HasGamepad);
|
||||
IM_DELETE(bd);
|
||||
}
|
||||
|
||||
static bool ImGui_ImplWin32_UpdateMouseCursor(ImGuiIO& io, ImGuiMouseCursor imgui_cursor)
|
||||
{
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange)
|
||||
return false;
|
||||
|
||||
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor)
|
||||
{
|
||||
// Hide OS mouse cursor if imgui is drawing it or if it wants no cursor
|
||||
::SetCursor(nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show OS mouse cursor
|
||||
LPTSTR win32_cursor = IDC_ARROW;
|
||||
switch (imgui_cursor)
|
||||
{
|
||||
case ImGuiMouseCursor_Arrow: win32_cursor = IDC_ARROW; break;
|
||||
case ImGuiMouseCursor_TextInput: win32_cursor = IDC_IBEAM; break;
|
||||
case ImGuiMouseCursor_ResizeAll: win32_cursor = IDC_SIZEALL; break;
|
||||
case ImGuiMouseCursor_ResizeEW: win32_cursor = IDC_SIZEWE; break;
|
||||
case ImGuiMouseCursor_ResizeNS: win32_cursor = IDC_SIZENS; break;
|
||||
case ImGuiMouseCursor_ResizeNESW: win32_cursor = IDC_SIZENESW; break;
|
||||
case ImGuiMouseCursor_ResizeNWSE: win32_cursor = IDC_SIZENWSE; break;
|
||||
case ImGuiMouseCursor_Hand: win32_cursor = IDC_HAND; break;
|
||||
case ImGuiMouseCursor_NotAllowed: win32_cursor = IDC_NO; break;
|
||||
}
|
||||
::SetCursor(::LoadCursor(nullptr, win32_cursor));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsVkDown(int vk)
|
||||
{
|
||||
return (::GetKeyState(vk) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_AddKeyEvent(ImGuiIO& io, ImGuiKey key, bool down, int native_keycode, int native_scancode = -1)
|
||||
{
|
||||
io.AddKeyEvent(key, down);
|
||||
io.SetKeyEventNativeData(key, native_keycode, native_scancode); // To support legacy indexing (<1.87 user code)
|
||||
IM_UNUSED(native_scancode);
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_ProcessKeyEventsWorkarounds(ImGuiIO& io)
|
||||
{
|
||||
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
|
||||
if (ImGui::IsKeyDown(ImGuiKey_LeftShift) && !IsVkDown(VK_LSHIFT))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftShift, false, VK_LSHIFT);
|
||||
if (ImGui::IsKeyDown(ImGuiKey_RightShift) && !IsVkDown(VK_RSHIFT))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightShift, false, VK_RSHIFT);
|
||||
|
||||
// Sometimes WM_KEYUP for Win key is not passed down to the app (e.g. for Win+V on some setups, according to GLFW).
|
||||
if (ImGui::IsKeyDown(ImGuiKey_LeftSuper) && !IsVkDown(VK_LWIN))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftSuper, false, VK_LWIN);
|
||||
if (ImGui::IsKeyDown(ImGuiKey_RightSuper) && !IsVkDown(VK_RWIN))
|
||||
ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightSuper, false, VK_RWIN);
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_UpdateKeyModifiers(ImGuiIO& io)
|
||||
{
|
||||
io.AddKeyEvent(ImGuiMod_Ctrl, IsVkDown(VK_CONTROL));
|
||||
io.AddKeyEvent(ImGuiMod_Shift, IsVkDown(VK_SHIFT));
|
||||
io.AddKeyEvent(ImGuiMod_Alt, IsVkDown(VK_MENU));
|
||||
io.AddKeyEvent(ImGuiMod_Super, IsVkDown(VK_LWIN) || IsVkDown(VK_RWIN));
|
||||
}
|
||||
|
||||
static void ImGui_ImplWin32_UpdateMouseData(ImGuiIO& io)
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
IM_ASSERT(bd->hWnd != 0);
|
||||
|
||||
HWND focused_window = ::GetForegroundWindow();
|
||||
const bool is_app_focused = (focused_window == bd->hWnd);
|
||||
if (is_app_focused)
|
||||
{
|
||||
// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when io.ConfigNavMoveSetMousePos is enabled by user)
|
||||
if (io.WantSetMousePos)
|
||||
{
|
||||
POINT pos = { (int)io.MousePos.x, (int)io.MousePos.y };
|
||||
if (::ClientToScreen(bd->hWnd, &pos))
|
||||
::SetCursorPos(pos.x, pos.y);
|
||||
}
|
||||
|
||||
// (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
|
||||
// This also fills a short gap when clicking non-client area: WM_NCMOUSELEAVE -> modal OS move -> gap -> WM_NCMOUSEMOVE
|
||||
if (!io.WantSetMousePos && bd->MouseTrackedArea == 0)
|
||||
{
|
||||
POINT pos;
|
||||
if (::GetCursorPos(&pos) && ::ScreenToClient(bd->hWnd, &pos))
|
||||
io.AddMousePosEvent((float)pos.x, (float)pos.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gamepad navigation mapping
|
||||
static void ImGui_ImplWin32_UpdateGamepads(ImGuiIO& io)
|
||||
{
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
//if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0) // FIXME: Technically feeding gamepad shouldn't depend on this now that they are regular inputs.
|
||||
// return;
|
||||
|
||||
// Calling XInputGetState() every frame on disconnected gamepads is unfortunately too slow.
|
||||
// Instead we refresh gamepad availability by calling XInputGetCapabilities() _only_ after receiving WM_DEVICECHANGE.
|
||||
if (bd->WantUpdateHasGamepad)
|
||||
{
|
||||
XINPUT_CAPABILITIES caps = {};
|
||||
bd->HasGamepad = bd->XInputGetCapabilities ? (bd->XInputGetCapabilities(0, XINPUT_FLAG_GAMEPAD, &caps) == ERROR_SUCCESS) : false;
|
||||
bd->WantUpdateHasGamepad = false;
|
||||
}
|
||||
|
||||
io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad;
|
||||
XINPUT_STATE xinput_state;
|
||||
XINPUT_GAMEPAD& gamepad = xinput_state.Gamepad;
|
||||
if (!bd->HasGamepad || bd->XInputGetState == nullptr || bd->XInputGetState(0, &xinput_state) != ERROR_SUCCESS)
|
||||
return;
|
||||
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
|
||||
|
||||
#define IM_SATURATE(V) (V < 0.0f ? 0.0f : V > 1.0f ? 1.0f : V)
|
||||
#define MAP_BUTTON(KEY_NO, BUTTON_ENUM) { io.AddKeyEvent(KEY_NO, (gamepad.wButtons & BUTTON_ENUM) != 0); }
|
||||
#define MAP_ANALOG(KEY_NO, VALUE, V0, V1) { float vn = (float)(VALUE - V0) / (float)(V1 - V0); io.AddKeyAnalogEvent(KEY_NO, vn > 0.10f, IM_SATURATE(vn)); }
|
||||
MAP_BUTTON(ImGuiKey_GamepadStart, XINPUT_GAMEPAD_START);
|
||||
MAP_BUTTON(ImGuiKey_GamepadBack, XINPUT_GAMEPAD_BACK);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceLeft, XINPUT_GAMEPAD_X);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceRight, XINPUT_GAMEPAD_B);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceUp, XINPUT_GAMEPAD_Y);
|
||||
MAP_BUTTON(ImGuiKey_GamepadFaceDown, XINPUT_GAMEPAD_A);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadLeft, XINPUT_GAMEPAD_DPAD_LEFT);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadRight, XINPUT_GAMEPAD_DPAD_RIGHT);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadUp, XINPUT_GAMEPAD_DPAD_UP);
|
||||
MAP_BUTTON(ImGuiKey_GamepadDpadDown, XINPUT_GAMEPAD_DPAD_DOWN);
|
||||
MAP_BUTTON(ImGuiKey_GamepadL1, XINPUT_GAMEPAD_LEFT_SHOULDER);
|
||||
MAP_BUTTON(ImGuiKey_GamepadR1, XINPUT_GAMEPAD_RIGHT_SHOULDER);
|
||||
MAP_ANALOG(ImGuiKey_GamepadL2, gamepad.bLeftTrigger, XINPUT_GAMEPAD_TRIGGER_THRESHOLD, 255);
|
||||
MAP_ANALOG(ImGuiKey_GamepadR2, gamepad.bRightTrigger, XINPUT_GAMEPAD_TRIGGER_THRESHOLD, 255);
|
||||
MAP_BUTTON(ImGuiKey_GamepadL3, XINPUT_GAMEPAD_LEFT_THUMB);
|
||||
MAP_BUTTON(ImGuiKey_GamepadR3, XINPUT_GAMEPAD_RIGHT_THUMB);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickLeft, gamepad.sThumbLX, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickRight, gamepad.sThumbLX, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickUp, gamepad.sThumbLY, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadLStickDown, gamepad.sThumbLY, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickLeft, gamepad.sThumbRX, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickRight, gamepad.sThumbRX, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickUp, gamepad.sThumbRY, +XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, +32767);
|
||||
MAP_ANALOG(ImGuiKey_GamepadRStickDown, gamepad.sThumbRY, -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, -32768);
|
||||
#undef MAP_BUTTON
|
||||
#undef MAP_ANALOG
|
||||
#else // #ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
IM_UNUSED(io);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImGui_ImplWin32_NewFrame()
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData();
|
||||
IM_ASSERT(bd != nullptr && "Context or backend not initialized? Did you call ImGui_ImplWin32_Init()?");
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Setup display size (every frame to accommodate for window resizing)
|
||||
RECT rect = { 0, 0, 0, 0 };
|
||||
::GetClientRect(bd->hWnd, &rect);
|
||||
io.DisplaySize = ImVec2((float)(rect.right - rect.left), (float)(rect.bottom - rect.top));
|
||||
|
||||
// Setup time step
|
||||
INT64 current_time = 0;
|
||||
::QueryPerformanceCounter((LARGE_INTEGER*)¤t_time);
|
||||
io.DeltaTime = (float)(current_time - bd->Time) / bd->TicksPerSecond;
|
||||
bd->Time = current_time;
|
||||
|
||||
// Update OS mouse position
|
||||
ImGui_ImplWin32_UpdateMouseData(io);
|
||||
|
||||
// Process workarounds for known Windows key handling issues
|
||||
ImGui_ImplWin32_ProcessKeyEventsWorkarounds(io);
|
||||
|
||||
// Update OS mouse cursor with the cursor requested by imgui
|
||||
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor();
|
||||
if (bd->LastMouseCursor != mouse_cursor)
|
||||
{
|
||||
bd->LastMouseCursor = mouse_cursor;
|
||||
ImGui_ImplWin32_UpdateMouseCursor(io, mouse_cursor);
|
||||
}
|
||||
|
||||
// Update game controllers (if enabled and available)
|
||||
ImGui_ImplWin32_UpdateGamepads(io);
|
||||
}
|
||||
|
||||
// Map VK_xxx to ImGuiKey_xxx.
|
||||
// Not static to allow third-party code to use that if they want to (but undocumented)
|
||||
ImGuiKey ImGui_ImplWin32_KeyEventToImGuiKey(WPARAM wParam, LPARAM lParam);
|
||||
ImGuiKey ImGui_ImplWin32_KeyEventToImGuiKey(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// There is no distinct VK_xxx for keypad enter, instead it is VK_RETURN + KF_EXTENDED.
|
||||
if ((wParam == VK_RETURN) && (HIWORD(lParam) & KF_EXTENDED))
|
||||
return ImGuiKey_KeypadEnter;
|
||||
|
||||
switch (wParam)
|
||||
{
|
||||
case VK_TAB: return ImGuiKey_Tab;
|
||||
case VK_LEFT: return ImGuiKey_LeftArrow;
|
||||
case VK_RIGHT: return ImGuiKey_RightArrow;
|
||||
case VK_UP: return ImGuiKey_UpArrow;
|
||||
case VK_DOWN: return ImGuiKey_DownArrow;
|
||||
case VK_PRIOR: return ImGuiKey_PageUp;
|
||||
case VK_NEXT: return ImGuiKey_PageDown;
|
||||
case VK_HOME: return ImGuiKey_Home;
|
||||
case VK_END: return ImGuiKey_End;
|
||||
case VK_INSERT: return ImGuiKey_Insert;
|
||||
case VK_DELETE: return ImGuiKey_Delete;
|
||||
case VK_BACK: return ImGuiKey_Backspace;
|
||||
case VK_SPACE: return ImGuiKey_Space;
|
||||
case VK_RETURN: return ImGuiKey_Enter;
|
||||
case VK_ESCAPE: return ImGuiKey_Escape;
|
||||
case VK_OEM_7: return ImGuiKey_Apostrophe;
|
||||
case VK_OEM_COMMA: return ImGuiKey_Comma;
|
||||
case VK_OEM_MINUS: return ImGuiKey_Minus;
|
||||
case VK_OEM_PERIOD: return ImGuiKey_Period;
|
||||
case VK_OEM_2: return ImGuiKey_Slash;
|
||||
case VK_OEM_1: return ImGuiKey_Semicolon;
|
||||
case VK_OEM_PLUS: return ImGuiKey_Equal;
|
||||
case VK_OEM_4: return ImGuiKey_LeftBracket;
|
||||
case VK_OEM_5: return ImGuiKey_Backslash;
|
||||
case VK_OEM_6: return ImGuiKey_RightBracket;
|
||||
case VK_OEM_3: return ImGuiKey_GraveAccent;
|
||||
case VK_CAPITAL: return ImGuiKey_CapsLock;
|
||||
case VK_SCROLL: return ImGuiKey_ScrollLock;
|
||||
case VK_NUMLOCK: return ImGuiKey_NumLock;
|
||||
case VK_SNAPSHOT: return ImGuiKey_PrintScreen;
|
||||
case VK_PAUSE: return ImGuiKey_Pause;
|
||||
case VK_NUMPAD0: return ImGuiKey_Keypad0;
|
||||
case VK_NUMPAD1: return ImGuiKey_Keypad1;
|
||||
case VK_NUMPAD2: return ImGuiKey_Keypad2;
|
||||
case VK_NUMPAD3: return ImGuiKey_Keypad3;
|
||||
case VK_NUMPAD4: return ImGuiKey_Keypad4;
|
||||
case VK_NUMPAD5: return ImGuiKey_Keypad5;
|
||||
case VK_NUMPAD6: return ImGuiKey_Keypad6;
|
||||
case VK_NUMPAD7: return ImGuiKey_Keypad7;
|
||||
case VK_NUMPAD8: return ImGuiKey_Keypad8;
|
||||
case VK_NUMPAD9: return ImGuiKey_Keypad9;
|
||||
case VK_DECIMAL: return ImGuiKey_KeypadDecimal;
|
||||
case VK_DIVIDE: return ImGuiKey_KeypadDivide;
|
||||
case VK_MULTIPLY: return ImGuiKey_KeypadMultiply;
|
||||
case VK_SUBTRACT: return ImGuiKey_KeypadSubtract;
|
||||
case VK_ADD: return ImGuiKey_KeypadAdd;
|
||||
case VK_LSHIFT: return ImGuiKey_LeftShift;
|
||||
case VK_LCONTROL: return ImGuiKey_LeftCtrl;
|
||||
case VK_LMENU: return ImGuiKey_LeftAlt;
|
||||
case VK_LWIN: return ImGuiKey_LeftSuper;
|
||||
case VK_RSHIFT: return ImGuiKey_RightShift;
|
||||
case VK_RCONTROL: return ImGuiKey_RightCtrl;
|
||||
case VK_RMENU: return ImGuiKey_RightAlt;
|
||||
case VK_RWIN: return ImGuiKey_RightSuper;
|
||||
case VK_APPS: return ImGuiKey_Menu;
|
||||
case '0': return ImGuiKey_0;
|
||||
case '1': return ImGuiKey_1;
|
||||
case '2': return ImGuiKey_2;
|
||||
case '3': return ImGuiKey_3;
|
||||
case '4': return ImGuiKey_4;
|
||||
case '5': return ImGuiKey_5;
|
||||
case '6': return ImGuiKey_6;
|
||||
case '7': return ImGuiKey_7;
|
||||
case '8': return ImGuiKey_8;
|
||||
case '9': return ImGuiKey_9;
|
||||
case 'A': return ImGuiKey_A;
|
||||
case 'B': return ImGuiKey_B;
|
||||
case 'C': return ImGuiKey_C;
|
||||
case 'D': return ImGuiKey_D;
|
||||
case 'E': return ImGuiKey_E;
|
||||
case 'F': return ImGuiKey_F;
|
||||
case 'G': return ImGuiKey_G;
|
||||
case 'H': return ImGuiKey_H;
|
||||
case 'I': return ImGuiKey_I;
|
||||
case 'J': return ImGuiKey_J;
|
||||
case 'K': return ImGuiKey_K;
|
||||
case 'L': return ImGuiKey_L;
|
||||
case 'M': return ImGuiKey_M;
|
||||
case 'N': return ImGuiKey_N;
|
||||
case 'O': return ImGuiKey_O;
|
||||
case 'P': return ImGuiKey_P;
|
||||
case 'Q': return ImGuiKey_Q;
|
||||
case 'R': return ImGuiKey_R;
|
||||
case 'S': return ImGuiKey_S;
|
||||
case 'T': return ImGuiKey_T;
|
||||
case 'U': return ImGuiKey_U;
|
||||
case 'V': return ImGuiKey_V;
|
||||
case 'W': return ImGuiKey_W;
|
||||
case 'X': return ImGuiKey_X;
|
||||
case 'Y': return ImGuiKey_Y;
|
||||
case 'Z': return ImGuiKey_Z;
|
||||
case VK_F1: return ImGuiKey_F1;
|
||||
case VK_F2: return ImGuiKey_F2;
|
||||
case VK_F3: return ImGuiKey_F3;
|
||||
case VK_F4: return ImGuiKey_F4;
|
||||
case VK_F5: return ImGuiKey_F5;
|
||||
case VK_F6: return ImGuiKey_F6;
|
||||
case VK_F7: return ImGuiKey_F7;
|
||||
case VK_F8: return ImGuiKey_F8;
|
||||
case VK_F9: return ImGuiKey_F9;
|
||||
case VK_F10: return ImGuiKey_F10;
|
||||
case VK_F11: return ImGuiKey_F11;
|
||||
case VK_F12: return ImGuiKey_F12;
|
||||
case VK_F13: return ImGuiKey_F13;
|
||||
case VK_F14: return ImGuiKey_F14;
|
||||
case VK_F15: return ImGuiKey_F15;
|
||||
case VK_F16: return ImGuiKey_F16;
|
||||
case VK_F17: return ImGuiKey_F17;
|
||||
case VK_F18: return ImGuiKey_F18;
|
||||
case VK_F19: return ImGuiKey_F19;
|
||||
case VK_F20: return ImGuiKey_F20;
|
||||
case VK_F21: return ImGuiKey_F21;
|
||||
case VK_F22: return ImGuiKey_F22;
|
||||
case VK_F23: return ImGuiKey_F23;
|
||||
case VK_F24: return ImGuiKey_F24;
|
||||
case VK_BROWSER_BACK: return ImGuiKey_AppBack;
|
||||
case VK_BROWSER_FORWARD: return ImGuiKey_AppForward;
|
||||
default: return ImGuiKey_None;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow compilation with old Windows SDK. MinGW doesn't have default _WIN32_WINNT/WINVER versions.
|
||||
#ifndef WM_MOUSEHWHEEL
|
||||
#define WM_MOUSEHWHEEL 0x020E
|
||||
#endif
|
||||
#ifndef DBT_DEVNODES_CHANGED
|
||||
#define DBT_DEVNODES_CHANGED 0x0007
|
||||
#endif
|
||||
|
||||
// Helper to obtain the source of mouse messages.
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
|
||||
// Prefer to call this at the top of the message handler to avoid the possibility of other Win32 calls interfering with this.
|
||||
static ImGuiMouseSource ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo()
|
||||
{
|
||||
LPARAM extra_info = ::GetMessageExtraInfo();
|
||||
if ((extra_info & 0xFFFFFF80) == 0xFF515700)
|
||||
return ImGuiMouseSource_Pen;
|
||||
if ((extra_info & 0xFFFFFF80) == 0xFF515780)
|
||||
return ImGuiMouseSource_TouchScreen;
|
||||
return ImGuiMouseSource_Mouse;
|
||||
}
|
||||
|
||||
// Win32 message handler (process Win32 mouse/keyboard inputs, etc.)
|
||||
// Call from your application's message handler. Keep calling your message handler unless this function returns TRUE.
|
||||
// When implementing your own backend, you can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if Dear ImGui wants to use your inputs.
|
||||
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
|
||||
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
|
||||
// Generally you may always pass all inputs to Dear ImGui, and hide them from your application based on those two flags.
|
||||
// PS: We treat DBLCLK messages as regular mouse down messages, so this code will work on windows classes that have the CS_DBLCLKS flag set. Our own example app code doesn't set this flag.
|
||||
|
||||
// Copy either line into your .cpp file to forward declare the function:
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Use ImGui::GetCurrentContext()
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandlerEx(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ImGuiIO& io); // Doesn't use ImGui::GetCurrentContext()
|
||||
|
||||
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// Most backends don't have silent checks like this one, but we need it because WndProc are called early in CreateWindow().
|
||||
// We silently allow both context or just only backend data to be nullptr.
|
||||
if (ImGui::GetCurrentContext() == NULL)
|
||||
return 0;
|
||||
return ImGui_ImplWin32_WndProcHandlerEx(hwnd, msg, wParam, lParam, ImGui::GetIO());
|
||||
}
|
||||
|
||||
// This version is in theory thread-safe in the sense that no path should access ImGui::GetCurrentContext().
|
||||
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandlerEx(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, ImGuiIO& io)
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
if (bd == NULL)
|
||||
return 0;
|
||||
switch (msg)
|
||||
{
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_NCMOUSEMOVE:
|
||||
{
|
||||
// We need to call TrackMouseEvent in order to receive WM_MOUSELEAVE events
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
const int area = (msg == WM_MOUSEMOVE) ? 1 : 2;
|
||||
bd->MouseHwnd = hwnd;
|
||||
if (bd->MouseTrackedArea != area)
|
||||
{
|
||||
TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 };
|
||||
TRACKMOUSEEVENT tme_track = { sizeof(tme_track), (DWORD)((area == 2) ? (TME_LEAVE | TME_NONCLIENT) : TME_LEAVE), hwnd, 0 };
|
||||
if (bd->MouseTrackedArea != 0)
|
||||
::TrackMouseEvent(&tme_cancel);
|
||||
::TrackMouseEvent(&tme_track);
|
||||
bd->MouseTrackedArea = area;
|
||||
}
|
||||
POINT mouse_pos = { (LONG)GET_X_LPARAM(lParam), (LONG)GET_Y_LPARAM(lParam) };
|
||||
if (msg == WM_NCMOUSEMOVE && ::ScreenToClient(hwnd, &mouse_pos) == FALSE) // WM_NCMOUSEMOVE are provided in absolute coordinates.
|
||||
return 0;
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMousePosEvent((float)mouse_pos.x, (float)mouse_pos.y);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSELEAVE:
|
||||
case WM_NCMOUSELEAVE:
|
||||
{
|
||||
const int area = (msg == WM_MOUSELEAVE) ? 1 : 2;
|
||||
if (bd->MouseTrackedArea == area)
|
||||
{
|
||||
if (bd->MouseHwnd == hwnd)
|
||||
bd->MouseHwnd = nullptr;
|
||||
bd->MouseTrackedArea = 0;
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
if (bd->MouseHwnd == hwnd && bd->MouseTrackedArea != 0)
|
||||
{
|
||||
TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 };
|
||||
::TrackMouseEvent(&tme_cancel);
|
||||
bd->MouseHwnd = nullptr;
|
||||
bd->MouseTrackedArea = 0;
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
return 0;
|
||||
case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK:
|
||||
case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK:
|
||||
case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK:
|
||||
case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK:
|
||||
{
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
int button = 0;
|
||||
if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONDBLCLK) { button = 0; }
|
||||
if (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONDBLCLK) { button = 1; }
|
||||
if (msg == WM_MBUTTONDOWN || msg == WM_MBUTTONDBLCLK) { button = 2; }
|
||||
if (msg == WM_XBUTTONDOWN || msg == WM_XBUTTONDBLCLK) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
|
||||
if (bd->MouseButtonsDown == 0 && ::GetCapture() == nullptr)
|
||||
::SetCapture(hwnd); // Allow us to read mouse coordinates when dragging mouse outside of our window bounds.
|
||||
bd->MouseButtonsDown |= 1 << button;
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMouseButtonEvent(button, true);
|
||||
return 0;
|
||||
}
|
||||
case WM_LBUTTONUP:
|
||||
case WM_RBUTTONUP:
|
||||
case WM_MBUTTONUP:
|
||||
case WM_XBUTTONUP:
|
||||
{
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
int button = 0;
|
||||
if (msg == WM_LBUTTONUP) { button = 0; }
|
||||
if (msg == WM_RBUTTONUP) { button = 1; }
|
||||
if (msg == WM_MBUTTONUP) { button = 2; }
|
||||
if (msg == WM_XBUTTONUP) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
|
||||
bd->MouseButtonsDown &= ~(1 << button);
|
||||
if (bd->MouseButtonsDown == 0 && ::GetCapture() == hwnd)
|
||||
::ReleaseCapture();
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMouseButtonEvent(button, false);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEWHEEL:
|
||||
io.AddMouseWheelEvent(0.0f, (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
|
||||
return 0;
|
||||
case WM_MOUSEHWHEEL:
|
||||
io.AddMouseWheelEvent(-(float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f);
|
||||
return 0;
|
||||
case WM_KEYDOWN:
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYDOWN:
|
||||
case WM_SYSKEYUP:
|
||||
{
|
||||
const bool is_key_down = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
|
||||
if (wParam < 256)
|
||||
{
|
||||
// Submit modifiers
|
||||
ImGui_ImplWin32_UpdateKeyModifiers(io);
|
||||
|
||||
// Obtain virtual key code and convert to ImGuiKey
|
||||
const ImGuiKey key = ImGui_ImplWin32_KeyEventToImGuiKey(wParam, lParam);
|
||||
const int vk = (int)wParam;
|
||||
const int scancode = (int)LOBYTE(HIWORD(lParam));
|
||||
|
||||
// Special behavior for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit the key down event.
|
||||
if (key == ImGuiKey_PrintScreen && !is_key_down)
|
||||
ImGui_ImplWin32_AddKeyEvent(io, key, true, vk, scancode);
|
||||
|
||||
// Submit key event
|
||||
if (key != ImGuiKey_None)
|
||||
ImGui_ImplWin32_AddKeyEvent(io, key, is_key_down, vk, scancode);
|
||||
|
||||
// Submit individual left/right modifier events
|
||||
if (vk == VK_SHIFT)
|
||||
{
|
||||
// Important: Shift keys tend to get stuck when pressed together, missing key-up events are corrected in ImGui_ImplWin32_ProcessKeyEventsWorkarounds()
|
||||
if (IsVkDown(VK_LSHIFT) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftShift, is_key_down, VK_LSHIFT, scancode); }
|
||||
if (IsVkDown(VK_RSHIFT) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightShift, is_key_down, VK_RSHIFT, scancode); }
|
||||
}
|
||||
else if (vk == VK_CONTROL)
|
||||
{
|
||||
if (IsVkDown(VK_LCONTROL) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftCtrl, is_key_down, VK_LCONTROL, scancode); }
|
||||
if (IsVkDown(VK_RCONTROL) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightCtrl, is_key_down, VK_RCONTROL, scancode); }
|
||||
}
|
||||
else if (vk == VK_MENU)
|
||||
{
|
||||
if (IsVkDown(VK_LMENU) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftAlt, is_key_down, VK_LMENU, scancode); }
|
||||
if (IsVkDown(VK_RMENU) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightAlt, is_key_down, VK_RMENU, scancode); }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_SETFOCUS:
|
||||
case WM_KILLFOCUS:
|
||||
io.AddFocusEvent(msg == WM_SETFOCUS);
|
||||
return 0;
|
||||
case WM_INPUTLANGCHANGE:
|
||||
ImGui_ImplWin32_UpdateKeyboardCodePage(io);
|
||||
return 0;
|
||||
case WM_CHAR:
|
||||
if (::IsWindowUnicode(hwnd))
|
||||
{
|
||||
// You can also use ToAscii()+GetKeyboardState() to retrieve characters.
|
||||
if (wParam > 0 && wParam < 0x10000)
|
||||
io.AddInputCharacterUTF16((unsigned short)wParam);
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t wch = 0;
|
||||
::MultiByteToWideChar(bd->KeyboardCodePage, MB_PRECOMPOSED, (char*)&wParam, 1, &wch, 1);
|
||||
io.AddInputCharacter(wch);
|
||||
}
|
||||
return 0;
|
||||
case WM_SETCURSOR:
|
||||
// This is required to restore cursor when transitioning from e.g resize borders to client area.
|
||||
if (LOWORD(lParam) == HTCLIENT && ImGui_ImplWin32_UpdateMouseCursor(io, bd->LastMouseCursor))
|
||||
return 1;
|
||||
return 0;
|
||||
case WM_DEVICECHANGE:
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
if ((UINT)wParam == DBT_DEVNODES_CHANGED)
|
||||
bd->WantUpdateHasGamepad = true;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
// DPI-related helpers (optional)
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
// - Use to enable DPI awareness without having to create an application manifest.
|
||||
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
|
||||
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
|
||||
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
|
||||
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// This is the scheme successfully used by GLFW (from which we borrowed some of the code) and other apps aiming to be highly portable.
|
||||
// ImGui_ImplWin32_EnableDpiAwareness() is just a helper called by main.cpp, we don't call it automatically.
|
||||
// If you are trying to implement your own backend for your own engine, you may ignore that noise.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Perform our own check with RtlVerifyVersionInfo() instead of using functions from <VersionHelpers.h> as they
|
||||
// require a manifest to be functional for checks above 8.1. See https://github.com/ocornut/imgui/issues/4200
|
||||
static BOOL _IsWindowsVersionOrGreater(WORD major, WORD minor, WORD)
|
||||
{
|
||||
typedef LONG(WINAPI* PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*, ULONG, ULONGLONG);
|
||||
static PFN_RtlVerifyVersionInfo RtlVerifyVersionInfoFn = nullptr;
|
||||
if (RtlVerifyVersionInfoFn == nullptr)
|
||||
if (HMODULE ntdllModule = ::GetModuleHandleA("ntdll.dll"))
|
||||
RtlVerifyVersionInfoFn = (PFN_RtlVerifyVersionInfo)GetProcAddress(ntdllModule, "RtlVerifyVersionInfo");
|
||||
if (RtlVerifyVersionInfoFn == nullptr)
|
||||
return FALSE;
|
||||
|
||||
RTL_OSVERSIONINFOEXW versionInfo = { };
|
||||
ULONGLONG conditionMask = 0;
|
||||
versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);
|
||||
versionInfo.dwMajorVersion = major;
|
||||
versionInfo.dwMinorVersion = minor;
|
||||
VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL);
|
||||
VER_SET_CONDITION(conditionMask, VER_MINORVERSION, VER_GREATER_EQUAL);
|
||||
return (RtlVerifyVersionInfoFn(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, conditionMask) == 0) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
#define _IsWindowsVistaOrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0600), LOBYTE(0x0600), 0) // _WIN32_WINNT_VISTA
|
||||
#define _IsWindows8OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0602), LOBYTE(0x0602), 0) // _WIN32_WINNT_WIN8
|
||||
#define _IsWindows8Point1OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0603), LOBYTE(0x0603), 0) // _WIN32_WINNT_WINBLUE
|
||||
#define _IsWindows10OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0A00), LOBYTE(0x0A00), 0) // _WIN32_WINNT_WINTHRESHOLD / _WIN32_WINNT_WIN10
|
||||
|
||||
#ifndef DPI_ENUMS_DECLARED
|
||||
typedef enum { PROCESS_DPI_UNAWARE = 0, PROCESS_SYSTEM_DPI_AWARE = 1, PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS;
|
||||
typedef enum { MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2, MDT_DEFAULT = MDT_EFFECTIVE_DPI } MONITOR_DPI_TYPE;
|
||||
#endif
|
||||
#ifndef _DPI_AWARENESS_CONTEXTS_
|
||||
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE (DPI_AWARENESS_CONTEXT)-3
|
||||
#endif
|
||||
#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (DPI_AWARENESS_CONTEXT)-4
|
||||
#endif
|
||||
typedef HRESULT(WINAPI* PFN_SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS); // Shcore.lib + dll, Windows 8.1+
|
||||
typedef HRESULT(WINAPI* PFN_GetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); // Shcore.lib + dll, Windows 8.1+
|
||||
typedef DPI_AWARENESS_CONTEXT(WINAPI* PFN_SetThreadDpiAwarenessContext)(DPI_AWARENESS_CONTEXT); // User32.lib + dll, Windows 10 v1607+ (Creators Update)
|
||||
|
||||
// Helper function to enable DPI awareness without setting up a manifest
|
||||
void ImGui_ImplWin32_EnableDpiAwareness()
|
||||
{
|
||||
if (_IsWindows10OrGreater())
|
||||
{
|
||||
static HINSTANCE user32_dll = ::LoadLibraryA("user32.dll"); // Reference counted per-process
|
||||
if (PFN_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContextFn = (PFN_SetThreadDpiAwarenessContext)::GetProcAddress(user32_dll, "SetThreadDpiAwarenessContext"))
|
||||
{
|
||||
SetThreadDpiAwarenessContextFn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_IsWindows8Point1OrGreater())
|
||||
{
|
||||
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
|
||||
if (PFN_SetProcessDpiAwareness SetProcessDpiAwarenessFn = (PFN_SetProcessDpiAwareness)::GetProcAddress(shcore_dll, "SetProcessDpiAwareness"))
|
||||
{
|
||||
SetProcessDpiAwarenessFn(PROCESS_PER_MONITOR_DPI_AWARE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#if _WIN32_WINNT >= 0x0600
|
||||
::SetProcessDPIAware();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_MSC_VER) && !defined(NOGDI)
|
||||
#pragma comment(lib, "gdi32") // Link with gdi32.lib for GetDeviceCaps(). MinGW will require linking with '-lgdi32'
|
||||
#endif
|
||||
|
||||
float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor)
|
||||
{
|
||||
UINT xdpi = 96, ydpi = 96;
|
||||
if (_IsWindows8Point1OrGreater())
|
||||
{
|
||||
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
|
||||
static PFN_GetDpiForMonitor GetDpiForMonitorFn = nullptr;
|
||||
if (GetDpiForMonitorFn == nullptr && shcore_dll != nullptr)
|
||||
GetDpiForMonitorFn = (PFN_GetDpiForMonitor)::GetProcAddress(shcore_dll, "GetDpiForMonitor");
|
||||
if (GetDpiForMonitorFn != nullptr)
|
||||
{
|
||||
GetDpiForMonitorFn((HMONITOR)monitor, MDT_EFFECTIVE_DPI, &xdpi, &ydpi);
|
||||
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
|
||||
return xdpi / 96.0f;
|
||||
}
|
||||
}
|
||||
#ifndef NOGDI
|
||||
const HDC dc = ::GetDC(nullptr);
|
||||
xdpi = ::GetDeviceCaps(dc, LOGPIXELSX);
|
||||
ydpi = ::GetDeviceCaps(dc, LOGPIXELSY);
|
||||
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
|
||||
::ReleaseDC(nullptr, dc);
|
||||
#endif
|
||||
return xdpi / 96.0f;
|
||||
}
|
||||
|
||||
float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd)
|
||||
{
|
||||
HMONITOR monitor = ::MonitorFromWindow((HWND)hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
return ImGui_ImplWin32_GetDpiScaleForMonitor(monitor);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// Transparency related helpers (optional)
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#pragma comment(lib, "dwmapi") // Link with dwmapi.lib. MinGW will require linking with '-ldwmapi'
|
||||
#endif
|
||||
|
||||
// [experimental]
|
||||
// Borrowed from GLFW's function updateFramebufferTransparency() in src/win32_window.c
|
||||
// (the Dwm* functions are Vista era functions but we are borrowing logic from GLFW)
|
||||
void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd)
|
||||
{
|
||||
if (!_IsWindowsVistaOrGreater())
|
||||
return;
|
||||
|
||||
BOOL composition;
|
||||
if (FAILED(::DwmIsCompositionEnabled(&composition)) || !composition)
|
||||
return;
|
||||
|
||||
BOOL opaque;
|
||||
DWORD color;
|
||||
if (_IsWindows8OrGreater() || (SUCCEEDED(::DwmGetColorizationColor(&color, &opaque)) && !opaque))
|
||||
{
|
||||
HRGN region = ::CreateRectRgn(0, 0, -1, -1);
|
||||
DWM_BLURBEHIND bb = {};
|
||||
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
|
||||
bb.hRgnBlur = region;
|
||||
bb.fEnable = TRUE;
|
||||
::DwmEnableBlurBehindWindow((HWND)hwnd, &bb);
|
||||
::DeleteObject(region);
|
||||
}
|
||||
else
|
||||
{
|
||||
DWM_BLURBEHIND bb = {};
|
||||
bb.dwFlags = DWM_BB_ENABLE;
|
||||
::DwmEnableBlurBehindWindow((HWND)hwnd, &bb);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
// dear imgui: Platform Backend for Windows (standard windows API for 32-bits AND 64-bits applications)
|
||||
// This needs to be used along with a Renderer (e.g. DirectX11, OpenGL3, Vulkan..)
|
||||
|
||||
// Implemented features:
|
||||
// [X] Platform: Clipboard support (for Win32 this is actually part of core dear imgui)
|
||||
// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen.
|
||||
// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy VK_* values are obsolete since 1.87 and not supported since 1.91.5]
|
||||
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
|
||||
// [X] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'.
|
||||
|
||||
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
|
||||
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
|
||||
// Learn about Dear ImGui:
|
||||
// - FAQ https://dearimgui.com/faq
|
||||
// - Getting Started https://dearimgui.com/getting-started
|
||||
// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
|
||||
// - Introduction, links and more at the top of imgui.cpp
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_IMPL_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
// Follow "Getting Started" link and check examples/ folder to learn about using backends!
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_Init(void* hwnd);
|
||||
IMGUI_IMPL_API bool ImGui_ImplWin32_InitForOpenGL(void* hwnd);
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_Shutdown();
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_NewFrame();
|
||||
|
||||
// Win32 message handler your application need to call.
|
||||
// - Intentionally commented out in a '#if 0' block to avoid dragging dependencies on <windows.h> from this helper.
|
||||
// - You should COPY the line below into your .cpp code to forward declare the function and then you can call it.
|
||||
// - Call from your application's message handler. Keep calling your message handler unless this function returns TRUE.
|
||||
|
||||
#if 0
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
#endif
|
||||
|
||||
// DPI-related helpers (optional)
|
||||
// - Use to enable DPI awareness without having to create an application manifest.
|
||||
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
|
||||
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
|
||||
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
|
||||
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_EnableDpiAwareness();
|
||||
IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd); // HWND hwnd
|
||||
IMGUI_IMPL_API float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor); // HMONITOR monitor
|
||||
|
||||
// Transparency related helpers (optional) [experimental]
|
||||
// - Use to enable alpha compositing transparency with the desktop.
|
||||
// - Use together with e.g. clearing your framebuffer with zero-alpha.
|
||||
IMGUI_IMPL_API void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd); // HWND hwnd
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
-3578
File diff suppressed because it is too large.
Load diff
-4467
File diff suppressed because it is too large.
Load diff
-10310
File diff suppressed because it is too large.
Load diff
-627
@@ -1,627 +0,0 @@
|
||||
// [DEAR IMGUI]
|
||||
// This is a slightly modified version of stb_rect_pack.h 1.01.
|
||||
// Grep for [DEAR IMGUI] to find the changes.
|
||||
//
|
||||
// stb_rect_pack.h - v1.01 - public domain - rectangle packing
|
||||
// Sean Barrett 2014
|
||||
//
|
||||
// Useful for e.g. packing rectangular textures into an atlas.
|
||||
// Does not do rotation.
|
||||
//
|
||||
// Before #including,
|
||||
//
|
||||
// #define STB_RECT_PACK_IMPLEMENTATION
|
||||
//
|
||||
// in the file that you want to have the implementation.
|
||||
//
|
||||
// Not necessarily the awesomest packing method, but better than
|
||||
// the totally naive one in stb_truetype (which is primarily what
|
||||
// this is meant to replace).
|
||||
//
|
||||
// Has only had a few tests run, may have issues.
|
||||
//
|
||||
// More docs to come.
|
||||
//
|
||||
// No memory allocations; uses qsort() and assert() from stdlib.
|
||||
// Can override those by defining STBRP_SORT and STBRP_ASSERT.
|
||||
//
|
||||
// This library currently uses the Skyline Bottom-Left algorithm.
|
||||
//
|
||||
// Please note: better rectangle packers are welcome! Please
|
||||
// implement them to the same API, but with a different init
|
||||
// function.
|
||||
//
|
||||
// Credits
|
||||
//
|
||||
// Library
|
||||
// Sean Barrett
|
||||
// Minor features
|
||||
// Martins Mozeiko
|
||||
// github:IntellectualKitty
|
||||
//
|
||||
// Bugfixes / warning fixes
|
||||
// Jeremy Jaussaud
|
||||
// Fabian Giesen
|
||||
//
|
||||
// Version history:
|
||||
//
|
||||
// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section
|
||||
// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles
|
||||
// 0.99 (2019-02-07) warning fixes
|
||||
// 0.11 (2017-03-03) return packing success/fail result
|
||||
// 0.10 (2016-10-25) remove cast-away-const to avoid warnings
|
||||
// 0.09 (2016-08-27) fix compiler warnings
|
||||
// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0)
|
||||
// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0)
|
||||
// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort
|
||||
// 0.05: added STBRP_ASSERT to allow replacing assert
|
||||
// 0.04: fixed minor bug in STBRP_LARGE_RECTS support
|
||||
// 0.01: initial release
|
||||
//
|
||||
// LICENSE
|
||||
//
|
||||
// See end of file for license information.
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// INCLUDE SECTION
|
||||
//
|
||||
|
||||
#ifndef STB_INCLUDE_STB_RECT_PACK_H
|
||||
#define STB_INCLUDE_STB_RECT_PACK_H
|
||||
|
||||
#define STB_RECT_PACK_VERSION 1
|
||||
|
||||
#ifdef STBRP_STATIC
|
||||
#define STBRP_DEF static
|
||||
#else
|
||||
#define STBRP_DEF extern
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct stbrp_context stbrp_context;
|
||||
typedef struct stbrp_node stbrp_node;
|
||||
typedef struct stbrp_rect stbrp_rect;
|
||||
|
||||
typedef int stbrp_coord;
|
||||
|
||||
#define STBRP__MAXVAL 0x7fffffff
|
||||
// Mostly for internal use, but this is the maximum supported coordinate value.
|
||||
|
||||
STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects);
|
||||
// Assign packed locations to rectangles. The rectangles are of type
|
||||
// 'stbrp_rect' defined below, stored in the array 'rects', and there
|
||||
// are 'num_rects' many of them.
|
||||
//
|
||||
// Rectangles which are successfully packed have the 'was_packed' flag
|
||||
// set to a non-zero value and 'x' and 'y' store the minimum location
|
||||
// on each axis (i.e. bottom-left in cartesian coordinates, top-left
|
||||
// if you imagine y increasing downwards). Rectangles which do not fit
|
||||
// have the 'was_packed' flag set to 0.
|
||||
//
|
||||
// You should not try to access the 'rects' array from another thread
|
||||
// while this function is running, as the function temporarily reorders
|
||||
// the array while it executes.
|
||||
//
|
||||
// To pack into another rectangle, you need to call stbrp_init_target
|
||||
// again. To continue packing into the same rectangle, you can call
|
||||
// this function again. Calling this multiple times with multiple rect
|
||||
// arrays will probably produce worse packing results than calling it
|
||||
// a single time with the full rectangle array, but the option is
|
||||
// available.
|
||||
//
|
||||
// The function returns 1 if all of the rectangles were successfully
|
||||
// packed and 0 otherwise.
|
||||
|
||||
struct stbrp_rect
|
||||
{
|
||||
// reserved for your use:
|
||||
int id;
|
||||
|
||||
// input:
|
||||
stbrp_coord w, h;
|
||||
|
||||
// output:
|
||||
stbrp_coord x, y;
|
||||
int was_packed; // non-zero if valid packing
|
||||
|
||||
}; // 16 bytes, nominally
|
||||
|
||||
|
||||
STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes);
|
||||
// Initialize a rectangle packer to:
|
||||
// pack a rectangle that is 'width' by 'height' in dimensions
|
||||
// using temporary storage provided by the array 'nodes', which is 'num_nodes' long
|
||||
//
|
||||
// You must call this function every time you start packing into a new target.
|
||||
//
|
||||
// There is no "shutdown" function. The 'nodes' memory must stay valid for
|
||||
// the following stbrp_pack_rects() call (or calls), but can be freed after
|
||||
// the call (or calls) finish.
|
||||
//
|
||||
// Note: to guarantee best results, either:
|
||||
// 1. make sure 'num_nodes' >= 'width'
|
||||
// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1'
|
||||
//
|
||||
// If you don't do either of the above things, widths will be quantized to multiples
|
||||
// of small integers to guarantee the algorithm doesn't run out of temporary storage.
|
||||
//
|
||||
// If you do #2, then the non-quantized algorithm will be used, but the algorithm
|
||||
// may run out of temporary storage and be unable to pack some rectangles.
|
||||
|
||||
STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem);
|
||||
// Optionally call this function after init but before doing any packing to
|
||||
// change the handling of the out-of-temp-memory scenario, described above.
|
||||
// If you call init again, this will be reset to the default (false).
|
||||
|
||||
|
||||
STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic);
|
||||
// Optionally select which packing heuristic the library should use. Different
|
||||
// heuristics will produce better/worse results for different data sets.
|
||||
// If you call init again, this will be reset to the default.
|
||||
|
||||
enum
|
||||
{
|
||||
STBRP_HEURISTIC_Skyline_default=0,
|
||||
STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default,
|
||||
STBRP_HEURISTIC_Skyline_BF_sortHeight
|
||||
};
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// the details of the following structures don't matter to you, but they must
|
||||
// be visible so you can handle the memory allocations for them
|
||||
|
||||
struct stbrp_node
|
||||
{
|
||||
stbrp_coord x,y;
|
||||
stbrp_node *next;
|
||||
};
|
||||
|
||||
struct stbrp_context
|
||||
{
|
||||
int width;
|
||||
int height;
|
||||
int align;
|
||||
int init_mode;
|
||||
int heuristic;
|
||||
int num_nodes;
|
||||
stbrp_node *active_head;
|
||||
stbrp_node *free_head;
|
||||
stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2'
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// IMPLEMENTATION SECTION
|
||||
//
|
||||
|
||||
#ifdef STB_RECT_PACK_IMPLEMENTATION
|
||||
#ifndef STBRP_SORT
|
||||
#include <stdlib.h>
|
||||
#define STBRP_SORT qsort
|
||||
#endif
|
||||
|
||||
#ifndef STBRP_ASSERT
|
||||
#include <assert.h>
|
||||
#define STBRP_ASSERT assert
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define STBRP__NOTUSED(v) (void)(v)
|
||||
#define STBRP__CDECL __cdecl
|
||||
#else
|
||||
#define STBRP__NOTUSED(v) (void)sizeof(v)
|
||||
#define STBRP__CDECL
|
||||
#endif
|
||||
|
||||
enum
|
||||
{
|
||||
STBRP__INIT_skyline = 1
|
||||
};
|
||||
|
||||
STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic)
|
||||
{
|
||||
switch (context->init_mode) {
|
||||
case STBRP__INIT_skyline:
|
||||
STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight);
|
||||
context->heuristic = heuristic;
|
||||
break;
|
||||
default:
|
||||
STBRP_ASSERT(0);
|
||||
}
|
||||
}
|
||||
|
||||
STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem)
|
||||
{
|
||||
if (allow_out_of_mem)
|
||||
// if it's ok to run out of memory, then don't bother aligning them;
|
||||
// this gives better packing, but may fail due to OOM (even though
|
||||
// the rectangles easily fit). @TODO a smarter approach would be to only
|
||||
// quantize once we've hit OOM, then we could get rid of this parameter.
|
||||
context->align = 1;
|
||||
else {
|
||||
// if it's not ok to run out of memory, then quantize the widths
|
||||
// so that num_nodes is always enough nodes.
|
||||
//
|
||||
// I.e. num_nodes * align >= width
|
||||
// align >= width / num_nodes
|
||||
// align = ceil(width/num_nodes)
|
||||
|
||||
context->align = (context->width + context->num_nodes-1) / context->num_nodes;
|
||||
}
|
||||
}
|
||||
|
||||
STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i=0; i < num_nodes-1; ++i)
|
||||
nodes[i].next = &nodes[i+1];
|
||||
nodes[i].next = NULL;
|
||||
context->init_mode = STBRP__INIT_skyline;
|
||||
context->heuristic = STBRP_HEURISTIC_Skyline_default;
|
||||
context->free_head = &nodes[0];
|
||||
context->active_head = &context->extra[0];
|
||||
context->width = width;
|
||||
context->height = height;
|
||||
context->num_nodes = num_nodes;
|
||||
stbrp_setup_allow_out_of_mem(context, 0);
|
||||
|
||||
// node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly)
|
||||
context->extra[0].x = 0;
|
||||
context->extra[0].y = 0;
|
||||
context->extra[0].next = &context->extra[1];
|
||||
context->extra[1].x = (stbrp_coord) width;
|
||||
context->extra[1].y = (1<<30);
|
||||
context->extra[1].next = NULL;
|
||||
}
|
||||
|
||||
// find minimum y position if it starts at x1
|
||||
static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste)
|
||||
{
|
||||
stbrp_node *node = first;
|
||||
int x1 = x0 + width;
|
||||
int min_y, visited_width, waste_area;
|
||||
|
||||
STBRP__NOTUSED(c);
|
||||
|
||||
STBRP_ASSERT(first->x <= x0);
|
||||
|
||||
#if 0
|
||||
// skip in case we're past the node
|
||||
while (node->next->x <= x0)
|
||||
++node;
|
||||
#else
|
||||
STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency
|
||||
#endif
|
||||
|
||||
STBRP_ASSERT(node->x <= x0);
|
||||
|
||||
min_y = 0;
|
||||
waste_area = 0;
|
||||
visited_width = 0;
|
||||
while (node->x < x1) {
|
||||
if (node->y > min_y) {
|
||||
// raise min_y higher.
|
||||
// we've accounted for all waste up to min_y,
|
||||
// but we'll now add more waste for everything we've visted
|
||||
waste_area += visited_width * (node->y - min_y);
|
||||
min_y = node->y;
|
||||
// the first time through, visited_width might be reduced
|
||||
if (node->x < x0)
|
||||
visited_width += node->next->x - x0;
|
||||
else
|
||||
visited_width += node->next->x - node->x;
|
||||
} else {
|
||||
// add waste area
|
||||
int under_width = node->next->x - node->x;
|
||||
if (under_width + visited_width > width)
|
||||
under_width = width - visited_width;
|
||||
waste_area += under_width * (min_y - node->y);
|
||||
visited_width += under_width;
|
||||
}
|
||||
node = node->next;
|
||||
}
|
||||
|
||||
*pwaste = waste_area;
|
||||
return min_y;
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int x,y;
|
||||
stbrp_node **prev_link;
|
||||
} stbrp__findresult;
|
||||
|
||||
static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height)
|
||||
{
|
||||
int best_waste = (1<<30), best_x, best_y = (1 << 30);
|
||||
stbrp__findresult fr;
|
||||
stbrp_node **prev, *node, *tail, **best = NULL;
|
||||
|
||||
// align to multiple of c->align
|
||||
width = (width + c->align - 1);
|
||||
width -= width % c->align;
|
||||
STBRP_ASSERT(width % c->align == 0);
|
||||
|
||||
// if it can't possibly fit, bail immediately
|
||||
if (width > c->width || height > c->height) {
|
||||
fr.prev_link = NULL;
|
||||
fr.x = fr.y = 0;
|
||||
return fr;
|
||||
}
|
||||
|
||||
node = c->active_head;
|
||||
prev = &c->active_head;
|
||||
while (node->x + width <= c->width) {
|
||||
int y,waste;
|
||||
y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste);
|
||||
if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL
|
||||
// bottom left
|
||||
if (y < best_y) {
|
||||
best_y = y;
|
||||
best = prev;
|
||||
}
|
||||
} else {
|
||||
// best-fit
|
||||
if (y + height <= c->height) {
|
||||
// can only use it if it first vertically
|
||||
if (y < best_y || (y == best_y && waste < best_waste)) {
|
||||
best_y = y;
|
||||
best_waste = waste;
|
||||
best = prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = &node->next;
|
||||
node = node->next;
|
||||
}
|
||||
|
||||
best_x = (best == NULL) ? 0 : (*best)->x;
|
||||
|
||||
// if doing best-fit (BF), we also have to try aligning right edge to each node position
|
||||
//
|
||||
// e.g, if fitting
|
||||
//
|
||||
// ____________________
|
||||
// |____________________|
|
||||
//
|
||||
// into
|
||||
//
|
||||
// | |
|
||||
// | ____________|
|
||||
// |____________|
|
||||
//
|
||||
// then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned
|
||||
//
|
||||
// This makes BF take about 2x the time
|
||||
|
||||
if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) {
|
||||
tail = c->active_head;
|
||||
node = c->active_head;
|
||||
prev = &c->active_head;
|
||||
// find first node that's admissible
|
||||
while (tail->x < width)
|
||||
tail = tail->next;
|
||||
while (tail) {
|
||||
int xpos = tail->x - width;
|
||||
int y,waste;
|
||||
STBRP_ASSERT(xpos >= 0);
|
||||
// find the left position that matches this
|
||||
while (node->next->x <= xpos) {
|
||||
prev = &node->next;
|
||||
node = node->next;
|
||||
}
|
||||
STBRP_ASSERT(node->next->x > xpos && node->x <= xpos);
|
||||
y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste);
|
||||
if (y + height <= c->height) {
|
||||
if (y <= best_y) {
|
||||
if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) {
|
||||
best_x = xpos;
|
||||
//STBRP_ASSERT(y <= best_y); [DEAR IMGUI]
|
||||
best_y = y;
|
||||
best_waste = waste;
|
||||
best = prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
tail = tail->next;
|
||||
}
|
||||
}
|
||||
|
||||
fr.prev_link = best;
|
||||
fr.x = best_x;
|
||||
fr.y = best_y;
|
||||
return fr;
|
||||
}
|
||||
|
||||
static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height)
|
||||
{
|
||||
// find best position according to heuristic
|
||||
stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height);
|
||||
stbrp_node *node, *cur;
|
||||
|
||||
// bail if:
|
||||
// 1. it failed
|
||||
// 2. the best node doesn't fit (we don't always check this)
|
||||
// 3. we're out of memory
|
||||
if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) {
|
||||
res.prev_link = NULL;
|
||||
return res;
|
||||
}
|
||||
|
||||
// on success, create new node
|
||||
node = context->free_head;
|
||||
node->x = (stbrp_coord) res.x;
|
||||
node->y = (stbrp_coord) (res.y + height);
|
||||
|
||||
context->free_head = node->next;
|
||||
|
||||
// insert the new node into the right starting point, and
|
||||
// let 'cur' point to the remaining nodes needing to be
|
||||
// stiched back in
|
||||
|
||||
cur = *res.prev_link;
|
||||
if (cur->x < res.x) {
|
||||
// preserve the existing one, so start testing with the next one
|
||||
stbrp_node *next = cur->next;
|
||||
cur->next = node;
|
||||
cur = next;
|
||||
} else {
|
||||
*res.prev_link = node;
|
||||
}
|
||||
|
||||
// from here, traverse cur and free the nodes, until we get to one
|
||||
// that shouldn't be freed
|
||||
while (cur->next && cur->next->x <= res.x + width) {
|
||||
stbrp_node *next = cur->next;
|
||||
// move the current node to the free list
|
||||
cur->next = context->free_head;
|
||||
context->free_head = cur;
|
||||
cur = next;
|
||||
}
|
||||
|
||||
// stitch the list back in
|
||||
node->next = cur;
|
||||
|
||||
if (cur->x < res.x + width)
|
||||
cur->x = (stbrp_coord) (res.x + width);
|
||||
|
||||
#ifdef _DEBUG
|
||||
cur = context->active_head;
|
||||
while (cur->x < context->width) {
|
||||
STBRP_ASSERT(cur->x < cur->next->x);
|
||||
cur = cur->next;
|
||||
}
|
||||
STBRP_ASSERT(cur->next == NULL);
|
||||
|
||||
{
|
||||
int count=0;
|
||||
cur = context->active_head;
|
||||
while (cur) {
|
||||
cur = cur->next;
|
||||
++count;
|
||||
}
|
||||
cur = context->free_head;
|
||||
while (cur) {
|
||||
cur = cur->next;
|
||||
++count;
|
||||
}
|
||||
STBRP_ASSERT(count == context->num_nodes+2);
|
||||
}
|
||||
#endif
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static int STBRP__CDECL rect_height_compare(const void *a, const void *b)
|
||||
{
|
||||
const stbrp_rect *p = (const stbrp_rect *) a;
|
||||
const stbrp_rect *q = (const stbrp_rect *) b;
|
||||
if (p->h > q->h)
|
||||
return -1;
|
||||
if (p->h < q->h)
|
||||
return 1;
|
||||
return (p->w > q->w) ? -1 : (p->w < q->w);
|
||||
}
|
||||
|
||||
static int STBRP__CDECL rect_original_order(const void *a, const void *b)
|
||||
{
|
||||
const stbrp_rect *p = (const stbrp_rect *) a;
|
||||
const stbrp_rect *q = (const stbrp_rect *) b;
|
||||
return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed);
|
||||
}
|
||||
|
||||
STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects)
|
||||
{
|
||||
int i, all_rects_packed = 1;
|
||||
|
||||
// we use the 'was_packed' field internally to allow sorting/unsorting
|
||||
for (i=0; i < num_rects; ++i) {
|
||||
rects[i].was_packed = i;
|
||||
}
|
||||
|
||||
// sort according to heuristic
|
||||
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare);
|
||||
|
||||
for (i=0; i < num_rects; ++i) {
|
||||
if (rects[i].w == 0 || rects[i].h == 0) {
|
||||
rects[i].x = rects[i].y = 0; // empty rect needs no space
|
||||
} else {
|
||||
stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h);
|
||||
if (fr.prev_link) {
|
||||
rects[i].x = (stbrp_coord) fr.x;
|
||||
rects[i].y = (stbrp_coord) fr.y;
|
||||
} else {
|
||||
rects[i].x = rects[i].y = STBRP__MAXVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unsort
|
||||
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order);
|
||||
|
||||
// set was_packed flags and all_rects_packed status
|
||||
for (i=0; i < num_rects; ++i) {
|
||||
rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL);
|
||||
if (!rects[i].was_packed)
|
||||
all_rects_packed = 0;
|
||||
}
|
||||
|
||||
// return the all_rects_packed status
|
||||
return all_rects_packed;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
This software is available under 2 licenses -- choose whichever you prefer.
|
||||
------------------------------------------------------------------------------
|
||||
ALTERNATIVE A - MIT License
|
||||
Copyright (c) 2017 Sean Barrett
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
------------------------------------------------------------------------------
|
||||
ALTERNATIVE B - Public Domain (www.unlicense.org)
|
||||
This is free and unencumbered software released into the public domain.
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
|
||||
software, either in source code form or as a compiled binary, for any purpose,
|
||||
commercial or non-commercial, and by any means.
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this
|
||||
software dedicate any and all copyright interest in the software to the public
|
||||
domain. We make this dedication for the benefit of the public at large and to
|
||||
the detriment of our heirs and successors. We intend this dedication to be an
|
||||
overt act of relinquishment in perpetuity of all present and future rights to
|
||||
this software under copyright law.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
-1469
File diff suppressed because it is too large.
Load diff
-5085
File diff suppressed because it is too large.
Load diff
-24765
File diff suppressed because it is too large.
Load diff
@@ -1,26 +0,0 @@
|
||||
# Builds the release and packs what players download: dist\bbbridge-<version>-windows.zip
|
||||
# holding a "bbbridge" folder to drop into bbport's folder (next to Bloodborne.exe).
|
||||
# Never includes an account file, logs or build output.
|
||||
# powershell -ExecutionPolicy Bypass -File tools\make_release.ps1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$vc = 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat'
|
||||
if (-not (Test-Path $vc)) {
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
$vc = Join-Path (& $vswhere -latest -products * -property installationPath) 'VC\Auxiliary\Build\vcvars64.bat'
|
||||
}
|
||||
cmd /c "`"$vc`" >nul && cd /d `"$root`" && cmake --preset x64-release >nul && cmake --build --preset x64-release --target bbbridge"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'build failed' }
|
||||
|
||||
$version = (Select-String -Path "$root\CMakeLists.txt" -Pattern 'project\(bbbridge VERSION ([0-9.]+)').Matches[0].Groups[1].Value
|
||||
$stage = Join-Path $root "dist\stage\bbbridge"
|
||||
Remove-Item (Join-Path $root 'dist\stage') -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force $stage | Out-Null
|
||||
Copy-Item "$root\build\x64-release\bbbridge.exe", "$root\README.md", "$root\CHANGELOG.md", "$root\LICENSE", "$root\THIRD_PARTY_NOTICES.txt" $stage
|
||||
$zip = Join-Path $root "dist\bbbridge-$version-windows.zip"
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $stage -DestinationPath $zip
|
||||
Remove-Item (Join-Path $root 'dist\stage') -Recurse -Force
|
||||
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
"$hash $(Split-Path -Leaf $zip)" | Set-Content -Encoding ascii "$zip.sha256"
|
||||
Write-Output "$zip ($([math]::Round((Get-Item $zip).Length / 1MB, 2)) MB) sha256 $hash"
|
||||
@@ -28,6 +28,12 @@ pgo=${BB_PGO:-}
|
||||
if [[ -z $pgo ]]; then
|
||||
if [[ -n $(find pgo -name '*.gcda' -print -quit 2>/dev/null) ]]; then pgo=use; else pgo=off; fi
|
||||
fi
|
||||
# A profile only applies when its files are named as this build names them (relative to out/gpu,
|
||||
# -fprofile-prefix-path): one recorded elsewhere matched nothing and PGO was silently off.
|
||||
if [[ $pgo == use && -z $(find pgo -name 'CMakeFiles#bbgpu.dir#*.gcda' -print -quit 2>/dev/null) ]]; then
|
||||
echo 'PGO: the profile in pgo/ is from another build layout (rebuild it: BB_PGO=generate); built without it' >&2
|
||||
pgo=off
|
||||
fi
|
||||
mkdir -p pgo
|
||||
# Submodules (git clone --recursive, or: git submodule update --init) and this port's changes
|
||||
# to FSR-Vulkan (gpu/patches/fsr-vulkan), applied to its working tree once.
|
||||
@@ -50,36 +56,34 @@ 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
|
||||
if ! ninja -C out/gpu bbgpu $(grep -q "^build bbnet20:" out/gpu/build.ninja && echo bbnet20) > out/gpu-build.log 2>&1; then
|
||||
grep -v '^\[' out/gpu-build.log | tail -40 >&2
|
||||
echo 'GPU library build failed (full log: out/gpu-build.log)' >&2; exit 1
|
||||
fi
|
||||
# Optional DLSS (NVIDIA RTX): DLSS_SDK_ROOT=<github.com/NVIDIA/DLSS checkout> builds the bridge
|
||||
# (gpu/dlss_bridge, the only code using NVIDIA's SDK) and puts it next to bb-probe with NVIDIA's
|
||||
# libnvidia-ngx-dlss.so. Without them the DLSS upscaler is listed as unavailable.
|
||||
# libnvidia-ngx-dlss.so (Super Resolution) and libnvidia-ngx-dlssg.so (Frame Generation, GeForce
|
||||
# RTX 40 and newer; SDK 310.7 or newer has it). Without them the DLSS upscaler and DLSS Frame
|
||||
# Generation are listed as unavailable.
|
||||
if [[ -n ${DLSS_SDK_ROOT:-} ]]; then
|
||||
cmake -S gpu/dlss_bridge -B out/dlss-bridge -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DDLSS_SDK_ROOT="$DLSS_SDK_ROOT" >/dev/null
|
||||
ninja -C out/dlss-bridge >/dev/null
|
||||
rm -f out/libnvidia-ngx-dlss.so.*
|
||||
rm -f out/libnvidia-ngx-dlss.so.* out/libnvidia-ngx-dlssg.so.*
|
||||
cp out/dlss-bridge/libbbport_dlss.so "$DLSS_SDK_ROOT"/lib/Linux_x86_64/rel/libnvidia-ngx-dlss.so.* out/
|
||||
if ls "$DLSS_SDK_ROOT"/lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.* >/dev/null 2>&1; then
|
||||
cp "$DLSS_SDK_ROOT"/lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.* out/
|
||||
echo "DLSS Frame Generation library: $(ls out/libnvidia-ngx-dlssg.so.*)"
|
||||
else
|
||||
echo "DLSS Frame Generation library not in this SDK (lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.*): DLSS Super Resolution only"
|
||||
fi
|
||||
cp "$DLSS_SDK_ROOT/LICENSE.txt" out/NVIDIA-DLSS-LICENSE.txt
|
||||
echo "DLSS bridge: out/libbbport_dlss.so"
|
||||
fi
|
||||
# The Hunter's Dream co-op pool (launcher: Online): bbbridge (MaskZeus, GPL-3.0-or-later) stands in
|
||||
# for the shadNet server on this PC and talks to The Hunter's Dream as bbhost does. Needs libcurl and
|
||||
# OpenSSL's development files; without them the launcher offers shadNet servers only.
|
||||
if pkg-config --exists libcurl openssl; then
|
||||
if cmake -S bbbridge -B out/bbbridge -G Ninja -DCMAKE_BUILD_TYPE=Release >out/bbbridge-build.log 2>&1 &&
|
||||
ninja -C out/bbbridge bbbridge-cli >>out/bbbridge-build.log 2>&1; then
|
||||
echo "The Hunter's Dream bridge: out/bbbridge/bbbridge-cli"
|
||||
else
|
||||
echo "bbbridge build failed (out/bbbridge-build.log): The Hunter's Dream is not offered" >&2
|
||||
fi
|
||||
fi
|
||||
# Installing from PS4 packages (launcher: Install from PKG, scripts/pkg_install.py): pkgextract
|
||||
# (github.com/paulomanrique/ps4-pkg-extractor, Unlicense) at a reviewed revision, with its
|
||||
# Cargo.lock. Needs cargo; without it the launcher asks for a game folder only.
|
||||
@@ -110,9 +114,13 @@ echo "Built $PWD/out/bb-probe"
|
||||
if [[ ${1:-} == --test ]]; then
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -lm -o out/pad-test
|
||||
out/pad-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad_atlas.c "${libraries[@]}" -lm -o out/pad-atlas-test
|
||||
out/pad-atlas-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_runtime.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/runtime-test
|
||||
out/runtime-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_file_mods.c -o out/file-mods-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -Isrc tests/test_movbe.c src/runtime_movbe.c -o out/movbe-test
|
||||
out/movbe-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_file_mods.c src/runtime_remote.c -o out/file-mods-test
|
||||
out/file-mods-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_sema.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/sema-test
|
||||
out/sema-test
|
||||
@@ -120,4 +128,25 @@ if [[ ${1:-} == --test ]]; then
|
||||
out/online-test
|
||||
"$CC" -std=c11 -D_GNU_SOURCE -O2 -g -Wall -Wextra -Werror -I. -Isrc tests/test_content.c src/runtime_content.c -o out/content-test
|
||||
out/content-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_trophy.c src/runtime_trophy.c -o out/trophy-test
|
||||
out/trophy-test out/trophy_test_tmp
|
||||
# Hunter's Atlas: its guarded game-memory readers on synthetic images.
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -Isrc tests/test_map_location.c src/runtime_map.c -lm -o out/map-location-test
|
||||
out/map-location-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_atlas_state.c src/runtime_atlas_state.c -lm -o out/atlas-state-test
|
||||
out/atlas-state-test
|
||||
# Hunter's Atlas: its tile's state machine, map styles, floor interiors, controller shortcut.
|
||||
# (-Wno-misleading-indentation: Atlas's compact one-line style.) bbport (Linux fork):
|
||||
# nlohmann/json from the system, as gpu/CMakeLists.txt.
|
||||
atlas_cxx=(-std=c++20 -O2 -g -Wall -Wextra -Werror -Wno-misleading-indentation -Igpu/shim -Igpu/third_party/imgui "${includes[@]}")
|
||||
for test in test_bbport_atlas_menu_state test_bbport_atlas_style test_bbport_atlas_interiors; do
|
||||
"${CXX:-c++}" "${atlas_cxx[@]}" "tests/$test.cpp" -o "out/$test" || exit 1
|
||||
(cd out && "./$test") || exit 1
|
||||
done
|
||||
"${CXX:-c++}" "${atlas_cxx[@]}" tests/test_map_controller.cpp gpu/shim/bbport_map_controller.cpp "${libraries[@]}" -o out/map-controller-test
|
||||
out/map-controller-test
|
||||
"${CXX:-c++}" "${atlas_cxx[@]}" tests/test_atlas_features.cpp gpu/shim/bbport_map_features.cpp "${libraries[@]}" -o out/atlas-features-test
|
||||
rm -rf out/atlas-features && out/atlas-features-test out/atlas-features
|
||||
"${CXX:-c++}" "${atlas_cxx[@]}" tests/test_atlas_i18n.cpp -o out/atlas-i18n-test
|
||||
out/atlas-i18n-test
|
||||
fi
|
||||
@@ -1,273 +0,0 @@
|
||||
# Изменения в 0.4 (после 0.3, с 2026-10-06)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, Yahar'gul стоя, 1080p без лимита FPS; «4 ядра» — `CPUS=0-3,8-11`
|
||||
(4 ядра / 8 потоков, как у Steam Deck).
|
||||
|
||||
## Модель памяти ПК — снова по умолчанию, и для NVIDIA
|
||||
|
||||
- `BB_GUEST_IN_PLACE=1` по умолчанию (`run.sh`). В лаунчере вместо «Новой модели памяти» —
|
||||
переключатель *Старая модель памяти* (ключ `legacy_memory_model`): модель 0.2 как запасной вариант.
|
||||
- NVIDIA отображает dma-buf только со смещения 0. Раньше порт тогда возвращался к старой модели;
|
||||
теперь у таких драйверов каждое выделение прямой памяти игры — отдельный кусок памяти Vulkan
|
||||
(игра делает 14 выделений и каждое отображает один раз целиком, со смещения 0). Проверка при
|
||||
запуске пишет «the driver's dma-buf maps at offset 0 only: one chunk per direct memory allocation».
|
||||
`BB_GUEST_WHOLE_CHUNKS=1` включает этот режим и на других драйверах (проверка без NVIDIA):
|
||||
на RX 7800 XT 188–190 FPS, картинка в порядке. **На NVIDIA не проверено.**
|
||||
|
||||
## Уровень API: libGnm игры
|
||||
|
||||
Игра собрана со статической libGnm: пакеты PM4 пишет не драйвер, а сама игра, через функции
|
||||
`sce::Gnm::DrawCommandBuffer` / `DispatchCommandBuffer` / `ConstantCommandBuffer` в
|
||||
`0x1068000–0x10a0000` и `0x11fd000–0x11fe000`. Это и есть графический API игры — уровень, на
|
||||
котором работают слои вроде DXVK.
|
||||
|
||||
- `tools/re/`: поиск пакетов PM4 в коде (`pm4_scan.py`), граф вызовов по границам функций из
|
||||
`.eh_frame` (`callgraph.py`), сигнатуры функций по регистрам (`gnm_sigs.py`), таблица перехватов
|
||||
(`gen_gnm_table.py` → `gpu/shim/bbport_gnm_table.inc`), имена функций (`gnm_names.txt`).
|
||||
- `gpu/shim/bbport_gnm_hooks.cpp`: перехват 513 функций libGnm в начале (jmp на заглушку рядом с
|
||||
образом, перенесённый пролог), до запуска кода игры; каждая точка сверяется с eboot 1.09.
|
||||
- `BB_GNM_CENSUS=1` — счёт вызовов (`BB_GNM_CENSUS_FILE` — полный список в файл).
|
||||
- `BB_GNM_OBSERVE=1` — каждый метод командного буфера отмечает пакеты, которые записал;
|
||||
при отправке проверяется, откуда пришёл каждый пакет. Итог: **100,0 % пакетов (99,9 % слов)
|
||||
пишут известные вызовы API** — командный поток игры целиком описывается вызовами Gnm.
|
||||
- `BB_GNM_TRACE=адрес[:число]` — аргументы, вызывающий код и записанные пакеты.
|
||||
- Найдено: игра вызывает 99 методов командных буферов; обратный вызов резервирования
|
||||
`0x26af230` начинает новый кусок буфера (список кусков уходит в отправку отдельными DCB);
|
||||
`writeDataInline` (`0x1073b80`) из GX обнуляет 192 байта по разу на проход; метки EOP
|
||||
(`writeAtEndOfPipe`, значение 1) ставит распределитель памяти GPU, `waitOnAddress` ждёт их
|
||||
же — это барьер в порядке очереди, а не ожидание CPU.
|
||||
|
||||
## Быстрее на слабых процессорах
|
||||
|
||||
- `WRITE_DATA` и DMA ждали все отложенные копии хоста (22 мкс на каждый `WRITE_DATA`,
|
||||
~35 за кадр, 12 % времени потока записи отрисовок на 4 ядрах). Теперь ждут только копии,
|
||||
которые читают записываемый диапазон (`Scheduler::WaitHostCopiesFor`, таблица источников по
|
||||
64 КиБ); командный процессор PS4 тоже не ждёт предыдущих отрисовок перед такой записью.
|
||||
Ожидания: 37 → 2 за кадр. `BB_HOST_COPY_WAITS=all` — прежнее поведение.
|
||||
- Проверка одновременной записи в планировщик (`ProducerScope`) включается только с
|
||||
`BB_PRODUCER_CHECK=1` (лаунчер: *Диагностика вылетов*).
|
||||
- 4 ядра: 158 → 167 FPS.
|
||||
- `BB_SECTIONS=1` показывает и задачи стадии B по функциям.
|
||||
|
||||
## Исправления
|
||||
|
||||
- Свёрнутое окно (Win+D, «Показать рабочий стол») больше не завершает игру (issue #31):
|
||||
поверхность 0×0 не попадает ни в размер кадра, ни в swapchain; кадры, пока окно свёрнуто,
|
||||
не выводятся. Проверено только, что обычный вывод не изменился: в Hyprland окно не
|
||||
сворачивается до 0×0.
|
||||
|
||||
## Сравнение с 0.3
|
||||
|
||||
Лаунчер, раздел «Для разработчика»: *Синхронизация как в 0.3* (`BB_AS_0_3=1` в `run.sh`)
|
||||
возвращает то, что изменилось после релиза: старая модель памяти, `WRITE_DATA`/DMA ждут все
|
||||
копии хоста (`BB_HOST_COPY_WAITS=all`), проверка одновременной записи (`BB_PRODUCER_CHECK=1`).
|
||||
Yahar'gul стоя, 1080p: 123 FPS против ~186 с новой моделью памяти и трансляции. Из командной
|
||||
строки: `bash tools/play_ab.sh v03 BB_AS_0_3=1`.
|
||||
|
||||
## Глубина резкости (DoF) снова работает (issue #44)
|
||||
|
||||
С DoF и любым апскейлером (FSR 3/4/4.1.1, TAA) в дальнем размытии — небо, туман — появлялись
|
||||
тёмные прямоугольники с резкими краями, полосы и перевёрнутая копия сцены; в 0.2 этого не было.
|
||||
Причина — прицельные ожидания дампов констант из 0.3 (`DumpConstRam` ждёт поток записи отрисовок,
|
||||
только если пересекается с их чтениями): в список чтений попадали лишь константные буферы, а
|
||||
вершины и индексы поток записи тоже читает из памяти игры. Игра пишет мелкие данные отрисовок
|
||||
(в том числе полноэкранные квады DoF) через движок констант, и дамп для следующих отрисовок
|
||||
перезаписывал их раньше, чем их успевали прочитать. Окно открывалось, когда поток записи
|
||||
задерживался прямо перед проходами DoF — на проходах апскейлера. Теперь диапазоны вершин и
|
||||
индексов тоже в списке (неподготовленные отрисовки ждут целиком). Ожиданий стало ~850/с вместо
|
||||
~150/с, FPS в проверенных местах не изменился. `BB_CONSTRAM_PIPE=0` — полное ожидание на каждом
|
||||
дампе (A/B).
|
||||
|
||||
Возможно, это же чинит мерцающие значки и глифы интерфейса (issue #11) — не проверено.
|
||||
|
||||
## Кэш шейдеров больше не ломает запуск (issues #28, #38, #39)
|
||||
|
||||
- Файлы кэша пишутся под временным именем и переименовываются: вылет или отключение питания
|
||||
оставляют старый файл или никакого, а не обрезанный.
|
||||
- Обрезанная запись при чтении больше не останавливает игру (`Invalid or corrupted
|
||||
deserialization container`, код 23): повреждённые записи считаются, кэш пересобирается.
|
||||
То же для SPIR-V с неверным заголовком и для пайплайнов, которые драйвер отверг при
|
||||
предзагрузке. Код SRT walker из кэша регистрируется только после проверки длины.
|
||||
|
||||
## Прочее
|
||||
|
||||
- Процессор без AVX, BMI1, MOVBE, LZCNT, POPCNT или SSE4.2 (Intel до Haswell) — понятное
|
||||
сообщение при запуске вместо `Guest fault (signal 4)`, код 132 (issue #26).
|
||||
`BB_SKIP_CPU_CHECK=1` — запустить всё равно.
|
||||
- `tools/fsr4cap/build_assets.sh` молча завершался, если в одной из папок Steam не было
|
||||
GE-Proton (issue #40).
|
||||
- Копии памяти игры, поставленные в прямом режиме записи (отрисовка после проходов апскейлера,
|
||||
`BB_VK_RECORD_THREAD=0`), выполняются сразу, а не со следующей отправкой.
|
||||
- Внутриигровое меню на английском или русском — переключатель «Language» в меню (PR #36);
|
||||
по умолчанию язык лаунчера. Лаунчер на бразильском португальском (PR #10); строки без перевода
|
||||
— на английском. README: адрес репозитория в команде клонирования (PR #43).
|
||||
- Переключатель `BB_TOGGLE_FILE`: бит 31 отключает сборщик текстур; второе число в файле —
|
||||
временные биты экспериментов (`BbToggle::Experiment`).
|
||||
|
||||
## Векторы движения (ночь на 2026-10-07, проверяется)
|
||||
|
||||
- Знак оси Y векторов камеры берётся из вьюпорта G-buffer каждый кадр (`BB_CAMERA_Y=up/down` —
|
||||
принудительно), а не задан константой: 0.2 и 0.3 ошибались каждая в своих местах
|
||||
(issues #41, #42, #47, #18).
|
||||
- Если в кадре несколько проходов G-buffer, камера берётся у того, чья глубина идёт в векторы.
|
||||
- FSR 4 на пресетах с апскейлом получает кадр в линейном свете (`BB_FSR4_LINEAR=0` — как раньше).
|
||||
- Маска реактивности работает и с FSR 4: его результат смешивается с текущим кадром на
|
||||
эффектах (туман, свет, частицы).
|
||||
|
||||
## Поток записи отрисовок не ждёт потоки записи команд (2026-10-07)
|
||||
|
||||
- Презентер (барьер кадра, FSR 1, постобработка) записывается так же, как отрисовки, на потоках
|
||||
записи команд. Раньше он брал командный буфер напрямую: поток отрисовок ждал, пока потоки
|
||||
записи закончат всё переданное (~90 мкс каждый кадр), и сам отправлял кадр (~45 мкс).
|
||||
- Где команды всё же пишутся напрямую (проходы апскейлера: векторы движения, TAA, FSR 3/4,
|
||||
резкость), они идут в отдельный сегмент со своим пулом команд, после переданных сегментов,
|
||||
без ожидания (`BB_DIRECT_SEGMENT=0` — ожидание, как раньше). Сигналы и упорядоченные задачи
|
||||
в таком сегменте остаются в очереди, чтобы не обогнать более ранние метки.
|
||||
- Поток показа ждёт, пока отправка кадра (с потока записи) ушла в очередь, прежде чем ставить
|
||||
своё ожидание на неё: иначе одна очередь ждала бы более позднюю отправку (зависание GPU при
|
||||
запуске, найдено на этой работе).
|
||||
- DMA в память, которую GPU использует на месте (модель ПК) или которую писал GPU, — это копия
|
||||
на GPU в порядке потока; поток отрисовок больше не ждёт перед ней копии и отложенные сигналы
|
||||
(~16 мкс на каждую, ~900 в секунду: 4-байтовые счётчики GDS).
|
||||
- Исправлены два давних редких случая записи в уже отправленный командный буфер: смена размера
|
||||
при резкости больше 1 (`Finish()` посреди прохода апскейлера) и смена пресета FSR 4 в меню.
|
||||
- `BB_FINAL_DUMP_TRIGGER` (`KIND=final tools/frame.sh`): снимок кадра после FSR и постобработки.
|
||||
|
||||
Замеры (Hunter's Dream, стоя, 4 ядра / 8 потоков, модель ПК): 143–144 → 146 FPS; поток
|
||||
отрисовок больше не блокируется на записи команд (было 1,8–3,3 % времени). 16 потоков: модель ПК
|
||||
~186, старая модель 144–151 (разброс между запусками ±3 %, без разницы с прежней сборкой).
|
||||
|
||||
## Режим работы в лаунчере (2026-10-07)
|
||||
|
||||
- Группа *Режим работы*, переключатель *Новая модель памяти и трансляции* (`BB_PC_MODEL=1`),
|
||||
по умолчанию выключен. Выключен — старая модель памяти, как в 0.3, со всеми исправлениями
|
||||
после неё. Включён — память игры используется видеочипом на месте, работа командного
|
||||
процессора переводится в команды Vulkan (экспериментально).
|
||||
- Только видеокарты AMD: лаунчер находит их по `/sys/class/drm/card*/device/vendor` (0x1002) и
|
||||
без AMD делает переключатель недоступным; библиотека GPU сама держит режим выключенным, если
|
||||
выбранное устройство Vulkan не AMD (гибридные ноутбуки). `BB_PC_MODEL_ANY_GPU=1` — включить
|
||||
всё равно, для проверки.
|
||||
- Прежний переключатель *Как в версии 0.3* переехал в раздел «Для разработчика» под именем
|
||||
*Синхронизация как в 0.3*: это откат для поиска регрессий, а не один из двух режимов.
|
||||
- README (оба) описывают оба режима, таблицу отличий от shadPS4 и планы.
|
||||
|
||||
## FSR 4.1.1 из своей DLL — кнопкой в лаунчере (2026-10-07)
|
||||
|
||||
- *Апскейлер → FSR 4.1.1 из своей DLL AMD → Выбрать DLL…*: выбираешь
|
||||
`amd_fidelityfx_upscaler_dx12.dll` 4.1.x, лаунчер собирает ассеты (`tools/fsr4cap/build_assets.sh`)
|
||||
в фоне, ход — в подписи строки и во вкладке «Журнал», в конце FSR 4.1.1 выбирается. Отмена той
|
||||
же кнопкой. Без окна: `--build-fsr411 <DLL> [загрузчик]`.
|
||||
- DLL проверяется до записи (`tools/fsr4cap/dll_info.py`: версия из свойств файла, модели FSR 4
|
||||
по именам шейдеров). Сборки сообщества OptiScaler (4.0.2b/c: модель `v07_i8`, 16 проходов вместо
|
||||
29, другие веса) пока не подходят — об этом сразу сообщение, без записи. Загрузчик ищется рядом
|
||||
с DLL и папкой выше под обоими именами (`amd_fidelityfx_loader_dx12.dll`, у OptiScaler —
|
||||
`amd_fidelityfx_dx12.dll`), иначе лаунчер его спрашивает.
|
||||
- Proton запускается через umu-run, если он установлен, иначе в контейнере Steam, который требует
|
||||
этот Proton (`toolmanifest.vdf`: Steam Linux Runtime 4 для GE-Proton 11) — umu больше не нужен.
|
||||
- AppImage содержит собранные заранее dxil-spirv и fsr4cap.exe (Nix), SPIRV-Tools: компиляторы и
|
||||
сеть не нужны, только GE-Proton. Рабочая папка (префикс Proton ~300 МБ) удаляется после сборки.
|
||||
- Заголовки FidelityFX API (MIT) лежат в `tools/fsr4cap/ffx`: SDK больше не клонируется.
|
||||
- Новый набор заменяет старый только целиком; коды выхода скрипта (2–7) лаунчер показывает
|
||||
понятным текстом. Собранная ранее dxil-spirv, которая не запускается (дерево переехало),
|
||||
собирается заново.
|
||||
|
||||
Проверено: из исходников (Nix, umu) и пакет (Steam Linux Runtime 4 в FHS-окружении) — результат
|
||||
побайтно совпадает с прежним `fsr4_411`; кнопка в лаунчере (headless-сессия): ход «N из 20»,
|
||||
отказ для 4.0.2c. Не проверено: AppImage на обычном дистрибутиве / Steam Deck (на NixOS AppImage
|
||||
скрывает системный `/nix` и FHS-ссылки).
|
||||
- После первой проверки (DLL из Pragmata): это официальная FSR 4.0.3 с той же моделью
|
||||
`v07_fp8_no_scale`, но её провайдер AMD показывает только на RDNA4 — на RX 7800 XT под любым
|
||||
Proton DLL предлагает лишь FSR 3.1.5 и 2.3.4. Теперь 4.0.x отклоняется сразу с объяснением.
|
||||
Отклоняется и «загрузчик», который на самом деле DLL апскейлера (так получилось в видео:
|
||||
загрузчика рядом не было, и в окне выбора оказалась та же DLL).
|
||||
- Proton выбирается перебором: первый, под которым DLL включает FSR 4.1 (GE-Proton 10+,
|
||||
Proton-CachyOS, Proton Experimental, затем Proton 11.0/10.0 из Steam), у каждого свой префикс.
|
||||
Проверено на RX 7800 XT: GE-Proton 11-6, Proton-CachyOS 11 (июль), Experimental (октябрь) —
|
||||
да; Proton 11.0-2c и GE-Proton 9 — нет. На Steam Deck был только GE-Proton 9.
|
||||
- На NixOS AppImage сразу говорит, что рантайм Steam там не запустить (код 8), вместо
|
||||
непонятного «не запустился под Proton».
|
||||
|
||||
## FSR 4.1.1: одна DLL и вариант FP8 для RDNA4 (2026-10-07, день)
|
||||
|
||||
- **Загрузчик больше не нужен.** DLL апскейлера AMD сама экспортирует FidelityFX API
|
||||
(`ffxCreateContext` …), и `fsr4cap.exe` загружает её напрямую; `dll_info.py` проверяет экспорт
|
||||
по таблице PE. Загрузчик спрашивается только у DLL без этих функций (таких пока не встречалось).
|
||||
Запись с DLL напрямую совпадает с записью через загрузчик (кроме двух байт неинициализированной
|
||||
константы в кадре 0), набор — побайтно.
|
||||
- Повторная сборка из AppImage падала: `cp: cannot create regular file …/fsr4cap.exe: Permission
|
||||
denied` — копия файла из AppImage только для чтения, и вторая копия поверх неё не создаётся.
|
||||
Теперь старый файл удаляется перед копированием (и DLL, если она только для чтения).
|
||||
- **FP8 на RDNA4.** На RDNA4 vkd3d-proton даёт DLL матрицы FP8, и она запускает другой вариант
|
||||
проходов (issue #12): те же 29 проходов, константы и буферы, но шейдеры на WMMA FP8, свои веса и
|
||||
своя сетка групп (у каждого прохода свой тайл: 16×8, 32×1, 16×4 …; `_post` — ширина тензора
|
||||
выровнена до 32, а не до 4). Сборка после INT8 пробует FP8 на первом размере и, если DLL
|
||||
отдаёт другие шейдеры (RDNA4), записывает все 20 размеров во второй раз (`fp8/capture_*`).
|
||||
`extract.py` проверяет правила обоих вариантов и пишет `fsr4_411/fp8/` (матрицы FP8,
|
||||
`VK_EXT_shader_float8`) и `fsr4_411/fp8emu/` (FP8 через FP16-матрицы, как vkd3d-proton на
|
||||
RDNA3 — только для проверки). Игра выбирает FP8 сама, если у видеокарты есть матрицы FP8 и
|
||||
набор собран; `BB_FSR411_VARIANT=int8|fp8|fp8emu` — принудительно. В строке
|
||||
`Upscaler: FSR 4.1.1 replay, …` теперь указан вариант.
|
||||
- Проходы с матрицами запускаются с подгруппой 32 и полными подгруппами, как у vkd3d-proton
|
||||
(эвристика размера волны у них совпадает с использованием матриц — `extract.py` это проверяет).
|
||||
Устройство игры включает `VK_KHR_cooperative_matrix`, `VK_EXT_shader_float8`,
|
||||
`vulkanMemoryModel`, `computeFullSubgroups`, если они есть.
|
||||
- Проверено на RX 7800 XT: FP8 с эмуляцией (GE-Proton 11-6, `BB_FSR4CAP_FP8=1`) — DLL и повтор
|
||||
**побайтно одинаковы** на 7 размерах, обе модели, 4 кадра (`VARIANT=fp8emu verify.sh`; с
|
||||
dxil-spirv той же версии, что в vkd3d-proton GE-Proton 11-6 — наша закреплённая новее и
|
||||
иначе округляет преобразования FP8 в эмуляции). INT8 — как прежде (1600×900 расходится у
|
||||
правого края из-за гонки в самой DLL). На RDNA4 вживую **не проверено**: проходы модели FP8
|
||||
от обеих версий dxil-spirv одинаковы, различаются только `_post`.
|
||||
- На RDNA3 FP8 по умолчанию не записывается: эмуляция почти вдвое медленнее INT8 (1440p Balanced,
|
||||
RX 7800 XT: 3,35 мс против 1,82 мс). `BB_FSR4CAP_FP8=1` —
|
||||
записать и там, `BB_FSR4CAP_FP8=0` — не записывать нигде.
|
||||
|
||||
## Управление и игровое меню (2026-10-07)
|
||||
|
||||
- **Меню жестов с тачпада.** Касания тачпада приходили с id 0 и без времени удержания, и игра
|
||||
их не принимала: ни тачпад, ни Back, ни Tab не открывали жесты. Теперь каждое новое касание
|
||||
получает свой id (1–127), как у DualShock 4, и время удержания. Проверено в игре: левая половина
|
||||
тачпада — «Жесты», правая — «Личные вещи» (с включённым debug menu левая половина открывает его).
|
||||
В лаунчере это подписано у привязок тачпада.
|
||||
- **Клавиатура работает и при подключённом геймпаде.** Раньше она управляла игрой, только когда
|
||||
геймпада нет (а на Steam Deck он есть всегда); работали лишь Tab и Backspace. Теперь кнопки
|
||||
клавиатуры добавляются к кнопкам геймпада, а зажатая клавиша движения/камеры отклоняет стик до
|
||||
конца. Вживую с клавиатурой не проверено (только сборка и тесты).
|
||||
- **Меню настроек (Insert / L3+R3) помнит своё место** — и между открытиями, и между запусками
|
||||
(`menu_pos` в bbport.ini, доля экрана: переживает смену разрешения). Раньше оно каждый раз
|
||||
открывалось у левого края.
|
||||
- **Курсор мыши над меню.** Пока меню открыто, показывается системный курсор (в полноэкранном
|
||||
режиме и после 3 с без движения он скрыт), а меню сразу знает, где мышь, а не после первого
|
||||
движения.
|
||||
- **Настройки из меню больше не стирают назначения кнопок.** Меню записывало bbport.ini
|
||||
целиком, только со своими ключами: назначения клавиатуры и геймпада из лаунчера (`key.*`,
|
||||
`pad.*`) и другие его ключи пропадали после любого изменения в игре. Теперь меню меняет только
|
||||
свои строки, остальное оставляет, файл заменяется целиком (обрыв записи оставляет старый).
|
||||
Тест: `ninja -C out/gpu settings-test`.
|
||||
|
||||
## BB_READBACKS=2 больше не зависает при запуске
|
||||
|
||||
Точный режим обратного чтения защищает страницы, записанные GPU, и от чтения. Их читали и
|
||||
потоки копирования порта (выгрузка в VRAM: `bb:Copy*`, `bb:VkRecorder`): чтение падало в
|
||||
обработчик сбоев, тот ставил обратное чтение в очередь потоку GPU и ждал, а поток GPU в это время
|
||||
ждал поток записи отрисовок, который ждал эти копии. Теперь копии порта в точном режиме читают
|
||||
память игры через второе отображение тех же страниц, которое защита не трогает. Проверено: с
|
||||
`BB_READBACKS=2` игра доходит до титульного экрана и работает (раньше вставала на загрузке).
|
||||
В самой игре в этом режиме не проверено. `BB_PROBE=<обёртка>` в run.sh — запуск под gdb и т. п.
|
||||
и из исходников.
|
||||
|
||||
## FSR 4.1.1 из AppImage на NixOS (2026-10-07, вечер)
|
||||
|
||||
AppImage работает в своём пространстве монтирования с chroot, где её `/nix/store` заменяет
|
||||
системный: изнутри не видны ни `umu-run`, ни `nix-shell`, ни FHS-окружение Steam, и кнопка
|
||||
сборки на NixOS заканчивалась ошибкой 8. Теперь `capture_all.sh` на NixOS без `umu-run`:
|
||||
- если есть `nix-shell` (из исходников, на самой системе) — перезапускается в
|
||||
`nix-shell -p umu-launcher`;
|
||||
- в AppImage — копирует себя и `proton.sh` в рабочую папку (`~/.local/share/bbport/fsr4cap`,
|
||||
её видят обе стороны) и запускает запись на системе через `systemd-run --user --wait --pipe`
|
||||
в login-shell (PATH и NIX_PATH пользователя); ход и код выхода приходят обратно, отмена
|
||||
останавливает службу. Перевод в SPIR-V — уже внутри AppImage. `systemd-run` из
|
||||
systemd-minimal (он и так был в пакете) добавлен в PATH лаунчера.
|
||||
|
||||
Проверено на этом ПК (NixOS): кнопка без окна (`--build-fsr411`) из AppImage собирает FSR 4.1.1
|
||||
до конца, набор побайтно как эталонный, рабочая папка удалена; отмена посреди записи
|
||||
останавливает службу на системе.
|
||||
@@ -1,107 +0,0 @@
|
||||
# 0.5-pre2 — pre-release for testing
|
||||
|
||||
Second 0.5 prerelease, built from the `testing` branch. This release focuses on the experimental
|
||||
memory and GPU command translation path. Changes are listed relative to
|
||||
[0.5-pre1](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1).
|
||||
|
||||
## English
|
||||
|
||||
- **New memory module for NVIDIA and other non-AMD GPUs.** When the experimental mode is enabled,
|
||||
the layer binds chunks of game memory directly and keeps frequently used data in separate VRAM
|
||||
buffers. It does not use `vkQueueBindSparse`. The launcher now exposes the mode on all GPU
|
||||
vendors; the startup capability check is retained. AMD still selects the sparse arena by
|
||||
default within this mode; `BB_LAYER_MEMORY=1` selects the new module manually.
|
||||
- **Memory coherence fixes.** CPU writes to mirrored blocks are tracked selectively. Buffer
|
||||
bindings and command-processor copies use the current GPU data, CPU writes invalidate stale
|
||||
GPU ownership, and mixed ranges can use refreshed VRAM fringes. Promotion respects the VRAM
|
||||
budget, and cached binding decisions reduce repeated checks and locking.
|
||||
- **Original GPU copy shader enabled by default in the new model.** The game's copy shader is
|
||||
translated to SPIR-V and reads its copy list on the GPU. Its writes update both the current
|
||||
GPU view and game memory, keeping VRAM mirrors and CPU consumers consistent.
|
||||
`BB_COPY_SHADER_NATIVE=0` retains the HLE path for comparison.
|
||||
- **Portable GPU completion labels.** Supported EOP/EOS/RELEASE_MEM labels can be written by
|
||||
standard Vulkan commands on GPUs without `VK_AMD_buffer_marker`. Prior GPU results become
|
||||
visible before the completion label; the AMD fast path remains available. The portable path
|
||||
can be forced for testing with `BB_GPU_LABELS_PORTABLE=1`.
|
||||
- **Synchronization and bounds fixes.** Corrected update/inline-data stage dependencies and
|
||||
index-buffer read tracking. Paged buffer accesses reject invalid ranges before accessing the
|
||||
page table. Vulkan initialization also enables the required surface extension for headless
|
||||
validation.
|
||||
- **Shader-cache migration.** The shader binary format is now version 9. Binary, metadata and
|
||||
pipeline versions are checked together before loading the cache, preventing old and new
|
||||
shader variants from being mixed. AppImage startup chooses the GPU cache directory before
|
||||
loading the GPU library, following the selected user-data directory.
|
||||
- **Regression checks.** Added GPU tests for paged copies, mirror/guest writes, range guards and
|
||||
32/64-bit portable labels. The PM4 occlusion check now spans multiple submissions to avoid
|
||||
false failures when a single submission contains no draws.
|
||||
|
||||
**How to test:** enable *New memory and translation model* in the launcher, or set
|
||||
`BB_PC_MODEL=1`. The experimental mode remains **off by default**. For testing the new module
|
||||
on AMD, also set `BB_LAYER_MEMORY=1`.
|
||||
|
||||
**Validation and limits:** runtime tests, packaged Vulkan-driver discovery tests, the new GPU
|
||||
tests and all eight in-game PM4 checks passed. GPU and AppImage checks used an RX 7800 XT/RADV;
|
||||
the portable label path was forced on AMD. Physical NVIDIA/Intel hardware and NVIDIA host-memory
|
||||
import have not been tested. Full-game Vulkan validation still reports graphics-pipeline and
|
||||
image-layout errors. A consistent FPS improvement over the previous copy path has not been
|
||||
established, and long gameplay sessions still need testing.
|
||||
|
||||
The earlier 0.5 changes remain included: settings in the game's System menu, display/monitor
|
||||
selection, effect and patch controls, the updated overlay, launcher log export, Chinese
|
||||
translation, DLC add-on folders and atomic save-file writes. See the
|
||||
[0.5-pre1 notes](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1) for details.
|
||||
|
||||
The AppImage contains no game files. Proprietary FSR 4 model assets and NVIDIA's DLSS SDK/libraries
|
||||
are not bundled. `SHA256SUMS` accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
- **Новый модуль памяти для NVIDIA и остальных не-AMD GPU.** При включённом экспериментальном
|
||||
режиме прослойка привязывает куски памяти игры напрямую и держит часто используемые данные
|
||||
в отдельных буферах VRAM. `vkQueueBindSparse` не используется. Переключатель в лаунчере
|
||||
доступен для всех производителей GPU, стартовая проверка возможностей драйвера сохранена.
|
||||
На AMD внутри нового режима по умолчанию остаётся sparse-арена; новый модуль можно выбрать
|
||||
через `BB_LAYER_MEMORY=1`.
|
||||
- **Согласование памяти CPU и GPU.** Записи CPU в блоки с зеркалами отслеживаются выборочно.
|
||||
Привязки буферов и копии командного процессора используют актуальные данные GPU; новые записи
|
||||
CPU снимают устаревшие пометки GPU. Смешанные диапазоны могут использовать обновляемые края
|
||||
зеркал VRAM. Переносы соблюдают бюджет видеопамяти, кэш решений уменьшает повторные проверки
|
||||
и блокировки.
|
||||
- **Оригинальный GPU copy shader по умолчанию в новой модели.** Шейдер игры транслируется
|
||||
в SPIR-V и сам читает список копий на видеокарте. Результат записывается и в актуальное
|
||||
GPU-представление, и в память игры: зеркала VRAM и потребители на CPU видят согласованные
|
||||
данные. `BB_COPY_SHADER_NATIVE=0` оставляет HLE для сравнения.
|
||||
- **Переносимые метки завершения GPU.** Поддерживаемые метки EOP/EOS/RELEASE_MEM записываются
|
||||
стандартными командами Vulkan на GPU без `VK_AMD_buffer_marker`. Предшествующие результаты
|
||||
GPU становятся видимыми до метки завершения. Быстрый путь AMD сохранён;
|
||||
`BB_GPU_LABELS_PORTABLE=1` принудительно выбирает переносимый путь для проверки.
|
||||
- **Синхронизация и границы доступа.** Исправлены зависимости update/inline data и учёт чтения
|
||||
index buffer. Недопустимые диапазоны отклоняются до обращения к таблице страниц. Для
|
||||
headless-проверки Vulkan включается необходимое расширение surface.
|
||||
- **Миграция кэша шейдеров.** Формат shader binary поднят до версии 9. Версии binary, metadata
|
||||
и pipeline проверяются вместе до загрузки кэша, чтобы старые и новые варианты шейдеров
|
||||
не смешивались. AppImage выбирает GPU-каталог до загрузки GPU-библиотеки, с учётом выбранного
|
||||
каталога пользовательских данных.
|
||||
- **Проверки регрессий.** Добавлены GPU-тесты копирования через таблицу страниц, записей
|
||||
в зеркало и память игры, границ доступа и переносимых 32/64-битных меток. PM4-проверка
|
||||
occlusion теперь охватывает несколько отправок команд, чтобы отправка без отрисовок
|
||||
не давала ложный FAIL.
|
||||
|
||||
**Как проверить:** включить *Новую модель памяти и трансляции* в лаунчере или задать
|
||||
`BB_PC_MODEL=1`. Экспериментальный режим по умолчанию **выключен**. Для нового модуля на AMD
|
||||
также нужен `BB_LAYER_MEMORY=1`.
|
||||
|
||||
**Проверки и ограничения:** пройдены тесты рантайма, поиска Vulkan-драйвера в пакете, новые
|
||||
GPU-тесты и все восемь PM4-проверок в игре. GPU и AppImage проверялись на RX 7800 XT/RADV;
|
||||
переносимые метки принудительно включались на AMD. Физические NVIDIA/Intel и импорт host memory
|
||||
на NVIDIA пока не проверены. Полная Vulkan-валидация игры ещё сообщает об ошибках графических
|
||||
конвейеров и раскладок изображений. Стабильный прирост FPS относительно предыдущего пути
|
||||
копирования не подтверждён; длительное прохождение также требует проверки.
|
||||
|
||||
Все изменения первого prerelease 0.5 включены: настройки в меню System самой игры, выбор
|
||||
дисплея/монитора, управление эффектами и патчами, обновлённый оверлей, экспорт логов лаунчера,
|
||||
китайский перевод, папки DLC и атомарная запись сохранений. Подробности — в
|
||||
[описании 0.5-pre1](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1).
|
||||
|
||||
Игровых файлов в AppImage нет. Проприетарные модели FSR 4 и SDK/библиотеки DLSS не включены.
|
||||
К загрузке приложен `SHA256SUMS`.
|
||||
@@ -1,103 +0,0 @@
|
||||
# 0.5-pre3 — pre-release for testing
|
||||
|
||||
Third 0.5 prerelease, built from the `testing` branch. It addresses the first hardware logs of the
|
||||
new memory module on NVIDIA (GTX 1660 Ti laptop, 6 GB, driver 615.71). Changes are listed relative
|
||||
to [0.5-pre2](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre2). Details and
|
||||
measurements: [NVIDIA_MEMORY_MODEL.ru.md](NVIDIA_MEMORY_MODEL.ru.md).
|
||||
|
||||
## English
|
||||
|
||||
- **VRAM copies were disabled on NVIDIA.** The fixed-arena workaround for NVIDIA's sparse binding
|
||||
was also applied to the memory module, which has no sparse arena: it made no VRAM copies of the
|
||||
game's memory at all (10–15 FPS on the GTX 1660 Ti). The workaround no longer applies to the
|
||||
module; `BB_FIXED_ARENA=0` is no longer needed.
|
||||
- **One VRAM budget for textures and copies of the game's memory.** The texture collector counts
|
||||
all of our VRAM. On cards below ~9 GB its critical mark (3.9 GB on the 6 GB card) was below what
|
||||
the copies and the textures need together, so it kept evicting textures in use (155–1255 every
|
||||
5 s, stutters) while the copies kept growing up to 90 % of the driver's budget. Copies now stop
|
||||
384 MiB short of that mark; close to it the module frees room itself, first the unused room of
|
||||
its copies, then copies not used for 3 s (`BB_VRAM_PRESSURE_IDLE_SECONDS`).
|
||||
- **Fewer reallocations of VRAM copies.** A copy that met newly loaded data was replaced by a
|
||||
bigger one each time: loading a level allocated 15–27 GB for 1.3 GB of data, with up to 7.4 GB
|
||||
of VRAM in use at once, and streaming areas did the same while playing. A growing copy now gets
|
||||
room to grow (up to 64 MiB, `BB_LAYER_MIRROR_GROWTH_MB`): 5–10 GB, and the worst loading frame of
|
||||
a test route went from 555 to 87 ms (RX 7800 XT). Replaced copies are freed as soon as the GPU is
|
||||
done with them.
|
||||
- **Command-processor writes (WRITE_DATA)** update the VRAM copy as well, instead of sending the
|
||||
block back to system memory (with watched blocks, below, up to 2 GB a minute went back and forth).
|
||||
- **Watched blocks** (`BB_LAYER_WATCH`, on by default). Memory the CPU writes without announcing
|
||||
it was always read by the GPU from system memory, over the bus on NVIDIA. It now gets a VRAM
|
||||
copy for reading bindings too; a write trap catches the next CPU write and the block is uploaded
|
||||
again before its next use. This also replaces most volatile-block refreshes (RX 7800 XT:
|
||||
1.3 GB/s of copies → 36 MB/s; the NVIDIA log showed 455 MB/s). Blocks the CPU writes nearly every
|
||||
frame still stay in system memory: uploading them before draws ended render passes (GPU frame
|
||||
4.8 → 7.2 ms on the RX 7800 XT); `BB_LAYER_WATCH_HOT=1` turns that on for experiments.
|
||||
- **File reads retry on EFAULT** (a page protected again between the runtime's touch and the
|
||||
kernel's copy).
|
||||
- **Testing aids:** `BB_VRAM_LIMIT_MB=N` takes the driver's VRAM budget as at most N MiB (5600
|
||||
gives the texture-collector marks of a 6 GB card); `BB_RESIDENCY_MIX=1` also says why blocks are
|
||||
not watched; the `Layer memory:` lines report watched blocks and write traps by kind.
|
||||
|
||||
**How to test:** enable *New memory and translation model* in the launcher, or set
|
||||
`BB_PC_MODEL=1`; the mode remains **off by default**. Logs with `BB_FRAME_STATS=1
|
||||
BB_GPU_PROFILE=1` at one place, once as is, once with `BB_LAYER_WATCH=0` and once with the mode off,
|
||||
show where the time goes.
|
||||
|
||||
**Validation and limits:** RX 7800 XT/RADV (PCIe 3.0) with the memory module forced on and
|
||||
`BB_VRAM_LIMIT_MB=5600`: model of 0.3 ~195 FPS, module ~195–213 FPS (GPU 4.6–5.2 ms a frame; runs
|
||||
vary by about ±10 %), VRAM kept under the collector's critical mark, a recorded route without
|
||||
frames over 25 ms, a 3-minute walk without errors, unchanged images; runtime, unit (101) and GPU
|
||||
tests passed. Not tested on physical NVIDIA or Intel hardware: on AMD reads of the game's memory
|
||||
in place cost little, so whether NVIDIA now reaches the speed of the 0.3 model is not established.
|
||||
|
||||
The AppImage contains no game files. Proprietary FSR 4 model assets and NVIDIA's DLSS SDK/libraries
|
||||
are not bundled. `SHA256SUMS` accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
- **На NVIDIA не было копий в VRAM.** Обход sparse binding на NVIDIA («фиксированная арена»)
|
||||
применялся и к модулю памяти, у которого sparse-арены нет: модуль вообще не копировал память
|
||||
игры в VRAM (10–15 FPS на GTX 1660 Ti). Теперь обход на модуль не действует,
|
||||
`BB_FIXED_ARENA=0` больше не нужен.
|
||||
- **Общий бюджет VRAM для текстур и копий памяти игры.** Сборщик кэша текстур считает всю нашу
|
||||
VRAM. На картах меньше ~9 ГБ его критическая отметка (3,9 ГБ на 6 ГБ) оказалась ниже того, что
|
||||
нужно копиям и текстурам вместе: он постоянно выселял используемые текстуры (155–1255 за 5 с,
|
||||
фризы), а копии росли до 90 % бюджета драйвера. Теперь копии встают за 384 МиБ до отметки, а
|
||||
рядом с ней модуль сам освобождает место: сначала неиспользуемый запас своих копий, затем
|
||||
копии, не нужные 3 с (`BB_VRAM_PRESSURE_IDLE_SECONDS`).
|
||||
- **Меньше перевыделений копий.** Копия, к которой примыкали новые данные, каждый раз заменялась
|
||||
большей: загрузка уровня выделяла 15–27 ГБ на 1,3 ГБ данных, до 7,4 ГБ VRAM одновременно; то же
|
||||
при подгрузке районов во время игры. Растущая копия теперь получает запас под рост (до 64 МиБ,
|
||||
`BB_LAYER_MIRROR_GROWTH_MB`): 5–10 ГБ, худший кадр загрузки в тестовом маршруте 555 → 87 мс
|
||||
(RX 7800 XT). Заменённые копии освобождаются сразу, как только видеочип с ними закончил.
|
||||
- **Записи командного процессора (WRITE_DATA)** обновляют и копию в VRAM, а не возвращают блок
|
||||
в системную память (с наблюдаемыми блоками, ниже, туда и обратно ходило до 2 ГБ в минуту).
|
||||
- **Наблюдаемые блоки** (`BB_LAYER_WATCH`, по умолчанию включено). Память, в которую процессор
|
||||
пишет без предупреждения, видеочип всегда читал из системной памяти, на NVIDIA через шину.
|
||||
Теперь для читающих привязок у неё тоже есть копия в VRAM: ловушка записи ловит следующую
|
||||
запись процессора, и блок перезаливается перед следующим использованием. Это же заменило
|
||||
большую часть обновлений летучих блоков (RX 7800 XT: 1,3 ГБ/с копий → 36 МБ/с; в логе с NVIDIA
|
||||
было 455 МБ/с). Блоки, которые процессор пишет почти каждый кадр, остаются в системной памяти:
|
||||
их заливка перед отрисовками завершала render pass (кадр видеочипа 4,8 → 7,2 мс на RX 7800 XT);
|
||||
`BB_LAYER_WATCH_HOT=1` включает это для экспериментов.
|
||||
- **Чтение файлов повторяется при EFAULT** (страница снова защищена между касанием рантайма и
|
||||
копированием ядром).
|
||||
- **Для проверок:** `BB_VRAM_LIMIT_MB=N` — бюджет VRAM драйвера считается не больше N МиБ (5600
|
||||
даёт пороги сборщика текстур карты на 6 ГБ); `BB_RESIDENCY_MIX=1` объясняет, почему блоки не
|
||||
наблюдаются; строки `Layer memory:` показывают наблюдаемые блоки и срабатывания ловушек по видам.
|
||||
|
||||
**Как проверить:** включить *Новую модель памяти и трансляции* в лаунчере или задать
|
||||
`BB_PC_MODEL=1`; режим по-прежнему **выключен по умолчанию**. Логи с `BB_FRAME_STATS=1
|
||||
BB_GPU_PROFILE=1` в одном месте — как есть, с `BB_LAYER_WATCH=0` и с выключенным режимом —
|
||||
покажут, куда уходит время.
|
||||
|
||||
**Проверки и ограничения:** RX 7800 XT/RADV (PCIe 3.0), модуль памяти включён вручную,
|
||||
`BB_VRAM_LIMIT_MB=5600`: модель 0.3 ~195 FPS, модуль ~195–213 FPS (видеочип 4,6–5,2 мс за кадр;
|
||||
разброс между запусками около ±10 %), VRAM держится ниже критической отметки сборщика,
|
||||
записанный маршрут без кадров дольше 25 мс, 3 минуты прогулки без ошибок, картинка не
|
||||
изменилась; тесты рантайма, юнит-тесты (101) и GPU-тесты пройдены. На физических NVIDIA и Intel
|
||||
не проверялось: на AMD чтения памяти игры на месте почти бесплатны, поэтому достигает ли NVIDIA
|
||||
теперь скорости модели 0.3, не установлено.
|
||||
|
||||
Игровых файлов в AppImage нет. Проприетарные модели FSR 4 и SDK/библиотеки DLSS не включены.
|
||||
К загрузке приложен `SHA256SUMS`.
|
||||
@@ -1,147 +0,0 @@
|
||||
# 0.5-pre4 — pre-release for testing
|
||||
|
||||
Fourth 0.5 prerelease, built from the `testing` branch, after the first tests of 0.5-pre3 on
|
||||
NVIDIA (GTX 1060 6 GB, GTX 1660 Ti 6 GB and a 4 GB card). Changes are listed relative to
|
||||
[0.5-pre3](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre3). Details and
|
||||
measurements: [NVIDIA_MEMORY_MODEL.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/testing/docs/NVIDIA_MEMORY_MODEL.ru.md).
|
||||
|
||||
## English
|
||||
|
||||
- **The new memory model on NVIDIA no longer stutters while areas stream in.** On NVIDIA (and
|
||||
any GPU other than AMD) the new model keeps VRAM copies of the game's memory itself. After
|
||||
each run of blocks it copied to VRAM, it rewrote the page table entries of the whole copy (up
|
||||
to 160 000 entries in one frame), looking up every block in the runtime's memory map under a
|
||||
lock the game's threads hold while they map memory: the GPU thread waited 30–70 ms in frames
|
||||
where an area streamed in. Only the entries that changed are written now, with one lookup per
|
||||
mapping. On a recorded route (RX 7800 XT forced into this path, five runs) the 99th percentile
|
||||
frame went from 11–13 ms to 5.1–7.5 ms (9.4 ms in a run where the GPU itself ran slower), the
|
||||
worst frame from 32–57 ms to 9–16 ms, close to AMD's default path (5.2–6.0 ms).
|
||||
- **No more copying the same memory to VRAM and back every frame.** Where the GPU writes
|
||||
memory the CPU also wrote, the write goes to the game's memory; a reading binding then copied
|
||||
the range to VRAM again, the next writing binding sent it back, every frame (seen at one
|
||||
spot with 42 FPS: about 200 MB/s each way). A range sent
|
||||
back for the GPU's writes a second time now stays where it is. Write traps on VRAM copies are
|
||||
set and cleared with one call per run of blocks instead of one per 64 KiB.
|
||||
- **Fewer shader-compilation hitches.** A new graphics pipeline of a pass that draws into a
|
||||
render target used in each of the last 8 frames is compiled on worker threads while its draws
|
||||
go without it for a frame or two. Translating a new shader and compute pipelines still happen at once, and so
|
||||
do passes rendered once and loading screens. `BB_ASYNC_PIPELINES=0` restores the old
|
||||
behaviour, `BB_ASYNC_PIPELINE_THREADS=N` sets the worker count. The drivers' own shader caches
|
||||
are kept between sessions (NVIDIA: no cleanup, 10 GB; Mesa: 4 GB). As before, the first start
|
||||
of a new build compiles its pipeline cache again.
|
||||
- **4 GB cards: the new memory model no longer collapses to ~5 FPS.** 0.5-pre3 stopped copying
|
||||
the game's memory to VRAM short of the texture collector's critical mark; on a 4 GB card the
|
||||
textures alone held the usage there, so no copy was made again and everything was read over
|
||||
the bus after a few minutes. VRAM copies now have their own share (25 % of the driver's
|
||||
budget, `BB_LAYER_MIRROR_FLOOR_PERCENT`) that the texture collector's mark does not take.
|
||||
Cards with 4 GB are still faster with the 0.3 memory model.
|
||||
- **No VRAM left behind after travelling.** Copies of an area left behind stayed in VRAM (large
|
||||
bindings over all memory marked every copy as in use), and the copies of the new area waited,
|
||||
read over the bus (about 10 FPS less after a lamp travel on a GTX 1060). When VRAM is short,
|
||||
copies not bound directly for 3 s now go back first, and copies with memory left without
|
||||
valid data are made again over their valid data only, giving that memory back.
|
||||
- **Damaged game extractions are reported instead of hanging (#81).** Some extraction tools
|
||||
(LibOrbisPkg PkgTool on .NET 6 or newer) leave parts of compressed sectors stale without an
|
||||
error; the game then hung while loading shaders (`shaderBinarySize … is not equal to Program
|
||||
size`). The launcher and the game's preparation now unpack the shader files (under a second)
|
||||
and say that the extraction is damaged. `scripts/game_check.py GAME_DIR` checks every file.
|
||||
- **NVIDIA driver diagnostics (#107).** `--vulkan-info` now lists the NVIDIA libraries linked
|
||||
for the package, the kernel module, the `/dev/nvidia*` nodes, hybrid-graphics variables and
|
||||
the NVIDIA libraries the dynamic linker searched for and did not load: one log shows why the
|
||||
host driver gave no `vkCreateInstance` inside the AppImage.
|
||||
- **A crash window in command recording closed.** Handing recorded commands to the recording
|
||||
threads left the current command chunk empty for a moment; anything recording on that thread
|
||||
then (a fault handler) read a null chunk. The same crash was reported for a Windows build
|
||||
(#100). The replacement chunk is now swapped in before the old one leaves.
|
||||
- **The in-game settings menu (Insert, L3+R3) looks as in 0.3 and 0.4 again:** one movable window
|
||||
with sections, check boxes, sliders and `(?)` hints, keyboard and gamepad navigation. Its
|
||||
content is that of 0.5 (DLSS, debug views, current hints).
|
||||
|
||||
**How to test:** enable *New memory and translation model* in the launcher, or set
|
||||
`BB_PC_MODEL=1`; the mode remains **off by default**. On AMD it uses the sparse arena;
|
||||
`BB_LAYER_MEMORY=1` forces NVIDIA's path there. Logs with `BB_FRAME_STATS=1` show compiles per 5 s,
|
||||
a line `GPU: N pipelines compiled in the background, M draws went without theirs`, and for
|
||||
frames over 40 ms where the new model's VRAM copies spent their time (`layer memory ms`).
|
||||
|
||||
**Validation and limits:** RX 7800 XT/RADV, a recorded 25 s route: AMD's default path p99
|
||||
5.2–6.0 ms; NVIDIA's path forced, before/after as above; no frames over 20 ms in either. A 4-minute
|
||||
session on NVIDIA's path with the VRAM budget limited to 4.5 GB (`BB_VRAM_LIMIT_MB=4608`): 177 FPS
|
||||
on average, p99 9.7 ms, VRAM at most 3.0 GB; 11 of the 12 frames over 40 ms were compiles or the
|
||||
loading screen. Runtime, unit (106) and GPU tests passed. NVIDIA's own path (the game's memory imported as
|
||||
host memory) cannot run on AMD: the gains there are not measured yet, logs from NVIDIA testers
|
||||
are welcome. Physical Intel GPUs are not tested.
|
||||
|
||||
The AppImage contains no game files. Proprietary FSR 4 model assets and NVIDIA's DLSS SDK/libraries
|
||||
are not bundled. `SHA256SUMS` accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
- **Новая модель памяти на NVIDIA больше не дёргается при подгрузке локаций.** На NVIDIA (и
|
||||
любой видеокарте, кроме AMD) новая модель сама держит копии памяти игры в VRAM. После каждого
|
||||
куска, скопированного в VRAM, она переписывала записи таблицы страниц по всей копии (до 160 000
|
||||
записей за кадр) и для каждого блока искала его в карте памяти рантайма под блокировкой,
|
||||
которую держат потоки игры, когда отображают память: поток видеокарты ждал по 30–70 мс в
|
||||
кадрах, где подгружалась локация. Теперь пишутся только изменившиеся записи, с одним поиском
|
||||
на отображение. На записанном маршруте (RX 7800 XT, принудительно на этом пути, пять прогонов)
|
||||
99-й процентиль кадра — 5,1–7,5 мс вместо 11–13 (9,4 мс в прогоне, где медленнее работала сама
|
||||
видеокарта), худший кадр — 9–16 мс вместо 32–57, близко к обычному пути AMD (5,2–6,0 мс).
|
||||
- **Память больше не гоняется в VRAM и обратно каждый кадр.** Где видеокарта пишет в память,
|
||||
в которую писал и процессор, запись идёт в память игры; привязка на чтение снова копировала
|
||||
этот диапазон в VRAM, следующая привязка на запись отправляла обратно — и так каждый кадр
|
||||
(замечено в одном месте при 42 FPS: около 200 МБ/с в каждую сторону). Диапазон, который вернулся из-за записи видеокарты второй раз, теперь
|
||||
остаётся на месте. Ловушки записи на копиях в VRAM ставятся и снимаются одним вызовом на
|
||||
участок, а не на каждые 64 КБ.
|
||||
- **Меньше рывков от компиляции шейдеров.** Новый графический конвейер прохода, который рисует
|
||||
в цель рендера, использованную в каждом из последних 8 кадров, собирается в фоновых потоках, а
|
||||
его отрисовки кадр-другой идут без него. Перевод нового шейдера и compute-конвейеры
|
||||
по-прежнему компилируются сразу, как и разовые проходы и экраны загрузки.
|
||||
`BB_ASYNC_PIPELINES=0` — как раньше, `BB_ASYNC_PIPELINE_THREADS=N` — число потоков. Дисковые
|
||||
кэши шейдеров драйверов сохраняются между сессиями (NVIDIA — без очистки, 10 ГБ; Mesa — 4 ГБ).
|
||||
Как и раньше, при первом запуске новой сборки кэш конвейеров собирается заново.
|
||||
- **Карты на 4 ГБ: новая модель памяти больше не падает до ~5 FPS.** В 0.5-pre3 копии памяти
|
||||
игры в VRAM останавливались, не доходя до критической отметки сборщика текстур; на карте 4 ГБ
|
||||
одни текстуры держали память там, копии больше не создавались, и через несколько минут всё
|
||||
читалось через шину. Теперь у копий есть своя доля VRAM (25 % бюджета драйвера,
|
||||
`BB_LAYER_MIRROR_FLOOR_PERCENT`), которую отметка сборщика текстур не забирает. На картах
|
||||
4 ГБ модель памяти 0.3 по-прежнему быстрее.
|
||||
- **После перемещений VRAM не остаётся занятой.** Копии покинутой локации оставались в VRAM
|
||||
(большие привязки на всю память помечали каждую копию как используемую), а копии новой ждали
|
||||
и читались через шину (около 10 FPS меньше после телепорта к лампе на GTX 1060). Теперь при
|
||||
нехватке VRAM сначала уходят копии, к которым напрямую не привязывались 3 с, а копии, в
|
||||
которых осталась память без действительных данных, пересоздаются только поверх
|
||||
действительных данных и отдают эту память.
|
||||
- **Повреждённая распаковка игры теперь называется прямо, а не приводит к зависанию (#81).**
|
||||
Некоторые распаковщики (LibOrbisPkg PkgTool на .NET 6 и новее) без ошибки оставляют части
|
||||
сжатых секторов устаревшими; игра зависала на загрузке шейдеров (`shaderBinarySize … is not
|
||||
equal to Program size`). Лаунчер и подготовка игры теперь распаковывают файлы шейдеров (меньше
|
||||
секунды) и сообщают, что распаковка повреждена. `scripts/game_check.py ПАПКА_ИГРЫ` проверяет
|
||||
все файлы.
|
||||
- **Диагностика драйвера NVIDIA (#107).** `--vulkan-info` теперь показывает библиотеки NVIDIA,
|
||||
подключённые для пакета, модуль ядра, узлы `/dev/nvidia*`, переменные гибридной графики и
|
||||
библиотеки NVIDIA, которые динамический компоновщик искал и не загрузил: по одному логу видно,
|
||||
почему драйвер хоста не отдал `vkCreateInstance` внутри AppImage.
|
||||
- **Закрыто окно для вылета при записи команд.** При передаче записанных команд потокам записи
|
||||
текущий кусок команд на мгновение оставался пустым; если в этот момент на том же потоке
|
||||
что-то записывало (обработчик ошибки доступа), оно читало пустой указатель. Такой же вылет был
|
||||
у сборки для Windows (#100). Теперь новый кусок подставляется до того, как уходит старый.
|
||||
- **Меню настроек в игре (Insert, L3+R3) снова выглядит как в 0.3 и 0.4:** одно перемещаемое
|
||||
окно с разделами, флажками, ползунками и подсказками `(?)`, управление клавиатурой и
|
||||
геймпадом. Содержимое — как в 0.5 (DLSS, отладочные виды, актуальные подсказки).
|
||||
|
||||
**Как проверить:** включить *Новую модель памяти и трансляции* в лаунчере или задать
|
||||
`BB_PC_MODEL=1`; режим по-прежнему **выключен по умолчанию**. На AMD он работает через
|
||||
sparse-арену; `BB_LAYER_MEMORY=1` включает на AMD путь NVIDIA. В логе с `BB_FRAME_STATS=1` —
|
||||
число компиляций за 5 с, строка `GPU: N pipelines compiled in the background, M draws went
|
||||
without theirs` и для кадров дольше 40 мс — на что ушло время копий новой модели в VRAM
|
||||
(`layer memory ms`).
|
||||
|
||||
**Проверки и ограничения:** RX 7800 XT/RADV, записанный маршрут на 25 с: обычный путь AMD —
|
||||
p99 5,2–6,0 мс; путь NVIDIA, принудительно, — до и после, как выше; ни одного кадра дольше
|
||||
20 мс. Сессия на 4 минуты на пути NVIDIA с бюджетом VRAM 4,5 ГБ (`BB_VRAM_LIMIT_MB=4608`):
|
||||
в среднем 177 FPS, p99 9,7 мс, VRAM не больше 3,0 ГБ; 11 из 12 кадров дольше 40 мс — компиляция
|
||||
или экран загрузки. Тесты рантайма, юнит-тесты (106) и GPU-тесты пройдены. Собственный путь NVIDIA (память
|
||||
игры импортируется как память хоста) на AMD не запустить: выигрыш там ещё не измерен, логи от
|
||||
тестеров с NVIDIA приветствуются. Физические Intel не проверялись.
|
||||
|
||||
Игровых файлов в AppImage нет. Проприетарные модели FSR 4 и SDK/библиотеки DLSS не включены.
|
||||
К загрузке приложен `SHA256SUMS`.
|
||||
@@ -1,172 +0,0 @@
|
||||
**English** · [Русский](#русский)
|
||||
|
||||
bbport is the counterpart of Wine + DXVK for a single game: Bloodborne (CUSA03173, v1.09) on Linux.
|
||||
The game's x86-64 code runs directly on the CPU, a runtime written for this game replaces the PS4
|
||||
system libraries, and the graphics are translated to Vulkan.
|
||||
|
||||
Questions about this project: Discord https://discord.gg/KYZRKk9CB — not the shadPS4 server.
|
||||
|
||||
### What's new since 0.4
|
||||
|
||||
Details and measurements of each step are in the prerelease notes:
|
||||
[pre1](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1),
|
||||
[pre2](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre2),
|
||||
[pre3](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre3),
|
||||
[pre4](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre4).
|
||||
|
||||
**The new memory and translation model — the default on AMD**
|
||||
- Launcher: *Mode → Memory and translation model*. *Auto* (the default) gives AMD GPUs the new
|
||||
model and NVIDIA, Intel and others the memory model of 0.3 with all its fixes; *New* and *Old*
|
||||
choose by hand. Without the launcher: `BB_PC_MODEL=1/0` (unset: as *Auto*).
|
||||
- The GPU uses the game's memory where it is, as a PC game's buffers; what it reads often is kept
|
||||
in VRAM — on AMD in a sparse buffer (the arena), on other GPUs by the layer's memory module
|
||||
without sparse rebinding (`BB_LAYER_MEMORY=1/0` by hand). The PS4 command processor's work is
|
||||
translated rather than emulated: memory writes and fences are done by the GPU in stream order,
|
||||
`COPY_DATA`, `COND_EXEC`, predication, `MEM_SEMAPHORE` and the GPU clock (100 MHz, as on the
|
||||
PS4) follow the GPU's timeline.
|
||||
- The game's occlusion queries as Vulkan queries are available with `BB_OCCLUSION_QUERIES=1`, off
|
||||
by default: with an upscaler the scene is drawn smaller and jittered, and a lamp's glow then
|
||||
flashed grey over the whole frame now and then. Off, everything counts as visible, as in the
|
||||
0.3 model.
|
||||
- The layer's memory module (NVIDIA and others): some launches no longer run the GPU 1.3–1.7 times
|
||||
slower (copies that broke render passes up to ~100 times a frame).
|
||||
- On NVIDIA the new model can be chosen by hand; testers' results differ by scene (some scenes are
|
||||
slower than with the old model), so it is not the default there yet. Logs are welcome.
|
||||
|
||||
**Settings inside the game's own menu**
|
||||
- *System* has *Display* and *Effects*, real pages of the game's menu, on the title screen and
|
||||
in the pause menu: output resolution, quality preset and sharpness as sliders, FPS counter,
|
||||
model detail and upscaler; chromatic aberration, depth of field, motion blur, SSAO, dynamic
|
||||
shadows and SSR. The game's patches and its own AA are in the overlay menu and the launcher.
|
||||
- The overlay menu (Insert / L3+R3) looks as in 0.3 again.
|
||||
- No "Play online / offline" screen: there is no PSN, the game opens its main menu offline.
|
||||
|
||||
**Smoother play**
|
||||
- New graphics pipelines of passes drawn every frame compile in the background: fewer hitches.
|
||||
Only geometry goes without its pipeline meanwhile (it shows a frame later); full-screen passes
|
||||
(lighting, fog, post-processing) wait for theirs — skipping one left whole frames grey. The
|
||||
drivers' shader caches are kept between sessions. In the game's menu the output resolution and
|
||||
preset sliders apply once they rest for 0.4 s, not at every step.
|
||||
- Cards with little VRAM: FPS no longer falls the longer you play (the texture collector evicted
|
||||
textures used a moment ago and read them again over the bus: 32 915 in ~7 minutes in an NVIDIA
|
||||
log); the game no longer stops when VRAM for its memory copies runs out; VRAM is given back
|
||||
after travelling.
|
||||
|
||||
**New**
|
||||
- DLC: add-on folders under `user/addcont/` are reported as installed; saves with The Old Hunters
|
||||
load (#33, PR #84).
|
||||
- DLSS on Linux for NVIDIA RTX when built with NVIDIA's DLSS SDK (PR #83; not in the AppImage).
|
||||
- Launcher: monitor choice (#69), *Copy* and *Export log…* (PR #78), Simplified Chinese (PR #75).
|
||||
- Damaged game extractions are reported instead of hanging while loading shaders (#81);
|
||||
`scripts/game_check.py GAME_DIR` checks every file.
|
||||
- NVIDIA in the AppImage: the host's EGL vendor files are passed along (without them NVIDIA's
|
||||
driver gave no `vkCreateInstance` on some hosts, #107; `BB_NVIDIA_EGL=0`: off); `--vulkan-info`
|
||||
prints the driver report together with the summary.
|
||||
- AMD Polaris, Vega and their APUs under RADV: `RADV_DEBUG=nohiz` is set automatically (black
|
||||
halos and vanishing geometry, #54, #99; your own `RADV_DEBUG` or `BB_AUTO_NOHIZ=0` keeps it off).
|
||||
- The FSR 4.1.1 build also finds Proton installed system-wide (PR #112). The bundled Dear ImGui
|
||||
is private to the GPU library: Vulkan layers with their own copy no longer crash the game
|
||||
(PR #113).
|
||||
|
||||
**Fixes**
|
||||
- Saves are no longer damaged by a crash during saving (#64).
|
||||
- FSR 4.1.1 build: Proton runs without Steam's container where it cannot start (#61).
|
||||
- The item picture on the loading screen at outputs other than 1080p (#67).
|
||||
- GPU hangs from garbage indirect dispatch sizes (#34); a crash window while recording commands
|
||||
(the same crash was reported for a Windows build, #100).
|
||||
- Small mips of tiled textures are detiled the way the GPU stores them (from shadPS4 #5196).
|
||||
- Chinese/Japanese/Korean Windows locales in the game preparation (#55); an `eboot.bin` dumped as a
|
||||
plain ELF (#49); stick calibration for Switch Pro clones (#70, PR #71); `BB_FSR4_STATS=1`
|
||||
(PR #80).
|
||||
|
||||
**Known limits:** one machine was tested thoroughly (RX 7800 XT, Mesa/RADV). Banding in dark
|
||||
gradients is not confirmed fixed. The Windows build is a separate port.
|
||||
|
||||
The AppImage contains no game files. Proprietary FSR 4 model assets and NVIDIA's DLSS SDK/libraries
|
||||
are not bundled. `SHA256SUMS` accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
bbport — аналог Wine + DXVK для одной игры: Bloodborne (CUSA03173, v1.09) на Linux. Код игры x86-64
|
||||
выполняется прямо на процессоре, системные библиотеки PS4 заменяет рантайм, написанный под эту игру,
|
||||
графика переводится в Vulkan.
|
||||
|
||||
Вопросы по проекту: Discord https://discord.gg/KYZRKk9CB — не сервер shadPS4.
|
||||
|
||||
### Что нового после 0.4
|
||||
|
||||
Подробности и замеры каждого шага — в описаниях пререлизов:
|
||||
[pre1](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre1),
|
||||
[pre2](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre2),
|
||||
[pre3](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre3),
|
||||
[pre4](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5-pre4).
|
||||
|
||||
**Новая модель памяти и трансляции — по умолчанию на AMD**
|
||||
- Лаунчер: *Режим работы → Модель памяти и трансляции*. *Авто* (по умолчанию) даёт видеокартам
|
||||
AMD новую модель, а NVIDIA, Intel и остальным — модель памяти 0.3 со всеми исправлениями;
|
||||
*Новая* и *Старая* — вручную. Без лаунчера — `BB_PC_MODEL=1/0` (не задано — как *Авто*).
|
||||
- Видеокарта работает с памятью игры на месте, как с буферами игры для ПК; то, что она читает
|
||||
часто, держится в VRAM — на AMD в sparse-буфере (арене), на остальных видеокартах модулем
|
||||
памяти прослойки без sparse-перепривязок (`BB_LAYER_MEMORY=1/0` — вручную). Работа командного
|
||||
процессора PS4 переводится, а не эмулируется: записи в память и метки делает видеочип в порядке
|
||||
потока, `COPY_DATA`, `COND_EXEC`, предикация, `MEM_SEMAPHORE` и часы видеочипа (100 МГц, как у
|
||||
PS4) — во времени видеочипа.
|
||||
- Occlusion-запросы игры как запросы Vulkan — по `BB_OCCLUSION_QUERIES=1`, по умолчанию выключены:
|
||||
с апскейлером сцена рисуется меньше и со сдвигом, и свечение лампы время от времени вспыхивало
|
||||
серым на весь кадр. Без них всё считается видимым, как в модели 0.3.
|
||||
- Модуль памяти прослойки (NVIDIA и другие): некоторые запуски больше не гоняют видеокарту в
|
||||
1,3–1,7 раза медленнее (копирования, разрывавшие проходы рендера до ~100 раз за кадр).
|
||||
- На NVIDIA новую модель можно выбрать вручную; у тестеров результат зависит от сцены (в части
|
||||
сцен медленнее старой модели), поэтому там она пока не по умолчанию. Логи приветствуются.
|
||||
|
||||
**Настройки — в меню самой игры**
|
||||
- В *Системе* — *Изображение* и *Эффекты*, настоящие страницы меню игры, и на титульном экране,
|
||||
и в паузе: разрешение вывода, пресет качества и резкость ползунками, счётчик FPS, детализация
|
||||
моделей и апскейлер; хроматическая аберрация, глубина резкости, размытие в движении, SSAO,
|
||||
динамические тени и SSR. Патчи игры и её собственное сглаживание — в меню оверлея и лаунчере.
|
||||
- Меню оверлея (Insert / L3+R3) снова выглядит как в 0.3.
|
||||
- Без экрана «Играть онлайн / офлайн»: PSN нет, игра сразу открывает главное меню офлайн.
|
||||
|
||||
**Ровнее**
|
||||
- Новые графические конвейеры проходов, которые рисуются каждый кадр, собираются в фоне: меньше
|
||||
рывков. Без своего конвейера в это время остаётся только геометрия (она появляется кадром
|
||||
позже); полноэкранные проходы (освещение, туман, постобработка) ждут свой — пропуск такого
|
||||
прохода давал целые серые кадры. Дисковые кэши шейдеров драйверов сохраняются между сессиями. В
|
||||
меню игры ползунки разрешения вывода и пресета применяются, когда простоят 0,4 с, а не на
|
||||
каждом шаге.
|
||||
- Карты с малым объёмом VRAM: FPS больше не падает со временем (сборщик текстур выгружал только что
|
||||
использованные текстуры и читал их снова через шину: 32 915 за ~7 минут в логе с NVIDIA); игра
|
||||
не останавливается, когда кончается VRAM для копий её памяти; после перемещений VRAM
|
||||
освобождается.
|
||||
|
||||
**Новое**
|
||||
- DLC: папки дополнений в `user/addcont/` сообщаются игре как установленные, сохранения с The Old
|
||||
Hunters загружаются (#33, PR #84).
|
||||
- DLSS на Linux для NVIDIA RTX при сборке с SDK DLSS от NVIDIA (PR #83; в AppImage не входит).
|
||||
- Лаунчер: выбор монитора (#69), *Копировать* и *Экспорт лога…* (PR #78), упрощённый китайский
|
||||
(PR #75).
|
||||
- Повреждённая распаковка игры называется прямо, а не приводит к зависанию на загрузке шейдеров
|
||||
(#81); `scripts/game_check.py ПАПКА_ИГРЫ` проверяет все файлы.
|
||||
- NVIDIA в AppImage: передаются файлы EGL-вендоров хоста (без них драйвер NVIDIA на некоторых
|
||||
системах не отдавал `vkCreateInstance`, #107; `BB_NVIDIA_EGL=0` — выключить); `--vulkan-info`
|
||||
печатает отчёт о драйвере вместе со сводкой.
|
||||
- AMD Polaris, Vega и их APU под RADV: `RADV_DEBUG=nohiz` ставится сам (чёрные ореолы и пропадающая
|
||||
геометрия, #54, #99; свой `RADV_DEBUG` или `BB_AUTO_NOHIZ=0` — не ставить).
|
||||
- Сборка FSR 4.1.1 находит и Proton, установленный в систему (PR #112). Встроенный Dear ImGui
|
||||
скрыт внутри библиотеки GPU: слои Vulkan со своей копией больше не роняют игру (PR #113).
|
||||
|
||||
**Исправления**
|
||||
- Сохранения не портятся при вылете во время сохранения (#64).
|
||||
- Сборка FSR 4.1.1: Proton запускается без контейнера Steam там, где тот не стартует (#61).
|
||||
- Картинка предмета на экране загрузки при выводе не в 1080p (#67).
|
||||
- Зависания видеочипа от мусорных размеров непрямых запусков (#34); окно для вылета при записи
|
||||
команд (такой же вылет был у сборки для Windows, #100).
|
||||
- Мелкие мипы тайловых текстур раскладываются так, как их хранит видеокарта (из shadPS4 #5196).
|
||||
- Китайская/японская/корейская локали Windows при подготовке игры (#55); `eboot.bin`, снятый как
|
||||
обычный ELF (#49); калибровка стиков у клонов Switch Pro (#70, PR #71); `BB_FSR4_STATS=1` (PR #80).
|
||||
|
||||
**Ограничения:** подробно проверена одна машина (RX 7800 XT, Mesa/RADV). Полосы в тёмных
|
||||
градиентах — исправление не подтверждено. Сборка для Windows — отдельный порт.
|
||||
|
||||
Игровых файлов в AppImage нет. Проприетарные модели FSR 4 и SDK/библиотеки DLSS не включены.
|
||||
К загрузке приложен `SHA256SUMS`.
|
||||
@@ -1,109 +0,0 @@
|
||||
**English** · [Русский](#русский)
|
||||
|
||||
bbport is the counterpart of Wine + DXVK for a single game: Bloodborne (v1.09) on Linux.
|
||||
Changes since [0.5](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5); details and
|
||||
measurements: [CHANGES_after_0.5.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_after_0.5.md) (Russian).
|
||||
|
||||
Questions about this project: Discord https://discord.gg/KYZRKk9CB — not the shadPS4 server.
|
||||
|
||||
### New
|
||||
|
||||
- **Online play (shadNet)**, as a separate module (`libbbnet.so`, loaded only when online play is
|
||||
on): messages, bloodstains and ghosts from The Hunter's Dream, bells and summons through a
|
||||
shadNet server (`srv.shadps4.net:31313` by default) with a shadNet account. Launcher → *Online*:
|
||||
servers, account, UPnP, connection test. The router port is opened by UPnP (#121). The same
|
||||
servers as the Windows port. The network and PSN libraries come from shadp2p (a shadPS4 fork,
|
||||
GPL-2.0) by way of the Linux and Windows forks of bbport.
|
||||
- **Mouse look**: click into the game to take the mouse, F1 releases it; mouse buttons and the
|
||||
wheel can be bound like keys (Mouse Left = R1, Mouse Right = L2, Middle = R3, side buttons
|
||||
R2/L1, wheel = quick items). Launcher → *Controls*: mouse look, sensitivity, invert Y (#51, #52).
|
||||
- **DLSS in the AppImage** (NVIDIA GeForce RTX): the bridge and NVIDIA's DLSS 310.9.1 library are
|
||||
included (NVIDIA's RTX SDK license allows it within an application; its text is in the package).
|
||||
A newer `libnvidia-ngx-dlss.so.*` can be chosen in the launcher (*Upscaler → DLSS: NVIDIA's
|
||||
library*), like the FSR 4.1.1 DLL.
|
||||
- **Every retail release** of Bloodborne with update 1.09 is accepted (US CUSA00900, EU, UK, JP,
|
||||
Asia and The Old Hunters editions; PR #117); an `eboot.bin` with a 60 fps patch baked in is
|
||||
reported.
|
||||
- **Quit the game** from the overlay (Insert / L3+R3; press twice) — #74, #116.
|
||||
|
||||
### Fixes
|
||||
|
||||
- The US release (and other regions) crashed with code 139 on entering the world in the new
|
||||
memory model: they ran without the game's profile (#119).
|
||||
- Motion blur smeared the whole frame while the camera turned in the new memory model (#122).
|
||||
- Cards with 4 GB (NVIDIA): "Failed to allocate 64 MiB for guest memory copies" stopped the game;
|
||||
now it frees textures sooner and uses other memory instead.
|
||||
- The main menu flickered with output resolutions above 1080p (live resolution).
|
||||
- Online, "Could not get information from the Bloodborne server" in the AppImage on NixOS (HTTPS
|
||||
certificates).
|
||||
- NVIDIA on Wayland: the swapchain is recreated through `oldSwapchain` (PR #93).
|
||||
- Building from git after updating from 0.4: "patch failed: src/ffx_vk_fsr4_v07.c" (#124).
|
||||
- `bb-launcher.sh --play` through nix-shell (PR #120); FSR 4.1.1 recording from the AppImage on
|
||||
SteamOS (PR #89).
|
||||
|
||||
### Smaller download
|
||||
|
||||
The AppImage is **399 MB instead of 925 MB**: only Mesa's Vulkan drivers (RADV without LLVM and
|
||||
ANV) instead of all of Mesa, no debug info in the game binary, MangoHud without its 32-bit half,
|
||||
the launcher's GTK without GStreamer.
|
||||
|
||||
**Known limits:** online login and summons were not tested by the author (no shadNet account);
|
||||
the UDP side of the public shadNet server did not answer in #121. Mouse look and DLSS need
|
||||
testing on more systems.
|
||||
|
||||
The AppImage contains no game files. AMD's FSR 4 model assets are not bundled. `SHA256SUMS`
|
||||
accompanies the download.
|
||||
|
||||
## Русский
|
||||
|
||||
bbport — аналог Wine + DXVK для одной игры: Bloodborne (v1.09) на Linux. Изменения после
|
||||
[0.5](https://github.com/deadinside28/bloodborne_pc/releases/tag/0.5); подробности и замеры —
|
||||
[CHANGES_after_0.5.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_after_0.5.md).
|
||||
|
||||
Вопросы по проекту: Discord https://discord.gg/KYZRKk9CB — не сервер shadPS4.
|
||||
|
||||
### Новое
|
||||
|
||||
- **Игра по сети (shadNet)** — отдельным модулем (`libbbnet.so`, загружается, только когда онлайн
|
||||
включён): сообщения, пятна крови и призраки — с The Hunter's Dream, колокола и призывы — через
|
||||
сервер shadNet (по умолчанию `srv.shadps4.net:31313`) с учётной записью shadNet. Лаунчер →
|
||||
*Онлайн*: серверы, учётная запись, UPnP, проверка соединения. Порт на роутере открывается по UPnP
|
||||
(#121). Серверы те же, что у Windows-версии. Сетевые и PSN-библиотеки — из shadp2p (форк shadPS4,
|
||||
GPL-2.0), через Linux- и Windows-форки bbport.
|
||||
- **Камера мышью**: щелчок в окне игры захватывает мышь, F1 — отпускает; кнопки мыши и колесо
|
||||
назначаются как клавиши (левая — R1, правая — L2, средняя — R3, боковые — R2/L1, колесо —
|
||||
быстрые предметы). Лаунчер → *Управление*: камера мышью, чувствительность, инверсия (#51, #52).
|
||||
- **DLSS в AppImage** (NVIDIA GeForce RTX): мост и библиотека DLSS 310.9.1 от NVIDIA в пакете
|
||||
(лицензия RTX SDK разрешает это в составе приложения; её текст — в пакете). Более новую
|
||||
`libnvidia-ngx-dlss.so.*` можно выбрать в лаунчере (*Апскейлер → DLSS: библиотека NVIDIA*), как DLL
|
||||
для FSR 4.1.1.
|
||||
- **Любое розничное издание** Bloodborne с обновлением 1.09 (США CUSA00900, Европа, Великобритания,
|
||||
Япония, Азия и издания с The Old Hunters; PR #117); о `eboot.bin` со вшитым патчем 60 FPS
|
||||
сообщается.
|
||||
- **Выход из игры** из оверлея (Insert / L3+R3; нажать дважды) — #74, #116.
|
||||
|
||||
### Исправления
|
||||
|
||||
- Американское и другие издания падали с кодом 139 при входе в мир в новой модели памяти: они шли
|
||||
без профиля игры (#119).
|
||||
- Размытие в движении размазывало весь кадр при повороте камеры в новой модели памяти (#122).
|
||||
- Карты на 4 ГБ (NVIDIA): «Failed to allocate 64 MiB for guest memory copies» останавливало игру;
|
||||
теперь текстуры освобождаются раньше, используется другая память.
|
||||
- Мерцание главного меню при выводе выше 1080p (живая смена разрешения).
|
||||
- Онлайн: «Не удалось получить информацию от сервера Bloodborne» в AppImage на NixOS (сертификаты
|
||||
HTTPS).
|
||||
- NVIDIA на Wayland: swapchain пересоздаётся через `oldSwapchain` (PR #93).
|
||||
- Сборка из git после обновления с 0.4: «patch failed: src/ffx_vk_fsr4_v07.c» (#124).
|
||||
- `bb-launcher.sh --play` через nix-shell (PR #120); запись FSR 4.1.1 из AppImage на SteamOS (PR #89).
|
||||
|
||||
### Меньше скачивать
|
||||
|
||||
AppImage — **399 МБ вместо 925 МБ**: из Mesa только Vulkan-драйверы (RADV без LLVM и ANV), без
|
||||
отладочной информации в бинарнике игры, MangoHud без 32-битной части, GTK лаунчера без GStreamer.
|
||||
|
||||
**Ограничения:** вход в shadNet и призыв автор не проверял (нет учётной записи); UDP-часть
|
||||
публичного сервера shadNet в #121 не отвечала. Камеру мышью и DLSS нужно проверить на большем числе
|
||||
систем.
|
||||
|
||||
Файлов игры в AppImage нет. Ассеты моделей FSR 4 от AMD не входят. К загрузке прилагается
|
||||
`SHA256SUMS`.
|
||||
@@ -1,160 +0,0 @@
|
||||
# Изменения 2026-10-02 (локально, не опубликовано)
|
||||
|
||||
Что сделано, как проверить, и что из этого подтверждено замерами. Все замеры — на RX 7800 XT /
|
||||
RADV; «слабый CPU» — та же машина, ограниченная `taskset` до 4 ядер / 8 потоков.
|
||||
|
||||
## Производительность и плавность
|
||||
|
||||
### Steam Deck и слабые GPU: путь из релиза 0.1 снова по умолчанию
|
||||
|
||||
Для вывода не 1080p (720p на Деке, 1440p, 2160p) вся игра снова рисуется в разрешении пресета
|
||||
(патч при запуске), как в 0.1. Живое масштабирование оставляло постобработку в 1080p и каждый
|
||||
кадр копировало цели сцены туда-обратно: на Деке и GTX 1060 это давало 7–8 FPS.
|
||||
|
||||
- Выбор пути — настройка «Смена разрешения на лету» (лаунчер, меню, `bbport.ini`
|
||||
`live_resolution=auto|0|1`, применяется после перезапуска). **Авто** (было по умолчанию; с 03.10 по умолчанию «выключена») включает
|
||||
живую смену на дискретных GPU с 8+ ГБ памяти, кроме NVIDIA старше Turing: на RX 7800 XT
|
||||
разрешение и пресеты снова меняются в меню на лету, а Steam Deck и GTX 1060 получают быстрый
|
||||
путь. Решение пишется в журнал (`GPU: … live resolution changes on/off`); `BB_LIVE_RES=0/1`
|
||||
перекрывает всё.
|
||||
- Смена пресета, разрешения вывода или переход на TAA в меню при этом пути — через кнопку
|
||||
«Применить и перезапустить игру».
|
||||
- Глубина/stencil уменьшенных целей на GPU без `VK_EXT_shader_stencil_export` (Pascal) снова
|
||||
в полном размере, без 9-проходного копирования.
|
||||
|
||||
### Рваный фреймтайм при упоре в GPU (NVIDIA)
|
||||
|
||||
Две причины, обе исправлены:
|
||||
|
||||
1. Показ кадра (`vkAcquireNextImage`, `vkQueuePresent`) выполнялся в потоке vblank игры. Когда
|
||||
GPU загружен, эти вызовы блокируются (на NVIDIA особенно), и vblank-события игры приходили
|
||||
неравномерно. Теперь показ — в отдельном потоке `bb:Present` (`BB_PRESENT_THREAD=0` — как
|
||||
раньше).
|
||||
2. Поток команд GPU убегал вперёд и упирался в случайные ресурсы, флипы шли пачками. Теперь он
|
||||
опережает GPU не больше чем на `BB_FRAMES_AHEAD` кадров (по умолчанию 1; 0 — без ограничения).
|
||||
|
||||
4K без апскейлера, ~78 FPS, медленный поворот камеры (`Frame pacing` в журнале, окна по 5 с):
|
||||
|
||||
| | FPS | разброс кадра | p99 | кадров > 1.5× медианы |
|
||||
|---|---|---|---|---|
|
||||
| было | 79.5 | 9.9 мс | 25.8 мс | ~125 |
|
||||
| отдельный поток показа | 79.4 | 5.9 мс | 25.0 мс | ~80 |
|
||||
| + опережение 1 кадр | 77.4 | **0.9 мс** | **14.6 мс** | **0** |
|
||||
|
||||
На слабом CPU (720p FSR 3 Performance, без лимита) опережение на FPS не влияет (166 FPS при 0, 1
|
||||
и 2), а отдельный поток показа дал +3.5% (160 → 166 FPS).
|
||||
|
||||
Новая строка `Frame pacing:` в `BB_FRAME_STATS=1`: медиана, разброс, p99 и число всплесков.
|
||||
|
||||
### Кэш шейдеров отключался навсегда (фризы при входе в каждую новую область)
|
||||
|
||||
Кэш пайплайнов при запуске сравнивает сохранённый `Shader::Profile` с текущим. В исходном
|
||||
shadPS4 при несовпадении кэш **закрывается на всю сессию** и ничего не записывает; файл профиля
|
||||
так и остаётся старым, поэтому дальше кэш не работает никогда, и каждая сессия компилирует все
|
||||
шейдеры заново. Сегодня профиль изменился (флаг для Pascal), и с ~12:16 кэш у всех запусков был
|
||||
выключен — включая забег у арены Людвига: в окне фриза 64 компиляции на 114 мс.
|
||||
|
||||
Теперь несовместимый кэш очищается и строится заново (одна пересборка, как при обновлении). В
|
||||
журнале: `Pipeline cache isn't compatible with current system: rebuilding it`. Проверка: забег
|
||||
по маршруту дважды подряд — 241 компиляция в первом, 28 во втором.
|
||||
|
||||
Тот же фриз на выходе с арены (115 мс, забег игрока): кроме компиляций — регистрация 1057
|
||||
текстур и загрузка 220 МБ изображений и 245 МБ буферов за один кадр (защита страниц 17 мс,
|
||||
резидентность 10.5 мс, создание изображений 7.9 мс, остальное — копирование). Эту часть ещё
|
||||
предстоит разнести по кадрам или потокам.
|
||||
|
||||
### CPU на слабых процессорах
|
||||
|
||||
Профиль (`perf`, 4 ядра / 8 потоков, 720p FSR 3 Performance, без лимита, ходьба): ~166 FPS,
|
||||
предел — второй этап конвейера (`bb:DrawRec`, занят 88–90%; первый этап ждёт его ~40%).
|
||||
Время второго этапа размазано по привязке ресурсов и записи draw, единственного горячего места
|
||||
нет. Для Деки и Haswell (ядра примерно вдвое медленнее) это порядка 85–100 FPS — выше 60, то
|
||||
есть средний FPS там упирается не в CPU. Главная проблема слабых CPU — фризы подгрузки (см.
|
||||
«Инструменты»).
|
||||
|
||||
- Поток записи Vulkan выбирал время активного ожидания по числу всех потоков машины, а не
|
||||
доступных процессу: под ограничением ядер он крутился 200 мкс вместо 20.
|
||||
- Ожидание второго этапа теперь тоже зависит от числа ядер (`BB_PIPE_SPIN_US`); на FPS не
|
||||
влияет (166.3–166.8 при 0, 50 и 200 мкс), оставлено короче для машин с 8 потоками.
|
||||
- Ридбэки (Relaxed, защищают лица от «взрыва» вершин): в той же сцене 166.4 FPS против 166.3 с
|
||||
`BB_READBACKS=0` — ничего не стоят, выключать их незачем.
|
||||
|
||||
## Стабильность
|
||||
|
||||
### Вылет `Guest fault … 0x263b8e7` с двухэтапным конвейером
|
||||
|
||||
`sceGnmSubmitDone` не блокировал игру, если поток команд уже **разобрал** все буферы
|
||||
(`num_submits == 0`), хотя второй этап конвейера и отложенные метки fence ещё не записали память.
|
||||
Игра освобождала объекты с метками, поздняя запись портила кучу. Добавлен признак
|
||||
`work_retired`: GPU считается свободным только после опустошения конвейера и записи меток.
|
||||
|
||||
Проверка: 40 минут на 4 ядрах / 8 потоках без падения. Вылет редкий (старая сборка на моей машине
|
||||
тоже прожила 40 минут), поэтому подтверждением будет только отсутствие вылета у вас.
|
||||
|
||||
## Изображение
|
||||
|
||||
- **Резкость до 2.0** (лаунчер, меню). До 1 — RCAS самого апскейлера, выше — ещё один проход
|
||||
RCAS по готовому кадру (FSR 3, FSR 4, FSR 4.1.1, в том числе вывод 720p на Деке; у TAA — свой).
|
||||
- **Ctrl+клик по ползунку в меню** теперь позволяет ввести число (раньше клавиатура только
|
||||
стирала: в меню не передавался текстовый ввод SDL).
|
||||
- **Мерцающие объекты на NVIDIA GTX 10xx (Pascal)** — гробы на мосту и т. п. (shadPS4 PR #1644
|
||||
чинит это только на Turing и новее через барицентрики). Для GPU NVIDIA без
|
||||
`VK_KHR_fragment_shader_barycentric` рекомпилятор теперь умножает интерполированное значение
|
||||
на 1 + 2⁻¹⁶ перед переводом в целое: 0.99999994 снова даёт 1. Нашёл этот приём в 27 шейдерах
|
||||
Bloodborne; на AMD с принудительным включением (`BB_INTERP_INT_FIX=1`) картинка та же.
|
||||
**Нужна проверка на GTX 10xx.** Кэш шейдеров один раз пересоберётся у всех (новая версия).
|
||||
- **Исправления рекомпилятора шейдеров из свежего shadPS4** (вышли после версии, от которой взят
|
||||
код порта): `V_CVT_PK_U8_F32`, `V_BFM_B32`, `V_ALIGNBIT/ALIGNBYTE_B32` (сдвиг на 32 при нулевом
|
||||
выравнивании давал неопределённый результат), перенос SCC в `S_ADDC/S_SUBB_U32`, 32-битные
|
||||
литералы в 64-битных числах, режим плавающей точки compute-шейдеров, селекторы P10/P20 у
|
||||
`V_INTERP_MOV_F32`. Список — в `gpu/VENDOR.txt`. Такие инструкции встречаются в упаковке
|
||||
глубины/теней; связь с багами теней (shadPS4 #4384, #3465) не подтверждена — проверьте те места.
|
||||
- **TAA.** Пробовал: смешивание в тонмапленном пространстве, обрезку истории в YCoCg, обрезку
|
||||
по дисперсии, восстановление текущего кадра фильтром 3×3. Сравнение в одном запуске с
|
||||
переключением на лету (неподвижная камера и поворот): ни одна техника не дала выигрыша сверх
|
||||
разброса между замерами, обрезка YCoCg и фильтр 3×3 — хуже. По умолчанию остался прежний
|
||||
алгоритм; техники доступны для экспериментов (`BB_TOGGLE_FILE`, биты 51–54). Нужен пример,
|
||||
где именно TAA нестабилен (место, что видно), чтобы чинить конкретно это.
|
||||
|
||||
## Моды, патчи, лаунчер
|
||||
|
||||
- **Моды проверены в игре**: мод, подменяющий тексты меню, работает (пути в верхнем регистре,
|
||||
папка-обёртка). Новое: регистр имён не важен, принимаются `Мод/CUSA03173/dvdroot_ps4`,
|
||||
папка-обёртка архива и моды без `dvdroot_ps4` (сразу `chr/`, `parts/`…). В журнале:
|
||||
`Mods: N game files replaced, M added`.
|
||||
- **Сторонние XML-патчи shadPS4**: папка `patches/` в каталоге данных, группа «Сторонние патчи» в
|
||||
лаунчере, выбор в `patches.json`. Подробности — [MODS.md](MODS.md).
|
||||
- **60/90 FPS** больше не падают с `patch overlaps a relocation`: патчи, заменяющие указатели,
|
||||
загрузчик переносит на реальную базу образа.
|
||||
- **Лаунчер на английском** (по умолчанию — язык системы; переключатель вверху страницы).
|
||||
- Подпись «Ограничение FPS»: 0 — по частоте экрана, не выше 120 Гц.
|
||||
|
||||
## Память
|
||||
|
||||
15 циклов «Выйти из игры → Продолжить» (перезагрузка уровня), 720p FSR 3:
|
||||
|
||||
| циклы | RSS процесса | VRAM |
|
||||
|---|---|---|
|
||||
| 0 → 7 | +179 МБ | +751 МБ |
|
||||
| 11 → 15 | +8 МБ | +15 МБ |
|
||||
|
||||
Рост затухает: при повторных загрузках это прогрев кэшей (пайплайны, текстуры), а не
|
||||
неограниченная утечка. `heaptrack` на 8 таких циклах тоже не нашёл источников, растущих от
|
||||
загрузки к загрузке (крупные — один раз при запуске: таблица страниц 279 МБ, пул текстурного
|
||||
кэша 92 МБ). Кэш текстур по замыслу освобождает память только у бюджета VRAM. 40-минутный
|
||||
прогон в одной локации: RSS ~2.64 ГБ, VRAM ~4.29 ГБ всё время, без роста. Не проверено:
|
||||
долгий проход через много разных локаций (рост кэша до бюджета там ожидаем) — для этого нужен
|
||||
записанный маршрут.
|
||||
|
||||
## Инструменты для тестов
|
||||
|
||||
- **Запись маршрута**: `BB_PAD_RECORD=/путь/route.txt` — F9 в игре начинает и останавливает запись
|
||||
геймпада/клавиатуры. `BB_PAD_REPLAY=/путь/route.txt` + слово `replay` в `BB_PAD_FILE` —
|
||||
воспроизведение с того же момента. Так можно один раз пробежать маршрут (от лампы у Людвига к
|
||||
другой лампе), а потом повторять его в замерах фризов. Воспроизведение проверено; запуск записи
|
||||
клавишей F9 автоматически проверить нельзя — проверьте, пожалуйста.
|
||||
|
||||
Как записать: отдохнуть у лампы у Людвига, выйти в меню и «Продолжить» (персонаж появится у
|
||||
лампы), в лаунчере в «Доп. переменные» указать `BB_PAD_RECORD=/home/<вы>/route.txt`, после
|
||||
появления нажать F9, пробежать до другой лампы, F9. Сохранение тоже понадобится (папка
|
||||
`user/savedata`), чтобы старт был в той же точке.
|
||||
@@ -1,107 +0,0 @@
|
||||
# Изменения 2026-10-03 (локально, не опубликовано)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, ограничение `taskset` до 4 ядер / 8 потоков, настройки Steam Deck
|
||||
(вывод 1280×720, FSR 3 Performance, сцена 640×360), без лимита FPS. Стенд:
|
||||
`out/bb-probe` из исходников, сохранение `docs/savedata_route`, скриптовый вход в уровень.
|
||||
|
||||
## Смена разрешения на лету — по умолчанию выключена
|
||||
|
||||
`live_resolution` без значения (новая установка, лаунчер, меню) теперь означает «выключена»:
|
||||
для вывода не 1080p — патч разрешения при старте, как в 0.1. `auto` (проверка видеокарты) и `1`
|
||||
выбираются явно. Тесты `test_run_settings`.
|
||||
|
||||
## AppImage без моделей FSR 4.1.1
|
||||
|
||||
Модели извлекаются из DLL AMD и в публичный пакет не входят (как в 0.1): `packaging/default.nix`
|
||||
кладёт `fsr4_411` только с `BB_PACKAGE_FSR411=1`; иначе их ищут в каталоге данных.
|
||||
|
||||
## Steam Deck: запуск и сборщик текстур
|
||||
|
||||
- **AppImage не запускала игру на NixOS** (код 127 без сообщения): `run.sh` читал
|
||||
`live_resolution` через `sed`, которого нет в окружении AppImage. Теперь только встроенные
|
||||
команды bash; тест `test_live_resolution_without_sed_or_grep`.
|
||||
- **Сборщик текстур на встроенных GPU** (`TextureCache::GarbageCollectImages`). Бюджет считался
|
||||
как «все кучи минус 8 ГБ для системы» (на Деке ~1 ГБ), а занятость — по кучам GTT, где лежат и
|
||||
буферы с памятью игры (~4 ГБ). Сборщик постоянно работал в «агрессивном» режиме: на каждой
|
||||
отправке команд (~66 за кадр) выкидывал до 40 текстур, не использованных 2–3 кадра, и снова
|
||||
их загружал, а изменённые GPU асинхронно записывал обратно в память игры. Теперь на встроенных
|
||||
GPU пороги — доля текущего бюджета драйвера (`VK_EXT_memory_budget`): сбор с 70%, нехватка с
|
||||
85%, критично с 95%. В журнале при нехватке: `Texture cache: memory pressure, …`.
|
||||
`BB_GC_BUDGET_MB=N` — то же правило с заданным бюджетом на любом GPU (проверка на ПК).
|
||||
- **Запись вытесненной текстуры обратно в память игры теперь синхронная**, пока страницы ещё
|
||||
защищены. Отложенная запись приходила после снятия защиты — поверх того, что игра успела туда
|
||||
записать (например, кучу после выгрузки локации). Возможная причина `Guest fault 0x263b8e7` на
|
||||
Деке при подгрузке карт; **на ПК этот вылет тоже был один раз за ~10 прогонов, его причина не
|
||||
найдена** (порог нехватки памяти на ПК ~11 ГБ, сборщик туда не попадает).
|
||||
|
||||
## Живая смена разрешения дешевле
|
||||
|
||||
Раньше уменьшались только цели 1920×1080; постобработка игры в полуразрешении (960×540, в том
|
||||
числе цепочка bloom с mip-уровнями) шла в полном размере и каждый кадр пересэмплировала цвет
|
||||
сцены. На Деке один такой проход стоил ~8.4 мс (против ~1.4 мс при патче разрешения).
|
||||
|
||||
- Цели 960×540 уменьшаются тем же коэффициентом, что сцена (ProxySize). Пересчёт FragCoord в
|
||||
шейдерах не меняется: 960/320 = 1920/640.
|
||||
- Mip-цепочки полуразрешения: отдельная уменьшенная копия на каждый уровень (ключ — изображение и
|
||||
уровень); выборка одного уровня читает копию, выборка всей цепочки — родное изображение после
|
||||
пересэмплирования.
|
||||
- Переключатель `BB_TOGGLE_FILE` бит 50 (`SceneHalfRes`) — прежнее поведение.
|
||||
- `BB_SCENE_DEBUG`: ещё печатает пересэмплирования (`Scene resolve/fill`) и чтения уменьшенных
|
||||
целей в полном размере с причиной (`Scene native read`).
|
||||
- Тест: `scene-resolution-test` — копия mip-уровня 2 цепочки 960×540 и её возврат в родной уровень.
|
||||
|
||||
Результат на ПК (4c/8t): занятость GPU живого пути 3.41 → 3.23 мс на кадр, проход `5a619b89` —
|
||||
320×180 вместо 960×540. Картинка совпадает с режимом патча (bloom и туман считаются в
|
||||
пониженном разрешении, как при патче). Осталось: ~9 пересэмплирований и ~7 обратных заливок
|
||||
цвета сцены 1080p за кадр (копирования, `9a9cf8a9`, композиция UI).
|
||||
|
||||
**Почему живой путь всё равно дороже на CPU:** игра считает экран 1080p и рисует в проходе
|
||||
освещения (`vs 8747a367`) 335 источников света вместо 42 при патче (мелкие источники
|
||||
отбрасываются по размеру на экране): 975 draw-вызовов на кадр против 679. Это решение самой игры.
|
||||
|
||||
## TAA: мерцание тонких объектов вдали
|
||||
|
||||
Прутья окон, перила и края обломков мерцали: в фазе jitter, где тонкий объект не попадает в
|
||||
центр пикселя, проверка глубины отбрасывала историю (в ней объект ближе текущего фона). Теперь
|
||||
при движении меньше пикселя такая история остаётся и ограничивается цветом соседей (как в TAA без
|
||||
отбрасывания по глубине). Переключатель бит 55 — прежнее правило.
|
||||
|
||||
Метрика: 30 кадров подряд при неподвижной камере (`BB_PRESENT_DUMP_COUNT=30`), средняя вторая
|
||||
разность яркости по времени (мерцание кадр-к-кадру, плавная анимация почти не влияет), ×1e-3:
|
||||
|
||||
| | среднее | p99.9 |
|
||||
|---|---|---|
|
||||
| было (2 прогона) | 0.367 / 0.381 | 23.2 / 22.0 |
|
||||
| стало (2 прогона) | 0.266 / 0.293 | 12.2 / 13.1 |
|
||||
| без TAA и jitter | 0.428 | 16.8 |
|
||||
|
||||
Мягкое ограничение по цвету для неподвижных пикселей и обрезка по дисперсии мерцание не
|
||||
уменьшили (дисперсия — увеличила). Тесты `taa-shader-test`: добавлены «thin line missed by this
|
||||
frame keeps history», «static disocclusion is clipped», «moving disocclusion is rejected».
|
||||
|
||||
## Апскейлеры: сверка с аудитом от 2026-10-02
|
||||
|
||||
Аудит смотрел версию с GitHub (`cc001f9`); по локальному коду:
|
||||
|
||||
- Native AA вне 1080p (2.1) — уже исправлено (`w > ow`).
|
||||
- FSR 4 v07 и HDR-флаг (2.2): провайдер сохраняет флаги контекста, но не читает их — флаг на
|
||||
результат не влияет. На пути с патчем вход действительно LDR (после тонмаппинга) — ограничение
|
||||
самого пути.
|
||||
- Небо (4.1) и порог глубины объектных векторов (4.4) — уже исправлены (поворот без переноса,
|
||||
относительный порог, полная точность глубины).
|
||||
- **Mip bias (4.7) — сделано:** в проходах G-buffer (5+ целей) при уменьшенной сцене смещение
|
||||
LOD сэмплеров log2(рендер / вывод) (на Деке −1); тени, постобработка и UI без изменений. В
|
||||
журнале `Upscaler: scene texture LOD bias -1.00`. Переключатель бит 57.
|
||||
- Не сделано: классификация частиц для jitter (4.2), сброс истории на смену камеры (4.8).
|
||||
|
||||
## CPU на слабых процессорах: замер
|
||||
|
||||
Режим патча, 4c/8t, 209 FPS. Потоки (% одного ядра, пользователь + ядро): `bb:DrawRec` 72 + 14,
|
||||
поток команд GPU 56 + 20, `bb:VkRecorder` 72 + 1 (в основном время RADV), основной поток игры
|
||||
46 + 10, четыре потока подготовки draw по ~20. Всего занято ~545% из 800%: предел —
|
||||
последовательная цепочка «поток команд → запись draw → запись Vulkan». Дальнейший рост требует
|
||||
распараллелить запись Vulkan (несколько командных буферов) — крупная переделка, не начата.
|
||||
|
||||
## Инструменты
|
||||
|
||||
- `BB_PRESENT_DUMP_COUNT=N` — `BB_PRESENT_DUMP_TRIGGER` снимает N кадров подряд.
|
||||
@@ -1,145 +0,0 @@
|
||||
# Изменения 2026-10-04 — 2026-10-06 (версия 0.3)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, Mesa 26, вывод 1080p без лимита FPS, если не сказано иначе.
|
||||
|
||||
## Модель памяти ПК — экспериментальная, по умолчанию выключена
|
||||
|
||||
Включается переключателем лаунчера «Новая модель памяти (экспериментально)» или
|
||||
`BB_GUEST_IN_PLACE=1`; по умолчанию работает модель 0.2. Проверена только на RX 7800 XT и Steam
|
||||
Deck, может вылетать; на NVIDIA dma-buf не отображается со смещением, и порт возвращается к
|
||||
старой модели. Прямая память игры выделяется через Vulkan
|
||||
(dma-buf, видимая GPU системная память), и GPU читает её на месте, как буферы игры для ПК. Копий
|
||||
всей памяти в VRAM и отслеживания записей через защиту страниц больше нет. Метки готовности
|
||||
(EOP/EOS) пишутся, когда GPU действительно выполнил работу.
|
||||
|
||||
- Кадры перекрываются: первые две отправки команд кадра (`BB_SUBMIT_AHEAD=2`) не ждут конца
|
||||
предыдущего кадра. При K ≥ 8 куча игры течёт (~2 объекта за кадр), поэтому есть сторож: рост
|
||||
живых выделений на 100 000 выключает перекрытие до конца сессии.
|
||||
- Отправка команд асинхронная (`BB_ASYNC_SUBMIT`): command buffer'ы сегментов закрывают их потоки
|
||||
записи, сама отправка — упорядоченная задача на них же.
|
||||
- Сигналы GPU копятся на стадии записи draw и публикуются пачкой; ожидания ConstRam — точечные.
|
||||
- Замеры, Yahar'gul, стоя: старая модель 138,7 → 143,6 FPS, новая 101 → 152–155 FPS. Тяжёлое
|
||||
место (~2000 draw за кадр): 95,5 → 115–124 FPS; там теперь упор в стадию записи draw.
|
||||
|
||||
Ключи настроек `pc_memory` и `old_memory_model` больше не читаются: новый ключ `new_memory_model`
|
||||
по умолчанию выключен у всех.
|
||||
|
||||
## Видеопамять больше не растёт от локации к локации
|
||||
|
||||
До исправлений VRAM за 13 минут игры вырос с 3,8 до 7,3 ГБ, теперь держится на 4–5,5 ГБ.
|
||||
|
||||
- **Сборщик текстур не освобождал ничего.** Десять самых старых текстур, в которые рисовал
|
||||
GPU, расходовали весь лимит прохода. Теперь такие текстуры пропускаются и уходят в конец
|
||||
очереди; без нехватки памяти выгружаются текстуры, не использованные `BB_GC_IDLE_SECONDS`
|
||||
(20) секунд. Возраст считается в секундах: прежние 16 отправок команд (~3 кадра) выкидывали
|
||||
всё, что ненадолго ушло с экрана.
|
||||
- **Блоки памяти игры в VRAM никогда не освобождались.** Теперь блоки, не использованные
|
||||
`BB_VRAM_IDLE_SECONDS` (60) секунд, возвращаются в память игры и снова переносятся в VRAM при
|
||||
обращении (без этого просадка 90 → 55 FPS: GPU читал их по PCIe). Блоки памяти, которую игра
|
||||
снимает с отображения, освобождаются. Свободные слоты используются и многоблочными запросами;
|
||||
новые блоки занимают самые заполненные куски по 64 МБ, почти пустые выселяются, пустые
|
||||
возвращаются драйверу.
|
||||
- VMA берёт память блоками по 64 МБ вместо 256 (`BB_VMA_BLOCK_MB`), текстуры от 4 МБ получают
|
||||
отдельное выделение (`BB_VMA_DEDICATED_MB`): удалённые текстуры не держат большие блоки.
|
||||
- Статистика кадров: строка `Memory:` — VRAM и GTT игры по данным ядра, RSS, блоки VMA, все
|
||||
изображения Vulkan, кэш текстур, блоки памяти игры в VRAM, работа сборщика.
|
||||
`tools/load_watch.sh` пишет VRAM/GTT/RSS каждые 5 с.
|
||||
|
||||
## Зависания GPU: найдено место
|
||||
|
||||
Метки GPU (`vk_breadcrumbs.*`, `VK_AMD_buffer_marker`; `BB_BREADCRUMBS=0` — выключить): до и
|
||||
после каждой отрисовки, dispatch и прохода порта GPU пишет номер команды. При потере устройства
|
||||
журнал называет команду, на которой GPU встал, её шейдеры и параметры.
|
||||
|
||||
Найдено (сессия ~20 минут): зависание в непрямом dispatch шейдера частиц `cs 2da7fe60` сразу после
|
||||
`cs 4ca76892` (1×1×1, генератор случайных чисел эмиттеров, счётчик в GDS). Число итераций циклов
|
||||
`2da7fe60` берётся из памяти. Аргументы непрямых dispatch теперь копируются GPU в буфер, видимый
|
||||
процессору: отчёт показывает, что GPU прочитал, а статистика — самые большие непрямые dispatch.
|
||||
**Причина ещё не устранена**: нужен следующий случай с этими данными.
|
||||
|
||||
Зависание больше не выглядит как тихая заморозка картинки при играющем звуке (Steam Deck, бой с
|
||||
Германом): ожидания GPU замечают потерю устройства, а сторож через 10 с без прогресса GPU пишет
|
||||
в журнал отчёт меток и останавливает игру с понятной ошибкой.
|
||||
|
||||
## Падения и запуск
|
||||
|
||||
- Гостевой сбой `0x263b8e7` (частый на Steam Deck): метки EOP писались через `memcpy`, который
|
||||
записывает 4–7 байт двумя перекрывающимися словами. Игра успевала освободить блок между ними.
|
||||
Теперь метки пишутся одной атомарной записью (`store_once`, `StoreLabel`). На Deck не проверено.
|
||||
- NVIDIA с новой моделью памяти падала при запуске: драйвер не отображает dma-buf со смещением.
|
||||
Перед включением модели порт проверяет это и при неудаче остаётся на старой модели.
|
||||
- Проверка файлов игры (`scripts/game_check.py`, issue #7, #13, #14): хеш загруженного образа
|
||||
eboot сравнивается с CUSA03173 1.09; без обновления, со старым eboot или другим изданием запуск
|
||||
останавливается с объяснением, лаунчер показывает то же. `BB_SKIP_GAME_CHECK=1` — обход.
|
||||
|
||||
## Лаунчер и ввод
|
||||
|
||||
- Выпадающие списки с длинными вариантами («Фоновая загрузка в видеопамять», конвейер GPU,
|
||||
чтение данных GPU, контроллер) показывают короткие названия, а пояснение к выбранному —
|
||||
строкой под заголовком: текст больше не обрезается.
|
||||
|
||||
- Выбор контроллера (issue #15): *Управление → Контроллер* (`BB_GAMEPAD`, GUID или часть
|
||||
имени); выбранный берётся, как только подключится. Список — `bb-gpu-capabilities --gamepads`.
|
||||
- Ввод имени персонажа в окне поверх игры (issue #19, патч из issue); ввод в игру на это время
|
||||
нейтрален (Backspace не нажимает тачпад).
|
||||
- Курсор скрыт в полноэкранном режиме и через 3 с без движения мыши в окне (issue #3).
|
||||
- **Переназначение клавиатуры и геймпада**: *Управление → Клавиатура / Геймпад*, кнопка
|
||||
«Назначить» ждёт нажатия. В `bbport.ini` — строки `key.<кнопка>=` и `pad.<кнопка>=` с именами
|
||||
SDL; нет строки — назначение по умолчанию.
|
||||
|
||||
## Патчи игры
|
||||
|
||||
- **Sprint Fix (High FPS)**: выше ~147 FPS (а иногда и выше ~90) бег
|
||||
падал до половины скорости — детектор стены игры мерил расстояние за кадр. Патч включается
|
||||
всегда вместе с режимами 90 FPS и «без ограничения».
|
||||
- **Чёрный тонмаппинг на процессорах Intel** (темнее, чем на PS4; сильнее всего в DLC): код игры
|
||||
выполняется нативно, а приближённые float-инструкции Intel дают другие значения. Патч
|
||||
исправления включается сам на процессорах Intel; `BB_INTEL_TONEMAP_FIX=0/1` — принудительно.
|
||||
|
||||
## Сборка ассетов FSR 4.1.1
|
||||
|
||||
- Новейший GE-Proton выбирается по версии (`ls | tail -1` брал 9-x вместо 10-x; issue #4), также
|
||||
`~/.steam/root` и Flatpak; захват без кадров останавливает сборку с объяснением.
|
||||
- На RDNA4 DLL, вероятно, выбирает FP8-вариант шейдеров (issue #12): захват скрывает от
|
||||
vkd3d-proton `VK_KHR_cooperative_matrix`, `VK_NV_cooperative_matrix2`, `VK_EXT_shader_float8`. На
|
||||
RX 7800 XT захват совпадает побайтно; на RDNA4 не проверено. `extract.py` при нарушении правил
|
||||
ничего не пишет и объясняет причину.
|
||||
- Инструменты из системы без Nix; Nix без канала — `channel:nixos-unstable` (issue #22).
|
||||
|
||||
## Апскейлеры: мыло и полосы на лестницах
|
||||
|
||||
- **Векторы камеры: перевёрнутая вертикаль.** `camera_motion.comp` и проверка глубины в
|
||||
`taa.comp` считали, что ось +y вида направлена вверх по экрану; у игры она вниз (по строкам).
|
||||
Ошибка затрагивала наклон камеры и вертикальное движение, поворот влево-вправо был верным.
|
||||
Поэтому видно было там, где камера покачивается вверх-вниз: на лестнице к собору от гробницы
|
||||
Идон и в канализации. FSR 4 и 4.1.1 давали полосатое мыло, FSR 3.1 — общее размытие, а персонаж
|
||||
с векторами объектов оставался чётким. Проверка: на статичных мешах, у которых есть и векторы
|
||||
объектов (позиции вершин с GPU), новые векторы камеры совпадают с ними в пределах 0,05 px на
|
||||
17 кадрах бега по лестнице, прежние расходились на 1,3–3,7 px. В игре на FSR 4.1.1 1080p
|
||||
Performance «стало сильно лучше».
|
||||
- **Анизотропная фильтрация 16x** для текстур отрисовок сцены (`BB_ANISO`, по умолчанию 16;
|
||||
0 — фильтры игры). Игра ставит большинству текстур 1x, а при апскейле поверхности под острым
|
||||
углом теряют детали. Переключатель для A/B на лету — бит 61 в `BB_TOGGLE_FILE`.
|
||||
- Дамп апскейлера (`BB_DUMP_TRIGGER`) работает и на HDR-пути, пишет матрицы камеры кадра;
|
||||
`tools/dump_view.py` переводит дамп в PNG.
|
||||
|
||||
## MangoHud: двойной оверлей
|
||||
|
||||
Текст MangoHud двоился со сдвигом и разными значениями: рисовались два оверлея. Встроенный в
|
||||
AppImage слой называется `VK_LAYER_MANGOHUD_overlay_64_x86_64` (nixpkgs), системный — иначе,
|
||||
и загрузчик Vulkan грузил оба. На Steam Deck в игровом режиме то же дают оверлей
|
||||
производительности Steam (mangoapp) и MangoHud из лаунчера. Теперь `run.sh` при `MANGOHUD=1`
|
||||
не включает MangoHud в игре, если запущен mangoapp, и отключает встроенный слой, если в системе
|
||||
есть свой (`VK_LOADER_LAYERS_DISABLE`). На NixOS без системного MangoHud не воспроизводилось.
|
||||
|
||||
## Окклюжн куллинг (ветка rainmakerv3/shadPS4 `opts`)
|
||||
|
||||
Коммит «correct queries» заменяет фальшивый счётчик ZPASS_DONE настоящими occlusion queries
|
||||
Vulkan. В Соборном округе Bloodborne делает ~0–8 таких запросов за 5 с и не использует
|
||||
предикацию (`SET_PREDICATION` — 0), то есть отрисовки по ним не отсекает: переносить нечего.
|
||||
Сетевой поток в офлайне (коммит про epoll в той же ветке) у нас ядро не занимает.
|
||||
|
||||
## Тесты
|
||||
|
||||
`tests/test_game_check.py`; `build.sh --test` снова собирается (заглушка
|
||||
`runtime_memory_note_write` в `tests/test_file_mods.c`).
|
||||
@@ -1,434 +0,0 @@
|
||||
# Изменения после 0.4 (с 2026-10-08)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, Кошмар охотника у лампы (сохранение пользователя), 1080p без лимита
|
||||
FPS, новая модель (`BB_PC_MODEL=1`); «4 ядра» — `CPUS=0-3,8-11` (4 ядра / 8 потоков).
|
||||
Подробности и что осталось — [EMULATION_REMOVAL_PLAN.ru.md](EMULATION_REMOVAL_PLAN.ru.md),
|
||||
«Шаг 3».
|
||||
|
||||
## Метки EOP/EOS пишет видеочип (новая модель)
|
||||
|
||||
- `EVENT_WRITE_EOP` без прерывания, `EVENT_WRITE_EOS` и `RELEASE_MEM` очередей compute:
|
||||
буферный маркер Vulkan (`VK_AMD_buffer_marker`, конец конвейера) прямо в память игры, в той же
|
||||
точке потока, где их пишет командный процессор PS4. Раньше каждую метку писал поток процессора
|
||||
после завершения всей отправки Vulkan — ~330 меток за кадр, 61 тыс. в секунду.
|
||||
- 64-битная метка — две 32-битные записи, старшая половина первой (кто видит новую младшую,
|
||||
видит и новую старшую).
|
||||
- Метки с прерыванием, а также без `VK_AMD_buffer_marker` (NVIDIA) и с `BB_FREE_CHECK=1` — как
|
||||
раньше. `BB_GPU_LABELS=0` — как раньше везде.
|
||||
- EOS с записью из GDS: копия видеочипом в порядке потока вместо ожидания всего видеочипа
|
||||
(`Finish()`) и записи процессором.
|
||||
- Статистика: строка `EOP fences` показывает, сколько меток за кадр записал видеочип.
|
||||
- FPS тот же (16 потоков: 233; 4 ядра: ~217 против ~215), картинка та же; 8 минут ходьбы — см.
|
||||
ниже.
|
||||
|
||||
## Перехваты кода игры без ловушек
|
||||
|
||||
- Точки, где игра выделяет память GPU (аллокатор диапазонов, три места кучи) и где загрузчики
|
||||
копируют ресурсы (`memcpy`), раньше были `int3`: каждый вызов — сигнал ядра (SIGTRAP), ~1200
|
||||
в секунду только у аллокатора. Теперь переход в заглушку рядом с образом игры: заглушка
|
||||
вызывает обработчик и выполняет перекрытые инструкции; вызов `memcpy` идёт через трамплин.
|
||||
- Байты переписываются поверх уже стоящего `int3` (поток, наткнувшийся на него в этот момент,
|
||||
уходит в заглушку). `BB_GUEST_HOOK_TRAPS=1` — ловушки, как раньше. Диагностические точки
|
||||
(`BB_FRAME_STATS`, `BB_HEAP_SITES`) остались ловушками.
|
||||
|
||||
## Новая модель памяти на NVIDIA
|
||||
|
||||
- Новый способ отдать память игры видеочипу — импорт памяти хоста
|
||||
(`VK_EXT_external_memory_host`): кусок прямой памяти остаётся в memfd рантайма, видеочип
|
||||
импортирует его второе отображение. Процессор работает с обычной кэшируемой памятью, видеочип —
|
||||
через шину, как с upload heap игры для ПК. Освобождённая память в таких кусках обнуляется, а не
|
||||
выбрасывается из memfd (видеочип держит эти страницы).
|
||||
- При запуске самопроверка в точности как в работе: импорт куска memfd, привязка к sparse-буферу,
|
||||
запись видеочипом — чтение процессором через другое отображение, запись процессором — копия
|
||||
видеочипом; sparse-буфер для такой памяти должен принимать и VRAM. Не прошла — dma-buf (на
|
||||
NVIDIA — «кусок на выделение»), для которого теперь меряется скорость чтения процессором через
|
||||
отображение драйвера (меньше 1 ГБ/с — не используется: код игры работает на этой памяти). Не
|
||||
прошло и это — старая модель. В журнале: `Guest memory: host memory imported by the GPU`,
|
||||
`host memory import does not work here: …`, `CPU reads through the driver's dma-buf: N MB/s`.
|
||||
- Порядок: на AMD — dma-buf (как раньше; RADV не импортирует memfd —
|
||||
`ErrorInvalidExternalHandle`, проверено), на остальных — сначала импорт. `BB_GUEST_MEMORY=host`
|
||||
или `dmabuf` выбирает сам.
|
||||
- Арена (sparse-буфер памяти игры) создаётся с тем типом внешней памяти, который выбран; если
|
||||
драйвер с ним не пускает в арену VRAM — старая модель с сообщением в журнале.
|
||||
- Лаунчер: переключатель *Новая модель памяти и трансляции* доступен на AMD и NVIDIA (тексты на
|
||||
трёх языках). Без лаунчера — `BB_PC_MODEL=1`.
|
||||
- **На NVIDIA не проверено** (у автора только AMD): нужен журнал с NVIDIA.
|
||||
|
||||
## Диагностика
|
||||
|
||||
- `BB_DCB_STATS=1`: кроме структуры буферов команд — пакеты PM4 в секунду по типам, события
|
||||
`EVENT_WRITE` по типам, EOP по источнику данных и прерыванию, EOS по команде, `WAIT_REG_MEM` по
|
||||
месту и движку, и чем закончились ожидания (метка VO, метка в порядке потока, выполнено сразу,
|
||||
ждали — и сколько мс в секунду).
|
||||
|
||||
## Шейдер копирования игры: записи остаются в памяти игры; трансляция вместо подмены — по выбору
|
||||
|
||||
- Вычислительный шейдер копирования игры (`cs fefebf9f`, 2–50 запусков за кадр) выполнялся
|
||||
подменой: процессор при записи команд читал список копий из памяти игры и делал копии своим
|
||||
шейдером. Теперь его можно транслировать как есть (`BB_COPY_SHADER_NATIVE=1`, только новая
|
||||
модель): список читает видеочип во время выполнения, как на PS4.
|
||||
- Найдено: рядом с тем, куда он копирует, лежат код шейдеров и таблицы описателей, которые наш
|
||||
транслятор читает процессором. Пока эти блоки в памяти игры, всё верно; копия в VRAM (правило
|
||||
«видеочип пишет это 3 кадра из 60») давала транслятору устаревший код — чёрная сцена. Подмена
|
||||
держала их в памяти игры случайно (её диапазоны каждый раз разные). Теперь записи этого шейдера
|
||||
в обоих режимах остаются в памяти игры (`force_writes_in_place`).
|
||||
- По умолчанию — подмена: родной шейдер на 4 ядрах стоит потоку записи отрисовок ~0,3 мкс на
|
||||
пакет (+10 %), видеочипу +2 %. Бит 2 второго числа `BB_TOGGLE_FILE` переключает на ходу.
|
||||
|
||||
## Константы на месте (по выбору) и привязка буферов
|
||||
|
||||
- `BB_CONSTANTS_IN_PLACE=1` (новая модель): маленькие буферы только для чтения (константы)
|
||||
видеочип читает прямо из памяти игры, а не из копии, сделанной при разборе (кольцо констант) или
|
||||
при записи (стрим-буфер). Копии защищали отрисовку от записей командного процессора, которые
|
||||
процессор выполнял «наперёд»; в новой модели эти записи выполняет видеочип в порядке потока,
|
||||
так что копия больше не нужна для правильности. Замер (4 ядра, у лампы): разбор −0,8 мкс на
|
||||
отрисовку, видеочип +3 % (константы идут по PCIe), FPS тот же — поэтому выключено по умолчанию.
|
||||
Бит 1 второго числа `BB_TOGGLE_FILE` переключает на ходу.
|
||||
- Привязка буфера только для чтения, уже лежащего в памяти игры, запоминается до изменения
|
||||
резидентности (без обхода блоков на каждую привязку); проверки «на месте ли буфер» для
|
||||
диагностики `BB_TRACE_SHADER` больше не выполняются, когда она выключена (были две на привязку).
|
||||
|
||||
## Исправление
|
||||
|
||||
- Реестр обработчиков ошибок доступа к памяти терял обработчик с тем же приоритетом, что у уже
|
||||
зарегистрированного (множество упорядочивалось только по приоритету). Порядок теперь явный:
|
||||
диагностическая ловушка VRAM, менеджер страниц, SRT walker.
|
||||
|
||||
## Диагностика (новая)
|
||||
|
||||
- `BB_RES_STATS=1`: откуда шейдеры берут описатели ресурсов — из регистров или из таблиц в памяти
|
||||
(SRT walker), по видам, на привязку.
|
||||
- `BB_VRAM_ACCESS_TRAP=1` (+ `BB_VRAM_TRAP_RANGE=адрес,размер`): блоки памяти игры, перенесённые
|
||||
в VRAM, закрываются от процессора; печатаются места кода, которые к ним обращаются (кто читает
|
||||
устаревшую копию).
|
||||
- `BB_WATCH=адрес,размер`: что транслятор читает из диапазона (указатели в пользовательских
|
||||
данных, буферы, код шейдеров). `BB_TRACE_ADDR=адрес,размер` (с `BB_TRACE_SIZE`): привязки над
|
||||
диапазоном с самого начала. `BB_TILE_BACK_LOG=1`: образы, записанные обратно в память, потому
|
||||
что их читают как текстурный буфер. `BB_COPY_SHADER_TRACE=1`: буферы шейдера копирования.
|
||||
|
||||
## Occlusion-запросы игры — настоящие запросы Vulkan (новая модель)
|
||||
|
||||
- Bloodborne делает около 60 occlusion-запросов за кадр (`ZPASS_DONE`); раньше они получали
|
||||
фиктивную последовательность «всё видно». Теперь сэмплы считают запросы Vulkan, а результаты
|
||||
записывает видеочип в память игры в формате PS4 (счётчики блоков глубины с битом готовности).
|
||||
Проходы рендера не разрываются: запрос режется на границах проходов и буферов команд, запись
|
||||
результатов откладывается до ближайшей точки вне прохода и делается одним запуском шейдера. На
|
||||
4 ядрах разница с фиктивным счётчиком — в пределах шума. `BB_OCCLUSION_QUERIES=0` — как раньше.
|
||||
- `COPY_DATA` (константы, регистры, часы, память, GDS) — записи и копии видеочипа в порядке потока;
|
||||
`COND_EXEC` — условие по метке в порядке потока или после работы видеочипа до пакета;
|
||||
`SET_PREDICATION` (ZPass, Bool64/32) — отрисовки и запуски с битом предиката пропускаются, если
|
||||
предикат не выполнен. Bloodborne последние три не использует; это для переноса других игр.
|
||||
- `BB_PM4_SELFTEST=1`: самопроверка всего этого в работающей игре (8 проверок, печатает PASS/FAIL).
|
||||
- Исправлена взаимоблокировка при чтении процессором данных видеочипа из потока разбора.
|
||||
|
||||
## Шейдер копирования игры — по умолчанию как есть
|
||||
|
||||
- Шейдер копирования игры (`cs fefebf9f`, ~36 запусков за кадр) теперь по умолчанию переводится и
|
||||
выполняется сам: список копий читает видеочип. `BB_COPY_SHADER_NATIVE=0` — прежняя подмена (список
|
||||
читает процессор). Цена на 4 ядрах — +6 % к потоку записи отрисовок, видеочип и FPS те же.
|
||||
- Чтобы цена была такой: запись видеочипа в память ищет затронутую текстуру только по первому байту
|
||||
(нужна лишь текстура, начинающаяся с этого адреса; раньше обходился весь диапазон под мьютексом), а
|
||||
запись поверх страниц, которые процессор не менял и которые уже помечены как данные видеочипа,
|
||||
проходит без блокировки трекера.
|
||||
- Подмены «копирование текстуры через буферы» и «очистка текстуры через буфер» оставлены: путь через
|
||||
память (выгрузить отрисованный кадр в память, выполнить шейдер игры, перезалить текстуру) сейчас
|
||||
даёт застывшую картинку — содержимое целей рендера не доходит до памяти игры, а при рендере сцены в
|
||||
пониженном разрешении его там и нет. `BB_HLE_OFF=1|2|4` — опыт с отключением подмен,
|
||||
`BB_HLE_STATS=1` — сколько раз они срабатывают (~4, ~15 и ~10 за кадр).
|
||||
- Опыт `BB_GUEST_VRAM=1`: вся память игры в VRAM через Resizable BAR — 16 FPS уже в меню (процессор
|
||||
читает её через PCIe), не годится.
|
||||
|
||||
## Сохранения не портятся при вылете (#64)
|
||||
|
||||
- Bloodborne сохраняется примерно раз в секунду: удаляет `backupN`, переименовывает `userdataN` в
|
||||
`backupN` и пишет новый `userdataN` (1,3 МБ). Вылет или снятие процесса посреди записи оставляли
|
||||
обрезанный `userdataN`.
|
||||
- Теперь файл сохранения (`/savedataN/...`), открытый на запись, пишется во временную копию
|
||||
(`<имя>.<n>.bbtmp` рядом), а при закрытии она сбрасывается на диск (`fsync`) и одной операцией
|
||||
`rename` встаёт на место файла. Вылет посреди записи оставляет прежний файл (или его отсутствие,
|
||||
если игра уже переименовала его в `backupN`: тогда игра сама читает `backupN` — проверено).
|
||||
- Пока файл открыт, операции по пути (`stat`, `access`, `truncate`, повторное открытие) видят копию:
|
||||
игра проверяет размер записываемого файла по пути.
|
||||
- Копия не попадает в списки каталога; оставшиеся после вылета копии удаляются при монтировании.
|
||||
- `param.bin` и `icon0.png` записываются так же (временный файл, `fsync`, `rename`).
|
||||
- `BB_SAVE_TRACE=1` — все операции игры с файлами сохранений в журнал.
|
||||
|
||||
## Подготовка файлов игры и геймпад
|
||||
|
||||
- `prepare.py` и остальные скрипты, лаунчер: текстовые файлы читаются и пишутся в UTF-8 явно, вывод в
|
||||
консоль не падает на символах вне её кодировки (#55: китайская, японская, корейская локаль Windows,
|
||||
`™` в названии игры).
|
||||
- `eboot.bin` (и модули `sce_module`) принимаются и обычным ELF, без обёртки SELF: так их пишут
|
||||
некоторые программы дампа (#49). Проверено: ELF, извлечённый из нашего eboot, даёт тот же `boot.bin`
|
||||
байт в байт.
|
||||
- Стики (PR #71; #70): клон Switch Pro, у которого все четыре оси в покое смещены на
|
||||
~±16380 и ход вдвое меньше, калибруется — нейтраль берётся из спокойного окна после подключения,
|
||||
ход каждой оси — по её собственному диапазону. Мёртвая зона как в shadPS4: `BB_PAD_DEADZONE`
|
||||
(по умолчанию 5 из 127) и `BB_PAD_DEADZONE_OUTER` (127). `BB_PAD_CENTER=lx,ly,rx,ry` — нейтраль
|
||||
вручную, `BB_PAD_CENTER_CAL=0` — без замера.
|
||||
Отличие от PR: нейтраль принимается, только если смещены все четыре оси (иначе обычный геймпад,
|
||||
подключённый с зажатым стиком, запомнил бы этот наклон как нейтраль до переподключения); пока
|
||||
нейтраль не известна, стики читаются как раньше, а не стоят на месте.
|
||||
- README.ru.md: адрес репозитория в команде `git clone` (PR #43; в английском README он уже был).
|
||||
|
||||
## Зависания видеочипа: непрямые запуски, VRAM, сведения о видеокарте
|
||||
|
||||
- Непрямые запуски (#34, комментарий к #32; новая модель памяти): дважды игра теряла устройство на
|
||||
симуляции частиц, получившей вместо числа групп мусор (1,2 и 3,8 млрд групп; у нас в логах от
|
||||
2026-10-06 такое тоже было — 1,4 млрд и 6,4 млн, обошлось). Настоящие числа у Bloodborne — не больше
|
||||
нескольких сотен. Теперь перед каждым непрямым запуском шейдер из одного потока
|
||||
(`indirect_guard.comp`) копирует числа групп в наш буфер, обнуляя неправдоподобные (больше
|
||||
пределов устройства или `BB_INDIRECT_GUARD_MAX` групп всего, по умолчанию 4M), и запуск идёт из
|
||||
этой копии. Худшее, что теперь бывает, — пропущенный шаг частиц вместо вылета; пропуски
|
||||
печатаются строкой `Indirect dispatches: N skipped ...`. `BB_INDIRECT_GUARD=0` — как раньше.
|
||||
Причина мусора в памяти игры не найдена (только новая модель, редко). Цена: в пределах шума
|
||||
(183–184 FPS, видеочип 5,42–5,47 мс в одной сцене). Проверено с порогом 10 (пропуски
|
||||
срабатывают, игра идёт) и под слоем валидации (новых ошибок нет).
|
||||
- Перенос блоков памяти игры в VRAM теперь учитывает бюджет видеопамяти драйвера: если занято больше
|
||||
`BB_VRAM_PROMOTE_PERCENT` (по умолчанию 90 %), новые копии не создаются — блоки читаются на месте
|
||||
(медленнее, но без переполнения VRAM на картах с 8 ГБ), а перенос возобновляется, когда память
|
||||
освободится. Строка в логе при переключении. Число «MiB of loaded blocks moved ... to VRAM» —
|
||||
накопительное (сколько перенесено за сессию), а не занятое место; занятое — в строке `Memory:`.
|
||||
- В начале лога теперь строка с видеокартой, объёмом VRAM, драйвером и версией Vulkan (в отчётах
|
||||
о зависаниях, как #56, её не было).
|
||||
- #56 (старая модель, зависание при загрузке карты): два лога — два разных места остановки (обычная
|
||||
отрисовка из 5 индексов и копирования между проходами), шейдер из первого у нас работает 4–12 раз
|
||||
за кадр без проблем, те же патчи (Disable DoF, Motion Blur) у нас включены. Воспроизвести не
|
||||
удалось; без модели видеокарты и версии Mesa дальше не продвинуться (теперь они будут в логе).
|
||||
|
||||
## Баги из issues (2026-10-08, день)
|
||||
|
||||
- **Сборка FSR 4.1.1 там, где контейнер Steam не запускается (#61).** Bazzite + 9070 XT: ни один
|
||||
Proton «не запустил FSR 4.1», причина в списке была пустой, а в конце лога — `bwrap: No permissions
|
||||
to create a new namespace`: контейнер Steam (pressure-vessel, и под umu-run тоже) не может создать
|
||||
пространство имён — так бывает, когда сам лаунчер запущен в песочнице (Flatpak, ярлык Steam с
|
||||
рантаймом) или ядро это запрещает. Теперь при такой ошибке Proton запускается напрямую, без
|
||||
контейнера (сообщение `Steam's container could not start here (...): running Proton without it`,
|
||||
дальше до конца сборки так же); `BB_FSR4CAP_RUNNER=direct` — сразу так.
|
||||
Причина отказа каждого Proton — последняя непустая строка журнала записи, иначе ошибка контейнера
|
||||
или Wine (раньше — последняя строка, часто пустая). Один и тот же Proton больше не пробуется дважды
|
||||
по путям `/home/...` и `/var/home/...` (Fedora Atomic): одинаковые папки узнаются по устройству и
|
||||
inode, а не по `realpath`.
|
||||
Проверено: запись внутри `bwrap --disable-userns` (та же ошибка контейнера) — запасной путь
|
||||
записал кадры FSR 4.1.1 под GE-Proton 11-6, результат совпадает с обычной записью побайтно (кроме
|
||||
адресов указателей в `trace.txt`).
|
||||
- **MangoHud снова работает с системным MangoHud (#66).** С 0.3 `run.sh` при системном MangoHud
|
||||
отключал встроенный в AppImage слой по имени (`VK_LOADER_LAYERS_DISABLE`), но у системного слоя
|
||||
обычно то же имя (`VK_LAYER_MANGOHUD_overlay_64_x86_64`) — загрузчик Vulkan отключал оба. Теперь
|
||||
встроенный MangoHud убирается из путей поиска слоёв (`XDG_DATA_DIRS`), а системный остаётся.
|
||||
Проверено загрузчиком Vulkan (`VK_LOADER_DEBUG=layer`): раньше «forced disabled», теперь слой
|
||||
вставляется.
|
||||
- **Картинка предмета на экране загрузки не пропадает (#67, патч bmy).** Маски Scaleform рисуются
|
||||
без записи цвета; при выводе не 1080p они попадали в глубину игры, а следующая отрисовка UI
|
||||
переключалась на глубину UI размера вывода с очищенным трафаретом — картинка не проходила тест.
|
||||
Теперь отрисовки UI без цели цвета тоже идут в глубину UI. В игре не проверено (на macOS у автора —
|
||||
да).
|
||||
- **Выбор монитора (#69).** Окно и полноэкранный режим шли на монитор, который SDL считает основным.
|
||||
*Экран → Монитор* в лаунчере (`BB_DISPLAY`: номер или часть имени; список —
|
||||
`bb-gpu-capabilities --displays`), в логе при запуске — строки `Display N: ...` с отметкой
|
||||
выбранного. Проверено на Hyprland с двумя мониторами: полноэкранное окно открывается на выбранном
|
||||
(обычное окно на Wayland размещает композитор).
|
||||
- **Дебаг-меню: шрифты из мода находятся.** Патч дебаг-меню читает шрифты из
|
||||
`dvdroot_ps4/adhoc/font/DbgFont14h.ccm` и `.tpf` (и шейдеры `adhoc/FontShader/debugFont_*`),
|
||||
причём игра запрашивает имена в нижнем регистре (`dbgfont14h.ccm`, найдено с новым
|
||||
`BB_FILE_TRACE=1`). Проверка в `patches.py` искала их в `dvdroot_ps4/font` с точным регистром, а
|
||||
рантайм на Linux различал регистр — шрифты не находились ни модом, ни в дампе, и запуск падал с
|
||||
трассировкой Python. Теперь: рантайм, не найдя файл игры, ищет его без учёта регистра (как PS4 и
|
||||
shadPS4; только при промахе, без затрат для найденных файлов); проверка смотрит в `adhoc/`
|
||||
без учёта регистра; без шрифтов патч дебаг-меню не применяется (строка `Patches: debug menu off:
|
||||
...`, подсказка, если шрифты лежат в старом `dvdroot_ps4/font`), игра запускается; папка `adhoc`
|
||||
в моде узнаётся как папка игры. Проверено: мод `Adhoc/Font/DbgFont14h.*` открывается игрой
|
||||
(`Mods: open /app0/dvdroot_ps4/adhoc/font/dbgfont14h.ccm -> .../Adhoc/Font/DbgFont14h.ccm`).
|
||||
С настоящими шрифтами меню не проверено (их нет).
|
||||
- `BB_FILE_TRACE=1`: каждый путь, который игра открывает, проверяет или читает (`File trace: ...`).
|
||||
- **Экрана «Играть по сети / вне сети» больше нет.** У порта нет PSN; теперь игра сразу открывает
|
||||
главное меню в режиме вне сети — при запуске и после «Выйти из игры», с сохранением и без. Патч игры
|
||||
«Skip Online/Offline Choice» (`patches/Bloodborne.xml`): функция, которая строит этот диалог
|
||||
(`0x1b39030`, найдена по ID строк 401010/401011 из `menu.msgbnd`), возвращает то же, что вернул бы
|
||||
её пункт «ИГРАТЬ ВНЕ СЕТИ» (вызов его обработчика `0x1b49cd0`, vtable `0x533be30`): следующий
|
||||
диалог цепочки, так что выбор не показывается. Включается `patches.py` по умолчанию (лаунчер:
|
||||
*Эффекты игры → Пропускать выбор*, `BB_SKIP_NETWORK_CHOICE=0` — показывать). Прежний вариант
|
||||
(порт нажимал «крест», экран мелькал) удалён. `tools/restart.sh` по-прежнему показывает выбор
|
||||
(его сценарий сам нажимает на титуле). Проверено скриншотами: запуск без ввода — сразу главное
|
||||
меню; «Продолжить» → «Выйти из игры» — сразу главное меню.
|
||||
- Память при повторной загрузке локации (6 циклов «Продолжить → выйти», новая модель): RSS
|
||||
707 → 733 МиБ, VRAM выходит на ~3,7 ГБ; старая модель (4 цикла): RSS 3735 → 3795 МиБ, VRAM ~3 ГБ.
|
||||
Утечки на гигабайты (сообщение из Discord про телепорты) так не видно; телепорт между разными
|
||||
локациями не проверен.
|
||||
- **Нехватка видеопамяти у копий памяти игры не роняет игру.** Старая модель держит копии памяти
|
||||
игры в блоках VRAM по 64 МиБ; на картах с 4 ГБ (RTX 3050 mobile, сообщение из Discord) выделение
|
||||
блока могло вернуть `ErrorOutOfDeviceMemory`, и игра останавливалась (`vk_platform.h:73`, код 23).
|
||||
Теперь такой блок берётся из системной памяти, которую GPU читает по шине (строка `Guest memory:
|
||||
VRAM is full ...`): медленнее, но без вылета. `BB_RESIDENCY_VRAM_LIMIT_MB=N` — проверка (VRAM
|
||||
«кончается» после N МиБ блоков). Проверено с 32 МиБ: игра идёт, картинка верна, 28 FPS (всё в
|
||||
системной памяти; в жизни туда уходит только излишек). То ли это место, где упал отчёт из
|
||||
Discord, не доказано — строки перед остановкой в нём нет.
|
||||
- **«Чтение данных GPU: Precise» больше не замораживает первую катсцену.** Новая игра → катсцена
|
||||
стояла на первом кадре (звук шёл): видеоплеер писал кадр ролика в память, которую GPU писал
|
||||
раньше; обработчик попросил поток GPU о чтении, а тот, снимая копию байтов игры для «двойников»
|
||||
(`BB_GPU_WRITE_TWINS`) под их мьютексом, сам попал на защищённую от чтения страницу и в
|
||||
обработчике ждал тот же мьютекс. Найдено по стекам потоков (SIGUSR2). Теперь байты игры для
|
||||
«двойников» читаются через вид без защиты (`ReadBacking`), а с `BB_READBACKS=2` «двойники»
|
||||
выключены: страница с байтами GPU, в которую игре можно писать, была бы «только для записи»
|
||||
(следующей остановкой был `page_manager.cpp: ... write-only`). Проверено: новая игра с
|
||||
`BB_READBACKS=2` и без — катсцена идёт до создания персонажа.
|
||||
- **Меню настроек — как в ПК-играх, в стиле Bloodborne.** Вместо окна ImGui поверх игры
|
||||
(Insert / L3+R3): игра затемнена, по центру панель с орнаментными линиями, как у диалогов самой
|
||||
игры (шрифт с засечками DejaVu Serif, встроен рядом с DejaVu Sans), заголовок и вкладки
|
||||
«Изображение / Апскейлер / Эффекты игры / Дополнительно» (L1/R1, Q/E, PageUp/PageDown, мышь).
|
||||
Каждая настройка — строка «название ◀ значение ▶»: вверх/вниз (крестовина, стик, стрелки) —
|
||||
выбор строки, влево/вправо — значение, Крест/Enter — следующее значение, мышь — клик по
|
||||
стрелкам или строке, ползунки тянутся мышью. Описание выбранной настройки — внизу (вместо «(?)»),
|
||||
там же предупреждение о перезапуске; строка «Применить и перезапустить игру» — когда нужно.
|
||||
Подсказки кнопок — в подвале; Esc / Круг / Insert / L3+R3 — закрыть. Недоступные значения
|
||||
(FSR 4 без поддержки) пропускаются, зависимые строки гаснут (пресет при TAA, сила резкости без
|
||||
резкости, маска реактивности). Фокус — свой (навигация ImGui до строк не доходила).
|
||||
`BB_MENU_KEYS_FILE=<файл>` — сценарное управление меню для проверок (`toggle up down left right
|
||||
enter back l1 r1 kup kdown`).
|
||||
- **Настройки порта — родные страницы в меню игры «Система».** После «Экран/звук» появились пункты
|
||||
«Изображение», «Эффекты игры» и «Патчи игры»: их строит и рисует сама игра (её строки, списки,
|
||||
ползунок, подсказка внизу, «По умолчанию» с подтверждением), и на титульном экране, и в паузе.
|
||||
«Изображение»: разрешение вывода, апскейлер (в списке только то, что эта видеокарта и сборка
|
||||
умеют: без NVIDIA нет DLSS), пресет, резкость (ползунок 0–10, 0 — выкл, 10 = 2.0), счётчик FPS —
|
||||
меняются сразу и пишутся в `bbport.ini`. «Эффекты игры» / «Патчи игры»: детализация моделей и
|
||||
патчи-эффекты (после перезапуска). «По умолчанию» ставит значения порта по умолчанию. Тексты на
|
||||
языке самой игры (русский — если её тексты на кириллице, иначе английский). Как устроено
|
||||
(`gpu/shim/bbport_game_menu.cpp`): перехват поиска текстов игры (свои id 0x7F0000+) и добавления
|
||||
пункта в меню (0x1b4e2a0, после «Экран/звук»); страница — макет «ControllSetting» со своими
|
||||
строками и заголовком; последний аргумент строк игры — указатель на значение по умолчанию.
|
||||
Каждая перехваченная функция сверяется побайтно, `BB_GAME_MENU=0` — выключить. Проверено: 16
|
||||
открытий страниц — рост памяти как у родных страниц игры; оверлей (Insert / L3+R3) остался.
|
||||
- Ровность 60 FPS со временем (сообщение из Discord: «через некоторое время кадры неровные»):
|
||||
6 минут стоя, `BB_FPS=60` — каждую минуту 16,67 мс, разброс 0,00–0,01 мс, ни одного кадра
|
||||
дольше 20 мс (кроме загрузки в первой минуте). На RX 7800 XT не воспроизводится.
|
||||
|
||||
## Влитые PR (2026-10-08, вечер)
|
||||
|
||||
- **PR #84: DLC.** Папки `<user>/addcont/<title>/<метка>` (как у shadPS4) сообщаются игре
|
||||
как установленные дополнения: сохранения с The Old Hunters загружаются, области DLC доступны
|
||||
(#33). Данные дополнений не монтируются — The Old Hunters нужна только метка, данные в патче
|
||||
1.09. При вливании: `_GNU_SOURCE` под `#ifndef` (тест собирает с `-D_GNU_SOURCE` и `-Werror`), в
|
||||
тест AppContent — заглушка папки пользователя и проверка: папка `addcont/CUSA00900/SPEXPANSIONDLC03`
|
||||
даёт одну запись «установлено».
|
||||
- **PR #80:** `BB_FSR4_STATS=1` больше не ломает пайплайны FSR 4 v07 без
|
||||
`VK_EXT_pipeline_executable_statistics`. Патч к подмодулю FSR-Vulkan изменился: рабочую копию
|
||||
подмодуля нужно вернуть (`git -C gpu/third_party/fsr-vulkan checkout -- src/ffx_vk_fsr4_v07.c`), и
|
||||
`build.sh` наложит новую версию.
|
||||
- **PR #78:** лаунчер — кнопки «Копировать» и «Экспорт лога…» на вкладке «Журнал».
|
||||
- **PR #75:** лаунчер на упрощённом китайском (строки, добавленные сегодня, — на
|
||||
английском до перевода).
|
||||
- **PR #83: DLSS на Linux.** Мост `gpu/dlss_bridge` (MIT, единственный код с SDK NVIDIA)
|
||||
собирается с `DLSS_SDK_ROOT=<github.com/NVIDIA/DLSS> bash build.sh` в `out/libbbport_dlss.so`,
|
||||
рядом копируется библиотека NVIDIA; игра загружает мост при запуске. Новый апскейлер `dlss`
|
||||
(лаунчер, меню, `bbport.ini`): те же входы, что у FSR 3.1, Native AA = DLAA, при ошибке — FSR 3.1.
|
||||
Без моста и библиотеки, на не-NVIDIA или с `BB_DLSS=0` ничего не меняется (DLSS в меню недоступен).
|
||||
При вливании: пункт DLSS и его описание перенесены в новое меню, строка в списке апскейлеров
|
||||
лаунчера — после строки языков PR #75. Проверено здесь: сборка без SDK, игра на AMD (FSR 4.1.1,
|
||||
FSR 4 v07 со статистикой). С DLSS не проверено (нет NVIDIA); автор — RTX 4090. В AppImage мост и
|
||||
библиотека NVIDIA не входят.
|
||||
|
||||
## После 0.5-pre1 (2026-10-09)
|
||||
|
||||
- **Новая модель памяти снова только для AMD.** Лог с GTX 1660 Ti (драйвер 615.71, Hyprland):
|
||||
импорт памяти хоста прошёл проверку при запуске, но после первого переноса блоков в VRAM
|
||||
(`84 MiB of loaded blocks moved`) — `GPU watchdog: submission 4114 not finished after 10 s`:
|
||||
видеочип закончил шейдер копирования и не начинал следующий запуск. Отправка ждёт семафор
|
||||
перепривязки памяти (`vkQueueBindSparse` арены), и на NVIDIA каждая такая перепривязка держала
|
||||
видеочип 10 секунд и дольше; рабочий стол вставал вместе с ним, со стороны — «зависает вся
|
||||
система». Проверка при запуске этого не ловит (там одна маленькая привязка). Теперь на NVIDIA
|
||||
переключатель недоступен, у кого он был включён — старая модель (лаунчер передаёт
|
||||
`BB_PC_MODEL=0`, библиотека GPU тоже не включает её на NVIDIA); `BB_PC_MODEL_ANY_GPU=1` — всё же
|
||||
попробовать. README и тексты лаунчера исправлены.
|
||||
- `BB_RESIDENCY_REPORT=<файл>` (диагностика новой модели): когда файл появляется, в журнал —
|
||||
блоки памяти игры, которые видеочип недавно читал на месте, а не из VRAM, и почему они не
|
||||
перенесены (процессор пишет часто; байты, о записи которых мы не знаем; ждут переноса).
|
||||
У лампы в Кошмаре охотника: ~1,2 ГБ таких блоков памяти Garlic целиком «неизвестны».
|
||||
- Сообщение «первая загрузка медленнее, после телепорта 90 FPS» (RX 7900 XTX, новая модель): здесь
|
||||
не воспроизвелось — первая и повторная загрузка (выход в титул, «Продолжить») 131,7 и 131,9 FPS
|
||||
на RX 7800 XT; переносы в VRAM после загрузки заканчиваются за секунды. Нужен лог тестера.
|
||||
|
||||
## Новая модель на NVIDIA: разбор (2026-10-09, продолжение)
|
||||
|
||||
- **Причина зависания найдена.** Это известная регрессия проприетарного драйвера NVIDIA на Linux
|
||||
(с 555 и до сих пор, у тестера 615): время `vkQueueBindSparse` растёт с числом страниц, **уже**
|
||||
привязанных во всех sparse-ресурсах, и вызов держит глобальную блокировку (форум разработчиков
|
||||
NVIDIA, «Sparse texture binding is painfully slow»: 9200 страниц — 3,6 с; исправления нет).
|
||||
В арене новой модели — гигабайты страниц по 64 КиБ (у тестера 863 МиБ привязано на месте ещё
|
||||
до первых переносов), так что каждый перенос блоков в VRAM стоил секунды. Игра сама заметила
|
||||
остановку видеочипа ещё до первого переноса («Stall during rendering at flush 1»).
|
||||
- **Почему не обойти «одной правкой».** Замер `BB_RESIDENCY_MIX=1` (новая диагностика): ~17 тыс.
|
||||
привязок в секунду (~85 за кадр) — диапазоны в сотни МБ, где одновременно блоки в VRAM и на
|
||||
месте (шейдеры, привязанные ко всей памяти; треть — поверх данных, записанных видеочипом).
|
||||
Одна sparse-арена смешивает их бесплатно; схема «VRAM-арена + отдельные буферы памяти игры»
|
||||
их не обслужит. Без перепривязок вообще (`BB_FIXED_ARENA=1`, всё читается на месте через шину):
|
||||
**48,6 FPS против ~205** на RX 7800 XT — хуже старой модели.
|
||||
- Итог: новая модель остаётся только для AMD. Для NVIDIA нужна либо починка драйвера (или
|
||||
Mesa NVK, где sparse работает нормально), либо программная подкачка страниц в транслированных
|
||||
шейдерах вместо sparse — многодневная переделка.
|
||||
- Добавлено: `BB_FIXED_ARENA=1/0` (арена держит первые привязки: без переносов в VRAM, блоки вне
|
||||
кусков памяти игры — в системной памяти; по умолчанию — при импорте памяти хоста, т. е. на NVIDIA
|
||||
с `BB_PC_MODEL_ANY_GPU=1`); строка в журнале `vkQueueBindSparse of N ranges took X ms` (дольше
|
||||
50 мс, в обеих моделях); в отчёте сторожа видеочипа — `Arena binds: ...` (ждут ли отправки
|
||||
незавершённую привязку); `BB_RESIDENCY_MIX=1` — привязки «на месте / VRAM / смешанные» за 2 с.
|
||||
|
||||
## Новая модель на всех видеокартах: модуль памяти прослойки (2026-10-09)
|
||||
|
||||
- **Новая модель включается на любой видеокарте.** На AMD — как раньше, sparse-арена. На NVIDIA,
|
||||
Intel и остальных — модуль памяти прослойки (`gpu/layer`, docs/MEMORY_MODULE_PLAN.ru.md): память
|
||||
игры привязывается на месте через буферы её кусков, копии в VRAM — отдельные буферы («зеркала»),
|
||||
перенос — копия плюс учёт в модуле. `vkQueueBindSparse`, из-за которого на NVIDIA зависали игра
|
||||
и рабочий стол, в этом режиме не вызывается ни разу. Вручную: `BB_LAYER_MEMORY=1/0`. Лаунчер
|
||||
больше не прячет переключатель на не-AMD; README и подсказка (три языка) исправлены. **На
|
||||
NVIDIA не проверено** — нужен лог тестера.
|
||||
- Замеры на RX 7800 XT, стоя: было 48–60 FPS в режиме модуля, стало ~205 (sparse ~205–238, старая
|
||||
модель ~220). В другом месте: модуль 167,5, sparse 164,5, старая модель 146. 5 минут ходьбы с
|
||||
подгрузкой областей — без ошибок, 166–240 FPS.
|
||||
- Что мешало (найдено по профилю видеочипа):
|
||||
- **HLE копий и очисток образов не срабатывало.** Детекторы `IsComputeImageCopy` /
|
||||
`IsComputeImageClear` требуют у шейдера ровно два буфера, а в режиме модуля каждому шейдеру
|
||||
с буферами игры добавлялся третий — таблица страниц. Копия памяти целей рендеринга
|
||||
(`cs 3d5ebf4e`) и очистка (`cs 8b355b5a`, 31 запуск за кадр) шли настоящими шейдерами через
|
||||
память, а образы перезагружались из неё каждый кадр (~16 образов 1080p вместо ~2,6).
|
||||
Теперь таблица страниц не считается (`ShaderBufferCount`): 60 → 136 FPS.
|
||||
- **Смешанные диапазоны.** Диапазон, у которого все блоки в VRAM, кроме одного с «неизвестными»
|
||||
байтами (край, промежуток между образами), целиком читался через PCIe (8 МБ на привязку).
|
||||
Теперь такой блок «летучий» в зеркале: обновляется из памяти игры перед каждой привязкой, то,
|
||||
что запуск записал, копируется обратно после запуска. 139 → 207 FPS. `BB_LAYER_VOLATILE=0` —
|
||||
выключить.
|
||||
- Перенос в VRAM при первой привязке диапазона известных данных (как делала арена); он
|
||||
соблюдает `BB_GARLIC_VRAM=0` и бюджет VRAM (`BB_VRAM_PROMOTE_PERCENT`, важно для карт с 6 ГБ).
|
||||
- Стартовая проверка импорта памяти хоста в режиме модуля проверяет запись видеочипа и процессора
|
||||
через сам импортированный буфер, без sparse-буфера.
|
||||
- Диагностика: `BB_RESIDENCY_MIX=2` — ещё и самые тяжёлые привязки на месте / смешанные с
|
||||
разбором блоков (VRAM, подходят, «динамические», неизвестные байты); в строке `Layer memory:` —
|
||||
число вызовов `vkQueueBindSparse` и байты, скопированные обратно.
|
||||
|
||||
## Модуль памяти: взрывы вершин и скорость (2026-10-09, день)
|
||||
|
||||
Итог работы над новой моделью для NVIDIA — в `docs/NVIDIA_MEMORY_MODEL.ru.md`: ловушки записи на
|
||||
блоках с копией в VRAM, чтения через модуль везде, где есть копия, данные процессора больше не
|
||||
затираются данными видеочипа, запись видеочипа в VRAM только в блоки, которые процессор не трогал
|
||||
(`BB_LAYER_GPU_WRITES`), шейдер копирования в режиме модуля через HLE, кэши ответов модуля.
|
||||
|
||||
## 0.5-pre2: оригинальное GPU-копирование и переносимые метки
|
||||
|
||||
В итоговой сборке шейдер копирования в новой модели теперь нативный по умолчанию: его записи
|
||||
согласованы между памятью игры и зеркалами VRAM. Добавлены стандартные Vulkan-метки завершения
|
||||
для GPU без `VK_AMD_buffer_marker`, исправлены зависимости записи/чтения буферов и миграция
|
||||
всего кэша при изменении формата. Подробное описание релиза и проверок:
|
||||
[CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md).
|
||||
|
||||
## 0.5-pre3: модуль памяти на NVIDIA
|
||||
|
||||
По первым логам с GTX 1660 Ti: копии в VRAM снова включены для модуля (их выключал обход
|
||||
sparse-арены), общий бюджет VRAM с кэшем текстур, запас роста копий вместо их перевыделения,
|
||||
записи WRITE_DATA в копию, наблюдаемые блоки (`BB_LAYER_WATCH`), повтор чтения файлов при EFAULT.
|
||||
Подробности: [CHANGES_0.5-pre3.md](CHANGES_0.5-pre3.md) и
|
||||
[NVIDIA_MEMORY_MODEL.ru.md](NVIDIA_MEMORY_MODEL.ru.md).
|
||||
|
||||
## 0.5-pre4: компиляция шейдеров в фоне, VRAM на картах 4–6 ГБ, меню как в 0.3
|
||||
|
||||
Новые конвейеры проходов, рисуемых каждый кадр, компилируются в фоновых потоках
|
||||
(`BB_ASYNC_PIPELINES`); у копий памяти игры в VRAM своя доля бюджета (регрессия pre3 на картах
|
||||
4 ГБ); после перемещений VRAM освобождается от копий покинутой локации; меню настроек в игре —
|
||||
снова одно окно, как в 0.3 и 0.4. Подробности: [CHANGES_0.5-pre4.md](CHANGES_0.5-pre4.md).
|
||||
@@ -1,159 +0,0 @@
|
||||
# Изменения после 0.5 (с 2026-10-10)
|
||||
|
||||
Замеры — RX 7800 XT / RADV, сохранение пользователя (Соборный округ), 1080p.
|
||||
|
||||
## Все розничные издания получают профиль игры (#119)
|
||||
|
||||
- Профиль Bloodborne (`gpu/games/bloodborne.cpp`) выбирался только для CUSA03173. Американское
|
||||
издание (CUSA00900) и остальные регионы запускают тот же исполняемый файл 1.09, но шли без
|
||||
профиля, и новая модель падала при входе в мир: `Guest fault ... at guest offset 0x2085xxx,
|
||||
address (nil)` в аллокаторе игры (#119, RX 7600, US-издание).
|
||||
- Воспроизведено у себя на CUSA03173 с `BB_GAME_PROFILE=none`: то же падение (`0x208596a`) при
|
||||
входе в мир. С профилем, но без перехватов (`BB_GUEST_HOOKS=0`) — не падает: спасает
|
||||
`DynamicWriters` — код аллокатора, чьи служебные данные лежат в памяти GPU; без этой отметки
|
||||
их блоки уходят в VRAM-копию.
|
||||
- Профиль, проверка игры (`scripts/game_check.py`), патчи (`scripts/patches.py`), «звуковой
|
||||
фикс» сохранений и папки модов теперь знают все издания: CUSA00900, CUSA00207, CUSA00208,
|
||||
CUSA00299, CUSA01363 и издания с The Old Hunters CUSA03179, CUSA03173, CUSA03014, CUSA03023.
|
||||
- Проверка игры: другие регионы базовой игры без обновления просят 1.09 вместо «поддерживается
|
||||
только CUSA03173»; eboot.bin со вшитым патчем 60 FPS (падает в меню жестов) — отдельное
|
||||
сообщение (PR #117).
|
||||
- Осталось: общий путь без профиля должен не падать сам (служебные данные аллокатора в
|
||||
VRAM-копии). Сейчас он нужен только для чужих игр.
|
||||
|
||||
## Размытие в движении на весь кадр в новой модели (#122)
|
||||
|
||||
- С включённым размытием в движении (по умолчанию) кадр при повороте камеры размазывался целиком,
|
||||
в старой модели — нет (#122, RX 9060 XT). Воспроизведено: резкость кадра во время поворота
|
||||
(стандартное отклонение лапласиана) 6,9 против 8,5–12 в старой модели.
|
||||
- Поиск: `BB_GARLIC_VRAM_RANGE=начало,конец` (новый диагностический ключ: VRAM-копии только в
|
||||
этом диапазоне) и деление диапазона пополам — блок 64 КиБ `0x104ff00000`. Его читает только
|
||||
видеочип (буфер 11,4 МиБ каждый кадр), записей буферами и процессором нет: это память
|
||||
рендер-таргета, который игра потом читает как буфер.
|
||||
- Причина: выгрузка изображения в память игры шла через staging и отложенную запись процессора,
|
||||
когда у этой памяти была VRAM-копия; копия обновлялась лишь после той записи (инвалидацией) —
|
||||
буфер читал данные прошлых кадров. Теперь видеочип копирует изображение и в арену (VRAM-копию)
|
||||
в порядке потока; в память игры — как раньше. И `IsRegionGpuModified` считает изменённым то,
|
||||
что видеочип записал в VRAM-копию (`gpu_written_bytes`, не на месте): без этого обновление после
|
||||
записи процессора поверх затирало копию старыми байтами (`BB_GPU_DATA_CHECK=0` — как раньше).
|
||||
Нужны обе правки. После: 11,9–13,0, как в старой модели; FPS тот же (127–131 в обоих
|
||||
вариантах, по три запуска).
|
||||
|
||||
## Нехватка VRAM на 4 ГБ больше не останавливает игру
|
||||
|
||||
- RTX 3050 4 ГБ, старая модель (Авто на NVIDIA), 0.5: три падения за вечер —
|
||||
`Failed to allocate 64 MiB for guest memory copies: ErrorOutOfDeviceMemory`. Запасного типа
|
||||
памяти (системной) для арены нет: разреженные буферы NVIDIA берут только VRAM. В pre5
|
||||
сборщик текстур стал держать текстуры по 5 с (FPS не падал со временем), и на 4 ГБ VRAM
|
||||
заканчивалась.
|
||||
- Теперь: блок поменьше (ровно запрошенный), затем любой другой тип памяти, к которому можно
|
||||
привязать арену; и 30 с сборщик под давлением снова выселяет по отправкам, как до 0.5. Он же
|
||||
делает так сам, когда занято больше 92 % бюджета драйвера (`VK_EXT_memory_budget`, раз в
|
||||
секунду). Проверить можно только на такой карте.
|
||||
|
||||
## Выход из игры с геймпада (#74, #116)
|
||||
|
||||
- Оверлей (Insert или L3+R3): кнопка «Выйти из игры», второе нажатие подтверждает; игра
|
||||
закрывается как по кнопке окна.
|
||||
|
||||
## Мышь
|
||||
|
||||
- Камера мышью: щелчок в окне игры захватывает мышь (относительный режим SDL), F1 или уход из
|
||||
окна — отпускает; меню настроек и ввод имени тоже отпускают. Движение мыши — правый стик:
|
||||
отклонение по скорости мыши (~1000 пикселей/с — полный поворот при чувствительности 1), за
|
||||
60 мс спадает; движущаяся мышь сразу проходит мёртвую зону игры (`BB_MOUSE_DEADZONE`, 16 из
|
||||
127), так что медленное прицеливание тоже поворачивает камеру. Одинаково при любом FPS.
|
||||
- Кнопки мыши и колесо — те же «клавиши» в привязках: `Mouse Left` R1, `Mouse Right` L2 (огнестрел),
|
||||
`Mouse Middle` R3 (захват цели), `Mouse X1` R2, `Mouse X2` L1, колесо — крестовина вверх/вниз
|
||||
(быстрые предметы); каждое деление колеса — короткое нажатие. Кнопки работают только при
|
||||
захваченной мыши: щелчок, который её захватывает, не атакует.
|
||||
- bbport.ini: `mouse_look=0/1`, `mouse_sensitivity` (0.05–20), `mouse_invert_y`; лаунчер —
|
||||
*Управление*: «Камера мышью», чувствительность, инверсия; «Клавиатура и мышь» — назначение
|
||||
кнопок, «Назначить» принимает и кнопку мыши/колесо. Основа — PR #108 (виртуальный стик от
|
||||
движения мыши); раскладка клавиатуры не менялась.
|
||||
|
||||
## Прочее
|
||||
|
||||
- Swapchain пересоздаётся через `oldSwapchain` (PR #93): на NVIDIA под Wayland пересоздание
|
||||
падало с «Surface already has a tearing controller».
|
||||
- `launcher/bb-launcher.sh` передаёт аргументы (`--play`) и через nix-shell (PR #120).
|
||||
- Запись FSR 4.1.1 из AppImage на SteamOS и других дистрибутивах (PR #89): запись на хосте
|
||||
через systemd, повтор с GPU, представленным как RX 7800 XT, если DLL не предлагает FSR 4.1.
|
||||
- Лаунчер: у DLSS — подсказка, есть ли он в этой сборке (в AppImage его нет: DLSS SDK от NVIDIA
|
||||
не распространяется, игра включает FSR 3.1).
|
||||
|
||||
## DLSS в AppImage и своя библиотека NVIDIA
|
||||
|
||||
- Сборка с `DLSS_SDK_ROOT` (DLSS SDK 310.9.1 с github.com/NVIDIA/DLSS) кладёт в пакет мост
|
||||
`libbbport_dlss.so`, `libnvidia-ngx-dlss.so.310.9.1` и `NVIDIA-DLSS-LICENSE.txt` (лицензия RTX
|
||||
SDK разрешает распространение в составе приложения, не отдельно). Тестовый AppImage:
|
||||
`dist/…-test-1010-dlss.AppImage`.
|
||||
- Лаунчер, *Апскейлер → DLSS: библиотека NVIDIA → Выбрать файл…* — как DLL для FSR 4.1.1:
|
||||
`libnvidia-ngx-dlss.so.<версия>` копируется в `<user>/dlss/`, игра берёт её раньше встроенной
|
||||
(лог: `DLSS: NVIDIA's library from …`). Выбранная `nvngx_dlss.dll` — подсказка, что нужна
|
||||
библиотека для Linux. Мост ищется рядом с `bb-probe`, затем в `<user>/dlss/`.
|
||||
- Мост собирается и с защитными флагами Nix (`-Werror=format-security`).
|
||||
- Проверено на RX 7800 XT только до «not an NVIDIA GPU»: путь к библиотеке (встроенной и
|
||||
своей) выбирается верно. Сам DLSS — на карте NVIDIA RTX.
|
||||
|
||||
## Сборка из git после обновления с 0.4 (#124)
|
||||
|
||||
- `build.sh` останавливался: «patch failed: src/ffx_vk_fsr4_v07.c:35» — в подмодуле FSR-Vulkan
|
||||
оставался применённым патч прошлой версии, а новый не применялся ни вперёд, ни назад. Теперь
|
||||
подмодуль в этом случае сбрасывается и патчи применяются заново (проверено на копии в таком
|
||||
состоянии).
|
||||
|
||||
## Онлайн (shadNet) — отдельным модулем
|
||||
|
||||
- `gpu/bbnet` → `out/gpu/libbbnet.so`: сетевые и PSN-библиотеки shadPS4 из shadp2p (форк shadPS4,
|
||||
GPL-2.0) с клиентом shadNet, перенесены из Linux-форка bbport с онлайном (а тот — из Windows-порта).
|
||||
Модуль собирается отдельной целью, его C++ скрыт (`-fvisibility=hidden`, `-Bsymbolic`), наружу —
|
||||
только `bbnet_init` и `bbnet_resolve` (`gpu/bbnet/bbnet.h`). Свои заголовки модуля
|
||||
(`gpu/bbnet/include`) только дополняют общие: в `gpu/shim` и `gpu/shadps4` ничего не менялось.
|
||||
- Рантайм (`src/runtime_net.c`): с `BB_ONLINE=1` на первом сетевом/PSN-импорте загружает модуль
|
||||
(`dlopen`, `RTLD_LOCAL`; `BB_NET_MODULE` — другой путь), отдаёт ему данные игры и гостевые потоки
|
||||
для обратных вызовов NP; трофеи, голос и диалоги магазина остаются в рантайме. Без `BB_ONLINE`
|
||||
модуль не загружается; без модуля или если он не стартовал — офлайн, как раньше.
|
||||
- Сборка: protobuf, nlohmann_json, OpenSSL, zlib, miniupnpc (`shell.nix`); без них модуль не
|
||||
собирается (`BB_NET=OFF` — вручную), порт собирается как раньше. AppImage содержит модуль.
|
||||
- `run.sh`: перенаправления адресов (`scripts/online.py`: сервер игры → The Hunter's Dream,
|
||||
знаки призыва → WebAPI shadNet), сервер shadNet по умолчанию `srv.shadps4.net:31313`.
|
||||
- Лаунчер: страница *Онлайн* (сервер игры, сервер кооператива и WebAPI, учётная запись shadNet,
|
||||
UPnP, проверка соединения); `settings.json` — только для владельца (0600, там пароль).
|
||||
- UPnP: порт P2P теперь открывается на роутере, когда транспорт готов, и закрывается при выходе
|
||||
(#121: `AddMapping` не вызывался нигде).
|
||||
- Проверено: `tests/test_online.c` (импорты сети и NP → модуль, трофеи → рантайм); игра с
|
||||
`BB_ONLINE=1` без учётной записи доходит до уровня (модуль: 1399 функций; «no users connected
|
||||
to shadNet»). Вход в shadNet и призыв не проверены: нет учётной записи, а UDP-часть публичного
|
||||
сервера, по #121, не отвечает.
|
||||
- HTTPS в AppImage: «Не удалось получить информацию от сервера Bloodborne» — проверка сертификата
|
||||
The Hunter's Dream проваливалась (`SSL server verification failed`): OpenSSL из Nix берёт корневые
|
||||
сертификаты из `/etc/ssl/certs/ca-certificates.crt`, а на NixOS это ссылка в `/nix/store` хоста,
|
||||
которого внутри AppImage не видно. Обёртки пакета теперь задают `SSL_CERT_FILE` на свой пакет
|
||||
сертификатов (`nss-cacert`), если указанный не читается; `run.sh` из исходников берёт системный.
|
||||
Лаунчер: текст ошибки в проверке соединения не терялся из-за разметки (`<urlopen error …>`), значки
|
||||
— из темы Adwaita.
|
||||
|
||||
## Мерцание главного меню при выводе выше 1080p
|
||||
|
||||
- С живой сменой разрешения (вывод 3840×2160 на мониторе 2560×1080) главное меню мерцало: часть
|
||||
кадров показывалась в родных 1920×1080 игры, остальные — с интерфейсом в разрешении вывода
|
||||
(4 из 50 снимков). `BB_UI_TRACE=1` (новая диагностика: отрисовки кадров без интерфейса в
|
||||
разрешении вывода) показал: пока меню анимирует подсветку, игра рисует его другими шейдерами
|
||||
Scaleform (`bb4d5f8c`, `e94b5b06`, `9951d199`, `44072322`, `a334a845`), а не `09957251`; их не было в
|
||||
списке шейдеров интерфейса (`ui_composition.h`), и такие кадры не собирались в разрешении
|
||||
вывода. После: 0 из 80; в уровне картинка та же.
|
||||
|
||||
## AppImage меньше
|
||||
|
||||
- В пакете были весь Mesa с LLVM (~800 МБ: нужны только Vulkan-драйверы RADV и ANV), полный GCC
|
||||
и заголовки Vulkan (их пути в отладочной информации `bb-probe`), 32-битная половина MangoHud
|
||||
(вторые glibc, gtk3, icu, perl) и GStreamer у GTK4 лаунчера. Теперь: свои RADV (без LLVM, ACO)
|
||||
и ANV без остального Mesa, `bb-probe` без отладочной информации, MangoHud 64-битный, GTK4 без
|
||||
GStreamer, печати и tracker.
|
||||
- Ещё: SDL3 без зашитого zenity (тот тянул вторую GTK4 с GStreamer; теперь zenity хоста, если
|
||||
есть), libdecor без плагина GTK 3 (у SDL3 и у glfw MangoHud; рамки рисует его cairo-плагин),
|
||||
MangoHud без xdg-utils (Perl; `xdg-open` хоста). Сборка своей Mesa включает Dozen только потому,
|
||||
что рецепт nixpkgs ждёт его выход `spirv2dxil`; в пакет он не попадает.
|
||||
- Итог: AppImage 925 → 399 МБ, пакет без сжатия 2,85 → 1,06 ГБ. Проверено: лаунчер открывается,
|
||||
игра из пакета стартует на встроенном RADV до титульного экрана.
|
||||
@@ -30,8 +30,13 @@ how yours goes is welcome either way.
|
||||
|
||||
The alpha tests (`CUSA01318`, `CUSA01322`, `CUSA01333`, `CUSA01335`) are not supported.
|
||||
|
||||
The Game of the Year and The Old Hunters editions include the DLC. With a base game, dump the
|
||||
add-ons too and choose their folder in the launcher (see the README's *DLC* section).
|
||||
The Game of the Year and The Old Hunters editions include the DLC. With a base game (such as the
|
||||
US `CUSA00900`), dump your add-ons too: each one is a folder (`SPEXPANSIONDLC03`,
|
||||
`SPDLCMESSENGER00`…) with only `sce_sys` inside, because their data ships with the 1.09 update. Put
|
||||
them in one folder and choose it in the launcher (**Setup → DLC folder**), or set
|
||||
`BB_DLC_DIR=/path/to/dlc` for `run.sh`; the launcher shows how many it found. Each folder must keep
|
||||
the add-on's name (its entitlement label, such as `SPEXPANSIONDLC03`). Copying them to
|
||||
`user/addcont/<title id>/`, shadPS4's layout, works too.
|
||||
|
||||
## Tested with this fork
|
||||
|
||||
@@ -41,8 +46,8 @@ On Linux (CachyOS, NVIDIA GeForce RTX 3060), with the US release (`CUSA00900`):
|
||||
messenger add-ons), from the launcher's DLC folder.
|
||||
- A dump whose `eboot.bin` had Lance McDonald's 60 fps patch baked in: the launcher reports it,
|
||||
and the clean 1.09 `eboot.bin` runs.
|
||||
- Both regional texts through *Game region*: Spanish from Spain (Europe) and Latin American
|
||||
Spanish (the Americas).
|
||||
- Every text and voice language the files have, through *Game text language* and *Voice
|
||||
language*: Spanish from Spain and Latin American Spanish among them.
|
||||
|
||||
The European Game of the Year edition (`CUSA03173`) is the release the original bbport is
|
||||
developed and tested with.
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# Credits
|
||||
|
||||
Everyone whose work is in this fork, and every project it took something from. Everyone listed
|
||||
under *People* and *Help and testing* also appears in the launcher's **About** section. Components and their licenses: [bbport's notes](../BBPORT.md#credits-and-licenses).
|
||||
|
||||
## Projects
|
||||
|
||||
| Project | What this fork takes from it |
|
||||
|---|---|
|
||||
| [deadinside28/bloodborne_pc](https://github.com/deadinside28/bloodborne_pc) | **bbport**, the native Linux port everything here is built on. Its README is [here](../BBPORT.md). |
|
||||
| [Supermedo/bloodborne_pc](https://github.com/Supermedo/bloodborne_pc) | The Windows port: online play, the cheats page, Add to Steam, the field of view, the game's audio at full level and many fixes. |
|
||||
| [Awakenchan/LanternEngine](https://github.com/Awakenchan/LanternEngine) (formerly BloodBorneButPc) | Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine) (GPL-3.0-or-later): crash fixes, stick options, the controller in use, save import, the version selector, and renderer fixes (vertex inputs, index buffers, camera motion vectors). |
|
||||
| [shadPS4](https://github.com/shadps4-emu/shadPS4) | The renderer and shader recompiler they started from, and the online code: shadPS4 0.20's network, NP and shadNet libraries, put together for bbport by Awaken ([gpu/bbnet20](../gpu/bbnet20)). |
|
||||
| [Wozzardman/shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) | The shadNet server for a group's own co-op server ([hosting your own server](SERVER.md)). |
|
||||
| [Classmg22/BBSC](https://github.com/Classmg22/BBSC) | Seamless Co-op, by CLS_AMG, with Wozzardman's seamless summon ([gpu/bbnet20/bbsc](../gpu/bbnet20/bbsc), its game server in [scripts/bbsc_gameserver](../scripts/bbsc_gameserver)). |
|
||||
| [fhody125-web/NoDreamForHunter](https://github.com/fhody125-web/NoDreamForHunter) | The base of BBSC's game server. |
|
||||
| [MikeTat1988/HuntersAtlas](https://github.com/MikeTat1988/HuntersAtlas) | Hunter's Atlas: the fullscreen in-game map, its quick-menu tile, lamp travel and the map generator ([tools/hunters-atlas](../tools/hunters-atlas)), which runs with Grimrukh's [soulstruct](https://github.com/Grimrukh/soulstruct). |
|
||||
| [The Hunter's Dream](https://thehuntersdream.com) | The online server, by droogie and its team; droogie's [bbhost](https://github.com/droogie/bbhost) is the base of its backend in the online module ([gpu/bbnet20/thd](../gpu/bbnet20/thd)) and brought fast loading after a death or travel. |
|
||||
| [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR) | The FSR 4 model without trails. |
|
||||
| [NVIDIA DLSS SDK](https://github.com/NVIDIA/DLSS) | DLSS. |
|
||||
| [FireBurn/FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) | FSR 3.1, with the AMD FidelityFX SDK. |
|
||||
| [pkgforge-dev/Anylinux-AppImages](https://github.com/pkgforge-dev/Anylinux-AppImages) | quick-sharun, which builds the AppImage. |
|
||||
| [paulomanrique/ps4-pkg-extractor](https://github.com/paulomanrique/ps4-pkg-extractor) | The launcher's *Install from PKG*, on [orbis-pkg-util](https://crates.io/crates/orbis-pkg-util). |
|
||||
| [herumi/xbyak](https://github.com/herumi/xbyak), [fmtlib/fmt](https://github.com/fmtlib/fmt) | Used by the shader translator and the online module. |
|
||||
| [GoldHEN_Patch_Repository](https://github.com/GoldHEN/GoldHEN_Patch_Repository) | Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and the Bloodborne community: high frame rates, resolution, ultrawide and more. |
|
||||
| [Nexus Mods: mod 30](https://www.nexusmods.com/bloodborne/mods/30) | Dommo's Xbox and Switch button prompts and stripped-down gestures menu, uploaded by goomab ([assets/menu_prompts](../assets/menu_prompts)). |
|
||||
|
||||
## bbport 0.7's contributors
|
||||
|
||||
This fork is built on bbport 0.7, which took pull requests and outside work from Qiming-Liu,
|
||||
AndiMajore, adds39939, iannitram, maiconjsv, whp199, DominickCobb-rs, Sarxzer, wheat32,
|
||||
ArenaMMORPG, guutol (Luís Gustavo P. Bomfim), redditor69, SrShadowy and Teefpyre, Supermedo and
|
||||
Awaken, and in earlier releases SrDeTs, IgorWarzocha, itachi-re, Bainianzzz, hideoneric,
|
||||
guitaripod, unTanana, andrR89, JuanraCM, SaeedShow, msisovic, AyrtonSanabio, axpico, Aryxa,
|
||||
Obrikash and noelmiller. What each one did:
|
||||
[bbport's list](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CONTRIBUTORS_0.7.md).
|
||||
|
||||
## People
|
||||
|
||||
- **[deadinside28](https://github.com/deadinside28)**: bbport, the port this whole project is built on,
|
||||
and 0.6's menu pointer and keyboard layout.
|
||||
- **[Supermedo](https://github.com/Supermedo)**: online play, the cheats page, Add to Steam, and many
|
||||
fixes from the Windows port.
|
||||
- **[0xCydral](https://github.com/0xCydral)**: gathered and adapted dozens of fixes (crashes while
|
||||
loading, black images, the upscaler, the performance line in the log).
|
||||
- **[Awakenchan](https://github.com/Awakenchan)**: crash fixes (flips, descriptors, page faults),
|
||||
stick options, the controller in use, save import, the version selector, vertex shaders with
|
||||
the right bindings, stale vertex inputs read again, index buffers of indirect draws, and camera
|
||||
motion vectors without NaN.
|
||||
- **[Classmg22](https://github.com/Classmg22)** (CLS_AMG): BBSC, Seamless Co-op.
|
||||
- **[Wozzardman](https://github.com/Wozzardman)**: the server for a group's own co-op, and BBSC's seamless summon.
|
||||
- **[droogie](https://github.com/droogie)**: The Hunter's Dream server, and fast loading (bbhost).
|
||||
- **[fhody125-web](https://github.com/fhody125-web)**: NoDreamForHunter, the base of BBSC's game server.
|
||||
- **[MikeTat1988](https://github.com/MikeTat1988)**: Hunter's Atlas, the fullscreen in-game map.
|
||||
- **[IFreemz](https://github.com/IFreemz)**: the FSR 4 model without trails.
|
||||
- **[FireBurn](https://github.com/FireBurn)**: FSR 3.1.
|
||||
- **[Jhony-Jhansen](https://github.com/Jhony-Jhansen)**: keyboard movement (the last of two opposite keys wins).
|
||||
- **[cassioKenji](https://github.com/cassioKenji)**: the keyboard sprint measurements.
|
||||
- **[msisovic](https://github.com/msisovic)**: why loading hung after a lamp travel (guest thread handles above 40 bits).
|
||||
- **[axpico](https://github.com/axpico)**: with VRAM full, the game waits for evicted textures to be freed instead of closing.
|
||||
- **[emurphy04](https://github.com/emurphy04)**: a video player crash when it stops, the field of
|
||||
view, and an online crash guard.
|
||||
- **[yumlevi](https://github.com/yumlevi)**: the black character preview, the loading screen's
|
||||
picture, and a crash on an unreadable shader program.
|
||||
- **[GoncaloLobo0](https://github.com/GoncaloLobo0)**: crashes while loading areas.
|
||||
- **[bmy](https://github.com/bmy)**: motion vectors for the large beasts, and a memory fault.
|
||||
- **[Mrsuss60](https://github.com/Mrsuss60)**: the gestures menu from the keyboard, a deleted
|
||||
image's slot, and the log of failed memory reservations.
|
||||
- **[SrShadowy](https://github.com/SrShadowy)**: the on-screen keyboard for the character's name,
|
||||
and the shader compiling notice.
|
||||
- **[Qiming-Liu](https://github.com/Qiming-Liu)**: the Simplified Chinese translation.
|
||||
- **[andrecabralgit](https://github.com/andrecabralgit)**: the game's audio at full level.
|
||||
- **[Jgamrot](https://github.com/Jgamrot)**: the in-game menu's checkboxes and sliders.
|
||||
- **[GerasimosKan](https://github.com/GerasimosKan)**: trophy names without the key, and the
|
||||
sound when one is earned.
|
||||
- **[artemkaVG](https://github.com/artemkaVG)**: a stencil buffer fix.
|
||||
- **[parsifal295](https://github.com/parsifal295)**: the touchpad halves on Xbox pads.
|
||||
- **[AyrtonSanabio](https://github.com/AyrtonSanabio)**: the in-game menu's language without the launcher.
|
||||
- **[Aryxa](https://github.com/Aryxa)**: DLSS in the AppImage on NVIDIA.
|
||||
- **[AndiMajore](https://github.com/AndiMajore)**: crash debugging (`BB_FAULT_WAIT`).
|
||||
- **[iannitram](https://github.com/iannitram)**: FSR 3.1 without colour noise on the Ryzen Z1 (ROG Ally, Legion Go).
|
||||
- **[whp199](https://github.com/whp199)**: the GX occlusion query guard against the 0x2169edc crash.
|
||||
- **[nick24051998](https://github.com/nick24051998)**: the SRT walker without faults on null tables.
|
||||
|
||||
## Help and testing
|
||||
|
||||
- **[willthehuman](https://github.com/willthehuman)**: detailed reports with logs, and testing the fixes.
|
||||
- **[nr1971](https://github.com/nr1971)**: the game not starting on some graphics cards, the
|
||||
camera speeding up on diagonals, and a crash on quitting.
|
||||
- **[Joomzie](https://github.com/Joomzie)**: swapping A and B on Nintendo-style controllers.
|
||||
- **[Kaka556Kevin](https://github.com/Kaka556Kevin)**: testing the video memory fix.
|
||||
- **[segasonicye](https://github.com/segasonicye)**: why the game did not start in the Steam Deck's Game Mode.
|
||||
- **[Monochrome0907](https://github.com/Monochrome0907)**: which effects freeze loading screens on NVIDIA.
|
||||
- **[bruxis](https://github.com/bruxis)**: the log that showed why Steam Input did not reach the game.
|
||||
- **[shauryasagar](https://github.com/shauryasagar)** and **[ZeroSTF](https://github.com/ZeroSTF)**: the Hunter's Dream going black after long sessions.
|
||||
- **[popcar2](https://github.com/popcar2)**: ideas for the launcher.
|
||||
- **[shaneo88](https://github.com/shaneo88)**: shadPS4's patch file in the patches folder.
|
||||
- **[n3vrs0ft](https://github.com/n3vrs0ft)**: the "Transaction failed" crash on Seamless Hunt, the logs that stopped a BBSC guest from following a stranger's world, and testing BBSC 0.0.2 with a Windows player.
|
||||
- **[cappp](https://github.com/cappp)**: the "Transaction failed" crash on Seamless Hunt.
|
||||
- **[simo1730](https://github.com/simo1730)**: the Trophies page not finding the dump's files.
|
||||
- **[Nahnahchi](https://github.com/Nahnahchi)**: logs and tests of a GPU freeze in a boss fight on NVIDIA, a crash in Yahar'gul, stretched models, a crash on the Steam Deck, effects turning black, rainbow textures and crashes in the game's memory manager.
|
||||
- **[thomasgarafano1](https://github.com/thomasgarafano1)**: Hunter's Atlas not preparing the map with a GOTY dump, and testing the fix.
|
||||
- **[TacticalBill](https://github.com/TacticalBill)**: marking trophies already earned on the PS4.
|
||||
- **[holydingus](https://github.com/holydingus)**: BBSC's items not given to a new character.
|
||||
- **[OPGZ2](https://github.com/OPGZ2)**: the launcher's blank window at start.
|
||||
- **[kuday248](https://github.com/kuday248)**: settings from the in-game menu that the launcher overwrote.
|
||||
- **[BlazinKarp](https://github.com/BlazinKarp)**: the Steam overlay in the game.
|
||||
- **[molnar-a](https://github.com/molnar-a)**: a crash on the Steam Deck after lamp travel, and testing the fix.
|
||||
- **[Aman9das](https://github.com/Aman9das)**: asking for bbport 0.6's keyboard layout.
|
||||
+1
-1
@@ -5,7 +5,7 @@ one line (`Host fault`, `Guest fault`, `STOP: GPU library assertion failed`) and
|
||||
`BB_FAULT_WAIT=1` keeps the process alive at that point, stopped inside its handler, so a
|
||||
debugger can look at the faulting thread with its registers, stack and memory still there.
|
||||
|
||||
1. Launcher → *Mode → Extra environment*: `BB_FAULT_WAIT=1` (or export it before `run.sh`).
|
||||
1. Launcher → *Advanced → Developer → Extra variables*: `BB_FAULT_WAIT=1` (or export it before `run.sh`).
|
||||
Nothing changes while the game runs; it costs nothing.
|
||||
2. When it crashes, the game freezes, a desktop notification says so, and the log ends with
|
||||
`bbport: ... the process stays alive for a debugger: bash tools/debug_attach.sh <pid>`.
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
# Depth-Adaptive TAA Improvements
|
||||
|
||||
**Дата:** 2026-10-01
|
||||
**Статус:** Исторический эксперимент, заменён исправлением temporal reprojection.
|
||||
|
||||
Описанные ниже пороги и веса больше не используются. Уменьшение веса истории вдали
|
||||
усиливало мерцание; сравнение device depth между разными камерами и хранение глубины
|
||||
в FP16 не позволяли корректно отличать дальние поверхности. Актуальный разбор,
|
||||
изменения и результаты проверок: [TEMPORAL_REVIEW_2026-10-01.md](TEMPORAL_REVIEW_2026-10-01.md).
|
||||
|
||||
## Проблема
|
||||
|
||||
Текущая реализация TAA использует фиксированные пороги для:
|
||||
1. Определения disocclusion (раскрытие новых поверхностей): `0.0005`
|
||||
2. Временного веса накопления: `0.9` → `0.65` в зависимости от движения
|
||||
|
||||
Эти параметры не учитывают глубину пикселя, что приводит к проблемам:
|
||||
- **Далёкие объекты (depth → 1.0)**: временная нестабильность, мерцание
|
||||
- **Близкие объекты (depth → 0.0)**: чрезмерное размытие при быстром движении
|
||||
|
||||
## Решение: Адаптивные пороги по глубине
|
||||
|
||||
### 1. Depth-Adaptive Disocclusion Threshold
|
||||
|
||||
**Файл:** `gpu/shadps4/video_core/host_shaders/taa.comp`
|
||||
|
||||
```glsl
|
||||
float depthThreshold(float depth) {
|
||||
// Near: 0.001, Far (>0.95): 0.0002
|
||||
return mix(0.001, 0.0002, smoothstep(0.7, 0.95, depth));
|
||||
}
|
||||
```
|
||||
|
||||
**Логика:**
|
||||
- Близкие поверхности (depth < 0.7): порог `0.001` — допускает небольшие расхождения
|
||||
- Дальние поверхности (depth > 0.95): порог `0.0002` — строже, меньше false positives
|
||||
- Плавный переход через `smoothstep(0.7, 0.95, depth)`
|
||||
|
||||
**Применение:**
|
||||
```glsl
|
||||
float depthDiff = abs(history.a - depth);
|
||||
float threshold = depthThreshold(depth);
|
||||
if (depthDiff < threshold && ...) {
|
||||
// Accept history
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Depth-Adaptive Temporal Weight
|
||||
|
||||
```glsl
|
||||
float temporalWeight(float depth, float motionLength) {
|
||||
// Base weight decreases for distant objects: 0.9 near, 0.8 far
|
||||
float baseWeight = mix(0.9, 0.8, smoothstep(0.7, 0.95, depth));
|
||||
// Motion reduces weight more aggressively for distant pixels
|
||||
float motionFactor = mix(0.65, 0.5, smoothstep(0.7, 0.95, depth));
|
||||
return mix(baseWeight, motionFactor, clamp(motionLength / 16.0, 0.0, 1.0));
|
||||
}
|
||||
```
|
||||
|
||||
**Логика:**
|
||||
- **Базовый вес без движения:**
|
||||
- Близко: `0.9` (агрессивное накопление истории)
|
||||
- Далеко: `0.8` (консервативнее, меньше мерцания)
|
||||
|
||||
- **При быстром движении (motionLength > 16px):**
|
||||
- Близко: снижается до `0.65`
|
||||
- Далеко: снижается до `0.5` (ещё консервативнее)
|
||||
|
||||
**Применение:**
|
||||
```glsl
|
||||
float motionLength = length(motion);
|
||||
float weight = temporalWeight(depth, motionLength);
|
||||
result = mix(current, clipped, weight);
|
||||
```
|
||||
|
||||
### 3. Motion Vector Validation (camera_motion.comp)
|
||||
|
||||
**Файл:** `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
|
||||
|
||||
```glsl
|
||||
// Depth-adaptive validation: distant surfaces need tighter depth matching.
|
||||
float depth_threshold = mix(0.001, 0.0003, smoothstep(0.7, 0.95, depth));
|
||||
if (object_motion.b > 0.99 && abs(object_motion.a - depth) < depth_threshold &&
|
||||
!any(isnan(object_motion.rg)) && !any(isinf(object_motion.rg))) {
|
||||
result = object_motion.rg;
|
||||
}
|
||||
```
|
||||
|
||||
**Что изменено:**
|
||||
- Старый порог: фиксированный `0.001`
|
||||
- Новый порог: `0.001` → `0.0003` для далёких поверхностей
|
||||
- Предотвращает использование object motion vectors от неправильных пикселей
|
||||
|
||||
### 4. Улучшенная визуализация debug режимов
|
||||
|
||||
**Файл:** `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
|
||||
|
||||
Режимы `BB_DEBUG_MOTION=1` теперь структурированы:
|
||||
```glsl
|
||||
if (mode == 1) {
|
||||
// Raw depth visualization
|
||||
imageStore(color_img, pixel, vec4(fract(depth * 4.0), depth, depth < 0.0 ? 1.0 : 0.0, 1.0));
|
||||
} else if (mode == 2) {
|
||||
// View-space depth visualization (z / 50)
|
||||
const float v = z / 50.0;
|
||||
imageStore(color_img, pixel, vec4(fract(v * 4.0), v, v < 0.0 ? 1.0 : 0.0, 1.0));
|
||||
}
|
||||
```
|
||||
|
||||
## Ожидаемые улучшения
|
||||
|
||||
### Дальние объекты
|
||||
- ✅ Меньше мерцания на далёких стенах и небе
|
||||
- ✅ Более стабильная история при небольших движениях камеры
|
||||
- ✅ Меньше ложных disocclusion от численных ошибок глубины
|
||||
|
||||
### Близкие объекты
|
||||
- ✅ Меньше размытия при быстрых движениях (персонаж, оружие)
|
||||
- ✅ Более точные object motion vectors
|
||||
- ✅ Лучший баланс между накоплением и отзывчивостью
|
||||
|
||||
### Адаптивная зона (depth 0.7-0.95)
|
||||
- Плавный переход между режимами
|
||||
- Предотвращает резкие артефакты на границе
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Визуальная проверка
|
||||
```bash
|
||||
# TAA в нативном разрешении
|
||||
BB_UPSCALER=taa bash run.sh
|
||||
|
||||
# Проверить:
|
||||
# 1. Мерцание далёких стен (Hunter's Dream, верхние этажи)
|
||||
# 2. Размытие оружия при быстром повороте
|
||||
# 3. Стабильность при медленном движении камеры
|
||||
# 4. Disocclusion при обходе углов
|
||||
```
|
||||
|
||||
### Debug режимы
|
||||
```bash
|
||||
# Визуализация глубины
|
||||
BB_DEBUG_MOTION=1 bash run.sh
|
||||
# Toggle: (1<<21) = raw depth, (1<<22) = view-space z
|
||||
|
||||
# Проверка motion vectors
|
||||
# Toggle: (1<<20) = blended motion, (1<<23) = reprojection, (1<<24) = difference
|
||||
```
|
||||
|
||||
### A/B сравнение
|
||||
```bash
|
||||
# Создать файл для runtime-переключения
|
||||
echo "0" > /tmp/bb_toggle
|
||||
|
||||
# В игре переключать биты для сравнения
|
||||
# Потребуется временный механизм для старых/новых весов
|
||||
```
|
||||
|
||||
## Потенциальные риски
|
||||
|
||||
1. **Слишком консервативные веса на далёких объектах**
|
||||
- Может увеличить смазывание при быстрых поворотах
|
||||
- Решение: настроить `baseWeight` (0.8 → 0.85?)
|
||||
|
||||
2. **Слишком строгий порог глубины для object motion**
|
||||
- Может отбрасывать валидные векторы
|
||||
- Решение: `0.0003` → `0.0005` для depth > 0.95
|
||||
|
||||
3. **Smoothstep диапазон 0.7-0.95 может быть неоптимальным**
|
||||
- Bloodborne: near=0.05, far=3000
|
||||
- Большинство геометрии в диапазоне 0.1-0.9
|
||||
- Решение: профилирование распределения глубины в типичных сценах
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
1. ✅ Реализовать depth-adaptive пороги
|
||||
2. ⏳ Собрать и протестировать в игре
|
||||
3. ⏳ Собрать статистику распределения глубины
|
||||
4. ⏳ Настроить пороги на основе реальных данных
|
||||
5. ⏳ Добавить runtime-переключение для A/B тестов
|
||||
6. ⏳ Документировать финальные параметры
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [upscaler.md](upscaler.md) — общая архитектура апскейлера
|
||||
- [motion_vectors.md](motion_vectors.md) — векторы движения
|
||||
- TAA shader: `gpu/shadps4/video_core/host_shaders/taa.comp`
|
||||
- Camera motion: `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
|
||||
@@ -1,447 +0,0 @@
|
||||
# Bloodborne: первый эксперимент нативного запуска
|
||||
|
||||
## Частота кадров, звук, диагностика (2026-09-25)
|
||||
|
||||
- **FPS**: `BB_FPS=uncap` (по умолчанию) применяет патч «Uncap FPS++»
|
||||
(Lance McDonald, Kyo) из `patches/Bloodborne.xml`: интервал флипа 1 и
|
||||
шаги симуляции от реального дельтатайма кадра. Vblank при этом следует за
|
||||
частотой монитора (`BB_VBLANK_HZ=0`). `BB_FPS=60`/`90` — патчи с
|
||||
фиксированным шагом, `BB_FPS=30` — без патчей. Дополнительные патчи из XML:
|
||||
`BB_PATCHES="Skip Intro;Disable Motion Blur (perf increase)"`.
|
||||
`patches.py` компилирует их в `out/patches.bin`; адреса в XML — это
|
||||
виртуальные адреса PS4 (база eboot 0x400000), загрузчик пишет их по
|
||||
смещению `адрес-0x400000` после релокаций (`--patches`).
|
||||
- **Звук**: `sceAudioOutOutput` теперь возвращает управление строго раз в
|
||||
период буфера (как железо). Раньше вызовы шли пачками раз в квант PipeWire
|
||||
(21 мс), поток вывода FMOD обгонял микшер и проигрывал незаписанные половины
|
||||
512-кадровых блоков — щелчок каждые 10.7 мс.
|
||||
- **Производительность потока GPU** (`shadPS4:GpuCommandProcessor` — узкое
|
||||
место: ~97% одного ядра). Замер в одной сцене: 26.5 → 47 FPS.
|
||||
- `runtime_memory.c`: таблица регионов под rwlock вместо рекурсивного
|
||||
мьютекса (18% времени поток GPU ждал его), плюс потоковый кеш регионов,
|
||||
проверяемый по счётчику поколений таблицы, — запросы видеоядра на каждый
|
||||
draw идут без блокировки.
|
||||
- `vk_scheduler.cpp`: `PopPendingOperations` на каждом draw делал ioctl
|
||||
опроса timeline-семафора; теперь только при непустой очереди и не чаще
|
||||
раза в 250 мкс.
|
||||
- `fetch_shader.cpp`: разбор fetch-шейдера кешируется по адресу кода с
|
||||
проверкой копии кода.
|
||||
- `region_manager.h`: без изменённых страниц в диапазоне защита не
|
||||
пересчитывается.
|
||||
- Запись команд Vulkan в отдельном потоке `bb:VkRecorder`
|
||||
(`Scheduler::Record`, команды и их данные в блоках по 128 КиБ; draw,
|
||||
dispatch, дескрипторы, барьеры, копирования, dynamic state, детайлинг).
|
||||
`CommandBuffer()` для непереведённого кода синхронизируется и включает
|
||||
прямую запись до конца draw. `BB_VK_RECORD_THREAD=0` — старое поведение.
|
||||
- Кеш описаний текстур (`ImageDesc`) по T# и флагам ресурса, кеш
|
||||
`FindImage`, проверка изменённых страниц без блокировки для привязок
|
||||
только на чтение, загрузка гостевых данных буферов в потоке записи.
|
||||
- Замер в одной сцене: все оптимизации 64 FPS, без них 40 FPS (×1.6).
|
||||
- `BB_TOGGLE_FILE=<файл>`: маска выключенных оптимизаций, меняется во время
|
||||
игры (1 кеш регионов, 2 кеш fetch-шейдеров, 4 ранний выход трекинга
|
||||
страниц, 8 лимит опроса драйвера, 16 поток записи, 32 кеш текстур,
|
||||
64 проверка страниц без блокировки, 128 кеш FindImage, 256 загрузка
|
||||
буферов в потоке записи, 512 мемо трекера барьеров) — для поиска
|
||||
регрессий без перезапуска.
|
||||
- **Звук**: при старте для Bloodborne выставляется флаг `0x204E` в
|
||||
`SPRJ0005/userdata0010` (хак rainvmaker из форка Diegolix29): с нулевым
|
||||
флагом часть звуков, например оружие игрока, не проигрывается.
|
||||
`BB_SOUND_HACK=0` отключает.
|
||||
Профилирование без perf: загрузчик по SIGUSR2 печатает RIP, `rdi` (адрес
|
||||
мьютекса при ожидании futex) и цепочку кадров потока; GPU-библиотека
|
||||
собирается с `-fno-omit-frame-pointer`.
|
||||
- **Диагностика**: `BB_FRAME_STATS=1` — FPS, худший кадр и время компиляции
|
||||
шейдеров/пайплайнов раз в 5 с; `BB_AUDIO_STATS=1` — ритм и заполнение
|
||||
очереди звука; `BB_AUDIO_DUMP=путь` — сырой PCM каждого порта.
|
||||
|
||||
## Текущее состояние (2026-09-25)
|
||||
|
||||
Оригинальный x86-64 код `eboot.bin` выполняется напрямую на Linux и проходит
|
||||
глобальные конструкторы, инициализацию памяти и запуск собственной системы
|
||||
потоков игры: создано 13 гостевых потоков (`FD4JobWorker:0`, `EzWorkPool_*` ×9,
|
||||
`MOMainThread`, логгер). Игра выделяет ~4.8 ГиБ direct- и ~337 МиБ
|
||||
flexible-памяти. Текущая остановка — `sceAjmInitialize` (аудиокодек ATRAC9),
|
||||
caller `0xd18a6a`.
|
||||
|
||||
Изменения этого этапа:
|
||||
|
||||
- `runtime_memory.c` переписан: единый sparse memfd на весь direct-пул
|
||||
(5056 МиБ, как `sceKernelGetDirectMemorySize` на PS4), таблица регионов
|
||||
с разбиением, flexible memory (448 МиБ), `ReserveVirtualRange`, `Mprotect`,
|
||||
`QueryMemoryProtection`, `VirtualQuery`, `GetDirectMemoryType`, `BatchMap(2)`,
|
||||
частичные unmap/release, `MAP_FIXED`/`NO_OVERWRITE`, GPU-биты защиты.
|
||||
- **Все видимые гостю адреса размещаются ниже 1 ТиБ**: образ, стеки потоков,
|
||||
главный стек, трамплины (`runtime_low_map`), гостевая память
|
||||
(`0x10_0000_0000..0xfc_0000_0000`), кучи host-объектов (non-PIE + `mallopt`).
|
||||
Игра упаковывает указатели в 40 бит: например, lock по адресу `0x20368c0`
|
||||
хранит владельца как `scePthreadSelf() & 0xffffffffff` и сравнивает с полным
|
||||
значением. С хэндлами `0x7f…` это давало вечную взаимоблокировку.
|
||||
- Watchdog (`--timeout`) печатает RIP и цепочку кадров всех потоков
|
||||
(смещения относительно образа и `bb-probe` для `addr2line`).
|
||||
`BB_TRACE_SEMA=1` печатает блокирующие ожидания семафоров.
|
||||
`got_import.py <адрес GOT>` называет импорт за PLT-заглушкой.
|
||||
- Сеть в режиме «кабель отключён»: HTTP-шаблоны/соединения/запросы создаются,
|
||||
отправка возвращает сетевую ошибку; NpWebApi/Matching2/Signaling/Score —
|
||||
«не в PSN»; голосовой чат — порты без звука.
|
||||
|
||||
Следующие подсистемы по порядку появления: Ajm (ATRAC9 через ffmpeg),
|
||||
Fios2, SaveData, Rtc, Pad, AudioOut, AvPlayer, VideoOut и GnmDriver.
|
||||
|
||||
## Предыдущее описание
|
||||
|
||||
|
||||
**Оригинальный x86-64 код `eboot.bin` выполняется непосредственно процессором
|
||||
Linux и проходит начальный участок конструкторов.** Теперь он возвращается из
|
||||
`_init_env`, регистрирует три обработчика `atexit` и 279 C++-деструкторов,
|
||||
проходит 1416 guard инициализации, создаёт 1422 mutex, один rwlock и 17 семафоров,
|
||||
выделяет/отображает 116 МиБ для игрового аллокатора.
|
||||
Оригинальная `libc.prx` загружается вместе с eboot, её инициализатор возвращает 0,
|
||||
конструктор `std::ios_base::Init` пройден. Следующая контролируемая остановка —
|
||||
`sceSysmoduleLoadModule(0xb4)` (`g8cM39EUZ6o#M#N`, caller `0x2026149`).
|
||||
Отдельный тест Vulkan выполняет команду заполнения буфера и проверяет
|
||||
возвращённые 4096 байт. shadPS4 в этом эксперименте не запускается.
|
||||
|
||||
**Это ещё не рекомпиляция всей игры и не играбельный порт.** Нет окна игры,
|
||||
меню, отрисовки игровых ресурсов или работающего игрового цикла. Vulkan пока
|
||||
не соединён с графическими командами игры. Контролируемая остановка на очередном
|
||||
неподдерживаемом импорте — измеримая граница прототипа; запуск игры не завершён.
|
||||
|
||||
## Запуск в текущем окружении
|
||||
|
||||
Из `game_files`:
|
||||
|
||||
```bash
|
||||
bash native_probe/run.sh --software
|
||||
```
|
||||
|
||||
Скрипт находит локальные Python, GCC и Vulkan SDK, подготавливает образ,
|
||||
собирает загрузчик и запускает его. На этой машине инструменты найдены
|
||||
в `/nix/store`; ничего скачивать или устанавливать не понадобилось.
|
||||
`--software` выбирает Lavapipe и отключает неявные Vulkan layers для теста.
|
||||
В проверенном окружении `/dev/dri` отсутствует, поэтому реальный GPU не проверен.
|
||||
|
||||
Ожидаемый результат:
|
||||
|
||||
```text
|
||||
Vulkan: llvmpipe (LLVM 21.1.8, 256 bits); command submission + 4096-byte readback PASS
|
||||
Mapped 91973152 bytes, 4 segments; applied 237083 relocations
|
||||
Starting native libc.prx at image offset 0x56e0000; fallback exports=202
|
||||
Runtime: application heap API registered
|
||||
Native libc module initializer returned 0
|
||||
Entering original x86-64 code at guest offset 0xa0
|
||||
Runtime: _init_env returned (verified libc implementation: RET)
|
||||
Runtime: direct allocation offset=0x0, size=121634816 bytes
|
||||
Runtime: mapped direct memory, size=121634816, alignment=0x200000
|
||||
Runtime: semaphore id=1, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=2, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=3, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=4, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=5, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=6, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=7, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=8, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=9, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=10, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=11, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=12, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=13, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=14, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=15, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=16, attr=2, initial=0, max=1
|
||||
Runtime: semaphore id=17, attr=2, initial=0, max=1
|
||||
STOP: first unsupported PS4 import: g8cM39EUZ6o#M#N (index 249)
|
||||
API: sceSysmoduleLoadModule
|
||||
Caller return offset: 0x2026149; first argument: 0xb4
|
||||
Runtime: _init_env=1, atexit=3, __cxa_atexit=279, registered handlers=282
|
||||
Runtime: static guards acquired=1416, released=1416
|
||||
Runtime: mutexes created=1422, locks=3208, unlocks=3208
|
||||
Runtime: rwlocks created=1, reads=21, writes=1, unlocks=22
|
||||
Runtime: semaphores created=17, deleted=0, acquired=0, signals=0, timeouts=0
|
||||
Runtime: direct memory allocations=1, maps=1, live=121634816, budget=536870912
|
||||
Runtime: memory operations=9884
|
||||
Original guest entry instructions executed; game initialization is incomplete.
|
||||
```
|
||||
|
||||
**Код завершения 20 означает ожидаемую остановку на отсутствующей реализации
|
||||
PS4 API.** Он намеренно отличается от 0. Ошибки загрузки возвращают 1;
|
||||
явно неподдерживаемые режимы runtime — 21, срабатывание stack protector — 22.
|
||||
Linux-сигналы — 128 + номер сигнала. На выполнение гостевого кода в Linux
|
||||
установлен таймер 10 секунд.
|
||||
|
||||
На системе с доступным GPU можно попробовать `bash native_probe/run.sh`.
|
||||
Это использует обычный выбор устройства Vulkan; аппаратный результат здесь
|
||||
не проверен. Для повторного запуска уже собранных файлов:
|
||||
|
||||
```bash
|
||||
native_probe/out/bb-probe native_probe/out/boot-libc.bin --cpu-only
|
||||
native_probe/out/bb-probe --vulkan-only
|
||||
```
|
||||
|
||||
Базовый образ без нативной libc сохраняется в `out/boot.bin` для сравнения
|
||||
с прежней остановкой на `std::ios_base::Init`.
|
||||
|
||||
Из каталога `native_probe` команда остаётся прежней: `bash run.sh --software`.
|
||||
Для воспроизведения самой ранней остановки на `_init_env` используйте
|
||||
`out/bb-probe out/boot.bin --cpu-only --strict-imports` из этого каталога.
|
||||
|
||||
Последняя команда использует обычные настройки Vulkan. Для программного
|
||||
драйвера задайте `VK_DRIVER_FILES` на его JSON и
|
||||
`VK_LOADER_LAYERS_DISABLE='~implicit~'`, как делает `run.sh --software`.
|
||||
|
||||
## Что найдено в файлах
|
||||
|
||||
| Параметр | Результат |
|
||||
|---|---|
|
||||
| Игра | Bloodborne, CUSA03173 |
|
||||
| Версия из `param.sfo` | APP_VER 01.09, VERSION 01.00 |
|
||||
| Контейнер | SELF, 8 записей сегментов |
|
||||
| CPU | x86-64, ELF64 little-endian, PS4/FreeBSD ABI |
|
||||
| Тип ELF | ET_SCE_DYNEXEC (`0xfe10`) |
|
||||
| Точка входа | `0xa0` относительно базы загрузки |
|
||||
| Загружаемые сегменты | 2: код/данные только для чтения и изменяемые данные/BSS |
|
||||
| Образ в памяти | 91 042 036 байт |
|
||||
| Импорты | 686 уникальных импортированных символов |
|
||||
| Зависимости DT_NEEDED | 42 модуля |
|
||||
| Перемещения адресов | 231 478 RELATIVE, 2 838 ABS64, 23 GLOB_DAT, 661 JUMP_SLOT |
|
||||
| Ресурсы `dvdroot_ps4` | 31 322 968 005 байт |
|
||||
|
||||
Среди зависимостей: libc, libkernel, GnmDriver, VideoOut, Pad, Fios2,
|
||||
AudioOut, SaveData, Sysmodule, сетевые и диалоговые API. В `sce_module`
|
||||
имеются только семь файлов; наличие файлов модулей не заменяет поддержку их ABI.
|
||||
|
||||
Используемые сегменты SELF доступны открытым текстом. Извлечение не выполняет
|
||||
расшифровку. Неподдерживаемые сжатые/зашифрованные сегменты отклоняются.
|
||||
Один незагружаемый сегмент метаданных (`program header 9`, тип `0x6fffff01`)
|
||||
не представлен в доступных SELF-сегментах. В диагностическом `eboot.elf` его
|
||||
диапазон заполнен нулями: **это не побайтово полное восстановление ELF**.
|
||||
Все сегменты, нужные этому эксперименту, проверены на наличие.
|
||||
|
||||
Подробности, SHA-256 исходного eboot, список зависимостей, NID, сегментов
|
||||
и количество ресурсов по каталогам находятся в `out/analysis.json`.
|
||||
Имена нескольких импортов проверены вычислением NID из предполагаемых имён;
|
||||
результаты записаны в `import_name_hints`. Для первого вызова `_init_env`
|
||||
получен NID `bzQExy189ZI`, следующий вызов соответствует `atexit`.
|
||||
|
||||
## Как устроен прототип
|
||||
|
||||
`prepare.py` читает SELF и SFO, проверяет границы, восстанавливает доступные
|
||||
сегменты, разбирает PS4 dynamic/symbol/RELA-таблицы и формирует `out/boot.bin`.
|
||||
Исходные файлы открываются только для чтения. Все результаты — в `out/`.
|
||||
|
||||
Дополнительно проверяется фактический экспорт `_init_env` в `sce_module/libc.prx`:
|
||||
NID `bzQExy189ZI`, адрес `0x5fef0`, размер 1, байт `c3` (RET).
|
||||
SHA-256 libc и доказательство записываются в `analysis.json/libc_evidence`.
|
||||
Первоначальное предположение о сложной инициализации внутри `_init_env`
|
||||
для этого дампа не подтвердилось. Возврат соответствует реальному коду функции.
|
||||
Формат `BBPROBE2` содержит флаг этой проверки; без флага runtime отключён.
|
||||
Старые образы `BBPROBE1` читаются с отключённым runtime.
|
||||
|
||||
`link_libc.py` создаёт отдельный `out/boot-libc.bin` формата `BBPROBE3`:
|
||||
добавляет два сегмента оригинальной libc, её перемещения адресов и 202
|
||||
резервные привязки экспортов. Сопоставляет NID вместе с именами и версиями
|
||||
библиотеки/модуля: локальные суффиксы libc `#C#A` и eboot `#q#q` различаются.
|
||||
Реализованные host-контракты имеют приоритет. Остальные совпавшие импорты
|
||||
ссылаются на настоящий код/данные libc, включая объекты и таблицы C++.
|
||||
Исходные файлы игры не изменяются; сведения о привязках и SHA-256 libc
|
||||
записываются в `out/libc-link.json`.
|
||||
|
||||
Перед входом в eboot выполняется `DT_INIT` libc. Поддержан TLS этой библиотеки:
|
||||
модуль 2, шаблон 544 байта, блок 1184 байта на host-поток, обнулённый остаток;
|
||||
14 перемещений `DTPMOD64` записывают номер модуля, а не указатель на образ.
|
||||
`__tls_get_addr` проверяет модуль и границы. Полный TLS всех модулей PS4,
|
||||
его освобождение при завершении гостевых потоков и прямой доступ через FS
|
||||
не реализованы. Для текущего одного гостевого потока блок живёт до выхода.
|
||||
`sceKernelGetProcParam` возвращает исходный перемещённый procparam eboot;
|
||||
`_sceKernelRtldSetApplicationHeapAPI` сохраняет таблицу callbacks аллокатора.
|
||||
|
||||
`runtime_thread.c` реализует self и запросы init/get/getaffinity/destroy
|
||||
атрибутов текущего гостевого потока. Для этого прототипа существует один
|
||||
виртуальный CPU (маска 1); это не маска host CPU и не настройка его affinity.
|
||||
Создание гостевых потоков и изменение атрибутов остаются неподдержанными.
|
||||
|
||||
`probe.c` выделяет память, копирует сегменты и применяет перемещения адресов.
|
||||
Неизвестные функции направляются в исполняемые перехватчики, печатающие NID
|
||||
и завершающие процесс. Неподдерживаемые импортированные данные направляются в недоступные
|
||||
страницы: их использование вызывает ошибку, а не возвращает фиктивные данные.
|
||||
После загрузки устанавливаются права страниц по ELF, код получает права RX.
|
||||
Затем выполняется прямой вызов оригинальной точки входа по SysV ABI.
|
||||
Подготовки аргументов достаточно для наблюдаемого участка; полноценный стек
|
||||
запуска ядра PS4, исключения и общий механизм загрузки зависимых модулей
|
||||
не реализованы; пока специально подключена только libc.
|
||||
|
||||
`runtime.c` реализует проверенный возврат из `_init_env`, реестр `atexit`/
|
||||
`__cxa_atexit` с обратным порядком и фильтрацией DSO, `__cxa_finalize`,
|
||||
`exit`, guard acquire/release/abort, случайный process-local stack canary,
|
||||
аварийный обработчик повреждения стека, четыре функции работы с памятью и `strlen`.
|
||||
Сопоставление выполняется по полному NID с library/module suffix именно
|
||||
этого eboot, без автоматического «вернуть 0» для неизвестных импортов.
|
||||
Реестр завершения и guards рассчитаны на текущий один гостевой поток;
|
||||
рекурсивная/конкурентная инициализация guard останавливает выполнение с кодом 21.
|
||||
|
||||
`runtime_mutex.c` связывает PS4 mutex/атрибуты с Linux pthread, преобразует
|
||||
типы и коды ошибок, поддерживает рекурсивные и статически созданные mutex.
|
||||
Приоритетные протоколы, PS4-планировщик и гостевые потоки не реализованы.
|
||||
Opaque handles предназначены для реализованных API; двоичный layout
|
||||
внутренних PS4 mutex не воспроизводится. Наблюдаемый участок libc работает
|
||||
через эти API; прямой доступ к внутренностям mutex потребует отдельной реализации.
|
||||
|
||||
POSIX-импорты mutex из `libScePosix` используют тот же backend через
|
||||
отдельные wrappers: возвращают положительные PS4 errno, а не отрицательные
|
||||
коды `scePthread`. Двухаргументный `pthread_mutex_init` не читает отсутствующий
|
||||
третий аргумент. Linux errno не передаётся гостю напрямую.
|
||||
|
||||
`runtime_sema.c` реализует kernel create/delete, wait/poll, signal/cancel.
|
||||
Handles — уникальные 32-битные ID; удалённые ID не переиспользуются.
|
||||
Проверены счётчики и переполнение, таймаут в микросекундах с монотонными
|
||||
часами, резервирование токенов до пробуждения, отмена и удаление с ожидающими
|
||||
host-потоками. Объект освобождается после возврата всех ожидающих вызовов.
|
||||
В режиме FIFO обслуживаются подходящие запросы в порядке очереди; запрос
|
||||
меньшего размера может пройти ожидающий запрос, для которого токенов ещё нет.
|
||||
Это соответствует использованной справочной модели shadPS4; сопоставление
|
||||
на реальной PS4 пока не выполнено.
|
||||
Атрибуты 0/2 допускают создание и неблокирующие операции, но блокирующее
|
||||
ожидание явно останавливает прототип (21): приоритетный PS4-планировщик ещё
|
||||
отсутствует. Ненулевые option parameters также не реализованы. В текущем
|
||||
запуске игра создала 17 семафоров с attr=2, initial=0, max=1, но пока не
|
||||
выполняла wait/signal; эти операции проверены отдельными C-тестами.
|
||||
|
||||
`runtime_time.c` поддерживает POSIX `gettimeofday`: время берётся у host,
|
||||
поля копируются в явные PS4 timeval/timezone. Проверены границы полей и
|
||||
диапазон микросекунд. Неожиданная ошибка host API останавливает прототип:
|
||||
полный POSIX errno/TLS bridge ещё не реализован.
|
||||
|
||||
`runtime_rwlock.c` поддерживает init/destroy, read/write, try-read/try-write,
|
||||
timed-read/timed-write и unlock поверх Linux pthread. Реестр синхронизирует
|
||||
инициализацию статических handles, отслеживает владельцев, читателей и
|
||||
ожидающие вызовы: уничтожение занятой блокировки возвращает EBUSY,
|
||||
чужой unlock — EPERM, таймаут — Orbis ETIMEDOUT. Ожидание не удерживает
|
||||
mutex реестра. Проверена одновременная работа нескольких host-потоков,
|
||||
но создание гостевых потоков пока отсутствует. Поддержаны только атрибуты
|
||||
по умолчанию; нестандартные атрибуты останавливают прототип с кодом 21.
|
||||
|
||||
`runtime_memory.c` даёт **ограниченный CPU-пул 512 МиБ для исследования**.
|
||||
Это выбранный бюджет прототипа, не объём RAM PS4. Выделение возвращает
|
||||
виртуальный физический offset; Linux memfd обеспечивает реальное общее
|
||||
хранилище для нескольких отображений и выравнивание 16 КиБ/2 МиБ.
|
||||
Реализованы allocate/map, полное unmap и полное release. Поддержаны типы
|
||||
памяти 0/3 для CPU, permissions read/write, flags=0, автоматический адрес;
|
||||
иной режим останавливается с диагностикой. Частичные release/unmap пока
|
||||
возвращают EINVAL. GPU coherency, резервирование фиксированных адресов,
|
||||
полное адресное пространство PS4 и подключение этого пула к Vulkan отсутствуют.
|
||||
|
||||
`vulkan_smoke.c` создаёт instance/device/queue, отправляет `vkCmdFillBuffer`,
|
||||
ставит барьер для чтения CPU, ожидает завершения и сравнивает все слова буфера.
|
||||
Это проверка будущего графического backend, не переводчик GNM.
|
||||
|
||||
## Сборка на других системах
|
||||
|
||||
Linux x86-64: Python 3, GCC/Clang, Vulkan headers и loader. Обычно достаточно:
|
||||
|
||||
```bash
|
||||
python3 native_probe/prepare.py CUSA03173
|
||||
python3 native_probe/link_libc.py CUSA03173
|
||||
bash native_probe/build.sh
|
||||
```
|
||||
|
||||
Скрипт поддерживает `CC`, `VULKAN_INCLUDE`, `VULKAN_LIB`;
|
||||
при наличии `pkg-config vulkan` использует его настройки.
|
||||
|
||||
Для Windows в исходнике есть ветка `VirtualAlloc`/`VirtualProtect` и явный
|
||||
SysV ABI. Предполагается **MinGW-w64 GCC/Clang x86-64**, не MSVC.
|
||||
Эта ветка **не собрана и не проверена** в текущем Linux-окружении.
|
||||
Пример ручной сборки в окружении MinGW с установленным Vulkan SDK:
|
||||
|
||||
```text
|
||||
python native_probe/prepare.py CUSA03173
|
||||
gcc -std=c11 -O2 -Wall -Wextra native_probe/probe.c native_probe/runtime.c native_probe/runtime_mutex.c native_probe/runtime_thread.c native_probe/runtime_rwlock.c native_probe/runtime_sema.c native_probe/runtime_time.c native_probe/runtime_memory.c native_probe/vulkan_smoke.c -I<SDK>/Include -L<SDK>/Lib -lvulkan-1 -o native_probe/out/bb-probe.exe
|
||||
native_probe/out/bb-probe.exe native_probe/out/boot.bin
|
||||
```
|
||||
|
||||
Пути SDK заменяются фактическими; бинарник Linux непереносим на Windows.
|
||||
Диагностика сигналов и таймер в текущем исходнике реализованы только для Linux.
|
||||
Новые mutex/rwlock/semaphore/time/direct-memory backends также реализованы только для Linux;
|
||||
Windows дойдёт до более раннего неподдерживаемого импорта.
|
||||
|
||||
## Проверки
|
||||
|
||||
```bash
|
||||
bash native_probe/build.sh --test
|
||||
python3 -m unittest discover -s native_probe -v
|
||||
```
|
||||
|
||||
Пройдено 35 Python-тестов, включая запуск набора C-проверок runtime:
|
||||
границы SELF/образа, проверка инструкции `_init_env`, прямое исполнение
|
||||
x86-64, перемещения адресов, режим strict, отключение неподтверждённого runtime,
|
||||
диагностика сбоев. C-проверки охватывают обратный порядок деструкторов,
|
||||
DSO и повторную финализацию, регистрацию при финализации и рост реестра,
|
||||
guard acquire/abort/release, ошибки и рекурсивность mutex, выравнивание,
|
||||
непересечение физических выделений, shared aliasing и повторное выделение
|
||||
очищенной памяти. Stack protector и рекурсивный guard проверены в subprocess.
|
||||
Дополнительно проверены жизненный цикл rwlock, повторные read-захваты,
|
||||
чужой unlock, занятое уничтожение, несколько одновременных читателей,
|
||||
ожидающий writer, преобразование таймаутов и `strlen` с NUL внутри строки.
|
||||
Добавлены проверки нативного инициализатора/экспорта, приоритета host API,
|
||||
отключения libc в strict-режиме, прав исполнения и границ новых metadata,
|
||||
совпадения идентичности при разных локальных ID, отказа при другой версии
|
||||
библиотеки и обработки TLS-релокаций. C-тест проверяет отдельные TLS-блоки
|
||||
в host-потоках, шаблон/BSS, запросы атрибутов и регистрацию heap API.
|
||||
Отдельный `sema-test` проверяет 32-битную запись handle без порчи соседнего
|
||||
поля, устаревшие ID, переполнение, таймаут, очередь из нескольких host-потоков,
|
||||
резервирование токенов, отмену/удаление с ожидающими и явный отказ от
|
||||
приоритетного блокирующего ожидания. Проверены положительные ошибки POSIX
|
||||
mutex, рекурсивный захват и выходные структуры `gettimeofday`.
|
||||
Linux-сборка выполнена с `-Wall -Wextra -Werror`.
|
||||
Актуальный интеграционный запуск настоящего eboot записан в `out/run.log`;
|
||||
результат старого режима — в `out/strict-run.log`.
|
||||
|
||||
## Следующие этапы настоящего порта
|
||||
|
||||
1. Реализовать загрузку/реестр системных модулей и необходимые контракты
|
||||
AppContent. Игра вызвала `sceSysmoduleLoadModule(0xb4)` из `0x2026149`;
|
||||
по справочной таблице этот ID означает `libSceAppContent`. Такого файла
|
||||
среди семи локальных `sce_module/*.prx` нет. Потребуется реализация
|
||||
используемых API и состояния модуля; простой возврат успеха не добавлен.
|
||||
2. Продолжить конструкторы, дополнить память/синхронизацию, затем реализовать
|
||||
TLS остальных модулей, запуск потоков и загрузку необходимых модулей.
|
||||
3. Дойти до `main`, добавить файловую систему (`/app0` → каталог ресурсов)
|
||||
и фиксировать каждый следующий неподдерживаемый контракт.
|
||||
4. Перевести GNM/PM4-команды, форматы ресурсов и AMD GCN-шейдеры в Vulkan/SPIR-V;
|
||||
затем связать VideoOut с окном и swapchain. Наличие Vulkan само по себе
|
||||
не делает графический код PS4 совместимым.
|
||||
5. Для статической рекомпиляции восстанавливать функции и контрольный поток,
|
||||
таблицы виртуальных вызовов, исключения и ABI; проверять поведение сравнением
|
||||
с исходным машинным кодом. На x86-64 прямое исполнение с совместимым runtime
|
||||
уже позволяет исследовать запуск без предварительного перевода всего CPU-кода.
|
||||
|
||||
Приоритет следующего изменения — Sysmodule/AppContent и следующие конструкторы.
|
||||
|
||||
## Справочные источники
|
||||
|
||||
Формат SELF и правила соотнесения сегментов сверены с
|
||||
[заголовками загрузчика shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/loader/elf.h)
|
||||
и [загрузкой сегментов](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/loader/elf.cpp).
|
||||
PS4 dynamic-таблицы и вычисление NID сверены с
|
||||
[module.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/module.cpp),
|
||||
контекст запуска — с [linker.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/linker.cpp).
|
||||
Сигнатуры mutex и значения типов сверены с
|
||||
[mutex.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/mutex.cpp)
|
||||
и [pthread.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/pthread.h),
|
||||
rwlock — с [rwlock.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/rwlock.cpp),
|
||||
сигнатуры direct memory — с
|
||||
[memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/memory.cpp).
|
||||
Семафоры сверены с
|
||||
[semaphore.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/semaphore.cpp),
|
||||
структуры времени — с
|
||||
[time.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/time.h).
|
||||
Идентификатор AppContent — с
|
||||
[sysmodule_table.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/sysmodule/sysmodule_table.h).
|
||||
Новые контракты регистрации heap API и procparam сверены с
|
||||
[linker.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/linker.h)
|
||||
и [process.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp),
|
||||
атрибуты потоков — с
|
||||
[pthread_attr.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/pthread_attr.cpp).
|
||||
Тело `_init_env` и устройство guards проверены дизассемблированием локальной libc.
|
||||
Реализация прототипа написана отдельно; зависимости на исполняемый shadPS4 нет.
|
||||
|
||||
Каталог `out/` содержит производные данные игры, имеет `.gitignore` и не нужен
|
||||
для распространения исходников инструмента.
|
||||
@@ -1,355 +0,0 @@
|
||||
# Уход от эмуляции: что сделано и что дальше (2026-10-06)
|
||||
|
||||
Исторический план и замеры по этапам разработки. В `0.5-pre2` новая модель остаётся
|
||||
экспериментальной и выключена по умолчанию; для NVIDIA и других не-AMD GPU добавлен модуль
|
||||
памяти без sparse-перепривязок. Актуальное состояние и проверки:
|
||||
[CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md).
|
||||
|
||||
## Где проходит граница API
|
||||
|
||||
Bloodborne собран со статической libGnm. Пакеты PM4 пишет не драйвер, а сама игра, функциями
|
||||
`sce::Gnm::*CommandBuffer` (`0x1068000–0x10a0000`, `0x11fd000–0x11fe000`). Перехват всех таких
|
||||
функций (`BB_GNM_OBSERVE=1`) показал: 100 % отправляемых пакетов пишут известные вызовы API, всего
|
||||
99 методов. Gnm — API, который пишет регистры: `setDepthStencilControl` = один `SET_CONTEXT_REG`.
|
||||
Поэтому разбор PM4 от этих 99 функций и трансляция самих вызовов дают одно и то же; PM4 здесь —
|
||||
формат записи вызовов, как командный список. Он уже разбирается параллельно (DrawPreparation).
|
||||
|
||||
Эмуляция железа — не разбор пакетов, а то, что делается вместо GPU:
|
||||
|
||||
| Что | Состояние |
|
||||
|---|---|
|
||||
| Модель памяти 0.2 (копии в VRAM, защита страниц, обратное чтение, ранние метки) | Запасной вариант. Модель ПК — по умолчанию, для NVIDIA режим «кусок на выделение». Удалить код 0.2 после проверки на NVIDIA. |
|
||||
| Метки EOP/EOS | Модель ПК: пишутся после завершения работы GPU (честно). |
|
||||
| `WaitRegMem` на метки той же очереди | Транслируется как выполненное ожидание (порядок очереди); ждать CPU не нужно. |
|
||||
| `WRITE_DATA`/DMA ждали все копии хоста | Ждут только пересекающиеся копии (как командный процессор, который не ждёт шейдеров). |
|
||||
| Constant Engine (CE RAM, дампы, счётчики) | Выполняется на CPU в порядке потока: наш разборщик читает описатели из памяти. |
|
||||
| SRT walker (код шейдера GCN, исполняемый на CPU) | Нужен по той же причине. |
|
||||
| Учёт записей GPU для разборщика (`PendingWriteOverlaps`, `IsRegionGpuModified`) | ~15 % потока GpuComm, та же причина. |
|
||||
| Кэш текстур по адресу + хеш | Та же причина плюс отсутствие объектов «текстура» в PM4. |
|
||||
|
||||
Почти вся оставшаяся эмуляция существует потому, что **транслятор читает описатели ресурсов
|
||||
(T#/V#/S#) из памяти игры на CPU во время разбора**. Драйвер ПК и DXVK так не делают: память
|
||||
приложения читает GPU во время выполнения.
|
||||
|
||||
## Дальше, по порядку
|
||||
|
||||
1. **NVIDIA.** Тестовая сборка `dist/Bloodborne-bbport-x86_64-test-1006.AppImage` (модель ПК по
|
||||
умолчанию, режим «кусок на выделение»). Если работает — удалить модель 0.2 целиком
|
||||
(`page_manager`, `fault_manager`, обратное чтение, BB_UFFD, предзагрузка, ранние метки).
|
||||
2. **Распараллелить стадию B** (запись отрисовок; на 4 ядрах занята на 99 %, ~3,3 мкс на
|
||||
отрисовку, равномерно по привязке буферов/текстур, началу прохода, вершинам). Варианты:
|
||||
(а) несколько потоков стадии B по диапазонам отрисовок внутри прохода рендеринга (у каждого
|
||||
свой сегмент команд; общие кэши уже под блокировками; порядок барьеров — по проходам);
|
||||
(б) разрешение привязок (буферы/текстуры → Vulkan) на стадии A, где 28 % простоя, а B только
|
||||
записывает. Нужны прогулочные тесты.
|
||||
3. **Описатели на GPU (главный шаг к «как DXVK»).** Шейдер читает T#/V#/S# сам (буферы на месте
|
||||
и BDA уже есть); текстуры — bindless-массив, T# → индекс через таблицу на GPU, которую
|
||||
ведёт кэш текстур при создании образов. Тогда не нужны SRT walker, CE на CPU, учёт ожидающих
|
||||
записей; `WRITE_DATA`/DMA/дампы CE становятся записями GPU в порядке потока. Большой проект.
|
||||
4. **Текстуры как объекты.** Перехват `Gnm::Texture::init*` и загрузчика TPF
|
||||
(`CSGXTextureCreateFragment`): образ создаётся при загрузке, а не угадывается по адресу;
|
||||
хеши и наложения адресов уходят. Ассеты можно заранее перевести в линейный вид (детайлинг
|
||||
один раз при установке).
|
||||
5. **Шейдеры заранее.** Все шейдеры GCN из файлов игры → SPIR-V при установке; без компиляции
|
||||
во время игры.
|
||||
|
||||
Что остаётся трансляцией, как у DXVK: шейдеры GCN → SPIR-V, описатели и форматы → Vulkan.
|
||||
|
||||
## Шаг 1 (2026-10-07): движок констант больше не синхронизируется с записью отрисовок
|
||||
|
||||
Замеры (`BB_CE_STATS=1`, Яр'гул): ~14 000 `DumpConstRam` в секунду, ~4–5 МБ/с, всего 25 страниц
|
||||
(кольцо ~100 КБ). Каждый дамп читается ровно одним указателем из user data — это таблицы
|
||||
описателей для SRT walker. Константы, вершины, индексы, большие буферы из этих страниц не
|
||||
читаются; шейдеров, читающих память самостоятельно (`uses_dma`), нет. То есть дампы CE нужны
|
||||
только транслятору — это построение таблиц описателей, а не данные для GPU.
|
||||
|
||||
Ожидания стадии A на дампах (`WaitForPendingReads`) были нужны только потому, что стадия B для
|
||||
отрисовок без подготовки сама перечитывала таблицы V# (описатели вершинных буферов) — к тому
|
||||
времени перезаписанные следующим дампом (~15 % V# оказывались чужими: баг DoF, issue #44).
|
||||
|
||||
Сделано:
|
||||
- стадия A снимает V# в пакет отрисовки, стадия B берёт их оттуда и таблицы не читает;
|
||||
- источник DMA отмечается точным диапазоном, а не «читает неизвестно что».
|
||||
|
||||
Итог: ожиданий на дампах ~850/с → ~46/с, стадия A ждёт 1,5 % времени вместо ~30 %
|
||||
(16 потоков). На 4 ядрах / 8 потоках FPS тот же (~170): там узкое место — стадия B
|
||||
(3,4 мкс на пакет; больше всего — `ObtainBuffer` 0,57 мкс: сопоставление адресов памяти игры с
|
||||
буферами GPU).
|
||||
|
||||
Записи командного процессора (`BB_CE_STATS`): `WRITE_DATA` ~8 400/с в 2 страницы (читаются как
|
||||
константы ~240/с), DMA ~1 000/с в 15 страниц. Они пишут данные для GPU, и их перевод — команды
|
||||
GPU в порядке потока (`vkCmdUpdateBuffer`, у DMA для памяти на месте уже так). Для `WRITE_DATA`
|
||||
это упирается в правило переноса часто записываемых GPU блоков в VRAM — делать вместе с
|
||||
упрощением модели памяти.
|
||||
|
||||
## Шаг 2 (2026-10-07): синхронизации стадии B
|
||||
|
||||
Стадия B (запись отрисовок) — узкое место на 4 ядрах (занята 99 %). Задачи кадра на ней:
|
||||
- сигнал flip = подготовка кадра презентером; 600 тыс. тактов на кадр, из них работы ~40 тыс.,
|
||||
остальное — ожидание потоков записи команд (`CommandBuffer()`) и синхронная отправка. Теперь
|
||||
~50 тыс.: презентер записывается через `Record`, отправка асинхронная;
|
||||
- проходы апскейлера пишут команды напрямую — для них отдельный сегмент без ожидания;
|
||||
- DMA (~900/с, 4 байта) ждали копии и сигналы даже когда запись делает GPU — ждут только для
|
||||
записей CPU.
|
||||
|
||||
Что осталось на стадии B (мкс на пакет, 3,5 всего): привязка ресурсов 1,0 (буферы 0,4, текстуры
|
||||
0,45), `ObtainBuffer` 0,6, начало прохода 0,26, вершины 0,34, задачи 0,2. Дальше —
|
||||
разделение стадии B или привязка ресурсов на стадии A (см. п. 2 выше).
|
||||
|
||||
## Шаг 3 (2026-10-08): метки пишет видеочип, перехваты без сигналов, NVIDIA
|
||||
|
||||
Что игра реально отправляет (новый счётчик `BB_DCB_STATS=1`: пакеты PM4 по типам, события,
|
||||
метки, исходы `WAIT_REG_MEM`; Кошмар охотника, у лампы, ~185 FPS): `EVENT_WRITE_EOP` 61 тыс./с
|
||||
(~330 за кадр, все без прерывания, 32- и 64-битные метки), `WAIT_REG_MEM` 31 тыс./с (все по
|
||||
памяти и все выполнены по меткам, ожидающим в порядке потока — процессор не ждёт ни разу),
|
||||
`EVENT_WRITE_EOS` 371/с, `WRITE_DATA` 3,5 тыс./с, `DUMP_CONST_RAM` 6,6 тыс./с, сбросы кэшей
|
||||
(`EVENT_WRITE`, `ACQUIRE_MEM`) — около 70 тыс./с. Не встречаются: occlusion-запросы
|
||||
(`ZPASS_DONE`), `SET_PREDICATION`, `COND_EXEC`, `MEM_SEMAPHORE`, `REWIND`, `COPY_DATA`, метки
|
||||
со временем GPU, отправки в очереди compute.
|
||||
|
||||
Сделано:
|
||||
- **Метки EOP/EOS пишет видеочип** (`Rasterizer::WriteLabelOnGpu`, `BB_GPU_LABELS=0` — как
|
||||
раньше): буферный маркер Vulkan (`VK_AMD_buffer_marker`, конец конвейера) прямо в память игры
|
||||
(кусок гостевой памяти), в той же точке потока, где метку пишет командный процессор PS4.
|
||||
64-битная метка — две 32-битные записи, старшая половина первой. Раньше метку писал поток
|
||||
процессора после завершения всей отправки Vulkan (61 тыс. записей в секунду). Метки с
|
||||
прерыванием по-прежнему сигналятся с процессора (прерывание — оттуда). То же для меток
|
||||
`RELEASE_MEM` очередей compute. EOS с записью из GDS — копия видеочипом в порядке потока вместо
|
||||
`Finish()` и чтения процессором.
|
||||
- **Перехваты кода игры без ловушек** (`gpu/shim/bbport_guest_hooks.cpp`, `BB_GUEST_HOOK_TRAPS=1`
|
||||
— ловушки `int3`): точки загрузчиков, аллокатора диапазонов и кучи GPU переходят в заглушки
|
||||
рядом с образом, которые вызывают обработчик и выполняют перекрытые инструкции; вызовы `memcpy`
|
||||
загрузчиков идут через трамплин. Раньше каждый вызов был сигналом ядра (SIGTRAP), ~1200 в
|
||||
секунду только у аллокатора.
|
||||
- **Новая модель на NVIDIA** (`gpu/shim/bbport_guest_memory.cpp`): кроме кусков dma-buf (AMD) —
|
||||
режим «память хоста, импортированная видеочипом» (`VK_EXT_external_memory_host`): кусок
|
||||
остаётся в memfd рантайма, видеочип импортирует его второе отображение. При запуске
|
||||
самопроверка в точности как в работе: импорт memfd, привязка к sparse-буферу, запись
|
||||
видеочипом и чтение процессором через другое отображение, и обратно. Не прошла — dma-buf
|
||||
(у NVIDIA «кусок на выделение»), для которого теперь меряется скорость чтения процессором
|
||||
(некэшируемое отображение замедлило бы код игры; меньше 1 ГБ/с — не используется). Не прошло
|
||||
и это — старая модель. Выбор: `BB_GUEST_MEMORY=host|dmabuf`. Переключатель в лаунчере доступен
|
||||
на AMD и NVIDIA. На AMD (RADV) импорт memfd драйвер не принимает — там dma-buf, как раньше.
|
||||
На NVIDIA не проверено: в журнале видно, какой путь выбран и почему.
|
||||
|
||||
Замеры (у лампы): 16 потоков — 233 FPS и с метками видеочипа, и без; 4 ядра / 8 потоков —
|
||||
~217 против ~215 FPS (в пределах шума; выигрыш — свободный поток сигналов).
|
||||
|
||||
### Что ещё осталось и почему
|
||||
|
||||
Всё, что осталось от эмуляции, держится на одном: **транслятор читает описатели ресурсов
|
||||
(V#/T#/S#) и таблицы с ними из памяти игры во время разбора**. Из-за этого:
|
||||
- `DUMP_CONST_RAM` выполняет процессор (дампы читает только транслятор — это его таблицы
|
||||
описателей, видеочип их не читает ни разу);
|
||||
- DMA и `WRITE_DATA` в память не «на месте» выполняет процессор (туда приходят код шейдеров и
|
||||
описатели, которые транслятор читает сразу);
|
||||
- SRT walker (код шейдера, переведённый в x86) обходит таблицы на процессоре;
|
||||
- шейдер специализируется по полям описателей (шаг, формат, перестановка каналов, тип
|
||||
изображения), конвейер выбирается по регистрам;
|
||||
- текстуры узнаются по адресу и формату из T#.
|
||||
|
||||
Перенос на видеочип — новый режим рекомпилятора: шейдер сам читает V# (адрес через BDA-арену,
|
||||
формат и шаг — во время выполнения), изображения через bindless-массив с таблицей T# → индекс,
|
||||
барьеры из сбросов кэшей самой игры (`ACQUIRE_MEM`/`EVENT_WRITE`), резидентность всей памяти
|
||||
сразу при отображении. Это несколько дней работы с риском для картинки: не начато.
|
||||
|
||||
Остальное — не эмуляция в смысле «вместо видеочипа»: перевод GCN → SPIR-V во время игры (как у
|
||||
DXVK), синтетический vblank (HLE VideoOut), заглушки для неиспользуемых игрой пакетов
|
||||
(occlusion — фиктивный счётчик, `SET_PREDICATION`, `COND_EXEC`, `MEM_SEMAPHORE`).
|
||||
|
||||
## Шаг 4 (2026-10-08): где проходит граница между эмуляцией и трансляцией
|
||||
|
||||
Замер (`BB_RES_STATS=1`, Кошмар охотника у лампы): ~97 тыс. привязок ресурсов в секунду; почти у
|
||||
каждой (0,96 стадии на привязку) описатели лежат в таблицах в памяти, а не в регистрах: на одну
|
||||
привязку буферы — 1,3 из регистров и 2,8 из памяти, текстуры — 0,6 и 3,3, сэмплеры — 1,0 и 0,9.
|
||||
|
||||
**Описатели на видеочипе для текстур сделать правильно нельзя.** Текстура PS4 лежит в памяти в
|
||||
раскладке PS4 (tiling), которой нет в Vulkan: её нужно перевести в образ Vulkan, а для этого
|
||||
процессор должен знать T# (адрес, формат, размеры) до того, как отрисовка выполнится. Если шейдер
|
||||
будет сам читать T# из памяти и искать образ в таблице на видеочипе, то при первом использовании
|
||||
текстуры образа ещё нет — правильного запасного пути у видеочипа нет (только кадр с неверной
|
||||
текстурой). Поэтому чтение описателей процессором (SRT walker, таблицы, которые заполняет движок
|
||||
констант) — не эмуляция, а трансляция привязки ресурсов, как у DXVK и vkd3d, которые тоже читают
|
||||
привязки на процессоре и создают по ним объекты Vulkan. Для буферов описатели на видеочипе
|
||||
возможны (BDA), но выигрыш — только запись дескрипторов (~0,1 мкс на пакет), если не переделать
|
||||
резидентность и барьеры целиком; не начато.
|
||||
|
||||
Что из эмуляции на самом деле осталось, и используется ли это Bloodborne:
|
||||
|
||||
| Что | Bloodborne |
|
||||
|---|---|
|
||||
| Ожидание флипа (метка VideoOut) блокирует поток разбора | да — это синхронизация с дисплеем, как ожидание в Present у DXVK |
|
||||
| Счётчик occlusion-запросов — фиктивный | **да**, ~60 событий `ZPASS_DONE` за кадр (в счётчике пакетов их не было видно: они во вложенных буферах); переведено — «Шаг 5» |
|
||||
| `SET_PREDICATION`, `COND_EXEC`, `COPY_DATA` не транслируются | нет; переведено — «Шаг 5» |
|
||||
| `MEM_SEMAPHORE`, `REWIND` — ожидание на процессоре | нет |
|
||||
| Очереди compute (ASC) — корутины в одном потоке | нет (0 отправок за кадр) |
|
||||
| Запись процессором в память, видимую видеочипу, «объявляется» (обёртки `memcpy`, перехваты загрузчиков, кучи, аллокатора, чтение файлов) | да — для копий в VRAM и текстур; `BB_WRITE_VERIFY` на маршруте не нашёл необъявленных записей, кроме ожидаемых |
|
||||
| Шейдер копирования игры — подмена (список читается процессором) | да; теперь можно транслировать (`BB_COPY_SHADER_NATIVE=1`) |
|
||||
|
||||
Строки «нет» нужны только при переносе на другие игры (Driveclub, inFAMOUS: там асинхронный
|
||||
compute и occlusion есть) — проверить их на Bloodborne нельзя.
|
||||
|
||||
Шейдер копирования (`cs fefebf9f`): подмену можно заменить трансляцией самого шейдера. Она нашла
|
||||
настоящую опасность новой модели: рядом с тем, куда он копирует, лежат код шейдеров и таблицы
|
||||
описателей, которые транслятор читает процессором; если эти блоки уходят в VRAM («видеочип пишет
|
||||
это 3 кадра из 60»), транслятор читает устаревший код (чёрная сцена). Подмена обходила это
|
||||
случайно. Теперь записи этого шейдера всегда остаются в памяти игры. Общее правило «то, что
|
||||
видеочип пишет, но сам не читает, остаётся в памяти игры» (как readback heap) пробовалось: ломает
|
||||
производительность (видеочип 4,2 → 6,1 мс за кадр — так попадают в оперативную память и области
|
||||
размером с цели рендера), убрано.
|
||||
|
||||
Константы: копирование в кольцо при разборе было нужно, пока записи командного процессора
|
||||
выполнялись процессором «наперёд». В новой модели оно больше не нужно для правильности —
|
||||
`BB_CONSTANTS_IN_PLACE=1` читает их на месте; по умолчанию выключено (видеочип +3 % из-за PCIe,
|
||||
FPS не выше). То есть кольцо констант теперь — оптимизация (копия в VRAM), а не эмуляция.
|
||||
|
||||
Дальше по трансляции для Bloodborne делать почти нечего; дальнейшая работа — производительность
|
||||
(стадия B на слабых процессорах) и перенос: настоящие запросы occlusion (`vkCmdBeginQuery` +
|
||||
копия результата видеочипом в память игры), предикация (`VK_EXT_conditional_rendering`),
|
||||
очереди compute как очередь Vulkan с семафорами — когда появится игра, которая их использует.
|
||||
|
||||
## Шаг 5 (2026-10-08): occlusion-запросы, COPY_DATA, COND_EXEC, предикация
|
||||
|
||||
Поправка к шагам 3 и 4: Bloodborne **использует** occlusion-запросы — около 60 событий
|
||||
`ZPASS_DONE` за кадр (~14 тыс. в секунду у лампы в Кошмаре охотника). Счётчик пакетов их не
|
||||
показал: он смотрел только буферы команд верхнего уровня. До сих пор игра получала фиктивную
|
||||
последовательность «всё видно».
|
||||
|
||||
Сделано (только новая модель; `BB_OCCLUSION_QUERIES=0` — как раньше, бит 4 второго числа
|
||||
`BB_TOGGLE_FILE` — переключение на ходу):
|
||||
|
||||
- **Occlusion-запросы → запросы Vulkan** (`vk_occlusion.{h,cpp}`, `occlusion_resolve.comp`). На
|
||||
PS4 каждый блок глубины ведёт счётчик сэмплов, прошедших тест глубины; событие `ZPASS_DONE`
|
||||
записывает счётчики (16 байт между блоками, бит готовности 63), игра берёт разность «после −
|
||||
до». Здесь с первого события всегда активен запрос Vulkan; он режется на отрезки на каждом
|
||||
событии и на каждой границе, через которую запрос нельзя протянуть (начало и конец прохода
|
||||
рендера, буфер команд — `Scheduler::CarriedScope`, Suspend/Resume). Событие запоминает, сколько
|
||||
отрезков закончилось до него. В ближайшей точке вне прохода результаты отрезков копируются, и
|
||||
один запуск шейдера складывает их по порядку и пишет каждому ожидающему событию его накопленное
|
||||
значение в память игры через адрес видеочипа (BDA). Проходы рендера не разрываются; цена на
|
||||
4 ядрах — в пределах шума (220 против 219 FPS с фиктивным счётчиком, видеочип 4,54 против
|
||||
4,56 мс). Результаты появляются чуть позже, чем на PS4, до этого бит готовности снят — игра
|
||||
проверяет его, как на PS4. Счётчики всегда остаются в памяти игры (их читает процессор).
|
||||
- **COPY_DATA**: значения, известные при разборе (константа, регистр, часы), записывает видеочип
|
||||
в порядке потока, как `WRITE_DATA`; копии из памяти и GDS — копии видеочипа в порядке потока,
|
||||
как `DMA_DATA`. Запись в регистр — пока предупреждение.
|
||||
- **COND_EXEC**: условие берётся из ожидающей метки в порядке потока, иначе — после того, как
|
||||
видеочип выполнил работу до пакета (раньше читалось при разборе, впереди видеочипа).
|
||||
- **SET_PREDICATION** (ZPass, Bool64, Bool32; CONTINUE — ИЛИ с предыдущим): пропускаются
|
||||
отрисовки и запуски, у которых в заголовке стоит бит предиката. Предикат вычисляется при
|
||||
первом таком пакете, после того как видеочип выполнил работу до него (правильно, но с
|
||||
ожиданием видеочипа; Bloodborne предикацию не использует). `VK_EXT_conditional_rendering` не
|
||||
подошёл: он пропускал бы и наши внутренние запуски (детайлинг текстур, копии), и не переходит
|
||||
через границы буферов команд.
|
||||
|
||||
Проверка: `BB_PM4_SELFTEST=1` — через ~40 с игры в отправки игры подмешиваются тестовые пакеты
|
||||
(в память, выделенную самой проверкой), через несколько секунд результаты сверяются: occlusion
|
||||
по отправке с отрисовками (> 0 сэмплов) и пустой интервал (0), `COPY_DATA` (32 и 64 бита,
|
||||
память → память в порядке, часы), `COND_EXEC` (ложь пропускает запись, истина выполняет),
|
||||
предикация (ZPass и Bool32: 2 из 4 пустых предикатных отрисовок пропущены). Все 8 — PASS. Слой
|
||||
валидации Vulkan на 67 тыс. событий не нашёл нарушений правил запросов.
|
||||
|
||||
Попутно найдена и исправлена взаимоблокировка: чтение памяти для процессора из потока разбора
|
||||
(`ReadMemory`) отправляло задачу этому же потоку и ждало её.
|
||||
|
||||
Что осталось без перевода: очереди compute (ASC) по-прежнему выполняются в том же потоке, что и
|
||||
графика, и отправляются в ту же очередь Vulkan (это допустимая сериализация); `MEM_SEMAPHORE`
|
||||
меняет счётчик процессором при разборе; время видеочипа в метках и `COPY_DATA` — часы процессора.
|
||||
Bloodborne ничего из этого не использует.
|
||||
|
||||
## Дополнение 2026-10-09: copy shader в модуле памяти и GPU-метки без AMD-расширения
|
||||
|
||||
В `BB_PC_MODEL=1 BB_LAYER_MEMORY=1` оригинальный `fefebf9f` теперь исполняется по умолчанию: paged stores обновляют память игры и актуальные зеркала. HLE доступен через `BB_COPY_SHADER_NATIVE=0`.
|
||||
|
||||
Без `VK_AMD_buffer_marker` метки EOP/EOS и применимый RELEASE_MEM пишет GPU стандартными Vulkan updates с барьерами. AMD сохраняет быстрые markers; `BB_GPU_LABELS_PORTABLE=1` позволяет проверить переносимый вариант. Подробности и аппаратные ограничения: [TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md).
|
||||
|
||||
## План от 2026-10-10: что осталось и как перевести на трансляцию
|
||||
|
||||
Требование: одинаково для AMD и NVIDIA и без опоры на код именно Bloodborne (адреса функций игры,
|
||||
хеши её шейдеров) там, где модель трансляции отвечает за правильность.
|
||||
|
||||
| Что осталось | Где | Почему это эмуляция или привязка |
|
||||
|---|---|---|
|
||||
| Модель памяти 0.3 (копии в VRAM с защитой страниц, обратное чтение, `fault_manager`, BB_UFFD, предзагрузка) | **по умолчанию** | делает вместо видеочипа то, чего у ПК нет: одна память |
|
||||
| Два пути новой модели: sparse-арена (AMD) и модуль памяти (остальные) | новая модель | sparse-перепривязки — путь одного производителя (на NVIDIA держат видеочип секундами) |
|
||||
| Записи загрузчиков в текстуры и буферы видны видеочипу через перехваты кода Bloodborne (`memcpy` игры в 0x216d420/0x2171d60, аллокатор 0x26aa070, куча TLSF) | новая модель | в другой игре этих адресов нет: записи остались бы незамеченными |
|
||||
| `MEM_SEMAPHORE` меняется при разборе (впереди видеочипа), время видеочипа в метках и `COPY_DATA` — часы процессора, очереди compute — в одном потоке с графикой | командный процессор | Bloodborne не использует; нужно для других игр |
|
||||
| CE и SRT walker читают описатели на процессоре, кэш текстур по адресу, синтетический vblank | — | трансляция привязок и платформенный слой, как у DXVK/vkd3d (шаг 4): остаётся |
|
||||
|
||||
Этапы:
|
||||
|
||||
1. **Один путь памяти для всех GPU.** Модуль памяти — по умолчанию и на AMD (на маршруте 2026-10-09
|
||||
вровень со sparse-ареной: p99 5,1–7,5 мс против 5,2–6,0). Sparse-арена — запасной вариант
|
||||
`BB_LAYER_MEMORY=0`. Заодно на AMD по умолчанию исполняется сам шейдер копирования игры вместо
|
||||
подмены по его хешу.
|
||||
2. **Согласование памяти без перехватов игры.** Записи процессора в память, которую видеочип
|
||||
держит как образ или копию, ловятся общим механизмом (ловушки записи по 64 КиБ, как у модуля),
|
||||
перехваты Bloodborne остаются ускорением (меньше срабатываний ловушек). Проверка: прогон с
|
||||
`BB_GUEST_HOOKS=0` и `BB_WRITE_VERIFY=1` (необъявленные записи), картинка и время кадра.
|
||||
3. **Остатки командного процессора — на видеочип.** `MEM_SEMAPHORE`: сигнал — запись видеочипа в
|
||||
порядке потока; время видеочипа — метки времени Vulkan, переведённые в часы PS4 (100 МГц) и
|
||||
записанные видеочипом. Проверка — `BB_PM4_SELFTEST` (Bloodborne их не использует). Очереди
|
||||
compute как отдельная очередь Vulkan — после, когда будет игра для проверки.
|
||||
4. **Новая модель по умолчанию** (после логов с NVIDIA на 0.5-pre4), затем удаление модели 0.3 и
|
||||
sparse-арены.
|
||||
|
||||
### Ход работ (2026-10-10)
|
||||
|
||||
- **Этап 1 сделан:** модуль памяти по умолчанию на всех GPU (на AMD тоже; sparse-арена —
|
||||
`BB_LAYER_MEMORY=0`), 0 вызовов `vkQueueBindSparse`.
|
||||
- **Модульность:** профили игр `gpu/games/` (`game_profile.h`, `bloodborne.cpp`): перехваты кода
|
||||
игры, её код, пишущий данные каждый кадр, хеш шейдера копирования — только там. Профиль
|
||||
выбирается по TITLE_ID; `BB_GAME_PROFILE=none` — игра как незнакомая, `BB_GUEST_HOOKS=0` — без
|
||||
перехватов.
|
||||
- **Этап 2, запись:** общий учёт ловушек записи в рантайме (`runtime_memory_trap`, причины по
|
||||
страницам; `gpu/layer/bblayer_write_traps.h`): копии модуля в VRAM и текстуры ловят записи на
|
||||
одних страницах, не мешая друг другу; `mprotect` игры ловушки сохраняет, новое отображение
|
||||
начинается без них (тест в `tests/test_runtime.c`). Страницы под текстурами под ловушками по
|
||||
умолчанию (`BB_IMAGE_TRAPS=0` — как раньше), записи libc и перехваченных загрузчиков
|
||||
объявляются до записи (без срабатываний). На маршруте с профилем — как без ловушек (4,18 мс
|
||||
видеочипа), 4–29 срабатываний за 5 с.
|
||||
- **Найдено попутно — разброс «некоторые запуски в 1,3–1,7 раза медленнее»:** «летучие» блоки
|
||||
модуля (блоки, в которые пишет процессор, в общей копии диапазона в VRAM) обновлялись копией
|
||||
перед каждой привязкой на чтение; копия разрывает проход рендера, и объёмы света (по отрисовке на
|
||||
каждый) рвали свой проход до ~100 раз за кадр (`BB_PASS_BREAK_TRACE=1`: 92–109 тыс. разрывов за
|
||||
5 с из `Runtime::CopyBuffer` против ~700). Летучие блоки выключены по умолчанию
|
||||
(`BB_LAYER_VOLATILE=1` — включить; тогда чтения внутри открытого прохода идут на месте): 10 из 10
|
||||
прогонов быстрые. Порядок «результаты occlusion до последующих меток»
|
||||
(`OcclusionQueries::WriteBeforeLabel`) оставлен как поведение PS4, но причиной не был: игра читает
|
||||
результаты через 2–10 мс и находит их записанными (`BB_OCCLUSION_READ_TRACE=1`, ловушки на
|
||||
чтение в рантайме — причины от 16 делают страницу без доступа).
|
||||
- **Незнакомая игра (этап 2, чтение):** прежний вывод «без профиля — снова медленный режим» был
|
||||
тем же разбросом из-за летучих блоков (один прогон). Без профиля маршрут идёт так же быстро
|
||||
(3 из 3, 4,29–4,30 мс) и выглядит так же. Ловушки на чтение для данных, которые есть только в
|
||||
VRAM (копия обратно при чтении процессором), остаются нужны для правильности в других играх;
|
||||
механизм причин «без доступа» в рантайме уже есть и проверен тестом.
|
||||
- **Этап 3 сделан (кроме отдельной очереди compute):**
|
||||
- `MEM_SEMAPHORE`: сигнал — в порядке потока, после завершения работы видеочипа перед ним
|
||||
(`RunMemSemaphoreSignal`, как метки EOP); ожидание сбрасывает конвейер записи и отправляет
|
||||
записанное, как `WAIT_REG_MEM`. `BB_MEM_SEMAPHORE_ORDER=0` — сигнал при разборе (как раньше).
|
||||
- Часы видеочипа: у PS4 100 МГц; теперь одна шкала для процессора и видеочипа — монотонные часы
|
||||
хоста в тиках по 10 нс (`GetGpuClock64`, раньше — настенные наносекунды). Метки EOP/RELEASE_MEM
|
||||
с часами и `COPY_DATA` из часов пишет сам видеочип: метка времени Vulkan → копия из запроса →
|
||||
короткий шейдер (`gpu_timestamp.comp`) переводит в шкалу PS4 и пишет в память игры
|
||||
(`vk_timestamps.cpp`). Привязка тиков к часам хоста — один раз, на простаивающем видеочипе
|
||||
(на RX 7800 XT точность 152 мкс). `BB_GPU_TIMESTAMPS=0` — пишет процессор.
|
||||
- Проверка — `BB_PM4_SELFTEST=1`: 11 из 11; новые проверки «сигнал не раньше предшествующей
|
||||
метки» (со старым порядком — FAIL), «ожидание и списание», «часы 100 МГц в шкале процессора»
|
||||
(2 с между метками: 1,961–1,999 с). Bloodborne ни то, ни другое не использует.
|
||||
- **Этап 2, чтение — готово, включается `BB_LAYER_READ_TRAPS=1`:** то, что видеочип записал только в
|
||||
копию в VRAM (запись привязки в зеркало), под ловушкой «без доступа» (причина `VramData` в
|
||||
рантайме); первое касание процессора (игра, транслятор, libc рантайма) копирует данные блока
|
||||
назад через очередь обратного чтения (`FastReadback`, как в модели 0.3: ждёт только тик этой
|
||||
записи) и снимает ловушку. Запись в файл из такой страницы сначала её читает (`touch_for_read`),
|
||||
чтение файла в неё — уже касалось (`touch_for_write`). На маршруте: 60 МиБ под ловушками, 0–1
|
||||
касание за 10 с, время видеочипа то же (4,28–4,30 мс). Bloodborne такие данные процессором не
|
||||
читает, поэтому само копирование назад здесь не проверено — по умолчанию выключено до игры,
|
||||
которой это нужно.
|
||||
- **Пересмотрено перед 0.5 (вечер 2026-10-10):** на AMD по умолчанию снова sparse-арена
|
||||
(`BB_LAYER_MEMORY=1` — модуль). Модуль шёл вровень с ареной на маршруте в Ярнаме, но в Сне
|
||||
охотника держал вне VRAM ~1 ГБ данных области (диапазоны с несколькими «неизвестными» блоками
|
||||
без летучих блоков остаются на месте): 100 FPS против 154 с ареной; с летучими блоками — 120, но
|
||||
с ~225 тыс. срабатываний ловушек в секунду. Нужен доступ по блокам для смешанных диапазонов.
|
||||
Occlusion-запросы как запросы Vulkan — по выбору (`BB_OCCLUSION_QUERIES=1`): с апскейлером
|
||||
(сцена меньше и со сдвигом) свечение лампы вспыхивало серым на весь кадр; счёт нужно сделать
|
||||
устойчивым к этому. Фоновая компиляция больше не пропускает полноэкранные проходы (серые кадры).
|
||||
@@ -0,0 +1,775 @@
|
||||
# Environment variables
|
||||
|
||||
The port reads a few hundred `BB_*` environment variables. Some are for players (upscaler, frame
|
||||
rate, controls); most are diagnostics and developer switches. The list below is every one the code
|
||||
reads, from the port's own registry (`gpu/shim/bb_env_vars.def`); `tests/test_env_vars.py` fails
|
||||
when the code and the registry disagree, so it stays complete. Everything from "Every `BB_*`
|
||||
variable" on is the output of `python3 scripts/env_vars.py --doc` (without its title); the sections
|
||||
above it are written by hand.
|
||||
|
||||
## How to set them
|
||||
|
||||
**In the launcher:** *Advanced → Developer → Extra variables (NAME=value, space-separated)*. The
|
||||
settings key is `extra_env` in the launcher's `settings.json`, read by `game_environment()` in
|
||||
`launcher/bbport_launcher.py`:
|
||||
|
||||
- The field is split on whitespace, and each piece at its **first** `=`:
|
||||
`BB_UPSCALER=fsr4 BB_AUDIO_GAIN=1.5 RADV_DEBUG=nohiz`.
|
||||
- **There is no quoting.** A value cannot contain spaces, and quotes stay part of the value. Use
|
||||
separators the variable itself accepts, such as `;` or `,`.
|
||||
- A piece without `=` is ignored. If a name appears twice, the last one wins.
|
||||
- Any name is accepted, third-party ones included (`RADV_DEBUG`, `MANGOHUD`, `VK_*`, `__GL_*`).
|
||||
- The result goes to `run.sh`. Both the launcher's *Play* button and `--play` (a Steam shortcut,
|
||||
the Steam Deck's Game Mode) use it.
|
||||
|
||||
**From a terminal (source checkout):** `BB_GAME_DIR=/path/to/CUSA03173 BB_FPS=60 bash run.sh`.
|
||||
Without the launcher, nothing sets the launcher's variables. `run.sh` then uses its own defaults
|
||||
(game folder `../CUSA03173`, uncapped frame rate, and so on).
|
||||
|
||||
**From a terminal (AppImage):** `BB_UPSCALER=fsr4 ./Bloodborne-bbport-x86_64.AppImage --play`
|
||||
starts the game with the launcher's saved settings. Mind the precedence below: the launcher's own
|
||||
settings replace a variable of the same name from the shell. Extra variables replace both. The
|
||||
AppImage keeps its data (bbport.ini, saves, logs) in `BB_DATA_DIR`, by default
|
||||
`~/.local/share/bbport`.
|
||||
|
||||
**bbport.ini:** many graphics options live in `bbport.ini` and the in-game menu, not in the
|
||||
environment. A few variables override those keys at start: `BB_UPSCALER`, `BB_UPSCALE_PRESET`,
|
||||
`BB_FSR_SHARPNESS`, `BB_JITTER`, `BB_REACTIVE*` and `BB_OBJECT_MOTION`
|
||||
(`gpu/shim/bbport_settings.cpp`). Values set this way stay in force until the next start.
|
||||
|
||||
## Precedence
|
||||
|
||||
The environment is built in this order; a later step wins:
|
||||
|
||||
1. The environment the launcher (or the AppImage) was started with.
|
||||
2. The launcher's own settings (`game_environment()`): some are always written (the game folder,
|
||||
mods, languages, frame rate, fullscreen and the like) and replace a shell value; others only
|
||||
when the setting is on.
|
||||
3. The launcher's **Extra variables**.
|
||||
4. `run.sh`, which fills in what is still unset (the data folder, the online module, Discord).
|
||||
|
||||
Every `BB_*` variable the port reads, by kind, from `gpu/shim/bb_env_vars.def`
|
||||
(`scripts/env_vars.py --doc`). The game prints the variables set
|
||||
at its start (`Environment:` in the log) and warns about `BB_*` names it does not know.
|
||||
|
||||
`=1: ...` / `=0: ...` / `=N: ...` say what that value does; `set: ...` means any value
|
||||
turns it on (even `0`).
|
||||
|
||||
## Settings (107)
|
||||
|
||||
What players and the launchers set. The menu, bbport.ini and the launcher write most of them; set one by hand to override them.
|
||||
|
||||
| Variable | What it does |
|
||||
|---|---|
|
||||
| `BB_ANISO` | =N: anisotropic filtering of scene textures (default 16) |
|
||||
| `BB_ASYNC_PIPELINES` | =0: new pipelines of passes drawn every frame compiled at once instead of skipping a draw for a frame or two |
|
||||
| `BB_ATLAS_ENABLED` | =1 / 0: Hunter's Atlas, the in-game map, on or off (the launcher's Map switch; unset: the map's own settings.ini) |
|
||||
| `BB_AUTO_RENDER_RES` | =1: render resolution picked from the measured frame rate |
|
||||
| `BB_BBSC` | =1: seamless co-op (BBSC), the launcher's co-op switch |
|
||||
| `BB_BBSC_LOCAL_SERVER` | =1 / auto (default) / 0: the BBSC game server on this PC (scripts/bbsc_gameserver) |
|
||||
| `BB_BUTTON_PROMPTS` | =0: the game's PlayStation button icons always |
|
||||
| `BB_CJK_FONT` | =path: a font file with Chinese characters for the menu in Simplified Chinese (default: fontconfig's choice for zh-cn) |
|
||||
| `BB_COMMUNITY_SERVER` | URL of the online pool server (default: the community server) |
|
||||
| `BB_CONFIG` | =path: the controls file (default bbport.ini) |
|
||||
| `BB_CONTENT_SKU` | =full / ...: the content profile run.py prepares |
|
||||
| `BB_CURSOR` | =0: the system's mouse cursor instead of the port's |
|
||||
| `BB_DECK_DESKTOP_INPUT` | =1: keep keyboard and mouse on a Steam Deck in game mode |
|
||||
| `BB_DISCORD` | =0: no Discord Rich Presence (the launcher's Discord switch) |
|
||||
| `BB_DISPLAY` | =N: the monitor to open the window on |
|
||||
| `BB_DLC_DIR` | =path: the dumped add-ons of this title, one folder each, linked as <user>/addcont/<title id> (the launcher's DLC folder) |
|
||||
| `BB_DLSS` | =0: no DLSS even on NVIDIA RTX |
|
||||
| `BB_DMEM_EXTRA_MB` | =N (0-2048, default 512): direct memory beyond the PS4's for the port's extras |
|
||||
| `BB_DMEM_MB` | =N: the game's direct memory size in MiB (PS4: 5056; raised for high resolutions and large heaps) |
|
||||
| `BB_EVDEV_TOUCHPAD` | =0: no DualSense/DualShock touchpad through evdev |
|
||||
| `BB_FFX_DIR` | the folder with AMD's FSR DLLs for Vulkan, amd_fidelityfx_loader_vk.dll and amd_fidelityfx_upscaler_vk.dll (upscaler fsr_amd; default <user>/fsr, then beside the game) |
|
||||
| `BB_FFX_FG_ML` | =1: AMD's ML frame generation (4.0.x) accepts any GPU (AMD ships it for RDNA4); =0: refuses it (FSR 3.1) |
|
||||
| `BB_FFX_FG_VERSION` | =<part of a name>: the frame generation AMD's DLL runs, from those the log lists (default: theirs) |
|
||||
| `BB_FFX_VERSION` | =<part of a name>: the upscaler AMD's FSR DLLs run, from those the log lists (default: theirs) |
|
||||
| `BB_FPS` | =uncap (default) / 90 / 60 / 30: the frame rate (the launcher's) |
|
||||
| `BB_FPS_LIMIT` | =N: frame rate limit (the launcher's) |
|
||||
| `BB_FRAME_GEN` | =fsr3 / dlss / fsr_amd (1: fsr3): frame generation |
|
||||
| `BB_FRAME_GEN_MULTIPLIER` | =2..4: frames shown per rendered one with frame generation |
|
||||
| `BB_FRAME_STATS` | set: frame statistics in the log every 5 s and slow frames (the launcher's statistics switch) |
|
||||
| `BB_FSR411_DIR` | the FSR 4.1.1 assets folder (default fsr4_411) |
|
||||
| `BB_FSR411_VARIANT` | =int8 / fp8 / fp8emu / navi2 / lite: FSR 4.1.1 model variant (default: the menu's model) |
|
||||
| `BB_FSR4_DIR` | the FSR 4 v07 assets folder (default fsr4_shaders) |
|
||||
| `BB_FSR_SHARPNESS` | =0..1: RCAS sharpening (the menu's) |
|
||||
| `BB_FULLSCREEN` | =0 / 1 / exclusive: window mode |
|
||||
| `BB_GAMEPAD` | =SDL GUID or part of the name: the controller to use (the launcher's) |
|
||||
| `BB_GAME_DIR` | the game's folder (with eboot.bin) |
|
||||
| `BB_GAME_MENU` | =0: no port pages in the game's System menu |
|
||||
| `BB_GESTURES_NO_MOTION` | =1: the gestures menu without the motion-control icons (Dommo's mod; the launcher's switch) |
|
||||
| `BB_GPU_ID` | =N: the GPU to use, by Vulkan index (the launcher's) |
|
||||
| `BB_GPU_PROFILE` | =1: GPU profiler: time per labelled pass in the log (the launcher's) |
|
||||
| `BB_HDR` | =1: HDR output allowed |
|
||||
| `BB_HUD_ANCHOR` | =0: on wide screens the HUD stays in the 16:9 middle |
|
||||
| `BB_INTEL_TONEMAP_FIX` | =0 / 1: Intel CPU tone mapping fix patch off / on (default on on Intel) |
|
||||
| `BB_JITTER` | =0 / 1: upscaler camera jitter (the menu's) |
|
||||
| `BB_JUMP_GAP_MS` | =N (default 70): pause around a keyboard jump while running |
|
||||
| `BB_JUMP_TAP_MS` | =N (default 70): press length of a keyboard jump while running |
|
||||
| `BB_KEYBOARD` | =0: local co-op, this game ignores the keyboard and the mouse |
|
||||
| `BB_KEY_FALL_MS` | =N (default 45): keyboard stick release time |
|
||||
| `BB_KEY_RISE_MS` | =N (default 70): keyboard stick rise time |
|
||||
| `BB_KEY_TURN_DPS` | =N (default 1100): keyboard stick turn speed in degrees a second |
|
||||
| `BB_LANGUAGE` | =N: the game's language, PS4 number (1 English, 8 Russian; the launcher's) |
|
||||
| `BB_LARGE_HEAPS` | =1 / 0: larger graphics heaps for texture packs on / off (default: detected) |
|
||||
| `BB_LAYER_MEMORY` | =1 / 0: the layer's memory module (default on non-AMD GPUs) |
|
||||
| `BB_LOW_LATENCY` | =off / on / boost: NVIDIA Reflex / AMD Anti-Lag (the menu's) |
|
||||
| `BB_MAP_ART_DIR` | folder: the map's pictures (the launcher's) |
|
||||
| `BB_MAP_CATALOG` | file: the names of the map's places and items (the launcher's) |
|
||||
| `BB_MAP_CONFIG` | file: the map's settings (the launcher's) |
|
||||
| `BB_MAP_DIR` | folder: the maps Hunter's Atlas generated (the launcher's) |
|
||||
| `BB_MAP_FOG_ENABLED` | =1: areas not visited yet stay hidden on the map |
|
||||
| `BB_MAP_FONT` | file: the font of the map's names (the launcher's) |
|
||||
| `BB_MAP_ITEMS_ENABLED` | =1: items on the map (the launcher's) |
|
||||
| `BB_MAP_ITEM_CATEGORIES` | =list: the items the map shows (healing,insight,equipment,echoes) |
|
||||
| `BB_MAP_NPC_MODE` | =all / new_dialogue / ...: the NPCs the map shows |
|
||||
| `BB_MAP_SOUND_GAIN` | =0-2: the level of the map's cursor sounds (0.024, the game's own quick-menu cursor) |
|
||||
| `BB_MAP_TRAVEL_ENABLED` | =1: travel to lamps from the map (the launcher's) |
|
||||
| `BB_MENU_LANGUAGE` | =ru / en / ...: the port's menu language (default: the launcher's, else the system's) |
|
||||
| `BB_MENU_POINTER` | =0: no mouse pointer in the game's menus |
|
||||
| `BB_MODS_ENABLED` | =0: no mods |
|
||||
| `BB_MOUSE_AUTO_ROTATE` | =1: the game's camera auto rotation also with the mouse |
|
||||
| `BB_MOUSE_DEADZONE` | =N (default 16 of 127): mouse look dead zone |
|
||||
| `BB_NATIVE_MOUSE` | =0: mouse look through the right stick instead of the camera hook |
|
||||
| `BB_NET_BACKEND` | =thd: The Hunter's Dream online pool (the launcher's online pool) |
|
||||
| `BB_OBJECT_MOTION` | =0 / 1: motion vectors of moving objects (the menu's) |
|
||||
| `BB_ONLINE` | =1: online play (the launcher's) |
|
||||
| `BB_P2P_PORT` | =N: the UDP port for P2P online traffic (default 3658, 0: any) |
|
||||
| `BB_PAD_BACKGROUND` | =1: local co-op, the controller keeps driving the game and its sound plays while another window has the focus |
|
||||
| `BB_PAD_DEADZONE` | =N (default 5): stick inner dead zone |
|
||||
| `BB_PAD_DEADZONE_OUTER` | =N (default 127): stick outer limit |
|
||||
| `BB_PAD_LIGHTBAR` | =RRGGBB: the light bar colour of DualShock 4 and DualSense pads (the launcher's Controls) |
|
||||
| `BB_PAD_MOTION` | =0 / 1: motion sensor emulation (over bbport.ini pad_motion) |
|
||||
| `BB_PATCHES` | =name,...: extra patches by name (patches.py) |
|
||||
| `BB_PROCESS_PRIORITY` | =normal / above / high: process priority on Windows (default above) |
|
||||
| `BB_REACTIVE` | =1: FSR reactive mask (the menu's) |
|
||||
| `BB_REACTIVE_MAX` | reactive mask maximum (the menu's) |
|
||||
| `BB_REACTIVE_SCALE` | reactive mask scale (the menu's) |
|
||||
| `BB_REACTIVE_THRESHOLD` | reactive mask threshold (the menu's) |
|
||||
| `BB_RENDER_RES` | =WxH: render resolution through the startup patch (compatibility) |
|
||||
| `BB_SAVE_LOG` | =1: the log and frame statistics also kept in logs/ |
|
||||
| `BB_SEAMLESSHUNT_TOMBS` | =1: keep Seamless Hunt's bloodstains and death visions (they crash the game; scripts/online.py) |
|
||||
| `BB_SHADER_PROGRESS` | =0: no shader compilation progress on screen |
|
||||
| `BB_SHADNET_NPID` | the shadNet account name (the launcher's) |
|
||||
| `BB_SHADNET_PASSWORD` | the shadNet account password (the launcher's) |
|
||||
| `BB_SHADNET_SERVER` | the shadNet server |
|
||||
| `BB_SHADNET_TOKEN` | the shadNet account token (the launcher's) |
|
||||
| `BB_SHADNET_WEBAPI` | the shadNet web API server |
|
||||
| `BB_SKIP_CPU_CHECK` | =1: skip the check of the processor's instructions (AVX, BMI1, LZCNT...) before the game starts |
|
||||
| `BB_SKIP_GAME_CHECK` | =1: start with game files that did not pass the check |
|
||||
| `BB_STEAM_INPUT_X11` | =0: keep the native Wayland window when Steam made a virtual controller for the game (default: Xwayland, so Steam Input reaches it) |
|
||||
| `BB_TEXTURE_QUALITY` | texture quality over bbport.ini's texture_quality |
|
||||
| `BB_THD_ONLINE_ID` | The Hunter's Dream online name (the launcher's) |
|
||||
| `BB_THD_TOKEN` | The Hunter's Dream account token (the launcher's) |
|
||||
| `BB_TROPHY_KEY` | the trophy data key (from the environment or the port's .env) |
|
||||
| `BB_UPNP` | =1: UPnP port forwarding for online play |
|
||||
| `BB_UPSCALER` | =none / fsr3 / fsr4 / dlss / ...: the upscaler over the menu's (scripts, A/B) |
|
||||
| `BB_UPSCALE_PRESET` | the upscaler quality preset (the menu's) |
|
||||
| `BB_USER_NAME` | the PS4 user name (default Hunter) |
|
||||
| `BB_WINDOW_RECT` | =x,y,w,h: local co-op, the window's place and size as fractions of its display, without borders |
|
||||
|
||||
## Tuning (235)
|
||||
|
||||
Defaults to change on a machine where they misbehave, workarounds, and switches that turn a fix off for comparison.
|
||||
|
||||
| Variable | What it does |
|
||||
|---|---|
|
||||
| `BB_ADB` | path of adb for BB_APP0_ADB (default: adb) |
|
||||
| `BB_ANTI_LAG` | =1: AMD Anti-Lag 2 through Mesa's layer (measured ~0 gain, #289) |
|
||||
| `BB_APP0_ADB` | =serial: the game's files fetched from a phone over adb on first open |
|
||||
| `BB_ASYNC_PIPELINE_THREADS` | =N: threads compiling pipelines in the background |
|
||||
| `BB_ASYNC_SUBMIT` | =0: the presenter submits the frame itself instead of a submit thread |
|
||||
| `BB_AUDIO` | =none: no audio device (headless runs) |
|
||||
| `BB_AUDIO_GAIN` | =N (0-8, default 1): the game's sound level on top of the fold to the device's channels |
|
||||
| `BB_AUTO_NOHIZ` | =0 (run.sh): no automatic RADV_DEBUG=nohiz on the GPUs that need it |
|
||||
| `BB_AVPLAYER_ASYNC_READY` | =1: movie StateReady on the controller thread, as before (crashed at 200+ FPS) |
|
||||
| `BB_BDA_DEDICATED_MB` | =N: buffers with GPU addresses get dedicated memory from N MiB |
|
||||
| `BB_BIND_MEMO` | =0: every resource binding takes the full path (no memo) |
|
||||
| `BB_BREADCRUMBS` | =0: no GPU breadcrumbs (what the GPU was drawing at a hang) |
|
||||
| `BB_CHUNK_TEXTURES` | =0: texture data copied instead of read in place from guest memory chunks |
|
||||
| `BB_CHUNK_UPLOADS` | =0: uploads through CPU staging instead of binding guest memory in place |
|
||||
| `BB_COMMIT_IMPORT_LOCKED` | =1: guest memory commits import under the lock |
|
||||
| `BB_COMPUTE_RELEASE_FLUSH` | =0: work before a compute release waits for the rest of the frame |
|
||||
| `BB_CONSTANTS_IN_PLACE` | =1: shader constants bound in place from guest memory instead of copied |
|
||||
| `BB_CONSTANT_RING_MB` | =N: size of the constant ring in MiB |
|
||||
| `BB_CONSTRAM_GPU_WAIT` | =0: a constant RAM dump does not wait for queued draws reading it (DoF artifacts #44) |
|
||||
| `BB_CONSTRAM_PIPE` | =0: a constant RAM dump waits for all queued draws |
|
||||
| `BB_COPY_DEST_GAP` | =bytes: merge gap of copy shader destinations (default 256) |
|
||||
| `BB_COPY_DEST_PRECISE` | =0: the whole bound destination of a copy shader counts as written |
|
||||
| `BB_COPY_FILTER_GATE` | =1 with BB_COPY_SKIP_SAME: the copy filter off while the draw recording thread is over 92% busy |
|
||||
| `BB_COPY_GPU_BUFFERS` | =0: command buffers decoded from guest memory instead of a copy per submission |
|
||||
| `BB_COPY_MERGE_KB` | =N: copy shader ranges closer than N KiB merged (default 64) |
|
||||
| `BB_COPY_PAGED_RANGES` | =0: copy shader ranges whole instead of by page |
|
||||
| `BB_COPY_SHADER_NATIVE` | =0: the copy shader's list read on the CPU |
|
||||
| `BB_COPY_SKIP_SAME` | =0: off (default on): of the game's copy shader calls only the copies that change their destinations run (checked on thread bb:CopyFilter) |
|
||||
| `BB_COPY_THREADS` | =N (0-16): threads for large guest memory copies |
|
||||
| `BB_CORE_RESERVE` | =0 / 1: keep the game's threads off a reserved core (Linux, SMT CPUs with 6+ cores) |
|
||||
| `BB_CRASH_GUARDS` | =0: without the guards of the game's own unchecked pointers that crashed players (scripts/crash_guards.py) |
|
||||
| `BB_DEAD_IMAGES` | =0: images of freed memory left to the collector instead of dropped on the GPU thread |
|
||||
| `BB_DEAD_IMAGES_LATE` | set: dead images dropped at the frame's end only, as before |
|
||||
| `BB_DETILE_SKIPPED` | =1: texture levels the quality setting leaves out are still detiled and read |
|
||||
| `BB_DIRECT_IMPORT_LOCKED` | =1: direct memory imports under the lock |
|
||||
| `BB_DIRECT_MEMORY_ACCESS` | =1: shadPS4's direct memory access mode of the shader translator |
|
||||
| `BB_DIRECT_SEGMENT` | =0: the presenter waits for the recording threads instead of taking its own command segment |
|
||||
| `BB_DISABLE_HTTPS` | =1: the game's HTTPS requests go out as plain HTTP (online) |
|
||||
| `BB_DRAW_PIPE` | =0 / N: the draw pipeline off, or its depth |
|
||||
| `BB_FAST_WRITER_FAULTS` | =1: fast path for guest write faults (off: a device loss on a GTX 1050) |
|
||||
| `BB_FAULT_WINDOW` | =KiB: window unprotected around a guest write fault (default 256) |
|
||||
| `BB_FFX_OWN_SHADERS` | =0: FSR frame generation with the packaged shaders instead of the port's own |
|
||||
| `BB_FG_ASYNC` | =1: FSR 3.1 frame generation on a second compute queue (no gain measured) |
|
||||
| `BB_FG_EARLY_RETIRE` | =0: with frame generation the frame counts as finished only after it |
|
||||
| `BB_FIXED_ARENA` | =1 / 0: force the fixed arena on or off (tests) |
|
||||
| `BB_FIX_CHARA_ID` | =0: without the character id guard fix |
|
||||
| `BB_FIX_HIT_COLLECTOR` | =0: without the hit collector overflow fix |
|
||||
| `BB_FIX_PLAY_TIME` | =0: without the play time fix at high frame rates |
|
||||
| `BB_FLIP_AHEAD` | =N (default 1, 0 off): the buffer shown before a flip goes back to the game that many flips early |
|
||||
| `BB_FLIP_RATE` | =N: the flip rate the game asks for replaced (0: every vblank; games without frame rate hooks) |
|
||||
| `BB_FRAMES_AHEAD` | =N (default 1): guest frames the command processor may run ahead of the GPU |
|
||||
| `BB_FRAME_HOOKS` | =0: the community's frame rate patches instead of the port's hooks |
|
||||
| `BB_FSR3_FP32` | =1: FSR 3.1 without its FP16 shader variants (#85) |
|
||||
| `BB_FSR3_WAVE32` | =1: FSR 3.1 without its wave64 shader variants (#85) |
|
||||
| `BB_FSR4_LINEAR` | =0: FSR 4 gets the encoded frame as before |
|
||||
| `BB_FSR4_OPT` | =0: FSR 4 v07 passes without the optimized set (opt/) |
|
||||
| `BB_FX_INIT_CLEAR` | =0: the effect buffers' initialization clears through the game's path |
|
||||
| `BB_GARLIC_VRAM` | =0 / 1: VRAM copies of the game's GPU memory off / on (default: on discrete GPUs) |
|
||||
| `BB_GC_BUDGET_MB` | =N: texture cache budget in MiB, as on integrated GPUs |
|
||||
| `BB_GC_IDLE_SECONDS` | =N (default 20): unused textures evicted after N seconds |
|
||||
| `BB_GC_KEEP_GPU_WRITTEN` | =0: the collector downloads and frees linear images the GPU wrote under mere memory pressure; default only past the critical mark and unused for a minute (#30) |
|
||||
| `BB_GC_LIVE_BUDGET` | =0: texture cache marks from the startup budget instead of the driver's current one |
|
||||
| `BB_GC_PRESSURE_IDLE_SECONDS` | =N (default 5): the same under memory pressure |
|
||||
| `BB_GC_SMALL_MARKS` | =P,C: pressure and critical marks in percent on small cards |
|
||||
| `BB_GPU_COMMAND_WRITES` | =0: GPU command writes not done in place |
|
||||
| `BB_GPU_DATA_CHECK` | =0: GPU-written data not checked before CPU reads (#122) |
|
||||
| `BB_GPU_LABELS` | =1 / 0: GPU-written labels on / off (default off on AMD GFX8) |
|
||||
| `BB_GPU_LABELS_PORTABLE` | =1: portable GPU label writes |
|
||||
| `BB_GPU_SPIN_US` | =N: microseconds the GPU thread spins before sleeping (default 200 with 12+ threads) |
|
||||
| `BB_GPU_TIMESTAMPS` | =0: timestamps written by the CPU instead of the GPU |
|
||||
| `BB_GPU_WRITES_IN_PLACE` | =1: GPU-written buffers in place with write tracking, as before |
|
||||
| `BB_GPU_WRITE_TWINS` | =1: VRAM twins of GPU-written guest pages |
|
||||
| `BB_GPU_WRITE_TWINS_MAX` | =N: pages with at most N GPU-written ranges get twins (default 1024) |
|
||||
| `BB_GUEST_HOOKS` | =0: no guest code hooks at all (aspect ratio, HUD, mouse, fixes) |
|
||||
| `BB_GUEST_LAZY` | =1: lazy guest memory with the layer's memory module (off: broke buffers) |
|
||||
| `BB_GUEST_MEMORY` | =dmabuf / host / private: how guest memory is shared with the GPU (default per vendor) |
|
||||
| `BB_GUEST_UNCACHED` | =1: guest memory only in memory types the GPU reads past its caches |
|
||||
| `BB_GUEST_VRAM` | =1 (experiment): direct memory in VRAM mapped through the PCI BAR |
|
||||
| `BB_GUEST_VRAM_RANGES` | =lo-hi,...: physical ranges for BB_GUEST_VRAM (default: the graphics heaps) |
|
||||
| `BB_GX_COMMAND_HEAP` | =0: without the larger GX command heap patch (patches.py) |
|
||||
| `BB_GX_QUERY_GUARD` | =0: without the GX query creation guard |
|
||||
| `BB_HEAP_GROW` | =0: the graphics heap whole from the start instead of growing on demand |
|
||||
| `BB_HEAP_GROW_AHEAD` | =N (default 2, 0 off): heap pieces made ahead |
|
||||
| `BB_HEAP_GROW_INITIAL_MB` | =N (default 512): heap memory backed from the start |
|
||||
| `BB_HEAP_GROW_PIECE_MB` | =N (default 320, at least 258): heap growth piece |
|
||||
| `BB_HEAP_SHRINK` | =0: the heap only grows |
|
||||
| `BB_HEAP_SHRINK_DELAY` | =seconds (default 60): a piece free this long is given back |
|
||||
| `BB_HEAP_TOP_HOOK` | =0: without the hook on allocations from the heap's top |
|
||||
| `BB_HIT_COLLECTOR_CAP` | =N (default 65536): hits per call before the overflow fix takes over |
|
||||
| `BB_HONEST_DRAIN_FLUSH` | =0: no flush when the GPU is on its last submission |
|
||||
| `BB_HONEST_FLIPS` | =1 (experiment): a flip completes once the GPU has finished the frame |
|
||||
| `BB_HONEST_FLUSH_US` | =N (default 2000): submissions batched over N microseconds while the GPU has work |
|
||||
| `BB_HONEST_IDLE_BACKLOG` | =bytes (default 64 KiB): queued draws that make an idle GPU get a flush |
|
||||
| `BB_HONEST_IDLE_FLUSH` | =0: no flush for an idle GPU |
|
||||
| `BB_HONEST_IDLE_US` | =N (default 200): microseconds of GPU idle before the guest may be waiting |
|
||||
| `BB_HOST_COPY_WAITS` | =all: host copies always wait, as before |
|
||||
| `BB_HOST_ONLY_DEFAULT` | =0: none of the game profile's allocations in host memory only |
|
||||
| `BB_HOT_EFFECTS` | =0: effect settings applied as patches at start instead of live |
|
||||
| `BB_HOT_PAGES` | =1: hot page tracking |
|
||||
| `BB_HOT_VERTEX_COPY` | =1: vertex data bound in place copied into VRAM once per submission (no reads over the bus per pass) |
|
||||
| `BB_IMAGE_RECYCLE_MB` | =N (default 256, 0 off): memory of freed images kept for reuse |
|
||||
| `BB_IMAGE_RECYCLE_SECONDS` | =N (default 90): recycled images older than this go |
|
||||
| `BB_IMAGE_SPARES` | =1 / 0: spare images made ahead on a thread (default on on Windows) |
|
||||
| `BB_IMAGE_SPARES_MB` | =N: memory for spare images |
|
||||
| `BB_IMAGE_TRAPS` | =0: no write traps on image memory |
|
||||
| `BB_IMPORT_GRANULE_MB` | =N (2..64, default 32): lazy guest memory import granule |
|
||||
| `BB_IMPORT_OVERLAP_MB` | =N: overlap budget of lazy imports |
|
||||
| `BB_INDIRECT_GUARD` | =0: indirect draw and dispatch counts used as the game gives them |
|
||||
| `BB_INDIRECT_GUARD_MAX` | =N (default 4M): indirect dispatch groups allowed in all |
|
||||
| `BB_INDIRECT_GUARD_MAX_VERTICES` | =N (default 64M): indirect draws over this run nothing |
|
||||
| `BB_INPLACE_TEXTURES` | =1: all texture data read in place (off: looked smeared) |
|
||||
| `BB_INTERP_INT_FIX` | =0 / 1: flat integer interpolation fix off / on (default: on where barycentrics exist) |
|
||||
| `BB_JOB_SLOT_FIX` | =0: without the render job slot leak fix |
|
||||
| `BB_KEEP_DECOMPRESS_DRAWS` | =1: the game's depth decompress passes are drawn |
|
||||
| `BB_KERNEL_CLAMP` | =0: without the effect kernel clamp fix |
|
||||
| `BB_KIT_RESTORE` | =0: the online kit's code changes are not restored |
|
||||
| `BB_LAYER_CPU_MARK_SECONDS` | =N (default 5, 0: until handed out): CPU marks of layer blocks expire after N seconds |
|
||||
| `BB_LAYER_DOMAINS` | =0: mirrors not per game allocation |
|
||||
| `BB_LAYER_GPU_WRITES` | =0 / 1 / 2: where GPU writes may land in VRAM copies (0 nowhere, 1 default) |
|
||||
| `BB_LAYER_MIRROR_FLOOR_PERCENT` | =N (max 90): VRAM budget share kept for mirrors |
|
||||
| `BB_LAYER_MIRROR_GROWTH_MB` | =N (default 64, 0 none): room a growing mirror gets |
|
||||
| `BB_LAYER_MIRROR_HEADROOM_MB` | =N (0: the whole floor): mirror headroom |
|
||||
| `BB_LAYER_READ_TRAPS` | =1: data the GPU wrote into a VRAM copy only is trapped for CPU reads |
|
||||
| `BB_LAYER_RELOAD_RESET` | =0: blocks reloaded in place keep their state, as before |
|
||||
| `BB_LAYER_STITCH` | =0: bindings across heap pieces from the arena, as before (exploding vertices) |
|
||||
| `BB_LAYER_UNMAP_RESET` | =0: unmapped blocks keep their state, as before |
|
||||
| `BB_LAYER_VOLATILE` | =1: volatile blocks |
|
||||
| `BB_LAYER_VOLATILE_IN_PASS` | =0: volatile blocks refreshed in render passes too, as before |
|
||||
| `BB_LAYER_WATCH` | =0: blocks with unannounced bytes get no watched VRAM copy |
|
||||
| `BB_LAYER_WRITE_TRAPS` | =0: no write traps on layer blocks |
|
||||
| `BB_LIGHT_GRID_HOOK` | =0: without the light grid hook |
|
||||
| `BB_LIGHT_GRID_OPT` | =1 / 0: light grid optimization on / off, over the setting (tests) |
|
||||
| `BB_LIVE_HYBRID` | =1: the startup resolution patch with live presets over it |
|
||||
| `BB_MEM_SEMAPHORE_ORDER` | =0: memory semaphores without ordering |
|
||||
| `BB_MIRROR_ALLOC_PERCENT` | =N (0 off): a mirror that would take device memory past N percent is refused |
|
||||
| `BB_MIRROR_COMPACT_AGE` | =N (default 30): mirrors younger than N seconds are left alone |
|
||||
| `BB_MIRROR_COMPACT_MB` | =N (default 64): mirror memory without valid blocks over this is given back |
|
||||
| `BB_MIRROR_COMPACT_SECONDS` | =N (default 1): mirror compaction interval |
|
||||
| `BB_MIRROR_FREE_BACKLOG_MB` | =N (default 128): retired mirror memory allowed to wait |
|
||||
| `BB_MIRROR_FREE_MB_PER_FRAME` | =N (default 64): mirror memory freed per frame |
|
||||
| `BB_MIRROR_MERGE_GROWTH` | =0: a mirror made over neighbours grows at the top only, as before |
|
||||
| `BB_MIRROR_POOL_MB` | =N: retired mirrors kept for new ones |
|
||||
| `BB_MIRROR_REPROMOTE_SECONDS` | =N (default 0): blocks sent back are not copied again that soon |
|
||||
| `BB_MIRROR_UNBOUND_SECONDS` | =N (0 never): blocks no binding named for N seconds leave VRAM |
|
||||
| `BB_MIRROR_VRAM_ONLY` | =1: mirrors in VRAM or not at all |
|
||||
| `BB_MOTION_LARGE` | =0: bone palettes of 16-64 KiB (the large beasts) get camera motion vectors only, not object motion |
|
||||
| `BB_MOTION_VERTICES_MI` | =N (1-8): object motion vertex buffer in Mi vertices |
|
||||
| `BB_NET_MODULE` | =path: the online module to load (default gpu/libbbnet20.so beside bb-probe) |
|
||||
| `BB_NP_COUNTRY` | =code: the online user's country (default us) |
|
||||
| `BB_NP_LANGUAGE` | =code: the online user's language (default en) |
|
||||
| `BB_NVIDIA_EGL` | =0: the launcher leaves the EGL vendor files as they are (#107) |
|
||||
| `BB_NVIDIA_LIB_DIR` | =dir: extra folder with NVIDIA's libraries for the package |
|
||||
| `BB_NVIDIA_MAILBOX` | set: keep Mailbox presentation on NVIDIA Windows instead of FIFO |
|
||||
| `BB_NV_POWER` | =0: leave the NVIDIA driver profile of bb-probe.exe alone (Windows; by default its power management mode is set to prefer maximum performance unless the user chose one) |
|
||||
| `BB_OCCLUSION_INTERVALS` | =0: occlusion samples counted all the time, not only while a query is open |
|
||||
| `BB_OCCLUSION_LABEL_ORDER` | =0: occlusion labels may go ahead |
|
||||
| `BB_OCCLUSION_QUERIES` | =0: a fixed occlusion result sequence instead of real queries |
|
||||
| `BB_OF_FAST` | =1: optical flow without a search on the finest level |
|
||||
| `BB_PACE_CATCHUP` | =1: frame pacing catches up after a late frame |
|
||||
| `BB_PAD_CENTER` | =lx,ly,rx,ry: the sticks' neutral position |
|
||||
| `BB_PAD_CENTER_CAL` | =0: no measurement of the sticks' neutral position |
|
||||
| `BB_PAD_DEADZONE_RADIAL` | =0: the sticks' dead zone per axis instead of round |
|
||||
| `BB_PAD_WORLD_DRAWS` | =N (default 150): a frame with this many draws counts as the world for pad timing |
|
||||
| `BB_PAGED_CROSSING` | =0: paged bindings only in windows |
|
||||
| `BB_PAGED_FORMATTED` | =0: formatted buffers never paged |
|
||||
| `BB_PAGED_HOLES` | =1 (experiment): paged reads skip memory the game never wrote |
|
||||
| `BB_PAGED_MIN_MB` | =N (default 64): bindings from this size are paged |
|
||||
| `BB_PENDING_SUMMARY` | =0: pending checks without the summary level |
|
||||
| `BB_PIPELINE_CACHE` | =0: no pipeline cache on disk |
|
||||
| `BB_PIPE_SIGNALS` | =0: the frame's cache work on the GPU thread instead of the recording thread |
|
||||
| `BB_PIPE_SPIN_US` | =N: microseconds the draw pipe's threads spin before sleeping |
|
||||
| `BB_PM4_RECOVER` | =0: a command buffer the game rewrote while read stops decoding, as before |
|
||||
| `BB_PM4_REPAIR` | =0: corrupt submitted command buffers decoded as they are |
|
||||
| `BB_PM4_WAIT_US` | =N (default 20000): command processor wait timeout in microseconds |
|
||||
| `BB_PRELOAD_BACKGROUND` | =0: cached pipelines loaded before the game starts instead of in the background |
|
||||
| `BB_PREP_WORKERS` | =N (0-16): draw preparation threads |
|
||||
| `BB_PRESENT_MODE` | =Mailbox / Fifo / Immediate: swapchain present mode (default Mailbox) |
|
||||
| `BB_PRESENT_THREAD` | =0: present on the vblank thread instead of its own |
|
||||
| `BB_PROMOTE_MB_PER_FRAME` | =N (0 none; default 32 on cards up to 4 GiB, else 256): VRAM copies per frame |
|
||||
| `BB_READBACKS` | =0 / 1 / 2: GPU readbacks off / relaxed (default) / precise |
|
||||
| `BB_READBACK_LINEAR` | =1: linear images read back to guest memory |
|
||||
| `BB_READBACK_QUEUE` | =0: no separate queue for readbacks |
|
||||
| `BB_REGS_FULL_RESET` | set: the whole register file reset, as before |
|
||||
| `BB_RENDER_RESOURCE_CACHE_MB` | =N (160: the game's own): the game's render resource cache size |
|
||||
| `BB_REPAIRED_DRAWS` | =1: repaired command buffers still draw |
|
||||
| `BB_REPAIRED_WAIT_MS` | =N (default 500): waits of repaired command buffers give up after this |
|
||||
| `BB_RESOLVE_WORKERS` | =N (0-4): threads that check and copy draw resources (stage 2) |
|
||||
| `BB_RING_DEDUP` | =0: off (default on): constants already in the constant ring with the same bytes bound there again, not copied over the bus once more |
|
||||
| `BB_SCENE_DEPTH_PROXIES` | =0: depth images read at the native size, not as reduced proxies |
|
||||
| `BB_SCENE_STENCIL_ZERO` | =0: no depth proxies where stencil export is missing, the scene stays native |
|
||||
| `BB_SCENE_UPSIZE` | =0: a scene larger than native drawn at the native size |
|
||||
| `BB_SCENE_WITHOUT_UI` | =0: cinematics without a UI pass are not finished as scenes |
|
||||
| `BB_SHADOWS` | =1: shadow copies of written buffers on discrete GPUs |
|
||||
| `BB_SKIP_EMPTY_DRAWS` | =1 (experiment): draws that write nothing are left out |
|
||||
| `BB_SKIP_NOOP_DRAWS` | =1: draws without effect left out (#352, no gain measured) |
|
||||
| `BB_SOUND_HACK` | =0: saves left untouched (no sound settings fix) |
|
||||
| `BB_SPEC_MATCH` | =0: shader specializations always built and compared |
|
||||
| `BB_STAGE_PRIORITY` | =0 (default) / 1 / 2: draw stage thread priority off / above normal / highest |
|
||||
| `BB_STAGING_KEEP_MB` | =N (default 512): upload staging kept when idle |
|
||||
| `BB_STAGING_VRAM_IDLE_MS` | =N (default 3000): VRAM staging given back after this long unused |
|
||||
| `BB_STREAM_BUFFER_MB` | =N: stream buffer size |
|
||||
| `BB_SUBMIT_AHEAD` | =N (default 2): a frame's first submissions go straight to the GPU |
|
||||
| `BB_SUBMIT_LOCK` | =frame: a frame's last submission decoded under the lock |
|
||||
| `BB_SWAPCHAIN_IMAGES` | =N: swapchain images |
|
||||
| `BB_TEXTURE_HELPER` | =0 / 1: spinning texture helper thread (default: with cores to spare) |
|
||||
| `BB_TEXTURE_UPLOAD_MB_PER_FRAME` | =N (default 32, 0 off): texture bytes uploaded a frame before new textures start small |
|
||||
| `BB_THD_AUTH` | The Hunter's Dream account service URL |
|
||||
| `BB_THD_P2P_ADDR` | our address for The Hunter's Dream peers (default: detected) |
|
||||
| `BB_THD_RULESET` | The Hunter's Dream ruleset (default vanilla) |
|
||||
| `BB_THD_SERVER` | The Hunter's Dream session service URL |
|
||||
| `BB_THD_STUN` | =host[:port] / off: STUN server (default: the server's host on 3478) |
|
||||
| `BB_THD_VERIFY_TLS` | =0: The Hunter's Dream TLS certificates not checked |
|
||||
| `BB_TILE_BIND_VRAM` | =0: tiled data read in place over the bus instead of from a VRAM copy |
|
||||
| `BB_TIMELINE_POLL_US` | =N (default 50): timeline semaphore poll interval |
|
||||
| `BB_UNIFIED_MEMORY` | =1 / 0: treat the GPU as unified memory or not (over the device type) |
|
||||
| `BB_VBAO_BLUR` | =N (default 2, 0: raw): VBAO denoise taps per side |
|
||||
| `BB_VBAO_BLUR_DEPTH` | =f (default 0.05): relative depth difference that stops the VBAO blur |
|
||||
| `BB_VBAO_BLUR_NORMAL` | =f (default 8, 0 ignores normals): how sharply the VBAO blur stops at normal changes |
|
||||
| `BB_VBAO_NORMALS` | =0: VBAO from depth only |
|
||||
| `BB_VBAO_POWER` | =f (default 1.5): VBAO strength |
|
||||
| `BB_VBAO_RADIUS` | =metres (default 1.5): VBAO radius |
|
||||
| `BB_VBAO_THICKNESS` | =metres (default 0.4): VBAO assumed thickness |
|
||||
| `BB_VERTEX_READ_COPY` | =1 (experimental): vertex data bound in place copied into VRAM for the vertices each draw reads (once per guest submission) |
|
||||
| `BB_VK_RECORD_THREAD` | =0: Vulkan commands recorded on the calling thread |
|
||||
| `BB_VK_RECORD_THREADS` | =N (1-8): Vulkan recording threads (default 2 from 8 hardware threads) |
|
||||
| `BB_VK_SEGMENT_KB` | =N (default 64): recording segment size |
|
||||
| `BB_VMA_BLOCK_MB` | =N (default 64): VMA block size |
|
||||
| `BB_VMA_DEDICATED_MB` | =N (default 4): images from this size get dedicated memory |
|
||||
| `BB_VRAM_FLOOR_IDLE_SECONDS` | =N (default 30): mirror floor idle time before textures make room |
|
||||
| `BB_VRAM_IDLE_SECONDS` | =N (default 60): VRAM copies unused this long are given back |
|
||||
| `BB_VRAM_LIMIT_MB` | =N: the driver's VRAM budget taken as at most N MiB |
|
||||
| `BB_VRAM_PRESSURE_IDLE_SECONDS` | =N (default 10): under pressure, VRAM groups unused this long go |
|
||||
| `BB_VRAM_PROMOTE_PERCENT` | =N (default 90): no new VRAM copies past this share of the budget |
|
||||
| `BB_VULKANINFO` | path of vulkaninfo for the launcher |
|
||||
| `BB_WORK_RETIRED` | =0: the recording thread and fences do not wait for retired work |
|
||||
|
||||
## Diagnostics (282)
|
||||
|
||||
Logs, dumps, traces, probes and experiments, for finding bugs. Most cost frame time; none is meant for playing.
|
||||
|
||||
| Variable | What it does |
|
||||
|---|---|
|
||||
| `BB_ARENA_VRAM_ONLY` | =1 (tests): the arena in VRAM only, as NVIDIA's sparse buffers |
|
||||
| `BB_ATLAS_DEBUG` | =1: Hunter's Atlas logs what its game-state readers find |
|
||||
| `BB_ATLAS_QA` | =1: Hunter's Atlas's test commands (BB_MAP_TEST_CONTROL) may also send mouse, key and resize events |
|
||||
| `BB_ATLAS_QA_DEVICE` | =controller / keyboard: with BB_ATLAS_QA, the prompts Hunter's Atlas shows |
|
||||
| `BB_AUDIO_DUMP` | =dir: raw converted PCM per audio port |
|
||||
| `BB_AUDIO_STATS` | set: audio port statistics |
|
||||
| `BB_AUDIO_TRACE` | =1: Ajm instance configurations and jobs that fail |
|
||||
| `BB_AVPLAYER_TRACE` | =1: the game's movie player calls with results and times |
|
||||
| `BB_BARRIER_STATS` | =1: images whose subresource states cost the most |
|
||||
| `BB_BBSC_MEMSCAN` | set: BBSC finds its sites by scanning memory instead of the known addresses |
|
||||
| `BB_BBSC_PROBE` | =address: BBSC probe of that guest address |
|
||||
| `BB_BDA_LOG` | set: GPU addresses of buffer sources, to place a GPUVM fault |
|
||||
| `BB_BIG_ALLOC_STACKS` | =1: call stacks of device memory allocations of 64 MiB and more |
|
||||
| `BB_BREADCRUMB_READS` | =1 / 0: breadcrumbs of T# and V# reads everywhere / nowhere (default: NVIDIA only) |
|
||||
| `BB_BUFFER_STATS` | =1: how many frames behind the GPU is when a frame starts |
|
||||
| `BB_CAMERA_FRAME_LOG` | set: projection of every G-buffer pass's camera with its GPU frame |
|
||||
| `BB_CAMERA_INVERSE` | =own: motion vectors from the inverse of the view matrix instead of the game's stored one |
|
||||
| `BB_CAMERA_LOG` | set: camera position every 100 ms |
|
||||
| `BB_CAMERA_Y` | =up / down: force the camera's y direction for motion vectors |
|
||||
| `BB_CAMLOG` | set: camera yaw when it changes (#325) |
|
||||
| `BB_CAPTURE_DIR` | folder for frame captures (BB_CAPTURE_TRIGGER) |
|
||||
| `BB_CAPTURE_TRIGGER` | =file: creating that file records the next frame for the frame analyzer |
|
||||
| `BB_CE_STATS` | =1: what the constant engine's dumps feed |
|
||||
| `BB_CE_WAIT_LOG` | set: constant engine waits that were not satisfied |
|
||||
| `BB_CE_WATCH` | =hex address: constant RAM dumps and WRITE_DATA packets over that address |
|
||||
| `BB_CLOTH_STIFFNESS` | =value: cloth stiffness bound at any frame rate |
|
||||
| `BB_COMPILE_TRACE` | =1: every shader and pipeline compiled during the session |
|
||||
| `BB_CONSTRAM_WAIT_LOG` | set: constant RAM dumps that waited for the GPU, sampled |
|
||||
| `BB_COPY_SHADER_TRACE` | set: the copy shader's buffers every 2 s |
|
||||
| `BB_COPY_WHERE` | =1: the copy shader's sources and destinations per frame, VRAM or in place (log lines) |
|
||||
| `BB_DCB_STATS` | =1: structure of the graphics command buffers |
|
||||
| `BB_DEBUG_MOTION` | =1: motion vectors blended over the picture |
|
||||
| `BB_DISPATCH_LOG` | =1: the shape of each kind of compute dispatch, once |
|
||||
| `BB_DISPATCH_START_LOG` | set: dispatches with a nonzero thread group start |
|
||||
| `BB_DISPLAY_WATCH` | set: every draw into a display buffer with its sizes and state |
|
||||
| `BB_DISPLAY_WATCH_TARGET` | =hex address: also every draw into that color target |
|
||||
| `BB_DLSSG_CAMERA_MOTION` | =0: DLSS-G told the vectors have no camera motion |
|
||||
| `BB_DLSSG_DEPTH_INVERTED` | =1: DLSS-G told the depth is the other way round |
|
||||
| `BB_DLSSG_HUDLESS` | =0: DLSS-G gets no hudless frame |
|
||||
| `BB_DLSSG_MATRICES` | =0: DLSS-G gets no camera matrices |
|
||||
| `BB_DLSSG_MVEC_SCALE` | =f: DLSS-G motion vector scale (default 1) |
|
||||
| `BB_DLSSG_PREFILL` | =0: DLSS-G outputs not filled with the real frame first (black frames when it skips) |
|
||||
| `BB_DLSSG_SUBRECTS` | =0: DLSS-G gets no input sub-rectangles |
|
||||
| `BB_DRAW_WATCH` | =1: draws whose viewport, targets or textures differ from the previous frame's |
|
||||
| `BB_DRAW_WATCH_TEXTURES` | set: BB_DRAW_WATCH also lists bound textures |
|
||||
| `BB_DUMP_DIR` | =dir: where upscaler dumps go (default out/dump) |
|
||||
| `BB_DUMP_EVERY` | set: dump every frame while the trigger file exists |
|
||||
| `BB_DUMP_FRAMES` | =N: frames per upscaler dump (default 8) |
|
||||
| `BB_DUMP_GOT` | =offset: which host function a guest GOT slot calls |
|
||||
| `BB_DUMP_ONLY` | =name,name: dump only those upscaler images |
|
||||
| `BB_DUMP_SHADERS` | =1: translated shader programs written to the dump folder |
|
||||
| `BB_DUMP_TRIGGER` | =file: creating it dumps the upscaler's inputs and outputs |
|
||||
| `BB_DYNAMIC_STATE_ALWAYS` | =1: all dynamic state set again at every draw |
|
||||
| `BB_DYNSTATE_CHECK` | =1: the scissor each command buffer really has, and who set it |
|
||||
| `BB_EFFECT_DUMP` | set: the game's effect options objects every 10 s |
|
||||
| `BB_EXPERIMENT_CYCLE` | =bits,bits,...:seconds: the experiment bits step through the list, over and over (test builds) |
|
||||
| `BB_EXP_BARRIER_ALL` | set (experiment): a full memory barrier before every draw and dispatch |
|
||||
| `BB_EXP_BARRIER_TARGET` | =hex address (experiment): a full barrier before each draw into that target |
|
||||
| `BB_EXP_CONSTRAM_FINISH` | =1 (experiment): every constant RAM dump waits for all earlier GPU work |
|
||||
| `BB_EXP_COPY_SRC_SNAPSHOT` | set (experiment): copy shader sources snapshotted as with the old memory model |
|
||||
| `BB_EXP_CPU_READS_GPU_WAIT` | set (experiment): CPU-side reads also wait for the GPU, as before |
|
||||
| `BB_EXP_DMA_UNTRACKED` | set (experiment): stages reading memory through pointers count as unlisted reads |
|
||||
| `BB_EXP_EXTRA_BREAKS` | =N (experiment): the render pass ended before every Nth draw, to measure breaks |
|
||||
| `BB_EXP_FINISH_BEFORE_TARGET` | =hex address (experiment): the GPU finishes everything before a draw into that target |
|
||||
| `BB_EXP_FINISH_FRAMES` | set (experiment): the GPU finishes each frame before the next |
|
||||
| `BB_EXP_FLIP_FINISH` | set (experiment): the flip is signalled once the GPU has run the frame |
|
||||
| `BB_EXP_FLIP_LAG` | set (experiment): at most one frame in flight |
|
||||
| `BB_EXP_NOTED_GPU_WAIT` | set (experiment): constant RAM waits cover every read noted on stage A, as before |
|
||||
| `BB_EXP_NO_MEMORY_HOOKS` | set (experiment): no guest hooks for GPU allocations and copies |
|
||||
| `BB_EXP_NO_STREAM` | set (experiment): nothing goes through the stream buffer |
|
||||
| `BB_EXP_PAGED_IN_PLACE` | =1 (experiment): paged buffers bound in place |
|
||||
| `BB_EXP_TRANSIT_TARGET` | =hex address (experiment): that target goes through TransferSrcOptimal |
|
||||
| `BB_FAULT_WAIT` | =1: a fatal fault or a GPU library assertion leaves the process alive for a debugger (tools/debug_attach.sh) |
|
||||
| `BB_FENCE_DELAY_TEST` | =1 (test): every 200th fence comes 80 ms late |
|
||||
| `BB_FFX_FG_DEBUG` | =1: AMD's frame generation draws its tear lines and reset marks |
|
||||
| `BB_FG_DUMP_TRIGGER` | =file: creating it saves real and generated frames, raw |
|
||||
| `BB_FG_LUT` | =0: generated frames without the game's display LUT |
|
||||
| `BB_FG_PROFILE` | =1: GPU time of each FSR 3.1 frame generation pass |
|
||||
| `BB_FILE_TRACE` | =1: every guest path the game opens or checks (mods, missing files) |
|
||||
| `BB_FINAL_DUMP_TRIGGER` | =file: creating it saves the presented frame |
|
||||
| `BB_FI_CHECK` | =1: frame interpolation samples drawn as their difference to the SDK's |
|
||||
| `BB_FPS_PRINT` | =1: the present rate every 5 s, without BB_FRAME_STATS's cost |
|
||||
| `BB_FRAME_DT_SMOOTH` | =a (0..1): the game's frame step follows the measured frame time smoothed by a |
|
||||
| `BB_FRAME_GEN_ONLY` | set: only the generated frames are shown |
|
||||
| `BB_FRAME_LOG` | =path (with BB_FRAME_STATS): one line per flip |
|
||||
| `BB_FREE_CHECK` | =1: guest heap corruption diagnostic around the free list |
|
||||
| `BB_FSR411_POSTPASS` | =lds / orig: FSR 4.1.1 post pass variant |
|
||||
| `BB_FSR4_MODEL_CYCLE` | =N: the FSR 4 model switched to the next one every N frames |
|
||||
| `BB_FSR4_PROFILE` | =1: FSR 4.1.1 GPU time per pass every 300 frames |
|
||||
| `BB_FSR4_STATS` | =1: driver statistics of every FSR 4 pass (fsr4-bench --stats) |
|
||||
| `BB_GAME_ASSERT_STOP` | =1: the game's known harmless asserts stop it, as before |
|
||||
| `BB_GAME_HUGE` | =0 (tests): no huge pages for the game's image |
|
||||
| `BB_GAME_MENU_TRACE` | =1: how often the game asks for the port's menu texts |
|
||||
| `BB_GAME_PROFILE` | =none: no game profile (hooks, fixes), the plain runtime |
|
||||
| `BB_GARLIC_VRAM_RANGE` | =begin:end: VRAM copies only for guest addresses in that range |
|
||||
| `BB_GC_TRIGGER_MB` | =N: the texture collector starts at N MiB (tests) |
|
||||
| `BB_GNM_CENSUS` | =1: calls per Gnm function, printed |
|
||||
| `BB_GNM_CENSUS_FILE` | =path: every Gnm function's total calls written there |
|
||||
| `BB_GNM_CENSUS_RANGE` | =first:last: census only for those table indices (bisecting) |
|
||||
| `BB_GNM_NATIVE_ONLY` | =1: native Gnm observers without checksum analysis |
|
||||
| `BB_GNM_OBSERVE` | =1: submitted command buffers checked against the hooked API calls' packets |
|
||||
| `BB_GNM_RACE_STACKS` | =1: call stacks of racing Gnm command buffer writers |
|
||||
| `BB_GNM_TRACE` | =offset[:count]: arguments, caller and packets of that Gnm function |
|
||||
| `BB_GNM_WRITES_TRACE` | =1: recent Gnm builders overlapping a bad submitted buffer |
|
||||
| `BB_GPU_LOG` | =trace / debug / info / warning / error: GPU layer log level |
|
||||
| `BB_GPU_PROFILE_DRAWS` | =1: the GPU profiler times every draw |
|
||||
| `BB_GPU_PROFILE_TOP` | =N: the GPU profiler prints the N heaviest labels (default 30) |
|
||||
| `BB_GPU_WRITE_WATCH` | =hex address: GPU writes noted over the 64 KiB page holding it |
|
||||
| `BB_GUEST_HOOK_TRAPS` | =1: guest hooks stay int3 traps instead of native detours |
|
||||
| `BB_GUEST_WHOLE_CHUNKS` | =1 (tests): as if offset mappings failed (the NVIDIA path) |
|
||||
| `BB_GX_QUERY_FAIL` | =N: every Nth GX query creation fails as with a full heap |
|
||||
| `BB_HEAP_DUMP` | =addr,...: those guest objects and the ones they point to |
|
||||
| `BB_HEAP_FIND` | =hex value: once, 60 s in, where the game's data holds that value |
|
||||
| `BB_HEAP_HOOKS` | =0 (bisecting): the heap's ranges not reported |
|
||||
| `BB_HEAP_SITES` | =1: the game's malloc replacement sites |
|
||||
| `BB_HEAP_TRACE` | =1: which 16 MiB ranges the game's GPU allocators hand out, every 60 s |
|
||||
| `BB_HEAP_WALK` | =core,...: walk of the game's range allocator cores |
|
||||
| `BB_HLE_OFF` | =mask (experiment): 1 image copy, 2 metadata clear, 4 image clear run as real shaders |
|
||||
| `BB_HLE_STATS` | set: how often the compute shader substitutions fire |
|
||||
| `BB_HOST_ONLY` | =phys,... (experiment): those direct allocations in host memory only |
|
||||
| `BB_HUD_DX` | =f: another HUD shift |
|
||||
| `BB_IMAGE_CLEAR_LOG` | =1: why an image clear went to the real shader, once per reason |
|
||||
| `BB_IMAGE_COPY_LOG` | set: also why a large copy shader was not substituted |
|
||||
| `BB_IMAGE_SYNC_LOG` | set: images refreshed most per 5 s, with their flags |
|
||||
| `BB_IMAGE_WATCH` | =address: what happens to the image at that address |
|
||||
| `BB_IME_TEXT` | =text: the on-screen keyboard answers with that text at once (tests) |
|
||||
| `BB_IMITATE_NVIDIA` | =1 (tests on AMD): the port treats this GPU as NVIDIA's |
|
||||
| `BB_IMPORT_LOG` | set: lazy import requests per 10 s |
|
||||
| `BB_IMPORT_TRACE` | =lo-hi (MiB): who asked for imports starting there |
|
||||
| `BB_IMPORT_WRITTEN_BUDGET` | =1 (tests): written bindings under the import budget too, as before |
|
||||
| `BB_INPLACE_READ_CHECK` | =1: checks of the GPU's in-place reads of game memory |
|
||||
| `BB_LABEL_TRAP` | =1 / 2: with BB_FREE_CHECK, read-only label pages first |
|
||||
| `BB_LANG_TABLES` | set: the sell regions' language tables (#269) |
|
||||
| `BB_LAYER_CHECK` | =1: stop on a stale page table entry of the layer |
|
||||
| `BB_LAYER_MARK_LOG` | =lo-hi (hex): who marks layer blocks there as the CPU's |
|
||||
| `BB_LAYER_PAGED` | =0 (tests): huge bindings cut in place instead of the page table |
|
||||
| `BB_LAYER_PAGED_STATS` | set: paged bindings per shader every 5 s |
|
||||
| `BB_LAYER_PROBE` | =1: GPU writes kept in place because the CPU once wrote the block |
|
||||
| `BB_LAYER_VERIFY` | =1 / 2: verify the layer's mirrors (2: also those a paged binding reads) |
|
||||
| `BB_LAYER_WATCH_HOT` | =1 (experiment): often written blocks watched and uploaded after every write |
|
||||
| `BB_LAYER_WRITE_LOG` | set: large GPU writes kept in place, and the images near them |
|
||||
| `BB_LIST_UNRESOLVED` | =1 (porting another game): every unresolved import, once |
|
||||
| `BB_LOW_INFO_TEST` | set: test of the memory queries for the game's statics (#282) |
|
||||
| `BB_MAP_TEST_CONTROL` | =file: one command at a time for Hunter's Atlas's automated tests (open, close, zoom-in, area <id>, ...), removed once read |
|
||||
| `BB_MAP_TEST_TRACE` | =file: with BB_MAP_TEST_CONTROL, Hunter's Atlas writes its state there as JSON |
|
||||
| `BB_MEMORY_LOCK_REPORT_MS` | =N (default 20, 0 off): memory map lock holds longer than this, with the code |
|
||||
| `BB_MENU_KEYS_FILE` | =file (scripted tests): menu key tokens read from it |
|
||||
| `BB_MENU_POINTER_FILE` | =file (tests): menu pointer moves and clicks read from it |
|
||||
| `BB_MENU_POINTER_LOG` | =1: the signals of the in-menu check, with every change |
|
||||
| `BB_META_FIXED_BASE` | set: shader prediction from metadata at a fixed program base |
|
||||
| `BB_META_NO_SYNTH` | set: shader prediction without synthesized inputs |
|
||||
| `BB_META_RECIPE_FETCH` | set: shader prediction without generated fetch shaders |
|
||||
| `BB_MIRROR_TRACE` | set: mirror creation and growth |
|
||||
| `BB_MOD_TRACE` | =1: every file a mod replaces |
|
||||
| `BB_MOTION_SELECT_LOG` | =1: each G-buffer vertex shader once with the motion vector selection |
|
||||
| `BB_MOTION_TRACE` | =file: the frame after the file appears prints its motion draws |
|
||||
| `BB_MOVBE_TRAP` | =file: the movbe at the image offsets listed there (tools/movbe_sites.py) made ud2 and run by the port, as on a processor without MOVBE |
|
||||
| `BB_MSG_TRACE` | =1: the game's text lookups per 2 s by category (#255) |
|
||||
| `BB_NGX_LOG` | =1 / 2: NVIDIA NGX log into the port's (2: verbose) |
|
||||
| `BB_NULL_GPU` | =1: no GPU work at all (tests) |
|
||||
| `BB_OBJECT_MOTION_ALL` | =1: the old broad object motion selection (A/B) |
|
||||
| `BB_OCCLUSION_READ_TRACE` | =1: the game's occlusion result reads |
|
||||
| `BB_OCCLUSION_SAMPLES` | =N: samples the game read, for N queries |
|
||||
| `BB_OCCLUSION_TRACE` | =N: the first N occlusion events with frame and render pass |
|
||||
| `BB_OVERLAY_OPEN` | set (tests): the port's menu open from the start, unfolded |
|
||||
| `BB_OVERLAY_TRACE` | =1: the menu's mouse buttons as SDL and ImGui see them |
|
||||
| `BB_PAD_CLOCK` | =wall: pad timestamps from the wall clock, as before |
|
||||
| `BB_PAD_FILE` | =file: scripted pad input for automated runs |
|
||||
| `BB_PAD_RECORD` | =file: F9 starts and stops recording the pad state into it |
|
||||
| `BB_PAD_REPLAY` | =file: plays a BB_PAD_RECORD recording |
|
||||
| `BB_PAD_TAP_TEST` | =ms: the right stick fully right for that long (#325) |
|
||||
| `BB_PAD_TRACE` | =1 / 2: gamepad buttons and touches as read (2: every sample) |
|
||||
| `BB_PAGED_COPY_CROSSING` | =0 (tests): copy programs paged for size alone are not paged |
|
||||
| `BB_PAGED_VERIFY` | =1: verify every block a paged binding reads |
|
||||
| `BB_PARTIAL_TRACE` | set: partial image refreshes |
|
||||
| `BB_PASS_BREAK_TRACE` | =1: what ended each render pass |
|
||||
| `BB_PASS_DUMP` | =addr,...: rows of those render targets at each display pass (with BB_PASS_DUMP_TRIGGER) |
|
||||
| `BB_PASS_DUMP_AT` | =hex address: BB_PASS_DUMP's images also before the first draw into that target |
|
||||
| `BB_PASS_DUMP_STEP` | =N (default 8): BB_PASS_DUMP every Nth row |
|
||||
| `BB_PASS_DUMP_TRIGGER` | =file: creating it starts BB_PASS_DUMP |
|
||||
| `BB_PENDING_STATS` | set: pending notes and granules a second, by level |
|
||||
| `BB_PIN_CLEAR_LOG` | set: large releases of GPU-pinned memory that were cleared |
|
||||
| `BB_PIPE_VERIFY` | =N: verify the draw pipe's results every Nth frame |
|
||||
| `BB_PM4_SELFTEST` | =1: tests of command processor packets Bloodborne never sends |
|
||||
| `BB_PM4_SELFTEST_STEPS` | =mask: the self test steps to run |
|
||||
| `BB_POOL_WATCH` | =1: watch of the game's pool corruption (#32) |
|
||||
| `BB_PRESENT_DUMP_COUNT` | =N: BB_PRESENT_DUMP_TRIGGER saves N consecutive frames |
|
||||
| `BB_PRESENT_DUMP_TRIGGER` | =file: creating it saves the presented frame with the UI |
|
||||
| `BB_PRESET_FILE` | =file: 'preset [output [upscaler [aspect]]]' read about once a second (scripted tests) |
|
||||
| `BB_PRODUCER_CHECK` | =1: command producer checks (the launcher's crash diagnostics) |
|
||||
| `BB_PROMPT_TRACE` | =1: key guide icon textures found for button prompts |
|
||||
| `BB_READBACK_LOG` | =path: the newest GPU-written bindings behind readbacks |
|
||||
| `BB_READ_SAMPLE` | =spec: sampling of which guest memory the CPU reads |
|
||||
| `BB_RECORDER_TRACE` | =1: most frequent command buffer callers every 2000 calls |
|
||||
| `BB_REGS_CHECK` | set: registers compared with the defaults after a reset |
|
||||
| `BB_RESIDENCY_MIX` | =1 / 2: bindings per kind every 2 s |
|
||||
| `BB_RESIDENCY_REPORT` | =file: when it appears, blocks bound in place and why they have no VRAM copy |
|
||||
| `BB_RESIDENCY_VRAM_LIMIT_MB` | =N (tests): pretend VRAM ends at N MiB |
|
||||
| `BB_RES_STATS` | =1: where bound shaders read their resource descriptors from |
|
||||
| `BB_RE_ADVANCE` | =1: probe of Scaleform's MovieImpl::Advance |
|
||||
| `BB_RE_ANALOG` | =1: probe of the analog getter |
|
||||
| `BB_RE_CAMERA` | =1: camera probe |
|
||||
| `BB_RE_CAMSTICK` | =1: probe of the camera stick getter |
|
||||
| `BB_RE_CAM_ADD` | =radians: turns the camera by that much (probe) |
|
||||
| `BB_RE_ENTRY` | =offset,...: entry probes at those guest offsets |
|
||||
| `BB_RE_GX_QUERY` | =1: first GX query creations and guard events with their thread |
|
||||
| `BB_RE_VIEWPORT` | =1: the game's viewport calls |
|
||||
| `BB_RE_WATCH` | =offset: hardware watchpoint on that guest offset |
|
||||
| `BB_RE_WATCH_ADDR` | =address: hardware watchpoint on that address |
|
||||
| `BB_RE_WATCH_AT` | =N (default 3000): watchpoint set after that many pad reads |
|
||||
| `BB_RE_WATCH_CAMANGLE` | =1: watch who writes the camera angle |
|
||||
| `BB_RE_WATCH_CAMIN` | =1: watchpoint on the camera input |
|
||||
| `BB_RE_WATCH_CAMSTATE` | =1: watch who reads the camera's aspect ratio |
|
||||
| `BB_RE_WATCH_FILE` | =file: watch the hex address written in it once it appears |
|
||||
| `BB_RE_WATCH_FOLLOW` | =N: follow the watched bytes through host copies N times |
|
||||
| `BB_RE_WATCH_INDEX` | =N (default 62): virtual input index for BB_RE_WATCH_VIRTUAL |
|
||||
| `BB_RE_WATCH_VIRTUAL` | =1: watchpoint on the virtual right stick X |
|
||||
| `BB_RE_ZERO_VIRTUAL` | =1: virtual inputs fed from the right stick's X get 0 |
|
||||
| `BB_RING_REASONS` | set: why small read-only bindings were not copied, per 5 s |
|
||||
| `BB_ROBUSTNESS` | =0 (experiment): buffer and image robustness off |
|
||||
| `BB_SAMPLE_ALL` | =1: every sampled address as module+offset |
|
||||
| `BB_SAMPLE_RANGE` | =lo-hi: guest offsets the sampler reports |
|
||||
| `BB_SAMPLE_REPORT_SECONDS` | =N: thread samples every N seconds |
|
||||
| `BB_SAMPLE_THREAD` | =name: where that thread runs, sampled |
|
||||
| `BB_SAVE_TRACE` | =1: every operation on save files |
|
||||
| `BB_SCENE_DEBUG` | =file: when it appears, where the scene image is accessed |
|
||||
| `BB_SCENE_NO_RESOLVE` | =file:line,...: those accesses read the native scene image |
|
||||
| `BB_SCENE_SAMPLED_RESOLVE` | =0: shader reads take the native scene image as it is |
|
||||
| `BB_SCENE_STENCIL_BITS` | =1 (tests): the eight-draw stencil reconstruction |
|
||||
| `BB_SECTIONS` | =1: timing of the draw recording thread's sections |
|
||||
| `BB_SECTIONS_SLOW` | =1 (with BB_SECTIONS): slow frames' sections at each flip |
|
||||
| `BB_SEQUENCE_DUMP_COUNT` | =N (default 16): images per BB_SEQUENCE_DUMP_TRIGGER |
|
||||
| `BB_SEQUENCE_DUMP_TRIGGER` | =file: creating it saves the next images shown (frame generation checks) |
|
||||
| `BB_SHADER_CAPTURE` | =dir: shader inputs captured there |
|
||||
| `BB_SKIP_PS` | =hash,...: draws with these pixel shaders left out |
|
||||
| `BB_SLOW_BIND_LOG` | =microseconds: image memory binds slower than this |
|
||||
| `BB_SPEC_MATCH_CHECK` | set: the built specialization must find the same one |
|
||||
| `BB_STALL_REPORT` | =0: no report when the queues stop making progress |
|
||||
| `BB_STALL_THREADS` | =0: a stall report covers the command thread only |
|
||||
| `BB_STREAM_STATS` | =1: what a frame streams |
|
||||
| `BB_TAA_DIAGNOSTICS` | set: TAA diagnostic image dumps |
|
||||
| `BB_TEST_VPAD` | =1 (tests): a virtual Xbox-like gamepad |
|
||||
| `BB_THD_TRACE` | =1: every The Hunter's Dream datagram and callback |
|
||||
| `BB_TICK_LOG` | set: ranges marked written by the GPU most often, per 5 s |
|
||||
| `BB_TILE_BACK_LOG` | set: which shader a 'Tile back' line is for |
|
||||
| `BB_TIMELINE` | =file: a timeline of frames on their way to the GPU |
|
||||
| `BB_TIMELINE_START` | =seconds (default 30): BB_TIMELINE starts recording this late |
|
||||
| `BB_TLS_WATCH` | =offset: who writes that static TLS variable of the main thread |
|
||||
| `BB_TOGGLE_FILE` | =file: experiment bits re-read every 250 ms |
|
||||
| `BB_TRACE_ADDR` | =addr:size: that range traced from the start (with BB_TRACE_SIZE) |
|
||||
| `BB_TRACE_CB` | =hash: how that shader's constant buffers were bound |
|
||||
| `BB_TRACE_CB_LIST` | =hash,...: BB_TRACE_CB for several shaders |
|
||||
| `BB_TRACE_CB_RANGE` | =lo-hi: constant buffer bindings over that range |
|
||||
| `BB_TRACE_GPU_INPUTS` | =vs hash: the traced draw's buffer ranges, dumped at the display pass |
|
||||
| `BB_TRACE_PIPELINE` | =vs hash: that draw's pipeline handle and push data |
|
||||
| `BB_TRACE_SEMA` | set: blocking semaphore waits (deadlocks) |
|
||||
| `BB_TRACE_SHADER` | =vs hash: that draw's counts and streams once a second |
|
||||
| `BB_TRACE_SIZE` | =bytes: bindings over GPU-written buffers of that size, counted |
|
||||
| `BB_TRACE_VERTEX_DATA` | set (with BB_TRACE_SHADER): the stream bytes whenever they change |
|
||||
| `BB_TROPHY_PREVIEW` | =id: at the game's first trophy call, that trophy's notice without recording it |
|
||||
| `BB_TROPHY_TEST` | =id: that trophy's announcement once, nothing recorded |
|
||||
| `BB_UI_DRAWS` | =file: while it exists, one frame's UI draws (another aspect ratio, or the upscaler's UI phase) logged with their targets, stencil, blend, clip rects and redirection |
|
||||
| `BB_UI_DRAWS_PS` | =hex pixel shader hash: BB_UI_DRAWS also dumps that shader's draws' vertex streams, first constant buffer and indices |
|
||||
| `BB_UI_TEXTURE_LOG` | =file: when it appears, the textures the UI binds over the next 2 s |
|
||||
| `BB_UI_TRACE` | set: draws of frames that ended without the output-size UI |
|
||||
| `BB_UI_TRIGGER_VS` | =hex hash: the vertex shader that starts the UI phase |
|
||||
| `BB_UPSCALE_BEFORE_CS` | =hex hash: upscale before that compute shader |
|
||||
| `BB_VERTEX_IN_PLACE` | =1: vertex data bound in place per frame, by why it has no VRAM copy, and what draws read of it (log lines) |
|
||||
| `BB_VK_MARKERS` | =1: the game's debug labels as Vulkan markers |
|
||||
| `BB_VK_VALIDATION` | =1: Vulkan validation layers |
|
||||
| `BB_VK_VALIDATION_SYNC` | =1: Vulkan synchronization validation |
|
||||
| `BB_VRAM_ACCESS_TRAP` | set: blocks moved to VRAM are closed to the CPU |
|
||||
| `BB_VRAM_TRAP_RANGE` | =addr:size: BB_VRAM_ACCESS_TRAP only over that range |
|
||||
| `BB_WAIT_LOG` | =file: the game's kernel waits written there |
|
||||
| `BB_WAIT_TRACE` | =1: where the time waiting for GPU ticks goes |
|
||||
| `BB_WARN_META_TEXTURE` | set: warn about textures read from metadata memory |
|
||||
| `BB_WATCH` | =addr:size: what the translator reads from that range |
|
||||
| `BB_WRITE_LOG` | =1: GPU writes to guest memory near a fault |
|
||||
| `BB_WRITE_SHADOWS` | =1: ranges the game's copy shader writes in place copied into VRAM after it, small read-only bindings read there (no gain measured) |
|
||||
| `BB_WRITE_SHADOW_TRACE` | =1: why BB_WRITE_SHADOWS lookups miss and who drops the copies (log lines) |
|
||||
| `BB_WRITE_VERIFY` | =1: page protection to catch writes nothing announced |
|
||||
| `BB_ZERO_SCAN` | =seconds: that long after the first mapping, how much direct memory is zero |
|
||||
|
||||
## Internal (44)
|
||||
|
||||
Passed between the launchers, run.py / run.sh and the game. Not meant to be set by hand.
|
||||
|
||||
| Variable | What it does |
|
||||
|---|---|
|
||||
| `BB_BUNDLED_VK_DRIVER_FILES` | the package's own Vulkan driver manifests (set by the launcher) |
|
||||
| `BB_BUNDLED_VK_LAYER_PATH` | the package's own Vulkan layers, found before the host's (set by the launcher) |
|
||||
| `BB_CLIENT_VERSION` | the online client's name for the server (bbport VERSION, set by run.py) |
|
||||
| `BB_DATA_DIR` | the port's data folder (set by run.py) |
|
||||
| `BB_DEBUG_KEEP` | =1: tools/debug_attach.sh leaves the process stopped after writing its report (--keep) |
|
||||
| `BB_DEBUG_OUT` | where tools/debug_attach.sh writes its report (set by the script) |
|
||||
| `BB_DLSS_DIR` | the DLSS bridge and NVIDIA's DLSS libraries' folder in place of bb-probe's (set by the AppImage without Nix) |
|
||||
| `BB_FSR411_OUT` | where the launcher's FSR 4.1.1 build puts its assets |
|
||||
| `BB_FSR4CAP_CLEAN` | =1: the FSR 4.1.1 build removes its work folder afterwards (set by the launcher) |
|
||||
| `BB_FSR4CAP_TOOLS` | the FSR 4.1.1 build's tools folder (set by the launcher) |
|
||||
| `BB_FSR4CAP_WORK` | the FSR 4.1.1 build's work folder (set by the launcher) |
|
||||
| `BB_GAMEPAD_STRICT` | =1 (local co-op): only the chosen gamepad, never another one instead |
|
||||
| `BB_GPU_USER_DIR` | the user folder as the GPU library and the online module see it (set by the runtime) |
|
||||
| `BB_HOST_VK_ADD_DRIVER_FILES` | the host's VK_ADD_DRIVER_FILES under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
|
||||
| `BB_HOST_VK_ADD_LAYER_PATH` | the host's VK_ADD_LAYER_PATH under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
|
||||
| `BB_HOST_VK_DRIVER_FILES` | the host's VK_DRIVER_FILES under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
|
||||
| `BB_HOST_VK_ICD_FILENAMES` | the host's VK_ICD_FILENAMES under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
|
||||
| `BB_HOST_VK_IMPLICIT_LAYER_PATH` | the host's VK_IMPLICIT_LAYER_PATH under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
|
||||
| `BB_HOST_VK_LAYER_PATH` | the host's VK_LAYER_PATH under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
|
||||
| `BB_IMAGE_SHAPES` | the package's image shapes list for spare images (set by run.py) |
|
||||
| `BB_INTERNAL_HEIGHT` | the game's internal screen height (default 1080; set by run.py) |
|
||||
| `BB_INTERNAL_WIDTH` | the game's internal screen width (default 1920; set by run.py) |
|
||||
| `BB_LIVE_RES` | =1: render resolution changes live (set by run.py) |
|
||||
| `BB_MODS_CONFIG` | the mods list (default <data>/mods.json) |
|
||||
| `BB_MODS_DIR` | the folder of enabled mods' files (set by run.py) |
|
||||
| `BB_MONITOR_SIZE` | =WxH: the monitor's size the startup patch was made for (set by the launcher) |
|
||||
| `BB_OUTPUT_RES` | =WxH: the output the startup patch was made for (set by run.py) |
|
||||
| `BB_PATCHES_CONFIG` | the patch choice file (default <data>/patches.json) |
|
||||
| `BB_PATCHES_DIR` | the patches folder (default <data>/patches) |
|
||||
| `BB_PKGEXTRACT` | pkgextract for the launcher's Install from PKG (the AppImage's copy) |
|
||||
| `BB_PREBUILT` | =1: packaged build, run.sh builds nothing (set by the AppImage) |
|
||||
| `BB_PRESENCE_DIR` | Discord Rich Presence's folder for this start (run.sh; scripts/discord_presence.py) |
|
||||
| `BB_PROBE` | path of bb-probe for run.py |
|
||||
| `BB_PROMPT_ART` | the Xbox and Switch button art for the live prompts, KG_*.rgba by style (scripts/prompt_art.py; set by run.sh) |
|
||||
| `BB_SAVEDATA_TITLE` | the saves' title id (set by the runtime), for BBSC's save folder before the game has written one |
|
||||
| `BB_SHADER_GAME_DIR` | the game's files the shader recipes' programs come from (set by run.py) |
|
||||
| `BB_SHADER_RECIPES` | the shader recipes pack (set by run.py) |
|
||||
| `BB_SKIP_NETWORK_CHOICE` | =1: no online choice at start (the launcher's) |
|
||||
| `BB_STALL_THREADS_DONE` | set by the port once all threads were reported |
|
||||
| `BB_STEAM_OVERLAY` | the Steam overlay's preload kept for the game (=0: off; set by the AppImage entry) |
|
||||
| `BB_TIMEOUT` | =seconds: run.py stops the game after this long (tests) |
|
||||
| `BB_TROPHY_DIR` | the trophy list and icons scripts/trophies.py prepared, where the earned ones are recorded (set by run.sh) |
|
||||
| `BB_USER_DIR` | the user folder (saves, settings; set by run.py) |
|
||||
| `BB_VBLANK_HZ` | =N (default 60, 0: 480): the guest vblank rate (1000 for uncapped; set by run.py) |
|
||||
|
||||
## Build (15)
|
||||
|
||||
Read by the build and packaging scripts only.
|
||||
|
||||
| Variable | What it does |
|
||||
|---|---|
|
||||
| `BB_FSR4CAP_CHECKED` | internal to tools/fsr4cap: the tools were checked |
|
||||
| `BB_FSR4CAP_FP8` | =1: also the FP8 FSR 4.1.1 variant through Proton's emulation; 0: never |
|
||||
| `BB_FSR4CAP_ON_HOST` | set: the FSR 4.1.1 capture runs without systemd-run |
|
||||
| `BB_FSR4CAP_RUNNER` | =steam: the FSR 4.1.1 capture uses Steam's runtime |
|
||||
| `BB_FSR4CAP_SHELL` | internal to tools/fsr4cap: already inside the nix shell with the tools |
|
||||
| `BB_FSR4CAP_SPOOF` | =0: the FSR 4.1.1 capture does not present an RDNA4 GPU to AMD's DLL |
|
||||
| `BB_FSR4CAP_UMU_SHELL` | internal to tools/fsr4cap: already inside the umu nix shell |
|
||||
| `BB_FSR4_DRIVER_MODELS` | FSR 4 driver models to build (default: navi2 lite) |
|
||||
| `BB_FSR4_TIERS` | FSR 4 v07 asset tiers to download (tools/fetch_fsr4_assets.sh) |
|
||||
| `BB_IN_NIX_SHELL` | internal to run.sh: already inside the nix shell |
|
||||
| `BB_LTO` | =1: link-time optimization (packaging) |
|
||||
| `BB_PACKAGE_FSR411` | =1: the AppImage carries FSR 4.1.1 models (never in public packages) |
|
||||
| `BB_PGO` | =generate / use: profile-guided optimization (CMake option) |
|
||||
| `BB_PGO_DIR` | the PGO profile folder (CMake option) |
|
||||
| `BB_PUBLISH_BRANCH` | =1: tools/publish_guide.sh publishes the players' guide from a branch other than main |
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Layer memory module: PS4 GPU addresses without sparse (plan, 2026-10-09)
|
||||
|
||||
> Design notes from when this work was planned, kept for their reasoning. bbport 0.7 has one
|
||||
> memory model and its own renderer, and some switches named here (`BB_PC_MODEL`) no longer exist; the
|
||||
> current list is in [ENVIRONMENT.md](ENVIRONMENT.md).
|
||||
|
||||
## Why
|
||||
|
||||
The new memory model keeps game memory in a single sparse arena and moves blocks between game
|
||||
memory and VRAM by rebinding (`vkQueueBindSparse`). On NVIDIA (Linux, drivers ≥ 555), the bind
|
||||
time grows with the number of pages already bound, and the call holds a global lock: each
|
||||
move to VRAM cost seconds, and the game and the desktop froze (GTX 1660 Ti, 615.71). Without moves
|
||||
(`BB_FIXED_ARENA=1`) it runs at 48.6 FPS versus ~205 on an RX 7800 XT. We need a model that works the
|
||||
same on AMD, NVIDIA and Intel and is not tied to the shadPS4 buffer cache: part of a future layer
|
||||
for running PS4 games.
|
||||
|
||||
## Idea
|
||||
|
||||
The PS4 GPU has its own virtual memory (GPUVM) on top of shared memory; every V#, T# and pointer in
|
||||
user data is a virtual address. On PC we do the same with Vulkan 1.2 means (BDA):
|
||||
|
||||
- **Memory sources**: ordinary (non-sparse) Vulkan buffers:
|
||||
- chunks of game memory: dma-buf (AMD) or host memory import (NVIDIA, Intel), one chunk per
|
||||
game direct memory allocation (allocations are mapped whole, so a range inside one
|
||||
mapping is always contiguous within one chunk);
|
||||
- VRAM mirrors: segments aligned to game addresses (game page X sits in the segment at the
|
||||
same offset), allocated when the first VRAM page appears in the segment.
|
||||
- **Page table** (a buffer on the GPU): page of a PS4 address → device address (BDA) of its
|
||||
current source. It is changed by a copy in the command stream, in order with draws, without semaphores.
|
||||
- **Direct path**: the whole V# range is in one source (almost all bindings) → an ordinary
|
||||
descriptor (source buffer, offset, size). The shader translator does not change.
|
||||
- **Paged path**: the range is mixed (≈85 huge bindings per frame: shaders bound
|
||||
to all of memory) → the descriptor points to a record {PS4 address, size}, and the shader
|
||||
translates each address through the table. A pipeline variant only for such bindings
|
||||
(specialization key).
|
||||
- **Vertices, indices, indirect arguments, copies, texture uploads**: the direct path; otherwise,
|
||||
assembly into a contiguous temporary buffer (read-only) or per-page copies (the CPU knows the table).
|
||||
- **Moving to VRAM and back**: the policy of the current new model (data whose writes we
|
||||
know about: loaders, the GPU; unknown writers stay in place), but the execution is a copy + a
|
||||
table write + choosing the source at bind time. Bindings recorded before a move read the old
|
||||
source, which is still valid; VRAM is freed once the GPU has passed those commands.
|
||||
|
||||
## Order of work
|
||||
|
||||
1. **Module core** (`gpu/layer/`, no dependencies on shadPS4): sources, pages, table,
|
||||
`Resolve(address, size)` → a direct source or "mixed", walking over sources for copies.
|
||||
The command recording interface is abstract (copy, table write), so it does not pull in the scheduler.
|
||||
2. **Hook-up without VRAM**: the new model returns ranges in place through chunk buffers, not
|
||||
through the arena; the new model does not create the sparse arena. Expected: ~48 FPS on AMD (like
|
||||
`BB_FIXED_ARENA`), no `vkQueueBindSparse` at all. Check the image, then make a build for NVIDIA.
|
||||
3. **VRAM mirrors + moves**: the policy from `BufferCache`, the page table, the paged path in
|
||||
shaders for mixed bindings. Goal: parity with the sparse arena on AMD (~205 FPS).
|
||||
4. **NVIDIA/Intel**: a test build, a tester log; then remove sparse from the new model entirely.
|
||||
|
||||
Measurements: RX 7800 XT, standing still (`tools/bench.sh`), `BB_PC_MODEL=1`; diagnostics `BB_RESIDENCY_MIX=1`.
|
||||
|
||||
## Status (2026-10-09, morning)
|
||||
|
||||
- Steps 1–3 are done. On the RX 7800 XT, standing still: module ~205 FPS, sparse arena ~205–238, old model
|
||||
~220 (at the lamp in the Hunter's Nightmare); in another spot (1170 draw calls): 167.5 / 164.5 / 146.
|
||||
`vkQueueBindSparse` is never called in module mode (counter in the `Layer memory:` line).
|
||||
- Mixed bindings are solved without the paged path: "volatile" blocks (`layer_volatile`). A block with
|
||||
unknown bytes (the edge of a range, a gap between images) goes into the mirror together with
|
||||
the rest of the range, is refreshed from game memory before each binding, and whatever a dispatch
|
||||
wrote into it is copied back after the dispatch (`LayerAfterBindings`). The paged path remains for
|
||||
bindings of 64 MiB and larger. `BB_LAYER_VOLATILE=0` turns it off (for comparison).
|
||||
- Default: AMD uses the sparse arena, other GPUs use the module (`BB_LAYER_MEMORY=1/0` to
|
||||
choose manually). Step 4 (NVIDIA): needs a tester log; the host memory import path cannot be
|
||||
checked on AMD (RADV does not import memfd).
|
||||
|
||||
## Addendum: original copying and portable labels (2026-10-09)
|
||||
|
||||
In the new module, the paged path of the original copy shader is fixed: writes update game memory and the current mirror; the native shader is now selected by default. For GPU labels, a standard Vulkan path without AMD buffer marker was added. The implementation, switches and verification limits are described in [TRANSLATION_GPU_COPY_LABELS.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/TRANSLATION_GPU_COPY_LABELS.ru.md). This removes the dependency of label writes on the AMD extension, but does not replace hardware testing on NVIDIA.
|
||||
@@ -1,68 +0,0 @@
|
||||
# Модуль памяти прослойки: адреса видеочипа PS4 без sparse (план, 2026-10-09)
|
||||
|
||||
## Зачем
|
||||
|
||||
Новая модель памяти держит память игры в одной sparse-арене и переносит блоки между памятью
|
||||
игры и VRAM перепривязкой (`vkQueueBindSparse`). На NVIDIA (Linux, драйверы ≥ 555) время
|
||||
привязки растёт с числом уже привязанных страниц, вызов держит глобальную блокировку: каждый
|
||||
перенос в VRAM стоил секунды, игра и рабочий стол вставали (GTX 1660 Ti, 615.71). Без переносов
|
||||
(`BB_FIXED_ARENA=1`) — 48,6 FPS против ~205 на RX 7800 XT. Нужна модель, работающая одинаково на
|
||||
AMD, NVIDIA и Intel, и не привязанная к буферному кэшу shadPS4: часть будущей прослойки запуска
|
||||
игр PS4.
|
||||
|
||||
## Идея
|
||||
|
||||
У видеочипа PS4 своя виртуальная память (GPUVM) над общей памятью; любой V#, T#, указатель в
|
||||
user data — виртуальный адрес. На ПК то же самое делаем средствами Vulkan 1.2 (BDA):
|
||||
|
||||
- **Источники памяти** — обычные (не sparse) буферы Vulkan:
|
||||
- куски памяти игры: dma-buf (AMD) или импорт памяти хоста (NVIDIA, Intel), по куску на
|
||||
выделение прямой памяти игры (выделения отображаются целиком — диапазон внутри одного
|
||||
отображения всегда непрерывен в одном куске);
|
||||
- зеркала в VRAM: сегменты, выровненные по адресам игры (страница игры X лежит в сегменте по
|
||||
тому же смещению), выделяются, когда в сегменте появляется первая страница в VRAM.
|
||||
- **Таблица страниц** (буфер на видеочипе): страница адреса PS4 → адрес устройства (BDA) её
|
||||
текущего источника. Меняется копией в потоке команд — в порядке с отрисовками, без семафоров.
|
||||
- **Прямой путь**: диапазон V# целиком в одном источнике (почти все привязки) → обычный
|
||||
дескриптор (буфер источника, смещение, размер). Транслятор шейдеров не меняется.
|
||||
- **Страничный путь**: диапазон смешанный (≈85 огромных привязок за кадр: шейдеры, привязанные
|
||||
ко всей памяти) → дескриптор указывает на запись {адрес PS4, размер}, шейдер переводит каждый
|
||||
адрес через таблицу. Вариант пайплайна только для таких привязок (ключ специализации).
|
||||
- **Вершины, индексы, аргументы indirect, копии, загрузка текстур**: прямой путь, иначе сборка
|
||||
в непрерывный временный буфер (только чтение) или копии по страницам (процессор знает таблицу).
|
||||
- **Перенос в VRAM и обратно** — политика нынешней новой модели (данные, о записи которых мы
|
||||
знаем: загрузчики, видеочип; неизвестные писатели — на месте), но исполнение — копия + запись в
|
||||
таблицу + выбор источника при привязке. Привязки, записанные до переноса, читают старый
|
||||
источник — он ещё верен; освобождение VRAM — когда видеочип прошёл эти команды.
|
||||
|
||||
## Порядок работ
|
||||
|
||||
1. **Ядро модуля** (`gpu/layer/`, без зависимостей от shadPS4): источники, страницы, таблица,
|
||||
`Resolve(адрес, размер)` → прямой источник или «смешанный», обход по источникам для копий.
|
||||
Интерфейс записи команд — абстрактный (копия, запись таблицы), чтобы не тянуть планировщик.
|
||||
2. **Подключение без VRAM**: новая модель отдаёт диапазоны на месте через буферы кусков, а не
|
||||
через арену; sparse-арена в новой модели не создаётся. Ожидание: ~48 FPS на AMD (как
|
||||
`BB_FIXED_ARENA`), никаких `vkQueueBindSparse`. Проверка картинки, затем сборка для NVIDIA.
|
||||
3. **Зеркала в VRAM + перенос**: политика из `BufferCache`, таблица страниц, страничный путь в
|
||||
шейдерах для смешанных привязок. Цель — паритет с sparse-ареной на AMD (~205 FPS).
|
||||
4. **NVIDIA/Intel**: тестовая сборка, лог тестера; затем убрать sparse из новой модели совсем.
|
||||
|
||||
Замеры — RX 7800 XT, стоя (`tools/bench.sh`), `BB_PC_MODEL=1`; диагностика `BB_RESIDENCY_MIX=1`.
|
||||
|
||||
## Состояние (2026-10-09, утро)
|
||||
|
||||
- Шаги 1–3 сделаны. На RX 7800 XT стоя: модуль ~205 FPS, sparse-арена ~205–238, старая модель
|
||||
~220 (у лампы в Кошмаре охотника); в другом месте (1170 вызовов отрисовки) — 167,5 / 164,5 / 146.
|
||||
`vkQueueBindSparse` в режиме модуля не вызывается ни разу (счётчик в строке `Layer memory:`).
|
||||
- Смешанные привязки решены без страничного пути: «летучие» блоки (`layer_volatile`) — блок с
|
||||
неизвестными байтами (край диапазона, промежуток между образами) кладётся в зеркало вместе с
|
||||
остальным диапазоном, обновляется из памяти игры перед каждой привязкой, а то, что запуск в него
|
||||
записал, копируется обратно после запуска (`LayerAfterBindings`). Страничный путь остаётся для
|
||||
привязок от 64 МиБ. `BB_LAYER_VOLATILE=0` — выключить (для сравнения).
|
||||
- По умолчанию: AMD — sparse-арена, остальные видеокарты — модуль (`BB_LAYER_MEMORY=1/0` —
|
||||
вручную). Шаг 4 (NVIDIA): нужен лог тестера; путь импорта памяти хоста на AMD не проверить
|
||||
(RADV не импортирует memfd).
|
||||
|
||||
## Дополнение: оригинальное копирование и переносимые метки (2026-10-09)
|
||||
|
||||
В новом модуле исправлен paged-путь оригинального copy shader: записи обновляют память игры и актуальное зеркало; нативный шейдер теперь выбран по умолчанию. Для GPU-меток добавлен стандартный Vulkan-путь без AMD buffer marker. Реализация, переключатели и границы проверки описаны в [TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md). Это закрывает зависимость записи меток от AMD-расширения, но не заменяет аппаратную проверку NVIDIA.
|
||||
+15
-8
@@ -1,6 +1,6 @@
|
||||
# Mods and third-party patches
|
||||
|
||||
Both are on the launcher's **Settings** page and apply at the next start. The game folder itself
|
||||
Both are in the launcher's **Mods** section and apply at the next start. The game folder itself
|
||||
is never changed.
|
||||
|
||||
## File mods
|
||||
@@ -25,11 +25,16 @@ These layouts work as well:
|
||||
|
||||
- `Mod name/app0/dvdroot_ps4/...`, and `Mod name/<title ID>/dvdroot_ps4/...` with whichever
|
||||
release the mod was made for (`CUSA03173`, `CUSA00900`, ...).
|
||||
- One extra wrapper folder, as extracting an archive into a folder of its name leaves it:
|
||||
`Mod name/Mod name v1.2/dvdroot_ps4/...` (readme files and pictures may sit next to it).
|
||||
- Up to three extra wrapper folders, as extracting an archive into a folder of its name leaves it:
|
||||
`Mod name/Mod name v1.2/dvdroot_ps4/...` (notes, pictures and links may sit next to them:
|
||||
`README`, `.txt`, `.pdf`, `.png`, `.webp`, `.url`, ...).
|
||||
- The game's folders without `dvdroot_ps4`: `Mod name/chr/...`, `Mod name/parts/...` (`chr`,
|
||||
`parts`, `map`, `menu`, `msg`, `param`, `sfx`, `sound`, `font` and the other folders of
|
||||
`dvdroot_ps4`).
|
||||
`dvdroot_ps4`), also next to other folders such as `Screenshots` or `Optional`, which stay out
|
||||
of the game.
|
||||
|
||||
A folder in the mods folder that is not a mod is listed in the launcher under the mods with the
|
||||
reason: still an archive, several mods in one folder, no game files, or empty.
|
||||
|
||||
Upper or lower case does not matter: a mod made on Windows with
|
||||
`DVDROOT_PS4/Chr/C0000.chrbnd.dcx` replaces the game's `dvdroot_ps4/chr/c0000.chrbnd.dcx`.
|
||||
@@ -52,9 +57,8 @@ Whether a mod works with Bloodborne 1.09 is up to its author.
|
||||
A shadPS4-style folder next to the game folder, `<game folder>-mods/dvdroot_ps4/...` (for
|
||||
example `CUSA03173-mods` next to `CUSA03173`), is loaded too, before the mods in the list. You
|
||||
can also choose a folder that holds `dvdroot_ps4` directly as the mods folder: it is then loaded
|
||||
as one mod. **Load mods** turns all of them off. The launcher's button prompts and gestures
|
||||
menu work the same way, as files laid under the mods; they are settings of their own and stay
|
||||
on when mods are off.
|
||||
as one mod. **Load mods** turns all of them off. The field of view works the same way, as files
|
||||
laid under the mods; it is a setting of its own and stays on when mods are off.
|
||||
|
||||
### How it works
|
||||
|
||||
@@ -101,9 +105,12 @@ the launcher.
|
||||
executable is skipped whole, and the log says so.
|
||||
- They apply after bbport's own patches (`patches/Bloodborne.xml`), so where both change the
|
||||
same bytes, the third-party patch wins.
|
||||
- shadPS4's patch file can stay named `Bloodborne.xml`. In a file of that name, the patches
|
||||
named as one of bbport's own are left out (bbport's apply), and the rest start off until you
|
||||
turn them on, whatever the file says. An older package's copy of bbport's file adds nothing.
|
||||
- Patches that replace pointers in tables, such as *60 FPS++*, are moved to where the game is
|
||||
loaded.
|
||||
|
||||
From a terminal: `BB_PATCHES_DIR=/path BB_PATCHES_CONFIG=/path/patches.json bash run.sh`.
|
||||
|
||||
This guide is based on bbport's [MODS.md](MODS.md) (Russian), by deadinside28.
|
||||
This guide is based on bbport's [MODS.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/MODS.md) (Russian), by deadinside28.
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
# Файловые моды
|
||||
|
||||
В лаунчере появилась группа **Моды**: выбор папки, общий переключатель,
|
||||
включение каждого мода и стрелки порядка загрузки. Изменения применяются при
|
||||
следующем запуске. Нижний включённый мод имеет больший приоритет; при совпадении
|
||||
файлов в журнале выводится, какой мод заменил предыдущий.
|
||||
|
||||
По умолчанию используется `bbport/mods/`, а в AppImage —
|
||||
`~/.local/share/bbport/mods/`. Распакуйте скачанный мод так:
|
||||
|
||||
```text
|
||||
mods/
|
||||
Название мода/
|
||||
dvdroot_ps4/
|
||||
chr/...
|
||||
param/...
|
||||
menu/...
|
||||
```
|
||||
|
||||
Также принимаются:
|
||||
|
||||
- `Название мода/app0/dvdroot_ps4/...` и `Название мода/CUSA03173/dvdroot_ps4/...`;
|
||||
- лишняя папка-обёртка, как после распаковки архива в папку с его именем:
|
||||
`Название мода/Название мода v1.2/dvdroot_ps4/...` (рядом могут лежать readme/картинки);
|
||||
- мод без `dvdroot_ps4`, в котором сразу лежат папки игры: `Название мода/chr/...`,
|
||||
`Название мода/parts/...` (`chr`, `parts`, `map`, `menu`, `msg`, `param`, `sfx`,
|
||||
`sound`, `font` и другие папки `dvdroot_ps4`).
|
||||
|
||||
Регистр имён в моде не важен: `DVDROOT_PS4/Chr/C0000.chrbnd.dcx` из мода, собранного
|
||||
под Windows, заменяет игровой `dvdroot_ps4/chr/c0000.chrbnd.dcx`. Раньше такой файл
|
||||
добавлялся рядом под своим именем, и игра его не видела. В журнале запуска видно, сколько
|
||||
файлов игры мод заменил и сколько добавил: `Mods: 12 game files replaced, 0 added`.
|
||||
Если мод должен заменять файлы, а пишет `0 game files replaced`, проверьте пути внутри мода.
|
||||
Архивы ZIP/7z нужно предварительно распаковать. Поддерживаются готовые замены
|
||||
файлов `dvdroot_ps4`: текстуры, модели, параметры, интерфейс, звук и шрифты.
|
||||
Менеджер не объединяет содержимое двух одинаковых `.dcx`/`.bnd` файлов: последняя
|
||||
замена выигрывает целиком.
|
||||
|
||||
Для уже подготовленной папки shadPS4 работает соседний каталог
|
||||
`CUSA03173-mods/dvdroot_ps4/...` рядом с `CUSA03173/`. Он подключается автоматически
|
||||
перед модами из списка. Общий переключатель выключает и его. Можно также выбрать
|
||||
папку, непосредственно содержащую `dvdroot_ps4`, как папку модов; такая папка
|
||||
подключается целиком, без списка отдельных модов.
|
||||
Соглашение `-mods` проверено по
|
||||
[исходникам shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/file_sys/fs.cpp).
|
||||
|
||||
Оригиналы игры не перезаписываются. На время запуска создаётся отдельное
|
||||
объединённое дерево ссылок в каталоге данных `out/mod-game-*`; чтение, stat и
|
||||
перечисление каталогов видят выбранные замены и остальные оригинальные файлы.
|
||||
Игровые `/app0` и `/hostapp` доступны только для чтения. Сохранения остаются
|
||||
в прежней папке и доступны для записи. При обычном завершении или остановке
|
||||
лаунчером временное дерево удаляется. После SIGKILL/аварийного отключения оно
|
||||
может остаться; такие каталоги не используются в следующем запуске.
|
||||
|
||||
Моды с заменой `eboot.bin`, `sce_module` или `sce_sys`, инжекторы DLL и скриптовые
|
||||
плагины этим загрузчиком не поддерживаются. Патчи исполняемого кода остаются
|
||||
в существующей системе `patches/` / `BB_PATCHES`. Символьные ссылки внутри мода
|
||||
не принимаются. Совместимость конкретного мода с CUSA03173 1.09 определяется
|
||||
его автором.
|
||||
|
||||
Для запуска из терминала:
|
||||
|
||||
```sh
|
||||
BB_GAME_DIR=/path/to/CUSA03173 BB_MODS_DIR=/path/to/mods bash run.sh
|
||||
BB_GAME_DIR=/path/to/CUSA03173 BB_MODS_ENABLED=0 bash run.sh
|
||||
```
|
||||
|
||||
Порядок и отключения сохраняются в каталоге данных `mods.json`:
|
||||
|
||||
```json
|
||||
{"order": ["Первый", "Второй"], "disabled": ["Первый"]}
|
||||
```
|
||||
|
||||
Новые папки включаются автоматически и добавляются в конец по алфавиту.
|
||||
`BB_MODS_CONFIG` меняет путь профиля. `BB_MOD_TRACE=1` с `BB_MODS_DIR` выводит
|
||||
первые чтения реально подключённых файлов для диагностики.
|
||||
|
||||
# Сторонние патчи (XML shadPS4)
|
||||
|
||||
Папка `patches/` в каталоге данных (в AppImage — `~/.local/share/bbport/patches/`, в лаунчере
|
||||
группа **Сторонние патчи**, там же можно выбрать другую папку). Подходят XML-файлы патчей в
|
||||
формате shadPS4/GoldHEN: берутся `Metadata` для `AppVer="01.09"` и `eboot.bin`, файлы с
|
||||
`TitleID` других игр пропускаются. Включение по умолчанию — из `isEnabled` файла, выбор в
|
||||
лаунчере сохраняется в `patches.json` каталога данных:
|
||||
|
||||
```json
|
||||
{"enabled": ["файл.xml/Имя патча"], "disabled": ["файл.xml/Другой патч"]}
|
||||
```
|
||||
|
||||
Поддерживаются строки `bytes`, `bytes16/32/64`, `float32/64`, `utf8`, `utf16`.
|
||||
Патч со строками `mask` (поиск по шаблону) или с адресом вне eboot пропускается целиком,
|
||||
с сообщением в журнале. Сторонние патчи применяются после встроенных
|
||||
(`patches/Bloodborne.xml`): при пересечении побеждает сторонний. Из терминала:
|
||||
`BB_PATCHES_DIR=/путь BB_PATCHES_CONFIG=/путь/patches.json bash run.sh`.
|
||||
|
||||
Патчи, которые заменяют указатели в таблицах (как `60 FPS++` и `90 FPS++`), загрузчик
|
||||
переносит на реальный адрес образа. Раньше такие патчи останавливали запуск ошибкой
|
||||
`patch overlaps a relocation`.
|
||||
|
||||
# Резкость TAA
|
||||
|
||||
Настройки `sharpen` и `sharpness` теперь работают также для TAA, в лаунчере и
|
||||
меню игры. Диапазон — **0…1**; при 0 или выключенном переключателе дополнительного
|
||||
прохода нет. RCAS применяется к готовой сцене перед HUD. В историю TAA сохраняется
|
||||
цвет до повышения резкости, чтобы она не усиливалась при повторном накоплении.
|
||||
Ровные участки сохраняют исходный цвет, включая HDR; дополнительный буфер не нужен.
|
||||
@@ -1,232 +0,0 @@
|
||||
# Новая модель памяти на NVIDIA: итог работы (2026-10-09)
|
||||
|
||||
Замеры ниже относятся к этапу разработки модуля. Актуальные изменения `0.5-pre2` и результаты
|
||||
проверки оригинального copy shader: [CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md) и
|
||||
[TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md).
|
||||
|
||||
## Задача
|
||||
|
||||
Новая модель памяти и трансляции (`BB_PC_MODEL=1`) держала память игры в одном sparse-буфере
|
||||
(«арене») и переносила горячие блоки в VRAM перепривязкой страниц (`vkQueueBindSparse`). На
|
||||
проприетарном драйвере NVIDIA для Linux это не работает: начиная с версии 555 время
|
||||
`vkQueueBindSparse` растёт с числом уже привязанных страниц, а сам вызов держит глобальную
|
||||
блокировку. У тестера (GTX 1660 Ti, драйвер 615.71) каждый перенос держал видеочип 10 секунд и
|
||||
дольше, и вместе с игрой вставал рабочий стол. Поэтому новая модель была доступна только на AMD.
|
||||
|
||||
Цель: новая модель памяти на всех видеокартах.
|
||||
|
||||
## Решение: модуль памяти прослойки
|
||||
|
||||
Вместо sparse-арены работает собственный модуль памяти (`gpu/layer/bblayer_gpu_memory.*`,
|
||||
`gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp`, план — `MEMORY_MODULE_PLAN.ru.md`):
|
||||
|
||||
- память игры привязывается **на месте**, через буферы её кусков (dma-buf на AMD, импорт памяти
|
||||
хоста `VK_EXT_external_memory_host` на NVIDIA);
|
||||
- часто читаемое копируется в VRAM в отдельные буферы-«зеркала». Перенос сводится к копии и
|
||||
учёту в модуле, без перепривязки страниц;
|
||||
- привязка берёт зеркало, если диапазон целиком в VRAM, иначе память игры на месте. Диапазоны от
|
||||
64 МиБ идут через таблицу страниц в транслированных шейдерах.
|
||||
|
||||
**`vkQueueBindSparse` в этом режиме не вызывается ни разу**: счётчик выводится в строке журнала
|
||||
`Layer memory: ... vkQueueBindSparse: 0 calls`.
|
||||
|
||||
### Когда включается
|
||||
|
||||
| Видеокарта | По умолчанию |
|
||||
|---|---|
|
||||
| AMD | sparse-арена, как раньше |
|
||||
| NVIDIA, Intel и остальные | модуль памяти, автоматически |
|
||||
|
||||
`BB_LAYER_MEMORY=1` / `=0` выбирает вручную. Лаунчер больше не прячет переключатель «Новая модель
|
||||
памяти и трансляции» на видеокартах не от AMD. Стартовая проверка импорта памяти хоста в режиме
|
||||
модуля проверяет запись видеочипа и процессора через сам импортированный буфер, без sparse-буфера.
|
||||
|
||||
## Что пришлось исправить, чтобы модуль заработал правильно и быстро
|
||||
|
||||
### Скорость: с 48–60 FPS до ~205 FPS на AMD (стоя у лампы)
|
||||
|
||||
1. **Подмена копий и очисток образов не срабатывала.** Детекторы `IsComputeImageCopy` и
|
||||
`IsComputeImageClear` ждали у шейдера ровно два буфера, а модуль добавлял каждому шейдеру
|
||||
третий — таблицу страниц. Копия целей рендеринга (`cs 3d5ebf4e`) и очистка (`cs 8b355b5a`,
|
||||
31 раз за кадр) выполнялись настоящими шейдерами через память, а образы 1080p
|
||||
перезагружались каждый кадр. Теперь таблица страниц не считается (`ShaderBufferCount`):
|
||||
60 → 136 FPS.
|
||||
2. **Смешанные диапазоны.** Если у диапазона все блоки в VRAM, кроме одного-двух крайних с
|
||||
«неизвестными» байтами (промежуток между образами, чужие данные), весь диапазон читался через
|
||||
шину. Такие блоки теперь «летучие»: лежат в зеркале и обновляются из памяти игры перед каждой
|
||||
привязкой на чтение. 139 → 207 FPS. Для привязок на запись летучие блоки не используются.
|
||||
3. **Перенос в VRAM при первой привязке** диапазона известных данных (как делала арена). Он
|
||||
соблюдает `BB_GARLIC_VRAM=0` и бюджет VRAM (`BB_VRAM_PROMOTE_PERCENT`, важно для карт на 6 ГБ).
|
||||
4. **Нагрузка на процессор.** Ответы модуля кэшируются по поколению размещения; повторные
|
||||
проверки «можно ли перенести» — не чаще раза за кадр на диапазон; проверка «есть ли у
|
||||
диапазона копия в VRAM» — без блокировок, по счётчику версий модуля. Это сняло около 20%
|
||||
времени привязок и почти все блокировки в потоке командного процессора.
|
||||
|
||||
### Правильность: взрывы вершин и мусорные текстуры
|
||||
|
||||
Причина была одна: **копия в VRAM расходилась с памятью игры**. Найдено и закрыто несколько
|
||||
путей:
|
||||
|
||||
1. **Код игры пишет в память без нашего ведома**: заголовки её кучи (`0x2085a60` / `0x20858a0`),
|
||||
часть загрузчика ресурсов (`0x216d420`). Блоки с копией в VRAM теперь защищены от записи
|
||||
собственными ловушками модуля. Запись проходит сразу, а блок возвращается на место перед
|
||||
следующей привязкой. Часто записываемые блоки остаются на месте навсегда (`NoteCpuWrite`).
|
||||
2. **Чтения мимо модуля.** Мелкие привязки, кольцо констант и копирования командного процессора
|
||||
брали данные из памяти игры, хотя новые данные видеочипа лежали в VRAM: в новой модели
|
||||
`IsRegionGpuModified` знает только о незавершённых записях. Теперь всё, что задевает блок с
|
||||
копией в VRAM, идёт через модуль (`LayerMirrored`).
|
||||
3. **Данные видеочипа затирали новые данные процессора.** При возврате блока на место и при
|
||||
повторной выдаче памяти игре модуль копировал «изменённое видеочипом» обратно, в том числе
|
||||
поверх того, что процессор успел записать позже: пометки ставились слишком широко. Теперь
|
||||
заявленные записи процессора снимают пометки со своих байтов (`LayerForgetGpuData`), выдача
|
||||
памяти заново снимает пометки со своего диапазона, а после незаявленной записи данные видеочипа
|
||||
в блоке считаются мёртвыми и назад не копируются.
|
||||
4. **Куда видеочипу можно писать в VRAM** (`BB_LAYER_GPU_WRITES`): по умолчанию (`1`) — только в
|
||||
блоки, которые процессор не трогал с момента выделения: цели рендеринга и другие данные одного
|
||||
видеочипа. `0` — никогда (правильно, но 60–70 FPS: записи идут через шину), `2` — везде (давало
|
||||
взрывы).
|
||||
5. **Шейдер копирования игры** в `0.5-pre2` по умолчанию транслируется в SPIR-V и выполняется
|
||||
на GPU. Таблица страниц содержит актуальное GPU-представление и адреса памяти игры;
|
||||
copy shader записывает результат в оба представления. Это устраняет расхождение зеркала
|
||||
VRAM и памяти игры без подмены оригинального списка копий на CPU.
|
||||
`BB_COPY_SHADER_NATIVE=0` оставляет HLE для сравнения.
|
||||
|
||||
На предыдущем этапе проверено в игре на AMD (модуль включён вручную, шейдер копирования через HLE): с
|
||||
`BB_LAYER_GPU_WRITES=0` и с правилом «видеочип пишет в VRAM только внутри образов» — Сон
|
||||
Охотника, собаки и трупы в Ярнаме чистые. На последней сборке (правило «блоки, которые процессор
|
||||
не трогал») артефактов при игре не замечено, но отдельной проверки всех мест ещё не было.
|
||||
|
||||
## Замеры (RX 7800 XT, AMD, модуль включён вручную)
|
||||
|
||||
| Место | Модуль | Sparse-арена | Старая модель |
|
||||
|---|---|---|---|
|
||||
| У лампы, утром (до исправлений правильности) | ~205 FPS | ~205–238 | ~220 |
|
||||
| Сон Охотника, сейчас | 130–144 FPS | — | — |
|
||||
|
||||
В Сне Охотника упираемся то в поток команд видеочипа на процессоре (~3,9 мкс на вызов при
|
||||
~1500 вызовах за кадр), то в сам видеочип: проходы G-буфера 960×540, FSR ~1,07 мс.
|
||||
|
||||
**Открытый вопрос:** в части запусков на том же месте видеочип работает заметно медленнее
|
||||
(70–95 FPS, FSR до 3,2 мс вместо 1,07). Причина не найдена; версии с компиляцией шейдеров,
|
||||
поучастковым профилем `BB_SECTIONS` и обновлением летучих блоков не подтвердились. Сравнение
|
||||
запусков осложняет то, что игра рисует разные проходы в зависимости от ракурса и сцены.
|
||||
|
||||
## Что не проверено
|
||||
|
||||
- **Сама NVIDIA.** На AMD путь NVIDIA (импорт памяти хоста + модуль) не воспроизвести: RADV не
|
||||
импортирует memfd. Нужен лог тестера.
|
||||
- Карты с малым объёмом VRAM (6 ГБ): поведение при заполненной VRAM проверено только
|
||||
искусственно (`BB_VRAM_PROMOTE_PERCENT=1`: копий нет, ~38 FPS, стабильно).
|
||||
- Intel.
|
||||
|
||||
## Что попросить у тестера на NVIDIA
|
||||
|
||||
Сборку с включённой в лаунчере «Новой моделью памяти и трансляции». В журнале должны быть строки:
|
||||
|
||||
- `Guest memory: host memory imported by the GPU (VK_EXT_external_memory_host ...)`;
|
||||
- `Guest memory: the new memory model through the layer's memory module on this GPU (vendor 0x10de)`;
|
||||
- `Layer memory: ... vkQueueBindSparse: 0 calls, 0 ranges; write traps hit N`;
|
||||
- `Frame stats: ... FPS`.
|
||||
|
||||
Если видна строка `vkQueueBindSparse of N ranges took X ms`, значит, sparse где-то всё же
|
||||
используется — это ошибка.
|
||||
|
||||
## Первые логи с NVIDIA и исправления (вечер 2026-10-09, `0.5-pre3`)
|
||||
|
||||
Тестер: GTX 1660 Ti (ноутбук, 6 ГБ, PCIe 3.0), драйвер 615.71, сборка 0.5-pre2. Импорт памяти
|
||||
хоста и модуль включились, `vkQueueBindSparse: 0`.
|
||||
|
||||
1. **Зеркал не было вовсе** (10–15 FPS, мерцание фона на титульном экране): `FixedArena()` по
|
||||
умолчанию включалась при импорте памяти хоста и через `GarlicInVramForLayer()` запрещала модулю
|
||||
переносы. Теперь в режиме модуля она всегда выключена. До новой сборки обход — `BB_FIXED_ARENA=0`:
|
||||
с ним у тестера 30–35 FPS (видео), против 60–70 в старой модели.
|
||||
2. **Кэш текстур и зеркала делили VRAM без общего бюджета.** Сборщик кэша текстур сравнивает с
|
||||
порогами всю VRAM процесса, зеркала тоже, а пороги на картах меньше ~9 ГБ — 40 и 80 %
|
||||
стартового бюджета (у тестера давление 1963 МиБ, критическая отметка 3927 МиБ). Переносы же
|
||||
вставали только на 90 % бюджета драйвера. Итог — 3,9–4,0 ГБ, сборщик постоянно за критической
|
||||
отметкой: 155–1255 выселенных образов за 5 с против 0–29 на AMD. Теперь:
|
||||
- переносы в VRAM встают за 384 МиБ до критической отметки (`VramPromotionsPaused`, строка
|
||||
`copies of loaded blocks wait`);
|
||||
- за 128 МиБ до неё модуль сам освобождает место (`LayerYieldToImages`): сначала обрезает запас
|
||||
роста зеркал, затем возвращает на место целые зеркала, не использовавшиеся 3 с
|
||||
(`BB_VRAM_PRESSURE_IDLE_SECONDS`).
|
||||
3. **Перестройка зеркал.** Участок, примыкающий к зеркалу, создавал новое зеркало целиком с
|
||||
копией старого. При загрузке уровня это 15–27 ГБ выделений на 1,3 ГБ данных и всплеск VRAM до
|
||||
7,4 ГБ (у тестера в логе 5,6 ГБ при физических 6). Во время игры, при подгрузке районов,
|
||||
то же самое. Теперь растущее вверх зеркало получает запас под рост, равный своему размеру, но
|
||||
не больше 64 МиБ (`BB_LAYER_MIRROR_GROWTH_MB`): 5–10 ГБ выделений, худший кадр загрузки 555 →
|
||||
87 мс. Заменённые зеркала освобождаются на каждой отправке, а не раз в секунду.
|
||||
4. **Записи `WRITE_DATA` в зеркало.** Командный процессор пишет на видеочипе в память игры; блок
|
||||
с зеркалом раньше уходил на место. Если диапазон целиком в одном зеркале, запись идёт в оба
|
||||
представления (`CommandWriteMirror`).
|
||||
5. **Наблюдаемые блоки** (`BB_LAYER_WATCH`, по умолчанию включено). Блоки, в которых процессор
|
||||
когда-то писал без предупреждения, раньше всегда читались на месте, а значит на NVIDIA через
|
||||
шину. Теперь для читающих привязок они тоже получают зеркало под ловушкой записи. Запись
|
||||
процессора не возвращает блок на место: он перезаливается перед следующей привязкой, а
|
||||
ловушка взводится снова. Видеочип в такие блоки пишет на месте; блок, куда он записал,
|
||||
больше не наблюдается. Наблюдение почти полностью заменило летучие блоки (RX 7800 XT:
|
||||
1,3 ГБ/с перезаливок → 36 МБ/с; у тестера летучих было 455 МБ/с). Блоки, которые процессор
|
||||
пишет часто (3 кадра из 60), по-прежнему уходят на место: перезаливка их перед привязками
|
||||
завершала render pass (видеочип 4,8 → 7,2 мс за кадр). `BB_LAYER_WATCH_HOT=1` — эксперимент
|
||||
с ними.
|
||||
6. Чтения файлов рантайма (`read`/`pread`) повторяются, если ядро вернуло `EFAULT`: ловушку могли
|
||||
взвести снова между касанием страниц и чтением.
|
||||
|
||||
Проверка на RX 7800 XT с `BB_VRAM_LIMIT_MB=5600` (пороги сборщика как у тестера: 1960/3920):
|
||||
старая модель ~195 FPS; модуль ~200–213 FPS, видеочип 4,6–5,2 мс; VRAM держится ниже
|
||||
критической отметки; маршрут `docs/route2.txt` без кадров дольше 25 мс; 3 минуты прогулки без
|
||||
ошибок; картинка в Сне Охотника и в конце маршрута как без наблюдения. Сама NVIDIA этими
|
||||
замерами не проверена: на AMD чтения на месте почти бесплатны.
|
||||
|
||||
## Отзывы на 0.5-pre3 и исправления (`0.5-pre4`)
|
||||
|
||||
- **GTX 1060 6 ГБ:** новая модель вровень со старой (41–48 FPS при ~1500 вызовах за кадр в обеих,
|
||||
видеочип 21–24 мс). Ранние просадки — компиляция шейдеров в первой сессии (160–241 компиляция
|
||||
за 5 с, рывки 1–3,7 с): теперь новые конвейеры проходов, рисуемых каждый кадр, собираются в
|
||||
фоне (`BB_ASYNC_PIPELINES`), а их отрисовки кадр-другой пропускаются.
|
||||
- **Карта на 4 ГБ:** через ~4,5 минуты 5 FPS — регрессия pre3. Переносы в VRAM вставали за
|
||||
384 МиБ до критической отметки сборщика текстур (у карты 4 ГБ — нижние значения порогов,
|
||||
1536/3072 МиБ), а текстуры и служебные выделения уже держали 2808 МиБ: копий стало 124 МиБ на
|
||||
1,9 ГБ данных, видеочип ~200 мс на кадр. Теперь у зеркал своя доля (25 % бюджета драйвера,
|
||||
`BB_LAYER_MIRROR_FLOOR_PERCENT`): ниже неё пауза из-за сборщика не действует.
|
||||
- **После телепорта ~10 FPS меньше:** большие привязки на всю память (через таблицу страниц)
|
||||
помечали каждые 2 МиБ как используемые, поэтому в игре ничего не простаивало, и зеркала
|
||||
покинутой локации держали VRAM. Теперь учитываются только прямые привязки
|
||||
(`layer_group_use`): при нехватке VRAM сначала уходят данные, к которым напрямую не
|
||||
привязывались 3 с, а зеркала с памятью без действительных блоков пересоздаются только поверх
|
||||
действительных (`LayerCompactMirrors`) — иначе частично вернувшееся на место зеркало держало
|
||||
всю свою память. На RX 7800 XT с `BB_VRAM_LIMIT_MB=4200–4600` это освобождало 160–490 МиБ за
|
||||
раз, картинка не менялась. На NVIDIA не проверено.
|
||||
|
||||
## Переключатели и диагностика
|
||||
|
||||
| Переменная | Что делает |
|
||||
|---|---|
|
||||
| `BB_LAYER_MEMORY=1/0` | модуль памяти вкл/выкл (по умолчанию: на не-AMD вкл) |
|
||||
| `BB_LAYER_GPU_WRITES=0/1/2` | где видеочипу можно писать в VRAM (см. выше), по умолчанию 1 |
|
||||
| `BB_LAYER_WRITE_TRAPS=0` | без ловушек записи (только для сравнения: вернутся взрывы) |
|
||||
| `BB_LAYER_VOLATILE=1` | с летучими блоками (с 2026-10-10 выключены по умолчанию: их копии разрывали проходы рендера) |
|
||||
| `BB_LAYER_PAGED=0` | без таблицы страниц для привязок от 64 МиБ |
|
||||
| `BB_COPY_SHADER_NATIVE=0/1` | HLE / нативный шейдер копирования игры; в новой модели по умолчанию нативный |
|
||||
| `BB_RESIDENCY_MIX=1/2` | привязки на месте / в VRAM / смешанные; `2` — ещё и самые тяжёлые с разбором блоков |
|
||||
| `BB_LAYER_WRITE_LOG=1` | крупные записи видеочипа, оставленные на месте, и образы рядом |
|
||||
| `BB_TOGGLE_FILE`, бит эксперимента 8 | чтение на месте вместо копий в VRAM (проверка устаревших копий) |
|
||||
| `BB_TOGGLE_FILE`, бит эксперимента 16 | без обновлений летучих блоков |
|
||||
| `BB_LAYER_WATCH=0` | без наблюдаемых блоков (блоки, где писал процессор, читаются на месте) |
|
||||
| `BB_LAYER_WATCH_HOT=1` | наблюдать и часто записываемые процессором блоки (эксперимент) |
|
||||
| `BB_LAYER_MIRROR_GROWTH_MB=N` | запас роста зеркала, по умолчанию 64; `0` — без запаса |
|
||||
| `BB_VRAM_PRESSURE_IDLE_SECONDS=N` | при нехватке VRAM на место уходят данные, к которым напрямую не привязывались N с (3; `0` — нет) |
|
||||
| `BB_LAYER_MIRROR_FLOOR_PERCENT=N` | доля бюджета VRAM, которую сборщик текстур не забирает у зеркал (25) |
|
||||
| `BB_ASYNC_PIPELINES=0` | новые конвейеры компилируются сразу, игра ждёт (по умолчанию — в фоне для проходов, рисуемых каждый кадр) |
|
||||
| `BB_ASYNC_PIPELINE_THREADS=N` | число потоков фоновой компиляции (по умолчанию четверть потоков процессора, 1–4) |
|
||||
| `BB_VRAM_LIMIT_MB=N` | тест: бюджет VRAM драйвера считается не больше N МиБ (имитация карты на 6 ГБ: 5600) |
|
||||
| `BB_RESIDENCY_MIX=1` | строка `Layer in place` теперь также говорит, почему блоки не наблюдаются |
|
||||
|
||||
## Что дальше
|
||||
|
||||
1. Лог с NVIDIA.
|
||||
2. Найти причину медленных запусков (или доказать, что это разная сцена).
|
||||
3. Летучие блоки обновляются на 0,9–1,6 ГБ/с — дешевле было бы обновлять их только после
|
||||
записи процессора (ловушки уже есть).
|
||||
4. Если модуль на NVIDIA подтвердится, перевести на него и AMD и убрать sparse-арену из новой
|
||||
модели.
|
||||
+94
-33
@@ -1,37 +1,56 @@
|
||||
# Online play and co-op
|
||||
|
||||
Online play is a beta. Messages, bloodstains and ghosts come from the community game server,
|
||||
[The Hunter's Dream](https://thehuntersdream.com). Bells and summons go through a co-op pool,
|
||||
Online play is a beta. Messages, bloodstains and ghosts come from a community game server:
|
||||
Seamless Hunt's (the default) or [The Hunter's Dream](https://thehuntersdream.com). Bells and summons go through a co-op pool,
|
||||
and then the players' games connect to each other. Linux and Windows players can play together:
|
||||
everyone needs the game at version 1.09 and the same pool.
|
||||
|
||||
> **shadPS4's public shadNet server no longer accepts bbport builds**: it signs the game out
|
||||
> right after the login ("This shadNet server doesn't accept your shadPS4 build" in the log), and
|
||||
> the launcher says so. The two pools for bbport are The Hunter's Dream's and a server of your own.
|
||||
> right after the login ("The shadNet server doesn't accept this shadPS4 build" in the log), and
|
||||
> the launcher says so. The pools for bbport are Seamless Hunt (the default), The Hunter's Dream's and a server of your own.
|
||||
|
||||
## Seamless Hunt
|
||||
|
||||
[Seamless Hunt](https://install.seamlesshunt.com) is a community server for Seamless Co-op (BBSC),
|
||||
where Windows players join with BBSC's package for the Windows port.
|
||||
|
||||
1. In the launcher's **Multiplayer → Online** tab, turn on **Play online** and choose **Seamless Hunt**: its
|
||||
shadNet server, WebAPI and game server come with it.
|
||||
2. Type a name and a password under **Account** and press **Create** (once). The server wants an
|
||||
email for each account; the launcher makes one from the name, as Seamless Hunt's own tool does.
|
||||
3. Turn on BBSC in the **Seamless co-op (BBSC)** group and press **Test** in the **Status** row.
|
||||
|
||||
Its bloodstains and death visions crash the game with "Transaction failed", on Windows too: they
|
||||
are off by default, as in Seamless Hunt's package (**Bloodstains on Seamless Hunt** brings them
|
||||
back). Messages, ghosts and summon signs are not affected.
|
||||
|
||||
## The Hunter's Dream (with bbhost's players)
|
||||
|
||||
Choose **The Hunter's Dream (with bbhost players)** in the launcher's **Online** page.
|
||||
[bbbridge](../bbbridge/README.md) by MaskZeus starts with the game, stands in for a shadNet server
|
||||
on this computer and talks to The Hunter's Dream the way bbhost does, its relay included, so no
|
||||
router port is needed. The account is The Hunter's Dream's, kept in `user/bbbridge` (no password):
|
||||
Choose **The Hunter's Dream (with bbhost players)** in the launcher's **Multiplayer → Online** tab.
|
||||
The game's online module talks to The Hunter's Dream itself, the way bbhost does (from bbport 0.7):
|
||||
nothing else runs next to the game. Its account is your Discord sign-in, the same one bbhost uses:
|
||||
|
||||
- **Sign in with Discord**: the launcher shows a code and opens The Hunter's Dream's page; approve
|
||||
it there. The account is the same one bbhost uses.
|
||||
- **Without Discord**: type a name and **Create**. Keep the recovery code the launcher shows:
|
||||
with the name, it brings the account back on another computer (**Recover**).
|
||||
1. Press **Sign in with Discord**. The launcher shows a code, copies it and opens The Hunter's
|
||||
Dream's page.
|
||||
2. On that page, press *Sign in with Discord to link* and sign in; back on the page, enter the code
|
||||
in the code field (not in *Sign in with a code from the game*).
|
||||
|
||||
The game's play log (its own telemetry) stays blocked, so The Hunter's Dream's live map does not
|
||||
show you.
|
||||
No password is kept: the launcher keeps the sign-in's token with its settings. **Sign out** forgets
|
||||
it. **Test** checks The Hunter's Dream's game server and its co-op service. Seamless co-op (BBSC)
|
||||
is not available there.
|
||||
|
||||
Report problems here ([issues](https://github.com/ayozetr/bloodborne_linux/issues)), not to The
|
||||
Hunter's Dream's support.
|
||||
|
||||
## Your own server
|
||||
|
||||
Its owner creates the account ([SERVER.md](SERVER.md)).
|
||||
Its owner creates the account ([hosting your own server](SERVER.md)), or you make it with **Create** under
|
||||
**Account** when the server lets anyone register.
|
||||
|
||||
1. In the launcher's **Online** page, turn on **Play online**, choose **Your own server** with
|
||||
1. In the launcher's **Multiplayer → Online** tab, turn on **Play online**, choose **Your own server** with
|
||||
its address, and enter the **account name** (not the email) and the password. The password is
|
||||
kept on this computer with the launcher's settings.
|
||||
2. **Test the connection**: the game server, the shadNet server, its UDP port and its WebAPI
|
||||
2. Press **Test** in the **Status** row: the game server, the shadNet server, its UDP port and its WebAPI
|
||||
should all answer, and with the account filled in, the sign-in should work. A wrong name or
|
||||
password shows there (the server does not say which of the two).
|
||||
|
||||
@@ -39,10 +58,56 @@ If the game says you are not signed in to PlayStation Network, the shadNet login
|
||||
then has a shadNet login error (code 8 is a wrong name or password). If the server did not answer
|
||||
when the game started, the game keeps trying in the background.
|
||||
|
||||
## Seamless co-op (BBSC)
|
||||
|
||||
[BBSC](https://github.com/Classmg22/BBSC) (Bloodborne Seamless Co-op, by CLS_AMG) keeps a co-op
|
||||
session going through deaths, lamps, areas, bosses and the Hunter's Dream, for up to 5 hunters:
|
||||
the guest follows the host through lamps, sees the host's world (doors, bosses, lifts), keeps the
|
||||
items and echoes it gets, and enemies get stronger for every guest. It runs inside the game's
|
||||
online module (`gpu/bbnet20/bbsc`, BBSC 0.0.2 from bbport 0.7). It is new, and its own author
|
||||
marks much of it as untested.
|
||||
|
||||
1. In the launcher's **Multiplayer → Online** tab, **Turn on BBSC** in the **Seamless co-op (BBSC)**
|
||||
group (on by default). While it is on, BBSC's bells replace the game's; turned off, the game's
|
||||
come back the next time the character is loaded (the save is copied to `save-backups` first).
|
||||
**BBSC settings**, folded under it, has BBSC's own options: what the guest shares with the host,
|
||||
five hunters, enemy scaling and its percentages per guest, shared progress and the rest; only
|
||||
the ones changed from BBSC's defaults are passed to the game.
|
||||
2. Every player of the session needs BBSC, the same version: on Linux this port, on Windows
|
||||
BBSC's package for the Windows port (its *BBport* release). A player without it plays the
|
||||
normal game.
|
||||
3. The server decides how much of it works:
|
||||
- **BBSC's game server** (Seamless Hunt's, which comes with it, or your group's,
|
||||
[hosted with it](SERVER.md#seamless-co-op-bbsc)): everything, summons across areas and 5 hunters included. Set it as the
|
||||
**Game server** of the Online tab.
|
||||
- **The Hunter's Dream** has no BBSC: the normal game's co-op, with bbhost's players.
|
||||
|
||||
With BBSC and BBSC's game server, two players who cannot reach each other directly (CGNAT,
|
||||
strict routers, two games on one PC) are connected through that server's UDP relay (port
|
||||
3478); `SHADPS4_P2P_RELAY=0` turns it off.
|
||||
|
||||
BBSC gives every character its own items, which cost no Insight: the **Hunt Bell** (call other
|
||||
players), the **Kin Bell** (join another player), the **Bloodlust Bell** (invade), the **Duel Bell**
|
||||
(PvP on and off during co-op) and the **Parting Blank** (end co-op). It takes the game's Beckoning
|
||||
Bell, Small Resonant Bell and Silencing Blank out of the inventory, and they do not come back when
|
||||
BBSC is turned off; BBSC's own items stay, with their names (scripts/bbsc_kit_texts.py), and work again
|
||||
when it is back on. Before changing anything it backs the saves up to `user/bbsc/save-backups/`:
|
||||
copy a character's save from there to play it without BBSC with its old bells. Its texts follow the
|
||||
language the game shows its text in (*Game text language* in the launcher, else the system's):
|
||||
Spanish, French, German, Italian, Portuguese and Russian; English otherwise. **F8** puts you back
|
||||
on the ground if you get stuck inside another player.
|
||||
|
||||
**Not yet tested with more than one player on Linux:** one player alone works (its items, deaths,
|
||||
lamps, the Hunter's Dream). Report what you find with the log.
|
||||
|
||||
Its log is in `user/bbsc/logs/` (in the AppImage, `~/.local/share/bbport/user/bbsc/logs/`), and
|
||||
the game's log has its warnings (`BBSC:` lines). Not in the Linux build: BBSC's cinematic camera
|
||||
(L3 + R3 opens bbport's menu) and its own Discord status (bbport has one).
|
||||
|
||||
## Summoning in the game
|
||||
|
||||
- The host rings the **Beckoning Bell** (one Insight each time); the helper rings the **Small
|
||||
Resonant Bell**.
|
||||
Resonant Bell**. With BBSC: the **Hunt Bell** and the **Kin Bell**, at no Insight cost.
|
||||
- Use the same **password** in the game's network settings, so you find each other whatever
|
||||
your levels.
|
||||
- Be in the same area, close to each other, with the area's boss still alive in the host's
|
||||
@@ -51,26 +116,24 @@ when the game started, the game keeps trying in the background.
|
||||
## Ports
|
||||
|
||||
Co-op traffic is UDP, straight between the players' computers. With your own server on
|
||||
Tailscale ([SERVER.md](SERVER.md)) it goes through Tailscale: turn **UPnP** off and skip this
|
||||
Tailscale ([hosting your own server](SERVER.md)) it goes through Tailscale: turn **UPnP** off and skip this
|
||||
section. Otherwise each player has to be reachable from the Internet:
|
||||
|
||||
1. **The router.** With **UPnP** on (the default), the game opens its port on the router itself:
|
||||
the log says `UPNP: mapped UDP port <port>`. A router without UPnP needs a port forward by
|
||||
hand (UDP, the pinned port below, to your computer's LAN address).
|
||||
2. **Pin the port** when you open it by hand or in a firewall: **Co-op port (UDP)** in the
|
||||
Online page, for example 3658 (0, the default, is a random port each time). The log then says
|
||||
`P2P transport ready: UDP physical_port=3658`.
|
||||
the log says `P2P on UDP port <port>: asking the router to forward it (UPnP)`. A router
|
||||
without UPnP needs a port forward by hand (UDP, the port below, to your computer's LAN
|
||||
address).
|
||||
2. **The port** to open by hand or in a firewall is UDP 3658, the PS4's own, which the game uses
|
||||
by default (**Co-op port (UDP)** in the Online tab: 0 keeps it). The log says
|
||||
`P2P transport on UDP port 3658`.
|
||||
3. **Open it in the firewall.** UPnP opens the port on the router only, not in this computer's
|
||||
firewall. With ufw or firewalld on, the launcher's **Online** page says
|
||||
firewall. With ufw or firewalld on, the launcher's **Multiplayer → Online** tab says
|
||||
so and gives the commands. For ufw:
|
||||
```sh
|
||||
sudo ufw allow 3658/udp
|
||||
sudo ufw allow from <router IP> proto udp # the router's UPnP answers
|
||||
```
|
||||
On Windows, in a terminal as administrator:
|
||||
```
|
||||
netsh advfirewall firewall add rule name="Bloodborne co-op" dir=in action=allow protocol=UDP localport=3658
|
||||
```
|
||||
Windows players open the same UDP port in their own firewall.
|
||||
|
||||
A router whose public address is shared by many customers (CG-NAT) cannot forward ports: the
|
||||
log's `UPNP: IGD found - external IP=...` should be your real public address. Tailscale works
|
||||
@@ -82,9 +145,7 @@ Run with `BB_GPU_LOG=info` in the extra variables for the online details. In a s
|
||||
|
||||
| Line | Meaning |
|
||||
|---|---|
|
||||
| `Logged in npid='...'` | The shadNet login worked. |
|
||||
| `signed in npid='...'` | The shadNet login worked. |
|
||||
| `summon_messenger/create` | Your sign is placed (helper). |
|
||||
| `summon_messenger/get`, `sceNpMatching2CreateJoinRoom`, `summon_messenger/request` | The host found the sign, made the room and sent the summon. |
|
||||
| `peer '...' endpoint unresolved` | shadNet has no address for the other player: its UDP port did not answer. |
|
||||
| `Online: the shadNet server ... does not accept this build` | The server refuses bbport (shadPS4's public one): use The Hunter's Dream or your own server. |
|
||||
| `event=0(DEAD)` | The connection gave up. |
|
||||
| `signed out by the server` | The server refuses this build (shadPS4's public one): use Seamless Hunt, The Hunter's Dream or your own server. |
|
||||
+26
-27
@@ -2,39 +2,38 @@
|
||||
|
||||
## For players
|
||||
|
||||
The same documents, with a few pages written for players, make up the
|
||||
[players' guide](https://ayozetr.github.io/bloodborne_linux/) (built from this folder: `mkdocs.yml`; published with `tools/publish_guide.sh`).
|
||||
|
||||
- [getting-started.md](getting-started.md): what you need, the first launch, saves and updates.
|
||||
- [steam-deck.md](steam-deck.md): Add to Steam, the Steam Deck's Game Mode, Steam Input and the overlay.
|
||||
- [logs.md](logs.md): getting a log, also from Game Mode, and sending it.
|
||||
- [faq.md](faq.md): the questions players ask most.
|
||||
- [COMPATIBILITY.md](COMPATIBILITY.md): which releases of Bloodborne run, and which have been
|
||||
checked.
|
||||
- [MODS.en.md](MODS.en.md): file mods and third-party patches.
|
||||
- [ONLINE.md](ONLINE.md): online play and co-op: the account, the bells, the ports.
|
||||
- [SERVER.md](SERVER.md): hosting your own co-op server (shadnet-p2p) for a group of friends.
|
||||
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md): common problems and what to do.
|
||||
- [ONLINE.md](ONLINE.md): online play and co-op: the account, the bells, the ports.
|
||||
- [SERVER.md](SERVER.md): hosting your own co-op server for a group of friends.
|
||||
- [MODS.en.md](MODS.en.md): file mods and third-party patches.
|
||||
|
||||
## For this fork
|
||||
## For developers
|
||||
|
||||
- [ENVIRONMENT.md](ENVIRONMENT.md): every environment variable the port reads, with its default
|
||||
and where in the code it is read.
|
||||
- [DEBUGGING.md](DEBUGGING.md): catching a crash with a debugger (`BB_FAULT_WAIT`).
|
||||
- [RELEASE_TEMPLATE.md](RELEASE_TEMPLATE.md): the format of every release's notes.
|
||||
- [CREDITS.md](CREDITS.md): whose work is in the port, and its licenses.
|
||||
|
||||
## bbport's development notes
|
||||
## Design notes from upstream bbport
|
||||
|
||||
Everything else here comes from [bbport](https://github.com/deadinside28/bloodborne_pc) by
|
||||
deadinside28, kept as it is so that its updates merge cleanly. Many are in Russian (`.ru.md`, or
|
||||
a Russian title).
|
||||
The port is built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28. These
|
||||
notes explain parts of its engine that this port still runs, and the code points to them. Some
|
||||
describe plans or measurements at the date in their title.
|
||||
|
||||
- **What changed in each version:** [CHANGES_0.5.md](CHANGES_0.5.md),
|
||||
[CHANGES_0.5-pre4.md](CHANGES_0.5-pre4.md), [CHANGES_0.5-pre3.md](CHANGES_0.5-pre3.md),
|
||||
[CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md), [CHANGES_after_0.4.md](CHANGES_after_0.4.md),
|
||||
[CHANGES_0.4.md](CHANGES_0.4.md), [CHANGES_2026-10-06.md](CHANGES_2026-10-06.md) (0.3),
|
||||
[CHANGES_2026-10-03.md](CHANGES_2026-10-03.md), [CHANGES_2026-10-02.md](CHANGES_2026-10-02.md).
|
||||
- **Where the port is going:** [ROADMAP.md](ROADMAP.md),
|
||||
[EMULATION_REMOVAL_PLAN.ru.md](EMULATION_REMOVAL_PLAN.ru.md),
|
||||
[TRANSLATOR_ARCHITECTURE.ru.md](TRANSLATOR_ARCHITECTURE.ru.md).
|
||||
- **Memory and GPU commands:** [MEMORY_MODULE_PLAN.ru.md](MEMORY_MODULE_PLAN.ru.md),
|
||||
[NVIDIA_MEMORY_MODEL.ru.md](NVIDIA_MEMORY_MODEL.ru.md),
|
||||
[TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md),
|
||||
[parallel_gpu.md](parallel_gpu.md).
|
||||
- **Upscaling, TAA and motion vectors:** [upscaler.md](upscaler.md),
|
||||
[DEPTH_ADAPTIVE_TAA.md](DEPTH_ADAPTIVE_TAA.md),
|
||||
[TEMPORAL_REVIEW_2026-10-01.md](TEMPORAL_REVIEW_2026-10-01.md),
|
||||
[motion_vectors.md](motion_vectors.md).
|
||||
- **Mods, patches and TAA sharpness** (the original of MODS.en.md): [MODS.md](MODS.md).
|
||||
- **The first experiments:** [DEVELOPMENT_LOG.ru.md](DEVELOPMENT_LOG.ru.md), and the session
|
||||
notes in [history/](history/).
|
||||
- **GPU commands in parallel:** [parallel_gpu.md](parallel_gpu.md).
|
||||
- **Upscaling, TAA and frame generation:** [upscaler.md](upscaler.md).
|
||||
- **The translator that replaces emulation:** [TRANSLATOR_ARCHITECTURE.md](TRANSLATOR_ARCHITECTURE.md).
|
||||
- **The GPU layer's memory module:** [MEMORY_MODULE_PLAN.md](MEMORY_MODULE_PLAN.md).
|
||||
|
||||
Upstream's release notes, roadmap, session logs and its other notes (most in Russian) stay in
|
||||
[its docs folder](https://github.com/deadinside28/bloodborne_pc/tree/master/docs).
|
||||
@@ -43,4 +43,5 @@ SHA-256: `<sha256sum of the AppImage>`
|
||||
|
||||
**Full changes:** https://github.com/ayozetr/bloodborne_linux/compare/<previous tag>...<tag>
|
||||
**Credits and licenses:** https://github.com/ayozetr/bloodborne_linux#credits
|
||||
Includes Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine).
|
||||
```
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
# Нативный порт Bloodborne: что сделано и что дальше
|
||||
|
||||
Основной текст — состояние на 2026-09-26; ниже первым разделом — что изменилось к 30.09.
|
||||
Самый подробный срез: [CURRENT_STATUS_2026-09-29.md](history/CURRENT_STATUS_2026-09-29.md).
|
||||
Отчёт о работе 30.09–01.10 (конвейер GPU, апскейлер, падение, лаунчер, AppImage):
|
||||
[SESSION_2026-09-30.md](history/SESSION_2026-09-30.md).
|
||||
Подробности по отдельным темам:
|
||||
[parallel_gpu.md](parallel_gpu.md) (многопоточность GPU, подтормаживания при подгрузке),
|
||||
[upscaler.md](upscaler.md) (анализ кадра игры, константы сцены).
|
||||
|
||||
## Обновление 30.09
|
||||
|
||||
- Сделано после 26.09: FSR 4 INT8 (v07), векторы движения объектов, смена пресета без
|
||||
перезапуска (уменьшенные цели сцены; глубина D32S8 копируется шейдером), LTO и PGO,
|
||||
масштабирование подготовки draw под число потоков (Steam Deck тоже).
|
||||
- Проблемы из раздела «Известные проблемы» ниже: пункт 3 (перезапуск при смене пресета)
|
||||
решён; пункт 1 (векторы объектов) реализован, у FSR 4 остаётся гостинг на персонаже при
|
||||
Ultra Performance; пункт 2 (мыльный интерфейс) — UI рисуется в 1920×1080.
|
||||
- Ограничение: уменьшенный рендер сцены (`SceneTargets::Eligible`) требует цель 1920×1080,
|
||||
то есть работает только при выводе 1080p.
|
||||
- Распараллеливание потока GPU-команд (шаг 2 [parallel_gpu.md](parallel_gpu.md)): текстуры на
|
||||
отдельном потоке параллельно с буферами работают корректно, но прироста не дают —
|
||||
данные каждого draw переезжают между ядрами. Узкое место — поток GPU-команд: 91% на CPU,
|
||||
~9% ожидания мелких копий гостевой памяти перед fence. Дальше — двухстадийный конвейер
|
||||
(второй поток владеет текстурным кешем и записью draw, копия регистров из дельт сканера)
|
||||
или мемоизация привязок текстур между кадрами; см. «What would scale» в parallel_gpu.md.
|
||||
- **Двухстадийный конвейер draw** (`vk_draw_pipe.h`): поток GPU-команд декодирует PM4 и
|
||||
выбирает пайплайн, второй поток (`bb:DrawRec`) со своей копией регистров привязывает ресурсы
|
||||
и записывает draw. +19% FPS на 16 потоках, +18% на 8 (как Steam Deck). Включён по умолчанию
|
||||
при 8+ потоках (`BB_DRAW_PIPE=0/1`). Узкое место теперь — вторая стадия; подробности и план —
|
||||
в parallel_gpu.md.
|
||||
- Пресеты и нагрузка GPU: сцена на RX 7800 XT стоит мало, поэтому пресет меняет время GPU
|
||||
лишь на ~0.5 мс, а сам FSR 4 при выводе 1080p — ~1.4 мс (FSR 4 Ultra Performance дороже
|
||||
нативного рендера без апскейлера). Исправлено: проходы освещения оставались в 1920×1080 при
|
||||
любом пресете (проверка смотрела на чужие слоты целей); уменьшенные цели теперь сэмплируются
|
||||
напрямую, без ресэмплинга в натив (кэш шейдеров пересобирается один раз). Следующее для
|
||||
Steam Deck — гостевая копия памяти целей рендеринга `3d5ebf4e` (см. parallel_gpu.md).
|
||||
- **Исправлено падение с конвейером** (порча гостевой кучи, guest offset `0x263b8e7`, через
|
||||
13–17 минут): прерывание «GPU простаивает» уходило раньше меток EOP, отложенных в поток записи
|
||||
Vulkan, и игра освобождала объекты с этими метками. Теперь перед ним конвейер и отложенные
|
||||
метки дожидаются; 2 прогона по 30 минут без падений (до исправления — 7 из 7 падений).
|
||||
- **Разрешение вывода 1440p/2160p** (меню «Разрешение вывода», bbport.ini `output_res`, после
|
||||
перезапуска): сцена рисуется в разрешении пресета от выхода, апскейлер (FSR 3 или FSR 4)
|
||||
дорисовывает кадр, интерфейс растеризуется в выходном разрешении. Эффекты игры (аберрация,
|
||||
DoF, размытие, SSAO, сглаживание игры, динамические тени, SSR, заставки, LOD) — там же.
|
||||
- **Лаунчер** (`launcher/bb-launcher.sh`, GTK4/libadwaita) и **AppImage** для Steam Deck
|
||||
(`bash packaging/appimage.sh` → `dist/`; данные в `~/.local/share/bbport`, `--play` —
|
||||
запуск без окна лаунчера).
|
||||
- Инструменты: `BB_PAD_FILE` (скриптовый ввод геймпада), `ab.sh` (A/B битов
|
||||
`BB_TOGGLE_FILE` в запущенной игре), в `Frame stats` — простой и блокировки потока GPU.
|
||||
|
||||
## Как устроен порт
|
||||
|
||||
- Оригинальный eboot (CUSA03173 v01.09) исполняется напрямую на x86-64. Системные библиотеки
|
||||
PS4 заменены своим HLE-рантаймом на C (`runtime_*.c`, `probe.c`).
|
||||
- Графика: видеоядро и рекомпилятор шейдеров shadPS4 (GPL), собранные в `libbbgpu.so`
|
||||
(`gpu/`), с собственными доработками. Эмулятор shadPS4 целиком не используется.
|
||||
- Патчи игры (FPS, разрешение и др.) — XML сообщества (`patches/Bloodborne.xml`),
|
||||
компилируются `patches.py` и накладываются загрузчиком при старте.
|
||||
|
||||
## Сделано
|
||||
|
||||
### Производительность и плавность
|
||||
|
||||
| Что | Результат |
|
||||
|---|---|
|
||||
| Запись команд Vulkan в отдельном потоке, кеши текстур/буферов, rwlock вместо мьютекса | 26 → 64 FPS в одной сцене |
|
||||
| Подготовка draw-вызовов на 4 потоках (`bb:DrawPrep`), проверка на потоке GPU | +11.5% |
|
||||
| Окно снятия защиты 256 КиБ при записи игры в память (меньше page fault) | +22% |
|
||||
| Кеш описаний текстур, быстрый путь для тех же render target | до ~94 FPS на улице |
|
||||
| Копирования гостевой памяти на пуле потоков, пул staging-буферов | фризы при подгрузке 60–170 мс → 40–50 мс |
|
||||
| Ожидание копий перед видимыми игре fence | убрано мерцание интерфейса |
|
||||
| Режим «Readbacks Relaxed» | нет «взрывов вершин» (FaceGen) |
|
||||
| Vblank 480 Гц + лимит кадров до min(монитор, 120), вывод сразу по флипу | ровная подача кадров; бег не замедляется выше 120 FPS |
|
||||
|
||||
Всё переключается во время игры через `BB_TOGGLE_FILE` (битовая маска) для A/B-сравнений.
|
||||
Статистика: `BB_FRAME_STATS=1`.
|
||||
|
||||
### Звук, ввод
|
||||
|
||||
- Вывод звука с правильным темпом (раньше щелчки каждые 10.7 мс).
|
||||
- Геймпад через SDL3, запасная раскладка клавиатуры.
|
||||
|
||||
### Анализ кадра
|
||||
|
||||
- `BB_CAPTURE_TRIGGER=<файл>` записывает один кадр: проходы, цели, шейдеры, текстуры,
|
||||
константы, режимы смешивания.
|
||||
- Разобрано устройство кадра Bloodborne (G-buffer, свет, цвет сцены RGBA16F, туман,
|
||||
пост-обработка, тонмаппинг, сглаживание игры, интерфейс) и 864-байтные константы сцены
|
||||
(матрицы вида и проекции).
|
||||
|
||||
### Временной апскейлер (FSR 3.1)
|
||||
|
||||
- **Векторы движения камеры**: из глубины сцены и матриц текущего и прошлого кадра
|
||||
(`vk_camera_motion.cpp`, `camera_motion.comp`). Проверены перепроецированием прошлого
|
||||
кадра: статичная геометрия совпадает.
|
||||
- **Jitter**: субпиксельный сдвиг геометрии сцены через viewport, последовательность
|
||||
Halton(2,3); число фаз 8·(вывод/рендер)². Интерфейс и пост-обработка не сдвигаются.
|
||||
- **FSR 3.1** (FireBurn/FSR-Vulkan, нативный Vulkan, подмодуль `gpu/third_party/fsr-vulkan`):
|
||||
- *Native AA*: на HDR-цвете сцены перед пост-обработкой, 1:1.
|
||||
- *Пресеты Quality/Balanced/Performance/Ultra Performance* (x1.5/1.7/2/3): игра рендерит в
|
||||
пониженном разрешении (патч разрешения, который `patches.py` выводит из патча 720p
|
||||
сообщества), FSR увеличивает готовый кадр до 1920x1080 перед первым проходом интерфейса.
|
||||
Интерфейс и проход вывода перенаправлены в изображения полного размера, презентер
|
||||
показывает их вместо буфера игры.
|
||||
- **Маска реактивности** (снимок сцены до прозрачных отрисовок, сравнение перед наложением
|
||||
тумана): работает, но на дымке Bloodborne даёт дрожание — по умолчанию выключена.
|
||||
- **Внутриигровое меню** (Dear ImGui, Insert или L3+R3): апскейлер, пресет, резкость,
|
||||
jitter, маска, счётчик FPS. Настройки в `bbport.ini`. Смена пресета — кнопка
|
||||
«Применить и перезапустить игру».
|
||||
|
||||
## Известные проблемы
|
||||
|
||||
Состояние последнего коммита. В рабочем дереве подготовлен перенос пресетов на уменьшенные
|
||||
цели рендера при сохранении гостевых буферов, постобработки и UI в 1920×1080. Переходы
|
||||
между пресетами и копирование цвета/глубины/stencil прошли Vulkan-тест на Lavapipe.
|
||||
RX 7800 XT не поддерживает blit D32S8, поэтому новый путь оставлял FSR вход 1920×1080
|
||||
и снижал FPS. Для таких GPU запуск теперь автоматически выбирает проверенный путь с патчем
|
||||
разрешения игры и UI 1920×1080; смена пресета снова требует перезапуска. На GPU с нужной
|
||||
поддержкой остаётся переключение без перезапуска. Пункты 2–3 ниже относятся к последнему
|
||||
коммиту, до этих изменений рабочего дерева.
|
||||
|
||||
Общий проход векторов движения объектов был слишком дорогим: Performance давал около
|
||||
49 FPS с ним и 77–78 FPS без него. Теперь дополнительная цель и шейдеры включаются только
|
||||
для G-buffer отрисовок с буфером позы/костей; остальные используют векторы камеры.
|
||||
В пробном запуске выборочный путь держал около 81–87 FPS, без пропусков из-за ёмкости
|
||||
истории. Векторы объектов включены по умолчанию; `BB_OBJECT_MOTION=0` отключает их, а
|
||||
`BB_OBJECT_MOTION_ALL=1` возвращает старый общий путь для сравнения. Выбор по размеру
|
||||
буфера пока эвристический: качество оружия и одежды в движении требует проверки в игре.
|
||||
|
||||
1. **Движущиеся объекты «рассыпаются»** (оружие на спине, персонаж, ткань, враги): есть
|
||||
только векторы движения камеры. Камера от третьего лица следует за персонажем, поэтому
|
||||
векторы камеры для него неверны, и апскейлер берёт историю не из того места. Это касается
|
||||
любого временного апскейлера (FSR 3/4, DLSS, XeSS).
|
||||
2. **Интерфейс в пресетах мыльный**: с патчем разрешения игра считает себя 540p-игрой и,
|
||||
судя по всему, готовит шрифты и элементы интерфейса под это разрешение. Меню
|
||||
(титульное, пауза) вообще не проходят через апскейлер.
|
||||
3. **Смена пресета требует перезапуска**: разрешение задаётся патчем кода при старте, а
|
||||
буферы игра выделяет один раз.
|
||||
|
||||
## План
|
||||
|
||||
### 1. Векторы движения объектов
|
||||
|
||||
Нужны всем апскейлерам и генерации кадров. Игра их не считает, поэтому считаем сами.
|
||||
|
||||
Как:
|
||||
- Для каждой отрисовки в G-buffer (проходы с 5+ целями) рекомпилятор делает второй вариант
|
||||
вершинного шейдера, который выдаёт позицию дважды: с текущими константами и с константами
|
||||
прошлого кадра. Разница проекций — вектор движения, пишется во вспомогательную цель
|
||||
RG16F как дополнительный выход пиксельного шейдера.
|
||||
- Константы прошлого кадра: для каждого draw запоминаем содержимое константных буферов
|
||||
(матрица объекта, кости скелета) по ключу «шейдер + номер draw в проходе» или по адресу
|
||||
данных. Нужно выяснить, как игра хранит матрицы скелета: в буферах, которые перезаписываются
|
||||
каждый кадр (тогда копируем прошлую версию), или в кольцевом буфере (тогда достаточно
|
||||
адреса прошлого кадра).
|
||||
- Где объект не найден (первый кадр, новый объект) — векторы камеры, как сейчас.
|
||||
- Проверка: отладочный вид векторов (`BB_DEBUG_MOTION`), перепроецирование прошлого кадра
|
||||
по полным векторам, визуально — пила на спине при беге.
|
||||
|
||||
### 2. Масштабирование на уровне рендерера (вместо патча разрешения)
|
||||
|
||||
Игра работает в 1920x1080 (или в разрешении монитора), а в пониженном разрешении рисуются
|
||||
только проходы 3D-сцены. Интерфейс и меню остаются чёткими, пресет меняется на лету.
|
||||
|
||||
Как:
|
||||
- Кеш текстур выделяет для целей сцены (G-buffer, глубина, свет, цвет сцены, эффекты) копию
|
||||
уменьшенного размера; гостевая память и описания игры не меняются.
|
||||
- Viewport и scissor проходов в эти цели масштабируются (как уже сделано для интерфейса,
|
||||
включая проходы с отключённым отсечением).
|
||||
- Шейдеры, которые считают по пиксельным координатам (`gl_FragCoord`, `texelFetch`,
|
||||
compute-шейдеры по пикселям), получают коэффициент масштаба через push-константы
|
||||
рекомпилятора. Выборки по нормированным координатам работают без изменений.
|
||||
- FSR ставится туда же, где Native AA: цвет сцены перед пост-обработкой (HDR, лучшее
|
||||
качество); пост-обработка и интерфейс идут в полном разрешении.
|
||||
- Риски: проходы, читающие цели сцены по пиксельным координатам в пост-обработке, и
|
||||
чтения из гостевой памяти этих целей (их нужно запретить или масштабировать обратно).
|
||||
|
||||
### 3. Другие апскейлеры
|
||||
|
||||
Все используют одни и те же входы (цвет, глубина, векторы движения, jitter, маски).
|
||||
|
||||
- **FSR 4 (INT8)**: в FSR-Vulkan есть экспериментальный провайдер FSR 4 v07 INT8 — добавить
|
||||
как вариант в меню и сравнить с FSR 3.1 после шага 1.
|
||||
- **DLSS**: нативный Linux Vulkan SDK (NGX). На RX 7800 XT не работает, но нужен для
|
||||
пользователей NVIDIA.
|
||||
- **XeSS / XeFG и OptiScaler**: Intel поставляет их только как Windows DLL. Варианты:
|
||||
загрузчик Windows DLL (PE) внутри процесса либо запуск всей сборки под Proton с
|
||||
OptiScaler, которому отдаём входы через интерфейс DLSS.
|
||||
Проверено 29.09: libxess.dll (одинаковая у DOOM: The Dark Ages, PRAGMATA и др.) имеет
|
||||
Vulkan-путь (`xessVKCreateContext`, `xessVKExecute`), но импортирует ~300 функций:
|
||||
MSVCP140 (iostream), VCRUNTIME140 (C++-исключения), UCRT, kernel32, setupapi, dxgi.
|
||||
Решено отложить; самый реалистичный путь — хелпер под Wine с общей памятью Vulkan.
|
||||
|
||||
### 4. Генерация кадров
|
||||
|
||||
- FSR 3.1 Frame Generation (есть в FSR-Vulkan): промежуточный кадр между двумя готовыми,
|
||||
по тем же векторам движения; интерфейс нужно рисовать поверх отдельно (есть перенаправление
|
||||
интерфейса — его можно рендерить в отдельный слой).
|
||||
- XeFG — после решения с Windows DLL (см. выше).
|
||||
- Требование: подача кадров через презентер (очередь и VRR уже работают).
|
||||
|
||||
### 5. Прочее
|
||||
|
||||
- Steam Deck: портативная сборка (patchelf, без Nix), проверка пресетов на 1280x800.
|
||||
- Дальнейшая разгрузка потока GPU (шаг 2 из [parallel_gpu.md](parallel_gpu.md)).
|
||||
+44
-18
@@ -1,14 +1,14 @@
|
||||
# Hosting your own co-op server
|
||||
|
||||
Bells and summons go through a shadNet server. shadPS4's public one (`srv.shadps4.net`) no longer
|
||||
accepts bbport builds, and the public pool for bbport is now The Hunter's Dream's, through
|
||||
bbbridge (the launcher's **Online** page; [README](../README.md#online-play)). A group of friends
|
||||
accepts bbport builds, and the public pools for bbport are Seamless Hunt (the default) and The
|
||||
Hunter's Dream's (the launcher's **Multiplayer → Online** tab; [README](../README.md#online-play)). A group of friends
|
||||
can also run their own server:
|
||||
[shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) by Wozzardman, the shadNet server with
|
||||
that UDP port working and a summon service of its own.
|
||||
|
||||
Messages, bloodstains and ghosts still come from The Hunter's Dream, whichever shadNet server you
|
||||
use. Windows players of [Supermedo's port](https://github.com/Supermedo/bloodborne_pc) can join
|
||||
With your own shadNet server, messages, bloodstains and ghosts come from the **Game server** field
|
||||
of the Online tab: The Hunter's Dream by default, or BBSC's game server ([below](#seamless-co-op-bbsc)). Windows players of [Supermedo's port](https://github.com/Supermedo/bloodborne_pc) can join
|
||||
the same server.
|
||||
|
||||
Tested with two players, one on this Linux port and one on Supermedo's Windows port, over
|
||||
@@ -133,7 +133,7 @@ To back up the accounts, stop the server and copy the volume's `db/shadnet.db`.
|
||||
|
||||
## 4. Each player's launcher
|
||||
|
||||
In the **Online** page:
|
||||
In **Multiplayer → Online**:
|
||||
|
||||
| Setting | Value |
|
||||
|---|---|
|
||||
@@ -144,26 +144,52 @@ In the **Online** page:
|
||||
| Online ID / Password | The player's own account |
|
||||
| UPnP | Off (Tailscale needs no router ports) |
|
||||
|
||||
Then **Test the connection**. Every line should be green, the shadNet server's UDP among them: it
|
||||
shows the address the server sees for you, a `100.` one through Tailscale. A red UDP line means
|
||||
summons cannot connect: check that the server is running and that its log lists the UDP listener.
|
||||
Then press **Test** in the **Status** row. Every line should get a check mark, the shadNet server's UDP among them: it
|
||||
shows the address the server sees for you, a `100.` one through Tailscale. An error mark on the UDP line
|
||||
means summons cannot connect: check that the server is running and that its log lists the UDP listener.
|
||||
|
||||
Summons go through the server's own summon service: the log of a game says
|
||||
`Co-op: summon signs through http://100.x.y.z:31315`.
|
||||
|
||||
Windows players set the same server, WebAPI and account in the Windows port's launcher.
|
||||
|
||||
## Seamless co-op (experimental)
|
||||
## Seamless co-op (BBSC)
|
||||
|
||||
shadnet-p2p can also summon between different areas: the guest is moved to the host's area. To
|
||||
use it, the server and every player must have it on:
|
||||
For [BBSC](ONLINE.md#seamless-co-op-bbsc) with everything on (summons across areas, 5 hunters),
|
||||
the group also runs BBSC's game server next to shadNet. It is in this repository,
|
||||
`scripts/bbsc_gameserver/bbsc_gameserver.py` (Python 3, nothing to install), and it takes the
|
||||
place of The Hunter's Dream for messages, bloodstains and the server list.
|
||||
|
||||
- The server: add `-e SHADNET_BLOODBORNE_SEAMLESS_COOP=1` to `docker run`, or set
|
||||
`BloodborneSeamlessCoop=true` in `shadnet.cfg`. Its log then says `seamless co-op enabled`.
|
||||
- Each player: **Seamless co-op (experimental)** in the launcher's Online page.
|
||||
1. Turn on shadNet's seamless mode: run the shadNet container again with
|
||||
`-e SHADNET_BLOODBORNE_SEAMLESS_COOP=1` (it is kept in its `shadnet.cfg`).
|
||||
2. Build and start the game server on the same computer, with the same Tailscale address:
|
||||
|
||||
Ring the host's Beckoning Bell first, then the guest's Small Resonant Bell. Travelling together
|
||||
by lantern once the session has started does not work yet.
|
||||
```sh
|
||||
mkdir bbsc-server && cd bbsc-server
|
||||
for f in Dockerfile entrypoint.sh; do
|
||||
curl -fsSLO "https://raw.githubusercontent.com/ayozetr/bloodborne_linux/main/tools/bbsc-server/$f"
|
||||
done
|
||||
docker build -t bbport-bbsc-server .
|
||||
docker run -d --name bbsc-server --restart unless-stopped --network host \
|
||||
-v bbsc-data:/data -e BBSC_HOST=100.x.y.z bbport-bbsc-server
|
||||
docker logs bbsc-server
|
||||
```
|
||||
|
||||
`BBSC_SHADNET` is the shadNet server's address when it is on another computer. Messages,
|
||||
bloodstains and its logs live in the `bbsc-data` volume. Without Docker:
|
||||
`python3 scripts/bbsc_gameserver/bbsc_gameserver.py --shadnet 100.x.y.z --host 100.x.y.z`.
|
||||
|
||||
| Port | Protocol | What |
|
||||
|---|---|---|
|
||||
| 20443 | TCP | The game server (messages, bloodstains, ghosts) |
|
||||
| 20444 | TCP | BBSC's relay (warps, world sync, scaling) |
|
||||
| 18671 | TCP | Its bbhost-compatible service (`BBSC_BBHOST=0`, `--no-bbhost`, turns it off) |
|
||||
| 3478 | UDP | The relay BBSC players fall back to when they cannot reach each other directly |
|
||||
|
||||
Each player then sets **Game server** to `http://100.x.y.z:20443` in the launcher's
|
||||
**Multiplayer → Online** tab, keeps the same shadNet server and account, and turns on BBSC in the **Seamless co-op (BBSC)** group.
|
||||
BBSC finds its relay at the game server's address (or `BBSC_RELAY`, the relay address in BBSC's
|
||||
settings).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -173,8 +199,8 @@ by lantern once the session has started does not work yet.
|
||||
wrong (`tailscale ip -4`).
|
||||
- **The game says it is not signed in to PlayStation Network:** a wrong name or password (code 8 in
|
||||
the log), or two players using one account.
|
||||
- **The test is green but summons fail:** every player's computer must be in the tailnet, not only
|
||||
- **The test passes but summons fail:** every player's computer must be in the tailnet, not only
|
||||
the server; `tailscale ping <player>` from each side. Everyone needs the game at version 1.09
|
||||
and the same co-op mode.
|
||||
|
||||
See also [ONLINE.md](ONLINE.md) for the bells, passwords and log lines of a summon.
|
||||
See also [Online play and co-op](ONLINE.md) for the bells, passwords and log lines of a summon.
|
||||
@@ -1,134 +0,0 @@
|
||||
# Проверка TAA и входов временных апскейлеров — 2026-10-01
|
||||
|
||||
## Состояние проекта
|
||||
|
||||
bbport — специализированный Linux-рантайм для оригинального x86-64 кода Bloodborne
|
||||
CUSA03173 1.09 с переводом GPU-команд в Vulkan на основе shadPS4. Это экспериментальный
|
||||
порт: загрузчик, HLE системных вызовов, звук, ввод, сохранения, игровой интерфейс настроек
|
||||
и temporal upscaling уже реализованы. В этой рабочей копии доступны исходники, собранная
|
||||
библиотека, игровой дамп и сохранение. Git-метаданных проекта нет, поэтому историю
|
||||
изменений и чистоту рабочего дерева проверить нельзя.
|
||||
|
||||
Полное прохождение, переносимость на другие драйверы и Steam Deck этой проверкой не
|
||||
подтверждаются. В документации остаются ограничения: искусственные occlusion query,
|
||||
неполный predication, прозрачные эффекты без собственных векторов движения и гонки
|
||||
в модели FSR 4.1.1 для некоторых нестандартных размеров вывода.
|
||||
|
||||
## Видео и причины
|
||||
|
||||
Исходная запись `video_2026-10-01_21-21-02.mp4`: 94.89 с, 2560×1080, 60 FPS.
|
||||
В ней переключаются TAA, FSR 4.1.1, FSR 4 и FSR 3.1. На архитектуре, тонких контурах
|
||||
и движущемся оружии видна временная нестабильность. В меню включён игровой motion blur;
|
||||
его вклад нельзя приписывать TAA. Файл `video_fixed_taa.mp4` имеет ту же длительность,
|
||||
но другой размер кадра (2560×1082); сама эта запись не подтверждает исправление рендерера.
|
||||
Кадры и последовательности для просмотра лежат в `out/dev/temporal-review/`.
|
||||
|
||||
Выявлены ошибки в коде, независимо от субъективной оценки видео:
|
||||
|
||||
1. **FP16 depth в TAA history.** Шаг half около 1 составляет примерно 0.000488.
|
||||
При near ≈ 0.05 дальние поверхности становятся неразличимыми. Порог проверки истории
|
||||
был меньше этого шага, поэтому история могла как ошибочно отбрасываться, так и смешиваться
|
||||
с другой поверхностью. История теперь RGBA32F.
|
||||
2. **Несопоставимые камеры.** Старая проверка сравнивала глубину прошлого кадра с глубиной
|
||||
текущего. Теперь текущая точка переводится в систему предыдущей камеры, и сравнение
|
||||
выполняется с ожидаемой глубиной там. Допуск масштабируется по перспективной глубине.
|
||||
3. **Обнуление дальних векторов.** `depth >= 0.99999` считалось небом и получало нулевое
|
||||
движение. Для проекции с near ≈ 0.05, far 3000 этот порог затрагивает геометрию примерно
|
||||
от 1875 единиц. Теперь только clear depth 1 считается бесконечным фоном; фон получает
|
||||
вращательное движение камеры без переноса. Дальняя геометрия получает обычную репроекцию.
|
||||
4. **FP16 depth в object motion.** В RGBA16F терялась глубина, используемая для проверки,
|
||||
не перекрыт ли вектор более поздним draw. Буфер и формат графического pipeline теперь
|
||||
RGBA32F; проверка глубины больше не принимает другую дальнюю поверхность с широким
|
||||
фиксированным допуском. Финальные векторы для FSR по-прежнему RG16F.
|
||||
5. **Проверка смешанной глубины на краях.** TAA интерполировал глубину истории между
|
||||
разными поверхностями. Теперь четыре bilinear tap проверяются независимо, цвет собирается
|
||||
только из согласованных tap; при частичном покрытии вес истории уменьшается.
|
||||
6. **Слишком слабое накопление вдали.** Прежнее depth-adaptive снижение веса до 0.8/0.5
|
||||
заменено весом 0.98…0.85 в зависимости от движения при валидной истории (нижний
|
||||
вес достигается при 8 px/frame). Диагностический прогон с весом 0.94 показал
|
||||
остаточные колебания цвета между фазами jitter даже с принятой историей. RGB истории
|
||||
ограничивается диапазоном соседей текущего кадра.
|
||||
7. **Jitter глубины на склонах и тонких деталях.** Глубина теперь переносится в
|
||||
стабильную позицию пикселя по локальному одностороннему градиенту, с защитой от
|
||||
разрывов поверхности. Для motion/depth выбирается ближайшая поверхность в 3×3;
|
||||
цвет и диапазон clipping берутся вокруг исходного цветового пикселя. Это позволяет
|
||||
тонким контурам сохранять историю при чередовании foreground/background под jitter.
|
||||
8. **Нестабильная адресация истории при почти нулевом движении — основная причина
|
||||
обнаруженного дребезга TAA.** Раньше целая и дробная части bilinear-координаты
|
||||
вычислялись после сложения большого номера пикселя с очень маленьким motion.
|
||||
На GPU это давало несогласованные части и выбор соседнего texel. Теперь сначала
|
||||
разделяется сам motion: `base = p + ivec2(floor(motion))`,
|
||||
`fraction = motion - floor(motion)`. Регрессионная проверка на пикселе x=2010
|
||||
с движением около −0.00003 px воспроизводит ошибку старой формулы:
|
||||
цвет 0.250000 вместо ожидаемого 0.739985. Исправленная формула проходит проверку.
|
||||
|
||||
Исправления 3–4 относятся к общим входам TAA/FSR 3.1/FSR 4/FSR 4.1.1. Алгоритмы и
|
||||
ML-ассеты AMD не изменялись. Проверка ABI shader/pipeline и диагностический dump object
|
||||
motion обновлены вместе с форматом ресурса.
|
||||
|
||||
## Проверки и ограничения
|
||||
|
||||
Сборка `bbgpu`, `taa-shader-test`, `camera-motion-test` через `shell.nix`.
|
||||
На Vulkan GPU прошли 18 случаев production TAA shader: reset с NaN, unjitter, накопление,
|
||||
clipping, раскрытие поверхности, выход за кадр, NaN history, точная дальняя глубина,
|
||||
отказ другой дальней поверхности, перенос камеры, край с частично валидной историей,
|
||||
небо и граница небо/геометрия, наклонная глубина при обоих знаках jitter, тонкий контур,
|
||||
снижение persistence при движении и очень малое отрицательное движение на большой координате.
|
||||
Прошли 6 проверок production camera-motion shader:
|
||||
дальняя геометрия, вращение неба, отсутствие переноса неба, исключение jitter,
|
||||
валидный и устаревший object vector. Лог: `out/dev/temporal-review/tests.log`.
|
||||
Дополнительная проверка unjitter: `out/dev/temporal-review/taa-final-tests.log`.
|
||||
Воспроизведение регрессии старой формулы и 18 успешных проверок новой:
|
||||
`out/dev/temporal-review/regression-proof.log`.
|
||||
Финальная проверка игры: `out/dev/temporal-review/subpixel-validation.log`.
|
||||
Все 64 существующих Python-теста прошли (`out/dev/temporal-review/python-tests.log`).
|
||||
|
||||
Live smoke: копия сохранения загружена в Сне Охотника. В одном процессе проверены
|
||||
TAA → Off → FSR 3.1 → FSR 4 v07 → FSR 4.1.1 → TAA, вывод 2560×1440,
|
||||
по шесть финальных кадров на режим и движение камеры между переключениями.
|
||||
FSR работают с входом 1706×960; TAA — с нативным 2560×1440. Контексты AMD
|
||||
подтверждены логом, подмены на FSR 3.1 не было. Процесс завершился штатно;
|
||||
после компиляции pipeline выдерживал лимит 60 FPS. Логи и raw-кадры:
|
||||
`out/dev/temporal-review/subpixel-validation.log`, `out/dev/temporal-review/far-refined/`.
|
||||
Игровые motion blur и AA отключены в тестовой конфигурации.
|
||||
|
||||
При неподвижной камере на участке дальней башни (x=2010…2099, y=400…779)
|
||||
среднее абсолютное изменение RGB между шестью кадрами TAA снизилось с 1.892 до
|
||||
0.299 по шкале 0…255, примерно в 6.3 раза. После возврата к TAA — 0.279.
|
||||
В финальном прогоне FSR 3.1: 0.328, FSR 4: 0.278, FSR 4.1.1: 0.364.
|
||||
Это результат выбранной сцены, а не универсальная оценка качества: разрешение входа
|
||||
FSR отличается от TAA, а до/после здесь сравнивается именно последняя правка
|
||||
адресации истории. Данные: `out/dev/temporal-review/far-refined/stability.json`;
|
||||
предыдущий прогон: `out/dev/temporal-review/final-validation.log`.
|
||||
|
||||
Для диагностики добавлен `BB_TAA_DIAGNOSTICS=1`: альфа выхода TAA содержит вес принятой
|
||||
истории вместо 1. Его можно прочитать в `BB_DUMP_TRIGGER` raw RGBA16F. RGB не меняется,
|
||||
альфа истории по-прежнему содержит depth. Обычный режим сохраняет непрозрачный выход.
|
||||
Дополнительные режимы 2 и 3 выводят текущий цвет или ограниченную историю;
|
||||
диагностический образ истории позволяет сравнить прямое чтение с репроекцией.
|
||||
Эти сравнения выявили ошибку адресации даже при принятой истории и почти нулевом motion.
|
||||
|
||||
Исходники и `out/gpu/libbbgpu.so` обновлены. Пересобран
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage` (832 MiB); пакетный `--vulkan-info`
|
||||
успешно определил RX 7800 XT через RADV. Логи: `appimage-build.log` и
|
||||
`appimage-vulkan.log` в `out/dev/temporal-review/`. Полный игровой прогон выше
|
||||
выполнен с локальной библиотекой; отдельный полный игровой прогон AppImage не выполнялся.
|
||||
|
||||
Эти тесты подтверждают конкретные исправления, но не доказывают исчезновение любого
|
||||
мерцания в игре. Движение объектов по истории вершин, прозрачности, анимированные
|
||||
материалы, выбор LOD и субпиксельная геометрия при сильном уменьшении разрешения
|
||||
по-прежнему требуют проверки в отдельных сценах.
|
||||
|
||||
Цена полной точности: две истории TAA занимают 253 MiB вместо 127 MiB в 4K;
|
||||
object motion — 127 MiB вместо 63 MiB при 4K render. Это также повышает расход
|
||||
пропускной способности памяти. Возможная следующая оптимизация — отдельный R32F depth
|
||||
при сохранении цвета и motion в FP16, с теми же тестами корректности.
|
||||
|
||||
Для регрессий:
|
||||
|
||||
```sh
|
||||
nix-shell shell.nix --run 'ninja -C out/gpu taa-shader-test camera-motion-test && out/gpu/taa-shader-test && out/gpu/camera-motion-test'
|
||||
```
|
||||
|
||||
Подход с движением для всех входных пикселей согласуется с рекомендациями
|
||||
[AMD FSR 3.1 integration](https://gpuopen.com/presentations/2024/FidelityFX_Super_Resolution_3-1_Release-Overview_and_Integration.pdf).
|
||||
@@ -1,42 +0,0 @@
|
||||
# Оригинальное GPU-копирование и переносимые метки
|
||||
|
||||
Срез 09.10.2026. Область проверки: `BB_PC_MODEL=1 BB_LAYER_MEMORY=1`; новая модель, без оценки старой модели памяти.
|
||||
|
||||
## Оригинальный copy shader
|
||||
|
||||
В режиме памяти игры на месте шейдер `fefebf9f` теперь по умолчанию транслируется в SPIR-V и исполняется на GPU. Его список копий читает сам шейдер при исполнении; CPU не заменяет его списком Vulkan-копий. `BB_COPY_SHADER_NATIVE=0` сохраняет HLE для сравнения. Эксперимент 2 по-прежнему инвертирует выбор.
|
||||
|
||||
Для больших привязок таблица BDA содержит два представления: в первой половине — актуальные адреса (зеркало или память игры), во второй — адреса памяти игры. Существующая адресация первой половины сохранена для DMA/ReadConst. Описание привязки содержит флаг записи насквозь и смещение второй таблицы.
|
||||
|
||||
Оригинальный copy shader записывает результат в актуальный GPU-адрес и в память игры. Поэтому CPU-потребители видят результат после синхронизации, а GPU-потребители не читают устаревшее зеркало. Когда обе записи адресуют память игры, они дают тот же результат. Остальные вычислительные шейдеры сохраняют свой учёт GPU-владения зеркалами. Записи насквозь не помечают всё зеркало огромной привязки как требующее обратного копирования; ранее накопленные GPU-записи при этом сохраняются в учёте.
|
||||
|
||||
Проверка диапазона не допускает переполнения `offset + size`; отвергнутый доступ не индексирует произвольную страницу таблицы. Версия shader binary cache поднята до 9. Профиль кэша теперь содержит версии binary/meta/pipeline: при их смене весь кэш перестраивается до загрузки модулей. Старые метаданные и SPIR-V с совпадающими именами индексов вариаций не смешиваются с новыми.
|
||||
|
||||
`run.sh` задаёт `BB_GPU_USER_DIR` до запуска процесса, из `BB_USER_DIR` или `$BB_DATA_DIR/user`. GPU-каталоги создаются при загрузке библиотеки, поэтому установка этого значения внутри `bbgpu_init` была слишком поздней. Это обеспечивает правильный выбор кэша для AppImage и отдельных профилей сохранений; явный `BB_GPU_USER_DIR` сохраняется.
|
||||
|
||||
## EOP/EOS и RELEASE_MEM
|
||||
|
||||
Если `VK_AMD_buffer_marker` доступно, сохраняется быстрый путь AMD. Без расширения метку пишет GPU через стандартный `vkCmdUpdateBuffer`. `BB_GPU_LABELS_PORTABLE=1` принудительно выбирает этот путь и позволяет проверить его на AMD. `BB_GPU_LABELS=0` оставляет прежний callback CPU после завершения GPU.
|
||||
|
||||
Переносимый путь завершает текущий render pass, публикует предшествующие GPU-записи для CPU, записывает старшую половину 64-битной метки, ставит зависимость и записывает младшую половину. Следующая зависимость обеспечивает видимость метки для GPU и CPU. Уведомление планировщику и обработка WAIT_REG_MEM сохраняются. Неподходящие адреса и метки с прерыванием сохраняют соответствующие запасные пути.
|
||||
|
||||
Стандартная команда разрешена вне render pass: [vkCmdUpdateBuffer](https://docs.vulkan.org/refpages/latest/refpages/source/vkCmdUpdateBuffer.html). Маски стадий и доступа соответствуют [правилам Vulkan synchronization](https://docs.vulkan.org/spec/latest/chapters/synchronization.html). Завершение проходов и барьеры имеют стоимость; принудительный переносимый путь на AMD не выбран по умолчанию.
|
||||
|
||||
При проверке также исправлены две зависимости соседних путей: update/inline data учитываются на стадии CLEAR, а диапазон index buffer учитывается после draw, чтобы последующая загрузка в зеркало ожидала чтения индексов.
|
||||
|
||||
## Проверки
|
||||
|
||||
`translation-memory-test` исполняет SPIR-V производственного backend и производственную функцию переносимых меток на Vulkan GPU. Проверяются чтение актуального источника, запись в зеркало и память игры, граница страниц, unmapped/out-of-range store, отключённая запись насквозь, 32/64-битные метки, порядок половин, видимость предшествующего GPU-результата при опросе CPU, последующий GPU-потребитель и нулевое значение. Диагностические AMD breadcrumbs отключены, чтобы тест не записывал AMD-маркеры.
|
||||
|
||||
```sh
|
||||
nix-shell shell.nix --run 'ninja -C out/gpu translation-memory-test'
|
||||
# VK_LAYER_PATH при необходимости указывает каталог установленного Khronos validation layer.
|
||||
BB_VK_VALIDATION_SYNC=1 out/gpu/translation-memory-test out/translation-shaders
|
||||
spirv-val --target-env vulkan1.3 out/translation-shaders/paged-copy.spv
|
||||
```
|
||||
|
||||
PM4 self-test измеряет положительный occlusion-интервал через несколько отправок. Большая отдельная отправка могла содержать только compute/state и давать ложный FAIL. Требования positive samples, valid bits, нулевой интервал и predication сохранены.
|
||||
|
||||
Аппаратная проверка выполнена на RX 7800 XT / RADV. Переносимый путь проверен принудительно на AMD; выполнение на настоящей NVIDIA, импорт её host memory и длительное прохождение игры требуют отдельного аппаратного прогона.
|
||||
|
||||
Локальные журналы, копии исходников перед изменениями, замеры и снимки: `out/dev/translation-fixes-20261009/`. Игровые проверки используют копии сохранений и настроек. Исходные сохранения и настройки не заменяются. Журналы и игровые данные в релиз не включены.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Our own translator instead of shadPS4: structure and order of work (2026-10-10)
|
||||
|
||||
> Design notes from when this work was planned, kept for their reasoning. bbport 0.7 has one
|
||||
> memory model and its own renderer, and some switches named here (`BB_CP_SHADOW` and `BB_CP_DECODER`) no longer exist; the
|
||||
> current list is in [ENVIRONMENT.md](ENVIRONMENT.md).
|
||||
|
||||
Goal: move away from emulation and from shadPS4 code. We want our own PS4 command translator and
|
||||
our own graphics renderer that scale to other games (everything specific to one game lives in its
|
||||
profile, `gpu/games/`) and behave the same on AMD and NVIDIA. After that comes shadnet (online play).
|
||||
|
||||
## Current state
|
||||
|
||||
Borrowed from shadPS4 (`gpu/VENDOR.txt`, commit b4e7ae7 from 2026-09-24): about 116k lines.
|
||||
|
||||
| Part | Lines | What it does | Our work in it |
|
||||
|---|---|---|---|
|
||||
| `video_core/amdgpu` | 8.3k | PM4 parsing (command processor `liverpool.cpp`), registers, tiling | GPU-side labels, occlusion, predication, PM4 self-test |
|
||||
| `video_core/renderer_vulkan` | 28.6k | Vulkan renderer: pipelines, rasterizer, scheduler, presenter | draw pipeline (stages A/B), recording threads, upscalers, background compilation |
|
||||
| `video_core/buffer_cache` | 7.4k | game memory for the GPU | almost all rewritten (memory module, VRAM copies, traps) |
|
||||
| `video_core/texture_cache` | 4.8k | textures by address, detiling | time-based collector, uploads |
|
||||
| `shader_recompiler` | 38.6k | GCN → IR → SPIR-V | paged buffers (BDA), fixes |
|
||||
| `core`, `common` | 21.8k | VideoOut, GnmDriver, event queues, utilities | vblank, labels |
|
||||
|
||||
Our own code, outside shadPS4: `gpu/layer/` (memory module `BbLayer::GpuMemory`), `gpu/games/` (game
|
||||
profiles), `gpu/shim/` (overlay, settings, hooks), the runtime in `src/` (kernel, libc, files, saves).
|
||||
|
||||
## Target structure
|
||||
|
||||
Modules with narrow interfaces. None of them knows addresses in the game's code (that belongs to the
|
||||
profile), and none does the GPU's work on the CPU (that is emulation):
|
||||
|
||||
1. **memory**: game memory in place, VRAM copies as a cache, shared tracking of page protection
|
||||
(write traps for copies and textures). Exists: `layer/` + the module in `buffer_cache_layer.cpp`.
|
||||
2. **cp**: command processor. Parses PM4 into the translator's command stream (state, draws,
|
||||
dispatches, synchronization) in the order the GPU executes it. Replaces `liverpool.cpp`.
|
||||
3. **state**: context registers → pipeline keys and Vulkan dynamic state. Replaces
|
||||
`liverpool_to_vk` and the key building in `vk_pipeline_cache`.
|
||||
4. **resources**: V#/T#/S# descriptors → buffers, images, samplers (caches, detiling). Replaces
|
||||
`buffer_cache`, `texture_cache` (the buffer part is already almost ours).
|
||||
5. **render**: Vulkan. Command recording per thread, barriers, submission, presentation. Replaces
|
||||
`renderer_vulkan` (the scheduler and stages A/B are already ours).
|
||||
6. **shaders**: GCN → SPIR-V. The largest part; it is replaced last, stage by stage (decoder,
|
||||
IR, passes, code generation).
|
||||
7. **platform**: VideoOut, GnmDriver, kernel events (`core/libraries`), next to the runtime.
|
||||
8. **games**: game profiles (exists).
|
||||
|
||||
## Order
|
||||
|
||||
One module is replaced at a time. First an interface in our code with the shadPS4 code behind it
|
||||
as is, then our own implementation behind the same interface. After each step: the route (a route
|
||||
benchmark script (not in this repository), frames and snapshots), the PM4 self-test
|
||||
(`BB_PM4_SELFTEST=1`), unit tests, the game on AMD and (from tester logs) on NVIDIA.
|
||||
|
||||
| Step | What | Why in this order |
|
||||
|---|---|---|
|
||||
| 1 | memory: shared tracking of page protection; write traps under textures by default; model 0.3 and the sparse arena are removed | removes the rest of memory emulation and the dependency on game hooks |
|
||||
| 2 | cp: the translator's command stream; `MEM_SEMAPHORE`, GPU time, compute queues done by the GPU | the last emulation left is in the command processor |
|
||||
| 3 | state + resources | they build on 1 and 2; the buffer part is already ours |
|
||||
| 4 | render | the scheduler and stages are already ours; the rasterizer and pipelines remain |
|
||||
| 5 | shaders | the largest and riskiest; by then everything else is already ours |
|
||||
| 6 | shadnet | networking, once the graphics are our own |
|
||||
|
||||
Steps 1 and 2 are the plan to move away from emulation
|
||||
([`EMULATION_REMOVAL_PLAN.ru.md`](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/EMULATION_REMOVAL_PLAN.ru.md),
|
||||
section dated 2026-10-10).
|
||||
|
||||
## Progress
|
||||
|
||||
### 2026-10-10: our own PM4 decoder (step 2, start)
|
||||
|
||||
- `gpu/cp/`: our own code, without shadPS4. Packet format and opcodes (`cp_pm4.h`), register file and
|
||||
decoder (`cp_decoder.{h,cpp}`). The decoder walks the buffer in stream order, applies register
|
||||
packets (`SET_*_REG`, `CLEAR_STATE`, registers written by draw packets), descends into
|
||||
nested buffers and hands each packet to a "sink" (`BbCp::Sink`) with the state already applied:
|
||||
draws and dispatches as typed calls, the rest as packets. The sink decides `COND_EXEC`. The
|
||||
decoder knows nothing about Vulkan, the game or where execution happens.
|
||||
- Unit test `tests/test_cp_decoder.cpp` (target `cp-decoder-test`): registers, partial writes,
|
||||
draws, dispatches, nested buffers, `COND_EXEC`, `CLEAR_STATE`, corrupt buffers.
|
||||
- Register addresses are checked against the shadPS4 layout at build time (`static_assert`). This
|
||||
found a bug: on CIK, `CP_STRMOUT_CNTL` is in uconfig (`0xC03F`), not in config.
|
||||
- Shadow check in the game (`BB_CP_SHADOW=1`): the new decoder parses every top-level buffer
|
||||
from the same state, and the register files are compared at the end of the buffer. On the route,
|
||||
all buffers matched (~800k buffers, ~200 million packets, 0 mismatches, 0 parse errors).
|
||||
Bloodborne does not use nested buffers (they are covered by the unit test).
|
||||
- Next: the decoder as a cursor (packet by packet, so the command processor loop can yield to
|
||||
other queues while waiting), the register file as shared state storage (its layout matches
|
||||
shadPS4 `Regs` word for word), then moving packet handling from `liverpool.cpp` into the
|
||||
translator's sink one packet type at a time, each time with the shadow check and the route.
|
||||
|
||||
### 2026-10-10: the new decoder is the real parsing path
|
||||
|
||||
- The decoder became a cursor (`BbCp::Cursor`: packet by packet, nested buffers at the
|
||||
caller's choice). The register file can live in the owner's storage (for the command processor,
|
||||
its `regs`, with the same word layout). A write observer (`RegisterObserver`, one call per
|
||||
packet) provides what draw preparation needs: the written blocks and the packet checksum.
|
||||
- `ProcessGraphics` takes packets and register writes from the cursor (`BB_CP_DECODER=0` selects
|
||||
the old walk). What each packet does is still the old code. PM4 self-test: 11 of 11. The share of
|
||||
prepared draws is the same (747k used, 19k not, versus 755k/24k), so the checksums
|
||||
match. The image is the same.
|
||||
@@ -1,92 +0,0 @@
|
||||
# Свой транслятор вместо shadPS4: устройство и порядок работ (2026-10-10)
|
||||
|
||||
Цель: уйти от эмуляции и от кода shadPS4 — собственный транслятор команд PS4 и собственный
|
||||
графический рендер, которые масштабируются на другие игры (всё, что про одну игру, — в её
|
||||
профиле, `gpu/games/`), одинаковые для AMD и NVIDIA. Потом — shadnet (сетевая игра).
|
||||
|
||||
## Что сейчас
|
||||
|
||||
Заимствовано из shadPS4 (`gpu/VENDOR.txt`, коммит b4e7ae7 от 2026-09-24) около 116 тыс. строк:
|
||||
|
||||
| Часть | Строк | Что делает | Наше в ней |
|
||||
|---|---|---|---|
|
||||
| `video_core/amdgpu` | 8,3 тыс. | разбор PM4 (командный процессор `liverpool.cpp`), регистры, тайлинг | метки видеочипом, occlusion, предикация, самотест PM4 |
|
||||
| `video_core/renderer_vulkan` | 28,6 тыс. | рендер Vulkan: конвейеры, растеризатор, планировщик, презентер | конвейер отрисовок (стадии A/B), потоки записи, апскейлеры, фоновая компиляция |
|
||||
| `video_core/buffer_cache` | 7,4 тыс. | память игры для видеочипа | почти всё переписано (модуль памяти, копии в VRAM, ловушки) |
|
||||
| `video_core/texture_cache` | 4,8 тыс. | текстуры по адресу, детайлинг | сборщик по времени, загрузки |
|
||||
| `shader_recompiler` | 38,6 тыс. | GCN → IR → SPIR-V | постраничные буферы (BDA), исправления |
|
||||
| `core`, `common` | 21,8 тыс. | VideoOut, GnmDriver, очереди событий, утилиты | vblank, метки |
|
||||
|
||||
Своё, вне кода shadPS4: `gpu/layer/` (модуль памяти `BbLayer::GpuMemory`), `gpu/games/` (профили
|
||||
игр), `gpu/shim/` (оверлей, настройки, перехваты), рантайм `src/` (ядро, libc, файлы, сохранения).
|
||||
|
||||
## Устройство, к которому идём
|
||||
|
||||
Модули с узкими интерфейсами; ни один не знает адресов кода игры (это профиль) и не делает
|
||||
работу видеочипа на процессоре (это эмуляция):
|
||||
|
||||
1. **memory** — память игры на месте, копии в VRAM как кэш, общий учёт защиты страниц (ловушки
|
||||
записи для копий и текстур). Есть: `layer/` + модуль в `buffer_cache_layer.cpp`.
|
||||
2. **cp** — командный процессор: разбор PM4 в поток команд транслятора (состояние, отрисовки,
|
||||
запуски, синхронизация) в порядке исполнения видеочипом. Заменяет `liverpool.cpp`.
|
||||
3. **state** — регистры контекста → ключи конвейеров и динамическое состояние Vulkan. Заменяет
|
||||
`liverpool_to_vk`, сборку ключей в `vk_pipeline_cache`.
|
||||
4. **resources** — описатели V#/T#/S# → буферы, образы, сэмплеры (кэши, детайлинг). Заменяет
|
||||
`buffer_cache`, `texture_cache` (буферная часть уже почти наша).
|
||||
5. **render** — Vulkan: запись команд по потокам, барьеры, отправка, вывод. Заменяет
|
||||
`renderer_vulkan` (планировщик и стадии A/B уже наши).
|
||||
6. **shaders** — GCN → SPIR-V. Самая большая часть; заменяется последней, по стадиям (декодер,
|
||||
IR, проходы, генерация).
|
||||
7. **platform** — VideoOut, GnmDriver, события ядра (`core/libraries`), рядом с рантаймом.
|
||||
8. **games** — профили игр (есть).
|
||||
|
||||
## Порядок
|
||||
|
||||
Замена по одному модулю: сначала интерфейс в нашем коде и за ним — код shadPS4 как есть, потом
|
||||
наша реализация за тем же интерфейсом. После каждого шага: маршрут (`route_bench.sh`, кадры и
|
||||
снимки), самотест PM4 (`BB_PM4_SELFTEST=1`), юнит-тесты, игра на AMD и (по логам тестеров) NVIDIA.
|
||||
|
||||
| Шаг | Что | Почему в этом порядке |
|
||||
|---|---|---|
|
||||
| 1 | memory: общий учёт защиты страниц; ловушки под текстурами по умолчанию; модель 0.3 и sparse-арена удаляются | убирает остаток эмуляции памяти и привязку к перехватам игры |
|
||||
| 2 | cp: поток команд транслятора; `MEM_SEMAPHORE`, время видеочипа, очереди compute — видеочипом | в командном процессоре осталась последняя эмуляция |
|
||||
| 3 | state + resources | опираются на 1 и 2; буферная часть уже наша |
|
||||
| 4 | render | планировщик и стадии уже наши; остаются растеризатор и конвейеры |
|
||||
| 5 | shaders | самый большой и рискованный; к этому времени остальное уже наше |
|
||||
| 6 | shadnet | сеть, когда графика своя |
|
||||
|
||||
Шаги 1 и 2 — это план ухода от эмуляции (`EMULATION_REMOVAL_PLAN.ru.md`, раздел от 2026-10-10).
|
||||
|
||||
## Ход работ
|
||||
|
||||
### 2026-10-10: свой декодер PM4 (шаг 2, начало)
|
||||
|
||||
- `gpu/cp/` — свой код, без shadPS4: формат пакетов и опкоды (`cp_pm4.h`), файл регистров и
|
||||
декодер (`cp_decoder.{h,cpp}`). Декодер идёт по буферу в порядке потока, применяет пакеты
|
||||
регистров (`SET_*_REG`, `CLEAR_STATE`, регистры, которые пишут пакеты отрисовки), спускается во
|
||||
вложенные буферы и отдаёт каждый пакет «приёмнику» (`BbCp::Sink`) с уже применённым состоянием:
|
||||
отрисовки и запуски — типизированно, остальное — пакетами. `COND_EXEC` решает приёмник. Про
|
||||
Vulkan, игру и место исполнения декодер не знает.
|
||||
- Юнит-тест `tests/test_cp_decoder.cpp` (цель `cp-decoder-test`): регистры, частичные записи,
|
||||
отрисовки, запуски, вложенные буферы, `COND_EXEC`, `CLEAR_STATE`, битые буферы.
|
||||
- Адреса регистров сверены с раскладкой shadPS4 на этапе сборки (`static_assert`): так нашлась
|
||||
ошибка — на CIK `CP_STRMOUT_CNTL` в uconfig (`0xC03F`), не в config.
|
||||
- Теневая проверка в игре (`BB_CP_SHADOW=1`): новый декодер разбирает каждый буфер верхнего уровня
|
||||
с того же состояния, в конце буфера файлы регистров сравниваются. На маршруте — все буферы
|
||||
совпали (~800 тыс. буферов, ~200 млн пакетов, 0 расхождений, 0 ошибок разбора). Вложенных
|
||||
буферов Bloodborne не использует (проверены юнит-тестом).
|
||||
- Дальше: декодер как курсор (пакет за пакетом, чтобы цикл командного процессора мог уступать
|
||||
другим очередям на ожиданиях), файл регистров — общее хранилище состояния (раскладка совпадает
|
||||
с `Regs` shadPS4 по словам), затем перенос обработки пакетов из `liverpool.cpp` в приёмник
|
||||
транслятора по одному виду пакетов, каждый раз с теневой проверкой и маршрутом.
|
||||
|
||||
### 2026-10-10: новый декодер — настоящий путь разбора
|
||||
|
||||
- Декодер стал курсором (`BbCp::Cursor`: пакет за пакетом, вложенные буферы — по желанию
|
||||
вызывающего), файл регистров может жить в хранилище владельца (у командного процессора — его
|
||||
`regs`, раскладка по словам та же), наблюдатель записей (`RegisterObserver`, один вызов на
|
||||
пакет) даёт то, что нужно подготовке отрисовок: записанные блоки и контрольную сумму пакетов.
|
||||
- `ProcessGraphics` берёт пакеты и запись регистров у курсора (`BB_CP_DECODER=0` — старый обход);
|
||||
что делает каждый пакет — пока прежний код. PM4-самопроверка — 11 из 11; доля подготовленных
|
||||
отрисовок та же (747 тыс. использовано, 19 тыс. нет, против 755/24 тыс.) — контрольные суммы
|
||||
сходятся; картинка та же.
|
||||
+75
-16
@@ -1,6 +1,6 @@
|
||||
# Troubleshooting
|
||||
|
||||
When asking for help, attach the log: in the launcher's **Log** page, **Export log…** saves it to
|
||||
When asking for help, attach the log: in the launcher's **Log** section, **Export log…** saves it to
|
||||
a file and **Copy** puts it on the clipboard.
|
||||
|
||||
## The launcher rejects the game
|
||||
@@ -12,7 +12,7 @@ The launcher checks the game folder before starting. What it says, and what to d
|
||||
| bbport needs the 1.09 update merged into the game folder | Copy everything from the dumped 1.09 update into the game folder, replacing files (`eboot.bin` and `sce_sys` too). |
|
||||
| param.sfo is from 1.09 but eboot.bin is not | Copy `eboot.bin` from the dumped 1.09 update over the old one. |
|
||||
| This eboot.bin has the 60 fps patch by Lance McDonald applied | Use a clean `eboot.bin` from the 1.09 update. bbport has its own 60 FPS. |
|
||||
| This is not a retail release of Bloodborne | Alpha tests and other games do not run. See [COMPATIBILITY.md](COMPATIBILITY.md). |
|
||||
| This is not a retail release of Bloodborne | Alpha tests and other games do not run. See [supported releases](COMPATIBILITY.md). |
|
||||
| eboot.bin could not be read as a decrypted PS4 executable | Dump the game and the update again. |
|
||||
| The game files are damaged | The extraction tool broke compressed files (LibOrbisPkg PkgTool on .NET 6 or newer); the game would hang while loading. Extract the game and the update again with a fixed tool. |
|
||||
|
||||
@@ -27,22 +27,22 @@ To move a save, close the game, back up both folders, and copy the `savedata` fo
|
||||
|
||||
## DLSS is "not supported by this GPU"
|
||||
|
||||
The log says why, on a line starting with `DLSS:`.
|
||||
The log says why, on a line `DLSS: unavailable (...)`; the game then uses another upscaler.
|
||||
|
||||
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's own driver; it does not work with
|
||||
NVK.
|
||||
- The 0.4-linux.1 AppImage ships a modified copy of NVIDIA's DLSS library, which the driver
|
||||
refuses (`Feature requirement query failed: 0xbad00012`): update to 0.5-linux.1 or newer.
|
||||
NVK (`not an NVIDIA GPU`, or `this GPU or driver does not support DLSS`).
|
||||
- `NVIDIA NGX initialization failed`: update the driver, and check that NVIDIA's CUDA/UVM module
|
||||
is loaded (`sudo modprobe nvidia_uvm`; the `nvidia-modprobe` package loads it when needed). With
|
||||
Secure Boot, a module signing key that is not enrolled stops it (`Key was rejected by service`).
|
||||
- DLSS Frame Generation needs a GeForce RTX 40 or newer; on older cards the log says so and DLSS
|
||||
itself still works.
|
||||
- From the repository, DLSS needs `bash tools/build_dlss.sh` once after `build.sh`.
|
||||
- If the log says `Device creation with DLSS extensions failed`, NVIDIA's CUDA/UVM
|
||||
module is missing: the game then uses FSR 3.1. Load it with `sudo modprobe nvidia_uvm` (the
|
||||
`nvidia-modprobe` package loads it when needed); with Secure Boot, a module signing key that
|
||||
is not enrolled stops it (`Key was rejected by service`).
|
||||
|
||||
## Spanish shows the Latin American text (or the other way round)
|
||||
|
||||
The game picks a language's regional text by the dump's region. Choose it in **Settings → Game
|
||||
region**: *Europe* for Spanish from Spain, *The Americas* for Latin American Spanish.
|
||||
The game picks a language's regional text by the dump's region and the system language. Choose the
|
||||
text you want in **Setup → Game text language**: *Español* for Spanish from Spain, *Español
|
||||
(Latinoamérica)* for Latin American Spanish. **Voice language** does the same for the voices.
|
||||
|
||||
## The game crashes after playing a while, on a card with little VRAM
|
||||
|
||||
@@ -51,12 +51,71 @@ On a 6 GB card the texture cache can run out of video memory: the log then repea
|
||||
a lower output resolution, lower **Model detail** in the game's *Display* settings, and close
|
||||
other programs that use the GPU (browsers, video players, other games).
|
||||
|
||||
## The picture is stretched on an ultrawide screen
|
||||
|
||||
Choose the screen's shape in **Graphics → Display → Aspect ratio** (*Auto* follows the output
|
||||
resolution): 4:3, 16:10, 16:9, 21:9 or 32:9. The view gets wider (or taller) instead of stretched,
|
||||
and the HUD keeps to the screen's edges; it changes live, in the game's *Display* menu too.
|
||||
**Output resolution** *Auto* is the monitor's own; any other size written as `output_res=WxH` in
|
||||
bbport.ini works too.
|
||||
|
||||
## Fullscreen opens on the wrong monitor, or drops back to a window
|
||||
|
||||
With several monitors, choose the one to play on in **Display → Monitor**: the port's *Primary*
|
||||
is the one the system reports first, which on Wayland is not always the one KDE or GNOME show as
|
||||
primary. On Wayland a program cannot place its own window, so the compositor may open it on
|
||||
another monitor first and then move it; when that leaves it as a plain window, the port asks for
|
||||
fullscreen again during the first seconds (the log says
|
||||
`Display: the compositor left the window out of fullscreen, asked again`). If it still ends up
|
||||
windowed, add `SDL_VIDEO_DRIVER=x11` under **Advanced → Developer → Extra variables**: through
|
||||
Xwayland the window places itself.
|
||||
|
||||
## Exit to desktop
|
||||
|
||||
**Exit to desktop** is in the game's System menu and on the title screen, from bbport 0.7. The
|
||||
*Quit to Desktop* switch of earlier Linux versions is gone with it, and a third-party *Exit Game*
|
||||
patch is no longer needed. In the world it asks first and goes through the game's own Quit Game,
|
||||
so your progress is saved, and the game closes once it is back at the title.
|
||||
|
||||
## The port's menu opens by accident with L3 + R3
|
||||
|
||||
From bbport 0.7, L3 + R3 opens the port's menu only when both are held for 0.8 seconds, so pressing
|
||||
them in a fight (lock-on and camera reset) no longer opens it. **Controls → Port menu from the
|
||||
controller** can make it 2 seconds, or turn it off so that only Insert on the keyboard opens the
|
||||
menu. Closing it with L3 + R3 always works.
|
||||
|
||||
## The controller does nothing when the game is started from Steam
|
||||
|
||||
With Steam Input on, Steam hides the controller and the game gets Steam's virtual one, whose input
|
||||
Steam sends only to the window it sees focused. On a Wayland desktop Steam sees only X11 windows,
|
||||
so the game now opens its window through Xwayland when Steam made a virtual controller for it (the
|
||||
log says `Window: X11 (Xwayland), so Steam Input reaches the game`). On older releases, put
|
||||
`SDL_VIDEO_DRIVER=x11 %command% --play` in the shortcut's launch options (keep `--play` after
|
||||
`%command%`), or turn Steam Input off for the shortcut: the game reads the controller itself.
|
||||
|
||||
## The Steam overlay does not open in the game
|
||||
|
||||
Steam's overlay (Shift+Tab, its FPS counter, F12 screenshots) needs the game started from Steam,
|
||||
with **Steam overlay** on under **Add to Steam** in the launcher (on by default). The log then says
|
||||
`Steam overlay: …/ubuntu12_64/gameoverlayrenderer.so`. Without that line, the game was not started
|
||||
by Steam.
|
||||
|
||||
## The camera turns much faster on diagonals
|
||||
|
||||
With the right stick pushed all the way, the game switches to its fast turn (about twice the
|
||||
speed), and it does so on diagonals too, not only straight sideways. That is how the game turns on
|
||||
a PS4 as well, with any controller. **Controls → Even camera speed on diagonals** keeps diagonals at
|
||||
the normal speed; the fast turn stays when the stick is pushed straight sideways. With it on the
|
||||
camera can feel heavier: the game's own *Camera speed* setting makes up for it.
|
||||
|
||||
## Summons never connect
|
||||
|
||||
Run **Test the connection** in the **Online** page. A red line for the shadNet server's UDP means
|
||||
the server never learns your address, and no summon can connect: shadPS4's public server does
|
||||
this. Use your own server: [SERVER.md](SERVER.md).
|
||||
Press **Test** in the **Status** row of **Multiplayer → Online**. An error mark on the shadNet server's UDP line means
|
||||
the server never learns your address, and no summon can connect: check that the server is running
|
||||
and that its log lists the UDP listener, and that no firewall blocks UDP 31314 on its side
|
||||
([hosting your own server](SERVER.md)). shadPS4's public server does not accept bbport builds at all: use The
|
||||
Hunter's Dream, Seamless Hunt or your own server.
|
||||
|
||||
## Other online problems
|
||||
|
||||
See [ONLINE.md](ONLINE.md): the account, the bells, the ports and the log lines of a summon.
|
||||
See [Online play and co-op](ONLINE.md): the account, the bells, the ports and the log lines of a summon.
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# FAQ
|
||||
|
||||
## Is this an emulator?
|
||||
|
||||
No. The game's own code runs directly on the CPU, and its graphics are translated to Vulkan. It is
|
||||
built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28.
|
||||
|
||||
## Where do I get the game?
|
||||
|
||||
From your own PS4: a dump of your copy with update 1.09. No game files are included, and none can
|
||||
be shared here.
|
||||
|
||||
## Can I play with Windows players?
|
||||
|
||||
Yes. On Seamless Hunt or The Hunter's Dream, Linux and Windows players play together; for seamless
|
||||
co-op everyone needs the same BBSC version (0.0.2). [Online play](ONLINE.md).
|
||||
|
||||
## Summons take a long time
|
||||
|
||||
Some waiting is normal: the game checks for summon signs about every 35 seconds, as the PS4's
|
||||
servers had it, so a sign can take over half a minute to show up.
|
||||
|
||||
## My friend never gets summoned with the Kin Bell
|
||||
|
||||
It depends on the server:
|
||||
|
||||
- On **Seamless Hunt** (or your own server with BBSC's game server), seamless co-op works fully:
|
||||
summons across areas and up to 5 hunters.
|
||||
- On **The Hunter's Dream** there is no seamless co-op: summons are the normal game's, with the
|
||||
Beckoning Bell and the Small Resonant Bell. Both hunters must be in the **same area**, the area's
|
||||
boss must still be alive in the host's world, and your levels must be within range (or use the
|
||||
same password).
|
||||
|
||||
[Seamless co-op on each server](ONLINE.md#seamless-co-op-bbsc).
|
||||
|
||||
## Do I need to open ports on my router?
|
||||
|
||||
Usually not: with **UPnP** on (the default) the game opens its port on the router by itself, and
|
||||
with seamless co-op (BBSC) on Seamless Hunt relays players who cannot reach each other directly. A
|
||||
router without UPnP, or a firewall on your computer, needs UDP 3658 opened
|
||||
([ports](ONLINE.md#ports)). With [your own server](SERVER.md), Tailscale takes care of it.
|
||||
|
||||
## How do I use the in-game map?
|
||||
|
||||
In the launcher, **Mods → Map (Hunter's Atlas)**: turn on **In-game map** and press **Prepare**
|
||||
once. It makes the maps from your own game files (it downloads about 200 MB of tools the first
|
||||
time). In the game, choose **Map** in the quick menu (its fourth tile), press **M**, or with a
|
||||
controller open the game's menu and click the right stick (R3). The port's menu (Insert, or hold
|
||||
L3 + R3) changes the key and the controller shortcut.
|
||||
**Travel to lamps from the map** and **Items on the map** are in the same launcher group.
|
||||
|
||||
## How do I get FSR 4.1.2 (INT8, for RDNA2 and the Steam Deck)?
|
||||
|
||||
FSR 4.1.2's models are built from AMD's own files, which can't be shipped here. In
|
||||
**Graphics → Upscaler**, on **FSR 4.1.2 from AMD's driver**, press **Choose a file…** and pick
|
||||
AMD's Adrenalin 26.10.1 or newer installer (the `.exe`) or the `amdxcffx64.dll` inside it. It also
|
||||
needs AMD's FSR 4.1.x upscaler DLL (`amd_fidelityfx_upscaler_dx12.dll`, from OptiScaler or a game
|
||||
with FSR 4.1): if you built FSR 4.1.1 before, it uses that one, otherwise it asks for it. It takes
|
||||
about 4 minutes. In the game, the port's menu chooses the model (INT8 RDNA2 or INT8 Lite).
|
||||
|
||||
## How do I get a log on the Steam Deck?
|
||||
|
||||
Turn on *Advanced → Developer → Save the log and statistics to a file* once in Desktop Mode; Game
|
||||
Mode sessions then save theirs too. [Getting a log](logs.md).
|
||||
|
||||
## The game updated and my Steam shortcut stopped working
|
||||
|
||||
Press **Add to Steam** again in the launcher: it updates the shortcut.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Getting started
|
||||
|
||||
## What you need
|
||||
|
||||
- **Your own dump of Bloodborne with update 1.09**, from your PS4: any retail release (US, Europe,
|
||||
UK, Japan, Asia, and the editions with The Old Hunters). No game files are included, and other
|
||||
versions of the game's executable start and then crash. [Which releases work](COMPATIBILITY.md).
|
||||
- **Linux x86-64** and a GPU with **Vulkan 1.3**: AMD or Intel with Mesa, or NVIDIA with NVIDIA's
|
||||
driver. The Steam Deck works too.
|
||||
- About **6 GB of free memory**, 10 GB for 1440p or 4K.
|
||||
|
||||
## First launch
|
||||
|
||||
1. Download `Bloodborne-bbport-x86_64.AppImage` from the
|
||||
[latest release](https://github.com/ayozetr/bloodborne_linux/releases/latest).
|
||||
2. Make it executable (in a file manager: *Properties → Permissions → Allow executing*, or
|
||||
`chmod +x Bloodborne-bbport-x86_64.AppImage`) and run it.
|
||||
3. In the launcher, choose your **game folder**: the one with `eboot.bin` in it.
|
||||
4. Press **Play**.
|
||||
|
||||
!!! tip "The first minutes"
|
||||
The first time, the game builds its shaders as it meets them, so it can stutter for a few
|
||||
minutes. The next sessions start with them ready.
|
||||
|
||||
## Saves, settings and the DLC
|
||||
|
||||
- Saves and settings go to `~/.local/share/bbport`.
|
||||
- **Setup → Saves** exports your saves into a `.zip` and imports them back, also from shadPS4.
|
||||
- The Game of the Year edition already has *The Old Hunters*. With the base game, put its DLC
|
||||
folder in **Setup → DLC folder**.
|
||||
|
||||
## Updating
|
||||
|
||||
The launcher's **About** page says when a new release is out and installs it; it can also go back
|
||||
to an earlier one. Your saves and settings stay as they are.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Play online](ONLINE.md), alone or in seamless co-op.
|
||||
- [Add the game to Steam](steam-deck.md), or play on the Steam Deck.
|
||||
- Something wrong? [Troubleshooting](TROUBLESHOOTING.md) and [getting a log](logs.md).
|
||||
@@ -1,67 +0,0 @@
|
||||
# Исправления — 2026-09-27
|
||||
|
||||
## ✅ Что исправлено
|
||||
|
||||
### 1. Маска реактивности не сохранялась
|
||||
- **Файл:** `gpu/shim/bbport_settings.cpp`
|
||||
- **Проблема:** Чекбокс "Показать маску (отладка)" не сохранялся в `bbport.ini`
|
||||
- **Решение:** Добавлена загрузка/сохранение параметра `debug_view`
|
||||
|
||||
### 2. 🔥 ДИНАМИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ПРЕСЕТОВ на RX 7800 XT
|
||||
- **Файлы:**
|
||||
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp`
|
||||
- `gpu/shim/bbport_overlay.cpp`
|
||||
- **Проблема:** RX 7800 XT не поддерживает `blit D32S8`, поэтому требовался перезапуск
|
||||
- **Решение:**
|
||||
- Fallback: UI depth очищается вместо копирования (работает корректно)
|
||||
- Убрана блокировка `BB_LIVE_SCALING_UNSUPPORTED`
|
||||
- Обновлено меню — убрано требование перезапуска для пресетов
|
||||
|
||||
### 3. Правильная конфигурация
|
||||
- **Файл:** `bbport.ini`
|
||||
- **Критично:** `jitter=1` (БЫЛО ВЫКЛЮЧЕНО!)
|
||||
- Оптимизированы параметры резкости и маски
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Что теперь работает БЕЗ перезапуска
|
||||
|
||||
- ✅ **Смена пресетов** (Native AA / Quality / Balanced / Performance / Ultra Performance)
|
||||
- ✅ Включение/выключение апскейлера
|
||||
- ✅ Резкость
|
||||
- ✅ Jitter
|
||||
- ✅ Маска реактивности
|
||||
- ✅ Отладочные режимы
|
||||
|
||||
**Перезапуск нужен ТОЛЬКО для:** векторов движения объектов (`object_motion`)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### Сборка
|
||||
✅ Успешно
|
||||
|
||||
### Проверить в игре:
|
||||
|
||||
1. **Динамическое переключение:**
|
||||
- Открыть меню (Insert или L3+R3)
|
||||
- Сменить пресет
|
||||
- **Должно применится СРАЗУ**
|
||||
|
||||
2. **Шлейфы на оружии:**
|
||||
- С `jitter=1` должны стать НАМНОГО меньше
|
||||
- Сравнить с `object_motion=0`
|
||||
|
||||
3. **Маска реактивности:**
|
||||
- Должна отображаться (красный цвет)
|
||||
- Помечены прозрачные эффекты
|
||||
|
||||
---
|
||||
|
||||
## 📝 Изменённые файлы
|
||||
|
||||
- `gpu/shim/bbport_settings.cpp`
|
||||
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp`
|
||||
- `gpu/shim/bbport_overlay.cpp`
|
||||
- `bbport.ini`
|
||||
@@ -1,248 +0,0 @@
|
||||
# Bloodborne на ПК: фактическое состояние порта и апскейлера
|
||||
|
||||
Срез на **29 сентября 2026 года**. Источник — файлы в `game_files`, история Git
|
||||
`native_probe` до `ac9fe90`, текущие незакоммиченные изменения, сохранённые логи
|
||||
запусков и локальные тесты. В документе «есть в коде», «запускалось» и «проверено
|
||||
по изображению» не считаются одним и тем же.
|
||||
|
||||
## Краткий итог
|
||||
|
||||
Это работающий экспериментальный **нативный Linux-порт**, а не запуск через
|
||||
полный эмулятор: исходный x86-64 `CUSA03173/eboot.bin` исполняется на CPU ПК,
|
||||
системные вызовы PS4 обрабатывает собственный HLE-рантайм, графику выводит
|
||||
Vulkan-ядро на основе shadPS4. Игра доходит до сцены с персонажем, принимает
|
||||
ввод, работает со звуком и сохранениями; это подтверждают логи запуска,
|
||||
снимки и видео в корне `native_probe`. Прохождение всей игры, все локации и
|
||||
отсутствие сбоев этими материалами **не подтверждены**.
|
||||
|
||||
**FSR 3.1 temporal upscaling реально работает по проверенному пути с разрешением
|
||||
сцены, заданным при старте**: лог `логи.md` показывает `960×540 → 1920×1080`,
|
||||
компоновку UI в 1920×1080 и запуск на RX 7800 XT. Текущий путь смены пресета
|
||||
без перезапуска остаётся проблемным на этой видеокарте: последний
|
||||
`last_run.log` показывает переключение запрошенных размеров сцены, но FSR-контекст
|
||||
`1920×1080 → 1920×1080`. По коду уменьшенные цели здесь не выбираются, когда
|
||||
`D32S8` не поддерживает blit. Следовательно, лог переключения пресетов **не
|
||||
доказывает реальное снижение разрешения 3D и выигрыш FPS**.
|
||||
|
||||
## Состав проекта и развитие
|
||||
|
||||
| Путь | Роль |
|
||||
|---|---|
|
||||
| `../CUSA03173/` | Локальные файлы игры, `eboot.bin`, ресурсы и модули PS4. |
|
||||
| `../np_base/` | Снимок ранней базы порта; рабочая разработка идёт в `native_probe`. |
|
||||
| `prepare.py`, `link_libc.py`, `link_modules.py`, `probe.c`, `runtime_*.c` | Подготовка исполняемого образа, привязка модулей, загрузчик и HLE. |
|
||||
| `gpu/` | Vulkan-видеоядро, рекомпилятор шейдеров, доработки для игры, FSR и ImGui. Заимствованное ядро shadPS4 описано в `gpu/VENDOR.txt`. |
|
||||
| `patches/Bloodborne.xml`, `patches.py` | Патчи FPS и разрешения, применяемые к гостевому коду при старте. |
|
||||
| `bbport.ini`, `run.sh`, `build.sh` | Настройки, полный запуск и сборка. |
|
||||
| `out/`, `out/dev/`, `last_run.log`, `логи.md` | Собранные бинарники и следы проверок; часть логов относится к более ранним версиям кода. |
|
||||
|
||||
Развитие по Git: 25 сентября добавлены загрузчик, HLE, графика, звук и
|
||||
оптимизации CPU/GPU; 26 сентября — анализ кадра, векторы камеры, jitter,
|
||||
FSR 3.1, пресеты и меню; 27 сентября последний коммит `ac9fe90` исправил
|
||||
переключение пресетов и отображение маски. После него в рабочем дереве есть
|
||||
незакоммиченные доработки векторов объектов, уменьшенных целей рендера,
|
||||
разделения разрешений сцены/UI, скриптов и тестов. Это **текущее рабочее
|
||||
дерево**, не готовый релиз. Старые разделы `README.md`, `docs/upscaler.md`
|
||||
и `docs/ROADMAP.md` описывают разные этапы и местами уже устарели.
|
||||
|
||||
## Что реализовано
|
||||
|
||||
### Запуск и производительность
|
||||
|
||||
- Гостевой `eboot` и часть оригинальных PRX запускаются вместе с реализациями
|
||||
памяти, потоков, файлов, времени, контроллера, аудио, сохранений и сервисов.
|
||||
Сеть работает в офлайн-режиме; PSN не реализован.
|
||||
- SDL3 обслуживает геймпад и запасное управление с клавиатуры. Звук выводится
|
||||
через host-аудиосистему; в коде есть исправление темпа `AudioOut` и флаг
|
||||
сохранения для отсутствовавших звуков.
|
||||
- Vulkan-рендерер получил отдельный поток записи команд, подготовку draw на
|
||||
четырёх потоках, кеши текстур и буферов, копирование гостевой памяти и пул
|
||||
staging-буферов. `docs/parallel_gpu.md` и `docs/ROADMAP.md` содержат
|
||||
замеры в отдельных сценах; их нельзя переносить на все конфигурации.
|
||||
- `BB_FPS=uncap` по умолчанию применяет патч с шагом симуляции по времени
|
||||
кадра; доступны `30`, `60`, `90`. Частота вывода ограничивается монитором
|
||||
и максимумом 120 FPS. Для диагностики есть `BB_FRAME_STATS=1`,
|
||||
`BB_TOGGLE_FILE` и захват структуры кадра через `BB_CAPTURE_TRIGGER`.
|
||||
|
||||
### Апскейлер
|
||||
|
||||
- В игре нет готового буфера скорости. Порт вычисляет векторы камеры из
|
||||
глубины и матриц двух кадров; для геометрии сцены делает субпиксельный
|
||||
jitter Halton. Перепроекция статичной геометрии проверялась ранее
|
||||
(`docs/upscaler.md`).
|
||||
- Интегрирован FSR 3.1 через `gpu/third_party/fsr-vulkan`. Native AA работает
|
||||
1:1 на HDR-цвете сцены перед постобработкой. Проверенный режим с пониженным
|
||||
разрешением использует патч гостевого разрешения, FSR после тонмаппинга и
|
||||
отдельную компоновку интерфейса в 1920×1080. Патч UI в текущем дереве
|
||||
оставляет его координаты нативными. Явный `BB_RENDER_RES=ШxВ` включает
|
||||
этот путь; например, лог `логи.md` подтверждает `960x540`.
|
||||
- Пресеты в коде: Native AA `1920×1080`, Quality `1280×720`, Balanced
|
||||
`1130×636`, Performance `960×540`, Ultra Performance `640×360` для вывода
|
||||
1920×1080. Меню ImGui открывается `Insert` или `L3+R3`. В `bbport.ini`
|
||||
сохраняются FSR, пресет, резкость, jitter, маска реактивности, режим
|
||||
отладки, векторы объектов и счётчик FPS. Текущий конфиг содержит
|
||||
`upscaler=fsr3`, `preset=4`, `jitter=1`, `reactive=1`,
|
||||
`object_motion=1`, `debug_view=0`.
|
||||
- Маска реактивности формируется сравнением цвета до и после прозрачных
|
||||
проходов. Она опциональна: прежние наблюдения отмечают дрожание тонкой
|
||||
дымки при её включении. Настройка по умолчанию в коде выключена, но
|
||||
в текущем `bbport.ini` включена.
|
||||
- В рабочем дереве есть экспериментальные векторы анимированных объектов:
|
||||
прошлые позиции вершин, дополнительная цель G-buffer, сопоставление
|
||||
отрисовок между кадрами и ограничение на подходящие буферы позы/костей.
|
||||
Изменение `object_motion` требует перезапуска. По
|
||||
`out/dev/motion-selective.log` история в выбранных draws часто находится,
|
||||
без пропусков по ёмкости в показанных интервалах. Это метрика draw, **не
|
||||
оценка покрытия пикселей или визуального качества**.
|
||||
|
||||
## Текущий дефект динамических пресетов
|
||||
|
||||
В `vk_scene_resolution.cpp` функция `SceneTargets::Eligible` требует
|
||||
`BlitSrc|BlitDst` у формата каждого кандидата. В `vk_rasterizer.cpp`
|
||||
уменьшенный проход отключается, если неподходящая глубина; от неё же зависит
|
||||
выбор уменьшенного входа FSR в `vk_temporal_upscaler.cpp`. Сохранённый лог
|
||||
для RX 7800 XT сообщает об отсутствии `blit` у `D32S8`. Позднее добавлен
|
||||
fallback, который **очищает глубину UI** вместо её копирования, но он не
|
||||
устраняет требование `blit` у глубины **3D-сцены**.
|
||||
|
||||
Итог на этой машине: в `last_run.log` 28 сентября перечислены переключения
|
||||
`640×360`, `1280×720`, `960×540` и других пресетов, однако созданный
|
||||
FSR-контекст остался `1920×1080 → 1920×1080 (HDR scene color)`. Сообщение
|
||||
`Scene resolution: raster ...` отражает запрошенный размер proxy-целей;
|
||||
оно не измеряет фактически использованный размер каждого draw. Заявления
|
||||
`README_FIXES.md` и подсказки меню о работающем на всех GPU динамическом
|
||||
масштабировании сильнее, чем позволяют код и последний лог. `логи.md`
|
||||
относится к более раннему запуску с автоматическим выбором стартового патча,
|
||||
который затем изменили в `run.sh`.
|
||||
|
||||
## Проверки и границы уверенности
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| Логи игрового запуска и кадры от 27–28 сентября | Загрузка в игровую сцену и вывод изображения подтверждены. Сквозной тест прохождения не проводился. |
|
||||
| `логи.md` на RX 7800 XT | Подтверждены патч сцены `960×540`, UI `1920×1080`, FSR-контекст `960×540 → 1920×1080`. |
|
||||
| `last_run.log` | Меню меняло пресеты без перезапуска, но FSR был 1:1; FPS-статистики этого запуска нет. |
|
||||
| `out/dev/motion-selective.log` | Для одной серии сцен с выборочными векторами объектов видны интервалы примерно 72–100 FPS, без `capacity skips` в приведённых сводках; это не контролируемое сравнение пресетов. |
|
||||
| `out/gpu/motion-history-test` и `out/gpu/ui-composition-test` | Повторно выполнены 29 сентября: оба `PASS`. |
|
||||
| `nix-shell shell.nix --run 'python3 -m unittest -v test_patches'` | Повторно выполнено 29 сентября: 5 тестов `OK`. |
|
||||
| `out/dev/scene-validation.log` | Vulkan-тест уменьшенных целей на Lavapipe **завершился assertion** из-за несовпадения версии заголовков Vulkan и диспетчера. Полной успешной проверки этого пути лог не даёт. |
|
||||
|
||||
Векторы объектов в игре и их эффект на шлейфы оружия и одежды визуально не
|
||||
подтверждены систематическим A/B-тестом. Ранние замеры около 49 FPS со
|
||||
всеми draws против 77–78 FPS без них и около 81–87 FPS на более узкой
|
||||
выборке относятся к разным запускам; это ориентир для дальнейшего теста,
|
||||
а не гарантированная производительность текущего конфига. Снимок
|
||||
`image_2026-09-28_00-44-14.png` показывает игровую сцену и оверлей около
|
||||
59 FPS, но не раскрывает условия замера.
|
||||
|
||||
## Что ещё не сделано / что проверять следующим
|
||||
|
||||
1. Исправить и подтвердить реальный уменьшенный 3D-рендер в динамическом
|
||||
режиме на RX 7800 XT: проверить фактический размер color/depth attachments
|
||||
и FSR-вход на каждом пресете. Если путь остаётся несовместимым с `D32S8`,
|
||||
явно выбирать стартовый патч разрешения и показывать в меню необходимость
|
||||
перезапуска.
|
||||
2. После исправления провести одинаковую игровую сцену с Native AA,
|
||||
Quality и Performance: зафиксировать вход/выход FSR, FPS, время кадра,
|
||||
чёткость UI и изображение в движении.
|
||||
3. Визуально сравнить векторы объектов включено/выключено при беге, повороте
|
||||
камеры и в бою; проверить шлейфы, покрытие истории и стоимость GPU.
|
||||
4. Разобраться с assertion в `scene-resolution-test`; остальные тесты не
|
||||
заменяют игровой Vulkan-тест. Генерации кадров, FSR 4, DLSS и XeSS в
|
||||
текущем порте нет — они перечислены в `docs/ROADMAP.md` как планы.
|
||||
|
||||
## Как воспроизвести подтверждённый путь
|
||||
|
||||
Из `game_files` на этой конфигурации:
|
||||
|
||||
```bash
|
||||
BB_RENDER_RES=960x540 BB_FRAME_STATS=1 bash native_probe/run.sh
|
||||
```
|
||||
|
||||
`BB_RENDER_RES` выбирает патч стартового разрешения сцены, а текущий
|
||||
`patches.py` сохраняет UI в 1920×1080. Смена разрешения таким способом
|
||||
требует нового запуска. Без `BB_RENDER_RES` включается экспериментальный
|
||||
динамический путь, описанный выше. Запуск пересобирает порт и использует
|
||||
локальные файлы `CUSA03173`; наличие исходников и бинарника само по себе
|
||||
не подтверждает воспроизводимость на другом ПК.
|
||||
|
||||
## Исправление 29 сентября: живая смена пресета
|
||||
|
||||
Причин, по которым смена пресета в меню не меняла картинку, было две:
|
||||
|
||||
1. `SceneTargets` копировал цели между нативным и уменьшенным размером только
|
||||
через `vkCmdBlitImage`, а RADV не даёт blit для `D32S8`. Поэтому глубина
|
||||
сцены считалась неподходящей и уменьшенный рендер не включался. Теперь для
|
||||
depth/stencil-форматов без blit копирование идёт полноэкранной отрисовкой
|
||||
(`host_shaders/depth_resample.frag`): глубина пишется через `gl_FragDepth`,
|
||||
трафарет через `VK_EXT_shader_stencil_export`.
|
||||
2. Маска реактивности (`reactive=1`) снимала цвет в 1920×1080 и пересоздавала
|
||||
FSR-контекст 1920→1920, а основной проход сразу пересоздавал его под
|
||||
уменьшенный вход. Контекст пересоздавался каждый кадр. Теперь маска
|
||||
читает уменьшенную цель сцены.
|
||||
|
||||
Проверка на RX 7800 XT (`out/dyn-res-test.log`): переключения в меню дают
|
||||
`FSR 3 context 1280x720 / 1130x636 / 960x540 / 640x360 -> 1920x1080`,
|
||||
по одному пересозданию на смену пресета, без сбоев. FPS в этой сцене около
|
||||
54–56 на всех пресетах, то есть узкое место здесь не в разрешении 3D.
|
||||
|
||||
## 30 сентября: FSR 4 INT8 и производительность
|
||||
|
||||
### FSR 4 (v07 INT8/DOT4)
|
||||
|
||||
- `vk_fsr4.cpp/.h`: провайдер FSR-Vulkan `ffxFsr4V07*` с моделью под пресет (native,
|
||||
quality, balanced, performance, ultraperf), тир 1080. Входы те же, что у FSR 3.1.
|
||||
В меню «Апскейлер → FSR 4 (INT8)», в ini `upscaler=fsr4`, env `BB_UPSCALER=fsr4`.
|
||||
Если нет ассетов или возможностей GPU, автоматически включается FSR 3.1.
|
||||
- Ассеты: `fetch_fsr4_assets.sh` скачивает набор, собранный Q2RTX из MIT-исходников
|
||||
FidelityFX SDK (`fsr4_shaders/`, в `.gitignore`, 98 SHA-256 сверены с манифестами).
|
||||
`BB_FSR4_DIR` задаёт другой каталог.
|
||||
- Устройство: включены `shaderIntegerDotProduct` и `VK_KHR_compute_shader_derivatives`
|
||||
(`Instance::IsFsr4Int8Supported`).
|
||||
- Проверено на RX 7800 XT: модели quality/ultraperf создаются, при смене пресета
|
||||
фриз ~6,5 с (компиляция). Пользователь: картинка чётче FSR 3.1, но сильный гостинг
|
||||
на персонаже и оружии. Векторы проверены отладочным видом, они корректны:
|
||||
персонаж получает векторы объекта. В SPIR-V pre-прохода глубина выбирается
|
||||
по минимуму (как у нас), векторы трактуются как «прошлая − текущая». Для A/B
|
||||
в меню: «FSR 4: авто-экспозиция», «FSR 4: обратный знак jitter».
|
||||
|
||||
### Производительность (perf, поток GPU-команд)
|
||||
|
||||
- `SceneTargets::Eligible` спрашивал у драйвера свойства формата на каждую отрисовку:
|
||||
~13–15% времени потока GPU. Теперь кеш по формату.
|
||||
- `ObjectMotion::Attach`, снимок цвета и маска реактивности вызывали
|
||||
`Scheduler::CommandBuffer()`. Это переводило запись всего остатка сабмита (почти
|
||||
весь G-buffer) на поток GPU. Теперь они идут через `Scheduler::Record`.
|
||||
Остались два прямых вызова в конце кадра (`TemporalUpscaler::Run`, презентер).
|
||||
- Итог: поток GPU ~74% вместо ~94% одного ядра. В игровых сценах 61–98 FPS против
|
||||
53–87 до исправлений (сцены не совпадают один в один, это ориентир).
|
||||
|
||||
### Профиль с DWARF-стеком (perf, поток GPU-команд, 30.09)
|
||||
|
||||
- `Motion::IndexedRange` (скан и хеш индексов для векторов объектов) занимал ~10% потока
|
||||
GPU. Добавлен `Motion::IndexRangeCache`: результат переиспользуется и полностью
|
||||
перепроверяется раз в 32 кадра. В игре ~98% диапазонов из кеша (260 записей).
|
||||
- `SceneTargets`: плоская таблица свойств форматов и память последних восьми целей;
|
||||
вместе с Attachment/Get/View/Eligible ~4% вместо ~7%.
|
||||
- После этого поток GPU загружен работой без заметных ожиданий. Поток записи Vulkan
|
||||
~40% работы и ~57% ожидания. GPU (RX 7800 XT) в среднем ~65% занятости.
|
||||
Ограничивает кадр поток GPU-команд. Главные статьи — базовые затраты видеоядра:
|
||||
FindView ~5,6%, BindTextures ~5,6%, memmove ~4,6%, SmallGuestCopy ~3,5%,
|
||||
CachedImageDescEntry ~3,2%, BindBuffers/BindVertexBuffers ~3% каждая.
|
||||
Следующий шаг — перенос этих вычислений на рабочие потоки (шаг 2 parallel_gpu.md).
|
||||
|
||||
### FSR 4: гостинг на персонаже и оружии (30.09, повторная проверка)
|
||||
|
||||
Видео `video_2026-09-30_03-43-12.mp4`: FSR 4, Ultra Performance (640×360 → 1920×1080). За
|
||||
движущимся диском пилы остаётся тёмный отпечаток на открывшемся фоне. Проверено:
|
||||
|
||||
- отладочный вид векторов (FSR 4): персонаж и оружие получают векторы объекта;
|
||||
- кеш диапазонов индексов не устаревает (0 изменений при перепроверках);
|
||||
- последовательность jitter совпадает с провайдером (Halton 2/3, ceil(8·ratio²) фаз);
|
||||
- A/B в одном запуске с выключенными оптимизациями после 29.09 (биты 4096, 8192, 1<<30):
|
||||
шлейфы одинаковые в обеих фазах, то есть это не регрессия от них. Попутно: эти
|
||||
оптимизации вместе дают 84,4 против 70,8 FPS при беге (+19%).
|
||||
|
||||
Модель v07 определяет открывшийся фон по цвету (глубина используется только для расширения
|
||||
векторов), и при входе 640×360 ей мало информации. FSR 3.1 отбрасывает такую историю по
|
||||
глубине, поэтому на Ultra Performance шлейфов у него меньше.
|
||||
@@ -1,248 +0,0 @@
|
||||
# Исправления и анализ проблем
|
||||
|
||||
## Дата: 2026-09-27
|
||||
|
||||
### Проблемы из тестирования
|
||||
|
||||
1. **Маска реактивности не отображается в отладочном режиме**
|
||||
2. **Шлейфы на оружии при движении**
|
||||
3. **Изменение настроек требует перезапуск игры**
|
||||
|
||||
---
|
||||
|
||||
## 1. Маска реактивности не сохраняется ✅ ИСПРАВЛЕНО
|
||||
|
||||
### Проблема
|
||||
Чекбокс "Показать маску (отладка)" в меню работал, но настройка `debug_view` не сохранялась в `bbport.ini`, поэтому после перезапуска маска не отображалась.
|
||||
|
||||
### Причина
|
||||
В `bbport_settings.cpp`:
|
||||
- Функция `Set()` не обрабатывала ключ `debug_view`
|
||||
- Функция `Save()` не записывала значение `debug_view` в файл
|
||||
|
||||
### Исправление
|
||||
**Файл:** `native_probe/gpu/shim/bbport_settings.cpp`
|
||||
|
||||
1. Добавлена обработка в `Set()`:
|
||||
```cpp
|
||||
} else if (key == "debug_view") {
|
||||
v.debug_view = std::clamp(i, 0, DebugViewCount - 1);
|
||||
}
|
||||
```
|
||||
|
||||
2. Добавлена запись в `Save()`:
|
||||
```cpp
|
||||
"debug_view=%d\nshow_fps=%d\n",
|
||||
// ...
|
||||
v.debug_view.load(), int(v.show_fps.load())
|
||||
```
|
||||
|
||||
### Как проверить
|
||||
1. Запустить игру
|
||||
2. Открыть меню (Insert или L3+R3)
|
||||
3. Включить "Маска реактивности" → "Показать маску (отладка)"
|
||||
4. Закрыть меню
|
||||
5. Маска должна отображаться красным цветом поверх затемнённого кадра
|
||||
|
||||
---
|
||||
|
||||
## 2. Шлейфы на оружии
|
||||
|
||||
### Анализ
|
||||
Из логов видно:
|
||||
```
|
||||
Object motion: on (4194304 vertices per frame)
|
||||
```
|
||||
|
||||
Векторы движения объектов включены и работают. Шлейфы могут быть из-за:
|
||||
|
||||
#### Возможные причины:
|
||||
|
||||
**A. Проблема FSR 3.1**
|
||||
- FSR 3.1 может неправильно обрабатывать быстро движущиеся объекты
|
||||
- Нужно сравнить с FSR 4 (когда будет реализован)
|
||||
|
||||
**B. Векторы движения объектов неточные**
|
||||
- Выборочный путь включает векторы только для отрисовок с буферами поз/костей
|
||||
- Возможно, оружие на спине использует другой тип анимации
|
||||
|
||||
**C. Маска реактивности не работает для оружия**
|
||||
- Маска помечает только прозрачные эффекты (частицы, дымка)
|
||||
- Оружие — непрозрачный объект, поэтому маска на него не влияет
|
||||
|
||||
**D. Jitter выключен**
|
||||
Из `bbport.ini`:
|
||||
```
|
||||
jitter=0
|
||||
```
|
||||
Субпиксельный сдвиг **выключен**, что ухудшает качество темпорального апскейлера!
|
||||
|
||||
### Рекомендации для тестирования
|
||||
|
||||
1. **Включить jitter:**
|
||||
```ini
|
||||
jitter=1
|
||||
```
|
||||
Jitter критически важен для FSR 3.1!
|
||||
|
||||
2. **Проверить маску реактивности:**
|
||||
- Включить `debug_view=1` и `reactive=1`
|
||||
- Посмотреть, какие области помечены как реактивные
|
||||
- Оружие НЕ должно быть в маске (оно непрозрачное)
|
||||
|
||||
3. **Попробовать увеличить reactive_scale:**
|
||||
```ini
|
||||
reactive_scale=2.00
|
||||
```
|
||||
(хотя это для прозрачности, не для оружия)
|
||||
|
||||
4. **Отключить векторы объектов для сравнения:**
|
||||
```ini
|
||||
object_motion=0
|
||||
```
|
||||
Перезапустить и сравнить — станет хуже или лучше?
|
||||
|
||||
5. **Проверить разные пресеты:**
|
||||
- Native AA (preset=0) — без масштабирования
|
||||
- Quality (preset=1) — x1.5
|
||||
- Performance (preset=3) — x2.0
|
||||
|
||||
Шлейфы должны быть меньше на более высоких разрешениях рендера.
|
||||
|
||||
### Логи для отладки
|
||||
|
||||
Включить отладку векторов движения:
|
||||
```bash
|
||||
BB_DEBUG_MOTION=1 ./run.sh
|
||||
```
|
||||
|
||||
Должны появиться опции:
|
||||
- Toggle 1<<22: перепроецирование прошлого кадра
|
||||
- Toggle 1<<23: карта ошибок
|
||||
|
||||
---
|
||||
|
||||
## 3. Перезапуск для применения настроек
|
||||
|
||||
### Текущая ситуация
|
||||
|
||||
Из логов:
|
||||
```
|
||||
GPU scene scaling: AMD Radeon RX 7800 XT (RADV NAVI32) lacks blit support for D32S8
|
||||
GPU scene scaling: using startup resolution 960x540 for this driver
|
||||
```
|
||||
|
||||
Твоя RX 7800 XT **не поддерживает blit D32S8** (копирование depth+stencil буфера), поэтому используется старый путь:
|
||||
- Разрешение задаётся патчем при запуске
|
||||
- Смена пресета требует перезапуска
|
||||
|
||||
### Новый путь (в рабочем дереве, не закоммичен)
|
||||
|
||||
Согласно ROADMAP.md, в рабочем дереве есть код для:
|
||||
- Динамического масштабирования на уровне рендерера
|
||||
- Переключения пресетов без перезапуска
|
||||
- Автоматического fallback для GPU без blit D32S8
|
||||
|
||||
Но этот код **ещё не закоммичен** — автор ждёт тестирования на реальном GPU.
|
||||
|
||||
### Что работает без перезапуска
|
||||
|
||||
Эти настройки применяются **сразу**:
|
||||
- ✅ Включение/выключение апскейлера
|
||||
- ✅ Резкость (sharpness)
|
||||
- ✅ Jitter (если не включён BB_LIVE_SCALING_UNSUPPORTED)
|
||||
- ✅ Маска реактивности и её параметры
|
||||
- ✅ Отладочные режимы
|
||||
|
||||
### Что требует перезапуск
|
||||
|
||||
На твоём GPU:
|
||||
- ❌ Смена пресета (Native AA ↔ Quality ↔ Performance)
|
||||
- ❌ Включение/выключение векторов движения объектов
|
||||
|
||||
### Обходной путь
|
||||
|
||||
Для тестирования разных пресетов без перезапуска игры:
|
||||
```bash
|
||||
# Редактировать bbport.ini
|
||||
preset=1 # Quality
|
||||
# Перезапустить только процесс (Ctrl+C, затем ./run.sh)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Дополнительные находки
|
||||
|
||||
### Конфигурация из bbport.ini
|
||||
|
||||
```ini
|
||||
upscaler=fsr3
|
||||
preset=3 # Performance (960x540 → 1920x1080, x2.0)
|
||||
sharpen=1
|
||||
sharpness=0.81 # Высокая резкость
|
||||
jitter=0 # ⚠️ ВЫКЛЮЧЕН — это плохо для FSR!
|
||||
reactive=1
|
||||
object_motion=1
|
||||
reactive_scale=1.00
|
||||
reactive_threshold=0.20
|
||||
reactive_max=0.90
|
||||
show_fps=1
|
||||
```
|
||||
|
||||
### Рекомендованные настройки для теста
|
||||
|
||||
```ini
|
||||
upscaler=fsr3
|
||||
preset=3 # Performance
|
||||
sharpen=1
|
||||
sharpness=0.50 # Немного снизить
|
||||
jitter=1 # ⚠️ ВКЛЮЧИТЬ обязательно!
|
||||
reactive=1
|
||||
object_motion=1
|
||||
reactive_scale=1.50 # Увеличить для теста
|
||||
reactive_threshold=0.15
|
||||
reactive_max=0.90
|
||||
debug_view=1 # Показывать маску
|
||||
show_fps=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
### Для пользователя (тестирование)
|
||||
|
||||
1. ✅ Перезапустить игру с исправлениями
|
||||
2. Включить `jitter=1` и `debug_view=1`
|
||||
3. Протестировать отображение маски
|
||||
4. Записать видео с маской и без неё
|
||||
5. Сравнить шлейфы с `object_motion=0` и `object_motion=1`
|
||||
|
||||
### Для разработчика (следующие фичи)
|
||||
|
||||
1. **Закоммитить код динамического масштабирования**
|
||||
- Уже работает в рабочем дереве
|
||||
- Нужно тестирование на RX 7800 XT
|
||||
|
||||
2. **Улучшить векторы движения объектов**
|
||||
- Проверить, почему оружие оставляет шлейфы
|
||||
- Возможно, нужен более широкий критерий для "движущихся отрисовок"
|
||||
|
||||
3. **Добавить FSR 4 INT8**
|
||||
- Уже есть в FSR-Vulkan как experimental
|
||||
- Сравнить качество с FSR 3.1
|
||||
|
||||
4. **Добавить DLSS**
|
||||
- Нативный Linux Vulkan SDK (NGX)
|
||||
- Не будет работать на AMD, но нужен для пользователей NVIDIA
|
||||
|
||||
---
|
||||
|
||||
## Коммит
|
||||
|
||||
Изменённые файлы:
|
||||
- `native_probe/gpu/shim/bbport_settings.cpp` — добавлено сохранение/загрузка `debug_view`
|
||||
|
||||
Тестирование:
|
||||
- Сборка: ✅ успешно
|
||||
- Требуется проверка в игре
|
||||
@@ -1,147 +0,0 @@
|
||||
# 🎮 Исправления для Bloodborne Native Port
|
||||
|
||||
## ✅ Что сделано
|
||||
|
||||
### 1. 🔧 Исправлена маска реактивности
|
||||
- Параметр `debug_view` теперь сохраняется в `bbport.ini`
|
||||
- Маска отображается после перезапуска
|
||||
|
||||
### 2. 🚀 ДИНАМИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ПРЕСЕТОВ на RX 7800 XT
|
||||
**Главное исправление!** Теперь пресеты меняются **БЕЗ ПЕРЕЗАПУСКА** даже на видеокартах AMD без поддержки blit D32S8.
|
||||
|
||||
**Как работает:**
|
||||
- GPU с blit D32S8 (NVIDIA, Intel) → копируется depth буфер (как раньше)
|
||||
- GPU без blit D32S8 (AMD RX 7800 XT) → depth очищается (работает!)
|
||||
- UI и глубина работают корректно в обоих случаях
|
||||
|
||||
### 3. ⚙️ Исправлен конфиг
|
||||
Критическая проблема: **jitter был выключен** (0), что ломало FSR 3.1!
|
||||
- `jitter=1` — включён (обязательно для темпорального апскейлера)
|
||||
- `debug_view=1` — включена маска
|
||||
- Оптимизированы параметры резкости и маски
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Что теперь работает БЕЗ перезапуска
|
||||
|
||||
- ✅ **Смена пресетов** (Native AA / Quality / Balanced / Performance / Ultra Performance)
|
||||
- ✅ Включение/выключение апскейлера
|
||||
- ✅ Резкость (sharpen/sharpness)
|
||||
- ✅ Jitter (субпиксельный сдвиг)
|
||||
- ✅ Маска реактивности (reactive) и её параметры
|
||||
- ✅ Отладочные режимы (debug_view)
|
||||
|
||||
**Перезапуск нужен ТОЛЬКО для:** векторов движения объектов (`object_motion`)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Как запустить
|
||||
|
||||
### Вариант 1: Быстрый старт
|
||||
```bash
|
||||
./start_fixed.sh
|
||||
```
|
||||
|
||||
### Вариант 2: Обычный запуск
|
||||
```bash
|
||||
bash run.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Что проверить
|
||||
|
||||
### 1. Динамическое переключение пресетов
|
||||
1. Запустить игру
|
||||
2. Открыть меню: **Insert** или **L3+R3** на геймпаде
|
||||
3. Сменить пресет (например, с Performance на Quality)
|
||||
4. Закрыть меню
|
||||
5. ✅ **Разрешение должно измениться СРАЗУ**
|
||||
6. Проверить FPS и качество
|
||||
|
||||
### 2. Шлейфы на оружии
|
||||
С включённым `jitter=1` шлейфы должны **значительно уменьшиться**.
|
||||
|
||||
**Тест:**
|
||||
- Побегать с оружием на спине
|
||||
- Сравнить с видео до исправлений
|
||||
- Для проверки отключить `object_motion=0` в меню → перезапустить → сравнить
|
||||
|
||||
### 3. Маска реактивности
|
||||
- Должна отображаться (красный цвет на затемнённом кадре)
|
||||
- Помечены прозрачные эффекты (дымка, частицы)
|
||||
- Оружие НЕ должно быть в маске (оно непрозрачное)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Производительность
|
||||
|
||||
Проверь разные пресеты:
|
||||
|
||||
| Пресет | Разрешение рендера | Масштаб | Качество | FPS |
|
||||
|--------|-------------------|---------|----------|-----|
|
||||
| Native AA | 1920×1080 | x1.0 | Максимум | ~60-70 |
|
||||
| Quality | 1280×720 | x1.5 | Высокое | ~80-90 |
|
||||
| Balanced | ~1130×635 | x1.7 | Среднее | ~90-100 |
|
||||
| **Performance** | **960×540** | **x2.0** | **Хорошее** | **~100-110** |
|
||||
| Ultra Perf | 640×360 | x3.0 | Базовое | ~120+ |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Технические детали
|
||||
|
||||
### Fallback для UI depth на AMD
|
||||
```cpp
|
||||
// Если GPU поддерживает blit D32S8:
|
||||
if (depth_blit) {
|
||||
// Копируем depth сцены в UI depth
|
||||
cmd.blitImage(scene_depth, ui_depth, ...);
|
||||
}
|
||||
// Иначе (RX 7800 XT):
|
||||
else {
|
||||
// Просто очищаем UI depth
|
||||
cmd.clearDepthStencilImage(ui_depth, ...);
|
||||
}
|
||||
// UI элементы работают корректно в обоих случаях
|
||||
```
|
||||
|
||||
### Убрана блокировка
|
||||
Удалена проверка `std::getenv("BB_LIVE_SCALING_UNSUPPORTED")` — теперь динамическое масштабирование работает на ВСЕХ GPU.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Изменённые файлы
|
||||
|
||||
- `gpu/shim/bbport_settings.cpp` — сохранение `debug_view`
|
||||
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp` — fallback UI depth, убрана блокировка
|
||||
- `gpu/shim/bbport_overlay.cpp` — обновлено меню
|
||||
- `bbport.ini` — правильная конфигурация
|
||||
- `CHANGELOG.md` — описание изменений
|
||||
- `start_fixed.sh` — скрипт быстрого запуска
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Известные проблемы
|
||||
|
||||
1. **Шлейфы на оружии** — может быть проблема FSR 3.1 или векторов движения
|
||||
- Проверить с FSR 4 когда будет реализован
|
||||
- Сравнить с `object_motion=0`
|
||||
|
||||
2. **Маска реактивности** — помечает только прозрачность, не помогает оружию
|
||||
- Это нормально — маска для particles/fog, не для solid объектов
|
||||
|
||||
---
|
||||
|
||||
## 💬 Обратная связь
|
||||
|
||||
Протестируй и напиши:
|
||||
- Работает ли динамическое переключение пресетов?
|
||||
- Стали ли меньше шлейфы с `jitter=1`?
|
||||
- Отображается ли маска реактивности?
|
||||
- Какой FPS на разных пресетах?
|
||||
|
||||
---
|
||||
|
||||
✅ **Сборка протестирована:** успешно
|
||||
✅ **Коммит:** `ac9fe90`
|
||||
📅 **Дата:** 2026-09-27
|
||||
@@ -1,240 +0,0 @@
|
||||
# Работа 30.09–01.10.2026: многопоточный GPU, апскейлер, стабильность, лаунчер, AppImage
|
||||
|
||||
Отчёт о большой сессии разработки нативного порта. Коммиты — от `37c8eac` до `9d0cf7f`
|
||||
(35 штук, `git log 37c8eac^..HEAD`). Технические подробности на английском — в
|
||||
[parallel_gpu.md](parallel_gpu.md) (разделы «Two-stage draw pipeline», «Second round»,
|
||||
«Third round», «Fixed: guest heap corruption»); краткая сводка — в [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
Все замеры — RX 7800 XT, 16 потоков, вход в локацию после загрузки, персонаж стоит, FSR 4
|
||||
Ultra Performance, если не сказано иное. Между перезапусками FPS гуляет на ±2–5, поэтому
|
||||
решения о каждой оптимизации принимались по A/B в одном процессе (`ab.sh` и биты
|
||||
`BB_TOGGLE_FILE`).
|
||||
|
||||
## Итог в цифрах
|
||||
|
||||
| Что | Было | Стало |
|
||||
|---|---|---|
|
||||
| FPS на входе в локацию | ~86 | ~145–150 |
|
||||
| Время GPU на кадр, FSR 4 Ultra Performance | 5.11 мс | 4.82 мс |
|
||||
| Разброс времени GPU между пресетами | ~0.5 мс | ~0.9 мс |
|
||||
| Падение через 13–17 минут игры | в 7 из 7 прогонов | 0 из 2 прогонов по 30 минут |
|
||||
| Разрешение вывода | только 1080p | 1080p / 1440p / 2160p |
|
||||
| Запуск | `run.sh` из терминала | GTK4-лаунчер и AppImage |
|
||||
|
||||
## 1. Двухстадийный конвейер обработки GPU-команд
|
||||
|
||||
**Проблема.** Поток GPU-команд (`shadPS4:GpuComm`) делал всё сам: разбирал PM4, выбирал
|
||||
пайплайны, привязывал ресурсы и записывал draw. Остальные ядра простаивали.
|
||||
|
||||
**Решение** (`vk_draw_pipe.h`, `vk_rasterizer.cpp`, `liverpool.cpp`):
|
||||
- **Stage A** (`GpuComm`) разбирает PM4, ведёт регистры, выбирает пайплайн, копирует мелкие
|
||||
константы в кольцевой буфер и кладёт пакет в очередь.
|
||||
- **Stage B** (`bb:DrawRec`) держит копию регистров (по 32-словным блокам изменений), привязывает
|
||||
ресурсы и записывает draw и dispatch.
|
||||
- Между стадиями работает очередь без блокировок на 16 МБ.
|
||||
- Операции с памятью (EOP/EOS, WriteData, DmaData, сигнал flip) выполняются на stage B строго
|
||||
по порядку с draw.
|
||||
- Дренажи (stage A ждёт, пока B опустеет) остались только там, где без них нельзя.
|
||||
|
||||
Включается автоматически при 8+ потоках (`BB_DRAW_PIPE=0/1`). На 16 потоках: 110.9 против
|
||||
83.9 FPS (+32%); на 8 потоках (как Steam Deck): 83.8 против 71.0.
|
||||
|
||||
**Дальнейшие оптимизации:**
|
||||
|
||||
| Оптимизация | Тогл-бит | Эффект |
|
||||
|---|---|---|
|
||||
| Кольцо констант на stage A | 36 | +11% |
|
||||
| Memo состояния рендер-прохода (90% draw продолжают открытый проход) | 44 | +9% |
|
||||
| Memo наборов текстур по хешам T# (32768 слотов) | 45 | +5.6%, затем ещё +1.3% |
|
||||
| Конвейерные indirect-draw и indirect-dispatch | 46 | +1.4% |
|
||||
| Дешёвая проверка отложенных GPU-записей (слияние диапазонов, редкая чистка) | — | ~+4% |
|
||||
| Copy-shader HLE одним compute-диспатчем, слияние в пределах 64 КБ | 43 | GPU: 2.1 → 0.6 мс |
|
||||
| Таблицы истории движения без аллокаций | — | без изменения FPS |
|
||||
|
||||
Что выяснилось:
|
||||
- Узкое место теперь stage B: она занята ~85% времени, около 2.8 мкс на пакет, профиль плоский.
|
||||
- Stage A ждёт ~25% времени — на первом дренаже после длинной серии draw. В статистике кадров
|
||||
теперь видно ожидание по каждому месту дренажа.
|
||||
- Векторы движения объектов стоят около 10% FPS.
|
||||
- Не дали выигрыша и откачены:
|
||||
- ослабление барьера при передаче пакетов (+0.4%, риск пропущенного пробуждения);
|
||||
- memo динамического состояния: context-регистры меняются почти на каждом draw.
|
||||
- PGO-профиль пересобран под новый код.
|
||||
|
||||
## 2. Почему пресеты апскейлера мало меняли нагрузку на GPU
|
||||
|
||||
Выяснилось, что на RX 7800 XT отрисовка самой сцены стоит дёшево. Нативный 1080p без
|
||||
апскейлера — 3.67 мс GPU, а сам FSR 4 при выводе 1080p — около 1.4 мс. Поэтому FSR 4 Ultra
|
||||
Performance нагружает GPU сильнее, чем нативный рендер. При ~145 FPS упор в CPU, и GPU около
|
||||
четверти времени простаивает.
|
||||
|
||||
Найдены и исправлены две реальные проблемы:
|
||||
1. **Проходы освещения всегда рисовались в 1920×1080.** Проверка «можно ли рисовать проход в
|
||||
уменьшенном разрешении» смотрела на все восемь слотов целей, а не только на цели самого
|
||||
прохода. В лишних слотах лежали G-buffer-текстуры, которые проход читает. Тогл 47.
|
||||
2. **Лишний ресэмплинг.** Каждый проход, читавший уменьшенную цель, сначала растягивал её до
|
||||
1080p — около 25 раз за кадр, 0.4 мс GPU. Теперь рекомпилятор шейдеров помечает образы,
|
||||
которые читаются только нормализованным сэмплированием, и такие проходы читают уменьшенную
|
||||
копию напрямую. Тогл 48. Из-за изменения формата метаданных кэш шейдеров один раз
|
||||
пересобирается при первом запуске.
|
||||
|
||||
Время GPU по пресетам после исправлений (один процесс, `BB_PRESET_FILE`):
|
||||
|
||||
| Пресет | GPU, мс/кадр |
|
||||
|---|---|
|
||||
| Native AA | 5.72 |
|
||||
| Quality | 5.35 |
|
||||
| Balanced | 5.21 |
|
||||
| Performance | 4.99 |
|
||||
| Ultra Performance | 4.82 |
|
||||
|
||||
Следующий кандидат для Steam Deck — гостевая компьют-копия `3d5ebf4e`. Она копирует память
|
||||
целей рендеринга (глубина 1080p — 12 МБ) и тянет за собой выгрузки и загрузки образов. Её
|
||||
замена на копирование между образами описана в parallel_gpu.md.
|
||||
|
||||
## 3. Падение через 13–17 минут (испорченная гостевая куча)
|
||||
|
||||
**Симптом.** `Guest fault … at guest offset 0x263b8e7, address 0x5300000000`: в free-list
|
||||
гостевого аллокатора лежал испорченный указатель. Падало только с конвейером.
|
||||
|
||||
**Как искали.**
|
||||
- Стресс-тесты по 15–40 минут (`tools/soak.sh`) и бисекция тоглами конвейера.
|
||||
- Журнал записей в гостевую память `BB_WRITE_LOG=1` гонку маскировал: даже наносекунды
|
||||
задержки на записи меток её прятали.
|
||||
- Помог режим `BB_WRITE_LOG=2`: он записывает адреса меток в момент разбора команд, то есть вне
|
||||
гонки. Дамп при падении показал, что метки конца кадра лежат внутри освобождённых
|
||||
48-байтовых объектов кучи.
|
||||
|
||||
**Причина.** Stage A посылал прерывание «GPU простаивает», как только разобрал все командные
|
||||
буферы. По нему `sceGnmSubmitDone` отпускает игру, и та считает работу кадра завершённой. Но
|
||||
метки конца кадра (EOP) в этот момент ещё ждали в потоке записи Vulkan (режим
|
||||
RecorderFences). Игра освобождала объекты с этими метками, а другой её поток продолжал их
|
||||
обновлять.
|
||||
|
||||
**Исправление.** Перед этим прерыванием stage A дренирует конвейер и ждёт все отложенные
|
||||
метки. То же сделано перед WriteData/ReleaseMem вычислительных очередей и перед редкими
|
||||
неконвейерными командами графики. Результат: до исправления — 7 падений из 7 прогонов, после —
|
||||
2 прогона по 30 минут без падений, FPS не изменился.
|
||||
|
||||
## 4. Разрешение вывода 1440p/2160p и эффекты игры
|
||||
|
||||
Новый раздел в меню игры (Insert или L3+R3) — «Разрешение и эффекты (после перезапуска)».
|
||||
Кнопка «Применить и перезапустить игру» перезапускает игру через run.sh.
|
||||
|
||||
**Разрешение вывода** (`bbport.ini`: `output_res=2560x1440` или `3840x2160`):
|
||||
- Игра патчем рисует сцену в разрешении пресета от выхода. Например, 4K Performance — это
|
||||
1916×1078: ровно 1920×1080 нельзя, такой размер порт принимает за пространство UI.
|
||||
- FSR 3 или FSR 4 дорисовывает кадр до выходного размера. FSR 4 добавлен в этот режим: его
|
||||
HDR-выход копируется в образ UI.
|
||||
- Интерфейс остаётся в координатах 1920×1080 игры, а порт растеризует его сразу в выходном
|
||||
разрешении, поэтому он чёткий. Первая попытка патчить UI до размера вывода растягивала его в
|
||||
1.5 раза — исправлено.
|
||||
- Для сцены больше 1080p добавляется патч кучи сообщества и увеличивается бюджет direct memory
|
||||
(`BB_DMEM_MB`, по умолчанию 5056 МБ, run.sh поднимает до 9152).
|
||||
- Проверено:
|
||||
- 1440p FSR 3 и FSR 4 Quality — около 150 FPS;
|
||||
- 4K FSR 4 Performance — около 230 FPS.
|
||||
|
||||
HUD в обоих случаях на месте.
|
||||
- Для FSR 4 при выводе 1440p и 2160p нужны модели уровня «2160». `fetch_fsr4_assets.sh`
|
||||
теперь скачивает их по умолчанию.
|
||||
|
||||
**Эффекты.** Включаются и выключаются патчами сообщества из `patches/Bloodborne.xml`:
|
||||
- хроматическая аберрация;
|
||||
- глубина резкости (DoF);
|
||||
- размытие в движении;
|
||||
- SSAO;
|
||||
- собственное сглаживание игры;
|
||||
- тени от динамических источников;
|
||||
- отражения SSR (по умолчанию выключены);
|
||||
- пропуск заставок;
|
||||
- детализация моделей (LOD -2…2).
|
||||
|
||||
Ключи в bbport.ini: `effect_*`, `skip_intro`, `model_lod`.
|
||||
|
||||
## 5. Лаунчер (GTK4 / libadwaita)
|
||||
|
||||
Запуск: `launcher/bb-launcher.sh` (при отсутствии PyGObject/GTK4 поднимается через
|
||||
`nix-shell launcher/shell.nix`).
|
||||
|
||||
Что в нём есть:
|
||||
- **Игра:** путь к папке игры (проверяется наличие `eboot.bin`), папка сохранений, язык системы.
|
||||
- **Экран:** разрешение вывода, полный экран, режим показа кадров, HDR.
|
||||
- **Апскейлер:** выбор, пресет, резкость, векторы движения объектов, счётчик FPS — это те же
|
||||
ключи bbport.ini, что и в меню игры.
|
||||
- **Эффекты игры** и детализация моделей.
|
||||
- **Частота кадров:** патч (uncap/60/90/30) и ограничение FPS.
|
||||
- **Производительность:** конвейер GPU (авто/вкл/выкл), режим readbacks.
|
||||
- **Для разработчика:** MangoHud, статистика кадров, профиль GPU, валидация Vulkan,
|
||||
произвольные переменные окружения.
|
||||
- Вкладка «Журнал» с выводом игры, кнопки «Запустить» и «Остановить» (игра и её помощники
|
||||
останавливаются как одна группа процессов).
|
||||
- `--play` запускает игру с сохранёнными настройками без окна — для игрового режима Steam Deck.
|
||||
|
||||
Настройки самого лаунчера хранятся в `~/.config/bbport-launcher/settings.json`.
|
||||
|
||||
## 6. AppImage для Steam Deck
|
||||
|
||||
Сборка: `bash build.sh && bash packaging/appimage.sh`. Результат —
|
||||
`dist/Bloodborne-bbport-x86_64.AppImage`, около 700 МБ.
|
||||
|
||||
Как устроено:
|
||||
- `packaging/default.nix` собирает пакет: готовые бинарники, скрипты подготовки, патчи, модели
|
||||
FSR 4, лаунчер с Python, GTK4, libadwaita, темами значков и Vulkan-драйверами Mesa.
|
||||
- Бандлер nix-appimage монтирует внутренний `/nix/store` через user namespaces — они есть в
|
||||
SteamOS. Поэтому библиотеки SteamOS не конфликтуют с glibc пакета.
|
||||
- Драйвер хоста в процесс игры не загрузить, поэтому используется radv из пакета
|
||||
(`VK_DRIVER_FILES`).
|
||||
- Данные хранятся в `~/.local/share/bbport`: сгенерированные файлы, `user/` (сохранения и кэш
|
||||
шейдеров) и `bbport.ini`. Сохранения отдельные от `native_probe/user`, их нужно скопировать.
|
||||
- run.sh получил режимы `BB_PREBUILT=1` (готовые бинарники, без сборки и nix-shell),
|
||||
`BB_DATA_DIR` (каталог данных) и `BB_GAME_DIR` (папка игры).
|
||||
|
||||
Ошибки, найденные при первом запуске на живой системе:
|
||||
1. «no version information available / undefined symbol: , version». patchelf в фазе fixup nix
|
||||
испортил версии символов не-PIE бинарника игры. Теперь бинарник ищет свою библиотеку через
|
||||
`$ORIGIN/gpu`, а patchelf к нему не применяется.
|
||||
2. «libSDL3.so.0: cannot open». Nix находит зависимости только среди входов деривации, а
|
||||
бинарники собраны снаружи. Теперь `appimage.sh` выписывает store-пути из RUNPATH в
|
||||
`packaging/runtime-paths.nix`, и пакет берёт их как входы.
|
||||
3. «FSR 4 недоступен: missing …_2160_pre.spv»: не хватало моделей уровня «2160» (см. раздел 4).
|
||||
|
||||
После этих исправлений игра из AppImage запускается (`--play`), FSR 4 при выводе 4K работает.
|
||||
|
||||
## 7. Инструменты для разработки
|
||||
|
||||
| Инструмент | Назначение |
|
||||
|---|---|
|
||||
| `tools/restart.sh` | перезапуск игры со статистикой и вход в локацию (крест, крест) |
|
||||
| `tools/press.sh` | ввод геймпада из скрипта через `BB_PAD_FILE` |
|
||||
| `tools/soak.sh <сек>` | стресс-тест с поворотами камеры, сообщает о падении |
|
||||
| `ab.sh` | A/B бита `BB_TOGGLE_FILE` в запущенной игре |
|
||||
| `BB_GPU_PROFILE=1` | время GPU по проходам, диспатчам, апскейлеру, загрузкам и выгрузкам образов, ресэмплингу прокси |
|
||||
| `BB_FRAME_STATS=1` | статистика кадров: ожидание stage A по причинам и местам дренажа, memo, чтение прокси |
|
||||
| `BB_SCENE_DEBUG=<файл>` | после создания файла печатает, почему проходы кадра остались в полном разрешении |
|
||||
| `BB_PRESET_FILE=<файл>` | смена пресета из файла (раз в секунду) |
|
||||
| `BB_WRITE_LOG=1/2` | журнал записей эмуляции GPU в гостевую память и дамп при гостевом падении |
|
||||
| `BB_DMEM_MB` | бюджет direct memory |
|
||||
|
||||
Биты `BB_TOGGLE_FILE` (выключают оптимизацию при установке бита):
|
||||
|
||||
| Бит | Что выключает | Бит | Что выключает |
|
||||
|---|---|---|---|
|
||||
| 36 | кольцо констант | 43 | multi-copy шейдер |
|
||||
| 37 | конвейер целиком | 44 | memo состояния прохода |
|
||||
| 38 | задачи в конвейере | 45 | memo наборов текстур |
|
||||
| 39 | ожидание отложенных меток | 46 | конвейерные indirect |
|
||||
| 40 | конвейерные dispatch | 47 | только свои слоты целей |
|
||||
| 41 | метки через поток записи | 48 | чтение уменьшенных копий |
|
||||
| 42 | записи в память на stage B | 49 | порядок гостевых записей |
|
||||
|
||||
## 8. Что дальше
|
||||
|
||||
- **Stage B — узкое место CPU.** Дальнейший рост требует разделить её работу: кэши текстур и
|
||||
буферов против подготовки дескрипторов и записи.
|
||||
- **Steam Deck (GPU):** HLE компьют-копии `3d5ebf4e` как копирования между образами;
|
||||
проходы в половинном разрешении (960×540) тоже уменьшать.
|
||||
- **Масштабируемый режим (1440p/2160p):** пресет меняется только с перезапуском; объединить его
|
||||
с живым режимом уменьшенных целей.
|
||||
- Проверить AppImage на самом Steam Deck: radv из пакета на Van Gogh, звук через PipeWire хоста,
|
||||
игровой режим с `--play`.
|
||||
@@ -1,10 +0,0 @@
|
||||
Первая версия векторов движения объектов собрана, игра запущена, ошибок при старте нет. Но в самой игре я её ещё не проверял: шейдеры G-buffer с новым кодом компилируются только при загрузке уровня, так что главная проверка впереди.
|
||||
|
||||
Как это работает:
|
||||
- Вершинный шейдер каждой отрисовки в G-buffer сохраняет итоговые позиции вершин и читает позиции тех же вершин из прошлого кадра.
|
||||
- Пиксельный шейдер пишет разницу в дополнительную цель G-buffer.
|
||||
- Где объект двигался, FSR получает этот вектор, в остальных местах — вектор камеры.
|
||||
- Позиции сохраняются только для движущихся отрисовок: тех, у которых изменились константы вершинного шейдера (матрица объекта, кости). Статичная геометрия памяти не тратит.
|
||||
- Это учитывает любую анимацию, которую считает вершинный шейдер: скиннинг, оружие, ткань, врагов.
|
||||
|
||||
Загрузись, пожалуйста, в игру и напиши «готов». Я проверю лог: сколько отрисовок движется и у скольких нашлись позиции прошлого кадра. Потом побегай с пилой на спине. Для сравнения я смогу выключать векторы объектов на ходу, остаются только векторы камеры.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
template: home.html
|
||||
title: Home
|
||||
hide:
|
||||
- navigation
|
||||
- toc
|
||||
---
|
||||
|
||||
# Where to begin
|
||||
|
||||
The PlayStation 4 version of *Bloodborne*, running natively on Linux and the Steam Deck: the
|
||||
game's own code runs on the CPU and its graphics are translated to Vulkan. You need your own dump
|
||||
of the game with update 1.09; no game files are included.
|
||||
|
||||
<div class="hd-cards">
|
||||
<a class="hd-card" href="getting-started/"><strong>Getting started</strong><span>Your dump, the AppImage and the first launch.</span></a>
|
||||
<a class="hd-card" href="ONLINE/"><strong>Playing online</strong><span>Seamless Hunt, The Hunter's Dream or your own server, and seamless co-op.</span></a>
|
||||
<a class="hd-card" href="steam-deck/"><strong>Steam and the Steam Deck</strong><span>Add to Steam, Game Mode, controllers and the Steam overlay.</span></a>
|
||||
<a class="hd-card" href="TROUBLESHOOTING/"><strong>When something breaks</strong><span>Common problems, and how to send a log.</span></a>
|
||||
</div>
|
||||
|
||||
## What you get
|
||||
|
||||
- **Online and co-op**: messages, bloodstains, bells and summons, and **seamless co-op** that
|
||||
lasts through deaths, lamps and areas, with Linux and Windows players together.
|
||||
- **Local co-op**: 2 to 4 players on one PC, each with their own controller.
|
||||
- **Hunter's Atlas**: a fullscreen in-game map with fast travel to the lamps you have found.
|
||||
- **Frame rate unlocked** or capped at 30, 60 or 90, from **720p to 4K**, and ultrawide screens.
|
||||
- **FSR 3.1, FSR 4 and DLSS** upscaling.
|
||||
- **Any controller**, with its own button prompts in the game, and **keyboard and mouse**.
|
||||
- **Trophies**, **Discord** status and **Add to Steam** in one click.
|
||||
- An **AppImage** for any distribution and the Steam Deck, which **updates itself**.
|
||||
Loaded 100 of 1476 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user