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() ;