Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a070a41
feat(macos): define ARM64 guest profile and emulator paths
DeathofBrain Nov 5, 2026
55f5530
chore: remove test changes from macOS profile PR
DeathofBrain Nov 5, 2026
47e52c2
Merge upstream main and retain 1080p resolution defaults
DeathofBrain Nov 5, 2026
5ad74e7
feat: implement Darwin process runtime primitives
DeathofBrain Nov 5, 2026
1432755
Merge remote-tracking branch 'origin/main' into codex/darwin-process-…
DeathofBrain Nov 5, 2026
fcefa74
refactor: separate host routing from platform backends
DeathofBrain Nov 5, 2026
41fa471
Merge main into codex/darwin-process-runtime and resolve host conflicts
DeathofBrain Nov 5, 2026
a24cd4c
Merge latest main and retain portable host test imports
DeathofBrain Nov 5, 2026
f829e44
Merge remote-tracking branch 'origin/main' into codex/macos-arm64-pro…
DeathofBrain Nov 5, 2026
acd96b5
test: sync existing platform expectations for ARM64 profiles
DeathofBrain Nov 5, 2026
36197b7
Merge remote-tracking branch 'origin/main' into codex/macos-arm64-pro…
DeathofBrain Nov 5, 2026
e1ddfd2
Merge latest main and retain host routing for compact and logcat
DeathofBrain Nov 5, 2026
81ef060
Merge implemented macOS stages 1 and 2
DeathofBrain Nov 5, 2026
5adf4c7
feat: add macOS host and ARM64 boot preflight checks
DeathofBrain Nov 5, 2026
6f92a12
feat: fetch pinned Darwin ARM64 SDK components
DeathofBrain Nov 5, 2026
d22e58a
Merge remote-tracking branch 'origin/main' into codex/macos-1-api35-r…
DeathofBrain Nov 6, 2026
22e3dbf
fix: select Android API level from the host platform table
DeathofBrain Nov 6, 2026
71bb88c
Merge current main and preserve host routing for new upstream calls
DeathofBrain Nov 6, 2026
489f193
Merge branch 'codex/macos-1-api35-review' into codex/macos-review-base
DeathofBrain Nov 6, 2026
d8e8a46
Merge branch 'codex/macos-review-base' into codex/macos-4-review
DeathofBrain Nov 6, 2026
dd5463c
fix: check the platform API level in Darwin image metadata
DeathofBrain Nov 6, 2026
f32641c
Merge branch 'codex/macos-4-review' into codex/macos-6-review
DeathofBrain Nov 6, 2026
5c9f495
fix: pin Darwin system image to API 35 revision 9
DeathofBrain Nov 6, 2026
e7baf75
fix: identify Darwin SDK busy paths by directory identity
DeathofBrain Nov 6, 2026
4531acb
fix: use named macOS system image constant
DeathofBrain Nov 6, 2026
8c61f79
Merge branch 'codex/macos-1-api35-review' into codex/macos-review-base
DeathofBrain Nov 6, 2026
b8769d4
Merge branch 'codex/macos-review-base' into codex/macos-4-review
DeathofBrain Nov 6, 2026
1f7835f
Merge branch 'codex/macos-4-review' into codex/macos-6-review
DeathofBrain Nov 6, 2026
702f23b
Merge main into macOS preflight checks
DeathofBrain Nov 6, 2026
3d6c561
Merge commit '702f23b1bb60c944dc8e80a405642b395a5539cd' into codex/ma…
DeathofBrain Nov 6, 2026
7a315db
fix: align macOS component consent with manager download sources
DeathofBrain Nov 6, 2026
ea308bf
Merge main and preserve platform-specific component installation
DeathofBrain Nov 6, 2026
447c956
feat: prepare Google Play ARM64 components on macOS
DeathofBrain Nov 6, 2026
16190f1
macOS: enable owned ARM64 instance lifecycle
DeathofBrain Nov 6, 2026
09bd4a9
macOS: complete lifecycle lock probing and SDK emulator status
DeathofBrain Nov 6, 2026
812e0ab
macOS: complete lifecycle lock probing and SDK emulator status
DeathofBrain Nov 6, 2026
873c37d
macOS: support APFS disk maintenance and safe log retention
DeathofBrain Nov 6, 2026
bf21749
macOS: adapt native proxies and post-boot capabilities
DeathofBrain Nov 6, 2026
bf26706
Merge latest main into macOS lifecycle support
DeathofBrain Nov 7, 2026
ea9d773
Merge synchronized macOS lifecycle into daily capabilities
DeathofBrain Nov 7, 2026
f88a166
fix(macos): launch and identify native logcat helpers
DeathofBrain Nov 7, 2026
13d89f2
Merge latest main into macOS daily support
DeathofBrain Nov 10, 2026
cc72b51
Merge remote-tracking branch 'origin/main' into codex/macos-8-daily
DeathofBrain Nov 10, 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
4 changes: 3 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ modavd [--root <dir>] [--json] [--quiet] [--verbose] <command> [args]
| `bridge <i> install\|uninstall\|verify` | Install, uninstall or check the arm64 execution bridge; it is installed automatically at start, so you normally do not run it by hand (see below) |
| `playstore <i> install\|uninstall\|status` | Add the Google Play Store to an instance, remove it, or check it (see below) |
| `gles32 <i> on\|off\|status` | Experimental: make an instance report GLES 3.2 (see below) |
| `disk <i> compact` | Compact a stopped instance's data disk, or skip rewriting when estimated savings are too small. Verify before atomically replacing the original, and report host usage before and after, including growth. Large disks have budgets of many hours and effectively do not time out in normal use; cancel to stop an operation. Requires NTFS/ReFS, no snapshots and room for the temporary image. See [budgets and result fields](internal/emu/COMPACT.md) |
| `disk <i> compact` | Compact a stopped instance's data disk, or skip rewriting when estimated savings are too small. Verify before atomically replacing the original, and report host usage before and after, including growth. Large disks have budgets of many hours and effectively do not time out in normal use; cancel to stop an operation. Supports NTFS/ReFS on Windows and APFS on Apple Silicon macOS; linked images and snapshots are rejected, and room for the temporary image is required. See [budgets and result fields](internal/emu/COMPACT.md) |
| `check [--memory M]` | Computer check before starting. Memory is not checked (the page file makes up for missing physical memory); `--memory` is still accepted but does nothing. When the emulator really cannot get the memory, `start` fails with `insufficient_memory` (exit code 10) |
| `report-config [--no-clipboard]` | Copy the computer configuration and the game info from the running instances (read-only) |
| `collect-info [--out-dir D] [--stage-dir D] [--keep-stage] [--no-explorer]` | Collect a problem report and zip it (read-only). Besides the emulator logs it takes the logs of the manager, the boot watch and the private adb servers (`modavd-ui.log`, `modavd-bootwatch.log`, `adb-server.log` and their `.1`, the last 5 MB of each), every adb.exe process and the adb server log (the last 2 MB of `%TEMP%\adb.log`), and the TCP ports Windows reserves (netsh), saying whether a port this root uses is among them. Ctrl+C before ZIP completion returns `canceled`, deletes the unfinished ZIP and removes staging; `--keep-stage` preserves staging and includes its path in the error |
Expand All @@ -86,6 +86,8 @@ Apple Silicon macOS supports `report-config` and `collect-info`, including hardw
- Root folder: `--root`; if not given, the environment variable `MODAVD_ROOT`; otherwise the folder containing `modavd.exe`, or its parent.
- From 0.3.0 there is no `restart`: `stop`, then `start`. An old call gets a usage error (exit code 2) that says so.

