① Enter 洪流:usb_kbd._on_int_complete 的按下沿依赖 kbd_prev_keycodes,而状态提交在函数末尾第三趟循环;kbd_report 是常驻 DMA 缓冲,按键保持/NAK/重复完成都会让回调反复看到同一份报告 ⇒ 每次中断都注入一次 Enter,Terminal 刷出 [cmd] enter,explorer 被拖死。新增 kbd_prev_sig(modifier+6 keycode 的标量签名)整份报告去重;同一趟循环里先提交 prev 再判定;全 0 报告只更新状态;抽出 _rearm() 保证所有出口都重排中断 IN 且 DMA 读取先于重排。② explorer 限流:kb_pend[64] + _deliver_kb,单轮最多投递 64 字节,ring_push 返回 -1 时停在当前字节(不再当成功丢弃),pending 清空才补读;鼠标取包同样 64 包/轮上限。③ 关窗卡死根因:process.vp 的 _tramp_code 是 mov eax,34; int 0x80; jmp \$,而 Syscall.THREAD_EXIT=60 ⇒ 用户线程 return 后走 syscall 34(无匹配分支)返回 Ring3 在 jmp \$ 空转,空转不再产生 syscall,而内核只在 syscall 返回路径消费 needs_reschedule ⇒ 协作式调度彻底停摆,整机冻结。sched.create_user_thread 在跳板映射后把立即数低字节改为 60(幂等)。④ 次生:主线程先退会立即回收自己的用户栈,而 Scene3D 渲染线程还引用其栈上对象。Scene3D 渲染线程退出改为显式 process.thread_exit() 并置 _render_done;主线程 mainloop 返回后先 yield 等渲染线程退出(上限 10000 次)再 exit。⑤ window.flush 在槽位 used==0 时直接返回;vpui.Tk.redraw 入口判 win_id 与 is_active,窗口已回收时不碰 arena 不 flush。
264 lines
9.8 KiB
Plaintext
264 lines
9.8 KiB
Plaintext
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 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 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)
|
||
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):
|
||
# TODO(S3c-2):光标形状目前由 explorer 统一画成方框,尚无按窗口切换的请求。
|
||
pass
|
||
|
||
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)
|