Files
Mrsuss60 ed73550c85 gpu, scripts: resolve startup stalls via async pipeline warmup and cached pre-launch
Eliminate multi-minute boot delays across pre-window launch and initial black screen presentation:

- Vulkan (vk_pipeline_serialization): Dispatch cached graphics pipelines asynchronously to worker threads via pending_graphics during WarmUp, removing the synchronous driver compilation stall on the render thread before the first frame.
- Vulkan (vk_shader_rebuild): Protect RebuildDirectory on Windows with Vectored Exception Handling to safely bypass incompatible shaders, and persist ShaderBinaryVersion in profile.bin on failure to stop repeating broken rebuilds every launch.
- Vulkan (vk_pipeline_import): Cap startup shader recipe merges to a 5 ms budget, deferring remaining imports to per-frame polls.
- Host Runtime (probe.c): Defer bbgpu_rebuild_shader_caches() until after bbgpu_init() so the SDL window and presenter open immediately.
- Pre-launch Scripts (prepare, link_libc, link_modules): Add mtime checks against source binaries to reuse existing boot images, and remove the redundant 28,000-file traversal in prepare.py.
- Platform (nvidia_profile): Add a single-run guard to BbNvidiaProfile::Apply() to prevent redundant NVAPI driver queries.
2026-11-02 17:47:31 +00:00

276 lines
14 KiB
Python

