Skip to content

Fix virtual output - #1380

Open
Groveer wants to merge 4 commits into
linuxdeepin:masterfrom
Groveer:fix_virtual_output
Open

Fix virtual output#1380
Groveer wants to merge 4 commits into
linuxdeepin:masterfrom
Groveer:fix_virtual_output

Conversation

@Groveer

@Groveer Groveer commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fix virtual output(虚拟输出 / 复制模式修复)

概述

本 PR 解决两个层面的问题:

  1. layer surface 生命周期缺陷:复制/镜像模式下直接禁用 source 输出(如 HDMI-A-1 镜像 eDP-1 后禁用 eDP-1)时,复制模式折叠会销毁重建同一物理输出的 Output 包装对象(rewrap),旧实现把绑定在镜像输出上的 layer-shell 表面(如 dde-desktop 桌面)当作热插拔关闭,导致桌面窗口被销毁后回退为普通窗口。
  2. 热插拔/复制模式鲁棒性缺陷:禁用/启用显示器、热插拔、复制模式下批处理输出时,窗口和光标乱跳、窗口搁浅在被禁用的屏幕上、过期的存储配置恢复出残缺的复制组。

共 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)

  • handleCopyModeSourceDisabled(替代旧 handleCopyModeOutputDisable):不再无条件塌缩成扩展模式。先统计存活镜像数(排除自身、同批禁用、已禁用),少于 2 个才返回 false 走塌缩;否则提升新复制源并重定向其余镜像,新组 id 记入 pending 配置,提交成功后才持久化。禁用单个镜像不等于复制模式损坏(重启用时通过有效代理自动恢复镜像),只有禁用 source 才必须重选源。
  • promoteCopyOutputToSource:从 onOutputRemoved 抽出的共用逻辑——把选中镜像换装为新 source、设为主屏、迁移源上搁浅的窗口、重建其余镜像。消除两套相似代码,镜像提升/源拔出行为统一。
  • convertCopyOutputsToNormal(skip) / convertCopyModeToExtension(preservedOutput):把非 source 的复制输出全部转回独立输出,skip 谓词约束哪些不参与(被禁用的源保留为 disabled normal 输出);混合拓扑(镜像+独立屏)塌成扩展模式。
  • onOutputTestOrApply 复制模式分支:仅当拓扑变化(enabled 翻转)才处理:禁用 source 走提升路径;启用独立屏(source 被启用但非 primary)塌成扩展;纯刷新率/模式切换不视为拓扑变化、保持复制模式。旧代码无条件把复制输出全转普通输出,一次刷新切换就拆散镜像组。

④ 提交回调健壮化

抽出公共 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(),把包装交换误当物理摘除。

修复

  • detachOutputSurfaces(Output)*:三步摘下而不关闭——① container->removeSurface(清容器归属、setOutputs({}),保留 QObject parent);② unparentSurface(卸掉 QObject parent 与 parentItem,防止旧容器销毁时连带销毁表面);③ doAddSurface(surface, false) 无容器挂回该 layer 自身 model,供新包装 addOutput 时经 updateSurfacesContainer 自动 re-enter。remove/re-add 在同一事件循环内完成、中间不跑事件循环,监听者只见瞬态,永不触发 closed。
  • detachLayerSurfaces(Output)*:一个物理输出在 4 个层级各有一个 per-output 容器,遍历全部摘除。
  • unparentSurface(SurfaceWrapper)*:一行 surface->setParent(nullptr),通过 SurfaceContainer 的 friend 权限调用 SurfaceWrapper 的 private setParent 重载,同时清 QObject parent 与 QQuickItem parentItem,是配对解挂的正确入口。
  • createNormalOutput / createCopyOutput 工厂内聚:两个工厂开头对旧包装自动 detachLayerSurfaces,8 处 rewrap 调用点(进入/退出复制模式、恢复、禁用源折叠、热插拔源失败等)全部自动安全,新增 rewrap 点无遗漏风险。
  • removeOutput() 重排序:primary 切换移到 layer surface 搬迁之前。removeOutput 会把无输出绑定的全局 layer surface 搬到新主屏;若切换滞后,primaryOutput 仍指向正被删除的容器,全局表面会被发进待销毁容器导致崩溃。
  • addSurfaceToContainer 守卫:容器查找为空时由断言崩溃改为告警 + closed()。唯一剩余输出刚被移除、primary 仍指向它时容器必然为 null,优雅关闭优于崩溃。

