Link Vulkan instead of dlopening it, because a PS5 title cannot dlopen a driver

../PS5_Vulkan measured this on the console with their e2-module runner, and it
overturns the delivery route this port was built around: sceKernelLoadStartModule
refuses a linker-produced .so with ENOEXEC (0x80020008), a bare name gives ENOENT
(0x80020002), dlopen answers NULL for all twelve candidates - including modules the
process already holds - with dlerror() NULL every time, and sceKernelDlsym gives
ESRCH for every name on the modules that do load. A PS5 module image carries SCE
module parameters and an export table; a linker .so does not. So no name, path or
container fixes it, and RetroArch's dylib_load("libvulkan.so.1") can never succeed.

patches/series 0012 replaces the dylib_load/dylib_proc pair in
gfx/common/vulkan_common.c with a direct reference to the linked
vkGetInstanceProcAddr, declared locally because RetroArch's Vulkan headers carry the
PFN_ type but not the function. The frontend compiles 276 of 276 with Vulkan on and
no longer performs a dynamic load at all.

tools/build-title.sh names the sibling's four released archives (libps5vk.ps5.a,
Mesa's libvk_runtime.ps5.a, libpsbc_driver.ps5.a, libpsbc_support.ps5.a), with
PS5_VULKAN_DIR to point at a release kept elsewhere, and fails with a clear message
when one is missing rather than linking an unresolved entry point.
tools/build.sh links them --whole-archive with the sibling's
--no-dynamic-linker -z nodynamic-undefined-weak, and adds libc++.a, libc++abi.a,
libunwind.a and the compiler builtins in a --start-group, because the shader
compiler and Mesa's runtime are C++.

The link is not finished: it still reports __eh_frame_start/__eh_frame_end/
__eh_frame_hdr_start/__eh_frame_hdr_end, which come from ../PS5_Vulkan's own
tooling/psbc/ps5-pie-unwind.ld linker script, and u_thread_create and
util_barrier_init, which its tools/psbc-link.sh satisfies from its psbc runtime
group. That script names the whole recipe.

Two corrections recorded in docs/FINDINGS.md. The "console re-signs on write"
observation was wrong: the difference is this project's FTP tooling decrypting a
SELF on read (driver 16,695,760 -> 16,706,040, libc.prx 1,284,674 -> 1,335,962), and
nothing re-signs on write. And /app0 is the only path a title can see, so the /app0
dlopen fallbacks are moot - the load is refused for every path, not just the bare
name.

gates: bash tools/verify.sh unit -> PASS; the build gate is red until the link
recipe above is adopted.
This commit is contained in:
Mihawk committed 2026-09-18 22:40:22 -04:00
1 parent ff6c31205c
commit 9d29a1f6a5
4 files changed
+181 -36

No files matched your search

