Skip to content

fix(Device): 适配 MuMu 6.0 (Android 15) 多虚拟显示 - #1952

Open
Enough1122 wants to merge 2 commits into
runhey:devfrom
Enough1122:fix/mumu15-multi-display
Open

Enough1122 wants to merge 2 commits into
runhey:devfrom
Enough1122:fix/mumu15-multi-display

Conversation

@Enough1122

@Enough1122 Enough1122 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

问题

MuMu 6.0(Android 15 引擎)给每个 app 分配独立的虚拟显示(mumuscreenNNN),而 screencap / input 不带 -d 时默认作用于 display 0(桌面)。结果 OAS 截到的是桌面、点击也落在桌面,页面识别全部失败。

本机诊断(MuMu 6.0,127.0.0.1:16416):

$ adb shell dumpsys window displays
  Display: mDisplayId=0 ... mFocusedApp=ActivityRecord{ ... app.lawnchair/.LawnchairLauncher }
  Display: mDisplayId=3 ... mFocusedApp=ActivityRecord{ ... com.netease.onmyoji.wyzymnqsd_cps/com.netease.onmyoji.Client }

$ adb exec-out screencap -p            # → 桌面(Lawnchair)
$ adb exec-out screencap -p -d 4619826888814064386   # → 游戏画面

input 同理:input -d <small display id> tap x y 才能点进游戏。

改动

  • 新增 _mumu_display_ids():从 dumpsys window displays 找「非 0 且带前台 ActivityRecord」的显示,dumpsys display 取 mumuscreenNNN 名字,再用 dumpsys SurfaceFlinger --display-id 解析长 id;结果缓存
  • screenshot_adb / screenshot_adb_nc:追加 screencap -d <long id>
  • click_adb / swipe_adb / long_click_adb / motionevent 滑动:统一走 _input_prefix(),即 input -d <small id>
  • app_start_adb / app_stop_adb:清缓存(每次启动虚拟显示 id 会变)
  • 负结果缓存 20 秒:单屏设备不会因为每次截图/点击都多付一次 dumpsys 而变慢

兼容性

三重门控:非 0 显示上有前台 ActivityRecord + 显示名匹配 mumuscreen\d+ + SurfaceFlinger 能解析出长 id。任一不满足就返回 None,所有命令保持改动前的形状。老 MuMu 12 上游戏在 display 0,按设计不会激活。

诚实声明:回退路径是按机制推理 + 历史抓帧判断的,没有在 Android 12 引擎实例上实测(手头只有 A15 实例)。有 A12 环境的朋友麻烦帮忙验一下,有问题我改。

验证

2026-10-18 起在本机 MuMu 6.0 / Android 15 上:

  • 截图、OCR、点击、滑动正常;RealmRaid 跑过此前约 80 秒必死的线,连续运行稳定
  • 更新到最新 dev(6d718fec)后重新验证:screenshot_adb_nc / screenshot_adb 均返回 1280x720 游戏画面,任务正常完成:
2026-10-19 15:39:02.170 | script.py:0696 | INFO | Scheduler: Start task `TalismanPass`
2026-10-19 15:39:30.837 | script.py:0704 | INFO | Scheduler: End task `TalismanPass`
  • 未打补丁时:截图落在桌面,任务在页面识别阶段即失败(Page detect miss / Game page unknown)

Sourcery 总结

在 MuMu 6.0 的虚拟显示屏上定位应用,确保截图和用户输入能够可靠地传送到正在运行的游戏。

Bug Fixes:

  • 在 MuMu 6.0 上,将目标从桌面显示屏改为应用的虚拟显示屏,以修复 ADB 截图和触摸输入问题。

Enhancements:

  • 添加带缓存且受兼容性条件限制的 MuMu 虚拟显示屏 ID 检测,并在应用启动或停止时重置检测状态。
  • 对于没有匹配的 MuMu 多显示屏配置的设备,保留现有的 ADB 命令行为。
Original summary in English

Sourcery 摘要

将 MuMu 6.0 应用定位到其虚拟显示屏上,确保截图和用户输入能够可靠地传递到正在运行的游戏。

错误修复:

  • 通过将目标设为游戏的虚拟显示屏而非桌面显示屏,修复 MuMu 6.0 上的 ADB 截图和输入事件问题。

