0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windows x86_64でAndroid arm64-v8aエミュレータを動かす(API 27)

0
Posted at

色々な都合で、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のフォント描画不具合もエミュレータ側だけで修正します。
before_after_lefttop_260x100_horizontal.png

なお、書くのが面倒だったので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 からも戻しやすいです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?