Skip to content

Android: runtime build + CI emulator tests#3194

Open
xen2 wants to merge 35 commits into
stride3d:masterfrom
xen2:feature/ci-android
Open

Android: runtime build + CI emulator tests#3194
xen2 wants to merge 35 commits into
stride3d:masterfrom
xen2:feature/ci-android

Conversation

@xen2
Copy link
Copy Markdown
Member

@xen2 xen2 commented May 28, 2026

PR Details

Brings Android Vulkan to CI parity with the other platforms: runtime builds on Linux/macOS hosts, gameplay tests run on a KVM-accelerated x86_64 emulator, plus the platform fixes the path turned up.

What's in

  • Android runtime — Linux/macOS-host builds, unified-sysroot NDK, API 21 floor, 16KB page alignment, Vortice.Vulkan, auto-download Vulkan validation layers (Debug), SDK glue for mobile workflows.
  • Android-targeted engine fixes — Stride.Games asset loading, MediaCodec video backend, ASTC textures, image-load R↔B swap, UI headless safety, image regression normalization across SwiftShader/WARP/llvmpipe.
  • Test harness on emulator — xunit.runner.stride brought up on Android, headless MainActivity intent extras, host-side pwsh driver that pushes APK + gold and pulls TRX via adb. Audio tests use a headless GameContext; desktop-only tests skipped.
  • CI workflow (test-android-game.yml) — Ubuntu + KVM x86_64 emulator (API 34, SwiftShader), per-suite serial run, source-keyed APK cache for fast test-step iteration. Path-filtered auto-trigger on Android-touching PRs; /test android-game chatops fallback; nightly schedule on master. New Android-Runtime compile job mirrors iOS-Runtime.
  • ToolingStride.CompareGold --port for parallel checkouts; tests/android/start-emulator.ps1 cross-platform launcher matching CI's emulator flags.

Known limitations

  • Build uses -m:1 (serial) — workaround for parallel-build .nupkg/.aar lock races, structural fix pending.

Test plan

  • Android-Runtime build green on this branch
  • test-android-game workflow green
  • /test android-game chatops command works from a PR comment

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

@xen2 xen2 changed the title Feature/ci android Android CI May 28, 2026
@xen2 xen2 changed the title Android CI Android: runtime build + CI emulator tests May 28, 2026
@xen2 xen2 closed this May 28, 2026
@xen2 xen2 reopened this May 28, 2026
@xen2 xen2 force-pushed the feature/ci-android branch 3 times, most recently from 52974e4 to f108233 Compare May 28, 2026 03:09
<ItemGroup>
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
<PackageVersion Include="Avalonia.Android" Version="$(AvaloniaVersion)" />
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: sort by name

xen2 added 16 commits May 29, 2026 19:11
…to 21

Remove deprecated per-platform sysroot paths (platforms/android-9/arch-*)
that no longer exist in modern NDK (r21+). The unified sysroot from
StrideNativeAndroidClang is already passed. Also bump 32-bit min API
from 16 to 21 (Android 5.0).
…dows)

CompileNativeClang_Android assumed Windows-only: hardcoded
'prebuilt/windows-x86_64/bin/clang.exe' for the toolchain folder and '\'
path separators in every Exec command. On Linux the toolchain folder
resolved empty → '/bin/clang.exe' (exit 127) and backslashes ended up as
literal characters in output paths (e.g. obj/.../\Celt_armeabi-v7a.o).

Detect the host tag (windows-x86_64 / linux-x86_64 / darwin-x86_64) and
the exe extension up front, then thread them through every clang
invocation; switch the Android Exec templates to forward slashes (which
NDK clang accepts on every host). Fail fast with a clear error if no
matching toolchain dir exists.
Downloads from KhronosGroup GitHub releases, caches locally in
%LOCALAPPDATA%\Stride\VulkanValidation\. The .so is bundled into the
APK so the Vulkan loader picks it up automatically. Only active for
Android + Vulkan + Debug configuration.
Add NuGetResolverModuleInitializerGenerate and _GetProjectVersion to
StrideAppendWorktreeVersion's BeforeTargets. Without them the entry-point
.cs and the nuspec <dependency> entries kept the un-suffixed version, so
CompilerApp in a secondary worktree asked the resolver for "4.4.0.2" while
the actually-built packages were "4.4.0.2-devN".
The unconditional `AndroidResgenNamespace = $(AssemblyName)` override broke
projects whose assembly name isn't a valid C# identifier (Stride.Graphics.Tests
.10_0/.11_0, where the segment starts with a digit). Wire the assignment
through the standard `Condition="'$(AndroidResgenNamespace)' == ''"` so a
csproj can override.

Override applied in Stride.Graphics.Tests.10_0/.11_0 to use the bare
`Stride.Graphics.Tests` namespace for the generated Resource class.
AssetCompiler runs in-process on the host and loads native libs (libstrideaudio
for Celt, etc). Mutate the local _StridePlatforms helper rather than the global
StridePlatforms — the latter is immutable when set via -p: and also drives
StrideTestPlatforms, which should stay scoped to the mobile target. Opt out
with -p:StridePlatformAutoAddHost=false for CI runtime-only mobile builds.
Otherwise MSBuild's Inputs/Outputs check on StrideCompileAsset sees the
freshly-touched bundle and skips re-running CompilerApp next build, silently
packing the partial bundle into the APK. Now a failed compile leaves no marker
and the next build retries.
…phics API

