diff --git a/.gitignore b/.gitignore index 5d9f719..075c1d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /build_linux +/build_linux_softfp /build_mingw /.idea /build __pycache__/ +/softfp-shim/wrappers diff --git a/CMakeLists.txt b/CMakeLists.txt index 299078d..6ecb936 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,47 @@ list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") # Toolchain arch target set(target_arch arm-vita-eabi) + +# Float ABI baked into the toolchain: a build parameter, never a branch (see +# PLAN-softfp.md). Selects --with-float in the GCC configure; world_arch is +# the identity stamped into the core (pacman Architecture, makepkg CARCH, +# version_info.txt) so a softfp core can never be mistaken for the default one. +set(VITASDK_FLOAT_ABI "hard" CACHE STRING + "Float ABI baked into the toolchain: hard (default) or softfp") +set_property(CACHE VITASDK_FLOAT_ABI PROPERTY STRINGS hard softfp) + +# A profile names a world, and the world decides the ABI rather than the other +# way round: CI asks for a profile because that is what the lock carries, and +# describe already refuses one this repository does not publish. +# +# A profile wins over VITASDK_FLOAT_ABI, and does so silently. CMake cannot +# tell a cache entry left at its default from one somebody passed, so a check +# that the two agree would either miss -DVITASDK_FLOAT_ABI=hard or refuse +# every re-configure of an existing build directory. Passing both is passing +# the same thing twice; the profile is the one that says which world this is. +include(Profiles) +set(VITASDK_PROFILE "" CACHE STRING "World to build; selects the float ABI") +if(VITASDK_PROFILE) + if(NOT VITASDK_PROFILE IN_LIST VITASDK_PROFILES) + message(FATAL_ERROR + "unknown profile '${VITASDK_PROFILE}'; this tree publishes: ${VITASDK_PROFILES}") + endif() + set(profile_float_abi ${VITASDK_PROFILE_FLOAT_ABI_${VITASDK_PROFILE}}) + if(NOT profile_float_abi) + message(FATAL_ERROR "profile '${VITASDK_PROFILE}' declares no float ABI") + endif() + set(VITASDK_FLOAT_ABI ${profile_float_abi} CACHE STRING "" FORCE) +endif() + +if(NOT VITASDK_FLOAT_ABI STREQUAL "hard" AND NOT VITASDK_FLOAT_ABI STREQUAL "softfp") + message(FATAL_ERROR "VITASDK_FLOAT_ABI must be 'hard' or 'softfp', got '${VITASDK_FLOAT_ABI}'") +endif() +if(VITASDK_FLOAT_ABI STREQUAL "softfp") + set(world_arch "vita-softfp") +else() + set(world_arch "vita") +endif() + # Build date string(TIMESTAMP build_date "%Y-%m-%d_%H-%M-%S") string(TIMESTAMP default_package_version "0.%Y%m%d.%H%M%S" UTC) diff --git a/cmake/Profiles.cmake b/cmake/Profiles.cmake index 981b42e..ca3f557 100644 --- a/cmake/Profiles.cmake +++ b/cmake/Profiles.cmake @@ -2,3 +2,9 @@ include_guard(GLOBAL) # Published profile (world) names; describe rejects anything else. set(VITASDK_PROFILES vita vita-softfp) + +# The float ABI each profile bakes into the toolchain. A profile is a world and +# a world is named by the architecture its packages carry, so the name is the +# same on both sides of the build. +set(VITASDK_PROFILE_FLOAT_ABI_vita hard) +set(VITASDK_PROFILE_FLOAT_ABI_vita-softfp softfp) diff --git a/cmake/WriteMakepkgConf.cmake b/cmake/WriteMakepkgConf.cmake new file mode 100644 index 0000000..dbcad89 --- /dev/null +++ b/cmake/WriteMakepkgConf.cmake @@ -0,0 +1,19 @@ +# +# Stamps the world's CARCH into the makepkg.conf shipped inside the SDK. +# vita-makepkg's own makepkg.conf.sample always says CARCH="vita": this is +# the one place that turns it into the world the core was actually built for. +# + +if(NOT DEFINED INPUT OR NOT DEFINED OUTPUT OR NOT DEFINED WORLD_ARCH) + message(FATAL_ERROR "INPUT, OUTPUT and WORLD_ARCH are required") +endif() + +file(READ "${INPUT}" contents) +string(REGEX REPLACE "\nCARCH=\"[^\"]*\"\n" "\nCARCH=\"${WORLD_ARCH}\"\n" + contents "${contents}") + +get_filename_component(output_directory "${OUTPUT}" DIRECTORY) +file(MAKE_DIRECTORY "${output_directory}") +set(temporary "${OUTPUT}.tmp") +file(WRITE "${temporary}" "${contents}") +file(RENAME "${temporary}" "${OUTPUT}") diff --git a/cmake/WritePacmanConfig.cmake b/cmake/WritePacmanConfig.cmake index eed652a..a96f417 100644 --- a/cmake/WritePacmanConfig.cmake +++ b/cmake/WritePacmanConfig.cmake @@ -1,5 +1,5 @@ -if(NOT DEFINED OUTPUT OR NOT DEFINED HOST_ARCHITECTURE) - message(FATAL_ERROR "OUTPUT and HOST_ARCHITECTURE are required") +if(NOT DEFINED OUTPUT OR NOT DEFINED HOST_ARCHITECTURE OR NOT DEFINED WORLD_ARCH) + message(FATAL_ERROR "OUTPUT, HOST_ARCHITECTURE and WORLD_ARCH are required") endif() get_filename_component(output_directory "${OUTPUT}" DIRECTORY) @@ -7,7 +7,7 @@ file(MAKE_DIRECTORY "${output_directory}") set(temporary "${OUTPUT}.tmp") file(WRITE "${temporary}" "[options]\n" - "Architecture = ${HOST_ARCHITECTURE} vita\n" + "Architecture = ${HOST_ARCHITECTURE} ${WORLD_ARCH}\n" "# vdpm verifies signed channel metadata and the selected database hash before use.\n" "SigLevel = Never\n") file(RENAME "${temporary}" "${OUTPUT}") diff --git a/cmake/create_version.cmake b/cmake/create_version.cmake index 3cdbde1..adb60af 100644 --- a/cmake/create_version.cmake +++ b/cmake/create_version.cmake @@ -7,6 +7,7 @@ file(READ ${INPUT_DIR}/pthread-embedded-version.txt _pthread_sha1) file(READ ${INPUT_DIR}/samples-version.txt _samples_sha1) file(WRITE ${OUTPUT_FILE} "Built at ${BUILD_DATE}\n") +file(APPEND ${OUTPUT_FILE} "world ${WORLD_ARCH} (float-abi=${VITASDK_FLOAT_ABI})\n") file(APPEND ${OUTPUT_FILE} "newlib ${_newlib_sha1}") file(APPEND ${OUTPUT_FILE} "pthread-embedded ${_pthread_sha1}") file(APPEND ${OUTPUT_FILE} "samples ${_samples_sha1}") diff --git a/cmake/recipes/BuildHostTools.cmake b/cmake/recipes/BuildHostTools.cmake index cc1287f..b1a07cd 100644 --- a/cmake/recipes/BuildHostTools.cmake +++ b/cmake/recipes/BuildHostTools.cmake @@ -79,6 +79,10 @@ ExternalProject_Add(vita-makepkg INSTALL_COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_INSTALL_PREFIX}/bin/ COMMAND ${CMAKE_COMMAND} -E copy_directory /libmakepkg ${CMAKE_INSTALL_PREFIX}/bin/libmakepkg COMMAND ${CMAKE_COMMAND} -E copy /vita-makepkg ${CMAKE_INSTALL_PREFIX}/bin/ - COMMAND ${CMAKE_COMMAND} -E copy /makepkg.conf.sample ${CMAKE_INSTALL_PREFIX}/bin/makepkg.conf + COMMAND ${CMAKE_COMMAND} + -DINPUT=/makepkg.conf.sample + -DOUTPUT=${CMAKE_INSTALL_PREFIX}/bin/makepkg.conf + -DWORLD_ARCH=${world_arch} + -P ${PROJECT_SOURCE_DIR}/cmake/WriteMakepkgConf.cmake ${UPDATE_DISCONNECTED_SUPPORT} ) diff --git a/cmake/recipes/BuildSdkComponents.cmake b/cmake/recipes/BuildSdkComponents.cmake index 3888f0a..8dff66d 100644 --- a/cmake/recipes/BuildSdkComponents.cmake +++ b/cmake/recipes/BuildSdkComponents.cmake @@ -25,6 +25,30 @@ ExternalProject_add(vita-headers ${UPDATE_DISCONNECTED_SUPPORT} ) +# The softfp world calls the same hard-float Sce* stubs vita-headers just +# installed, but with float/double arguments and return values sitting in +# the wrong registers (AAPCS-base vs AAPCS-VFP). Splice the 23 shims from +# softfp-shim/ into the affected stub archives in place -- see +# PLAN-softfp.md, "Fase 1 - los 23 shims de serie" and +# scripts/patch-softfp-stub-archives.sh for why this is an archive rewrite +# and not a -lvita_softfp_shim added to LIB_SPEC. +if(VITASDK_FLOAT_ABI STREQUAL "softfp") + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/softfp-shim.stamp + COMMAND ${PROJECT_SOURCE_DIR}/scripts/build-softfp-shim.sh + ${binutils_prefix}-gcc ${binutils_prefix}-ar ${binutils_prefix}-objcopy + ${PROJECT_SOURCE_DIR}/softfp-shim + ${CMAKE_INSTALL_PREFIX}/${target_arch}/include + ${CMAKE_INSTALL_PREFIX}/${target_arch}/lib + ${toolchain_build_install_dir}/${target_arch}/lib + COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_BINARY_DIR}/softfp-shim.stamp + DEPENDS vita-headers gcc-base binutils_${build_suffix} + COMMENT "Splicing the softfp ABI shims into the stub archives" + VERBATIM + ) + add_custom_target(softfp-shim ALL DEPENDS ${CMAKE_BINARY_DIR}/softfp-shim.stamp) +endif() + ExternalProject_Add(newlib DEPENDS binutils_${target_suffix} vita-headers GIT_REPOSITORY ${NEWLIB_REPOSITORY} @@ -82,4 +106,3 @@ ExternalProject_Add(samples COMMAND ${GIT_EXECUTABLE} -C rev-parse HEAD > ${CMAKE_BINARY_DIR}/samples-version.txt ${UPDATE_DISCONNECTED_SUPPORT} ) - diff --git a/cmake/recipes/FinalizeSdk.cmake b/cmake/recipes/FinalizeSdk.cmake index ea4246f..589eaf5 100644 --- a/cmake/recipes/FinalizeSdk.cmake +++ b/cmake/recipes/FinalizeSdk.cmake @@ -23,6 +23,7 @@ if(BUILD_PACMAN_CLIENT) COMMAND ${CMAKE_COMMAND} -DOUTPUT=${CMAKE_INSTALL_PREFIX}/etc/pacman.conf -DHOST_ARCHITECTURE=${host_published} + -DWORLD_ARCH=${world_arch} -P ${PROJECT_SOURCE_DIR}/cmake/WritePacmanConfig.cmake DEPENDS vdpm VERBATIM) @@ -43,6 +44,11 @@ set(finalize_sdk_dependencies if(BUILD_PACMAN_CLIENT) list(APPEND finalize_sdk_dependencies package-client-configuration) endif() +if(VITASDK_FLOAT_ABI STREQUAL "softfp") + # Splice the softfp shims into the stub archives before finalize-sdk + # strips them (cmake/strip_target_objects.cmake), not after. + list(APPEND finalize_sdk_dependencies softfp-shim) +endif() # Target objects are stripped where they are produced. In stage 2 they arrive # already stripped from stage 1, and the objcopy that would do it belongs to diff --git a/cmake/recipes/GccCommonArgs.cmake b/cmake/recipes/GccCommonArgs.cmake index 604f04e..82e7c0a 100644 --- a/cmake/recipes/GccCommonArgs.cmake +++ b/cmake/recipes/GccCommonArgs.cmake @@ -47,7 +47,7 @@ set(common_gcc_configure_args --with-arch=armv7-a --with-tune=cortex-a9 --with-fpu=neon - --with-float=hard + --with-float=${VITASDK_FLOAT_ABI} --with-mode=thumb "--with-pkgversion=${pkgversion}" ) diff --git a/cmake/recipes/Provenance.cmake b/cmake/recipes/Provenance.cmake index dd64a2a..5590a30 100644 --- a/cmake/recipes/Provenance.cmake +++ b/cmake/recipes/Provenance.cmake @@ -25,6 +25,7 @@ if(VITASDK_STAGE1_DIR) else() add_custom_command(OUTPUT ${version_info_file} COMMAND ${CMAKE_COMMAND} -DINPUT_DIR=${CMAKE_BINARY_DIR} -DOUTPUT_FILE=${version_info_file} + -DWORLD_ARCH=${world_arch} -DVITASDK_FLOAT_ABI=${VITASDK_FLOAT_ABI} -P ${CMAKE_SOURCE_DIR}/cmake/create_version.cmake DEPENDS vita-headers vita-toolchain_${target_suffix} newlib pthread-embedded samples COMMENT "Creating version_info.txt" diff --git a/scripts/build-softfp-shim.sh b/scripts/build-softfp-shim.sh new file mode 100755 index 0000000..6917b74 --- /dev/null +++ b/scripts/build-softfp-shim.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# +# Generate softfp-shim/wrappers/*.S from the vita-headers this build just +# installed, assemble them, and splice them into every stub archive they +# touch, in every given lib dir. See patch-softfp-stub-archives.sh for what +# "splice" means; generation is cheap enough (a handful of regex passes over +# a few headers) to redo on every softfp build rather than trust a checked-in +# copy to still match vita-headers -- see softfp-shim/README.md. + +set -euo pipefail + +if [[ $# -lt 6 ]]; then + printf 'usage: %s ...\n' "$0" >&2 + exit 2 +fi + +cc=$1 +ar=$2 +objcopy=$3 +shim_dir=$4 +headers_dir=$5 +shift 5 + +work_dir=$(mktemp -d) +trap 'rm -rf "$work_dir"' EXIT + +python3 "$shim_dir/generate.py" "$headers_dir" --out "$work_dir/wrappers" + +for src in "$work_dir"/wrappers/*.S; do + name=$(basename "$src" .S) + "$cc" -c -mfpu=neon -mfloat-abi=softfp "$src" -o "$work_dir/$name.o" +done + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +for lib_dir in "$@"; do + "$script_dir/patch-softfp-stub-archives.sh" "$lib_dir" "$ar" "$objcopy" \ + "$work_dir" "$shim_dir/functions.tsv" +done diff --git a/scripts/ci/build-host.sh b/scripts/ci/build-host.sh index 66d91d8..912ff00 100755 --- a/scripts/ci/build-host.sh +++ b/scripts/ci/build-host.sh @@ -71,9 +71,6 @@ for required in host stage artifacts_dir out_dir build_id version revision profi exit 2 } done -# profile is required but not yet consumed: no profile -> VITASDK_FLOAT_ABI -# mapping exists in CMakeLists.txt yet. -: "$profile" actual_revision=$(git -C "$repo_root" rev-parse HEAD) [[ $actual_revision == "$revision" ]] || { @@ -175,6 +172,9 @@ build_and_stage() { local -a extra_args=("$@") local -a cmake_args=( -S "$repo_root" -B build ${extra_args[@]+"${extra_args[@]}"} + # The lock says which world this is; the profile is how the tree is + # told, and it is what decides the float ABI baked into the toolchain. + -DVITASDK_PROFILE="$profile" -DVITASDK_SOURCE_REVISION="$revision" -DVITASDK_SOURCE_DATE_EPOCH="$source_date_epoch" # The lock names the host; artifacts published under any other name @@ -363,6 +363,7 @@ if [[ $stage == 1 ]]; then enable_ccache cmake -S "$repo_root" -B build \ -DVITASDK_TARGET_ONLY=ON \ + -DVITASDK_PROFILE="$profile" \ -DVITASDK_SOURCE_REVISION="$revision" \ -DVITASDK_SOURCE_DATE_EPOCH="$source_date_epoch" cmake --build build --target sysroot --parallel "$(ci_nproc)" diff --git a/scripts/patch-softfp-stub-archives.sh b/scripts/patch-softfp-stub-archives.sh new file mode 100755 index 0000000..4c2dfe9 --- /dev/null +++ b/scripts/patch-softfp-stub-archives.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# Splice the softfp ABI shims (softfp-shim/wrappers/*.S, already assembled to +# *.o) into the stub archives vita-headers just installed, for the 23 +# functions listed in softfp-shim/functions.tsv (see PLAN-softfp.md, "Fase 1 +# - los 23 shims de serie"). +# +# For each function this renames the real stub object's exported symbol to +# __vita_softfp_target_ (objcopy --redefine-sym) and adds the matching +# wrapper object as a new archive member under the original name. A static +# archive resolves symbols through its member index, not by member order, so +# after the rename there is exactly one provider of left in the +# archive and no link-order dependency is introduced -- packages keep linking +# -lSceGxm_stub, -lScePvf_stub, etc. exactly as before. + +set -euo pipefail + +if [[ $# -ne 5 ]]; then + printf 'usage: %s \n' "$0" >&2 + exit 2 +fi + +lib_dir=$1 +ar=$2 +objcopy=$3 +wrapper_dir=$4 +functions_tsv=$5 + +[[ -d $lib_dir ]] || { printf 'lib dir not found: %s\n' "$lib_dir" >&2; exit 1; } + +work_dir=$(mktemp -d) +trap 'rm -rf "$work_dir"' EXIT + +patch_archive() { + local archive=$1 name=$2 + [[ -f $archive ]] || return 0 + + local member + member=$("$ar" t "$archive" | grep -E "_${name}\.(o|wo)\$" || true) + if [[ -z $member ]]; then + printf 'symbol %s not found in %s\n' "$name" "$archive" >&2 + exit 1 + fi + + local extract_dir="$work_dir/$(basename "$archive")-$name" + mkdir -p "$extract_dir" + (cd "$extract_dir" && "$ar" x "$archive" "$member") + "$objcopy" --redefine-sym "${name}=__vita_softfp_target_${name}" \ + "$extract_dir/$member" + + "$ar" r "$archive" "$extract_dir/$member" + "$ar" r "$archive" "$wrapper_dir/$name.o" + "$ar" s "$archive" +} + +while IFS=$'\t' read -r name header module; do + [[ -n $name ]] || continue + [[ -f "$wrapper_dir/$name.o" ]] || { + printf 'wrapper object not found: %s/%s.o\n' "$wrapper_dir" "$name" >&2 + exit 1 + } + patch_archive "$lib_dir/lib${module}_stub.a" "$name" + patch_archive "$lib_dir/lib${module}_stub_weak.a" "$name" +done < "$functions_tsv" diff --git a/scripts/repack-core.sh b/scripts/repack-core.sh index 0a1c7bd..cde4033 100755 --- a/scripts/repack-core.sh +++ b/scripts/repack-core.sh @@ -143,7 +143,10 @@ cp -a "$vita_makepkg/libmakepkg" "$sdk_root/bin/libmakepkg" cp -a "$vita_makepkg/vita-makepkg" "$sdk_root/bin/vita-makepkg" cp -a "$vita_makepkg/makepkg.conf.sample" "$sdk_root/bin/makepkg.conf" +# Repacking only ever handles releases of the default world: softfp cores +# are never repacked by this script. cmake -DOUTPUT="$sdk_root/etc/pacman.conf" -DHOST_ARCHITECTURE="$host" \ + -DWORLD_ARCH=vita \ -P "$script_directory/../cmake/WritePacmanConfig.cmake" vdpm_version=$(awk -F= '$1 == "version" { print $2; exit }' \ @@ -167,6 +170,12 @@ fi # The component lines above this block still describe what produced the # binaries. These say who repacked them and with what, and name the source # archive by digest so the claim can be checked against what is published. +# Archives from before the world was stamped in version_info.txt never carry +# a "world" line; backfill it here since this script only ever repacks the +# default world (see the pacman.conf write above). +if ! grep -q '^world ' "$version_info"; then + printf 'world vita (float-abi=hard)\n' >> "$version_info" +fi { printf 'repacked from %s\n' "$(basename "$archive")" printf 'source sha256 %s\n' "${archive_digest%% *}" diff --git a/scripts/validate-core-package.sh b/scripts/validate-core-package.sh index a103a5e..f9cf339 100755 --- a/scripts/validate-core-package.sh +++ b/scripts/validate-core-package.sh @@ -60,6 +60,33 @@ if [[ $pkgname == vitasdk-core ]]; then printf 'core package does not contain provenance information\n' >&2 exit 1 } + + # The world (vita, vita-softfp, ...) is stamped in two independent places; + # a mismatch means the build tagged the release for one world while + # actually configuring the toolchain, or makepkg, for another. + world_from_version=$(bsdtar -xOf "$package" version_info.txt | + awk '$1 == "world" { print $2; exit }') + [[ -n $world_from_version ]] || { + printf 'version_info.txt does not declare a world\n' >&2 + exit 1 + } + + grep -qx 'bin/makepkg.conf' <<< "$archive_entries" || { + printf 'core package does not contain bin/makepkg.conf\n' >&2 + exit 1 + } + world_from_makepkg=$(bsdtar -xOf "$package" bin/makepkg.conf | + awk -F '"' '/^CARCH=/ { print $2; exit }') + [[ -n $world_from_makepkg ]] || { + printf 'bin/makepkg.conf does not declare CARCH\n' >&2 + exit 1 + } + + [[ $world_from_version == "$world_from_makepkg" ]] || { + printf 'world mismatch: version_info.txt says %s, bin/makepkg.conf CARCH says %s\n' \ + "$world_from_version" "$world_from_makepkg" >&2 + exit 1 + } else grep -Eq '^bin/vdpm(\.exe)?$' <<< "$archive_entries" || { printf 'client package does not contain the package client\n' >&2 diff --git a/softfp-shim/README.md b/softfp-shim/README.md new file mode 100644 index 0000000..81484a1 --- /dev/null +++ b/softfp-shim/README.md @@ -0,0 +1,93 @@ +# softfp ABI shims + +The 23 functions in `functions.tsv` are the ones where an AAPCS-base +(softfp) caller and the AAPCS-VFP (hard) Sce* stub disagree about where a +float/double argument or return value lives — see +`../artifacts/softfp-abi-audit/README.md` and `PLAN-softfp.md`, "Fase 1 - los +23 shims de serie", for how that inventory was built and why it stops at +these 23 (no kernel imports, no callbacks, no HFAs by value are affected). + +## What gets built + +`wrappers/*.S` are small naked Thumb-2 functions, one per affected symbol, +named exactly like the real function. Each one moves its float/double +arguments from their softfp slot into the register the hard-float stub +expects, calls into it, and — for a float/double return — moves the result +back before returning. `generate.py` derives the move sequence from the +AAPCS rules themselves (which argument gets which register, on each side), +not from a hand-picked template. + +`wrappers/` is generated, not checked in (see `.gitignore`): +`scripts/build-softfp-shim.sh` runs `generate.py` against the vita-headers +this same build just installed before assembling anything, every time a +softfp world is built. The generation itself is a handful of regex passes +over a few headers — cheap enough that trusting a committed copy to still +match vita-headers, instead of just re-deriving it, would be the wrong +trade: a stale `.S` from before a vita-headers signature change is a wrong +ABI shim that still assembles and links without complaint. + +The trade this makes instead: `python3` becomes a real build-time +dependency, where none of the rest of this repo needs one (checked before +committing to this: nothing under `CMakeLists.txt`/`cmake/` invokes it). +Scoped to the softfp world only — `cmake/recipes/BuildSdkComponents.cmake` +only runs `build-softfp-shim.sh` inside `VITASDK_FLOAT_ABI STREQUAL +"softfp"` — so the default hard-float build stays exactly as +dependency-free as it was. + +`scripts/build-softfp-shim.sh` and `scripts/patch-softfp-stub-archives.sh` +(one level up, in `buildscripts/`) assemble these wrappers and splice them +into the installed `libSceGxm_stub.a`, `libSceMotion_stub.a`, +`libScePaf_stub.a`, `libScePgf_stub.a` and `libScePvf_stub.a` (and their +`_weak` counterparts): the real object exporting e.g. `sceGxmSetViewport` is +renamed in place to `__vita_softfp_target_sceGxmSetViewport` +(`objcopy --redefine-sym`), and the wrapper is added as a new archive member +under the original name. A static archive resolves symbols through its +member index, not member order, so after the rename there is exactly one +provider of each name left — no link-order dependency, no new `-l` flag, no +GCC/LIB_SPEC patch. Packages keep linking `-lSceGxm_stub` etc. exactly as +before; see `cmake/recipes/BuildSdkComponents.cmake` (guarded by +`VITASDK_FLOAT_ABI STREQUAL "softfp"`) for where this runs in the build. + +An earlier version of this plan meant to do the interception through +`LIB_SPEC` (`-lvita_softfp_shim`, unconditionally appended, "in front of the +stubs"). That does not work: GCC's `LINK_COMMAND_SPEC` expands `%o` — every +object file and every `-l` flag a caller passes explicitly — before it +expands `LIB_SPEC` (`%L`). SceGxm/SceMotion/ScePaf/ScePgf/ScePvf are never +part of the implicit baseline (unlike `SceLibKernel_stub` and friends); +every package links them explicitly, so their `-lSceGxm_stub` would always +land on the command line ahead of anything LIB_SPEC could add, and the real +stub would win the archive race every time. The archive-splice above has no +such ordering dependency. + +## Updating `functions.tsv` + +The build regenerates `wrappers/*.S` itself, but `functions.tsv` (function, +header path, stub module) is hand-curated, not derived automatically — the +module column especially: `scePafGraphicsUpdateCurrentWave` and +`sce_paf_strtod` are both exported from `libScePaf_stub.a` but from two +different internal libraries (`ScePafGraphics`, `ScePafStdc`), and `ScePgf` +is the archive that ships the `sceFont*` names. Update it by hand +(`ar t libSceXxx_stub.a` against a real build finds the current mapping) +when `../artifacts/softfp-abi-audit/scan-float-abi.py` reports a different +set of user-facing float/double crossings than the 23 rows here today, and +commit that change deliberately. + +To see the generated output without a full build, run it the same way the +build does: + +```sh +python3 generate.py ../vita-headers/include +``` + +The generator raises `NotImplementedError` rather than guess if it ever +meets a double or HFA parameter (none of the current 23 have one) — that +shape needs its own move-sequence case before it can be trusted. + +## Known gap, not ours to close here + +A caller with its own pre-existing hard→softfp wrapper (the +`vitasdk-softfp` org's vitaGL fork, built with `SOFTFP_ABI=1`) would now +double-translate if built against this series without removing that +wrapper first. Flagged for the Fase 5 migration guide, not fixed here: a new +world has no binary legacy to be compatible with, so the fix is "stop +wrapping", not a mechanism in this shim. diff --git a/softfp-shim/functions.tsv b/softfp-shim/functions.tsv new file mode 100644 index 0000000..7a378a1 --- /dev/null +++ b/softfp-shim/functions.tsv @@ -0,0 +1,23 @@ +sceGxmSetViewport psp2/gxm.h SceGxm +sceGxmSetWClampValue psp2/gxm.h SceGxm +sceGxmDepthStencilSurfaceSetBackgroundDepth psp2/gxm.h SceGxm +sceGxmDepthStencilSurfaceGetBackgroundDepth psp2/gxm.h SceGxm +sceMotionSetAngleThreshold psp2/motion.h SceMotion +sceMotionGetAngleThreshold psp2/motion.h SceMotion +sceMotionRotateYaw psp2/motion.h SceMotion +scePafGraphicsUpdateCurrentWave psp2/paf/graphics.h ScePaf +sce_paf_strtod psp2/paf/stdc.h ScePaf +sceFontSetResolution psp2/pgf.h ScePgf +sceFontPixelToPointH psp2/pgf.h ScePgf +sceFontPixelToPointV psp2/pgf.h ScePgf +sceFontPointToPixelH psp2/pgf.h ScePgf +sceFontPointToPixelV psp2/pgf.h ScePgf +scePvfSetCharSize psp2/pvf.h ScePvf +scePvfSetEM psp2/pvf.h ScePvf +scePvfSetEmboldenRate psp2/pvf.h ScePvf +scePvfSetResolution psp2/pvf.h ScePvf +scePvfSetSkewValue psp2/pvf.h ScePvf +scePvfPixelToPointH psp2/pvf.h ScePvf +scePvfPixelToPointV psp2/pvf.h ScePvf +scePvfPointToPixelH psp2/pvf.h ScePvf +scePvfPointToPixelV psp2/pvf.h ScePvf diff --git a/softfp-shim/generate.py b/softfp-shim/generate.py new file mode 100755 index 0000000..72acd4e --- /dev/null +++ b/softfp-shim/generate.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Generate the softfp-world naked ABI shims for the 23 float/double crossings +found by ../artifacts/softfp-abi-audit/scan-float-abi.py (see results.tsv and +PLAN-softfp.md, "Fase 1" / los 23 shims de serie). + +A shim translates a single AAPCS-base (softfp) call into the AAPCS-VFP (hard) +call the real Sce* stub expects: move each float/double argument from its +softfp slot (core register or stack, counted together with every other +argument) into its hard-float slot (an S/D register, counted only among the +float/double arguments), and move a float/double return value back from +s0/d0 into r0 or r0:r1. Everything else (pointers, plain integers) already +sits in the same core register on both sides and is left untouched. + +Each generated file defines exactly one symbol, named like the real +function, and branches or calls into `__vita_softfp_target_` -- the +real stub, renamed by scripts/patch-softfp-stub-archives.sh so the wrapper +can carry the original name without colliding with it. This script only +emits the wrapper .S files; splicing them into the installed stub archives +is that shell script's job. + +Regenerate when scan-float-abi.py reports a different set of user-facing +crossings: + + python3 generate.py /include + +`functions.tsv` (name, header path relative to include/, stub module) is +hand-curated from that report, not derived automatically -- it is small and +its correctness matters more than saving the curation step. +""" + +import argparse +import os +import re +import sys + +FLOAT_TYPES = {"float", "SceFloat", "SceFloat32", "ScePvfFloat32"} +DOUBLE_TYPES = {"double", "SceDouble", "SceDouble64"} + +FUNCTION = re.compile(r"([\w\s\*]+?)\b(\w+)\s*\(([^()]*)\)\s*;") + +HEADER = """\ +\t.syntax unified +\t.thumb +\t.text +\t.align\t2 +\t.global\t{name} +\t.thumb_func +\t.type\t{name}, %function +{name}: +""" + + +def classify_type(text): + text = text.strip() + if "*" in text: + return "core" + tokens = text.split() + if any(token in FLOAT_TYPES for token in tokens): + return "float" + if any(token in DOUBLE_TYPES for token in tokens): + return "double" + return "core" + + +def find_signature(header_text, name): + flat = re.sub(r"\s+", " ", header_text) + for return_type, found_name, parameters in FUNCTION.findall(flat): + if found_name != name: + continue + params = [] + parameters = parameters.strip() + if parameters and parameters != "void": + for parameter in parameters.split(","): + params.append(classify_type(parameter)) + ret_kind = "void" if return_type.strip() == "void" else classify_type(return_type) + return ret_kind, params + raise LookupError(f"declaration of {name} not found") + + +def softfp_slots(params): + """Every argument, float or not, consumes one word in declaration order.""" + return list(enumerate(params)) + + +def hard_destinations(params): + core_idx = 0 + float_idx = 0 + dests = [] + for kind in params: + if kind == "float": + dests.append(("vfp", float_idx)) + float_idx += 1 + elif kind == "core": + dests.append(("core", core_idx)) + core_idx += 1 + else: + raise NotImplementedError("double/HFA parameters are not handled") + if core_idx > 4 or float_idx > 16: + raise NotImplementedError("more core/vfp arguments than fit in registers") + return dests + + +def slot_source(slot): + if slot < 4: + return ("reg", slot) + return ("stack", (slot - 4) * 4) + + +def emit_body(name, target, ret_kind, params): + dests = hard_destinations(params) + lines = [] + + # Float args first: every vmov reads an *original* softfp register/stack + # slot, before anything below overwrites r0-r3. Register-sourced floats + # are read first (they alias r0-r3 directly); stack-sourced floats are + # loaded through a scratch core register afterwards, since by then their + # register-sourced siblings have already been consumed. + scratch_regs = ["r1", "r2", "r3"] + pending_stack = [] + for slot, kind in softfp_slots(params): + if kind != "float": + continue + dest_idx = dests[slot][1] + source_kind, source = slot_source(slot) + if source_kind == "reg": + lines.append(f"\tvmov\ts{dest_idx}, r{source}") + else: + pending_stack.append((dest_idx, source)) + for dest_idx, offset in pending_stack: + scratch = scratch_regs.pop(0) + lines.append(f"\tldr\t{scratch}, [sp, #{offset}]") + lines.append(f"\tvmov\ts{dest_idx}, {scratch}") + + # Core (pointer/integer) args next, in increasing destination-register + # order -- always safe, since a core argument's source slot number is + # never lower than its destination register number (interleaved float + # arguments only ever push a source slot further right). + core_moves = [] + for slot, kind in softfp_slots(params): + if kind != "core": + continue + dest_idx = dests[slot][1] + source_kind, source = slot_source(slot) + if source_kind != "reg": + raise NotImplementedError("stack-sourced core argument") + if source != dest_idx: + core_moves.append((dest_idx, source)) + for dest_idx, source in sorted(core_moves): + lines.append(f"\tmov\tr{dest_idx}, r{source}") + + if ret_kind == "float": + lines.insert(0, "\tpush\t{lr}") + lines.append(f"\tbl\t{target}") + lines.append("\tvmov\tr0, s0") + lines.append("\tpop\t{lr}") + lines.append("\tbx\tlr") + elif ret_kind == "double": + lines.insert(0, "\tpush\t{lr}") + lines.append(f"\tbl\t{target}") + lines.append("\tvmov\tr0, r1, d0") + lines.append("\tpop\t{lr}") + lines.append("\tbx\tlr") + else: + lines.append(f"\tb\t{target}") + + return "\n".join(lines) + "\n" + + +def generate_one(name, header_text, out_dir): + ret_kind, params = find_signature(header_text, name) + target = f"__vita_softfp_target_{name}" + body = emit_body(name, target, ret_kind, params) + path = os.path.join(out_dir, f"{name}.S") + with open(path, "w", encoding="utf-8") as handle: + handle.write(HEADER.format(name=name)) + handle.write(body) + handle.write(f"\t.size\t{name}, . - {name}\n") + return path + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("include_dir", help="vita-headers include/ directory") + parser.add_argument("--functions", default=os.path.join( + os.path.dirname(__file__), "functions.tsv")) + parser.add_argument("--out", default=os.path.join( + os.path.dirname(__file__), "wrappers")) + options = parser.parse_args() + + os.makedirs(options.out, exist_ok=True) + + headers = {} + with open(options.functions, encoding="utf-8") as handle: + rows = [line.rstrip("\n").split("\t") for line in handle if line.strip()] + + generated = [] + for name, header, _module in rows: + if header not in headers: + with open(os.path.join(options.include_dir, header), + encoding="utf-8") as handle: + headers[header] = handle.read() + generated.append(generate_one(name, headers[header], options.out)) + + print(f"generated {len(generated)} wrappers in {options.out}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/ci/test-build-host-args.sh b/tests/ci/test-build-host-args.sh index f69a821..cc7fe47 100755 --- a/tests/ci/test-build-host-args.sh +++ b/tests/ci/test-build-host-args.sh @@ -124,4 +124,28 @@ grep -q 'curl disabled in test' <<< "$output" || { exit 1 } +# 6. The profile reaches cmake. The lock is where CI says which world it is +# building, and the profile is the only thing that carries it into the tree; +# until this existed the flag was parsed, required, and then dropped, so a +# softfp lock produced a hard-float toolchain that said nothing. +recorded="$temporary_directory/cmake-args" +cat > "$fake_bin/cmake" <> "$recorded" +exit 0 +EOF +chmod +x "$fake_bin/cmake" + +run_build_host \ + --host x86_64-linux-gnu --stage 1 \ + --artifacts-dir "$temporary_directory/artifacts-6" --out-dir "$temporary_directory/out-6" \ + --build-id sha256:test --version 0.1.1 --revision "$revision" \ + --profile vita-softfp --packaged false >/dev/null 2>&1 || true + +grep -qx -- '-DVITASDK_PROFILE=vita-softfp' "$recorded" || { + printf 'stage 1 did not pass the profile to cmake:\n%s\n' "$(cat "$recorded")" >&2 + exit 1 +} +rm -f "$fake_bin/cmake" "$recorded" + printf 'build-host.sh argument contract tests passed\n' diff --git a/tests/package/test-core-package.sh b/tests/package/test-core-package.sh index dada811..6fa785c 100755 --- a/tests/package/test-core-package.sh +++ b/tests/package/test-core-package.sh @@ -35,7 +35,8 @@ chmod +x "$sdk_root/bin/arm-vita-eabi-gcc" "$sdk_root/libexec/vdpm/pacman" \ "$sdk_root/libexec/vdpm/pacman-conf" "$sdk_root/bin/vdpm" "$sdk_root/bin/vdpm-channel" printf 'refresh\n' > "$sdk_root/bin/include/refresh-repositories.sh" printf 'archive\n' > "$sdk_root/arm-vita-eabi/lib/libfixture.a" -printf 'source=fixture\n' > "$sdk_root/version_info.txt" +printf 'source=fixture\nworld vita (float-abi=hard)\n' > "$sdk_root/version_info.txt" +printf 'CARCH="vita"\n' > "$sdk_root/bin/makepkg.conf" printf 'notices\n' > "$sdk_root/share/vdpm/THIRD_PARTY_NOTICES.md" printf 'vdpm license\n' > "$sdk_root/share/vdpm/licenses/vdpm-LGPL-2.1.txt" printf 'pacman license\n' > "$sdk_root/share/vdpm/licenses/pacman-GPL-2.0.txt" @@ -134,7 +135,8 @@ mkdir -p "$windows_root/bin" "$windows_root/arm-vita-eabi/lib" \ printf 'gcc\n' > "$windows_root/bin/arm-vita-eabi-gcc.exe" printf 'vdpm\n' > "$windows_root/bin/vdpm.exe" printf 'archive\n' > "$windows_root/arm-vita-eabi/lib/libfixture.a" -printf 'source=fixture\n' > "$windows_root/version_info.txt" +printf 'source=fixture\nworld vita (float-abi=hard)\n' > "$windows_root/version_info.txt" +printf 'CARCH="vita"\n' > "$windows_root/bin/makepkg.conf" printf 'notices\n' > "$windows_root/share/vdpm/THIRD_PARTY_NOTICES.md" printf 'refresh\n' > "$windows_root/share/vdpm/refresh-repositories.ps1" printf 'vdpm license\n' > "$windows_root/share/vdpm/licenses/vdpm-LGPL-2.1.txt" diff --git a/tests/toolchain-contract/abi.c b/tests/toolchain-contract/abi.c index b962a6f..e93d661 100644 --- a/tests/toolchain-contract/abi.c +++ b/tests/toolchain-contract/abi.c @@ -9,9 +9,11 @@ #error "Vita uses ARM EABI" #endif -#ifndef __ARM_PCS_VFP -#error "Vita uses the hard-float procedure-call standard" -#endif +/* + * The float ABI (hard vs softfp) is a build parameter, not a fixed property + * of this source file: run.sh detects which one this compiler defaults to + * and asserts the __ARM_PCS_VFP / Tag_ABI_VFP_args contract for that world. + */ _Static_assert(sizeof(char) == 1, "unexpected char size"); _Static_assert((char)-1 < 0, "plain char must be signed"); diff --git a/tests/toolchain-contract/run.sh b/tests/toolchain-contract/run.sh index 1e59260..fc4dab7 100755 --- a/tests/toolchain-contract/run.sh +++ b/tests/toolchain-contract/run.sh @@ -46,14 +46,43 @@ echo "checking predefined macros" macros=$("${CC}" -dM -E -x c /dev/null) expect_text "${macros}" "#define __vita__ 1" "__vita__ macro" expect_text "${macros}" "#define __ARM_EABI__ 1" "ARM EABI macro" -expect_text "${macros}" "#define __ARM_PCS_VFP 1" "hard-float PCS macro" expect_text "${macros}" "#define __ARM_ARCH 7" "ARMv7 default" expect_text "${macros}" "#define __ARM_NEON 1" "NEON default" +echo "checking float ABI contract" +# The float ABI (hard vs softfp) is a build parameter (VITASDK_FLOAT_ABI): +# detect which one this compiler defaults to, then assert the contract for +# that world in both directions instead of assuming hard-float. +expect_text "${macros}" "#define __ARM_FP " \ + "hardware FPU macro (both float ABIs compute on VFP hardware)" +if printf '%s\n' "${macros}" | grep -qF '#define __ARM_PCS_VFP 1'; then + float_abi=hard +else + float_abi=softfp +fi +echo "detected float ABI: ${float_abi}" + +if [ "${float_abi}" = "hard" ]; then + expect_text "${macros}" "#define __ARM_PCS_VFP 1" "hard-float PCS macro" +else + reject_text "${macros}" "#define __ARM_PCS_VFP 1" \ + "hard-float PCS macro in a softfp toolchain" +fi + echo "checking C ABI" "${CC}" -std=c11 -Wall -Wextra -Werror -c \ "${srcdir}/abi.c" -o "${workdir}/abi.o" +echo "checking ARM float ABI attribute" +abi_attributes=$("${READELF}" -A "${workdir}/abi.o") +if [ "${float_abi}" = "hard" ]; then + expect_text "${abi_attributes}" "Tag_ABI_VFP_args" \ + "VFP-args ABI attribute in a hard-float object" +else + reject_text "${abi_attributes}" "Tag_ABI_VFP_args" \ + "VFP-args ABI attribute in a softfp object" +fi + echo "checking C++ ABI, exceptions and RTTI" "${CXX}" -std=c++17 -Wall -Wextra -Werror -c \ "${srcdir}/cxx.cpp" -o "${workdir}/cxx.o" @@ -114,4 +143,30 @@ echo "checking ARM unwind section generation" echo "checking that public headers compile on their own" "${srcdir}/self-contained-headers.sh" +if [ "${float_abi}" = "softfp" ]; then + echo "checking the softfp ABI shims (23 functions) resolve against the shim, not the raw stub" + shim_map="${workdir}/softfp-shim.map" + "${CC}" -std=c11 -Wall -Wextra -Werror \ + "${srcdir}/softfp-shim.c" -o "${workdir}/softfp-shim.elf" \ + -lSceGxm_stub -lSceMotion_stub -lScePaf_stub -lScePgf_stub -lScePvf_stub \ + -Wl,-Map="${shim_map}" + + shim_map_text=$(cat "${shim_map}") + for shimmed_function in \ + sceGxmSetViewport sceGxmSetWClampValue \ + sceGxmDepthStencilSurfaceSetBackgroundDepth \ + sceGxmDepthStencilSurfaceGetBackgroundDepth \ + sceMotionSetAngleThreshold sceMotionGetAngleThreshold sceMotionRotateYaw \ + scePafGraphicsUpdateCurrentWave sce_paf_strtod \ + sceFontSetResolution sceFontPixelToPointH sceFontPixelToPointV \ + sceFontPointToPixelH sceFontPointToPixelV \ + scePvfSetCharSize scePvfSetEM scePvfSetEmboldenRate scePvfSetResolution \ + scePvfSetSkewValue scePvfPixelToPointH scePvfPixelToPointV \ + scePvfPointToPixelH scePvfPointToPixelV + do + expect_text "${shim_map_text}" "(${shimmed_function}.o)" \ + "${shimmed_function} resolved against the softfp shim wrapper" + done +fi + echo "Vita GCC/binutils contract OK" diff --git a/tests/toolchain-contract/softfp-shim.c b/tests/toolchain-contract/softfp-shim.c new file mode 100644 index 0000000..7275679 --- /dev/null +++ b/tests/toolchain-contract/softfp-shim.c @@ -0,0 +1,56 @@ +/* Exercises all 23 functions covered by softfp-shim/ -- see run.sh, which + * links this against the real stub archives (-lSceGxm_stub etc., exactly as + * a package would) and checks the link map to confirm each one resolved to + * the shim wrapper, not the raw hard-float stub. */ + +#include +#include +#include +#include +#include +#include + +void exercise_softfp_shims(void) +{ + SceGxmContext *context = 0; + sceGxmSetViewport(context, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f); + sceGxmSetWClampValue(context, 1.0f); + + SceGxmDepthStencilSurface *surface = 0; + sceGxmDepthStencilSurfaceSetBackgroundDepth(surface, 1.0f); + (void)sceGxmDepthStencilSurfaceGetBackgroundDepth(surface); + + sceMotionSetAngleThreshold(1.0f); + (void)sceMotionGetAngleThreshold(); + sceMotionRotateYaw(1.0f); + + scePafGraphicsUpdateCurrentWave(0, 1.0f); + (void)sce_paf_strtod("1.5", 0); + + SceFontLibHandle libHandle = 0; + unsigned int fontErrorCode = 0; + sceFontSetResolution(libHandle, 1.0f, 1.0f); + (void)sceFontPixelToPointH(libHandle, 1.0f, &fontErrorCode); + (void)sceFontPixelToPointV(libHandle, 1.0f, &fontErrorCode); + (void)sceFontPointToPixelH(libHandle, 1.0f, &fontErrorCode); + (void)sceFontPointToPixelV(libHandle, 1.0f, &fontErrorCode); + + ScePvfLibId libID = 0; + ScePvfFontId fontID = 0; + ScePvfError pvfError = 0; + scePvfSetCharSize(fontID, 1.0f, 1.0f); + scePvfSetEM(libID, 1.0f); + scePvfSetEmboldenRate(fontID, 1.0f); + scePvfSetResolution(libID, 1.0f, 1.0f); + scePvfSetSkewValue(fontID, 1.0f, 1.0f); + (void)scePvfPixelToPointH(libID, 1.0f, &pvfError); + (void)scePvfPixelToPointV(libID, 1.0f, &pvfError); + (void)scePvfPointToPixelH(libID, 1.0f, &pvfError); + (void)scePvfPointToPixelV(libID, 1.0f, &pvfError); +} + +int main(void) +{ + exercise_softfp_shims(); + return 0; +}