Skip to content

DroidExtend 1.6.3

Choose a tag to compare

@Blank2007 Blank2007 released this 16 Oct 16:14

1.6.3 —— 两个菜单都问错了问题(含 1.6.0 / 1.6.1 / 1.6.2 的全部改动)

用一根 USB 数据线,把安卓手机/平板变成 Windows 的第二块显示器 —— 真扩展桌面,不是投屏镜像,支持触摸反控。

这一次发布一次跨了 四个版本号(1.6.0 / 1.6.1 / 1.6.2 / 1.6.3)。它们不是四件独立的事,而是
同一份清单连着做完的四步:先是用户列的十四条需求,然后是「触控逻辑还是有问题」,
接着是三条收尾的小要求,最后是两个「看着没生效」的菜单。

装这一个安装包就够了,下面四节会一起生效。不想看长文的,看这张表:

版本 一句话 关键数字
1.6.0 十四条需求逐条落地,其中八个是真 bug ffmpeg 自称支持 11 个编码器,实际只有 7 个能跑
1.6.1 触控重做:死区 / 长按右键 / 按帧率节流 手机端 91 次 write/秒 → 45 次
1.6.2 安装目录 / 隐私按钮 / 双击打不开文件 Windows 的双击判定框只有 4×4 像素
1.6.3 分辨率和编码器两个菜单都问错了问题 22 个尺寸里 11 个驱动根本没有;11 个编码器里只有 7 个能跑

一、1.6.0 —— 十四件被答应下来的事

这一版没有新概念,只有一堆**「本来应该是这样」**。十四条需求逐条落地:

# 需求 结论
① 双端隐私协议 + 首次弹窗同意 新增,条款版本号是字符串,改条款会重新问
② 双端原生动画 新增,尊重系统「关闭动画」设置
③ 托盘驻留 + 自启动 + 彻底退出 纯 ctypes,不引入任何第三方依赖
④ 安卓后台保活引导 13 个厂商的自启动页,每个都先验证存在
⑤ 源码全量开源 已满足
⑥ 自动更新补全 更新日志随包 + 下载位置可改 + 装后删包
⑦ 触控偏移 + 关窗后失灵 真 bug,两处(见下)
⑧ 熄屏/被关闭时通知 Windows 输入协议新增 NOTICE 记录
⑨ README 与日志加 emoji 已满足
⑩ 开机未启动就激活虚拟屏 + 位置自定义 真 bug(见下)
⑪ 多编码 + 自动检测与降级 真 bug,三处(见下)
⑫ 卡顿排查 + 显卡占用说明 + 分辨率切不了 真 bug(见下)
⑬ 安装界面与程序本体中英切换 安装程序、Windows 端、安卓端三处都能切
⑭ adb 通道占用检查 + 驱动安装 工具菜单新增「诊断连接问题…」

① 编码不再只有 H.264(需求 ⑪)

症状:编码器下拉里有 hevc_nvenc、av1_amf 这些名字,选完也能"连上",但画面是绿的、或者干脆黑屏。

根因有三个,全在打包好的代码里躺着:

  1. 拼 ffmpeg 命令行时,最后硬编码了一段 -f h264。选任何非 H.264 的编码器,
    产出的流都被当成 H.264 拆包 → 单元全是垃圾。
  2. 切分器只认 H.264 的 NAL 头(1 字节),而 HEVC 的 NAL 头是 2 字节,
    于是永远找不到图像单元。
  3. AV1 的 -profile:v main 会让 NVENC 拒绝初始化,报的还是
    maybe incorrect parameters such as bit_rate, rate, width or height ——
    消息里既不提 profile 也不提 codec,只有把 -profile:v 去掉才好。

顺带发现第四个:旧切分逻辑「连续两个 VCL NAL 就切一帧」会把多 slice 的一帧切成多份,
实测 libx264 的 60 帧被切成 299 个单元。现在按 slice header 的最高位判断,回到正确的 60。

编码器检测也不再靠猜:以前只看 ffmpeg -encoders 列出什么名字,现在会真跑一帧
(合成画面 1 秒)看有没有输出。本机 ffmpeg 自称支持 11 个编码器,实际只有 7 个能用:

可用 说明
h264_nvenc / hevc_nvenc / av1_nvenc NVIDIA,首选
h264_amf / hevc_amf AMD
libx264 / libx265 纯 CPU 兜底