净效果: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 验证器,为复制模式/主屏/启停/分辨率/色彩管理提供可交互复现手段。

  • wlroutputmanager.{h,cpp}(新增):基于 zwlr_output_manager_v1 的封装。ModeTracker/HeadTracker 按「声明-完成」模式收集 mode/head 数据(current_mode 用下标记录,事件到达顺序不保证,done 后再映射);ConfigWatcher 把 succeeded/failed/cancelled 统一为结果信号(deleteLater 先发信号再释放,防止在回调里销毁自己)。applyConfig 必须对每个 head 重述状态——wlr 配置对象只改一个 head 会让其他 head 被隐式关闭,这是协议最容易踩的坑。done(serial) 触发 headsChanged,真实热插拔时 UI 自动刷新。
  • virtualoutputmanager:客户端 VERSION 提到 3 以接收 virtual_output_modified 事件;virtual_output_list 空数组容错(空列表是合法状态,必须发出空信号清残留映射)。
  • virtualclient(重构):三张映射(组名→VirtualOutput、组名→成员屏、屏名→组名)替代旧「单当前虚拟输出」模型,支持多复制组与成员逆查。getWlOutput 每次现查——镜像切换会替换 wl_output 代理。updateMirrorGroup 创建/销毁 copyscreen_ 组,wl_array 顺序强制 [被镜像屏, 当前屏] 以符合服务端复制语义。refreshScreenInfo 是所有事件汇集的唯一刷新点,blockSignals + 恢复选中防止控件信号级联回灌。分辨率/刷新率联动保留精确 mHz 值,避免 double→int 精度丢失导致 set_mode 匹配失败。亮度/色温经 treeland_color_control_v1(set + commit 模型、绑定在 wl_output 上),代理更换后销毁重建重绑。组列表对账以服务端为权威清理本地幽灵组。

提交 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 通知双侧收敛,两侧状态一致。


测试

  • tests/protocols/treeland-virtual-output-desktop-v1:layer surface 贯穿 4 次包装交换,closed 计数保持 0(本地 PASS)。
  • treeland-virtual-output-manager-v1:协议套件验证重名组拒绝、消失组空事件、modified 通知。
  • examples/test_virtual_output:GUI 工具端到端驱动复制组创建/销毁、输出启停、主屏切换、分辨率与色彩控制。

PMS

TASK-390751

@deepin-ci-robot

Copy link
Copy Markdown

[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.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai

sourcery-ai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

The 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 restoration

sequenceDiagram
    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
Loading

Sequence diagram for virtual-output updates and protocol notifications

sequenceDiagram
    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)
Loading

Flow diagram for safe output layout restoration

flowchart 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]
Loading

File-Level Changes

Change Details Files
Rebuild the virtual-output example into an interactive display-management client.
  • Add Treeland color-control and primary-output bindings.
  • Add wlroots output-management bindings for head discovery and configuration.
  • Replace command-line cloning with a Qt UI for screen selection, mirroring, enablement, geometry, mode, refresh, rotation, scale, brightness, color temperature, and primary-output control.
  • Track virtual-output groups and refresh state when the compositor reports modifications.
examples/test_virtual_output/CMakeLists.txt
examples/test_virtual_output/main.cpp
examples/test_virtual_output/virtualclient.cpp
examples/test_virtual_output/virtualclient.h
examples/test_virtual_output/wlroutputmanager.cpp
examples/test_virtual_output/wlroutputmanager.h
Extend the virtual-output protocol implementation to support lifecycle and modification notifications.
  • Upgrade the manager interface to version 3 and handle empty output lists safely.
  • Emit modification events when virtual-output groups are created, updated, or removed.
  • Implement manager-resource destruction and safely handle groups that disappear between listing and lookup.
  • Rebind newly requested virtual-output resources to current server-side configurations.
examples/test_virtual_output/virtualoutputmanager.cpp
examples/test_virtual_output/virtualoutputmanager.h
src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp
src/modules/virtual-output/virtualoutputmanagerinterfacev1.h
Make output layout and cursor behavior resilient to stale or changing output topology.
  • Pin automatically positioned outputs after placement to prevent wlroots reflow.
  • Avoid restoring saved positions that overlap enabled outputs and de-overlap extension-mode states.
  • Move the cursor to the closest remaining layout point when it leaves all outputs.
  • Ignore disabled outputs during cursor hit testing.
src/core/rootsurfacecontainer.cpp
src/core/rootsurfacecontainer.h
src/seat/helper.cpp
waylib/src/server/kernel/woutputlayout.cpp
waylib/src/server/kernel/woutputlayout.h
Improve copy-mode transitions, output disable/enable handling, and persistence.
  • Promote surviving mirrors when a copy source is disabled, or collapse copy mode when insufficient mirrors remain.
  • Convert mirror outputs to normal outputs for extension-mode transitions and mixed topologies.
  • Persist rebuilt copy groups only after all output commits succeed.
  • Restore disabled-output surfaces and clear snapshots on re-enable.
  • Persist primary-output restoration and reject incomplete stale copy configurations.
src/output/outputmanager.cpp
src/seat/helper.cpp
src/seat/helper.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@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.

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>

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

Comment thread src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp
Comment thread src/modules/virtual-output/virtualoutputmanagerinterfacev1.cpp
@Groveer
Groveer marked this pull request as draft September 8, 2026 09:40
@Groveer Groveer closed this Sep 10, 2026
@Groveer
Groveer deleted the fix_virtual_output branch September 10, 2026 02:19
@Groveer
Groveer restored the fix_virtual_output branch September 10, 2026 02:19
@Groveer Groveer reopened this Sep 10, 2026
@Groveer
Groveer force-pushed the fix_virtual_output branch 2 times, most recently from f5fe682 to 23f3a56 Compare September 10, 2026 02:38
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>
@Groveer
Groveer marked this pull request as ready for review September 10, 2026 05:08

@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.

Sorry @Groveer, your pull request is larger than the review limit of 150,000 diff characters

@deepin-bot

deepin-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

TAG Bot

New tag: 0.10.0
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1392

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