功能增强:

  • 添加兼容性门控的 MuMu 虚拟显示屏标识符检测,并缓存检测结果;在应用启动或停止时刷新检测结果。
  • 对于没有匹配的 MuMu 多显示屏配置的设备,保留现有的 ADB 命令行为。
Original summary in English

Sourcery 总结

将 MuMu 6.0 上的截图和输入操作定位到运行应用的虚拟显示屏。

错误修复:

  • 修复 MuMu 6.0 多虚拟显示环境下,ADB 截图和输入操作错误作用于桌面的问题。

功能增强:

  • 增加兼容性受限且带缓存的 MuMu 虚拟显示检测,并在应用启动或停止时刷新显示标识。
  • 在与 MuMu 多显示配置不匹配的设备上保留原有 ADB 命令行为。

测试:

  • 新增针对 MuMu 虚拟显示检测、缓存、回退及输入目标选择的自动化测试。
Original summary in English

Summary by Sourcery

将 MuMu 6.0 上的截图和输入操作定位到运行应用的虚拟显示屏。

Bug Fixes:

  • 修复 MuMu 6.0 多虚拟显示环境下 ADB 截图和输入操作错误作用于桌面的问题。

Enhancements:

  • 增加兼容性受限、带缓存的 MuMu 虚拟显示检测,并在应用启动或停止时刷新显示标识。
  • 在不匹配 MuMu 多显示配置的设备上保留原有 ADB 命令行为。

Tests:

  • 新增 MuMu 虚拟显示检测、缓存、回退及输入目标选择的自动化测试。

MuMu 6.0 的 A15 引擎给每个 app 分配独立虚拟显示(mumuscreenNNN),
而 screencap/input 默认作用于 display 0(桌面),导致截图与点击全部落在桌面。

- 新增 _mumu_display_ids():定位游戏所在的非 0 显示并解析 SurfaceFlinger 长 id
- 截图走 screencap -d <long id>,点击/滑动/长按走 input -d <small id>
- app_start/app_stop 清缓存(每次启动显示 id 会变);负结果缓存 20s
- 门控不满足时命令形状与改动前完全一致(老 MuMu 12 不受影响)
@github-actions

Copy link
Copy Markdown
Contributor

Findings

  • C7 [风险]
    位置:module/device/method/adb.py:_mumu_display_ids、screenshot_adb*、_input_prefix
    原因:全局 ADB 截图和输入都依赖启发式选择非 0 且存在前台 Activity 的显示屏;当前仅有 MuMu 6.0/Android 15 实测,Android 12 及其他多显示屏场景未验证,可能改变基础设备行为。
    修改:补充模拟 dumpsys 输出的单元测试,覆盖 display 0、MuMu 6/A15、旧 MuMu 12 和多个非游戏显示屏,并完成至少一组旧版本设备回归。

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • ab.chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"

See Network Configuration for more information.

Generated by PR Review Checklist for #1952 · codex · gpt56 · 13.6 AIC · ⌖ 2 AIC · ⊞ 12.1K · ◷

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

嘿——我发现了 5 个问题

AI 代理提示词
请处理这次代码审查中的评论:

## 各条评论

### 评论 1
<location path="module/device/method/adb.py" line_range="198" />
<code_context>
+                if m:
+                    current = int(m.group(1))
+                    continue
+                if current and 'mFocusedApp=ActivityRecord' in line and 'launcher' not in line.lower():
+                    display_id = current
+                    break
</code_context>
<issue_to_address>
**截图指向了另一个应用**

当另一个非启动器应用在游戏显示屏之前列出的非零 MuMu 显示屏上处于前台时,`_mumu_display_ids()` 不会将前台 Activity 与 `self.package` 匹配,就接受了该显示屏,并缓存其 ID,导致截图和输入被发送到另一个应用,而不是游戏。

接受该显示屏之前,请将前台 Activity 与 `self.package` 进行匹配。
</issue_to_address>

### 评论 2
<location path="module/device/method/adb.py" line_range="361" />
<code_context>
         if not package_name:
             package_name = self.package
+        # MuMu 6.0 assigns the app a new virtual display on every launch
+        self._reset_mumu_display_ids()
         result = self.adb_shell([
             'monkey', '-p', package_name, '-c',
</code_context>
<issue_to_address>
**ADB 仍在使用旧显示屏**

当应用通过绕过 ADB 重置方法的生命周期路径停止或重新启动时,`AppControl.app_start()` 和 `app_stop()` 可能会使用 uiautomator2,却没有清除 ADB 显示屏缓存。因此,后续 ADB 截图和输入会重复使用旧显示屏 ID,并指向错误的显示屏。

无论选择哪种启动或停止方法,都应在应用共用的生命周期路径中清除 ADB 显示屏缓存。

另请参阅 `module/device/method/adb.py:179-180`、`module/device/method/adb.py:224`、`module/device/method/adb.py:412`。
</issue_to_address>

### 评论 3
<location path="module/device/method/adb.py" line_range="187" />
<code_context>
+        now = time.monotonic()
+        if now < self._mumu_display_ids_retry_at:
+            return None
+        self._mumu_display_ids_retry_at = now + self._MUMU_DISPLAY_RETRY_SEC
+        try:
+            # 1) Which display is the game running on? (skip display 0, the desktop)
</code_context>
<issue_to_address>
**游戏检测仍停留在桌面**

如果在 MuMu 暴露游戏所在的前台虚拟显示屏之前执行显示屏查询,或者 `dumpsys` 调用暂时失败,`_mumu_display_ids()` 会在检查显示屏之前设置 20 秒的重试截止时间,并在查询失败或显示屏尚未就绪时返回 `None`。这样一来,即使游戏显示屏随后已经可用,截图和输入仍会使用显示屏 0,导致启动检测和交互失败。

查询失败或显示屏尚未就绪时,不要将结果缓存整个重试窗口;应尽快重试,包括在启动后重试。

另请参阅 `module/device/method/adb.py:227-228`、`module/device/method/adb.py:361`。
</issue_to_address>

### 评论 4
<location path="module/device/method/adb.py" line_range="224" />
<code_context>
+            if long_id is None:
+                return None
+            logger.info(f'MuMu multi-display: game on display {display_id} ({name}), screencap id {long_id}')
+            self._mumu_display_ids_cache = (display_id, long_id)
+            return self._mumu_display_ids_cache
+        except Exception as e:
</code_context>
<issue_to_address>
**过时的显示屏 ID 覆盖了重置操作**

如果显示屏检测与应用启动或停止同时发生,`_mumu_display_ids()` 可能会在 `_reset_mumu_display_ids()` 执行后才完成,并将旧显示屏 ID 写入缓存。之后的截图和输入便会指向已销毁的显示屏。

请阻止在缓存重置之前启动的检测在重置之后发布其结果。

另请参阅 `module/device/method/adb.py:167-170`。
</issue_to_address>

### 评论 5
<location path="module/device/method/adb.py" line_range="190" />
<code_context>
+        self._mumu_display_ids_retry_at = now + self._MUMU_DISPLAY_RETRY_SEC
+        try:
+            # 1) Which display is the game running on? (skip display 0, the desktop)
+            out = self.adb_shell(['dumpsys', 'window', 'displays'])
+            display_id = None
+            current = None
</code_context>
<issue_to_address>
**检测耗时过长会延误命令**

当设备对显示屏检测的 `dumpsys` 调用响应缓慢时,每次 `dumpsys` 调用都会使用 `adb_shell` 默认的 10 秒超时,而检测回退逻辑会吞掉异常。因此,设备响应慢或无响应时,截图或输入处理可能会被卡住,直到代码回退到显示屏 0;这会延误任务,还可能导致操作落在桌面上。请为显示屏检测使用有上限且更短的超时时间,并让调用方能够获知回退情况。

为检测调用显式设置更短的超时时间,并在检测失败时将失败情况反馈出来,而不是静默地继续使用显示屏 0。

另请参阅 `module/device/method/adb.py:227`。
</issue_to_address>

Sourcery 评估

等待批准。 请先处理 5 项问题。

阻塞性问题:module/device/method/adb.py:198、module/device/method/adb.py:361、module/device/method/adb.py:187、module/device/method/adb.py:224、module/device/method/adb.py:190


Sourcery 对开源项目免费——如果您喜欢我们的审查,欢迎分享 ✨
Original comment in English

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="module/device/method/adb.py" line_range="198" />
<code_context>
+                if m:
+                    current = int(m.group(1))
+                    continue
+                if current and 'mFocusedApp=ActivityRecord' in line and 'launcher' not in line.lower():
+                    display_id = current
+                    break
</code_context>
<issue_to_address>
**Screenshots target another app**

When another non-launcher app is focused on a nonzero MuMu display listed before the game’s display, `_mumu_display_ids()` accepts that display without matching the focused activity to `self.package`, caches its IDs, and sends screenshots and input to the other app instead of the game.

Match the focused activity against `self.package` before accepting its display.
</issue_to_address>

### Comment 2
<location path="module/device/method/adb.py" line_range="361" />
<code_context>
         if not package_name:
             package_name = self.package
+        # MuMu 6.0 assigns the app a new virtual display on every launch
+        self._reset_mumu_display_ids()
         result = self.adb_shell([
             'monkey', '-p', package_name, '-c',
</code_context>
<issue_to_address>
**ADB keeps using an old display**

When the app is stopped or relaunched through a lifecycle path that bypasses the ADB reset methods, `AppControl.app_start()` and `app_stop()` can use uiautomator2 without clearing the ADB display cache, so subsequent ADB screenshots and input reuse the old display ID and target the wrong display.

Clear the ADB display cache in the shared app lifecycle paths regardless of which start or stop method they select.

Also at `module/device/method/adb.py:179-180`, `module/device/method/adb.py:224`, `module/device/method/adb.py:412`.
</issue_to_address>

### Comment 3
<location path="module/device/method/adb.py" line_range="187" />
<code_context>
+        now = time.monotonic()
+        if now < self._mumu_display_ids_retry_at:
+            return None
+        self._mumu_display_ids_retry_at = now + self._MUMU_DISPLAY_RETRY_SEC
+        try:
+            # 1) Which display is the game running on? (skip display 0, the desktop)
</code_context>
<issue_to_address>
**Game detection stays on desktop**

When a display lookup runs before MuMu exposes the game’s focused virtual display, or a `dumpsys` call fails transiently, `_mumu_display_ids()` sets the 20-second retry deadline before checking for the display and returns `None` on a failed or not-yet-ready lookup. Screenshots and input then use display 0 even after the game display is available, so startup detection and interaction fail.

Do not cache failed or not-yet-ready lookups for the full retry window; retry promptly, including after launch.

Also at `module/device/method/adb.py:227-228`, `module/device/method/adb.py:361`.
</issue_to_address>

### Comment 4
<location path="module/device/method/adb.py" line_range="224" />
<code_context>
+            if long_id is None:
+                return None
+            logger.info(f'MuMu multi-display: game on display {display_id} ({name}), screencap id {long_id}')
+            self._mumu_display_ids_cache = (display_id, long_id)
+            return self._mumu_display_ids_cache
+        except Exception as e:
</code_context>
<issue_to_address>
**Stale display IDs overwrite resets**

When display detection overlaps an app start or stop, `_mumu_display_ids()` can finish after `_reset_mumu_display_ids()` and publish the old display ID to the cache. Later screenshots and input then target the destroyed display.

Prevent a detection started before a cache reset from publishing its result afterward.

Also at `module/device/method/adb.py:167-170`.
</issue_to_address>

### Comment 5
<location path="module/device/method/adb.py" line_range="190" />
<code_context>
+        self._mumu_display_ids_retry_at = now + self._MUMU_DISPLAY_RETRY_SEC
+        try:
+            # 1) Which display is the game running on? (skip display 0, the desktop)
+            out = self.adb_shell(['dumpsys', 'window', 'displays'])
+            display_id = None
+            current = None
</code_context>
<issue_to_address>
**Slow detection delays commands**

When a device responds slowly to a display-detection `dumpsys` call, each `dumpsys` call uses `adb_shell`’s default 10-second timeout, and an exception is swallowed by the detection fallback. A slow or unresponsive device can therefore stall screenshot or input handling before the code falls back to display 0, leaving the task delayed and potentially operating on the desktop. Use a bounded, shorter timeout for display detection and make the fallback visible to callers.

Apply a shorter explicit timeout to detection calls and surface detection failure rather than silently continuing with display 0.

Also at `module/device/method/adb.py:227`.
</issue_to_address>

Sourcery assessment

Approval pending. 5 findings to address first.

Blocking findings: module/device/method/adb.py:198, module/device/method/adb.py:361, module/device/method/adb.py:187, module/device/method/adb.py:224, module/device/method/adb.py:190


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread module/device/method/adb.py Outdated
Comment thread module/device/method/adb.py Outdated
Comment thread module/device/method/adb.py Outdated
Comment thread module/device/method/adb.py
Comment thread module/device/method/adb.py Outdated
- 优先选择承载本包窗口的显示,系统弹窗抢焦点时退回首个非桌面前台显示
- 未就绪/异常的探测不再缓存,仅确认无虚拟显示时才短路 20s
- 探测期间 app 重启(代际号变化)则丢弃结果,不写入过期显示 id
- 检测用 dumpsys 走 3s 短超时
- app_start/app_stop 的缓存清理移到共用生命周期,覆盖 u2/adb 两条分支
- 新增 tests/module/device/test_mumu_display_ids.py(7 个用例,模拟 dumpsys 输出)
@runhey

runhey commented Oct 19, 2026

Copy link
Copy Markdown
Owner

你能否先讲一下怎么可以配置出模拟器多虚拟显示

@Enough1122

Copy link
Copy Markdown
Contributor Author

可以。这个不是配置出来的——升级到 Android 15 引擎后,应用会自己跑在独立的虚拟屏上,桌面还是在 0 号屏,不需要动任何多屏相关的设置。

我们的环境:

  • MuMu 模拟器 6.8.7.0(实例设置里的 product_version 就是这个,客户端直接更新上来的)
  • 实例引擎 Android 15:实例目录 vms/MuMuPlayer-15.0-1,adb 端口 16416(旧引擎是 16384)
  • 游戏:com.netease.onmyoji.wyzymnqsd_cps

复现步骤:

  1. 用管理器把实例的安卓版本升到 15(管理器里的"升级安卓版本",对应 mumu-cli upgrade;我们就是升级原实例的,数据保留,稳妥起见先备份);
  2. 启动游戏,然后:
adb connect 127.0.0.1:16416
adb shell dumpsys SurfaceFlinger --display-id
# Display 4619827820427265280 (HWC display 0): ... displayName="mumuscreen000"   ← 桌面
# Display 4619827767814508545 (HWC display 1): ... displayName="mumuscreen001"
# Display 4619826888814064386 (HWC display 2): ... displayName="mumuscreen002"

adb shell dumpsys window displays | grep -E "mDisplayId=|mFocusedApp=ActivityRecord"
# mDisplayId=0 → app.lawnchair/.LawnchairLauncher(桌面)
# mDisplayId=2 → com.netease.onmyoji.../Client(游戏;屏号会变,我们也见过 3)

如果只看到 mDisplayId=0、游戏也在 0 号屏,说明实例还没升到 A15 引擎。

补丁在意的两个 id 口径(实测最容易踩的坑):

  • 截图 screencap -p -d <id> 要的是 SurfaceFlinger 的 long uniqueId(上面 4619… 那两个),传小数字会直接失败(Status: -2);不带 -d 的 screencap 固定截 0 号屏(桌面)——这就是之前"人看着有画面、脚本截图全黑"的原因。
  • 输入 input -d <id> tap 用的才是 mDisplayId 小数字(2/3),而且屏号是动态的:应用重启后会变,不能写死,只能运行时探测。

所以补丁是"探测到才启用":非 0 号屏 + 该屏有前台 Activity + 屏名匹配 mumuscreen\d+ + SurfaceFlinger 能解析 long id,四条全中才走新命令,否则完全回退旧形状。旧版 A12 引擎游戏在 0 号屏,按设计不会激活——这点我在描述里也写了"没有 A12 实例实测",如果你手头有旧引擎方便验一下,我按结果调整。

另外 CI 提到的那组 mock dumpsys 单测,我可以在这个 PR 上补;A12 实机回归还是得借一台旧引擎实例。

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