不可用:*_qsv(本机没有 Intel GPU,Error creating a MFX session: -9)、
av1_amf(核显不支持 AV1 编码)、libsvtav1(这个 ffmpeg 构建里没有)。
保存的选择失效时会自动回退并在日志里说明,不会再给你一个绿屏。

② 触摸失灵与坐标偏移(需求 ⑦)

症状 A:主副屏位置一变,点哪都偏。
根因是拿虚拟桌面矩形的方式:GetSystemMetrics 对非 per-monitor DPI aware 的进程会说谎 ——
本机实测报 2048x2501,真实是 3200x3736。现在改用实时显示器清单求并集,并加 1 秒 TTL 跟随显示器移动。

症状 B:关掉前台窗口之后,整个手机屏就点不动了,但画面还在跑。
根因是左键卡在按下状态:手指按着时窗口被销毁/手机断开,UP 事件永远不来,
Windows 带着「左键按下」卡在模态拖动循环里,之后所有注入都被吞掉。
现在有三道保险:补发 LEFTUP、按住超过 30 秒的 watchdog 自动解卡、断开与退出时释放。

顺带修掉一个从未生效过的功能:垂直滚动。 输入协议里 z 字段是 uint16,
而手机端把 dx | (dy << 16) 塞进去,host 端用 z >> 16 取 dy ——
16 位变量右移 16 位恒为 0,纵向滚轮从来没动过。协议从 7 字节改到 9 字节(两端一起发,无兼容问题)。

③ 开机没启动程序,虚拟屏却已经在桌上(需求 ⑩)

根因:Windows 把显示配置持久化在
HKLM\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\Connectivity,
其中 Recent 就是开机重建拓扑的依据。挂屏走的是持久化的 API,
而摘屏没有传 SDC_SAVE_TO_DATABASE —— 一正一反不对称:挂屏活过了重启、摘屏没有。

实测往返证据:Recent 从 BOE0D550_26_07E8_EC(干净)→ 挂屏后多出一条
MTT1337518463207 → 摘屏后回到原值。

同时加了防误伤:本机有 21 个显示输出,其中 10 个属于网易 UU 远程。
原来的认屏逻辑按「已连接且有 monitor」排序,UU 一跑就会把它的适配器当成我们的虚拟屏。
现在按驱动身份判定,只认我们自己的 MttVDD。

另外新增工具菜单 「清理残留的虚拟屏」(启动时也会检查一次并在日志里提示,只提示、不擅自动手),
虚拟屏位置也不再只有上下左右 —— 下拉里选「自定义」可以直接填 X / Y(允许重叠)。

④ 改了分辨率没反应(需求 ⑫)

根因:应用显示的代码只在点「开始投屏」时被调用一次,运行中改分辨率没有任何代码
去叫 Windows 换模式 —— 设置存了,但没人执行。

现在改分辨率 / 刷新率 / 位置会立刻应用(600 ms 去抖,因为每次都要重启采集),
最多重试 4 次,并一律以实际生效的模式为准判断成功,而不是「显示器在就算成功」。
(Windows 在每次拓扑变更前后会短暂锁住显示配置,期间相关调用会失败 —— 所以必须重试。)

⑤ 第八个 bug:带 BOM 的设置文件(不在需求清单里)

这一个是打包之后拿真正的安装包做冒烟测试才撞上的。

测试写了一份 "language": "en" 的设置文件,程序起来标题栏却是中文。查下去发现:
PowerShell 的 Set-Content -Encoding UTF8(以及记事本的「UTF-8」选项)会在文件开头
写三个字节 EF BB BF,而读设置时用的是 encoding="utf-8" —— json.load 见到 BOM 直接抛异常,
被 except Exception 当成「文件损坏」吞掉,于是整个设置文件被静默丢弃,一切回到默认值。

这是普通用户完全踩得到的:settings.json 就放在我们请用户去看的那个目录里,
用记事本打开改一个数、保存,所有设置就没了,而且日志里一个字都不说。

修法是两个字符:encoding="utf-8-sig"(自动吃掉 BOM,对没有 BOM 的文件行为完全一致)。

发现它的过程本身也值得记:如果最后没有真的把装好的 exe 跑起来,这个 bug 会原样发出去 ——
因为 17 个测试脚本里没有一个会去写带 BOM 的文件。

