Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7da82bf
Add tiny AMY socket client library for Godot POC
linuxificator Aug 23, 2026
34da5dc
Add raw socket client JNI for Godot POC
linuxificator Aug 23, 2026
f63fbc2
Add minimal Java socket client for Godot raw-wire POC
linuxificator Aug 23, 2026
a19046f
Auto-start separate AMY process outside Godot code
linuxificator Aug 23, 2026
9626f8a
Start separate AMY process via Android lifecycle hook
linuxificator Aug 23, 2026
f4607e8
Add minimal Godot raw-wire C-scale proof of concept
linuxificator Aug 23, 2026
b6fc6a8
Add Godot POC scene
linuxificator Aug 23, 2026
7f42654
Add minimal Godot Android POC project
linuxificator Aug 23, 2026
0418e85
Package AMY service AAR in Godot Android export
linuxificator Aug 23, 2026
149a460
Register AMY Android export plugin
linuxificator Aug 23, 2026
9958834
Prepare only the AMY AAR for Godot POC
linuxificator Aug 23, 2026
94e2829
Add ARM64 and emulator Godot Android export presets
linuxificator Aug 23, 2026
446661f
Remove unnecessary Godot export plugin layer
linuxificator Aug 23, 2026
ad52d01
Prepare only debug AAR for raw-wire POC
linuxificator Aug 23, 2026
72bd737
Delete redundant Godot export plugin
linuxificator Aug 23, 2026
a82a06c
Delete redundant Godot export plugin config
linuxificator Aug 23, 2026
d6f8985
Add end-to-end Godot raw-wire Android CI
linuxificator Aug 23, 2026
3fb8c6a
Run raw-wire POC CI on validation pull request
linuxificator Aug 23, 2026
ab739b3
Fix JavaClass method dispatch in Godot raw-wire POC
linuxificator Aug 23, 2026
7793569
Package AMY AAR in Godot Android export
linuxificator Aug 23, 2026
66b542f
Register AMY Android export plugin
linuxificator Aug 23, 2026
b0b446a
Enable minimal Android AAR export plugin
linuxificator Aug 23, 2026
c21c608
Fix Godot emulator validation KVM permissions
linuxificator Aug 23, 2026
fdb167b
Fix emulator APK install path in raw-wire validation
linuxificator Aug 23, 2026
6bb6354
Launch Godot explicitly in emulator smoke test
linuxificator Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions .github/workflows/godot-android-raw-wire.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
name: Godot Android raw-wire POC

on:
push:
branches:
- feature/godot-android-raw-wire-poc
pull_request:
branches:
- upstream/android-oboe
workflow_dispatch:

permissions:
contents: read

jobs:
raw-wire-poc:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5

- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"

- uses: android-actions/setup-android@v3

- name: Install Android SDK components
run: |
yes | sdkmanager --licenses >/dev/null
sdkmanager \
"platforms;android-36" \
"build-tools;35.0.1" \
"ndk;27.0.12077973" \
"cmake;3.22.1"

- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.13"

- name: Build AMY AAR
run: bash godot/android-hello-world/prepare.sh

- name: Verify AMY AAR contents
run: |
AAR=godot/android-hello-world/addons/amy_android/amy-service-debug.aar
test -s "$AAR"
unzip -l "$AAR" | tee /tmp/amy-aar.txt
grep -q 'jni/arm64-v8a/libamy_android.so' /tmp/amy-aar.txt
grep -q 'jni/arm64-v8a/libamy_android_client.so' /tmp/amy-aar.txt
grep -q 'jni/x86_64/libamy_android.so' /tmp/amy-aar.txt
grep -q 'jni/x86_64/libamy_android_client.so' /tmp/amy-aar.txt
unzip -p "$AAR" classes.jar > /tmp/amy-classes.jar
unzip -l /tmp/amy-classes.jar | tee /tmp/amy-classes.txt
grep -q 'org/amy/audio/AmyService.class' /tmp/amy-classes.txt
grep -q 'org/amy/audio/AmyClient.class' /tmp/amy-classes.txt
grep -q 'org/amy/audio/AmyAutoStartProvider.class' /tmp/amy-classes.txt