Apple Silicon macOS uses the SDK's native `qemu-img`. Host usage counts allocated filesystem blocks, excluding sparse holes. APFS clones can share extents, so a decrease in file allocation does not guarantee the same increase in volume free space. Ordinary log cleanup checks actual open files and preserves candidates that are active, changed, linked, or contain unexpected subdirectories. The diagnostics switch remains unavailable on macOS.

**The arm64 execution bridge**:

- It applies only to the games on the built-in list; other apps keep using the original translation layer. The bridge is installed in the instance's own system / vendor overlay and needs an emulator from 0.36 on.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ modavd [--root <目录>] [--json] [--quiet] [--verbose] <命令> [参数]
| `gles32 <i> on\|off\|status` | 实验功能:让实例报告 GLES 3.2(见下) |
| `disk <i> trim` | 磁盘回收:在开着的实例里对数据盘触发一次 FITRIM;成功只表示触发请求已返回,不等待或确认客体回收完成;模拟器太旧、不支持回收时返回不支持(退出码 4) |
| `disk <i> info` | 看数据盘的大小:逻辑大小、覆盖层在宿主上实际占的空间;关机且取得启动锁后才运行只读的 qcow2 元数据检查,给出已分配和估计可回收大小。整理中或锁不可用时,仅返回可读取的文件头、宿主占用和原因;不可读的数值省略,不修复镜像 |
| `disk <i> compact` | 整理已关机实例的数据盘;预计收益不足时跳过重写;转换后先校验再原子替换,原盘在替换成功前不动;报告前后占用和增减;大盘预算可达数十小时,正常整理基本不会超时,停止请取消;仅支持 NTFS/ReFS,拒绝快照或重写空间不足,详见[预算与结果契约](internal/emu/COMPACT.md) |
| `disk <i> compact` | 整理已关机实例的数据盘;预计收益不足时跳过重写;转换后先校验再原子替换,原盘在替换成功前不动;报告前后占用和增减;大盘预算可达数十小时,正常整理基本不会超时,停止请取消;Windows 支持 NTFS/ReFS,Apple Silicon macOS 支持 APFS,拒绝链接、快照或重写空间不足,详见[预算与结果契约](internal/emu/COMPACT.md) |
| `check [--memory M]` | 开机前的电脑检查。不查内存(页面文件会补上物理内存的不足),`--memory` 留着兼容、不再起作用;真分不到内存时 `start` 报 `insufficient_memory`(退出码 10) |
| `report-config [--no-clipboard]` | 复制电脑配置和开着实例里的游戏信息(只读) |
| `collect-info [--out-dir D] [--stage-dir D] [--keep-stage] [--no-explorer]` | 收集问题信息并打包(只读)。除模拟器日志外,还收管理器、开机守护和私有 adb server 的日志(`modavd-ui.log`、`modavd-bootwatch.log`、`adb-server.log` 及其 `.1`,各取最后 5 MB)、所有 adb.exe 进程和 adb server 日志(`%TEMP%\adb.log` 最后 2 MB)、Windows 保留的 TCP 端口(netsh),并指出本目录要用的端口有没有落在里面;ZIP 完成前的 Ctrl+C 返回 `canceled`,删除未完成 ZIP 并清理暂存目录;`--keep-stage` 则保留暂存目录并在错误中给出路径 |
Expand All @@ -92,6 +92,8 @@ Apple Silicon macOS 支持 `report-config` 和 `collect-info`:读取硬件、m
自动回收日志里的秒数是 adb/su 命令往返耗时。客体是否完成或失败,需要另看客体信号,
不能用暂时稳定的 discard 计数判定。