⑥ 其余九条

  • 隐私协议:双端首次启动弹窗,不同意就退出;条款版本号是字符串,改条款后所有人会被重新询问。
  • 原生动画:Windows 端用分层窗口 alpha 淡入淡出(不用 AnimateWindow,
    它阻塞调用线程整个时长);安卓端用 Material 的动画 API。尊重系统「关闭动画」设置,低内存设备自动降级。
  • 托盘驻留 + 自启动:纯 ctypes 调 Shell_NotifyIconW,不引入任何第三方依赖。
    「彻底退出」会摘虚拟屏、清 adb 端口转发、关日志 —— 不留残余。
  • 安卓后台保活引导:一键跳到省电白名单,并说明唯一没有公开 Intent 的那一步:在最近任务里下拉锁定。
  • 更新:更新日志随程序打包(中英双语,因为 release notes 在断网时看不到)、下载位置可自定义、安装完自动删包。
    删安装包必须在安装程序返回之后 —— 删一个还在跑的安装程序就是半装状态的来源。
  • 熄屏通知:手机熄屏/亮屏/被系统关闭时主动告诉 Windows。
    故意没有「退到后台」通知 —— Android 的 onPause/onStop 分不清熄屏与切后台,否则你会收到两句说同一件事的话。
  • 中英切换:安装程序、Windows 程序、安卓端三处都能切,默认跟随系统。
  • adb 诊断:工具菜单新增「诊断连接问题…」,会报端口占用情况、指出最常见的那条原因
    (手机插上后停在「仅充电」模式),并提供 USB 驱动安装引导。

二、1.6.1 —— 触控:先把数字量出来

用户的原话是:「触摸逻辑还是有问题,参考一下其他远程控制软件的触控逻辑好吗,
而且为什么触摸延迟比画面延迟还要大」
。

为什么触摸延迟一定比画面延迟大

这不是 bug,是恒等式:

你感觉到的触摸延迟 = 手指 → 注入(触摸链路)
                   + 注入之后那一帧走完 采集 → 编码 → 传输 → 解码 → 显示

而界面上那个延迟数字只量了后半段。所以它必然更大,问题只在于前半段能不能更小。

三个数字

测什么 结果
隧道逐条发 600 条 9 字节记录 只到了 22 条,每条间隔 31 ms
同样 600 条一次发出去 600 条瞬时全部到达
PC 端注入一次鼠标移动 中位 0.457 ms(上限约 2180 次/秒)
手机端旧版本发位置更新的频率 约 91 次/秒,比隧道能力超标 3 倍

结论很清楚:瓶颈是「每次 write」的开销,不是带宽;PC 端注入完全不是瓶颈;
而手机端 91 次/秒远超隧道能承受的量,拖动时积压会持续增长——
这就是「越拖越滞后」的来源。

改了四处

  1. 手机端死区:手指落下时那一点点抖动不再被当成拖动。点一下就是点一下。
  2. 手机端长按 0.5 秒 = 右键,和常见远程控制软件一致。
  3. 手机端拖动按帧率节流:16 ms 一帧,也就是约每秒 60 次位置。
    比下一帧更快的位移人眼看不见,而且不便宜。
  4. 两端都只留下最新位置:中间那些已经过时的位置不再逐个发出去、也不再逐个注入。
    位置是幂等的所以可以丢,事件(按下、抬起、右键、滚动)一个都不丢。

改完的数字

改之前 改之后
一次拖动产生的批次 95 批 38 批
批次间隔 ~11 ms(91 次/秒) ~21–24 ms(约 45 次/秒)
拖动时注入的移动次数 每个位置都注入 只注入最新的一个

顺手发现的一件事:铺满屏幕会吃掉两侧边缘

手机端有个选项叫「让画面铺满屏幕(裁掉多余边缘)」,默认开着。
它按屏幕高度算出画面宽度,在 2136×3200 的机器上是 3797 像素,
而横屏窗口只有 3200 —— 居中的画面被父容器裁掉,两侧各 298.5 像素永远收不到触摸,
也就是 PC 桌面上最左和最右各约 7.9% 点不到(1920 宽的桌面上各约 151 像素,
开始菜单和最大化窗口的关闭按钮都在里面)。

这不是 bug 而是用户可选的偏好,所以默认值一个字符都没动,
只是在文档里把这个代价写清楚了。


三、1.6.2 —— 四件说出口的事,其中一件底下是个真坑

① 更新要装回老目录

