diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 923883dc..ff6710d8 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -426,3 +426,42 @@ jobs: name: LGPT-${{ github.job }}-${{ github.sha }}.zip path: projects/*.zip if-no-files-found: error + + + android: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4.1.7 + with: + submodules: recursive + + - name: Setup build environment + run: | + sudo apt update + sudo apt install -y make python3-pillow + echo "sdk.dir=$ANDROID_SDK_ROOT" > android/local.properties + $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager "ndk;27.0.12077973" + echo "ndk.dir=$ANDROID_SDK_ROOT/ndk/27.0.12077973" >> android/local.properties + + - name: Build Android APK + working-directory: android + run: ./gradlew assembleDebug + + - name: Package build + working-directory: projects + run: | + TAG=$(curl -s https://api.github.com/repos/djdiskmachine/lgpt-resources/releases/latest | grep '"tag_name"' | head -1 | cut -d'"' -f4) + curl -L -o lgpt-resources.zip https://github.com/djdiskmachine/lgpt-resources/archive/refs/tags/${TAG}.zip + unzip lgpt-resources.zip + mv lgpt-resources-${TAG}/*/ ./resources/packaging + rm -rf lgpt-resources* + mv ../android/app/build/outputs/apk/debug/app-debug.apk ./LittlePiggyTracker.apk + ./resources/packaging/lgpt_package.sh + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: LGPT-${{ github.job }}-${{ github.sha }}.zip + path: projects/*.zip + if-no-files-found: error diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..482d85a6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "android"] + path = android + url = git@github.com:djdiskmachine/littlepiggytracker-android.git diff --git a/android b/android new file mode 160000 index 00000000..f00d4d94 --- /dev/null +++ b/android @@ -0,0 +1 @@ +Subproject commit f00d4d94af4aed37cde85e0236107b06d59c5894 diff --git a/projects/Makefile b/projects/Makefile index 9fc7e43d..33ff12df 100644 --- a/projects/Makefile +++ b/projects/Makefile @@ -111,6 +111,7 @@ RG35XXPLUSDIRS := $(LINUXDIRS) $(DUMMYMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) STEAMDIRS := $(LINUXDIRS) $(JACKDIRS) $(RTAUDIODIRS) $(RTMIDIDIRS) $(SDLDIRS) X64DIRS := $(LINUXDIRS) $(RTMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) X86DIRS := $(LINUXDIRS) $(RTMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) +ANDROIDDIRS := $(LINUXDIRS) $(DUMMYMIDIDIRS) $(SDL2DIRS) $(SDL2AUDIODIRS) $(ANDROIDJNIDIRS) #--------------------------------------------------------------------------------- # Consoles/Embedded @@ -330,6 +331,7 @@ RG35XXPLUSFILES := $(LINUXFILES) $(SDLAUDIOFILES) $(DUMMYMIDIFILES) STEAMFILES := $(LINUXFILES) $(RTAUDIOFILES) $(RTMIDIFILES) $(JACKFILES) X64FILES := $(LINUXFILES) $(RTMIDIFILES) $(SDLAUDIOFILES) X86FILES := $(LINUXFILES) $(RTMIDIFILES) $(SDLAUDIOFILES) +ANDROIDFILES := $(LINUXFILES) $(DUMMYMIDIFILES) $(SDLAUDIOFILES) $(ANDROIDJNIFILES) #--------------------------------------------------------------------------------- # Consoles/Embedded @@ -464,7 +466,7 @@ W32FILES := \ #--------------------------------------------------------------------------------- TARGET := $(TITLE) -BUILD := build$(PLATFORM) +BUILD ?= build$(PLATFORM) SOURCES := $($(PLATFORM)DIRS) $(COMMONDIRS) INCLUDES := diff --git a/projects/Makefile.ANDROID b/projects/Makefile.ANDROID new file mode 100644 index 00000000..668d54f4 --- /dev/null +++ b/projects/Makefile.ANDROID @@ -0,0 +1,102 @@ +-include $(PWD)/rules_base + +#--------------------------------------------------------------------------------- +# Android-specific Makefile for LittleGPTracker +# This Makefile produces libmain.so for SDL2 Android apps +#--------------------------------------------------------------------------------- + +# Determine ABI and set toolchain accordingly +ABI ?= arm64-v8a +NDK_PATH ?= $(ANDROID_NDK_HOME) + +# Override BUILD directory to be ABI-specific +BUILD := build$(PLATFORM)_$(ABI) + +ifeq ($(ABI),armeabi-v7a) + TOOLCHAIN_PREFIX := armv7a-linux-androideabi + ARCH_FLAGS := -march=armv7-a -mfloat-abi=softfp -mfpu=neon + ANDROID_API := 21 +else ifeq ($(ABI),arm64-v8a) + TOOLCHAIN_PREFIX := aarch64-linux-android + ARCH_FLAGS := -march=armv8-a + ANDROID_API := 21 +else + $(error Unsupported ABI: $(ABI)) +endif + +# Set up NDK toolchain +TOOLCHAIN := $(NDK_PATH)/toolchains/llvm/prebuilt/linux-x86_64 +CC := $(TOOLCHAIN)/bin/$(TOOLCHAIN_PREFIX)$(ANDROID_API)-clang +CXX := $(TOOLCHAIN)/bin/$(TOOLCHAIN_PREFIX)$(ANDROID_API)-clang++ +AR := $(TOOLCHAIN)/bin/llvm-ar +STRIP := $(TOOLCHAIN)/bin/llvm-strip + +# Platform definition +PLATFORM := ANDROID + +# Android-specific defines +DEFINES := \ + -DPLATFORM_$(PLATFORM) \ + -DANDROID \ + -D__ANDROID__ \ + -DCPP_MEMORY \ + -DSDL2 \ + -DSDLAUDIO \ + -DDUMMYMIDI + +# Android doesn't support RtMidi - use dummy MIDI instead +# -DRTMIDI \ +# -D_FEAT_MIDI_MULTITHREAD + +# Add _64BIT for arm64-v8a builds +ifeq ($(ABI),arm64-v8a) +DEFINES += -D_64BIT +endif +# SDL2 paths (these should be set by Gradle) +SDL_INCLUDE ?= /path/to/SDL2/include +SDL_LIB ?= /path/to/SDL2/libs/$(ABI) + +# Optimization and compilation flags +OPT_FLAGS := -O2 -fno-strict-aliasing +# For debugging, use: +# OPT_FLAGS := -g -O0 + +INCLUDES := -I$(SDL_INCLUDE) -I$(PWD)/../sources + +# Android requires position-independent code +CFLAGS := $(OPT_FLAGS) $(DEFINES) $(INCLUDES) $(ARCH_FLAGS) \ + -fPIC \ + -ffunction-sections \ + -fdata-sections \ + -Wall \ + -Wno-unused-variable + +CXXFLAGS := $(CFLAGS) -std=gnu++11 -fexceptions -frtti + +# Linker flags for shared library +LDFLAGS := -shared \ + $(ARCH_FLAGS) \ + -Wl,--gc-sections \ + -Wl,--no-undefined \ + -L$(SDL_LIB) + +# Libraries to link +LIBS := -lSDL2 \ + -lGLESv2 \ + -lGLESv1_CM \ + -llog \ + -landroid \ + -lOpenSLES + +# Android-specific JNI extras +ANDROIDJNIDIRS := ../sources/System/Android +ANDROIDJNIFILES := AndroidJNI.o + +# Output configuration +OUTPUT := ../libmain_$(ABI) +EXTENSION := so + +# Build rule for shared library +%.so: $(OFILES) + $(CXX) $(LDFLAGS) -o $@ $(OFILES) $(LIBS) + $(STRIP) --strip-unneeded $@ \ No newline at end of file diff --git a/projects/resources/ANDROID/mapping.xml b/projects/resources/ANDROID/mapping.xml new file mode 100644 index 00000000..ea90d496 --- /dev/null +++ b/projects/resources/ANDROID/mapping.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/projects/resources/packaging/lgpt_package.sh b/projects/resources/packaging/lgpt_package.sh index 968f7731..c66d4f3e 100755 --- a/projects/resources/packaging/lgpt_package.sh +++ b/projects/resources/packaging/lgpt_package.sh @@ -22,6 +22,7 @@ collect_resources() { #1PLATFORM #2lgpt.*-exe if [ "$1" == "PSP" ] || [ "$1" == "GARLIC" ] || [ "$1" == "RG35XXPLUS" ] || + [ "$1" == "ANDROID" ] || [ "$1" == "BITTBOY" ]; then # All files go in the root folder zip -9 $PACKAGE -j $CONTENTS elif [ "$1" == "MACOS" ]; then # .app is a folder @@ -55,5 +56,6 @@ collect_resources BITTBOY lgpt-bittboy.elf collect_resources GARLICPLUS lgpt-garlicplus.elf collect_resources RG35XXPLUS lgpt-rg35xxplus.elf collect_resources MACOS LittleGPTracker.app +collect_resources ANDROID LittlePiggyTracker.apk # collect_resources RS97 lgpt.dge # collect_resources STEAM lgpt.steam-exe diff --git a/sources/Adapters/LINUX/System/LINUXSystem.cpp b/sources/Adapters/LINUX/System/LINUXSystem.cpp index 39031ff1..aae6de0a 100644 --- a/sources/Adapters/LINUX/System/LINUXSystem.cpp +++ b/sources/Adapters/LINUX/System/LINUXSystem.cpp @@ -64,6 +64,25 @@ void LINUXSystem::Boot(int argc,char **argv) { FileSystem::Install(new UnixFileSystem()); // Install aliases +#ifdef __ANDROID__ + // On Android, we MUST use external storage so users can access files via + // file manager + const char* storagePath = SDL_AndroidGetExternalStoragePath(); + if (storagePath) { + Trace::Log("ANDROID", "External storage path: %s", storagePath); + Path::SetAlias("bin", storagePath); + Path::SetAlias("root", storagePath); + } else { + Trace::Error("Failed to get Android external storage - app won't be able to access files!"); + // Still set internal storage as last resort, but warn user + storagePath = SDL_AndroidGetInternalStoragePath(); + if (storagePath) { + Trace::Error("Using internal storage (not accessible via file manager): %s", storagePath); + Path::SetAlias("bin", storagePath); + Path::SetAlias("root", storagePath); + } + } +#else char buff[1024]; ssize_t len = ::readlink("/proc/self/exe",buff,sizeof(buff)-1); if (len != -1) @@ -76,18 +95,19 @@ void LINUXSystem::Boot(int argc,char **argv) { } Path::SetAlias("bin",dirname(buff)) ; Path::SetAlias("root",".") ; +#endif - // always use stdout, user can capture in launch script - Trace::GetInstance()->SetLogger(*(new StdOutLogger())); + // always use stdout, user can capture in launch script + Trace::GetInstance()->SetLogger(*(new StdOutLogger())); - // Process arguments - Config::GetInstance()->ProcessArguments(argc,argv) ; + // Process arguments + Config::GetInstance()->ProcessArguments(argc, argv); - // Install GUI Factory - I_GUIWindowFactory::Install(new GUIFactory()) ; + // Install GUI Factory + I_GUIWindowFactory::Install(new GUIFactory()); - // Install Timers - TimerService::GetInstance()->Install(new SDLTimerService()) ; + // Install Timers + TimerService::GetInstance()->Install(new SDLTimerService()); #ifdef JACKAUDIO Trace::Log("System","Installing JACK audio") ; diff --git a/sources/Adapters/SDL2/GUI/SDLEventManager.cpp b/sources/Adapters/SDL2/GUI/SDLEventManager.cpp index 0c035b7e..a3a314b3 100644 --- a/sources/Adapters/SDL2/GUI/SDLEventManager.cpp +++ b/sources/Adapters/SDL2/GUI/SDLEventManager.cpp @@ -141,12 +141,19 @@ int SDLEventManager::MainLoop() switch (event.window.event) { case SDL_WINDOWEVENT_EXPOSED: + // SDL2 docs: surface re-acquisition is NOT needed + // on expose, only on resize. Just update content. + appWindow->Update() ; + break; case SDL_WINDOWEVENT_RESIZED: case SDL_WINDOWEVENT_SIZE_CHANGED: sdlWindow->ProcessExpose() ; break; } break ; + case SDL_APP_DIDENTERFOREGROUND: + sdlWindow->ProcessExpose() ; + break ; case SDL_USEREVENT: sdlWindow->ProcessUserEvent(event) ; break ; diff --git a/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp b/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp index 7f4eb05a..3e88bf34 100644 --- a/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp +++ b/sources/Adapters/SDL2/GUI/SDLGUIWindowImp.cpp @@ -13,7 +13,7 @@ SDLGUIWindowImp *instance_ ; unsigned short appWidth=320 ; -unsigned short appHeight=240 ; +unsigned short appHeight = 240; SDLGUIWindowImp::SDLGUIWindowImp(GUICreateWindowParams &p) { @@ -37,7 +37,7 @@ SDLGUIWindowImp::SDLGUIWindowImp(GUICreateWindowParams &p) if (displayModeRet < 0) { Trace::Error("DISPLAY","No display mode found. Error Code: %d.", displayModeRet); } - + NAssert(displayModeRet >= 0); #if defined(PLATFORM_PSP) @@ -48,18 +48,24 @@ SDLGUIWindowImp::SDLGUIWindowImp(GUICreateWindowParams &p) int screenWidth = 320; int screenHeight = 240; windowed_ = false; - #else +#elif defined(__ANDROID__) + // Use full screen dimensions on Android int screenWidth = displayMode.w; int screenHeight = displayMode.h; - #endif - - #if defined(RS97) + windowed_ = false; + SDL_Log("DISPLAY: Android - using full display: %dx%d", screenWidth, screenHeight); +#else + int screenWidth = displayMode.w; + int screenHeight = displayMode.h; +#endif + +#if defined(RS97) /* Pick the best bitdepth for the RS97 as it will select 32 as its default, even though that's slow */ bitDepth_ = 16; - #else +#else bitDepth_ = SDL_BITSPERPIXEL(displayMode.format); - #endif - +#endif + const char * driverName = SDL_GetVideoDriver(0); Trace::Log("DISPLAY","Using driver %s. Screen (%d,%d) Bpp:%d",driverName,screenWidth,screenHeight,bitDepth_); @@ -79,8 +85,8 @@ SDLGUIWindowImp::SDLGUIWindowImp(GUICreateWindowParams &p) } #ifdef PLATFORM_PSP - mult_ = 1; - #else + mult_ = 1; +#else int multFromSize=MIN(screenHeight/appHeight,screenWidth/appWidth); const char *mult=Config::GetInstance()->GetValue("SCREENMULT") ; if (mult) @@ -519,6 +525,8 @@ void SDLGUIWindowImp::ProcessExpose() { // Expose and resize events will cause a new surface to be needed. surface_ = SDL_GetWindowSurface(window_); + _window->ForceFullRedraw(); // surface re-acquired: force full bitmap+char + // redraw _window->Update() ; } diff --git a/sources/Adapters/Unix/FileSystem/UnixFileSystem.cpp b/sources/Adapters/Unix/FileSystem/UnixFileSystem.cpp index c4a38950..db13732c 100644 --- a/sources/Adapters/Unix/FileSystem/UnixFileSystem.cpp +++ b/sources/Adapters/Unix/FileSystem/UnixFileSystem.cpp @@ -4,7 +4,7 @@ #include #include #include -#ifdef _64BIT +#if defined(_64BIT) || defined(__ANDROID__) #include #else #include diff --git a/sources/Adapters/Unix/Process/UnixProcess.cpp b/sources/Adapters/Unix/Process/UnixProcess.cpp index 9912ded5..8df06601 100644 --- a/sources/Adapters/Unix/Process/UnixProcess.cpp +++ b/sources/Adapters/Unix/Process/UnixProcess.cpp @@ -22,13 +22,23 @@ SysSemaphore *UnixProcessFactory::CreateNewSemaphore(int initialcount, int maxco } ; UnixSysSemaphore::UnixSysSemaphore(int initialcount,int maxcount) { +#ifdef __ANDROID__ + // Android doesn't reliably support named semaphores, use unnamed ones + sem_init(&unnamed_sem_, 0, initialcount); + sem_ = &unnamed_sem_; +#else sem_=sem_open("n0ssemaphore",O_CREAT,S_IRUSR|S_IWUSR , 0 ); -} ; +#endif +} UnixSysSemaphore::~UnixSysSemaphore() { +#ifdef __ANDROID__ + sem_destroy(&unnamed_sem_); +#else sem_close(sem_) ; sem_unlink("n0ssemaphore") ; -} ; +#endif +} SysSemaphoreResult UnixSysSemaphore::Wait() { sem_wait(sem_) ; diff --git a/sources/Adapters/Unix/Process/UnixProcess.h b/sources/Adapters/Unix/Process/UnixProcess.h index f69181e3..c62731b8 100644 --- a/sources/Adapters/Unix/Process/UnixProcess.h +++ b/sources/Adapters/Unix/Process/UnixProcess.h @@ -20,5 +20,8 @@ class UnixSysSemaphore:public SysSemaphore { virtual SysSemaphoreResult Post() ; private: sem_t *sem_ ; +#ifdef __ANDROID__ + sem_t unnamed_sem_; // Storage for unnamed semaphore on Android +#endif } ; #endif diff --git a/sources/Application/AppWindow.cpp b/sources/Application/AppWindow.cpp index 587ccc47..169f9228 100644 --- a/sources/Application/AppWindow.cpp +++ b/sources/Application/AppWindow.cpp @@ -445,6 +445,14 @@ AppWindow *AppWindow::Create(GUICreateWindowParams ¶ms) { void AppWindow::SetDirty() { _isDirty = true; }; +void AppWindow::ForceFullRedraw() { + // Invalidate screen cache so Flush() redraws every character cell, + // not just changed ones. Needed after surface re-acquisition on resume. + memset(_preScreen, 0xFF, 1200); + memset(_preScreenProp, 0xFF, 1200); + _isDirty = true; +}; + bool AppWindow::onEvent(GUIEvent &event) { // We need to tell the app to quit once we're out of the @@ -523,6 +531,10 @@ void AppWindow::onUpdate() { LoadProject(_newProjectToLoad.c_str()); return; } + if (_isDirty) { + _isDirty = false; + Redraw(); + } Flush(); }; diff --git a/sources/Application/AppWindow.h b/sources/Application/AppWindow.h index a312bda6..1bdcdc75 100644 --- a/sources/Application/AppWindow.h +++ b/sources/Application/AppWindow.h @@ -34,7 +34,8 @@ class AppWindow : public GUIWindow, I_Observer, Status { virtual void Clear(bool all = false); virtual void ClearRect(GUIRect &rect); virtual void SetColor(ColorDefinition cd); - void SetDirty(); + virtual void SetDirty(); + virtual void ForceFullRedraw(); protected: // GUIWindow implementation virtual bool onEvent(GUIEvent &event); diff --git a/sources/Application/Instruments/WavFile.cpp b/sources/Application/Instruments/WavFile.cpp index 96fc2cc4..c18f324a 100644 --- a/sources/Application/Instruments/WavFile.cpp +++ b/sources/Application/Instruments/WavFile.cpp @@ -72,16 +72,15 @@ WavFile *WavFile::Open(const char *path) { WavFile *wav=new WavFile(file) ; - - // Get data - -/* file->Seek(0,SEEK_SET) ; - file->Read(fileBuffer,filesize,1) ; - uchar *ptr=fileBuffer ;*/ - -//Trace::Dump("Loading sample from %s",path) ; - - long position=0 ; + // Get data + + /* file->Seek(0,SEEK_SET) ; + file->Read(fileBuffer,filesize,1) ; + uchar *ptr=fileBuffer ;*/ + + Trace::Log("WAV", "Loading sample from %s", path); + + long position=0 ; // Read 'RIFF' diff --git a/sources/System/Android/AndroidJNI.cpp b/sources/System/Android/AndroidJNI.cpp new file mode 100644 index 00000000..a0f764a9 --- /dev/null +++ b/sources/System/Android/AndroidJNI.cpp @@ -0,0 +1,23 @@ +#ifdef __ANDROID__ + +#include +#include "Application/Application.h" +#include "Application/AppWindow.h" + +extern "C" { + +JNIEXPORT void JNICALL +Java_org_neocities_djdiskmachine_lgpt_1android_LgptSDLActivity_invalidateScreen(JNIEnv *env, jobject thiz) +{ + Application *app = Application::GetInstance(); + if (app) { + AppWindow *appWindow = static_cast(app->GetWindow()); + if (appWindow) { + appWindow->SetDirty(); + } + } +} + +} + +#endif // __ANDROID__ diff --git a/sources/System/Console/Logger.cpp b/sources/System/Console/Logger.cpp index 6dfa7bbc..8263d12f 100644 --- a/sources/System/Console/Logger.cpp +++ b/sources/System/Console/Logger.cpp @@ -1,9 +1,16 @@ #include "Logger.h" #include +#ifdef __ANDROID__ +#include +#endif void StdOutLogger::AddLine(const char *line) { +#ifdef __ANDROID__ + SDL_Log("%s", line); +#else std::cout << line << std::endl ; +#endif } // ---------------------------------------------- diff --git a/sources/UIFramework/SimpleBaseClasses/GUIWindow.h b/sources/UIFramework/SimpleBaseClasses/GUIWindow.h index 04fbbae1..85d56c2e 100644 --- a/sources/UIFramework/SimpleBaseClasses/GUIWindow.h +++ b/sources/UIFramework/SimpleBaseClasses/GUIWindow.h @@ -42,6 +42,8 @@ class GUIWindow: public I_GUIGraphics { virtual void Lock() ; virtual void Unlock() ; virtual void Update() ; + virtual void SetDirty() {}; + virtual void ForceFullRedraw() {} ; virtual void onUpdate()=0 ; // virtual void Save() ; // virtual void Restore() ;