diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
new file mode 100644
index 0000000..0b85b60
--- /dev/null
+++ b/.gitea/workflows/build.yml
@@ -0,0 +1,195 @@
+name: Build
+
+on:
+ push:
+ branches:
+ - master
+ - main
+ pull_request:
+ workflow_dispatch:
+
+env:
+ BUILD_JOBS: 4
+
+jobs:
+ linux:
+ name: Linux ${{ matrix.target }}
+ runs-on: ubuntu-latest
+ container:
+ image: catthehacker/ubuntu:act-24.04
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: x64
+ extra_arch: ""
+ packages: "build-essential binutils cmake file git libgl1-mesa-dev libsdl2-dev make pkg-config python3 zip"
+ - target: x32
+ extra_arch: "i386"
+ packages: "binutils cmake file gcc-multilib g++-multilib git libc6-dev-i386 libgl1-mesa-dev:i386 libsdl2-dev:i386 make pkg-config python3 zip"
+ - target: arm64
+ extra_arch: "arm64"
+ packages: "binutils cmake file gcc-aarch64-linux-gnu g++-aarch64-linux-gnu git libgl1-mesa-dev:arm64 libsdl2-dev:arm64 make pkg-config python3 zip"
+ - target: arm32
+ extra_arch: "armhf"
+ packages: "binutils cmake file gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf git libgl1-mesa-dev:armhf libsdl2-dev:armhf make pkg-config python3 zip"
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install Linux dependencies
+ shell: bash
+ run: |
+ set -euo pipefail
+ if [ -n "${{ matrix.extra_arch }}" ]; then
+ dpkg --add-architecture "${{ matrix.extra_arch }}"
+ fi
+ apt-get update
+ DEBIAN_FRONTEND=noninteractive apt-get install -y ${{ matrix.packages }}
+
+ - name: Build Linux package
+ shell: bash
+ run: ./scripts/ci/build-linux-target.sh "${{ matrix.target }}"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: armsx-linux-${{ matrix.target }}
+ path: artifacts/linux/${{ matrix.target }}/armsx-linux-${{ matrix.target }}.zip
+
+ windows:
+ name: Windows ${{ matrix.target }}
+ runs-on: ubuntu-latest
+ container:
+ image: catthehacker/ubuntu:act-24.04
+ strategy:
+ fail-fast: false
+ matrix:
+ target: [x64, x32]
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install MinGW toolchains
+ shell: bash
+ run: |
+ set -euo pipefail
+ apt-get update
+ DEBIAN_FRONTEND=noninteractive apt-get install -y \
+ binutils-mingw-w64 \
+ cmake \
+ git \
+ gcc-mingw-w64 \
+ g++-mingw-w64 \
+ make \
+ python3 \
+ zip
+
+ - name: Build Windows package
+ shell: bash
+ run: ./scripts/ci/build-windows-cross.sh "${{ matrix.target }}"
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: armsx-windows-${{ matrix.target }}
+ path: artifacts/windows/${{ matrix.target }}/armsx-windows-${{ matrix.target }}.zip
+
+ android:
+ name: Android Debug APK
+ runs-on: ubuntu-latest
+ container:
+ image: catthehacker/ubuntu:act-24.04
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install Android build dependencies
+ shell: bash
+ run: |
+ set -euo pipefail
+ apt-get update
+ DEBIAN_FRONTEND=noninteractive apt-get install -y \
+ cmake \
+ curl \
+ git \
+ make \
+ ninja-build \
+ openjdk-17-jdk \
+ pkg-config \
+ python3 \
+ unzip \
+ zip
+
+ - name: Build Android APK
+ shell: bash
+ run: |
+ set -euo pipefail
+ export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
+ export PATH="$JAVA_HOME/bin:$PATH"
+
+ export ANDROID_HOME="$PWD/.android-sdk"
+ export ANDROID_SDK_ROOT="$ANDROID_HOME"
+ mkdir -p "$ANDROID_HOME/cmdline-tools"
+
+ curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o /tmp/android-cmdline-tools.zip
+ unzip -q /tmp/android-cmdline-tools.zip -d "$ANDROID_HOME/cmdline-tools"
+ mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest"
+
+ export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"
+ yes | sdkmanager --sdk_root="$ANDROID_HOME" --licenses >/dev/null
+ sdkmanager --sdk_root="$ANDROID_HOME" \
+ "build-tools;34.0.0" \
+ "ndk;27.1.12297006" \
+ "platform-tools" \
+ "platforms;android-34"
+
+ export ANDROID_NDK_ROOT="$ANDROID_HOME/ndk/27.1.12297006"
+ export ANDROID_NDK_HOME="$ANDROID_NDK_ROOT"
+ export NDK_HOME="$ANDROID_NDK_ROOT"
+ export GRADLE_USER_HOME="$PWD/.gradle-home"
+
+ curl -fsSL https://services.gradle.org/distributions/gradle-8.14.3-bin.zip -o /tmp/gradle.zip
+ unzip -q /tmp/gradle.zip -d "$PWD/.gradle-dist"
+ export GRADLE_BIN="$PWD/.gradle-dist/gradle-8.14.3/bin/gradle"
+
+ ./scripts/ci/build-android-all.sh
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: armsx-android-debug
+ path: android/app/build/outputs/apk/debug/app-debug.apk
+
+ wasm:
+ name: WASM
+ runs-on: ubuntu-latest
+ container:
+ image: emscripten/emsdk:3.1.72
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Install WASM packaging tools
+ shell: bash
+ run: |
+ set -euo pipefail
+ apt-get update
+ DEBIAN_FRONTEND=noninteractive apt-get install -y zip
+
+ - name: Build WebAssembly package
+ shell: bash
+ run: |
+ set -euo pipefail
+ ./build.sh wasm
+ mkdir -p artifacts/wasm
+ (
+ cd bin
+ zip -r ../artifacts/wasm/armsx-wasm.zip wasm
+ )
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: armsx-wasm
+ path: artifacts/wasm/armsx-wasm.zip
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
deleted file mode 100644
index 10f2145..0000000
--- a/.github/workflows/macos.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: macOS CI
-
-on:
- push:
- branches: [ "master" ]
-
-jobs:
- build:
- runs-on: macos-latest
-
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
- - name: Install SDL2 and dylibbundler
- run: |
- brew install sdl2
- brew install dylibbundler
- - name: Build and pack PSXE
- run: |
- git fetch --all --tags
- ./build.sh
- tar -czf psxe-macos-latest.tar.gz psxe.app
- - uses: actions/upload-artifact@v4
- with:
- name: psxe-macos-latest
- path: ./psxe-macos-latest.tar.gz
diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml
deleted file mode 100644
index 61ce78c..0000000
--- a/.github/workflows/ubuntu.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: Ubuntu CI
-
-on:
- push:
- branches: [ "master" ]
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
- - name: Install SDL2
- run: |
- sudo apt update
- sudo apt install libsdl2-dev
- - name: Build PSXE
- run: |
- git fetch --all --tags
- make
- - name: Pack executable
- run: |
- chmod +x ./bin/psxe
- mv ./bin/psxe ./
- tar -czf psxe-ubuntu-latest.tar.gz ./psxe
- - uses: actions/upload-artifact@v4
- with:
- name: psxe-ubuntu-latest
- path: ./psxe-ubuntu-latest.tar.gz
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
deleted file mode 100644
index bb8165d..0000000
--- a/.github/workflows/windows.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Windows CI
-
-on:
- push:
- branches: [ "master" ]
-
-jobs:
- build:
- runs-on: windows-latest
-
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
- - name: Run build-deps
- run: ./build-deps.ps1
- - name: Run build-win64
- run: |
- ./build-win64.ps1
- New-Item -Path "psxe" -ItemType Directory
- Copy-Item -Recurse "bin" -Destination "psxe"
- - uses: actions/upload-artifact@v4
- with:
- name: psxe-win64-latest
- path: psxe/
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 8552111..1b77c1e 100644
--- a/Makefile
+++ b/Makefile
@@ -245,6 +245,8 @@ ALL_OBJS_SHARED := $(C_OBJS_SHARED) $(CPP_OBJS)
# Force dynamic SDL when building the shared library
ifneq (,$(filter shared,$(MAKECMDGOALS)))
override SDL_STATIC := 0
+BASE_CFLAGS += -D__DLL_BUILD -fPIC
+BASE_CXXFLAGS += -D__DLL_BUILD -fPIC
endif
ifeq ($(SDL_STATIC),1)
diff --git a/README.md b/README.md
index 065d852..e703c97 100644
--- a/README.md
+++ b/README.md
@@ -4,13 +4,11 @@ A simple and portable Sony PlayStation emulator and emulation library written in
## Screenshots
| Windows | Ubuntu | macOS |
| ------------- | ------------- | -------------
-|  |  |  |
-|  |  |  |
+|  |  |  |
+|  |  |  |
-### CI status
-
-
-
+### CI status
+CI now lives in `.gitea/workflows/build.yml`.
## Running
You can download the latest automated build for your platform on Releases. If your system isn't supported, you can easily build the emulator from source, instructions on "Building" below.
@@ -24,15 +22,15 @@ Use the `-b` or `--bios` setting to configure the BIOS file.
## Progress
All major components have been implemented, including memory card support.
-
CPU
-
DMA
-
GPU
-
SPU
-
MDEC
-
GTE
-
Timers
-
CDROM
-
Memory cards
+
CPU
+
DMA
+
GPU
+
SPU
+
MDEC
+
GTE
+
Timers
+
CDROM
+
Memory cards
## Building
Building the emulator should be easy, just use the scripts provided in this repo.
@@ -58,8 +56,8 @@ Assuming you did everything described above, you should be able to build the emu
### Windows
```
-git clone https://github.com/allkern/psxe
-cd psxe
+git clone https://git.nanodata.cloud/moonpower/ARMSX.git
+cd ARMSX
./build-deps
./build-win64.ps1
```
@@ -67,15 +65,15 @@ On rare cases these scripts might not work (PowerShell/Windows bugs). If so, ple
### Ubuntu
```
-git clone https://github.com/allkern/psxe
-cd psxe
+git clone https://git.nanodata.cloud/moonpower/ARMSX.git
+cd ARMSX
make clean && make
```
### macOS
```
-git clone https://github.com/allkern/psxe
-cd psxe
+git clone https://git.nanodata.cloud/moonpower/ARMSX.git
+cd ARMSX
./build.sh
```
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 57f561b..40b8fd2 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -67,12 +67,14 @@ dependencies {
def repoRootDir = rootProject.projectDir.parentFile
def nativeOutput = file("$projectDir/src/main/jniLibs/arm64-v8a/libarmsx.so")
+def skipNativePrepare = System.getenv("ARMSX_SKIP_NATIVE_PREPARE") == "1"
tasks.register("prepareNativeBinaries") {
group = "native"
description = "Build SDL2/libarmsx via build.sh android."
inputs.file(new File(repoRootDir, "build.sh"))
outputs.file(nativeOutput)
+ onlyIf { !skipNativePrepare }
doLast {
exec {
workingDir repoRootDir
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 01ec193..41aef35 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,4 @@
-
+
+ android:theme="@style/AppTheme">
diff --git a/android/build.gradle b/android/build.gradle
index c56152f..fc32c6f 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -30,7 +30,7 @@ allprojects { project ->
configurations.all { config ->
config.resolutionStrategy.eachDependency { details ->
- if (details.requested.group == 'org.jetbrains.kotlin' && details.requested.name == 'kotlin-gradle-plugin') {
+ if (details.requested.group == 'org.jetbrains.kotlin') {
details.useVersion(rootProject.ext.kotlinVersion)
}
}
diff --git a/build.sh b/build.sh
index 42f6149..35c751a 100755
--- a/build.sh
+++ b/build.sh
@@ -163,12 +163,19 @@ elif [ "$MODE" = "android" ]; then
case "${ANDROID_ABI}" in
arm64-v8a)
ANDROID_TRIPLE="aarch64-linux-android"
+ ANDROID_CXX_RUNTIME_TRIPLE="aarch64-linux-android"
;;
armeabi-v7a)
ANDROID_TRIPLE="armv7a-linux-androideabi"
+ ANDROID_CXX_RUNTIME_TRIPLE="arm-linux-androideabi"
+ ;;
+ x86)
+ ANDROID_TRIPLE="i686-linux-android"
+ ANDROID_CXX_RUNTIME_TRIPLE="i686-linux-android"
;;
x86_64)
ANDROID_TRIPLE="x86_64-linux-android"
+ ANDROID_CXX_RUNTIME_TRIPLE="x86_64-linux-android"
;;
*)
echo "Unsupported ANDROID_ABI '${ANDROID_ABI}'."
@@ -204,17 +211,23 @@ elif [ "$MODE" = "android" ]; then
echo "Using Android NDK at ${ANDROID_NDK_ROOT} (toolchain ${HOST_TAG}, ABI ${ANDROID_ABI}, API ${ANDROID_API})"
- SDL_BUILD_ROOT="build/android/sdl"
+ ANDROID_TOOLCHAIN_FILE="${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake"
+ if [ ! -f "${ANDROID_TOOLCHAIN_FILE}" ]; then
+ echo "Android toolchain file not found at ${ANDROID_TOOLCHAIN_FILE}"
+ exit 1
+ fi
+
+ REPO_ROOT="$(pwd)"
+
+ SDL_BUILD_ROOT="${REPO_ROOT}/build/android/sdl"
SDL_INSTALL_DIR="${SDL_BUILD_ROOT}/install"
mkdir -p "${SDL_BUILD_ROOT}"
cmake -S third_party/SDL -B "${SDL_BUILD_ROOT}" \
- -DANDROID=ON \
+ -DCMAKE_TOOLCHAIN_FILE="${ANDROID_TOOLCHAIN_FILE}" \
-DANDROID_ABI="${ANDROID_ABI}" \
-DANDROID_PLATFORM="${ANDROID_PLATFORM}" \
-DANDROID_STL=c++_shared \
- -DCMAKE_SYSTEM_NAME=Android \
- -DCMAKE_ANDROID_NDK="${ANDROID_NDK_ROOT}" \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON \
-DSDL_STATIC=OFF \
@@ -227,13 +240,18 @@ elif [ "$MODE" = "android" ]; then
SDL_LIB_PATH="${SDL_INSTALL_DIR}/lib"
SDL_INCLUDE_PATH="${SDL_INSTALL_DIR}/include/SDL2"
SDL_SHARED_LIB="${SDL_LIB_PATH}/libSDL2.so"
+ CXX_SHARED_RUNTIME="${TOOLCHAIN_DIR}/sysroot/usr/lib/${ANDROID_CXX_RUNTIME_TRIPLE}/libc++_shared.so"
if [ ! -f "${SDL_SHARED_LIB}" ]; then
echo "SDL shared library not found at ${SDL_SHARED_LIB}"
exit 1
fi
+ if [ ! -f "${CXX_SHARED_RUNTIME}" ]; then
+ echo "Android C++ shared runtime not found at ${CXX_SHARED_RUNTIME}"
+ exit 1
+ fi
- JNI_LIB_DIR="android/app/src/main/jniLibs/${ANDROID_ABI}"
- SDL_HEADER_STAGE="android/native-deps/SDL2/include"
+ JNI_LIB_DIR="${REPO_ROOT}/android/app/src/main/jniLibs/${ANDROID_ABI}"
+ SDL_HEADER_STAGE="${REPO_ROOT}/android/native-deps/SDL2/include"
mkdir -p "${JNI_LIB_DIR}"
rm -rf "${SDL_HEADER_STAGE}"
mkdir -p "${SDL_HEADER_STAGE}"
@@ -248,25 +266,34 @@ elif [ "$MODE" = "android" ]; then
SDL_CFLAGS="-D_REENTRANT -DANDROID -I${SDL_INCLUDE_PATH}"
SDL_LIBS="-L${SDL_LIB_PATH} -lSDL2 -llog -landroid -lGLESv3 -lEGL -lOpenSLES -lm -lc++_shared"
- FSUI_BUILD_ROOT="build/fsui/android/${ANDROID_ABI}"
+ FSUI_BUILD_ROOT="${REPO_ROOT}/build/fsui/android/${ANDROID_ABI}"
cmake -S third_party/fsui-lib -B "${FSUI_BUILD_ROOT}" \
-DFSUI_BUILD_SAMPLES=OFF \
-DFSUI_PLATFORM_BACKEND=SDL2 \
-DFSUI_USE_SYSTEM_SDL2=ON \
- -DANDROID=ON \
+ -DCMAKE_TOOLCHAIN_FILE="${ANDROID_TOOLCHAIN_FILE}" \
-DANDROID_ABI="${ANDROID_ABI}" \
-DANDROID_PLATFORM="${ANDROID_PLATFORM}" \
-DANDROID_STL=c++_shared \
- -DCMAKE_SYSTEM_NAME=Android \
- -DCMAKE_ANDROID_NDK="${ANDROID_NDK_ROOT}" \
-DCMAKE_BUILD_TYPE=Release \
- -DCMAKE_PREFIX_PATH="${SDL_INSTALL_DIR}"
+ -DCMAKE_PREFIX_PATH="${SDL_INSTALL_DIR}" \
+ -DSDL2_DIR="${SDL_INSTALL_DIR}/lib/cmake/SDL2"
cmake --build "${FSUI_BUILD_ROOT}" --config Release -j"${BUILD_JOBS}"
make clean
- SDL_STATIC=0 SDL_CFLAGS="${SDL_CFLAGS}" SDL_LIBS_DYNAMIC="${SDL_LIBS}" FSUI_BUILD_DIR="$(pwd)/${FSUI_BUILD_ROOT}" make shared
+ make \
+ SDL_STATIC=0 \
+ SDL_CFLAGS="${SDL_CFLAGS}" \
+ SDL_LIBS_DYNAMIC="${SDL_LIBS}" \
+ FSUI_BUILD_DIR="${FSUI_BUILD_ROOT}" \
+ PLATFORM=Android \
+ OS_INFO=Android \
+ PLATFORM_EXTRA_LDFLAGS= \
+ PLATFORM_EXTRA_LIBS= \
+ shared
cp "${SDL_SHARED_LIB}" "${JNI_LIB_DIR}/"
+ cp "${CXX_SHARED_RUNTIME}" "${JNI_LIB_DIR}/"
cp bin/libarmsx.so "${JNI_LIB_DIR}/"
elif [ "$MODE" = "psvita" ]; then
diff --git a/scripts/ci/build-android-all.sh b/scripts/ci/build-android-all.sh
new file mode 100755
index 0000000..7a7566c
--- /dev/null
+++ b/scripts/ci/build-android-all.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+abis="${ANDROID_ABIS:-x86 x86_64 armeabi-v7a arm64-v8a}"
+
+if [ -z "${ANDROID_NDK_ROOT:-}" ]; then
+ echo "ANDROID_NDK_ROOT must be set." >&2
+ exit 1
+fi
+
+if [ -z "${GRADLE_BIN:-}" ]; then
+ echo "GRADLE_BIN must be set to a Gradle executable." >&2
+ exit 1
+fi
+
+rm -rf "$repo_root/android/app/src/main/jniLibs" \
+ "$repo_root/android/native-deps"
+
+for abi in $abis; do
+ echo "Building Android ABI: $abi"
+ (
+ cd "$repo_root"
+ ANDROID_ABI="$abi" ./build.sh android
+ )
+done
+
+(
+ cd "$repo_root"
+ export ARMSX_SKIP_NATIVE_PREPARE=1
+ "$GRADLE_BIN" -p android --no-daemon assembleDebug
+)
diff --git a/scripts/ci/build-linux-target.sh b/scripts/ci/build-linux-target.sh
new file mode 100755
index 0000000..b4767e0
--- /dev/null
+++ b/scripts/ci/build-linux-target.sh
@@ -0,0 +1,134 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+target="${1:-}"
+build_jobs="${BUILD_JOBS:-4}"
+
+if [ -z "$target" ]; then
+ echo "Usage: $0 " >&2
+ exit 1
+fi
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+fsui_build_dir="$repo_root/build/fsui/ci-linux-$target"
+artifact_root="$repo_root/artifacts/linux/$target"
+package_root="$artifact_root/armsx-linux-$target"
+package_zip="$artifact_root/armsx-linux-$target.zip"
+
+cmake_args=(
+ -S "$repo_root/third_party/fsui-lib"
+ -B "$fsui_build_dir"
+ -DFSUI_BUILD_SAMPLES=OFF
+ -DFSUI_PLATFORM_BACKEND=SDL2
+ -DFSUI_USE_SYSTEM_SDL2=ON
+ -DCMAKE_BUILD_TYPE=Release
+)
+
+extra_cflags=""
+extra_ldflags=""
+pkg_config_libdir=""
+dep_search_dirs=""
+
+case "$target" in
+ x64)
+ cc="gcc"
+ cxx="g++"
+ pkg_config_libdir="/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig"
+ dep_search_dirs="/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/lib64:/usr/lib64"
+ ;;
+ x32|x86)
+ target="x32"
+ cc="gcc"
+ cxx="g++"
+ extra_cflags="-m32"
+ extra_ldflags="-m32"
+ pkg_config_libdir="/usr/lib/i386-linux-gnu/pkgconfig:/usr/share/pkgconfig"
+ dep_search_dirs="/lib/i386-linux-gnu:/usr/lib/i386-linux-gnu"
+ ;;
+ arm64)
+ cc="aarch64-linux-gnu-gcc"
+ cxx="aarch64-linux-gnu-g++"
+ pkg_config_libdir="/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig"
+ dep_search_dirs="/lib/aarch64-linux-gnu:/usr/lib/aarch64-linux-gnu:/usr/aarch64-linux-gnu/lib"
+ cmake_args+=(
+ -DCMAKE_SYSTEM_NAME=Linux
+ -DCMAKE_SYSTEM_PROCESSOR=aarch64
+ )
+ ;;
+ arm32)
+ cc="arm-linux-gnueabihf-gcc"
+ cxx="arm-linux-gnueabihf-g++"
+ pkg_config_libdir="/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/share/pkgconfig"
+ dep_search_dirs="/lib/arm-linux-gnueabihf:/usr/lib/arm-linux-gnueabihf:/usr/arm-linux-gnueabihf/lib"
+ cmake_args+=(
+ -DCMAKE_SYSTEM_NAME=Linux
+ -DCMAKE_SYSTEM_PROCESSOR=arm
+ )
+ ;;
+ *)
+ echo "Unsupported Linux target '$target'." >&2
+ exit 1
+ ;;
+esac
+
+export PKG_CONFIG_LIBDIR="$pkg_config_libdir"
+unset PKG_CONFIG_PATH
+
+if [ -n "$extra_cflags" ]; then
+ cmake_args+=(
+ -DCMAKE_C_FLAGS="$extra_cflags"
+ -DCMAKE_CXX_FLAGS="$extra_cflags"
+ -DCMAKE_EXE_LINKER_FLAGS="$extra_ldflags"
+ )
+fi
+
+cmake_args+=(
+ -DCMAKE_C_COMPILER="$cc"
+ -DCMAKE_CXX_COMPILER="$cxx"
+)
+
+sdl_cflags="$(pkg-config --cflags sdl2)"
+sdl_libs="$(pkg-config --libs sdl2)"
+
+if [ -n "$extra_cflags" ]; then
+ sdl_cflags="$sdl_cflags $extra_cflags"
+ sdl_libs="$sdl_libs $extra_ldflags"
+fi
+
+rm -rf "$fsui_build_dir" "$artifact_root"
+mkdir -p "$artifact_root"
+
+cmake "${cmake_args[@]}"
+cmake --build "$fsui_build_dir" -j"$build_jobs"
+
+cd "$repo_root"
+make clean
+make \
+ CC="$cc" \
+ CXX="$cxx" \
+ FSUI_BUILD_DIR="$fsui_build_dir" \
+ SDL_STATIC=0 \
+ SDL_CFLAGS="$sdl_cflags" \
+ SDL_LIBS_DYNAMIC="$sdl_libs" \
+ PLATFORM_EXTRA_LDFLAGS="-static-libstdc++ -static-libgcc"
+
+mkdir -p "$package_root/lib" "$package_root/icons"
+cp "$repo_root/bin/armsx" "$package_root/armsx.bin"
+cp -R "$repo_root/icons/." "$package_root/icons/"
+"$repo_root/scripts/ci/copy-linux-runtime-deps.sh" "$repo_root/bin/armsx" "$package_root/lib" "$dep_search_dirs"
+
+cat > "$package_root/armsx" <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+app_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+export LD_LIBRARY_PATH="$app_dir/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+exec "$app_dir/armsx.bin" "$@"
+EOF
+
+chmod +x "$package_root/armsx"
+
+(
+ cd "$artifact_root"
+ zip -r "$(basename "$package_zip")" "$(basename "$package_root")"
+)
diff --git a/scripts/ci/build-windows-cross.sh b/scripts/ci/build-windows-cross.sh
new file mode 100755
index 0000000..b39b646
--- /dev/null
+++ b/scripts/ci/build-windows-cross.sh
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+target="${1:-}"
+build_jobs="${BUILD_JOBS:-4}"
+
+if [ -z "$target" ]; then
+ echo "Usage: $0 " >&2
+ exit 1
+fi
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+artifact_root="$repo_root/artifacts/windows/$target"
+package_root="$artifact_root/armsx-windows-$target"
+package_zip="$artifact_root/armsx-windows-$target.zip"
+sdl_build_dir="$repo_root/build/sdl/windows-$target"
+sdl_install_dir="$sdl_build_dir/install"
+fsui_build_dir="$repo_root/build/fsui/ci-windows-$target"
+
+case "$target" in
+ x64)
+ triplet="x86_64-w64-mingw32"
+ toolchain_file="$repo_root/third_party/SDL/build-scripts/cmake-toolchain-mingw64-x86_64.cmake"
+ ;;
+ x32|x86)
+ target="x32"
+ triplet="i686-w64-mingw32"
+ toolchain_file="$repo_root/third_party/SDL/build-scripts/cmake-toolchain-mingw64-i686.cmake"
+ ;;
+ *)
+ echo "Unsupported Windows target '$target'." >&2
+ exit 1
+ ;;
+esac
+
+rm -rf "$artifact_root" "$sdl_build_dir" "$fsui_build_dir"
+mkdir -p "$artifact_root"
+
+cmake -S "$repo_root/third_party/SDL" -B "$sdl_build_dir" \
+ -DCMAKE_TOOLCHAIN_FILE="$toolchain_file" \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBUILD_SHARED_LIBS=ON \
+ -DSDL_SHARED=ON \
+ -DSDL_STATIC=OFF \
+ -DSDL_TEST=OFF \
+ -DCMAKE_INSTALL_PREFIX="$sdl_install_dir"
+cmake --build "$sdl_build_dir" -j"$build_jobs"
+cmake --install "$sdl_build_dir"
+
+sdl_include="$sdl_install_dir/include/SDL2"
+sdl_lib="$sdl_install_dir/lib"
+sdl_dll="$(find "$sdl_install_dir" -type f \( -name 'SDL2.dll' -o -name 'libSDL2.dll' \) | head -n 1)"
+if [ -z "$sdl_dll" ]; then
+ echo "Unable to locate SDL2.dll in $sdl_install_dir" >&2
+ exit 1
+fi
+
+cmake -S "$repo_root/third_party/fsui-lib" -B "$fsui_build_dir" \
+ -DCMAKE_TOOLCHAIN_FILE="$toolchain_file" \
+ -DFSUI_BUILD_SAMPLES=OFF \
+ -DFSUI_PLATFORM_BACKEND=SDL2 \
+ -DFSUI_USE_SYSTEM_SDL2=ON \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_PREFIX_PATH="$sdl_install_dir"
+cmake --build "$fsui_build_dir" -j"$build_jobs"
+
+cd "$repo_root"
+make clean
+make \
+ CC="$triplet-gcc" \
+ CXX="$triplet-g++" \
+ WINDRES="$triplet-windres" \
+ WINDOWS_TARGET=1 \
+ SDL_STATIC=0 \
+ FSUI_BUILD_DIR="$fsui_build_dir" \
+ SDL_CFLAGS="-I$sdl_include" \
+ SDL_LIBS_DYNAMIC="-L$sdl_lib -lSDL2 -lsetupapi -limm32 -lversion -lwinmm -lgdi32 -lole32 -loleaut32 -lshell32 -luuid -lopengl32" \
+ PLATFORM_EXTRA_LDFLAGS="-static-libstdc++ -static-libgcc"
+
+mkdir -p "$package_root/icons"
+cp "$repo_root/bin/armsx.exe" "$package_root/"
+cp "$sdl_dll" "$package_root/SDL2.dll"
+cp -R "$repo_root/icons/." "$package_root/icons/"
+
+for runtime_dll in \
+ "$("$triplet-g++" -print-file-name=libstdc++-6.dll)" \
+ "$("$triplet-g++" -print-file-name=libwinpthread-1.dll)"; do
+ if [ -n "$runtime_dll" ] && [ -f "$runtime_dll" ]; then
+ cp "$runtime_dll" "$package_root/"
+ fi
+done
+
+gcc_dir="$(dirname "$("$triplet-gcc" -print-libgcc-file-name)")"
+for libgcc_dll in "$gcc_dir"/libgcc_s_*.dll; do
+ if [ -f "$libgcc_dll" ]; then
+ cp "$libgcc_dll" "$package_root/"
+ fi
+done
+
+(
+ cd "$artifact_root"
+ zip -r "$(basename "$package_zip")" "$(basename "$package_root")"
+)
diff --git a/scripts/ci/copy-linux-runtime-deps.sh b/scripts/ci/copy-linux-runtime-deps.sh
new file mode 100755
index 0000000..dbb42d7
--- /dev/null
+++ b/scripts/ci/copy-linux-runtime-deps.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+if [ "$#" -ne 3 ]; then
+ echo "Usage: $0 " >&2
+ exit 1
+fi
+
+binary="$1"
+dest_dir="$2"
+search_dirs="$3"
+
+mkdir -p "$dest_dir"
+
+declare -A seen=()
+queue=("$binary")
+
+resolve_library() {
+ local lib_name="$1"
+ local dir=
+ local old_ifs="$IFS"
+ IFS=':'
+ for dir in $search_dirs; do
+ if [ -f "$dir/$lib_name" ]; then
+ printf '%s\n' "$dir/$lib_name"
+ IFS="$old_ifs"
+ return 0
+ fi
+ done
+ IFS="$old_ifs"
+ return 1
+}
+
+while [ "${#queue[@]}" -gt 0 ]; do
+ current="${queue[0]}"
+ queue=("${queue[@]:1}")
+
+ while IFS= read -r needed; do
+ [ -n "$needed" ] || continue
+
+ case "$needed" in
+ linux-vdso.so.*|ld-linux*.so.*)
+ continue
+ ;;
+ esac
+
+ if [ -n "${seen[$needed]:-}" ]; then
+ continue
+ fi
+
+ resolved="$(resolve_library "$needed" || true)"
+ if [ -z "$resolved" ]; then
+ echo "Unable to resolve runtime dependency '$needed' for '$current'." >&2
+ exit 1
+ fi
+
+ cp -L "$resolved" "$dest_dir/$needed"
+ seen["$needed"]=1
+ queue+=("$resolved")
+ done < <(readelf -d "$current" 2>/dev/null | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p')
+done