程序内更新本来就会传 /DIR="<当前安装目录>",缺口在安装程序自己的目录页:
原来的设置是「永远显示目录页」,于是静默安装时可能落到默认目录,装出第二份程序。

现在改成:只有在安装程序自己找到旧安装时才隐藏目录页并复用那个目录,
首次安装仍然会给你页面让你选。

② 「同意并继续」贴在协议正文最下面

用户的原话是「按钮直接放在协议最下面,不然窗口没拉伸的时候容易找不到同意按钮在哪」。

改动:两个按钮从底部固定的位置移进协议正文的最后一行,跟着文本一起滚;
窗口高度也会按屏幕高度自适应(想要 620,最多不超过屏幕高度减 140,下限 360)。
实测对话框高 620 像素(屏幕 1280),把窗口压到 300 像素高时按钮仍然滚得到。

③ 双击打不开文件 —— 那个真坑

根因不在我们这边,在 Windows 里。 Windows 判定「这是一次双击」的容错框默认只有
4×4 像素(SM_CXDOUBLECLK)。鼠标伸得进去,指尖永远伸不进去 ——
两次点击落点差几十像素,就必然被当成两次独立单击。

我们这边还有一半责任:抬手时发的是手指漂移之后的位置,而死区内最多会漂移几十像素,
正好把落点推出那个本来就很小的双击矩形。

两边各修一半:

  • 主机侧:第二次按下如果落在上一次单击的时间窗内(读系统的双击时间,默认 0.5 秒,
    夹在 0.1–1.0 秒之间)且容差内(约 1920 宽桌面的 58 像素),
    就把这一对的坐标缝到第一次的落点上 —— 两次按下和两次抬起都对齐,Windows 无法不认。
  • 手机侧:点击在按下点抬手,只有拖动才跟手指。

配套还有一整套「不许乱缝」的规则:中间发生过拖动、滚轮、右键、或者连接断开,配对立刻作废;
连点三下也有效(第三下继续链在同一个坐标上)。

④ 手机端设置里写明手势

安卓端设置面板里新增「触摸操作」一节,六行手势表:

手势 效果
单指点击 单击左键
单指连点两下 双击左键
单指拖动 按住左键拖动
单指长按半秒 单击右键
双指上下滑动 滚轮
双指快速轻点 单击右键

下面还有三句说明:死区(轻微抖动不算拖动)、按帧率发送且只发最新位置但事件不丢、
连点两下要对齐落点。设置面板本身也包进了可滚动容器 —— 内容变多之后,
「关闭」按钮不能跑到屏幕外面去。


四、1.6.3 —— 两个菜单都问错了问题

这一版修的是你连着问的两件事:「为什么分辨率更改后还是没变化」和「还是有部分视频编码不可用」。
两个都不是「又忘了做」,而是程序问错了问题——一次问的是「驱动应该支持什么」,一次问的是
「这个 ffmpeg 编译进了什么」,两次问的都不是「这台机器上到底行不行」。

① 改了分辨率还是没变化 —— 驱动只认它自己那张表

症状:分辨率下拉里挑一个,点下去什么都没发生。画面不闪、尺寸不变、也没有任何提示。

实测(tools\tmp\res_change_probe.py,真挂虚拟显示器):

set_mode \\.\DISPLAY10 1920x1080@60  -> rc =  0  SUCCESSFUL   1920x1080@60
set_mode \\.\DISPLAY10 2280x1080@60  -> rc = -2  BADMODE      1920x1080@60   (一点没变)
set_mode \\.\DISPLAY10 1920x1080@75  -> rc = -2  BADMODE      1920x1080@60   (一点没变)
set_mode \\.\DISPLAY10 3200x2136@120 -> rc =  0  SUCCESSFUL   3200x2136@120

根因:虚拟显示器驱动的模式表不是「随便什么尺寸都收」。它把
C:\VirtualDisplayDriver\vdd_settings.xml 里每一行 <resolution> 交叉每一档 <g_refresh_rate>
乘成一张平表——本机是 11 行 × 5 档 = 55 个模式。而程序的分辨率下拉是写死的 22 个尺寸,
其中 11 个驱动根本没有:

1600x720   1920x864   2000x1200  2280x1080  2340x1080  2412x1080
2700x1224  2712x1220  2800x1260  3040x1440  3120x1440

刷新率列表里的 75 / 165 / 240 同样不存在(驱动只有 30/60/90/120/144)。

