diff --git a/.agents/docs/2026-09-01-cross-platform-desktop-design.md b/.agents/docs/2026-09-01-cross-platform-desktop-design.md
new file mode 100644
index 00000000..108e9826
--- /dev/null
+++ b/.agents/docs/2026-09-01-cross-platform-desktop-design.md
@@ -0,0 +1,270 @@
+# 任何地方都是一块屏幕 —— 跨平台用户态桌面的设计
+
+2026-09-01
+
+四家桌面系统是同一副骨架,只是把「操作系统的边界」画在了不同高度。真正不可移植
+的只有两件事:**把一帧像素放上去**、**把输入收进来**。其余全部是纯计算 —— 这决定
+了整个设计只需要一个抽象。
+
+---
+
+## §1 五层骨架 × 四个平台
+
+| 层 | Linux | Windows | macOS | Android |
+|---|---|---|---|---|
+| **① 硬件访问** | DRM/KMS + evdev | WDDM / DXGI + RawInput | IOKit + CoreGraphics | **Composer HAL(HWC)+ Gralloc**;底下仍是 Linux 内核,输入 evdev → InputReader |
+| **② 合成器** | wlroots / mutter / KWin(或 X server) | DWM + DirectComposition | WindowServer + Quartz | **SurfaceFlinger** —— HWC 走硬件叠加层,RenderEngine/Skia 兜底 |
+| **③ 客户端协议** | Wayland / X11(线协议 + fd 传递) | Win32 消息 + DComp 视觉树 | Cocoa + CoreAnimation 远端图层 | **Binder RPC + BufferQueue**(共享 dma-buf 句柄,**没有线协议**) |
+| **④ 渲染** | Mesa(GL/Vulkan)、cairo | D3D / Direct2D | Metal / CoreGraphics | **HWUI / Skia** + GLES/Vulkan |
+| **⑤ 控件** | GTK / Qt | WinUI / WPF | AppKit / SwiftUI | **View 体系 / Jetpack Compose** |
+| **你能换到第几层** | ✅ ②③ 都能换
(所以能自己写合成器) | ⚠️ 只有 ④⑤
②③ 锁死 | ⚠️ 只有 ④⑤ | ⚠️ 只有 ④⑤
但 NDK 接口最干净 |
+
+> ⭐ 四家锁死了不同的东西,却**都给你同一样东西:一个窗口 + 一个输入流。**
+> 这就是整个跨平台设计唯一能站住的地基。
+
+### Android 那一列为什么值得单独看
+
+它是四家里唯一一个**「客户端协议」不是协议**的 —— Binder RPC 加共享缓冲区句柄。
+两个直接后果:
+
+- **接管不了。** Wayland 那条路(自己当服务端、让真实应用连进来)在 Android 上
+ **没有对应物**。Linux 上那个加分项到 Android 就是零。
+- **但 NDK 给的东西异常干净** —— `ANativeWindow_lock` / `unlockAndPost` 就是
+ 「拿一块可写像素、交回去」,`AInputQueue` 就是输入流。它几乎是照着下面那个
+ 抽象长的。
+
+还有一点结构上的巧合:SurfaceFlinger 本来就把每个应用当成一个 layer 生产者,
+**你的桌面只是多一个生产者** —— 嵌套形态在 Android 上是最自然的,不是将就。
+
+---
+
+## §2 时序 · 一块屏幕是怎么到手的
+
+```
+ ┌─────────────── 平台层:唯一不可移植的部分 ───────────────┐
+ │ │
+ Linux 嵌套 Linux 独占 Windows macOS Android Headless
+ wl_surface + DRM/KMS CreateWindowEx NSWindow ANativeWindow 内存缓冲
+ xdg_toplevel scanout + DXGI + CALayer + AInputQueue → PNG
+ │ │ │ │ │ │
+ └──────────────┴─────────────┴──────┬──────┴───────────┴─────────────┘
+ ▼
+ ┌─────────────────────────────────┐
+ │ Screen │
+ │ size / acquire / present / pump│
+ └────────────────┬────────────────┘
+ ▼
+ ┌────────── 可移植核心:纯计算,零平台依赖 ──────────┐
+ │ 场景图 · 损伤 · 层叠 · 命中测试 │
+ │ 窗管策略 · 焦点 · 摆放 │
+ │ 控件 · 布局 · 事件分发 │
+ │ 文本与矢量绘制(pango + cairo) │
+ └───────────────────────────────────────────────────┘
+```
+
+判断标准很硬:**把 Screen 换成 Headless,核心的行为必须一字不变。**
+
+---
+
+## §3 时序 · 一帧的生命周期
+
+最容易写错的一张,因为错了不报错。
+
+```
+ 应用 合成核心 Screen(平台)
+ │ │ │
+ ├─ attach(buffer) ────────►│ │
+ │ + damage(区域) │ 损伤并入损伤环 │
+ │ + commit │ ⚠️ 不立刻渲染 │
+ │ │ │
+ │ │◄──── frame 时钟到点 ───────┤
+ │ │ │
+ │ ├─ acquire() ───────────────►│
+ │ │ 只重画损伤区域 │
+ │ │ 层叠/遮挡在场景图里算好 │
+ │ ├─ present(damage) ─────────►│
+ │ │ │
+ │◄──── frame_done ─────────┤ │
+ │ 收到才画下一帧 │ │
+ ├─ 下一帧 commit ─────────►│ │
+```
+
+> ⚠️ **实测踩过的坑 —— 帧回调死锁**
+>
+> `frame_done` 只发给「这次渲染真的画到了的」表面。如果核心因为「没有新损伤」
+> 而跳过渲染,就没人收到 `frame_done`;客户端于是永远不画下一帧,核心也就永远
+> 没有新损伤 —— **两边都在等对方,都不报错。**
+>
+> 普通应用碰不到(画一次就完事),**只有当客户端本身也是个合成器时才会暴露**。
+> 套娃 demo 就是这么卡住的:内层两块屏幕是白板,而每一项状态单独看都正常
+> (mapped / buffer / texture / enabled 逐项都有)。
+
+---
+
+## §4 时序 · 一个窗口从创建到上屏
+
+这一段的形状在四个平台上惊人地一致 —— 因为它本来就是纯协议,和硬件无关。
+
+```
+ 应用 合成核心 窗管策略
+ │ │ │
+ ├─ 创建表面 ────────────►│ │
+ ├─ 要一个「顶层窗口」角色►│ │
+ ├─ commit(空) ─────────►│ │
+ │ ⚠️ 此刻还不能贴像素 │ │
+ │ 要先谈好尺寸 ├─ 新窗口 ──────────────►│
+ │ │◄─ 尺寸 / 位置 / 状态 ──┤
+ │◄─ configure(尺寸,状态)┤ │
+ ├─ ack_configure ───────►│ │
+ ├─ attach(第一块像素) ──►│ │
+ │ + commit ├─ 已 map ──────────────►│
+ │ │◄─ 摆放 + 抬到最上层 ───┤
+ │ │ + 设为激活 │
+ │◄─ configure(ACTIVATED)┤ │
+ │ ⭐ 据此画高亮标题栏 │ │
+ │ 不是自己猜的 │ │
+ ├─ 重画 + commit ───────►│ │
+```
+
+⚠️ 「先提交一个空表面」是所有人第一次都会漏的:**尺寸要先谈好,才能贴像素。**
+在 configure 之前发就撞 `Assertion 'surface->initialized' failed`。
+
+---
+
+## §5 时序 · 一次点击怎么变成焦点
+
+```
+ Screen(平台) 合成核心 原焦点窗口 被点的窗口
+ │ │ │ │
+ ├─ pump() 吐出 ───────►│ │ │
+ │ 按下(x, y) │ 命中测试:场景图 │ │
+ │ │ 从上往下问,被遮住 │ │
+ │ │ 的部分不命中 │ │
+ │ ├─ 取消激活 ──────────►│ │
+ │ ├─ 抬到最上层 ─────────┼────────────────►│
+ │ ├─ 设为激活+键盘焦点 ──┼────────────────►│
+ │ ├─ configure(无 ACT) ─►│ │
+ │ ├─ configure(含 ACT) ──┼────────────────►│
+ │ │◄─ 画成「未聚焦」─────┤ │
+ │ │◄─ 画成「已聚焦」─────┼─────────────────┤
+ │ │ 两块损伤进环 │ │
+ │ │ 下一帧一起呈现 │ │
+```
+
+⭐ **除了第 1 步,没有任何一步是平台特有的。** 命中测试、层叠、激活状态推送,
+全是核心里的纯计算。
+
+---
+
+## §6 设计 · 把「屏幕」定义成能力,而不是设备
+
+不要把屏幕定义成「一块显示器」。定义成**一个能满足三件事的东西**:
+
+```cpp
+struct Screen {
+ virtual Size size() const = 0; // 我有多大
+ virtual Pixels acquire() = 0; // 给我一块可写的像素
+ virtual void present(const Damage &) = 0; // 收下这一帧
+ virtual void pump(EventSink &) = 0; // 输入 / 尺寸变化 / 关闭
+};
+```
+
+接口小到这个程度,不是为了简洁 —— 是因为**接口每大一圈,就多一个平台概念漏进
+上层的机会**。凡是能满足这四个方法的,都是一块屏幕:
+
+| 实现 | 是什么 |
+|---|---|
+| **原生窗口** | Win32 / Cocoa / Android / X11 / Wayland —— 最常见的一种 |
+| **独占显示器** | Linux 上 DRM/KMS 直接扫描输出,真正的「全屏系统」 |
+| **文件** | 写成 PNG 或编码成视频 —— CI 里跑的就是它 |
+| **网络对端** | 把帧和损伤发出去,远端渲染;输入反向回来 |
+| **浏览器画布** | WASM + Canvas,桌面直接开在网页里 |
+| **另一个桌面** | 屏幕本身是另一套桌面的客户端 —— 套娃 |
+
+最后一条**不是设想,已经跑通了**:同一个二进制,把 headless 换成嵌套后端,它的
+每块屏幕就变成宿主里的一个普通窗口,而场景图、焦点、层叠、摆放**一个字没改**。
+
+```
+ 应用 A ─┐
+ ├─► 你的桌面 #1 ──► Screen = 嵌套窗口 ─┐
+ 应用 B ─┘ (合成核心) │
+ ▼
+ 你的桌面 #2 / 系统合成器
+ (它眼里那只是个普通窗口)
+ │
+ ▼
+ Screen = 原生窗口 / DRM / 画布
+ │
+ ▼
+ 真实显示器
+```
+
+⭐ **屏幕即客户端。** 这一层可以无限套,也可以在中间任意一环换成网络 ——
+远程桌面、多屏、嵌套调试,全是同一个机制的不同接线,不需要各写一套。
+
+---
+
+## §7 模块怎么切
+
+```
+xdesk.core 场景图 / 损伤 / 层叠 / 命中测试 / 窗管策略 ← 纯计算
+xdesk.paint 文本与矢量绘制(pango + cairo) ← 今天就跨平台
+xdesk.ui 控件 / 布局 / 事件分发 ← 要写的那层
+xdesk.screen Screen 抽象(上面那四个方法)
+ ├ .headless 离屏 + 截图 —— CI 三平台跑的是它
+ ├ .wayland Linux:嵌套,或独占 TTY(DRM)
+ ├ .win32 Windows:一个窗口 + GDI/D3D 呈现
+ ├ .cocoa macOS:一个 NSWindow + CALayer
+ └ .android Android:ANativeWindow + AInputQueue
+xdesk.host.wayland Linux 专属:让真实 Wayland 应用也能进来
+```
+
+> ⚠️ **最容易走歪的一步**
+>
+> 跨平台的难点从来不是抽象有多复杂,而是**别让平台概念往上漏**。
+>
+> 昨天那个 demo 就是反面教材 —— 它直接写在 wlroots 上,`wlr_scene`、`wlr_seat`、
+> `wl_listener` 渗透在每一层,**它按构造就是 Linux 专属的**。作为 demo 完全没
+> 问题,但它**不能**当这套系统的底座。
+>
+> 防线只有一条,而且必须一开始就立起来:**`headless` 和一个真后端从第一天就
+> 并存,且 CI 三平台都跑。** 越晚做,漏得越深。
+
+---
+
+## §8 索引现状 —— 实测,不是估计
+
+| 能力 | Linux | Windows | macOS | Android |
+|---|---|---|---|---|
+| **文本/绘制**
pango·cairo·freetype
harfbuzz·pixman | ✅ 有 | ✅ 有
无平台限制字段,CI 跑 | ✅ 有 | ✗ 无
包本身能跨,缺的是工具链 |
+| **合成器构件**
wayland·wlroots
mesa·libinput | ✅ 有 | —
按定义不适用 | — | — |
+| **出画后端** | ✅ 嵌套已验证
DRM 要独占 TTY | ✗ 要写 | ✗ 要写 | ✗ 要写 |
+| **工具链/交叉编译** | ✅ | ✅
clang++ 打 MSVC ABI | ✅ | ✗ **没有 NDK subos**
`mcpp --target` 在,缺供给 |
+| **控件层** | ✗ | ✗ | ✗ | ✗ —— **这是真正的大头** |
+
+CI 矩阵实测:`macos 1 · linux 3 · windows 2`。`freedesktop.cairo` / `gnome.pango` /
+`compat.freetype` / `compat.harfbuzz` / `compat.pixman` 都没有平台限制字段。
+
+Android 在索引里是**零**(只有 `compat.ffmpeg` 里的一堆配置常量),
+`xlings subos list` 里也没有任何 android/ndk 条目。
+
+---
+
+## §9 排序
+
+1. **先把 `headless` 和一个真后端跑在同一套核心上,CI 三平台都跑。**
+ 这一步不产出任何功能,价值在于它会立刻暴露所有漏上来的平台概念。
+
+2. **控件层。** 没有它,桌面里只能放手搓两百行连协议的窗口,撑不起第二个应用。
+ 这也是唯一一处我建议**自己写而不是包 GTK/Qt** 的地方 —— 那两家各自带着一整套
+ 窗口系统假设(GTK 认 GdkSurface、Qt 认 QPA),引进来等于同时维护两套概念。
+
+3. **Android 工具链供给**(NDK subos + 一批包按 android triple 重过一遍)。
+ 放在控件层之后,因为**只有一个平台后端的时候去铺第四个平台,验证不了任何东西**。
+
+4. **最后才是 Wayland 兼容后端。** 它让真实 Linux 应用能进来,是加分项;
+ 放前面会把整个架构拖成 Linux 形状。
+
+关于代价的实话:Chromium 的 `viz`+`aura` 是几十万行,一个**够用**的版本大概一到
+两万行,其中控件层占大头。这不是一个周末的项目 —— 但**最难的合成那部分,已经
+跑通过一遍了**(见 `Sunrisepeak/mcpp-demos` 的 `wayland-desktop`)。
diff --git a/pkgs/o/openarch.lua b/pkgs/o/openarch.lua
index 0afbc433..f83f3153 100644
--- a/pkgs/o/openarch.lua
+++ b/pkgs/o/openarch.lua
@@ -18,6 +18,19 @@
-- mechanism that is a table of 256 gates. One probe source runs on all three
-- and prints byte-identical output.
--
+-- ⭐⭐ 0.8.0 ADDS A FOURTH MACHINE AND THE FIRST *PARTIAL* BACKEND.
+--
+-- ARM Cortex-M has no memory management unit and no per-CPU register, so
+-- `openarch-cortex-m` declares `openarch-backend` and `openarch:preemption`
+-- and NOT `openarch:address-space` or `openarch:percpu-register`. A kernel that
+-- needs either is refused BY NAME at resolution rather than by a wall of
+-- `undefined reference to arch_pte_*` at link time.
+--
+-- Splitting the capability is what made admitting the machine possible;
+-- `openarch:preemption` — `arch_trap_switch`, the action the trap group was
+-- missing — is what made it worth doing. A microcontroller is exactly where a
+-- hand-written task switcher is otherwise re-invented per project.
+--
-- ⚠️ THE TARBALL CARRIES FOUR PACKAGES AND A CONSUMER NAMES ONE. Since 0.4.0
-- the repository root is BOTH the interface package and a workspace; the ABI
-- contract and the three backends are members reached by `path` from inside the
@@ -35,6 +48,13 @@ package = {
xpm = {
linux = {
+ ["0.8.0"] = {
+ url = {
+ GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.8.0.tar.gz",
+ CN = "https://gitcode.com/mcpp-res/openarch/releases/download/0.8.0/openarch-0.8.0.tar.gz",
+ },
+ sha256 = "592a20562e3f02c523970bedaed44ef806daa3fc2a2b22a0188d92b34afb8277",
+ },
["0.7.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.7.0.tar.gz",
@@ -93,6 +113,13 @@ package = {
},
},
macosx = {
+ ["0.8.0"] = {
+ url = {
+ GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.8.0.tar.gz",
+ CN = "https://gitcode.com/mcpp-res/openarch/releases/download/0.8.0/openarch-0.8.0.tar.gz",
+ },
+ sha256 = "592a20562e3f02c523970bedaed44ef806daa3fc2a2b22a0188d92b34afb8277",
+ },
["0.7.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.7.0.tar.gz",
@@ -151,6 +178,13 @@ package = {
},
},
windows = {
+ ["0.8.0"] = {
+ url = {
+ GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.8.0.tar.gz",
+ CN = "https://gitcode.com/mcpp-res/openarch/releases/download/0.8.0/openarch-0.8.0.tar.gz",
+ },
+ sha256 = "592a20562e3f02c523970bedaed44ef806daa3fc2a2b22a0188d92b34afb8277",
+ },
["0.7.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.7.0.tar.gz",