"""Link the eboot with the game's bundled system modules into one probe image.
Successor of link_libc.py for several modules (default: libc.prx, then
libSceFios2.prx). Every module is placed after the previous one; imports of
all modules share one import table, so the C runtime can override any
function (host contracts first) and otherwise binds the native export.
Exports are matched by (NID, library identity, module identity). Imports of
libSceLibcInternal are served by libc.prx's same-NID exports: the internal
library is not shipped with the game and exposes the same functions.
Output: out/boot-linked.bin (format BBPROBE5) and out/link.json.
"""
import collections
import hashlib
import json
from pathlib import Path
import struct
import sys
from prepare import parse_self, span, unpack
from link_libc import encode_id
DEFAULT_MODULES = ('libc.prx', 'libSceFios2.prx')
FS_LOAD = bytes.fromhex('64488b042500000000') # mov rax, fs:[0]
def module(path):
source = path.read_bytes()
elf, header, ph, _, missing = parse_self(source)
dp = next(p for p in ph if p['type'] == 2)
dynamic = []
for pos in range(dp['offset'], dp['offset'] + dp['filesz'], 16):
tag, value = unpack('<QQ', elf, pos)
if not tag:
break
dynamic.append((tag, value))
tags = dict(dynamic)
lp = next(p for p in ph if p['type'] == 0x61000000)
blob = span(elf, lp['offset'], lp['filesz'])
strings = span(blob, tags[0x61000035], tags[0x61000037])
def string(offset):
span(strings, offset, 1)
return strings[offset:strings.index(0, offset)].decode('ascii')
libraries, modules = {}, {}
for tag, value in dynamic:
if tag in (0x61000013, 0x61000015, 0x6100000d, 0x6100000f):
table = libraries if tag in (0x61000013, 0x61000015) else modules
key = encode_id(value >> 48)
identity = (string(value & 0xffffffff), (value >> 32) & 65535)
if key in table and table[key] != identity:
raise ValueError('conflicting module/library IDs')
table[key] = identity
def identity(name):
parts = name.split('#')
if len(parts) != 3:
return None # plain names such as module_start are not linkable
n, lib, mod = parts
return (n, libraries[lib], modules[mod])
symbols = []
table = span(blob, tags[0x61000039], tags[0x6100003f])
for pos in range(0, len(table), 24):
n, info, other, section, value, size = unpack('<IBBHQQ', table, pos)
name = string(n)
symbols.append(dict(name=name, type=info & 15, binding=info >> 4, section=section,
value=value, size=size, identity=identity(name) if name else None))
relocations = []
for ot, st in ((0x61000029, 0x6100002d), (0x6100002f, 0x61000031)):
table = span(blob, tags[ot], tags[st])
for pos in range(0, len(table), 24):
target, info, addend = unpack('<QQq', table, pos)
relocations.append((target, info & 0xffffffff, info >> 32, addend))
return dict(elf=elf, header=header, ph=ph, tags=tags, symbols=symbols, relocs=relocations,
identity=identity, libraries=libraries, modules=modules,
sha256=hashlib.sha256(source).hexdigest(), missing=missing)
def patch_fs_loads(image, ph, base):
"""Rewrite initial-exec `mov rax, fs:[0]` to GS: glibc owns FS on Linux."""
if sys.platform == 'win32':
return 0
patched = 0
for p in ph:
if p['type'] != 1 or not p['flags'] & 1:
continue
start, end = base + p['vaddr'], base + p['vaddr'] + p['filesz']
at = image.find(FS_LOAD, start, end)
while at >= 0:
image[at] = 0x65
patched += 1
at = image.find(FS_LOAD, at + len(FS_LOAD), end)
return patched
def link(game, out, module_names=DEFAULT_MODULES):
main_eboot = game / 'eboot.bin' if (game / 'eboot.bin').is_file() else game / 'eboot.elf'
boot_linked = out / 'boot-linked.bin'
link_json = out / 'link.json'
boot_bin = out / 'boot.bin'
if boot_linked.is_file() and link_json.is_file() and boot_bin.is_file():
m = boot_linked.stat().st_mtime
if m >= boot_bin.stat().st_mtime and m >= Path(__file__).stat().st_mtime:
try:
rep = json.loads(link_json.read_text(encoding='utf-8', errors='replace'))
table = rep.get('modules', [])
summary = ', '.join(f"{t.get('file', 'mod')}@{t.get('base', '0')}" for t in table)
bindings = rep.get('bindings', 0)
unresolved = len(rep.get('unresolved_imports', []))
fs_patched = rep.get('fs_loads_patched', 0)
print(f'Linked modules: {summary}; {bindings} native bindings, {unresolved} imports left to the host runtime, '
f'fs->gs patched={fs_patched}')
return
except Exception:
pass
main = module(main_eboot)
raw = (out / 'boot.bin').read_bytes()
magic, size, entry, ns, nr, ni, flags = unpack('<8s6Q', raw, 0)
if magic != b'BBPROBE2':
raise ValueError('linker expects freshly prepared BBPROBE2')
pos = 56
segments = [unpack('<3Q', raw, pos + i * 24) for i in range(ns)]
pos += ns * 24
names = [span(raw, pos + i * 128, 128).split(b'\0', 1)[0].decode() for i in range(ni)]
pos += ni * 128
relocs = [unpack('<QQqq', raw, pos + i * 32) for i in range(nr)]
pos += nr * 32
image = bytearray(span(raw, pos, size))
if pos + size != len(raw):
raise ValueError('unexpected boot trailer')
identities = {main['identity'](name): i for i, name in enumerate(names)}
main_libraries = {identity: key for key, identity in main['libraries'].items()}
main_modules = {identity: key for key, identity in main['modules'].items()}
def imported(symbol):
key = symbol['identity']
if key not in identities:
n, lib, mod = key
# Main-program spelling when the identity matches (existing host
# tables use it); otherwise NID#library for new libraries.
name = (f'{n}#{main_libraries[lib]}#{main_modules[mod]}'
if lib in main_libraries and mod in main_modules else f'{n}#{lib[0]}')
identities[key] = len(names)
names.append(name)
return identities[key]
exports, by_nid = {}, collections.defaultdict(list)
table = []
base = (size + 65535) & ~65535
fs_patched = patch_fs_loads(image, main['ph'], 0)
tls_module = 2
for filename in module_names:
m = module(game / 'sce_module' / filename)
loads = [p for p in m['ph'] if p['type'] in (1, 0x61000010)]
modsize = max(p['vaddr'] + p['memsz'] for p in loads)
if base + modsize > 512 * 1024 * 1024:
raise ValueError('linked image exceeds probe limit')
image.extend(bytes(base + modsize - len(image)))
def mapped(address, length=8, loads=loads):
return any(p['vaddr'] <= address and address + length <= p['vaddr'] + p['memsz'] for p in loads)
for p in loads:
if p['filesz'] > p['memsz']:
raise ValueError(f'{filename}: invalid segment size')
image[base + p['vaddr']:base + p['vaddr'] + p['filesz']] = span(m['elf'], p['offset'], p['filesz'])
segments.append((base + p['vaddr'], p['memsz'], p['flags']))
tls = next((p for p in m['ph'] if p['type'] == 7), None)
module_id = 0
if tls:
if tls['filesz'] > tls['memsz'] or not mapped(tls['vaddr'], tls['memsz']):
raise ValueError(f'{filename}: unsupported TLS layout')
module_id = tls_module
tls_module += 1
tls_relocations = 0
for target, kind, symid, addend in m['relocs']:
if not mapped(target):
raise ValueError(f'{filename}: relocation outside image {target:#x}')
target += base
if kind == 8:
relocs.append((target, 0, base + addend, 0))
elif kind == 16:
symbol = m['symbols'][symid]
if symbol['name'] or symbol['type'] != 3 or addend or not module_id:
raise ValueError(f'{filename}: unsupported external TLS relocation')
struct.pack_into('<Q', image, target, module_id)
tls_relocations += 1
elif kind in (1, 6, 7):
symbol = m['symbols'][symid]
if symbol['section']:
if symbol['section'] == 0xfff1:
struct.pack_into('<Q', image, target, symbol['value'] + addend)
else:
relocs.append((target, 0, base + symbol['value'] + addend, 0))
elif not symbol['identity'] and symbol['binding'] == 2:
struct.pack_into('<Q', image, target, 0) # weak undefined (module_start/stop)
else:
rk = 2 if symbol['type'] == 1 else 1
if (rk == 1 and addend) or not 0 <= addend < 4096:
raise ValueError(f'{filename}: unsupported import addend')
relocs.append((target, rk, imported(symbol), addend))
else:
raise ValueError(f'{filename}: unsupported relocation type {kind}')
count = 0
for s in m['symbols']:
if s['section'] and s['binding'] in (1, 2) and s['identity'] and s['type'] in (1, 2):
if not mapped(s['value'], max(1, s['size'])):
raise ValueError(f'{filename}: export outside load segments')
if s['identity'] in exports:
raise ValueError(f'{filename}: ambiguous export')
entry_value = (base + s['value'], 2 if s['type'] == 1 else 1, filename)
exports[s['identity']] = entry_value
by_nid[(s['identity'][0], s['identity'][1][0])].append(entry_value)
count += 1
fs_patched += patch_fs_loads(image, m['ph'], base)
table.append(dict(file=filename, base=base, size=modsize, init=base + m['tags'].get(12, 0),
tls_address=base + tls['vaddr'] if tls else 0, tls_memsz=tls['memsz'] if tls else 0,
tls_filesz=tls['filesz'] if tls else 0, tls_module=module_id,
tls_relocations=tls_relocations, exports=count, sha256=m['sha256']))
base = (base + modsize + 65535) & ~65535
bindings, unresolved = [], []
for identity, index in identities.items():
found = exports.get(identity)
if not found and identity and identity[1][0] == 'libSceLibcInternal':
candidates = by_nid.get((identity[0], 'libc'), [])
found = candidates[0] if len(candidates) == 1 else None
if found:
bindings.append((index, found[0], found[1]))
else:
unresolved.append(names[index])
main_tls = next((p for p in main['ph'] if p['type'] == 7), None)
main_tls_values = (main_tls['vaddr'], main_tls['filesz'], main_tls['memsz'], main_tls['align']) if main_tls else (0, 0, 0, 0)
if main_tls and (main_tls['filesz'] > main_tls['memsz'] or main_tls['memsz'] > 1024 * 1024
or main_tls['vaddr'] + main_tls['filesz'] > size):
raise ValueError('unsupported eboot TLS layout')
procparam = next(p for p in main['ph'] if p['type'] == 0x61000001)
with (out / 'boot-linked.bin').open('wb') as f:
f.write(struct.pack('<8s6Q', b'BBPROBE5', len(image), entry, len(segments), len(relocs), len(names), flags))
f.write(struct.pack('<Q', procparam['vaddr']))
f.write(struct.pack('<4Q', *main_tls_values))
f.write(struct.pack('<Q', len(table)))
for t in table:
f.write(struct.pack('<7Q', t['base'], t['size'], t['init'], t['tls_address'], t['tls_memsz'],
t['tls_filesz'], t['tls_module']))
f.write(struct.pack('<Q', len(bindings)))
for binding in bindings:
f.write(struct.pack('<3Q', *binding))
for segment in segments:
f.write(struct.pack('<3Q', *segment))
for name in names:
if len(name.encode()) >= 128:
raise ValueError('name too long')
f.write(name.encode().ljust(128, b'\0'))
for relocation in relocs:
f.write(struct.pack('<QQqq', *relocation))
f.write(image)
report = dict(modules=[{k: (hex(v) if k in ('base', 'init', 'tls_address') else v) for k, v in t.items()} for t in table],
bindings=len(bindings), imports=len(names), fs_loads_patched=fs_patched,
main_tls=dict(zip(('vaddr', 'filesz', 'memsz', 'align'), main_tls_values)),
unresolved_imports=unresolved)
(out / 'link.json').write_text(json.dumps(report, indent=2) + '\n')
summary = ', '.join(f"{t['file']}@{t['base']:#x}" for t in table)
print(f'Linked modules: {summary}; {len(bindings)} native bindings, {len(unresolved)} imports left to the host runtime, '
f'fs->gs patched={fs_patched}')
if __name__ == '__main__':
import argparse
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('game', type=Path)
p.add_argument('--out', type=Path, default=Path(__file__).resolve().parent.parent / 'out')
p.add_argument('--modules', nargs='*', default=list(DEFAULT_MODULES))
a = p.parse_args()
link(a.game, a.out, a.modules)