- name: Install Godot 4.7.2 and Android export templates
run: |
curl -L --fail --retry 3 \
-o /tmp/godot.zip \
https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_linux.x86_64.zip
unzip -q /tmp/godot.zip -d /tmp/godot
GODOT_BIN="$(find /tmp/godot -maxdepth 1 -type f -name 'Godot*' | head -n1)"
chmod +x "$GODOT_BIN"
sudo cp "$GODOT_BIN" /usr/local/bin/godot

curl -L --fail --retry 3 \
-o /tmp/godot-templates.tpz \
https://github.com/godotengine/godot/releases/download/4.7.2-stable/Godot_v4.7.2-stable_export_templates.tpz
rm -rf /tmp/godot-templates
mkdir -p /tmp/godot-templates
unzip -q /tmp/godot-templates.tpz -d /tmp/godot-templates
mkdir -p "$HOME/.local/share/godot/export_templates/4.7.2.stable"
cp -a /tmp/godot-templates/templates/. \
"$HOME/.local/share/godot/export_templates/4.7.2.stable/"
godot --version

- name: Import Godot project
run: godot --headless --path godot/android-hello-world --import --quit

- name: Export ARM64 phone APK
run: |
mkdir -p godot/android-hello-world/build
godot --headless \
--path godot/android-hello-world \
--install-android-build-template \
--export-debug "Android ARM64" build/amy-godot-raw-wire-arm64.apk
test -s godot/android-hello-world/build/amy-godot-raw-wire-arm64.apk

- name: Export x86_64 emulator APK
run: |
godot --headless \
--path godot/android-hello-world \
--export-debug "Android CI x86_64" build/amy-godot-raw-wire-x86_64.apk
test -s godot/android-hello-world/build/amy-godot-raw-wire-x86_64.apk

- name: Verify APK packaging
run: |
ARM=godot/android-hello-world/build/amy-godot-raw-wire-arm64.apk
X86=godot/android-hello-world/build/amy-godot-raw-wire-x86_64.apk

unzip -l "$ARM" | tee /tmp/arm-apk.txt
grep -q 'lib/arm64-v8a/libgodot_android.so' /tmp/arm-apk.txt
grep -q 'lib/arm64-v8a/libamy_android.so' /tmp/arm-apk.txt
grep -q 'lib/arm64-v8a/libamy_android_client.so' /tmp/arm-apk.txt
! grep -q 'lib/x86_64/' /tmp/arm-apk.txt

unzip -l "$X86" | tee /tmp/x86-apk.txt
grep -q 'lib/x86_64/libgodot_android.so' /tmp/x86-apk.txt
grep -q 'lib/x86_64/libamy_android.so' /tmp/x86-apk.txt
grep -q 'lib/x86_64/libamy_android_client.so' /tmp/x86-apk.txt
! grep -q 'lib/arm64-v8a/' /tmp/x86-apk.txt

for apk in "$ARM" "$X86"; do
for dex in $(zipinfo -1 "$apk" | grep -E '^classes([0-9]+)?\.dex$'); do
unzip -p "$apk" "$dex"
done | strings > /tmp/classes.txt
grep -q 'Lorg/amy/audio/AmyService;' /tmp/classes.txt
grep -q 'Lorg/amy/audio/AmyClient;' /tmp/classes.txt
grep -q 'Lorg/amy/audio/AmyAutoStartProvider;' /tmp/classes.txt
done

- name: Upload ARM64 APK
uses: actions/upload-artifact@v4
with:
name: amy-godot-raw-wire-arm64-apk
path: godot/android-hello-world/build/amy-godot-raw-wire-arm64.apk
if-no-files-found: error

- name: Enable KVM for Android emulator
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Run Godot -> amy.sock -> AMY smoke test
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
profile: pixel_2
disable-animations: true
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -no-boot-anim
script: |
set -e
adb uninstall org.amy.godothello >/dev/null 2>&1 || true
adb install godot/android-hello-world/build/amy-godot-raw-wire-x86_64.apk

