Byrgenwerthnotes of Paleblood, rebuilding Bloodborne one verified function at a time

Last updated: October 7, 2026

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.

  1. 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.
  2. parse_self rebuilds an ELF header, program headers and segment contents from the SELF's blocked segments.
  3. The loadable segments (PT_LOAD and the Sony 0x61000010) are copied to their p_vaddr into one flat buffer sized by the highest p_vaddr + p_memsz. That buffer is "the image". Image offset 0 is p_vaddr 0.
  4. scripts/game_check.py pins the SHA-256 of that image to one value. Anything else (base game 1.00, another region or edition) is refused with an explanation.
  5. The dynamic data (PT_DYNAMIC and the Sony dynlib data segment 0x61000000) is parsed: symbol table, string table and both relocation tables.
  6. Base-relative relocations are kept as such.
  7. Relocations against symbols the image defines are resolved to offsets.
  8. 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.
  9. link_modules.py places libc.prx and libSceFios2.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.
  10. The output is out/boot-linked.bin (a small custom format: header, load descriptors, 128-byte import names, relocations, the image), a reconstructed eboot.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_core and shader_recompiler (the renderer and the GCN to SPIR-V recompiler), common, and from the libraries only gnmdriver, videoout, parts of avplayer, videodec utilities and a few kernel and system headers.
  • Externals: gcn, sirit, half.
  • About 108k lines under gpu/shadps4, with around 290 bbport: 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.bin and sce_sys too). Verified: the maintainer's merged dump has that layout and passes game_check.py (problem() returns nothing). BB_GAME_DIR points 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 in shell.nix), then BB_GAME_DIR=<dump> bash run.sh or the GTK4 launcher. Saves, shader cache and out/ go in the repo or in BB_DATA_DIR, so build it outside our repos. run.sh --software selects a software Vulkan driver (lavapipe), useful for tests that must not touch the GPU. probe --cpu-only runs 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_PLACE off, 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=fsr3 or taa; 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=0 first.
  • 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.cpp patches a handful of hard-coded sites. Patch() reads the expected bytes at ImageBase + offset, refuses if they differ (so another game version is not patched), and writes 0xcc (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's memcpy, 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 in src/runtime_memory.c are GPU memory notifications, unrelated.

What a hook and trampoline layer would need, in a fork:

  1. 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 in probe.c.
  2. 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.
  3. 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.
  4. 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 18 at 0x26aa255). At V + 0x400000 they do not.
  • The community "Performance Patch" writes C7 45 B4 09 00 00 00 at address 0x0261B108. At that address minus 0x400000 our image holds c7 45 b4 06 00 00 00, a mov dword [rbp-0x4c], 6 that 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 LICENSE is the GPL version 2 text. Its README says "GNU GPL v2 or later". 36 files in its own GPU shim, tools and tests carry SPDX-License-Identifier: GPL-2.0-or-later, and the vendored shadPS4 code is GPL-2.0-or-later per gpu/VENDOR.txt.
  • The loader and runtime in src/ and the Python in scripts/ 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).