From 752ae86bb61493012990063fabedeb81ad0328d8 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Fri, 4 Sep 2026 12:22:41 +0800 Subject: [PATCH] =?UTF-8?q?openarch=200.8.0=20=E2=80=94=20a=20fourth=20mac?= =?UTF-8?q?hine,=20and=20the=20first=20partial=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## The action the trap group was missing `arch_trap_set_handler` lets a kernel SEE a trap and `arch_trap_enable_interrupts` lets it MASK one. Neither lets it change what the trap returns to — and that is the whole of preemption, which is the principal reason to use this layer on a microcontroller. void arch_trap_switch(arch_trap_frame* f, void* from, void* to); Called from a handler. It returns normally; the switch happens when the trap does. Same storage and same `arch_context_init` as `arch_context_switch`, so a task can be resumed by either — the two differ only in WHEN. ## ⭐⭐ Four machines, four implementations, one name riscv64 saves `mepc` across a cooperative switch inside its dispatcher; aarch64 saves `ELR_EL1` and `SPSR_EL1`; x86_64 saves nothing because its `iret` frame travels with the stack. Cortex-M does none of that: its handler runs on MSP while the task runs on PSP, so it pends PendSV and the hardware performs the switch at exception exit. Three of four share a mechanism; the fourth cannot, and that is exactly what an interface function earns its place by hiding. ## The first PARTIAL backend `openarch-cortex-m` declares `openarch-backend` and `openarch:preemption`, and NOT `openarch:address-space` or `openarch:percpu-register`: M-profile has a region-based MPU with no page-table entry to construct, and no TPIDR-class 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 made admitting the machine possible; `openarch:preemption` made it worth doing. ## Measured `examples/switch` on riscv64, aarch64 and x86_64 — each now asserting that a trap resumed a DIFFERENT context, via a counter the other context advanced. `examples/preempt` on `mps2-an385` under `xim:qemu-arm@9.2.4-1`: two tasks that never yield, each proving it was interrupted, 15 consecutive runs. Tarball sha256 verified against the bytes and re-fetched from the CN mirror, byte-identical. Index lints: mirror-urls, package-name, platform parity, duplicate versions and cross-package refs all pass. --- ...026-09-01-cross-platform-desktop-design.md | 270 ++++++++++++++++++ pkgs/o/openarch.lua | 34 +++ 2 files changed, 304 insertions(+) create mode 100644 .agents/docs/2026-09-01-cross-platform-desktop-design.md 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",