# Arm the already-existing AMY integration-test audio capture before
# Android creates the application process/provider. This is test-only;
# Godot itself does not start or control AmyService.
adb shell run-as org.amy.godothello mkdir -p files
adb shell run-as org.amy.godothello touch files/amy-audio-capture.enable

adb logcat -c
adb shell am start -W -n org.amy.godothello/com.godot.game.GodotAppLauncher
sleep 12

adb logcat -d > /tmp/amy-godot.log
adb shell ps -A > /tmp/amy-processes.txt
adb shell run-as org.amy.godothello ls -la files > /tmp/amy-files.txt

echo '===== GODOT/AMY LOG ====='
grep -E 'AmyService|AmyAndroid|AmyAudioCapture|Godot (connected|wire:|raw-wire)' /tmp/amy-godot.log || true
echo '===== PROCESSES ====='
grep 'org.amy.godothello' /tmp/amy-processes.txt || true
echo '===== FILES ====='
cat /tmp/amy-files.txt

grep -q 'AMY listening on private socket' /tmp/amy-godot.log
grep -q 'AMY/Oboe started:' /tmp/amy-godot.log
grep -q 'Godot connected to amy.sock' /tmp/amy-godot.log
grep -q 'Godot wire: v0w0V10.0Z' /tmp/amy-godot.log
test "$(grep -Ec 'Godot wire: v0n(60|62|64|65|67|69|71|72)l1Z' /tmp/amy-godot.log)" -eq 8
grep -q 'Godot wire: v0n60l1Z' /tmp/amy-godot.log
grep -q 'Godot wire: v0n72l1Z' /tmp/amy-godot.log
grep -q 'Godot raw-wire C scale complete' /tmp/amy-godot.log
! grep -q 'Godot raw-wire error:' /tmp/amy-godot.log
grep -q 'org.amy.godothello:amy' /tmp/amy-processes.txt
grep -q 'amy.sock' /tmp/amy-files.txt
grep -q 'amy-render.wav' /tmp/amy-files.txt
grep -q 'amy-oboe.wav' /tmp/amy-files.txt

mkdir -p android/godot-audio-capture
adb exec-out run-as org.amy.godothello cat files/amy-render.wav > android/godot-audio-capture/amy-render.wav
adb exec-out run-as org.amy.godothello cat files/amy-oboe.wav > android/godot-audio-capture/amy-oboe.wav
adb exec-out run-as org.amy.godothello cat files/amy-audio-levels.txt > android/godot-audio-capture/amy-audio-levels.txt
test -s android/godot-audio-capture/amy-render.wav
test -s android/godot-audio-capture/amy-oboe.wav
test -s android/godot-audio-capture/amy-audio-levels.txt

- name: Analyze audio produced by Godot wire commands
run: |
cat android/godot-audio-capture/amy-audio-levels.txt
python3 tests/check_android_audio_capture.py \
android/godot-audio-capture/amy-render.wav \
android/godot-audio-capture/amy-oboe.wav

- name: Upload diagnostics and captured audio
if: always()
uses: actions/upload-artifact@v4
with:
name: amy-godot-raw-wire-diagnostics
path: |
/tmp/amy-godot.log
/tmp/amy-processes.txt
/tmp/amy-files.txt
android/godot-audio-capture/
if-no-files-found: warn
6 changes: 6 additions & 0 deletions android/amy-service/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<provider
android:name="org.amy.audio.AmyAutoStartProvider"
android:authorities="${applicationId}.amy-autostart"
android:exported="false"
android:initOrder="100" />

<service
android:name="org.amy.audio.AmyService"
android:exported="false"
Expand Down
12 changes: 12 additions & 0 deletions android/amy-service/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ add_library(amy_android SHARED
${AMY_SOURCES}
)