- Include Vortice.Vulkan PackageReference for Android TFM
- Guard DefaultWindow, OES texture, and MediaCodec video with
  STRIDE_GRAPHICS_API_OPENGLES instead of just STRIDE_PLATFORM_ANDROID
Bitmap.Config.Argb8888 stores pixels as RGBA in memory on Android (per the
NDK: ANDROID_BITMAP_FORMAT_RGBA_8888 — bytes R, G, B, A), not BGRA.
LoadFromMemory was swapping R<->B in the wrong case, which cancelled out
for round-trip through Android-only code (so existing load/save tests
passed) but produced channel-swapped images visible only when comparing
against externally-rendered references.

Drop the swap in LoadFromMemory so bytes flow straight through to R8G8B8A8
image storage.
Three latent bugs surfaced when compiling textures to ASTC for the Android
target:

- AstcEncNative.AstcEncSwizzle was declared `: byte`, but the native
  astcenc_swz enum is int-sized; marshalling the 4-byte struct fed garbage
  for G/B/A and astcenc returned ASTCENC_ERR_BAD_SWIZZLE.
- AstcTexLib.Compress/Decompress freed image.Data twice when the source
  came through a library that owns the buffer (e.g. ImageSharpTexLib):
  DisposingLibrary.Dispose(image) frees it, then the explicit FreeHGlobal
  freed it again → "double free or corruption" abort.
- TextureTool.ExecuteRequest used the outer-scope `library` variable in
  the 2-step fallback branch where it is always null, throwing NRE.
@xen2 xen2 force-pushed the feature/ci-android branch from f108233 to fd2d190 Compare May 29, 2026 13:39
xen2 added 5 commits May 30, 2026 16:37
…emulator

Multi-target xunit.runner.stride for net10.0-android with an Avalonia
activity entry; the Tests SDK packages test projects as APKs. Re-derive
StrideGraphicsApi to Vulkan and pin RuntimeIdentifier=android-x64 for
mobile, gate per-RID inner builds (StrideHostGraphicsApi decouples
build-host from device API), and collapse MainActivity onto
AvaloniaMainActivity<App>.
…nt extra)

MainActivity reads Intent xunit_command=run and sets App.HeadlessMode; the
single-view branch awaits discovery, runs the assembly via RunTestsAsync,
and exits with FailedCount. Discovery runs on a background task so Android
doesn't ANR. trx/test root live in FilesDir (targetSdk 30+ scoped storage).
SDL can't init its window without an Activity in the xunit harness.
xen2 added 9 commits May 30, 2026 16:37
Cross-platform (Windows PowerShell 5.1 / PowerShell Core 7+ on Linux/macOS):
boots an emulator if no device is connected (swiftshader_indirect cold boot),
installs the APK, pushes gold images to /sdcard/Android/data/<pkg>/files/tests/,
launches MainActivity with --es xunit_command run, polls until the process
exits, then pulls tests/local/ (TRX + generated images) back. Captures logcat
in threadtime format alongside the results, parses the TRX for pass/fail.
20+ min APK build dominates the workflow; cache key uses sources/**,
tests/**, build/**, deps/** and project files but excludes workflow
YAML and tests/android/run-*.{sh,ps1}. Iterating on the test step
or workflow tweaks reuses cached APKs; any engine/test source change
invalidates correctly.
Texture.NewFromAndroidHardwareBuffer imports an AHardwareBuffer as a
sampleable Vulkan Texture. RGBA buffers sample directly; YUV external
formats attach a VkSamplerYcbcrConversion (usable from custom shaders;
ordinary materials need immutable-sampler descriptor wiring not yet
exposed by Stride's effect system).

Enables VK_ANDROID_external_memory_android_hardware_buffer and the
extensions in its dependency chain when available.
ImageReader-fed Yuv420888 decoder; CPU YUV->RGBA into the
VideoComponent target. Zero-copy via Texture.NewFromAndroidHardwareBuffer
needs YcbcrConversion sampler wiring not yet in Stride's effect system.
- OpenSL: destroy orphan sources before outputMix/Engine teardown; stop+clear queue before per-source destroy.
- C#: drain the static DynamicSoundSource worker before tearing down the native device.
@xen2 xen2 force-pushed the feature/ci-android branch from fd2d190 to c6d8ded Compare May 30, 2026 07:38
xen2 added 5 commits May 30, 2026 17:10
Launch the emulator with VK_DRIVER_FILES pointing at the platform's
Lavapipe ICD from Stride.Dependencies.Lavapipe, so local runs match CI
Vulkan rendering and gold paths. Linux uses -gpu host + -feature
ForceGpuHost to forward Vulkan to the host loader, plus a small implicit
layer that stamps the host OS into the device name for per-host gold.
OnImageAvailable doesn't fire reliably under the emulator's gfxstream
BufferQueue, so MediaCodec now pulls frames via AcquireLatestImage each
Update tick. Adds VideoInstance.FramesPresented (incremented by every
backend on present) and the video smoke test polls it with a wall-clock
timeout instead of a fixed frame budget. Drops the now-unused
IMediaCodecBackend interface.
Microsoft.Android defaults to Fast Deployment in Debug, which assumes
VS will push managed assemblies via /t:Install. CLI builds rarely do
that, so the resulting APK aborts on launch with 'No assemblies found
in .__override__/x86_64'. Default EmbedAssembliesIntoApk=true when
BuildingInsideVisualStudio != true.
@xen2 xen2 force-pushed the feature/ci-android branch from c6d8ded to e179e51 Compare May 30, 2026 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants