Compare commits

..
12 Commits
Author SHA1 Message Date
ayozetr 98750d0687 AppImage: give appimagetool the version
appimagetool writes the desktop entry's X-AppImage-Version itself, from
the VERSION environment variable, and replaced the one the build had
added with "UNKNOWN" (#49). The build now exports VERSION instead.
2026-11-08 01:21:34 +00:00
ayozetr 0b3dac11d7 Launcher: version 0.70-linux.3 2026-11-08 01:15:44 +00:00
ayozetr 5041586f9a AppImage: version and description for AppImage managers
Gear Lever showed the AppImage's version as "UNKNOWN" (#49): its desktop
entry had no X-AppImage-Version and there was no AppStream metainfo.
The build now writes both: the version is taken from the launcher's
VERSION (bbport_update.py), so it is kept in one place, and the
metainfo carries the name, summary, description, licence, links and
that version. Both pass desktop-file-validate and appstreamcli validate.
2026-11-08 01:15:38 +00:00
ayozetr e77bce746f Menu pointer: clicks reach the quick menu while the Atlas tile shows
The pointer left a click alone while the port's overlay was open, and
that check was also true whenever Hunter's Atlas's Map tile showed in
the game's quick menu. Clicks on Status, System, Inventory and the rest
did nothing there; only the tile's Map opened.

The pointer now steps aside only when the overlay or the open map takes
the input, and leaves a click on the tile, with its release, to the
tile. With BB_MENU_POINTER_FILE set, each click is traced: whether the
pointer took it, why not, and the button it pressed.
2026-11-08 01:15:30 +00:00
ayozetr d1dea1b8e4 Menu pointer: the right mouse button is L2 in the game's menus
In a game menu the right button went back, and the mouse's other
buttons did not reach the game at all, so with keyboard and mouse
nothing could press L2 or R2 there. The storage box switches to the
storage with them (#44), and its prompt shows the right button for it:
pressing it closed the box instead.

Only the left button is the pointer's now. The others keep their
bindings in menus as well (the right one L2, the side ones R2 and L1),
as the game's prompts show them; Escape goes back. The test file's
"right" action is "back".
2026-11-08 01:15:30 +00:00
ayozetr 6796fbbd13 Window: ask for fullscreen again when Wayland drops it at start
On Wayland a program cannot place its window. With several monitors
KWin could map the game on the one SDL reports as primary, move it to
the monitor chosen in the launcher and leave it there as a plain
window (#39); asked again, fullscreen holds, as KDE's own shortcut
showed. During the first 10 seconds the port now asks again, at most
three times and a second apart, and logs each try. After that the
desktop decides, so its own shortcuts keep working.

TROUBLESHOOTING.md: choosing the monitor, the retry and
SDL_VIDEO_DRIVER=x11 as the fallback.
2026-11-07 12:57:45 +00:00
ayozetr badd460277 Texture cache: name the images when an overlap resolve fails
The warning said only "Image overlap resolve failed". It now gives the
image the game asked for and the one dropped for it (address, size,
dimensions, format, levels and layers), and whether the dropped image
held what the GPU drew: then its contents are lost and the new image is
read from guest memory. Issue #25 had two of these warnings around a
garbled texture that went away after a reload.
2026-11-07 11:56:21 +00:00
ayozetr 1d0bbec978 Launcher: version 0.70-linux.2 2026-11-04 11:11:38 +00:00
ayozetrandMark Hall 843424de13 Guest memory: allow device-local host types on single-pool GPUs
On integrated Intel Arc/Xe with Mesa's ANV driver every memory type is
device-local and host-pointer imports are allowed only into those, so no
type was found and the game stopped at startup with exit code 23. A second
pass now accepts a device-local host-visible type when no system-memory
type is allowed; the search is unchanged on other GPUs.

From deadinside28/bloodborne_pc#429.

Co-Authored-By: Mark Hall <[email protected]>
2026-11-04 11:11:26 +00:00
ayozetr 21e1ce4224 Game menu: Exit to desktop saves first in the world
Exit to desktop closed the window at once, so whatever the game had not
saved yet was lost (#25). In the world the System menu's entry now runs the
game's own Quit Game: its question becomes "Quit the game and return to the
desktop?", the game saves and goes back to the title, and the runtime closes
it there (the intro logos or the title menu, once no save data is mounted).

Answering No leaves the quit armed for two minutes at most; the game's own
Quit Game disarms it. On the title screen the entry still closes the game
at once, as there is nothing to save there.
2026-11-04 11:11:26 +00:00
ayozetr 498a37d24b Runtime: run the game's movbe on processors without MOVBE
The game's code was built for the PS4's CPU and has 987 movbe. AMD's FX
processors of the Piledriver generation have every other instruction the
game uses but not this one, so the game stopped with SIGILL (exit code 132)
at the first one, while reading a file header (#39).

The SIGILL handler now decodes the movbe at the faulting address, does the
load or store with its bytes swapped and resumes after it. On a processor
with MOVBE this never runs. The CPU check no longer refuses to start
without MOVBE and says in the log that the port runs them; the log also
counts them every 10 s (a few thousand a second while loading, about ten a
second while playing).

BB_MOVBE_TRAP=file turns the movbe listed by tools/movbe_sites.py into ud2,
so the emulation can be tested on any processor. tests/test_movbe.c checks
the forms the game uses.
2026-11-04 11:11:26 +00:00
ayozetr e8c7eaaf9c Players' guide: brought up to 0.70
The guide still described 0.51 in a few places:

- FAQ: The Hunter's Dream has no seamless co-op in 0.70 (summons are the
  normal game's), and no bridge relays there; the ports answer now says
  what UPnP, BBSC's relay and Tailscale each cover. New answers on the
  in-game map (preparing it, opening it with the quick menu, M or R3)
  and on FSR 4.1.2 (AMD's installer or amdxcffx64.dll, plus the FSR 4.1.x
  DLL, and the model in the port's menu).
- Troubleshooting: DLSS failures as 0.70 logs them ("DLSS: unavailable
  (...)"), Frame Generation on RTX 40 and newer, without the 0.4-linux.1
  note and a log line that no longer exists.
- Getting a log: the first line as 0.70 writes it.
- Online: the BBSC switch is "Turn on BBSC", as the launcher says.
2026-11-03 21:54:23 +00:00
29 changed files with 757 additions and 56 deletions

No files matched your search

+2
View File
@@ -118,6 +118,8 @@ if [[ ${1:-} == --test ]]; then
out/pad-atlas-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_runtime.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/runtime-test
out/runtime-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -Isrc tests/test_movbe.c src/runtime_movbe.c -o out/movbe-test
out/movbe-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_file_mods.c src/runtime_remote.c -o out/file-mods-test
out/file-mods-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_sema.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/sema-test
+5 -3
View File
@@ -55,7 +55,7 @@ at its start (`Environment:` in the log) and warns about `BB_*` names it does no
`=1: ...` / `=0: ...` / `=N: ...` say what that value does; `set: ...` means any value
turns it on (even `0`).
## Settings (106)
## Settings (107)
What players and the launchers set. The menu, bbport.ini and the launcher write most of them; set one by hand to override them.
@@ -98,6 +98,7 @@ What players and the launchers set. The menu, bbport.ini and the launcher write
| `BB_GAMEPAD` | =SDL GUID or part of the name: the controller to use (the launcher's) |
| `BB_GAME_DIR` | the game's folder (with eboot.bin) |
| `BB_GAME_MENU` | =0: no port pages in the game's System menu |
| `BB_GESTURES_NO_MOTION` | =1: the gestures menu without the motion-control icons (Dommo's mod; the launcher's switch) |
| `BB_GPU_ID` | =N: the GPU to use, by Vulkan index (the launcher's) |
| `BB_GPU_PROFILE` | =1: GPU profiler: time per labelled pass in the log (the launcher's) |
| `BB_HDR` | =1: HDR output allowed |
@@ -155,7 +156,7 @@ What players and the launchers set. The menu, bbport.ini and the launcher write
| `BB_SHADNET_SERVER` | the shadNet server |
| `BB_SHADNET_TOKEN` | the shadNet account token (the launcher's) |
| `BB_SHADNET_WEBAPI` | the shadNet web API server |
| `BB_SKIP_CPU_CHECK` | =1: skip the check of the CPU's bsr/bsf behaviour before the game starts |
| `BB_SKIP_CPU_CHECK` | =1: skip the check of the processor's instructions (AVX, BMI1, LZCNT...) before the game starts |
| `BB_SKIP_GAME_CHECK` | =1: start with game files that did not pass the check |
| `BB_STEAM_INPUT_X11` | =0: keep the native Wayland window when Steam made a virtual controller for the game (default: Xwayland, so Steam Input reaches it) |
| `BB_TEXTURE_QUALITY` | texture quality over bbport.ini's texture_quality |
@@ -410,7 +411,7 @@ Defaults to change on a machine where they misbehave, workarounds, and switches
| `BB_VULKANINFO` | path of vulkaninfo for the launcher |
| `BB_WORK_RETIRED` | =0: the recording thread and fences do not wait for retired work |
## Diagnostics (281)
## Diagnostics (282)
Logs, dumps, traces, probes and experiments, for finding bugs. Most cost frame time; none is meant for playing.
@@ -572,6 +573,7 @@ Logs, dumps, traces, probes and experiments, for finding bugs. Most cost frame t
| `BB_MOD_TRACE` | =1: every file a mod replaces |
| `BB_MOTION_SELECT_LOG` | =1: each G-buffer vertex shader once with the motion vector selection |
| `BB_MOTION_TRACE` | =file: the frame after the file appears prints its motion draws |
| `BB_MOVBE_TRAP` | =file: the movbe at the image offsets listed there (tools/movbe_sites.py) made ud2 and run by the port, as on a processor without MOVBE |
| `BB_MSG_TRACE` | =1: the game's text lookups per 2 s by category (#255) |
| `BB_NGX_LOG` | =1 / 2: NVIDIA NGX log into the port's (2: verbose) |
| `BB_NULL_GPU` | =1: no GPU work at all (tests) |
+1 -1
View File
@@ -67,7 +67,7 @@ items and echoes it gets, and enemies get stronger for every guest. It runs insi
online module (`gpu/bbnet20/bbsc`, BBSC 0.0.2 from bbport 0.7). It is new, and its own author
marks much of it as untested.
1. In the launcher's **Multiplayer → Online** tab, **Enable BBSC** in the **Seamless co-op (BBSC)**
1. In the launcher's **Multiplayer → Online** tab, **Turn on BBSC** in the **Seamless co-op (BBSC)**
group (on by default). While it is on, BBSC's bells replace the game's; turned off, the game's
come back the next time the character is loaded (the save is copied to `save-backups` first).
**BBSC settings**, folded under it, has BBSC's own options: what the guest shares with the host,
+20 -9
View File
@@ -27,17 +27,16 @@ To move a save, close the game, back up both folders, and copy the `savedata` fo
## DLSS is "not supported by this GPU"
The log says why, on a line starting with `DLSS:`.
The log says why, on a line `DLSS: unavailable (...)`; the game then uses another upscaler.
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's own driver; it does not work with
NVK.
- The 0.4-linux.1 AppImage ships a modified copy of NVIDIA's DLSS library, which the driver
refuses (`Feature requirement query failed: 0xbad00012`): update to 0.5-linux.1 or newer.
NVK (`not an NVIDIA GPU`, or `this GPU or driver does not support DLSS`).
- `NVIDIA NGX initialization failed`: update the driver, and check that NVIDIA's CUDA/UVM module
is loaded (`sudo modprobe nvidia_uvm`; the `nvidia-modprobe` package loads it when needed). With
Secure Boot, a module signing key that is not enrolled stops it (`Key was rejected by service`).
- DLSS Frame Generation needs a GeForce RTX 40 or newer; on older cards the log says so and DLSS
itself still works.
- From the repository, DLSS needs `bash tools/build_dlss.sh` once after `build.sh`.
- If the log says `Device creation with DLSS extensions failed`, NVIDIA's CUDA/UVM
module is missing: the game then uses FSR 3.1. Load it with `sudo modprobe nvidia_uvm` (the
`nvidia-modprobe` package loads it when needed); with Secure Boot, a module signing key that
is not enrolled stops it (`Key was rejected by service`).
## Spanish shows the Latin American text (or the other way round)
@@ -60,11 +59,23 @@ and the HUD keeps to the screen's edges; it changes live, in the game's *Display
**Output resolution** *Auto* is the monitor's own; any other size written as `output_res=WxH` in
bbport.ini works too.
## Fullscreen opens on the wrong monitor, or drops back to a window
With several monitors, choose the one to play on in **Display → Monitor**: the port's *Primary*
is the one the system reports first, which on Wayland is not always the one KDE or GNOME show as
primary. On Wayland a program cannot place its own window, so the compositor may open it on
another monitor first and then move it; when that leaves it as a plain window, the port asks for
fullscreen again during the first seconds (the log says
`Display: the compositor left the window out of fullscreen, asked again`). If it still ends up
windowed, add `SDL_VIDEO_DRIVER=x11` under **Advanced → Developer → Extra variables**: through
Xwayland the window places itself.
## Exit to desktop
**Exit to desktop** is in the game's System menu and on the title screen, from bbport 0.7. The
*Quit to Desktop* switch of earlier Linux versions is gone with it, and a third-party *Exit Game*
patch is no longer needed.
patch is no longer needed. In the world it asks first and goes through the game's own Quit Game,
so your progress is saved, and the game closes once it is back at the title.
## The port's menu opens by accident with L3 + R3
+26 -5
View File
@@ -26,16 +26,37 @@ It depends on the server:
- On **Seamless Hunt** (or your own server with BBSC's game server), seamless co-op works fully:
summons across areas and up to 5 hunters.
- On **The Hunter's Dream**, BBSC keeps the session together, but summons are the normal game's:
both hunters must be in the **same area**, the area's boss must still be alive in the host's world,
and your levels must be within range.
- On **The Hunter's Dream** there is no seamless co-op: summons are the normal game's, with the
Beckoning Bell and the Small Resonant Bell. Both hunters must be in the **same area**, the area's
boss must still be alive in the host's world, and your levels must be within range (or use the
same password).
[Seamless co-op on each server](ONLINE.md#seamless-co-op-bbsc).
## Do I need to open ports on my router?
No. With seamless co-op (BBSC) on, Seamless Hunt relays players who cannot reach each other
directly, and The Hunter's Dream's bridge relays by itself. Only [your own server](SERVER.md) needs a few ports, through Tailscale.
Usually not: with **UPnP** on (the default) the game opens its port on the router by itself, and
with seamless co-op (BBSC) on Seamless Hunt relays players who cannot reach each other directly. A
router without UPnP, or a firewall on your computer, needs UDP 3658 opened
([ports](ONLINE.md#ports)). With [your own server](SERVER.md), Tailscale takes care of it.
## How do I use the in-game map?
In the launcher, **Mods → Map (Hunter's Atlas)**: turn on **In-game map** and press **Prepare**
once. It makes the maps from your own game files (it downloads about 200 MB of tools the first
time). In the game, choose **Map** in the quick menu (its fourth tile), press **M**, or with a
controller open the game's menu and click the right stick (R3). The port's menu (Insert, or hold
L3 + R3) changes the key and the controller shortcut.
**Travel to lamps from the map** and **Items on the map** are in the same launcher group.
## How do I get FSR 4.1.2 (INT8, for RDNA2 and the Steam Deck)?
FSR 4.1.2's models are built from AMD's own files, which can't be shipped here. In
**Graphics → Upscaler**, on **FSR 4.1.2 from AMD's driver**, press **Choose a file…** and pick
AMD's Adrenalin 26.10.1 or newer installer (the `.exe`) or the `amdxcffx64.dll` inside it. It also
needs AMD's FSR 4.1.x upscaler DLL (`amd_fidelityfx_upscaler_dx12.dll`, from OptiScaler or a game
with FSR 4.1): if you built FSR 4.1.1 before, it uses that one, otherwise it asks for it. It takes
about 4 minutes. In the game, the port's menu chooses the model (INT8 RDNA2 or INT8 Lite).
## How do I get a log on the Steam Deck?
+1 -1
View File
@@ -13,7 +13,7 @@ This also works when the game is started from Steam or from the Steam Deck's Gam
option on once in the launcher, and every session saves its log.
The log's first line says which version ran, and for a test build which one
(`bbport Linux 0.51-linux.18, build 626ec908 fix/issue1-lamp-blur 2026-10-29`).
(`bbport Linux 0.70-linux.1, build 74b1a308 HEAD 2026-11-03 (bbport 0.7)`).
## Sending it
+42 -16
View File
@@ -41,6 +41,7 @@
#include "bbgpu.h"
#include "bbport_guest_code_hooks.h"
#include "bbport_atlas_menu.h"
#include "bbport_overlay.h"
#include "bbport_toggles.h"
#include "common/types.h"
@@ -520,24 +521,35 @@ void PS4_SYSV_ABI OnListUpdate(std::uint64_t, const Core::GuestRegisterSnapshot*
}
return;
}
static const bool trace = bb_env("BB_MENU_POINTER_FILE") != nullptr;
if ((!moved && !clicked) || pad_steering.load(std::memory_order_relaxed)) {
if (trace && clicked) {
std::printf("Menu pointer: list %p click waits (pad steering)\n",
static_cast<void*>(component));
}
return;
}
std::uint64_t slot = 0;
const int index = list.ItemAt(x, y, slot);
if (index < 0) {
if (trace && clicked) {
std::printf("Menu pointer: list %p click at %.0f,%.0f, on none of its items\n",
static_cast<void*>(component), x, y);
}
return;
}
if (clicked) {
click.store(false, std::memory_order_relaxed);
}
static const bool trace = bb_env("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());
if (trace) {
std::printf("Menu pointer: list %p presses %#x\n", static_cast<void*>(component), button);
}
presses.fetch_or(button, std::memory_order_relaxed);
// A held click on a slider's arrow keeps moving it.
if (button == Left || button == Right) {
@@ -602,27 +614,41 @@ int OnEvent(const void* sdl_event, void* sdl_window) {
default:
break;
}
const bool ours = !bbgpu_mouse_captured() && !BbOverlay::MenuOpen() && InMenu();
// Not BbOverlay::MenuOpen(): that is also true while Hunter's Atlas's tile shows in the game's
// quick menu, and then no click reached the game's own items (only the tile's Map opened).
// A click on the tile itself is left to it (BbAtlasMenu::HandleMouse, after this filter),
// and so is its release, on which it opens the map.
const bool atlas_tile = BbAtlasMenu::Visible() && BbAtlasMenu::Hovered();
const bool ours =
!bbgpu_mouse_captured() && !BbOverlay::CapturesInput() && !atlas_tile && InMenu();
static const bool trace = bb_env("BB_MENU_POINTER_FILE") != nullptr;
if (trace && event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
std::printf("Menu pointer: button %d down (taken %d: captured %d, overlay %d, Atlas tile %d, "
"menu %d)\n",
int(event.button.button), ours ? 1 : 0, bbgpu_mouse_captured() ? 1 : 0,
BbOverlay::CapturesInput() ? 1 : 0, atlas_tile ? 1 : 0, InMenu() ? 1 : 0);
}
static bool left_taken = false; ///< the left button's press was the pointer's: so is its release
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;
const bool taken = left_taken;
left_taken = false;
return taken ? 1 : 0;
}
if (!ours) {
return 0;
}
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
// Only the left button is the pointer's. The others keep their bindings in a menu too
// (runtime_pad/sample.c), as the game's own prompts show them: the right button is L2, which
// switches the storage box's pages (#44); it used to be the way back, and with it nothing
// could press L2 in a menu. Escape is the way back.
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
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) {
left_held.store(true, std::memory_order_relaxed);
click_ms.store(NowMs(), std::memory_order_relaxed);
click.store(true, std::memory_order_relaxed);
left_taken = true;
return 1;
}
if (event.type == SDL_EVENT_MOUSE_WHEEL) {
@@ -636,7 +662,7 @@ int OnEvent(const void* sdl_event, void* sdl_window) {
return 0;
}
/// BB_MENU_POINTER_FILE=<file> (tests): "x y [click|right|up|down]" in stage pixels, applied
/// BB_MENU_POINTER_FILE=<file> (tests): "x y [click|back|up|down]" in stage pixels, applied
/// when the file changes, as the mouse would.
void ReadTestFile() {
static const char* path = bb_env("BB_MENU_POINTER_FILE");
@@ -668,7 +694,7 @@ void ReadTestFile() {
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")) {
} else if (!std::strcmp(action, "back")) {
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);
+2 -1
View File
@@ -106,7 +106,7 @@ BB_ENV(BB_SHADNET_PASSWORD, SETTING, "the shadNet account password (the launcher
BB_ENV(BB_SHADNET_SERVER, SETTING, "the shadNet server")
BB_ENV(BB_SHADNET_TOKEN, SETTING, "the shadNet account token (the launcher's)")
BB_ENV(BB_SHADNET_WEBAPI, SETTING, "the shadNet web API server")
BB_ENV(BB_SKIP_CPU_CHECK, SETTING, "=1: skip the check of the CPU's bsr/bsf behaviour before the game starts")
BB_ENV(BB_SKIP_CPU_CHECK, SETTING, "=1: skip the check of the processor's instructions (AVX, BMI1, LZCNT...) before the game starts")
BB_ENV(BB_SKIP_GAME_CHECK, SETTING, "=1: start with game files that did not pass the check")
BB_ENV(BB_STEAM_INPUT_X11, SETTING, "=0: keep the native Wayland window when Steam made a virtual controller for the game (default: Xwayland, so Steam Input reaches it)")
BB_ENV(BB_TEXTURE_QUALITY, SETTING, "texture quality over bbport.ini's texture_quality")
@@ -410,6 +410,7 @@ BB_ENV(BB_EXP_COPY_SRC_SNAPSHOT, DEBUG, "set (experiment): copy shader sources s
BB_ENV(BB_EXP_CPU_READS_GPU_WAIT, DEBUG, "set (experiment): CPU-side reads also wait for the GPU, as before")
BB_ENV(BB_EXP_DMA_UNTRACKED, DEBUG, "set (experiment): stages reading memory through pointers count as unlisted reads")
BB_ENV(BB_FAULT_WAIT, DEBUG, "=1: a fatal fault or a GPU library assertion leaves the process alive for a debugger (tools/debug_attach.sh)")
BB_ENV(BB_MOVBE_TRAP, DEBUG, "=file: the movbe at the image offsets listed there (tools/movbe_sites.py) made ud2 and run by the port, as on a processor without MOVBE")
BB_ENV(BB_WRITE_SHADOWS, DEBUG, "=1: ranges the game's copy shader writes in place copied into VRAM after it, small read-only bindings read there (no gain measured)")
BB_ENV(BB_WRITE_SHADOW_TRACE, DEBUG, "=1: why BB_WRITE_SHADOWS lookups miss and who drops the copies (log lines)")
BB_ENV(BB_HOT_VERTEX_COPY, TUNING, "=1: vertex data bound in place copied into VRAM once per submission (no reads over the bus per pass)")
+1
View File
@@ -71,6 +71,7 @@ bool Interacting(){return visible || BbMap::IsOpen() || runtime_map_menu_open();
bool UsesControllerGlyphs(){const char* qa=bb_env("BB_ATLAS_QA_DEVICE");return bb_env("BB_ATLAS_QA")&&qa?std::strcmp(qa,"controller")==0:bool(controller);}
bool UsesPlayStationGlyphs(){const auto type=SDL_GetGamepadTypeForID(last_controller.load());return type==SDL_GAMEPAD_TYPE_PS3||type==SDL_GAMEPAD_TYPE_PS4||type==SDL_GAMEPAD_TYPE_PS5;}
bool Focused(){return focused;}
bool Hovered(){return hovered;}
void Suppress(bool value){suppressed=value;if(value){visible=false;state.Reset();}}
void Reset(){state.Reset();focused=false;visible=false;hovered=false;mouse_held=false;map_previous=0;}
void Tick(){Refresh();if(BbMap::IsOpen()&&!runtime_map_menu_open()&&state.focused){BbMap::Close();Reset();}}
+2
View File
@@ -8,6 +8,8 @@ bool Interacting();
bool UsesControllerGlyphs();
bool UsesPlayStationGlyphs();
bool Focused();
/// The pointer is over the tile (the game's own menu pointer leaves its clicks to it).
bool Hovered();
void Suppress(bool value);
void Tick();
void Draw();
+76 -3
View File
@@ -26,6 +26,10 @@
#include "bbport_settings.h"
#include <fmt/format.h>
// bb-probe (runtime_services.c); weak: the GPU library's tests link without it.
extern "C" void runtime_quit_arm(int on) __attribute__((weak));
extern "C" void runtime_quit_at_title(void) __attribute__((weak));
namespace BbGameMenu {
namespace {
@@ -78,11 +82,23 @@ struct Layout {
};
constexpr Layout ControlsLayout{0x4934065, 113020, 0, 6}; // "ControllSetting"
constexpr u32 TextName = 0xc8, TextHelp = 0xc9; // SP_menu text / SP_one-line help
constexpr u32 TextDialog = 0xcc; // dialog texts
constexpr u32 ScreenSoundItem = 110001; // "Screen/Sound" in the System menu: ours follow it
/// The System menu's builder: (out, arg, in the world). Out of the world it drops Exit Game.
constexpr u64 SystemMenu = 0x1bb3ad0;
/// (menu, name+help texts, {vtable, fn} function, 0): an item that runs `fn` like a page factory
/// (Exit Game's, 0x1bb436d).
constexpr u64 AddActionItem = 0x1b4c9a0;
/// Exit Game's (out, arg): its question (dialog text ExitQuestion), then a save and the title.
constexpr u64 ExitGame = 0x1c0c9f0;
constexpr u32 ExitQuestion = 110000;
/// The title menu's items (0x1b4a4aa...): built when the game is back at the title.
constexpr u32 TitleItemFirst = 401301, TitleItemLast = 401307;
const u8 MsgLookupPrologue[] = {0x89, 0xf0, 0x48, 0x8b, 0x77, 0x08, 0x48, 0x8b, 0x34, 0xc6};
const u8 AddPageItemPrologue[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57};
const u8 RowInputPrologue[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57};
const u8 SystemMenuPrologue[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57};
u64 image_base = 0;
template <typename F>
@@ -337,7 +353,9 @@ struct Page {
std::vector<Page> pages;
// "Exit to desktop" in the System menu (the title screen's too): a Quit Game to the desktop, as PC
// games have (Discord requests 2026-10-21; the Nexus mod "Just Exit Game" did it until now).
u32 exit_name = 0, exit_help = 0;
// In the world it is the game's Exit Game first, so the game saves (runtime_services.c closes it
// at the title), with its question as ours: closing at once lost what was not saved yet (#25).
u32 exit_name = 0, exit_help = 0, exit_help_world = 0, exit_question = 0;
void DefineTexts() {
const u32 off = AddText("Off", "Выкл"), on = AddText("On", "Вкл");
@@ -457,6 +475,8 @@ void DefineTexts() {
exit_name = AddText("Exit to desktop", "Выйти на рабочий стол");
exit_help = AddText("Closes the game, as its window's close button does",
"Закрывает игру, как кнопка закрытия окна");
exit_help_world = AddText("Saves and closes the game", "Сохраняет и закрывает игру");
exit_question = AddText("Quit the game and return to the desktop?", "Выйти из игры на рабочий стол?");
}
// ---- The game's structures, as its own code builds them. ----
@@ -650,6 +670,19 @@ void* PS4_SYSV_ABI QuitFactory(void* out, void* arg) {
SDL_PushEvent(&event);
return Factory(out, arg, reinterpret_cast<void*>(&Content<0>), 0);
}
/// In the world: the game's Exit Game, with its question as ours, and the quit armed.
thread_local bool asking_exit = false;
void* PS4_SYSV_ABI SaveQuitFactory(void* out, void* arg) {
std::printf("Game menu: exit to desktop, through Exit Game (it saves)\n");
if (runtime_quit_arm) {
runtime_quit_arm(1);
}
using Factory = void* (PS4_SYSV_ABI*)(void*, void*);
asking_exit = true;
void* result = Game<Factory>(ExitGame)(out, arg);
asking_exit = false;
return result;
}
constexpr std::size_t MaxPages = 6;
void* const factories[MaxPages] = {
reinterpret_cast<void*>(&PageFactory<0>), reinterpret_cast<void*>(&PageFactory<1>),
@@ -675,6 +708,17 @@ AddFn add_original = nullptr;
std::atomic<const void*> screen_sound_text{nullptr};
const char16_t* PS4_SYSV_ABI LookupHook(void* repository, u32 table, u32 category, u32 id) {
if (category == TextDialog && id == ExitQuestion) {
if (asking_exit && exit_question) {
return TextOf(exit_question);
}
if (runtime_quit_arm) {
runtime_quit_arm(0); // the game's own Exit Game: back to the title only
}
}
if (category == TextName && id >= TitleItemFirst && id <= TitleItemLast && runtime_quit_at_title) {
runtime_quit_at_title();
}
if (OurText(id)) {
// BB_GAME_MENU_TRACE=1: how often the game asks for our texts (per 2 s).
static const bool count = bb_env_on("BB_GAME_MENU_TRACE");
@@ -766,6 +810,31 @@ void AddItem(void* menu, u32 name, u32 help, void* factory) {
DestroyTexts(pair);
}
/// An item like Exit Game's (AddActionItem): `factory` runs when it is chosen, its result shown
/// as the game shows Exit Game's question.
void AddActionEntry(void* menu, u32 name, u32 help, void* factory) {
alignas(16) u8 pair[2 * TextSize] = {};
MakeTexts(pair, name, help);
alignas(16) u8 function[FunctionSize] = {};
*reinterpret_cast<u64*>(function) = image_base + FactoryVtable;
*reinterpret_cast<void**>(function + 8) = factory;
*reinterpret_cast<void**>(function + 0x20) = function;
Game<AddFn>(AddActionItem)(menu, pair, function, nullptr);
DestroyFunction(function);
DestroyTexts(pair);
}
/// The System menu being built is the world's (it has Exit Game).
bool building_in_world = false;
using SystemMenuFn = void* (PS4_SYSV_ABI*)(void*, void*, u64);
SystemMenuFn system_menu_original = nullptr;
void* PS4_SYSV_ABI SystemMenuHook(void* out, void* arg, u64 in_world) {
building_in_world = (in_world & 0xff) != 0;
void* result = system_menu_original(out, arg, in_world);
building_in_world = false;
return result;
}
void* PS4_SYSV_ABI AddHook(void* menu, void* pair, void* function, void* out) {
void* result = add_original(menu, pair, function, out);
const void* screen_sound = screen_sound_text.load();
@@ -773,7 +842,9 @@ void* PS4_SYSV_ABI AddHook(void* menu, void* pair, void* function, void* out) {
for (std::size_t i = 0; i < pages.size() && i < MaxPages; ++i) {
AddItem(menu, pages[i].name, pages[i].help, factories[i]);
}
if (exit_name) {
if (exit_name && building_in_world) {
AddActionEntry(menu, exit_name, exit_help_world, reinterpret_cast<void*>(&SaveQuitFactory));
} else if (exit_name) {
AddItem(menu, exit_name, exit_help, reinterpret_cast<void*>(&QuitFactory));
}
}
@@ -858,7 +929,9 @@ void PatchImage(unsigned char* image, std::uint64_t size) {
Detour(image, AddPageItem, AddPageItemPrologue, sizeof(AddPageItemPrologue),
reinterpret_cast<void*>(&AddHook), reinterpret_cast<void**>(&add_original), cursor) &&
Detour(image, RowInput, RowInputPrologue, sizeof(RowInputPrologue),
reinterpret_cast<void*>(&RowInputHook), reinterpret_cast<void**>(&row_input_original), cursor);
reinterpret_cast<void*>(&RowInputHook), reinterpret_cast<void**>(&row_input_original), cursor) &&
Detour(image, SystemMenu, SystemMenuPrologue, sizeof(SystemMenuPrologue),
reinterpret_cast<void*>(&SystemMenuHook), reinterpret_cast<void**>(&system_menu_original), cursor);
mprotect(stubs, page, PROT_READ | PROT_EXEC);
std::printf("Game menu: %s (%zu pages in System)\n", ok ? "the port's pages are in" : "hooks failed, off",
pages.size());
+15 -9
View File
@@ -124,16 +124,22 @@ void FreeChunk(u64 phys, u64 size) {
std::uint32_t FindHostType(std::uint32_t bits) {
// Cached host memory first, then any host-visible type the import allows.
for (const bool cached : {true, false}) {
for (std::uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i) {
const auto flags = memory_properties.memoryTypes[i].propertyFlags;
if (!(bits & (1u << i)) || !(flags & vk::MemoryPropertyFlagBits::eHostVisible) ||
(flags & vk::MemoryPropertyFlagBits::eDeviceLocal) || !TypeAllowed(flags) ||
(cached &&!(flags & vk::MemoryPropertyFlagBits::eHostCached))) {
continue;
// Cached host memory first, then any host-visible type the import allows. System-memory types
// (not device-local) come first; a GPU with one memory pool (Intel ANV on integrated Arc/Xe:
// every type is device-local, the imports allow only those) then takes a device-local
// host-visible type: the pages stay the CPU's own, cached, memory.
for (const bool device_local : {false, true}) {
for (const bool cached : {true, false}) {
for (std::uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i) {
const auto flags = memory_properties.memoryTypes[i].propertyFlags;
if (!(bits & (1u << i)) || !(flags & vk::MemoryPropertyFlagBits::eHostVisible) ||
bool(flags & vk::MemoryPropertyFlagBits::eDeviceLocal) != device_local ||
!TypeAllowed(flags) ||
(cached && !(flags & vk::MemoryPropertyFlagBits::eHostCached))) {
continue;
}
return i;
}
return i;
}
}
return ~0u;
+14
View File
@@ -115,6 +115,20 @@ inline const char* MenuTranslation(MenuLanguage language, const char* english) {
"Schließt das Spiel wie die Schließen-Schaltfläche seines Fensters",
"Fecha o jogo, como o botão de fechar da janela",
"关闭游戏,与窗口的关闭按钮相同"}},
{"Saves and closes the game",
{"Guarda y cierra el juego",
"Salva e chiude il gioco",
"Sauvegarde et ferme le jeu",
"Speichert und schließt das Spiel",
"Salva e fecha o jogo",
"保存并关闭游戏"}},
{"Quit the game and return to the desktop?",
{"¿Salir del juego y volver al escritorio?",
"Uscire dal gioco e tornare al desktop?",
"Quitter le jeu et revenir au bureau ?",
"Spiel beenden und zum Desktop zurückkehren?",
"Sair do jogo e voltar para a área de trabalho?",
"要退出游戏并返回桌面吗?"}},
{"Bloom",
{"Bloom",
"Bloom",
+5
View File
@@ -60,6 +60,11 @@ private:
u64 last_mouse_motion_ms{}; ///< SDL_GetTicks of the last mouse motion (UpdateCursor)
bool cursor_hidden{};
u64 capture_settle_ms{}; ///< mouse motion before this (SDL_GetTicks) is not looking around
/// Issue #39: fullscreen asked again on Wayland when the compositor did not keep it (FixFullscreen).
bool fullscreen_wanted{};
u64 fullscreen_since_ms{}, fullscreen_tried_ms{};
int fullscreen_tries{};
void FixFullscreen();
bool pointer_shown{}; ///< a game module's pointer menu is open (bbgpu_pointer_wanted)
bool released_for_pointer{}; ///< the mouse was let go for it: taken back after
void CaptureMouse(bool capture);
+30 -2
View File
@@ -223,8 +223,8 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
if (gamescope && !(fullscreen && fullscreen[0] == '1')) {
std::printf("Display: gamescope: fullscreen\n");
}
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN,
!placed && (gamescope || (fullscreen && fullscreen[0] == '1')));
fullscreen_wanted = !placed && (gamescope || (fullscreen && fullscreen[0] == '1'));
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, fullscreen_wanted);
// Issue #234: the swapchain at the display's pixels on a scaled desktop (Wayland 200%), not at
// the logical size stretched by the compositor. Sizes below are read in pixels already.
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
@@ -249,6 +249,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
window_info.render_surface = reinterpret_cast<void*>(SDL_GetNumberProperty(wp, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0));
} else if (driver && !std::strcmp(driver, "wayland")) {
window_info.type = WindowSystemType::Wayland;
fullscreen_since_ms = SDL_GetTicks();
window_info.display_connection = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);
window_info.render_surface = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);
} else {
@@ -519,9 +520,36 @@ bool WindowSDL::PollEvents() {
}
pointer_shown = pointer;
UpdateCursor();
FixFullscreen();
return is_open;
}
// Issue #39: on Wayland a program cannot place its window. With several monitors KWin can map
// it on another one first (the one SDL calls primary, not KDE's) and move it to the monitor
// asked for, and the window is left there as a plain window. Asked again, fullscreen holds (as
// KDE's own shortcut did). Only in the first seconds and a few times: there is no fullscreen
// toggle in the game, but the desktop's own shortcuts must keep working after that.
void WindowSDL::FixFullscreen() {
constexpr u64 FirstMs = 10000, BetweenMs = 1000;
constexpr int MaxTries = 3;
if (!fullscreen_wanted || !fullscreen_since_ms || fullscreen_tries >= MaxTries) {
return;
}
const u64 now = SDL_GetTicks();
if (now - fullscreen_since_ms > FirstMs) {
fullscreen_since_ms = 0; // past the start: the desktop decides from here
return;
}
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) || now - fullscreen_tried_ms < BetweenMs) {
return;
}
fullscreen_tried_ms = now;
++fullscreen_tries;
std::printf("Display: the compositor left the window out of fullscreen, asked again (%d of %d)\n",
fullscreen_tries, MaxTries);
SDL_SetWindowFullscreen(window, true);
}
// 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 or a game's pointer menu is open.
void WindowSDL::UpdateCursor() {
+15 -1
View File
@@ -251,9 +251,23 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
if (exact_fmt && info.pixel_format != resolved.guest_info.pixel_format) {
image_id = {}; // the very format is needed
} else if (resolved.guest_info.resources < info.resources) {
// bbport (Linux fork): which images, and whether the dropped one held what the GPU
// drew (then lost: the new image is read from guest memory). Issue #25: a texture
// garbled until the game was restarted.
const ImageInfo& had = resolved.guest_info;
LOG_WARNING(Render_Vulkan,
"Image overlap resolve failed: wanted {:#x}+{:#x} {}x{} {} levels {} layers {}; "
"dropped {:#x}+{:#x} {}x{} {} levels {} layers {}{}",
info.guest_address, info.guest_size, info.size.width, info.size.height,
vk::to_string(info.pixel_format), info.resources.levels, info.resources.layers,
had.guest_address, had.guest_size, had.size.width, had.size.height,
vk::to_string(had.pixel_format), had.resources.levels, had.resources.layers,
True(resolved.flags & ImageFlagBits::GpuModified)
? (resolved.SafeToDownload() ? ", drawn by the GPU (its contents are lost)"
: ", drawn by the GPU, then written")
: "");
FreeImage(image_id); // picked up wrongly
image_id = {};
LOG_WARNING(Render_Vulkan, "Image overlap resolve failed");
}
}
if (!image_id) {
+1 -1
View File
@@ -14,7 +14,7 @@ import re
import tempfile
import urllib.request
VERSION = "0.70-linux.1"
VERSION = "0.70-linux.3"
def _build():
+37
View File
@@ -89,6 +89,43 @@ EOF
chmod +x "$appdir/bin/bloodborne"
sed -e 's|@LAUNCHER@|bloodborne|' -e 's|@ICON@|bloodborne|' \
launcher/io.github.bbport.Launcher.desktop.in > "$work/bloodborne.desktop"
# The version and a description for AppImage managers (Gear Lever showed "UNKNOWN", #49): the
# desktop entry's X-AppImage-Version, which appimagetool writes from VERSION (it replaces one in
# the entry), and AppStream metainfo named after the desktop entry. The version is the
# launcher's own (bbport_update.py), so it is kept in one place.
version=$(sed -n 's/^VERSION = "\(.*\)"$/\1/p' launcher/bbport_update.py)
[[ -n $version ]] || { echo "No VERSION in launcher/bbport_update.py" >&2; exit 1; }
export VERSION=$version
mkdir -p "$appdir/share/metainfo"
cat > "$appdir/share/metainfo/bloodborne.appdata.xml" <<METAINFO
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>io.github.ayozetr.bloodborne_linux</id>
<name>Bloodborne (bbport)</name>
<developer id="io.github.ayozetr">
<name>bloodborne_linux contributors</name>
</developer>
<summary>Native Linux port launcher for your own copy of Bloodborne</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<description>
<p>Runs Bloodborne natively on Linux from your own decrypted dump of the PS4 game with update
1.09. No game files are included. Upscaling (DLSS, FSR), online play, mods and the community
patches are set up from the launcher, which also updates itself.</p>
</description>
<launchable type="desktop-id">bloodborne.desktop</launchable>
<url type="homepage">https://github.com/ayozetr/bloodborne_linux</url>
<url type="bugtracker">https://github.com/ayozetr/bloodborne_linux/issues</url>
<categories><category>Game</category></categories>
<content_rating type="oars-1.1">
<content_attribute id="violence-fantasy">intense</content_attribute>
<content_attribute id="violence-bloodshed">intense</content_attribute>
</content_rating>
<releases>
<release version="$version" date="$(date +%F)"/>
</releases>
</component>
METAINFO
# GTK, libadwaita and their typelibs are loaded by PyGObject at run time (not linked): named here.
# So are Python's ssl and hashlib modules, which quick-sharun leaves out by default: the launcher's
+29 -3
View File
@@ -14,6 +14,7 @@
#include "runtime_trophy.h"
#include "runtime_map.h"
#include "runtime_atlas_state.h"
#include "runtime_movbe.h"
#include "gpu/bbgpu.h"
#if !defined(__x86_64__) || !defined(__GNUC__)
#error This prototype requires x86-64 GCC or Clang (including MinGW).
@@ -100,8 +101,12 @@ static void print_environment(void) {
/* The game's code was compiled for the PS4's CPU (AMD Jaguar) and runs as it is: AVX, BMI1
* (andn/bextr/blsr/tzcnt), MOVBE, LZCNT and POPCNT, thousands of each in eboot.bin. A CPU without
* them stops at the first one with SIGILL (exit code 132, issue #26), and lzcnt/tzcnt even run as
* bsr/bsf there, with other results. Said before the game starts; BB_SKIP_CPU_CHECK=1 skips it. */
* bsr/bsf there, with other results. Said before the game starts; BB_SKIP_CPU_CHECK=1 skips it.
* MOVBE alone is no obstacle: the port runs those itself (runtime_movbe.c), so AMD's FX
* processors, which lack only that one, play (issue #39). */
static void check_cpu(void) {
if (!runtime_movbe_supported())
printf("CPU: no MOVBE; the port runs the game's movbe itself (runtime_movbe.c)\n");
const char *skip = bb_env("BB_SKIP_CPU_CHECK");
if (skip && !strcmp(skip, "1")) return;
unsigned a, b, c, d, leaf1_c = 0, leaf7_b = 0, ext1_c = 0;
@@ -115,7 +120,7 @@ static void check_cpu(void) {
avx = (lo & 6) == 6; /* the OS saves SSE and AVX state */
}
const struct { int present; const char *name; } features[] = {
{avx, "AVX"}, {(leaf7_b & bit_BMI) != 0, "BMI1"}, {(leaf1_c & bit_MOVBE) != 0, "MOVBE"},
{avx, "AVX"}, {(leaf7_b & bit_BMI) != 0, "BMI1"},
{(ext1_c & bit_LZCNT) != 0, "LZCNT"}, {(leaf1_c & bit_POPCNT) != 0, "POPCNT"},
{(leaf1_c & bit_SSE4_2) != 0, "SSE4.2"},
};
@@ -129,7 +134,8 @@ static void check_cpu(void) {
fprintf(stderr,
"ERROR: this CPU lacks %s. Bloodborne's code was compiled for the PS4's CPU and uses "
"these instructions directly: it needs an Intel Haswell (4th generation Core, 2013) or "
"newer, or an AMD Ryzen. BB_SKIP_CPU_CHECK=1 starts anyway.\n", missing);
"newer, or an AMD FX-4300, 6300 or 8300 series (Piledriver) or newer. "
"BB_SKIP_CPU_CHECK=1 starts anyway.\n", missing);
exit(1);
}
static uint64_t read64(FILE *f) {
@@ -244,6 +250,23 @@ void bb_fault_wait(const char *what) {
#endif
}
static void fault(int sig, siginfo_t *info, void *context) {
#ifndef _WIN32
/* A movbe on a processor without MOVBE (AMD FX, issue #39): run by the port, the game goes on. */
if (sig == SIGILL) {
greg_t *g = ((ucontext_t *)context)->uc_mcontext.gregs;
static const int order[16] = {REG_RAX, REG_RCX, REG_RDX, REG_RBX, REG_RSP, REG_RBP, REG_RSI, REG_RDI,
REG_R8, REG_R9, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15};
uint64_t regs[16], rip = (uint64_t)g[REG_RIP];
if (rip - (uintptr_t)image < 0x10000000) {
for (int i = 0; i < 16; ++i) regs[i] = (uint64_t)g[order[i]];
if (runtime_movbe_fault(regs, &rip)) {
for (int i = 0; i < 16; ++i) g[order[i]] = (greg_t)regs[i];
g[REG_RIP] = (greg_t)rip;
return;
}
}
}
#endif
/* GPU page tracking (write-protected guest pages) is resolved first. */
if (gpu_enabled && sig == SIGSEGV && bbgpu_handle_fault(context, info->si_addr)) return;
/* A speculative guest memory read (runtime_memory.c) failed: resume its recovery point. */
@@ -810,6 +833,9 @@ int main(int argc, char **argv) {
runtime_atlas_state_bind(image, size);
}
}
#ifndef _WIN32
runtime_movbe_bind(image, size); /* BB_MOVBE_TRAP (testing): movbe made ud2 */
#endif
#ifdef _WIN32
patch_guest_tls(segments, ns);
#endif
+7
View File
@@ -99,6 +99,13 @@ void runtime_file_report(void);
void runtime_file_configure(const char *app0, const char *user);
int runtime_file_mount(const char *guest, const char *host);
void runtime_file_unmount(const char *guest);
/* Exit to desktop from the world (runtime_services.c): armed by the game menu's entry, the game
* closes once it is back at the title with its save written. */
void runtime_quit_arm(int on);
void runtime_quit_at_title(void);
void runtime_quit_on_open(const char *guest);
void runtime_quit_on_save_done(void);
int runtime_savedata_mounted(void);
int runtime_file_translate(const char *guest, char *out, size_t size);
int64_t runtime_file_open(const char *path, int flags, int mode);
int64_t runtime_file_close(int fd);
+1
View File
@@ -508,6 +508,7 @@ static void presence_map(const char *guest) {
pthread_mutex_unlock(&presence_lock);
}
static int64_t do_open(const char *guest,int flags,int mode) {
runtime_quit_on_open(guest);
if (game_path(guest) && (flags & (3|0x8|0x200|0x400|0x800))) return -EROFS;
char path[1024];
int e=translate(guest,path,sizeof(path));
+210
View File
@@ -0,0 +1,210 @@
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
/* MOVBE on processors without it. The game's code was built for the PS4's CPU (AMD Jaguar) and
* has 987 movbe (a load or store with its bytes swapped: the big-endian headers of the game's
* files, network data). AMD's FX processors (Bulldozer to Piledriver) have every other instruction
* the game uses, but not this one: the first movbe stopped the game with SIGILL (issue #39). The
* SIGILL handler (probe.c) does it here instead and resumes after it; on a processor with MOVBE
* this code never runs.
*
* BB_MOVBE_TRAP=file (testing on any processor): the image offsets of movbe in the file
* (one hex number per line, tools/movbe_sites.py) become ud2, which this code runs as the movbe
* that was there, as a processor without MOVBE would. */
#define _GNU_SOURCE
#include "runtime_movbe.h"
#include "../gpu/shim/bb_env.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
/* x86 register numbers (rax 0 ... r15 15). */
enum { RSP = 4, RBP = 5 };
int runtime_movbe_run(const uint8_t *code, uint64_t regs[16], uint64_t rip) {
const uint8_t *p = code;
int rex = 0, operand16 = 0;
for (;; ++p) {
if (p - code >= 4) return 0;
if (*p == 0x66) operand16 = 1;
else break;
}
if ((*p & 0xf0) == 0x40) rex = *p++;
if (p[0] != 0x0f || p[1] != 0x38 || (p[2] != 0xf0 && p[2] != 0xf1)) return 0;
const int store = p[2] == 0xf1;
p += 3;
const uint8_t modrm = *p++;
const int mod = modrm >> 6, rm = modrm & 7;
const int reg = ((modrm >> 3) & 7) | (rex & 4 ? 8 : 0);
if (mod == 3) return 0; /* the register form does not exist */
uint64_t address = 0;
int rip_relative = 0;
if (rm == 4) {
const uint8_t sib = *p++;
const int scale = sib >> 6, index = ((sib >> 3) & 7) | (rex & 2 ? 8 : 0);
const int base = (sib & 7) | (rex & 1 ? 8 : 0);
if (index != RSP) address += regs[index] << scale;
if ((base & 7) == RBP && mod == 0) {
int32_t disp;
memcpy(&disp, p, 4);
p += 4;
address += (uint64_t)(int64_t)disp;
} else {
address += regs[base];
}
} else if (rm == 5 && mod == 0) {
rip_relative = 1;
} else {
address = regs[rm | (rex & 1 ? 8 : 0)];
}
if (mod == 1) {
address += (uint64_t)(int64_t)(int8_t)*p++;
} else if (mod == 2 || rip_relative) {
int32_t disp;
memcpy(&disp, p, 4);
p += 4;
address += (uint64_t)(int64_t)disp;
}
const int length = (int)(p - code);
if (rip_relative) address += rip + (uint64_t)length;
uint8_t *memory = (uint8_t *)(uintptr_t)address;
if (rex & 8) {
uint64_t value;
if (store) {
value = __builtin_bswap64(regs[reg]);
memcpy(memory, &value, 8);
} else {
memcpy(&value, memory, 8);
regs[reg] = __builtin_bswap64(value);
}
} else if (operand16) {
uint16_t value;
if (store) {
value = __builtin_bswap16((uint16_t)regs[reg]);
memcpy(memory, &value, 2);
} else {
memcpy(&value, memory, 2);
regs[reg] = (regs[reg] & ~(uint64_t)0xffff) | __builtin_bswap16(value);
}
} else {
uint32_t value;
if (store) {
value = __builtin_bswap32((uint32_t)regs[reg]);
memcpy(memory, &value, 4);
} else {
memcpy(&value, memory, 4);
regs[reg] = __builtin_bswap32(value); /* a 32-bit result clears the upper half */
}
}
return length;
}
/* BB_MOVBE_TRAP: the replaced instructions, by image offset. */
typedef struct {
uint64_t offset;
uint8_t code[16];
} Trapped;
static Trapped *trapped;
static size_t trapped_count;
static uintptr_t trapped_image;
static int by_offset(const void *a, const void *b) {
const uint64_t x = ((const Trapped *)a)->offset, y = ((const Trapped *)b)->offset;
return x < y ? -1 : x > y;
}
void runtime_movbe_bind(unsigned char *image, uint64_t size) {
const char *path = bb_env("BB_MOVBE_TRAP");
if (!path || !path[0]) return;
FILE *f = fopen(path, "r");
if (!f) {
printf("MOVBE: BB_MOVBE_TRAP=%s cannot be read\n", path);
return;
}
size_t capacity = 0, skipped = 0;
char line[64];
const uint64_t page = (uint64_t)sysconf(_SC_PAGESIZE);
while (fgets(line, sizeof(line), f)) {
char *end;
const uint64_t offset = strtoull(line, &end, 16);
if (end == line) continue;
if (offset + 16 > size) {
++skipped;
continue;
}
Trapped t = {.offset = offset};
memcpy(t.code, image + offset, sizeof(t.code));
const uint8_t *op = t.code + ((t.code[0] & 0xf0) == 0x40); /* past a REX prefix */
if (op[0] != 0x0f || op[1] != 0x38 || (op[2] != 0xf0 && op[2] != 0xf1)) {
++skipped;
continue;
}
if (trapped_count == capacity) {
capacity = capacity ? capacity * 2 : 1024;
trapped = realloc(trapped, capacity * sizeof(*trapped));
if (!trapped) {
fclose(f);
trapped_count = 0;
return;
}
}
trapped[trapped_count++] = t;
const uint64_t first = offset & ~(page - 1);
mprotect(image + first, (size_t)((offset + 2 + page - 1) / page * page - first), PROT_READ | PROT_WRITE | PROT_EXEC);
image[offset] = 0x0f;
image[offset + 1] = 0x0b; /* ud2 */
}
fclose(f);
qsort(trapped, trapped_count, sizeof(*trapped), by_offset);
trapped_image = (uintptr_t)image;
printf("MOVBE: BB_MOVBE_TRAP: %zu movbe made ud2 and run by the port (%zu lines skipped)\n", trapped_count,
skipped);
}
int runtime_movbe_supported(void) {
unsigned a, b, c, d;
__asm__("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(1), "c"(0));
return (c >> 22) & 1;
}
/* How many the port ran (the log says it every 10 s). */
static volatile uint64_t emulated;
int runtime_movbe_fault(uint64_t regs[16], uint64_t *rip) {
const uint8_t *code = (const uint8_t *)(uintptr_t)*rip;
if (trapped_count && code[0] == 0x0f && code[1] == 0x0b) {
const Trapped key = {.offset = *rip - trapped_image};
const Trapped *t = bsearch(&key, trapped, trapped_count, sizeof(*trapped), by_offset);
if (t) code = t->code;
}
const int length = runtime_movbe_run(code, regs, *rip);
if (!length) return 0;
*rip += (uint64_t)length;
const uint64_t total = __atomic_add_fetch(&emulated, 1, __ATOMIC_RELAXED);
/* In the log: the first one, then how many every 10 s (a signal each: a cost if many). */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
static int64_t next_report;
static uint64_t reported;
int64_t due = __atomic_load_n(&next_report, __ATOMIC_RELAXED);
if (total == 1 || (now.tv_sec >= due &&
__atomic_compare_exchange_n(&next_report, &due, (int64_t)now.tv_sec + 10, 0,
__ATOMIC_RELAXED, __ATOMIC_RELAXED))) {
if (total == 1) __atomic_store_n(&next_report, (int64_t)now.tv_sec + 10, __ATOMIC_RELAXED);
char line[96] = "MOVBE: run by the port: ";
size_t at = strlen(line);
char digits[24];
size_t n = 0;
uint64_t value = total - __atomic_exchange_n(&reported, total, __ATOMIC_RELAXED);
do digits[n++] = (char)('0' + value % 10); while ((value /= 10) && n < sizeof(digits));
while (n) line[at++] = digits[--n];
static const char tail[] = " (this processor has no MOVBE)\n";
memcpy(line + at, tail, sizeof(tail) - 1);
ssize_t written = write(1, line, at + sizeof(tail) - 1);
(void)written;
}
return 1;
}
+17
View File
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
/* MOVBE on processors without it (runtime_movbe.c). */
#ifndef BBPORT_RUNTIME_MOVBE_H
#define BBPORT_RUNTIME_MOVBE_H
#include <stdint.h>
/* Runs the movbe in `code` (at address `rip`) on `regs` (rax 0 ... r15 15) and its memory:
* its length, or 0 when `code` is no movbe (nothing is changed then). */
int runtime_movbe_run(const uint8_t *code, uint64_t regs[16], uint64_t rip);
/* The SIGILL handler's part: the movbe at *rip run and *rip moved past it (1), or 0. */
int runtime_movbe_fault(uint64_t regs[16], uint64_t *rip);
/* BB_MOVBE_TRAP=file: the listed movbe of the image made ud2 (while it is still writable). */
void runtime_movbe_bind(unsigned char *image, uint64_t size);
/* Whether this processor has MOVBE (CPUID 1, ECX bit 22). */
int runtime_movbe_supported(void);
#endif
+6 -1
View File
@@ -101,7 +101,12 @@ static void sample_host(PadData *d) {
* it) and sample_atlas() gives the game the neutral input. */
if (bbgpu_overlay_captures_input() && !atlas_on()) { apply_mouse_look(d,0,0,d->timestamp,0); return; }
const bool *k=SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetKeyboardState(NULL) : NULL;
mouse_buttons=captured && SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetMouseState(NULL,NULL) : 0;
/* In a game's pointer menu (the mouse released) the buttons count too, but the left one, which
* is the pointer's click (gpu/games/bloodborne_menu_pointer.cpp): the right one is L2 there as
* the game's prompts show it (the storage box's pages, #44). */
const int pointer_menu=!captured && bbgpu_pointer_wanted();
mouse_buttons=(captured || pointer_menu) && SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetMouseState(NULL,NULL) : 0;
if (!captured) mouse_buttons&=~SDL_BUTTON_LMASK;
const int pad_only=steam_pad_only(g) || !keyboard_enabled();
atomic_store_explicit(&steam_only_now,pad_only,memory_order_relaxed);
if (pad_only) {
+9
View File
@@ -214,8 +214,17 @@ static ABI int32_t save_umount(const MountPoint *point) {
int slot=slot_of(point);
if (slot>=0) { runtime_file_unmount(point->data); slots[slot].used=0; }
pthread_mutex_unlock(&lock);
if (slot>=0) runtime_quit_on_save_done();
return slot>=0 ? 0 : ERR_NOT_FOUND;
}
/* Save data mounted now (Exit to desktop waits for the last unmount). */
int runtime_savedata_mounted(void) {
pthread_mutex_lock(&lock);
int used=0;
for (int i=0;i<SLOTS;++i) used+=slots[i].used;
pthread_mutex_unlock(&lock);
return used;
}
static ABI int32_t save_set_param(const MountPoint *point, uint32_t type, const void *buffer, uint64_t size) {
if (!initialized) return ERR_NOT_INITIALIZED;
if (!buffer) return ERR_PARAMETER;
+45
View File
@@ -17,6 +17,7 @@
#include <pthread.h>
#include <time.h>
#include <arpa/inet.h>
#include <SDL3/SDL_events.h>
#define USER_ID 1
#define ORBIS_OK 0
@@ -132,6 +133,50 @@ static ABI int32_t system_status(unsigned char *status) {
static ABI int32_t system_event(void *event) { (void)event; return SYSTEM_NO_EVENT; }
static ABI int32_t hide_splash(void) { note("SystemService: splash screen hidden"); return 0; }
static ABI int32_t launch_browser(void) { note("SystemService: web browser request ignored (offline)"); return 0; }
/* Exit to desktop in the world (gpu/shim/bbport_game_menu.cpp): the System menu's entry runs the
* game's Exit Game, which saves and leaves the world, and arms the quit here. The game closes
* once it is back at the title: when the intro logos start loading (a menu/logo_ file opens) or
* the title menu is built (its texts: runtime_quit_at_title), with no save data mounted, or after
* the last save unmounts. The game's own Exit Game disarms it, and so do two minutes without
* getting there (the question answered No). Closing is the window's own close (SDL_EVENT_QUIT:
* the window thread ends the process with exit code 0). */
#define QUIT_ARMED_SECONDS 120
static int quit_waiting;
static time_t quit_armed;
static pthread_mutex_t quit_lock=PTHREAD_MUTEX_INITIALIZER;
static void quit_game(void) {
note("Exit to desktop: the game is saved and back at the title, closing");
fflush(stdout);
SDL_Event quit={.type=SDL_EVENT_QUIT};
if (!SDL_PushEvent(&quit)) printf("Runtime: Exit to desktop: %s\n",SDL_GetError());
}
static int quit_ready(void) { return quit_armed && time(NULL)-quit_armed<QUIT_ARMED_SECONDS; }
void runtime_quit_arm(int on) {
pthread_mutex_lock(&quit_lock);
if (on || quit_armed || quit_waiting) note(on ? "Exit to desktop: armed (Exit Game saves first)" : "Exit to desktop: disarmed");
__atomic_store_n(&quit_armed,on ? time(NULL) : 0,__ATOMIC_RELAXED);
quit_waiting=0;
pthread_mutex_unlock(&quit_lock);
}
static void quit_at_title(void) {
if (!__atomic_load_n(&quit_armed,__ATOMIC_RELAXED)) return; /* the usual case: one load */
pthread_mutex_lock(&quit_lock);
if (quit_ready()) {
__atomic_store_n(&quit_armed,0,__ATOMIC_RELAXED);
/* A save still writing finishes first (runtime_quit_on_save_done). */
if (runtime_savedata_mounted()) quit_waiting=1; else quit_game();
}
pthread_mutex_unlock(&quit_lock);
}
void runtime_quit_at_title(void) { quit_at_title(); }
void runtime_quit_on_open(const char *guest) {
if (guest && strstr(guest,"/menu/logo_")) quit_at_title();
}
void runtime_quit_on_save_done(void) {
pthread_mutex_lock(&quit_lock);
if (quit_waiting && !runtime_savedata_mounted()) { quit_waiting=0; quit_game(); }
pthread_mutex_unlock(&quit_lock);
}
/* ---- NetCtl / Net / Http / Ssl: no network ---- */
static int32_t net_errno;
+1
View File
@@ -10,6 +10,7 @@ int32_t runtime_guest_errno(int e) { return e; }
void runtime_memory_note_write(uintptr_t address, uint64_t size) { (void)address; (void)size; }
void runtime_memory_note_cpu_write(uintptr_t address, uint64_t size) { (void)address; (void)size; }
void runtime_memory_prepare_cpu_write(uintptr_t address, uint64_t size) { (void)address; (void)size; }
void runtime_quit_on_open(const char *guest) { (void)guest; }
uintptr_t runtime_lookup(const RuntimeExport *table,size_t n,const char *name) {
for (size_t i=0;i<n;++i) if (!strcmp(table[i].name,name)) return (uintptr_t)table[i].function;
return 0;
+96
View File
@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
/* runtime_movbe.c: the movbe forms the game uses, run on registers and a buffer. */
#include "runtime_movbe.h"
#include <stdio.h>
#include <string.h>
static int failures;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++failures; \
} \
} while (0)
enum { RAX, RCX, RDX, RBX, RSP, RBP, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15 };
int main(void) {
uint8_t memory[64];
uint64_t regs[16];
const uint64_t base = (uint64_t)(uintptr_t)memory;
/* movbe eax, [rcx+4] (0f 38 f0 41 04): the game's DCX header read (issue #39). */
{
static const uint8_t code[] = {0x0f, 0x38, 0xf0, 0x41, 0x04};
memset(regs, 0, sizeof(regs));
memcpy(memory + 4, "\x00\x01\x10\x00", 4);
regs[RCX] = base;
regs[RAX] = 0xffffffffffffffffull;
CHECK(runtime_movbe_run(code, regs, 0) == 5);
CHECK(regs[RAX] == 0x00011000); /* upper half cleared */
}
/* movbe [rax+r12*4-0x10], r9d (46 0f 38 f1 4c a0 f0): a store, REX.R and REX.X, disp8. */
{
static const uint8_t code[] = {0x46, 0x0f, 0x38, 0xf1, 0x4c, 0xa0, 0xf0};
memset(regs, 0, sizeof(regs));
memset(memory, 0, sizeof(memory));
regs[RAX] = base + 0x10;
regs[R12] = 2;
regs[R9] = 0xaabbccdd11223344ull;
CHECK(runtime_movbe_run(code, regs, 0) == 7);
CHECK(memcmp(memory + 8, "\x11\x22\x33\x44", 4) == 0); /* big-endian */
CHECK(regs[R9] == 0xaabbccdd11223344ull); /* the source is left as it was */
}
/* movbe rdx, [r13+0x100] (49 0f 38 f0 95 00 01 00 00): 64-bit, REX.B, disp32. */
{
static const uint8_t code[] = {0x49, 0x0f, 0x38, 0xf0, 0x95, 0x00, 0x01, 0x00, 0x00};
memset(regs, 0, sizeof(regs));
memcpy(memory + 8, "\x01\x02\x03\x04\x05\x06\x07\x08", 8);
regs[R13] = base + 8 - 0x100;
CHECK(runtime_movbe_run(code, regs, 0) == 9);
CHECK(regs[RDX] == 0x0102030405060708ull);
}
/* movbe [rsp+8], ecx (0f 38 f1 4c 24 08): rsp base through a SIB with no index. */
{
static const uint8_t code[] = {0x0f, 0x38, 0xf1, 0x4c, 0x24, 0x08};
memset(regs, 0, sizeof(regs));
memset(memory, 0, sizeof(memory));
regs[RSP] = base;
regs[RCX] = 0x12345678;
CHECK(runtime_movbe_run(code, regs, 0) == 6);
CHECK(memcmp(memory + 8, "\x12\x34\x56\x78", 4) == 0);
}
/* movbe esi, [rbp+0] (0f 38 f0 75 00): rbp needs a displacement byte. */
{
static const uint8_t code[] = {0x0f, 0x38, 0xf0, 0x75, 0x00};
memset(regs, 0, sizeof(regs));
memcpy(memory, "\xde\xad\xbe\xef", 4);
regs[RBP] = base;
CHECK(runtime_movbe_run(code, regs, 0) == 5);
CHECK(regs[RSI] == 0xdeadbeef);
}
/* movbe ax, [rip+0x10] (66 0f 38 f0 05 10 00 00 00): 16 bits keep the rest, rip-relative. */
{
static const uint8_t code[] = {0x66, 0x0f, 0x38, 0xf0, 0x05, 0x10, 0x00, 0x00, 0x00};
memset(regs, 0, sizeof(regs));
memcpy(memory + 0x20, "\xab\xcd", 2);
regs[RAX] = 0x1111111111111111ull;
const uint64_t rip = base + 0x20 - 0x10 - sizeof(code);
CHECK(runtime_movbe_run(code, regs, rip) == 9);
CHECK(regs[RAX] == 0x111111111111abcdull);
}
/* Not a movbe: nothing is run. */
{
static const uint8_t crc32[] = {0xf2, 0x0f, 0x38, 0xf1, 0xc1};
static const uint8_t ud2[] = {0x0f, 0x0b};
static const uint8_t reg_form[] = {0x0f, 0x38, 0xf0, 0xc1};
memset(regs, 0, sizeof(regs));
CHECK(runtime_movbe_run(crc32, regs, 0) == 0);
CHECK(runtime_movbe_run(ud2, regs, 0) == 0);
CHECK(runtime_movbe_run(reg_form, regs, 0) == 0);
}
printf("movbe: %s\n", failures ? "FAILED" : "all passed");
return failures != 0;
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""The image offsets of the movbe in the game's code, one per line, for BB_MOVBE_TRAP
(src/runtime_movbe.c): a game run with them made ud2 tests the port's movbe on any processor.
python3 tools/movbe_sites.py out/eboot.elf > out/movbe-sites.txt
BB_MOVBE_TRAP=out/movbe-sites.txt bash run.sh
Needs objdump (binutils)."""
import struct
import subprocess
import sys
import tempfile
def code_segments(elf):
phoff, = struct.unpack_from('<Q', elf, 0x20)
phentsize, phnum = struct.unpack_from('<HH', elf, 0x36)
for i in range(phnum):
kind, flags, offset, vaddr, _, filesz = struct.unpack_from('<IIQQQQ', elf, phoff + i * phentsize)
if kind == 1 and flags & 1:
yield vaddr, elf[offset:offset + filesz]
def main():
elf = open(sys.argv[1], 'rb').read()
for vaddr, code in code_segments(elf):
with tempfile.NamedTemporaryFile() as f:
f.write(code)
f.flush()
listing = subprocess.run(
['objdump', '-D', '--insn-width=16', '-b', 'binary', '-m', 'i386:x86-64',
f'--adjust-vma={vaddr:#x}', f.name], check=True, capture_output=True, text=True).stdout
for line in listing.splitlines():
fields = line.split('\t')
if len(fields) >= 3 and fields[2].startswith('movbe'):
print(fields[0].strip().rstrip(':'))
if __name__ == '__main__':
main()