Files
TermiNexus a3c1d221e6 fix(kbd,sched,scene3d): 键盘报告去重;用户线程 return 跳板修正;关窗时序
① 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。
2026-10-17 16:49:37 +08:00

264 lines
9.8 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)