Fix virtual output - #1380
Conversation
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: Groveer The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Reviewer's GuideThe pull request turns the virtual-output example into a full interactive output-management client and expands the virtual-output protocol, while hardening compositor output topology transitions, layout positioning, cursor placement, primary-output persistence, and surface restoration across disable/enable and copy-mode changes. Sequence diagram for copy-source disable and restorationsequenceDiagram
participant Client as OutputManagementClient
participant Helper
participant RootSurfaceContainer
participant OutputManager
participant Wlroots
Client->>Helper: onOutputTestOrApply(config)
Helper->>Helper: handleCopyModeSourceDisabled(disabledSource, requestedDisabled)
alt surviving mirrors remain
Helper->>Helper: promoteCopyOutputToSource(promotedMirror, surfaces, surfacesFrom, shouldRetarget)
Helper->>RootSurfaceContainer: setPrimaryOutput(normalSource)
Helper->>Helper: m_pendingOutputConfig.copyOutputIds = copyOutputIds
else no mirror remains
Helper->>Helper: convertCopyModeToExtension(disabledSource)
end
Helper->>Wlroots: commit output states
Wlroots-->>Helper: commit callback
Helper->>OutputManager: onOutputCommitFinished(config, success)
alt successful re-enable in extension mode
Helper->>Helper: surface->setPosition(savedPosition)
Helper->>RootSurfaceContainer: updateSurfaceOutputs(surface)
end
Sequence diagram for virtual-output updates and protocol notificationssequenceDiagram
participant Client as WaylandClient
participant Manager as VirtualOutputManagerInterfaceV1
participant Binding as VirtualOutputInterfaceV1
participant Config as VirtualOutputConfig
Client->>Manager: create_virtual_output(resource, id, name, outputs)
Manager->>Config: storeVirtualOutput(name, outputs, virtualOutput)
Manager->>Manager: notifyVirtualOutputModified(name)
Manager-->>Client: send_virtual_output_modified(resource, name)
Client->>Manager: get_virtual_output(resource, name, id)
Manager->>Binding: createVirtualOutput(resource, id, name)
alt virtual output exists
Manager->>Config: backfill virtualOutput binding
Manager-->>Binding: sendOutputs(name, outputs)
else virtual output was removed
Manager-->>Binding: sendOutputs(name, empty outputs)
end
Manager->>Manager: updateVirtualOutput(name, outputs)
Manager->>Binding: sendOutputs(name, outputs)
Manager->>Manager: notifyVirtualOutputModified(name)
Client->>Manager: destroy(resource)
Manager->>Manager: wl_resource_destroy(resource)
Flow diagram for safe output layout restorationflowchart TD
A[Output added or layout restored] --> B[Compute effective saved size]
B --> C{Saved position overlaps enabled output?}
C -- Yes --> D[Keep auto-added position]
C -- No --> E["layout.move(output, savedPos)"]
D --> F["layout.pin(output)"]
E --> F
F --> G[Output remains stable during sibling geometry changes]
H[Output-management apply] --> I{Restored states overlap?}
I -- Yes --> J[Reject restored topology]
I -- No --> K[Apply restored states]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp" line_range="256-259" />
<code_context>
- const QByteArray arrSend = config.outputs.join('\0').toLatin1();
- virtualOutput->sendOutputs(name, arrSend);
+ if (it != m_configs.end()) {
+ // Backfill so updateVirtualOutput() pushes to this newest binding
+ // instead of a stale (possibly disconnected) proxy.
+ it.value().virtualOutput = virtualOutput;
+ virtualOutput->sendOutputs(name, it.value().outputs.join('\0').toLatin1());
+ } else {
+ virtualOutput->sendOutputs(name, QByteArray());
</code_context>
<issue_to_address>
**issue (bug_risk):** Rebinding `m_configs[name].virtualOutput` to the newest proxy does not make the destroy cleanup proxy-specific: when the older proxy is later destroyed, its `beforeDestroy` handler unconditionally removes `m_configs[name]`, including the configuration now bound to the newer proxy.
**Triggers:** When a client calls `get_virtual_output` for an existing group more than once and then destroys the older proxy.
**Suggested fix:** Capture the proxy in the cleanup callback and remove the config only when `m_configs[name].virtualOutput` still points to that proxy.
</issue_to_address>
### Comment 2
<location path="src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp" line_range="261" />
<code_context>
+ it.value().virtualOutput = virtualOutput;
+ virtualOutput->sendOutputs(name, it.value().outputs.join('\0').toLatin1());
+ } else {
+ virtualOutput->sendOutputs(name, QByteArray());
+ }
}
</code_context>
<issue_to_address>
**issue (bug_risk):** The vanished-group path sends an empty outputs array, but the example client's `VirtualOutput::treeland_virtual_output_v1_outputs` handler returns without emitting `outputsReceived` when the array is empty, so the client never observes the required empty outputs event.
**Triggers:** When a listed virtual-output group disappears before the client calls `get_virtual_output`.
**Suggested fix:** Emit `outputsReceived(name, {})` for an empty array instead of returning before the signal emission.
</issue_to_address>5762917 to
6f2935c
Compare
f5fe682 to
23f3a56
Compare
1. Clamp child resource version to the interface version on create 2. Reject duplicate group names with INVALID_GROUP_NAME protocol error 3. Return an empty proxy for vanished groups instead of a fatal error 4. Notify v3 clients via virtual_output_modified on config changes 5. Restore manager destroy() override; default leaves resource alive 6. Rebind config QPointer to the newest proxy in get_virtual_output; only drop a config still bound to the destroyed proxy on cleanup Log: Virtual output groups now notify clients on changes; getting a vanished group returns an empty proxy instead of killing the client; a config binding survives the destruction of an older proxy Influence: 1. Run ctest protocol suites treeland-virtual-output-desktop-v1 and treeland-virtual-output-manager-v1 2. Create two copy groups with the same name; the second create must fail with INVALID_GROUP_NAME and not corrupt the first group 3. Get a group that no longer exists; the client must survive and see an empty outputs event 4. Update copy outputs while test-virtual-output runs; the client receives virtual_output_modified without reconnecting 5. Bind a group twice via get_virtual_output and destroy the older proxy; the newer binding still receives configuration updates fix(virtual-output): 加固虚拟输出协议层 1. 创建子资源时将版本钳制到接口版本 2. 重名组返回 INVALID_GROUP_NAME 协议错误 3. 已消失的组返回空代理而非致命错误 4. 配置变化时通过 virtual_output_modified 通知 v3 客户端 5. 恢复 manager 的 destroy() 重写;默认实现不会销毁资源 6. get_virtual_output 将配置 QPointer 重新绑定到最新代理; 仅删除仍绑定在被销毁代理上的配置 Log: 虚拟输出组变化时通知客户端;获取已消失的组返回空代理而非断开客户端;配置绑定不受旧代理销毁影响 Influence: 1. 运行 ctest 协议测试 treeland-virtual-output-desktop-v1 与 manager-v1 2. 用同名创建两个复制组;第二次必须报 INVALID_GROUP_NAME 且不影响第一组 3. 获取不存在的组;客户端存活并收到空 outputs 事件 4. test-virtual-output 运行时更新复制输出;客户端无需重连即可收到通知 5. 对同一组两次 get_virtual_output 后销毁旧代理;新绑定仍收到配置更新 PMS: TASK-390751 Signed-off-by: groveer <guoyao@uniontech.com>
1. Add WOutputLayout::pin() to freeze settled auto-arranged outputs 2. Restore saved extension positions only when they do not overlap an enabled output; revalidate before expanding from one screen 3. Migrate surfaces stranded on a disabled output back when it is re-enabled 4. Warp the cursor to the closest layout point instead of the primary center when it leaves the output layout 5. Persist the auto-restored primary and copy group ids only after every commit of the configuration succeeds 6. Refuse copy mode restore when a configured id no longer resolves; guard onRestoreCopyOutput against an empty output list Log: Re-enabling a screen no longer strands windows or drags the cursor erratically; stale copy configs no longer restore a partial group Influence: 1. In extension mode, disable then re-enable the primary screen; windows return to it and the cursor does not jump 2. Switch the primary monitor resolution, re-enable; saved stale coordinates are abandoned for auto layout 3. Configure copy mode, unplug one member, reboot; copy mode must not restore and a warning is logged 4. Toggle outputs rapidly in a 3-screen layout; the cursor stays on the nearest enabled output fix(output): 热插拔恢复与复制模式鲁棒性处理 1. 新增 WOutputLayout::pin() 冻结已排定的自动布局输出 2. 扩展模式仅在不与已启用输出重叠时恢复保存位置;单屏扩展前重新校验 3. 禁用的输出重新启用时迁移其上搁浅的窗口 4. 光标离开布局时移动到布局最近点而非主屏中心 5. 仅在该配置全部提交成功后持久化自动恢复的主屏与复制组 ID 6. 配置 ID 失效时拒绝恢复复制模式;onRestoreCopyOutput 增加空列表守卫 Log: 重新启用屏幕不再搁浅窗口或导致光标乱跳;过期的复制配置不再恢复出缺员组 Influence: 1. 扩展模式下禁用再启用主屏;窗口返回原屏且光标不跳动 2. 更换主屏分辨率后再启用;过期的保存坐标被放弃改用自动布局 3. 配置复制模式后拔掉一个成员重启;复制模式不得恢复并输出警告日志 4. 三屏布局下快速开关输出;光标停留在最近的已启用输出上 PMS: TASK-390751 Signed-off-by: groveer <guoyao@uniontech.com>
1. Add wlr-output-management client wrapper for real output discovery 2. Rework VirtualClient to drive copy mode end to end via treeland virtual output protocol and wlr output management events 3. Drop dead connects and redundant comments Log: Example client discovers real outputs via the wlr protocol and drives virtual copy groups end to end Influence: 1. Build the examples target; test-virtual-output compiles and links 2. Create and destroy a copy group from the client; the compositor applies and restores the output topology 3. Toggle real monitor outputs; the client list refreshes through wlr-output-management events feat(test-virtual-output): 扩展示例客户端 1. 新增 wlr-output-management 客户端封装以发现真实输出 2. 重构 VirtualClient,通过 treeland 虚拟输出协议与 wlr 输出管理事件端到端驱动复制模式 3. 清理无效连接与冗余注释 Log: 示例客户端可通过 wlr 协议发现真实输出,并端到端驱动虚拟复制组 Influence: 1. 构建 examples 目标;test-virtual-output 可编译链接 2. 从客户端创建/销毁复制组;合成器正确应用并恢复输出拓扑 3. 切换真实显示器输出;客户端列表通过 wlr-output-management 事件刷新 PMS: TASK-390751 Signed-off-by: groveer <guoyao@uniontech.com>
1. Always emit outputsReceived in the example client, even for an empty outputs array, so a vanished group is observable by the client Log: A vanished group is observable via an empty outputs event Influence: 1. Get a group that no longer exists; the example client emits outputsReceived with an empty output list and clears the group 2. Run ctest protocol suites treeland-virtual-output-desktop-v1 and treeland-virtual-output-manager-v1 fix(virtual-output): 组消失时发出空 outputs 事件 1. 示例客户端对空 outputs 数组也发出 outputsReceived,使已消失的组 可被客户端观察 Log: 组消失通过空 outputs 事件可观察 Influence: 1. 获取已消失的组;示例客户端收到空 outputs 列表并清理该组映射 2. 运行 ctest 协议测试 treeland-virtual-output-desktop-v1 与 manager-v1 PMS: TASK-390751 Signed-off-by: groveer <guoyao@uniontech.com>
23f3a56 to
37c6781
Compare
There was a problem hiding this comment.
Sorry @Groveer, your pull request is larger than the review limit of 150,000 diff characters
|
TAG Bot New tag: 0.10.0 |
Fix virtual output(虚拟输出 / 复制模式修复)
概述
本 PR 解决两个层面的问题:
共 4 笔提交,依赖顺序:协议层 v3 通知(提交 1)→ 服务端复制模式/热插拔行为(提交 2)→ 交互验证工具(提交 3)→ 客户端空事件语义(提交 4)。
提交 1:fix(virtual-output): harden protocol layer
把 treeland_virtual_output_manager_v1 从「v1 一次性创建」升级为「v3 动态通知 + 完整生命周期」。
InterfaceVersion 1 → 3
Wayland 全局对象只能绑定到 min(全局版本, 客户端支持版本)。版本为 1 时,destroy 请求(since 2)与 virtual_output_modified 事件(since 3)在协议层不可达。升级版本以开通 v2/v3 通道,是所有通知功能的前提。
子资源版本钳制(createVirtualOutputInternal)
子资源 new_id 的 version 必须 ≤ 父资源版本且 ≤ 子接口自身声明版本。manager 是 v3 而子接口 treeland_virtual_output_v1 只定义到 v1,直接传父版本会被 libwayland 判定为版本越界并发 implementation error。改为取两者最小值,子对象始终以合法最高版本创建。
attachDestroyCleanup 销毁路径通知
组被销毁(客户端 destroy 或断开)也是配置集合的变化。beforeDestroy 里移除配置后广播 virtual_output_modified,让其他 v3 客户端感知「组已消失」并刷新列表。
notifyVirtualOutputModified(新增)
遍历所有 manager 绑定,只对 version ≥ 3 的绑定发送 virtual_output_modified。生成的 sender 不做版本检查,给 v1/v2 绑定发 since-3 事件会让不认识该 opcode 的客户端直接协议错误断开,显式按版本过滤是唯一安全做法。
storeVirtualOutput 存储后通知
新建组 / 恢复配置后广播 modified,其他客户端及时重拉列表。
manager destroy() 重写恢复
协议 request destroy 进入生成的 handle_destroy,基类默认实现为空操作、不会销毁资源,只有 wl_resource_destroy 才触发 destroy_func 并从 resourceMap 摘除。外部协议包补上 since-2 的 destroy 请求后恢复该重写,客户端主动销毁时资源立即释放,避免挂起到断线才回收的泄漏与悬挂。
get_virtual_output:已消失组返回空代理
原来组不存在时用 wl_resource_post_error 返回致命协议错误(杀死整个客户端连接);「列表后组消失」是合法竞态(如列表下发后复制组被热插拔塌缩),不应惩罚客户端。改为仍创建子资源并发送空 outputs 事件,语义为「请重新查询」。
get_virtual_output:QPointer 重绑 + 销毁守卫
配置按设计不随创建者断开而消失,其 QPointer 若停在旧代理上,后续更新会推给空壳。get_virtual_output 每次把配置 QPointer 重绑到最新代理,保证「每次查询的最新绑定」接管后续推送。配套在 attachDestroyCleanup 的清理 lambda 中捕获代理指针,仅当配置仍绑定在被销毁的代理上时才移除(it != m_configs.end() && it.value().virtualOutput == virtualOutput),否则静默跳过——只有一个 name 一个活跃绑定槽位,无条件 remove 会把「代理销毁」误当「组被移除」,误删已重绑新代理的配置。注意必须用迭代器判断而非 contains()+value():QHash::value 对缺失键返回默认构造的配置(virtualOutput 为空),会与「绑定被拒/从未绑定」混为一谈。
updateVirtualOutput / removeVirtualOutput 追加通知
更新 outputs 并推送后广播 modified;remove 改为仅当移除成功(配置存在)才通知。helper 的 copyOutputConfigurationChanged 在这两处驱动复制模式的热更新与删除(hotplug 事件入口),UI 客户端无需重连即可刷新。
提交 2:fix(output): robust hotplug restore and copy mode handling
核心提交,两条功能线:热插拔/复制模式鲁棒性 + layer surface 生命周期修复。
① WOutputLayout::pin(waylib)
新增 pin(WOutput*):对仍处于 auto_configured 的输出,用 wlr_output_layout_add 以当前坐标重新添加,清除 auto 标记、坐标转为「手动冻结」。wlroots 对 auto 输出会在兄弟屏几何变化时触发 output_layout_reconfigure 整体重排,悄悄挪动已排定的位置;pin 后热插拔/改分辨率不再导致布局跳动。
② 位置恢复去重叠(helper.cpp 静态助手 + 4 个应用点)
effectiveOutputLayoutSize 按 transform 交换宽高、按 scale 折算模拟 wlroots 布局尺寸;enabledStatesOverlap 检测恢复拓扑中任意两个 enabled 输出在布局坐标下相交;deOverlapEnabledStates 在扩展模式提交前把重叠状态贪心平移到最右边缘之后(兜住客户端用 (0,0) 恢复屏幕);savedPositionOverlapsEnabledOutput 恢复单输出保存位置前与当前已启用输出比对(用 wlr_output_layout_get_box 取真实矩形)。
应用点:onOutputAdded(恢复保存拓扑)、onOutputTestOrApply 扩展模式恢复分支、onOutputTestOrApply 提交尾部、restoreExtensionModeFromConfig。统一策略:过期保存坐标与当前拓扑重叠即放弃恢复,避免叠屏塌缩与窗口/光标乱跳。
③ 复制模式转换(helper.cpp)
④ 提交回调健壮化
抽出公共 syncLayoutAndRefresh lambda(布局增删 + state-only commit 强制重渲染),两个分支共用防行为漂移。committedState 与 extraState 不一致时不再一律判失败,而是对照输出真实硬件状态逐项比对(enabled 方向、mode 尺寸/刷新率、scale、transform),匹配即按成功处理。复制模式塌缩会为幸存镜像并发重建新 Output,其初始化 commit 顶替已排定状态是正常现象而非失败;但仅比对方向会掩盖丢掉的刷新率/模式变更,导致持久化配置与硬件脱节。
⑤ 持久化时机
每个 apply 开始时清空 pending 的 copyOutputIds,防止上一轮 source-disable 记录的过期组在后续纯刷新开关成功时被重存。全部 commit 成功后:复制模式且有重建组才写入新组;非复制模式才清空旧复制配置。纯刷新开关期间保持复制模式不再误清持久化配置。copyModeRestoreConfig 守卫改为 id 数量与 name 数量必须相等(旧逻辑只看 <2,id 失效会静默恢复出残缺组),拒绝恢复并告警。restoreScreenAsPrimary 显式回写 dconfig(去掉内存值相同就 early-return 的路径),保证禁用/启用循环后主屏设定不丢失。
⑥ 搁浅窗口恢复
新增 m_disabledOutputSurfaces(输出 id → 表面列表 + 绝对坐标快照,QPointer 防止禁用期间窗口被关导致悬垂)。每次 enabled 翻转禁用都重新拍快照(上次失败提交的残留位置不可信)。提交成功路径:扩展模式下还原表面位置、重挂 ownsOutput 并刷新输出归属,任何模式都清理条目(长期禁用不泄漏);复制模式不还原(复制逻辑自持放置权)。getOutputAtCursor 跳过未启用的输出(禁用屏的 outputItem 保留旧几何,命中它会把交互/弹窗吸到死屏)。ensureCursorVisible 改从「warp 到主屏中心」为 wlr_output_layout_closest_point 全布局最近点,与 wlroots 自身的 layout-change 处理统一目标,消除光标在两个目标间来回跳。
⑦ layer surface 生命周期(核心)
机制:RootSurfaceContainer → 4 个按 layer 层级的 LayerSurfaceContainer → 每输出一个 OutputLayerSurfaceContainer。layer surface 通过 doAddSurface(surface, true) 把 QObject parent 与 QQuickItem parentItem 都挂到 per-output 容器;容器被 deleteLater 时 Qt 级联销毁所有子 QObject,未走 destroy() 的 SurfaceWrapper 析构会 invalidate 拆掉 shell surface——容器销毁即表面销毁。关闭链:WLayerSurface::closed(zwlr_layer_surface_v1.closed)→ ShellHandler → dde-desktop 收到 closed 后重建,作为输出绑定的层表面表现为回退普通窗口。
旧 bug 根因:复制模式折叠对同一物理 WOutput 删除再重建 Output 包装对象(rewrap),旧实现把「绑定过具体输出」的表面一律 closed(),把包装交换误当物理摘除。
修复:
净效果:copy↔normal 包装交换 → detach(不关闭)→ removeOutput(空转)→ addOutput(re-enter);只有真物理热拔插(onOutputRemoved)才关闭层表面,符合 layer-shell 规范。
⑧ 协议回归测试(tests/protocols/treeland-virtual-output-desktop-v1)
C 客户端跨 3 个协议(虚拟输出 + layer-shell + output-management)。layer surface 贯穿「进入复制 → 恢复 → 再进入 → 禁用 source 折叠」4 次包装交换,每步断言层表面存活、归属新包装、closed 事件计数保持 0;最终态断言 source 输出禁用、镜像输出转为普通输出并成为主屏。headless 后端不自产帧,测试通过强制渲染驱动 disable commit 完成。wlroots 对 NULL listener 直接 abort,每个协议回调均注册。
提交 3:feat(test-virtual-output): extend example client
把旧的命令行工具(./test-virtual-output HDMI-A-1 VGA-1)扩展为 GUI 验证器,为复制模式/主屏/启停/分辨率/色彩管理提供可交互复现手段。
提交 4:fix(virtual-output): emit empty outputs events for vanished groups
客户端 VirtualOutput::outputs 事件处理:删除对空 outputs 数组的提前 return,改为空数组也走解析流程并无条件发出 outputsReceived(输出列表保持空)。
为什么:服务端对「组已消失」的 get_virtual_output 返回合法的空 outputs 事件(提交 1 的 else 分支),这是协议语义中的「消失通知」而非异常。旧代码把空数组当「无事发生」直接丢弃,客户端观察不到组消失,镜像组映射残留。
解决:VirtualClient::onVirtualOutputOutputs 收到空事件后清空该组映射并刷新 UI;updateMirrorGroup 的移除分支不再基于脏数据操作;与服务端 virtual_output_modified 通知双侧收敛,两侧状态一致。
测试
PMS
TASK-390751