mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-12 18:08:46 -10:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10c369e05e | ||
|
|
18205f83d1 | ||
|
|
e10e2a8fb4 | ||
|
|
14c0d9b426 | ||
|
|
b76c950a66 | ||
|
|
cd8b014ab3 |
No files matched your search
@@ -32,7 +32,8 @@ seamless co-op, the Steam Deck, and what to do when something goes wrong.
|
||||
**720p to 4K**, and **ultrawide** (21:9, 32:9, [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.
|
||||
- **Any controller:** PlayStation, Xbox and Switch pads with their own **button prompts in the
|
||||
game**, and **keyboard and mouse**, with a direct 1:1 mouse camera and keys you can rebind.
|
||||
game**, and **keyboard and mouse**, with a direct 1:1 mouse camera, keys you can rebind
|
||||
(or bbport 0.6's Dark Souls III-style layout) and the mouse as a pointer in the game's menus.
|
||||
- **Quit to Desktop** from the game's title and System menus (it saves first).
|
||||
- **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.
|
||||
|
||||
+4
-1
@@ -30,7 +30,8 @@ under *People* and *Help and testing* also appears in the launcher's **About** s
|
||||
|
||||
## People
|
||||
|
||||
- **[deadinside28](https://github.com/deadinside28)**: bbport, the port this whole project is built on.
|
||||
- **[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, DLSS, the cheats page, Add to Steam,
|
||||
the icon, and many fixes from the Windows port.
|
||||
- **[0xCydral](https://github.com/0xCydral)**: gathered and adapted dozens of fixes (crashes while
|
||||
@@ -110,3 +111,5 @@ under *People* and *Help and testing* also appears in the launcher's **About** s
|
||||
- **[BlazinKarp](https://github.com/BlazinKarp)**: the Steam overlay in the game.
|
||||
- **[molnar-a](https://github.com/molnar-a)**: a crash on the Steam Deck after lamp travel, and testing the fix.
|
||||
- **[gibs2193](https://github.com/gibs2193)**: the mouse camera turning slower sideways than up and down.
|
||||
- **[zFenrys](https://github.com/zFenrys)**: asking for the mouse pointer in the game's menus.
|
||||
- **[Aman9das](https://github.com/Aman9das)**: asking for bbport 0.6's keyboard layout.
|
||||
+72
-68
@@ -10,7 +10,7 @@ the code disagree, the code wins.
|
||||
|
||||
**In the launcher:** go to *Advanced → Developer → Extra variables (NAME=value, space-separated)*.
|
||||
The settings key is `extra_env` in the launcher's `settings.json`. The field is parsed in
|
||||
`game_environment()` (`launcher/bbport_launcher.py:706`):
|
||||
`game_environment()` (`launcher/bbport_launcher.py:707`):
|
||||
|
||||
```python
|
||||
for item in s["extra_env"].split():
|
||||
@@ -49,7 +49,7 @@ environment. A few variables override those keys at start: `BB_UPSCALER`, `BB_UP
|
||||
The environment is built in this order. A later step wins.
|
||||
|
||||
1. **The environment the launcher (or the AppImage) was started with.**
|
||||
2. **The launcher's own settings** (`game_environment()`, `launcher/bbport_launcher.py:632-711`).
|
||||
2. **The launcher's own settings** (`game_environment()`, `launcher/bbport_launcher.py:633-712`).
|
||||
- Some are always written: `BB_GAME_DIR`, `BB_MODS_*`, `BB_PROMPTS`, `BB_GESTURES_NO_MOTION`,
|
||||
`BB_PATCHES_*`, `BB_LANGUAGE`, `BB_REGION`, `BB_MENU_LANGUAGE`, `BB_FULLSCREEN`,
|
||||
`BB_PRESENT_MODE`, `BB_SKIP_NETWORK_CHOICE`, `BB_FPS`, `BB_DISCORD` and `BB_STEAM_OVERLAY`. For these, a shell value
|
||||
@@ -58,7 +58,7 @@ The environment is built in this order. A later step wins.
|
||||
`BB_DRAW_PIPE`, `BB_READBACKS`, `BB_PREUPLOAD`, `BB_PC_MODEL`, `BB_GPU_PROFILE`,
|
||||
`BB_PAD_DEADZONE` (only when the dead zone is not 4 %), `BB_PAD_LIGHTBAR` and others. For these, a shell value
|
||||
survives while the setting is off.
|
||||
- **Hunter's Atlas** (`atlas_environment()`, `launcher/bbport_launcher.py:611`): always
|
||||
- **Hunter's Atlas** (`atlas_environment()`, `launcher/bbport_launcher.py:612`): always
|
||||
`BB_ATLAS_ENABLED` (0 when the map is off or not prepared); with the map on also `BB_MAP_DIR`,
|
||||
`BB_MAP_ART_DIR`, `BB_MAP_FONT`, `BB_MAP_CATALOG`, `BB_MAP_CONFIG`, `BB_MAP_TRAVEL_ENABLED`,
|
||||
`BB_MAP_ITEMS_ENABLED`, `BB_MAP_ITEM_CATEGORIES`, `BB_MAP_FOG_ENABLED=0` and
|
||||
@@ -136,20 +136,20 @@ behaviour when the variable is unset. In the tables:
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_CJK_FONT` | path to a font file (unset: fontconfig's choice for zh-cn) | Font merged into the in-game menu font for Simplified Chinese texts; without one, fc-match picks a system font (Noto Sans CJK, WenQuanYi...), else Chinese text shows as boxes. | `gpu/shim/bbport_overlay.cpp:1145` |
|
||||
| `BB_DISPLAY` | monitor number (1, 2, ... in SDL order) or part of its name (unset: SDL's primary display) | **Launcher:** Graphics > Display > Monitor. The monitor the window and fullscreen go to (issue #69); `bb-gpu-capabilities --displays` lists them, and the log says which one was taken. No match: the primary one. | `gpu/shim/window.cpp:16`, `launcher/bbport_launcher.py:657` |
|
||||
| `BB_FULLSCREEN` | 1 or 0 (windowed) | **Launcher:** Graphics > Display > Fullscreen. Creates the game window fullscreen when the value starts with 1. | `gpu/shim/window.cpp:116`, `launcher/bbport_launcher.py:652` |
|
||||
| `BB_DISPLAY` | monitor number (1, 2, ... in SDL order) or part of its name (unset: SDL's primary display) | **Launcher:** Graphics > Display > Monitor. The monitor the window and fullscreen go to (issue #69); `bb-gpu-capabilities --displays` lists them, and the log says which one was taken. No match: the primary one. | `gpu/shim/window.cpp:76`, `launcher/bbport_launcher.py:658` |
|
||||
| `BB_FULLSCREEN` | 1 or 0 (windowed) | **Launcher:** Graphics > Display > Fullscreen. Creates the game window fullscreen when the value starts with 1. | `gpu/shim/window.cpp:176`, `launcher/bbport_launcher.py:653` |
|
||||
| `BB_GPU_ID` | Vulkan device index (-1: automatic ranking) | Which GPU the game uses, by its index in the Vulkan device list; out of range asserts. bb-gpu-capabilities follows it too. | `gpu/shim/core/emulator_settings.h:22`, `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:146`, `tools/gpu_capabilities.c:257` |
|
||||
| `BB_HDR` | 1/y/t or unset (off) | **Launcher:** Graphics > Display > Allow HDR (set only when on). Allows an HDR swapchain (the colour-space extension and an HDR surface format) when the display offers it. | `gpu/shim/core/emulator_settings.h:60`, `gpu/shadps4/video_core/renderer_vulkan/vk_swapchain.cpp:246`, `launcher/bbport_launcher.py:665` |
|
||||
| `BB_HDR` | 1/y/t or unset (off) | **Launcher:** Graphics > Display > Allow HDR (set only when on). Allows an HDR swapchain (the colour-space extension and an HDR surface format) when the display offers it. | `gpu/shim/core/emulator_settings.h:60`, `gpu/shadps4/video_core/renderer_vulkan/vk_swapchain.cpp:246`, `launcher/bbport_launcher.py:666` |
|
||||
| `BB_LIVE_RES` | 0 \| 1 (default: bbport.ini live_resolution, else off) | For output sizes other than 1080p: 1 = live resolution changes (guest at 1080p, host targets scaled at run time, change without restart; slower), 0 = startup resolution patch. Overrides bbport.ini live_resolution=0/1/auto. Outputs wider than 16:9 always take the startup patch. | `run.sh:75-122` |
|
||||
| `BB_MENU_LANGUAGE` | en, ru, es, it, fr, de, pt_BR, zh_CN (first two letters compared; unset: LC_ALL/LC_MESSAGES/LANG/LANGUAGE) | **Launcher:** the launcher's own language. Language of the in-game menu until one is chosen in it (bbport.ini menu_language wins). | `gpu/shim/bbport_settings.cpp:165`, `launcher/bbport_launcher.py:651` |
|
||||
| `BB_MENU_LANGUAGE` | en, ru, es, it, fr, de, pt_BR, zh_CN (first two letters compared; unset: LC_ALL/LC_MESSAGES/LANG/LANGUAGE) | **Launcher:** the launcher's own language. Language of the in-game menu until one is chosen in it (bbport.ini menu_language wins). | `gpu/shim/bbport_settings.cpp:165`, `launcher/bbport_launcher.py:652` |
|
||||
| `BB_NVIDIA_EGL` | 0 = off (default on) | Packaged builds with the host NVIDIA driver: sets __EGL_VENDOR_LIBRARY_FILENAMES to the host's EGL vendor files, NVIDIA first (issue #107); 0 leaves it unchanged. | `launcher/bbport_vulkan.py:116-124` |
|
||||
| `BB_NVIDIA_LIB_DIR` | directory (default none) | Packaged builds: an extra directory searched for the host NVIDIA Vulkan driver library. | `launcher/bbport_vulkan.py:74-76` |
|
||||
| `BB_OUTPUT_RES` | WxH | **Internal** (run.sh exports it from bbport.ini output_res for non-1080p outputs without live resolution; unset again on restart). Output size the upscaler fills when the scene is rendered at a startup-patched size (BB_RENDER_RES); also passed to scripts/patches.py. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:199`, `run.sh:119`, `run.sh:190` |
|
||||
| `BB_PRESENT_MODE` | Mailbox, Fifo, FifoRelaxed, Immediate (Mailbox) | **Launcher:** Graphics > Display > Present mode. Vulkan present mode; a mode the driver lacks falls back to Fifo. Any other value logs an error and uses Mailbox. | `gpu/shim/core/emulator_settings.h:25`, `gpu/shadps4/video_core/renderer_vulkan/vk_swapchain.cpp:281`, `launcher/bbport_launcher.py:653` |
|
||||
| `BB_PRESENT_MODE` | Mailbox, Fifo, FifoRelaxed, Immediate (Mailbox) | **Launcher:** Graphics > Display > Present mode. Vulkan present mode; a mode the driver lacks falls back to Fifo. Any other value logs an error and uses Mailbox. | `gpu/shim/core/emulator_settings.h:25`, `gpu/shadps4/video_core/renderer_vulkan/vk_swapchain.cpp:281`, `launcher/bbport_launcher.py:654` |
|
||||
| `BB_PRESENT_THREAD` | 0 = off; else on (on) | Swapchain work runs on a separate swap thread next to the present thread; 0 runs it on the present thread. | `gpu/shadps4/core/libraries/videoout/driver.cpp:76-81` |
|
||||
| `BB_RENDER_RES` | WxH (unset = live resolution path; scene at the menu's preset) | Explicitly sets the game's render resolution with a patch at start (the older path). Fixes the scene size: presets and resolution then need a restart, and TAA at another size is refused. Above 1920x1080 run.sh raises BB_DMEM_MB to 9152. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:208`, `run.sh:71`, `run.sh:119` |
|
||||
| `BB_UI_RES` | WxH (unset: 1920x1080) | **Internal** (run.sh exports it from `patches.py --print-scaled` for outputs wider than 16:9 on the startup patch, unless bbport.ini aspect_patch=0; unset again on restart). The game's UI coordinate space: 1080 high and as wide as the output's aspect ratio (2580x1080 for 3440x1440), the HUD canvas width patches.py writes (ultrawide_writes). The UI composition recognizes UI draws by this size and scales them to the output; scripts/hud_layout.py lays out the menu movies for it. | `gpu/shadps4/video_core/renderer_vulkan/ui_composition.h:24`, `run.sh:119-122`, `run.sh:124` |
|
||||
| `LC_ALL / LC_MESSAGES / LANG / LANGUAGE` | locale | In-game menu language when BB_MENU_LANGUAGE is unset; the launcher also derives its UI and game language from the system locale. | `gpu/shim/bbport_settings.cpp:151-163`, `launcher/bbport_i18n.py:4984-5010` |
|
||||
| `LC_ALL / LC_MESSAGES / LANG / LANGUAGE` | locale | In-game menu language when BB_MENU_LANGUAGE is unset; the launcher also derives its UI and game language from the system locale. | `gpu/shim/bbport_settings.cpp:151-163`, `launcher/bbport_i18n.py:5090-5116` |
|
||||
| `VK_ADD_DRIVER_FILES` | ICD JSON list | Packaged builds: prepended to the bundled driver list. | `launcher/bbport_vulkan.py:72-73` |
|
||||
| `VK_DRIVER_FILES` | ICD JSON list (default: loader's) | Vulkan loader's driver list. run.sh --software sets Lavapipe; packaged builds set it to the host NVIDIA ICD plus bundled Mesa unless you set it (or VK_ICD_FILENAMES) yourself; dropped when it points into Steam's pressure-vessel. | `run.sh:4-13`, `launcher/bbport_vulkan.py:66-80`, `packaging/bbport-entry.c:37-49` |
|
||||
| `VK_ICD_FILENAMES` | ICD JSON list | Older name of VK_DRIVER_FILES; if set, packaged builds leave driver selection alone. | `launcher/bbport_vulkan.py:66-67` |
|
||||
@@ -162,8 +162,8 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_ANISO` | integer (16); 0 = the game's own samplers | Mipmapped linear samplers of graphics draws get this anisotropic filtering ratio instead of the PS4's (sharper floors, stairs and walls at grazing angles). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3595` |
|
||||
| `BB_AUTO_NOHIZ` | 0 = off (default 1) | On GCN 3–5 GPUs under RADV (Polaris, Vega and their APUs) run.sh sets RADV_DEBUG=nohiz to avoid black halos/missing geometry from depth compression; 0 disables that. Ignored if you set RADV_DEBUG yourself. | `run.sh:212` |
|
||||
| `BB_DLSS` | 0 = off; unset = on when available | NVIDIA DLSS through the optional bridge library (libbbport_dlss.so + NVIDIA's libnvidia-ngx-dlss). 0 disables it; without the libraries or on other GPUs nothing changes. | `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.cpp:176`, `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.h:8` |
|
||||
| `BB_FSR1` | 1/y/t or 0 (off) | shadPS4's FSR 1 pass in the presenter (spatial upscale of the final image); separate from the port's upscalers (bbport.ini upscaler). | `gpu/shim/core/emulator_settings.h:59`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:175` |
|
||||
| `BB_FSR411_DIR` | directory (unset = fsr4_411 next to run.sh) | **Internal** (run.sh sets it to <data>/fsr4_411 when that exists). Where the FSR 4.1.1 assets built by tools/fsr4cap/build_assets.sh are looked up. | `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.cpp:244`, `run.sh:34`, `launcher/bbport_launcher.py:462` |
|
||||
| `BB_FSR1` | 1/y/t or 0 (off) | shadPS4's FSR 1 pass in the presenter (spatial upscale of the final image); separate from the port's upscalers (bbport.ini upscaler). | `gpu/shim/core/emulator_settings.h:59`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:178` |
|
||||
| `BB_FSR411_DIR` | directory (unset = fsr4_411 next to run.sh) | **Internal** (run.sh sets it to <data>/fsr4_411 when that exists). Where the FSR 4.1.1 assets built by tools/fsr4cap/build_assets.sh are looked up. | `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.cpp:244`, `run.sh:34`, `launcher/bbport_launcher.py:463` |
|
||||
| `BB_FSR411_VARIANT` | int8 / fp8emu (unset = FP8 when the GPU supports it and its assets exist, else INT8) | Forces which FSR 4.1.1 model variant runs: int8 skips FP8; fp8emu uses the emulated-FP8 set (needs FP16 cooperative matrices). | `gpu/shadps4/video_core/renderer_vulkan/fsr411/fsr411.cpp:295` |
|
||||
| `BB_FSR4_DIR` | directory (fsr4_shaders) | Where the FSR 4 (v07) SPIR-V passes and weights from tools/fetch_fsr4_assets.sh are loaded from. | `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.cpp:26`, `gpu/shadps4/video_core/renderer_vulkan/vk_fsr4.h:7` |
|
||||
| `BB_FSR4_LINEAR` | 0 = off; unset/other = on | FSR 4 gets the game's tonemapped sRGB frame decoded to linear light and the output encoded again. 0 (or toggle bit 63): the encoded frame as before (A/B). | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1853` |
|
||||
@@ -176,8 +176,8 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_OBJECT_MOTION` | 0/1 (bbport.ini object_motion, off by default) | Per-object motion vectors for the temporal upscaler: less ghosting on characters, about 10% FPS. The variable overrides bbport.ini at start. | `gpu/shim/bbport_settings.cpp:202`, `gpu/shadps4/video_core/renderer_vulkan/vk_object_motion.cpp:58` |
|
||||
| `BB_OCCLUSION_QUERIES` | 1 = on (off; BB_PM4_SELFTEST=1 also turns it on) | Opt-in: the game's occlusion queries become real Vulkan occlusion queries. Off: counters are a fixed "everything visible" sequence (the 0.3 model), because with an upscaler jittered tests made light glows flash. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:253` |
|
||||
| `BB_ZPASS_IN_ORDER` | 0 = off; unset/other = on | Without BB_OCCLUSION_QUERIES: the stand-in occlusion counts are written in order with the draws, after the DMA that zeroes the query's block, as on the PS4. Written ahead of it, the game's queries never finished and it made new ones until it read a null handle (issue #23). 0: written at decode, as before. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:588` |
|
||||
| `BB_RCAS` | 1/y/t or 0 (1) | RCAS sharpening in shadPS4's presenter FSR 1 pass (only with BB_FSR1); not the in-game Sharpening (RCAS) setting. | `gpu/shim/core/emulator_settings.h:65`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:176` |
|
||||
| `BB_RCAS_ATTENUATION` | integer, thousandths (250 = 0.25) | RCAS attenuation of shadPS4's presenter FSR 1 pass. | `gpu/shim/core/emulator_settings.h:26`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:178` |
|
||||
| `BB_RCAS` | 1/y/t or 0 (1) | RCAS sharpening in shadPS4's presenter FSR 1 pass (only with BB_FSR1); not the in-game Sharpening (RCAS) setting. | `gpu/shim/core/emulator_settings.h:65`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:182` |
|
||||
| `BB_RCAS_ATTENUATION` | integer, thousandths (250 = 0.25) | RCAS attenuation of shadPS4's presenter FSR 1 pass. | `gpu/shim/core/emulator_settings.h:26`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:184` |
|
||||
| `BB_REACTIVE` | 0/1 (bbport.ini reactive, off by default) | Reactive mask for the upscaler (opt-in): on Bloodborne's thin mist it trades trails for jitter shimmer. Overrides bbport.ini at start. | `gpu/shim/bbport_settings.cpp:200`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:954` |
|
||||
| `BB_REACTIVE_MAX` | 0-1 (0.9; overrides bbport.ini reactive_max) | Upper bound of the TAA reactive mask (never fully reactive). | `gpu/shim/bbport_settings.cpp:201`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1071` |
|
||||
| `BB_REACTIVE_SCALE` | 0-16 (1; overrides bbport.ini reactive_scale) | Reactive mask = relative colour change times this scale. | `gpu/shim/bbport_settings.cpp:200`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:1071` |
|
||||
@@ -201,11 +201,11 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_COPY_THREADS` | 0-16 (a quarter of the available hardware threads, 1-4) | Number of threads that split large guest memory copies. | `gpu/shim/bbport_copy.cpp:30`, `src/runtime_memory.c:738` |
|
||||
| `BB_DIRECT_SEGMENT` | 0 = off; unset/other = on | With BB_ASYNC_SUBMIT, commands recorded on the GPU thread (e.g. upscaler passes) get their own command-buffer segment instead of waiting for the recording threads (~90 µs). 0: the wait. | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:647` |
|
||||
| `BB_DMEM_MB` | MiB, 5056–16384 (default 5056; run.sh sets 9152 for render sizes above 1080p) | Direct memory size reported to the game (retail PS4: 5056 MiB); the resolution patches above 1080p need more or the game exits at start. | `src/runtime_memory.c:22-30`, `run.sh:132`, `run.sh:132` |
|
||||
| `BB_DRAW_PIPE` | 1 = on, 0 = off (unset = on with 8+ CPU threads) | **Launcher:** Advanced > Performance > "Two-stage GPU pipeline" (Auto/On/Off). Two-stage GPU command pipeline (decode and record on separate threads): 20–30% faster; turn off if unstable. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:830`, `launcher/bbport_launcher.py:670` |
|
||||
| `BB_DRAW_PIPE` | 1 = on, 0 = off (unset = on with 8+ CPU threads) | **Launcher:** Advanced > Performance > "Two-stage GPU pipeline" (Auto/On/Off). Two-stage GPU command pipeline (decode and record on separate threads): 20–30% faster; turn off if unstable. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:830`, `launcher/bbport_launcher.py:671` |
|
||||
| `BB_FAULT_SKIP_TEXTURES` | 0 = off; unset/other = on | A write fault on a page no tracked image protects skips the texture cache lock (guest threads no longer wait 0.3–0.6 s behind texture uploads). 0: every fault takes the lock, as before. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:4339` |
|
||||
| `BB_FAULT_WINDOW` | KiB, clamped 4-1024 and rounded up to a power of two (256) | Size of the window unprotected around a guest write fault (write tracking), so neighbouring writes do not fault again. Window at or below one tracker page = feature off. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:691-698` |
|
||||
| `BB_FPS_LIMIT` | frames per second; 0 or unset = no limit | **Launcher:** Graphics > Frame rate > FPS limit (set only when > 0). Frames per second the present thread lets through. | `gpu/shim/core/emulator_settings.h:51`, `launcher/bbport_launcher.py:668` |
|
||||
| `BB_FRAMES_AHEAD` | integer ≥0 (1); 0 = no bound | The GPU command thread runs at most this many guest frames ahead of the GPU, so GPU-bound frames finish at an even rate instead of in bursts. | `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:475` |
|
||||
| `BB_FPS_LIMIT` | frames per second; 0 or unset = no limit | **Launcher:** Graphics > Frame rate > FPS limit (set only when > 0). Frames per second the present thread lets through. | `gpu/shim/core/emulator_settings.h:51`, `launcher/bbport_launcher.py:669` |
|
||||
| `BB_FRAMES_AHEAD` | integer ≥0 (1); 0 = no bound | The GPU command thread runs at most this many guest frames ahead of the GPU, so GPU-bound frames finish at an even rate instead of in bursts. | `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:479` |
|
||||
| `BB_GARLIC_VRAM` | 0 = off; any other value on (unset = on, except on integrated GPUs) | With the PC memory model, Garlic memory (what the GPU reads a lot: geometry, textures) gets a VRAM copy kept current like a default heap; 0 keeps it in place too. Ignored when the fixed arena is in use. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:474-479`, `2189-2192` |
|
||||
| `BB_GC_IDLE_SECONDS` | seconds, clamped 1-63 (20) | Texture collector: below the pressure mark an image is evicted once unused for this long (the textures of an area left behind). | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1396`, `1367-1371` |
|
||||
| `BB_GC_PRESSURE_IDLE_SECONDS` | seconds, clamped 0-63 (5) | Texture collector: under VRAM pressure an image is evicted once unused for this long (over the critical mark for 1 s first, then by submissions). | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1430-1439` |
|
||||
@@ -238,15 +238,15 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_LAYER_UNMAP_RESET` | 0 = off; else on (on) | Layer memory module: blocks the game unmaps whole lose what kept them in place (CPU writes, GPU writes in place, failed watches); memory mapped there again starts as new. 0: as before. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3308` |
|
||||
| `BB_LAYER_READ_TRAPS` | 1 = on (off) | What the GPU writes into a VRAM copy only is made no-access for the CPU until it is copied back, so the CPU never reads stale data. Off by default for now. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:1285` |
|
||||
| `BB_LAYER_WATCH` | 0 = off; else on (on; needs BB_LAYER_WRITE_TRAPS) | Layer memory module: blocks the CPU writes unannounced get VRAM copies too, write-trapped and uploaded again after the CPU's next write. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:410-424` |
|
||||
| `BB_PC_MODEL` | 1 = new \| 0 = 0.3 model (default: 1 on AMD GPUs, 0 elsewhere) | **Launcher:** Advanced → "Memory and translation model" (Auto leaves it to run.sh). Memory model: 1 = the GPU uses the game's memory in place and the command processor's work is translated (implies BB_GUEST_IN_PLACE=1); 0 = 0.3 model (VRAM copies, write tracking). A value in extra variables wins over "Auto". | `run.sh:212`, `run.sh:199`, `launcher/bbport_launcher.py:684` |
|
||||
| `BB_PC_MODEL` | 1 = new \| 0 = 0.3 model (default: 1 on AMD GPUs, 0 elsewhere) | **Launcher:** Advanced → "Memory and translation model" (Auto leaves it to run.sh). Memory model: 1 = the GPU uses the game's memory in place and the command processor's work is translated (implies BB_GUEST_IN_PLACE=1); 0 = 0.3 model (VRAM copies, write tracking). A value in extra variables wins over "Auto". | `run.sh:212`, `run.sh:199`, `launcher/bbport_launcher.py:685` |
|
||||
| `BB_PC_MODEL_ANY_GPU` | 1 or unset (off) | Lets the new memory model's sparse arena run on non-AMD GPUs that would otherwise fall back to the 0.3 model (NVIDIA: rebinding memory held the GPU for seconds). | `gpu/shim/bbport_guest_memory.cpp:513`, `gpu/shim/bbport_guest_memory.h:35` |
|
||||
| `BB_PIPELINE_CACHE` | 1/y/t or 0 (1) | The port's pipeline cache: shaders are compiled again at startup from the stored list (warm-up). 0 turns it off. | `gpu/shim/core/emulator_settings.h:64`, `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_serialization.cpp:330` |
|
||||
| `BB_PIPE_SIGNALS` | 0 = off; unset/other = on (with honest labels and the draw pipe) | Fences, GPU-idle/frame signals and their submissions are handed to the draw recording thread as ordered tasks instead of draining it first (~15 waits a frame). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:376`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.h:234` |
|
||||
| `BB_PIPE_SPIN_US` | integer µs ≥0 (200 with 12+ CPU threads, else 50) | How long the draw-pipe consumer spins waiting for work before sleeping. Lower values leave more CPU to the game on few-core machines (Steam Deck). | `gpu/shadps4/video_core/renderer_vulkan/vk_draw_pipe.h:209` |
|
||||
| `BB_PREP_WORKERS` | integer 0–16 (half the CPU threads, clamped 1–8) | Number of idle-priority worker threads that prepare draws speculatively ahead of the GPU thread (pipeline and resource selection). 0 = none. | `gpu/shadps4/video_core/renderer_vulkan/vk_draw_prep.cpp:54` |
|
||||
| `BB_PREUPLOAD` | 0 = off, 1 = rewritten memory already in VRAM only (no new VRAM), 2 = all of the game's GPU memory (~3 GB more VRAM); without run.sh unset = 0 | **Launcher:** Advanced > Performance > "Background pre-upload into VRAM" (run.sh exports 1 when unset). Background upload of the game's GPU memory into VRAM ahead of use: fewer hitches when areas load. Always off with BB_GUEST_IN_PLACE (the GPU reads the game's memory itself). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:386`, `run.sh:197`, `launcher/bbport_launcher.py:674` |
|
||||
| `BB_PREUPLOAD` | 0 = off, 1 = rewritten memory already in VRAM only (no new VRAM), 2 = all of the game's GPU memory (~3 GB more VRAM); without run.sh unset = 0 | **Launcher:** Advanced > Performance > "Background pre-upload into VRAM" (run.sh exports 1 when unset). Background upload of the game's GPU memory into VRAM ahead of use: fewer hitches when areas load. Always off with BB_GUEST_IN_PLACE (the GPU reads the game's memory itself). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:386`, `run.sh:197`, `launcher/bbport_launcher.py:675` |
|
||||
| `BB_PREUPLOAD_MB` | integer MiB per frame (16) | Most memory the background pre-upload (BB_PREUPLOAD) puts into VRAM per frame. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1625` |
|
||||
| `BB_READBACKS` | 0 = off, 1 = relaxed, 2 = precise (1 = relaxed) | **Launcher:** Advanced > Performance > "GPU data readbacks by the CPU" (Relaxed = unset, Off = 0, Precise = 2). shadPS4's GPU readback mode. Relaxed by default: without readbacks FaceGen reads stale GPU-written vertices (vertex explosions). 2 also read-protects GPU-written pages and disables BB_GPU_WRITE_TWINS. | `gpu/shim/core/emulator_settings.h:27-29`, `launcher/bbport_launcher.py:672`, `524-525` |
|
||||
| `BB_READBACKS` | 0 = off, 1 = relaxed, 2 = precise (1 = relaxed) | **Launcher:** Advanced > Performance > "GPU data readbacks by the CPU" (Relaxed = unset, Off = 0, Precise = 2). shadPS4's GPU readback mode. Relaxed by default: without readbacks FaceGen reads stale GPU-written vertices (vertex explosions). 2 also read-protects GPU-written pages and disables BB_GPU_WRITE_TWINS. | `gpu/shim/core/emulator_settings.h:27-29`, `launcher/bbport_launcher.py:673`, `524-525` |
|
||||
| `BB_READBACK_LINEAR` | 1/y/t or 0 (off) | shadPS4's readback of linear (untiled or narrow) images to guest memory. | `gpu/shim/core/emulator_settings.h:66`, `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:812` |
|
||||
| `BB_READBACK_QUEUE` | 0 = off; unset/other = on | Uses a separate compute/transfer queue to read back GPU data finished long ago, without waiting behind the queued frame. 0 turns it off. | `gpu/shadps4/video_core/renderer_vulkan/vk_instance.cpp:499` |
|
||||
| `BB_STAGING_KEEP_MB` | integer MiB (512) | How much of the staging upload ring is kept (and populated at startup) instead of being trimmed when idle; re-creating blocks during streaming bursts caused stutter. | `gpu/shadps4/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp:27` |
|
||||
@@ -261,7 +261,7 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_VMA_DEDICATED_MB` | MiB; 0 = never (4) | Images of this size or more get dedicated memory, returned to the driver when freed (shared VMA blocks left ~0.9 GB of holes after texture evictions). | `gpu/shadps4/video_core/texture_cache/image.cpp:109-115` |
|
||||
| `BB_VRAM_IDLE_SECONDS` | seconds (60) | Mirrored (VRAM-copied) blocks not bound for this long go back in place. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:1823`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:412` |
|
||||
| `BB_VRAM_PRESSURE_IDLE_SECONDS` | seconds (3) | Near the critical VRAM mark, blocks no binding used directly for this long (an area left behind) go back in place first, longest unused first. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:903-913` |
|
||||
| `BB_VRAM_PRESSURE_PERCENT` | percent, max 95; 0 = off (85) | How full VRAM is against the driver's budget: "tight" from this percent, "full" 5 points above (fewer VRAM copies then). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2045` |
|
||||
| `BB_VRAM_PRESSURE_PERCENT` | percent, max 95; 0 = off (85) | How full VRAM is against the driver's budget: "tight" from this percent, "full" 5 points above (fewer VRAM copies then). In the 0.3 memory model also "tight" 384 MiB short of the texture collector's critical mark and "full" at it. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2045` |
|
||||
| `BB_VRAM_PROMOTE_PERCENT` | percent (90) | Past this share of the driver's VRAM budget no new VRAM copies are made; candidates wait and are read in place meanwhile (issue #32: 8 GB cards). | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3950` |
|
||||
| `MESA_SHADER_CACHE_MAX_SIZE` | size (run.sh default 4G) | Mesa's shader cache size, larger than Mesa's default. | `run.sh:31` |
|
||||
| `__GL_SHADER_DISK_CACHE` | run.sh default 1 | NVIDIA's shader disk cache on (run.sh default; keeps compiled pipelines between sessions). | `run.sh:25-30` |
|
||||
@@ -274,19 +274,21 @@ behaviour when the variable is unset. In the tables:
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_GAMEPAD` | SDL GUID, part of the name, or the device path (`/dev/hidrawN`, `/dev/input/eventN`; the log's `gamepad connected` line shows it) (default: first controller) | **Launcher:** Controls → Gamepad → "Controller". Preferred controller, so wheels and other always-connected devices do not take over. The path tells two pads of the same model apart (local co-op). | `src/runtime_pad.c` (preferred_gamepad), `launcher/bbport_launcher.py:655` |
|
||||
| `BB_GAMEPAD` | SDL GUID, part of the name, or the device path (`/dev/hidrawN`, `/dev/input/eventN`; the log's `gamepad connected` line shows it) (default: first controller) | **Launcher:** Controls → Gamepad → "Controller". Preferred controller, so wheels and other always-connected devices do not take over. The path tells two pads of the same model apart (local co-op). | `src/runtime_pad.c` (preferred_gamepad), `launcher/bbport_launcher.py:656` |
|
||||
| `BB_GAMEPAD_STRICT` | 1 (default off) | Local co-op: only the BB_GAMEPAD controller, never the first one or the one in use instead; with no BB_GAMEPAD, no controller at all. | `src/runtime_pad.c` (gamepad_strict) |
|
||||
| `BB_KEYBOARD` | 0 (default on) | Local co-op: this game ignores the keyboard and the mouse, and never takes the mouse. | `src/runtime_pad.c` (keyboard_enabled) |
|
||||
| `BB_PAD_BACKGROUND` | 1 (default off) | Local co-op: the controller keeps driving the game while another window has the focus, and the game keeps its sound then (bbport.ini's mute_unfocused is ignored). **Launcher:** the Multiplayer → Local tab sets it, with BB_WINDOW_RECT, BB_GAMEPAD, BB_GAMEPAD_STRICT, BB_KEYBOARD, BB_AUDIO and BB_DATA_DIR for each player (`launcher/bbport_coop.py`). | `gpu/shim/window.cpp`, `gpu/shim/bbgpu.cpp` (bbgpu_audio_muted) |
|
||||
| `BB_MOUSE_DEADZONE` | 0–100 of 127 (default 16) | Minimum right-stick deflection used for mouse look, so slow mouse movement still turns the camera past the game's dead zone. | `src/runtime_pad.c:761` |
|
||||
| `BB_PAD_CENTER` | lx,ly,rx,ry (default: measured) | Overrides the measured neutral position of each stick axis. | `src/runtime_pad.c:27`, `src/runtime_pad.c:113-118` |
|
||||
| `BB_PAD_CENTER_CAL` | 0 = off (default on) | Disables measuring each axis' neutral and travel (for third-party pads whose axes are biased). | `src/runtime_pad.c:28`, `src/runtime_pad.c:110` |
|
||||
| `BB_PAD_DEADZONE` | 0–126 (default 5) | **Launcher:** Controls → "Stick dead zone (%)" (percent × 127 / 100; written only when not 4 %). Inner stick dead zone, as shadPS4's analog_deadzone. | `src/runtime_pad.c:113`, `launcher/bbport_launcher.py:658` |
|
||||
| `BB_PAD_DEADZONE_OUTER` | 1–127 (default 127) | Outer stick dead zone: travel up to this value maps to full deflection (helps pads that never reach full). | `src/runtime_pad.c:26`, `src/runtime_pad.c:115` |
|
||||
| `BB_PAD_LIGHTBAR` | RRGGBB hex colour, an optional leading # (unset: SDL's own, the player's colour) | **Launcher:** Controls → "Controller light bar colour" (set only when on). Every controller with a light bar (DualShock 4, DualSense) shows this colour, set when it is opened. | `src/runtime_pad.c:297`, `launcher/bbport_launcher.py:662`, `launcher/bbport_launcher.py:1475` |
|
||||
| `BB_PROMPTS` | playstation \| xbox \| switch \| keyboard (default playstation) | **Launcher:** Controls → Gamepad → "Button prompts" (Automatic follows the controller). Button prompts of the game's menus, made from the game's files at start. | `run.sh:136`, `scripts/menu_prompts.py:57`, `launcher/bbport_launcher.py:644` |
|
||||
| `BB_MOUSE_DEADZONE` | 0–100 of 127 (default 16) | Minimum right-stick deflection used for mouse look, so slow mouse movement still turns the camera past the game's dead zone. | `src/runtime_pad.c:883` |
|
||||
| `BB_JUMP_GAP_MS` / `BB_JUMP_TAP_MS` | ms, 0–1000 (70 / 70) | bbport.ini `keyboard_layout=0.6` (issue #33; **Launcher:** Controls → Keyboard and mouse → "Keyboard layout"): the jump from the keys, dodge while sprinting, is Circle let go for the gap, pressed for the tap, let go again, then held (the run goes on). | `src/runtime_pad.c` (keyboard_circle) |
|
||||
| `BB_MENU_POINTER` | 0 = off (default on) | The mouse as a pointer in the game's own menus (from bbport 0.6): the item under it gets the game's cursor, a left click confirms (or presses a slider's arrow), the right button goes back, the wheel moves up and down. While a game menu is open the window lets the mouse go and shows the cursor, and takes the mouse back after if it had it. Hooks the game's list update (checked byte for byte: another build leaves it off); BB_GUEST_HOOKS=0 turns it off too. | `gpu/games/bloodborne_menu_pointer.cpp:722`, `gpu/shim/bbport_guest_code_hooks.cpp`, `gpu/shim/window.cpp:313`, `src/runtime_pad.c:1199` |
|
||||
| `BB_PAD_CENTER` | lx,ly,rx,ry (default: measured) | Overrides the measured neutral position of each stick axis. | `src/runtime_pad.c:36`, `src/runtime_pad.c:122-127` |
|
||||
| `BB_PAD_CENTER_CAL` | 0 = off (default on) | Disables measuring each axis' neutral and travel (for third-party pads whose axes are biased). | `src/runtime_pad.c:37`, `src/runtime_pad.c:119` |
|
||||
| `BB_PAD_DEADZONE` | 0–126 (default 5) | **Launcher:** Controls → "Stick dead zone (%)" (percent × 127 / 100; written only when not 4 %). Inner stick dead zone, as shadPS4's analog_deadzone. | `src/runtime_pad.c:122`, `launcher/bbport_launcher.py:659` |
|
||||
| `BB_PAD_DEADZONE_OUTER` | 1–127 (default 127) | Outer stick dead zone: travel up to this value maps to full deflection (helps pads that never reach full). | `src/runtime_pad.c:35`, `src/runtime_pad.c:124` |
|
||||
| `BB_PAD_LIGHTBAR` | RRGGBB hex colour, an optional leading # (unset: SDL's own, the player's colour) | **Launcher:** Controls → "Controller light bar colour" (set only when on). Every controller with a light bar (DualShock 4, DualSense) shows this colour, set when it is opened. | `src/runtime_pad.c:306`, `launcher/bbport_launcher.py:663`, `launcher/bbport_launcher.py:1500` |
|
||||
| `BB_PROMPTS` | playstation \| xbox \| switch \| keyboard (default playstation) | **Launcher:** Controls → Gamepad → "Button prompts" (Automatic follows the controller). Button prompts of the game's menus, made from the game's files at start. | `run.sh:136`, `scripts/menu_prompts.py:66`, `launcher/bbport_launcher.py:645` |
|
||||
| `BB_STEAM_INPUT_X11` | 0 = keep Wayland (default on) | Launched from Steam with Steam Input on a Wayland desktop, the game window goes through Xwayland (`SDL_VIDEO_DRIVER=x11`): Steam sends its virtual controller's input only to a focused window it can see, and it sees only X11 windows (issue #12). Not when `SDL_VIDEO_DRIVER` is set. | `run.sh` |
|
||||
| `BB_STEAM_OVERLAY` | 1 = on (default 0) | **Launcher:** Setup → Game → "Steam overlay", under "Add to Steam" (saved as soon as it changes). Started from Steam, the game gets Steam's 64-bit `gameoverlayrenderer.so` in `LD_PRELOAD`, read from the nearest process above that still has it (the AppImage's sharun clears `LD_PRELOAD` in every program it runs), and nothing else Steam preloads. On a Wayland desktop the window goes through Xwayland (`SDL_VIDEO_DRIVER=x11`), since Steam draws its overlay only on X11 windows; not when `SDL_VIDEO_DRIVER` is set (issue #28). | `run.sh:370`, `launcher/bbport_launcher.py:700`, `launcher/bbport_launcher.py:1274` |
|
||||
| `BB_STEAM_OVERLAY` | 1 = on (default 0) | **Launcher:** Setup → Game → "Steam overlay", under "Add to Steam" (saved as soon as it changes). Started from Steam, the game gets Steam's 64-bit `gameoverlayrenderer.so` in `LD_PRELOAD`, read from the nearest process above that still has it (the AppImage's sharun clears `LD_PRELOAD` in every program it runs), and nothing else Steam preloads. On a Wayland desktop the window goes through Xwayland (`SDL_VIDEO_DRIVER=x11`), since Steam draws its overlay only on X11 windows; not when `SDL_VIDEO_DRIVER` is set (issue #28). | `run.sh:370`, `launcher/bbport_launcher.py:701`, `launcher/bbport_launcher.py:1299` |
|
||||
|
||||
## Audio
|
||||
|
||||
@@ -305,15 +307,15 @@ behaviour when the variable is unset. In the tables:
|
||||
|---|---|---|---|
|
||||
| `BB_FPS` | uncap \| 60 \| 90 \| 30 (default uncap) | **Launcher:** Graphics → Frame rate → "Mode". Frame-rate patch: uncap = delta-time patch with 480 Hz vblank and sprint fix; 60/90 = fixed-timestep patches (90 adds the sprint fix); 30 = unpatched. Also picks BB_VBLANK_HZ when that is unset. | `run.sh:77`, `run.sh:77`, `scripts/patches.py:17-21` |
|
||||
| `BB_GAME_MENU` | 0, or unset (on) | 0 leaves the game's System menu as it is: no port pages (Display, Game effects, Game patches) built into the game's own menu. | `gpu/shim/bbport_game_menu.cpp:596`, `gpu/shim/bbport_game_menu.h:5` |
|
||||
| `BB_GESTURES_NO_MOTION` | 0 \| 1 (default 0) | **Launcher:** Controls → Gamepad → "Gestures menu without motion-control icons". Dommo's stripped-down gestures menu (no motion-control icons), made from the game's files at start. | `run.sh:137`, `scripts/menu_prompts.py:15`, `launcher/bbport_launcher.py:645` |
|
||||
| `BB_GESTURES_NO_MOTION` | 0 \| 1 (default 0) | **Launcher:** Controls → Gamepad → "Gestures menu without motion-control icons". Dommo's stripped-down gestures menu (no motion-control icons), made from the game's files at start. | `run.sh:137`, `scripts/menu_prompts.py:15`, `launcher/bbport_launcher.py:646` |
|
||||
| `BB_GX_COMMAND_HEAP` | 0 = off (default 1) | Patch that enlarges the game's two per-frame GX command heaps to 1 MiB (crashes when a frame fills the 256 KiB heap); 0 keeps the game's size. | `scripts/patches.py:83-91` |
|
||||
| `BB_LANGUAGE` | PS4 system language number: 0 Japanese, 1 English, 2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian, ... (runtime default 1) | **Launcher:** Setup > Game language (Auto: from the desktop locale). The system language the runtime reports to the game; also picks the trophy names' language and the in-game menu's default language. | `src/runtime_services.c:70`, `gpu/shim/bbport_game_menu.cpp:104`, `launcher/bbport_launcher.py:648` |
|
||||
| `BB_MODS_ENABLED` | 0 \| 1 (default 1) | **Launcher:** Mods → Mods → "Load mods". Whether mods are merged into the game's files for this launch. | `run.sh:166`, `launcher/bbport_launcher.py:642` |
|
||||
| `BB_LANGUAGE` | PS4 system language number: 0 Japanese, 1 English, 2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian, ... (runtime default 1) | **Launcher:** Setup > Game language (Auto: from the desktop locale). The system language the runtime reports to the game; also picks the trophy names' language and the in-game menu's default language. | `src/runtime_services.c:70`, `gpu/shim/bbport_game_menu.cpp:104`, `launcher/bbport_launcher.py:649` |
|
||||
| `BB_MODS_ENABLED` | 0 \| 1 (default 1) | **Launcher:** Mods → Mods → "Load mods". Whether mods are merged into the game's files for this launch. | `run.sh:166`, `launcher/bbport_launcher.py:643` |
|
||||
| `BB_PATCHES` | patch names separated by ";" (default none) | Extra patch names (from patches/Bloodborne.xml) applied at start. | `run.sh:190`, `run.sh:190` |
|
||||
| `BB_REGION` | eu \| na \| empty (default: the dump's param.sfo) | **Launcher:** Setup → "Game region". The game's region for this launch; picks the regional text (e.g. Spanish from Spain or Latin American). | `run.sh:189`, `scripts/content_profile.py:3-5`, `launcher/bbport_launcher.py:649` |
|
||||
| `BB_REGION` | eu \| na \| empty (default: the dump's param.sfo) | **Launcher:** Setup → "Game region". The game's region for this launch; picks the regional text (e.g. Spanish from Spain or Latin American). | `run.sh:189`, `scripts/content_profile.py:3-5`, `launcher/bbport_launcher.py:650` |
|
||||
| `BB_SKIP_GAME_CHECK` | 1 = skip (default check) | Skips the check that the game files are the supported 1.09 dump (wrong eboot, missing update, broken extraction). | `scripts/game_check.py:11`, `scripts/game_check.py:72`, `scripts/prepare.py:288` |
|
||||
| `BB_SKIP_NETWORK_CHOICE` | 0 \| 1 (default 1) | **Launcher:** Multiplayer → Online → "Skip the "play online / offline" choice" (default on). Patch: the title screen goes straight to the main menu, online when online play is on (BB_ONLINE=1), as PLAY ONLINE on a PS4, offline otherwise; 0 shows the online/offline dialog. | `scripts/patches.py:73-80`, `launcher/bbport_launcher.py:663` |
|
||||
| `BB_TROPHY_KEY` | 32 hex characters (default none) | **Launcher:** Trophies → "Trophy key". The PS4 trophy key; decrypts trophy names and descriptions in the game's language (without it they are in English). | `run.sh:351`, `scripts/trophies.py:495`, `launcher/bbport_launcher.py:702` |
|
||||
| `BB_SKIP_NETWORK_CHOICE` | 0 \| 1 (default 1) | **Launcher:** Multiplayer → Online → "Skip the "play online / offline" choice" (default on). Patch: the title screen goes straight to the main menu, online when online play is on (BB_ONLINE=1), as PLAY ONLINE on a PS4, offline otherwise; 0 shows the online/offline dialog. | `scripts/patches.py:73-80`, `launcher/bbport_launcher.py:664` |
|
||||
| `BB_TROPHY_KEY` | 32 hex characters (default none) | **Launcher:** Trophies → "Trophy key". The PS4 trophy key; decrypts trophy names and descriptions in the game's language (without it they are in English). | `run.sh:351`, `scripts/trophies.py:495`, `launcher/bbport_launcher.py:703` |
|
||||
| `BB_USER_NAME` | text (Hunter) | The PS4 user name reported to the game and the online module; also the default answer to a text-input dialog when the on-screen keyboard is unavailable. | `src/runtime_services.c:55`, `gpu/bbnet/include/core/user_manager.h:38` |
|
||||
|
||||
## Saves & files
|
||||
@@ -321,16 +323,16 @@ behaviour when the variable is unset. In the tables:
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BBSC_SAVE_DIR` | folder path; default unset | Folder holding userdata0000.. on hosts that keep saves elsewhere (bbhost); BBSC counts its slots as the active save and copies it in its save backups. On the port BBSC looks in ./user (working folder) otherwise. | `bbsc/emulator/src/core/bloodborne_re.cpp:5931`, `bbsc/emulator/src/core/bloodborne_re.cpp:14468` |
|
||||
| `BB_CONFIG` | path (run.sh: <data dir>/bbport.ini; the GPU library alone: ./bbport.ini) | **Internal** (run.sh default $BB_DATA_DIR/bbport.ini). Path of bbport.ini, the settings file read by the GPU library (in-game menu settings), the controls (key./pad. lines) and the launcher. | `run.sh:21`, `gpu/shim/bbport_settings.cpp:21`, `src/runtime_pad.c:520` |
|
||||
| `BB_CONFIG` | path (run.sh: <data dir>/bbport.ini; the GPU library alone: ./bbport.ini) | **Internal** (run.sh default $BB_DATA_DIR/bbport.ini). Path of bbport.ini, the settings file read by the GPU library (in-game menu settings), the controls (key./pad. lines) and the launcher. | `run.sh:21`, `gpu/shim/bbport_settings.cpp:21`, `src/runtime_pad.c:574` |
|
||||
| `BB_DATA_DIR` | directory (default: the port directory; AppImage/Nix: $XDG_DATA_HOME/bbport) | **Internal.** Writable directory for generated files (out/), saves (user/), bbport.ini, mods, patches and logs. | `run.sh:16-18`, `launcher/bbport_launcher.py:56`, `packaging/appimage-sharun.sh:74` |
|
||||
| `BB_DLC_DIR` | directory (default none) | **Launcher:** Setup → "DLC folder". Folder with the dumped add-ons (one folder each, e.g. SPEXPANSIONDLC03); linked as <user>/addcont/<title id> so the runtime reports them installed. | `run.sh:178`, `launcher/bbport_launcher.py:639` |
|
||||
| `BB_GAME_DIR` | directory (default ../CUSA03173) | **Launcher:** Setup → "Game folder". The game's folder (eboot.bin, sce_module, …). | `run.sh:63-65`, `launcher/bbport_launcher.py:635`, `scripts/patches.py:494` |
|
||||
| `BB_DLC_DIR` | directory (default none) | **Launcher:** Setup → "DLC folder". Folder with the dumped add-ons (one folder each, e.g. SPEXPANSIONDLC03); linked as <user>/addcont/<title id> so the runtime reports them installed. | `run.sh:178`, `launcher/bbport_launcher.py:640` |
|
||||
| `BB_GAME_DIR` | directory (default ../CUSA03173) | **Launcher:** Setup → "Game folder". The game's folder (eboot.bin, sce_module, …). | `run.sh:63-65`, `launcher/bbport_launcher.py:636`, `scripts/patches.py:494` |
|
||||
| `BB_GPU_USER_DIR` | directory | **Internal** (run.sh: defaults to BB_USER_DIR, else <data>/user). Where the GPU library keeps its caches (pipeline cache etc.) and the player's DLSS folder (<dir>/dlss). Set by run.sh before the GPU library loads. | `gpu/shadps4/common/path_util.cpp:88`, `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.cpp:95`, `run.sh:24` |
|
||||
| `BB_MODS_CONFIG` | file (default <data>/mods.json) | **Launcher:** always set to <data>/mods.json. The mods list/configuration used by scripts/mods.py. | `run.sh:165`, `launcher/bbport_launcher.py:641` |
|
||||
| `BB_MODS_DIR` | directory (default <data>/mods) | **Launcher:** Mods → Mods → "Mods folder". Folder the mods are loaded from (also used by BB_MOD_TRACE). | `run.sh:165`, `launcher/bbport_launcher.py:640`, `src/runtime_file.c:390` |
|
||||
| `BB_PATCHES_CONFIG` | file (default <data>/patches.json) | **Launcher:** always set to <data>/patches.json. Configuration of the external patches. | `run.sh:192`, `launcher/bbport_launcher.py:647` |
|
||||
| `BB_PATCHES_DIR` | directory (default <data>/patches) | **Launcher:** Mods → Third-party patches → "Patches folder". Folder of external patch files. | `run.sh:192`, `launcher/bbport_launcher.py:646` |
|
||||
| `BB_USER_DIR` | folder path; default <data dir>/user (run.sh); BBSC falls back to ./user | **Launcher:** Setup > Saves > "Saves folder". User folder: saves, trophies, DLC links, host_overrides.json, bbsc/ logs. The launcher sets it only when a saves folder is chosen; also the default of BB_GPU_USER_DIR. | `launcher/bbport_launcher.py:637`, `run.sh:24`, `88`, `313`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:114` |
|
||||
| `BB_MODS_CONFIG` | file (default <data>/mods.json) | **Launcher:** always set to <data>/mods.json. The mods list/configuration used by scripts/mods.py. | `run.sh:165`, `launcher/bbport_launcher.py:642` |
|
||||
| `BB_MODS_DIR` | directory (default <data>/mods) | **Launcher:** Mods → Mods → "Mods folder". Folder the mods are loaded from (also used by BB_MOD_TRACE). | `run.sh:165`, `launcher/bbport_launcher.py:641`, `src/runtime_file.c:390` |
|
||||
| `BB_PATCHES_CONFIG` | file (default <data>/patches.json) | **Launcher:** always set to <data>/patches.json. Configuration of the external patches. | `run.sh:192`, `launcher/bbport_launcher.py:648` |
|
||||
| `BB_PATCHES_DIR` | directory (default <data>/patches) | **Launcher:** Mods → Third-party patches → "Patches folder". Folder of external patch files. | `run.sh:192`, `launcher/bbport_launcher.py:647` |
|
||||
| `BB_USER_DIR` | folder path; default <data dir>/user (run.sh); BBSC falls back to ./user | **Launcher:** Setup > Saves > "Saves folder". User folder: saves, trophies, DLC links, host_overrides.json, bbsc/ logs. The launcher sets it only when a saves folder is chosen; also the default of BB_GPU_USER_DIR. | `launcher/bbport_launcher.py:638`, `run.sh:24`, `88`, `313`, `bbsc/emulator/mod/linux/bbsc_mod_linux.cpp:114` |
|
||||
|
||||
## Hunter's Atlas (map)
|
||||
|
||||
@@ -430,7 +432,7 @@ behaviour when the variable is unset. In the tables:
|
||||
|
||||
| Variable | Values (default) | What it does | Where |
|
||||
|---|---|---|---|
|
||||
| `BB_DISCORD` | 0 = off (default 1) | **Launcher:** Setup → "Discord status" (default on). Discord Rich Presence helper (scripts/discord_presence.py) that shows the area and co-op partners; 0 turns it off. | `run.sh:359`, `launcher/bbport_launcher.py:699` |
|
||||
| `BB_DISCORD` | 0 = off (default 1) | **Launcher:** Setup → "Discord status" (default on). Discord Rich Presence helper (scripts/discord_presence.py) that shows the area and co-op partners; 0 turns it off. | `run.sh:359`, `launcher/bbport_launcher.py:700` |
|
||||
| `SHADPS4_BBSC_DISCORD` | "0" or "false" off, else on (mod.json default true) | **Launcher:** Multiplayer > Online > Seamless co-op (BBSC) > BBSC settings > "Discord status". BBSC's own Discord status (area, boss fights, number of hunters). Read only by bbsc_discord.cpp, which the Linux libbbsc does not compile (bbsc/CMakeLists.txt), so it has no effect in this port; bbport's presence is BB_DISCORD. | `bbsc/emulator/mod/bbsc_discord.cpp:31-35`, `bbsc/CMakeLists.txt:22-29`, `bbsc/emulator/mod/mod.json:39` |
|
||||
|
||||
## Diagnostics & logs
|
||||
@@ -454,8 +456,8 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_FILE_TRACE` | 1 = on (default off) | Logs every guest path the game opens, stats or checks (mods, missing files). | `src/runtime_file.c:127-129` |
|
||||
| `BB_FINAL_DUMP_TRIGGER` | file path (unset = off) | Creating this file saves the next frame as presented (after FSR and post-processing) as final_<w>x<h> in BB_DUMP_DIR (used by tools/frame.sh KIND=final). | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:164`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.h:41` |
|
||||
| `BB_FRAME_LOG` | file path (unset = off) | With BB_FRAME_STATS: one CSV line per flip (frame time, waits, stage times, allocations…), to find what long frames (stutter) have in common. BB_SAVE_LOG=1 points it at logs/<time>.frames.csv. | `gpu/shadps4/core/libraries/videoout/driver.cpp:526`, `run.sh:47` |
|
||||
| `BB_FRAME_STATS` | Flag(): first char 1/y/t = on (off) | **Launcher:** Advanced > Developer > "Frame statistics in the log" (run.sh also sets it with BB_SAVE_LOG=1). Prints flip rate and frame-time spread (plus many internal counters) every 5 s. Without it a short summary line is printed every 30 s. | `gpu/shadps4/core/libraries/videoout/driver.cpp:380`, `launcher/bbport_launcher.py:678` |
|
||||
| `BB_FREE_CHECK` | 1 = on (off) | **Launcher:** Advanced > Developer > "Crash diagnostics" (together with BB_WRITE_LOG and BB_PRODUCER_CHECK). Diagnostic for the guest heap corruption at guest offset 0x263b8e7: every write the GPU emulation makes to guest memory is checked against the free list of the game's label pool, and a watcher thread reports corrupted free lists. Slightly slower. | `gpu/shim/bbport_free_check.h:2`, `gpu/shim/bbport_free_check.cpp:488-494`, `launcher/bbport_launcher.py:692` |
|
||||
| `BB_FRAME_STATS` | Flag(): first char 1/y/t = on (off) | **Launcher:** Advanced > Developer > "Frame statistics in the log" (run.sh also sets it with BB_SAVE_LOG=1). Prints flip rate and frame-time spread (plus many internal counters) every 5 s. Without it a short summary line is printed every 30 s. | `gpu/shadps4/core/libraries/videoout/driver.cpp:380`, `launcher/bbport_launcher.py:679` |
|
||||
| `BB_FREE_CHECK` | 1 = on (off) | **Launcher:** Advanced > Developer > "Crash diagnostics" (together with BB_WRITE_LOG and BB_PRODUCER_CHECK). Diagnostic for the guest heap corruption at guest offset 0x263b8e7: every write the GPU emulation makes to guest memory is checked against the free list of the game's label pool, and a watcher thread reports corrupted free lists. Slightly slower. | `gpu/shim/bbport_free_check.h:2`, `gpu/shim/bbport_free_check.cpp:488-494`, `launcher/bbport_launcher.py:693` |
|
||||
| `BB_FSR4_PROFILE` | 1 = on (off) | FSR 4 / 4.1.1: GPU time per pass, printed every 300 frames. fsr4-bench sets it itself. | `gpu/shadps4/video_core/renderer_vulkan/fsr411/fsr411.cpp:280`, `gpu/tools/fsr4_bench.cpp:305` |
|
||||
| `BB_FSR4_STATS` | 1 or unset (off) | Prints the driver's per-pass statistics of the FSR 4 pipelines (registers, spills, instruction counts) when VK_KHR_pipeline_executable_properties is available; read by the patched FSR 4 provider, set by fsr4_bench --stats. | `gpu/patches/fsr-vulkan/0001-FSR-4-v07-provider-BB_FSR4_PROFILE-pass-timings-BB_F.patch:50`, `gpu/tools/fsr4_bench.cpp:307` |
|
||||
| `BB_GAME_MENU_TRACE` | 1 or unset (off) | How often the game asks for the port's menu texts (per 2 s). | `gpu/shim/bbport_game_menu.cpp:474` |
|
||||
@@ -466,7 +468,7 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_GNM_OBSERVE` | 1 or unset (off) | Command buffer methods run through an observing stub; submissions are checked against the packets known API calls wrote (statistics in the census report). Also enables the hooks without BB_GNM_CENSUS. | `gpu/shim/bbport_gnm_hooks.cpp:69`, `gpu/shim/bbport_gnm_hooks.cpp:514` |
|
||||
| `BB_GNM_TRACE` | hex offset[:count] (count 40) | With the census or observer: prints the arguments, the caller and the packets written by the first `count` calls of the libGnm function at that offset. | `gpu/shim/bbport_gnm_hooks.cpp:84`, `gpu/shim/bbport_gnm_hooks.cpp:524` |
|
||||
| `BB_GPU_LOG` | trace, debug, info, warning, error, critical (warning) | Log level of the GPU library (shadPS4 log); unknown values mean warning. | `gpu/shim/common/logging/log.cpp:11`, `gpu/shim/common/logging/log.h:2` |
|
||||
| `BB_GPU_PROFILE` | 1 = on (off) | **Launcher:** Advanced > Developer > "GPU profile in the log". GPU time per render pass, dispatch and upscaler run, printed every 5 s (GPU ms per frame by label). | `gpu/shadps4/video_core/renderer_vulkan/vk_gpu_profiler.cpp:13`, `gpu/shadps4/video_core/renderer_vulkan/vk_gpu_profiler.h:2`, `launcher/bbport_launcher.py:696` |
|
||||
| `BB_GPU_PROFILE` | 1 = on (off) | **Launcher:** Advanced > Developer > "GPU profile in the log". GPU time per render pass, dispatch and upscaler run, printed every 5 s (GPU ms per frame by label). | `gpu/shadps4/video_core/renderer_vulkan/vk_gpu_profiler.cpp:13`, `gpu/shadps4/video_core/renderer_vulkan/vk_gpu_profiler.h:2`, `launcher/bbport_launcher.py:697` |
|
||||
| `BB_HEAP_SITES` | 1 or unset (off) | Wraps the game's malloc replacement and counts live allocations per call site; prints the sites whose live bytes grew (what leaks when the guest heap runs out). Also patches the deferred object release to count failed registry checks. | `gpu/shim/bbport_heap_sites.cpp:2`, `gpu/shim/bbport_guest_hooks.cpp:45` |
|
||||
| `BB_HLE_STATS` | any value = on (off) | Logs how often the compute-shader substitutions (HLE) fire. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2385` |
|
||||
| `BB_IMAGE_COPY_LOG` | any value = on (off) | Logs (first 40) the compute image copies the port recognizes and replaces. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2950` |
|
||||
@@ -481,15 +483,15 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_OCCLUSION_TRACE` | integer N (0) | Logs the first N occlusion events with their frame and render pass. | `gpu/shadps4/video_core/renderer_vulkan/vk_occlusion.cpp:274` |
|
||||
| `BB_PASS_BREAK_TRACE` | 1 = on (off) | Every 5 s: who ended a render pass that began again with the same state (attachments stored and loaded again). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:267`, `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:789` |
|
||||
| `BB_PRESENT_DUMP_COUNT` | integer ≥1 (1) | How many consecutive frames one BB_PRESENT_DUMP_TRIGGER saves. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:2229` |
|
||||
| `BB_PRESENT_DUMP_TRIGGER` | file path (unset = off) | Creating this file saves the completed display buffer (with the game's UI) as present_<w>x<h> in BB_DUMP_DIR; used by tools/frame.sh. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:2227`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:407` |
|
||||
| `BB_PRODUCER_CHECK` | 1 = on (off) | **Launcher:** Advanced > Developer > "Crash diagnostics" (also set by run.sh with BB_AS_0_3=1). Checks that only one thread at a time records into the scheduler (costs an atomic exchange per recorded command). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:845`, `launcher/bbport_launcher.py:694`, `run.sh:239` |
|
||||
| `BB_PRESENT_DUMP_TRIGGER` | file path (unset = off) | Creating this file saves the completed display buffer (with the game's UI) as present_<w>x<h> in BB_DUMP_DIR; used by tools/frame.sh. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:2227`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:410` |
|
||||
| `BB_PRODUCER_CHECK` | 1 = on (off) | **Launcher:** Advanced > Developer > "Crash diagnostics" (also set by run.sh with BB_AS_0_3=1). Checks that only one thread at a time records into the scheduler (costs an atomic exchange per recorded command). | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:845`, `launcher/bbport_launcher.py:695`, `run.sh:239` |
|
||||
| `BB_READBACK_LOG` | file path (unset = off) | **Internal** (set by run.sh when BB_SAVE_LOG=1 to logs/<time>.readbacks.csv). CSV of every download of GPU-written buffer memory back to guest memory: who asked, where, how much, how long it took, how old the GPU's write was, whether the readback queue served it. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:138-160`, `run.sh:48` |
|
||||
| `BB_RECORDER_TRACE` | 1 = on (off) | Prints the most frequent direct command-buffer callers every 2000 calls. | `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.cpp:315`, `gpu/shadps4/video_core/renderer_vulkan/vk_scheduler.h:822` |
|
||||
| `BB_RESIDENCY_MIX` | 1 or 2 (off) | Prints bindings per kind (none, in place, mirror, mixed) and written, every 2 s; 2 also lists the in-place or mixed bindings with the most bytes (layer module). | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:106-110`, `162-164`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:459-465` |
|
||||
| `BB_RESIDENCY_REPORT` | trigger file path (unset = off) | When the file appears (checked once a second), reports the blocks bound in place in the last seconds and why they have no VRAM copy. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:3108` |
|
||||
| `BB_RES_STATS` | 1 = on (off) | Every 5 s: whether bound shaders read their resource descriptors from user-data registers or from tables in memory, per draw and dispatch. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:2317` |
|
||||
| `BB_SAMPLE_THREAD` | thread name (default off) | With frame stats: samples where that guest thread runs with SIGPROF and reports it. | `src/runtime_kernel.c:464`, `src/runtime_kernel.c:464` |
|
||||
| `BB_SAVE_LOG` | 1 = on (default off) | **Launcher:** Advanced → Developer → "Save the log and statistics to a file". Also writes this run's output to <data>/logs/<time>.log and turns on BB_FRAME_STATS with .frames.csv and .readbacks.csv next to it. | `run.sh:41-53`, `launcher/bbport_launcher.py:680` |
|
||||
| `BB_SAVE_LOG` | 1 = on (default off) | **Launcher:** Advanced → Developer → "Save the log and statistics to a file". Also writes this run's output to <data>/logs/<time>.log and turns on BB_FRAME_STATS with .frames.csv and .readbacks.csv next to it. | `run.sh:41-53`, `launcher/bbport_launcher.py:681` |
|
||||
| `BB_SAVE_TRACE` | 1 = on (default off) | Logs every operation on save files (/savedataN). | `src/runtime_file.c:224-225` |
|
||||
| `BB_SCENE_DEBUG` | file path (unset = off) | Once the file exists (it is consumed), prints for one frame why passes keep the native size and which scene targets are read at native size. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1644`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1644` |
|
||||
| `BB_SECTIONS` | 1 = on (off) | Time the draw recording thread spends per part of a draw (and ordered tasks by function), printed with the draw-pipe statistics. | `gpu/shim/bbport_sections.h:51`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1300` |
|
||||
@@ -505,7 +507,7 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_TRACE_SIZE` | bytes (C integer) (0 = off) | Bindings over GPU-written buffers of that size are counted by the path they take and reported every 5 s. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:318-320`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2638` |
|
||||
| `BB_UI_TRACE` | any value = on (off) | Logs the draws of frames that ended without the output-size UI (shown at the game's own size), and of one that had it. | `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.h:229`, `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp:393` |
|
||||
| `BB_VK_MARKERS` | 1/y/t or 0 (off) | Vulkan debug markers for guest and host passes (for RenderDoc and similar tools). | `gpu/shim/core/emulator_settings.h:71`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:65` |
|
||||
| `BB_VK_VALIDATION` | 1/y/t or 0 (off) | **Launcher:** Advanced > Developer > Vulkan validation layers. Enables the Vulkan validation layers (much slower). | `gpu/shim/core/emulator_settings.h:74`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:155`, `launcher/bbport_launcher.py:698` |
|
||||
| `BB_VK_VALIDATION` | 1/y/t or 0 (off) | **Launcher:** Advanced > Developer > Vulkan validation layers. Enables the Vulkan validation layers (much slower). | `gpu/shim/core/emulator_settings.h:74`, `gpu/shadps4/video_core/renderer_vulkan/vk_presenter.cpp:158`, `launcher/bbport_launcher.py:699` |
|
||||
| `BB_VK_VALIDATION_SYNC` | 1/y/t or 0 (off) | Synchronisation validation in the Vulkan validation layer (with BB_VK_VALIDATION). | `gpu/shim/core/emulator_settings.h:76`, `gpu/shadps4/video_core/renderer_vulkan/vk_platform.cpp:311` |
|
||||
| `BB_VRAM_ACCESS_TRAP` | set (any value) = on (off) | Blocks moved to VRAM are closed to the CPU (PROT_NONE); the first CPU access to each is recorded (code site, read or write) and the block opened again. Finds CPU readers of data the GPU writes in VRAM. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2994` |
|
||||
| `BB_VRAM_TRAP_RANGE` | addr,size (C integers) (unset = all blocks) | With BB_VRAM_ACCESS_TRAP: only blocks over that range, re-armed for every packet, every access counted by site. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:2977` |
|
||||
@@ -513,9 +515,9 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_WAIT_TRACE` | 1 or unset (off) | Attributes each wait for GPU ticks (Scheduler::Wait) to its thread and call stack; the frame stats print the stacks that waited longest. | `gpu/shim/bbport_wait_trace.h:2` |
|
||||
| `BB_WARN_META_TEXTURE` | any value = on (off) | Logs a warning when a shader reads texture metadata as a texture. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:3347` |
|
||||
| `BB_WATCH` | addr,size (unset = off) | What the shader translator reads on the CPU from that guest range (user data pointing into it, constants copied from it), printed every 2 s. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1076` |
|
||||
| `BB_WRITE_LOG` | 1, 2, or unset/0 (off) | **Launcher:** Advanced > Developer > Crash diagnostics (with BB_FREE_CHECK, BB_PRODUCER_CHECK). Ring of the writes the GPU emulation makes to guest memory (fences, WriteData, DMA, downloads), dumped when the guest faults. 1 logs at the write itself (changes timing), 2 where the command thread decodes the fence or WriteData. | `gpu/shim/bbport_write_log.h:2`, `gpu/shim/bbport_write_log.cpp:37`, `launcher/bbport_launcher.py:693` |
|
||||
| `BB_WRITE_LOG` | 1, 2, or unset/0 (off) | **Launcher:** Advanced > Developer > Crash diagnostics (with BB_FREE_CHECK, BB_PRODUCER_CHECK). Ring of the writes the GPU emulation makes to guest memory (fences, WriteData, DMA, downloads), dumped when the guest faults. 1 logs at the write itself (changes timing), 2 where the command thread decodes the fence or WriteData. | `gpu/shim/bbport_write_log.h:2`, `gpu/shim/bbport_write_log.cpp:37`, `launcher/bbport_launcher.py:694` |
|
||||
| `BB_WRITE_VERIFY` | 1 = on (off; only without write tracking) | Pages the GPU side watches are write-protected anyway to verify that every CPU write to them is announced. | `gpu/shadps4/video_core/buffer_cache/buffer.h:53`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:497-503` |
|
||||
| `MANGOHUD` | 1 = on (default off) | **Launcher:** Advanced → Developer → "MangoHud". MangoHud overlay. run.sh keeps it off if Steam's performance overlay (mangoapp) runs, and uses the system MangoHud instead of the bundled one when installed. | `run.sh:246`, `launcher/bbport_launcher.py:676` |
|
||||
| `MANGOHUD` | 1 = on (default off) | **Launcher:** Advanced → Developer → "MangoHud". MangoHud overlay. run.sh keeps it off if Steam's performance overlay (mangoapp) runs, and uses the system MangoHud instead of the bundled one when installed. | `run.sh:246`, `launcher/bbport_launcher.py:677` |
|
||||
| `SHADPS4_BBSC_TRACE_MAPS` | "0" off, any other non-empty value on; default off | Logs which maps are resident (and which one the player is in) whenever the set changes. | `bbsc/emulator/src/core/bloodborne_re.cpp:6305` |
|
||||
| `SHADPS4_BBSC_TRACE_WALLS` | "0" off, any other value on; default on (BBSC default-on switch, not in mod.json) | Logs which map objects get shown/made solid (fog walls). | `bbsc/emulator/src/core/bbsc_persistence.h:28`, `bbsc/emulator/src/core/bloodborne_re.cpp:8840` |
|
||||
| `SHADPS4_CAPTURE_BLOODBORNE_SUMMON` | non-empty and not "0" = on; default off | Online module writes capture files of summon-messenger HTTP requests (for collect-logs). Listed as default-on in BBSC but only the online module reads it. | `gpu/bbnet/shadps4/core/libraries/network/http.cpp:734`, `bbsc/emulator/src/core/bbsc_persistence.h:37` |
|
||||
@@ -528,7 +530,7 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BBSC_EMULATOR` | path | **No effect in this port.** Windows bbsc-launch only: another program to start instead of the emulator. Not built for the Linux port. | `bbsc/emulator/mod/bbsc_launch.cpp:263-268` |
|
||||
| `BBSC_URL_OVERRIDES` | 0 = off | **No effect in this port.** Windows bbsc.dll only (bbhost): turns off its own URL overrides. The Linux build leaves URL overrides to the online module and never reads it. | `bbsc/emulator/mod/bbsc_http.cpp:158`, `176-181` |
|
||||
| `BB_ASYNC_FENCES` | 1 = on; anything else off (off) | Experiment: an end-of-pipe fence is written once the guest memory copies on the copy threads are done instead of waiting for them. Measured slower (79 vs 83 FPS), so off. Ignored when honest labels are in use. | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:476` |
|
||||
| `BB_AS_0_3` | 1 = on (default off) | **Launcher:** Advanced → Developer → "Synchronisation as in 0.3". Reverts what changed since the 0.3 release, for comparisons: run.sh forces BB_GUEST_IN_PLACE=0, BB_HOST_COPY_WAITS=all, BB_PRODUCER_CHECK=1 (overriding any value you set for those). | `run.sh:238`, `launcher/bbport_launcher.py:688` |
|
||||
| `BB_AS_0_3` | 1 = on (default off) | **Launcher:** Advanced → Developer → "Synchronisation as in 0.3". Reverts what changed since the 0.3 release, for comparisons: run.sh forces BB_GUEST_IN_PLACE=0, BB_HOST_COPY_WAITS=all, BB_PRODUCER_CHECK=1 (overriding any value you set for those). | `run.sh:238`, `launcher/bbport_launcher.py:689` |
|
||||
| `BB_CAMERA_INVERSE` | own (unset = the game's stored inverse) | own: camera motion vectors use the inverse of the view matrix computed by the port instead of the one the game stores (A/B test; they agree to float precision). | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:281` |
|
||||
| `BB_CAMERA_Y` | up / down (unset = from the G-buffer viewport) | Forces the screen y sign used by camera motion vectors (for tests); normally the frame's G-buffer viewport decides. | `gpu/shadps4/video_core/renderer_vulkan/vk_camera_motion.cpp:314` |
|
||||
| `BB_CHUNK_TEXTURES` | 0 = off; else on (on) | A/B switch for the PC memory model (BB_GUEST_IN_PLACE): texture data is read by the GPU straight from the guest memory chunk it is in (no CPU copy, no VRAM buffer copy) when the range is contiguous. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:419`, `gpu/shadps4/video_core/buffer_cache/buffer_cache.h:209` |
|
||||
@@ -542,19 +544,19 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_DUMP_GOT` | guest GOT offset (hex/dec) | With BB_SAMPLE_THREAD sampling: prints which host function a guest GOT slot calls. | `src/runtime_kernel.c:480` |
|
||||
| `BB_FENCE_DELAY_TEST` | 1 = on (off) | Experiment: holds every 200th fence back 80 ms, to test whether late labels corrupt the guest heap (the sequence numbers come from BB_FREE_CHECK=1). | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:407-414` |
|
||||
| `BB_FIXED_ARENA` | 1/0 forces on/off (unset = on when guest memory is host-imported, i.e. NVIDIA) | NVIDIA workaround for the sparse arena: the arena keeps the first binding of every block, so game data is read in place and never moved to VRAM. Not used with the layer memory module (BB_LAYER_MEMORY); only with BB_GUEST_IN_PLACE. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:435-457` |
|
||||
| `BB_FSR411_OUT` | directory (default fsr4_411 in the tree; launcher sets it) | **Internal.** Where tools/fsr4cap/build_assets.sh writes the FSR 4.1.1 asset set. | `tools/fsr4cap/build_assets.sh:37`, `launcher/bbport_launcher.py:479` |
|
||||
| `BB_FSR4CAP_CLEAN` | 1 = remove work folder (launcher sets it when packaged) | **Internal.** build_assets.sh removes its work folder after building FSR 4.1.1 assets. | `tools/fsr4cap/build_assets.sh:142`, `launcher/bbport_launcher.py:475` |
|
||||
| `BB_FSR411_OUT` | directory (default fsr4_411 in the tree; launcher sets it) | **Internal.** Where tools/fsr4cap/build_assets.sh writes the FSR 4.1.1 asset set. | `tools/fsr4cap/build_assets.sh:37`, `launcher/bbport_launcher.py:480` |
|
||||
| `BB_FSR4CAP_CLEAN` | 1 = remove work folder (launcher sets it when packaged) | **Internal.** build_assets.sh removes its work folder after building FSR 4.1.1 assets. | `tools/fsr4cap/build_assets.sh:142`, `launcher/bbport_launcher.py:476` |
|
||||
| `BB_FSR4CAP_FP8` | auto \| 1 \| 0 (default auto) | FSR 4.1.1 capture: also record the DLL's FP8 matrix variant outside RDNA4 via GE-Proton's FP16 emulation (1), or never (0); auto records it only where the DLL uses it (RDNA4). | `tools/fsr4cap/capture_all.sh:10-15`, `tools/fsr4cap/capture_all.sh:176` |
|
||||
| `BB_FSR4CAP_RUNNER` | steam \| direct (default automatic) | FSR 4.1.1 capture: steam = use Steam's runtime even where umu-run would be used; direct = run Proton directly from the start. | `tools/fsr4cap/capture_all.sh:37`, `tools/fsr4cap/proton.sh:118-135` |
|
||||
| `BB_FSR4CAP_SPOOF` | 0 = off (default 1) | FSR 4.1.1 capture: when AMD's DLL offers only FSR 3/2 on this GPU, retry with the GPU reported as an RX 7800 XT. | `tools/fsr4cap/capture_all.sh:16-19`, `tools/fsr4cap/capture_all.sh:131` |
|
||||
| `BB_FSR4CAP_WORK` | directory (default out/fsr4cap; launcher sets <data>/fsr4cap when packaged) | **Internal.** Work folder of build_assets.sh (must be under $HOME for Steam's runtime). | `tools/fsr4cap/build_assets.sh:19-20`, `tools/fsr4cap/build_assets.sh:36`, `launcher/bbport_launcher.py:474` |
|
||||
| `BB_FSR4CAP_WORK` | directory (default out/fsr4cap; launcher sets <data>/fsr4cap when packaged) | **Internal.** Work folder of build_assets.sh (must be under $HOME for Steam's runtime). | `tools/fsr4cap/build_assets.sh:19-20`, `tools/fsr4cap/build_assets.sh:36`, `launcher/bbport_launcher.py:475` |
|
||||
| `BB_FSR4_TIERS` | space-separated list (default "1080 2160") | Which FSR 4 asset tiers tools/fetch_fsr4_assets.sh downloads. | `tools/fetch_fsr4_assets.sh:6`, `tools/fetch_fsr4_assets.sh:18` |
|
||||
| `BB_GAME_PROFILE` | none, or unset (by TITLE_ID) | `none` runs the game as an unknown one, without the Bloodborne profile (the code notes Bloodborne then crashes). | `gpu/games/game_profile.cpp:22`, `gpu/games/bloodborne.cpp:17` |
|
||||
| `BB_GAME_PROFILE` | none, or unset (by TITLE_ID) | `none` runs the game as an unknown one, without the Bloodborne profile (the code notes Bloodborne then crashes). | `gpu/games/game_profile.cpp:22`, `gpu/games/bloodborne.cpp:18` |
|
||||
| `BB_GC_BUDGET_MB` | MiB (unset = 0 = driver budget, integrated GPUs only) | Texture collector: applies the integrated-GPU rule (marks at 70/85/95 % of the budget) with this fixed budget on any GPU. For tests on a desktop. | `gpu/shadps4/video_core/texture_cache/texture_cache.cpp:1375-1386` |
|
||||
| `BB_GPU_DATA_CHECK` | 0 = off; else on (on) | With the PC memory model, GPU writes not finished yet and data the GPU wrote into a VRAM copy count as GPU-modified for CPU access checks (issue #122). 0 = not counted. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:1366-1374` |
|
||||
| `BB_GPU_LABELS_PORTABLE` | anything but 0 = on (off) | Writes GPU labels with portable Vulkan buffer updates even when VK_AMD_buffer_marker is available (testing the non-AMD path). | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:326` |
|
||||
| `BB_GPU_WRITES_IN_PLACE` | 1 = on (off) | With the PC memory model and no write tracking, GPU-written buffers are kept in the game's memory (in place) instead of VRAM; costs ~9 ms a frame over PCIe on an RX 7800 XT. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:1118-1127` |
|
||||
| `BB_GUEST_HOOKS` | 0 = off; else on (on) | Hooks in the game's own code from its profile (gpu/games): the memory hooks with the PC memory model, the fixes of the game's own bugs (BB_JOB_SLOT_FIX, BB_KERNEL_CLAMP) with either model. 0 = none, as for a game the port knows nothing about. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:572-583`, `gpu/shim/bbgpu.cpp:384-393`, `gpu/games/bloodborne.cpp:17` |
|
||||
| `BB_GUEST_HOOKS` | 0 = off; else on (on) | Hooks in the game's own code from its profile (gpu/games): the memory hooks with the PC memory model, the fixes of the game's own bugs (BB_JOB_SLOT_FIX, BB_KERNEL_CLAMP) and the menu pointer (BB_MENU_POINTER) with either model. 0 = none, as for a game the port knows nothing about. | `gpu/shadps4/video_core/buffer_cache/buffer_cache.cpp:572-583`, `gpu/shim/bbgpu.cpp:384-393`, `gpu/games/bloodborne.cpp:43-48` |
|
||||
| `BB_JOB_SLOT_FIX` | 0 = off (default on) | A render job that fails gives its pool slot back (the game kept it: Laurence's cinematic above 30 FPS emptied the pool and hung); also when a runner is destroyed holding one. | `gpu/shim/bbport_guest_hooks.cpp:443-470` |
|
||||
| `BB_KERNEL_CLAMP` | 0 = off (default on) | Raises the blur kernel's base sample count to 3 at the entry of 0xbbc3e0 (game data now and then gives 2: exception 0xa0020007). | `gpu/shim/bbport_guest_hooks.cpp:472-491` |
|
||||
| `BB_GUEST_HOOK_TRAPS` | 1 or unset (off) | Keeps the guest hooks as int3 traps instead of native detours. | `gpu/shim/bbport_guest_hooks.cpp:72`, `gpu/shim/bbport_guest_hooks.cpp:529` |
|
||||
@@ -577,15 +579,17 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_LAYER_WRITE_TRAPS` | 0 = off; else on (on) | Layer memory module: blocks with a VRAM copy are write-protected so a write the game does not announce sends the block back in place before the next binding (else stale copies give exploding vertices and garbage textures). BB_LAYER_WATCH needs it. | `gpu/shadps4/video_core/buffer_cache/buffer_cache_layer.cpp:1386-1406` |
|
||||
| `BB_MEM_SEMAPHORE_ORDER` | 0 = off; else on (on, only with honest labels) | A MEM_SEMAPHORE signal is given once the GPU has finished the work before it, in stream order. 0 = signalled when decoded (as before). | `gpu/shadps4/video_core/amdgpu/liverpool.cpp:497-507` |
|
||||
| `BB_MENU_KEYS_FILE` | file path (unset: off) | Scripted menu tests: tokens in the file (toggle up down left right enter back l1 r1) are pressed one per frame when the file appears; it is then removed. | `gpu/shim/bbport_overlay.cpp:1087` |
|
||||
| `BB_MENU_POINTER_FILE` | file path (unset: off) | Scripted menu pointer tests: `x y [click\|right\|up\|down]` in the game's 1920x1080 stage pixels, applied when the file changes, as the mouse would; each item the pointer moves to is logged. | `gpu/games/bloodborne_menu_pointer.cpp:645` |
|
||||
| `BB_MENU_POINTER_LOG` | 1 (default off) | Logs each signal the menu pointer reads to tell a game menu is open (title, menu object, list focus) with the time it changed. | `gpu/games/bloodborne_menu_pointer.cpp:566` |
|
||||
| `BB_NULL_GPU` | 1/y/t or 0 (off) | shadPS4's null GPU: the command processor is not bound to the rasterizer, so nothing is drawn. | `gpu/shim/core/emulator_settings.h:61`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:75` |
|
||||
| `BB_OBJECT_MOTION_ALL` | 1 = on (off) | Keeps the old broad object-motion path (every candidate draw, without the bone-palette gate) for visual A/B tests. | `gpu/shadps4/video_core/renderer_vulkan/vk_pipeline_cache.cpp:921`, `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1705` |
|
||||
| `BB_STABLE_MOTION_HASH` | 1 = on (off) | Object motion: a draw's vertex streams are identified without their buffers' addresses, so streams the game rewrites into a ring buffer at a new address each frame keep one identity (steadier motion vectors); two objects laid out alike can share one. Off by default until compared; from Mrsuss60's Windows port. | `gpu/shadps4/video_core/renderer_vulkan/vk_draw_prep.h:44` |
|
||||
| `BB_OCCLUSION_LABEL_ORDER` | 0 = off; unset/other = on | With occlusion queries: results recorded before a label are written before it, as on the PS4. 0: labels may go ahead (statistics only). | `gpu/shadps4/video_core/renderer_vulkan/vk_occlusion.cpp:228` |
|
||||
| `BB_PACKAGE_FSR411` | 1 = include (build-time, default off) | Nix build: package the FSR 4.1.1 models (extracted from AMD's DLLs; never in a public package). | `packaging/default.nix:12-14` |
|
||||
| `BB_PAD_FILE` | file (default off) | Scripted input for automated runs: button names held while listed, lx=/ly=/rx=/ry= stick overrides, re-read when the file changes; token "replay" starts BB_PAD_REPLAY. | `src/runtime_pad.c:899` |
|
||||
| `BB_PAD_FILE` | file (default off) | Scripted input for automated runs: button names held while listed, lx=/ly=/rx=/ry= stick overrides, re-read when the file changes; token "replay" starts BB_PAD_REPLAY. | `src/runtime_pad.c:1021` |
|
||||
| `BB_CAM_TRACE` | file (default off) | Writes "ms yaw pitch" (radians) to this file whenever the camera turns, to measure how the game turns it for a stick input (with BB_PAD_FILE). Only with mouse_camera=stick in bbport.ini (the default is direct). | `gpu/shim/bloodborne_cam.cpp` (TraceTick) |
|
||||
| `BB_PAD_RECORD` | file (default off) | F9 starts/stops recording the pad state (gamepad or keyboard) to this file. | `src/runtime_pad.c:944` |
|
||||
| `BB_PAD_REPLAY` | file (default off) | Plays a BB_PAD_RECORD recording back, started by the token "replay" in BB_PAD_FILE. | `src/runtime_pad.c:976`, `src/runtime_pad.c:976` |
|
||||
| `BB_PAD_RECORD` | file (default off) | F9 starts/stops recording the pad state (gamepad or keyboard) to this file. | `src/runtime_pad.c:1066` |
|
||||
| `BB_PAD_REPLAY` | file (default off) | Plays a BB_PAD_RECORD recording back, started by the token "replay" in BB_PAD_FILE. | `src/runtime_pad.c:1098`, `src/runtime_pad.c:1098` |
|
||||
| `BB_PIPE_VERIFY` | integer N (0 = off) | Every Nth draw-pipe packet also carries the full register state, for verification. Exact check: see gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:1177. | `gpu/shadps4/video_core/renderer_vulkan/vk_rasterizer.cpp:822` |
|
||||
| `BB_PM4_SELFTEST` | 1 = on (off) | Self-test of command-processor packets Bloodborne never sends (occlusion queries, COPY_DATA, COND_EXEC, predication, MEM_SEMAPHORE, timestamps): injected ~40 s into the game and the results printed. Turns on BB_OCCLUSION_QUERIES. | `gpu/shadps4/video_core/amdgpu/pm4_selftest.cpp:221`, `gpu/shadps4/video_core/amdgpu/pm4_selftest.h:2` |
|
||||
| `BB_PM4_SELFTEST_STEPS` | bit mask (C integer, 0x allowed) (all steps) | PM4 self-test (BB_PM4_SELFTEST): which steps run (bit n = step n); others skipped. | `gpu/shadps4/video_core/amdgpu/pm4_selftest.cpp:250-254` |
|
||||
@@ -622,19 +626,19 @@ behaviour when the variable is unset. In the tables:
|
||||
| `BB_SAVEDATA_TITLE` | title id | **Set by the runtime**, not by you: the save folder's title id, for BBSC's save path (`BBSC_SAVE_DIR`). | `src/runtime_savedata.c:106` |
|
||||
| `BB_AUTO_RENDER_RES` | 1 or unset | **Internal** (run.sh sets it with BB_RENDER_RES/BB_OUTPUT_RES). Marks BB_RENDER_RES/BB_OUTPUT_RES as chosen by run.sh from bbport.ini output_res (startup patch); run.sh drops them (and BB_UI_RES) on an in-game restart so they are recomputed, and the menu shows the matching hint. | `run.sh:68-70`, `run.sh:119`, `gpu/shim/bbport_overlay.cpp:442` |
|
||||
| `BB_BBSC_LIB` | path to libbbsc.so; default out/bbsc/libbbsc.so or BB_BBSC_PACKAGED_LIB | **Internal.** Library bb-probe loads into the game for seamless co-op. run.sh unsets it unless BB_BBSC=1 and BB_ONLINE=1, then resolves it (an explicit value is kept as an override). | `src/probe.c:340`, `run.sh:341`, `287-295` |
|
||||
| `BB_BBSC_PACKAGED_LIB` | path (set by the AppImage entry) | **Internal.** Location of the packaged libbbsc.so; run.sh falls back to it when BB_BBSC_LIB is unset, the launcher uses it to know BBSC is available. | `packaging/appimage-sharun.sh:73`, `run.sh:341`, `launcher/bbport_launcher.py:572` |
|
||||
| `BB_BRIDGE_CLI` | path (default out/bbbridge/bbbridge-cli; AppImage sets its copy) | **Internal.** The bbbridge-cli binary used for The Hunter's Dream. | `run.sh:313`, `launcher/bbport_launcher.py:579`, `packaging/appimage-sharun.sh:71` |
|
||||
| `BB_BBSC_PACKAGED_LIB` | path (set by the AppImage entry) | **Internal.** Location of the packaged libbbsc.so; run.sh falls back to it when BB_BBSC_LIB is unset, the launcher uses it to know BBSC is available. | `packaging/appimage-sharun.sh:73`, `run.sh:341`, `launcher/bbport_launcher.py:573` |
|
||||
| `BB_BRIDGE_CLI` | path (default out/bbbridge/bbbridge-cli; AppImage sets its copy) | **Internal.** The bbbridge-cli binary used for The Hunter's Dream. | `run.sh:313`, `launcher/bbport_launcher.py:580`, `packaging/appimage-sharun.sh:71` |
|
||||
| `BB_BUNDLED_VK_DRIVER_FILES` | colon-separated ICD list (set by the Nix wrapper) | **Internal.** The package's own Mesa ICDs; bbport_vulkan.py puts them in VK_DRIVER_FILES (after the host NVIDIA ICD if one is found). | `launcher/bbport_vulkan.py:71`, `packaging/default.nix:215` |
|
||||
| `BB_DLSS_DIR` | directory (unset = next to bb-probe) | **Internal.** Where the DLSS bridge (libbbport_dlss.so) and NVIDIA's libnvidia-ngx-dlss are looked up. The AppImage entry sets it to its own dlss folder. | `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.cpp:61`, `packaging/appimage-sharun.sh:69`, `launcher/bbport_launcher.py:447` |
|
||||
| `BB_DLSS_DIR` | directory (unset = next to bb-probe) | **Internal.** Where the DLSS bridge (libbbport_dlss.so) and NVIDIA's libnvidia-ngx-dlss are looked up. The AppImage entry sets it to its own dlss folder. | `gpu/shadps4/video_core/renderer_vulkan/vk_dlss.cpp:61`, `packaging/appimage-sharun.sh:69`, `launcher/bbport_launcher.py:448` |
|
||||
| `BB_FSR4CAP_CHECKED` | 1 (set by the script itself) | **Internal.** build_assets.sh marks that the DLL check already ran (re-exec guard). | `tools/fsr4cap/build_assets.sh:49-51` |
|
||||
| `BB_FSR4CAP_ON_HOST` | 1 (set by the script itself) | **Internal.** capture_all.sh re-runs itself on the host through systemd-run; this marks the re-run. | `tools/fsr4cap/capture_all.sh:47`, `tools/fsr4cap/capture_all.sh:62` |
|
||||
| `BB_FSR4CAP_SHELL` | 1 (set by the script itself) | **Internal.** build_assets.sh re-exec guard inside nix-shell (missing build tools). | `tools/fsr4cap/build_assets.sh:65-68` |
|
||||
| `BB_FSR4CAP_TOOLS` | directory (launcher sets it when bundled) | **Internal.** Folder holding prebuilt dxil-spirv and fsr4cap.exe, so build_assets.sh needs no MinGW toolchain. | `tools/fsr4cap/build_assets.sh:13`, `tools/fsr4cap/build_assets.sh:38`, `launcher/bbport_launcher.py:478` |
|
||||
| `BB_FSR4CAP_TOOLS` | directory (launcher sets it when bundled) | **Internal.** Folder holding prebuilt dxil-spirv and fsr4cap.exe, so build_assets.sh needs no MinGW toolchain. | `tools/fsr4cap/build_assets.sh:13`, `tools/fsr4cap/build_assets.sh:38`, `launcher/bbport_launcher.py:479` |
|
||||
| `BB_FSR4CAP_UMU_SHELL` | 1 (set by the script itself) | **Internal.** capture_all.sh re-exec guard inside a nix-shell with umu-launcher (NixOS). | `tools/fsr4cap/capture_all.sh:40-44` |
|
||||
| `BB_HOST_<NAME>` | set by the packaged entry | **Internal.** Copies of host variables the packaged entry dropped (LD_PRELOAD, LD_LIBRARY_PATH, GTK_*, pressure-vessel VK_* …), kept only for the log. | `packaging/bbport-entry.c:19-28`, `packaging/appimage-sharun.sh:73-79` |
|
||||
| `BB_IN_NIX_SHELL` | 1 (set by run.sh) | **Internal.** run.sh re-exec guard when it re-runs itself inside nix-shell. | `run.sh:37-39` |
|
||||
| `BB_NET_MODULE` | path (AppImage sets its copy; default gpu/libbbnet.so beside bb-probe) | **Internal.** The online module (libbbnet.so) the runtime dlopens. | `src/runtime_net.c:43-50`, `packaging/appimage-sharun.sh:70`, `launcher/bbport_launcher.py:595` |
|
||||
| `BB_PKGEXTRACT` | path (AppImage sets its copy) | **Internal.** pkgextract binary for "Install from PKG". | `scripts/pkg_install.py:50-51`, `launcher/bbport_launcher.py:586`, `packaging/appimage-sharun.sh:72` |
|
||||
| `BB_NET_MODULE` | path (AppImage sets its copy; default gpu/libbbnet.so beside bb-probe) | **Internal.** The online module (libbbnet.so) the runtime dlopens. | `src/runtime_net.c:43-50`, `packaging/appimage-sharun.sh:70`, `launcher/bbport_launcher.py:596` |
|
||||
| `BB_PKGEXTRACT` | path (AppImage sets its copy) | **Internal.** pkgextract binary for "Install from PKG". | `scripts/pkg_install.py:50-51`, `launcher/bbport_launcher.py:587`, `packaging/appimage-sharun.sh:72` |
|
||||
| `BB_PREBUILT` | 1 (AppImage/Nix wrapper) | **Internal.** Packaged build: binaries are installed next to run.sh, nothing is built and no nix-shell is used; the launcher stores data in BB_DATA_DIR. | `run.sh:14`, `run.sh:284`, `launcher/bbport_launcher.py:55`, `packaging/appimage-sharun.sh:66` |
|
||||
| `BB_PRESENCE_DIR` | directory (run.sh: out/presence, new each start) | **Internal** (run.sh, when Discord is on). Where the game writes the current area (`area`) and co-op partners (`coop`) for the Discord Rich Presence helper. | `run.sh:360`, `src/runtime_file.c:317`, `gpu/bbnet/shadps4/core/libraries/np/np_signaling/np_signaling_state.cpp:248` |
|
||||
| `BB_TROPHY_DIR` | directory (run.sh sets it) | **Internal.** Trophy list and icons prepared by scripts/trophies.py; trophies are recorded there and shown in the overlay. | `run.sh:354`, `src/runtime_services.c:335` |
|
||||
|
||||
+17
@@ -49,6 +49,23 @@ void bbgpu_mouse_look_enable(int enabled);
|
||||
/* mouse_camera=direct in bbport.ini: the mouse turns the camera itself (gpu/shim/bloodborne_cam.h)
|
||||
* at this sensitivity (1: 900 counts per radian) instead of moving the right stick. */
|
||||
void bbgpu_mouse_camera_configure(int direct, float sensitivity);
|
||||
/* A module's look at the window's events first (an SDL_Event and the SDL_Window, on the window
|
||||
* thread): nonzero takes the event (the port and the game do not see it). Up to 4. */
|
||||
typedef int (*BbGpuEventFilter)(const void *sdl_event, void *sdl_window);
|
||||
void bbgpu_add_event_filter(BbGpuEventFilter filter);
|
||||
/* A module's menu pointer: nonzero while the game shows a menu the mouse points into. The window
|
||||
* then lets the mouse go and shows the cursor, and takes the mouse back after (as it had it).
|
||||
* Up to 4; pointer_wanted: any of them says so. */
|
||||
typedef int (*BbGpuPointerQuery)(void);
|
||||
void bbgpu_add_pointer_query(BbGpuPointerQuery query);
|
||||
int bbgpu_pointer_wanted(void);
|
||||
/* A module's changes to the pad state the game reads (runtime_pad.c, after the host's input and
|
||||
* the scripted one): the buttons (PS4 bits); the sticks (lx ly rx ry) as they are. Up to 4. */
|
||||
typedef void (*BbGpuPadFilter)(uint32_t *buttons, const uint8_t *sticks);
|
||||
void bbgpu_add_pad_filter(BbGpuPadFilter filter);
|
||||
void bbgpu_filter_pad(uint32_t *buttons, const uint8_t *sticks);
|
||||
/* The shown frame's width / height (the display buffer's; the window letterboxes it). */
|
||||
float bbgpu_frame_aspect(void);
|
||||
/* Patches the loaded image before the game runs (image still writable): libGnm entry hooks. */
|
||||
void bbgpu_patch_image(unsigned char *image, uint64_t size);
|
||||
/* Number of symbols registered by the vendored libraries (diagnostics). */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// the same one.
|
||||
#include "game_profile.h"
|
||||
#include "bbport_guest_hooks.h"
|
||||
#include "bloodborne_menu_pointer.h"
|
||||
|
||||
namespace Game {
|
||||
namespace {
|
||||
@@ -30,13 +31,20 @@ void InstallHooks(const CoreServices& services) {
|
||||
// The resource loaders' copies, the GPU memory range allocator and the heap (bbport_guest_hooks).
|
||||
BbGuestHooks::Install(services.fresh_range);
|
||||
}
|
||||
|
||||
void InstallFixes() {
|
||||
// The game's own bugs (bbport_guest_hooks), with either memory model.
|
||||
BbGuestHooks::InstallGameFixes();
|
||||
// The mouse as a pointer in the game's menus (bloodborne_menu_pointer), with either one too.
|
||||
BbMenuPointer::Install();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern const Profile BloodborneProfile = {
|
||||
.name = "Bloodborne",
|
||||
.serials = Serials,
|
||||
.install_hooks = InstallHooks,
|
||||
.install_fixes = BbGuestHooks::InstallGameFixes,
|
||||
.install_fixes = InstallFixes,
|
||||
.dynamic_writers = DynamicWriters,
|
||||
// The game's buffer-copy compute shader (not the export-stage copy shader).
|
||||
.buffer_copy_shader = 0xfefebf9f,
|
||||
|
||||
@@ -0,0 +1,743 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Bloodborne (1.09): the mouse as a pointer in the game's Scaleform menus.
|
||||
//
|
||||
// From bbport 0.6 (gpu/games/bloodborne_menu_pointer.cpp). Here Hunter's Atlas's map and tile,
|
||||
// while they use the mouse, keep it as the port's settings menu does (BbOverlay::ReleasesMouse).
|
||||
//
|
||||
// Reverse engineering (addresses are offsets in the 1.09 executable, image at 0x800000000; the
|
||||
// facts first published in a fork of bloodborne_pc, checked against the executable here):
|
||||
// - 0x1acff30 updates one list component each frame it is shown (rdi the component, rsi its
|
||||
// input: the first byte is nonzero while the list has the input focus). It is reached through
|
||||
// the list class's vtable and five direct calls; one hook at its entry sees them all.
|
||||
// - The component: +0x30 its listener (vtable +0x10: cursor moved, with a pointer to the index),
|
||||
// +0x70 item count, +0x74 cursor, +0x78 cursor override (>= 0 wins), +0x7c items per line,
|
||||
// +0x80 lines, +0x84 scroll within a line, +0x24c line scroll, +0x258 the item provider
|
||||
// (vtable +0x18 makes the Scaleform value of item (cell | line << 32)), +0x2b0 wraps around,
|
||||
// +0xc8/+0xd0 the type and data of its root value.
|
||||
// - 0x1acf6b0 sets the cursor (component, index; 1 when it scrolled), 0x1ad0dd0 scrolls by
|
||||
// (lines, cells), 0x1acf0e0 hides every item's cursor.
|
||||
// - Scaleform values are 0x80 bytes: +0x30 the value, +0x40 the object interface (vtable +0x18
|
||||
// releases), +0x48 the type (& 0x40: managed; & 0x8f == 10: a display object), +0x50 its data
|
||||
// (+0x88: the native display object). 0x1adb220 finds a child by path ("Cursor" at 0x4934044,
|
||||
// "Widgets/Slider/LeftArrow", ...); a value's own vtable +8 shows or hides it.
|
||||
// - Display objects: +0x38 parent, +0x6b flags (0x40 visible), vtable +0x1f0 local bounds
|
||||
// (twips, x1 y1 x2 y2); 0x2040 their world matrix (2x4 floats, twips). The stage is 1920x1080
|
||||
// pixels, 20 twips each.
|
||||
// - 0x5580e38 the menu flow (+0xb11 zero: the title menu; +0xa88: a menu is open; +0xa78: input
|
||||
// blocked), 0x5562878 another input block (+0x2e4), 0x5128948 the region (5: Japan, circle
|
||||
// confirms), 0x557fc60 the menu sound request (+8 its label: 0x49344e4 the cursor's move).
|
||||
#include "bloodborne_menu_pointer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <csetjmp>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "../bbgpu.h"
|
||||
#include "bbport_guest_code_hooks.h"
|
||||
#include "bbport_overlay.h"
|
||||
#include "bbport_toggles.h"
|
||||
#include "common/types.h"
|
||||
|
||||
namespace BbMenuPointer {
|
||||
namespace {
|
||||
|
||||
using Addr = std::uintptr_t;
|
||||
|
||||
constexpr Addr ImageBase = 0x800000000ull;
|
||||
constexpr Addr CodeEnd = 0x50d96dc;
|
||||
|
||||
constexpr Addr ListUpdate = 0x1acff30;
|
||||
constexpr std::uint8_t ListUpdateEntry[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57}; // push rbp...
|
||||
constexpr Addr SetCursorFn = 0x1acf6b0, ScrollByFn = 0x1ad0dd0, HideCursorsFn = 0x1acf0e0;
|
||||
constexpr Addr FindChildFn = 0x1adb220, WorldMatrixFn = 0x2040;
|
||||
constexpr Addr CursorName = 0x4934044;
|
||||
constexpr Addr MenuFlow = 0x5580e38, InputBlock = 0x5562878, Region = 0x5128948;
|
||||
constexpr Addr MenuSound = 0x557fc60, CursorMoveSound = 0x49344e4;
|
||||
|
||||
enum : std::uint32_t {
|
||||
Up = 0x10, Right = 0x20, Down = 0x40, Left = 0x80, Circle = 0x2000, Cross = 0x4000,
|
||||
};
|
||||
|
||||
/// List component fields.
|
||||
enum : std::size_t {
|
||||
Listener = 0x30, Count = 0x70, Cursor = 0x74, CursorOverride = 0x78, PerLine = 0x7c,
|
||||
Lines = 0x80, LineScroll = 0x84, RootType = 0xc8, RootData = 0xd0, Scroll = 0x24c,
|
||||
Provider = 0x258, Wraps = 0x2b0,
|
||||
};
|
||||
|
||||
Addr At(Addr offset) {
|
||||
return ImageBase + offset;
|
||||
}
|
||||
|
||||
/// Copies guest memory; false where nothing is mapped (the runtime's fault recovery).
|
||||
bool Read(Addr address, void* out, std::size_t size) {
|
||||
if (address < 0x10000) {
|
||||
return false;
|
||||
}
|
||||
sigjmp_buf recover;
|
||||
if (sigsetjmp(recover, 0)) {
|
||||
runtime_fault_recover = nullptr;
|
||||
return false;
|
||||
}
|
||||
runtime_fault_recover = &recover;
|
||||
std::atomic_signal_fence(std::memory_order_seq_cst);
|
||||
std::memcpy(out, reinterpret_cast<const void*>(address), size);
|
||||
std::atomic_signal_fence(std::memory_order_seq_cst);
|
||||
runtime_fault_recover = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T Get(Addr address, T fallback = {}) {
|
||||
T value = fallback;
|
||||
return Read(address, &value, sizeof(value)) ? value : fallback;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T Field(const std::uint8_t* object, std::size_t offset) {
|
||||
T value{};
|
||||
std::memcpy(&value, object + offset, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
/// A function of the game (its usual prologue), from a vtable or a field.
|
||||
bool IsGameFunction(Addr address) {
|
||||
if (address < At(0) || address >= At(CodeEnd)) {
|
||||
return false;
|
||||
}
|
||||
std::uint8_t prologue[4] = {};
|
||||
return Read(address, prologue, sizeof(prologue)) && prologue[0] == 0x55 &&
|
||||
prologue[1] == 0x48 && prologue[2] == 0x89 && prologue[3] == 0xe5;
|
||||
}
|
||||
|
||||
Addr VirtualFunction(Addr object, std::size_t slot_offset) {
|
||||
const Addr vtable = Get<Addr>(object);
|
||||
const Addr function = vtable ? Get<Addr>(vtable + slot_offset) : 0;
|
||||
return IsGameFunction(function) ? function : 0;
|
||||
}
|
||||
|
||||
std::int64_t NowMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
/// A Scaleform value the game fills in (released when it goes out of scope).
|
||||
class Value {
|
||||
public:
|
||||
Value() = default;
|
||||
Value(const Value&) = delete;
|
||||
Value& operator=(const Value&) = delete;
|
||||
~Value() {
|
||||
if (!(Field<std::uint32_t>(bytes, 0x48) & 0x40)) {
|
||||
return;
|
||||
}
|
||||
const Addr object_interface = Field<Addr>(bytes, 0x40);
|
||||
if (const Addr release = object_interface ? VirtualFunction(object_interface, 0x18) : 0) {
|
||||
using Fn = void(PS4_SYSV_ABI*)(void*, void*, Addr);
|
||||
reinterpret_cast<Fn>(release)(reinterpret_cast<void*>(object_interface), bytes + 0x30,
|
||||
Field<Addr>(bytes, 0x50));
|
||||
}
|
||||
}
|
||||
void* Raw() {
|
||||
return bytes;
|
||||
}
|
||||
/// The native display object, or 0 (not a display object).
|
||||
Addr DisplayObject() const {
|
||||
if ((Field<std::uint32_t>(bytes, 0x48) & 0x8f) != 10) {
|
||||
return 0;
|
||||
}
|
||||
const Addr data = Field<Addr>(bytes, 0x50);
|
||||
return data ? Get<Addr>(data + 0x88) : 0;
|
||||
}
|
||||
/// Its child at `path` ("Cursor", "Widgets/Slider/LeftArrow").
|
||||
void Child(const char* path, Value& out) {
|
||||
using Fn = void(PS4_SYSV_ABI*)(void*, const void*, const char*, int);
|
||||
reinterpret_cast<Fn>(At(FindChildFn))(out.Raw(), bytes, path, 0);
|
||||
}
|
||||
void SetVisible(bool visible) {
|
||||
const Addr vtable = Field<Addr>(bytes, 0);
|
||||
const Addr set = vtable ? Get<Addr>(vtable + 8) : 0;
|
||||
if (DisplayObject() && IsGameFunction(set)) {
|
||||
using Fn = void(PS4_SYSV_ABI*)(void*, int);
|
||||
reinterpret_cast<Fn>(set)(bytes, visible ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
alignas(16) std::uint8_t bytes[0x80]{};
|
||||
};
|
||||
|
||||
struct Rect {
|
||||
float x1, y1, x2, y2;
|
||||
float CenterX() const {
|
||||
return (x1 + x2) * 0.5f;
|
||||
}
|
||||
};
|
||||
|
||||
/// Shown: it and every parent visible.
|
||||
bool Shown(Addr object) {
|
||||
for (int depth = 0; object && depth < 32; ++depth) {
|
||||
if (!(Get<std::uint8_t>(object + 0x6b) & 0x40)) {
|
||||
return false;
|
||||
}
|
||||
object = Get<Addr>(object + 0x38);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Whether stage point (x, y) is in the display object (its bounds grown by `grow` pixels);
|
||||
/// `stage` gets its bounds on the stage.
|
||||
bool Contains(Addr object, float x, float y, Rect* stage, float grow = 0.0f) {
|
||||
const Addr get_bounds = VirtualFunction(object, 0x1f0);
|
||||
if (!get_bounds) {
|
||||
return false;
|
||||
}
|
||||
alignas(16) const float identity[8] = {1, 0, 0, 0, 0, 1, 0, 0};
|
||||
alignas(16) float local[4] = {};
|
||||
using BoundsFn = void(PS4_SYSV_ABI*)(float*, void*, const float*);
|
||||
reinterpret_cast<BoundsFn>(get_bounds)(local, reinterpret_cast<void*>(object), identity);
|
||||
if (!(local[2] > local[0]) || !(local[3] > local[1])) {
|
||||
return false;
|
||||
}
|
||||
alignas(16) float m[8] = {1, 0, 0, 0, 0, 1, 0, 0};
|
||||
using MatrixFn = void(PS4_SYSV_ABI*)(void*, float*);
|
||||
reinterpret_cast<MatrixFn>(At(WorldMatrixFn))(reinterpret_cast<void*>(object), m);
|
||||
constexpr float Twips = 20.0f;
|
||||
if (stage) {
|
||||
*stage = {1e9f, 1e9f, -1e9f, -1e9f};
|
||||
for (const float lx : {local[0], local[2]}) {
|
||||
for (const float ly : {local[1], local[3]}) {
|
||||
const float sx = (m[0] * lx + m[1] * ly + m[3]) / Twips;
|
||||
const float sy = (m[4] * lx + m[5] * ly + m[7]) / Twips;
|
||||
*stage = {std::min(stage->x1, sx), std::min(stage->y1, sy),
|
||||
std::max(stage->x2, sx), std::max(stage->y2, sy)};
|
||||
}
|
||||
}
|
||||
}
|
||||
// The point in the object's own space (the inverse of the matrix's 2x2 part).
|
||||
const float det = m[0] * m[5] - m[1] * m[4];
|
||||
if (std::fabs(det) < 1e-12f) {
|
||||
return false;
|
||||
}
|
||||
const float px = x * Twips - m[3], py = y * Twips - m[7];
|
||||
const float lx = (m[5] * px - m[1] * py) / det;
|
||||
const float ly = (m[0] * py - m[4] * px) / det;
|
||||
const float g = grow * Twips / std::sqrt(std::fabs(det));
|
||||
return lx >= local[0] - g && lx <= local[2] + g && ly >= local[1] - g && ly <= local[3] + g;
|
||||
}
|
||||
|
||||
/// A list component of the game.
|
||||
class List {
|
||||
public:
|
||||
explicit List(std::uint8_t* component) : c{component} {}
|
||||
|
||||
int CursorIndex() const {
|
||||
const int forced = Field<std::int32_t>(c, CursorOverride);
|
||||
return forced >= 0 ? forced : Field<std::int32_t>(c, Cursor);
|
||||
}
|
||||
/// A one-line spinner (one item shown of several: left and right change it).
|
||||
bool IsSpinner() const {
|
||||
return Field<std::int32_t>(c, Lines) == 1 && Field<std::int32_t>(c, PerLine) == 1 &&
|
||||
Field<std::int32_t>(c, Count) > 1;
|
||||
}
|
||||
/// Its root display object (spinners).
|
||||
Addr Root() const {
|
||||
if ((Field<std::uint32_t>(c, RootType) & 0x8f) != 10) {
|
||||
return 0;
|
||||
}
|
||||
const Addr data = Field<Addr>(c, RootData);
|
||||
return data ? Get<Addr>(data + 0x88) : 0;
|
||||
}
|
||||
|
||||
/// The shown item under stage point (x, y): its index, or -1; `slot` gets its cell and line.
|
||||
int ItemAt(float x, float y, std::uint64_t& slot) {
|
||||
const int count = Field<std::int32_t>(c, Count);
|
||||
const int per_line = Field<std::int32_t>(c, PerLine);
|
||||
const int lines = Field<std::int32_t>(c, Lines);
|
||||
if (count <= 0 || per_line <= 0 || lines <= 0 || per_line > 64 || lines > 64 ||
|
||||
!MakerReady()) {
|
||||
return -1;
|
||||
}
|
||||
const int stride = lines != 1 ? per_line : std::max(per_line, count);
|
||||
const int line_scroll = Field<std::int32_t>(c, Scroll);
|
||||
const int cell_scroll = Field<std::int32_t>(c, LineScroll);
|
||||
const bool wraps = Field<std::uint8_t>(c, Wraps) != 0;
|
||||
int best = -1;
|
||||
float best_distance = 0.0f;
|
||||
for (int line = 0; line < lines; ++line) {
|
||||
for (int cell = 0; cell < per_line; ++cell) {
|
||||
int index = (line_scroll + line) * stride + cell_scroll + cell;
|
||||
if (wraps) {
|
||||
index %= count;
|
||||
}
|
||||
if (index < 0 || index >= count) {
|
||||
continue;
|
||||
}
|
||||
const std::uint64_t at = std::uint32_t(cell) | (std::uint64_t(std::uint32_t(line)) << 32);
|
||||
Value item;
|
||||
MakeItem(at, item);
|
||||
const Addr object = item.DisplayObject();
|
||||
if (!object || !Shown(object)) {
|
||||
continue;
|
||||
}
|
||||
Rect bounds{};
|
||||
if (!ItemContains(item, object, x, y, bounds)) {
|
||||
continue;
|
||||
}
|
||||
const float dx = bounds.CenterX() - x, dy = (bounds.y1 + bounds.y2) * 0.5f - y;
|
||||
if (best < 0 || dx * dx + dy * dy < best_distance) {
|
||||
best = index;
|
||||
best_distance = dx * dx + dy * dy;
|
||||
slot = at;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// The button a click at (x, y) on item `slot` presses: a slider's arrow or a combo box's
|
||||
/// side, else confirm.
|
||||
std::uint32_t ClickButton(std::uint64_t slot, float x, float y, std::uint32_t confirm) {
|
||||
if (!MakerReady()) {
|
||||
return confirm;
|
||||
}
|
||||
Value item;
|
||||
MakeItem(slot, item);
|
||||
if (!item.DisplayObject()) {
|
||||
return confirm;
|
||||
}
|
||||
const auto on = [&](const char* path, float grow, Rect* bounds) {
|
||||
Value part;
|
||||
item.Child(path, part);
|
||||
const Addr object = part.DisplayObject();
|
||||
return object && Shown(object) && Contains(object, x, y, bounds, grow);
|
||||
};
|
||||
Rect bounds{};
|
||||
if (on("Widgets/Slider/LeftArrow", 14.0f, &bounds)) {
|
||||
return Left;
|
||||
}
|
||||
if (on("Widgets/Slider/RightArrow", 14.0f, &bounds)) {
|
||||
return Right;
|
||||
}
|
||||
if (on("Widgets/ComboBox", 0.0f, &bounds)) {
|
||||
return x < bounds.CenterX() ? Left : Right;
|
||||
}
|
||||
return confirm;
|
||||
}
|
||||
|
||||
/// Moves the game's cursor to `index` (item `slot`) as its own input would: the scroll, the
|
||||
/// cursor shown, the listener told, the sound. False: it did not move there.
|
||||
bool MoveTo(int index, std::uint64_t slot) {
|
||||
const int before = CursorIndex();
|
||||
if (index == before) {
|
||||
return true;
|
||||
}
|
||||
const int line_scroll = Field<std::int32_t>(c, LineScroll);
|
||||
const int scroll = Field<std::int32_t>(c, Scroll);
|
||||
using SetCursor = std::int32_t(PS4_SYSV_ABI*)(std::uint8_t*, int);
|
||||
const int scrolled = reinterpret_cast<SetCursor>(At(SetCursorFn))(c, index);
|
||||
const int d_line = line_scroll - Field<std::int32_t>(c, LineScroll);
|
||||
const int d_scroll = scroll - Field<std::int32_t>(c, Scroll);
|
||||
if (scrolled == 1 && (d_line || d_scroll)) {
|
||||
// The list scrolled to it: back where the pointer is, with the cursor on that item.
|
||||
using ScrollBy = void(PS4_SYSV_ABI*)(std::uint8_t*, int, int);
|
||||
using HideCursors = void(PS4_SYSV_ABI*)(std::uint8_t*);
|
||||
reinterpret_cast<ScrollBy>(At(ScrollByFn))(c, d_line, d_scroll);
|
||||
reinterpret_cast<HideCursors>(At(HideCursorsFn))(c);
|
||||
if (MakerReady()) {
|
||||
Value item, cursor;
|
||||
MakeItem(slot, item);
|
||||
item.Child(reinterpret_cast<const char*>(At(CursorName)), cursor);
|
||||
cursor.SetVisible(true);
|
||||
}
|
||||
}
|
||||
const int now = CursorIndex();
|
||||
if (now != before) {
|
||||
CursorMoved(now);
|
||||
}
|
||||
return now == index;
|
||||
}
|
||||
|
||||
private:
|
||||
bool MakerReady() {
|
||||
provider = Field<Addr>(c, Provider);
|
||||
make_item = provider ? VirtualFunction(provider, 0x18) : 0;
|
||||
return make_item != 0;
|
||||
}
|
||||
void MakeItem(std::uint64_t slot, Value& out) {
|
||||
using Fn = void(PS4_SYSV_ABI*)(void*, void*, const std::uint64_t*);
|
||||
reinterpret_cast<Fn>(make_item)(out.Raw(), reinterpret_cast<void*>(provider), &slot);
|
||||
}
|
||||
/// The item's own cursor area if it has one, else the item; its combo box and slider too.
|
||||
bool ItemContains(Value& item, Addr object, float x, float y, Rect& bounds) {
|
||||
Value cursor;
|
||||
item.Child(reinterpret_cast<const char*>(At(CursorName)), cursor);
|
||||
const Addr area = cursor.DisplayObject();
|
||||
if (Contains(area ? area : object, x, y, &bounds)) {
|
||||
return true;
|
||||
}
|
||||
for (const char* path : {"Widgets/ComboBox", "Widgets/Slider"}) {
|
||||
Value widget;
|
||||
item.Child(path, widget);
|
||||
const Addr part = widget.DisplayObject();
|
||||
if (part && Shown(part) && Contains(part, x, y, &bounds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void CursorMoved(int index) {
|
||||
const Addr listener = Field<Addr>(c, Listener);
|
||||
if (const Addr moved = listener ? VirtualFunction(listener, 0x10) : 0) {
|
||||
std::int32_t at = index;
|
||||
using Fn = void(PS4_SYSV_ABI*)(void*, std::int32_t*);
|
||||
reinterpret_cast<Fn>(moved)(reinterpret_cast<void*>(listener), &at);
|
||||
}
|
||||
// The cursor's sound, unless one is already queued.
|
||||
auto* sound = reinterpret_cast<std::int32_t*>(At(MenuSound));
|
||||
if (sound[1] <= 0) {
|
||||
sound[0] = 0;
|
||||
sound[1] = 0;
|
||||
*reinterpret_cast<Addr*>(At(MenuSound) + 8) = At(CursorMoveSound);
|
||||
}
|
||||
}
|
||||
|
||||
std::uint8_t* c;
|
||||
Addr provider = 0, make_item = 0;
|
||||
};
|
||||
|
||||
// The pointer (window thread) and its presses (pad thread), for the game's thread.
|
||||
std::atomic<float> pointer_x{0.0f}, pointer_y{0.0f};
|
||||
std::atomic<bool> pointer_in{false};
|
||||
std::atomic<bool> click{false}; ///< a left click not yet used
|
||||
std::atomic<std::int64_t> click_ms{0};
|
||||
std::atomic<bool> left_held{false};
|
||||
std::atomic<std::uint32_t> presses{0}; ///< buttons to press once (pad thread)
|
||||
std::atomic<std::uint32_t> repeat_press{0};///< an arrow held under the pointer
|
||||
std::atomic<bool> pad_steering{false}; ///< the pad's directions are in use: hover waits
|
||||
std::atomic<std::int64_t> list_focus_ms{0};
|
||||
|
||||
constexpr std::int64_t ClickLifeMs = 250;
|
||||
|
||||
bool InputBlocked() {
|
||||
const Addr flow = Get<Addr>(At(MenuFlow));
|
||||
if (flow && Get<std::uint64_t>(flow + 0xa78)) {
|
||||
return true;
|
||||
}
|
||||
const Addr block = Get<Addr>(At(InputBlock));
|
||||
return block && Get<std::uint32_t>(block + 0x2e4);
|
||||
}
|
||||
|
||||
bool InMenu() {
|
||||
const Addr flow = Get<Addr>(At(MenuFlow));
|
||||
const bool title = flow && !Get<std::uint8_t>(flow + 0xb11, 1);
|
||||
const bool menu = flow && Get<Addr>(flow + 0xa88);
|
||||
// The menu object going away is the menu closing: the list-focus tail (400 ms, for pop-up
|
||||
// lists that have no menu object) would keep the pointer ~0.4 s over the game after it.
|
||||
static std::atomic<bool> had_menu{false};
|
||||
if (had_menu.exchange(menu, std::memory_order_relaxed) && !menu) {
|
||||
list_focus_ms.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
return title || menu || NowMs() - list_focus_ms.load(std::memory_order_relaxed) < 400;
|
||||
}
|
||||
|
||||
std::uint32_t ConfirmButton() {
|
||||
return Get<std::int32_t>(At(Region), -1) == 5 ? Circle : Cross;
|
||||
}
|
||||
|
||||
std::uint32_t BackButton() {
|
||||
return ConfirmButton() == Circle ? Cross : Circle;
|
||||
}
|
||||
|
||||
bool TakeClick() {
|
||||
if (!click.load(std::memory_order_relaxed)) {
|
||||
return false;
|
||||
}
|
||||
if (NowMs() - click_ms.load(std::memory_order_relaxed) > ClickLifeMs) {
|
||||
click.store(false, std::memory_order_relaxed);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Where the pointer was over each list the last time it was seen: hovering acts on a move only,
|
||||
/// so a pointer resting somewhere does not fight the keyboard or the pad.
|
||||
struct Seen {
|
||||
const std::uint8_t* list = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
std::int64_t ms = 0;
|
||||
};
|
||||
Seen seen[32];
|
||||
unsigned seen_next = 0;
|
||||
|
||||
/// Whether the pointer moved over `list` since it was last seen (a list not seen for a while
|
||||
/// counts as not moved: it just opened under the pointer).
|
||||
bool PointerMoved(const std::uint8_t* list, float x, float y) {
|
||||
const std::int64_t now = NowMs();
|
||||
for (Seen& s : seen) {
|
||||
if (s.list == list) {
|
||||
const bool moved = now - s.ms <= 250 && (s.x != x || s.y != y);
|
||||
s = {list, x, y, now};
|
||||
return moved;
|
||||
}
|
||||
}
|
||||
seen[seen_next++ % 32] = {list, x, y, now};
|
||||
return false;
|
||||
}
|
||||
|
||||
/// The game's list update (0x1acff30) is about to run for `component`.
|
||||
void PS4_SYSV_ABI OnListUpdate(std::uint64_t, const BbGuestCode::RegisterSnapshot* registers) {
|
||||
auto* component = reinterpret_cast<std::uint8_t*>(registers->rdi);
|
||||
const auto* input = reinterpret_cast<const std::uint8_t*>(registers->rsi);
|
||||
if (!component || !input || !*input || InputBlocked()) {
|
||||
return;
|
||||
}
|
||||
list_focus_ms.store(NowMs(), std::memory_order_relaxed);
|
||||
if (BbOverlay::ReleasesMouse() || !pointer_in.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
const float x = pointer_x.load(std::memory_order_relaxed);
|
||||
const float y = pointer_y.load(std::memory_order_relaxed);
|
||||
const bool moved = PointerMoved(component, x, y);
|
||||
const bool clicked = TakeClick();
|
||||
List list{component};
|
||||
|
||||
if (list.IsSpinner()) {
|
||||
// One value shown: a click on its left half goes left, on the right half right.
|
||||
Rect bounds{};
|
||||
const Addr root = list.Root();
|
||||
if (clicked && root && Shown(root) && Contains(root, x, y, &bounds) &&
|
||||
click.exchange(false, std::memory_order_relaxed)) {
|
||||
presses.fetch_or(x < bounds.CenterX() ? Left : Right, std::memory_order_relaxed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((!moved && !clicked) || pad_steering.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
std::uint64_t slot = 0;
|
||||
const int index = list.ItemAt(x, y, slot);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (clicked) {
|
||||
click.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
static const bool trace = std::getenv("BB_MENU_POINTER_FILE") != nullptr;
|
||||
if (trace) {
|
||||
std::printf("Menu pointer: list %p item %d (cursor %d)%s\n", static_cast<void*>(component),
|
||||
index, list.CursorIndex(), clicked ? " click" : "");
|
||||
}
|
||||
if (list.MoveTo(index, slot) && clicked) {
|
||||
const std::uint32_t button = list.ClickButton(slot, x, y, ConfirmButton());
|
||||
presses.fetch_or(button, std::memory_order_relaxed);
|
||||
// A held click on a slider's arrow keeps moving it.
|
||||
if (button == Left || button == Right) {
|
||||
repeat_press.store(button, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Window coordinates to the stage's (1920x1080, centred in the shown frame, scaled by height).
|
||||
void SetPointer(SDL_Window* window, float wx, float wy) {
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSize(window, &w, &h);
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
const float aspect = std::max(bbgpu_frame_aspect(), 0.1f);
|
||||
const float frame_h = std::min(float(h), float(w) / aspect);
|
||||
const float scale = frame_h / 1080.0f;
|
||||
pointer_x.store((wx - w * 0.5f) / scale + 960.0f, std::memory_order_relaxed);
|
||||
pointer_y.store((wy - h * 0.5f) / scale + 540.0f, std::memory_order_relaxed);
|
||||
pointer_in.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int PointerWanted() {
|
||||
// BB_MENU_POINTER_LOG=1: each of the signals InMenu is made of, with the time of every change.
|
||||
static const bool log = [] {
|
||||
const char* env = std::getenv("BB_MENU_POINTER_LOG");
|
||||
return env && env[0] == '1';
|
||||
}();
|
||||
if (log) {
|
||||
static int last = -1;
|
||||
const Addr flow = Get<Addr>(At(MenuFlow));
|
||||
const int title = flow && !Get<std::uint8_t>(flow + 0xb11, 1);
|
||||
const int object = flow && Get<Addr>(flow + 0xa88) ? 1 : 0;
|
||||
const int focus = NowMs() - list_focus_ms.load(std::memory_order_relaxed) < 400;
|
||||
const int state = title | object << 1 | focus << 2;
|
||||
if (state != last) {
|
||||
last = state;
|
||||
std::printf("Menu pointer signals at %lld ms: title %d, menu object %d, list focus %d\n",
|
||||
static_cast<long long>(NowMs()), title, object, focus);
|
||||
}
|
||||
}
|
||||
const bool menu = InMenu();
|
||||
static bool was = false;
|
||||
if (menu != was) {
|
||||
was = menu;
|
||||
std::printf("Menu pointer: %s\n", menu ? "a game menu is open (pointer)" : "menus closed");
|
||||
}
|
||||
return menu ? 1 : 0;
|
||||
}
|
||||
|
||||
/// The window's events (window thread): the pointer, and in a game menu its clicks and wheel.
|
||||
int OnEvent(const void* sdl_event, void* sdl_window) {
|
||||
const auto& event = *static_cast<const SDL_Event*>(sdl_event);
|
||||
auto* window = static_cast<SDL_Window*>(sdl_window);
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
if (!bbgpu_mouse_captured()) {
|
||||
SetPointer(window, event.motion.x, event.motion.y);
|
||||
}
|
||||
return 0;
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
pointer_in.store(false, std::memory_order_relaxed);
|
||||
return 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const bool ours = !bbgpu_mouse_captured() && !BbOverlay::ReleasesMouse() && InMenu();
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
|
||||
left_held.store(false, std::memory_order_relaxed);
|
||||
repeat_press.store(0, std::memory_order_relaxed);
|
||||
return ours ? 1 : 0;
|
||||
}
|
||||
if (!ours) {
|
||||
return 0;
|
||||
}
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
|
||||
SetPointer(window, event.button.x, event.button.y);
|
||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||
left_held.store(true, std::memory_order_relaxed);
|
||||
click_ms.store(NowMs(), std::memory_order_relaxed);
|
||||
click.store(true, std::memory_order_relaxed);
|
||||
} else if (event.button.button == SDL_BUTTON_RIGHT) {
|
||||
presses.fetch_or(BackButton(), std::memory_order_relaxed);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
return 1;
|
||||
}
|
||||
if (event.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
const float notches =
|
||||
event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -event.wheel.y : event.wheel.y;
|
||||
if (notches != 0.0f) {
|
||||
presses.fetch_or(notches > 0.0f ? Up : Down, std::memory_order_relaxed);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// BB_MENU_POINTER_FILE=<file> (tests): "x y [click|right|up|down]" in stage pixels, applied
|
||||
/// when the file changes, as the mouse would.
|
||||
void ReadTestFile() {
|
||||
static const char* path = std::getenv("BB_MENU_POINTER_FILE");
|
||||
static std::int64_t next_ms = 0;
|
||||
static char last[128] = {};
|
||||
const std::int64_t now = NowMs();
|
||||
if (!path || now < next_ms) {
|
||||
return;
|
||||
}
|
||||
next_ms = now + 200;
|
||||
char text[128] = {};
|
||||
if (FILE* f = std::fopen(path, "r")) {
|
||||
const std::size_t n = std::fread(text, 1, sizeof(text) - 1, f);
|
||||
text[n] = 0;
|
||||
std::fclose(f);
|
||||
}
|
||||
if (!text[0] || std::strcmp(text, last) == 0) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(last, text, sizeof(last));
|
||||
float x = 0.0f, y = 0.0f;
|
||||
char action[16] = {};
|
||||
if (std::sscanf(text, "%f %f %15s", &x, &y, action) < 2) {
|
||||
return;
|
||||
}
|
||||
pointer_x.store(x, std::memory_order_relaxed);
|
||||
pointer_y.store(y, std::memory_order_relaxed);
|
||||
pointer_in.store(true, std::memory_order_relaxed);
|
||||
if (!std::strcmp(action, "click")) {
|
||||
click_ms.store(now, std::memory_order_relaxed);
|
||||
click.store(true, std::memory_order_relaxed);
|
||||
} else if (!std::strcmp(action, "right")) {
|
||||
presses.fetch_or(BackButton(), std::memory_order_relaxed);
|
||||
} else if (!std::strcmp(action, "up") || !std::strcmp(action, "down")) {
|
||||
presses.fetch_or(action[0] == 'u' ? Up : Down, std::memory_order_relaxed);
|
||||
}
|
||||
std::printf("Menu pointer: test %.0f %.0f %s (in a menu: %d)\n", x, y, action, InMenu() ? 1 : 0);
|
||||
}
|
||||
|
||||
/// The pad state the game reads (pad thread): the pointer's presses, each held long enough for
|
||||
/// the game to see it, with a gap after.
|
||||
void OnPad(std::uint32_t* buttons, const std::uint8_t* sticks) {
|
||||
const auto off_centre = [](std::uint8_t v) { return v < 64 || v > 192; };
|
||||
pad_steering.store((*buttons & (Up | Right | Down | Left)) || off_centre(sticks[0]) ||
|
||||
off_centre(sticks[1]),
|
||||
std::memory_order_relaxed);
|
||||
ReadTestFile();
|
||||
static std::uint32_t holding = 0;
|
||||
static std::int64_t until_ms = 0, repeat_ms = 0;
|
||||
const std::int64_t now = NowMs();
|
||||
if (!InMenu() || BbOverlay::ReleasesMouse()) {
|
||||
presses.store(0, std::memory_order_relaxed);
|
||||
repeat_press.store(0, std::memory_order_relaxed);
|
||||
holding = 0;
|
||||
return;
|
||||
}
|
||||
constexpr std::int64_t PressMs = 50, GapMs = 40, RepeatDelayMs = 400, RepeatMs = 120;
|
||||
if (holding && now >= until_ms) {
|
||||
holding = 0;
|
||||
until_ms = now + GapMs;
|
||||
}
|
||||
if (!holding && now >= until_ms) {
|
||||
if (const std::uint32_t next = presses.exchange(0, std::memory_order_relaxed)) {
|
||||
holding = next;
|
||||
until_ms = now + PressMs;
|
||||
repeat_ms = now + RepeatDelayMs;
|
||||
} else if (const std::uint32_t arrow = repeat_press.load(std::memory_order_relaxed);
|
||||
arrow && left_held.load(std::memory_order_relaxed) && now >= repeat_ms) {
|
||||
holding = arrow;
|
||||
until_ms = now + PressMs;
|
||||
repeat_ms = now + RepeatMs;
|
||||
}
|
||||
}
|
||||
*buttons |= holding;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Install() {
|
||||
// BB_MENU_POINTER=0: off.
|
||||
if (const char* setting = std::getenv("BB_MENU_POINTER"); setting && setting[0] == '0') {
|
||||
std::printf("Menu pointer: off (BB_MENU_POINTER=0)\n");
|
||||
return;
|
||||
}
|
||||
static std::atomic<bool> installed{false};
|
||||
if (installed.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
if (!BbGuestCode::InstallHook(reinterpret_cast<void*>(At(ListUpdate)), ListUpdateEntry, 0,
|
||||
OnListUpdate)) {
|
||||
std::printf("Menu pointer: the game's list update differs (another build): off\n");
|
||||
return;
|
||||
}
|
||||
bbgpu_add_event_filter(OnEvent);
|
||||
bbgpu_add_pointer_query(PointerWanted);
|
||||
bbgpu_add_pad_filter(OnPad);
|
||||
std::printf("Menu pointer: on (the mouse points and clicks in the game's menus)\n");
|
||||
}
|
||||
|
||||
} // namespace BbMenuPointer
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Bloodborne (1.09): the mouse as a pointer in the game's own menus (Scaleform lists: the title
|
||||
// menu, the inventory, the shops, the options). The item under the pointer gets the game's cursor,
|
||||
// a click presses the confirm button on it (or the slider's arrow it is on), the right button goes
|
||||
// back, the wheel moves up and down. bloodborne_menu_pointer.cpp has the reverse engineering.
|
||||
#pragma once
|
||||
|
||||
namespace BbMenuPointer {
|
||||
|
||||
/// Hooks the game's list update and registers the window and pad parts (bbgpu_add_*). Nothing
|
||||
/// happens when the game's bytes differ (another build).
|
||||
void Install();
|
||||
|
||||
} // namespace BbMenuPointer
|
||||
@@ -2039,7 +2039,8 @@ void BufferCache::ProcessIdleBlocks() {
|
||||
}
|
||||
|
||||
// bbport (after Awaken's BloodBorneButPc 0.9.17): how full VRAM is against the driver's budget.
|
||||
// BB_VRAM_PRESSURE_PERCENT (85): tight from there, full 5 points above; 0 turns it off.
|
||||
// BB_VRAM_PRESSURE_PERCENT (85): tight from there, full 5 points above; 0 turns it off (the
|
||||
// texture collector's mark below too).
|
||||
void BufferCache::UpdateVramLevel(u32 now) {
|
||||
static const u64 percent = [] {
|
||||
const char* env = std::getenv("BB_VRAM_PRESSURE_PERCENT");
|
||||
@@ -2053,6 +2054,16 @@ void BufferCache::UpdateVramLevel(u32 now) {
|
||||
if (budget != 0) {
|
||||
level = used * 100 >= budget * (percent + 5) ? 2 : used * 100 >= budget * percent ? 1 : 0;
|
||||
}
|
||||
// bbport (#30): the 0.3 memory model is short of the texture collector's critical mark
|
||||
// too. The collector compares all of our VRAM and can only free images: on an 8 GB card
|
||||
// the copies grew to 1.8 GiB in an hour (never tight against the driver's budget), the
|
||||
// usage stayed at the mark, and images the GPU drew were evicted (black effects).
|
||||
const u64 critical = BbStats::gc_critical_bytes.load(std::memory_order_relaxed);
|
||||
if (!GuestInPlace() && critical != 0) {
|
||||
level = std::max(level, used >= critical ? 2
|
||||
: used + PromoteCriticalMargin >= critical ? 1
|
||||
: 0);
|
||||
}
|
||||
}
|
||||
if (now < vram_full_until.load(std::memory_order_relaxed)) {
|
||||
level = 2;
|
||||
@@ -2070,8 +2081,9 @@ void BufferCache::UpdateVramLevel(u32 now) {
|
||||
"tight: the game's data unused for 30 s leaves VRAM",
|
||||
"full: the game's data unused for 10 s leaves VRAM",
|
||||
};
|
||||
std::printf("Guest memory: VRAM %llu of %llu MiB, %s\n", (unsigned long long)(used >> 20),
|
||||
(unsigned long long)(budget >> 20),
|
||||
std::printf("Guest memory: VRAM %llu of %llu MiB (texture collector critical at %llu), %s\n",
|
||||
(unsigned long long)(used >> 20), (unsigned long long)(budget >> 20),
|
||||
(unsigned long long)(BbStats::gc_critical_bytes.load() >> 20),
|
||||
GuestInPlace() ? kInPlace[level] : kCopies[level]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ static vk::Result WaitFrameFence(vk::Device device, vk::Fence fence, const char*
|
||||
}
|
||||
}
|
||||
|
||||
/// bbport: the shown frame's aspect (bbgpu_frame_aspect, the menu pointer's stage): any thread.
|
||||
static std::atomic<float> shown_aspect{16.0f / 9.0f};
|
||||
|
||||
static vk::Rect2D FitImage(s32 frame_width, s32 frame_height, s32 swapchain_width,
|
||||
s32 swapchain_height) {
|
||||
float frame_aspect = static_cast<float>(frame_width) / frame_height;
|
||||
@@ -418,6 +421,7 @@ Frame* Presenter::PrepareFrame(const Libraries::VideoOut::BufferAttributeGroup&
|
||||
runtime.FlushBarriers();
|
||||
}
|
||||
expected_ratio = static_cast<float>(image_size.width) / static_cast<float>(image_size.height);
|
||||
shown_aspect.store(expected_ratio, std::memory_order_relaxed);
|
||||
|
||||
// Vulkan has no sRGB variant of the 10-bit format, so an A2R10G10B10Srgb buffer reaches
|
||||
// the post process pass still sRGB encoded and has to be decoded there instead.
|
||||
@@ -826,3 +830,7 @@ void Presenter::SetExpectedGameSize(s32 width, s32 height) {
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
|
||||
extern "C" float bbgpu_frame_aspect(void) {
|
||||
return Vulkan::shown_aspect.load(std::memory_order_relaxed);
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// bbport: observers in a game's own code (bbport_guest_code_hooks.h), from bbport 0.6. The
|
||||
// trampoline follows shadPS4's InstallGuestCodeHook (core/cpu_patches.cpp of the shadp2p/BBSC
|
||||
// forks, GPL-2.0+); the trampoline memory and the patching are the port's: the jump is written
|
||||
// under a short write permission, and the page gets back the one it had (read and execute, or
|
||||
// read, write and execute once BBSC has written the game's code).
|
||||
|
||||
#include "bbport_guest_code_hooks.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <xbyak/xbyak.h>
|
||||
#include <Zydis/Zydis.h>
|
||||
|
||||
#include "common/decoder.h"
|
||||
|
||||
namespace BbGuestCode {
|
||||
namespace {
|
||||
|
||||
using namespace Xbyak::util;
|
||||
|
||||
constexpr std::size_t NearJumpBytes = 5;
|
||||
constexpr std::size_t GuestRedZoneBytes = 128;
|
||||
constexpr std::size_t AreaSize = 4u << 20; // trampolines (~400 bytes each)
|
||||
constexpr std::int64_t JumpReach = 0x7000'0000; // inside a rel32 jump's 2 GiB
|
||||
|
||||
std::mutex hook_mutex;
|
||||
std::set<std::uintptr_t> hooked_sites; // first bytes of installed hooks
|
||||
|
||||
/// An executable area within reach of its code, mapped on first use.
|
||||
struct Area {
|
||||
std::uintptr_t start = 0;
|
||||
std::unique_ptr<Xbyak::CodeGenerator> gen;
|
||||
};
|
||||
std::vector<Area> areas;
|
||||
|
||||
std::uintptr_t MapNear(std::uintptr_t code) {
|
||||
const long page = sysconf(_SC_PAGESIZE);
|
||||
// Below the code first (the port maps the game's image at 0x800000000 with room under it),
|
||||
// then above, in 64 MiB steps.
|
||||
for (std::int64_t step = 1; step * (64ll << 20) < JumpReach; ++step) {
|
||||
for (const std::int64_t sign : {-1ll, 1ll}) {
|
||||
const std::int64_t at = std::int64_t(code) + sign * step * (64ll << 20);
|
||||
if (at <= 0) {
|
||||
continue;
|
||||
}
|
||||
void* p = mmap(reinterpret_cast<void*>(at & ~std::int64_t(page - 1)), AreaSize,
|
||||
PROT_READ | PROT_WRITE | PROT_EXEC,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (p != MAP_FAILED) {
|
||||
return reinterpret_cast<std::uintptr_t>(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Xbyak::CodeGenerator* AreaFor(std::uintptr_t code) {
|
||||
for (auto& area : areas) {
|
||||
const std::int64_t distance = std::int64_t(area.start) - std::int64_t(code);
|
||||
if (distance > -JumpReach && distance < JumpReach &&
|
||||
area.gen->getSize() + 1024 < AreaSize) {
|
||||
return area.gen.get();
|
||||
}
|
||||
}
|
||||
const std::uintptr_t start = MapNear(code);
|
||||
if (!start) {
|
||||
return nullptr;
|
||||
}
|
||||
auto gen = std::make_unique<Xbyak::CodeGenerator>(AreaSize, reinterpret_cast<void*>(start));
|
||||
areas.push_back({start, std::move(gen)});
|
||||
return areas.back().gen.get();
|
||||
}
|
||||
|
||||
bool WholeAndAbsolute(const std::uint8_t* code, std::size_t size) {
|
||||
std::size_t offset = 0;
|
||||
while (offset < size) {
|
||||
ZydisDecodedInstruction instruction{};
|
||||
std::array<ZydisDecodedOperand, ZYDIS_MAX_OPERAND_COUNT> operands{};
|
||||
const auto status = Common::Decoder::Instance()->decodeInstruction(
|
||||
instruction, operands.data(), const_cast<std::uint8_t*>(code) + offset, size - offset);
|
||||
if (!ZYAN_SUCCESS(status) || instruction.length == 0 ||
|
||||
instruction.length > size - offset) {
|
||||
std::fprintf(stderr, "Guest code hook at %p: not whole instructions\n",
|
||||
static_cast<const void*>(code));
|
||||
return false;
|
||||
}
|
||||
for (std::uint8_t i = 0; i < instruction.operand_count; ++i) {
|
||||
const auto& operand = operands[i];
|
||||
if ((operand.type == ZYDIS_OPERAND_TYPE_IMMEDIATE && operand.imm.is_relative) ||
|
||||
(operand.type == ZYDIS_OPERAND_TYPE_MEMORY &&
|
||||
(operand.mem.base == ZYDIS_REGISTER_RIP || operand.mem.base == ZYDIS_REGISTER_EIP))) {
|
||||
std::fprintf(stderr, "Guest code hook at %p: relative addressing\n",
|
||||
static_cast<const void*>(code));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
offset += instruction.length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The protection of the page at `page` now (/proc/self/maps), or read and execute.
|
||||
int PageProtection(std::uintptr_t page) {
|
||||
int protection = PROT_READ | PROT_EXEC;
|
||||
if (FILE* maps = std::fopen("/proc/self/maps", "r")) {
|
||||
char line[512];
|
||||
while (std::fgets(line, sizeof(line), maps)) {
|
||||
unsigned long start = 0, end = 0;
|
||||
char perms[5] = {};
|
||||
if (std::sscanf(line, "%lx-%lx %4s", &start, &end, perms) == 3 && page >= start &&
|
||||
page < end) {
|
||||
protection = (perms[0] == 'r' ? PROT_READ : 0) | (perms[1] == 'w' ? PROT_WRITE : 0) |
|
||||
(perms[2] == 'x' ? PROT_EXEC : 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::fclose(maps);
|
||||
}
|
||||
return protection;
|
||||
}
|
||||
|
||||
/// Writes `bytes` over the game's code under a short write permission.
|
||||
void WriteCode(std::uint8_t* code, const std::uint8_t* bytes, std::size_t size) {
|
||||
const long page = sysconf(_SC_PAGESIZE);
|
||||
auto* first = reinterpret_cast<std::uint8_t*>(reinterpret_cast<std::uintptr_t>(code) &
|
||||
~std::uintptr_t(page - 1));
|
||||
const std::size_t span = (code + size) - first;
|
||||
const int protection = PageProtection(reinterpret_cast<std::uintptr_t>(first));
|
||||
mprotect(first, span, PROT_READ | PROT_WRITE | PROT_EXEC);
|
||||
// The jump's last four bytes first, its opcode last: a thread running here meanwhile meets
|
||||
// either the old instruction or the whole jump.
|
||||
std::memcpy(code + 1, bytes + 1, size - 1);
|
||||
__atomic_store_n(code, bytes[0], __ATOMIC_SEQ_CST);
|
||||
mprotect(first, span, protection);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool InstallHook(void* address, std::span<const std::uint8_t> expected, std::uint64_t tag,
|
||||
Hook hook, const std::uint8_t* skip_flag) {
|
||||
constexpr std::size_t SnapshotOffset = 0;
|
||||
constexpr std::size_t ScratchR11Offset = sizeof(RegisterSnapshot);
|
||||
constexpr std::size_t ScratchRflagsOffset = ScratchR11Offset + sizeof(std::uint64_t);
|
||||
constexpr std::size_t FrameReserve =
|
||||
(ScratchRflagsOffset + sizeof(std::uint64_t) + GuestRedZoneBytes + 15) & ~std::size_t(15);
|
||||
|
||||
auto* code = static_cast<std::uint8_t*>(address);
|
||||
if (!hook || expected.size() < NearJumpBytes) {
|
||||
return false;
|
||||
}
|
||||
std::scoped_lock lock{hook_mutex};
|
||||
if (std::memcmp(code, expected.data(), expected.size()) != 0) {
|
||||
std::fprintf(stderr, "Guest code hook at %p: the code differs (another build?)\n", address);
|
||||
return false;
|
||||
}
|
||||
if (!WholeAndAbsolute(code, expected.size())) {
|
||||
return false;
|
||||
}
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(code);
|
||||
const auto overlap = hooked_sites.lower_bound(begin > 16 ? begin - 16 : 0);
|
||||
if (overlap != hooked_sites.end() && *overlap < begin + expected.size()) {
|
||||
std::fprintf(stderr, "Guest code hook at %p overlaps another one\n", address);
|
||||
return false;
|
||||
}
|
||||
Xbyak::CodeGenerator* t = AreaFor(begin);
|
||||
if (!t) {
|
||||
std::fprintf(stderr, "Guest code hook at %p: no trampoline memory in reach\n", address);
|
||||
return false;
|
||||
}
|
||||
const std::size_t start_offset = t->getSize();
|
||||
const auto* trampoline = t->getCurr();
|
||||
const auto snapshot = [](std::size_t field) { return qword[rsp + SnapshotOffset + field]; };
|
||||
try {
|
||||
t->lea(rsp, ptr[rsp - FrameReserve]); // past the game's red zone
|
||||
t->mov(qword[rsp + ScratchR11Offset], r11);
|
||||
t->pushfq();
|
||||
t->pop(r11);
|
||||
t->mov(qword[rsp + ScratchRflagsOffset], r11);
|
||||
t->mov(r11, rsp);
|
||||
t->and_(rsp, -16);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rax)), rax);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rbx)), rbx);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rcx)), rcx);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rdx)), rdx);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rsi)), rsi);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rdi)), rdi);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rbp)), rbp);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, r8)), r8);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, r9)), r9);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, r10)), r10);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, r12)), r12);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, r13)), r13);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, r14)), r14);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, r15)), r15);
|
||||
t->mov(rax, qword[r11 + ScratchR11Offset]);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, r11)), rax);
|
||||
t->mov(rax, qword[r11 + ScratchRflagsOffset]);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rflags)), rax);
|
||||
t->lea(rax, ptr[r11 + FrameReserve]);
|
||||
t->mov(snapshot(offsetof(RegisterSnapshot, rsp)), rax);
|
||||
t->stmxcsr(dword[rsp + SnapshotOffset + offsetof(RegisterSnapshot, mxcsr)]);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
t->vmovdqu(yword[rsp + SnapshotOffset + offsetof(RegisterSnapshot, ymm) + i * 32],
|
||||
Xbyak::Ymm(i));
|
||||
}
|
||||
// The aligned snapshot in a callee-saved register across the call.
|
||||
t->mov(r12, rsp);
|
||||
t->mov(rdi, tag);
|
||||
t->lea(rsi, ptr[r12 + SnapshotOffset]);
|
||||
t->mov(rax, reinterpret_cast<std::uint64_t>(hook));
|
||||
t->call(rax);
|
||||
t->mov(rsp, r12);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
t->vmovdqu(Xbyak::Ymm(i),
|
||||
yword[rsp + SnapshotOffset + offsetof(RegisterSnapshot, ymm) + i * 32]);
|
||||
}
|
||||
t->ldmxcsr(dword[rsp + SnapshotOffset + offsetof(RegisterSnapshot, mxcsr)]);
|
||||
t->mov(rbx, snapshot(offsetof(RegisterSnapshot, rbx)));
|
||||
t->mov(rcx, snapshot(offsetof(RegisterSnapshot, rcx)));
|
||||
t->mov(rdx, snapshot(offsetof(RegisterSnapshot, rdx)));
|
||||
t->mov(rsi, snapshot(offsetof(RegisterSnapshot, rsi)));
|
||||
t->mov(rdi, snapshot(offsetof(RegisterSnapshot, rdi)));
|
||||
t->mov(rbp, snapshot(offsetof(RegisterSnapshot, rbp)));
|
||||
t->mov(r8, snapshot(offsetof(RegisterSnapshot, r8)));
|
||||
t->mov(r9, snapshot(offsetof(RegisterSnapshot, r9)));
|
||||
t->mov(r10, snapshot(offsetof(RegisterSnapshot, r10)));
|
||||
t->mov(r11, snapshot(offsetof(RegisterSnapshot, r11)));
|
||||
t->mov(r12, snapshot(offsetof(RegisterSnapshot, r12)));
|
||||
t->mov(r13, snapshot(offsetof(RegisterSnapshot, r13)));
|
||||
t->mov(r14, snapshot(offsetof(RegisterSnapshot, r14)));
|
||||
t->mov(r15, snapshot(offsetof(RegisterSnapshot, r15)));
|
||||
t->mov(rax, snapshot(offsetof(RegisterSnapshot, rflags)));
|
||||
t->push(rax);
|
||||
t->popfq();
|
||||
t->mov(rax, snapshot(offsetof(RegisterSnapshot, rax)));
|
||||
t->mov(rsp, snapshot(offsetof(RegisterSnapshot, rsp)));
|
||||
Xbyak::Label run_original;
|
||||
if (skip_flag) {
|
||||
// Function entries only: [rsp] is the return address, flags are not live across a
|
||||
// call; a set flag (cleared here) returns from the game's function at once.
|
||||
t->push(rax);
|
||||
t->mov(rax, reinterpret_cast<std::uint64_t>(skip_flag));
|
||||
t->cmp(byte[rax], 0);
|
||||
t->mov(byte[rax], 0);
|
||||
t->pop(rax);
|
||||
t->je(run_original, Xbyak::CodeGenerator::LabelType::T_NEAR);
|
||||
t->ret();
|
||||
}
|
||||
t->L(run_original);
|
||||
t->db(expected.data(), expected.size());
|
||||
t->jmp(reinterpret_cast<const void*>(code + expected.size()),
|
||||
Xbyak::CodeGenerator::LabelType::T_NEAR);
|
||||
} catch (const Xbyak::Error& error) {
|
||||
t->setSize(start_offset);
|
||||
std::fprintf(stderr, "Guest code hook at %p: trampoline failed (%s)\n", address,
|
||||
error.what());
|
||||
return false;
|
||||
}
|
||||
// The site: a jump to the trampoline, the rest of the span filled with nops.
|
||||
std::vector<std::uint8_t> patch(expected.size(), 0x90);
|
||||
const std::int64_t rel = std::int64_t(reinterpret_cast<std::uintptr_t>(trampoline)) -
|
||||
std::int64_t(begin + NearJumpBytes);
|
||||
patch[0] = 0xe9;
|
||||
const std::int32_t rel32 = std::int32_t(rel);
|
||||
std::memcpy(patch.data() + 1, &rel32, 4);
|
||||
WriteCode(code, patch.data(), patch.size());
|
||||
hooked_sites.insert(begin);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace BbGuestCode
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// bbport: observers in a game's own code (any game). At a byte-checked run of whole instructions
|
||||
// (at least 5 bytes, nothing relative to the instruction pointer) a jump goes to a trampoline that
|
||||
// saves every general register, the flags, MXCSR and the YMM registers into a snapshot, calls a
|
||||
// host function with it, puts the (possibly changed) registers back, runs the replaced
|
||||
// instructions and jumps back. With a skip flag (function entries only) the hook may set it to
|
||||
// return from the game's function at once.
|
||||
//
|
||||
// From bbport 0.6 (gpu/shim/bbport_guest_code_hooks.h), after shadPS4's InstallGuestCodeHook
|
||||
// (core/cpu_patches.h of the shadp2p/BBSC forks). Its own namespace here: libbbgpu's symbols are
|
||||
// global (it is linked into bb-probe), and libbbsc.so has a Core::InstallGuestCodeHook of its own
|
||||
// that a same-named one here would take the place of.
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
namespace BbGuestCode {
|
||||
|
||||
struct alignas(16) RegisterSnapshot {
|
||||
std::uint64_t rax;
|
||||
std::uint64_t rbx;
|
||||
std::uint64_t rcx;
|
||||
std::uint64_t rdx;
|
||||
std::uint64_t rsi;
|
||||
std::uint64_t rdi;
|
||||
std::uint64_t rbp;
|
||||
std::uint64_t rsp;
|
||||
std::uint64_t r8;
|
||||
std::uint64_t r9;
|
||||
std::uint64_t r10;
|
||||
std::uint64_t r11;
|
||||
std::uint64_t r12;
|
||||
std::uint64_t r13;
|
||||
std::uint64_t r14;
|
||||
std::uint64_t r15;
|
||||
std::uint64_t rflags;
|
||||
std::uint32_t mxcsr;
|
||||
alignas(16) std::array<std::array<std::uint8_t, 32>, 16> ymm;
|
||||
};
|
||||
|
||||
/// Called on the game's thread, on its stack (keep it small), with the System V ABI.
|
||||
using Hook = void(__attribute__((sysv_abi)) *)(std::uint64_t tag, const RegisterSnapshot* registers);
|
||||
|
||||
/// Installs a hook at `address` over `expected_instructions` (checked byte for byte). False: the
|
||||
/// bytes differ (another build of the game, or a port patch there), the span is not whole
|
||||
/// position-independent instructions, it overlaps a hook, or no trampoline memory is in reach.
|
||||
bool InstallHook(void* address, std::span<const std::uint8_t> expected_instructions,
|
||||
std::uint64_t tag, Hook hook, const std::uint8_t* skip_flag = nullptr);
|
||||
|
||||
} // namespace BbGuestCode
|
||||
@@ -54,6 +54,8 @@ private:
|
||||
void UpdateCursor();
|
||||
u64 last_mouse_motion_ms{}; ///< SDL_GetTicks of the last mouse motion (UpdateCursor)
|
||||
bool cursor_hidden{};
|
||||
bool pointer_shown{}; ///< a game menu the mouse points into is open (UpdateCursor)
|
||||
bool retake_after_pointer{}; ///< the mouse was let go for that menu: taken back after it
|
||||
void CaptureMouse(bool capture);
|
||||
/// Installs or removes the direct mouse camera's hook (bloodborne_cam.h) as the capture goes.
|
||||
void UpdateMouseCamera();
|
||||
|
||||
+89
-4
@@ -1,4 +1,6 @@
|
||||
// bbport: SDL3 window for the Vulkan swapchain (X11 or Wayland).
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <charconv>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -6,10 +8,68 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "../bbgpu.h"
|
||||
#include "sdl_window.h"
|
||||
#include "bbport_overlay.h"
|
||||
#include "bloodborne_cam.h"
|
||||
|
||||
// Game modules' parts of the window and the pad (bbgpu_add_*: the menu pointer), from bbport 0.6.
|
||||
namespace {
|
||||
std::array<std::atomic<BbGpuEventFilter>, 4> event_filters{};
|
||||
std::array<std::atomic<BbGpuPointerQuery>, 4> pointer_queries{};
|
||||
std::array<std::atomic<BbGpuPadFilter>, 4> pad_filters{};
|
||||
|
||||
template <typename T>
|
||||
void AddTo(std::array<std::atomic<T>, 4>& slots, T value) {
|
||||
for (auto& slot : slots) {
|
||||
T expected = nullptr;
|
||||
if (slot.compare_exchange_strong(expected, value)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FilteredByModule(const SDL_Event* event, SDL_Window* window) {
|
||||
for (const auto& slot : event_filters) {
|
||||
const BbGpuEventFilter filter = slot.load(std::memory_order_acquire);
|
||||
if (filter && filter(event, window)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void bbgpu_add_event_filter(BbGpuEventFilter filter) {
|
||||
AddTo(event_filters, filter);
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_add_pointer_query(BbGpuPointerQuery query) {
|
||||
AddTo(pointer_queries, query);
|
||||
}
|
||||
|
||||
extern "C" int bbgpu_pointer_wanted(void) {
|
||||
for (const auto& slot : pointer_queries) {
|
||||
const BbGpuPointerQuery query = slot.load(std::memory_order_acquire);
|
||||
if (query && query()) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_add_pad_filter(BbGpuPadFilter filter) {
|
||||
AddTo(pad_filters, filter);
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_filter_pad(uint32_t* buttons, const uint8_t* sticks) {
|
||||
for (const auto& slot : pad_filters) {
|
||||
if (const BbGpuPadFilter filter = slot.load(std::memory_order_acquire)) {
|
||||
filter(buttons, sticks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Frontend {
|
||||
|
||||
namespace {
|
||||
@@ -246,8 +306,13 @@ bool WindowSDL::PollEvents() {
|
||||
if (!text_active) {
|
||||
BbOverlay::UpdateTextInput(window);
|
||||
}
|
||||
// A game module's menu wants the mouse as a pointer (bbgpu_add_pointer_query).
|
||||
const bool pointer = bbgpu_pointer_wanted() != 0;
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (FilteredByModule(&event, window)) {
|
||||
continue;
|
||||
}
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
last_mouse_motion_ms = SDL_GetTicks();
|
||||
if (mouse_captured.load(std::memory_order_relaxed)) {
|
||||
@@ -285,13 +350,15 @@ bool WindowSDL::PollEvents() {
|
||||
// window let it go.
|
||||
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && !mouse_captured.load(std::memory_order_relaxed) &&
|
||||
mouse_look.load(std::memory_order_relaxed) && !BbOverlay::MenuOpen() &&
|
||||
!BbOverlay::ReleasesMouse()) { // Hunter's Atlas: its map and tile use the pointer
|
||||
!BbOverlay::ReleasesMouse() && // Hunter's Atlas: its map and tile use the pointer
|
||||
!pointer) { // a game menu: the click is the menu pointer's
|
||||
CaptureMouse(true);
|
||||
continue;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.key == SDLK_F1 &&
|
||||
mouse_captured.load(std::memory_order_relaxed)) {
|
||||
(mouse_captured.load(std::memory_order_relaxed) || retake_after_pointer)) {
|
||||
CaptureMouse(false);
|
||||
retake_after_pointer = false; // F1 in a game menu: it stays free after the menu too
|
||||
continue;
|
||||
}
|
||||
if (event.type == SDL_EVENT_MOUSE_WHEEL && mouse_captured.load(std::memory_order_relaxed)) {
|
||||
@@ -302,6 +369,7 @@ bool WindowSDL::PollEvents() {
|
||||
if (event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
|
||||
focused.store(false, std::memory_order_relaxed);
|
||||
CaptureMouse(false);
|
||||
retake_after_pointer = false;
|
||||
}
|
||||
if (event.type == SDL_EVENT_WINDOW_FOCUS_GAINED) {
|
||||
focused.store(true, std::memory_order_relaxed);
|
||||
@@ -323,6 +391,20 @@ bool WindowSDL::PollEvents() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A game menu the mouse points into (the menu pointer): the window lets the mouse go while it
|
||||
// is open, and takes it back after if it had it.
|
||||
if (pointer && mouse_captured.load(std::memory_order_relaxed)) {
|
||||
CaptureMouse(false);
|
||||
retake_after_pointer = true;
|
||||
} else if (!pointer && retake_after_pointer) {
|
||||
retake_after_pointer = false;
|
||||
if (focused.load(std::memory_order_relaxed) && !text_active &&
|
||||
mouse_look.load(std::memory_order_relaxed) && !BbOverlay::MenuOpen() &&
|
||||
!BbOverlay::ReleasesMouse()) {
|
||||
CaptureMouse(true);
|
||||
}
|
||||
}
|
||||
pointer_shown = pointer;
|
||||
// The menu, the name dialog, Hunter's Atlas's map and tile, and mouse_look=0 want the cursor.
|
||||
if (mouse_captured.load(std::memory_order_relaxed) &&
|
||||
(BbOverlay::MenuOpen() || BbOverlay::ReleasesMouse() || text_active ||
|
||||
@@ -349,11 +431,14 @@ void WindowSDL::UpdateMouseCamera() {
|
||||
}
|
||||
|
||||
// Issue #3: the OS cursor over the game. Hidden in fullscreen, and in a window after 3 s without
|
||||
// moving the mouse; always shown while the settings menu is open.
|
||||
// moving the mouse; always shown while the settings menu is open. In a game menu the mouse points
|
||||
// into (pointer_shown) it shows in fullscreen too, and goes after 1.5 s without moving, so that the
|
||||
// tail of a closing menu does not leave it hanging over the game; any motion brings it back.
|
||||
void WindowSDL::UpdateCursor() {
|
||||
const bool fullscreen = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||
const u64 idle = SDL_GetTicks() - last_mouse_motion_ms;
|
||||
const bool hide = !BbOverlay::MenuOpen() &&
|
||||
(fullscreen || SDL_GetTicks() - last_mouse_motion_ms > 3000);
|
||||
(pointer_shown ? idle > 1500 : (fullscreen || idle > 3000));
|
||||
if (hide != cursor_hidden) {
|
||||
cursor_hidden = hide;
|
||||
hide ? SDL_HideCursor() : SDL_ShowCursor();
|
||||
|
||||
+77
-7
@@ -301,6 +301,14 @@ EN = {
|
||||
"Камера влево": "Camera left",
|
||||
"Камера вправо": "Camera right",
|
||||
"Шаг (удерживать с движением)": "Walk (hold while moving)",
|
||||
"Бег (удерживать: Круг)": "Sprint (hold: Circle)",
|
||||
"Уклонение (короткое нажатие Круга; с бегом — прыжок)": "Dodge (a short Circle press; while sprinting, a jump)",
|
||||
"Раскладка клавиатуры": "Keyboard layout",
|
||||
"Назначенные вами клавиши сохраняются": "Keys you assigned are kept",
|
||||
"Этого порта": "This port's",
|
||||
"bbport 0.6 (как в Dark Souls III на ПК)": "bbport 0.6 (as in Dark Souls III on PC)",
|
||||
"Пробел — Крест, левый Shift — Круг": "Space Cross, Left Shift Circle",
|
||||
"Пробел — уклонение, Shift — бег, Shift + Пробел — прыжок, F — действие, Escape — меню и назад в меню игры": "Space dodge, Shift sprint, Shift + Space jump, F interact, Escape menu and back in the game's menus",
|
||||
"Контроллер": "Controller",
|
||||
"Выбранный берётся, как только подключится": "The chosen one is used as soon as it connects",
|
||||
"Тот, которым играете": "The one you play with",
|
||||
@@ -814,7 +822,7 @@ EN = {
|
||||
'Порт и его код': 'The port and its code',
|
||||
'Помощь и тесты': 'Help and testing',
|
||||
'Другие благодарности': 'Also thanks to',
|
||||
'bbport — порт, на котором построен весь этот проект': 'bbport, the port this whole project is built on',
|
||||
'bbport — порт, на котором построен весь этот проект, а также указатель мыши в меню и раскладка клавиатуры из 0.6': "bbport, the port this whole project is built on, and 0.6's menu pointer and keyboard layout",
|
||||
'Игра по сети, DLSS, страница читов, добавление в Steam, значок, пропуск конвейеров, которые драйвер не может создать, игра без вылета при нехватке видеопамяти и пропуск отрисовок с пустой таблицей вершин': 'Online play, DLSS, the cheats page, Add to Steam, the icon, skipping pipelines the driver cannot create, no crash when video memory runs out, and skipping draws with an empty vertex table',
|
||||
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Gathered and adapted dozens of fixes: crashes while loading, black images, the upscaler, the performance line in the log',
|
||||
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': "The black character preview, the loading screen's picture and the texture cache's memory",
|
||||
@@ -919,6 +927,8 @@ EN = {
|
||||
'FSR 3.1 без цветного шума на Ryzen Z1 (ROG Ally, Legion Go)': 'FSR 3.1 without colour noise on the Ryzen Z1 (ROG Ally, Legion Go)',
|
||||
'Вылет на Steam Deck после путешествия через фонарь и проверка исправления': 'A crash on the Steam Deck after lamp travel, and testing the fix',
|
||||
'Камера мышью, поворачивавшаяся по горизонтали медленнее, чем по вертикали': 'The mouse camera turning slower sideways than up and down',
|
||||
'Просьба об указателе мыши в меню игры': "Asking for the mouse pointer in the game's menus",
|
||||
'Просьба о раскладке клавиатуры из bbport 0.6': "Asking for bbport 0.6's keyboard layout",
|
||||
}
|
||||
|
||||
PT_BR = {
|
||||
@@ -1510,6 +1520,14 @@ PT_BR = {
|
||||
"Камера вправо":
|
||||
"Câmera para a direita",
|
||||
"Шаг (удерживать с движением)": "Andar (segurar enquanto se move)",
|
||||
"Бег (удерживать: Круг)": "Correr (segurar: Círculo)",
|
||||
"Уклонение (короткое нажатие Круга; с бегом — прыжок)": "Esquivar (toque curto em Círculo; correndo, um salto)",
|
||||
"Раскладка клавиатуры": "Layout do teclado",
|
||||
"Назначенные вами клавиши сохраняются": "As teclas que você atribuiu são mantidas",
|
||||
"Этого порта": "Deste port",
|
||||
"bbport 0.6 (как в Dark Souls III на ПК)": "bbport 0.6 (como no Dark Souls III para PC)",
|
||||
"Пробел — Крест, левый Shift — Круг": "Espaço Xis, Shift esquerdo Círculo",
|
||||
"Пробел — уклонение, Shift — бег, Shift + Пробел — прыжок, F — действие, Escape — меню и назад в меню игры": "Espaço esquiva, Shift corre, Shift + Espaço pula, F interage, Escape menu e voltar nos menus do jogo",
|
||||
"Контроллер":
|
||||
"Controle",
|
||||
"Выбранный берётся, как только подключится":
|
||||
@@ -1786,7 +1804,7 @@ PT_BR = {
|
||||
'Порт и его код': 'O port e seu código',
|
||||
'Помощь и тесты': 'Ajuda e testes',
|
||||
'Другие благодарности': 'Também agradecemos a',
|
||||
'bbport — порт, на котором построен весь этот проект': 'bbport, o port em que todo este projeto se baseia',
|
||||
'bbport — порт, на котором построен весь этот проект, а также указатель мыши в меню и раскладка клавиатуры из 0.6': 'bbport, o port em que todo este projeto se baseia, e o ponteiro nos menus e o layout de teclado da 0.6',
|
||||
'Игра по сети, DLSS, страница читов, добавление в Steam, значок, пропуск конвейеров, которые драйвер не может создать, игра без вылета при нехватке видеопамяти и пропуск отрисовок с пустой таблицей вершин': 'Jogo online, DLSS, a página de trapaças, Adicionar à Steam, o ícone, pular pipelines que o driver não consegue criar, nenhum travamento quando a memória de vídeo acaba e pular desenhos com a tabela de vértices vazia',
|
||||
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Reuniu e adaptou dezenas de correções: travamentos ao carregar, imagens pretas, o upscaler, a linha de desempenho no log',
|
||||
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'A prévia do personagem preta, a imagem da tela de carregamento e a memória do cache de texturas',
|
||||
@@ -1891,6 +1909,8 @@ PT_BR = {
|
||||
'FSR 3.1 без цветного шума на Ryzen Z1 (ROG Ally, Legion Go)': 'FSR 3.1 sem ruído colorido no Ryzen Z1 (ROG Ally, Legion Go)',
|
||||
'Вылет на Steam Deck после путешествия через фонарь и проверка исправления': 'Um crash no Steam Deck após viajar por uma lanterna, e o teste da correção',
|
||||
'Камера мышью, поворачивавшаяся по горизонтали медленнее, чем по вертикали': 'A câmera do mouse girando mais devagar na horizontal do que na vertical',
|
||||
'Просьба об указателе мыши в меню игры': 'O pedido do ponteiro do mouse nos menus do jogo',
|
||||
'Просьба о раскладке клавиатуры из bbport 0.6': 'O pedido do layout de teclado do bbport 0.6',
|
||||
}
|
||||
|
||||
|
||||
@@ -2181,6 +2201,14 @@ ES = {
|
||||
"Камера влево": "Cámara a la izquierda",
|
||||
"Камера вправо": "Cámara a la derecha",
|
||||
"Шаг (удерживать с движением)": "Andar (mantener al moverse)",
|
||||
"Бег (удерживать: Круг)": "Correr (mantener: Círculo)",
|
||||
"Уклонение (короткое нажатие Круга; с бегом — прыжок)": "Esquivar (pulsación corta de Círculo; corriendo, un salto)",
|
||||
"Раскладка клавиатуры": "Distribución del teclado",
|
||||
"Назначенные вами клавиши сохраняются": "Las teclas que hayas asignado se mantienen",
|
||||
"Этого порта": "La de este port",
|
||||
"bbport 0.6 (как в Dark Souls III на ПК)": "bbport 0.6 (como en Dark Souls III para PC)",
|
||||
"Пробел — Крест, левый Shift — Круг": "Espacio Equis, Shift izquierdo Círculo",
|
||||
"Пробел — уклонение, Shift — бег, Shift + Пробел — прыжок, F — действие, Escape — меню и назад в меню игры": "Espacio esquiva, Shift corre, Shift + Espacio salta, F interactúa, Escape menú y atrás en los menús del juego",
|
||||
"Контроллер": "Mando",
|
||||
"Выбранный берётся, как только подключится": "Se usa el elegido en cuanto se conecta",
|
||||
"Тот, которым играете": "El que estés usando",
|
||||
@@ -2673,7 +2701,7 @@ ES = {
|
||||
'Порт и его код': 'El port y su código',
|
||||
'Помощь и тесты': 'Ayuda y pruebas',
|
||||
'Другие благодарности': 'También gracias a',
|
||||
'bbport — порт, на котором построен весь этот проект': 'bbport, el port en el que se basa todo este proyecto',
|
||||
'bbport — порт, на котором построен весь этот проект, а также указатель мыши в меню и раскладка клавиатуры из 0.6': 'bbport, el port en el que se basa todo este proyecto, y el puntero en los menús y la distribución de teclado de la 0.6',
|
||||
'Игра по сети, DLSS, страница читов, добавление в Steam, значок, пропуск конвейеров, которые драйвер не может создать, игра без вылета при нехватке видеопамяти и пропуск отрисовок с пустой таблицей вершин': 'Juego online, DLSS, la página de trucos, Añadir a Steam, el icono, saltar los pipelines que el driver no puede crear, ningún cierre al quedarse sin memoria de vídeo y saltar los dibujos con la tabla de vértices vacía',
|
||||
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Reunió y adaptó decenas de arreglos: cierres al cargar, imágenes en negro, el escalado, la línea de rendimiento del registro',
|
||||
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'La vista previa del personaje en negro, la imagen de la pantalla de carga y la memoria de la caché de texturas',
|
||||
@@ -2778,6 +2806,8 @@ ES = {
|
||||
'FSR 3.1 без цветного шума на Ryzen Z1 (ROG Ally, Legion Go)': 'FSR 3.1 sin ruido de colores en el Ryzen Z1 (ROG Ally, Legion Go)',
|
||||
'Вылет на Steam Deck после путешествия через фонарь и проверка исправления': 'Un cierre en Steam Deck tras viajar con un farol, y la prueba del arreglo',
|
||||
'Камера мышью, поворачивавшаяся по горизонтали медленнее, чем по вертикали': 'La cámara con ratón girando más despacio en horizontal que en vertical',
|
||||
'Просьба об указателе мыши в меню игры': 'Pedir el puntero del ratón en los menús del juego',
|
||||
'Просьба о раскладке клавиатуры из bbport 0.6': 'Pedir la distribución de teclado de bbport 0.6',
|
||||
}
|
||||
|
||||
|
||||
@@ -3068,6 +3098,14 @@ IT = {
|
||||
"Камера влево": "Camera a sinistra",
|
||||
"Камера вправо": "Camera a destra",
|
||||
"Шаг (удерживать с движением)": "Camminare (tenere premuto mentre ci si muove)",
|
||||
"Бег (удерживать: Круг)": "Correre (tenere premuto: Cerchio)",
|
||||
"Уклонение (короткое нажатие Круга; с бегом — прыжок)": "Schivata (pressione breve di Cerchio; correndo, un salto)",
|
||||
"Раскладка клавиатуры": "Layout della tastiera",
|
||||
"Назначенные вами клавиши сохраняются": "I tasti che hai assegnato restano",
|
||||
"Этого порта": "Di questo port",
|
||||
"bbport 0.6 (как в Dark Souls III на ПК)": "bbport 0.6 (come in Dark Souls III su PC)",
|
||||
"Пробел — Крест, левый Shift — Круг": "Spazio Croce, Shift sinistro Cerchio",
|
||||
"Пробел — уклонение, Shift — бег, Shift + Пробел — прыжок, F — действие, Escape — меню и назад в меню игры": "Spazio schiva, Shift corre, Shift + Spazio salta, F interagisce, Esc menu e indietro nei menu del gioco",
|
||||
"Контроллер": "Controller",
|
||||
"Выбранный берётся, как только подключится": "Quello scelto viene usato non appena si connette",
|
||||
"Тот, которым играете": "Quello che stai usando",
|
||||
@@ -3560,7 +3598,7 @@ IT = {
|
||||
'Порт и его код': 'Il port e il suo codice',
|
||||
'Помощь и тесты': 'Aiuto e test',
|
||||
'Другие благодарности': 'Grazie anche a',
|
||||
'bbport — порт, на котором построен весь этот проект': 'bbport, il port su cui si basa tutto questo progetto',
|
||||
'bbport — порт, на котором построен весь этот проект, а также указатель мыши в меню и раскладка клавиатуры из 0.6': 'bbport, il port su cui si basa tutto questo progetto, e il puntatore nei menu e il layout di tastiera della 0.6',
|
||||
'Игра по сети, DLSS, страница читов, добавление в Steam, значок, пропуск конвейеров, которые драйвер не может создать, игра без вылета при нехватке видеопамяти и пропуск отрисовок с пустой таблицей вершин': 'Gioco online, DLSS, la pagina dei trucchi, Aggiungi a Steam, l’icona, saltare le pipeline che il driver non riesce a creare, nessun crash quando finisce la memoria video e saltare i disegni con la tabella dei vertici vuota',
|
||||
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Ha raccolto e adattato decine di correzioni: crash durante il caricamento, immagini nere, l’upscaler, la riga delle prestazioni nel log',
|
||||
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'L’anteprima nera del personaggio, l’immagine della schermata di caricamento e la memoria della cache delle texture',
|
||||
@@ -3665,6 +3703,8 @@ IT = {
|
||||
'FSR 3.1 без цветного шума на Ryzen Z1 (ROG Ally, Legion Go)': 'FSR 3.1 senza rumore colorato sul Ryzen Z1 (ROG Ally, Legion Go)',
|
||||
'Вылет на Steam Deck после путешествия через фонарь и проверка исправления': 'Un crash su Steam Deck dopo un viaggio con una lanterna, e il test della correzione',
|
||||
'Камера мышью, поворачивавшаяся по горизонтали медленнее, чем по вертикали': 'La camera col mouse che ruotava più lentamente in orizzontale che in verticale',
|
||||
'Просьба об указателе мыши в меню игры': 'La richiesta del puntatore del mouse nei menu del gioco',
|
||||
'Просьба о раскладке клавиатуры из bbport 0.6': 'La richiesta del layout di tastiera di bbport 0.6',
|
||||
}
|
||||
|
||||
|
||||
@@ -3955,6 +3995,14 @@ FR = {
|
||||
"Камера влево": "Caméra vers la gauche",
|
||||
"Камера вправо": "Caméra vers la droite",
|
||||
"Шаг (удерживать с движением)": "Marcher (maintenir en se déplaçant)",
|
||||
"Бег (удерживать: Круг)": "Courir (maintenir : Rond)",
|
||||
"Уклонение (короткое нажатие Круга; с бегом — прыжок)": "Esquive (appui bref sur Rond ; en courant, un saut)",
|
||||
"Раскладка клавиатуры": "Disposition du clavier",
|
||||
"Назначенные вами клавиши сохраняются": "Les touches que vous avez attribuées sont conservées",
|
||||
"Этого порта": "Celle de ce port",
|
||||
"bbport 0.6 (как в Dark Souls III на ПК)": "bbport 0.6 (comme Dark Souls III sur PC)",
|
||||
"Пробел — Крест, левый Shift — Круг": "Espace Croix, Maj gauche Rond",
|
||||
"Пробел — уклонение, Shift — бег, Shift + Пробел — прыжок, F — действие, Escape — меню и назад в меню игры": "Espace esquive, Maj court, Maj + Espace saute, F interagit, Échap menu et retour dans les menus du jeu",
|
||||
"Контроллер": "Manette",
|
||||
"Выбранный берётся, как только подключится": "Celle choisie est utilisée dès qu'elle se connecte",
|
||||
"Тот, которым играете": "Celle que vous utilisez",
|
||||
@@ -4447,7 +4495,7 @@ FR = {
|
||||
'Порт и его код': 'Le port et son code',
|
||||
'Помощь и тесты': 'Aide et tests',
|
||||
'Другие благодарности': 'Merci aussi à',
|
||||
'bbport — порт, на котором построен весь этот проект': 'bbport, le port sur lequel repose tout ce projet',
|
||||
'bbport — порт, на котором построен весь этот проект, а также указатель мыши в меню и раскладка клавиатуры из 0.6': 'bbport, le port sur lequel repose tout ce projet, ainsi que le pointeur dans les menus et la disposition du clavier de la 0.6',
|
||||
'Игра по сети, DLSS, страница читов, добавление в Steam, значок, пропуск конвейеров, которые драйвер не может создать, игра без вылета при нехватке видеопамяти и пропуск отрисовок с пустой таблицей вершин': 'Jeu en ligne, DLSS, la page des triches, Ajouter à Steam, l’icône, ignorer les pipelines que le pilote ne peut pas créer, plus de plantage quand la mémoire vidéo manque et ignorer les tracés à la table de sommets vide',
|
||||
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'A rassemblé et adapté des dizaines de correctifs : plantages au chargement, images noires, l’upscaler, la ligne de performances du journal',
|
||||
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'L’aperçu noir du personnage, l’image de l’écran de chargement et la mémoire du cache de textures',
|
||||
@@ -4552,6 +4600,8 @@ FR = {
|
||||
'FSR 3.1 без цветного шума на Ryzen Z1 (ROG Ally, Legion Go)': 'FSR 3.1 sans bruit coloré sur le Ryzen Z1 (ROG Ally, Legion Go)',
|
||||
'Вылет на Steam Deck после путешествия через фонарь и проверка исправления': 'Un plantage sur Steam Deck après un voyage par une lanterne, et le test du correctif',
|
||||
'Камера мышью, поворачивавшаяся по горизонтали медленнее, чем по вертикали': "La caméra à la souris qui tournait moins vite à l'horizontale qu'à la verticale",
|
||||
'Просьба об указателе мыши в меню игры': 'La demande du pointeur de la souris dans les menus du jeu',
|
||||
'Просьба о раскладке клавиатуры из bbport 0.6': 'La demande de la disposition du clavier de bbport 0.6',
|
||||
}
|
||||
|
||||
|
||||
@@ -4842,6 +4892,14 @@ DE = {
|
||||
"Камера влево": "Kamera nach links",
|
||||
"Камера вправо": "Kamera nach rechts",
|
||||
"Шаг (удерживать с движением)": "Gehen (beim Bewegen halten)",
|
||||
"Бег (удерживать: Круг)": "Sprinten (halten: Kreis)",
|
||||
"Уклонение (короткое нажатие Круга; с бегом — прыжок)": "Ausweichen (kurzer Kreis-Druck; beim Sprinten ein Sprung)",
|
||||
"Раскладка клавиатуры": "Tastaturbelegung",
|
||||
"Назначенные вами клавиши сохраняются": "Von dir zugewiesene Tasten bleiben erhalten",
|
||||
"Этого порта": "Die dieses Ports",
|
||||
"bbport 0.6 (как в Dark Souls III на ПК)": "bbport 0.6 (wie Dark Souls III auf dem PC)",
|
||||
"Пробел — Крест, левый Shift — Круг": "Leertaste Kreuz, linke Umschalttaste Kreis",
|
||||
"Пробел — уклонение, Shift — бег, Shift + Пробел — прыжок, F — действие, Escape — меню и назад в меню игры": "Leertaste ausweichen, Umschalt sprinten, Umschalt + Leertaste springen, F interagieren, Esc Menü und zurück in den Menüs des Spiels",
|
||||
"Контроллер": "Controller",
|
||||
"Выбранный берётся, как только подключится": "Der ausgewählte Controller wird verwendet, sobald er verbunden ist",
|
||||
"Тот, которым играете": "Der, mit dem du spielst",
|
||||
@@ -5334,7 +5392,7 @@ DE = {
|
||||
'Порт и его код': 'Der Port und sein Code',
|
||||
'Помощь и тесты': 'Hilfe und Tests',
|
||||
'Другие благодарности': 'Dank auch an',
|
||||
'bbport — порт, на котором построен весь этот проект': 'bbport, der Port, auf dem dieses ganze Projekt aufbaut',
|
||||
'bbport — порт, на котором построен весь этот проект, а также указатель мыши в меню и раскладка клавиатуры из 0.6': 'bbport, der Port, auf dem dieses ganze Projekt aufbaut, sowie der Mauszeiger in den Menüs und die Tastaturbelegung aus 0.6',
|
||||
'Игра по сети, DLSS, страница читов, добавление в Steam, значок, пропуск конвейеров, которые драйвер не может создать, игра без вылета при нехватке видеопамяти и пропуск отрисовок с пустой таблицей вершин': 'Online-Spiel, DLSS, die Cheat-Seite, Zu Steam hinzufügen, das Symbol, das Überspringen von Pipelines, die der Treiber nicht erstellen kann, kein Absturz bei vollem Videospeicher und das Überspringen von Zeichnungen mit leerer Vertex-Tabelle',
|
||||
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': 'Hat Dutzende Korrekturen gesammelt und angepasst: Abstürze beim Laden, schwarze Bilder, der Upscaler, die Leistungszeile im Log',
|
||||
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': 'Die schwarze Charaktervorschau, das Bild des Ladebildschirms und der Speicher des Texturcaches',
|
||||
@@ -5439,6 +5497,8 @@ DE = {
|
||||
'FSR 3.1 без цветного шума на Ryzen Z1 (ROG Ally, Legion Go)': 'FSR 3.1 ohne Farbrauschen auf dem Ryzen Z1 (ROG Ally, Legion Go)',
|
||||
'Вылет на Steam Deck после путешествия через фонарь и проверка исправления': 'Ein Absturz auf dem Steam Deck nach einer Laternenreise und der Test der Korrektur',
|
||||
'Камера мышью, поворачивавшаяся по горизонтали медленнее, чем по вертикали': 'Die Mauskamera, die sich seitlich langsamer drehte als nach oben und unten',
|
||||
'Просьба об указателе мыши в меню игры': 'Der Wunsch nach dem Mauszeiger in den Menüs des Spiels',
|
||||
'Просьба о раскладке клавиатуры из bbport 0.6': 'Der Wunsch nach der Tastaturbelegung von bbport 0.6',
|
||||
}
|
||||
|
||||
|
||||
@@ -5752,6 +5812,14 @@ ZH_CN = {
|
||||
"Камера влево": "镜头向左",
|
||||
"Камера вправо": "镜头向右",
|
||||
"Шаг (удерживать с движением)": "步行(移动时按住)",
|
||||
"Бег (удерживать: Круг)": "奔跑(按住:圆圈)",
|
||||
"Уклонение (короткое нажатие Круга; с бегом — прыжок)": "闪避(短按圆圈;奔跑时为跳跃)",
|
||||
"Раскладка клавиатуры": "键盘布局",
|
||||
"Назначенные вами клавиши сохраняются": "你指定的按键会保留",
|
||||
"Этого порта": "本移植版的",
|
||||
"bbport 0.6 (как в Dark Souls III на ПК)": "bbport 0.6(与 PC 版黑暗之魂 III 相同)",
|
||||
"Пробел — Крест, левый Shift — Круг": "空格为叉,左 Shift 为圆圈",
|
||||
"Пробел — уклонение, Shift — бег, Shift + Пробел — прыжок, F — действие, Escape — меню и назад в меню игры": "空格闪避,Shift 奔跑,Shift + 空格跳跃,F 互动,Esc 打开菜单并在游戏菜单中返回",
|
||||
"Контроллер": "控制器",
|
||||
"Выбранный берётся, как только подключится": "所选手柄一旦连接即会被使用",
|
||||
"Тот, которым играете": "你正在使用的手柄",
|
||||
@@ -6190,7 +6258,7 @@ ZH_CN = {
|
||||
'Порт и его код': '移植及其代码',
|
||||
'Помощь и тесты': '帮助与测试',
|
||||
'Другие благодарности': '同样感谢',
|
||||
'bbport — порт, на котором построен весь этот проект': 'bbport,整个项目所基于的移植',
|
||||
'bbport — порт, на котором построен весь этот проект, а также указатель мыши в меню и раскладка клавиатуры из 0.6': 'bbport,整个项目所基于的移植,以及 0.6 的菜单鼠标指针和键盘布局',
|
||||
'Игра по сети, DLSS, страница читов, добавление в Steam, значок, пропуск конвейеров, которые драйвер не может создать, игра без вылета при нехватке видеопамяти и пропуск отрисовок с пустой таблицей вершин': '联机、DLSS、作弊页面、添加到 Steam、图标、跳过驱动无法创建的管线、显存耗尽时不再崩溃,以及跳过顶点表为空的绘制',
|
||||
'Собрал и адаптировал десятки исправлений: вылеты при загрузке, чёрные изображения, апскейлер, строка производительности в журнале': '收集并改编了数十项修复:加载时崩溃、黑色图像、超分辨率、日志中的性能行',
|
||||
'Чёрный предпросмотр персонажа, картинка экрана загрузки и память кэша текстур': '黑色的角色预览、加载画面图片与纹理缓存的内存',
|
||||
@@ -6295,6 +6363,8 @@ ZH_CN = {
|
||||
'FSR 3.1 без цветного шума на Ryzen Z1 (ROG Ally, Legion Go)': 'Ryzen Z1(ROG Ally、Legion Go)上没有彩色噪点的 FSR 3.1',
|
||||
'Вылет на Steam Deck после путешествия через фонарь и проверка исправления': 'Steam Deck 上提灯传送后的崩溃,以及修复的测试',
|
||||
'Камера мышью, поворачивавшаяся по горизонтали медленнее, чем по вертикали': '鼠标镜头水平转动比垂直转动更慢',
|
||||
'Просьба об указателе мыши в меню игры': '提议在游戏菜单中使用鼠标指针',
|
||||
'Просьба о раскладке клавиатуры из bbport 0.6': '提议加入 bbport 0.6 的键盘布局',
|
||||
}
|
||||
|
||||
# Translations by language code; Russian is the key itself. Strings missing in a translation
|
||||
|
||||
@@ -317,6 +317,7 @@ INI_DEFAULTS = {
|
||||
"mouse_sensitivity": "1.00",
|
||||
"mouse_invert_y": "0",
|
||||
"mouse_camera": "direct",
|
||||
"keyboard_layout": "bbport",
|
||||
"pad_swap_sticks": "0",
|
||||
"pad_invert_look_x": "0",
|
||||
"pad_invert_look_y": "0",
|
||||
@@ -833,6 +834,27 @@ CONTROLS = [
|
||||
("look_left", "Камера влево", "Left", None),
|
||||
("look_right", "Камера вправо", "Right", None),
|
||||
("walk", "Шаг (удерживать с движением)", "Left Alt", None),
|
||||
("sprint", "Бег (удерживать: Круг)", "", None),
|
||||
("dodge", "Уклонение (короткое нажатие Круга; с бегом — прыжок)", "", None),
|
||||
]
|
||||
# keyboard_layout=0.6 (runtime_pad.c bind_defaults): bbport 0.6's keys, Dark Souls III's on PC
|
||||
# (issue #33). Circle has no key of its own there: sprint and dodge make it, Escape goes back in
|
||||
# the game's menus.
|
||||
KEY_DEFAULTS_06 = {
|
||||
"cross": "F, Return", "circle": "", "square": "Tab, R", "triangle": "Q",
|
||||
"l1": "E, Mouse X2", "r1": "Mouse Left", "l2": "Mouse Right",
|
||||
"r2": "Left Shift+Mouse Left, Mouse X1", "l3": "Left Ctrl", "r3": "C, Mouse Middle",
|
||||
"options": "Escape", "touchpad": "G", "touchpad_right": "Backspace",
|
||||
"up": "3, Up, Wheel Up", "down": "4, Down, Wheel Down", "left": "1, Left", "right": "2, Right",
|
||||
"move_up": "W", "move_down": "S", "move_left": "A", "move_right": "D",
|
||||
"look_up": "I", "look_down": "K", "look_left": "J", "look_right": "L",
|
||||
"walk": "Left Alt", "sprint": "Left Shift", "dodge": "Space",
|
||||
}
|
||||
KEYBOARD_LAYOUTS = [
|
||||
("Этого порта", "bbport", "Пробел — Крест, левый Shift — Круг"),
|
||||
("bbport 0.6 (как в Dark Souls III на ПК)", "0.6",
|
||||
"Пробел — уклонение, Shift — бег, Shift + Пробел — прыжок, F — действие, Escape — меню "
|
||||
"и назад в меню игры"),
|
||||
]
|
||||
|
||||
|
||||
@@ -933,7 +955,8 @@ def themed_icon(name):
|
||||
|
||||
THANKS = (
|
||||
("Порт и его код", (
|
||||
_github("deadinside28", "bbport — порт, на котором построен весь этот проект"),
|
||||
_github("deadinside28", "bbport — порт, на котором построен весь этот проект, а также указатель "
|
||||
"мыши в меню и раскладка клавиатуры из 0.6"),
|
||||
_github("Supermedo", "Игра по сети, DLSS, страница читов, добавление в Steam, значок, "
|
||||
"пропуск конвейеров, которые драйвер не может создать, игра без "
|
||||
"вылета при нехватке видеопамяти и пропуск отрисовок с пустой "
|
||||
@@ -1021,6 +1044,8 @@ THANKS = (
|
||||
_github("BlazinKarp", "Оверлей Steam в игре"),
|
||||
_github("molnar-a", "Вылет на Steam Deck после путешествия через фонарь и проверка исправления"),
|
||||
_github("gibs2193", "Камера мышью, поворачивавшаяся по горизонтали медленнее, чем по вертикали"),
|
||||
_github("zFenrys", "Просьба об указателе мыши в меню игры"),
|
||||
_github("Aman9das", "Просьба о раскладке клавиатуры из bbport 0.6"),
|
||||
)),
|
||||
("Другие благодарности", (
|
||||
("Dommo, goomab", "Иконки кнопок Xbox и Switch (Nexus Mods)",
|
||||
@@ -1494,6 +1519,12 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
self.lightbar_row.add_suffix(self.lightbar_switch)
|
||||
self.lightbar_row.set_activatable_widget(self.lightbar_switch)
|
||||
controls.add(self.lightbar_row)
|
||||
# The keyboard's layout (runtime_pad.c keyboard_layout): its keys are the defaults below.
|
||||
self.keyboard_layout_row = combo_row(tr("Раскладка клавиатуры"),
|
||||
tr("Назначенные вами клавиши сохраняются"),
|
||||
KEYBOARD_LAYOUTS,
|
||||
self.ini.get("keyboard_layout", "bbport"))
|
||||
keys.add(self.keyboard_layout_row)
|
||||
# Bindings: "Assign" waits for a key or button (bb-gpu-capabilities --read-input).
|
||||
self.control_rows = {}
|
||||
for kind, title, icon in (("key", tr("Клавиатура и мышь"), "input-keyboard-symbolic"),
|
||||
@@ -1514,6 +1545,7 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
self.control_rows[(kind, name)] = (row, default)
|
||||
self.show_control(kind, name)
|
||||
(keys if kind == "key" else controls).add(expander)
|
||||
self.keyboard_layout_row.connect("notify::selected", lambda *_: self.on_keyboard_layout())
|
||||
# Mouse look (runtime_pad.c): a click in the game takes the mouse, F1 lets it go.
|
||||
self.mouse_look_row = Adw.SwitchRow(
|
||||
title=tr("Камера мышью"),
|
||||
@@ -2197,8 +2229,16 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
self.store()
|
||||
self.choose_folder(tr("Папка игры (с eboot.bin)"), self.game_dir(), chosen)
|
||||
|
||||
def on_keyboard_layout(self):
|
||||
for name, _label, key_default, _pad in CONTROLS:
|
||||
if ("key", name) in self.control_rows:
|
||||
self.show_control("key", name)
|
||||
self.store()
|
||||
|
||||
def show_control(self, kind, name):
|
||||
row, default = self.control_rows[(kind, name)]
|
||||
if kind == "key" and combo_value(self.keyboard_layout_row) == "0.6":
|
||||
default = KEY_DEFAULTS_06.get(name, default)
|
||||
value = self.ini.get(f"{kind}.{name}")
|
||||
if value is None:
|
||||
row.set_subtitle(tr("{} (по умолчанию)").format(default) if default else tr("не назначено"))
|
||||
@@ -2378,6 +2418,7 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
"mouse_sensitivity": f"{self.mouse_sensitivity_row.get_value():.2f}",
|
||||
"mouse_invert_y": "1" if self.mouse_invert_row.get_active() else "0",
|
||||
"mouse_camera": "direct" if self.mouse_camera_row.get_active() else "stick",
|
||||
"keyboard_layout": combo_value(self.keyboard_layout_row),
|
||||
**{key: "1" if row.get_active() else "0" for key, row in self.stick_rows.items()},
|
||||
**{key: "1" if row.get_active() else "0" for key, row in self.effect_rows.items()},
|
||||
**{key: "1" if row.get_active() else "0" for key, row in self.cheat_rows.items()},
|
||||
|
||||
@@ -13,7 +13,7 @@ import re
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
VERSION = "0.51-linux.18"
|
||||
VERSION = "0.51-linux.19"
|
||||
|
||||
|
||||
def _build():
|
||||
|
||||
+21
-2
@@ -54,6 +54,15 @@ DEFAULT_KEYS = {
|
||||
'right': 'L', 'move_up': 'W', 'move_down': 'S', 'move_left': 'A', 'move_right': 'D',
|
||||
'look_up': 'Up', 'look_down': 'Down', 'look_left': 'Left', 'look_right': 'Right',
|
||||
}
|
||||
# keyboard_layout=0.6's (bbport 0.6's keys; the launcher's KEY_DEFAULTS_06). Circle has no key of
|
||||
# its own there: the way back in a game menu is Escape (Options' key), drawn for it.
|
||||
DEFAULT_KEYS_06 = {
|
||||
'cross': 'F', 'circle': '', 'square': 'Tab', 'triangle': 'Q', 'l1': 'E', 'r1': 'Mouse Left',
|
||||
'r2': 'Left Shift+Mouse Left', 'l2': 'Mouse Right', 'l3': 'Left Ctrl', 'r3': 'C',
|
||||
'options': 'Escape', 'touchpad': 'G', 'touchpad_right': 'Backspace', 'up': '3', 'down': '4',
|
||||
'left': '1', 'right': '2', 'move_up': 'W', 'move_down': 'S', 'move_left': 'A',
|
||||
'move_right': 'D', 'look_up': 'I', 'look_down': 'K', 'look_left': 'J', 'look_right': 'L',
|
||||
}
|
||||
PROMPTS = ('playstation', 'xbox', 'switch', 'keyboard')
|
||||
# DejaVu Sans Bold: next to the scripts in packaged builds (packaging/appimage-sharun.sh), else
|
||||
# the system's; Pillow's own font otherwise (it lacks the arrows).
|
||||
@@ -68,6 +77,8 @@ KEY_LABELS = {
|
||||
'keypad enter': 'Enter', 'backspace': '⌫', 'tab': 'Tab', 'escape': 'Esc', 'up': '↑',
|
||||
'down': '↓', 'left': '←', 'right': '→', 'capslock': 'Caps', 'delete': 'Del',
|
||||
'insert': 'Ins', 'home': 'Home', 'end': 'End', 'pageup': 'PgUp', 'pagedown': 'PgDn',
|
||||
'mouse left': 'LMB', 'mouse right': 'RMB', 'mouse middle': 'MMB', 'mouse x1': 'M4',
|
||||
'mouse x2': 'M5', 'wheel up': 'Wh↑', 'wheel down': 'Wh↓',
|
||||
}
|
||||
|
||||
|
||||
@@ -86,17 +97,20 @@ def prompts_for(pad_type):
|
||||
|
||||
|
||||
def key_bindings(config):
|
||||
"""input -> its first key (SDL name; '' unbound), from bbport.ini over the defaults."""
|
||||
keys = dict(DEFAULT_KEYS)
|
||||
"""input -> its first key (SDL name; '' unbound), from bbport.ini over its layout's defaults."""
|
||||
try:
|
||||
lines = Path(config).read_text().splitlines() if config else []
|
||||
except OSError:
|
||||
lines = []
|
||||
layout_06 = any(line.strip().startswith('keyboard_layout=0.6') for line in lines)
|
||||
keys = dict(DEFAULT_KEYS_06 if layout_06 else DEFAULT_KEYS)
|
||||
for line in lines:
|
||||
name, _, value = line.partition('=')
|
||||
name = name.strip()
|
||||
if name.startswith('key.') and name[4:] in keys:
|
||||
keys[name[4:]] = value.split(',')[0].strip()
|
||||
if layout_06 and not keys['circle']:
|
||||
keys['circle'] = keys['options']
|
||||
return keys
|
||||
|
||||
|
||||
@@ -131,6 +145,11 @@ def swap_atlas_glyphs(strip):
|
||||
def key_label(key):
|
||||
if not key:
|
||||
return '–'
|
||||
# A chord ("Left Shift+Mouse Left"): Shift as its arrow before the key.
|
||||
modifier, plus, rest = key.rpartition('+')
|
||||
if plus and modifier and rest:
|
||||
prefix = '⇧' if 'shift' in modifier.casefold() else key_label(modifier) + '+'
|
||||
return prefix + key_label(rest.strip())
|
||||
label = KEY_LABELS.get(key.casefold())
|
||||
if label:
|
||||
return label
|
||||
|
||||
+136
-11
@@ -9,6 +9,15 @@
|
||||
* Mouse (once the game window is clicked; F1 or leaving the window releases it): look (the
|
||||
* right stick), Left R1, Right L2, Middle R3, side buttons R2 (X1) and L1 (X2), wheel the
|
||||
* d-pad's up/down (quick items).
|
||||
* keyboard_layout=0.6 (bbport.ini, issue #33): bbport 0.6's layout instead, Dark Souls III's on PC:
|
||||
* WASD left stick, IJKL right stick, Space the dodge (a short Circle press: never a run), Left
|
||||
* Shift the run (Circle held), Shift + Space the jump, F and Enter Cross, Q Triangle, E L1, Tab
|
||||
* and R Square, C R3, Left Ctrl L3, Escape Options (Circle, the way back, while a game menu is
|
||||
* open), G left touchpad, Backspace right touchpad, the d-pad on 1 (left), 2 (right), 3 (up),
|
||||
* 4 (down) and the arrow keys. Mouse: Left R1, Shift + Left R2, Right L2, Middle R3, X1 R2, X2
|
||||
* L1, the wheel the d-pad's up/down.
|
||||
* A key binding may be a chord, "Left Shift+Mouse Left": while its modifier is held, the second
|
||||
* key's plain bindings do not count (Shift + Left is R2, not R2 and R1).
|
||||
*
|
||||
* Stick neutral: SDL exposes no way to read a pad's calibration and some clones report a
|
||||
* biased neutral (a Switch-style pad was seen returning both sticks at a constant ~ +/-16380).
|
||||
@@ -415,12 +424,12 @@ enum {
|
||||
IN_CROSS, IN_CIRCLE, IN_SQUARE, IN_TRIANGLE, IN_L1, IN_R1, IN_L2, IN_R2, IN_L3, IN_R3,
|
||||
IN_OPTIONS, IN_TOUCHPAD, IN_TOUCHPAD_RIGHT, IN_UP, IN_DOWN, IN_LEFT, IN_RIGHT,
|
||||
IN_MOVE_UP, IN_MOVE_DOWN, IN_MOVE_LEFT, IN_MOVE_RIGHT, IN_LOOK_UP, IN_LOOK_DOWN, IN_LOOK_LEFT,
|
||||
IN_LOOK_RIGHT, IN_WALK, IN_COUNT
|
||||
IN_LOOK_RIGHT, IN_WALK, IN_SPRINT, IN_DODGE, IN_COUNT
|
||||
};
|
||||
static const char *const input_names[IN_COUNT]={
|
||||
"cross","circle","square","triangle","l1","r1","l2","r2","l3","r3","options","touchpad",
|
||||
"touchpad_right","up","down","left","right","move_up","move_down","move_left","move_right",
|
||||
"look_up","look_down","look_left","look_right","walk",
|
||||
"look_up","look_down","look_left","look_right","walk","sprint","dodge",
|
||||
};
|
||||
static const uint32_t input_buttons[IN_COUNT]={
|
||||
BTN_CROSS,BTN_CIRCLE,BTN_SQUARE,BTN_TRIANGLE,BTN_L1,BTN_R1,BTN_L2,BTN_R2,BTN_L3,BTN_R3,
|
||||
@@ -437,11 +446,15 @@ static const struct { const char *name; int code; } mouse_keys[]={
|
||||
{"Mouse X2",MOUSE_KEY+SDL_BUTTON_X2}, {"Wheel Up",MOUSE_WHEEL_UP}, {"Wheel Down",MOUSE_WHEEL_DOWN},
|
||||
};
|
||||
enum { PAD_LEFT_TRIGGER=SDL_GAMEPAD_BUTTON_COUNT, PAD_RIGHT_TRIGGER }; /* triggers as buttons */
|
||||
typedef struct { int key_count, pad_count; int keys[MAX_BIND]; int pad[MAX_BIND]; } Binding;
|
||||
/* keys[i] needs mods[i] held too when mods[i] is not -1 (a chord: "Left Shift+Mouse Left"). */
|
||||
typedef struct { int key_count, pad_count; int keys[MAX_BIND]; int mods[MAX_BIND]; int pad[MAX_BIND]; } Binding;
|
||||
static Binding bindings[IN_COUNT];
|
||||
static int bindings_ready;
|
||||
/* keyboard_layout=0.6 in bbport.ini: bbport 0.6's keys (the header comment). */
|
||||
static int layout_06;
|
||||
|
||||
static void bind_defaults(void) {
|
||||
enum { NONE=-1, SHIFT=SDL_SCANCODE_LSHIFT };
|
||||
static const struct { int input; int key; } keys[]={
|
||||
{IN_CROSS,SDL_SCANCODE_SPACE}, {IN_CIRCLE,SDL_SCANCODE_LSHIFT}, {IN_SQUARE,SDL_SCANCODE_E},
|
||||
{IN_TRIANGLE,SDL_SCANCODE_Q}, {IN_L1,SDL_SCANCODE_1}, {IN_R1,SDL_SCANCODE_3},
|
||||
@@ -457,6 +470,29 @@ static void bind_defaults(void) {
|
||||
{IN_R3,MOUSE_KEY+SDL_BUTTON_MIDDLE}, {IN_R2,MOUSE_KEY+SDL_BUTTON_X1}, {IN_L1,MOUSE_KEY+SDL_BUTTON_X2},
|
||||
{IN_UP,MOUSE_WHEEL_UP}, {IN_DOWN,MOUSE_WHEEL_DOWN},
|
||||
};
|
||||
/* bbport 0.6's (deadinside28/bloodborne_pc, src/runtime_pad.c). */
|
||||
static const struct { int input; int key; int mod; } keys_06[]={
|
||||
{IN_DODGE,SDL_SCANCODE_SPACE,NONE}, {IN_SPRINT,SHIFT,NONE}, {IN_CROSS,SDL_SCANCODE_F,NONE},
|
||||
{IN_CROSS,SDL_SCANCODE_RETURN,NONE}, {IN_TRIANGLE,SDL_SCANCODE_Q,NONE},
|
||||
{IN_L1,SDL_SCANCODE_E,NONE}, {IN_SQUARE,SDL_SCANCODE_TAB,NONE}, {IN_SQUARE,SDL_SCANCODE_R,NONE},
|
||||
{IN_R3,SDL_SCANCODE_C,NONE}, {IN_L3,SDL_SCANCODE_LCTRL,NONE},
|
||||
{IN_OPTIONS,SDL_SCANCODE_ESCAPE,NONE}, {IN_TOUCHPAD,SDL_SCANCODE_G,NONE},
|
||||
{IN_TOUCHPAD_RIGHT,SDL_SCANCODE_BACKSPACE,NONE},
|
||||
{IN_LEFT,SDL_SCANCODE_1,NONE}, {IN_RIGHT,SDL_SCANCODE_2,NONE}, {IN_UP,SDL_SCANCODE_3,NONE},
|
||||
{IN_DOWN,SDL_SCANCODE_4,NONE},
|
||||
{IN_UP,SDL_SCANCODE_UP,NONE}, {IN_DOWN,SDL_SCANCODE_DOWN,NONE}, {IN_LEFT,SDL_SCANCODE_LEFT,NONE},
|
||||
{IN_RIGHT,SDL_SCANCODE_RIGHT,NONE},
|
||||
{IN_MOVE_UP,SDL_SCANCODE_W,NONE}, {IN_MOVE_DOWN,SDL_SCANCODE_S,NONE},
|
||||
{IN_MOVE_LEFT,SDL_SCANCODE_A,NONE}, {IN_MOVE_RIGHT,SDL_SCANCODE_D,NONE},
|
||||
{IN_LOOK_UP,SDL_SCANCODE_I,NONE}, {IN_LOOK_DOWN,SDL_SCANCODE_K,NONE},
|
||||
{IN_LOOK_LEFT,SDL_SCANCODE_J,NONE}, {IN_LOOK_RIGHT,SDL_SCANCODE_L,NONE},
|
||||
{IN_WALK,SDL_SCANCODE_LALT,NONE}, /* this fork's walk: Left Alt is free in 0.6's layout */
|
||||
{IN_R1,MOUSE_KEY+SDL_BUTTON_LEFT,NONE}, {IN_R2,MOUSE_KEY+SDL_BUTTON_LEFT,SHIFT},
|
||||
{IN_L2,MOUSE_KEY+SDL_BUTTON_RIGHT,NONE},
|
||||
{IN_R3,MOUSE_KEY+SDL_BUTTON_MIDDLE,NONE}, {IN_R2,MOUSE_KEY+SDL_BUTTON_X1,NONE},
|
||||
{IN_L1,MOUSE_KEY+SDL_BUTTON_X2,NONE},
|
||||
{IN_UP,MOUSE_WHEEL_UP,NONE}, {IN_DOWN,MOUSE_WHEEL_DOWN,NONE},
|
||||
};
|
||||
static const struct { int input, button; } pads[]={
|
||||
{IN_CROSS,SDL_GAMEPAD_BUTTON_SOUTH}, {IN_CIRCLE,SDL_GAMEPAD_BUTTON_EAST},
|
||||
{IN_SQUARE,SDL_GAMEPAD_BUTTON_WEST}, {IN_TRIANGLE,SDL_GAMEPAD_BUTTON_NORTH},
|
||||
@@ -469,8 +505,16 @@ static void bind_defaults(void) {
|
||||
{IN_LEFT,SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {IN_RIGHT,SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
};
|
||||
memset(bindings,0,sizeof bindings);
|
||||
for (size_t i=0;i<sizeof(keys)/sizeof(*keys);++i) {
|
||||
Binding *b=&bindings[keys[i].input]; b->keys[b->key_count++]=keys[i].key;
|
||||
if (layout_06) {
|
||||
for (size_t i=0;i<sizeof(keys_06)/sizeof(*keys_06);++i) {
|
||||
Binding *b=&bindings[keys_06[i].input];
|
||||
b->mods[b->key_count]=keys_06[i].mod; b->keys[b->key_count++]=keys_06[i].key;
|
||||
}
|
||||
} else {
|
||||
for (size_t i=0;i<sizeof(keys)/sizeof(*keys);++i) {
|
||||
Binding *b=&bindings[keys[i].input];
|
||||
b->mods[b->key_count]=NONE; b->keys[b->key_count++]=keys[i].key;
|
||||
}
|
||||
}
|
||||
for (size_t i=0;i<sizeof(pads)/sizeof(*pads);++i) {
|
||||
Binding *b=&bindings[pads[i].input]; b->pad[b->pad_count++]=pads[i].button;
|
||||
@@ -512,6 +556,16 @@ static int pad_swap_sticks, pad_invert_look_x, pad_invert_look_y;
|
||||
static int pad_camera_diagonal;
|
||||
/* key.<input>= / pad.<input>= lines of the settings file. */
|
||||
static void load_bindings(void) {
|
||||
/* The layout first: the key.<input> lines replace its defaults. */
|
||||
layout_06=0;
|
||||
const char *config=getenv("BB_CONFIG");
|
||||
FILE *lf=config ? fopen(config,"r") : NULL;
|
||||
if (lf) {
|
||||
char text[512];
|
||||
while (fgets(text,sizeof text,lf))
|
||||
if (!strncmp(text,"keyboard_layout=",16)) layout_06=!strncmp(text+16,"0.6",3);
|
||||
fclose(lf);
|
||||
}
|
||||
bind_defaults();
|
||||
mouse_look=1; mouse_invert_y=0; mouse_sensitivity=1.0f; mouse_camera_direct=1;
|
||||
stick_socd_last=1; stick_circular=0; pad_round_gate=1;
|
||||
@@ -558,9 +612,18 @@ static void load_bindings(void) {
|
||||
for (char *end=name+strlen(name); end>name && end[-1]==' ';) *--end=0;
|
||||
if (!*name) continue;
|
||||
if (keyboard) {
|
||||
/* A chord: "<modifier>+<key>" ("Keypad +" is one key: nothing after its '+'). */
|
||||
int mod=-1;
|
||||
char *plus=strrchr(name,'+');
|
||||
if (plus && plus>name && plus[1]) {
|
||||
*plus=0;
|
||||
mod=key_from_name(name);
|
||||
if (mod<0) *plus='+'; else name=plus+1;
|
||||
while (*name==' ') ++name;
|
||||
}
|
||||
const int s=key_from_name(name);
|
||||
if (s<0) printf("Runtime: controls: unknown key \"%s\" for %s\n",name,line+4);
|
||||
else if (b->key_count<MAX_BIND) b->keys[b->key_count++]=s;
|
||||
else if (b->key_count<MAX_BIND) { b->mods[b->key_count]=mod; b->keys[b->key_count++]=s; }
|
||||
} else {
|
||||
const int button=pad_button_from_name(name);
|
||||
if (button<0) printf("Runtime: controls: unknown gamepad button \"%s\" for %s\n",name,line+4);
|
||||
@@ -590,10 +653,23 @@ static int mouse_key_down(int code) {
|
||||
if (code==MOUSE_WHEEL_DOWN) return wheel_phase[1]==1;
|
||||
return (mouse_buttons & SDL_BUTTON_MASK(code-MOUSE_KEY))!=0;
|
||||
}
|
||||
static int code_down(const bool *k, int code) {
|
||||
return code>=MOUSE_KEY ? mouse_key_down(code) : k && k[code];
|
||||
}
|
||||
/* Whether a chord on `code` (any input's) has its modifier held: the plain bindings of the key
|
||||
* then wait (Shift + Left is R2 alone). */
|
||||
static int chord_held(const bool *k, int code) {
|
||||
for (int input=0;input<IN_COUNT;++input)
|
||||
for (int i=0;i<bindings[input].key_count;++i)
|
||||
if (bindings[input].keys[i]==code && bindings[input].mods[i]>=0 &&
|
||||
code_down(k,bindings[input].mods[i])) return 1;
|
||||
return 0;
|
||||
}
|
||||
static int key_down(const bool *k, int input) {
|
||||
for (int i=0;i<bindings[input].key_count;++i) {
|
||||
const int code=bindings[input].keys[i];
|
||||
if (code>=MOUSE_KEY ? mouse_key_down(code) : k && k[code]) return 1;
|
||||
const int code=bindings[input].keys[i], mod=bindings[input].mods[i];
|
||||
if (!code_down(k,code)) continue;
|
||||
if (mod>=0 ? code_down(k,mod) : !chord_held(k,code)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -710,9 +786,55 @@ static int key_move(int move_x, int move_y, uint64_t now, int *out_x, int *out_y
|
||||
last_y=*out_y=(int)(y<-127 ? -127 : y>127 ? 127 : y);
|
||||
return 1;
|
||||
}
|
||||
/* Circle from the sprint and dodge keys (bound in keyboard_layout=0.6). Dodge is a short press
|
||||
* whatever the key does: held, it would be the run (the game tells a tap from a hold by the
|
||||
* time). Sprint holds Circle: the run. Dodge while sprint is held is the jump: Circle let go,
|
||||
* pressed, let go, then held again (the run goes on), as Space twice while running; one press of
|
||||
* it only stopped the run. BB_JUMP_GAP_MS (70) is each pause, BB_JUMP_TAP_MS (70) the press
|
||||
* between them. While a game menu is open they do nothing. From bbport 0.6. */
|
||||
#define DODGE_PULSE_US 90000u
|
||||
static uint64_t dodge_pulse_until, jump_start_us;
|
||||
static uint32_t jump_gap_us, jump_tap_us;
|
||||
static int dodge_was_down;
|
||||
static uint32_t env_ms_us(const char *name, uint32_t fallback_ms) {
|
||||
const char *env=getenv(name);
|
||||
const long v=env && *env ? atol(env) : -1;
|
||||
return (uint32_t)(v>=0 && v<=1000 ? v : (long)fallback_ms)*1000u;
|
||||
}
|
||||
static int keyboard_circle(const bool *k, uint64_t now, int menu) {
|
||||
if (!jump_gap_us) {
|
||||
jump_gap_us=env_ms_us("BB_JUMP_GAP_MS",70);
|
||||
jump_tap_us=env_ms_us("BB_JUMP_TAP_MS",70);
|
||||
}
|
||||
const int dodge=key_down(k,IN_DODGE), sprint=key_down(k,IN_SPRINT);
|
||||
if (menu) { dodge_was_down=dodge; dodge_pulse_until=jump_start_us=0; return 0; }
|
||||
const int jumping=jump_start_us && now<jump_start_us+2u*jump_gap_us+jump_tap_us;
|
||||
if (dodge && !dodge_was_down && !jumping) {
|
||||
if (sprint) { jump_start_us=now; dodge_pulse_until=0; }
|
||||
else { dodge_pulse_until=now+DODGE_PULSE_US; jump_start_us=0; }
|
||||
}
|
||||
dodge_was_down=dodge;
|
||||
if (jump_start_us && now<jump_start_us+2u*jump_gap_us+jump_tap_us) {
|
||||
const uint64_t t=now-jump_start_us;
|
||||
return t>=jump_gap_us && t<jump_gap_us+jump_tap_us; /* low, the press, low */
|
||||
}
|
||||
return sprint || now<dodge_pulse_until;
|
||||
}
|
||||
static void apply_keyboard(PadData *d, const bool *k) {
|
||||
for (int i=IN_CROSS;i<=IN_RIGHT;++i)
|
||||
if (i!=IN_TOUCHPAD && i!=IN_TOUCHPAD_RIGHT && key_down(k,i)) d->buttons|=input_buttons[i];
|
||||
const uint64_t now=now_us();
|
||||
const int menu=bbgpu_pointer_wanted()!=0;
|
||||
/* keyboard_layout=0.6: the way back in a game menu is Escape (Options' key); it opens the menu
|
||||
* at other times. A press that began outside a menu is Options for as long as it is held: the
|
||||
* menu it opens must not take the same press for its way back. */
|
||||
static int options_latched;
|
||||
if (!key_down(k,IN_OPTIONS)) options_latched=0;
|
||||
else if (!menu) options_latched=1;
|
||||
const int options_back=layout_06 && menu && !options_latched;
|
||||
for (int i=IN_CROSS;i<=IN_RIGHT;++i) {
|
||||
if (i==IN_TOUCHPAD || i==IN_TOUCHPAD_RIGHT || !key_down(k,i)) continue;
|
||||
d->buttons|=(i==IN_OPTIONS && options_back) ? BTN_CIRCLE : input_buttons[i];
|
||||
}
|
||||
if (keyboard_circle(k,now,menu)) d->buttons|=BTN_CIRCLE;
|
||||
if (key_down(k,IN_TOUCHPAD)) touch_click(d,0);
|
||||
if (key_down(k,IN_TOUCHPAD_RIGHT)) touch_click(d,1);
|
||||
if (key_down(k,IN_L2)) d->l2=255;
|
||||
@@ -721,7 +843,7 @@ static void apply_keyboard(PadData *d, const bool *k) {
|
||||
const int up=key_down(k,IN_MOVE_UP), down=key_down(k,IN_MOVE_DOWN);
|
||||
const int move_x=key_direction(0,left,right), move_y=key_direction(1,up,down);
|
||||
int stick_x, stick_y;
|
||||
if (key_move(move_x,move_y,now_us(),&stick_x,&stick_y)) {
|
||||
if (key_move(move_x,move_y,now,&stick_x,&stick_y)) {
|
||||
/* Walk: the keys only ever ran (full deflection). 48 of 127 is under the game's run
|
||||
* threshold, as on a stick pushed part way; a diagonal keeps its shape. From HatsyRei's
|
||||
* walk key in Mrsuss60's port. */
|
||||
@@ -1072,6 +1194,9 @@ static void sample(PadData *d) {
|
||||
if (injected.buttons & BTN_R2) d->r2=255;
|
||||
uint8_t *axes[4]={&d->left_x,&d->left_y,&d->right_x,&d->right_y};
|
||||
for (int i=0;i<4;++i) if (injected.stick[i]>=0) *axes[i]=(uint8_t)injected.stick[i];
|
||||
/* Game modules (a menu pointer's presses). */
|
||||
const uint8_t sticks[4]={d->left_x,d->left_y,d->right_x,d->right_y};
|
||||
bbgpu_filter_pad(&d->buttons,sticks);
|
||||
camera_diagonal(d);
|
||||
touch_ids(d);
|
||||
}
|
||||
|
||||
@@ -127,6 +127,20 @@ class PromptTests(unittest.TestCase):
|
||||
'Right': 'RIGHT'}.get(key, key)
|
||||
self.assertIn(f'{{IN_{name.upper()},SDL_SCANCODE_{scancode}}}', source)
|
||||
|
||||
def test_layout_06_prompts(self):
|
||||
"""keyboard_layout=0.6: its keys, Circle drawn as Escape (the way back), Shift chords."""
|
||||
config = self.root / 'bbport.ini'
|
||||
config.write_text('keyboard_layout=0.6\nkey.square=X\n')
|
||||
keys = menu_prompts.key_bindings(config)
|
||||
self.assertEqual((keys['cross'], keys['circle'], keys['square'], keys['options']),
|
||||
('F', 'Escape', 'X', 'Escape'))
|
||||
self.assertEqual([menu_prompts.key_label(k) for k in (keys['r2'], keys['r1'], 'Keypad +')],
|
||||
['⇧LMB', 'LMB', 'Num+'])
|
||||
launcher = (ROOT / 'launcher' / 'bbport_launcher.py').read_text()
|
||||
for name, key in menu_prompts.DEFAULT_KEYS_06.items():
|
||||
if key:
|
||||
self.assertIn(f'"{name}": "{key}', launcher)
|
||||
|
||||
def test_prompts_follow_the_controller_type(self):
|
||||
cases = {'xbox360': 'xbox', 'xboxone': 'xbox', 'ps4': 'playstation', 'ps5': 'playstation',
|
||||
'switchpro': 'switch', 'joyconpair': 'switch', '': 'xbox', None: 'keyboard'}
|
||||
|
||||
+53
-1
@@ -12,6 +12,9 @@ uint32_t bbgpu_atlas_route_pad(uint32_t buttons, uint8_t lx, uint8_t ly, uint8_t
|
||||
void bbgpu_mouse_take(double *dx, double *dy, int *up, int *down) { *dx=*dy=0; *up=*down=0; }
|
||||
int bbgpu_mouse_captured(void) { return 0; }
|
||||
void bbgpu_mouse_look_enable(int enabled) { (void)enabled; }
|
||||
static int menu_open;
|
||||
int bbgpu_pointer_wanted(void) { return menu_open; }
|
||||
void bbgpu_filter_pad(uint32_t *buttons, const uint8_t *sticks) { (void)buttons; (void)sticks; }
|
||||
static int camera_direct=-1;
|
||||
static float camera_sensitivity;
|
||||
void bbgpu_mouse_camera_configure(int direct, float sensitivity) { camera_direct=direct; camera_sensitivity=sensitivity; }
|
||||
@@ -347,5 +350,54 @@ int main(void) {
|
||||
m.right_x=200; m.right_y=56; camera_diagonal(&m); assert(m.right_x==200 && m.right_y==56); /* 80% */
|
||||
pad_camera_diagonal=0;
|
||||
}
|
||||
puts("PASS: pad ABI, debug camera chord, left/right clicks, SDL touch coordinates, overlay capture, controls, mouse, keyboard movement, walk key, round gate, Back + d-pad touchpad halves, stick options, camera diagonals, pad in use");
|
||||
/* keyboard_layout=0.6 (issue #33): bbport 0.6's keys, Shift chords, dodge and sprint as
|
||||
* Circle, Escape back in a game menu. */
|
||||
{
|
||||
layout_06=1; bind_defaults();
|
||||
memset(wheel_pending,0,sizeof wheel_pending); memset(wheel_phase,0,sizeof wheel_phase);
|
||||
bool k[SDL_SCANCODE_COUNT]={0};
|
||||
PadData m;
|
||||
const uint32_t shift_left=SDL_BUTTON_LMASK;
|
||||
#define KB_READ() do { memset(&m,0,sizeof m); m.left_x=m.left_y=m.right_x=m.right_y=128; \
|
||||
m.timestamp=now_us(); apply_keyboard(&m,k); } while (0)
|
||||
k[SDL_SCANCODE_F]=true; KB_READ(); assert(m.buttons==BTN_CROSS); k[SDL_SCANCODE_F]=false;
|
||||
k[SDL_SCANCODE_E]=true; KB_READ(); assert(m.buttons==BTN_L1); k[SDL_SCANCODE_E]=false;
|
||||
k[SDL_SCANCODE_I]=true; KB_READ(); assert(m.right_y<128 && m.buttons==0); k[SDL_SCANCODE_I]=false;
|
||||
k[SDL_SCANCODE_3]=true; KB_READ(); assert(m.buttons==BTN_UP); k[SDL_SCANCODE_3]=false;
|
||||
/* Mouse Left is R1; with Shift held it is R2 alone (the chord), and Shift is the run. */
|
||||
mouse_buttons=shift_left; KB_READ(); assert(m.buttons==BTN_R1);
|
||||
k[SDL_SCANCODE_LSHIFT]=true; KB_READ();
|
||||
assert((m.buttons & (BTN_R1|BTN_R2))==BTN_R2 && m.r2==255 && (m.buttons & BTN_CIRCLE));
|
||||
mouse_buttons=0; KB_READ(); assert(m.buttons==BTN_CIRCLE); /* sprint: held */
|
||||
k[SDL_SCANCODE_LSHIFT]=false; KB_READ(); assert(m.buttons==0);
|
||||
/* Dodge: a short Circle press however long Space is held. */
|
||||
k[SDL_SCANCODE_SPACE]=true; KB_READ(); assert(m.buttons==BTN_CIRCLE);
|
||||
usleep(150000); KB_READ(); assert(m.buttons==0);
|
||||
k[SDL_SCANCODE_SPACE]=false; KB_READ(); assert(m.buttons==0);
|
||||
/* The jump, Space while sprinting: Circle let go, pressed, let go, held again. */
|
||||
k[SDL_SCANCODE_LSHIFT]=true; KB_READ(); assert(m.buttons==BTN_CIRCLE);
|
||||
k[SDL_SCANCODE_SPACE]=true; KB_READ(); assert(m.buttons==0);
|
||||
usleep(100000); KB_READ(); assert(m.buttons==BTN_CIRCLE); /* 70..140 ms */
|
||||
usleep(60000); KB_READ(); assert(m.buttons==0); /* 140..210 ms */
|
||||
usleep(100000); KB_READ(); assert(m.buttons==BTN_CIRCLE); /* the run again */
|
||||
k[SDL_SCANCODE_SPACE]=k[SDL_SCANCODE_LSHIFT]=false; KB_READ(); assert(m.buttons==0);
|
||||
/* Escape: Options, and Circle (back) while a game menu is open; a press that opened the
|
||||
* menu stays Options until it is let go. Space and Shift do nothing in a menu. */
|
||||
k[SDL_SCANCODE_ESCAPE]=true; KB_READ(); assert(m.buttons==BTN_OPTIONS);
|
||||
menu_open=1; KB_READ(); assert(m.buttons==BTN_OPTIONS);
|
||||
k[SDL_SCANCODE_ESCAPE]=false; KB_READ(); assert(m.buttons==0);
|
||||
k[SDL_SCANCODE_ESCAPE]=true; KB_READ(); assert(m.buttons==BTN_CIRCLE);
|
||||
k[SDL_SCANCODE_ESCAPE]=false;
|
||||
k[SDL_SCANCODE_SPACE]=k[SDL_SCANCODE_LSHIFT]=true; KB_READ(); assert(m.buttons==0);
|
||||
k[SDL_SCANCODE_SPACE]=k[SDL_SCANCODE_LSHIFT]=false; menu_open=0;
|
||||
/* This fork's layout: Return is Options in a menu too, Space is Cross. */
|
||||
layout_06=0; bind_defaults();
|
||||
menu_open=1; k[SDL_SCANCODE_RETURN]=true; KB_READ(); assert(m.buttons==BTN_OPTIONS);
|
||||
k[SDL_SCANCODE_RETURN]=false; menu_open=0;
|
||||
k[SDL_SCANCODE_SPACE]=true; KB_READ(); assert(m.buttons==BTN_CROSS); k[SDL_SCANCODE_SPACE]=false;
|
||||
#undef KB_READ
|
||||
uint64_t later=now_us()+200000; int x, y; key_move(0,0,later,&x,&y);
|
||||
load_bindings();
|
||||
}
|
||||
puts("PASS: pad ABI, debug camera chord, left/right clicks, SDL touch coordinates, overlay capture, controls, mouse, keyboard movement, walk key, round gate, Back + d-pad touchpad halves, stick options, camera diagonals, pad in use, bbport 0.6's keyboard layout");
|
||||
}
|
||||
@@ -15,6 +15,8 @@ uint32_t bbgpu_atlas_route_pad(uint32_t buttons, uint8_t lx, uint8_t ly, uint8_t
|
||||
void bbgpu_mouse_take(double *dx, double *dy, int *up, int *down) { *dx=*dy=0; *up=*down=0; }
|
||||
int bbgpu_mouse_captured(void) { return 0; }
|
||||
void bbgpu_mouse_look_enable(int enabled) { (void)enabled; }
|
||||
void bbgpu_filter_pad(uint32_t *buttons, const uint8_t *sticks) { (void)buttons; (void)sticks; }
|
||||
int bbgpu_pointer_wanted(void) { return 0; }
|
||||
void bbgpu_mouse_camera_configure(int direct, float sensitivity) { (void)direct; (void)sensitivity; }
|
||||
uintptr_t runtime_lookup(const RuntimeExport *table, size_t count, const char *name) {
|
||||
(void)table; (void)count; (void)name;
|
||||
|
||||
Reference in new issue
Block a user