bbport study
Read-only study of bbport (https://github.com/deadinside28/bloodborne_pc), a native
Linux port of Bloodborne 1.09, to decide whether it is the runtime scaffold for the
source port we are building. Studied from a shallow clone at commit 4d4d208 (2026-10-06, "0.3").
Nothing was built or run. Statements marked verified were checked against our own
dump with read-only scripts; the rest come from reading the code and docs.
The project says it is not related to shadPS4 and that questions go to its own Discord.
1. Eboot to flat image, offline
The step is scripts/prepare.py, plus scripts/link_modules.py for the bundled modules.
- Input is the game folder with the update merged in, as we have it:
eboot.bin(a plaintext SELF),sce_module/,sce_sys/param.sfo,dvdroot_ps4/. Encrypted or compressed SELF segments are refused. No decryption exists anywhere. parse_selfrebuilds an ELF header, program headers and segment contents from the SELF's blocked segments.- The loadable segments (
PT_LOADand the Sony0x61000010) are copied to theirp_vaddrinto one flat buffer sized by the highestp_vaddr + p_memsz. That buffer is "the image". Image offset 0 isp_vaddr0. scripts/game_check.pypins the SHA-256 of that image to one value. Anything else (base game 1.00, another region or edition) is refused with an explanation.- The dynamic data (
PT_DYNAMICand the Sony dynlib data segment0x61000000) is parsed: symbol table, string table and both relocation tables. - Base-relative relocations are kept as such.
- Relocations against symbols the image defines are resolved to offsets.
- Relocations against undefined symbols become imports, named by NID plus library and
module ids, for example
pO96TwzOm5E#p#J. NIDs are the SHA-1 based hash of the symbol name, so imports are identified by hash; known names come from a hint list. link_modules.pyplaceslibc.prxandlibSceFios2.prx(the game's own code) after the eboot in the same image and merges their imports into one table. Imports of the unshipped libSceLibcInternal are served by libc.prx's exports of the same NID.- The output is
out/boot-linked.bin(a small custom format: header, load descriptors, 128-byte import names, relocations, the image), a reconstructedeboot.elf(not byte-exact) and JSON reports.
At run time src/probe.c maps the image into a low host region (the image base is
0x800000000 on the host), applies relocations, binds each import to a function of the
bbport runtime by NID, points unresolved imports at traps that print
STOP: first unsupported PS4 import, applies patches.bin, and jumps to the entry point.
The game's x86-64 code runs directly on the CPU. There is no emulation.
Verified: the loaded-image hash that game_check.py pins is the hash of our dump, both
computed from our merged eboot.bin with their parser and from our converted eboot.elf
(071df19c8880086d97182dbc057bc8cb37badaca57d9112683836b24a0444c0a). Our target is the
executable bbport supports. This hash does not depend on which SelfUtil produced the ELF.
2. What comes from shadPS4 and what is its own
Vendored from shadPS4 commit b4e7ae7 (GPL-2.0-or-later), listed in gpu/VENDOR.txt:
video_coreandshader_recompiler(the renderer and the GCN to SPIR-V recompiler),common, and from the libraries onlygnmdriver,videoout, parts ofavplayer,videodecutilities and a few kernel and system headers.- Externals:
gcn,sirit,half. - About 108k lines under
gpu/shadps4, with around 290bbport:markers for local changes. Some recompiler fixes were backported from shadPS4 after that commit.
Its own code:
src/, about 7k lines of C: loader (probe.c) and the runtime that implements the PS4 OS functions Bloodborne calls: memory, kernel, threads, mutexes, rwlocks, semaphores, files, pad, audio and AJM (ATRAC9 through LibAtrac9), save data, services, content, time. This is not shadPS4's library layer: shadPS4's broad HLE is not vendored.scripts/, about 1.3k lines of Python: conversion, linking, patch compiler, mods.gpu/shim/, about 5k lines: guest hooks, guest memory model, overlay, settings, copies.- New GPU modules (two-stage draw pipeline, motion vectors, upscalers), the launcher and packaging.
Other third-party parts: FSR-Vulkan by FireBurn (MIT), AMD FidelityFX (MIT), LibAtrac9 (MIT), Dear ImGui (MIT), dxil-spirv (MIT, build time). AMD's FSR 4 DLLs are not distributed.
3. Difference from AnyPS5's relinker
| AnyPS5 relinker | bbport | |
|---|---|---|
| Target | PS5 executables (Agc, PPSA ids); the only listed game is a 2D platformer | One PS4 game, Bloodborne 1.09 |
| Input | A clean ELF plus its sce_module/ ELFs |
The plaintext SELF folder; it does its own SELF parsing |
| Output | A native Linux ELF or Windows PE, with system libraries as dynamically linked shared objects built from a library tree (about 110 PRX libraries declared) | A flat image and a custom boot file, loaded by a C loader into a running process |
| Linking | Static rewrite of the executable; import filtering and PLT compaction options | Dynamic: imports bound to host functions at load, unknown imports trap |
| Libraries | Implements many system libraries generically | Implements only what this game calls |
| Graphics | Shader recompiler to SPIR-V | Vendored shadPS4 renderer, PM4 decoded at run time |
| Hooks | None found | Ad hoc patch sites (section 5) |
| License | GPL-2.0 only | GPL-2.0-or-later |
AnyPS5 would give a standalone native executable but has no Bloodborne path and no PS4 graphics. bbport already runs the game. The two are not interchangeable.
4. Testing it needs on the RTX 3070 8 GB
Nothing here has been run.
- Dump format: the game folder dumped from your own console (its files as the console
dumped them), named like
CUSA03173, with the 1.09 update copied over the base game, replacing files (eboot.binandsce_systoo). Verified: the maintainer's merged dump has that layout and passesgame_check.py(problem()returns nothing).BB_GAME_DIRpoints at it, so the dump is not copied. - Version: 1.09, title CUSA03173. The base game alone crashes at start.
- Launch:
git clone --recursive,bash build.sh(GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers, the libraries inshell.nix), thenBB_GAME_DIR=<dump> bash run.shor the GTK4 launcher. Saves, shader cache andout/go in the repo or inBB_DATA_DIR, so build it outside our repos.run.sh --softwareselects a software Vulkan driver (lavapipe), useful for tests that must not touch the GPU.probe --cpu-onlyruns the game code without the GPU library. - NVIDIA caveats from its own docs:
- The experimental PC memory model does not work properly on NVIDIA (dma-buf mapping with
an offset); keep
BB_GUEST_IN_PLACEoff, which is the default. - FSR 4.1.1 needs a Valve Vulkan extension that only some Mesa drivers expose, so on NVIDIA
expect FSR 3.1. One user reported FSR 3 working on a GTX 1060 and a black window with
FSR 4. Start with
BB_UPSCALER=fsr3ortaa; try FSR 4 afterwards. - "Live resolution changes" turns on automatically for discrete GPUs with 8 GB or more, which
includes a 3070. It costs VRAM and draws; set
live_resolution=0first. - Only an AMD RX 7800 XT on Mesa is tested thoroughly by the author. Treat the 3070 as untested until a first run.
- Unconfirmed: whether the 8 GB of VRAM is enough with the default memory model, and whether the NVIDIA proprietary driver exposes everything the renderer needs. A first run is the only way to know. It is not scheduled.
5. Can it replace a game function at an address?
Not as a general mechanism today. What exists:
gpu/shim/bbport_guest_hooks.cpppatches a handful of hard-coded sites.Patch()reads the expected bytes atImageBase + offset, refuses if they differ (so another game version is not patched), and writes0xcc(int3). A SIGTRAP handler recognizes the site by RIP and does the work in C++: it reads registers, calls a host function, and sets RIP and RSP to resume. The sites are two call sites of the game'smemcpy, an allocator's return, a collector entry and a release check.patches.bin(from XML patches) writes literal bytes at image offsets. Pattern (mask) patches are rejected. Writing a jump by hand through that is possible but the target address of our code is not known at build time.- The
hook_*functions insrc/runtime_memory.care GPU memory notifications, unrelated.
What a hook and trampoline layer would need, in a fork:
- A load-time plug-in point: after relocations and patches, before entering the game, the
loader
dlopens a game library and calls its init with the image base. About one small change inprobe.c. - An installer that reads a table of original address and replacement, verifies the original bytes (as bbport already does), and writes a jump to the replacement. The game is System V AMD64 code, so a C replacement can be jumped to directly. A 5-byte relative jump needs a near target; a 14-byte absolute jump does not; shorter functions fall back to the int3 route. The int3 route costs a signal per call and is fine only for cold functions.
- Calling the original from a replacement needs a relocated prologue (an instruction decoder). The differential tester does not need it: it can run the original in place with the hook off, and the replacement directly, on the same captured state.
- Capture hooks for
verify.py: entry and exit snapshots through the same layer.
Effort, as an estimate and not a measurement: the installer, the plug-in point and a CPU-only
harness are a few days of work for one person, using --cpu-only or lavapipe for tests. The
continuing cost is rebasing, because upstream changes daily.
6. Address conventions
Three conventions, one constant apart. Let V be the ELF p_vaddr in our converted
eboot.elf (the first PT_LOAD has p_vaddr 0).
| Convention | Address | Used by |
|---|---|---|
| ELF vaddr, "guest offset" | V | bbport's loader logs ("guest offset 0x..."), its hook sites, game_check.py |
| PS4 virtual address | V + 0x400000 | the community XML patches (shadPS4 and GoldHEN format), and patches.py (EBOOT_BASE = 0x400000, which subtracts it) |
| Host address | V + 0x800000000 | bbport's own process (ImageBase in its hooks) |
Verified against our image with read-only scripts:
- All five bbport hook sites carry the bytes its code expects at V (for example
48 83 c4 18at0x26aa255). At V + 0x400000 they do not. - The community "Performance Patch" writes
C7 45 B4 09 00 00 00at address0x0261B108. At that address minus 0x400000 our image holdsc7 45 b4 06 00 00 00, amov dword [rbp-0x4c], 6that the patch changes to 9. At the raw value it is garbage.
So the community convention is the PS4 virtual address with the eboot at 0x400000.
7. License
- bbport's
LICENSEis the GPL version 2 text. Its README says "GNU GPL v2 or later". 36 files in its own GPU shim, tools and tests carrySPDX-License-Identifier: GPL-2.0-or-later, and the vendored shadPS4 code is GPL-2.0-or-later pergpu/VENDOR.txt. - The loader and runtime in
src/and the Python inscripts/carry no per-file header. For them the README is the only statement of "or later". Worth asking the author to add SPDX headers. - Third-party parts keep their own licenses (MIT for FSR-Vulkan, FidelityFX, LibAtrac9, ImGui).
- AnyPS5 is GPL-2.0 only. Code from it could not be combined with GPL-2.0-or-later code under "or later" terms; we use none of it.
What it means for us:
- If we vendor or fork bbport, the combined work is GPL-2.0-or-later. Our code that is loaded
into its process (
game/,runtime/) should be GPL-2.0-or-later too, so both of our repositories are licensed that way. That covers our own work only. - The GPL does not grant any right in the game's code, which belongs to its owners. A functional decompilation is derived from that code. Keeping the repositories private and distributing no game content is the working rule; publishing anything is a separate decision.
Recommendation
Fork, with a minimal diff. Use bbport as the third_party/ scaffold, pinned to a commit,
as a submodule of our own fork under the private organization.
- As-is is not enough: it has no hook layer, and there is no way to add one from outside the loader.
- Neither is the worse option: writing our own loader and OS layer repeats thousands of lines that already run this exact executable (verified identical to ours), and the GPU side would have to be rewritten or vendored anyway.
- Keep the fork small: a plug-in point in
probe.c, the hook installer, a CPU-only capture and call harness. Offer the plug-in point upstream. Track upstream by rebasing. runtime/in our repository stays generic and holds our hook table and the C interface; bbport supplies the PS4 layer.
Open questions for the maintainer: when to take the first fork commit; whether to ask the bbport author about SPDX headers and the plug-in point; first run on the 3070 (not scheduled).