diff --git a/libs/android/app/AndroidManifest.xml.in b/libs/android/app/AndroidManifest.xml.in new file mode 100644 index 00000000..d5c80b40 --- /dev/null +++ b/libs/android/app/AndroidManifest.xml.in @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/libs/android/app/psemek.keystore b/libs/android/app/psemek.keystore new file mode 100644 index 00000000..5f7f7d59 Binary files /dev/null and b/libs/android/app/psemek.keystore differ diff --git a/libs/android/app/src/psemek/app/MainActivity.java.in b/libs/android/app/src/psemek/app/MainActivity.java.in new file mode 100644 index 00000000..62670b10 --- /dev/null +++ b/libs/android/app/src/psemek/app/MainActivity.java.in @@ -0,0 +1,94 @@ +package psemek.app; + +import android.app.Activity; +import android.app.ActionBar; +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.opengl.GLSurfaceView.Renderer; +import android.view.WindowInsetsController; +import android.view.WindowInsets.Type; +import android.view.MotionEvent; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import android.opengl.GLES30; +import android.os.Bundle; +import java.lang.System; + +public class MainActivity extends Activity { + + class RendererImpl implements Renderer { + + @Override + public void onSurfaceCreated(GL10 gl10, EGLConfig config) { + MainActivity.this.nativeApp.init(); + } + + @Override + public void onSurfaceChanged(GL10 gl10, int width, int height) { + MainActivity.this.nativeApp.resize(width, height); + } + + @Override + public void onDrawFrame(GL10 gl10) { + MainActivity.this.nativeApp.drawFrame(); + } + + } + + class ViewImpl extends GLSurfaceView { + + private final RendererImpl renderer; + + public ViewImpl(Context context) { + super(context); + setEGLContextClientVersion(3); + setEGLConfigChooser(8, 8, 8, 8, 24, 8); + renderer = new RendererImpl(); + setRenderer(renderer); + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } + + @Override + public boolean onTouchEvent(MotionEvent e) { + if (e.getAction() == MotionEvent.ACTION_DOWN) { + MainActivity.this.nativeApp.touch((int)e.getX(), (int)e.getY()); + } + return true; + } + + } + + private PsemekApplication nativeApp; + private ViewImpl view; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + System.loadLibrary("boost_random"); + System.loadLibrary("TARGET_NAME"); + + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.hide(); + } + + nativeApp = new PsemekApplication(this.getAssets()); + + view = new ViewImpl(this); + setContentView(view); + + WindowInsetsController windowInsetsController = view.getWindowInsetsController(); + if (windowInsetsController != null) { + windowInsetsController.hide(Type.systemBars()); + windowInsetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } + + @Override + protected void onDestroy() { + nativeApp.destroy(); + super.onDestroy(); + } + +} diff --git a/libs/android/app/src/psemek/app/PsemekApplication.java b/libs/android/app/src/psemek/app/PsemekApplication.java new file mode 100644 index 00000000..3a089736 --- /dev/null +++ b/libs/android/app/src/psemek/app/PsemekApplication.java @@ -0,0 +1,136 @@ +package psemek.app; +import android.util.Log; +import android.content.res.AssetManager; +import android.media.AudioTrack; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import java.io.InputStream; +import java.io.IOException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.Arrays; +import java.lang.Thread; + +public class PsemekApplication { + + private AudioTrack audioTrack; + private Thread audioThread; + private long nativeApp; + + private static native void setupLogging(); + private static native void setAssetManager(AssetManager assetManager); + + private static native long createNativeApp(); + private static native void destroyNativeApp(long ptr); + private static native void resizeNative(long ptr, int width, int height); + private static native void touchNative(long ptr, int x, int y); + private static native void drawFrameNative(long ptr); + + private static native int audioFrequencyNative(); + private static native int audioGetSamples(float buffer[], int sampleOffset, int sampleCount); + + private class StreamEventCallbackImpl extends AudioTrack.StreamEventCallback { + + private float buffer[]; + + public StreamEventCallbackImpl() { + super(); + buffer = new float[1024]; + } + + @Override + public void onDataRequest(AudioTrack track, int sizeInFrames) { + Log.e("psemek", "Requested audio " + sizeInFrames); + int sizeInSamples = sizeInFrames * 2; + if (buffer.length < sizeInSamples) { + buffer = Arrays.copyOf(buffer, sizeInSamples); + } + + int samples = PsemekApplication.audioGetSamples(buffer, 0, buffer.length); + track.write(buffer, 0, samples, AudioTrack.WRITE_BLOCKING); + } + + } + + private class AudioThreadImpl extends Thread { + + private float buffer[]; + + public AudioThreadImpl(int bufferSizeInFrames) { + super("audio"); + + buffer = new float[bufferSizeInFrames * 2]; + } + + @Override + public void run() { + while (true) { + int samples = PsemekApplication.audioGetSamples(buffer, 0, buffer.length); + PsemekApplication.this.audioTrack.write(buffer, 0, samples, AudioTrack.WRITE_BLOCKING); + try { + Thread.sleep(1); + } + catch (InterruptedException e) {} + } + } + } + + public PsemekApplication(AssetManager assetManager) { + setupLogging(); + setAssetManager(assetManager); + + AudioAttributes audioAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_GAME) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build(); + + AudioFormat audioFormat = new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_FLOAT) + .setSampleRate(audioFrequencyNative()) + .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO) + .build(); + + int bufferSize = AudioTrack.getMinBufferSize(audioFrequencyNative(), AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_FLOAT); + + audioTrack = new AudioTrack.Builder() + .setAudioAttributes(audioAttributes) + .setAudioFormat(audioFormat) + .setBufferSizeInBytes(bufferSize) + .setTransferMode(AudioTrack.MODE_STREAM) + .setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY) + .build(); + + audioThread = new AudioThreadImpl(audioTrack.getBufferCapacityInFrames()); + audioThread.start(); + + audioTrack.play(); + } + + public void init() { + nativeApp = createNativeApp(); + } + + public void resize(int width, int height) { + resizeNative(nativeApp, width, height); + } + + public void touch(int x, int y) + { + touchNative(nativeApp, x, y); + } + + public void drawFrame() { + if (nativeApp != 0) { + drawFrameNative(nativeApp); + } + } + + public void destroy() { + audioThread.stop(); + if (nativeApp != 0) + destroyNativeApp(nativeApp); + nativeApp = 0; + } + +} diff --git a/libs/android/source/resource.cpp b/libs/android/source/resource.cpp index 7d554a0a..90694505 100644 --- a/libs/android/source/resource.cpp +++ b/libs/android/source/resource.cpp @@ -41,7 +41,7 @@ namespace psemek::app std::unique_ptr open_resource(std::filesystem::path const & relative_path) { - log::error() << "Opening resource " << relative_path; + log::info() << "Opening resource " << relative_path; auto asset = AAssetManager_open(assetManager, relative_path.c_str(), AASSET_MODE_STREAMING); if (!asset) diff --git a/package/CMakeLists.txt b/package/CMakeLists.txt index 32ec3027..ac851340 100644 --- a/package/CMakeLists.txt +++ b/package/CMakeLists.txt @@ -30,7 +30,13 @@ function(psemek_package_output_path target outvar) message(FATAL "psemek_package_output_path must only be called during packaging") endif() - set(${outvar} "${CMAKE_CURRENT_LIST_DIR}/${PSEMEK_PACKAGE_OUTPUT_PATH}/${target}${PSEMEK_PACKAGE_VERSION_SUFFIX}-${PSEMEK_PACKAGE_SUFFIX}.zip" PARENT_SCOPE) + if(ANDROID) + set(_PACKAGE_EXTENSION apk) + else() + set(_PACKAGE_EXTENSION zip) + endif() + + set(${outvar} "${CMAKE_CURRENT_LIST_DIR}/${PSEMEK_PACKAGE_OUTPUT_PATH}/${target}${PSEMEK_PACKAGE_VERSION_SUFFIX}-${PSEMEK_PACKAGE_SUFFIX}.${_PACKAGE_EXTENSION}" PARENT_SCOPE) endfunction() function(psemek_add_executable_impl target is_application) @@ -71,12 +77,17 @@ function(psemek_add_executable_impl target is_application) ) endif() - if(NOT ANDROID) - psemek_package_output_path(${target} _OUTPUT_PATH) + psemek_package_output_path(${target} _OUTPUT_PATH) + if(NOT ANDROID) add_custom_command(TARGET ${target} POST_BUILD COMMAND echo Packaging target ${target} into ${_OUTPUT_PATH} COMMAND zip -v "${_OUTPUT_PATH}" -j $ ${PSEMEK_PACKAGE_COPY_FILES} + COMMAND echo Packaged target ${target} into ${_OUTPUT_PATH} + ) + else() + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${PSEMEK_PACKAGE_HELPER} "${target}" "${PSEMEK_APPLICATION_NAME}" "${_OUTPUT_PATH}" ) endif() endif() @@ -151,10 +162,11 @@ function(psemek_package_files target) if(PSEMEK_PACKAGE_MODE) if(PSEMEK_PACKAGE_TARGET) if(ANDROID) - + add_custom_command(TARGET ${target} POST_BUILD + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + COMMAND ${PSEMEK_COPY_FILES} ${ARGN} + ) else() - psemek_package_output_path(${target} _OUTPUT_PATH) - add_custom_command(TARGET ${target} POST_BUILD WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} COMMAND zip -v "${_OUTPUT_PATH}" -r ${ARGN} diff --git a/package/android/Dockerfile b/package/android/Dockerfile new file mode 100644 index 00000000..b32c61e7 --- /dev/null +++ b/package/android/Dockerfile @@ -0,0 +1,62 @@ +FROM ubuntu:22.04 + +# Install tools +RUN apt-get update && apt-get upgrade -y +RUN apt-get install -y \ + build-essential cmake git default-jre openjdk-19-jdk \ + libxext-dev libgl-dev \ + wget zip zstd \ + libpng-dev libboost-all-dev \ + libxi-dev libxrender-dev + +# Set user +RUN useradd -u 1000 -U -d /home -M worker +RUN chown -R worker:worker /home /usr/local +USER worker + +# Install android sdkmanager +RUN mkdir -v /home/sdk +WORKDIR /home/sdk +RUN wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip && unzip commandlinetools-linux-9477386_latest.zip && rm commandlinetools-linux-9477386_latest.zip +RUN mv -v cmdline-tools latest +RUN mkdir -v cmdline-tools +RUN mv -v latest cmdline-tools + +# Install sdk +RUN yes | cmdline-tools/latest/bin/sdkmanager "build-tools;34.0.0" "platforms;android-34" + +# Install ndk separately +RUN wget https://dl.google.com/android/repository/android-ndk-r26-beta1-linux.zip && unzip android-ndk-r26-beta1-linux.zip && rm android-ndk-r26-beta1-linux.zip +RUN mkdir -v ndk +RUN mv -v android-ndk-r26-beta1 ndk/26.0.10404224-beta1 + +# Env variables +ENV SDK_ROOT=/home/sdk +ENV BUILD_TOOLS_ROOT=${SDK_ROOT}/build-tools/34.0.0 +ENV NDK_ROOT=${SDK_ROOT}/ndk/26.0.10404224-beta1 +ENV PLATFORM_ROOT=${SDK_ROOT}/platforms/android-34 +ENV PNG_ROOT=/home/png/install +ENV BOOST_ROOT=/home/boost/install + +# Build libpng +RUN mkdir -v /home/png +WORKDIR /home/png +RUN git clone https://github.com/glennrp/libpng.git -b libpng16 --depth 1 source +RUN mkdir -v build install +RUN cmake -S source -B build -DCMAKE_INSTALL_PREFIX=install/arm64-v8a -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake" \ + -DANDROID_USE_LEGACY_TOOLCHAIN_FILE=ON -DANDROID_PLATFORM=34 -DANDROID_STL=c++_shared -DANDROID_CPP_FEATURES="rtti exceptions" -DANDROID_ABI=arm64-v8a +RUN cmake --build build -t install -j + +# Build boost +RUN mkdir -v /home/boost +WORKDIR /home/boost +RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.82.0/source/boost_1_82_0.tar.gz && tar xvf boost_1_82_0.tar.gz && rm boost_1_82_0.tar.gz +RUN mv -v boost_1_82_0 source +RUN cd source && ./bootstrap.sh --with-libraries=random --prefix=../install/arm64-v8a +RUN mkdir -p build install +RUN echo "using clang : android : ${NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++ --target=aarch64-none-linux-android34 --sysroot=${NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/sysroot ;" > user-config.jam +RUN cd source && ./b2 toolset=clang-android target-os=android architecture=arm variant=release link=shared threading=single cxxflags=-fPIC --user-config=../user-config.jam --build-dir=../build install + +# Finalize +WORKDIR /home +COPY package.sh package-helper.sh copy-files.sh ./ \ No newline at end of file diff --git a/package/android/copy-files.sh b/package/android/copy-files.sh new file mode 100755 index 00000000..62af28ef --- /dev/null +++ b/package/android/copy-files.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +cp -rv "$@" /home/app/assets/ \ No newline at end of file diff --git a/package/android/package-helper.sh b/package/android/package-helper.sh new file mode 100755 index 00000000..72829c63 --- /dev/null +++ b/package/android/package-helper.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +if [ "$#" -ne 3 ]; then + echo "Usage: package-helper " + exit -1 +fi + +echo "$1" > /home/target-name +echo "$2" > /home/application-name +echo "$3" > /home/apk-name \ No newline at end of file diff --git a/package/android/package.sh b/package/android/package.sh new file mode 100755 index 00000000..fdbf3b45 --- /dev/null +++ b/package/android/package.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -e + +mkdir build-host tools +cmake -S source -B build-host -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=tools -DPSEMEK_PACKAGE_MODE=ON -DPSEMEK_PACKAGE_HOST=ON -DPSEMEK_BACKEND=OFF +cmake --build build-host -t install -j + +cp -r source/psemek/libs/android/app . + +mkdir -p app/assets + +mkdir -p build-target/arm64-v8a +cmake -S source -B build-target/arm64-v8a/build \ + -DCMAKE_BUILD_TYPE=Release -DANDROID_USE_LEGACY_TOOLCHAIN_FILE=ON -DANDROID_PLATFORM=34 -DANDROID_STL=c++_shared -DANDROID_CPP_FEATURES="rtti exceptions" \ + -DANDROID_ABI=arm64-v8a -DCMAKE_TOOLCHAIN_FILE="${NDK_ROOT}/build/cmake/android.toolchain.cmake" \ + -DCMAKE_FIND_ROOT_PATH="${BOOST_ROOT}/arm64-v8a;${PNG_ROOT}/arm64-v8a;$(pwd)/tools" -DCMAKE_INSTALL_PREFIX="build-target/arm64-v8a/install" \ + -DPSEMEK_BACKEND=ANDROID -DPSEMEK_PACKAGE_MODE=ON -DPSEMEK_PACKAGE_TARGET=ON -DPSEMEK_PACKAGE_TOOLS_PATH="" -DPSEMEK_PACKAGE_HELPER=$(pwd)/package-helper.sh -DPSEMEK_COPY_FILES=$(pwd)/copy-files.sh +cmake --build build-target/arm64-v8a/build -t install -j + +mkdir -p app/lib/arm64-v8a +cp -v build-target/arm64-v8a/install/lib/*.so app/lib/arm64-v8a/ +cp -v ${NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so app/lib/arm64-v8a/ +cp -v ${BOOST_ROOT}/arm64-v8a/lib/*.so app/lib/arm64-v8a/ +cp -v ${PNG_ROOT}/arm64-v8a/lib/*.so app/lib/arm64-v8a/ + +cd app + +cp ./AndroidManifest.xml.in ./AndroidManifest.xml +sed -i -e "s/APPLICATION_NAME/$(cat ../application-name)/g" ./AndroidManifest.xml + +export TARGET_NAME=$(cat ../target-name) +export APK_NAME=$(cat ../apk-name) + +cp src/psemek/app/MainActivity.java.in src/psemek/app/MainActivity.java +sed -i -e "s/TARGET_NAME/${TARGET_NAME}/g" src/psemek/app/MainActivity.java + +mkdir -p dex bin + +${BUILD_TOOLS_ROOT}/aapt package -f -m -J ./src -M ./AndroidManifest.xml -I ${PLATFORM_ROOT}/android.jar + +javac -d obj -classpath src -classpath ${PLATFORM_ROOT}/android.jar src/psemek/app/*.java + +${BUILD_TOOLS_ROOT}/d8 --output ./dex obj/psemek/app/*.class + +${BUILD_TOOLS_ROOT}/aapt package -f -m -F ./bin/${TARGET_NAME}.unaligned.apk -M ./AndroidManifest.xml -I ${PLATFORM_ROOT}/android.jar +zip -r ./bin/${TARGET_NAME}.unaligned.apk assets +${BUILD_TOOLS_ROOT}/aapt add ./bin/${TARGET_NAME}.unaligned.apk lib/arm64-v8a/* +cd dex +${BUILD_TOOLS_ROOT}/aapt add ../bin/${TARGET_NAME}.unaligned.apk classes.dex +cd .. + +${BUILD_TOOLS_ROOT}/zipalign -f 4 ./bin/${TARGET_NAME}.unaligned.apk "${APK_NAME}" + +${BUILD_TOOLS_ROOT}/apksigner sign --ks psemek.keystore --ks-pass "pass:${PSEMEK_KEYSTORE_PASSWORD}" "${APK_NAME}" + +echo "Packaged target ${TARGET_NAME} into ${APK_NAME}" \ No newline at end of file diff --git a/package/bin/psemek-package b/package/bin/psemek-package new file mode 100755 index 00000000..296993a9 --- /dev/null +++ b/package/bin/psemek-package @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -e + +if [ "$#" -ne 2 ]; then + echo "Usage: psemek-package " + echo "Supported platforms:" + echo " linux" + echo " windows" + echo " android" + exit 0 +fi + +case ${1} in + linux) ;; + windows) ;; + android) ;; + *) + echo "Unknown platform: ${1}" + exit -1 +esac + +PROJECT_DIR=`realpath "${2}"` + +docker run -u 1000 -v "${PROJECT_DIR}":/home/source -e PSEMEK_KEYSTORE_PASSWORD=${PSEMEK_KEYSTORE_PASSWORD} lisyarus/psemek:package-${1} /home/package.sh + +echo Packaging finished diff --git a/package/bin/psemek-package-linux b/package/bin/psemek-package-linux deleted file mode 100755 index 648fbba5..00000000 --- a/package/bin/psemek-package-linux +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [ "$#" -ne 1 ]; then - echo "Usage: psemek-package-linux " - exit 0 -fi - -PROJECT_DIR=`realpath "${1}"` - -docker run -u 1000 -v "${PROJECT_DIR}":/home/source lisyarus/psemek:package-linux /home/package.sh - -echo Packaging finished diff --git a/package/bin/psemek-package-win b/package/bin/psemek-package-win deleted file mode 100755 index e3531c3e..00000000 --- a/package/bin/psemek-package-win +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [ "$#" -ne 1 ]; then - echo "Usage: psemek-package-win " - exit 0 -fi - -PROJECT_DIR=`realpath "${1}"` - -docker run -u 1000 -v "${PROJECT_DIR}":/home/source lisyarus/psemek:package-win /home/package.sh - -echo Packaging finished