Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ jobs:
with:
go-version: ${{ steps.go.outputs.version }}
cache: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
- name: display coordinate tests
run: node --test scripts/test_display_tap.mjs
- name: gofmt
shell: pwsh
run: |
Expand Down
8 changes: 6 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ modavd [--root <dir>] [--json] [--quiet] [--verbose] <command> [args]
| --- | --- |
| `list` | All instances and their status |
| `info <i>` / `status <i>` | Details of one instance / its status |
| `create [--index N] [--title T] [--memory 6G] [--cpu N] [--data-gb N] [--fps 30\|60] [--no-bridge] [--timeout 1200]`, `create --resume N` | Create an instance, then start it once in the background, headless, to finish initialization, and shut it down (see below). The arm64 execution bridge is included by default; `--no-bridge` records that this instance does not want the bridge, so it is not installed on later starts either. `--resume N` redoes the initialization of an instance whose creation failed |
| `set <i> [--title T] [--memory M] [--cpu N] [--fps F] [--data-gb N]` | Change settings; they take effect at the next start. `--data-gb` can only be increased, and game data is untouched: the emulator (from 0.36) grows the disk image at start and grows the file system online after boot. A disk created before 0.36 at 16 GB or smaller can only be grown to just below the next backup group (a 16 GB disk to about 30 GB) |
| `create [--index N] [--title T] [--memory 6G] [--cpu N] [--data-gb N] [--fps 30\|60] [--resolution 1080\|720] [--no-bridge] [--timeout 1200]`, `create --resume N` | Create an instance, then start it once in the background, headless, to finish initialization, and shut it down (see below). The arm64 execution bridge is included by default; `--no-bridge` records that this instance does not want the bridge, so it is not installed on later starts either. `--resume N` redoes the initialization of an instance whose creation failed |
| `set <i> [--title T] [--memory M] [--cpu N] [--fps F] [--resolution 1080\|720] [--data-gb N]` | Change settings; they take effect at the next start. `--data-gb` can only be increased, and game data is untouched: the emulator (from 0.36) grows the disk image at start and grows the file system online after boot. A disk created before 0.36 at 16 GB or smaller can only be grown to just below the next backup group (a 16 GB disk to about 30 GB) |
| `proxy <i> [on\|off\|test] [--server URL\|auto] [--add PKG] [--remove PKG] [--apps PKG]` | Per-app proxy: the listed apps go through a proxy, the others direct (see below) |
| `delete <i> --yes` | Delete a stopped instance, together with its game data |
| `start <i> [--window \| --headless] [--timeout 300] [--no-bridge]` | Start (headless by default, with the window for `--window`), wait until boot finishes and the post-boot setup is done, and before returning confirm that files can be written to `/sdcard` (normally it is ready long before this; it waits up to 60 more seconds and only warns at the deadline). If the instance is shutting down (for example, right after a click on the emulator window's ×), it first waits for the shutdown to finish (up to 30 seconds) and then starts it. If the instance has no execution bridge yet, install it afterward; `--no-bridge` skips that this time. No switch while it runs: if the instance is already running and `--window` / `--headless` asks for the other way, it fails (exit code 5) without taking it over or restarting it; to switch, `stop` and then `start`. With neither, it is taken over as it runs |
Expand Down Expand Up @@ -148,6 +148,10 @@ modavd [--root <dir>] [--json] [--quiet] [--verbose] <command> [args]
- The number of loops never exceeds what the host has left: logical processors minus the vCPUs of every qemu (this instance's included; counted within its group when pinned).
- So on machines with fewer than twice as many logical processors as the instance has vCPUs, even a single instance gets fewer loops: a default 6-core instance gets 2 on an 8-thread machine and none on 6 threads or fewer, and downloads are slower than with all loops (measured on a 7950X3D, 1–2 loops do about as well as none).
- This is a deliberate trade-off: it leaves processors for qemu's own threads, Windows and other programs.
Two display presets are available: 1080p is 1920×1080 / DPI 280; 720p is 1280×720 / DPI 240. New instances default to 1080p. 720p uses fewer resources. The manager forms, CLI `--resolution 1080|720` and MCP `resolution` share these presets. Existing landscape or portrait 1920×1080 instances stay at 1080p; other configurations, including missing dimensions, stay at 720p. Changing the preset updates width, height and DPI together, preserves orientation, and takes effect at the next cold boot. DPI is not a separate option. No runtime `wm size` or `wm density` is used. Update the resolution in your automation tool as well.

AUTO-MAS currently prefers the packaged CLI, which preserves the selected preset. Old roots without a usable CLI fall back to the Python launch path, which still forces 720p. Update the packaged CLI before using 1080p there. AUTO-MAS creation and display settings still need matching two-preset support; use the ModAVD manager to set the preset meanwhile.

## MCP

```
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ modavd [--root <目录>] [--json] [--quiet] [--verbose] <命令> [参数]
| --- | --- |
| `list` | 全部实例和状态 |
| `info <i>` / `status <i>` | 一台实例的详细信息 / 状态 |
| `create [--index N] [--title T] [--memory 6G] [--cpu N] [--data-gb N] [--fps 30\|60] [--no-bridge] [--timeout 1200]`、`create --resume N` | 新建,并在后台无头开机一次把初始化做完再关机(见下)。默认带 arm64 执行桥;`--no-bridge` 记下这台不要桥,以后开机也不装。`--resume N` 重做新建失败的那台的初始化 |
| `set <i> [--title T] [--memory M] [--cpu N] [--fps F] [--data-gb N]` | 修改,下次开机生效。`--data-gb` 只能改大,游戏数据不动:模拟器(0.36 起)开机时扩磁盘镜像,开机后在线扩文件系统。0.36 之前按 16 GB 及以下建的盘只能扩到下一个备份组之前(16 GB 约到 30 GB) |
| `create [--index N] [--title T] [--memory 6G] [--cpu N] [--data-gb N] [--fps 30\|60] [--resolution 1080\|720] [--no-bridge] [--timeout 1200]`、`create --resume N` | 新建,并在后台无头开机一次把初始化做完再关机(见下)。默认带 arm64 执行桥;`--no-bridge` 记下这台不要桥,以后开机也不装。`--resume N` 重做新建失败的那台的初始化 |
| `set <i> [--title T] [--memory M] [--cpu N] [--fps F] [--resolution 1080\|720] [--data-gb N]` | 修改,下次开机生效。`--data-gb` 只能改大,游戏数据不动:模拟器(0.36 起)开机时扩磁盘镜像,开机后在线扩文件系统。0.36 之前按 16 GB 及以下建的盘只能扩到下一个备份组之前(16 GB 约到 30 GB) |
| `proxy <i> [on\|off\|test] [--server URL\|auto] [--add 包名] [--remove 包名] [--apps 包名]` | 按应用走代理:名单里的应用走代理,其余直连(见下) |
| `delete <i> --yes` | 删除已关机的实例,连同游戏数据 |
| `start <i> [--window \| --headless] [--timeout 300] [--no-bridge]` | 开机(默认无头,`--window` 带窗口),等开完并做完开机后的设置,返回前确认 `/sdcard` 能写进文件(一般这时早已就绪;最多再等 60 秒,到点只警告);实例正在关机时(比如刚点了模拟器窗口的 ×)先等它关完(最多 30 秒)再开机;实例还没有执行桥时接着装上,`--no-bridge` 这次不装。运行中不换方式:实例已经在运行时,给了 `--window` / `--headless` 而它不是这种方式,就报错(退出码 5),不接管也不重启,要换先 `stop` 再 `start`;两个都不给就照原样接管 |
Expand Down Expand Up @@ -151,6 +151,10 @@ modavd [--root <目录>] [--json] [--quiet] [--verbose] <命令> [参数]
- 循环数不超过宿主的余量:逻辑核数减去所有 qemu 的 vCPU 数(含本实例;绑了核就按那一组算)。
- 所以逻辑核数不到本实例 vCPU 两倍的电脑上,只开一台也会少开:默认 6 核的实例在 8 线程的电脑上只开 2 个,6 线程及以下不开,下载会比开满时慢(在 7950X3D 上实测,1–2 个循环和不开差不多)。
- 这是有意的取舍:给 qemu 自己的线程、Windows 和别的程序留出核。
分辨率只有两档:1080p 为 1920×1080 / DPI 280,720p 为 1280×720 / DPI 240。新建默认 1080p,720p 更省资源。管理器的新建和实例设置、CLI `--resolution 1080|720`、MCP `resolution` 使用同一套档位。已有实例按宽高识别:横屏或竖屏的 1920×1080 保持 1080p,其余配置及缺字段的旧配置保持 720p。换档联动宽、高、DPI,保留方向,下次冷启动生效;DPI 不单独设置,不使用运行时 `wm size` 或 `wm density`。自动化工具里的分辨率也要跟着改。

AUTO-MAS 当前优先调用包内 CLI,这条启动路径保持所选档位。没有可用 CLI 的旧根目录会回退到固定 720p 的 Python 启动路径,使用 1080p 前须更新包内 CLI。AUTO-MAS 的旧新建和显示配置表单仍需配套支持两档;可先在 ModAVD 管理器里设置。

## MCP

```
Expand Down
92 changes: 72 additions & 20 deletions internal/avd/avd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// mas-avd.json. File operations only; nothing here looks at processes or ports (callers pass a busy-port check).
//
// Port of AUTO-MAS app/utils/common/emulator2/avd/instances.py (+ the choices in constants.py). Files must come out
// byte-identical to what AUTO-MAS writes, so the two can manage the same root during the move to modavd.
// compatible with AUTO-MAS, except for the two display presets added in ModAVD#100.
package avd

import (
Expand All @@ -24,9 +24,10 @@ import (

// Instance options (user decisions: memory and cores only in these steps, no silent rounding).
var (
MemoryChoicesMB = []int{3072, 4096, 5120, 6144}
CPUChoices = []int{2, 4, 6}
FPSChoices = []int{30, 60}
MemoryChoicesMB = []int{3072, 4096, 5120, 6144}
CPUChoices = []int{2, 4, 6}
FPSChoices = []int{30, 60}
ResolutionChoices = []int{1080, 720}
)

const (
Expand All @@ -36,9 +37,9 @@ const (
DefaultDataPartitionGB = 64
MinDataPartitionGB = 16
MaxDataPartitionGB = 512
// Display is fixed at 720p (user 10-04), written into config.ini before every boot, never `wm size`.
ScreenWidth, ScreenHeight, ScreenDensity = 1280, 720, 240
TargetAPI = "android-34"
// New instances default to 1080p. Existing instances are identified by their dimensions.
DefaultResolution = 1080
TargetAPI = "android-34"
// image.sysdir.1, relative to the SDK root, with the trailing backslash AUTO-MAS writes.
SystemImageSysdir = `system-images\android-34\google_apis\x86_64\`
)
Expand Down Expand Up @@ -531,6 +532,18 @@ func ValidateFPS(fps *int) (int, error) {
return value, nil
}

// ValidateResolution defaults new instances to 1080p; DPI is part of the preset.
func ValidateResolution(resolution *int) (int, error) {
value := DefaultResolution
if resolution != nil {
value = *resolution
}
if !slices.Contains(ResolutionChoices, value) {
return 0, errs.New(errs.InvalidArgument, "分辨率只能选 1080 或 720")
}
return value, nil
}

// isLineBreak is every character str.splitlines() splits at: a title holding one would break config.ini apart.
func isLineBreak(r rune) bool {
switch r {
Expand All @@ -556,6 +569,7 @@ type CreateOptions struct {
CPU int
DataPartitionGB int
FPS int
Resolution int // 0 = DefaultResolution
// Creating also sets instances.<i>.creationState = "creating" (emu's create setup), in the same write as the other
// keys: the instance is never listed without it.
Creating bool
Expand Down Expand Up @@ -585,6 +599,15 @@ func Create(r *root.Root, i int, opt CreateOptions) (*Instance, error) {
if err != nil {
return nil, err
}
var resolutionArg *int
if opt.Resolution != 0 {
resolutionArg = &opt.Resolution
}
resolution, err := ValidateResolution(resolutionArg)
if err != nil {
return nil, err
}
display := displayChanges(nil, resolution)
if err := validateTitle(opt.Title); err != nil {
return nil, err
}
Expand Down Expand Up @@ -612,9 +635,9 @@ func Create(r *root.Root, i int, opt CreateOptions) (*Instance, error) {
{"disk.dataPartition.size", strconv.Itoa(dataGB) + "G"},
{"hw.cpu.ncore", strconv.Itoa(cpu)},
{"hw.ramSize", strconv.Itoa(memory) + "M"},
{"hw.lcd.width", strconv.Itoa(ScreenWidth)},
{"hw.lcd.height", strconv.Itoa(ScreenHeight)},
{"hw.lcd.density", strconv.Itoa(ScreenDensity)},
{"hw.lcd.width", display[0][1]},
{"hw.lcd.height", display[1][1]},
{"hw.lcd.density", display[2][1]},
{"hw.lcd.vsync", strconv.Itoa(fps)},
{"image.sysdir.1", SystemImageSysdir},
} {
Expand Down Expand Up @@ -703,10 +726,11 @@ func FreeIndex(plan root.PortPlan, existing map[int]bool, busy func(ports []int)
}

type UpdateOptions struct {
Title *string
MemoryMB *int
CPU *int
FPS *int
Title *string
MemoryMB *int
CPU *int
FPS *int
Resolution *int
// DataPartitionGB raises disk.dataPartition.size (modavd only; AUTO-MAS cannot change it): within
// MinDataPartitionGB..MaxDataPartitionGB and never below the current value. The emulator (sdk-mas36 on) grows the
// disk image at the next boot without touching the data, and the boot's after-boot step grows the file system.
Expand Down Expand Up @@ -750,6 +774,12 @@ func Update(r *root.Root, i int, opt UpdateOptions) (map[string]any, error) {
return nil, err
}
}
var resolution int
if opt.Resolution != nil {
if resolution, err = ValidateResolution(opt.Resolution); err != nil {
return nil, err
}
}
memoryArg := opt.MemoryMB
if memoryArg == nil {
current := MemoryFor(r, i)
Expand Down Expand Up @@ -786,6 +816,9 @@ func Update(r *root.Root, i int, opt UpdateOptions) (map[string]any, error) {
if opt.FPS != nil {
changes = append(changes, [2]string{"hw.lcd.vsync", strconv.Itoa(fps)})
}
if opt.Resolution != nil {
changes = append(changes, displayChanges(in.Config, resolution)...)
}
if opt.DataPartitionGB != nil {
changes = append(changes, [2]string{"disk.dataPartition.size", strconv.Itoa(data) + "G"})
}
Expand Down Expand Up @@ -817,6 +850,9 @@ func Update(r *root.Root, i int, opt UpdateOptions) (map[string]any, error) {
if opt.FPS != nil {
applied["fps"] = fps
}
if opt.Resolution != nil {
applied["resolution"] = resolution
}
if opt.DataPartitionGB != nil {
applied["data_partition_gb"] = data
}
Expand Down Expand Up @@ -900,19 +936,35 @@ func deleteInstanceFiles(r *root.Root, i int) error {

// ---- display -----------------------------------------------------------------------------------------------------

// DisplayChanges is the fixed 720p for cfg keeping its orientation (portrait when height > width), plus
// hw.mainKeys=yes: hw.lcd.width, hw.lcd.height, hw.lcd.density, hw.mainKeys in that order.
func DisplayChanges(cfg *Ini) [][2]string {
// Resolution identifies 1080p in either orientation. All other old configurations stay 720p.
func (in *Instance) Resolution() int { return resolutionFor(in.Config) }

func resolutionFor(cfg *Ini) int {
w, _ := intOrNone(cfg.Value("hw.lcd.width"))
h, _ := intOrNone(cfg.Value("hw.lcd.height"))
width, height := ScreenWidth, ScreenHeight
if (w == 1920 && h == 1080) || (w == 1080 && h == 1920) {
return 1080
}
return 720
}

// DisplayChanges normalizes the current preset's dimensions and DPI, preserving orientation.
func DisplayChanges(cfg *Ini) [][2]string { return displayChanges(cfg, resolutionFor(cfg)) }

func displayChanges(cfg *Ini, resolution int) [][2]string {
w, _ := intOrNone(cfg.Value("hw.lcd.width"))
h, _ := intOrNone(cfg.Value("hw.lcd.height"))
width, height, density := 1280, 720, 240
if resolution == 1080 {
width, height, density = 1920, 1080, 280
}
if h > w {
width, height = ScreenHeight, ScreenWidth
width, height = height, width
}
return [][2]string{
{"hw.lcd.width", strconv.Itoa(width)},
{"hw.lcd.height", strconv.Itoa(height)},
{"hw.lcd.density", strconv.Itoa(ScreenDensity)},
{"hw.lcd.density", strconv.Itoa(density)},
{"hw.mainKeys", "yes"},
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/avd/avd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ func TestListAndGet(t *testing.T) {
if d, ok := in.DataPartitionGB(); !ok || d != 64 {
t.Errorf("DataPartitionGB %d", d)
}
if w, h, dpi := in.Display(); in.FPS() != 60 || w != 1280 || h != 720 || dpi != 240 {
if w, h, dpi := in.Display(); in.FPS() != 60 || w != 1920 || h != 1080 || dpi != 280 {
t.Errorf("FPS / Display %d %d %d %d", in.FPS(), w, h, dpi)
}
// An instance whose config.ini cannot be read (opened elsewhere without sharing) is listed with an empty config
Expand Down Expand Up @@ -1066,7 +1066,7 @@ func TestApplyDisplay(t *testing.T) {
if err != nil || !wrote {
t.Fatalf("first: %v %v", wrote, err)
}
want := "hw.lcd.width=720\r\nhw.lcd.height=1280\r\nhw.lcd.density=240\r\nx=1\r\nhw.mainKeys=yes\r\n"
want := "hw.lcd.width=1080\r\nhw.lcd.height=1920\r\nhw.lcd.density=280\r\nx=1\r\nhw.mainKeys=yes\r\n"
if got := string(readFile(t, ConfigPath(r, 0))); got != want {
t.Fatalf("got %q", got)
}
Expand Down
Loading
Loading