Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
02ae65c
fix: exclude Windows native libs from Android build
xen2 Apr 8, 2026
e7e92e6
fix: update Android NDK linking to use unified sysroot, bump min API …
xen2 Apr 8, 2026
4a0a746
fix: add 16KB page alignment for Android 16+ compatibility
xen2 Apr 9, 2026
daf6151
fix: add Android/iOS platform support to NativeLibraryHelper
xen2 Apr 9, 2026
ffe2084
sdk/native: cross-host Android build (linux/macOS hosts, not just Win…
xen2 May 25, 2026
d18fad0
feat: auto-download Vulkan validation layers for Android Debug builds
xen2 Apr 9, 2026
09028ac
sdk: append worktree suffix in time for compile + cross-project query
xen2 May 25, 2026
e4a230d
Stride.Build.Sdk: respect caller-provided AndroidResgenNamespace
xen2 May 25, 2026
08f17a2
sdk: auto-add host platform to _StridePlatforms when targeting mobile
xen2 May 26, 2026
cd6ee2f
assets: skip up-to-date marker write when asset build failed
xen2 May 27, 2026
2e421d4
fix: add Vortice.Vulkan to Android, guard OpenGLES-only code with gra…
xen2 Apr 9, 2026
81cdba7
fix: correct R<->B swap in Android Bitmap Argb8888 load
xen2 May 19, 2026
a05cf1a
Stride.TextureConverter: fix Android ASTC asset compilation
xen2 May 25, 2026
60e526b
Stride.Video: enable MediaCodec backend on Android
xen2 May 26, 2026
8f63173
Stride.Games/Android: drop APK unpacking; read assets from read-only …
xen2 May 26, 2026
8091d3a
Stride.UI: EditText.Android: tolerate deactivate when popup was never…
xen2 May 25, 2026
28af9f6
tests/android: bring up xunit.runner.stride + Stride.Engine.Tests on …
xen2 May 14, 2026
ae5ff00
tests/android: headless mode for MainActivity (xunit_command=run inte…
xen2 May 27, 2026
4fec7f9
Stride.Graphics.Regression: normalise llvmpipe/SwiftShader/WARP adapt…
xen2 May 26, 2026
848df96
Stride.Graphics.Regression/ImageTester: accept all four 8888 RGBA/BGR…
xen2 May 26, 2026
fa13beb
tests/audio: use Headless GameContext on Android
xen2 May 26, 2026
9ccd604
tests: skip desktop-only / writable-FS tests on Android
xen2 May 26, 2026
770a095
tests/android: host-side pwsh script to drive APK + push gold + pull TRX
xen2 May 18, 2026
d74d317
tools/Stride.CompareGold: --port flag so a second instance can run ag…
xen2 May 26, 2026
84fa557
ci: add Android emulator game test workflow
xen2 May 25, 2026
411a549
tests/android: add Vulkan gold images for Android emulator runs
xen2 May 27, 2026
25f7ed3
ci/android: cache built APKs keyed on source tree
xen2 May 27, 2026
544efee
Stride.Graphics: AHardwareBuffer Vulkan import on Android
xen2 May 27, 2026
4f0ef9d
Stride.Video: MediaCodec YUV-upload backend on Android
xen2 May 27, 2026
ecf3db5
Stride.Audio: fix Android teardown crashes
xen2 May 28, 2026
feaf581
ci/android: route emulator Vulkan through Stride NuGet Lavapipe
xen2 May 27, 2026
5bca8bb
tests: enable TesselationTest on Android
xen2 May 29, 2026
231481c
video: add FramesPresented signal; MediaCodec pull-model frame delivery
xen2 May 30, 2026
7fe9621
tests: embed assemblies into Android APK by default outside VS
xen2 May 29, 2026
e179e51
tests/android: add host-bucketed emulator gold images
xen2 May 30, 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
48 changes: 48 additions & 0 deletions .github/workflows/dep-lavapipe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,40 @@ jobs:
name: lavapipe-linux-x64
path: lavapipe-out/

# Second Linux build: no-dma_buf variant for the Android emulator host. The emulator's
# gfxstream renderer aborts headless ("Failed to find memory type for ColorBuffers")
# when the host Vulkan ICD advertises VK_EXT_external_memory_dma_buf, and that breaks
# MediaCodec video. Stock Mesa advertises it whenever linux/udmabuf.h is found at build
# time (-> llvmpipe caps.dmabuf != 0). Dropping that one header probe leaves caps.dmabuf
# at 0 so the extension isn't advertised; x11/wayland WSI, external_memory_fd and fences
# (gated on libdrm, not udmabuf) are unaffected. Matches the AOSP-bundled Lavapipe.
- name: Build no-dma_buf variant (Android emulator)
working-directory: mesa-src
run: |
perl -0pi -e "s/,\s*'linux\/udmabuf\.h'//" meson.build
if grep -q "linux/udmabuf.h" meson.build; then echo "udmabuf probe still present"; exit 1; fi
meson setup _build_nodmabuf \
-Dvulkan-drivers=swrast -Dgallium-drivers=llvmpipe \
-Dplatforms=x11,wayland \
-Dglx=disabled -Degl=disabled \
-Dopengl=false -Dgles1=disabled -Dgles2=disabled \
-Dgallium-extra-hud=false -Dvideo-codecs= \
-Dllvm=enabled -Dshared-llvm=disabled \
-Dbuildtype=release
ninja -C _build_nodmabuf src/gallium/targets/lavapipe/libvulkan_lvp.so

- name: Collect no-dma_buf output
run: |
mkdir -p lavapipe-out-nodmabuf
find mesa-src/_build_nodmabuf -name "libvulkan_lvp.so*" -exec cp {} lavapipe-out-nodmabuf/ \;
ls -la lavapipe-out-nodmabuf/

- name: Upload no-dma_buf artifact
uses: actions/upload-artifact@v4
with:
name: lavapipe-linux-x64-nodmabuf
path: lavapipe-out-nodmabuf/

build-macos:
name: Build Lavapipe (macOS ARM64)
runs-on: macos-15
Expand Down Expand Up @@ -435,6 +469,10 @@ jobs:
uses: actions/download-artifact@v4
with: { name: lavapipe-linux-x64, path: artifacts/linux-x64 }

- name: Download Linux no-dma_buf artifact
uses: actions/download-artifact@v4
with: { name: lavapipe-linux-x64-nodmabuf, path: artifacts/linux-x64-nodmabuf }

- name: Download macOS artifact
uses: actions/download-artifact@v4
with: { name: lavapipe-osx-arm64, path: artifacts/osx-arm64 }
Expand All @@ -451,6 +489,16 @@ jobs:
# Note: lvp_icd.json is included but unused — Lavapipe.cs generates
# its own JSON at runtime pointing to an absolute library_path.

# No-dma_buf Lavapipe for the Android emulator host. Kept OUTSIDE runtimes/ so NuGet
# doesn't auto-deploy it into every consumer's bin — start-emulator.ps1 reads it
# straight from the package cache. Only linux-x64 needs it (Windows/macOS Lavapipe
# don't advertise dma_buf — no libdrm in those jobs). Synthetic RID so the NuGet
# signing tool preserves it (top-level folders are dropped on re-pack) while NuGet
# never auto-deploys it into consumers.
$androidEmu = "build/deps/lavapipe/runtimes/linux-x64-nodmabuf/native"
New-Item -Path $androidEmu -ItemType Directory -Force | Out-Null
Copy-Item artifacts/linux-x64-nodmabuf/libvulkan_lvp.so* $androidEmu

- name: Pack NuGet (dotnet pack)
shell: pwsh
run: |
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ jobs:
outputs:
runtime: ${{ steps.filter.outputs.runtime || 'true' }}
ios: ${{ steps.filter.outputs.ios || 'true' }}
android: ${{ steps.filter.outputs.android || 'true' }}
full: ${{ steps.filter.outputs.full || 'true' }}
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -85,6 +86,17 @@ jobs:
- 'sources/sdk/**'
- 'deps/**'
- 'build/Stride.iOS.slnf'
android:
- 'sources/engine/Stride.Games/**'
- 'sources/**/*.Android.*'
- 'sources/**/Platforms/Android/**'
- 'sources/native/**'
- 'sources/sdk/**/Android*'
- 'sources/sdk/**/*.Android.*'
- 'tests/android/**'
- 'tests/**/Android.Vulkan/**'
- 'build/Stride.Tests.Game.Android.slnf'
- '.github/workflows/test-android-game.yml'
full:
- 'sources/**'
- 'deps/**'
Expand Down Expand Up @@ -118,6 +130,14 @@ jobs:
with:
build-type: Release

Android-Runtime:
needs: changes
if: github.event.pull_request.draft != true && needs.changes.outputs.runtime == 'true'
uses: ./.github/workflows/build-android.yml
secrets: inherit
with:
build-type: Release

Linux-Runtime:
needs: changes
if: github.event.pull_request.draft != true && needs.changes.outputs.runtime == 'true'
Expand Down Expand Up @@ -275,3 +295,18 @@ jobs:
secrets: inherit
with:
build-type: Debug

# Android tests run on Linux + KVM-accelerated x86_64 emulator. Build APKs on the host,
# boot the emulator inside reactivecircus/android-emulator-runner, run each suite serially.
Android-Tests-Game:
needs: [changes, Linux-Runtime]
if: |
always()
&& github.event.pull_request.draft != true
&& needs.changes.outputs.android == 'true'
&& needs['Linux-Runtime'].result != 'failure'
&& needs['Linux-Runtime'].result != 'cancelled'
uses: ./.github/workflows/test-android-game.yml
secrets: inherit
with:
build-type: Debug
1 change: 1 addition & 0 deletions .github/workflows/pr-test-chatops.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ jobs:
'windows-game-api-neutral':{ kind: 'test', event: 'test-windows-game-api-neutral',workflow: 'test-windows-game-api-neutral.yml' },
'windows-simple': { kind: 'test', event: 'test-windows-simple', workflow: 'test-windows-simple.yml' },
'linux-game': { kind: 'test', event: 'test-linux-game', workflow: 'test-linux-game.yml' },
'android-game': { kind: 'test', event: 'test-android-game', workflow: 'test-android-game.yml' },
'linux-simple': { kind: 'test', event: 'test-linux-simple', workflow: 'test-linux-simple.yml' },
// Builds (mirrors main.yml jobs; new entries here also need a matching job in main.yml)
'assembly-processor': { kind: 'build', event: 'build-assembly-processor', workflow: 'build-assembly-processor.yml' },
Expand Down
230 changes: 230 additions & 0 deletions .github/workflows/test-android-game.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
name: Test Android Game (Vulkan)

run-name: >-
${{ github.event.client_payload.pr_number
&& format('Test Android Game (Vulkan) — PR #{0} @ {1}', github.event.client_payload.pr_number, github.event.client_payload.pr_head_sha)
|| (github.event.inputs.commit-sha != '' && format('Test Android Game (Vulkan) @ {0}', github.event.inputs.commit-sha))
|| '' }}

on:
workflow_dispatch:
inputs:
build-type:
description: Build Configuration
default: Debug
type: choice
options:
- Debug
- Release
commit-sha:
description: Commit SHA to test (default = HEAD of selected branch)
type: string
default: ''
workflow_call:
inputs:
build-type:
default: Debug
type: string
commit-sha:
default: ''
type: string
repository_dispatch:
types: [test-android-game]
schedule:
- cron: '37 3 * * *'


concurrency:
group: test-android-game-${{ github.event.pull_request.number || github.event.client_payload.pr_number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}
cancel-in-progress: true

jobs:
Android-Tests-Game:
name: Test Game Vulkan (${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }})
# Linux host: KVM-accelerated x86_64 Android emulator is the only path that runs in a
# reasonable amount of time on a hosted runner. macOS-14+ Apple Silicon runners don't
# support nested virt for AOSP emulator; older macOS-13 Intel HAXM is being retired.
runs-on: ubuntu-24.04
timeout-minutes: 90
env:
STRIDE_GRAPHICS_SOFTWARE_RENDERING: "1"
STRIDE_MAX_PARALLELISM: "8"
DOTNET_NUGET_SIGNATURE_VERIFICATION: "false"
# Multi-platform test suites we build & run on emulator. Suite name == assembly name
# (matches the master "tests key paths by assembly name" convention).
ANDROID_TEST_SUITES: |
Stride.Input.Tests
Stride.Audio.Tests
Stride.Particles.Tests
Stride.UI.Tests
Stride.Navigation.Tests
Stride.Physics.Tests
Stride.Engine.Tests
Stride.Graphics.Tests
Stride.Graphics.Tests.10_0
Stride.Graphics.Tests.11_0
steps:
- uses: actions/checkout@v4
with:
lfs: true
submodules: true
ref: ${{ (github.event.inputs.commit-sha != '' && github.event.inputs.commit-sha) || (inputs.commit-sha != '' && inputs.commit-sha) || (github.event.client_payload.pr_number && format('refs/pull/{0}/merge', github.event.client_payload.pr_number)) || github.ref }}

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Install Android Workload
run: dotnet workload install android

- name: Setup Android SDK + NDK
uses: android-actions/setup-android@v3
with:
cmdline-tools-version: 12266719
accept-android-sdk-licenses: true
packages: tools platform-tools ndk;21.4.7075529 build-tools;36.0.0 platforms;android-34 system-images;android-34;google_apis;x86_64

- name: Configure NDK Environment
run: |
echo "ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/21.4.7075529" >> $GITHUB_ENV
echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/21.4.7075529" >> $GITHUB_ENV

- name: Install Linux host runtime deps (for asset compilation)
run: |
# Asset compilation runs on the Linux host with Vulkan via Lavapipe. Mirrors the
# set used by test-linux-game.yml.
sudo apt-get update
sudo apt-get install -y libvulkan1 xvfb lld vulkan-validationlayers libva2 libva-drm2 libva-x11-2

- name: Start virtual display (asset compiler Vulkan)
run: |
Xvfb :99 -screen 0 1920x1080x24 &
echo "DISPLAY=:99" >> $GITHUB_ENV

- name: Register Lavapipe ICD
run: |
# Use the no-dma_buf Lavapipe variant (runtimes/linux-x64-nodmabuf/native/): gfxstream
# aborts headless on an ICD advertising VK_EXT_external_memory_dma_buf. AssetCompiler
# needs a Vulkan device at build time (Skybox/IBL etc.) and is indifferent to dma_buf,
# so the same ICD serves both; it then drives the emulator's Vulkan path via -gpu host
# -feature ForceGpuHost (gfxstream forwards Vulkan to the host loader, honouring
# VK_DRIVER_FILES).
dotnet restore build/Stride.Tests.Game.Runtime.slnf \
-p:StridePlatforms=Linux \
-p:StrideGraphicsApis=Vulkan
LIB=$(find ~/.nuget/packages/stride.dependencies.lavapipe -path "*/runtimes/linux-x64-nodmabuf/native/libvulkan_lvp.so" | head -1)
if [ -z "$LIB" ]; then
echo "::error::Lavapipe libvulkan_lvp.so (no-dma_buf variant) not found in NuGet cache"
exit 1
fi
LIB_ABS=$(readlink -f "$LIB")
ICD_JSON="$PWD/lvp_icd.json"
echo "{\"file_format_version\":\"1.0.0\",\"ICD\":{\"library_path\":\"$LIB_ABS\",\"api_version\":\"1.3.0\"}}" > "$ICD_JSON"
echo "VK_DRIVER_FILES=$ICD_JSON" >> $GITHUB_ENV

- name: Build Android emulator helper Vulkan layer
# Stamps the host OS into the device name so gold images bucket by emulator host
# (Lavapipe renders slightly differently per host RID, and the guest can't otherwise
# tell which host it's on).
run: pwsh tests/android/stride_android_emu_helper_layer/build-and-install.ps1

- name: Restore Android test APK cache
# Key on the source/build inputs only — workflow YAML and run-*.sh/.ps1 are excluded
# so the test step can be iterated on without invalidating cached APKs.
# Split into restore/save so the cache is saved after a successful build even if the
# test step later fails, letting test-step iteration reuse cached APKs.
id: apk-cache
uses: actions/cache/restore@v4
with:
path: bin/Tests/**/Android-Vulkan/**/*-Signed.apk
key: android-apks-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}-${{ hashFiles('sources/**', 'tests/**/*.cs', 'tests/**/*.csproj', 'tests/**/*.sdsl', 'tests/**/*.sdfx', 'build/**', 'deps/**', 'global.json', 'Directory.*.props', 'Directory.*.targets', '**/*.csproj', '**/*.props', '**/*.targets') }}

- name: Build Android test APKs
if: steps.apk-cache.outputs.cache-hit != 'true'
run: |
# Multi-platform test projects build their Android APK when StridePlatforms=Android.
# Embed assemblies into the APK so the test runner has everything (no Fast Deployment).
# Single slnf build evaluates the graph once; failed projects don't block independents,
# and the test step still runs whatever APKs were produced for partial coverage.
# -m:1 works around a Xamarin.Android parallel-hash collision on shared
# .aar files (e.g. Stride.Games.aar) — see dotnet/android.
dotnet build build/Stride.Tests.Game.Android.slnf \
-nr:false -m:1 -v:m -p:WarningLevel=0 \
-p:Configuration=${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} \
-p:StridePlatforms=Android \
-p:EmbedAssembliesIntoApk=true \
-p:AndroidUseSharedRuntime=false \
-p:_AndroidNdkDirectory="$ANDROID_NDK_ROOT" \
|| echo "::warning::Build had failures — test step will run whichever APKs were produced"

- name: Save Android test APK cache
# Save immediately after build so failing tests don't lose us the cached APKs.
if: steps.apk-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: bin/Tests/**/Android-Vulkan/**/*-Signed.apk
key: ${{ steps.apk-cache.outputs.cache-primary-key }}

- name: Enable KVM
# Hosted ubuntu runners expose /dev/kvm but the device node has restrictive perms
# by default. This udev rule grants 0666 so the emulator can use KVM acceleration.
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 tests on Android emulator
# Action handles emulator boot/teardown; we run the per-suite pwsh driver inside.
# API 34 / google_apis / x86_64 — API 36+ (Android 16) crashes Mono JIT on the
# emulator (W^X enforcement), so we pin to 34.
env:
# Empty bypasses gfxstream's built-in ICD overrides (initIcdPaths/setIcdPaths)
# which would clobber VK_DRIVER_FILES with bundled paths.
ANDROID_EMU_VK_ICD: ""
# Activates the host-OS-stamp implicit Vulkan layer installed above.
STRIDE_EMU_LAYER: "1"
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
force-avd-creation: false
# -no-audio omitted: Stride.Audio.Tests' AudioTrack init segfaults in native code
# if the emulator has no audio backend. Default backend on Linux is none/null
# which is enough to keep AudioTrack from crashing.
# -gpu host + -feature ForceGpuHost route Vulkan through the host loader so it
# honours VK_DRIVER_FILES (set in "Register Lavapipe ICD") and picks our Stride
# Lavapipe. The Linux emulator build has no -use-host-vulkan flag, so this
# combination is its equivalent.
emulator-options: -no-snapshot-load -no-window -no-boot-anim -gpu host -feature ForceGpuHost
disable-animations: true
# android-emulator-runner runs each `script:` line as its own `sh -c`, so multi-line
# shell blocks (while/if) break with "unexpected end of file". Loop lives in a
# standalone script invoked as a single command.
script: bash tests/android/run-suites.sh "${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}"

- name: Publish Test Report
if: always()
uses: phoenix-actions/test-reporting@v15
with:
name: 'Test Report: Android Game (Vulkan)'
path: TestResults/*.trx
reporter: dotnet-trx
output-to: step-summary
list-tests: 'failed'

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-android-vulkan
path: TestResults/
if-no-files-found: ignore

- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: test-artifacts-android-vulkan
path: tests/local/
if-no-files-found: ignore
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ bin/packages/*
!bin/packages/.gitkeep
ipch/
obj/
TestResults/
.vs/
.idea/
Cache/
Expand Down
17 changes: 17 additions & 0 deletions build/Stride.Tests.Game.Android.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"solution": {
"path": "Stride.sln",
"projects": [
"..\\sources\\engine\\Stride.Audio.Tests\\Stride.Audio.Tests.csproj",
"..\\sources\\engine\\Stride.Engine.Tests\\Stride.Engine.Tests.csproj",
"..\\sources\\engine\\Stride.Graphics.Tests\\Stride.Graphics.Tests.csproj",
"..\\sources\\engine\\Stride.Graphics.Tests.10_0\\Stride.Graphics.Tests.10_0.csproj",
"..\\sources\\engine\\Stride.Graphics.Tests.11_0\\Stride.Graphics.Tests.11_0.csproj",
"..\\sources\\engine\\Stride.Input.Tests\\Stride.Input.Tests.csproj",
"..\\sources\\engine\\Stride.Navigation.Tests\\Stride.Navigation.Tests.csproj",
"..\\sources\\engine\\Stride.Particles.Tests\\Stride.Particles.Tests.csproj",
"..\\sources\\engine\\Stride.Physics.Tests\\Stride.Physics.Tests.csproj",
"..\\sources\\engine\\Stride.UI.Tests\\Stride.UI.Tests.csproj"
]
}
}
Loading
Loading