Skip to content

Test Android Game (Vulkan) #74

Test Android Game (Vulkan)

Test Android Game (Vulkan) #74

name: Test Android Game (Vulkan)
run-name: >-
${{ 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
test-filter:
description: 'vstest --filter expression forwarded to the on-device runner (blank = all tests)'
type: string
default: ''
project:
description: 'Single test .csproj to build/run (blank = all Android suites)'
type: string
default: ''
commit-sha:
description: Commit SHA to test (default = HEAD of selected branch)
type: string
default: ''
upload-binlog:
description: Capture and upload build binlog (off by default; adds some I/O overhead)
default: false
type: boolean
use-combined:
description: 'Combine all test suites into one APK (faster build/install/run); false = per-suite APKs'
default: true
type: boolean
repeat:
description: 'Rerun the filtered test set up to N times in-process, stop on first failure (flake hunting). 1 = single run (default).'
type: number
default: 1
workflow_call:
inputs:
build-type:
default: Debug
type: string
test-filter:
default: ''
type: string
project:
default: ''
type: string
commit-sha:
default: ''
type: string
# Internal (callers only, e.g. test-gold-gen): tolerate test failures so generating new gold
# doesn't fail the run. Build failures still fail. Not exposed in the dispatch UI.
tolerate-test-failures:
default: false
type: boolean
upload-binlog:
default: false
type: boolean
use-combined:
default: true
type: boolean
repeat:
type: number
default: 1
schedule:
- cron: '37 3 * * *'
concurrency:
group: test-android-game-${{ github.event.pull_request.number || github.ref }}-${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}
cancel-in-progress: true
jobs:
Android-Tests-Game:
name: Android (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: 45
env:
STRIDE_GRAPHICS_SOFTWARE_RENDERING: "1"
STRIDE_MAX_PARALLELISM: "8"
DOTNET_NUGET_SIGNATURE_VERIFICATION: "false"
steps:
# Hosted runners vary per VM (free space, CPU model, noisy neighbors); record the
# initial state to diagnose "No space left on device" failures and timing variance.
- name: Report runner resources
run: |
df -h /
nproc
lscpu | grep -E 'Model name|MHz' || true
free -h
# Bootstrap .github so the local composite action below is resolvable pre-checkout.
- uses: actions/checkout@v7
with:
sparse-checkout: .github
- uses: ./.github/actions/stride-checkout
with:
ref: ${{ (github.event.inputs.commit-sha != '' && github.event.inputs.commit-sha) || (inputs.commit-sha != '' && inputs.commit-sha) || github.ref }}
exclude: samples
- name: Install Android Workload
uses: ./.github/actions/stride-workload
with:
workloads: android
- name: Setup Android SDK
# NDK: not installed/pinned — the runner image preinstalls NDKs and each project's
# Android workload targets resolve _AndroidNdkDirectory locally (Stride.Build.Sdk
# strips it from ProjectReference dispatches so the value never fragments build
# cache keys; Stride.Native.targets enforces a minimum version at compile time).
uses: android-actions/setup-android@v4
with:
cmdline-tools-version: 12266719
accept-android-sdk-licenses: true
packages: tools platform-tools build-tools;36.0.0 platforms;android-34 system-images;android-34;google_apis;x86_64
- 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 libvulkan-dev 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: Resolve build target / suite
run: |
# `project` set (gold-gen: one suite only) → that .csproj's APK + run-suites.sh.
# Unset → use-combined=true (default): Combined.csproj — all suites in one APK,
# one install, one process run (faster build/testing).
# use-combined=false → slnf build + per-suite APKs + per-suite install/run.
PROJ="${{ github.event.inputs.project || inputs.project }}"
USE_COMBINED="${{ github.event.inputs.use-combined || inputs.use-combined || 'true' }}"
if [ -n "$PROJ" ]; then
echo "ANDROID_BUILD_TARGET=$PROJ" >> "$GITHUB_ENV"
echo "ANDROID_TEST_SUITES=$(basename "$PROJ" .csproj)" >> "$GITHUB_ENV"
echo "ANDROID_RUN_SCRIPT=tests/android/run-suites.sh" >> "$GITHUB_ENV"
elif [ "$USE_COMBINED" = "true" ]; then
echo "ANDROID_BUILD_TARGET=sources/tests/Stride.Tests.Combined/Stride.Tests.Combined.csproj" >> "$GITHUB_ENV"
echo "ANDROID_RUN_SCRIPT=tests/android/run-combined.sh" >> "$GITHUB_ENV"
else
# slnf path: one build evaluates the graph once; per-suite APKs land under
# bin/Tests/<Suite>/Android-Vulkan/<Config>/. Run step iterates ANDROID_TEST_SUITES.
echo "ANDROID_BUILD_TARGET=build/Stride.Tests.Game.Android.slnf" >> "$GITHUB_ENV"
echo "ANDROID_RUN_SCRIPT=tests/android/run-suites.sh" >> "$GITHUB_ENV"
{
echo "ANDROID_TEST_SUITES<<EOF"
echo "Stride.Engine.NoAssets.Tests"
echo "Stride.Input.Tests"
echo "Stride.Audio.Tests"
echo "Stride.Particles.Tests"
echo "Stride.UI.Tests"
echo "Stride.Navigation.Tests"
echo "Stride.Physics.Tests"
echo "Stride.Engine.Tests"
echo "Stride.Graphics.Tests"
echo "Stride.Graphics.Tests.10_0"
echo "Stride.Graphics.Tests.11_0"
echo "EOF"
} >> "$GITHUB_ENV"
fi
- name: Reclaim disk before build
# The combined APK build embeds compiled assets for all suites and dies on the
# runner's stock ~20 GB free. Drop preinstalled trees this job never touches.
# Keep dotnet (drives the workload), the Android SDK/NDK, and pwsh (helper layer).
# large-packages stays off (slow apt removals); swift/CodeQL have no action toggle.
run: |
df -h /
sudo rm -rf /usr/share/swift /opt/hostedtoolcache/CodeQL
- name: Free disk space
uses: jlumbroso/free-disk-space@v1.3.1
with:
android: false
dotnet: false
tool-cache: false
large-packages: false
haskell: true
docker-images: true
- name: Build Android test APK(s)
run: |
# Embed assemblies into the APK so the test runner has everything (no Fast Deployment).
dotnet build "$ANDROID_BUILD_TARGET" \
-nr:false -v:m -p:WarningLevel=0 \
-p:StrideSkipAutoPack=true \
${{ inputs.upload-binlog && '-bl:build.binlog' || '' }} \
-p:Configuration=${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }} \
-p:StridePlatforms=Android \
-p:EmbedAssembliesIntoApk=true \
-p:AndroidUseSharedRuntime=false
- name: Upload build binlog
# failure() so we still get the binlog when Build dies (most useful case)
if: failure() || inputs.upload-binlog
uses: actions/upload-artifact@v7
with:
name: build-binlog-android-game
path: build.binlog
if-no-files-found: ignore
- 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
continue-on-error: ${{ inputs.tolerate-test-failures == true }}
# 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.
# -partition-size 8192 (MB): default userdata is too small for several ~90 MB test APKs (INSUFFICIENT_STORAGE).
emulator-options: -no-snapshot-load -no-window -no-boot-anim -gpu host -feature ForceGpuHost -partition-size 8192
disable-animations: true
# android-emulator-runner runs each `script:` line as its own `sh -c`, so multi-line
# shell blocks break with "unexpected end of file". The driver lives in a standalone
# script — Combined or per-suite — chosen by the Resolve step above.
script: bash "$ANDROID_RUN_SCRIPT" "${{ github.event.inputs.build-type || inputs.build-type || 'Debug' }}" "${{ github.event.inputs.test-filter || inputs.test-filter }}" "${{ github.event.inputs.repeat || inputs.repeat || '1' }}"
- name: Publish Test Report
if: always()
uses: phoenix-actions/test-reporting@v16
with:
name: 'Test Report: Android Game (Vulkan)'
path: TestResults/*.trx
reporter: dotnet-trx
output-to: step-summary
list-tests: 'failed'
# Tolerated (gold-gen) test failures shouldn't red the run via the reporter; the Build
# and Test steps still fail normally. Defaults to true for direct / PR-gate runs.
fail-on-error: ${{ inputs.tolerate-test-failures != true }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results-android-vulkan
path: TestResults/
if-no-files-found: ignore
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: test-artifacts-android-vulkan
# Single path so upload-artifact uses tests/local/ as the common ancestor: per-suite
# outputs land as Stride.<Suite>.Tests/... at the artifact root, matching what
# comparegold and the per-suite (master) flow expect. The driver moves the logcat
# into ResultsDir after the pre-pull wipe is past so it ships with this artifact.
path: tests/local/
if-no-files-found: ignore