feat(android): add support for building SDL2/libarmsx for Android

- Updated .gitignore to include Android native libraries and dependencies.
- Modified android/app/build.gradle to register a new task for preparing native binaries.
- Enhanced build.sh to support building SDL2/libarmsx for Android, including NDK setup and BIOS handling.
- Refactored iOS AppDelegate to integrate RNOverlayController and EmulatorRunner for better overlay management.
- Introduced EmulatorRunner class to handle the emulator lifecycle and argument passing.
- Created RNOverlayController to manage the React Native overlay, including mounting and unmounting logic.
- Updated Xcode project files to reflect new source files and dependencies.
This commit is contained in:
moonpower
2025-11-23 05:24:54 +03:00
parent 7a11440e97
commit 40ff6e2d15
10 changed files with 438 additions and 226 deletions
+6 -4
View File
@@ -28,7 +28,9 @@ snap/
psxe.app
.DS_Store
*/.DS_Store
node_modules
ios/HostApp/Pods/*
ios/HostApp/build/*
helpers/*
node_modules
ios/HostApp/Pods/*
ios/HostApp/build/*
helpers/*
android/app/src/main/jniLibs/
android/native-deps/
+19
View File
@@ -65,6 +65,7 @@ android {
java.srcDirs += ['../../third_party/SDL/android-project/app/src/main/java']
res.srcDirs += ['src/main/res', '../../third_party/SDL/android-project/app/src/main/res']
assets.srcDirs += ['src/main/assets']
jniLibs.srcDirs += ['src/main/jniLibs']
}
}
}
@@ -87,3 +88,21 @@ dependencies {
implementation 'com.facebook.react:react-android:0.76.0'
implementation 'com.facebook.react:hermes-android:0.76.0'
}
def repoRootDir = rootProject.projectDir.parentFile
def nativeOutput = file("$projectDir/src/main/jniLibs/arm64-v8a/libarmsx.so")
tasks.register("prepareNativeBinaries") {
group = "native"
description = "Build SDL2/libarmsx via build.sh android."
inputs.file(new File(repoRootDir, "build.sh"))
outputs.file(nativeOutput)
doLast {
exec {
workingDir repoRootDir
commandLine "./build.sh", "android"
}
}
}
preBuild.dependsOn(tasks.named("prepareNativeBinaries"))
+110
View File
@@ -8,6 +8,7 @@ set -e
# ./build.sh ios -> build iOS dylib using iPhone SDK + ios/Frameworks/SDL2.xcframework
# ./build.sh macosapp -> build desktop exe and bundle armsx.app
# ./build.sh wasm -> build WebAssembly target to bin/wasm using emscripten
# ./build.sh android -> build SDL2/libarmsx for Android and stage under android/app/src/main/jniLibs
MODE="$1"
@@ -57,6 +58,115 @@ elif [ "$MODE" = "shared" ]; then
make clean
SDL_STATIC=0 make shared
elif [ "$MODE" = "android" ]; then
ANDROID_NDK_ROOT="${ANDROID_NDK_ROOT:-${ANDROID_NDK_HOME:-${NDK_HOME:-}}}"
if [ -z "${ANDROID_NDK_ROOT}" ]; then
echo "ANDROID_NDK_ROOT (or ANDROID_NDK_HOME / NDK_HOME) must be set to a valid NDK path."
exit 1
fi
if [ ! -d "${ANDROID_NDK_ROOT}" ]; then
echo "Android NDK path ${ANDROID_NDK_ROOT} does not exist."
exit 1
fi
ANDROID_ABI="${ANDROID_ABI:-arm64-v8a}"
ANDROID_API="${ANDROID_API:-26}"
ANDROID_PLATFORM="android-${ANDROID_API}"
case "${ANDROID_ABI}" in
arm64-v8a)
ANDROID_TRIPLE="aarch64-linux-android"
;;
armeabi-v7a)
ANDROID_TRIPLE="armv7a-linux-androideabi"
;;
x86_64)
ANDROID_TRIPLE="x86_64-linux-android"
;;
*)
echo "Unsupported ANDROID_ABI '${ANDROID_ABI}'."
exit 1
;;
esac
HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
HOST_ARCH="$(uname -m)"
case "${HOST_ARCH}" in
arm64|aarch64)
HOST_ARCH_TAG="arm64"
;;
x86_64)
HOST_ARCH_TAG="x86_64"
;;
*)
HOST_ARCH_TAG="${HOST_ARCH}"
;;
esac
HOST_TAG="${HOST_OS}-${HOST_ARCH_TAG}"
TOOLCHAIN_DIR="${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/${HOST_TAG}"
if [ ! -d "${TOOLCHAIN_DIR}" ]; then
# Fallback to the first available prebuilt toolchain
TOOLCHAIN_DIR="$(ls -d "${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/"* 2>/dev/null | head -n 1)"
if [ -z "${TOOLCHAIN_DIR}" ]; then
echo "Unable to locate LLVM toolchain inside ${ANDROID_NDK_ROOT}."
exit 1
fi
HOST_TAG="$(basename "${TOOLCHAIN_DIR}")"
fi
echo "Using Android NDK at ${ANDROID_NDK_ROOT} (toolchain ${HOST_TAG}, ABI ${ANDROID_ABI}, API ${ANDROID_API})"
SDL_BUILD_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 \
-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 \
-DSDL_TEST=OFF \
-DCMAKE_INSTALL_PREFIX="${SDL_INSTALL_DIR}"
cmake --build "${SDL_BUILD_ROOT}" --config Release
cmake --install "${SDL_BUILD_ROOT}" --config Release
SDL_LIB_PATH="${SDL_INSTALL_DIR}/lib"
SDL_INCLUDE_PATH="${SDL_INSTALL_DIR}/include/SDL2"
SDL_SHARED_LIB="${SDL_LIB_PATH}/libSDL2.so"
if [ ! -f "${SDL_SHARED_LIB}" ]; then
echo "SDL shared library not found at ${SDL_SHARED_LIB}"
exit 1
fi
JNI_LIB_DIR="android/app/src/main/jniLibs/${ANDROID_ABI}"
SDL_HEADER_STAGE="android/native-deps/SDL2/include"
mkdir -p "${JNI_LIB_DIR}"
rm -rf "${SDL_HEADER_STAGE}"
mkdir -p "${SDL_HEADER_STAGE}"
cp -R "${SDL_INCLUDE_PATH}/." "${SDL_HEADER_STAGE}/"
TOOLCHAIN_BIN="${TOOLCHAIN_DIR}/bin"
CC="${TOOLCHAIN_BIN}/${ANDROID_TRIPLE}${ANDROID_API}-clang"
CXX="${TOOLCHAIN_BIN}/${ANDROID_TRIPLE}${ANDROID_API}-clang++"
export CC
export CXX
SDL_CFLAGS="-D_REENTRANT -DANDROID -I${SDL_INCLUDE_PATH}"
SDL_LIBS="-L${SDL_LIB_PATH} -lSDL2 -llog -landroid -lGLESv3 -lEGL -lOpenSLES -lm -lc++_shared"
make clean
SDL_STATIC=0 SDL_CFLAGS="${SDL_CFLAGS}" SDL_LIBS_DYNAMIC="${SDL_LIBS}" make shared
cp "${SDL_SHARED_LIB}" "${JNI_LIB_DIR}/"
cp bin/libarmsx.so "${JNI_LIB_DIR}/"
elif [ "$MODE" = "wasm" ]; then
make clean
if command -v emmake >/dev/null 2>&1; then
+69 -60
View File
@@ -7,16 +7,17 @@
objects = {
/* Begin PBXBuildFile section */
1085901F2ED1CE5200D77FB1 /* bios.bin in Resources */ = {isa = PBXBuildFile; fileRef = 1085901E2ED1CE5200D77FB1 /* bios.bin */; };
0F9A2930A40B75845584BE6B /* RNOverlayController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0D4D51B594CEA0DA332E5D3E /* RNOverlayController.mm */; };
3464725A44F4A69FBE2AB974 /* libarmsx.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 653A133BDB61BD1059CFAF23 /* libarmsx.dylib */; };
3A83F5253BCB4AD47A6742B1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F686B87C80EB4D3204CDEEC0 /* main.m */; };
4193FB5FD92D4849FE747E3C /* libPods-ARMSX.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6AC80B70529AD04509472C06 /* libPods-ARMSX.a */; };
48352C131E8B0327373C61EC /* SDL2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 518B2258235B15B15373B04A /* SDL2.xcframework */; };
5480B78F3661F4CB9446657C /* libarmsx.dylib in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 653A133BDB61BD1059CFAF23 /* libarmsx.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
7435C97274E9B2668E6C9C86 /* SDL2.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 518B2258235B15B15373B04A /* SDL2.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
8BC157B9E48AA3D81793AA6A /* ARMSXModule.m in Sources */ = {isa = PBXBuildFile; fileRef = B35E8D4127C001F1E6F6F2AF /* ARMSXModule.m */; };
B16F42A290ECFF7DF154F24F /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9484FEBF96C002F04D73B70C /* AppDelegate.mm */; };
CB535A71335895F3B1AFB7E3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = EB192CCB1425D7B15B24E93A /* PrivacyInfo.xcprivacy */; };
EFAA42EF186BC1A6B4315DDF /* libPods-ARMSX.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B15244F288D2AB7C06E0752A /* libPods-ARMSX.a */; };
D01D643FB3D4148C346B6200 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 150B82E62E95502476AFB049 /* PrivacyInfo.xcprivacy */; };
E46372898534C89975BD3888 /* EmulatorRunner.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6CFA7D190974AD439774FB40 /* EmulatorRunner.mm */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -36,21 +37,24 @@
/* Begin PBXFileReference section */
019479875FC7433ACB65A02E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
1085901E2ED1CE5200D77FB1 /* bios.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = bios.bin; path = ../../bios.bin; sourceTree = "<group>"; };
03BD4A1BC72E3DE47140D96E /* EmulatorRunner.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EmulatorRunner.h; sourceTree = "<group>"; };
0D4D51B594CEA0DA332E5D3E /* RNOverlayController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNOverlayController.mm; sourceTree = "<group>"; };
150B82E62E95502476AFB049 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
1760EE6722F9E2BEDFDCCC6D /* armsx_bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = armsx_bridge.h; sourceTree = "<group>"; };
2EC8F1F7ACC46D5FABC3595C /* Pods-ARMSX.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ARMSX.release.xcconfig"; path = "Target Support Files/Pods-ARMSX/Pods-ARMSX.release.xcconfig"; sourceTree = "<group>"; };
518B2258235B15B15373B04A /* SDL2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SDL2.xcframework; path = ../Frameworks/SDL2.xcframework; sourceTree = "<group>"; };
64E02E161FEB892FC40F8BE1 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
653A133BDB61BD1059CFAF23 /* libarmsx.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libarmsx.dylib; path = ../Frameworks/libarmsx.dylib; sourceTree = "<group>"; };
67F12B2E9A249BA431A15C7C /* Pods-ARMSX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ARMSX.debug.xcconfig"; path = "Target Support Files/Pods-ARMSX/Pods-ARMSX.debug.xcconfig"; sourceTree = "<group>"; };
6AC80B70529AD04509472C06 /* libPods-ARMSX.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ARMSX.a"; sourceTree = BUILT_PRODUCTS_DIR; };
6CFA7D190974AD439774FB40 /* EmulatorRunner.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = EmulatorRunner.mm; sourceTree = "<group>"; };
6E26D0ECD2CD966E5EA1C326 /* Pods-ARMSX.release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Pods-ARMSX.release.xcconfig"; sourceTree = "<group>"; };
87BA3EE4597141AA97A5557D /* ARMSX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ARMSX.app; sourceTree = BUILT_PRODUCTS_DIR; };
9484FEBF96C002F04D73B70C /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = "<group>"; };
996CB57F5DD4B88A0931BB95 /* ARMSXModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARMSXModule.h; sourceTree = "<group>"; };
B15244F288D2AB7C06E0752A /* libPods-ARMSX.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ARMSX.a"; sourceTree = BUILT_PRODUCTS_DIR; };
B35E8D4127C001F1E6F6F2AF /* ARMSXModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARMSXModule.m; sourceTree = "<group>"; };
C65B43834B690E6C938FFCEB /* Pods-ARMSX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ARMSX.debug.xcconfig"; path = "Target Support Files/Pods-ARMSX/Pods-ARMSX.debug.xcconfig"; sourceTree = "<group>"; };
D5E6D32A2F0405EA658F4F9F /* RNOverlayController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNOverlayController.h; sourceTree = "<group>"; };
E3FAF87D7E4A2345FC43CA68 /* Pods-ARMSX.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Pods-ARMSX.debug.xcconfig"; sourceTree = "<group>"; };
EB192CCB1425D7B15B24E93A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
F150862C7055ADDBB5B57CF1 /* Pods-ARMSX.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ARMSX.release.xcconfig"; path = "Target Support Files/Pods-ARMSX/Pods-ARMSX.release.xcconfig"; sourceTree = "<group>"; };
F686B87C80EB4D3204CDEEC0 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -61,7 +65,7 @@
files = (
48352C131E8B0327373C61EC /* SDL2.xcframework in Frameworks */,
3464725A44F4A69FBE2AB974 /* libarmsx.dylib in Frameworks */,
EFAA42EF186BC1A6B4315DDF /* libPods-ARMSX.a in Frameworks */,
4193FB5FD92D4849FE747E3C /* libPods-ARMSX.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -73,7 +77,7 @@
children = (
653A133BDB61BD1059CFAF23 /* libarmsx.dylib */,
518B2258235B15B15373B04A /* SDL2.xcframework */,
B15244F288D2AB7C06E0752A /* libPods-ARMSX.a */,
6AC80B70529AD04509472C06 /* libPods-ARMSX.a */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -81,13 +85,12 @@
48D57A41DAF59CF0D443A086 = {
isa = PBXGroup;
children = (
1085901E2ED1CE5200D77FB1 /* bios.bin */,
B6E3FD0B464E641CA58C9EA3 /* Pods-ARMSX */,
D0581165A1AB8CE875BD92E0 /* Sources */,
473DBC863A1B097C0DD3710F /* Frameworks */,
9B7D6BDFE7232B2E5D8A379F /* Products */,
EB192CCB1425D7B15B24E93A /* PrivacyInfo.xcprivacy */,
A221D24E0608A650E479FF55 /* Pods */,
150B82E62E95502476AFB049 /* PrivacyInfo.xcprivacy */,
A03BFBC5506510D43087F66D /* Pods */,
);
sourceTree = "<group>";
};
@@ -99,12 +102,13 @@
name = Products;
sourceTree = "<group>";
};
A221D24E0608A650E479FF55 /* Pods */ = {
A03BFBC5506510D43087F66D /* Pods */ = {
isa = PBXGroup;
children = (
67F12B2E9A249BA431A15C7C /* Pods-ARMSX.debug.xcconfig */,
F150862C7055ADDBB5B57CF1 /* Pods-ARMSX.release.xcconfig */,
C65B43834B690E6C938FFCEB /* Pods-ARMSX.debug.xcconfig */,
2EC8F1F7ACC46D5FABC3595C /* Pods-ARMSX.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -126,8 +130,12 @@
1760EE6722F9E2BEDFDCCC6D /* armsx_bridge.h */,
996CB57F5DD4B88A0931BB95 /* ARMSXModule.h */,
B35E8D4127C001F1E6F6F2AF /* ARMSXModule.m */,
03BD4A1BC72E3DE47140D96E /* EmulatorRunner.h */,
6CFA7D190974AD439774FB40 /* EmulatorRunner.mm */,
019479875FC7433ACB65A02E /* Info.plist */,
F686B87C80EB4D3204CDEEC0 /* main.m */,
D5E6D32A2F0405EA658F4F9F /* RNOverlayController.h */,
0D4D51B594CEA0DA332E5D3E /* RNOverlayController.mm */,
);
path = Sources;
sourceTree = "<group>";
@@ -139,13 +147,13 @@
isa = PBXNativeTarget;
buildConfigurationList = 1A8EEE030E970E82EC7B77F8 /* Build configuration list for PBXNativeTarget "ARMSX" */;
buildPhases = (
EFC8122B20E4CF7480A3496D /* [CP] Check Pods Manifest.lock */,
317B2A99C6743282DB0B4378 /* [CP] Check Pods Manifest.lock */,
21709ABD74D1031F0389C4EC /* Sources */,
D6910F21954EF81975654CFA /* Frameworks */,
8F7F79E31569D7CF1ACBC707 /* Embed Frameworks */,
469D63DB0DB7A815B1CF8164 /* Resources */,
DBCAE27636921EA4F4D6F40B /* [CP] Embed Pods Frameworks */,
15CF2083A041D419876ECE72 /* [CP] Copy Pods Resources */,
CC0F3B60AB5121E52FB88488 /* Resources */,
A1F84CFC47166FD1D4F87D5C /* [CP] Embed Pods Frameworks */,
A2B4EFBBBDD2A4D81A6CEE13 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -185,53 +193,18 @@
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
469D63DB0DB7A815B1CF8164 /* Resources */ = {
CC0F3B60AB5121E52FB88488 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1085901F2ED1CE5200D77FB1 /* bios.bin in Resources */,
CB535A71335895F3B1AFB7E3 /* PrivacyInfo.xcprivacy in Resources */,
D01D643FB3D4148C346B6200 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
15CF2083A041D419876ECE72 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DBCAE27636921EA4F4D6F40B /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
EFC8122B20E4CF7480A3496D /* [CP] Check Pods Manifest.lock */ = {
317B2A99C6743282DB0B4378 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -253,6 +226,40 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
A1F84CFC47166FD1D4F87D5C /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
A2B4EFBBBDD2A4D81A6CEE13 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ARMSX/Pods-ARMSX-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -262,6 +269,8 @@
files = (
8BC157B9E48AA3D81793AA6A /* ARMSXModule.m in Sources */,
B16F42A290ECFF7DF154F24F /* AppDelegate.mm in Sources */,
E46372898534C89975BD3888 /* EmulatorRunner.mm in Sources */,
0F9A2930A40B75845584BE6B /* RNOverlayController.mm in Sources */,
3A83F5253BCB4AD47A6742B1 /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -271,7 +280,7 @@
/* Begin XCBuildConfiguration section */
6C84D25B5BF8B28E170A1AC3 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 67F12B2E9A249BA431A15C7C /* Pods-ARMSX.debug.xcconfig */;
baseConfigurationReference = C65B43834B690E6C938FFCEB /* Pods-ARMSX.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
@@ -438,7 +447,7 @@
};
EA79FDBEDBB17F1B501A7D1A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F150862C7055ADDBB5B57CF1 /* Pods-ARMSX.release.xcconfig */;
baseConfigurationReference = 2EC8F1F7ACC46D5FABC3595C /* Pods-ARMSX.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
+14 -162
View File
@@ -1,6 +1,5 @@
#import "AppDelegate.h"
#import <SDL2/SDL.h>
#ifndef USE_HERMES
#define USE_HERMES 1
#endif
@@ -9,18 +8,12 @@
#else
#import <React/RCTAppSetupUtils.h>
#endif
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#include <vector>
#include <string>
#import "RNOverlayController.h"
#import "EmulatorRunner.h"
#import "armsx_bridge.h"
@interface AppDelegate () <RCTBridgeDelegate>
@property (nonatomic, assign) BOOL armsxRunning;
@property (nonatomic, strong) RCTBridge *reactBridge;
@property (nonatomic, strong) RCTRootView *reactRootView;
@interface AppDelegate ()
@property (nonatomic, strong) RNOverlayController *overlayController;
@property (nonatomic, strong) EmulatorRunner *emulatorRunner;
@end
@implementation AppDelegate
@@ -56,173 +49,32 @@
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor blackColor];
UIViewController *controller = [UIViewController new];
controller.view.backgroundColor = [UIColor blackColor];
self.window.rootViewController = controller;
self.overlayController = [[RNOverlayController alloc] initWithLaunchOptions:launchOptions];
self.window.rootViewController = self.overlayController;
[self.window makeKeyAndVisible];
[self attachReactNativeOverlay:launchOptions];
[self attachReactNativeOverlay];
return YES;
}
- (void)attachReactNativeOverlay:(NSDictionary *)launchOptions {
if (!self.window) {
return;
}
if (!self.reactBridge) {
self.reactBridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
}
self.reactRootView = [[RCTRootView alloc] initWithBridge:self.reactBridge moduleName:@"ARMSXOverlay" initialProperties:nil];
self.reactRootView.backgroundColor = [UIColor blackColor];
self.reactRootView.frame = self.window.bounds;
self.reactRootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
UIView *targetView = self.window.rootViewController.view ?: self.window;
[targetView addSubview:self.reactRootView];
- (void)attachReactNativeOverlay {
[self.overlayController mountOverlayIfNeeded];
}
- (void)teardownReactSurface {
if (self.reactRootView && self.reactRootView.superview) {
[self.reactRootView removeFromSuperview];
}
self.reactRootView = nil;
[self.overlayController unmountOverlay];
}
- (void)startSDLWithArgs:(NSArray<NSString *> *)args {
if (self.armsxRunning) {
return;
}
[self teardownReactSurface];
[self forceLandscapeIfNeeded];
SDL_SetMainReady();
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER | SDL_INIT_EVENTS) != 0) {
NSLog(@"SDL_Init failed: %s", SDL_GetError());
return;
if (!self.emulatorRunner) {
self.emulatorRunner = [EmulatorRunner new];
}
self.armsxRunning = YES;
// Run the SDL/armsx entry on the main thread to satisfy UIKit threading requirements
dispatch_async(dispatch_get_main_queue(), ^{
@autoreleasepool {
NSMutableArray<NSString *> *nativeArgs = [NSMutableArray arrayWithObject:@"armsx"];
NSFileManager *fm = [NSFileManager defaultManager];
// Look for bios.bin packaged in the bundle (root, Contents/, or resource dir)
NSMutableArray<NSString *> *candidatePaths = [NSMutableArray array];
NSBundle *bundle = [NSBundle mainBundle];
NSString *bundleRoot = bundle.bundlePath;
NSString *resourceRoot = bundle.resourcePath;
// Common bundle layouts
if (bundleRoot.length) {
[candidatePaths addObject:[bundleRoot stringByAppendingPathComponent:@"bios.bin"]];
[candidatePaths addObject:[bundleRoot stringByAppendingPathComponent:@"Contents/bios.bin"]];
}
if (resourceRoot.length) {
[candidatePaths addObject:[resourceRoot stringByAppendingPathComponent:@"bios.bin"]];
}
// XcodeGen resources (if the optional BIOS is present)
NSString *biosPath = [bundle pathForResource:@"bios" ofType:@"bin"];
if (biosPath.length) {
[candidatePaths insertObject:biosPath atIndex:0];
}
biosPath = nil;
for (NSString *candidate in candidatePaths) {
if ([fm fileExistsAtPath:candidate]) {
biosPath = candidate;
break;
}
}
// As a last resort, try SDL's base path
if (!biosPath.length) {
char *basePathC = SDL_GetBasePath();
if (basePathC) {
NSString *basePath = [NSString stringWithUTF8String:basePathC];
SDL_free(basePathC);
NSString *fallback = [basePath stringByAppendingPathComponent:@"bios.bin"];
if ([fm fileExistsAtPath:fallback]) {
biosPath = fallback;
}
}
}
// If found, copy to a writable pref path to avoid any translocation issues
if (biosPath.length && [fm fileExistsAtPath:biosPath]) {
char *prefPathC = SDL_GetPrefPath("nanodata", "armsx");
NSString *prefBase = prefPathC ? [NSString stringWithUTF8String:prefPathC] : nil;
if (prefPathC) {
SDL_free(prefPathC);
}
NSString *writableBios = prefBase ? [prefBase stringByAppendingPathComponent:@"bios.bin"] : nil;
NSError *copyErr = nil;
// Clean existing copy to avoid stale/corrupt data
if (writableBios.length && [fm fileExistsAtPath:writableBios]) {
[fm removeItemAtPath:writableBios error:nil];
}
if (writableBios.length && [fm copyItemAtPath:biosPath toPath:writableBios error:&copyErr]) {
biosPath = writableBios;
NSLog(@"Using bundled BIOS copied to writable path %@", biosPath);
} else if (writableBios.length) {
NSLog(@"Failed to copy BIOS to writable location (%@). Using bundle path. Error: %@", writableBios, copyErr);
}
}
if (biosPath.length && [fm fileExistsAtPath:biosPath]) {
[nativeArgs addObject:@"--bios"];
[nativeArgs addObject:biosPath];
NSLog(@"Passing BIOS path to libarmsx: %@", biosPath);
} else {
NSLog(@"No bundled BIOS found; libarmsx will rely on user-provided settings/CLI.");
}
if (args.count) {
[nativeArgs addObjectsFromArray:args];
}
std::vector<std::string> args;
std::vector<const char *> argv;
for (NSString *s in nativeArgs) {
args.emplace_back([s UTF8String]);
}
for (const auto &s : args)
argv.push_back(s.c_str());
argv.push_back(nullptr);
// Let the dylib create its own SDL window/renderer
external_main((int)args.size(), argv.data(), NULL, NULL);
}
});
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
- (void)applicationWillTerminate:(UIApplication *)application {
self.armsxRunning = NO;
[self.emulatorRunner startWithArgs:args ?: @[]];
}
@end
+13
View File
@@ -0,0 +1,13 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface EmulatorRunner : NSObject
@property (nonatomic, assign, readonly, getter=isRunning) BOOL running;
- (void)startWithArgs:(NSArray<NSString *> *)args;
@end
NS_ASSUME_NONNULL_END
+128
View File
@@ -0,0 +1,128 @@
#import "EmulatorRunner.h"
#import <SDL2/SDL.h>
#include <string>
#include <vector>
#import "armsx_bridge.h"
@interface EmulatorRunner ()
@property (nonatomic, assign, readwrite, getter=isRunning) BOOL running;
@end
@implementation EmulatorRunner
- (void)startWithArgs:(NSArray<NSString *> *)args {
if (self.running) {
return;
}
SDL_SetMainReady();
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER | SDL_INIT_EVENTS) != 0) {
NSLog(@"SDL_Init failed: %s", SDL_GetError());
return;
}
self.running = YES;
dispatch_async(dispatch_get_main_queue(), ^{
@autoreleasepool {
NSMutableArray<NSString *> *nativeArgs = [NSMutableArray arrayWithObject:@"armsx"];
NSFileManager *fm = [NSFileManager defaultManager];
NSMutableArray<NSString *> *candidatePaths = [NSMutableArray array];
NSBundle *bundle = [NSBundle mainBundle];
NSString *bundleRoot = bundle.bundlePath;
NSString *resourceRoot = bundle.resourcePath;
if (bundleRoot.length) {
[candidatePaths addObject:[bundleRoot stringByAppendingPathComponent:@"bios.bin"]];
[candidatePaths addObject:[bundleRoot stringByAppendingPathComponent:@"Contents/bios.bin"]];
}
if (resourceRoot.length) {
[candidatePaths addObject:[resourceRoot stringByAppendingPathComponent:@"bios.bin"]];
}
NSString *biosPath = [bundle pathForResource:@"bios" ofType:@"bin"];
if (biosPath.length) {
[candidatePaths insertObject:biosPath atIndex:0];
}
biosPath = nil;
for (NSString *candidate in candidatePaths) {
if ([fm fileExistsAtPath:candidate]) {
biosPath = candidate;
break;
}
}
if (!biosPath.length) {
char *basePathC = SDL_GetBasePath();
if (basePathC) {
NSString *basePath = [NSString stringWithUTF8String:basePathC];
SDL_free(basePathC);
NSString *fallback = [basePath stringByAppendingPathComponent:@"bios.bin"];
if ([fm fileExistsAtPath:fallback]) {
biosPath = fallback;
}
}
}
if (biosPath.length && [fm fileExistsAtPath:biosPath]) {
char *prefPathC = SDL_GetPrefPath("nanodata", "armsx");
NSString *prefBase = prefPathC ? [NSString stringWithUTF8String:prefPathC] : nil;
if (prefPathC) {
SDL_free(prefPathC);
}
NSString *writableBios = prefBase ? [prefBase stringByAppendingPathComponent:@"bios.bin"] : nil;
NSError *copyErr = nil;
if (writableBios.length && [fm fileExistsAtPath:writableBios]) {
[fm removeItemAtPath:writableBios error:nil];
}
if (writableBios.length && [fm copyItemAtPath:biosPath toPath:writableBios error:&copyErr]) {
biosPath = writableBios;
NSLog(@"Using bundled BIOS copied to writable path %@", biosPath);
} else if (writableBios.length) {
NSLog(@"Failed to copy BIOS to writable location (%@). Using bundle path. Error: %@", writableBios, copyErr);
}
}
if (biosPath.length && [fm fileExistsAtPath:biosPath]) {
[nativeArgs addObject:@"--bios"];
[nativeArgs addObject:biosPath];
NSLog(@"Passing BIOS path to libarmsx: %@", biosPath);
} else {
NSLog(@"No bundled BIOS found; libarmsx will rely on user-provided settings/CLI.");
}
if (args.count) {
[nativeArgs addObjectsFromArray:args];
}
std::vector<std::string> argvStorage;
std::vector<const char *> argv;
for (NSString *stringArg in nativeArgs) {
argvStorage.emplace_back([stringArg UTF8String]);
}
for (const auto &value : argvStorage) {
argv.push_back(value.c_str());
}
argv.push_back(nullptr);
external_main((int)argvStorage.size(), argv.data(), NULL, NULL);
}
});
}
@end
+13
View File
@@ -0,0 +1,13 @@
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNOverlayController : UIViewController
- (instancetype)initWithLaunchOptions:(NSDictionary *)launchOptions;
- (void)mountOverlayIfNeeded;
- (void)unmountOverlay;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,66 @@
#import "RNOverlayController.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
@interface RNOverlayController () <RCTBridgeDelegate>
@property (nonatomic, strong) NSDictionary *launchOptions;
@property (nonatomic, strong, nullable) RCTBridge *bridge;
@property (nonatomic, strong, nullable) RCTRootView *rootView;
@end
@implementation RNOverlayController
- (instancetype)initWithLaunchOptions:(NSDictionary *)launchOptions {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_launchOptions = launchOptions ?: @{};
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
[self mountOverlayIfNeeded];
}
- (void)mountOverlayIfNeeded {
if (!self.bridge) {
self.bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:self.launchOptions];
}
if (self.rootView) {
if (!self.rootView.superview) {
[self.view addSubview:self.rootView];
}
return;
}
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:self.bridge moduleName:@"ARMSXOverlay" initialProperties:nil];
rootView.frame = self.view.bounds;
rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
rootView.backgroundColor = [UIColor blackColor];
[self.view addSubview:rootView];
self.rootView = rootView;
}
- (void)unmountOverlay {
if (self.rootView && self.rootView.superview) {
[self.rootView removeFromSuperview];
}
self.rootView = nil;
}
#pragma mark - RCTBridgeDelegate
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}
@end