diff --git a/CMakeLists.txt b/CMakeLists.txt index ab06e7c..a6bc45d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ set(XML xml/treeland-wallpaper-shell-unstable-v1.xml xml/treeland-wine-window-management-unstable-v1.xml xml/treeland-wine-window-state-unstable-v1.xml + xml/treeland-cross-subsurface-unstable-v1.xml ) install(FILES ${XML} DESTINATION ${CMAKE_INSTALL_DATADIR}/treeland-protocols) diff --git a/docs/cross-subsurface-design.md b/docs/cross-subsurface-design.md new file mode 100644 index 0000000..1c1c75d --- /dev/null +++ b/docs/cross-subsurface-design.md @@ -0,0 +1,295 @@ +# treeland_cross_subsurface_unstable_v1 设计文档 + +## 1. 背景与问题 + +### 1.1 Wayland 的进程隔离限制 + +标准 `wl_subcompositor` 要求 parent 和 child 的 `wl_surface` 来自同一个 `wl_compositor`(即同一个客户端连接)。这意味着跨进程的 subsurface 关系在标准协议下是不可能的。 + +### 1.2 X11 的做法 + +X11 中 `XReparentWindow()` 是服务端操作,任何 X 客户端都可以把窗口 reparent 到另一个 X 窗口下,没有进程隔离限制。所有 Wine 进程共享同一个 `gdi_display`,跨进程父子窗口关系天然支持。 + +X11 驱动中 `attach_client_window()`(`winex11.drv/window.c:2309`)通过 `XReparentWindow()` 将 client window 嵌入 parent 的 `whole_window`,即使 parent 属于另一个进程也能正常工作。当跨进程 parent 查找失败时(`get_win_data(toplevel)` 返回 NULL),X11 只是跳过坐标偏移修正,client window 仍然正常存在和渲染。 + +### 1.3 Wine Wayland 后端的现状 + +Wine 的 `winewayland.drv` 中有两类 subsurface: + +1. **wayland_surface 的 wl_subsurface**(`wayland_surface.c:305`):WS_CHILD 窗口作为 toplevel 的 subsurface +2. **wayland_client_surface 的 wl_subsurface**(`wayland_surface.c:1179`):OpenGL/Vulkan 渲染区域作为 toplevel 的 subsurface + +两者都通过 `wl_subcompositor_get_subsurface()` 创建,必须在同一进程内。 + +`wayland_client_surface_attach()`(`wayland_surface.c:1150`)中,当 toplevel 属于另一个进程时: + +```c +if (!(toplevel_data = wayland_win_data_get_nolock(toplevel)) || !(surface = toplevel_data->wayland_surface)) +{ + wayland_client_surface_attach(client, NULL); // 直接 detach,渲染丢失 + return; +} +``` + +`wayland_win_data_get_nolock()` 只能查找当前进程的红黑树,跨进程查找返回 NULL,导致 client_surface 被 detach。 + +### 1.4 触发场景 + +实际触发跨进程失败需要同时满足三个条件: + +1. 窗口使用 OpenGL/Vulkan 渲染(有 `wayland_client_surface`) +2. 窗口 `!managed`(无 caption/thickframe/sysmenu/WS_EX_APPWINDOW) +3. `owner_hint`(来自 `GW_OWNER` 或 `NtUserWindowFromPoint`)指向另一个进程 + +这在真实应用中极为罕见,因为使用 GPU 渲染的窗口几乎总是有 caption 或 thickframe,会被判为 managed。但协议设计应覆盖这类场景以保证完整性。 + +### 1.5 place_above 的实际使用模式 + +Wine 中 `place_above` 只引用两种目标(`wayland_surface.c:611-694`): + +- **parent 的 `wl_surface`**(toplevel 窗口表面本身) +- **parent 的 `client_surface->wl_surface`**(OpenGL/Vulkan 渲染区域) + +并且只在 if/else 分支中使用,不需要引用任意中间 subsurface。 + +## 2. 设计目标 + +1. **通用性**:协议不耦合 Wine,任何 Wayland 客户端都可以使用 +2. **兼容性**:语义尽可能对齐 `wl_subcompositor`,减少客户端适配成本 +3. **统一 ID 空间**:parent、sibling(标准 + 跨进程)共用一个 uint32 命名空间 +4. **compositor 透明**:标准 `wl_subcompositor` 创建的 subsurface 也能被自动纳入 ID 管理 + +## 3. 协议设计 + +### 3.1 整体架构 + +``` +treeland_subsurface_manager_v1 (global) +├── export_surface(surface) → treeland_exported_surface_v1 +│ ├── surface_id 事件:分配 parent ID +│ ├── child_entered 事件:标准 wl_subsurface 进入时分配 sibling ID +│ └── child_left 事件:标准 wl_subsurface 离开时回收 ID +│ +└── create_remote_subsurface(surface, parent_id) → treeland_remote_subsurface_v1 + ├── subsurface_id 事件:分配跨进程 subsurface 的 sibling ID + ├── parent_geometry 事件:parent 几何信息(坐标换算用) + └── parent_destroyed 事件:parent 销毁通知 +``` + +### 3.2 ID 命名空间 + +所有 ID(surface_id 和 subsurface_id)在同一个 uint32 命名空间中: + +``` +值范围 | 含义 +[1, 0xFFFFFFFE] | compositor 分配的有效 ID +0 | 保留(无效值,触发 bad_sibling 错误) +0xFFFFFFFF | 保留(sibling_ref.top,放到 sibling 栈顶) +``` + +ID 来源有三个: + +| 来源 | 接口 | 分配时机 | +|------|------|---------| +| exported surface | `treeland_exported_surface_v1.surface_id` | export_surface 后立即 | +| standard subsurface | `treeland_exported_surface_v1.child_entered` | wl_subcompositor.get_subsurface 时自动 | +| remote subsurface | `treeland_remote_subsurface_v1.subsurface_id` | create_remote_subsurface 后立即 | + +compositor 内部维护一个全局 ID 分配器,保证三个来源不重复。 + +### 3.3 核心机制:标准 subsurface 的自动发现 + +compositor 在以下时机自动追踪标准 `wl_subcompositor` subsurface: + +1. 监听 `wl_subcompositor.get_subsurface(surface, parent)` 请求 +2. 如果 parent 是一个已 export 的 surface,为其 child 分配 subsurface_id +3. 通过 `child_entered` 事件通知 export owner +4. 通过 `child_left` 事件通知 export owner(wl_subsurface 销毁时) + +这个机制不需要改动标准 `wl_subcompositor` 协议,完全在 compositor 侧实现。 + +### 3.4 与 wl_subcompositor 的对应关系 + +| wl_subcompositor | treeland_cross_subsurface | 变化 | +|-----------------|--------------------------|------| +| `get_subsurface(id, surface, parent)` | `create_remote_subsurface(id, surface, parent_id)` | parent: wl_surface → uint32 | +| `set_position(x, y)` | `set_position(x, y)` | 不变 | +| `place_above(sibling: wl_surface)` | `place_above(sibling_ref: uint32)` | sibling: wl_surface → uint32 | +| `place_below(sibling: wl_surface)` | `place_below(sibling_ref: uint32)` | 同上 | +| `set_sync()` | `set_sync()` | 不变 | +| `set_desync()` | `set_desync()` | 不变 | +| (无) | `subsurface_id` 事件 | 新增:跨进程标识 | +| (无) | `parent_geometry` 事件 | 新增:坐标换算依据 | +| (无) | `parent_destroyed` 事件 | 新增:生命周期通知 | +| (无) | `export_surface` + `surface_id` | 新增:parent export 机制 | +| (无) | `child_entered` / `child_left` | 新增:标准 subsurface 自动发现 | + +### 3.5 place_above / place_below 的 sibling_ref 语义 + +```xml + +parent_id +sibling_subsurface_id +0xFFFFFFFF +0 +``` + +示例:`place_above(sibling_ref = parent_id)` 等价于标准协议中的 +`place_above(parent_wl_surface)`。 + +### 3.6 parent_geometry 事件 + +跨进程后,子进程没有 parent 的 `wl_surface` proxy,无法直接获取 parent 的 +buffer_scale 和 buffer_transform。`parent_geometry` 事件提供这些信息: + +- `x, y`:parent 在 compositor 全局逻辑坐标系中的原点位置 +- `scale`:parent 的 `wl_surface.buffer_scale` +- `transform`:parent 的 `wl_surface.buffer_transform`(遵循 `wl_output.transform` 语义) + +子进程使用这些值将自身逻辑坐标转换为 parent 的 surface-local 坐标后传给 `set_position()`。 + +## 4. Compositor 侧实现要点 + +### 4.1 ID 管理 + +```cpp +class SubsurfaceIdAllocator { + uint32_t next_id = 1; + + uint32_t allocate() { + // 跳过 0 和 0xFFFFFFFF + uint32_t id = next_id++; + if (id == 0) id = next_id++; + if (id == 0xFFFFFFFF) id = next_id++; + return id; + } +}; +``` + +所有 ID(exported surface、standard subsurface、remote subsurface)通过同一个分配器管理。 + +### 4.2 标准 subsurface 监听 + +compositor 在处理 `wl_subcompositor.get_subsurface` 时: + +```cpp +void on_subcompositor_get_subsurface(wl_subcompositor *subcompositor, + wl_surface *surface, + wl_surface *parent) +{ + // 检查 parent 是否是已 export 的 surface + auto export = find_export(parent); + if (!export) return; // 标准 subsurface,不干预 + + // 分配 subsurface_id,通知 export owner + uint32_t id = allocator.allocate(); + export->send_child_entered(id, surface); + track_standard_subsurface(surface, export, id); +} +``` + +### 4.3 place_above / place_below 处理 + +```cpp +void on_place_above(RemoteSubsurface *self, uint32_t sibling_ref) +{ + Surface *sibling; + + if (sibling_ref == 0) { + // 错误:无效值 + self->send_bad_sibling(); + return; + } + + if (sibling_ref == 0xFFFFFFFF) { + // 栈顶 + restack_to_top(self); + return; + } + + // 在 parent 的所有 subsurface(标准 + 跨进程)中查找 + sibling = find_surface_by_id(self->parent, sibling_ref); + if (!sibling || !is_sibling_of(self, sibling)) { + self->send_bad_sibling(); + return; + } + + restack_above(self, sibling); +} +``` + +### 4.4 安全策略 + +compositor 应通过以下机制限制访问: + +1. **绑定控制**:只允许受信任的客户端绑定 `treeland_subsurface_manager_v1`(通过 app-id 或 PID 白名单) +2. **parent_id 验证**:`create_remote_subsurface` 时验证 parent_id 对应的 export 属于受信任的客户端 +3. **进程隔离**:child_entered 事件中的 `wl_surface` 参数只在 export owner 的连接中有效,其他客户端无法通过这个 proxy 操作 surface + +## 5. Wine 集成方案 + +### 5.1 与现有 wine_window_management 的关系 + +`treeland_wine_window_management_v1` 的 `window_id` 和本协议的 `surface_id` +是两个独立系统,各自有独立的命名空间。Wine driver 需要在 wineserver 共享内存 +中同时维护两套映射: + +``` +wineserver 共享内存布局: +{ + HWND -> { + window_id, // 来自 wine_window_management(z-order 用) + surface_id, // 来自 cross_subsurface(subsurface parent/sibling 用) + } +} +``` + +### 5.2 集成流程 + +``` +进程 A (parent): + 1. wine_window_management.get_window_control(toplevel) + → window_id = 42 + 2. cross_subsurface.export_surface(toplevel) + → surface_id = 100 + 3. wl_subcompositor.get_subsurface(client_surface, toplevel) + → compositor 自动触发 child_entered(subsurface_id=200, client_surface) + 4. wineserver 共享: {toplevel: surface_id=100, client_surface: subsurface_id=200} + +进程 B (child): + 1. wineserver 查表 → parent surface_id = 100, sibling client_surface subsurface_id = 200 + 2. cross_subsurface.create_remote_subsurface(my_surface, parent_id=100) + → subsurface_id = 300 + 3. set_desync() + 4. set_position(x, y) // 用 parent_geometry 事件算坐标 + 5. place_above(sibling_ref=200) // above parent 的 OpenGL 渲染区 + 6. place_above(sibling_ref=100) // above parent 自身(window frame) + 7. place_above(0xFFFFFFFF) // 栈顶 +``` + +### 5.3 修改点(winewayland.drv) + +需要修改的函数: + +| 函数 | 文件:行 | 当前行为 | 修改后 | +|------|--------|---------|--------| +| `wayland_client_surface_attach()` | wayland_surface.c:1169 | 找不到 toplevel 时 detach | 尝试通过 surface_id 创建 remote_subsurface | +| `wayland_surface_make_subsurface()` | wayland_surface.c:294 | 只找本进程 toplevel_surface | 跨进程时使用 create_remote_subsurface | +| `wayland_surface_reconfigure_subsurface()` | wayland_surface.c:673 | 调用 wl_subsurface_set_position | 跨进程时使用 remote_subsurface.set_position | +| `wayland_surface_reconfigure_client()` | wayland_surface.c:588 | 调用 wl_subsurface_place_above | 跨进程时使用 remote_subsurface.place_above | + +## 6. 与 xdg-foreign-v2 的对比 + +| 特性 | xdg-foreign-v2 | treeland_cross_subsurface | +|------|---------------|--------------------------| +| 目标 | 窗口间 stacking(类似 X11 transient) | 完整的 subsurface 语义 | +| 导出粒度 | 仅 xdg_toplevel | 任意 wl_surface | +| 子 surface 的位置控制 | 无(依赖 WM) | set_position | +| 同步/异步模式 | 无 | set_sync/set_desync | +| sibling 排序 | 无 | place_above/place_below | +| 内容渲染 | 子进程独立渲染 | 子进程独立渲染 | +| 适用场景 | 对话框、弹出菜单 | 嵌入式渲染、子窗口 | + +xdg-foreign-v2 只解决了 "把我的窗口放在另一个窗口上面" 的问题,而 +treeland_cross_subsurface 解决的是 "把我的 surface 作为另一个 surface 的子 surface" +——包括定位、同步、排序在内的完整 subsurface 语义。 diff --git a/xml/treeland-cross-subsurface-unstable-v1.xml b/xml/treeland-cross-subsurface-unstable-v1.xml new file mode 100644 index 0000000..9d70b36 --- /dev/null +++ b/xml/treeland-cross-subsurface-unstable-v1.xml @@ -0,0 +1,396 @@ + + + + + + + Extends wl_subcompositor to support sub-surface relationships + between surfaces belonging to different client connections. + + In wl_subcompositor both the parent surface and sibling + surfaces must be local wl_surface objects from the same + client. This protocol removes that restriction: + + 1. Any wl_surface can be exported, receiving a stable numeric + surface_id. The owning client shares surface_id with other + clients through any out-of-band mechanism (shared memory, + D-Bus, socket, etc.). + + 2. A client creates a cross-process sub-surface by providing + its own local wl_surface and the parent's surface_id. + The compositor maintains the relationship server-side. + + 3. The compositor assigns a numeric subsurface_id to every + sub-surface under an exported surface, including standard + wl_subcompositor sub-surfaces. All IDs share one namespace + and can be used as sibling references in place_above and + place_below. + + A surface may be both a wl_subsurface (child of some parent) + and an exported surface (parent of its own children). This + allows arbitrary nesting of sub-surface trees across processes. + + All sub-surface semantics (positioning, sync/desync modes, + mapping rules) follow the wl_subcompositor specification + unless explicitly noted in this document. + + The compositor should restrict binding to trusted clients. + + The key words "must", "must not", "required", "shall", + "shall not", "should", "should not", "recommended", "may", + and "optional" in this document are to be interpreted as + described in RFC 2119. + + + + + + + + + Global factory object. Provides two operations: + + export_surface registers a wl_surface as a potential + parent for cross-process sub-surfaces and assigns it a + surface_id. + + create_remote_subsurface creates a cross-process + sub-surface by attaching the caller's local wl_surface to + a parent identified by surface_id. + + + + + + + + + + Informs the server that the client will not use this + protocol object anymore. Existing + treeland_exported_surface_v1 and + treeland_remote_subsurface_v1 objects are not affected. + + + + + + Register surface as a potential parent for cross-process + sub-surfaces. Upon success the compositor emits the + surface_id event on the new treeland_exported_surface_v1 + object. + + The surface must not have been previously exported + through this manager. Violating this rule results in a + bad_surface error. A surface may carry both an + xdg_toplevel (or any other) role and an export; the + export only marks it as addressable by ID. + + The compositor begins tracking standard + wl_subcompositor sub-surfaces created under this + surface and assigns each a subsurface_id, reported + via the child_entered event. + + + + + + + + Create a cross-process sub-surface by attaching the + caller's local surface to the parent identified by + parent_id. + + The surface must not already carry a wl_subsurface + role or a treeland_remote_subsurface_v1 role. Violating + this rule results in a bad_surface error. + + The parent_id must be a valid surface_id from a live + treeland_exported_surface_v1 object. Providing an + unknown or already-destroyed parent_id results in a + bad_parent_id error. The compositor should also reject + parent_id values that refer to surfaces belonging to + untrusted clients, according to its access-control policy. + + Upon success the compositor emits subsurface_id + followed by parent_geometry. The initial mode is + synchronized. + + + + + + + + + + + + + + Represents a wl_surface that has been registered as a + potential parent for cross-process sub-surfaces. + + The compositor monitors standard wl_subcompositor + sub-surfaces created under this surface. Each receives a + subsurface_id reported via child_entered, allowing the + surface owner to share sibling references with other + clients. + + When this object is destroyed the surface_id is revoked. + Any treeland_remote_subsurface_v1 objects that reference + this parent receive a parent_destroyed event and become + inert. + + + + + The surface_id is no longer valid. Cross-process + sub-surfaces under this surface receive + parent_destroyed. Standard wl_subcompositor + sub-surfaces are not affected. + + + + + + Emitted immediately after export_surface. The value + is unique within the compositor's namespace and + guaranteed to be in the range [1, 0xFFFFFFFE] + (0 and 0xFFFFFFFF are reserved). + + The owning client should share this ID with other + clients through an out-of-band mechanism so they can + create cross-process sub-surfaces or reference this + surface as a sibling. + + + + + + + The compositor emits this event whenever a standard + wl_subcompositor sub-surface is created under this + exported surface. The subsurface_id can be shared + with other clients as a sibling reference in + place_above / place_below. + + The surface argument identifies the child + wl_surface from the perspective of the exporting + client (it is a local proxy in the exporter's + connection). + + + + + + + + The subsurface_id is no longer valid. The + compositor emits this when the corresponding + wl_subsurface is destroyed. + + + + + + + The underlying wl_surface was destroyed. This + treeland_exported_surface_v1 object is now inert. + No further events will be emitted. The client + should destroy this object. + + + + + + + + + + + Interface to a wl_surface that has been made a sub-surface + of a parent in a different client connection. + + Semantics mirror wl_subsurface except for how the parent + and siblings are identified: + + - The parent is identified at creation time by surface_id + and cannot be changed. + + - place_above and place_below accept sibling_ref (uint32) + instead of a wl_surface proxy. See the sibling_ref + documentation for valid values. + + Mapping and unmapping rules follow wl_subsurface: the + sub-surface is mapped when both it has a non-NULL buffer + and the parent is mapped; it is hidden when the parent + is hidden or when a NULL buffer is attached. + + + + + + + + + + + The surface's association to the parent is deleted and + the surface is unmapped immediately, matching + wl_subsurface.destroy semantics. + + + + + + Sets the position of the sub-surface relative to the + parent surface's local coordinate system. Coordinates + are subject to the parent's buffer_transform and scale, + matching wl_subsurface.set_position semantics. + + Position is double-buffered state on the parent + surface. The child must commit its own surface for + the new position to take effect. + + + + + + + + This sub-surface is taken from the stack and placed + just above the surface identified by sibling_ref. + + Valid sibling_ref values: + + - The surface_id of the parent surface itself + (places the sub-surface directly above the parent). + + - The subsurface_id of any sibling sub-surface + under the same parent, regardless of whether that + sibling was created via wl_subcompositor + (reported by child_entered on the parent's + treeland_exported_surface_v1) or via this + protocol (reported by subsurface_id on the + sibling's treeland_remote_subsurface_v1). + + - 0xFFFFFFFF (sibling_ref.top): place at the very + top of the sibling stack. + + The value 0 is reserved and must result in a + bad_sibling error. + + Z-order is double-buffered state on the parent + surface. + + + + + + + Places this sub-surface just below the surface + identified by sibling_ref. The same sibling_ref + values are accepted as in place_above. + + + + + + + Change the commit behaviour to synchronized mode. + See wl_subsurface.set_sync. + + + + + + Change the commit behaviour to desynchronized mode. + See wl_subsurface.set_desync. + + + + + + + + Emitted immediately after create_remote_subsurface, + before parent_geometry. The value is in + [1, 0xFFFFFFFE]. + + Other clients may use this ID as a sibling_ref + in their own place_above / place_below requests. + The owning client should share this ID through an + out-of-band mechanism. + + + + + + + Provides the information needed by the child client + to convert its own logical coordinates into the + parent's surface-local coordinate system for + set_position. + + Emitted: + - immediately after subsurface_id on creation, + - whenever the parent surface's position or output + configuration changes. + + The x, y values give the parent surface origin in + the compositor's global logical coordinate space. + + The scale value is the parent surface's current + buffer_scale. + + The transform value follows the semantics of + wl_output.transform: 0 = normal, 1 = 90 degrees + counter-clockwise, etc. + + + + + + + + + + Emitted when the parent's treeland_exported_surface_v1 + is destroyed (via its destroy request) or when the + underlying wl_surface of the parent is destroyed. + + After this event this treeland_remote_subsurface_v1 + object is inert: all subsequent requests are ignored + and no further events will be emitted. The client + should destroy this object. + + + + + +