# Tiny client used by non-AMY processes (Godot in this proof of concept).
# It contains no synth/audio engine: only one AF_UNIX/SOCK_SEQPACKET connection
# to the separate :amy service process.
add_library(amy_android_client SHARED
amy_android_client.cpp
)

target_include_directories(amy_android PRIVATE
${AMY_SRC}
${CMAKE_CURRENT_SOURCE_DIR}
Expand All @@ -61,7 +68,12 @@ target_compile_options(amy_android PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:-O3;-Wall;-Wextra;-Wno-unused-parameter>
)

target_compile_options(amy_android_client PRIVATE
-O3 -Wall -Wextra -Wno-unused-parameter
)

target_compile_features(amy_android PRIVATE c_std_11 cxx_std_17)
target_compile_features(amy_android_client PRIVATE cxx_std_17)

target_link_libraries(amy_android PRIVATE
oboe::oboe
Expand Down
73 changes: 73 additions & 0 deletions android/amy-service/src/main/cpp/amy_android_client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#include <jni.h>

#include <cerrno>
#include <cstddef>
#include <cstring>

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

namespace {

int connect_socket(const char *path) {
if (path == nullptr || path[0] == '\0') return -EINVAL;

sockaddr_un addr{};
const size_t path_len = std::strlen(path);
if (path_len >= sizeof(addr.sun_path)) return -ENAMETOOLONG;

addr.sun_family = AF_UNIX;
std::memcpy(addr.sun_path, path, path_len + 1u);

const int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
if (fd < 0) return -errno;

if (connect(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
const int saved = errno;
close(fd);
return -saved;
}
return fd;
}

int send_wire(int fd, JNIEnv *env, jstring wire) {
if (fd < 0) return -ENOTCONN;
if (wire == nullptr) return -EINVAL;

const jsize len = env->GetStringUTFLength(wire);
if (len <= 0) return -EINVAL;

const char *bytes = env->GetStringUTFChars(wire, nullptr);
if (bytes == nullptr) return -ENOMEM;

const ssize_t sent = send(fd, bytes, static_cast<size_t>(len), MSG_NOSIGNAL);
const int saved = sent < 0 ? errno : 0;
env->ReleaseStringUTFChars(wire, bytes);

if (sent < 0) return -saved;
if (sent != len) return -EIO;
return 0;
}

} // namespace

extern "C" JNIEXPORT jint JNICALL
Java_org_amy_audio_AmyClient_nativeConnect(JNIEnv *env, jclass, jstring socketPath) {
if (socketPath == nullptr) return -EINVAL;
const char *path = env->GetStringUTFChars(socketPath, nullptr);
if (path == nullptr) return -ENOMEM;
const int result = connect_socket(path);
env->ReleaseStringUTFChars(socketPath, path);
return result;
}

extern "C" JNIEXPORT jint JNICALL
Java_org_amy_audio_AmyClient_nativeSend(JNIEnv *env, jclass, jint fd, jstring wire) {
return send_wire(fd, env, wire);
}

extern "C" JNIEXPORT void JNICALL
Java_org_amy_audio_AmyClient_nativeClose(JNIEnv *, jclass, jint fd) {
if (fd >= 0) close(fd);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.amy.audio;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;

import java.io.File;

/**
* Android lifecycle hook for starting the separate :amy process.
* Godot never starts or stops AmyService; it only connects to amy.sock.
*/
public final class AmyAutoStartProvider extends ContentProvider {
@Override
public boolean onCreate() {
Context context = getContext();
if (context == null) return false;

File socket = new File(context.getFilesDir(), AmyService.DEFAULT_SOCKET_NAME);
Intent intent = new Intent(context, AmyService.class);
intent.putExtra(AmyService.EXTRA_SOCKET_PATH, socket.getAbsolutePath());
context.startService(intent);
return true;
}

@Override public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) { return null; }
@Override public String getType(Uri uri) { return null; }
@Override public Uri insert(Uri uri, ContentValues values) { return null; }
@Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; }
@Override public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) { return 0; }
}
Loading
Loading