色々な都合で、Windows x86_64 PC上に Androidの arm64-v8a 環境を用意することになりました。
Android Studioのエミュレータを普通に使うだけなら、x86 / x86_64のsystem imageを選ぶのが非常に簡単です。今回は arm64-v8a 上で動かした場合の結果を確認したかったため、Android Studio Emulator の ARM64 system image を使います。ゲーム系エミュレータでも ABIを切り替えられる場合がありますが、Houdini / native bridge経由になるため、今回の確認には向きませんでした。
調べてみると、Stack Overflow で、-qemu -machine virt を付ける方法や、Windows x64でAPI 27のARM64 system imageを使う構成が紹介されていました。
要点をまとめると、次のような内容です。
- arm / arm64 system imageで
PCI bus not available for hdaが出ることがある -
-qemu -machine virtを付けると回避できる場合がある - 黒画面になる場合は
-gpu swiftshader_indirectが効くことがある - Windows x64でEmulator 34.2.16 + API 27 Android 8.1 ARM64 imageの組み合わせが動いた例がある
この記事では、それを参考にして Windows x86_64上でAndroid 8.1(API 27)の arm64-v8a エミュレータを動かします。
あわせて、手元で発生したGLES1のフォント描画不具合もエミュレータ側だけで修正します。

なお、書くのが面倒だったので8割がたCodex GPT-5.5 xHighに出力してもらいました。初稿はただの作業報告書でしたが、それでも手直しで済むのは楽ですね。:::note info:::とか全然使ってくれませんが。
先に結論
Windows x86_64でAndroid arm64-v8a を動かすため、今回は次の構成にします。
Emulator: 34.2.16 / build id 12038310
Android: 8.1 / API 27
System image: google_apis;arm64-v8a
GPU: swiftshader_indirect
QEMU machine: virt
Acceleration: off
起動時の要点はこれ。
& "$Workspace\emulator-34.2.16\emulator\emulator.exe" `
-avd "Pixel_2_arm64_720" `
-sysdir "$Sdk\system-images\android-27\google_apis\arm64-v8a" `
-gpu swiftshader_indirect `
-accel off `
-memory 4096 `
-cores 4 `
-no-snapshot `
-no-boot-anim `
-no-audio `
-qemu -machine virt
この状態で ro.product.cpu.abi が arm64-v8a、uname -m が aarch64 になることを確認します。
また、手元ではGLES1の GL_ALPHA テクスチャ描画で文字が矩形になる問題も発生したため、libgfxstream_backend.dll もパッチします。
パッチ内容は、shader source文字列の if を else if にするだけです。
- if (texture_format == kRGBA || texture_format == kLuminanceAlpha) {
+ else if (texture_format == kRGBA || texture_format == kLuminanceAlpha) {
環境
この記事では、次の場所にSDKと作業ディレクトリがあるものとして進めます。
workspace: %HOMEPATH%\workspace
Android SDK: C:\Users\%USERNAME%\AppData\Local\Android\Sdk
PowerShellでは、以降この変数を使います。
$Sdk = "$env:LOCALAPPDATA\Android\Sdk"
$Workspace = Join-Path $env:HOMEDRIVE "$env:HOMEPATH\workspace"
使用したものです。
Windows x86_64
Android 8.1 / API 27
system image: google_apis;arm64-v8a
AVD名: Pixel_2_arm64_720
Emulator: 34.2.16 / build id 12038310
SDKとsystem imageを入れる
Android Studioを入れている場合は、SDK Managerから次を入れる。
- Android SDK Platform-Tools
- Android 8.1(API 27)
- Google APIs ARM 64 v8a System Image
command line toolsを使う場合は、Android公式サイトから取得します。
https://developer.android.com/studio
sdkmanager.bat が使えるなら、次のように入れる。
$Sdk = "$env:LOCALAPPDATA\Android\Sdk"
$SdkManager = "$Sdk\cmdline-tools\latest\bin\sdkmanager.bat"
& $SdkManager `
"platform-tools" `
"platforms;android-27" `
"system-images;android-27;google_apis;arm64-v8a"
sdkmanager.bat が見つからない場合は、Android StudioのSDK Managerから入れるのが簡単です。
Emulator 34.2.16をSDKとは別に置く
SDK標準の emulator を直接上書きすると、他のAVDに影響します。
今回は workspace に別展開して、その emulator.exe を直接呼びます。
EmulatorをAndroid SDK Repositoryから取得します。
https://dl.google.com/android/repository/emulator-windows_x64-12038310.zip
PowerShellで落とす場合は次のようにします。
$Workspace = Join-Path $env:HOMEDRIVE "$env:HOMEPATH\workspace"
$EmulatorZipUrl = "https://dl.google.com/android/repository/emulator-windows_x64-12038310.zip"
$Zip = Join-Path $Workspace "emulator-windows_x64-12038310.zip"
New-Item -ItemType Directory -Force -Path $Workspace | Out-Null
Invoke-WebRequest -Uri $EmulatorZipUrl -OutFile $Zip
展開します。
$Workspace = Join-Path $env:HOMEDRIVE "$env:HOMEPATH\workspace"
$Zip = Join-Path $Workspace "emulator-windows_x64-12038310.zip"
$Out = Join-Path $Workspace "emulator-34.2.16"
New-Item -ItemType Directory -Force -Path $Out | Out-Null
Expand-Archive -LiteralPath $Zip -DestinationPath $Out -Force
確認します。
Get-Content "$Out\emulator\source.properties"
次のようになっていれば大丈夫です。
Pkg.Revision=34.2.16
Pkg.BuildId=12038310
この後使う emulator.exe はここです。
%HOMEPATH%\workspace\emulator-34.2.16\emulator\emulator.exe
SDK側のこちらは変更しません。
C:\Users\%USERNAME%\AppData\Local\Android\Sdk\emulator
AVDを作る
ここでは、AVD名を Pixel_2_arm64_720 にしておきます。
$Sdk = "$env:LOCALAPPDATA\Android\Sdk"
$AvdManager = "$Sdk\cmdline-tools\latest\bin\avdmanager.bat"
& $AvdManager create avd `
-n "Pixel_2_arm64_720" `
-k "system-images;android-27;google_apis;arm64-v8a" `
-d "pixel_2"
画面サイズは720px程度にしておくと負荷が軽めで動作確認しやすいです。メモリは4096MB、コア数は4くらいから始めると不安定になりにくいです。
(コア数の割り当て上限は8ですが、Android側では割り当ての半分だけ認識します。もう半分はQemu側で使用?)
起動スクリプト
workspaceにAPI 27用の起動スクリプトを置く。
%HOMEPATH%\workspace\run-arm64-api27-windows-34216.ps1
スクリプトの中身:
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$SdkRoot = Join-Path $env:LOCALAPPDATA "Android\Sdk"
$Emulator = Join-Path $Root "emulator-34.2.16\emulator\emulator.exe"
$Sysdir = Join-Path $SdkRoot "system-images\android-27\google_apis\arm64-v8a"
$AvdName = "Pixel_2_arm64_720"
$Args = @(
"-avd", $AvdName,
"-sysdir", $Sysdir,
"-gpu", "swiftshader_indirect",
"-accel", "off",
"-memory", "4096",
"-cores", "4",
"-no-snapshot",
"-no-boot-anim",
"-no-audio",
"-qemu", "-machine", "virt"
)
Start-Process -FilePath $Emulator -ArgumentList $Args
実行します。
$Workspace = Join-Path $env:HOMEDRIVE "$env:HOMEPATH\workspace"
cd $Workspace
.\run-arm64-api27-windows-34216.ps1 -MemoryMB 4096 -Cores 4
起動には時間がかかります。30秒以上待つこともあります。
arm64-v8aで動いているか確認する
起動したら、Houdini / native bridge経由になっていないことを確認します。
$Adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
& $Adb shell getprop ro.build.version.release
& $Adb shell getprop ro.build.version.sdk
& $Adb shell getprop ro.product.cpu.abi
& $Adb shell getprop ro.dalvik.vm.native.bridge
& $Adb shell uname -m
目安:
ro.build.version.release: 8.1.x
ro.build.version.sdk: 27
ro.product.cpu.abi: arm64-v8a
ro.dalvik.vm.native.bridge: 空、または未使用を示す値
uname -m: aarch64
ここまで確認できれば、Windows x86_64上でAndroid arm64-v8a が動いていると判断できます。
adbのバージョン違いで怒られる場合
別のAndroidエミュレータ製品がadb serverを起動していると、次のようなエラーになることがあります。
adb server version (36) doesn't match this client (41); killing...
別のPathからadbを読まないようにSDK側を絶対パスで使い、必要ならportも分けましょう。
$Adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
& $Adb -P 5038 start-server
& $Adb -P 5038 connect 127.0.0.1:5555
& $Adb -P 5038 devices
以降も同じport番号を付けて実行します。
& $Adb -P 5038 shell getprop ro.product.cpu.abi
ここからGLES1の不具合対応
ここまでで arm64-v8a エミュレータ自体は起動できます。
この構成で一部アプリを動かしたところ、GLES1の自前フォント描画が黒い矩形や白い矩形になることがありました。
原因は、gfxstream backendの GLES1 shader生成にあります。GL_ALPHA テクスチャを文字のマスクとして使っている場合に、alpha用の処理が後続のgenericなtexture branchで上書きされていました。
確認するときは、実際にフォントとして描画されている箇所を見ます。画像化されたテキストやスプラッシュ画像は、フォント描画の判定には使いません。
近い話として、gfxstream upstreamには CoreProfileEngineShaders.h の変更がGLES1.1のtexture handling regressionを起こしたため、同ファイルを reset した commit があります。
このcommitでは、kAlpha、kRGBA、kLuminanceAlpha などのtexture formatごとの分岐が整理されています。今回の問題と完全に同じ報告ではありませんが、同じファイル、同じGLES1.1 texture handlingの範囲なので、近い参考になります。
QEMU 側の issue には qemu-system-aarch64 + virtio-gpu まわりの黒画面報告があります。ただし、今回のようなAndroid Emulatorのgfxstream GLES1 GL_ALPHA shader生成に直接対応する報告は見つかりませんでした。
ソース側の問題箇所
対象はgfxstreamのこのファイル。
host\gl\glestranslator\GLES_CM\CoreProfileEngineShaders.h
簡単に書くと、元の構造はこうなっていました。
if (texture_format == kAlpha) {
// GL_ALPHA texture用のcurrentColorを生成
}
if (texture_format == kRGBA || texture_format == kLuminanceAlpha) {
// RGBA / LuminanceAlpha用
} else {
// kAlphaもここを通る。Wow!
}
kAlpha の処理をしたあとに、次の if が独立しています。そのため kAlpha でも後続の else に入ってしまいます。
ここは相互排他的に扱えばよいため、後続の if を else if にします。
diff --git a/host/gl/glestranslator/GLES_CM/CoreProfileEngineShaders.h b/host/gl/glestranslator/GLES_CM/CoreProfileEngineShaders.h
--- a/host/gl/glestranslator/GLES_CM/CoreProfileEngineShaders.h
+++ b/host/gl/glestranslator/GLES_CM/CoreProfileEngineShaders.h
@@
- if (texture_format == kRGBA || texture_format == kLuminanceAlpha) {
+ else if (texture_format == kRGBA || texture_format == kLuminanceAlpha) {
DLLへ当てるパッチ
配布版Emulatorと同じ条件でgfxstreamをビルドして差し替えるのは、少し手間がかかります。依存関係やビルドオプションがずれると、DLLだけ置き換えても起動時に失敗することがあります。
今回は、配布版 libgfxstream_backend.dll に含まれているshader source文字列を直接置き換えます。
ここで置き換えているのはC++のソースではなく、DLL 内に埋め込まれているGLSLのshader source文字列です。gfxstreamのソースでは CoreProfileEngineShaders.h に生文字列リテラルとして書かれており、ビルド後はその文字列がDLL内にASCIIとして入ります。
実行時には、そのshader sourceがOpenGL / SwiftShader側に渡され、glShaderSource / glCompileShader の流れでshaderとしてコンパイルされます。
%HOMEPATH%\workspace\emulator-34.2.16\emulator\lib64\libgfxstream_backend.dll
このDLLでは、対象文字列はoffset 0xfe9c5d にありました。
ただし、スクリプトではこの値を固定では使いません。DLL内から変更前または変更後のASCII文字列を検索し、1箇所だけ見つかった場合にその位置を使います。
変更前。
if (texture_format == kRGBA || texture_format == kLuminanceAlpha)
変更後。
else if (texture_format == kRGBA || texture_format == kLuminanceAlpha)
文字列長が変わらないように置き換え文字を調整しています。
hashも確認しておきます。
original:
00275B779CF809C3D86613261F73B86E5383AA78243E072F5B711E689F10151F
patched:
45E486ACCB25E211BC662F89E6F1B8957FF5AB6D6B6914070A498BC5E04E5F68
パッチ適用スクリプト
スクリプトは workspace に置く。
%HOMEPATH%\workspace\apply-gfxstream-gles1-alpha-shader-branch-patch.ps1
中では次の確認をします。
- emulator / qemu が起動中なら、DLLを書き換えずに処理を中断する
-
libgfxstream_backend.dllの hash を確認する - 未パッチなら backup を作る
- 変更前または変更後の ASCII 文字列をDLL内から検索する
- 対象が1箇所だけ見つかることを確認する
- 同じ長さの patched string で上書きする
- パッチ後の hash を確認する
-
-Restoreで戻せるようにする
スクリプト全文。
param(
[string]$EmulatorRoot = (Join-Path $PSScriptRoot "emulator-34.2.16\emulator"),
[switch]$Restore
)
$ErrorActionPreference = "Stop"
$Dll = Join-Path $EmulatorRoot "lib64\libgfxstream_backend.dll"
$Backup = Join-Path $EmulatorRoot "lib64\libgfxstream_backend.dll.before-gles1-alpha-shader-branch-patch"
$expectedHash = "00275B779CF809C3D86613261F73B86E5383AA78243E072F5B711E689F10151F"
$expectedPatchedHash = "45E486ACCB25E211BC662F89E6F1B8957FF5AB6D6B6914070A498BC5E04E5F68"
if (!(Test-Path -LiteralPath $Dll)) {
throw "libgfxstream_backend.dll not found: $Dll"
}
$running = Get-Process -Name emulator,qemu-system-aarch64 -ErrorAction SilentlyContinue
if ($running) {
throw "Stop emulator/qemu before replacing gfxstream DLL. Running PIDs: $($running.Id -join ', ')"
}
if ($Restore) {
if (!(Test-Path -LiteralPath $Backup)) {
throw "Backup not found: $Backup"
}
$backupHash = (Get-FileHash -LiteralPath $Backup -Algorithm SHA256).Hash
if ($backupHash -ne $expectedHash) {
throw "Backup SHA-256 is $backupHash; expected original $expectedHash. Refusing to restore from an unvalidated backup."
}
Copy-Item -LiteralPath $Backup -Destination $Dll -Force
$restoredHash = (Get-FileHash -LiteralPath $Dll -Algorithm SHA256).Hash
if ($restoredHash -ne $expectedHash) {
throw "Restore wrote unexpected SHA-256 $restoredHash; expected $expectedHash."
}
Write-Host "Restored: $Dll"
Write-Host "SHA-256: $restoredHash"
return
}
$original = [Text.Encoding]::ASCII.GetBytes(" if (texture_format == kRGBA || texture_format == kLuminanceAlpha)")
$patched = [Text.Encoding]::ASCII.GetBytes(" else if (texture_format == kRGBA || texture_format == kLuminanceAlpha)")
if ($original.Length -ne $patched.Length) {
throw "Internal patch error: replacement length mismatch."
}
function Find-BytePattern {
param(
[Parameter(Mandatory = $true)]
[byte[]]$Bytes,
[Parameter(Mandatory = $true)]
[byte[]]$Pattern
)
if (-not ("BytePatternSearch" -as [type])) {
Add-Type -TypeDefinition @"
using System;
using System.Collections.Generic;
public static class BytePatternSearch {
public static int[] FindAll(byte[] bytes, byte[] pattern) {
var hits = new List<int>();
if (bytes == null || pattern == null || pattern.Length == 0 || bytes.Length < pattern.Length) {
return hits.ToArray();
}
int[] skip = new int[256];
for (int i = 0; i < skip.Length; i++) {
skip[i] = pattern.Length;
}
for (int i = 0; i < pattern.Length - 1; i++) {
skip[pattern[i]] = pattern.Length - 1 - i;
}
int pos = 0;
while (pos <= bytes.Length - pattern.Length) {
int j = pattern.Length - 1;
while (j >= 0 && bytes[pos + j] == pattern[j]) {
j--;
}
if (j < 0) {
hits.Add(pos);
pos++;
} else {
pos += skip[bytes[pos + pattern.Length - 1]];
}
}
return hits.ToArray();
}
}
"@
}
return @([BytePatternSearch]::FindAll($Bytes, $Pattern))
}
$bytes = [IO.File]::ReadAllBytes($Dll)
$originalHits = @(Find-BytePattern -Bytes $bytes -Pattern $original)
$patchedHits = @(Find-BytePattern -Bytes $bytes -Pattern $patched)
if ($patchedHits.Count -eq 1 -and $originalHits.Count -eq 0) {
$patchedHash = (Get-FileHash -LiteralPath $Dll -Algorithm SHA256).Hash
if ($patchedHash -ne $expectedPatchedHash) {
throw "Shader bytes are patched, but DLL SHA-256 is $patchedHash; expected $expectedPatchedHash. Refusing to treat this as the validated binary."
}
if (!(Test-Path -LiteralPath $Backup)) {
throw "Already patched, but backup is missing: $Backup"
}
$backupHash = (Get-FileHash -LiteralPath $Backup -Algorithm SHA256).Hash
if ($backupHash -ne $expectedHash) {
throw "Already patched, but backup SHA-256 is $backupHash; expected original $expectedHash."
}
Write-Host "Already patched: $Dll"
Write-Host "SHA-256: $patchedHash"
Write-Host ("Patch offset: 0x{0:x}" -f $patchedHits[0])
return
}
if ($originalHits.Count -ne 1 -or $patchedHits.Count -ne 0) {
$originalText = (@($originalHits | ForEach-Object { "0x{0:x}" -f $_ }) -join ", ")
$patchedText = (@($patchedHits | ForEach-Object { "0x{0:x}" -f $_ }) -join ", ")
throw "Expected exactly one original shader branch and no patched branch. Found original=[$originalText], patched=[$patchedText]."
}
$currentHash = (Get-FileHash -LiteralPath $Dll -Algorithm SHA256).Hash
if ($currentHash -ne $expectedHash) {
throw "Unexpected libgfxstream_backend.dll SHA-256 $currentHash; expected $expectedHash. Restore original before applying this patch."
}
if (Test-Path -LiteralPath $Backup) {
$backupHash = (Get-FileHash -LiteralPath $Backup -Algorithm SHA256).Hash
if ($backupHash -ne $expectedHash) {
throw "Existing backup SHA-256 is $backupHash; expected original $expectedHash. Refusing to overwrite or patch."
}
} else {
Copy-Item -LiteralPath $Dll -Destination $Backup
}
$offset = $originalHits[0]
for ($i = 0; $i -lt $patched.Length; $i++) {
$bytes[$offset + $i] = $patched[$i]
}
[IO.File]::WriteAllBytes($Dll, $bytes)
$newHash = (Get-FileHash -LiteralPath $Dll -Algorithm SHA256).Hash
if ($newHash -ne $expectedPatchedHash) {
throw "Patch wrote unexpected SHA-256 $newHash; expected $expectedPatchedHash."
}
Write-Host "Patched GLES1 alpha shader branch in: $Dll"
Write-Host "SHA-256: $newHash"
Write-Host ("Patch offset: 0x{0:x}" -f $offset)
適用:
$Workspace = Join-Path $env:HOMEDRIVE "$env:HOMEPATH\workspace"
cd $Workspace
powershell -ExecutionPolicy Bypass -File .\apply-gfxstream-gles1-alpha-shader-branch-patch.ps1
戻すとき:
$Workspace = Join-Path $env:HOMEDRIVE "$env:HOMEPATH\workspace"
cd $Workspace
powershell -ExecutionPolicy Bypass -File .\apply-gfxstream-gles1-alpha-shader-branch-patch.ps1 -Restore
手動で hash を確認する場合です。
Get-FileHash .\emulator-34.2.16\emulator\lib64\libgfxstream_backend.dll -Algorithm SHA256
Get-FileHash .\emulator-34.2.16\emulator\lib64\libgfxstream_backend.dll.before-gles1-alpha-shader-branch-patch -Algorithm SHA256
動かして確認する
パッチ後に API 27 AVD を起動します。
$Workspace = Join-Path $env:HOMEDRIVE "$env:HOMEPATH\workspace"
cd $Workspace
.\run-arm64-api27-windows-34216.ps1 -MemoryMB 4096 -Cores 4
エミュレータ起動やアプリ初回起動には時間がかかります。30秒以上待つこともあります。
確認するところです。
- Android が再起動していない
- SystemUI が落ちていない
-
ro.product.cpu.abiがarm64-v8aのまま -
uname -mがaarch64のまま - 自前フォントが矩形ではなく文字として描画される
注意点
SDK標準のemulatorを上書きしない
Android Studio や他のAVDに影響するので、Emulator 34.2.16 は workspace に展開して使う。
-qemu -machine virtを忘れない
arm64 system image を Windows x86_64 で動かすときに重要な指定です。Stack Overflow のスレッドでも、この指定で PCI bus not available for hda を回避しています。
-gpu swiftshader_indirectにする
黒画面になる場合、ソフトウェアレンダリングに寄せると起動しやすい。速くはないが、ARM64 の挙動確認が目的なら扱いやすい。
DLL差し替えではなく文字列パッチにする
手元でビルドした gfxstream_backend.dll が、配布版 Emulator の libgfxstream_backend.dll と完全に同じ条件で作られているとは限りません。
今回必要なのは shader source の if を else if にすることだけなので、配布版 DLL の文字列を同じ長さで置き換える方が安全です。
動かす前に静的に確認する
エミュレータ起動は待ち時間が長い。先にこれを確認しておく。
- DLL の SHA-256
- 変更前または変更後の ASCII 文字列がDLL内に1箇所だけあること
- パッチ後の SHA-256
- 起動に使う
emulator.exe - adb のパス
まとめ
Stack Overflow の情報をもとに、Windows x86_64 でも Emulator 34.2.16 + API 27 ARM64 system image + -qemu -machine virt + -gpu swiftshader_indirect の組み合わせで、Android arm64-v8a 環境を動かせた。
その構成で GLES1 の GL_ALPHA テクスチャを使った自前フォントが矩形化する場合は、gfxstream backend の shader branch が原因になっていることがあります。
今回の修正は、texture_format == kAlpha の処理後に generic branch へ落ちないようにするだけです。配布版 libgfxstream_backend.dll の shader source 文字列を同じ長さで置き換えるため、既存 SDK を壊しにくく、backup からも戻しやすいです。
参考
- Stack Overflow: Windows x64でARM64 emulatorを動かす話
- Android Studio - Download Android Studio and SDK tools
- Android Emulator release notes
- gfxstream commit: gles1.1: reset CoreProfileEngineShaders.h
- QEMU issue #1141: virtio-gpu-gl-pci not working with arm/aarch64
- platform/hardware/google/gfxstream - CoreProfileEngineShaders.h