导引:点亮什么、为什么、设计图
到 029 为止,内核有了一块「画布」——能在屏幕上画矩形、画字、
flip()刷新一帧。但那张画是死的:开机画一次,之后就再也不变。你动鼠标没反应,敲键盘没反应,更别提什么窗口。这一章,我们把这块静态画布变成一个会动的桌面:接上第一个指针设备(PS/2 鼠标),搭起一套统一的事件管线,做出能拖动、能关闭、有 Z 序的窗口,再让一个窗口管理器每帧把所有窗口合成到屏幕上。做完这一步,内核第一次有了「图形交互」——你点哪儿,它知道。
这一章我们要点亮什么
一件最直观的事:GUI 模式开机后,屏幕上出现三个错落的窗口;你拖动标题栏,窗口跟着鼠标走;点窗口把它顶到最前面;点右上角红叉把它关掉。整个链路是:
你动鼠标 → PS/2 鼠标发 IRQ12 → 我们的 handler 读 3 字节包、解出 dx/dy/按键
→ 入一个统一事件队列 EventQueue
PIT 每个滴答 → 把队列里的事件排空 → 交给窗口管理器
→ 窗口管理器算命中、改窗口位置 → 把所有窗口按 Z 序重新合成到屏幕 → flip()这件「拖窗口」的小事背后,点亮了好几个全新的东西:
- 第一个指针设备。前面 014 接过键盘(IRQ1),这一章接鼠标(IRQ12)。和键盘一样是 PS/2,但鼠标有个键盘没有的麻烦:它只报告相对位移,你得自己累积成绝对坐标。
- 第一套统一事件系统。鼠标事件、键盘事件被收进同一个
EventQueue,由一个排空点统一消费。这是 GUI 内核里反复出现的「生产/消费」分离,和 014 的键盘 ring buffer 是同一思路的升级版。 - 第一个窗口管理器。Z 序、合成、命中测试、拖拽——一个窗口管理器该有的骨架,这一章全搭起来。
- 第一次被逼着正视 System V ABI 栈对齐。鼠标初始化时碰了 PS/2 控制器,触发了一个一直潜伏的栈对齐 bug,一开机就
#GP。这条调试线是这一章最有含金量的部分。
为什么现在需要它
回顾 029 给我们留下的东西:canvas.hpp 里的 Canvas 类,它有一个离屏的 back buffer、一个指向 framebuffer 的 front buffer,会 draw_pixel / draw_rect / draw_text,会 blit(把一块画布拷到另一块上)、会 flip()(把 back buffer 拷到屏幕)。gui_init() 用它画了 10 个随机色矩形 + 一句 Cinux GUI,然后 flip() 一下,完事。
问题很明显:
第一,没有输入。这块画布画完就不动了,内核对外面发生什么一无所知。要交互,得先有「能报告坐标和按键」的东西。
第二,没有「窗口」这个抽象。029 的画面是直接往整块屏幕画。可真正的桌面是「一块块独立的矩形区域叠在一起」,每个区域有自己的内容、能被拖动、能被点中、谁压在谁上面有讲究。没有窗口抽象,就没法谈交互——你连「点到的是哪个东西」都说不清。
所以 030 要补两层:底下是输入管线(鼠标驱动 + 事件队列),上面是窗口模型(Window + WindowManager)。两层合起来,画布才活起来。
030 还顺手给 Canvas 加了一个新构造函数 init(uint32_t w, uint32_t h),造一块不挂 framebuffer 的纯离屏画布:
void Canvas::init(uint32_t w, uint32_t h) {
front_buf_ = nullptr; // 没有 front buffer
width_ = w; height_ = h;
pitch_ = w * 4; // 4 字节/像素,无对齐填充
back_buf_ = new uint32_t[w * h];
memfill32(back_buf_, 0, w * h);
}为什么需要它?因为窗口要双缓冲:每个窗口先画到自己的离屏画布上(标题栏 + 内容),合成时再整块 blit 到屏幕。离屏画布的 flip() 是 no-op(没有 front buffer),完全合理。这个小改动是后面整个窗口模型的基石。
设计图
整个 030 的交互管线长这样:
展开代码 (共 28 行)收起代码
┌─────────────┐ IRQ1 ┌──────────────────────────┐
│ 键盘 PS/2 │────────▶│ Keyboard::irq1_handler │──┐
└─────────────┘ │ (双路分发:原队列 + GUI) │ │
└──────────────────────────┘ │
▼
┌─────────────┐ IRQ12 ┌──────────────────────────┐ enqueue
│ 鼠标 PS/2 │────────▶│ Mouse::irq12_handler │──────────┐
└─────────────┘ │ (3 字节包 → MouseEvent) │ │
└──────────────────────────┘ │
▼
┌─────────────────────┐
│ EventQueue (128) │ ← 统一事件队列
│ head_ / tail_ │
└──────────┬──────────┘
│ dequeue
┌──────────────────────────┐ │
PIT IRQ0 ──────────────▶│ gui_tick_callback │──────────┘
│ 排空队列 → handle_mouse │
│ → composite() │
└─────────────┬────────────┘
▼
┌──────────────────────────┐
│ WindowManager │
│ windows_[64] (Z 序) │
│ 命中 / 拖拽 / raise │
└─────────────┬────────────┘
▼
clear(桌面色) → 按 Z 序 blit 各窗口 → 画光标 → flip()窗口在 Z 序里的叠放,和合成时的绘制顺序,是理解「为什么点的是这个窗口」的关键:
windows_[] 数组: index 0 (最底) ............ index count-1 (最顶)
合成顺序: 先画 index 0 ──────────────────▶ 最后画 index count-1
命中测试顺序: 从 index count-1 ──────────────▶ 往下到 index 0 (顶上的先被命中)
┌──────────────┐
index 2 (顶) │ Window 3 │ ← 先画,但命中时最先检查
├──────┬───────┘
index 1 │ Win2 │
├──┬───┘
index 0 (底) │W1│ ← 最后检查的命中候选
└──┘数组的下标既表示 Z 序(0 最底),也直接决定了「画的时候从底往顶、点的时候从顶往底」这两条相反的扫描方向。这个对称性是窗口管理器最核心的设计。