+53
View File
@@ -1989,3 +1989,56 @@ inside `video_driver_init_internal` before our driver is called - and the new
The candidates are therefore the graphics-context step and the `[Video] Found video
driver` lookup, not the ICD, not the dlopen path, and not the delivered object. That
is a much smaller space than "exits 1 with no message".
## A PS5 title cannot dlopen a driver, so Vulkan has to be linked - and the link is now the only gap
**Measured by ../PS5_Vulkan, on the console, with their e2-module runner.** This
overturns the delivery route this port has been built around for three sessions:
sceKernelLoadStartModule("/app0/libvulkan.so.1") -> 0x80020008 (ENOEXEC)
the same for the FSELF-wrapped, soname-as-path and control variants
"libvulkan.so.1" bare -> 0x80020002 (ENOENT)
dlopen answered NULL for all twelve candidates, including modules the process
already holds, with dlerror() NULL every time
sceKernelDlsym -> ESRCH for every name, on modules that do load
The discriminator is the module shape: a PS5 module image carries SCE module
parameters and an export table, which a linker-produced `.so` does not. So no name,
path or container fixes this, and RetroArch's `dylib_load("libvulkan.so.1")` can
never succeed here. The route that is proven on this console is their runner title's:
**link `libps5vk.ps5.a` into `eboot.bin` and call `vkGetInstanceProcAddr` as an
ordinary symbol.**
**Two corrections this forces, both recorded rather than quietly dropped.**
1. The "the console re-signs on write" observation in the previous entry is wrong.
The byte difference is this project's own FTP tooling: `RETR` decrypts a SELF on
read (the driver 16,695,760 -> 16,706,040, and `libc.prx` 1,284,674 ->
1,335,962). Nothing re-signs on write, and the title sees the staged containers
at their staged sizes. The `.so` marker verification added on the strength of
that claim is harmless but was justified by the wrong reason.
2. `/app0` and `/app0/sce_module` are the only paths a title can see; `/data/homebrew/`
and `/temp0` do not exist inside a title. The `/app0` dlopen fallbacks are
therefore moot - the load is refused for every path, not just the bare name.
**What is in place now, and it is most of the work.** `patches/series` 0012 replaces
RetroArch's `dylib_load`/`dylib_proc` pair with a direct reference to the linked
`vkGetInstanceProcAddr` (with a local prototype, because RetroArch's Vulkan headers
declare the `PFN_` type but not the function). The frontend compiles **276 of 276**
with Vulkan on and no longer contains a dynamic load. `tools/build-title.sh` names
the sibling's four released archives - `libps5vk.ps5.a`, Mesa's
`libvk_runtime.ps5.a`, `libpsbc_driver.ps5.a`, `libpsbc_support.ps5.a` - and
`PS5_VULKAN_DIR` overrides their root; `tools/build.sh` links them whole
(`--whole-archive`) with the sibling's `--no-dynamic-linker
-z nodynamic-undefined-weak`, and adds `libc++.a`, `libc++abi.a`, `libunwind.a` and
the compiler builtins in a `--start-group`, because the shader compiler and Mesa's
runtime are C++.
**What the link still wants, and it is a short list.** With all of the above the
linker reports `__eh_frame_start`, `__eh_frame_end`, `__eh_frame_hdr_start`,
`__eh_frame_hdr_end`, `u_thread_create` and `util_barrier_init`. The first four come
from ../PS5_Vulkan's own linker script, `tooling/psbc/ps5-pie-unwind.ld`, which its
`tools/psbc-link.sh` passes with `-T`; the last two are Mesa's threading util, which
that script satisfies from its psbc runtime group. So the remaining work is to adopt
that link recipe rather than to discover anything: archives, the unwind script, and
the group ordering, all named in their `tools/psbc-link.sh`.
+55 -36
View File
@@ -134,51 +134,70 @@ EDITS = [
" &input_ps5,",
),
(
# Vulkan is loaded from the title's own folder, because the console's
# loader does not search it.
# Vulkan is linked, not loaded: a PS5 title cannot dlopen a driver.
#
# RetroArch does not link Vulkan; it dlopens by bare name
# (gfx/common/vulkan_common.c):
# RetroArch obtains every Vulkan entry point through one symbol,
# vkGetInstanceProcAddr, which it fetches by dlopen of "libvulkan.so.1".
# That cannot work in a title on this console, and ../PS5_Vulkan measured
# it rather than assumed it - their e2-module runner asked the console's
# loader directly:
#
# vulkan_library = dylib_load("libvulkan.so.1");
# if (!vulkan_library)
# vulkan_library = dylib_load("libvulkan.so");
# sceKernelLoadStartModule("/app0/libvulkan.so.1") -> 0x80020008 (ENOEXEC)
# the same for FSELF-wrapped, soname-as-path and control variants
# "libvulkan.so.1" bare -> 0x80020002 (ENOENT)
# dlopen answered NULL for all twelve candidates, including modules the
# process already holds, with dlerror() NULL every time
# sceKernelDlsym -> ESRCH for every name on the modules that do load
#
# A bare dlopen resolves through the loader's search path, which on this
# console is the system module directories - not the folder the title was
# started from. So the driver ../PS5_Vulkan delivers is invisible to it
# even when it sits beside eboot.bin, and the failure is silent: the title
# returns 1 with no line in its log, because a failed dylib_load logs
# nothing. That is the shape the Vulkan build has failed in all along.
# A linker-produced .so is not a PS5 module image (it has no SCE module
# parameters and no export table), so no name or path fixes this. The
# route that is proven on this console is the one their runner title uses:
# link libps5vk.ps5.a and call the entry point as an ordinary symbol.
#
# The title folder is mounted at /app0, so naming it absolutely is what
# makes the delivered object reachable. The bare names are tried first, so
# a future table that does search the app directory still wins and this
# changes nothing.
# So the loader path is replaced by a direct reference. Everything below
# this point in RetroArch is unchanged: it still goes through
# vkGetInstanceProcAddr for every other entry point, which is exactly what
# the driver expects (it exports the loader-facing spelling).
#
# The archives this needs are the sibling's released set, named in
# tools/build-title.sh: libps5vk.ps5.a, Mesa's libvk_runtime.ps5.a, the
# shader compiler libpsbc_driver.ps5.a, and libpsbc_support.ps5.a.
"gfx/common/vulkan_common.c",
" vulkan_library = dylib_load(\"libvulkan.so.1\");\n"
" if (!vulkan_library)\n"
" vulkan_library = dylib_load(\"libvulkan.so\");\n",
" /* Fallbacks added by this port (patches/series, 0011): the title's\n"
" * own folder, which the console mounts at /app0. */\n"
"#else\n"
" vulkan_library = dylib_load(\"libvulkan.so.1\");\n"
" if (!vulkan_library)\n"
" vulkan_library = dylib_load(\"libvulkan.so\");\n"
"#endif\n"
" }\n"
"\n"
" if (!vulkan_library)\n"
" {\n"
" RARCH_ERR(\"[Vulkan] Failed to open Vulkan loader.\\n\");\n"
" return false;\n"
" }\n"
"\n"
" RARCH_LOG(\"[Vulkan] Vulkan dynamic library loaded.\\n\");\n"
"\n"
" GetInstanceProcAddr =\n"
" (PFN_vkGetInstanceProcAddr)dylib_proc(vulkan_library, \"vkGetInstanceProcAddr\");\n",
"#else\n"
" vulkan_library = dylib_load(\"libvulkan.so.1\");\n"
" if (!vulkan_library)\n"
" vulkan_library = dylib_load(\"/app0/libvulkan.so.1\");\n"
" if (!vulkan_library)\n"
" vulkan_library = dylib_load(\"/app0/libvulkan.so\");\n"
" if (!vulkan_library)\n"
" vulkan_library = dylib_load(\"/app0/sce_module/libvulkan.so.1\");\n"
" /* A failed load is silent in upstream - dylib_load only returns NULL -\n"
" * and a silent Vulkan failure is what made this path undiagnosable for\n"
" * three sessions. The four names and the loader's own message go to the\n"
" * log so a console run says which path was tried and why it failed. */\n"
" if (!vulkan_library)\n"
" RARCH_ERR(\"[Vulkan] libvulkan not loadable; tried libvulkan.so.1, "
"libvulkan.so, /app0/libvulkan.so.1, /app0/libvulkan.so, "
"/app0/sce_module/libvulkan.so.1: %s\\n\", dylib_error());\n",
"/app0/libvulkan.so.1",
" vulkan_library = dylib_load(\"libvulkan.so\");\n"
"#endif\n"
" }\n"
"\n"
" /* Changed by this port (patches/series, 0012): this console refuses a\n"
" * title's dlopen, so the entry point is linked, not loaded. It is the\n"
" * same symbol the loader path looked up, taken from ../PS5_Vulkan's\n"
" * libps5vk.ps5.a, which tools/build-title.sh links into the title. The\n"
" * declaration is local because the Vulkan headers RetroArch carries\n"
" * declare the PFN_ type but not the function. */\n"
" extern VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL\n"
" vkGetInstanceProcAddr(VkInstance instance, const char *pName);\n"
" GetInstanceProcAddr =\n"
" (PFN_vkGetInstanceProcAddr)vkGetInstanceProcAddr;\n",
"(PFN_vkGetInstanceProcAddr)vkGetInstanceProcAddr",
),
(
# A core that has not loaded registers no controller-port callback, and
+38
View File
@@ -93,6 +93,42 @@ echo "==> [title] compiling src/ with ${#title_definition_names[@]} frontend def
# cannot disagree about what this build is.
cp -f -- "$root/build/ra-conf/config.h" "$root/build/config.h"
# The Vulkan driver is linked, not loaded. ../PS5_Vulkan measured that a PS5 title
# cannot dlopen a driver (sceKernelLoadStartModule refuses a linker-produced .so
# with ENOEXEC, a bare name gives ENOENT, dlopen answers NULL for every candidate
# and sceKernelDlsym gives ESRCH even for modules the process holds), so RetroArch's
# dlopen of "libvulkan.so.1" can never succeed here. The route proven on this
# console is their runner title's: link the driver and call its entry point as an
# ordinary symbol.
#
# Their released set, exactly as tools/build.sh links it for a driver-enabled title:
# libps5vk.ps5.a the driver (build/driver/ps5/)
# libvk_runtime.ps5.a Mesa's Vulkan runtime (.deps/native/vulkan-runtime/lib/)
# libpsbc_driver.ps5.a the shader compiler (build/driver/ps5/)
# libpsbc_support.ps5.a the package writer (.deps/native/psbc/lib/)
# PS5_VULKAN_DIR overrides the sibling's root, so a release kept elsewhere works.
vulkan_dir="${PS5_VULKAN_DIR:-$root/../PS5_Vulkan}"
vulkan_archives=(
"$vulkan_dir/build/driver/ps5/libps5vk.ps5.a"
"$vulkan_dir/.deps/native/vulkan-runtime/lib/libvk_runtime.ps5.a"
"$vulkan_dir/build/driver/ps5/libpsbc_driver.ps5.a"
"$vulkan_dir/.deps/native/psbc/lib/libpsbc_support.ps5.a"
)
vulkan_missing=()
for archive in "${vulkan_archives[@]}"; do
[[ -f $archive ]] || vulkan_missing+=("$archive")
done
if (( ${#vulkan_missing[@]} )); then
printf 'error: the Vulkan driver archives are missing; the title would link with\n' >&2
printf ' vkGetInstanceProcAddr unresolved. Build them in ../PS5_Vulkan\n' >&2
printf ' (tools/build-driver.sh) or set PS5_VULKAN_DIR.\n' >&2
printf ' missing: %s\n' "${vulkan_missing[@]}" >&2
exit 2
fi
# Mesa's weak entry points resolve at link time, and the driver's own symbols must
# survive the archive boundary (--whole-archive), which is how the sibling links it.
vulkan_flags="--no-dynamic-linker -z nodynamic-undefined-weak"
echo "==> [title] step 2/3: the title"
PS5_PAYLOAD_SDK="$sdk" \
PS5_CLANG=/usr/bin/clang \
@@ -100,6 +136,8 @@ PYTHONPATH="$root/tooling/pystub${PYTHONPATH:+:$PYTHONPATH}" \
APP_DEFINITIONS="${title_definition_names[*]}" \
APP_INCLUDE_PATHS="build/ra-conf build vendor/retroarch vendor/retroarch/libretro-common/include vendor/retroarch/deps vendor/retroarch/deps/stb" \
APP_STATIC_ARCHIVES="build/ra/libretroarch.a" \
APP_VULKAN_ARCHIVES="${vulkan_archives[*]}" \
APP_LINK_FLAGS="$vulkan_flags" \
make app
title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' \
+35
View File
@@ -209,6 +209,40 @@ agc_driver_stub=$(build_system_link_stub libSceAgcDriver vendor/ps5/sdk/stubs/ag
link_inputs=("$build/obj/app_crt.o" "$build/obj/app_cpp_runtime.o" "${objects[@]}")
link_inputs+=("$agc_stub" "$agc_driver_stub")
# The Vulkan driver, linked whole. These archives are the reason this title does
# not dlopen: a PS5 title cannot (../PS5_Vulkan measured ENOEXEC/ENOENT/NULL from
# sceKernelLoadStartModule and dlopen, and ESRCH from sceKernelDlsym), so the
# driver's entry points are ordinary symbols here. --whole-archive keeps its
# command tables, and the two flags are the sibling's, for Mesa's weak entry
# points (tools/build.sh there links its runner title the same way).
if [[ -n ${APP_VULKAN_ARCHIVES:-} ]]; then
read -r -a vulkan_archives <<< "$APP_VULKAN_ARCHIVES"
for archive in "${vulkan_archives[@]}"; do
[[ $archive =~ ^[A-Za-z0-9_./+-]+(/[A-Za-z0-9_.+-]+)*\.a$ && -f $archive ]] || {
echo "invalid Vulkan archive: $archive" >&2; exit 2;
}
done
link_inputs+=(--whole-archive "${vulkan_archives[@]}" --no-whole-archive)
# The shader compiler and Mesa's runtime are C++, so they need libc++,
# libc++abi and libunwind - the same three the sibling project links, plus the
# compiler's own builtins archive. They are grouped because the dependency runs
# both ways between them.
vulkan_cxx=(
"$sdk_root/target/lib/libc++.a"
"$sdk_root/target/lib/libc++abi.a"
"$sdk_root/target/lib/libunwind.a"
)
builtins="$("$PS5_CLANG" --print-resource-dir 2>/dev/null)/lib/linux/libclang_rt.builtins-x86_64.a"
[[ -f $builtins ]] || builtins=""
for archive in "${vulkan_cxx[@]}"; do
[[ -f $archive ]] || {
echo "missing Vulkan C++ runtime archive: $archive" >&2; exit 2;
}
done
link_inputs+=(-L "$sdk_root/target/lib" --start-group "${vulkan_cxx[@]}")
[[ -n $builtins ]] && link_inputs+=("$builtins")
link_inputs+=(--end-group)
fi
for archive in "${archives[@]}"; do
[[ $archive =~ ^[A-Za-z0-9_.-]+(/[A-Za-z0-9_.-]+)*\.a$ && -f $root/$archive ]] || {
echo "invalid static archive: $archive" >&2; exit 2;
@@ -219,6 +253,7 @@ if (( ${#pacbrew_libs[@]} > 0 )); then
link_inputs+=(--start-group "${pacbrew_libs[@]}" --end-group)
fi
"$sdk_root/bin/prospero-lld" -T "$native/ps5-pie.ld" --eh-frame-hdr \
${APP_LINK_FLAGS:-} \
--version-script "$native/app-symbols.map" \
--exclude-libs=ALL -L "$build/obj" \
-e _start -o "$build/llvm-pie.elf" "${link_inputs[@]}" \