Apple Silicon macOS 使用 SDK 中原生的 `qemu-img`。磁盘占用按宿主已分配块计算,排除稀疏文件空洞;APFS 克隆可能共享数据块,整理前后文件占用的差值不保证等于整个卷增加的可用空间。普通日志清理会检查实际打开的文件,保留正在使用、身份已变化、带链接或意外子目录的候选;macOS 的诊断模式开关仍未开放。

**arm64 执行桥**:

- 只对内置名单里的游戏生效,其余应用照旧走原来的转译层。桥装在实例自己的 system / vendor 覆盖层里,要 0.36 起的模拟器。
Expand Down
12 changes: 10 additions & 2 deletions internal/emu/COMPACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ opens the backing chain. Corruption, a missing backing, cancellation or a failed
check cannot become a successful skip. A leftover scratch image is still removed.

The current live allocation is `allocated-clusters * cluster size`. Host usage
comes from `GetCompressedFileSize`, so sparse or host-compressed files count their
actual allocation. Virtual capacity comes from the qcow2 header. These measure
comes from Windows compressed file size or macOS allocated filesystem blocks, so
sparse holes do not count as allocated space. Virtual capacity comes from the qcow2 header. These measure
different things. A tiny overlay can inherit data across a large virtual disk.

Windows compact supports NTFS/ReFS. Apple Silicon macOS uses the SDK's native
`qemu-img` and requires APFS. It rejects symlinked or hardlinked overlays, checks
the original file identity again before replacement, flushes the verified copy
with `F_FULLFSYNC`, and atomically renames it through a held parent directory
descriptor. APFS clones may share extents, so reported file allocation savings
are not a guarantee of the same volume-wide free-space increase. Tool cancellation
also terminates its process group so child tools cannot continue a canceled job.

