Skip to content

Commit 1200f5e

Browse files
authored
openarch 0.8.0 — a fourth machine, and the first partial backend (#334)
## 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.
1 parent 60fca19 commit 1200f5e

2 files changed

Lines changed: 304 additions & 0 deletions

File tree

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# 任何地方都是一块屏幕 —— 跨平台用户态桌面的设计
2+
3+
2026-09-01
4+
5+
四家桌面系统是同一副骨架,只是把「操作系统的边界」画在了不同高度。真正不可移植
6+
的只有两件事:**把一帧像素放上去****把输入收进来**。其余全部是纯计算 —— 这决定
7+
了整个设计只需要一个抽象。
8+
9+
---
10+
11+
## §1 五层骨架 × 四个平台
12+
13+
|| Linux | Windows | macOS | Android |
14+
|---|---|---|---|---|
15+
| **① 硬件访问** | DRM/KMS + evdev | WDDM / DXGI + RawInput | IOKit + CoreGraphics | **Composer HAL(HWC)+ Gralloc**;底下仍是 Linux 内核,输入 evdev → InputReader |
16+
| **② 合成器** | wlroots / mutter / KWin(或 X server) | DWM + DirectComposition | WindowServer + Quartz | **SurfaceFlinger** —— HWC 走硬件叠加层,RenderEngine/Skia 兜底 |
17+
| **③ 客户端协议** | Wayland / X11(线协议 + fd 传递) | Win32 消息 + DComp 视觉树 | Cocoa + CoreAnimation 远端图层 | **Binder RPC + BufferQueue**(共享 dma-buf 句柄,**没有线协议**) |
18+
| **④ 渲染** | Mesa(GL/Vulkan)、cairo | D3D / Direct2D | Metal / CoreGraphics | **HWUI / Skia** + GLES/Vulkan |
19+
| **⑤ 控件** | GTK / Qt | WinUI / WPF | AppKit / SwiftUI | **View 体系 / Jetpack Compose** |
20+
| **你能换到第几层** | ✅ ②③ 都能换<br>(所以能自己写合成器) | ⚠️ 只有 ④⑤<br>②③ 锁死 | ⚠️ 只有 ④⑤ | ⚠️ 只有 ④⑤<br>但 NDK 接口最干净 |
21+
22+
> ⭐ 四家锁死了不同的东西,却**都给你同一样东西:一个窗口 + 一个输入流。**
23+
> 这就是整个跨平台设计唯一能站住的地基。
24+
25+
### Android 那一列为什么值得单独看
26+
27+
它是四家里唯一一个**「客户端协议」不是协议**的 —— Binder RPC 加共享缓冲区句柄。
28+
两个直接后果:
29+
30+
- **接管不了。** Wayland 那条路(自己当服务端、让真实应用连进来)在 Android 上
31+
**没有对应物**。Linux 上那个加分项到 Android 就是零。
32+
- **但 NDK 给的东西异常干净** —— `ANativeWindow_lock` / `unlockAndPost` 就是
33+
「拿一块可写像素、交回去」,`AInputQueue` 就是输入流。它几乎是照着下面那个
34+
抽象长的。
35+
36+
还有一点结构上的巧合:SurfaceFlinger 本来就把每个应用当成一个 layer 生产者,
37+
**你的桌面只是多一个生产者** —— 嵌套形态在 Android 上是最自然的,不是将就。
38+
39+
---
40+
41+
## §2 时序 · 一块屏幕是怎么到手的
42+
43+
```
44+
┌─────────────── 平台层:唯一不可移植的部分 ───────────────┐
45+
│ │
46+
Linux 嵌套 Linux 独占 Windows macOS Android Headless
47+
wl_surface + DRM/KMS CreateWindowEx NSWindow ANativeWindow 内存缓冲
48+
xdg_toplevel scanout + DXGI + CALayer + AInputQueue → PNG
49+
│ │ │ │ │ │
50+
└──────────────┴─────────────┴──────┬──────┴───────────┴─────────────┘
51+
52+
┌─────────────────────────────────┐
53+
│ Screen │
54+
│ size / acquire / present / pump│
55+
└────────────────┬────────────────┘
56+
57+
┌────────── 可移植核心:纯计算,零平台依赖 ──────────┐
58+
│ 场景图 · 损伤 · 层叠 · 命中测试 │
59+
│ 窗管策略 · 焦点 · 摆放 │
60+
│ 控件 · 布局 · 事件分发 │
61+
│ 文本与矢量绘制(pango + cairo) │
62+
└───────────────────────────────────────────────────┘
63+
```
64+
65+
判断标准很硬:**把 Screen 换成 Headless,核心的行为必须一字不变。**
66+
67+
---
68+
69+
## §3 时序 · 一帧的生命周期
70+
71+
最容易写错的一张,因为错了不报错。
72+
73+
```
74+
应用 合成核心 Screen(平台)
75+
│ │ │
76+
├─ attach(buffer) ────────►│ │
77+
│ + damage(区域) │ 损伤并入损伤环 │
78+
│ + commit │ ⚠️ 不立刻渲染 │
79+
│ │ │
80+
│ │◄──── frame 时钟到点 ───────┤
81+
│ │ │
82+
│ ├─ acquire() ───────────────►│
83+
│ │ 只重画损伤区域 │
84+
│ │ 层叠/遮挡在场景图里算好 │
85+
│ ├─ present(damage) ─────────►│
86+
│ │ │
87+
│◄──── frame_done ─────────┤ │
88+
│ 收到才画下一帧 │ │
89+
├─ 下一帧 commit ─────────►│ │
90+
```
91+
92+
> ⚠️ **实测踩过的坑 —— 帧回调死锁**
93+
>
94+
> `frame_done` 只发给「这次渲染真的画到了的」表面。如果核心因为「没有新损伤」
95+
> 而跳过渲染,就没人收到 `frame_done`;客户端于是永远不画下一帧,核心也就永远
96+
> 没有新损伤 —— **两边都在等对方,都不报错。**
97+
>
98+
> 普通应用碰不到(画一次就完事),**只有当客户端本身也是个合成器时才会暴露**
99+
> 套娃 demo 就是这么卡住的:内层两块屏幕是白板,而每一项状态单独看都正常
100+
> (mapped / buffer / texture / enabled 逐项都有)。
101+
102+
---
103+
104+
## §4 时序 · 一个窗口从创建到上屏
105+
106+
这一段的形状在四个平台上惊人地一致 —— 因为它本来就是纯协议,和硬件无关。
107+
108+
```
109+
应用 合成核心 窗管策略
110+
│ │ │
111+
├─ 创建表面 ────────────►│ │
112+
├─ 要一个「顶层窗口」角色►│ │
113+
├─ commit(空) ─────────►│ │
114+
│ ⚠️ 此刻还不能贴像素 │ │
115+
│ 要先谈好尺寸 ├─ 新窗口 ──────────────►│
116+
│ │◄─ 尺寸 / 位置 / 状态 ──┤
117+
│◄─ configure(尺寸,状态)┤ │
118+
├─ ack_configure ───────►│ │
119+
├─ attach(第一块像素) ──►│ │
120+
│ + commit ├─ 已 map ──────────────►│
121+
│ │◄─ 摆放 + 抬到最上层 ───┤
122+
│ │ + 设为激活 │
123+
│◄─ configure(ACTIVATED)┤ │
124+
│ ⭐ 据此画高亮标题栏 │ │
125+
│ 不是自己猜的 │ │
126+
├─ 重画 + commit ───────►│ │
127+
```
128+
129+
⚠️ 「先提交一个空表面」是所有人第一次都会漏的:**尺寸要先谈好,才能贴像素。**
130+
在 configure 之前发就撞 `Assertion 'surface->initialized' failed`
131+
132+
---
133+
134+
## §5 时序 · 一次点击怎么变成焦点
135+
136+
```
137+
Screen(平台) 合成核心 原焦点窗口 被点的窗口
138+
│ │ │ │
139+
├─ pump() 吐出 ───────►│ │ │
140+
│ 按下(x, y) │ 命中测试:场景图 │ │
141+
│ │ 从上往下问,被遮住 │ │
142+
│ │ 的部分不命中 │ │
143+
│ ├─ 取消激活 ──────────►│ │
144+
│ ├─ 抬到最上层 ─────────┼────────────────►│
145+
│ ├─ 设为激活+键盘焦点 ──┼────────────────►│
146+
│ ├─ configure(无 ACT) ─►│ │
147+
│ ├─ configure(含 ACT) ──┼────────────────►│
148+
│ │◄─ 画成「未聚焦」─────┤ │
149+
│ │◄─ 画成「已聚焦」─────┼─────────────────┤
150+
│ │ 两块损伤进环 │ │
151+
│ │ 下一帧一起呈现 │ │
152+
```
153+
154+
**除了第 1 步,没有任何一步是平台特有的。** 命中测试、层叠、激活状态推送,
155+
全是核心里的纯计算。
156+
157+
---
158+
159+
## §6 设计 · 把「屏幕」定义成能力,而不是设备
160+
161+
不要把屏幕定义成「一块显示器」。定义成**一个能满足三件事的东西**:
162+
163+
```cpp
164+
struct Screen {
165+
virtual Size size() const = 0; // 我有多大
166+
virtual Pixels acquire() = 0; // 给我一块可写的像素
167+
virtual void present(const Damage &) = 0; // 收下这一帧
168+
virtual void pump(EventSink &) = 0; // 输入 / 尺寸变化 / 关闭
169+
};
170+
```
171+
172+
接口小到这个程度,不是为了简洁 —— 是因为**接口每大一圈,就多一个平台概念漏进
173+
上层的机会**。凡是能满足这四个方法的,都是一块屏幕:
174+
175+
| 实现 | 是什么 |
176+
|---|---|
177+
| **原生窗口** | Win32 / Cocoa / Android / X11 / Wayland —— 最常见的一种 |
178+
| **独占显示器** | Linux 上 DRM/KMS 直接扫描输出,真正的「全屏系统」 |
179+
| **文件** | 写成 PNG 或编码成视频 —— CI 里跑的就是它 |
180+
| **网络对端** | 把帧和损伤发出去,远端渲染;输入反向回来 |
181+
| **浏览器画布** | WASM + Canvas,桌面直接开在网页里 |
182+
| **另一个桌面** | 屏幕本身是另一套桌面的客户端 —— 套娃 |
183+
184+
最后一条**不是设想,已经跑通了**:同一个二进制,把 headless 换成嵌套后端,它的
185+
每块屏幕就变成宿主里的一个普通窗口,而场景图、焦点、层叠、摆放**一个字没改**。
186+
187+
```
188+
应用 A ─┐
189+
├─► 你的桌面 #1 ──► Screen = 嵌套窗口 ─┐
190+
应用 B ─┘ (合成核心) │
191+
192+
你的桌面 #2 / 系统合成器
193+
(它眼里那只是个普通窗口)
194+
195+
196+
Screen = 原生窗口 / DRM / 画布
197+
198+
199+
真实显示器
200+
```
201+
202+
⭐ **屏幕即客户端。** 这一层可以无限套,也可以在中间任意一环换成网络 ——
203+
远程桌面、多屏、嵌套调试,全是同一个机制的不同接线,不需要各写一套。
204+
205+
---
206+
207+
## §7 模块怎么切
208+
209+
```
210+
xdesk.core 场景图 / 损伤 / 层叠 / 命中测试 / 窗管策略 ← 纯计算
211+
xdesk.paint 文本与矢量绘制(pango + cairo) ← 今天就跨平台
212+
xdesk.ui 控件 / 布局 / 事件分发 ← 要写的那层
213+
xdesk.screen Screen 抽象(上面那四个方法)
214+
├ .headless 离屏 + 截图 —— CI 三平台跑的是它
215+
├ .wayland Linux:嵌套,或独占 TTY(DRM)
216+
├ .win32 Windows:一个窗口 + GDI/D3D 呈现
217+
├ .cocoa macOS:一个 NSWindow + CALayer
218+
└ .android Android:ANativeWindow + AInputQueue
219+
xdesk.host.wayland Linux 专属:让真实 Wayland 应用也能进来
220+
```
221+
222+
> ⚠️ **最容易走歪的一步**
223+
>
224+
> 跨平台的难点从来不是抽象有多复杂,而是**别让平台概念往上漏**。
225+
>
226+
> 昨天那个 demo 就是反面教材 —— 它直接写在 wlroots 上,`wlr_scene`、`wlr_seat`、
227+
> `wl_listener` 渗透在每一层,**它按构造就是 Linux 专属的**。作为 demo 完全没
228+
> 问题,但它**不能**当这套系统的底座。
229+
>
230+
> 防线只有一条,而且必须一开始就立起来:**`headless` 和一个真后端从第一天就
231+
> 并存,且 CI 三平台都跑。** 越晚做,漏得越深。
232+
233+
---
234+
235+
## §8 索引现状 —— 实测,不是估计
236+
237+
| 能力 | Linux | Windows | macOS | Android |
238+
|---|---|---|---|---|
239+
| **文本/绘制**<br>pango·cairo·freetype<br>harfbuzz·pixman | ✅ 有 | ✅ 有<br><small>无平台限制字段,CI 跑</small> | ✅ 有 | ✗ 无<br><small>包本身能跨,缺的是工具链</small> |
240+
| **合成器构件**<br>wayland·wlroots<br>mesa·libinput | ✅ 有 | —<br><small>按定义不适用</small> | — | — |
241+
| **出画后端** | ✅ 嵌套已验证<br><small>DRM 要独占 TTY</small> | ✗ 要写 | ✗ 要写 | ✗ 要写 |
242+
| **工具链/交叉编译** | ✅ | ✅<br><small>clang++ 打 MSVC ABI</small> | ✅ | ✗ **没有 NDK subos**<br><small>`mcpp --target` 在,缺供给</small> |
243+
| **控件层** | ✗ | ✗ | ✗ | ✗ —— **这是真正的大头** |
244+
245+
CI 矩阵实测:`macos 1 · linux 3 · windows 2`。`freedesktop.cairo` / `gnome.pango` /
246+
`compat.freetype` / `compat.harfbuzz` / `compat.pixman` 都没有平台限制字段。
247+
248+
Android 在索引里是**零**(只有 `compat.ffmpeg` 里的一堆配置常量),
249+
`xlings subos list` 里也没有任何 android/ndk 条目。
250+
251+
---
252+
253+
## §9 排序
254+
255+
1. **先把 `headless` 和一个真后端跑在同一套核心上,CI 三平台都跑。**
256+
这一步不产出任何功能,价值在于它会立刻暴露所有漏上来的平台概念。
257+
258+
2. **控件层。** 没有它,桌面里只能放手搓两百行连协议的窗口,撑不起第二个应用。
259+
这也是唯一一处我建议**自己写而不是包 GTK/Qt** 的地方 —— 那两家各自带着一整套
260+
窗口系统假设(GTK 认 GdkSurface、Qt 认 QPA),引进来等于同时维护两套概念。
261+
262+
3. **Android 工具链供给**(NDK subos + 一批包按 android triple 重过一遍)。
263+
放在控件层之后,因为**只有一个平台后端的时候去铺第四个平台,验证不了任何东西**。
264+
265+
4. **最后才是 Wayland 兼容后端。** 它让真实 Linux 应用能进来,是加分项;
266+
放前面会把整个架构拖成 Linux 形状。
267+
268+
关于代价的实话:Chromium 的 `viz`+`aura` 是几十万行,一个**够用**的版本大概一到
269+
两万行,其中控件层占大头。这不是一个周末的项目 —— 但**最难的合成那部分,已经
270+
跑通过一遍了**(见 `Sunrisepeak/mcpp-demos` 的 `wayland-desktop`)。

pkgs/o/openarch.lua

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@
1818
-- mechanism that is a table of 256 gates. One probe source runs on all three
1919
-- and prints byte-identical output.
2020
--
21+
-- ⭐⭐ 0.8.0 ADDS A FOURTH MACHINE AND THE FIRST *PARTIAL* BACKEND.
22+
--
23+
-- ARM Cortex-M has no memory management unit and no per-CPU register, so
24+
-- `openarch-cortex-m` declares `openarch-backend` and `openarch:preemption`
25+
-- and NOT `openarch:address-space` or `openarch:percpu-register`. A kernel that
26+
-- needs either is refused BY NAME at resolution rather than by a wall of
27+
-- `undefined reference to arch_pte_*` at link time.
28+
--
29+
-- Splitting the capability is what made admitting the machine possible;
30+
-- `openarch:preemption` — `arch_trap_switch`, the action the trap group was
31+
-- missing — is what made it worth doing. A microcontroller is exactly where a
32+
-- hand-written task switcher is otherwise re-invented per project.
33+
--
2134
-- ⚠️ THE TARBALL CARRIES FOUR PACKAGES AND A CONSUMER NAMES ONE. Since 0.4.0
2235
-- the repository root is BOTH the interface package and a workspace; the ABI
2336
-- contract and the three backends are members reached by `path` from inside the
@@ -35,6 +48,13 @@ package = {
3548

3649
xpm = {
3750
linux = {
51+
["0.8.0"] = {
52+
url = {
53+
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.8.0.tar.gz",
54+
CN = "https://gitcode.com/mcpp-res/openarch/releases/download/0.8.0/openarch-0.8.0.tar.gz",
55+
},
56+
sha256 = "592a20562e3f02c523970bedaed44ef806daa3fc2a2b22a0188d92b34afb8277",
57+
},
3858
["0.7.0"] = {
3959
url = {
4060
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.7.0.tar.gz",
@@ -93,6 +113,13 @@ package = {
93113
},
94114
},
95115
macosx = {
116+
["0.8.0"] = {
117+
url = {
118+
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.8.0.tar.gz",
119+
CN = "https://gitcode.com/mcpp-res/openarch/releases/download/0.8.0/openarch-0.8.0.tar.gz",
120+
},
121+
sha256 = "592a20562e3f02c523970bedaed44ef806daa3fc2a2b22a0188d92b34afb8277",
122+
},
96123
["0.7.0"] = {
97124
url = {
98125
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.7.0.tar.gz",
@@ -151,6 +178,13 @@ package = {
151178
},
152179
},
153180
windows = {
181+
["0.8.0"] = {
182+
url = {
183+
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.8.0.tar.gz",
184+
CN = "https://gitcode.com/mcpp-res/openarch/releases/download/0.8.0/openarch-0.8.0.tar.gz",
185+
},
186+
sha256 = "592a20562e3f02c523970bedaed44ef806daa3fc2a2b22a0188d92b34afb8277",
187+
},
154188
["0.7.0"] = {
155189
url = {
156190
GLOBAL = "https://github.com/mcpplibs/openarch/archive/refs/tags/0.7.0.tar.gz",

0 commit comments

Comments
 (0)