而且失败是静默的,这一点比上面更糟:

  • retarget() 里遇到「驱动没有这个模式」的分支连一句警告都没有;
  • _set_mode_retry() 会为一个注定失败的模式重试 4 次,每次 sleep(1.0) + sleep(0.5),白等约 9 秒;
  • 最后还是照样调 _recover_capture("显示模式已更改") 把 ffmpeg 重启一遍 —— 画面闪一下、
    回来还是原来大小。所以看起来「做了点什么」,其实什么都没做。

修法四条:

  1. host/vdd.py 新增 declared_modes():直接读驱动的设置文件,返回它会建出来的
    (尺寸列表, 刷新率列表)。
  2. host/app.py 新增 _load_resolutions():分辨率下拉和刷新率下拉都由这张表生成,
    读不到驱动才回落到原来那份静态列表(那份现在只当兜底,注释里写明白了)。
    一个存着的、驱动给不出的尺寸会被换成最接近的、并且说明原因。
  3. host/session.py 新增 _resolve_mode(),统一裁决一次请求:
    • 精确命中 → 原样用,无提示;
    • 同尺寸有别的刷新率 → 取最接近的那档,日志说明「3840x2160 没有 75 Hz,改用 60 Hz」;
    • 尺寸整个不在表里 → 报「驱动给不出 2280x1080(模式表里没有,也没能写进去)。现在能用的:…」,
      并标记为不可用。
  4. 不可用时各自做该做的事:retarget() 记一句「显示模式未更改」然后返回 False,
    不重启画面;apply_display()(点开始投屏那条路)退到 wd.best_mode() 找一个驱动
    真有的模式继续——开始投屏比精确尺寸重要。另外 _set_mode_retry() 碰到 DISP_CHANGE_BADMODE
    直接返回,不再白等那 9 秒。

顺带:驱动的模式表还有空位时(上限 MAX_TARGET_MODES = 90,本机用掉 55,还能再加 7 行),
缺的那个尺寸会被写进 vdd_settings.xml 并标注 auto="1",同时告诉你重装一次驱动(或重启)
之后才切得过去。表满时会回收我们自己加的行,绝不动驱动自带的行。

实测(tools\tmp\mode_live_test.py,真挂虚拟显示器走完整链路):

apply_display 2280x1080  -> 驱动给不出 2280x1080(模式表里没有,也没能写进去)。
                            现在能用的:1280x720、1600x900、1920x1080、1920x1200、2160x1080、…
                            改用驱动确实有的 2160x1080@60          -> SUCCESSFUL
retarget 2280x1080       -> 显示模式未更改                        -> 返回 False,采集没重启,尺寸不变
retarget 3200x2136@120   -> ChangeDisplaySettingsEx SUCCESSFUL     -> 返回 True,采集重启
retarget 同一个模式       -> 显示模式没变,无需切换
retarget 3840x2160@75    -> 3840x2160 没有 75 Hz,改用 60 Hz       -> SUCCESSFUL

② 还是有部分视频编码不可用 —— 菜单问的是「编译进去了吗」

症状:菜单里选 h264_qsv、av1_amf 这类,选完连不上,或者悄悄回退到别的编码器。

实测(capture.probe_encoder(),每个真编码一帧):

编译进这个 ffmpeg 的:av1_amf  av1_nvenc  av1_qsv  h264_amf  h264_nvenc
                      h264_qsv  hevc_amf  hevc_nvenc  hevc_qsv  libx264  libx265      (11 个)

真跑一帧活下来的:    h264_nvenc  hevc_nvenc  av1_nvenc  h264_amf  hevc_amf
                      libx264  libx265                                                  (7 个)

死掉的:              h264_qsv  hevc_qsv  av1_qsv   -> Error creating a MFX session: -9
                      av1_amf                          (Radeon 610M 核显没有 AV1 编码单元)
                      libsvtav1                        (这个构建里根本没编译)

根因:app._load_encoders() 用的是 list_hardware_encoders() —— 回答的是
「这个 ffmpeg 构建里编译进了什么」;而 1.6.0 新加的 available_encoders() 回答的才是
「这台机器上跑得起来什么」。后者加了,前者没换掉,于是菜单照样列 11 个。
host/capture.py 那段 docstring 早就写着这两问答案不同、GUI 不该用前者 —— 写对了,就是没接上。