The estimated saving is `max(0, host usage - live allocation)`, matching disk info.
It omits new qcow2 metadata and does not scan live clusters for zeroes. This is a
cheap estimate, not a promise of the exact saving. Compact skips rewriting when
Expand Down
11 changes: 11 additions & 0 deletions internal/emu/afterboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe
m.Log.Infof("实例 %d 宿主调度: %s", i, note)
},
func() {
if !root.CurrentPlatform().Windows() {
return
}
// 2) screenshot shared memory: SHM_videmulator<port> only exists while streaming, and a static screen
// stays black in it, so force one SurfaceFlinger repaint afterwards (1004 needs root)
if _, err := adb.Console(ctx, port, "screenrecord webrtc start", 10*time.Second); err != nil {
Expand All @@ -124,6 +127,10 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe
func() {
// 3) disk and screenshot channel state into the log (write_cache is root-only)
wc := m.su(ctx, i, "cat /sys/block/vd*/queue/write_cache | sort -u", 0)
if !root.CurrentPlatform().Windows() {
m.Log.Infof("实例 %d 磁盘缓存: %s", i, orText(joinNonEmptyLines(wc.Out, " / "), "读不到"))
return
}
shm := "缺失"
if host.SharedMemoryExists(shmName(ports, i)) {
shm = "在"
Expand Down Expand Up @@ -496,6 +503,10 @@ func parseWriteback(out string) (expire, writeback int, ok bool) {
}

func (m *Manager) setupBalloon(ctx context.Context, i int) {
if root.CurrentPlatform().MirrorOS == "macos" {
m.Log.Infof("实例 %d 当前 macOS 模拟器未启用空闲页上报气球", i)
return
}
const param = "/sys/module/page_reporting/parameters/page_reporting_order"
res := m.su(ctx, i, fmt.Sprintf("ls /sys/bus/virtio/drivers/virtio_balloon | grep -c virtio; echo %d > %s; cat %s",
balloonReportingOrder, param, param), 0)
Expand Down
31 changes: 27 additions & 4 deletions internal/emu/appproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"modavd/internal/guest"
"modavd/internal/hostproxy"
"modavd/internal/jsonord"
"modavd/internal/root"
)

// Per-app proxy (ModAVD/ModAVD#81): the listed apps of an instance go through a proxy, the rest direct. The setting is
Expand Down Expand Up @@ -171,7 +172,7 @@ func (m *Manager) guestUp(ctx context.Context, i int) (bool, error) {
// SetAppProxy changes instance i's per-app proxy and, while it runs, applies it at once. An address the host has to
// find (appproxy.Auto, or one without its kind) is probed on the host first.
func (m *Manager) SetAppProxy(ctx context.Context, i int, c appproxy.Change) (*AppProxyResult, error) {
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
if _, err := m.instance(i); err != nil {
Expand Down Expand Up @@ -209,9 +210,25 @@ func (m *Manager) SetAppProxy(ctx context.Context, i int, c appproxy.Change) (*A
if err != nil {
return nil, err
}
if _, _, err := cur.Apply(c, resolve); err != nil {
preview, _, err := cur.Apply(c, resolve)
if err != nil {
return nil, appProxyError(err, found.err)
}
// A source build often has no ARM64 payload. Reject enabling before persisting
// a setting that could otherwise appear protected while no daemon can run.
payloadChecked := false
checkPayload := func(s appproxy.Setting) error {
if root.CurrentPlatform().MirrorOS == "macos" && s.Active() && !payloadChecked {
if _, err := guest.ProxyDaemonForABI(root.CurrentPlatform().GuestABI); err != nil {
return errs.Wrap(errs.NotReady, err, err.Error())
}
payloadChecked = true
}
return nil
}
if err := checkPayload(preview); err != nil {
return nil, err
}

var next appproxy.Setting
var notes []string
Expand All @@ -223,6 +240,11 @@ func (m *Manager) SetAppProxy(ctx context.Context, i int, c appproxy.Change) (*A
if next, notes, err = cur.Apply(c, resolve); err != nil {
return appProxyError(err, found.err)
}
// Another edit can change the setting after preview; validate the final
// value while holding the metadata lock before making it persistent.
if err := checkPayload(next); err != nil {
return err
}
meta.Set(appproxy.MetaKey, settingValue(meta.Get(appproxy.MetaKey), next))
return nil
})
Expand Down Expand Up @@ -524,11 +546,12 @@ type carried struct {

// daemonFile is the daemon of this modavd (a variable for the tests: a test build carries none).
var daemonFile = func() (carried, error) {
b, err := guest.ProxyDaemon()
abi := root.CurrentPlatform().GuestABI
b, err := guest.ProxyDaemonForABI(abi)
if err != nil {
return carried{}, errs.Wrap(errs.NotReady, err, err.Error())
}
local, err := guest.LocalProxyDaemon(filepath.Join(os.TempDir(), "modavd-guest"))
local, err := guest.LocalProxyDaemonForABI(filepath.Join(os.TempDir(), "modavd-guest"), abi)
if err != nil {
return carried{}, errs.Wrap(errs.Internal, err, "写不了客体程序的本机副本: "+err.Error())
}
Expand Down
24 changes: 19 additions & 5 deletions internal/emu/capture_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,32 @@ func (m *Manager) writeCaptureState(i int, state captureState) error {
return os.Rename(tmp, path)
}

// Native process paths may name the same file through /var and /private/var.
// On non-Windows hosts only an existing file identity establishes ownership.
func capturePathMatches(a, b string) bool {
if root.CurrentPlatform().Windows() {
return strings.EqualFold(filepath.Clean(a), filepath.Clean(b))
}
left, err := os.Stat(a)
if err != nil {
return false
}
right, err := os.Stat(b)
return err == nil && os.SameFile(left, right)
}

func (m *Manager) ownsCaptureHelper(i int, state captureState) bool {
helper := host.Process{PID: state.HelperPID, Created: state.HelperCreated}
if helper.PID == 0 || helper.Created == 0 || !helper.Alive() {
return false
}
image, err := helper.ImagePath()
if err != nil || !strings.EqualFold(filepath.Clean(image), filepath.Join(m.Root.Dir, "tools", "modavd.exe")) {
if err != nil || !capturePathMatches(image, filepath.Join(m.Root.Dir, "tools", root.CurrentPlatform().CLI())) {
return false
}
args, err := helper.CommandLine()
if err != nil || (len(args) != 6 && len(args) != 7) || args[1] != LogcatCaptureArg ||
!strings.EqualFold(filepath.Clean(args[2]), m.Root.Dir) || args[3] != strconv.Itoa(i) ||
!capturePathMatches(args[2], m.Root.Dir) || args[3] != strconv.Itoa(i) ||
args[4] != strconv.FormatUint(uint64(state.QemuPID), 10) || args[5] != strconv.FormatUint(state.QemuCreated, 10) {
return false
}
Expand Down Expand Up @@ -102,7 +116,7 @@ func (m *Manager) ownedCapture(i int, state captureState) (host.Process, bool) {
return host.Process{}, false
}
image, err := client.ImagePath()
if err != nil || !strings.EqualFold(filepath.Clean(image), filepath.Clean(m.Root.AdbExe())) {
if err != nil || !capturePathMatches(image, m.Root.AdbExe()) {
return host.Process{}, false
}
args, err := client.CommandLine()
Expand All @@ -125,7 +139,7 @@ func captureChildOf(client, helper host.Process) bool {
// this is a transition to wait for, not an external capture. This weaker readiness fact never permits killing it.
func (m *Manager) pendingOwnedCapture(i int, client host.Process) bool {
image, err := client.ImagePath()
if err != nil || !strings.EqualFold(filepath.Clean(image), filepath.Clean(m.Root.AdbExe())) {
if err != nil || !capturePathMatches(image, m.Root.AdbExe()) {
return false
}
helper, ok := host.FindProcess(client.PPID)
Expand Down Expand Up @@ -200,7 +214,7 @@ func (m *Manager) waitDiagnosticCaptureRuntime(ctx context.Context, i int, timeo
}
if helperOurs && captureChildOf(client, host.Process{PID: state.HelperPID, Created: state.HelperCreated}) {
image, err := client.ImagePath()
if err == nil && strings.EqualFold(filepath.Clean(image), filepath.Clean(m.Root.AdbExe())) {
if err == nil && capturePathMatches(image, m.Root.AdbExe()) {
continue
}
}
Expand Down
9 changes: 8 additions & 1 deletion internal/emu/crash_archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ func (m *Manager) crashScanPath(i int) string {
// startDiagnosticArchive starts bounded background work which survives a short CLI. Ordinary starts do not scan
// crashpad at all. Each helper owns only one instance and exits after this copy, rather than becoming a service.
func (m *Manager) startDiagnosticArchive(i int) {
if !root.CurrentPlatform().Windows() {
return
}
states, err := diagnostics.Read(m.Root)
if err != nil {
m.Log.Warnf("实例 %d 读不了诊断策略,未复制崩溃转储: %v", i, err)
Expand All @@ -76,7 +79,7 @@ func (m *Manager) startDiagnosticArchive(i int) {
if !state.Enabled {
return
}
_, err = host.StartOrphan(filepath.Join(m.Root.Dir, "tools", "modavd.exe"), []string{DiagnosticArchiveArg,
_, err = host.StartOrphan(filepath.Join(m.Root.Dir, "tools", root.CurrentPlatform().CLI()), []string{DiagnosticArchiveArg,
m.Root.Dir, strconv.Itoa(i), state.Since, state.CreatedAt, time.Now().UTC().Format(time.RFC3339Nano)},
host.StartOptions{Env: m.ADB.Env()})
if err != nil {
Expand All @@ -88,6 +91,10 @@ func RunDiagnosticArchive(args []string, stderr io.Writer) int {
if len(args) != 5 {
return 2
}
if !root.CurrentPlatform().Windows() {
fmt.Fprintln(stderr, "当前平台尚不支持崩溃转储归档")
return 1
}
i, err := strconv.Atoi(args[1])
through, timeErr := time.Parse(time.RFC3339Nano, args[4])
if err != nil || !root.ValidIndex(i) || timeErr != nil {
Expand Down
Loading
Loading