mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-09 01:09:47 -10:00
Compare commits
12
Commits
0.70-linux.1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98750d0687 | ||
|
|
0b3dac11d7 | ||
|
|
5041586f9a | ||
|
|
e77bce746f | ||
|
|
d1dea1b8e4 | ||
|
|
6796fbbd13 | ||
|
|
badd460277 | ||
|
|
1d0bbec978 | ||
|
|
843424de13 | ||
|
|
21e1ce4224 | ||
|
|
498a37d24b | ||
|
|
e8c7eaaf9c |
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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();}}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import re
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
VERSION = "0.70-linux.1"
|
||||
VERSION = "0.70-linux.3"
|
||||
|
||||
|
||||
def _build():
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user