修法:菜单分两遍填。第一遍仍然是秒填(编译列表,保证窗口一开就有东西可选);
第二遍在后台逐个真编码一帧,回来后重建菜单,只留能跑起来的那几个,
被去掉的会写进日志:「本机跑不动这些编码器,已从列表里去掉: h264_qsv, hevc_qsv, av1_qsv, av1_amf」。
如果当前选中的那个恰好被去掉了,会自动换回一个能用的。


③ 顺手发现的:两个测试其实一直没通过

跑全量回归时才发现 tools/tmp 下有 31 个 *_test.py(不是一直报的 22 个),而且里面有两个红的。

tools\tmp\update_test.py 死在 KeyError: 'cleanup':host\update.py 的 WAIT_CMD 在 1.6.0
加了 {cleanup} 占位符(就是需求里那条「安装完成后自己删除安装包」),测试没跟着改。
也就是说:这个测试从 1.6.0 起每一次都是失败的,而之前一直报的「22 个脚本全绿」
根本没把它算进去。已经修好,并且补了两条本来就该有的断言:
删安装包必须排在 start "" /wait 之后(提前删正是「更新到一半」的成因),自己的 del "%~f0" 必须排最后。

tools\tmp\drag_test.py 则是从一开始就不可能通过。它把手机上的设置按钮拖 13 px,
再断言按钮的位置变化超过 0.1。可是 HudView 里有一条故意的防抖:

if (Math.abs(e.getX() - gearCx) + Math.abs(e.getY() - gearCy) > dp(9)) {
    moved = true;   // 手指走满 9dp 才算「拖动」
}

9dp 在这台机器(密度 352)上是 19.8 px,所以 13 px 的 swipe 会被当成点击,
而点击的功能是打开设置面板——按钮当然不动。就算侥幸算成拖动也没用:13/3068 ≈ 0.004,
断言要的是 > 0.1。实测对比,同一个按钮:

swipe 拖后位置(分数) 位移
13 px (0.14596808, 0.74855125) 0.0000
700 px (0.37389198, 0.74855125) 0.2279(预期 0.2282)

还有第三条、也是最隐蔽的一条:读位置用的正则是 gear=([-\d.]+),([-\d.]+),可 Java 打印小浮点数
会用科学计数法
——按钮停在顶边时日志里写的是 gear=0.99984306,2.8117592E-4(真实值是 0.9998
和 0.00028),那个正则只吃到 2.8117592,于是算出「按住屏幕下方 3565 px 的地方」,swipe 自然什么
都没碰到。改成允许指数、把结果夹进 [0, 1],并且按按钮所在的角落选拖动方向(原来写死往右上拖,
而按钮本来就在右上角,等于没得拖),就稳定了。

产品是对的,测试是错的。 已经改成:屏幕尺寸从截图的 PNG 头里读、边距按 wm density 自己算、
拖 700 px 且 x/y 两个方向分别断言,跑完把按钮拖回原处。


升级建议

  • 已经中招过的机器(虚拟屏开机自己回来):升级后进工具菜单点一次「清理残留的虚拟屏」。
  • 多编码:显卡不支持的编码器现在会从下拉里消失,而不是给你一个黑屏。
    如果之前手动选过 *_qsv 或 av1_amf,会自动回退到可用项并在日志里说明。
  • 触摸:死区、长按右键、双击、垂直滚动都在这一版修好了。
    如果你的手机没响应垂直滚动,这一版也修好了。
  • 设置文件:如果你用记事本改过 settings.json,之前的版本会静默丢弃整个文件;
    这一版不再有这个问题,但之前丢掉的设置需要重新设一次。
  • 升级不会丢目录:从 1.6.2 起,程序内更新与安装程序都会装回原来的目录。
  • 分辨率菜单变了:如果你之前手动选过一个驱动给不出的尺寸(比如 2280x1080),
    升级后菜单里看不到它了——那是故意的,因为它本来就切不过去。想要那个尺寸就选「自定义」填宽高,
    程序会把它写进驱动的模式表,按提示重装一次驱动(或重启)之后就能用。
  • 编码器菜单也变了:下拉里只剩这台机器真跑得起来的。之前存的选择如果是跑不动的,
    会自动换回一个能用的并在日志里说明。

验证

tools/tmp/ 下 33 个回归脚本全绿、非零退出 0,其中与这四版直接相关的:

encoder_matrix.py       7 个可用编码器全部 PASS(真跑一帧再拆包验证)
touch_test.py           25/25  坐标四角映射、自动解卡、release 语义
touch_batch_test.py     23/23  位置合并只留最新、事件一个不丢、跨接收边界重组
double_click_test.py    27/27  缝合规则与「不许乱缝」的每一条
retarget_test.py        32/32  运行中改分辨率、幂等、重试到第 4 次
install_dir_test.py     23/23  安装程序复用旧目录、/DIR= 与批处理顺序
privacy_layout_test.py  23/23  真 Tk 窗口,按钮在正文里且压到 300 px 仍滚得到
privacy_test.py         32/32  条款文本与版本号
settings_bom_test.py    23/23  带 BOM 的设置文件不再让设置全部丢失
notice_test.py          37/37  含「旧协议下 dy 恒为 0」的证明
db_roundtrip_test.py     4/4   挂屏 → 摘屏往返后 Recent 回到干净值
orphan_cleanup_test.py          摘屏后没有残留的虚拟屏
log_toc_test.py         19/19  两份日志的目录锚点、表格、代码块
mode_menu_test.py       76/76  真 Tk 窗口里两个菜单真的按驱动表 / 真探测结果生成
mode_live_test.py       19/19  真挂虚拟屏,走真的 ChangeDisplaySettingsEx
frozen_check_test.py           解开内嵌 PYZ,查新代码真的被打进 exe 了
drag_test.py                   真手机:把设置按钮拖走再重启,看它有没有留在原地

真机验证(小米 24091RPADC,Android 16,2136×3200):

点击        ['DOWN','UP']
长按半秒    ['DOWN','UP','RCLICK']      (长按在按下后 503 ms 触发)
拖动        ['DOWN'] + 27 个 MOVE + ['UP']   (间隔约 22 ms)
抖动点击    ['DOWN','UP']                (一个 MOVE 都没有 —— 死区生效)
连点两下    ['DOWN','UP','DOWN','UP']    (两次按下相隔 125.6 ms,抬起落在按下点)

整条链路端到端(真机 + 真虚拟屏 + 真的注入到 Windows):

capture backend: ddagrab 1920x1080@60
tap  手机(1600,700)  → Windows 光标 (3519,353)
swipe (320,900)→(1500,400)  → 20 条事件,光标 (3469,202)
logcat: touch action=0 at 1898.0,700.0 / action=1 at 1898.0,700.0   ← 抬手落在按下点
        touch action=0 at 618.0,900.0  / action=1 at 1798.0,400.0   ← 拖动跟手指

另外用打包好的 DroidExtend.exe 做了真实冒烟测试(窗口标题正确、日志正常落盘),
并直接读 exe 里的归档确认 20 个自有模块真的被冻了进去 —— 「构建成功」和「东西在里面」是两件事。

校验

文件 大小 SHA256
DroidExtend-Setup-1.6.3.exe 36559066 bytes 860A356FBC2460B17506BA2E803B8861B6F7D0148B306616B5569D110C37134A
DroidExtend.apk 210774 bytes D9CDE6FEC2B9F4F3DDA4FFFD0CD8F4D49AC1DFAD15FB26B68979722960533231

下载的每一步都要过五道检查(官方域名、官方校验和、字节数、SHA256、文件头是 MZ),
任何一条不过就不装,并当场告诉你哪一条没过。

这个项目没有代码签名证书,安装包始终是「未签名」。程序不会假装它是签过名的:
确认框里直接写明完整性靠校验和保证、不靠签名。校验和能挡什么(下载被篡改、中间人换包、
代理返回半截文件)和挡不住什么(仓库本身被控制)都写在 README 的常见问题里。

已知限制

  • 没有声音,只投画面。
  • 只在 Windows 11 + NVIDIA(NVENC) 上实测过。代码里有 h264_amf / libx264 兜底,
    编码器检测也是真跑一帧,但没在 AMD / Intel 机器上验证过。
  • 必须装内核级虚拟显示驱动(会弹一次 UAC)—— 这是"真扩展屏"绕不开的前提。
  • APK 用自动生成的调试密钥签名,不适合上架应用商店。

完整过程(每个坑的根因、实测数据、以及写错的每一次)记在仓库里:

这个项目的全部代码、文档和提交信息,都是 AI 编码代理(DeepSeek Harness / deepseek-flash)写的。
人类负责提需求、插上真机、在需要管理员权限时点「是」,以及验收。