Files
ViperOS/vpsdk/window.vp
T
TermiNexus 2f20cf42aa fix(vpui): 建窗失败改为可见错误上报;Scene3D 加分步诊断打点
背景:Tk.mainloop 的循环条件是 window.is_active(win_id),而 is_active 对 win_id < 0 直接返回 0 ⇒ 一旦 create_window 失败,app 一次循环都不进、一句日志都不打就 process.exit(0)(现场:Scene3D spawn 后静默退出,「没有窗口」就是这么来的)。改动:window.vp 的 _attach / _connect / create_window 四个失败返回点补 [win] 错误上报(open 失败 / mmap 失败 / desk 未就绪超时 / 请求环压入失败 / CREATED 超时),这些是错误可见性而非临时噪音,长期保留;Scene3D/main.vp 加 _dbg 分步打点(start / tk ok / canvas ok / scene ok / geometry 后的 win_id 与 buf / 渲染线程 / mainloop 返回),用于定位它到底死在哪一步,定位完成后撤除。
2026-10-16 20:51:07 +08:00

257 lines
9.5 KiB
Plaintext
Raw 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) -> 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
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) != 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):
_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)