import vpsdk.desk as desk import vpsdk.process as process import vpsdk.stdlib as stdlib # 不能删:app 的裸名 malloc 靠它把 stdlib 拉进链接图 import vpsdk.devfs as devfs import string define STYLE_CLASSIC = 0 define STYLE_WIN10 = 1 define STYLE_WIN11 = 2 define STYLE_WIN7 = 3 define STYLE_WINXP = 4 define EVENT_TYPE_NONE = 0 define EVENT_TYPE_CLICK = 1 define EVENT_TYPE_HOVER = 2 define EVENT_TYPE_RELEASE = 3 define EVENT_TYPE_HOVER_LEAVE = 4 define EVENT_TYPE_KEY = 5 define EVENT_TYPE_KEY_RELEASE = 6 define EVENT_TYPE_PING = 7 define EVENT_TYPE_RESIZE = 8 define EVENT_TYPE_WHEEL = 9 define KEY_STATE_PRESS = 1 define KEY_STATE_RELEASE = 2 define KEY_STATE_REPEAT = 3 define CURSOR_DEFAULT = 0 define CURSOR_IBEAM = 1 define CURSOR_RESIZE_EW = 2 define CURSOR_RESIZE_NS = 3 define CURSOR_RESIZE_NWSE = 4 define CURSOR_HAND = 5 define CURSOR_RESIZE_NESW = 6 define VK_LEFT = -1 define VK_RIGHT = -2 define VK_UP = -3 define VK_DOWN = -4 define VK_HOME = -5 define VK_END = -6 define VK_DELETE = -7 define VK_PRIOR = -8 define VK_NEXT = -9 @__attribute__(packed) struct InputEvent: type: uint32 win_id: int x: int y: int button: uint8 _pad0: uint8 _pad1: uint8 _pad2: uint8 # ============================================================================ # 窗口层:不再走内核 UI syscall,全部经 /dev/desk 共享区与用户态图形壳 explorer # 通信(S3d)。**对外 API 与函数签名一字未改**,因此 vpui 与各 app 无需改动。 # # 数据面:get_winbuf 直接返回共享 arena 里属于本窗口那块像素缓冲的线性地址, # app 就地画;画完压一条 FLUSH 请求让 explorer 合成 —— 全程零像素拷贝。 # 控制面:请求环(app → explorer)+ **按 pid 分**的事件环(explorer → app): # 按 pid 而不是按 win_id,是因为建窗答复 CREATED 必须在 app 知道 win_id # 之前送达;也不能只用一个共享单环(那是单消费者结构,多 app 会互相抢)。 # ============================================================================ # 等 explorer 就绪的上限(每次 yield_cpu 约 10ms 真让出一次 ⇒ 4000 次 ≈ 40s) define DESK_ATTACH_TRIES = 4000 _base: uint64 = 0 # 共享区首地址(0 = 还没接上) _fd: int = 0 # /dev/desk 的 fd(仅 _state == 1 时有效;devfs fd 0 是合法值) _state: int = 0 # 0 = 还没连过;1 = 已连上;-1 = 连接失败(不再重试) _my_pid: int = -1 _msg: desk.DeskMsg _evt: desk.DeskMsg def _attach() -> int: """映射共享区并等 explorer 置 ready。返回 0 成功 / -1 失败(结果缓存,只做一次)。""" global _base, _state, _my_pid if _state == 1: return 0 if _state < 0: return -1 b: uint64 = desk.desk_map(_fd) if b == 0: devfs.serial_puts("[win] desk mmap FAILED\n") _state = -1 return -1 i: int = 0 while desk.desk_ready(b) == 0 and i < DESK_ATTACH_TRIES: process.yield_cpu() i += 1 if desk.desk_ready(b) == 0: devfs.serial_puts("[win] desk not ready (attach timeout)\n") _state = -1 return -1 _base = b _state = 1 return 0 def _connect() -> int: """懒连接:第一次真正用到窗口层时才 open /dev/desk。""" global _fd, _state, _my_pid if _state == 1: return 0 if _state < 0: return -1 fd: int = desk.desk_open() if fd < 0: devfs.serial_puts("[win] open /dev/desk FAILED\n") _state = -1 return -1 _fd = fd _my_pid = process.get_pid() return _attach() def _send(etype: uint32, win: int, a: int, b: int, c: int, d: int, title: const str, style: int = 0) -> int: """组一条请求压进请求环。title 可为 None(只对 CREATE / TITLE 有意义)。""" if _connect() != 0: return -1 _msg.type = etype _msg.win = win _msg.pid = _my_pid _msg.a = a _msg.b = b _msg.c = c _msg.d = d _msg.style = uint32(style) string.memset(&_msg.title, 0, desk.DESK_TITLE_MAX) if title is not None: i: int = 0 while i < desk.DESK_TITLE_MAX - 1: ch: char = title[i] if ch == 0: break _msg.title[i] = ch i += 1 return desk.ring_push(_base + desk.desk_req_ring_off(), _msg) def _slot(win: int) -> desk.WinSlot*: return desk.WinSlot*(_base + desk.desk_slot_off(win)) def create_window(x: int, y: int, w: int, h: int, title: const str, style: int) -> int: # 建窗失败必须**可见**:Tk.mainloop 的循环条件是 window.is_active(win_id),win_id = -1 # 时一次都不循环就返回 ⇒ app 一句日志都没有就 exit(现场:Scene3D spawn 后静默退出)。 if _connect() != 0: devfs.serial_puts("[win] create_window: connect FAILED\n") return -1 if _my_pid < 0: return -1 if _send(uint32(desk.DESK_REQ_CREATE), 0, x, y, w, h, title, style) != 0: devfs.serial_puts("[win] create_window: request ring push FAILED\n") return -1 # 等 CREATED 答复(win_id 由 explorer 分配)。建窗通常在 app 初始化阶段、 # 事件环是空的;这期间若弹到非 CREATED 事件(属于本进程的其它窗口输入)就丢弃, # 只影响建窗这一瞬的输入,且不会把别人的事件吃掉(事件环是按 pid 分的)。 i: int = 0 while i < DESK_ATTACH_TRIES: if desk.ring_pop(_base + desk.desk_evt_ring_off(_my_pid), &_evt) != 0: if _evt.type == uint32(desk.DESK_EVT_CREATED): return _evt.a process.yield_cpu() i += 1 devfs.serial_puts("[win] create_window: CREATED timeout\n") return -1 def get_winbuf(win_id: int) -> uint32*: """本窗口的像素缓冲(共享区 arena 里那块),app 就地在用户态画。 与旧实现的关键差别:旧版是 app 自己的 stdlib.malloc 缓冲 + flush 时整块拷给内核; 现在缓冲本身就在 explorer 能看到的共享区里,flush 只压一条"请合成"的请求。 """ if _connect() != 0: return None if win_id < 0 or win_id >= desk.DESK_MAX_WIN: return None s: desk.WinSlot* = _slot(win_id) if s.used == 0: return None return uint32*(_base + uint64(s.fb_off)) def get_win_size(win_id: int) -> int: """返回 (内容宽 << 16) | 内容高;内容高不含标题栏 —— 与旧接口同口径。""" if _connect() != 0: return 0 if win_id < 0 or win_id >= desk.DESK_MAX_WIN: return 0 s: desk.WinSlot* = _slot(win_id) if s.used == 0: return 0 return (int(s.fb_w) << 16) | (int(s.fb_h) & 0xFFFF) def flush(win_id: int): # 槽位已被 explorer 回收(用户点了关闭):直接安全返回,绝不再发合成请求。 if _connect() != 0: return if win_id < 0 or win_id >= desk.DESK_MAX_WIN: return s: desk.WinSlot* = _slot(win_id) if s.used == 0: return _send(uint32(desk.DESK_REQ_FLUSH), win_id, 0, 0, 0, 0, None) def is_active(win_id: int) -> int: if _connect() != 0: return 0 if win_id < 0 or win_id >= desk.DESK_MAX_WIN: return 0 s: desk.WinSlot* = _slot(win_id) if s.used == 0: return 0 return s.active def mainloop(win_id: int): while is_active(win_id) != 0: process.yield_cpu() def poll_event(evt: InputEvent*) -> int: """取一条属于本进程的事件并翻译成 InputEvent。返回 1 = 有事件,0 = 空。""" if _connect() != 0: return 0 if desk.ring_pop(_base + desk.desk_evt_ring_off(_my_pid), &_evt) == 0: return 0 t: uint32 = _evt.type evt.win_id = _evt.win evt.button = uint8(0) evt._pad0 = uint8(0) evt._pad1 = uint8(0) evt._pad2 = uint8(0) if t == uint32(desk.DESK_EVT_KEY): evt.type = uint32(EVENT_TYPE_KEY) evt.x = _evt.a # ASCII evt.y = _evt.b # keycode evt.button = uint8(KEY_STATE_PRESS) return 1 if t == uint32(desk.DESK_EVT_RESIZE): evt.type = uint32(EVENT_TYPE_RESIZE) evt.x = _evt.a # 新内容宽 evt.y = _evt.b # 新内容高 return 1 if t == uint32(desk.DESK_EVT_MOUSE): kind: int = _evt.a if kind == desk.DESK_MOUSE_PRESS: evt.type = uint32(EVENT_TYPE_CLICK) elif kind == desk.DESK_MOUSE_RELEASE: evt.type = uint32(EVENT_TYPE_RELEASE) elif kind == desk.DESK_MOUSE_WHEEL: evt.type = uint32(EVENT_TYPE_WHEEL) else: evt.type = uint32(EVENT_TYPE_HOVER) evt.x = _evt.b # 窗口内坐标 evt.y = _evt.c evt.button = uint8(_evt.d) return 1 if t == uint32(desk.DESK_EVT_CLOSE): # 关闭只作为通知发一次;窗口的 usable 状态由 explorer 置 active=0 表达, # 因此 vpui 的 `while is_active()` 会自然退出。这里给 NONE 让调用方忽略。 evt.type = uint32(EVENT_TYPE_NONE) return 1 return 0 def set_cursor_type(win_id: int, cursor_t: int): """请求窗口内光标形状(输入框 I 形、按钮手指、窗口边缘缩放等)。""" if _connect() != 0: return if win_id < 0 or win_id >= desk.DESK_MAX_WIN: return _send(uint32(desk.DESK_REQ_CURSOR), win_id, cursor_t, 0, 0, 0, None) def set_title(win_id: int, title: const str) -> int: return _send(uint32(desk.DESK_REQ_TITLE), win_id, 0, 0, 0, 0, title) def set_geometry(win_id: int, x: int, y: int, w: int, h: int) -> int: """移动 + 换尺寸拆成两条请求(协议里 MOVE 与 RESIZE 是分开的)。""" r1: int = _send(uint32(desk.DESK_REQ_MOVE), win_id, x, y, 0, 0, None) r2: int = _send(uint32(desk.DESK_REQ_RESIZE), win_id, w, h, 0, 0, None) if r1 != 0 or r2 != 0: return -1 return 0 def set_resizable(win_id: int, rw: int, rh: int) -> int: # TODO(S3c-2):拖边缩放还没做,先接受调用并忽略(不返回错误,避免调用方误判建窗失败)。 return 0 def destroy_window(win_id: int) -> int: """主动销毁窗口(app 退出前调用;服务端会释放槽位并重画该区域)。""" return _send(uint32(desk.DESK_REQ_DESTROY), win_id, 0, 0, 0, 0, None)