From 6f856992da8330bf76d586d4be7f1c620322fc36 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 29 Oct 2026 00:42:21 +0800 Subject: [PATCH] feat: Linux ABI P0-P6, BMIDE DMA, ImageSee, 4KiB page pool and Explorer hardening --- .gitignore | 6 +- Apps/Explorer/desk_server.vp | 283 +- Apps/ImageSee/linker.ld | 39 + Apps/ImageSee/main.vp | 146 + Apps/ImageSee/project.vpj | 31 + Assets/background.jpg | Bin 0 -> 18568 bytes Assets/images/gray.jpg | Bin 0 -> 10556 bytes Assets/images/inter.png | Bin 0 -> 159870 bytes Assets/images/pal.png | Bin 0 -> 3779 bytes Assets/images/photo.jpg | Bin 0 -> 16804 bytes Assets/images/sample.png | Bin 0 -> 10366 bytes README.md | 12 +- Scripts/disk.ps1 | 79 + Scripts/gen_imagesee_assets.py | 170 + Scripts/syscall_table.py | 185 + VKernel/Kernel/drivers/devfs/devfs.vp | 29 +- .../Kernel/drivers/fs/fat32/fat32_diskio.vp | 10 +- .../Kernel/drivers/fs/fat32/fat32_types.vp | 37 + VKernel/Kernel/drivers/storage/ide/ide.vp | 274 +- VKernel/Kernel/execrunner/elf.vp | 266 +- VKernel/Kernel/intr/syscall.vp | 2022 +++++++- VKernel/Kernel/main.vp | 31 +- VKernel/Kernel/mm/mm.vp | 198 +- VKernel/Kernel/paging/paging.vp | 210 +- VKernel/Kernel/sched/process.vp | 135 +- VKernel/Kernel/sched/sched.vp | 111 +- VKernel/project.vpj | 4 +- abi/__init__.vp | 2 + abi/linux.vp | 206 + build.py | 67 +- docs/imagesee.md | 114 + docs/syscall.md | 382 ++ vpsdk/fs.vp | 98 +- vpsdk/imagesee/image.vp | 57 + vpsdk/imagesee/imagesee.vp | 67 + vpsdk/imagesee/inflate.vp | 261 ++ vpsdk/imagesee/jpeg.vp | 781 +++ vpsdk/imagesee/png.vp | 303 ++ vpsdk/process.vp | 30 +- vpsdk/syscall.vp | 349 +- vpsdk/vpui.vp | 4173 +++++++++-------- 41 files changed, 8697 insertions(+), 2471 deletions(-) create mode 100644 Apps/ImageSee/linker.ld create mode 100644 Apps/ImageSee/main.vp create mode 100644 Apps/ImageSee/project.vpj create mode 100644 Assets/background.jpg create mode 100644 Assets/images/gray.jpg create mode 100644 Assets/images/inter.png create mode 100644 Assets/images/pal.png create mode 100644 Assets/images/photo.jpg create mode 100644 Assets/images/sample.png create mode 100644 Scripts/gen_imagesee_assets.py create mode 100644 Scripts/syscall_table.py create mode 100644 abi/__init__.vp create mode 100644 abi/linux.vp create mode 100644 docs/imagesee.md create mode 100644 docs/syscall.md create mode 100644 vpsdk/imagesee/image.vp create mode 100644 vpsdk/imagesee/imagesee.vp create mode 100644 vpsdk/imagesee/inflate.vp create mode 100644 vpsdk/imagesee/jpeg.vp create mode 100644 vpsdk/imagesee/png.vp diff --git a/.gitignore b/.gitignore index 70fd5c5..ffdf319 100644 --- a/.gitignore +++ b/.gitignore @@ -97,4 +97,8 @@ _UNCADK/ # 编译 **/output/ **/temp/ -**/__vpcbuild__/ \ No newline at end of file +**/__vpcbuild__/ +# 本地大文件 / 临时文件 +Vmware/ +NotoSansSC/ +1.py diff --git a/Apps/Explorer/desk_server.vp b/Apps/Explorer/desk_server.vp index 8cabf4e..bad5281 100644 --- a/Apps/Explorer/desk_server.vp +++ b/Apps/Explorer/desk_server.vp @@ -5,6 +5,9 @@ import vpsdk.process as process import vpsdk.syscall as syscall import vpsdk.vpui as vpui # 复用它的 8x16 点阵字体表 import vpsdk.fonttools.fonttools as fonttools +import vpsdk.imagesee.imagesee as imagesee +import vpsdk.imagesee.image as image +import vpsdk.stdlib as stdlib from stdint import * import string @@ -55,6 +58,10 @@ define MIN_WIN_W = 160 define MIN_WIN_H = 100 # 窗口阴影(0 = 关闭) define SHADOW_ENABLED = 1 +# 画面同步开关: +# 1 = 每帧合成前等待垂直回扫(减少撕裂,VMware 下可能受 0x3DA 可用性影响) +# 0 = 不等回扫,最低输入延迟(拖动最跟手,但可能出现撕裂) +define SCREEN_SYNC = 1 define SHADOW_LEFT = 8 define SHADOW_RIGHT = 12 define SHADOW_TOP = 4 @@ -262,6 +269,35 @@ def _corner_alpha(px: int, py: int, cx: int, cy: int, r: int) -> int: return 255 +# --------------------------------------------------------------------------- +# 把已合成的一整段像素提交到 framebuffer。 +# +# 用 non-temporal store(movnti)+ sfence:即使 framebuffer 是回写映射, +# 也绕过数据缓存直接写到内存,扫描输出能立刻看到;同时 8 字节顺序写会合并 +# 成写合并缓冲,比逐字节/逐像素写快得多。QEMU TCG 下等价于普通写,不受影响。 +# --------------------------------------------------------------------------- +def _fb_copy(dst: uint64, src: uint64, nbytes: uint64): + if nbytes == 0: return + if (nbytes & uint64(7)) == 0: + qwords: uint64 = nbytes >> 3 + __asm__(f"""mov rdi, {asminp(dst, ASM_DESCR.REG_ANY)} + mov rsi, {asminp(src, ASM_DESCR.REG_ANY)} + mov rcx, {asminp(qwords, ASM_DESCR.REG_ANY)} + Lfbcopy: + mov rax, [rsi] + movnti qword ptr [rdi], rax + add rsi, 8 + add rdi, 8 + dec rcx + jnz Lfbcopy + sfence""", + op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX, + ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDI, + ASM_DESCR.CLOBBER_RSI]) + else: + string.memcpy(rawptr(dst), rawptr(src), nbytes) + + class DeskServer: # 屏幕 fb: uint32* # /dev/fb0 映射后的线性地址 @@ -271,6 +307,7 @@ class DeskServer: pitch_px: int fmt: uint32 # 0 = RGBA, 1 = BGRA bg_row: uint32* # 一行背景的预构造副本 + bg_full: uint32* # 壁纸(整屏);None = 使用 bg_row 纯色背景 back: uint32* # 后备缓冲:绘制先落这里,整帧后逐行拷到 fb # 共享区 shm: uint64 @@ -310,6 +347,14 @@ class DeskServer: resize_oh: int resize_sx: int resize_sy: int + # 内容双缓冲:vis_* 是当前正在合成的内容;s.fb_off/fb_w/fb_h 是应用正在 + # 绘制的新内容。应用 FLUSH 后才交换,避免 resize 过程中按新步长读旧内容。 + vis_off: [desk.DESK_MAX_WIN]uint32 + vis_cap: [desk.DESK_MAX_WIN]uint32 + vis_w: [desk.DESK_MAX_WIN]int + vis_h: [desk.DESK_MAX_WIN]int + spare_off: [desk.DESK_MAX_WIN]uint32 + spare_cap: [desk.DESK_MAX_WIN]uint32 # 标题栏按钮的 hover 状态(用于变色和刷新) hover_btn_win: int hover_btn_kind: int @@ -323,6 +368,79 @@ class DeskServer: return (uint32(255) << 24) | (r << 16) | (g << 8) | b return (uint32(255) << 24) | (b << 16) | (g << 8) | r + # 桌面壁纸:/sys/images/screen/background.jpg,等比“ cover”裁切铺满屏幕。 + # 加载失败时保持 init() 里预构造的纯色 bg_row,不影响启动。 + def _load_background(self): + img: image.Image* = imagesee.load("/sys/images/screen/background.jpg") + if img is None: + # FAT32 is shared with other booting apps; retry once before + # falling back to the solid color. + process.yield_cpu() + img = imagesee.load("/sys/images/screen/background.jpg") + if img is None: + devfs.serial_puts("[explorer] background: not found (solid color)\n") + return + w: int = self.fb_w + h: int = self.fb_h + if w <= 0 or h <= 0: return + buf: uint32* = uint32*(stdlib.malloc(uint64(w * h * 4))) + if buf is None: return + iw: int = img.width + ih: int = img.height + if iw <= 0 or ih <= 0: return + sw: int = iw + sh: int = ih + ox: int = 0 + oy: int = 0 + if iw * h > ih * w: + # 图片更宽:按高度铺满,左右裁切 + sw = (ih * w) / h + if sw < 1: sw = 1 + ox = (iw - sw) / 2 + else: + sh = (iw * h) / w + if sh < 1: sh = 1 + oy = (ih - sh) / 2 + xoff: int32* = int32*(stdlib.malloc(uint64(w * 4))) + if xoff is None: return + x: int = 0 + while x < w: + sx: int = ox + (x * sw) / w + if sx < 0: sx = 0 + if sx >= iw: sx = iw - 1 + xoff[x] = int32(sx) + x = x + 1 + y: int = 0 + while y < h: + sy: int = oy + (y * sh) / h + if sy < 0: sy = 0 + if sy >= ih: sy = ih - 1 + srow: int = sy * iw + drow: int = y * w + if self.fmt == uint32(1): + # ImageSee 的 0xAARRGGBB 与 BGRA framebuffer 内存布局一致 + x = 0 + while x < w: + buf[drow + x] = img.pixels[srow + int(xoff[x])] | uint32(0xFF000000) + x = x + 1 + else: + x = 0 + while x < w: + c: uint32 = img.pixels[srow + int(xoff[x])] + r: uint32 = (c >> 16) & uint32(0xFF) + g: uint32 = (c >> 8) & uint32(0xFF) + b: uint32 = c & uint32(0xFF) + buf[drow + x] = (uint32(0xFF) << 24) | (b << 16) | (g << 8) | r + x = x + 1 + y = y + 1 + self.bg_full = buf + devfs.serial_puts("[explorer] background: image loaded\n") + + def bg_src(self, y: int) -> uint32*: + if self.bg_full is not None: + return uint32*(uint64(self.bg_full) + uint64(y * self.fb_w) * uint64(4)) + return self.bg_row + # ---- 按 alpha/255 把 fg 混到 bg 上(RGBA/BGRA 同为 R/B@0,16、G@8,可共用 SWAR)---- def _blend(self, bg: uint32, fg: uint32, a: int) -> uint32: ia: int = 255 - a @@ -370,6 +488,14 @@ class DeskServer: self.resize_oh = 0 self.resize_sx = 0 self.resize_sy = 0 + i0: int = 0 + for i0 in range(desk.DESK_MAX_WIN): + self.vis_off[i0] = uint32(0) + self.vis_cap[i0] = uint32(0) + self.vis_w[i0] = 0 + self.vis_h[i0] = 0 + self.spare_off[i0] = uint32(0) + self.spare_cap[i0] = uint32(0) self.hover_btn_win = -1 self.hover_btn_kind = 0 # 预构造一行背景,渲染时整行 memcpy @@ -380,6 +506,9 @@ class DeskServer: col: uint32 = self.rgb(uint32(25), uint32(25), uint32(35)) for i in range(w): self.bg_row[i] = col + # 可选壁纸:整屏缓冲,失败则继续用上面的纯色行 + self.bg_full = None + self._load_background() # 后备缓冲:整屏像素,行距 = fb_w(与 framebuffer 的 pitch 无关) back_va: uint64 = syscall.mmap(0, uint64(w) * uint64(h) * uint64(4), uint64(syscall.PROT_READ | syscall.PROT_WRITE), @@ -517,24 +646,41 @@ class DeskServer: # 调整窗口外框几何。像素缓冲只在容量不足时扩容:arena 是 bump 分配, # 容量内复用可避免反复最大化/还原把 arena 耗尽。返回 1 成功 / 0 失败。 - def _set_window_rect(self, s: desk.WinSlot*, x: int, y: int, w: int, h: int) -> int: + def _set_window_rect(self, s: desk.WinSlot*, win: int, x: int, y: int, w: int, h: int) -> int: ch: int = self._content_h(h) need: int = w * ch if w <= 0 or need <= 0: return 0 - if need > int(s.fb_cap): - # 交互缩放时按 1.5 倍增长,避免拖拽过程中每加几像素就切一块新 arena; - # 最大化的一次性大跨度仍会直接按 need 分配。 - alloc_px: int = need - grow: int = int(s.fb_cap) + int(s.fb_cap) / 2 - if grow > alloc_px: alloc_px = grow - off: uint32 = desk.desk_arena_alloc(self.shm, uint32(alloc_px * 4)) - if off == uint32(0): return 0 - s.fb_off = off - s.fb_cap = uint32(alloc_px) - b: uint32* = uint32*(self.shm + uint64(off)) - col: uint32 = self.rgb(uint32(30), uint32(30), uint32(50)) - for k in range(alloc_px): - b[k] = col + # 纯移动:内容尺寸没变,不切缓冲 + if w == int(s.fb_w) and ch == int(s.fb_h): + s.ux = x + s.uy = y + s.w = w + s.h = h + return 1 + target_off: uint32 = s.fb_off + target_cap: uint32 = s.fb_cap + # 当前绘制目标若正在显示(上次已 FLUSH),或容量不足,就换到备用/新缓冲。 + # 这样 resize 期间合成器仍读旧 vis 缓冲,绝不会按新步长读旧内容。 + if target_off == self.vis_off[win] or need > int(target_cap): + if self.spare_off[win] != uint32(0) and need <= int(self.spare_cap[win]): + target_off = self.spare_off[win] + target_cap = self.spare_cap[win] + self.spare_off[win] = uint32(0) + self.spare_cap[win] = uint32(0) + else: + alloc_px: int = need + grow: int = int(target_cap) + int(target_cap) / 2 + if grow > alloc_px: alloc_px = grow + off: uint32 = desk.desk_arena_alloc(self.shm, uint32(alloc_px * 4)) + if off == uint32(0): return 0 + # 旧 target 还没显示过,可留作下一次 resize 的备用 + if s.fb_off != self.vis_off[win] and s.fb_off != uint32(0): + self.spare_off[win] = s.fb_off + self.spare_cap[win] = s.fb_cap + target_off = off + target_cap = uint32(alloc_px) + s.fb_off = target_off + s.fb_cap = target_cap s.ux = x s.uy = y s.w = w @@ -577,7 +723,7 @@ class DeskServer: if ny + nh > self.fb_h: nh = self.fb_h - ny if nx == s.ux and ny == s.uy and nw == s.w and nh == s.h: return self.mark_win(s.uy, s.h) - if self._set_window_rect(s, nx, ny, nw, nh) != 0: + if self._set_window_rect(s, self.resize_win, nx, ny, nw, nh) != 0: self.push_event(self.resize_win, uint32(desk.DESK_EVT_RESIZE), int(s.fb_w), int(s.fb_h), 0, 0) self.mark_win(s.uy, s.h) @@ -635,7 +781,7 @@ class DeskServer: mw: int = self.fb_w mh: int = self.fb_h - TASKBAR_H if mh < TITLE_H + TITLE_GAP + 1: return - if self._set_window_rect(s, 0, 0, mw, mh) == 0: return + if self._set_window_rect(s, win, 0, 0, mw, mh) == 0: return s.restore_x = ox s.restore_y = oy s.restore_w = ow @@ -652,7 +798,7 @@ class DeskServer: if s.used == 0 or s.maximized == 0: return 0 oy: int = s.uy oh: int = s.h - if self._set_window_rect(s, s.restore_x, s.restore_y, s.restore_w, s.restore_h) == 0: + if self._set_window_rect(s, win, s.restore_x, s.restore_y, s.restore_w, s.restore_h) == 0: return 0 s.maximized = 0 self.mark_win(oy, oh) @@ -727,6 +873,12 @@ class DeskServer: s.fb_cap = uint32(cap_px) s.fb_w = uint32(w) s.fb_h = uint32(content_h) + self.vis_off[i] = buf_off + self.vis_cap[i] = uint32(cap_px) + self.vis_w[i] = w + self.vis_h[i] = content_h + self.spare_off[i] = uint32(0) + self.spare_cap[i] = uint32(0) string.memset(&s.title, 0, desk.DESK_TITLE_MAX) # 像素缓冲先清成窗口底色,避免 app 还没画就露出垃圾 buf: uint32* = uint32*(self.shm + uint64(buf_off)) @@ -986,8 +1138,13 @@ class DeskServer: content_row: int = yy - content_y0 if row_is_title != 0 and title_alpha < 255: self._blend_row(wx + 1, yy, ww - 2, row_col, title_alpha) - elif rel >= TITLE_H + TITLE_GAP and rel < wh - 1 and content_row >= 0 and content_row < fh: - self.row_blit(wx + 1, yy, buf, content_row * fw + 1, ww - 2) + elif rel >= TITLE_H + TITLE_GAP and rel < wh - 1 and content_row >= 0 and content_row < fh and buf is not None: + blit_w: int = ww - 2 + if blit_w > fw - 2: blit_w = fw - 2 + if blit_w > 0: + self.row_blit(wx + 1, yy, buf, content_row * fw + 1, blit_w) + if blit_w < ww - 2: + self.row_fill(wx + 1 + blit_w, yy, (ww - 2) - blit_w, row_col) else: self.row_fill(wx, yy, ww, row_col) if rel == 0 or rel == wh - 1: @@ -1005,7 +1162,7 @@ class DeskServer: wh: int = s.h content_row: int = yy - content_y0 do_blit: int = 0 - if rel >= TITLE_H + TITLE_GAP and rel < wh - 1 and content_row >= 0 and content_row < fh: + if rel >= TITLE_H + TITLE_GAP and rel < wh - 1 and content_row >= 0 and content_row < fh and buf is not None: do_blit = 1 cyc: int = r if rel >= r: cyc = wh - r - 1 @@ -1026,7 +1183,7 @@ class DeskServer: if _is_in_corner(rx, rel, cxc, cyc, r) != 0: on_border: int = _is_on_border(rx, rel, ww, wh, r) idx: int = yy * self.fb_w + px - if on_border == 0 and do_blit != 0: + if on_border == 0 and do_blit != 0 and rx < fw: self.back[idx] = buf[content_row * fw + rx] px += 1 continue @@ -1052,7 +1209,12 @@ class DeskServer: if rel == 0 or rel == wh - 1: self.row_fill(mid0, yy, midw, outline_col) elif do_blit != 0: - self.row_blit(mid0, yy, buf, content_row * fw + r, midw) + mid_blit_w: int = midw + if mid_blit_w > fw - r: mid_blit_w = fw - r + if mid_blit_w > 0: + self.row_blit(mid0, yy, buf, content_row * fw + r, mid_blit_w) + if mid_blit_w < midw: + self.row_fill(mid0 + mid_blit_w, yy, midw - mid_blit_w, row_col) elif row_is_title != 0 and title_alpha < 255: self._blend_row(mid0, yy, midw, row_col, title_alpha) else: @@ -1069,7 +1231,7 @@ class DeskServer: if _is_in_corner(rx, rel, cxr, cyc, r) != 0: on_border: int = _is_on_border(rx, rel, ww, wh, r) idx: int = yy * self.fb_w + px - if on_border == 0 and do_blit != 0: + if on_border == 0 and do_blit != 0 and rx < fw: self.back[idx] = buf[content_row * fw + rx] px += 1 continue @@ -1446,7 +1608,7 @@ class DeskServer: w: int = order[i] if yy < sy[w] or yy >= sy[w] + sh[w]: continue if sx[w] > xx: - self.row_blit(xx, yy, self.bg_row, xx, sx[w] - xx) + self.row_blit(xx, yy, self.bg_src(yy), xx, sx[w] - xx) ex: int = sx[w] + sw[w] if ex > xx: xx = ex if smode[w] != 0: @@ -1457,9 +1619,9 @@ class DeskServer: elif rel < WIN_RADIUS or rel >= sh[w] - WIN_RADIUS: need = 1 if need != 0: - self.row_blit(sx[w], yy, self.bg_row, sx[w], sw[w]) + self.row_blit(sx[w], yy, self.bg_src(yy), sx[w], sw[w]) if xx < self.fb_w: - self.row_blit(xx, yy, self.bg_row, xx, self.fb_w - xx) + self.row_blit(xx, yy, self.bg_src(yy), xx, self.fb_w - xx) yy += 1 # 2) 窗口 for i in range(cnt): @@ -1546,13 +1708,22 @@ class DeskServer: self._draw_shadow(s, y0, y1, shadow_r) if wy + wh <= y0 or wy >= y1: return - buf: uint32* = uint32*(self.shm + uint64(s.fb_off)) - fw: int = int(s.fb_w) - fh: int = int(s.fb_h) - # 槽表被写坏时宁可跳过该窗口,也不要按越界 fb_off 读像素把整个合成器带崩。 - need_bytes: uint64 = uint64(fw) * uint64(fh) * uint64(4) - if s.fb_off < uint32(desk.DESK_OFF_ARENA) or uint64(s.fb_off) + need_bytes > uint64(desk.DESK_SHM_SIZE): - return + # 合成器只读“最后一次 FLUSH 的可见缓冲”。resize 期间 s.fb_* 已经是新尺寸, + # 但应用还没画完,绝不能拿新步长去读旧缓冲(否则就是截图里的乱码)。 + v_off: uint32 = self.vis_off[win] + fw: int = self.vis_w[win] + fh: int = self.vis_h[win] + buf: uint32* = None + if v_off >= uint32(desk.DESK_OFF_ARENA) and fw > 0 and fh > 0: + need_bytes: uint64 = uint64(fw) * uint64(fh) * uint64(4) + if uint64(v_off) + need_bytes <= uint64(desk.DESK_SHM_SIZE): + buf = uint32*(self.shm + uint64(v_off)) + else: + fw = 0 + fh = 0 + else: + fw = 0 + fh = 0 content_y0: int = wy + th + TITLE_GAP r: int = WIN_RADIUS yy: int = y0 @@ -1667,10 +1838,13 @@ class DeskServer: def render(self): if self.dirty_any == 0: return - devfs.fb_wait_vblank(self.fb_fd) - # 逐脏段渲染并立刻提交。**不要**合并成"最小脏行到最大脏行"的一整段:窗口在顶部、 - # 任务栏在底部时那会退化成每帧全屏渲染 + 全屏提交(4MB memcpy),帧率直接崩。 - # 段内先全画进 back 再逐行提交,保证每一行提交出去的都是完整一帧的内容。 + if SCREEN_SYNC != 0: + # 可选:等垂直回扫,减少撕裂。内核侧是有界轮询(2048 次), + # 不会像旧版 2×100000 次端口读那样拖慢一帧。 + devfs.fb_wait_vblank(self.fb_fd) + # 逐脏段渲染并提交。**不要**合并成"最小脏行到最大脏行"的一整段:窗口在顶部、 + # 任务栏在底部时那会退化成每帧全屏渲染 + 全屏提交,帧率直接崩。 + # 段内先全画进 back 再整段提交,保证提交出去的每一行都是完整一帧的内容。 y: int = 0 while y < self.fb_h: if self.dirty[y] == 0: @@ -1681,14 +1855,19 @@ class DeskServer: y += 1 y1: int = y self.render_range(y0, y1) - yy: int = y0 - while yy < y1: - if self.dirty[yy] != 0: - # fb 行距 = pitch_px,back 行距 = fb_w - string.memcpy(rawptr(uint64(self.fb) + uint64(yy * self.pitch_px) * uint64(4)), - rawptr(uint64(self.back) + uint64(yy * self.fb_w) * uint64(4)), - uint64(self.fb_w) * uint64(4)) - yy += 1 + # back 与 fb 行距一致时整段一次 memcpy:大范围拖动时比逐行 1080 次 + # rep movsb 少很多固定开销;行距不同才退回逐行。 + if self.pitch_px == self.fb_w: + _fb_copy(uint64(self.fb) + uint64(y0 * self.pitch_px) * uint64(4), + uint64(self.back) + uint64(y0 * self.fb_w) * uint64(4), + uint64((y1 - y0) * self.fb_w) * uint64(4)) + else: + yy: int = y0 + while yy < y1: + _fb_copy(uint64(self.fb) + uint64(yy * self.pitch_px) * uint64(4), + uint64(self.back) + uint64(yy * self.fb_w) * uint64(4), + uint64(self.fb_w) * uint64(4)) + yy += 1 self.clear_dirty() # ---------------- 事件投递 ---------------- @@ -1806,7 +1985,7 @@ class DeskServer: self.mark_win(s.uy, s.h) elif ty == uint32(desk.DESK_REQ_RESIZE): self.mark_win(s.uy, s.h) - if self._set_window_rect(s, s.ux, s.uy, m.a, m.b) != 0: + if self._set_window_rect(s, win, s.ux, s.uy, m.a, m.b) != 0: s.maximized = 0 self.mark_win(s.uy, s.h) elif ty == uint32(desk.DESK_REQ_TITLE): @@ -1819,6 +1998,16 @@ class DeskServer: if self.hit_test(self.cx, self.cy) == win: self._mark_cursor_band() elif ty == uint32(desk.DESK_REQ_FLUSH): + # 应用已经按 s.fb_w/fb_h 画完一帧:此刻才把"正在显示的内容"切到 + # 新缓冲。旧的 vis 缓冲留作下一次 resize 的备用。 + old_off: uint32 = self.vis_off[win] + if old_off != s.fb_off: + self.spare_off[win] = old_off + self.spare_cap[win] = self.vis_cap[win] + self.vis_off[win] = s.fb_off + self.vis_cap[win] = s.fb_cap + self.vis_w[win] = int(s.fb_w) + self.vis_h[win] = int(s.fb_h) # 最小化窗口仍在后台绘制/FLUSH:不要置脏,否则合成器每帧都要重画背景。 if s.minimized == 0: self.mark(s.uy, s.h) diff --git a/Apps/ImageSee/linker.ld b/Apps/ImageSee/linker.ld new file mode 100644 index 0000000..0d5a4d5 --- /dev/null +++ b/Apps/ImageSee/linker.ld @@ -0,0 +1,39 @@ +ENTRY(__entry) + +SECTIONS { + . = 0x400000; + + .text : { + __entry = .; + *(.text.startup) + *(.text) + *(.text.*) + } + + .rodata : { + *(.rodata) + *(.rodata.*) + } + + .data : { + *(.data) + *(.data.*) + } + + .bss : { + __bss_start = .; + *(.bss) + *(.bss.*) + __bss_end = .; + } + + /DISCARD/ : { + *(.comment) + *(.note) + *(.eh_frame) + *(.eh_frame_hdr) + *(.reloc) + *(.rela) + *(.debug*) + } +} diff --git a/Apps/ImageSee/main.vp b/Apps/ImageSee/main.vp new file mode 100644 index 0000000..cfc5b43 --- /dev/null +++ b/Apps/ImageSee/main.vp @@ -0,0 +1,146 @@ +# ImageSee demo: decode PNG and JPEG with vpsdk.imagesee and show them. + +import vpsdk.vpui as vpui +import vpsdk.window as window +import vpsdk.process as process +import vpsdk.stdlib as stdlib +import vpsdk.imagesee.imagesee as imagesee +import vpsdk.imagesee.image as im +import vpsdk.stdio as stdio +import string +from stdint import * + +define WIN_X = 40 +define WIN_Y = 36 +define WIN_W = 1120 +define WIN_H = 616 +define CANVAS_W = 536 +define CANVAS_H = 520 +define BG = 0xFF14141E +define PANEL = 0xFF20202C +define BORDER = 0xFF3A3A50 + +# Fill dst[0..w*h) with a dark checkerboard so transparent parts are visible. +def _checker(dst: uint32*, w: int, h: int): + y: int = 0 + while y < h: + x: int = 0 + while x < w: + c: uint32 = uint32(PANEL) + if ((x >> 4) + (y >> 4)) & 1: + c = uint32(BG) + dst[y * w + x] = c + x = x + 1 + y = y + 1 + +def _draw_fit(dst: uint32*, dw: int, dh: int, img: im.Image*, + ox: int, oy: int, bw: int, bh: int): + if img is None: return + tw: int = bw + th: int = (tw * img.height) / img.width + if th > bh: + th = bh + tw = (th * img.width) / img.height + if tw < 1 or th < 1: return + x0: int = ox + (bw - tw) / 2 + y0: int = oy + (bh - th) / 2 + y: int = 0 + while y < th: + sy: int = (y * img.height) / th + if sy >= img.height: sy = img.height - 1 + x: int = 0 + while x < tw: + sx: int = (x * img.width) / tw + if sx >= img.width: sx = img.width - 1 + ty: int = y0 + y + tx: int = x0 + x + if tx >= 0 and ty >= 0 and tx < dw and ty < dh: + dst[ty * dw + tx] = img.pixels[sy * img.width + sx] + x = x + 1 + y = y + 1 + +def _border(dst: uint32*, dw: int, dh: int, x: int, y: int, w: int, h: int, c: uint32): + i: int = 0 + while i < w: + if x + i >= 0 and x + i < dw: + if y >= 0 and y < dh: dst[y * dw + x + i] = c + if y + h - 1 >= 0 and y + h - 1 < dh: dst[(y + h - 1) * dw + x + i] = c + i = i + 1 + i = 0 + while i < h: + if y + i >= 0 and y + i < dh: + if x >= 0 and x < dw: dst[(y + i) * dw + x] = c + if x + w - 1 >= 0 and x + w - 1 < dw: dst[(y + i) * dw + x + w - 1] = c + i = i + 1 + +def _report(path: const str) -> im.Image*: + img: im.Image* = imagesee.load(path) + if img is None: + stdio.printf("[imagesee] %s: FAILED stage=%d\n", path, imagesee.last_error()) + return None + stdio.printf("[imagesee] %s: %s %dx%d sum=%x\n", + path, imagesee.format_name(img), img.width, img.height, + imagesee.checksum(img)) + return img + +def _panel_failed(dst: uint32*, w: int, h: int): + y: int = 0 + while y < h: + x: int = 0 + while x < w: + c: uint32 = uint32(0xFF300018) + if ((x >> 5) + (y >> 5)) & 1: + c = uint32(0xFF001830) + dst[y * w + x] = c + x = x + 1 + y = y + 1 + +@__attribute__(section(".text.startup"), aligned(16)) +export def _start() -> int: + stdio.printf("[imagesee] app start\n") + + png_img: im.Image* = _report("/IMAGES/SAMPLE.PNG") + jpg_img: im.Image* = _report("/IMAGES/PHOTO.JPG") + _report("/IMAGES/GRAY.JPG") + _report("/IMAGES/PAL.PNG") + _report("/IMAGES/INTER.PNG") + + root: vpui.Tk = vpui.Tk() + root.title("ImageSee - PNG | JPEG") + root.set_style(window.STYLE_WIN11) + root.geometry(WIN_X, WIN_Y, WIN_W, WIN_H) + root.bg_color = uint32(BG) + master: vpui.Tk* = &root + + title: vpui.Label = vpui.Label(master, "ImageSee left: PNG right: JPEG", uint32(0xFFE0E0F0)) + root.pack(0, vpui.PACK_TOP, vpui.FILL_X, 0, 12, 6) + + fb1: uint32* = uint32*(stdlib.malloc(uint64(CANVAS_W * CANVAS_H * 4))) + fb2: uint32* = uint32*(stdlib.malloc(uint64(CANVAS_W * CANVAS_H * 4))) + if fb1 is None or fb2 is None: + process.exit(1) + return 0 + _checker(fb1, CANVAS_W, CANVAS_H) + _checker(fb2, CANVAS_W, CANVAS_H) + if png_img is None: + _panel_failed(fb1, CANVAS_W, CANVAS_H) + else: + _draw_fit(fb1, CANVAS_W, CANVAS_H, png_img, 8, 8, CANVAS_W - 16, CANVAS_H - 16) + if jpg_img is None: + _panel_failed(fb2, CANVAS_W, CANVAS_H) + else: + _draw_fit(fb2, CANVAS_W, CANVAS_H, jpg_img, 8, 8, CANVAS_W - 16, CANVAS_H - 16) + _border(fb1, CANVAS_W, CANVAS_H, 0, 0, CANVAS_W, CANVAS_H, uint32(BORDER)) + _border(fb2, CANVAS_W, CANVAS_H, 0, 0, CANVAS_W, CANVAS_H, uint32(BORDER)) + + c1: vpui.FBCanvas = vpui.FBCanvas(master, CANVAS_W, CANVAS_H) + c1.set_fb(fb1, CANVAS_W, CANVAS_H) + root.pack(1, vpui.PACK_LEFT, 0, 1, 10, 8) + c2: vpui.FBCanvas = vpui.FBCanvas(master, CANVAS_W, CANVAS_H) + c2.set_fb(fb2, CANVAS_W, CANVAS_H) + root.pack(2, vpui.PACK_LEFT, 0, 1, 10, 8) + + stdio.printf("[imagesee] window ready\n") + root.mainloop() + process.exit(0) + return 0 diff --git a/Apps/ImageSee/project.vpj b/Apps/ImageSee/project.vpj new file mode 100644 index 0000000..28d1a27 --- /dev/null +++ b/Apps/ImageSee/project.vpj @@ -0,0 +1,31 @@ +{ + "name": "ImageSee", + "version": "1.0.0", + "source_dir": "./", + "build_dir": "./__vpcbuild__", + "compiler": { + "cmd": "llc", + "flags": ["-filetype=obj", "-mtriple=x86_64-none-elf", "-relocation-model=static", "-O2"] + }, + "linker": { + "cmd": "ld.lld.exe", + "flags": [ + "-m", "elf_x86_64", + "-T", "linker.ld", + "--oformat", "elf64-x86-64", + "--strip-all" + ], + "output": "imagesee.elf" + }, + "includes": ["../..", "../../../../includes"], + "target": { + "triple": "x86_64-none-elf", + "datalayout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" + }, + "options": { + "slice_level": 3, + "target": "llvm", + "strict_mode": true + } +} + diff --git a/Assets/background.jpg b/Assets/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d21f4a1b3142bc833fc0e5fff06ad69ea7ba5b6f GIT binary patch literal 18568 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 P2K+QtbyaGWY~cR^1GE@` literal 0 HcmV?d00001 diff --git a/Assets/images/pal.png b/Assets/images/pal.png new file mode 100644 index 0000000000000000000000000000000000000000..860c11b9b64ad4ed7000b1fd171d9b52d21b8f65 GIT binary patch literal 3779 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 literal 0 HcmV?d00001 diff --git a/Assets/images/photo.jpg b/Assets/images/photo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d42ba69b5725370d4857def5ea327438ead4e7d9 GIT binary patch literal 16804 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 aOI#G6lnkQPPh9JgYEHDh^md^5@&5}~gVsy{ literal 0 HcmV?d00001 diff --git a/Assets/images/sample.png b/Assets/images/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..726e12545c7017bf8be6e6f0b6f6d85d0aa8bb4e GIT binary patch literal 10366 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 R=3XJ-m9mynxq?OT{|5~;YJUI# literal 0 HcmV?d00001 diff --git a/README.md b/README.md index ab15cd5..f902d40 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,11 @@ A bare-metal 64-bit operating system written in the [Viper](https://git.gvsds.co - **VKernel** —— 64 位微内核(x86_64-none-elf 目标) - **UEFI 引导器** —— `boot/boot.c`(GNU-EFI 编译为 `disk/EFI/BOOT/bootx64.efi`) - **FAT32 驱动** —— 自研实现,支持挂载、目录、文件操作 -- **4 个用户应用** —— HelloWorld / Terminal / Gargantua / Scene3D +- **7 个用户应用** —— HelloWorld / Terminal / Gargantua / Scene3D / Explorer / FontDemo / ImageSee - **1 个动态库** —— SerialLogger -- **ViperPOS SDK** —— 用户态开发库(系统调用、窗口、文件系统、进程) +- **ViperPOS SDK** —— 用户态开发库(系统调用、窗口、文件系统、进程、图像解码) +- **syscall ABI 计划** —— 与 Linux x86-64 对齐的路线见 [`docs/syscall.md`](docs/syscall.md) +- **ImageSee** —— PNG/JPEG 解码库与示例应用见 [`docs/imagesee.md`](docs/imagesee.md) ### 目录结构 @@ -185,9 +187,11 @@ Main components: - **VKernel** — 64-bit microkernel (target `x86_64-none-elf`) - **UEFI bootloader** — `boot/boot.c` (compiled by GNU-EFI to `disk/EFI/BOOT/bootx64.efi`) - **FAT32 driver** — self-implemented, supports mount/dir/file operations -- **4 user applications** — HelloWorld / Terminal / Gargantua / Scene3D +- **7 user applications** — HelloWorld / Terminal / Gargantua / Scene3D / Explorer / FontDemo / ImageSee - **1 dynamic library** — SerialLogger -- **ViperPOS SDK** — user-space development kit (syscall, window, fs, process) +- **ViperPOS SDK** — user-space development kit (syscall, window, fs, process, image decoding) +- **Syscall ABI plan** — Linux x86-64 alignment roadmap in [`docs/syscall.md`](docs/syscall.md) +- **ImageSee** — PNG/JPEG decoder library and demo app, see [`docs/imagesee.md`](docs/imagesee.md) ### Directory layout diff --git a/Scripts/disk.ps1 b/Scripts/disk.ps1 index 10971fa..6116a3f 100644 --- a/Scripts/disk.ps1 +++ b/Scripts/disk.ps1 @@ -88,3 +88,82 @@ Write-Host "Unmounting disk image..." imdisk -D -m $MOUNT_LETTER Write-Host "Disk image created: $DISK_IMG" -ForegroundColor Green + +# ========================================================== +# Refresh the VMware disk next to the .vmx +# ========================================================== +# Vmware/ViperOS.vmdk is a monolithicFlat descriptor; the data lives in +# ViperOS-flat.vmdk. Both are regenerated from disk.img so "Open VM" always +# sees the image that was just packed. The .vmx already points at +# ide0:0.fileName = "ViperOS.vmdk"; it uses EFI firmware for bootx64.efi. +$VMWARE_DIR = Join-Path $PROJECT_DIR "Vmware" +$VMWARE_VMDK = Join-Path $VMWARE_DIR "ViperOS.vmdk" +$VMWARE_FLAT = Join-Path $VMWARE_DIR "ViperOS-flat.vmdk" +$VMWARE_VMX = Join-Path $VMWARE_DIR "Viper OS.vmx" + +if (Test-Path $VMWARE_DIR) { + Write-Host "=== Updating VMware disk ===" -ForegroundColor Cyan + $vmRunning = Get-Process -Name "vmware-vmx" -ErrorAction SilentlyContinue + if ($vmRunning) { + Write-Host "Warning: a VMware VM is running; close it and re-run to refresh the VMDK." -ForegroundColor Yellow + } else { + # A leftover .lck from a crash keeps VMware from opening the VM. + $vmLock = "$VMWARE_VMX.lck" + if (Test-Path $vmLock) { + Write-Host "Removing stale VMware lock: $vmLock" -ForegroundColor Yellow + Remove-Item -Recurse -Force $vmLock -ErrorAction SilentlyContinue + } + $size = (Get-Item $DISK_IMG).Length + $sectors = [int64]($size / 512) + $cyl = [int64][math]::Floor($sectors / (16 * 63)) + + # Keep the existing CID/UUID so VMware treats it as the same disk. + $cid = "fffffffe" + $uuid = "60 00 C2 96 88 c0 09 6d-c8 30 f7 71 57 ce 47 0d" + $longid = "6af48e7def7fae6bfa57a4affffffffe" + if (Test-Path $VMWARE_VMDK) { + foreach ($l in (Get-Content $VMWARE_VMDK -ErrorAction SilentlyContinue)) { + if ($l -match '^CID=([0-9a-fA-F]+)') { $cid = $Matches[1] } + if ($l -match '^ddb\.uuid\s*=\s*"(.+)"') { $uuid = $Matches[1] } + if ($l -match '^ddb\.longContentID\s*=\s*"(.+)"') { $longid = $Matches[1] } + } + } + + $descriptor = @" +# Disk DescriptorFile +version=1 +encoding="GBK" +CID=$cid +parentCID=ffffffff +createType="monolithicFlat" + +# Extent description +RW $sectors FLAT "ViperOS-flat.vmdk" 0 + +# The Disk Data Base +#DDB + +ddb.adapterType = "ide" +ddb.geometry.cylinders = "$cyl" +ddb.geometry.heads = "16" +ddb.geometry.sectors = "63" +ddb.longContentID = "$longid" +ddb.uuid = "$uuid" +ddb.virtualHWVersion = "21" +"@ + + try { + if (Test-Path $VMWARE_FLAT) { Remove-Item -Force $VMWARE_FLAT } + Copy-Item -Force $DISK_IMG $VMWARE_FLAT + Set-Content -Path $VMWARE_VMDK -Value $descriptor -Encoding ASCII + if (-not (Select-String -Path $VMWARE_VMX -Pattern 'ide0:0\.fileName\s*=\s*"ViperOS\.vmdk"' -Quiet)) { + Write-Host "Note: $VMWARE_VMX does not reference ViperOS.vmdk; check ide0:0.fileName." -ForegroundColor Yellow + } + Write-Host "VMDK updated: $VMWARE_VMDK ($sectors sectors, $([math]::Round($size / 1MB)) MiB)" -ForegroundColor Green + } catch { + Write-Host "Warning: failed to refresh the VMware disk: $($_.Exception.Message)" -ForegroundColor Yellow + } + } +} else { + Write-Host "Note: Vmware directory not found; skipping VMDK update." -ForegroundColor DarkGray +} diff --git a/Scripts/gen_imagesee_assets.py b/Scripts/gen_imagesee_assets.py new file mode 100644 index 0000000..cfcd021 --- /dev/null +++ b/Scripts/gen_imagesee_assets.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Generate the sample images shipped with the ImageSee demo. + +Run from the ViperOS root: + python Scripts/gen_imagesee_assets.py + +The generated files live in Assets/images/ and are copied to /IMAGES on the +ViperOS disk by build.py. Requires Pillow (only for generating the assets; +the OS itself does not need it). +""" + +import os +import struct +import zlib +from PIL import Image, ImageDraw, ImageFilter + + +W, H = 320, 220 +OUT = os.path.join("Assets", "images") +BG_OUT = os.path.join("Assets", "background.jpg") + + +def test_pattern(): + img = Image.new("RGBA", (W, H), (0, 0, 0, 255)) + px = img.load() + for y in range(H): + for x in range(W): + r = (x * 255) // (W - 1) + g = (y * 255) // (H - 1) + b = 255 - ((x + y) * 255) // (W + H - 2) + px[x, y] = (r, g, b, 255) + d = ImageDraw.Draw(img) + d.rectangle([12, 12, 92, 72], fill=(255, 255, 255, 255)) + d.rectangle([22, 22, 82, 62], fill=(0, 0, 0, 255)) + d.rectangle([108, 12, 188, 72], fill=(230, 40, 40, 255)) + d.rectangle([198, 12, 278, 72], fill=(40, 200, 80, 255)) + d.rectangle([288, 12, 308, 72], fill=(40, 80, 230, 255)) + d.ellipse([40, 100, 160, 210], fill=(255, 220, 40, 255)) + d.ellipse([150, 90, 250, 190], outline=(255, 255, 255, 255), width=5) + d.line([0, H - 1, W - 1, 80], fill=(255, 0, 255, 255), width=3) + d.line([0, 80, W - 1, H - 1], fill=(0, 255, 255, 255), width=3) + for i in range(0, 40): + d.rectangle([280 - i, 160 + i, 286 - i, 166 + i], fill=(i * 6, i * 6, i * 6, 255)) + return img + + +def png_chunk(typ: bytes, data: bytes) -> bytes: + c = struct.pack(">I", len(data)) + typ + data + c += struct.pack(">I", zlib.crc32(typ + data) & 0xFFFFFFFF) + return c + + +def write_interlaced_png(path, img): + """Adam7 interlaced RGBA PNG (filter 0), to exercise the pass decoder.""" + w, h = img.size + px = img.load() + passes = [(0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4), + (0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)] + raw = bytearray() + for x0, y0, dx, dy in passes: + pw = (w - x0 + dx - 1) // dx if w > x0 else 0 + ph = (h - y0 + dy - 1) // dy if h > y0 else 0 + if pw <= 0 or ph <= 0: + continue + for ry in range(ph): + raw.append(0) # filter: None + y = y0 + ry * dy + for rx in range(pw): + x = x0 + rx * dx + r, g, b, a = px[x, y] + raw += bytes((r, g, b, a)) + ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 1) + out = b"\x89PNG\r\n\x1a\n" + png_chunk(b"IHDR", ihdr) + out += png_chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + out += png_chunk(b"IEND", b"") + with open(path, "wb") as f: + f.write(out) + + +def make_background(): + """960x540 desktop wallpaper shipped as /sys/images/screen/background.jpg.""" + w, h = 960, 540 + base = Image.new("RGB", (w, h)) + px = base.load() + for y in range(h): + t = y / (h - 1) + r = int(12 + 34 * t) + g = int(16 + 20 * t) + b = int(34 + 44 * t) + for x in range(w): + # subtle diagonal tint + u = (x / (w - 1) + t) * 0.5 + px[x, y] = (min(255, r + int(10 * u)), + min(255, g + int(6 * u)), + min(255, b + int(18 * u))) + overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + d = ImageDraw.Draw(overlay) + blobs = [ + (140, 420, 210, (30, 120, 190, 120)), + (330, 140, 170, (150, 40, 180, 110)), + (700, 360, 260, (20, 90, 160, 120)), + (820, 110, 150, (220, 60, 130, 90)), + (520, 470, 190, (40, 170, 170, 90)), + ] + for cx, cy, rr, col in blobs: + d.ellipse([cx - rr, cy - rr, cx + rr, cy + rr], fill=col) + overlay = overlay.filter(ImageFilter.GaussianBlur(30)) + base = Image.alpha_composite(base.convert("RGBA"), overlay).convert("RGB") + # vignette keeps the corners dark so white window chrome stays readable + mask = Image.new("L", (w, h)) + mp = mask.load() + for y in range(h): + for x in range(w): + dx = (x - w / 2) / (w / 2) + dy = (y - h / 2) / (h / 2) + d2 = dx * dx + dy * dy + v = 255 - int(min(150, d2 * 110)) + mp[x, y] = max(0, v) + base = Image.composite(base, Image.new("RGB", (w, h), (0, 0, 0)), mask) + base.save(BG_OUT, "JPEG", quality=88, subsampling=2, optimize=False) + print("[assets] wrote", BG_OUT) + + +def main(): + os.makedirs(OUT, exist_ok=True) + img = test_pattern() + + # Opaque RGBA PNG (exercises color type 6, non-interlaced). + img.save(os.path.join(OUT, "sample.png"), "PNG") + + # Palette PNG with one transparent color (color type 3 + tRNS). + pal = img.convert("RGB").quantize(colors=64, method=Image.MEDIANCUT) + pal.info["transparency"] = 0 + pal.save(os.path.join(OUT, "pal.png"), "PNG", transparency=0) + + # Baseline 4:2:0 JPEG. + img.convert("RGB").save(os.path.join(OUT, "photo.jpg"), "JPEG", + quality=90, subsampling=2, optimize=False) + + # Grayscale JPEG (single component path). + img.convert("L").save(os.path.join(OUT, "gray.jpg"), "JPEG", quality=90) + + # Interlaced RGBA PNG with a vertical alpha ramp (Adam7 + alpha). + inter = img.copy() + ip = inter.load() + for y in range(H): + for x in range(W): + r, g, b, a = ip[x, y] + ip[x, y] = (r, g, b, (x * 255) // (W - 1)) + write_interlaced_png(os.path.join(OUT, "inter.png"), inter) + + make_background() + + # Reference samples for the app's serial output. + print("[assets] reference samples (PNG/JPEG decode with PIL):") + for name in ("sample.png", "photo.jpg", "gray.jpg", + "pal.png", "inter.png"): + path = os.path.join(OUT, name) + ref = Image.open(path).convert("RGBA") + pts = [(ref.width // 4, ref.height // 3), + (ref.width // 2, ref.height // 2), + ((ref.width * 3) // 4, (ref.height * 2) // 3)] + vals = [ref.getpixel(p) for p in pts] + print(f"[assets] {name:12s} {ref.width}x{ref.height} " + + " ".join(f"{p[0]},{p[1]},{p[2]}" for p in vals)) + print("[assets] wrote", OUT) + + +if __name__ == "__main__": + main() diff --git a/Scripts/syscall_table.py b/Scripts/syscall_table.py new file mode 100644 index 0000000..1ce80f1 --- /dev/null +++ b/Scripts/syscall_table.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Canonical ViperOS syscall table. + +The kernel and vpsdk keep separate copies (vpsdk must not depend on VKernel), +so both files are generated from this list. Never edit the generated block +by hand: + + # BEGIN GENERATED SYSCALL TABLE + ... + # END GENERATED SYSCALL TABLE + +Usage: + python Scripts/syscall_table.py --write # regenerate both files + python Scripts/syscall_table.py --check # fail if out of sync +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + +TARGETS = [ + ROOT / "VKernel" / "Kernel" / "intr" / "syscall.vp", + ROOT / "vpsdk" / "syscall.vp", +] + +BEGIN = "# BEGIN GENERATED SYSCALL TABLE" +END = "# END GENERATED SYSCALL TABLE" + +# (kind, payload...) kind: "section" | "entry" +# Entry numbers follow Linux x86-64; Linux calls we do not implement yet are +# simply absent (the dispatcher returns -ENOSYS for unknown numbers). +TABLE = [ + ("section", "Linux 对齐段(号 = Linux x86-64 的号)"), + ("entry", "READ", 0, "read(fd, buf, count)"), + ("entry", "WRITE", 1, "write(fd, buf, count)"), + ("entry", "OPEN", 2, "open(path, flags) - Linux O_*"), + ("entry", "CLOSE", 3, "close(fd)"), + ("entry", "STAT", 4, "stat(path, fileinfo*) - legacy struct"), + ("entry", "FSTAT", 5, "fstat(fd, struct stat*)"), + ("entry", "POLL", 7, "poll(fds, nfds, timeout) - stub 0"), + ("entry", "SEEK", 8, "lseek(fd, offset, whence) -> new offset"), + ("entry", "MMAP", 9, "mmap(addr, len, prot, flags, fd, off)"), + ("entry", "MPROTECT", 10, "mprotect(addr, len, prot)"), + ("entry", "MUNMAP", 11, "munmap(addr, len)"), + ("entry", "BRK", 12, "brk(addr) -> new break"), + ("entry", "RT_SIGACTION", 13, "rt_sigaction() - bookkeeping"), + ("entry", "RT_SIGPROCMASK", 14, "rt_sigprocmask() - bookkeeping"), + ("entry", "RT_SIGRETURN", 15, "rt_sigreturn()"), + ("entry", "IOCTL", 16, "ioctl(fd, request, arg)"), + ("entry", "PREAD64", 17, "pread64(fd, buf, count, offset)"), + ("entry", "PWRITE64", 18, "pwrite64(fd, buf, count, offset)"), + ("entry", "READV", 19, "readv(fd, iov, iovcnt)"), + ("entry", "WRITEV", 20, "writev(fd, iov, iovcnt)"), + ("entry", "ACCESS", 21, "access(path, mode)"), + ("entry", "PIPE", 22, "pipe(int[2])"), + ("entry", "YIELD", 24, "sched_yield()"), + ("entry", "MREMAP", 25, "mremap(old, oldsz, newsz, flags, newaddr)"), + ("entry", "MADVISE", 28, "madvise(addr, len, advice)"), + ("entry", "DUP", 32, "dup(fd)"), + ("entry", "DUP2", 33, "dup2(oldfd, newfd)"), + ("entry", "NANOSLEEP", 35, "nanosleep(req, rem)"), + ("entry", "GETPID", 39, "getpid()"), + ("entry", "CLONE", 56, "clone(flags, stack, ptid, ctid, tls)"), + ("entry", "FORK", 57, "fork() - copy address space"), + ("entry", "SPAWN", 59, "execve(path, argv, envp)"), + ("entry", "THREAD_EXIT", 60, "exit(): retire current thread"), + ("entry", "WAIT4", 61, "wait4(pid, status, options, rusage)"), + ("entry", "KILL", 62, "kill(pid, sig) - bookkeeping"), + ("entry", "UNAME", 63, "uname(utsname*)"), + ("entry", "FCNTL", 72, "fcntl(fd, cmd, arg)"), + ("entry", "GETCWD", 79, "getcwd(buf, size)"), + ("entry", "CHDIR", 80, "chdir(path)"), + ("entry", "RENAME", 82, "rename(old, new)"), + ("entry", "MKDIR", 83, "mkdir(path)"), + ("entry", "RMDIR", 84, "rmdir(path)"), + ("entry", "REMOVE", 87, "unlink(path)"), + ("entry", "READLINK", 89, "readlink(path, buf, size) - no symlinks"), + ("entry", "GETUID", 102, "getuid()"), + ("entry", "GETGID", 104, "getgid()"), + ("entry", "GETEUID", 107, "geteuid()"), + ("entry", "GETEGID", 108, "getegid()"), + ("entry", "GETTIMEOFDAY", 96, "gettimeofday(tv, tz)"), + ("entry", "GETPPID", 110, "getppid()"), + ("entry", "ARCH_PRCTL", 158, "arch_prctl(code, addr)"), + ("entry", "REBOOT_LINUX", 169, "reboot(magic1, magic2, cmd, arg)"), + ("entry", "GETTID", 186, "gettid()"), + ("entry", "FUTEX", 202, "futex(uaddr, op, val, timeout)"), + ("entry", "GETDENTS64", 217, "getdents64(fd, buf, count)"), + ("entry", "SET_TID_ADDRESS", 218, "set_tid_address(tidptr)"), + ("entry", "CLOCK_GETTIME", 228, "clock_gettime(clockid, timespec*)"), + ("entry", "CLOCK_GETRES", 229, "clock_getres(clockid, timespec*)"), + ("entry", "EXIT", 231, "exit_group(): terminate all threads"), + ("entry", "OPENAT", 257, "openat(dirfd, path, flags, mode)"), + ("entry", "NEWFSTATAT", 262, "newfstatat(dirfd, path, stat*, flags)"), + ("entry", "UNLINKAT", 263, "unlinkat(dirfd, path, flags)"), + ("entry", "READLINKAT", 267, "readlinkat(dirfd, path, buf, size)"), + ("entry", "FACCESSAT", 269, "faccessat(dirfd, path, mode, flags)"), + ("entry", "SET_ROBUST_LIST", 273, "set_robust_list(head, len)"), + ("entry", "DUP3", 292, "dup3(oldfd, newfd, flags)"), + ("entry", "PIPE2", 293, "pipe2(int[2], flags)"), + ("entry", "PRLIMIT64", 302, "prlimit64(pid, resource, new, old)"), + ("entry", "GETRANDOM", 318, "getrandom(buf, len, flags)"), + ("entry", "STATX", 332, "statx(...) - -ENOSYS, glibc falls back"), + ("entry", "RSEQ", 334, "rseq(rseq, len, flags) - -ENOSYS"), + ("section", "临时私有段(0x1000+):Linux 无对应,逐条迁出"), + ("entry", "LOAD_SO", 0x1006, "TODO(userspace): dynamic library loading"), + ("entry", "SO_CALL1", 0x1007, "TODO(userspace): dynamic library call"), + ("entry", "REBOOT", 0x1008, "TODO(linux): reboot(169)"), + ("entry", "SHUTDOWN", 0x1009, "TODO(linux): reboot(RB_POWER_OFF)"), + ("entry", "SPAWN_PROC", 0x100A, "TODO(userspace): Terminal run helper"), + ("entry", "THREAD_CREATE_OLD", 0x100B, "TODO(userspace): ViperOS thread_create"), +] + + +def render() -> str: + out = ["enum Syscall:"] + for item in TABLE: + if item[0] == "section": + out.append(f" # ---- {item[1]} ----") + continue + _, name, num, comment = item + value = f"0x{num:04X}" if num >= 0x1000 else f"{num}" + out.append(f" {name:<18}= {value:<7}# {comment}") + return "\n".join(out) + "\n" + + +def split_lines(text: str): + """Split keeping line endings (the tree mixes CRLF and a few LF lines).""" + return text.splitlines(keepends=True) + + +def replace_block(text: str) -> str: + lines = split_lines(text) + b = e = -1 + for i, line in enumerate(lines): + stripped = line.strip() + if stripped == BEGIN: + b = i + elif stripped == END: + e = i + break + if b < 0 or e < 0 or e <= b: + raise SystemExit(f"markers not found: {BEGIN} / {END}") + newline = "\r\n" if lines[b].endswith("\r\n") else "\n" + block = [ln + newline for ln in render().splitlines()] + return "".join(lines[: b + 1] + block + lines[e:]) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--check", action="store_true") + ap.add_argument("--write", action="store_true") + args = ap.parse_args() + if not args.check and not args.write: + ap.error("pass --check or --write") + + stale = [] + for path in TARGETS: + if not path.exists(): + print(f"[syscall] missing {path}") + return 1 + with open(path, encoding="utf-8", newline="") as fh: + old = fh.read() + new = replace_block(old) + if new != old: + stale.append(path) + if args.write: + with open(path, "w", encoding="utf-8", newline="") as fh: + fh.write(new) + print(f"[syscall] updated {path.relative_to(ROOT)}") + else: + print(f"[syscall] ok {path.relative_to(ROOT)}") + + if args.check and stale: + print("[syscall] out of sync; run: python Scripts/syscall_table.py --write") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/VKernel/Kernel/drivers/devfs/devfs.vp b/VKernel/Kernel/drivers/devfs/devfs.vp index 2209962..413de1e 100644 --- a/VKernel/Kernel/drivers/devfs/devfs.vp +++ b/VKernel/Kernel/drivers/devfs/devfs.vp @@ -111,6 +111,10 @@ virtual class devfs_file: # mmap 支持:默认不可映射(返回 -1)。可映射设备(/dev/fb0)覆写它, # 把自己的物理区间交给内核 mmap 去映射进用户地址空间。 def mmap_info(self, fd: int, out_phys: uint64*, out_size: uint64*) -> int: return -1 + # 1 = 映射该设备的用户页要按 MMIO 处理(PTE_PCD,禁用缓存)。 + # framebuffer 必须返回 1:VMware/真机上写回缓存的 MMIO 写入可能不会立刻 + # 反映到扫描输出,表现为系统还在跑但屏幕停在旧画面。共享内存(/dev/desk)返回 0。 + def mmap_uncached(self) -> int: return 0 class _DevFSObject: @@ -288,6 +292,12 @@ class _DevFSObject: file_ptr: devfs_file* = fd_table[fd].file return file_ptr.mmap_info(fd, out_phys, out_size) + def get_mmap_uncached(self, fd: int) -> int: + if fd < 0 or fd >= MAX_FILE_DESCRIPTORS or not fd_table[fd].file: + return 0 + file_ptr: devfs_file* = fd_table[fd].file + return file_ptr.mmap_uncached() + def list_directory(self, path: const str, buffer: str, size: uint32) -> int: @@ -332,6 +342,9 @@ def devfs_ioctl(fd: int, cmd: uint32, arg: rawptr) -> int: def devfs_get_mmap_info(fd: int, out_phys: uint64*, out_size: uint64*) -> int: return DevFSObject.get_mmap_info(fd, out_phys, out_size) +def devfs_mmap_uncached(fd: int) -> int: + return DevFSObject.get_mmap_uncached(fd) + def devfs_list_directory(path: const str, buffer: str, size: uint32) -> int: return DevFSObject.list_directory(path, buffer, size) @@ -527,11 +540,15 @@ virtual class framebuffer_device(devfs_file): if request == FB_WAIT_VBLANK: # 等进入垂直回扫期(0x3DA bit3)。返回等待消耗的迭代数:到上限即超时 #(当前没有 VGA 回扫信号),调用方可据此判断同步是否真的生效。 - t: int = 100000 - while (asm.inb(0x3DA) & 0x08) != 0 and t > 0: t -= 1 - t2: int = 100000 - while (asm.inb(0x3DA) & 0x08) == 0 and t2 > 0: t2 -= 1 - return 100000 - t2 + t: int = 2048 + while (asm.inb(0x3DA) & 0x08) != 0 and t > 0: + t -= 1 + asm.pause() + t2: int = 2048 + while (asm.inb(0x3DA) & 0x08) == 0 and t2 > 0: + t2 -= 1 + asm.pause() + return 2048 - t2 if request != FB_GET_INFO or not arg: return -1 info: FbInfo* = FbInfo*(uint64(arg)) @@ -548,6 +565,8 @@ virtual class framebuffer_device(devfs_file): if out_size: *out_size = self.fb_size return 0 + def mmap_uncached(self) -> int: return 1 + fb0_file: framebuffer_device* = framebuffer_device() def devfs_register_fb0(fb: rawptr, width: int, height: int, pitch: int, format: int, size: uint64) -> int: diff --git a/VKernel/Kernel/drivers/fs/fat32/fat32_diskio.vp b/VKernel/Kernel/drivers/fs/fat32/fat32_diskio.vp index b7ddc56..b188194 100644 --- a/VKernel/Kernel/drivers/fs/fat32/fat32_diskio.vp +++ b/VKernel/Kernel/drivers/fs/fat32/fat32_diskio.vp @@ -31,12 +31,10 @@ def read(pdrv: uint8, buff: uint8*, sector: uint32, count: uint32) -> types.DRES if pdrv >= MAX_DRIVES: return types.DRESULT.RES_PARERR if not drive_initialized[pdrv]: return types.DRESULT.RES_NOTRDY drive_lock.lock() - i: uint32 - for i in range(count): - rd_res: int = ide.readSector(sector + i, buff + (i * 512)) - if rd_res != 0: - drive_lock.unlock() - return types.DRESULT.RES_ERROR + rd_res: int = ide.readSectors(sector, buff, count) + if rd_res != 0: + drive_lock.unlock() + return types.DRESULT.RES_ERROR drive_lock.unlock() return types.DRESULT.RES_OK diff --git a/VKernel/Kernel/drivers/fs/fat32/fat32_types.vp b/VKernel/Kernel/drivers/fs/fat32/fat32_types.vp index 356f718..c5de502 100644 --- a/VKernel/Kernel/drivers/fs/fat32/fat32_types.vp +++ b/VKernel/Kernel/drivers/fs/fat32/fat32_types.vp @@ -6,6 +6,10 @@ import fat32_time as ftime import viperlib import drivers.serial.uart.serial as serial +# Max sectors per coalesced file read (64 KiB); the IDE layer further caps DMA +# commands at 128 sectors. +define DMA_READ_CLUSTER_CAP = 128 + def _read32(buf: uint8*, off: uint32) -> uint32: return uint32(buf[off]) | (uint32(buf[off + 1]) << 8) | (uint32(buf[off + 2]) << 16) | (uint32(buf[off + 3]) << 24) @@ -1525,6 +1529,39 @@ class fileobj: if self.file_pos >= self.file_size: break if self.first_cluster < 2: break if self.current_cluster < 2 or vol.is_eoc(self.current_cluster): break + # Fast path: at a cluster boundary, coalesce contiguous clusters and + # issue one multi-sector read (BMIDE DMA) instead of one PIO command + # per 512-byte sector. Capped at 64 KiB per call; the loop continues. + if (self.file_pos % vol.cluster_size) == 0 and remaining >= vol.cluster_size: + max_bytes: uint32 = remaining + fleft: uint32 = self.file_size - self.file_pos + if max_bytes > fleft: max_bytes = fleft + max_clusters: uint32 = max_bytes / vol.cluster_size + cap_clusters: uint32 = (uint32(DMA_READ_CLUSTER_CAP) * uint32(512)) / vol.cluster_size + if max_clusters > cap_clusters: max_clusters = cap_clusters + run: uint32 = 0 + c: uint32 = self.current_cluster + next_c: uint32 = 0 + while run < max_clusters: + if c < 2 or vol.is_eoc(c): break + run = run + 1 + next_c = vol.cluster_get_next(c) + if next_c != c + 1: break + c = next_c + if run >= 2: + sectors: uint32 = run * uint32(vol.cluster_sectors) + abs_sect: uint64 = vol.cluster_to_sector(self.current_cluster) + uint64(vol.hidden_sectors) + vol.lock.lock() + res_run: DRESULT = diskio.read(vol.pdrv, buf_ptr, uint32(abs_sect), sectors) + vol.lock.unlock() + if res_run != DRESULT.RES_OK: return FRESULT.FR_DISK_ERR + nbytes: uint32 = sectors * uint32(512) + buf_ptr = buf_ptr + nbytes + self.file_pos = self.file_pos + nbytes + total_read = total_read + nbytes + remaining = remaining - nbytes + self.current_cluster = next_c + continue cluster_offset: uint32 = self.file_pos % vol.cluster_size sector_in_cluster: uint32 = cluster_offset / 512 offset_in_sector: uint32 = cluster_offset % 512 diff --git a/VKernel/Kernel/drivers/storage/ide/ide.vp b/VKernel/Kernel/drivers/storage/ide/ide.vp index 2f25d42..505df6d 100644 --- a/VKernel/Kernel/drivers/storage/ide/ide.vp +++ b/VKernel/Kernel/drivers/storage/ide/ide.vp @@ -1,5 +1,9 @@ import asm import drivers.usb.pci as pci +import drivers.serial.uart.serial as serial +import paging.paging as paging +import viperlib +import string enum IDError: @@ -21,6 +25,119 @@ define SECTOR_SIZE = 512 _ide_initialized: int = 0 +# ---- Bus-master IDE (BMIDE) ---- +# One 64 KiB PRD entry per command; the DMA buffer is 64 KiB aligned in BSS so +# a single entry never crosses a 64 KiB boundary. +define DMA_MAX_SECTORS = 16 # 16 * 512 = 8 KiB per DMA command +define DMA_AREA_SIZE = 131072 +define DMA_SCRATCH_LBA = 0x7FFF0 # near the end of the 256 MiB image; safe to restore +define DMA_AREA_SIZE = 131072 +_bm_base: uint16 = 0 +_bm_ok: int = 0 +_dma_area: [DMA_AREA_SIZE]uint8 +_dma_buf: uint64 = 0 +_dma_buf_pa: uint64 = 0 +_prdt_area: [64]uint64 +_prdt: uint64 = 0 +_prdt_pa: uint64 = 0 +_dma_logged: int = 0 +_dma_tried: int = 0 +_dma_dir: uint8 = 0x08 +_pci_cmd: uint16 = 0 + +def _mfence(): + __asm__("mfence", op=[ASM_DESCR.CLOBBER_MEMORY]) + +def _clflush_range(addr: uint64, nbytes: uint64): + a: uint64 = addr & ~uint64(63) + end: uint64 = addr + nbytes + while a < end: + __asm__(f"clflush [{asminp(a, ASM_DESCR.REG_ANY)}]", + op=[ASM_DESCR.CLOBBER_MEMORY]) + a = a + uint64(64) + +def _ide_reset(): + asm.outb(0x3F6, 0x04) + for _ in range(10000): asm.nop() + asm.outb(0x3F6, 0x00) + for _ in range(10000): asm.nop() + +# Ask the drive to switch to a DMA transfer mode. After SRST the drive is in +# its default PIO mode; READ DMA (0xC8) may be aborted until SET FEATURES runs. +# use_mdma=0: Ultra DMA (word 88), use_mdma=1: multiword DMA (word 63). +def _set_dma_mode(use_mdma: int) -> int: + err: IDError = waitReady(IDE_baseport_PRIMARY) + if err != IDError.IDE_ERR_NONE: return -1 + asm.outb(IDE_baseport_PRIMARY + 6, uint8(0xE0)) + asm.outb(IDE_baseport_PRIMARY + 7, uint8(0xEC)) # IDENTIFY + err = waitDataReady(IDE_baseport_PRIMARY) + if err != IDError.IDE_ERR_NONE: return -1 + ident: [256]uint16 + i: int = 0 + while i < 256: + ident[i] = asm.inw(IDE_baseport_PRIMARY) + i = i + 1 + mode: int = -1 + value: int = 0 + if use_mdma != 0: + # word 63 bits 0..7: multiword DMA modes 0..7 (we use 0..2) + m: int = 2 + while m >= 0: + if ident[63] & uint16(1 << m): + mode = m + break + m = m - 1 + if mode < 0: return -1 + value = 0x20 | mode + else: + # word 88: Ultra DMA modes supported (bits 0..6). Pick the highest mode + # the drive advertises: the firmware/controller timing is usually already + # programmed for it. + m = 6 + while m >= 0: + if ident[88] & uint16(1 << m): + mode = m + break + m = m - 1 + if mode < 0: return -1 + value = 0x40 | mode + err = waitReady(IDE_baseport_PRIMARY) + if err != IDError.IDE_ERR_NONE: return -1 + asm.outb(IDE_baseport_PRIMARY + 1, uint8(0x03)) # SET FEATURES: transfer mode + asm.outb(IDE_baseport_PRIMARY + 2, uint8(value)) + asm.outb(IDE_baseport_PRIMARY + 6, uint8(0xE0)) + asm.outb(IDE_baseport_PRIMARY + 7, uint8(0xEF)) + err = waitReady(IDE_baseport_PRIMARY) + if err != IDError.IDE_ERR_NONE: return -1 + st: uint8 = asm.inb(IDE_baseport_PRIMARY + 7) + if st & uint8(0x01): return -1 + return mode + +# Read a scratch sector with DMA and compare against the PIO copy. A wrong +# bus-master direction bit may write the scratch sector; restore it via PIO. +def _dma_selftest(dr: uint8) -> int: + global _dma_dir + _dma_dir = dr + ref: [512]uint8 + rr: int = readSector(uint32(DMA_SCRATCH_LBA), &ref[0]) + if rr != IDError.IDE_ERR_NONE: return -1 + r: int = _dma_read(uint32(DMA_SCRATCH_LBA), 1) + ok: int = 0 + if r == IDError.IDE_ERR_NONE: + if string.memcmp(rawptr(_dma_buf), rawptr(&ref[0]), 512) == 0: ok = 1 + if ok == 0: + writeSector(uint32(DMA_SCRATCH_LBA), &ref[0]) + return -1 + return 0 + +# Reset, set the requested DMA class, and run the direction selftest. +def _try_dma(use_mdma: int, dr: uint8) -> int: + _ide_reset() + m: int = _set_dma_mode(use_mdma) + if m < 0: return -1 + if _dma_selftest(dr) == 0: return m + return -1 + def waitReady(baseport: uint16) -> static IDError: timeout: int = 100000 for _ in range(timeout, 0, -1): @@ -64,7 +181,8 @@ def flushStaleData(): p = p + 1 def init() -> int: - global _ide_initialized + global _ide_initialized, _bm_base, _bm_ok, _dma_buf, _prdt, _pci_cmd + global _dma_buf_pa, _prdt_pa if _ide_initialized: return IDError.IDE_ERR_NONE bus: uint8 dev_n: uint8 @@ -75,14 +193,56 @@ def init() -> int: vendor: uint16 = pci.pci_read16(bus, dev_n, func, 0x00) if vendor == 0xFFFF: continue cls: uint32 = pci.pci_get_class(bus, dev_n, func) - if cls == 0x010180 or cls == 0x0101: + # class/subclass = 0x0101 (IDE); progIF bit7 = bus-master capable. + # VMware's PIIX4 reports progIF 0x8A (not exactly 0x80), so do + # not compare the whole 24-bit class/progIF word. + cls_sub: uint32 = cls & uint32(0xFFFF00) + proto: uint32 = cls & uint32(0xFF) + if cls_sub == uint32(0x010100) and (proto & uint32(0x80)) != 0: + bar4: uint32 = pci.pci_read32(bus, dev_n, func, 0x20) + bar0: uint32 = pci.pci_read32(bus, dev_n, func, 0x10) + lb0: [128]char + viperlib.snprintf(&lb0, 128, "[ide] pci %d:%d.%d cls=0x%x bar0=0x%x bar4=0x%x\n", + uint32(bus), uint32(dev_n), uint32(func), + cls, bar0, bar4) + serial.puts(&lb0) cmd: uint16 = pci.pci_read16(bus, dev_n, func, 0x04) pci.pci_write32(bus, dev_n, func, 0x04, uint32(cmd | 0x0007)) + if _bm_base == 0 and (bar4 & uint32(1)) != 0: + _bm_base = uint16(bar4 & uint32(0xFFFC)) + _pci_cmd = pci.pci_read16(bus, dev_n, func, 0x04) asm.outb(0x3F6, 0x04) for _ in range(10000): asm.nop() asm.outb(0x3F6, 0x00) for _ in range(10000): asm.nop() status: uint8 = asm.inb(0x1F7) + if _bm_base != 0: + _dma_buf = (uint64(&_dma_area[0]) + uint64(0xFFFF)) & ~uint64(0xFFFF) + _prdt = (uint64(&_prdt_area[0]) + uint64(15)) & ~uint64(15) + _dma_buf_pa = paging.GetPagePhys(_dma_buf) + if _dma_buf_pa != 0: _dma_buf_pa = _dma_buf_pa + (_dma_buf & uint64(0xFFF)) + _prdt_pa = paging.GetPagePhys(_prdt) + if _prdt_pa != 0: _prdt_pa = _prdt_pa + (_prdt & uint64(0xFFF)) + lb: [160]char + viperlib.snprintf(&lb, 160, "[ide] BMIDE base=0x%x pci_cmd=0x%x dma_va=0x%x dma_pa=0x%x prdt_va=0x%x prdt_pa=0x%x\n", + uint32(_bm_base), uint32(_pci_cmd), uint32(_dma_buf), uint32(_dma_buf_pa), + uint32(_prdt), uint32(_prdt_pa)) + serial.puts(&lb) + _bm_ok = 1 + dm: int = _try_dma(0, uint8(0x08)) + if dm < 0: dm = _try_dma(0, uint8(0x00)) + if dm < 0: dm = _try_dma(1, uint8(0x08)) + if dm < 0: dm = _try_dma(1, uint8(0x00)) + if dm >= 0: + mb: [96]char + viperlib.snprintf(&mb, 96, "[ide] DMA selftest ok mode=%d dir=0x%x\n", + dm, uint32(_dma_dir)) + serial.puts(&mb) + else: + _bm_ok = 0 + serial.puts("[ide] DMA unavailable, using PIO\n") + else: + serial.puts("[ide] BMIDE unavailable, using PIO\n") _ide_initialized = 1 return IDError.IDE_ERR_NONE @@ -130,6 +290,116 @@ def readSector(lba: uint32, buffer: rawptr) -> int: # serial.puts(dbg) return IDError.IDE_ERR_NONE +# One bus-master DMA command (<= 64 KiB) into the internal aligned buffer. +def _dma_read(lba: uint32, sectors: uint32) -> int: + global _bm_ok, _dma_logged, _dma_tried, _dma_dir + if _bm_ok == 0 or _dma_buf == 0 or _prdt == 0: + return IDError.IDE_ERR_DEVICE_NOT_READY + if sectors == 0 or sectors > uint32(DMA_MAX_SECTORS): + return IDError.IDE_ERR_SECTOR_COUNT + err: IDError = waitReady(IDE_baseport_PRIMARY) + if err != IDError.IDE_ERR_NONE: + return err + # If a previous PIO command left data in the drive buffer (DRQ=1), command + # block writes are ignored. Drain it before starting the DMA command. + flushStaleData() + # Stop the engine, set direction = device -> memory. + asm.outb(_bm_base, uint8(0x00)) + bytes: uint32 = sectors * uint32(512) + buf_pa: uint64 = _dma_buf_pa + if buf_pa == 0: buf_pa = _dma_buf + prdt_pa: uint64 = _prdt_pa + if prdt_pa == 0: prdt_pa = _prdt + prd: uint32* = uint32*(_prdt) + prd[0] = uint32(buf_pa) + # count == 0 means 64 KiB; bit15 is EOT. + prd[1] = (bytes & uint32(0xFFFF)) | uint32(0x8000) + if _dma_tried == 0: + _dma_tried = 1 + dbg: [160]char + viperlib.snprintf(&dbg, 160, "[ide] dma try lba=0x%x n=%u buf=0x%x prdt=0x%x prd=%x,%x\n", + lba, sectors, uint32(buf_pa), uint32(prdt_pa), prd[0], prd[1]) + serial.puts(&dbg) + _clflush_range(_prdt, uint64(8)) + _clflush_range(_dma_buf, uint64(bytes)) + _mfence() + asm.outl(_bm_base + uint16(4), uint32(prdt_pa)) + asm.outb(_bm_base + uint16(2), uint8(0x06)) # clear error/IRQ + # Start the bus-master engine first; the PIIX4 waits for the drive's DMA + # request. QEMU tolerates either order. + asm.outb(_bm_base, uint8(_dma_dir | uint8(0x01))) # start + direction + # ATA READ DMA (LBA28, 0xC8) + asm.outb(IDE_baseport_PRIMARY + 2, uint8(sectors & uint32(0xFF))) + asm.outb(IDE_baseport_PRIMARY + 3, uint8(lba & uint32(0xFF))) + asm.outb(IDE_baseport_PRIMARY + 4, uint8((lba >> 8) & uint32(0xFF))) + asm.outb(IDE_baseport_PRIMARY + 5, uint8((lba >> 16) & uint32(0xFF))) + asm.outb(IDE_baseport_PRIMARY + 6, uint8(uint32(0xE0) | ((lba >> 24) & uint32(0x0F)))) + asm.outb(IDE_baseport_PRIMARY + 7, uint8(0xC8)) + ok: int = 0 + t: int = 2000000 + st: uint8 = 0 + while t > 0: + st = asm.inb(_bm_base + uint16(2)) + if st & uint8(0x02): break # error + if st & uint8(0x04): ok = 1; break # interrupt/completion + t = t - 1 + asm.pause() + asm.outb(_bm_base, uint8(0x00)) # stop + asm.outb(_bm_base + uint16(2), uint8(0x06)) + ata: uint8 = asm.inb(IDE_baseport_PRIMARY + 7) + if ok == 0: + fb: [96]char + viperlib.snprintf(&fb, 96, "[ide] DMA timeout st=0x%x ata=0x%x\n", uint32(st), uint32(ata)) + serial.puts(&fb) + return IDError.IDE_ERR_TIMEOUT + if ata & uint8(0x01): + fb2: [96]char + viperlib.snprintf(&fb2, 96, "[ide] DMA ata error st=0x%x ata=0x%x\n", uint32(st), uint32(ata)) + serial.puts(&fb2) + return IDError.IDE_ERR_DEVICE_ERROR + _clflush_range(_dma_buf, uint64(bytes)) + _mfence() + if _dma_logged == 0: + _dma_logged = 1 + serial.puts("[ide] DMA read ok\n") + return IDError.IDE_ERR_NONE + +# Multi-sector read: DMA for >=2 sectors, PIO for a single sector or after a +# DMA failure. Caller must hold the drive lock. +def readSectors(lba: uint32, buffer: rawptr, count: uint32) -> int: + global _bm_ok + if buffer is None: return IDError.IDE_ERR_BUFFER_NULL + if count == 0: return IDError.IDE_ERR_SECTOR_COUNT + if _bm_ok == 0 or count < 2: + i: uint32 = 0 + while i < count: + r: int = readSector(lba + i, rawptr(uint64(buffer) + uint64(i) * uint64(512))) + if r != IDError.IDE_ERR_NONE: return r + i = i + 1 + return IDError.IDE_ERR_NONE + remaining: uint32 = count + cur: uint32 = lba + dst: uint64 = uint64(buffer) + while remaining > 0: + chunk: uint32 = remaining + if chunk > uint32(DMA_MAX_SECTORS): chunk = uint32(DMA_MAX_SECTORS) + r = _dma_read(cur, chunk) + if r != IDError.IDE_ERR_NONE: + serial.puts("[ide] DMA failed, falling back to PIO\n") + _bm_ok = 0 + _ide_reset() + i = 0 + while i < remaining: + rr: int = readSector(cur + i, rawptr(dst + uint64(i) * uint64(512))) + if rr != IDError.IDE_ERR_NONE: return rr + i = i + 1 + return IDError.IDE_ERR_NONE + string.memcpy(rawptr(dst), rawptr(_dma_buf), uint64(chunk) * uint64(512)) + dst = dst + uint64(chunk) * uint64(512) + cur = cur + chunk + remaining = remaining - chunk + return IDError.IDE_ERR_NONE + def writeSector(lba: uint32, buffer: const rawptr) -> int: if buffer is None: return IDError.IDE_ERR_BUFFER_NULL err: IDError = waitReady(IDE_baseport_PRIMARY) diff --git a/VKernel/Kernel/execrunner/elf.vp b/VKernel/Kernel/execrunner/elf.vp index 95c8ea0..78d446b 100644 --- a/VKernel/Kernel/execrunner/elf.vp +++ b/VKernel/Kernel/execrunner/elf.vp @@ -32,6 +32,7 @@ define ET_DYN = 3 define EM_X86_64 = 62 define PT_LOAD = 1 define PT_DYNAMIC = 2 +define PT_INTERP = 3 define PF_X = 1 define PF_W = 2 define PF_R = 4 @@ -120,7 +121,7 @@ define MAX_SHDRS = 64 define MAX_DYNS = 64 define MAX_RELAS = 256 -def load_elf(path: const str, do_map: int = 1) -> rawptr: +def load_elf(path: const str, do_map: int = 1, base: uint64 = 0) -> rawptr: fp: fat32_types.fileobj* = fat32.open(path, fat32_types.FA_READ) if fp is None: err: uint32 = fat32.last_error() @@ -180,6 +181,10 @@ def load_elf(path: const str, do_map: int = 1) -> rawptr: serial.puts("[elf] not ET_EXEC or ET_DYN\n") fat32.close(fp) return None + if ehdr.e_type == ET_EXEC and base != 0: + serial.puts("[elf] ET_EXEC cannot be relocated\n") + fat32.close(fp) + return None if ehdr.e_machine != EM_X86_64: serial.puts("[elf] not x86_64\n") @@ -246,7 +251,12 @@ def load_elf(path: const str, do_map: int = 1) -> rawptr: viperlib.snprintf(&cr2, 80, "[elf] vaddr 0x%lx-0x%lx pages=%lu\n", lowest_vaddr, highest_vaddr_end, total_pages) serial.puts(cr2) - raw_alloc: rawptr = mm.malloc(total_pages * 0x1000 + 0x1000) + # Keep the image offset of the first LOAD segment: the final page table maps + # whole pages, so the physical buffer must start at a page boundary that + # corresponds to `virt_page_start` below. + low_off: uint64 = lowest_vaddr & uint64(0xFFF) + alloc_sz: uint64 = total_pages * uint64(0x1000) + uint64(0x2000) + raw_alloc: rawptr = mm.malloc(alloc_sz) _db_ra: [80]char viperlib.snprintf(&_db_ra, 80, "[elf] malloc ret raw_alloc=0x%lx\n", uint64(raw_alloc)) serial.puts(_db_ra) @@ -255,7 +265,8 @@ def load_elf(path: const str, do_map: int = 1) -> rawptr: fat32.close(fp) return None - phys_base_aligned: uint64 = (uint64(raw_alloc) + uint64(0xFFF)) & ~uint64(0xFFF) + phys_page_start_u: uint64 = (uint64(raw_alloc) + uint64(0xFFF)) & ~uint64(0xFFF) + phys_base_aligned: uint64 = phys_page_start_u + low_off phys_base: rawptr = rawptr(phys_base_aligned) string.memset(phys_base, 0, total_pages * 0x1000) @@ -280,9 +291,9 @@ def load_elf(path: const str, do_map: int = 1) -> rawptr: fat32.close(fp) - virt_page_start: uint64 = lowest_vaddr & ~uint64(0xFFF) - phys_page_start: uint64 = uint64(phys_base) & ~uint64(0xFFF) - map_end: uint64 = (highest_vaddr_end + 0xFFF) & ~uint64(0xFFF) + virt_page_start: uint64 = (base + lowest_vaddr) & ~uint64(0xFFF) + phys_page_start: uint64 = phys_page_start_u + map_end: uint64 = (base + highest_vaddr_end + uint64(0xFFF)) & ~uint64(0xFFF) num_map_pages: uint64 = (map_end - virt_page_start) // 0x1000 # Store mapping info for spawn_elf (used when do_map=0) @@ -298,7 +309,7 @@ def load_elf(path: const str, do_map: int = 1) -> rawptr: page_flags: uint64 = paging.PTE_PRESENT | paging.PTE_WRITABLE | paging.PTE_USER paging.MapPage(virt_addr, phys_addr, page_flags) - entry_addr: rawptr = rawptr(entry_point) + entry_addr: rawptr = rawptr(base + entry_point) return entry_addr @@ -319,7 +330,7 @@ def run_elf(path: const str) -> int: serial.puts("[elf] returned from entry\n") return 0 -define MAX_SPAWNED = 8 +define MAX_SPAWNED = 32 _elf_entries: [MAX_SPAWNED]rawptr # User stack virtual address: top of 48-bit user space, 64KB below @@ -336,6 +347,153 @@ _last_elf_phys_page_start: uint64 = 0 _last_elf_virt_page_start: uint64 = 0 _last_elf_num_map_pages: uint64 = 0 +# ============================================================================ +# Linux process startup stack: argc/argv/envp/auxv +# ============================================================================ + +define AT_NULL = 0 +define AT_PHDR = 3 +define AT_PHENT = 4 +define AT_PHNUM = 5 +define AT_PAGESZ = 6 +define AT_BASE = 7 +define AT_FLAGS = 8 +define AT_ENTRY = 9 +define AT_UID = 11 +define AT_EUID = 12 +define AT_GID = 13 +define AT_EGID = 14 +define AT_PLATFORM = 15 +define AT_HWCAP = 16 +define AT_CLKTCK = 17 +define AT_SECURE = 23 +define AT_RANDOM = 25 +define AT_EXECFN = 31 + +_rng_stack_state: uint64 = 0x243F6A8885A308D3 + +def _stack_rand() -> uint8: + global _rng_stack_state + _rng_stack_state = _rng_stack_state * uint64(6364136223846793005) + uint64(1442695040888963407) + return uint8((_rng_stack_state >> 33) & uint64(0xFF)) + +def _stk_u64(buf: uint8*, u: uint64*, v: uint64): + if u[0] < uint64(8): return + u[0] = u[0] - uint64(8) + q: uint64* = uint64*(uint64(buf) + u[0]) + q[0] = v + +def _stk_str(buf: uint8*, u: uint64*, s: const str, base_va: uint64) -> uint64: + n: uint64 = uint64(int(string.strlen(s)) + 1) + if n > uint64(256): n = uint64(256) + if u[0] < n: return uint64(0) + u[0] = u[0] - n + dst: uint8* = uint8*(uint64(buf) + u[0]) + i: int = 0 + while uint64(i) < n: + dst[i] = uint8(s[i]) + i += 1 + dst[n - uint64(1)] = uint8(0) + return base_va + u[0] + +# Build the stack image in a 4KB scratch buffer, then copy it to the top of the +# process' 64KB user stack. Pointers are virtual addresses (USER_STACK_VTOP-based). +def _build_argv_stack(p: proc.Process*, path: const str, argv: rawptr, envp: rawptr, + phdr_va: uint64, entry_va: uint64, phent: uint64, phnum: uint64, + interp_base: uint64) -> int: + buf: [4096]uint8 + string.memset(&buf[0], 0, 4096) + base_va: uint64 = USER_STACK_VTOP - uint64(4096) + u: uint64 = uint64(4096) + path_va: uint64 = _stk_str(&buf[0], &u, path, base_va) + argv_va: [17]uint64 + env_va: [17]uint64 + argc: int = 0 + envc: int = 0 + if argv is not None: + av: str* = str*(argv) + while argc < 16: + sp: str = av[argc] + if sp is None: break + argv_va[argc] = _stk_str(&buf[0], &u, sp, base_va) + argc += 1 + if argc == 0: + argv_va[0] = path_va + argc = 1 + if envp is not None: + ev: str* = str*(envp) + while envc < 16: + sp2: str = ev[envc] + if sp2 is None: break + env_va[envc] = _stk_str(&buf[0], &u, sp2, base_va) + envc += 1 + plat_va: uint64 = _stk_str(&buf[0], &u, "x86_64", base_va) + u = u - uint64(16) + ri: int = 0 + while ri < 16: + rp: uint8* = uint8*(uint64(&buf[0]) + u + uint64(ri)) + rp[0] = _stack_rand() + ri += 1 + random_va: uint64 = base_va + u + # keep every pushed word 8-byte aligned; the gap below the strings is unused + u = u & ~uint64(7) + aux_t: [24]uint64 + aux_v: [24]uint64 + naux: int = 0 + aux_t[naux] = uint64(AT_PHDR); aux_v[naux] = phdr_va; naux += 1 + aux_t[naux] = uint64(AT_PHENT); aux_v[naux] = phent; naux += 1 + aux_t[naux] = uint64(AT_PHNUM); aux_v[naux] = phnum; naux += 1 + aux_t[naux] = uint64(AT_PAGESZ); aux_v[naux] = uint64(4096); naux += 1 + aux_t[naux] = uint64(AT_BASE); aux_v[naux] = interp_base; naux += 1 + aux_t[naux] = uint64(AT_FLAGS); aux_v[naux] = uint64(0); naux += 1 + aux_t[naux] = uint64(AT_ENTRY); aux_v[naux] = entry_va; naux += 1 + aux_t[naux] = uint64(AT_UID); aux_v[naux] = uint64(0); naux += 1 + aux_t[naux] = uint64(AT_EUID); aux_v[naux] = uint64(0); naux += 1 + aux_t[naux] = uint64(AT_GID); aux_v[naux] = uint64(0); naux += 1 + aux_t[naux] = uint64(AT_EGID); aux_v[naux] = uint64(0); naux += 1 + aux_t[naux] = uint64(AT_PLATFORM); aux_v[naux] = plat_va; naux += 1 + aux_t[naux] = uint64(AT_HWCAP); aux_v[naux] = uint64(0); naux += 1 + aux_t[naux] = uint64(AT_CLKTCK); aux_v[naux] = uint64(100); naux += 1 + aux_t[naux] = uint64(AT_SECURE); aux_v[naux] = uint64(0); naux += 1 + aux_t[naux] = uint64(AT_RANDOM); aux_v[naux] = random_va; naux += 1 + aux_t[naux] = uint64(AT_EXECFN); aux_v[naux] = path_va; naux += 1 + # memory (low -> high): argc, argv[], NULL, envp[], NULL, auxv[], AT_NULL + # stack grows down, so push AT_NULL first and the rest in reverse. + _stk_u64(&buf[0], &u, uint64(0)) + _stk_u64(&buf[0], &u, uint64(0)) + ai: int = naux - 1 + while ai >= 0: + _stk_u64(&buf[0], &u, aux_v[ai]) + _stk_u64(&buf[0], &u, aux_t[ai]) + ai -= 1 + _stk_u64(&buf[0], &u, uint64(0)) + ai = envc - 1 + while ai >= 0: + _stk_u64(&buf[0], &u, env_va[ai]) + ai -= 1 + _stk_u64(&buf[0], &u, uint64(0)) + ai = argc - 1 + while ai >= 0: + _stk_u64(&buf[0], &u, argv_va[ai]) + ai -= 1 + _stk_u64(&buf[0], &u, uint64(argc)) + used: uint64 = uint64(4096) - u + if used == 0 or used > uint64(4096): return -1 + dst: uint8* = uint8*(p.user_stack - used) + src: uint8* = uint8*(uint64(&buf[0]) + u) + string.memcpy(dst, src, used) + p.entry_rsp = USER_STACK_VTOP - used + return 0 + +# Map the most recent load_elf() result into the *active* (cloned) PML4. +def _map_last_elf(): + mi: uint64 = 0 + while mi < _last_elf_num_map_pages: + paging.MapPage(_last_elf_virt_page_start + mi * uint64(0x1000), + _last_elf_phys_page_start + mi * uint64(0x1000), + paging.PTE_PRESENT | paging.PTE_WRITABLE | paging.PTE_USER) + mi += 1 + def _elf_entry_wrapper(arg: rawptr) -> int: pid: int = int(uint64(arg)) _db1: [64]char @@ -376,13 +534,24 @@ def _elf_entry_wrapper(arg: rawptr) -> int: # will set RFLAGS.IF from the saved frame (0x202) for user mode. # This prevents timer interrupt preemption between TSS setup and iretq. # Drop to Ring 3 user mode via iretq - proc.drop_to_user_mode(entry_fn, user_stack_vtop, cur_p.kern_stack) + user_rsp: uint64 = USER_STACK_VTOP + if cur_p.entry_rsp != 0: + user_rsp = cur_p.entry_rsp + proc.drop_to_user_mode(entry_fn, user_rsp, cur_p.kern_stack) cur: proc.Process* = proc.ProcessManager.current() cur.exit() return 0 -def spawn_elf(path: const str) -> int: +def spawn_elf(path: const str, argv: rawptr = None, envp: rawptr = None) -> int: if ELF_LOAD == 0: return -1 # [临时] 见文件头 ELF_LOAD 开关说明 + phdr_va: uint64 = 0 + phent_u: uint64 = 0 + phnum_u: uint64 = 0 + main_base: uint64 = 0 + main_entry: uint64 = 0 + interp_len: int = 0 + interp_path: [256]char + string.memset(&interp_path[0], 0, 256) fat32.lock() name_start: int = 0 si: int = 0 @@ -408,17 +577,35 @@ def spawn_elf(path: const str) -> int: fat32.read(fp_scan, &ehdr_buf_s, 64) ehdr_s: Elf64_Ehdr* = &ehdr_buf_s if ehdr_s.e_type == ET_EXEC or ehdr_s.e_type == ET_DYN: + if ehdr_s.e_type == ET_DYN: + main_base = uint64(0x60000000) + main_entry = main_base + ehdr_s.e_entry phoff_s: uint64 = ehdr_s.e_phoff phentsize_s: uint16 = ehdr_s.e_phentsize phnum_s: uint16 = ehdr_s.e_phnum if phentsize_s > 0 and phnum_s > 0 and phnum_s <= MAX_PHDRS: phdr_bufs_s: [MAX_PHDRS]Elf64_Phdr string.memset(&phdr_bufs_s, 0, MAX_PHDRS * 56) + phent_u = uint64(phentsize_s) + phnum_u = uint64(phnum_s) found_load_s: int = 0 for pi_s in range(phnum_s): fat32.seek(fp_scan, uint32(phoff_s + uint64(pi_s) * uint64(phentsize_s))) fat32.read(fp_scan, &phdr_bufs_s[pi_s], 56) + if phdr_bufs_s[pi_s].p_type == PT_INTERP: + ilen: uint64 = uint64(phdr_bufs_s[pi_s].p_filesz) + if ilen > uint64(255): ilen = uint64(255) + if ilen > 0: + fat32.seek(fp_scan, uint32(phdr_bufs_s[pi_s].p_offset)) + ir: int = int(fat32.read(fp_scan, &interp_path[0], uint32(ilen))) + if ir > 0: + interp_len = ir + interp_path[interp_len] = 0 if phdr_bufs_s[pi_s].p_type == PT_LOAD: + ph_off_s: uint64 = uint64(phdr_bufs_s[pi_s].p_offset) + ph_fsz_s: uint64 = uint64(phdr_bufs_s[pi_s].p_filesz) + if ehdr_s.e_phoff >= ph_off_s and ehdr_s.e_phoff < ph_off_s + ph_fsz_s: + phdr_va = main_base + uint64(phdr_bufs_s[pi_s].p_vaddr) + (uint64(ehdr_s.e_phoff) - ph_off_s) seg_end_s: uint64 = phdr_bufs_s[pi_s].p_vaddr + phdr_bufs_s[pi_s].p_memsz if found_load_s == 0: lowest_vaddr = phdr_bufs_s[pi_s].p_vaddr @@ -431,20 +618,10 @@ def spawn_elf(path: const str) -> int: highest_vaddr_end = seg_end_s fat32.close(fp_scan) - isolate_va: uint64 = lowest_vaddr + isolate_va: uint64 = main_base + lowest_vaddr if isolate_va == 0: isolate_va = 0x400000 - # Load ELF to physical memory WITHOUT mapping in kernel PML4. - # do_map=0: load_elf stores mapping info in globals for spawn_elf to use. - # ROOT CAUSE FIX: previously load_elf mapped ELF vaddr (e.g. 0x400000) into - # kernel PML4, overwriting identity mapping of buddy memory_block headers. - entry: rawptr = load_elf(path, 0) - if entry is None: - serial.puts("[elf] spawn: load failed\n") - fat32.unlock() - return -1 - # Clone kernel PML4 (identity mapping intact, no ELF mapping yet) proc_pml4: uint64 = paging.clone_for_process(isolate_va) slog1: [80]char @@ -455,21 +632,40 @@ def spawn_elf(path: const str) -> int: fat32.unlock() return -1 - # Map ELF pages in the CLONED PML4 (not kernel's). - # This prevents overwriting kernel identity mapping at 0x400000 which - # would corrupt buddy system memory_block headers. - # Must disable interrupts: @pml4 global is used by MapPage and a timer - # interrupt mid-section would call switch_pid, corrupting @pml4. + # Load the main image and (if present) the interpreter, mapping each into + # the cloned PML4 immediately so the next load can reuse the globals. + # Interrupts stay off: @pml4 is global. asm.cli() _kernel_cr3: uint64 = 0 __asm__(f"""mov {asmout(_kernel_cr3, ASM_DESCR.OUTPUT_REG)}, cr3""", op=[ASM_DESCR.CLOBBER_MEMORY]) __asm__(f"mov cr3, {asminp(proc_pml4, ASM_DESCR.REG_ANY)}", op=[ASM_DESCR.CLOBBER_MEMORY]) paging.set_active_pml4(proc_pml4) - mi: uint64 - for mi in range(_last_elf_num_map_pages): - _va: uint64 = _last_elf_virt_page_start + mi * 0x1000 - _pa: uint64 = _last_elf_phys_page_start + mi * 0x1000 - paging.MapPage(_va, _pa, paging.PTE_PRESENT | paging.PTE_WRITABLE | paging.PTE_USER) + + entry: rawptr = load_elf(path, 0, main_base) + if entry is None: + __asm__(f"mov cr3, {asminp(_kernel_cr3, ASM_DESCR.REG_ANY)}", op=[ASM_DESCR.CLOBBER_MEMORY]) + paging.set_active_pml4(_kernel_cr3) + asm.sti() + serial.puts("[elf] spawn: load failed\n") + fat32.unlock() + return -1 + _map_last_elf() + + interp_entry: rawptr = None + interp_base: uint64 = uint64(0) + if interp_len > 0: + interp_base = uint64(0x70000000) + interp_entry = load_elf(&interp_path[0], 0, interp_base) + if interp_entry is None: + __asm__(f"mov cr3, {asminp(_kernel_cr3, ASM_DESCR.REG_ANY)}", op=[ASM_DESCR.CLOBBER_MEMORY]) + paging.set_active_pml4(_kernel_cr3) + asm.sti() + serial.puts("[elf] spawn: interpreter load failed\n") + fat32.unlock() + return -1 + _map_last_elf() + entry = interp_entry + __asm__(f"mov cr3, {asminp(_kernel_cr3, ASM_DESCR.REG_ANY)}", op=[ASM_DESCR.CLOBBER_MEMORY]) paging.set_active_pml4(_kernel_cr3) asm.sti() @@ -482,8 +678,14 @@ def spawn_elf(path: const str) -> int: serial.puts("[elf] spawn: create_process failed\n") fat32.unlock() return -2 - p.elf_base = uint64(entry) + p.elf_base = main_base p.pml4_root = proc_pml4 + p.parent_pid = proc.current() + if _build_argv_stack(p, path, argv, envp, phdr_va, main_entry, phent_u, phnum_u, interp_base) != 0: + serial.puts("[elf] spawn: startup stack build failed\n") + p.exit() + fat32.unlock() + return -4 if p.pid >= 0 and p.pid < MAX_SPAWNED: _elf_entries[p.pid] = entry diff --git a/VKernel/Kernel/intr/syscall.vp b/VKernel/Kernel/intr/syscall.vp index af1d63e..b4b25a0 100644 --- a/VKernel/Kernel/intr/syscall.vp +++ b/VKernel/Kernel/intr/syscall.vp @@ -10,48 +10,131 @@ import paging.paging as paging import execrunner.elf as elf import drivers.devfs.devfs as devfs import platform.acpi.acpi as acpi +import platform.pch.timer as timer +import platform.pch.rtc as rtc +import intr.gdt as gdt +import drivers.core.cpu.cpu as cpu +import abi.linux as abi import asm +# ---------------------------------------------------------------------------- +# Syscall entry frame (set up by the compiler-provided syscall_entry): +# rbp+0x00 r15 +0x08 r14 +0x10 r13 +0x18 r12 +# +0x20 r10 +0x28 r9 +0x30 r8 +0x38 rbp +# +0x40 rdi +0x48 rsi +0x50 rdx +0x58 rbx +# +0x60 r11(user flags) +0x68 rcx(user rip) +# +0x70 from-user flag +0x78 user rsp +# The handler itself never touches rbp, so a naked read is stable. +# ---------------------------------------------------------------------------- +define FR_R15 = 0 +define FR_R14 = 8 +define FR_R13 = 16 +define FR_R12 = 24 +define FR_R10 = 32 +define FR_R9 = 40 +define FR_R8 = 48 +define FR_RBP = 56 +define FR_RDI = 64 +define FR_RSI = 72 +define FR_RDX = 80 +define FR_RBX = 88 +define FR_FLAGS = 96 +define FR_RIP = 104 +define FR_FROMU = 112 +define FR_RSP = 120 + +# Entry trampoline state (naked syscall_handler stores the entry rbp here). +export _entry_frame: uint64 = 0 +export _handler_ptr: rawptr = None + + # ============================================================================ -# 系统调用号表(唯一权威来源)。与 Linux x86-64 ABI **同名同号**: -# · Linux 有同名调用 → 直接用它的号;Linux 有而我们未实现的**留空不占号** -# (12 brk / 39 getpid / 84 rmdir / 217 getdents64 / 257 openat … 需要时按号补上); -# · 我们独有的能力**不再新增 syscall**,一律走 devfs 设备文件(open/read/write) -# 或下沉到用户层(见下面「临时私有段」的逐条迁移标注)。 -# 注意:vpsdk/syscall.vp 有一份逐字对应的镜像(vpsdk 不允许依赖 VKernel), -# 增删/改号/改名必须两边同步。 +# 系统调用号表(由 Scripts/syscall_table.py 生成,禁止手改)。 +# 生成源:Scripts/syscall_table.py;build.py 在编译前自动同步。 # ============================================================================ +# BEGIN GENERATED SYSCALL TABLE enum Syscall: - # ---- Linux 对齐段(号 = Linux x86-64 的号)---- - READ = 0 # read(fd, buf, count) - WRITE = 1 # write(fd, buf, count) - OPEN = 2 # open(path, flags) - CLOSE = 3 # close(fd) - STAT = 4 # stat(path, info)(结构体仍是本系统的 fileinfo) - SEEK = 8 # lseek(fd, offset, whence)(whence 目前恒 0 = SEEK_SET) - MMAP = 9 # mmap(size) → VA(ABI 待向 Linux 的 addr/len/prot/flags/fd/off 靠拢) - MUNMAP = 11 # munmap(ptr, size)(目前空实现) - IOCTL = 16 # ioctl(fd, request, arg):设备控制(当前用于 /dev/fb0 查几何) - YIELD = 24 # sched_yield() - GETPID = 39 # getpid():返回当前进程 pid(app 用它过滤 /dev/desk 事件) - THREAD_CREATE = 56 # clone():建用户线程,返回 tid - SPAWN = 59 # execve():加载并运行新 ELF,返回 pid - THREAD_EXIT = 60 # exit():退役当前线程 - MKDIR = 83 # mkdir(path) - REMOVE = 87 # unlink(path) - EXIT = 231 # exit_group():退役当前线程(窗口由 explorer 侧回收) - # ---- 临时私有段(0x1000+):Linux 无对应,且**逐条都会被消掉** ---- - # 目标形态:私有能力不占 syscall 号 → 改 devfs 设备文件。 - # S3e 删掉 UI 那 11 个;S4 删掉 SERIAL_PUTS(改 /dev/serial0);S5 删掉 TELL/SIZE - # (改为 lseek(fd,0,SEEK_CUR) / lseek(fd,0,SEEK_END),见 SEEK 的 whence 支持)。 - OPENDIR = 0x1003 # TODO(linux):openat(O_DIRECTORY) + getdents64 - READDIR = 0x1004 # TODO(linux):getdents64 - CLOSEDIR = 0x1005 # TODO(linux):close - LOAD_SO = 0x1006 # TODO(userspace): dynamic library loading - SO_CALL1 = 0x1007 # TODO(userspace): dynamic library call - REBOOT = 0x1008 # reboot() (ViperOS-specific) - SHUTDOWN = 0x1009 # shutdown() (ViperOS-specific) + # ---- Linux 对齐段(号 = Linux x86-64 的号) ---- + READ = 0 # read(fd, buf, count) + WRITE = 1 # write(fd, buf, count) + OPEN = 2 # open(path, flags) - Linux O_* + CLOSE = 3 # close(fd) + STAT = 4 # stat(path, fileinfo*) - legacy struct + FSTAT = 5 # fstat(fd, struct stat*) + POLL = 7 # poll(fds, nfds, timeout) - stub 0 + SEEK = 8 # lseek(fd, offset, whence) -> new offset + MMAP = 9 # mmap(addr, len, prot, flags, fd, off) + MPROTECT = 10 # mprotect(addr, len, prot) + MUNMAP = 11 # munmap(addr, len) + BRK = 12 # brk(addr) -> new break + RT_SIGACTION = 13 # rt_sigaction() - bookkeeping + RT_SIGPROCMASK = 14 # rt_sigprocmask() - bookkeeping + RT_SIGRETURN = 15 # rt_sigreturn() + IOCTL = 16 # ioctl(fd, request, arg) + PREAD64 = 17 # pread64(fd, buf, count, offset) + PWRITE64 = 18 # pwrite64(fd, buf, count, offset) + READV = 19 # readv(fd, iov, iovcnt) + WRITEV = 20 # writev(fd, iov, iovcnt) + ACCESS = 21 # access(path, mode) + PIPE = 22 # pipe(int[2]) + YIELD = 24 # sched_yield() + MREMAP = 25 # mremap(old, oldsz, newsz, flags, newaddr) + MADVISE = 28 # madvise(addr, len, advice) + DUP = 32 # dup(fd) + DUP2 = 33 # dup2(oldfd, newfd) + NANOSLEEP = 35 # nanosleep(req, rem) + GETPID = 39 # getpid() + CLONE = 56 # clone(flags, stack, ptid, ctid, tls) + FORK = 57 # fork() - copy address space + SPAWN = 59 # execve(path, argv, envp) + THREAD_EXIT = 60 # exit(): retire current thread + WAIT4 = 61 # wait4(pid, status, options, rusage) + KILL = 62 # kill(pid, sig) - bookkeeping + UNAME = 63 # uname(utsname*) + FCNTL = 72 # fcntl(fd, cmd, arg) + GETCWD = 79 # getcwd(buf, size) + CHDIR = 80 # chdir(path) + RENAME = 82 # rename(old, new) + MKDIR = 83 # mkdir(path) + RMDIR = 84 # rmdir(path) + REMOVE = 87 # unlink(path) + READLINK = 89 # readlink(path, buf, size) - no symlinks + GETUID = 102 # getuid() + GETGID = 104 # getgid() + GETEUID = 107 # geteuid() + GETEGID = 108 # getegid() + GETTIMEOFDAY = 96 # gettimeofday(tv, tz) + GETPPID = 110 # getppid() + ARCH_PRCTL = 158 # arch_prctl(code, addr) + REBOOT_LINUX = 169 # reboot(magic1, magic2, cmd, arg) + GETTID = 186 # gettid() + FUTEX = 202 # futex(uaddr, op, val, timeout) + GETDENTS64 = 217 # getdents64(fd, buf, count) + SET_TID_ADDRESS = 218 # set_tid_address(tidptr) + CLOCK_GETTIME = 228 # clock_gettime(clockid, timespec*) + CLOCK_GETRES = 229 # clock_getres(clockid, timespec*) + EXIT = 231 # exit_group(): terminate all threads + OPENAT = 257 # openat(dirfd, path, flags, mode) + NEWFSTATAT = 262 # newfstatat(dirfd, path, stat*, flags) + UNLINKAT = 263 # unlinkat(dirfd, path, flags) + READLINKAT = 267 # readlinkat(dirfd, path, buf, size) + FACCESSAT = 269 # faccessat(dirfd, path, mode, flags) + SET_ROBUST_LIST = 273 # set_robust_list(head, len) + DUP3 = 292 # dup3(oldfd, newfd, flags) + PIPE2 = 293 # pipe2(int[2], flags) + PRLIMIT64 = 302 # prlimit64(pid, resource, new, old) + GETRANDOM = 318 # getrandom(buf, len, flags) + STATX = 332 # statx(...) - -ENOSYS, glibc falls back + RSEQ = 334 # rseq(rseq, len, flags) - -ENOSYS + # ---- 临时私有段(0x1000+):Linux 无对应,逐条迁出 ---- + LOAD_SO = 0x1006 # TODO(userspace): dynamic library loading + SO_CALL1 = 0x1007 # TODO(userspace): dynamic library call + REBOOT = 0x1008 # TODO(linux): reboot(169) + SHUTDOWN = 0x1009 # TODO(linux): reboot(RB_POWER_OFF) + SPAWN_PROC = 0x100A # TODO(userspace): Terminal run helper + THREAD_CREATE_OLD = 0x100B # TODO(userspace): ViperOS thread_create +# END GENERATED SYSCALL TABLE define IA32_EFER = 0xC0000080 define IA32_STAR = 0xC0000081 @@ -110,6 +193,8 @@ def _fd_alloc(p: process.Process*) -> int: for i in range(process.PROC_MAX_FDS): if not p.fd_used[i]: p.fd_used[i] = 1 + p.fd_flags[i] = 0 + p.fd_shared[i] = 0 return i return -1 @@ -117,6 +202,20 @@ def _fd_free(p: process.Process*, fd: int): if fd >= 0 and fd < process.PROC_MAX_FDS: p.fd_used[fd] = None p.fd_table[fd] = None + p.fd_kind[fd] = 0 + p.fd_flags[fd] = 0 + p.fd_shared[fd] = 0 + +# dup()/dup2() share the same underlying object; the last close wins. +def _fd_other_ref(p: process.Process*, fd: int) -> int: + fp: uint64 = uint64(p.fd_table[fd]) + k: uint8 = p.fd_kind[fd] + for i in range(process.PROC_MAX_FDS): + if i == fd: continue + if not p.fd_used[i]: continue + if p.fd_kind[i] != k: continue + if uint64(p.fd_table[i]) == fp: return 1 + return 0 def _dir_alloc(p: process.Process*) -> int: for i in range(process.PROC_MAX_DIRS): @@ -130,20 +229,128 @@ def _dir_free(p: process.Process*, dd: int): p.dir_used[dd] = None p.dir_table[dd] = None +# ============================================================================ +# Linux ABI helpers +# ============================================================================ + +# All handlers return int: non-negative on success, -errno on failure. +def _err(e: int) -> int: + return 0 - e + +def _fr_errno(r: int) -> int: + """Translate a FAT32 FRESULT into -errno (0 stays 0).""" + if r == 0: return 0 + if r == int(fat32_types.FRESULT.FR_NO_FILE) or r == int(fat32_types.FRESULT.FR_NO_PATH): + return _err(abi.ENOENT) + if r == int(fat32_types.FRESULT.FR_INVALID_NAME): + return _err(abi.EINVAL) + if r == int(fat32_types.FRESULT.FR_DENIED): + return _err(abi.EACCES) + if r == int(fat32_types.FRESULT.FR_EXIST): + return _err(abi.EEXIST) + if r == int(fat32_types.FRESULT.FR_INVALID_OBJECT): + return _err(abi.EBADF) + if r == int(fat32_types.FRESULT.FR_WRITE_PROTECTED): + return _err(abi.EROFS) + if r == int(fat32_types.FRESULT.FR_NOT_ENOUGH_CORE): + return _err(abi.ENOMEM) + if r == int(fat32_types.FRESULT.FR_TOO_MANY_OPEN_FILES): + return _err(abi.EMFILE) + if r == int(fat32_types.FRESULT.FR_INVALID_PARAMETER): + return _err(abi.EINVAL) + if r == int(fat32_types.FRESULT.FR_TIMEOUT): + return _err(abi.EIO) + if r == int(fat32_types.FRESULT.FR_LOCKED): + return _err(abi.EBUSY) + return _err(abi.EIO) + +def _open_mode(flags: uint32) -> uint8: + """Linux O_* -> FatFs FA_* access/create mode.""" + m: int = 0 + acc: uint32 = flags & uint32(abi.O_ACCMODE) + if acc == uint32(abi.O_RDONLY): + m = int(fat32_types.FA_READ) + elif acc == uint32(abi.O_WRONLY): + m = int(fat32_types.FA_WRITE) + else: + m = int(fat32_types.FA_READ) | int(fat32_types.FA_WRITE) + if (flags & uint32(abi.O_EXCL)) != 0 and (flags & uint32(abi.O_CREAT)) != 0: + m = m | int(fat32_types.FA_CREATE_NEW) + elif (flags & uint32(abi.O_TRUNC)) != 0: + m = m | int(fat32_types.FA_CREATE_ALWAYS) + elif (flags & uint32(abi.O_CREAT)) != 0: + m = m | int(fat32_types.FA_OPEN_ALWAYS) + elif (flags & uint32(abi.O_APPEND)) != 0: + m = m | int(fat32_types.FA_OPEN_APPEND) + return uint8(m) + +# Resolve `path` against `cwd` (dirfd == AT_FDCWD) or require an absolute path. +# Directory-relative lookups from a dir fd are added with openat (P1). +def _resolve_path(dirfd: int, path: const str, out: char*) -> int: + if path is None: return _err(abi.EFAULT) + n: int = 0 + if path[0] == 47: + while path[n] != 0: + if n >= abi.PATH_MAX - 1: return _err(abi.ENAMETOOLONG) + out[n] = path[n] + n += 1 + out[n] = 0 + return 0 + if dirfd != abi.AT_FDCWD: + return _err(abi.ENOTSUP) + p: process.Process* = _current_proc() + c: int = 0 + while p.cwd[c] != 0: + if n >= abi.PATH_MAX - 1: return _err(abi.ENAMETOOLONG) + out[n] = p.cwd[c] + n += 1 + c += 1 + if n > 0 and out[n - 1] != 47: + out[n] = 47 + n += 1 + i: int = 0 + while path[i] != 0: + if n >= abi.PATH_MAX - 1: return _err(abi.ENAMETOOLONG) + out[n] = path[i] + n += 1 + i += 1 + out[n] = 0 + return 0 + +# Fill the common fields of a Linux struct stat. Times stay 0: FAT32 stores +# DOS timestamps, which are not converted yet (P1 with newfstatat). +def _fill_stat(st: abi.linux_stat*, size: uint64, mode: uint32, ino: uint64): + string.memset(rawptr(st), 0, uint64(abi.SIZEOF_LINUX_STAT)) + st.st_ino = ino + st.st_nlink = 1 + st.st_mode = mode + st.st_size = size + st.st_blksize = 512 + st.st_blocks = (size + 511) / 512 + +# Software DRNG for getrandom(): no hardware entropy requirement, only used +# for stack canaries and similar non-cryptographic purposes (see docs). +_rng_state: uint64 = 0x9E3779B97F4A7C15 + +def _rng_next() -> uint64: + global _rng_state + # 64-bit LCG; the high bits are returned because low LCG bits are weak. + _rng_state = _rng_state * uint64(6364136223846793005) + uint64(1442695040888963407) + return _rng_state >> 16 + def EXIT(code: int) -> int: serial.puts("[syscall] exit called\n") + p: process.Process* = _current_proc() + p.exit_code = code # S3e:不再在这里拆窗口 —— 窗口/输入已整体交给用户态图形壳 explorer(/dev/desk 共享区), # 内核不再持有任何窗口语义。 - # 真正的退出:退役本线程并让出,**绝不返回用户态**。 + # Linux exit_group:退役本进程的**所有**线程,绝不返回用户态。 + # (THREAD_EXIT=60 只退当前线程。) # 只 destroy 窗口不够:app 的 _start 在 process.exit(0) 之后还有 `return 0`,而内核是以 - # rsp = USER_STACK_VTOP 进入 _start 的(elf.vp:379),VTOP 正是用户栈映射 - # [VTOP-64K, VTOP) 的上界之外那一字节 ⇒ 一旦 _start 收尾 `ret` 就会去读 [VTOP]: + # rsp = USER_STACK_VTOP 进入 _start 的(elf.vp),一旦收尾 `ret` 就会去读 [VTOP]: # #PF cr2=rsp=0x7fffffe00000,rip 在 _start 内,rbp=0,rax=退出码(实测现场)。 - # 口径与异常终止路径一致(idt.vp 的 free_user_thread + state=THREAD_DONE + 不再回用户态)。 - sched.Scheduler.exit_current_thread() - # 【硬保护】这里绝不允许返回:一旦返回就会沿调用链回到 app 的 _start 末尾,那里 `ret` - # 会弹到一个 0 返回地址并去 0 取指(现场:[0E] rip=0x0 err=0x15 = 用户态取指缺页, - # 紧随 [syscall] exit called,tid=5)。停留在此让调度器彻底不再调度本线程。 + sched.Scheduler.exit_process(p.pid) + # 【硬保护】exit_process 不返回;这里仅作兜底。 while True: sched.Scheduler._yield() return 0 @@ -185,19 +392,19 @@ def SO_CALL1(sym_name_ptr: str, arg1: rawptr) -> int: serial.puts("[so] call returned\n") return 0 -def GETPID() -> uint64: - """getpid():当前进程 pid。/dev/desk 的事件按 pid 过滤,app 需要它。""" - return uint64(process.current()) - -def _is_dev_path(path: const str) -> int: - """路径是否以 "/dev/" 开头(走 devfs 设备节点而不是 FAT32)。逐字符比较,不引 str 库。""" - if path[0] != 47: return 0 # '/' - if path[1] != 100: return 0 # 'd' - if path[2] != 101: return 0 # 'e' - if path[3] != 118: return 0 # 'v' - if path[4] != 47: return 0 # '/' - return 1 - +def GETPID() -> uint64: + """getpid():当前进程 pid。/dev/desk 的事件按 pid 过滤,app 需要它。""" + return uint64(process.current()) + +def _is_dev_path(path: const str) -> int: + """路径是否以 "/dev/" 开头(走 devfs 设备节点而不是 FAT32)。逐字符比较,不引 str 库。""" + if path[0] != 47: return 0 # '/' + if path[1] != 100: return 0 # 'd' + if path[2] != 101: return 0 # 'e' + if path[3] != 118: return 0 # 'v' + if path[4] != 47: return 0 # '/' + return 1 + def OPEN(path_ptr: str, mode: uint32) -> int: p: process.Process* = _current_proc() path: const str = path_ptr @@ -206,56 +413,89 @@ def OPEN(path_ptr: str, mode: uint32) -> int: if _is_dev_path(path): dfd: int = devfs.devfs_open(path, int(mode)) if dfd < 0: - return -1 + return _err(abi.ENOENT) fd: int = _fd_alloc(p) if fd < 0: devfs.devfs_close(dfd) - return -1 + return _err(abi.EMFILE) # 存 devfs_fd+1:devfs fd 0 是合法值,直接用会与"空槽位"混淆 p.fd_table[fd] = rawptr(uint64(dfd + 1)) p.fd_kind[fd] = process.FD_KIND_DEVFS + p.fd_flags[fd] = mode return fd fat32.lock() - fp: rawptr = fat32.open(path, uint8(mode)) + fp: rawptr = fat32.open(path, _open_mode(mode)) if not fp: fat32.unlock() - return -1 + return _err(abi.ENOENT) fd: int = _fd_alloc(p) if fd < 0: fat32.close(fp) fat32.unlock() - return -1 + return _err(abi.EMFILE) p.fd_table[fd] = fp p.fd_kind[fd] = process.FD_KIND_FAT32 + p.fd_flags[fd] = mode fat32.unlock() return fd def CLOSE(fd: int) -> int: p: process.Process* = _current_proc() if fd < 0 or fd >= process.PROC_MAX_FDS: - return int(fat32_types.FRESULT.FR_INVALID_OBJECT) + return _err(abi.EBADF) fp: rawptr = p.fd_table[fd] if not fp: - return int(fat32_types.FRESULT.FR_INVALID_OBJECT) + return _err(abi.EBADF) + # Pipes carry their own reference counts: every inherited/dup'd end was + # counted, so always decrement (fork's shared flag must not skip this). + if p.fd_kind[fd] == process.FD_KIND_PIPE: + res_pipe: int = _pipe_close(fp) + _fd_free(p, fd) + return res_pipe + # fork() copies the fd table; shared entries must not close the + # underlying object (the parent still owns it). + if p.fd_shared[fd] != 0: + _fd_free(p, fd) + return 0 + # dup()/dup2() aliases: only the last close releases the object. + if _fd_other_ref(p, fd) != 0: + _fd_free(p, fd) + return 0 if p.fd_kind[fd] == process.FD_KIND_DEVFS: res_dev: int = devfs.devfs_close(int(uint64(fp)) - 1) _fd_free(p, fd) - return res_dev + if res_dev < 0: return _err(abi.EIO) + return 0 + if p.fd_kind[fd] == process.FD_KIND_DIR: + dd: int = int(uint64(fp)) - 1 + if dd >= 0 and dd < process.PROC_MAX_DIRS: + fat32.lock() + fat32.closedir(&p.dir_pool[dd]) + fat32.unlock() + _dir_free(p, dd) + _fd_free(p, fd) + return 0 fat32.lock() res: int = int(fat32.close(fp)) fat32.unlock() _fd_free(p, fd) - return res + return _fr_errno(res) def READ(fd: int, buf_ptr: rawptr, count: uint32) -> int: p: process.Process* = _current_proc() if fd < 0 or fd >= process.PROC_MAX_FDS: - return -1 + return _err(abi.EBADF) fp: rawptr = p.fd_table[fd] if not fp: - return -1 + return _err(abi.EBADF) + if p.fd_kind[fd] == process.FD_KIND_DIR: + return _err(abi.EISDIR) + if p.fd_kind[fd] == process.FD_KIND_PIPE: + return _pipe_read(fp, buf_ptr, count) if p.fd_kind[fd] == process.FD_KIND_DEVFS: - return devfs.devfs_read(int(uint64(fp)) - 1, buf_ptr, count) + r: int = devfs.devfs_read(int(uint64(fp)) - 1, buf_ptr, count) + if r < 0: return _err(abi.EIO) + return r fat32.lock() br: uint32 = fat32.read(fp, buf_ptr, count) fat32.unlock() @@ -264,12 +504,18 @@ def READ(fd: int, buf_ptr: rawptr, count: uint32) -> int: def WRITE(fd: int, buf_ptr: rawptr, count: uint32) -> int: p: process.Process* = _current_proc() if fd < 0 or fd >= process.PROC_MAX_FDS: - return -1 + return _err(abi.EBADF) fp: rawptr = p.fd_table[fd] if not fp: - return -1 + return _err(abi.EBADF) + if p.fd_kind[fd] == process.FD_KIND_DIR: + return _err(abi.EISDIR) + if p.fd_kind[fd] == process.FD_KIND_PIPE: + return _pipe_write(fp, buf_ptr, count) if p.fd_kind[fd] == process.FD_KIND_DEVFS: - return devfs.devfs_write(int(uint64(fp)) - 1, buf_ptr, count) + r: int = devfs.devfs_write(int(uint64(fp)) - 1, buf_ptr, count) + if r < 0: return _err(abi.EIO) + return r fat32.lock() bw: uint32 = fat32.write(fp, buf_ptr, count) fat32.unlock() @@ -279,106 +525,302 @@ def IOCTL(fd: int, request: uint32, arg: rawptr) -> int: """设备控制(Linux ioctl 的子集):当前只有 devfs 设备支持,用于 /dev/fb0 查几何。""" p: process.Process* = _current_proc() if fd < 0 or fd >= process.PROC_MAX_FDS: - return -1 + return _err(abi.EBADF) if not p.fd_used[fd]: - return -1 + return _err(abi.EBADF) if p.fd_kind[fd] != process.FD_KIND_DEVFS: - return -1 - return devfs.devfs_ioctl(int(uint64(p.fd_table[fd])) - 1, request, arg) + return _err(abi.ENOTTY) + r: int = devfs.devfs_ioctl(int(uint64(p.fd_table[fd])) - 1, request, arg) + if r < 0: return _err(abi.EINVAL) + return r -def SEEK(fd: int, offset: uint32, whence: uint32) -> int: - """lseek(fd, offset, whence):0=SET 1=CUR 2=END(END 时 offset 为"距末尾的距离")。 +def SEEK(fd: int, offset: uint64, whence: uint32) -> int: + """lseek(fd, offset, whence) -> new offset, or -errno. - 私有 syscall TELL/SIZE 已删除:用户态用 lseek(fd,0,SEEK_CUR/SEEK_END) 表达即可。 + offset is 64-bit on the ABI side; FAT32 caps files at 4 GiB so anything + beyond that range is rejected with -EINVAL. Whence: 0=SET 1=CUR 2=END. """ p: process.Process* = _current_proc() if fd < 0 or fd >= process.PROC_MAX_FDS: - return int(fat32_types.FRESULT.FR_INVALID_OBJECT) + return _err(abi.EBADF) fp: rawptr = p.fd_table[fd] if not fp: - return int(fat32_types.FRESULT.FR_INVALID_OBJECT) + return _err(abi.EBADF) + if p.fd_kind[fd] == process.FD_KIND_DIR: + return _err(abi.EINVAL) + if p.fd_kind[fd] == process.FD_KIND_PIPE: + return _err(abi.ESPIPE) + if p.fd_kind[fd] == process.FD_KIND_DEVFS: + return _err(abi.ESPIPE) fat32.lock() - r: int = -1 - if whence == uint32(0): - r = int(fat32.seek(fp, offset)) - elif whence == uint32(1): - r = int(fat32.seek(fp, fat32.tell(fp) + offset)) - elif whence == uint32(2): - sz: uint32 = fat32.size(fp) - if offset <= sz: - r = int(fat32.seek(fp, sz - offset)) + base: int = 0 + if whence == uint32(abi.SEEK_CUR): + base = int(fat32.tell(fp)) + elif whence == uint32(abi.SEEK_END): + base = int(fat32.size(fp)) + elif whence != uint32(abi.SEEK_SET): + fat32.unlock() + return _err(abi.EINVAL) + tgt: int = base + int(offset) + if tgt < 0 or tgt > 0x7FFFFFFF: + fat32.unlock() + return _err(abi.EINVAL) + r: int = int(fat32.seek(fp, uint32(tgt))) fat32.unlock() - return r + if r != 0: return _fr_errno(r) + return tgt def MKDIR(path_ptr: str) -> int: path: const str = path_ptr fat32.lock() r: int = int(fat32.mkdir(path)) fat32.unlock() - return r + return _fr_errno(r) def REMOVE(path_ptr: str) -> int: path: const str = path_ptr fat32.lock() r: int = int(fat32.remove(path)) fat32.unlock() - return r + return _fr_errno(r) -def OPENdir(path_ptr: str) -> int: +# ---------------- P1: file system ABI ---------------- + +def _open_dir(path: const str) -> int: p: process.Process* = _current_proc() - path: const str = path_ptr dd: int = _dir_alloc(p) - if dd < 0: - return -1 + if dd < 0: return _err(abi.EMFILE) dp: fat32_types.dirobj* = &p.dir_pool[dd] fat32.lock() res: int = int(fat32.opendir(path, dp)) fat32.unlock() if res != 0: _dir_free(p, dd) - return -1 + return _fr_errno(res) p.dir_table[dd] = rawptr(uint64(dp)) - return dd + fd: int = _fd_alloc(p) + if fd < 0: + fat32.lock() + fat32.closedir(dp) + fat32.unlock() + _dir_free(p, dd) + return _err(abi.EMFILE) + p.fd_table[fd] = rawptr(uint64(dd + 1)) + p.fd_kind[fd] = process.FD_KIND_DIR + return fd -def READdir(dd: int, info_ptr: rawptr) -> int: - p: process.Process* = _current_proc() - if dd < 0 or dd >= process.PROC_MAX_DIRS: - return int(fat32_types.FRESULT.FR_INVALID_OBJECT) - dp: rawptr = p.dir_table[dd] - if not dp: - return int(fat32_types.FRESULT.FR_INVALID_OBJECT) - info: fat32_types.fileinfo* = info_ptr - fat32.lock() - r: int = int(fat32.readdir(dp, info)) - fat32.unlock() - return r +def OPENAT(dirfd: int, path_ptr: str, flags: uint32, mode: uint32) -> int: + path: const str = path_ptr + buf: [256]char + r: int = _resolve_path(dirfd, path, &buf[0]) + if r != 0: return r + if (flags & uint32(abi.O_DIRECTORY)) != 0: + return _open_dir(&buf[0]) + return OPEN(&buf[0], flags) -def CLOSEdir(dd: int) -> int: +def GETDENTS64(fd: int, buf_ptr: rawptr, count: uint64) -> int: p: process.Process* = _current_proc() - if dd < 0 or dd >= process.PROC_MAX_DIRS: - return int(fat32_types.FRESULT.FR_INVALID_OBJECT) - dp: rawptr = p.dir_table[dd] - if not dp: - return int(fat32_types.FRESULT.FR_INVALID_OBJECT) + if fd < 0 or fd >= process.PROC_MAX_FDS: return _err(abi.EBADF) + if not p.fd_used[fd]: return _err(abi.EBADF) + if p.fd_kind[fd] != process.FD_KIND_DIR: return _err(abi.ENOTDIR) + dd: int = int(uint64(p.fd_table[fd])) - 1 + if dd < 0 or dd >= process.PROC_MAX_DIRS: return _err(abi.EBADF) + dp: fat32_types.dirobj* = &p.dir_pool[dd] + info: fat32_types.fileinfo fat32.lock() - fat32.closedir(dp) + r: int = int(fat32.readdir(dp, &info)) fat32.unlock() - _dir_free(p, dd) + if r != 0: + if r == int(fat32_types.FRESULT.FR_NO_FILE): return 0 + return _fr_errno(r) + name: str = &info.fname[0] + n: int = int(string.strlen(name)) + reclen: int = (19 + n + 1 + 7) / 8 * 8 + if uint64(reclen) > count: return _err(abi.EINVAL) + d: abi.linux_dirent64* = buf_ptr + d.d_ino = uint64(1) + d.d_off = uint64(0) + d.d_reclen = uint16(reclen) + if (int(info.attr) & 0x10) != 0: + d.d_type = uint8(abi.DT_DIR) + else: + d.d_type = uint8(abi.DT_REG) + dst: char* = char*(uint64(buf_ptr) + 19) + i: int = 0 + while i < n: + dst[i] = name[i] + i += 1 + dst[n] = 0 + i = n + 1 + while i < reclen - 19: + dst[i] = 0 + i += 1 + return reclen + +def FSTAT(fd: int, st_ptr: rawptr) -> int: + p: process.Process* = _current_proc() + if fd < 0 or fd >= process.PROC_MAX_FDS: return _err(abi.EBADF) + if not p.fd_used[fd]: return _err(abi.EBADF) + if st_ptr is None: return _err(abi.EFAULT) + st: abi.linux_stat* = st_ptr + if p.fd_kind[fd] == process.FD_KIND_DEVFS: + _fill_stat(st, uint64(0), uint32(abi.MODE_CHAR), uint64(fd + 1)) + elif p.fd_kind[fd] == process.FD_KIND_DIR: + _fill_stat(st, uint64(0), uint32(abi.MODE_DIR), uint64(fd + 1)) + elif p.fd_kind[fd] == process.FD_KIND_PIPE: + _fill_stat(st, uint64(0), uint32(abi.MODE_FIFO), uint64(fd + 1)) + else: + fp: rawptr = p.fd_table[fd] + size: uint64 = uint64(fat32.size(fp)) + _fill_stat(st, size, uint32(abi.MODE_FILE), uint64(fd + 1)) return 0 +def NEWFSTATAT(dirfd: int, path_ptr: str, st_ptr: rawptr, flags: uint32) -> int: + if st_ptr is None: return _err(abi.EFAULT) + path: const str = path_ptr + if path is None: return _err(abi.EFAULT) + if path[0] == 0: + if (flags & uint32(abi.AT_EMPTY_PATH)) != 0: + return FSTAT(dirfd, st_ptr) + return _err(abi.ENOENT) + st: abi.linux_stat* = st_ptr + buf: [256]char + r: int = _resolve_path(dirfd, path, &buf[0]) + if r != 0: return r + if _is_dev_path(&buf[0]): + _fill_stat(st, uint64(0), uint32(abi.MODE_CHAR), uint64(1)) + return 0 + if buf[0] == 47 and buf[1] == 0: + _fill_stat(st, uint64(0), uint32(abi.MODE_DIR), uint64(1)) + return 0 + info: fat32_types.fileinfo + fat32.lock() + res: int = int(fat32.stat(&buf[0], &info)) + fat32.unlock() + if res != 0: return _fr_errno(res) + if (int(info.attr) & 0x10) != 0: + _fill_stat(st, uint64(0), uint32(abi.MODE_DIR), uint64(1)) + else: + _fill_stat(st, uint64(info.file_size), uint32(abi.MODE_FILE), uint64(1)) + return 0 + +def DUP(fd: int) -> int: + p: process.Process* = _current_proc() + if fd < 0 or fd >= process.PROC_MAX_FDS: return _err(abi.EBADF) + if not p.fd_used[fd]: return _err(abi.EBADF) + nfd: int = _fd_alloc(p) + if nfd < 0: return _err(abi.EMFILE) + p.fd_table[nfd] = p.fd_table[fd] + p.fd_kind[nfd] = p.fd_kind[fd] + p.fd_flags[nfd] = p.fd_flags[fd] + p.fd_shared[nfd] = p.fd_shared[fd] + if p.fd_kind[nfd] == process.FD_KIND_PIPE: + _pipe_add_ref(p.fd_table[nfd]) + return nfd + +def DUP2(oldfd: int, newfd: int) -> int: + p: process.Process* = _current_proc() + if oldfd < 0 or oldfd >= process.PROC_MAX_FDS: return _err(abi.EBADF) + if not p.fd_used[oldfd]: return _err(abi.EBADF) + if newfd < 0 or newfd >= process.PROC_MAX_FDS: return _err(abi.EBADF) + if oldfd == newfd: return newfd + if p.fd_used[newfd]: + r: int = CLOSE(newfd) + if r != 0: return r + p.fd_used[newfd] = 1 + p.fd_table[newfd] = p.fd_table[oldfd] + p.fd_kind[newfd] = p.fd_kind[oldfd] + p.fd_flags[newfd] = p.fd_flags[oldfd] + p.fd_shared[newfd] = p.fd_shared[oldfd] + if p.fd_kind[newfd] == process.FD_KIND_PIPE: + _pipe_add_ref(p.fd_table[newfd]) + return newfd + +def DUP3(oldfd: int, newfd: int, flags: uint32) -> int: + return DUP2(oldfd, newfd) + +def FCNTL(fd: int, cmd: uint32, arg: uint64) -> int: + p: process.Process* = _current_proc() + if fd < 0 or fd >= process.PROC_MAX_FDS: return _err(abi.EBADF) + if not p.fd_used[fd]: return _err(abi.EBADF) + c: int = int(cmd) + if c == abi.F_DUPFD or c == abi.F_DUPFD_CLOEXEC: + return DUP(fd) + if c == abi.F_GETFD: + return 0 + if c == abi.F_SETFD: + return 0 + if c == abi.F_GETFL: + return int(p.fd_flags[fd]) + if c == abi.F_SETFL: + p.fd_flags[fd] = uint32(arg) + return 0 + return _err(abi.EINVAL) + +def PREAD64(fd: int, buf_ptr: rawptr, count: uint32, offset: uint64) -> int: + p: process.Process* = _current_proc() + if fd < 0 or fd >= process.PROC_MAX_FDS: return _err(abi.EBADF) + if not p.fd_used[fd]: return _err(abi.EBADF) + if p.fd_kind[fd] != process.FD_KIND_FAT32: return _err(abi.ESPIPE) + cur: int = SEEK(fd, uint64(0), uint32(abi.SEEK_CUR)) + if cur < 0: return cur + r: int = SEEK(fd, offset, uint32(abi.SEEK_SET)) + if r < 0: return r + n: int = READ(fd, buf_ptr, count) + SEEK(fd, uint64(cur), uint32(abi.SEEK_SET)) + return n + +def PWRITE64(fd: int, buf_ptr: rawptr, count: uint32, offset: uint64) -> int: + p: process.Process* = _current_proc() + if fd < 0 or fd >= process.PROC_MAX_FDS: return _err(abi.EBADF) + if not p.fd_used[fd]: return _err(abi.EBADF) + if p.fd_kind[fd] != process.FD_KIND_FAT32: return _err(abi.ESPIPE) + cur: int = SEEK(fd, uint64(0), uint32(abi.SEEK_CUR)) + if cur < 0: return cur + r: int = SEEK(fd, offset, uint32(abi.SEEK_SET)) + if r < 0: return r + n: int = WRITE(fd, buf_ptr, count) + SEEK(fd, uint64(cur), uint32(abi.SEEK_SET)) + return n + +def RENAME(old_ptr: str, new_ptr: str) -> int: + oldp: const str = old_ptr + newp: const str = new_ptr + fat32.lock() + r: int = int(fat32.rename(oldp, newp)) + fat32.unlock() + return _fr_errno(r) + +def RMDIR(path_ptr: str) -> int: + path: const str = path_ptr + fat32.lock() + r: int = int(fat32.rmdir(path)) + fat32.unlock() + return _fr_errno(r) + +def UNLINKAT(dirfd: int, path_ptr: str, flags: uint32) -> int: + path: const str = path_ptr + buf: [256]char + r: int = _resolve_path(dirfd, path, &buf[0]) + if r != 0: return r + if (flags & uint32(abi.AT_REMOVEDIR)) != 0: + return RMDIR(&buf[0]) + return REMOVE(&buf[0]) + def STAT(path_ptr: str, info_ptr: rawptr) -> int: path: const str = path_ptr info: fat32_types.fileinfo* = info_ptr fat32.lock() r: int = int(fat32.stat(path, info)) fat32.unlock() - return r + return _fr_errno(r) -def SPAWN(path_ptr: str, blocking: int) -> int: +# Private launcher kept for Terminal's `run` command (a new child, caller stays alive). +def SPAWN_PROC(path_ptr: str, blocking: int) -> int: path: const str = path_ptr pid: int = elf.spawn_elf(path) if pid < 0: - return pid + return _err(abi.ENOENT) if blocking: p: process.Process* = process.ProcessManager.get_process(pid) if p: @@ -386,65 +828,1138 @@ def SPAWN(path_ptr: str, blocking: int) -> int: sched.Scheduler.try_reschedule() return pid -define USER_HEAP_BASE = 0x20000000 +def WAIT4(pid: int, status_ptr: rawptr, options: uint32, rusage: rawptr) -> int: + cur: int = process.current() + while True: + found_child: int = 0 + i: int = 0 + while i < process.MAX_PROCESSES: + p: process.Process* = process.ProcessManager.get_process(i) + if p is not None: + matches: int = 0 + if pid <= 0: + matches = 1 # any child (pid 0: same group, approximated) + elif p.pid == pid: + matches = 1 + if matches != 0 and p.pid != cur and p.parent_pid == cur: + found_child = 1 + if p.state == process.PROC_DONE: + if status_ptr is not None: + st: int* = int*(status_ptr) + st[0] = (p.exit_code & 0xFF) << 8 + p.parent_pid = -1 + return p.pid + i += 1 + if found_child == 0: + return _err(abi.ECHILD) + if (options & uint32(1)) != 0: # WNOHANG + return 0 + sched.Scheduler.yield_now() + +# mmap bump allocator and brk base must not overlap (both live below the stack). +define USER_HEAP_BASE = 0x20000000 +define USER_BRK_BASE = 0x40000000 +define USER_VA_LIMIT = 0x7FFFFF000000 -# ---- mmap 的 prot / flags(值与 Linux 同名常量一致)---- -define PROT_NONE = 0x0 -define PROT_READ = 0x1 -define PROT_WRITE = 0x2 -define PROT_EXEC = 0x4 -define MAP_SHARED = 0x01 -define MAP_PRIVATE = 0x02 -define MAP_FIXED = 0x10 -define MAP_ANONYMOUS = 0x20 # fd = -1(Linux 匿名映射约定) define MMAP_FD_ANON: uint64 = 0xFFFFFFFFFFFFFFFF -def MMAP(addr: uint64, length: uint64, prot: uint64, flags: uint64, fd: uint64, offset: uint64) -> uint64: - """mmap(addr, length, prot, flags, fd, offset):与 Linux 同名同参。 +# ---- VMA bookkeeping ------------------------------------------------------- - · fd == MMAP_FD_ANON(-1) 且带 MAP_ANONYMOUS:匿名映射 —— 分配物理页、清零后映射到 - 本进程新 VA(对齐 Linux 的"匿名映射必为 0"语义); - · fd 是带 mmap_info 的 devfs 设备 fd(当前仅 /dev/fb0):把设备物理区间 - [phys+offset, phys+offset+length) 按页映射进来(用户可读写)。 - addr 与 MAP_FIXED 暂未实现:一律由内核挑 VA,返回真实起始地址。失败返回 0。 +def _vma_find(p: process.Process*, addr: uint64) -> int: + for i in range(process.MAX_VMAS): + if p.vma_kind[i] == 0: continue + b: uint64 = p.vma_base[i] + if addr >= b and addr < b + p.vma_size[i]: return i + return -1 + +def _vma_overlap(p: process.Process*, start: uint64, size: uint64) -> int: + if size == 0: return 0 + end: uint64 = start + size + for i in range(process.MAX_VMAS): + if p.vma_kind[i] == 0: continue + b: uint64 = p.vma_base[i] + e: uint64 = b + p.vma_size[i] + if start < e and b < end: return 1 + return 0 + +def _vma_add(p: process.Process*, base: uint64, size: uint64, prot: uint32, kind: int, flags: uint32) -> int: + for i in range(process.MAX_VMAS): + if p.vma_kind[i] == 0: + p.vma_base[i] = base + p.vma_size[i] = size + p.vma_prot[i] = prot + p.vma_kind[i] = uint8(kind) + p.vma_flags[i] = flags + return 0 + return _err(abi.ENOMEM) + +def _vma_del(p: process.Process*, i: int): + p.vma_base[i] = 0 + p.vma_size[i] = 0 + p.vma_prot[i] = 0 + p.vma_kind[i] = 0 + p.vma_flags[i] = 0 + +def _unmap_pages(p: process.Process*, start: uint64, end: uint64, free_pages: int): + va: uint64 = start + while va < end: + if free_pages != 0: + phys: uint64 = paging.GetPagePhys(va) + if phys != 0: + mm.free_page(rawptr(phys)) + paging.UnMapPage(va) + va = va + uint64(4096) + +# Remove [addr, addr+length) from the VMA table, splitting partially covered +# areas, and release the backing frames of anon/file mappings. +def _vma_unmap_range(p: process.Process*, addr: uint64, length: uint64) -> int: + if length == 0: return 0 + end: uint64 = addr + length + i: int = 0 + while i < process.MAX_VMAS: + if p.vma_kind[i] != 0: + b: uint64 = p.vma_base[i] + e: uint64 = b + p.vma_size[i] + if b < end and addr < e: + s: uint64 = addr + if b > s: s = b + t: uint64 = end + if e < t: t = e + free_pages: int = 1 + if p.vma_kind[i] == uint8(process.VMA_KIND_DEV): free_pages = 0 + _unmap_pages(p, s, t, free_pages) + if s <= b and t >= e: + _vma_del(p, i) + elif s <= b: + p.vma_base[i] = t + p.vma_size[i] = e - t + elif t >= e: + p.vma_size[i] = s - b + else: + r: int = _vma_add(p, t, e - t, uint32(p.vma_prot[i]), + int(p.vma_kind[i]), uint32(p.vma_flags[i])) + if r != 0: return r + p.vma_size[i] = s - b + i += 1 + return 0 + +def MMAP(addr: uint64, length: uint64, prot: uint64, flags: uint64, fd: uint64, offset: uint64) -> uint64: + """mmap(addr, length, prot, flags, fd, offset) -> base, or MAP_FAILED (-1). + + · MAP_ANONYMOUS: fresh zeroed pages; + · FAT32 fd: file contents copied into fresh pages (MAP_PRIVATE semantics); + · devfs fd: the device physical range is mapped directly (no frame owns it). """ p: process.Process* = _current_proc() - if length == 0: return 0 - aligned: uint64 = (length + 4095) & ~uint64(4095) - if p.heap_start == 0: - p.heap_start = USER_HEAP_BASE - va: uint64 = p.heap_start + p.heap_size - if fd == MMAP_FD_ANON: - if (flags & MAP_ANONYMOUS) == 0: return 0 - for off in range(0, aligned, 4096): - phys_page: rawptr = mm.malloc(4096) - if phys_page is None: return 0 - string.memset(phys_page, 0, 4096) - paging.MapPage(va + off, uint64(phys_page), - paging.PTE_PRESENT | paging.PTE_WRITABLE | paging.PTE_USER) + if length == 0: return abi.MAP_FAILED + aligned: uint64 = (length + uint64(0xFFF)) & ~uint64(0xFFF) + if aligned == 0: return abi.MAP_FAILED + fixed: int = 0 + if (flags & uint64(abi.MAP_FIXED)) != 0: fixed = 1 + base: uint64 = 0 + if fixed != 0: + if (addr & uint64(0xFFF)) != 0: return abi.MAP_FAILED + base = addr + if base < uint64(0x10000) or base + aligned > uint64(USER_VA_LIMIT): + return abi.MAP_FAILED + if p.brk_end > p.brk_start and base < p.brk_end and p.brk_start < base + aligned: + return abi.MAP_FAILED + else: + if p.heap_start == 0: p.heap_start = USER_HEAP_BASE + base = p.heap_start + p.heap_size + while True: + brk_hit: int = 0 + if p.brk_end > p.brk_start and base < p.brk_end and p.brk_start < base + aligned: + brk_hit = 1 + if _vma_overlap(p, base, aligned) == 0 and brk_hit == 0: break + p.heap_size = p.heap_size + aligned + base = p.heap_start + p.heap_size + if base + aligned > uint64(USER_VA_LIMIT): return abi.MAP_FAILED + if base + aligned > uint64(USER_VA_LIMIT): return abi.MAP_FAILED + # classify the mapping source + kind: int = process.VMA_KIND_ANON + uncached: int = 0 + dev_phys: uint64 = 0 + dev_size: uint64 = 0 + fp: rawptr = None + if (flags & uint64(abi.MAP_ANONYMOUS)) != 0: + kind = process.VMA_KIND_ANON + elif fd == MMAP_FD_ANON: + return abi.MAP_FAILED else: fdi: int = int(fd) - if fdi < 0 or fdi >= process.PROC_MAX_FDS: return 0 - if not p.fd_used[fdi]: return 0 - if p.fd_kind[fdi] != process.FD_KIND_DEVFS: return 0 - dev_phys: uint64 = 0 - dev_size: uint64 = 0 - if devfs.devfs_get_mmap_info(int(uint64(p.fd_table[fdi])) - 1, &dev_phys, &dev_size) != 0: - return 0 - if offset + aligned > dev_size: return 0 - for off in range(0, aligned, 4096): - paging.MapPage(va + off, dev_phys + offset + off, - paging.PTE_PRESENT | paging.PTE_WRITABLE | paging.PTE_USER) - p.heap_size = p.heap_size + aligned - return va + if fdi < 0 or fdi >= process.PROC_MAX_FDS: return abi.MAP_FAILED + if not p.fd_used[fdi]: return abi.MAP_FAILED + if p.fd_kind[fdi] == process.FD_KIND_DEVFS: + kind = process.VMA_KIND_DEV + if (offset & uint64(0xFFF)) != 0: return abi.MAP_FAILED + if devfs.devfs_get_mmap_info(int(uint64(p.fd_table[fdi])) - 1, &dev_phys, &dev_size) != 0: + return abi.MAP_FAILED + if devfs.devfs_mmap_uncached(int(uint64(p.fd_table[fdi])) - 1) != 0: + uncached = 1 + if offset + aligned > dev_size: return abi.MAP_FAILED + elif p.fd_kind[fdi] == process.FD_KIND_FAT32: + kind = process.VMA_KIND_FILE + if (offset & uint64(0xFFF)) != 0: return abi.MAP_FAILED + if offset > uint64(0xFFFFFFFF): return abi.MAP_FAILED + fp = p.fd_table[fdi] + else: + return abi.MAP_FAILED + if fixed != 0: + ur: int = _vma_unmap_range(p, base, aligned) + if ur != 0: return abi.MAP_FAILED + pte: uint64 = paging.PTE_PRESENT | paging.PTE_USER + if (prot & uint64(abi.PROT_WRITE)) != 0: + pte = pte | paging.PTE_WRITABLE + if uncached != 0: + # MMIO (framebuffer). Use PAT write-combining when the CPU exposes it; + # otherwise leave the mapping write-back (fast sequential blits, and the + # compositor does not rely on UC for visibility). UC (PTE_PCD) made + # dragging noticeably slower on VMware. + if paging.has_pat_wc() != 0: + pte = pte | paging.PTE_PAT + # 这一段会连续调用 MapPage,而 MapPage 依赖**全局 paging.pml4**。若中途被 + # 定时器抢占并切换进程,paging.pml4 会被 switch_pid 改成别的进程,后续页就 + # 映射进错误的地址空间(本进程留下未映射空洞;用户写空洞时若命中 identity + # 大页就会直接改物理内存 → BAD blk / 画面花屏)。整个映射循环必须原子。 + asm.cli() + va: uint64 = base + if kind == process.VMA_KIND_FILE: + fat32.lock() + oldpos: uint32 = fat32.tell(fp) + fat32.seek(fp, uint32(offset)) + fat32.unlock() + done: uint64 = 0 + while va < base + aligned: + fphys: rawptr = mm.alloc_page() + if fphys is None: + fat32.lock() + fat32.seek(fp, oldpos) + fat32.unlock() + _unmap_pages(p, base, va, 1) + asm.sti() + return abi.MAP_FAILED + string.memset(fphys, 0, 4096) + chunk: uint32 = 4096 + if length - done < uint64(4096): chunk = uint32(length - done) + if chunk > 0: + fat32.lock() + got: int = fat32.read(fp, fphys, chunk) + fat32.unlock() + if got > 0: + done = done + uint64(got) + paging.MapPage(va, uint64(fphys), pte) + va = va + uint64(4096) + fat32.lock() + fat32.seek(fp, oldpos) + fat32.unlock() + else: + while va < base + aligned: + if kind == process.VMA_KIND_DEV: + paging.MapPage(va, dev_phys + offset + (va - base), pte) + else: + aphys: rawptr = mm.alloc_page() + if aphys is None: + _mf: [128]char + viperlib.snprintf(&_mf, 128, "[mmap] FAIL pid=%d va=0x%lx len=0x%lx\n", + p.pid, va, aligned) + serial.puts(&_mf) + _unmap_pages(p, base, va, 1) + asm.sti() + return abi.MAP_FAILED + string.memset(aphys, 0, 4096) + paging.MapPage(va, uint64(aphys), pte) + va = va + uint64(4096) + r: int = _vma_add(p, base, aligned, uint32(prot), kind, uint32(flags)) + if r != 0: + if kind == process.VMA_KIND_DEV: + _unmap_pages(p, base, base + aligned, 0) + else: + _unmap_pages(p, base, base + aligned, 1) + asm.sti() + return abi.MAP_FAILED + if fixed == 0: + p.heap_size = p.heap_size + aligned + asm.sti() + return base def MUNMAP(ptr: uint64, size: uint64) -> int: + if size == 0: return _err(abi.EINVAL) + start: uint64 = ptr & ~uint64(0xFFF) + end: uint64 = (ptr + size + uint64(0xFFF)) & ~uint64(0xFFF) + if end <= start: return _err(abi.EINVAL) + if start < uint64(0x10000) or end > uint64(USER_VA_LIMIT): return _err(abi.EINVAL) + asm.cli() + _mr: int = _vma_unmap_range(_current_proc(), start, end - start) + asm.sti() + return _mr + +def MPROTECT(addr: uint64, length: uint64, prot: uint64) -> int: + p: process.Process* = _current_proc() + if length == 0: return 0 + start: uint64 = addr & ~uint64(0xFFF) + end: uint64 = (addr + length + uint64(0xFFF)) & ~uint64(0xFFF) + if end <= start: return _err(abi.EINVAL) + if start < uint64(0x10000) or end > uint64(USER_VA_LIMIT): return _err(abi.EINVAL) + va: uint64 = start + while va < end: + # Kernel-loaded ELF segments are not in the VMA table; accept any + # present 4KB page so RELRO mprotect() on the main image still works. + if _vma_find(p, va) < 0 and paging.GetPagePhys(va) == 0: + return _err(abi.ENOMEM) + va = va + uint64(4096) + # Snapshot the VMAs that overlap: splitting below adds new entries. + idx: [64]int + n: int = 0 + for i in range(process.MAX_VMAS): + if p.vma_kind[i] == 0: continue + b: uint64 = p.vma_base[i] + e: uint64 = b + p.vma_size[i] + if b < end and start < e: + idx[n] = i + n += 1 + j: int = 0 + while j < n: + i: int = idx[j] + b: uint64 = p.vma_base[i] + e: uint64 = b + p.vma_size[i] + s: uint64 = start + if b > s: s = b + t: uint64 = end + if e < t: t = e + old_prot: uint32 = uint32(p.vma_prot[i]) + kind: int = int(p.vma_kind[i]) + vflags: uint32 = uint32(p.vma_flags[i]) + if s <= b and t >= e: + p.vma_prot[i] = uint32(prot) + elif s <= b: + r: int = _vma_add(p, t, e - t, old_prot, kind, vflags) + if r != 0: return r + p.vma_size[i] = t - b + p.vma_prot[i] = uint32(prot) + elif t >= e: + r = _vma_add(p, s, e - s, uint32(prot), kind, vflags) + if r != 0: return r + p.vma_size[i] = s - b + else: + r = _vma_add(p, s, t - s, uint32(prot), kind, vflags) + if r != 0: return r + r = _vma_add(p, t, e - t, old_prot, kind, vflags) + if r != 0: return r + p.vma_size[i] = s - b + j += 1 + pte: uint64 = paging.PTE_PRESENT | paging.PTE_USER + if (prot & uint64(abi.PROT_WRITE)) != 0: + pte = pte | paging.PTE_WRITABLE + va = start + while va < end: + if paging.SetPageFlags(va, pte) == 0: return _err(abi.ENOMEM) + va = va + uint64(4096) return 0 +def BRK(addr: uint64) -> uint64: + p: process.Process* = _current_proc() + if p.brk_start == 0: + p.brk_start = USER_BRK_BASE + p.brk_end = USER_BRK_BASE + if addr == 0: return p.brk_end + if addr < p.brk_start or addr > uint64(USER_VA_LIMIT): return p.brk_end + old_map: uint64 = (p.brk_end + uint64(0xFFF)) & ~uint64(0xFFF) + new_map: uint64 = (addr + uint64(0xFFF)) & ~uint64(0xFFF) + if new_map > old_map: + asm.cli() + va: uint64 = old_map + while va < new_map: + if _vma_overlap(p, va, uint64(4096)) != 0: + _unmap_pages(p, old_map, va, 1) + asm.sti() + return p.brk_end + bphys: rawptr = mm.alloc_page() + if bphys is None: + _unmap_pages(p, old_map, va, 1) + asm.sti() + return p.brk_end + string.memset(bphys, 0, 4096) + paging.MapPage(va, uint64(bphys), + paging.PTE_PRESENT | paging.PTE_WRITABLE | paging.PTE_USER) + va = va + uint64(4096) + asm.sti() + elif new_map < old_map: + _unmap_pages(p, new_map, old_map, 1) + p.brk_end = addr + return p.brk_end + +def MREMAP(old_addr: uint64, old_size: uint64, new_size: uint64, flags: uint64, new_addr: uint64) -> uint64: + p: process.Process* = _current_proc() + if old_size == 0 or new_size == 0: return abi.MAP_FAILED + if (old_addr & uint64(0xFFF)) != 0: return abi.MAP_FAILED + old_len: uint64 = (old_size + uint64(0xFFF)) & ~uint64(0xFFF) + new_len: uint64 = (new_size + uint64(0xFFF)) & ~uint64(0xFFF) + if old_addr < uint64(0x10000) or old_addr + old_len > uint64(USER_VA_LIMIT): + return abi.MAP_FAILED + if _vma_find(p, old_addr) < 0: return abi.MAP_FAILED + if new_len <= old_len: + if new_len < old_len: + _vma_unmap_range(p, old_addr + new_len, old_len - new_len) + return old_addr + if (flags & uint64(abi.MREMAP_MAYMOVE)) == 0: + return abi.MAP_FAILED + mflags: uint64 = uint64(abi.MAP_PRIVATE | abi.MAP_ANONYMOUS) + hint: uint64 = 0 + if (flags & uint64(abi.MREMAP_FIXED)) != 0: + if new_addr < old_addr + old_len and old_addr < new_addr + new_len: + return abi.MAP_FAILED + mflags = mflags | uint64(abi.MAP_FIXED) + hint = new_addr + nv: uint64 = MMAP(hint, new_len, uint64(abi.PROT_READ | abi.PROT_WRITE), + mflags, MMAP_FD_ANON, 0) + if nv == abi.MAP_FAILED: return abi.MAP_FAILED + string.memcpy(rawptr(nv), rawptr(old_addr), old_len) + _vma_unmap_range(p, old_addr, old_len) + return nv + +def MADVISE(addr: uint64, length: uint64, advice: uint32) -> int: + # No paging/swapping policy yet; accept the hint. + return 0 + +# ============================================================================ +# P4/P5/P6: TLS, futex, time, signals, iovec, pipes, reboot +# ============================================================================ + +# ---- arch_prctl ---- +def ARCH_PRCTL(code: int, addr: uint64) -> int: + if code == abi.ARCH_SET_FS: + sched.set_fs_base(addr) + return 0 + if code == abi.ARCH_GET_FS: + if addr == 0: return _err(abi.EFAULT) + dst: uint64* = uint64*(addr) + dst[0] = sched.get_fs_base() + return 0 + return _err(abi.ENOSYS) + +# ---- futex: polling implementation (no sleep queue yet) ---- +def FUTEX(uaddr: uint64, op: uint32, val: uint32, timeout: rawptr) -> int: + if uaddr == 0: return _err(abi.EFAULT) + cmd: int = int(op) & 0x7F + u: uint32* = uint32*(uaddr) + if cmd == 0 or cmd == 9: + if u[0] != val: return _err(abi.EAGAIN) + while u[0] == val: + sched.Scheduler.yield_now() + return 0 + if cmd == 1 or cmd == 10: + return 0 + return _err(abi.ENOSYS) + +# ---- time ---- +def _rtc_epoch() -> uint64: + y: int = int(rtc.rtc_get_year()) + mo: int = int(rtc.rtc_get_month()) + d: int = int(rtc.rtc_get_day()) + h: int = int(rtc.rtc_get_hours()) + mi: int = int(rtc.rtc_get_minutes()) + s: int = int(rtc.rtc_get_seconds()) + yy: int = y + if mo <= 2: yy = yy - 1 + era: int = yy / 400 + yoe: int = yy - era * 400 + mp: int = (mo + 9) % 12 + doy: int = (153 * mp + 2) / 5 + d - 1 + doe: int = yoe * 365 + yoe / 4 - yoe / 100 + doy + days: int = era * 146097 + doe - 719468 + return uint64(days * 86400 + h * 3600 + mi * 60 + s) + +def CLOCK_GETTIME(clockid: int, ts_ptr: rawptr) -> int: + if ts_ptr is None: return _err(abi.EFAULT) + ts: abi.linux_timespec* = ts_ptr + if clockid == 0: + ts.tv_sec = _rtc_epoch() + ts.tv_nsec = uint64(0) + else: + t: uint64 = timer.timer_get_ticks() + ts.tv_sec = t / uint64(1000) + ts.tv_nsec = (t % uint64(1000)) * uint64(1000000) + return 0 + +def CLOCK_GETRES(clockid: int, ts_ptr: rawptr) -> int: + if ts_ptr is None: return _err(abi.EFAULT) + ts: abi.linux_timespec* = ts_ptr + ts.tv_sec = uint64(0) + ts.tv_nsec = uint64(1000000) + return 0 + +def GETTIMEOFDAY(tv_ptr: rawptr, tz_ptr: rawptr) -> int: + if tv_ptr is not None: + tv: uint64* = uint64*(tv_ptr) + tv[0] = _rtc_epoch() + tv[1] = uint64(0) + if tz_ptr is not None: + string.memset(tz_ptr, 0, 8) + return 0 + +def NANOSLEEP(req_ptr: rawptr, rem_ptr: rawptr) -> int: + if req_ptr is None: return _err(abi.EFAULT) + ts: abi.linux_timespec* = req_ptr + ms: uint64 = ts.tv_sec * uint64(1000) + ts.tv_nsec / uint64(1000000) + if ms > uint64(0x7FFFFFFF): ms = uint64(0x7FFFFFFF) + if ms > 0: + timer.timer_msleep(uint32(ms)) + return 0 + +# ---- signals: registration + delivery on syscall return ---- +define SIG_DFL = 0 +define SIG_IGN = 1 +define SA_NODEFER = 0x40000000 + +def RT_SIGACTION(sig: int, act_ptr: rawptr, old_ptr: rawptr, sigsetsize: uint32) -> int: + if sig < 1 or sig > 64: return _err(abi.EINVAL) + p: process.Process* = _current_proc() + if old_ptr is not None: + o: uint64* = uint64*(old_ptr) + o[0] = p.sig_handler[sig] + o[1] = p.sig_actflags[sig] + o[2] = p.sig_restorer[sig] + o[3] = uint64(0) + if act_ptr is not None: + a: uint64* = uint64*(act_ptr) + p.sig_handler[sig] = a[0] + p.sig_actflags[sig] = a[1] + p.sig_restorer[sig] = a[2] + return 0 + +def RT_SIGPROCMASK(how: int, set_ptr: rawptr, old_ptr: rawptr, sigsetsize: uint32) -> int: + p: process.Process* = _current_proc() + if old_ptr is not None: + o: uint64* = uint64*(old_ptr) + o[0] = p.sig_mask + if set_ptr is not None: + s: uint64* = uint64*(set_ptr) + m: uint64 = s[0] + if how == 0: + p.sig_mask = p.sig_mask | m + elif how == 1: + p.sig_mask = p.sig_mask & ~m + elif how == 2: + p.sig_mask = m + else: + return _err(abi.EINVAL) + return 0 + +def KILL(pid: int, sig: int) -> int: + if sig < 0 or sig > 64: return _err(abi.EINVAL) + if sig == 0: return 0 + target: process.Process* = None + if pid == 0 or pid == process.current(): + target = _current_proc() + elif pid > 0: + target = process.ProcessManager.get_process(pid) + if target is None: return _err(abi.ESRCH) + target.sig_pending = target.sig_pending | (uint64(1) << uint64(sig - 1)) + return 0 + +def RT_SIGRETURN() -> int: + f: uint64* = uint64*(sched.get_syscall_frame()) + sf: uint64 = f[15] - uint64(8) + w: uint64* = uint64*(sf) + i: int = 0 + while i < 12: + f[i] = w[1 + i] + i += 1 + f[13] = w[13] + f[12] = w[14] + f[15] = w[15] + p: process.Process* = _current_proc() + p.sig_mask = w[18] + return int(w[16]) + +# Deliver one pending signal by rewriting the syscall return frame. +# Returns 1 if a handler was entered (the syscall must return 0 in RAX). +def _deliver_signals(ret: uint64) -> int: + p: process.Process* = _current_proc() + avail: uint64 = p.sig_pending & ~p.sig_mask + # SIGKILL(9) / SIGSTOP(19) cannot be blocked + avail = avail | (p.sig_pending & (uint64(1) << uint64(8))) + avail = avail | (p.sig_pending & (uint64(1) << uint64(18))) + if avail == 0: return 0 + sig: int = 1 + while sig <= 64: + if (avail & (uint64(1) << uint64(sig - 1))) != 0: break + sig += 1 + if sig > 64: return 0 + bit: uint64 = uint64(1) << uint64(sig - 1) + p.sig_pending = p.sig_pending & ~bit + h: uint64 = p.sig_handler[sig] + if h == uint64(SIG_IGN): + return _deliver_signals(ret) + if h == uint64(SIG_DFL): + if sig == 1 or sig == 2 or sig == 3 or sig == 4 or sig == 5 or sig == 6 or sig == 7 or sig == 8 or sig == 9 or sig == 11 or sig == 13 or sig == 14 or sig == 15: + p.exit_code = 128 + sig + sched.Scheduler.exit_process(p.pid) + return _deliver_signals(ret) + rest: uint64 = p.sig_restorer[sig] + if rest == 0: + return _deliver_signals(ret) + f: uint64* = uint64*(sched.get_syscall_frame()) + user_rsp: uint64 = f[15] + user_rip: uint64 = f[13] + user_flags: uint64 = f[12] + # The handler is entered with RSP == sf and [sf] == restorer, exactly like a + # normal call: rsp % 16 == 8 at function entry. rt_sigreturn then runs with + # rsp == sf + 8 and recovers sf as (user_rsp - 8). + sf: uint64 = ((user_rsp - uint64(320)) & ~uint64(0xF)) - uint64(8) + w: uint64* = uint64*(sf) + w[0] = rest + i: int = 0 + while i < 12: + w[1 + i] = f[i] + i += 1 + w[13] = user_rip + w[14] = user_flags + w[15] = user_rsp + w[16] = ret + w[17] = uint64(sig) + w[18] = p.sig_mask + si: uint64 = sf + uint64(152) + string.memset(rawptr(si), 0, 128) + si32: uint32* = uint32*(si) + si32[0] = uint32(sig) # si_signo + si32[1] = uint32(0) # si_errno + si32[2] = uint32(0) # si_code + # handler(sig, &siginfo, &ucontext) + f[8] = uint64(sig) + f[9] = si + f[10] = sf + uint64(8) + f[13] = h + # Enter the handler with [sf] = restorer (normal call-frame shape). + f[15] = sf + if (p.sig_actflags[sig] & uint64(SA_NODEFER)) == 0: + p.sig_mask = p.sig_mask | bit + return 1 + +# ---- iovec ---- +define IOV_MAX = 1024 + +def READV(fd: int, iov_ptr: rawptr, iovcnt: int) -> int: + if iov_ptr is None or iovcnt < 0 or iovcnt > IOV_MAX: return _err(abi.EINVAL) + iov: abi.iovec* = iov_ptr + total: int = 0 + i: int = 0 + while i < iovcnt: + want: uint32 = 0xFFFFFFFF + if iov[i].len < uint64(0xFFFFFFFF): want = uint32(iov[i].len) + n: int = READ(fd, iov[i].base, want) + if n < 0: + if total > 0: return total + return n + total = total + n + if uint64(n) < iov[i].len: break + i += 1 + return total + +def WRITEV(fd: int, iov_ptr: rawptr, iovcnt: int) -> int: + if iov_ptr is None or iovcnt < 0 or iovcnt > IOV_MAX: return _err(abi.EINVAL) + iov: abi.iovec* = iov_ptr + total: int = 0 + i: int = 0 + while i < iovcnt: + want: uint32 = 0xFFFFFFFF + if iov[i].len < uint64(0xFFFFFFFF): want = uint32(iov[i].len) + n: int = WRITE(fd, iov[i].base, want) + if n < 0: + if total > 0: return total + return n + total = total + n + if uint64(n) < iov[i].len: break + i += 1 + return total + +def POLL(fds: rawptr, nfds: uint64, timeout: int) -> int: + return 0 + +# ---- pipes ---- +define MAX_PIPES = 8 +define PIPE_CAP = 4096 + +_pipe_buf: [MAX_PIPES]rawptr +_pipe_head: [MAX_PIPES]uint32 +_pipe_tail: [MAX_PIPES]uint32 +_pipe_cnt: [MAX_PIPES]uint32 +_pipe_rd: [MAX_PIPES]int +_pipe_wr: [MAX_PIPES]int +_pipe_used: [MAX_PIPES]uint8 + +def _pipe_alloc() -> int: + for i in range(MAX_PIPES): + if _pipe_used[i] == 0: + b: rawptr = mm.malloc(uint64(PIPE_CAP)) + if b is None: return -1 + _pipe_buf[i] = b + _pipe_head[i] = 0 + _pipe_tail[i] = 0 + _pipe_cnt[i] = 0 + _pipe_rd[i] = 0 + _pipe_wr[i] = 0 + _pipe_used[i] = 1 + return i + return -1 + +def PIPE2(fds_ptr: rawptr, flags: uint32) -> int: + if fds_ptr is None: return _err(abi.EFAULT) + p: process.Process* = _current_proc() + id: int = _pipe_alloc() + if id < 0: return _err(abi.EMFILE) + rfd: int = _fd_alloc(p) + if rfd < 0: + mm.free(_pipe_buf[id]) + _pipe_used[id] = 0 + return _err(abi.EMFILE) + wfd: int = _fd_alloc(p) + if wfd < 0: + _fd_free(p, rfd) + mm.free(_pipe_buf[id]) + _pipe_used[id] = 0 + return _err(abi.EMFILE) + p.fd_table[rfd] = rawptr(uint64(id * 2 + 1)) + p.fd_kind[rfd] = process.FD_KIND_PIPE + p.fd_flags[rfd] = flags + p.fd_table[wfd] = rawptr(uint64(id * 2 + 2)) + p.fd_kind[wfd] = process.FD_KIND_PIPE + p.fd_flags[wfd] = flags + _pipe_rd[id] = 1 + _pipe_wr[id] = 1 + fds: int* = int*(fds_ptr) + fds[0] = rfd + fds[1] = wfd + return 0 + +def PIPE(fds_ptr: rawptr) -> int: + return PIPE2(fds_ptr, uint32(0)) + +def _pipe_read(fp: rawptr, buf_ptr: rawptr, count: uint32) -> int: + id: int = (int(uint64(fp)) - 1) / 2 + if id < 0 or id >= MAX_PIPES or _pipe_used[id] == 0: return _err(abi.EBADF) + while _pipe_cnt[id] == 0: + if _pipe_wr[id] == 0: return 0 + sched.Scheduler.yield_now() + n: uint32 = count + if _pipe_cnt[id] < n: n = _pipe_cnt[id] + dst: uint8* = uint8*(buf_ptr) + i: uint32 = 0 + while i < n: + src: uint8* = uint8*(uint64(_pipe_buf[id]) + uint64(_pipe_head[id])) + dst[i] = src[0] + _pipe_head[id] = (_pipe_head[id] + uint32(1)) % uint32(PIPE_CAP) + i += 1 + _pipe_cnt[id] = _pipe_cnt[id] - n + return int(n) + +def _pipe_write(fp: rawptr, buf_ptr: rawptr, count: uint32) -> int: + id: int = (int(uint64(fp)) - 1) / 2 + if id < 0 or id >= MAX_PIPES or _pipe_used[id] == 0: return _err(abi.EBADF) + if _pipe_rd[id] == 0: return _err(abi.EPIPE) + while _pipe_cnt[id] >= uint32(PIPE_CAP): + if _pipe_rd[id] == 0: return _err(abi.EPIPE) + sched.Scheduler.yield_now() + n: uint32 = count + room: uint32 = uint32(PIPE_CAP) - _pipe_cnt[id] + if room < n: n = room + src: uint8* = uint8*(buf_ptr) + i: uint32 = 0 + while i < n: + dst: uint8* = uint8*(uint64(_pipe_buf[id]) + uint64(_pipe_tail[id])) + dst[0] = src[i] + _pipe_tail[id] = (_pipe_tail[id] + uint32(1)) % uint32(PIPE_CAP) + i += 1 + _pipe_cnt[id] = _pipe_cnt[id] + n + return int(n) + +# Count one more reference to a pipe end (dup/dup2/fork inherit). +def _pipe_add_ref(fp: rawptr): + v: int = int(uint64(fp)) - 1 + id: int = v / 2 + end: int = v % 2 + if id < 0 or id >= MAX_PIPES: return + if end == 0: _pipe_rd[id] = _pipe_rd[id] + 1 + else: _pipe_wr[id] = _pipe_wr[id] + 1 + +def _pipe_close(fp: rawptr) -> int: + v: int = int(uint64(fp)) - 1 + id: int = v / 2 + end: int = v % 2 + if id < 0 or id >= MAX_PIPES: return _err(abi.EBADF) + if end == 0: + if _pipe_rd[id] > 0: _pipe_rd[id] = _pipe_rd[id] - 1 + else: + if _pipe_wr[id] > 0: _pipe_wr[id] = _pipe_wr[id] - 1 + if _pipe_rd[id] == 0 and _pipe_wr[id] == 0: + if _pipe_buf[id] is not None: mm.free(_pipe_buf[id]) + _pipe_buf[id] = None + _pipe_used[id] = 0 + return 0 + +# ---- Linux reboot(169) ---- +define LINUX_REBOOT_MAGIC1 = 0xFEE1DEAD +define LINUX_REBOOT_MAGIC2 = 672274793 +define LINUX_REBOOT_CMD_RESTART = 0x01234567 +define LINUX_REBOOT_CMD_POWER_OFF = 0x4321FEDC + +def REBOOT_LINUX(magic1: uint32, magic2: uint32, cmd: uint32, arg: rawptr) -> int: + if magic1 != uint32(LINUX_REBOOT_MAGIC1) or magic2 != uint32(LINUX_REBOOT_MAGIC2): + return _err(abi.EINVAL) + if cmd == uint32(LINUX_REBOOT_CMD_RESTART): + power_reboot() + return 0 + if cmd == uint32(LINUX_REBOOT_CMD_POWER_OFF): + power_shutdown() + return 0 + return _err(abi.EINVAL) + +# ============================================================================ +# Linux clone()/fork(): resume the child at the caller's post-syscall RIP +# ============================================================================ + +define CLONE_VM = 0x00000100 +define CLONE_FS = 0x00000200 +define CLONE_FILES = 0x00000400 +define CLONE_SIGHAND = 0x00000800 +define CLONE_THREAD = 0x00010000 +define CLONE_SETTLS = 0x00080000 +define CLONE_PARENT_SETTID = 0x00100000 +define CLONE_CHILD_CLEARTID = 0x00200000 +define CLONE_CHILD_SETTID = 0x01000000 + +class ForkCtx: + pid: int + rip: uint64 + rsp: uint64 + flags: uint64 + fs: uint64 + kern_stack: uint64 + regs: [12]uint64 + +def _clone_context(flags: uint64, stack: uint64, tls: uint64) -> ForkCtx*: + fp: uint64 = sched.get_syscall_frame() + if fp == 0: return None + f: uint64* = uint64*(fp) + ctx: ForkCtx* = ForkCtx*(mm.malloc(uint64(ForkCtx.__sizeof__()))) + if ctx is None: return None + string.memset(rawptr(ctx), 0, uint64(ForkCtx.__sizeof__())) + ctx.rip = f[13] + ctx.flags = f[12] + ctx.rsp = f[15] + if stack != 0: ctx.rsp = stack + ctx.fs = sched.get_fs_base() + if (flags & uint64(CLONE_SETTLS)) != 0 and tls != 0: + ctx.fs = tls + i: int = 0 + while i < 12: + ctx.regs[i] = f[i] + i += 1 + return ctx + +def _fork_child_entry(arg: rawptr) -> int: + ctx: ForkCtx* = arg + asm.cli() + proc.switch_pid(ctx.pid) + ks: uint64 = ctx.kern_stack + gdt.set_tss_rsp0(ks) + cpu.set_kernel_rsp0(ks) + proc.set_resume(ctx.rip, ctx.rsp, ctx.flags, uint64(0), &ctx.regs[0]) + mm.free(rawptr(ctx)) + proc.drop_to_user_mode_ctx(ks) + return 0 + +def _clone_thread(flags: uint64, stack: uint64, ptid: uint64, ctid: uint64, tls: uint64) -> int: + cur: process.Process* = _current_proc() + ctx: ForkCtx* = _clone_context(flags, stack, tls) + if ctx is None: return _err(abi.EAGAIN) + ctx.pid = cur.pid + th: sched.Thread* = sched.Scheduler.create_thread(&_fork_child_entry, rawptr(ctx), cur.pid) + if th is None: + mm.free(rawptr(ctx)) + return _err(abi.EAGAIN) + th.kern_stack = uint64(th.stack) + th.stack_size + ctx.kern_stack = th.kern_stack + if cur.addThread(th.tid) != 0: + th.state = sched.THREAD_DONE + mm.free(rawptr(ctx)) + return _err(abi.EAGAIN) + sched.set_thread_fs_base(th.tid, ctx.fs) + if (flags & uint64(CLONE_PARENT_SETTID)) != 0 and ptid != 0: + pt: int* = int*(ptid) + pt[0] = th.tid + if (flags & uint64(CLONE_CHILD_SETTID)) != 0 and ctid != 0: + ct: int* = int*(ctid) + ct[0] = th.tid + if (flags & uint64(CLONE_CHILD_CLEARTID)) != 0 and ctid != 0: + sched.set_clear_child_tid(th.tid, ctid) + return th.tid + +def _alloc_page4k() -> rawptr: + return mm.alloc_page() + +# Fork/exec inherit the fd table as shared references: neither side may release +# an object the other still uses. Non-pipe objects have no cross-process +# refcount yet, so mark BOTH sides shared (they are leaked on close instead of +# being freed under a live user). Pipes do have counts and stay reclaimable. +def _copy_fds(dst: process.Process*, src: process.Process*): + i: int = 0 + while i < process.PROC_MAX_FDS: + if src.fd_used[i]: + dst.fd_used[i] = 1 + dst.fd_table[i] = src.fd_table[i] + dst.fd_kind[i] = src.fd_kind[i] + dst.fd_flags[i] = src.fd_flags[i] + if src.fd_kind[i] == process.FD_KIND_PIPE: + dst.fd_shared[i] = 0 + _pipe_add_ref(src.fd_table[i]) + else: + dst.fd_shared[i] = 1 + src.fd_shared[i] = 1 + i += 1 + +# Linux execve (placed after the pipe/fork helpers so it can inherit fds). +def EXECVE(path_ptr: str, argv: rawptr, envp: rawptr) -> int: + cur: process.Process* = _current_proc() + old_parent: int = cur.parent_pid + path: const str = path_ptr + pid: int = elf.spawn_elf(path, argv, envp) + if pid < 0: + return _err(abi.ENOENT) + np: process.Process* = process.ProcessManager.get_process(pid) + if np is not None: + np.parent_pid = old_parent + string.strcpy(&np.cwd[0], &cur.cwd[0]) + np.sig_mask = cur.sig_mask + i: int = 0 + while i < 65: + np.sig_handler[i] = 0 + np.sig_actflags[i] = 0 + np.sig_restorer[i] = 0 + i += 1 + np.sig_pending = 0 + _copy_fds(np, cur) + cur.exit_code = 0 + sched.Scheduler.exit_process(cur.pid) + return 0 + +def _fork_process(stack: uint64) -> int: + cur: process.Process* = _current_proc() + ctx: ForkCtx* = _clone_context(uint64(0), stack, uint64(0)) + if ctx is None: return _err(abi.EAGAIN) + np: process.Process* = process.ProcessManager.create_process(&cur.name[0], None) + if np is None: + mm.free(rawptr(ctx)) + return _err(abi.EAGAIN) + new_pml4: uint64 = paging.create_process_page_table() + if new_pml4 == 0: + mm.free(rawptr(ctx)) + np.exit() + return _err(abi.ENOMEM) + if paging.CopyUserSpace(cur.pml4_root, new_pml4, rawptr(uint64(&_alloc_page4k))) != 0: + mm.free(rawptr(ctx)) + np.exit() + return _err(abi.ENOMEM) + np.pml4_root = new_pml4 + np.elf_base = cur.elf_base + np.heap_start = cur.heap_start + np.heap_size = cur.heap_size + np.brk_start = cur.brk_start + np.brk_end = cur.brk_end + np.entry = cur.entry + np.parent_pid = cur.pid + string.strcpy(&np.cwd[0], &cur.cwd[0]) + i: int = 0 + while i < process.MAX_VMAS: + np.vma_base[i] = cur.vma_base[i] + np.vma_size[i] = cur.vma_size[i] + np.vma_prot[i] = cur.vma_prot[i] + np.vma_kind[i] = cur.vma_kind[i] + np.vma_flags[i] = cur.vma_flags[i] + i += 1 + i = 0 + while i < 65: + np.sig_handler[i] = cur.sig_handler[i] + np.sig_actflags[i] = cur.sig_actflags[i] + np.sig_restorer[i] = cur.sig_restorer[i] + i += 1 + np.sig_mask = cur.sig_mask + np.sig_pending = 0 + # shared fd table (the child must not close the parent's objects) + _copy_fds(np, cur) + # create_process allocated a private 64KB stack; the child uses the copy. + if np.user_stack != 0: + mm.free(rawptr(np.user_stack - uint64(process.USER_STACK_SIZE))) + np.user_stack = 0 + ctx.pid = np.pid + th: sched.Thread* = sched.Scheduler.create_thread(&_fork_child_entry, rawptr(ctx), np.pid) + if th is None: + mm.free(rawptr(ctx)) + np.exit() + return _err(abi.EAGAIN) + th.kern_stack = np.kern_stack + ctx.kern_stack = np.kern_stack + np.addThread(th.tid) + sched.set_thread_fs_base(th.tid, ctx.fs) + return np.pid + +def CLONE(flags: uint64, stack: uint64, ptid: uint64, ctid: uint64, tls: uint64) -> int: + if (flags & uint64(CLONE_VM)) != 0: + return _clone_thread(flags, stack, ptid, ctid, tls) + return _fork_process(stack) + +def FORK() -> int: + return _fork_process(uint64(0)) + +# ============================================================================ +# P0: identity / environment / random +# ============================================================================ + +def GETUID() -> uint64: + return uint64(0) + +def GETEUID() -> uint64: + return uint64(0) + +def GETGID() -> uint64: + return uint64(0) + +def GETEGID() -> uint64: + return uint64(0) + +def GETPPID() -> uint64: + return uint64(_current_proc().parent_pid) + +def GETTID() -> uint64: + return uint64(sched.thread_current()) + +def SET_TID_ADDRESS(tid_ptr: rawptr) -> uint64: + # Single-address-space threads: the scheduler tid is the Linux tid. + tid: int = sched.thread_current() + if tid_ptr is not None: + sched.set_clear_child_tid(tid, uint64(tid_ptr)) + return uint64(tid) + +def SET_ROBUST_LIST(head: rawptr, len: uint64) -> int: + # Recorded nowhere yet; futex-based thread exit lands with P4. + return 0 + +def RSEQ(rseq_ptr: rawptr, len: uint32, flags: uint32) -> int: + # glibc disables rseq when this returns -ENOSYS. + return _err(abi.ENOSYS) + +def UNAME(buf_ptr: rawptr) -> int: + if buf_ptr is None: return _err(abi.EFAULT) + u: abi.linux_utsname* = buf_ptr + string.memset(rawptr(u), 0, uint64(abi.SIZEOF_UTSNAME)) + # Report "Linux" so scripts and build systems do not special-case us; + # the ViperOS identity stays in the version string. + string.strcpy(&u.sysname[0], "Linux") + string.strcpy(&u.nodename[0], "viperos") + string.strcpy(&u.release[0], "6.1.0-viperos") + string.strcpy(&u.version[0], "#1 SMP ViperOS") + string.strcpy(&u.machine[0], "x86_64") + string.strcpy(&u.domainname[0], "(none)") + return 0 + +def GETCWD(buf_ptr: rawptr, size: uint64) -> int: + if buf_ptr is None: return _err(abi.EFAULT) + p: process.Process* = _current_proc() + n: int = int(string.strlen(&p.cwd[0])) + if size < uint64(n + 1): return _err(abi.ERANGE) + dst: char* = char*(buf_ptr) + string.strcpy(dst, &p.cwd[0]) + return n + 1 + +def CHDIR(path_ptr: str) -> int: + path: const str = path_ptr + buf: [256]char + r: int = _resolve_path(abi.AT_FDCWD, path, &buf[0]) + if r != 0: return r + p: process.Process* = _current_proc() + if buf[0] == 47 and buf[1] == 0: + string.strcpy(&p.cwd[0], "/") + return 0 + info: fat32_types.fileinfo + fat32.lock() + res: int = int(fat32.stat(&buf[0], &info)) + fat32.unlock() + if res != 0: return _fr_errno(res) + if (int(info.attr) & 0x10) == 0: + return _err(abi.ENOTDIR) + string.strcpy(&p.cwd[0], &buf[0]) + return 0 + +def FACCESSAT(dirfd: int, path_ptr: str, mode: uint32, flags: uint32) -> int: + path: const str = path_ptr + buf: [256]char + r: int = _resolve_path(dirfd, path, &buf[0]) + if r != 0: return r + if buf[0] == 47 and buf[1] == 0: return 0 + info: fat32_types.fileinfo + fat32.lock() + res: int = int(fat32.stat(&buf[0], &info)) + fat32.unlock() + if res != 0: return _fr_errno(res) + return 0 + +def ACCESS(path_ptr: str, mode: uint32) -> int: + return FACCESSAT(abi.AT_FDCWD, path_ptr, mode, uint32(0)) + +def READLINKAT(dirfd: int, path_ptr: str, buf_ptr: rawptr, bufsiz: uint64) -> int: + path: const str = path_ptr + tmp: [256]char + r: int = _resolve_path(dirfd, path, &tmp[0]) + if r != 0: return r + info: fat32_types.fileinfo + fat32.lock() + res: int = int(fat32.stat(&tmp[0], &info)) + fat32.unlock() + if res != 0: return _fr_errno(res) + # FAT32 has no symbolic links; an existing name is simply not a link. + return _err(abi.EINVAL) + +def READLINK(path_ptr: str, buf_ptr: rawptr, bufsiz: uint64) -> int: + return READLINKAT(abi.AT_FDCWD, path_ptr, buf_ptr, bufsiz) + +def PRLIMIT64(pid: int, resource: uint32, new_ptr: rawptr, old_ptr: rawptr) -> int: + if pid != 0 and pid != process.current(): + return _err(abi.ESRCH) + # Setting limits is accepted but not enforced yet. + if old_ptr is not None: + rl: abi.linux_rlimit* = old_ptr + cur: uint64 = abi.RLIM_INFINITY + mx: uint64 = abi.RLIM_INFINITY + if resource == uint32(abi.RLIMIT_STACK): + cur = uint64(process.USER_STACK_SIZE) + mx = uint64(process.USER_STACK_SIZE) + elif resource == uint32(abi.RLIMIT_NOFILE): + cur = uint64(process.PROC_MAX_FDS) + mx = uint64(process.PROC_MAX_FDS) + rl.cur = cur + rl.max = mx + return 0 + +def GETRANDOM(buf_ptr: rawptr, buflen: uint64, flags: uint32) -> int: + if buf_ptr is None: return _err(abi.EFAULT) + if buflen > uint64(0x7FFFFFFF): return _err(abi.EINVAL) + p: uint8* = uint8*(buf_ptr) + i: uint64 = 0 + while i < buflen: + v: uint64 = _rng_next() + j: int = 0 + while j < 8 and i < buflen: + p[i] = uint8(v & uint64(0xFF)) + v = v >> 8 + i += 1 + j += 1 + return int(buflen) + extern def syscall_entry() -export def syscall_handler(num: uint64, arg1: uint64, arg2: uint64, arg3: uint64, arg4: uint64, arg5: uint64, arg6: uint64) -> uint64: +def syscall_dispatch(num: uint64, arg1: uint64, arg2: uint64, arg3: uint64, arg4: uint64, arg5: uint64, arg6: uint64) -> uint64: n: int = int(num) if n == int(Syscall.EXIT): return uint64(EXIT(int(arg1))) @@ -462,21 +1977,13 @@ export def syscall_handler(num: uint64, arg1: uint64, arg2: uint64, arg3: uint64 elif n == int(Syscall.IOCTL): return uint64(IOCTL(int(arg1), uint32(arg2), rawptr(arg3))) elif n == int(Syscall.SEEK): - return uint64(SEEK(int(arg1), uint32(arg2), uint32(arg3))) + return uint64(SEEK(int(arg1), arg2, uint32(arg3))) elif n == int(Syscall.MKDIR): path_ptr: str = str(arg1) return uint64(MKDIR(path_ptr)) elif n == int(Syscall.REMOVE): path_ptr: str = str(arg1) return uint64(REMOVE(path_ptr)) - elif n == int(Syscall.OPENDIR): - path_ptr: str = str(arg1) - return uint64(OPENdir(path_ptr)) - elif n == int(Syscall.READDIR): - info_ptr: rawptr = rawptr(arg2) - return uint64(READdir(int(arg1), info_ptr)) - elif n == int(Syscall.CLOSEDIR): - return uint64(CLOSEdir(int(arg1))) elif n == int(Syscall.STAT): path_ptr: str = str(arg1) info_ptr: rawptr = rawptr(arg2) @@ -487,34 +1994,50 @@ export def syscall_handler(num: uint64, arg1: uint64, arg2: uint64, arg3: uint64 elif n == int(Syscall.SHUTDOWN): power_shutdown() return uint64(0) - elif n == int(Syscall.LOAD_SO): - path_ptr_so: rawptr = rawptr(arg1) - return LOAD_SO(path_ptr_so) - elif n == int(Syscall.SO_CALL1): - sym_ptr: rawptr = rawptr(arg1) - return uint64(SO_CALL1(sym_ptr, rawptr(arg2))) - elif n == int(Syscall.GETPID): - return GETPID() - elif n == int(Syscall.YIELD): - # sched_yield 是"主动放弃剩余时间片",必须无条件切换;try_reschedule() 只在 - # 时间片到期时才切,作为 syscall 会让等待外设的进程白等(见 Scheduler.yield_now 注释)。 - sched.Scheduler.yield_now() - return 0 + elif n == int(Syscall.LOAD_SO): + path_ptr_so: rawptr = rawptr(arg1) + return LOAD_SO(path_ptr_so) + elif n == int(Syscall.SO_CALL1): + sym_ptr: rawptr = rawptr(arg1) + return uint64(SO_CALL1(sym_ptr, rawptr(arg2))) + elif n == int(Syscall.GETPID): + return GETPID() + elif n == int(Syscall.YIELD): + # sched_yield 是"主动放弃剩余时间片",必须无条件切换;try_reschedule() 只在 + # 时间片到期时才切,作为 syscall 会让等待外设的进程白等(见 Scheduler.yield_now 注释)。 + sched.Scheduler.yield_now() + return 0 elif n == int(Syscall.SPAWN): - path_ptr_sp: rawptr = rawptr(arg1) - return uint64(SPAWN(path_ptr_sp, int(arg2))) + return uint64(EXECVE(str(arg1), rawptr(arg2), rawptr(arg3))) + elif n == int(Syscall.SPAWN_PROC): + path_ptr_sp2: rawptr = rawptr(arg1) + return uint64(SPAWN_PROC(path_ptr_sp2, int(arg2))) + elif n == int(Syscall.WAIT4): + return uint64(WAIT4(int(arg1), rawptr(arg2), uint32(arg3), rawptr(arg4))) elif n == int(Syscall.MMAP): return MMAP(arg1, arg2, arg3, arg4, arg5, arg6) elif n == int(Syscall.MUNMAP): return uint64(MUNMAP(arg1, arg2)) - elif n == int(Syscall.THREAD_CREATE): + elif n == int(Syscall.MPROTECT): + return uint64(MPROTECT(arg1, arg2, arg3)) + elif n == int(Syscall.BRK): + return BRK(arg1) + elif n == int(Syscall.MREMAP): + return MREMAP(arg1, arg2, arg3, arg4, arg5) + elif n == int(Syscall.MADVISE): + return uint64(MADVISE(arg1, arg2, uint32(arg3))) + elif n == int(Syscall.CLONE): + return uint64(CLONE(arg1, arg2, arg3, arg4, arg5)) + elif n == int(Syscall.FORK): + return uint64(FORK()) + elif n == int(Syscall.THREAD_CREATE_OLD): func_ptr: rawptr = rawptr(arg1) arg_ptr: rawptr = rawptr(arg2) cur_pid: int = process.current() # app 传进来的是**用户地址**:必须用 create_user_thread(独立用户栈 + 独立内核栈、 # 走 drop_to_user_mode 进 Ring 3),不能用 create_thread 在 Ring 0 直接 call。 th: sched.Thread* = sched.Scheduler.create_user_thread(cur_pid, func_ptr, arg_ptr) - if th is None: return uint64(0xFFFFFFFFFFFFFFFF) + if th is None: return uint64(_err(abi.EAGAIN)) p: process.Process* = process.ProcessManager.get_process(cur_pid) if p: p.addThread(th.tid) @@ -523,9 +2046,124 @@ export def syscall_handler(num: uint64, arg1: uint64, arg2: uint64, arg3: uint64 # 用户线程显式退出(回收用户栈等资源后让出,永不返回)。 sched.Scheduler.exit_current_thread() return 0 + elif n == int(Syscall.UNAME): + return uint64(UNAME(rawptr(arg1))) + elif n == int(Syscall.GETUID): + return GETUID() + elif n == int(Syscall.GETEUID): + return GETEUID() + elif n == int(Syscall.GETGID): + return GETGID() + elif n == int(Syscall.GETEGID): + return GETEGID() + elif n == int(Syscall.GETPPID): + return GETPPID() + elif n == int(Syscall.GETTID): + return GETTID() + elif n == int(Syscall.GETCWD): + return uint64(GETCWD(rawptr(arg1), arg2)) + elif n == int(Syscall.CHDIR): + return uint64(CHDIR(str(arg1))) + elif n == int(Syscall.ACCESS): + return uint64(ACCESS(str(arg1), uint32(arg2))) + elif n == int(Syscall.FACCESSAT): + return uint64(FACCESSAT(int(arg1), str(arg2), uint32(arg3), uint32(arg4))) + elif n == int(Syscall.READLINK): + return uint64(READLINK(str(arg1), rawptr(arg2), arg3)) + elif n == int(Syscall.READLINKAT): + return uint64(READLINKAT(int(arg1), str(arg2), rawptr(arg3), arg4)) + elif n == int(Syscall.SET_TID_ADDRESS): + return SET_TID_ADDRESS(rawptr(arg1)) + elif n == int(Syscall.SET_ROBUST_LIST): + return uint64(SET_ROBUST_LIST(rawptr(arg1), arg2)) + elif n == int(Syscall.RSEQ): + return uint64(RSEQ(rawptr(arg1), uint32(arg2), uint32(arg3))) + elif n == int(Syscall.PRLIMIT64): + return uint64(PRLIMIT64(int(arg1), uint32(arg2), rawptr(arg3), rawptr(arg4))) + elif n == int(Syscall.GETRANDOM): + return uint64(GETRANDOM(rawptr(arg1), arg2, uint32(arg3))) + elif n == int(Syscall.FSTAT): + return uint64(FSTAT(int(arg1), rawptr(arg2))) + elif n == int(Syscall.NEWFSTATAT): + return uint64(NEWFSTATAT(int(arg1), str(arg2), rawptr(arg3), uint32(arg4))) + elif n == int(Syscall.OPENAT): + return uint64(OPENAT(int(arg1), str(arg2), uint32(arg3), uint32(arg4))) + elif n == int(Syscall.GETDENTS64): + return uint64(GETDENTS64(int(arg1), rawptr(arg2), arg3)) + elif n == int(Syscall.DUP): + return uint64(DUP(int(arg1))) + elif n == int(Syscall.DUP2): + return uint64(DUP2(int(arg1), int(arg2))) + elif n == int(Syscall.DUP3): + return uint64(DUP3(int(arg1), int(arg2), uint32(arg3))) + elif n == int(Syscall.FCNTL): + return uint64(FCNTL(int(arg1), uint32(arg2), arg3)) + elif n == int(Syscall.PREAD64): + return uint64(PREAD64(int(arg1), rawptr(arg2), uint32(arg3), arg4)) + elif n == int(Syscall.PWRITE64): + return uint64(PWRITE64(int(arg1), rawptr(arg2), uint32(arg3), arg4)) + elif n == int(Syscall.RENAME): + return uint64(RENAME(str(arg1), str(arg2))) + elif n == int(Syscall.RMDIR): + return uint64(RMDIR(str(arg1))) + elif n == int(Syscall.UNLINKAT): + return uint64(UNLINKAT(int(arg1), str(arg2), uint32(arg3))) + elif n == int(Syscall.ARCH_PRCTL): + return uint64(ARCH_PRCTL(int(arg1), arg2)) + elif n == int(Syscall.FUTEX): + return uint64(FUTEX(arg1, uint32(arg2), uint32(arg3), rawptr(arg4))) + elif n == int(Syscall.CLOCK_GETTIME): + return uint64(CLOCK_GETTIME(int(arg1), rawptr(arg2))) + elif n == int(Syscall.CLOCK_GETRES): + return uint64(CLOCK_GETRES(int(arg1), rawptr(arg2))) + elif n == int(Syscall.GETTIMEOFDAY): + return uint64(GETTIMEOFDAY(rawptr(arg1), rawptr(arg2))) + elif n == int(Syscall.NANOSLEEP): + return uint64(NANOSLEEP(rawptr(arg1), rawptr(arg2))) + elif n == int(Syscall.RT_SIGACTION): + return uint64(RT_SIGACTION(int(arg1), rawptr(arg2), rawptr(arg3), uint32(arg4))) + elif n == int(Syscall.RT_SIGPROCMASK): + return uint64(RT_SIGPROCMASK(int(arg1), rawptr(arg2), rawptr(arg3), uint32(arg4))) + elif n == int(Syscall.RT_SIGRETURN): + return uint64(RT_SIGRETURN()) + elif n == int(Syscall.KILL): + return uint64(KILL(int(arg1), int(arg2))) + elif n == int(Syscall.READV): + return uint64(READV(int(arg1), rawptr(arg2), int(arg3))) + elif n == int(Syscall.WRITEV): + return uint64(WRITEV(int(arg1), rawptr(arg2), int(arg3))) + elif n == int(Syscall.POLL): + return uint64(POLL(rawptr(arg1), arg2, int(arg3))) + elif n == int(Syscall.PIPE): + return uint64(PIPE(rawptr(arg1))) + elif n == int(Syscall.PIPE2): + return uint64(PIPE2(rawptr(arg1), uint32(arg2))) + elif n == int(Syscall.REBOOT_LINUX): + return uint64(REBOOT_LINUX(uint32(arg1), uint32(arg2), uint32(arg3), rawptr(arg4))) + # Unknown/unimplemented Linux numbers return -ENOSYS (never success). + return uint64(_err(abi.ENOSYS)) + +# Normal C entry: dispatch, then deliver at most one pending signal by +# rewriting the saved user context. Returning 0 makes the signal handler's +# entry RAX = 0; the original syscall result is saved in the frame. +def _syscall_invoke(num: uint64, arg1: uint64, arg2: uint64, arg3: uint64, arg4: uint64, arg5: uint64, arg6: uint64) -> uint64: + sched.set_syscall_frame(_entry_frame) + r: uint64 = syscall_dispatch(num, arg1, arg2, arg3, arg4, arg5, arg6) + if _deliver_signals(r) != 0: + return uint64(0) + return r + +# The compiler-provided syscall_entry calls this symbol. Naked: save the +# entry frame pointer and tail-jump into _syscall_invoke (its ret returns to +# the entry's call site). +@__attribute__(naked) +export def syscall_handler(num: uint64, arg1: uint64, arg2: uint64, arg3: uint64, arg4: uint64, arg5: uint64, arg6: uint64) -> uint64: + __asm__("mov qword ptr [rip + _entry_frame], rbp\n jmp qword ptr [rip + _handler_ptr]") return 0 def init(): + global _handler_ptr + _handler_ptr = rawptr(uint64(&_syscall_invoke)) efer: uint64 = rdmsr(IA32_EFER) efer = efer | 1 wrmsr(IA32_EFER, efer) diff --git a/VKernel/Kernel/main.vp b/VKernel/Kernel/main.vp index f04f127..73102a7 100644 --- a/VKernel/Kernel/main.vp +++ b/VKernel/Kernel/main.vp @@ -111,13 +111,25 @@ class ViperKernel: op=[ASM_DESCR.CLOBBER_MEMORY]) if BootInfo is not None: paging.init(BootInfo.MemmapAddr, BootInfo.MemmapSize) + # PAT: entry 4 = write-combining. The framebuffer is mapped with + # PTE_PAT so large compositor blits stay fast on VMware/real HW + # (WB is fast but can be incoherent for scanout; UC is slow). + feats: uint32 = cpu.detect_features_static() + if (feats & uint32(cpu.CPU_FEATURE_PAT)) != 0: + pat_v: uint64 = cpu.msr_read(uint32(0x277)) + pat_v = (pat_v & ~(uint64(7) << uint64(32))) | (uint64(1) << uint64(32)) + cpu.msr_write(uint32(0x277), pat_v) + paging.set_pat_wc() + serial.puts("[main] PAT write-combining enabled\n") + fb_flags: uint64 = paging.PTE_WRITABLE + if paging.has_pat_wc() != 0: + fb_flags = fb_flags | paging.PTE_PAT fb_pages: uint64 = (fb_size + uint64(0xFFF)) >> uint64(12) for fpi in range(fb_pages): fva: uint64 = uint64(fb) + fpi * uint64(0x1000) - # 帧缓冲改用回写(去掉 PTE_PCD):此前按非缓存映射,每次写屏都直通设备, - # 全屏/大块刷新极慢。回写在 QEMU 下显示正确(TCG 不模拟缓存不一致); - # 真机若要严格正确应改为 write-combining(走 PAT)。 - paging.MapPage(fva, fva, paging.PTE_WRITABLE) + # 帧缓冲是 MMIO:优先 PAT WC(快且对扫描输出可见);CPU 不支持 + # PAT 时保持回写,避免用 UC 把整帧提交拖慢。 + paging.MapPage(fva, fva, fb_flags) mm.init(BootInfo.MemmapAddr, BootInfo.MemmapSize, BootInfo.MemmapDescSize, BootInfo.framebuffer_addr, fb_size, BootInfo.kernel_PhysAddr, _kern_end) pt_pool_buf: rawptr = mm.malloc(uint64(513) * uint64(4096)) paging.init_pool(pt_pool_buf, uint64(513) * uint64(4096)) @@ -240,6 +252,12 @@ class ViperKernel: viperlib.snprintf(&font_log, 64, "[main] FontDemo spawned as pid=%d\n", font_pid) serial.puts(&font_log) + img_pid: int = elf.spawn_elf("/APPS/imagesee.elf") + if img_pid >= 0: + img_log: [64]char + viperlib.snprintf(&img_log, 64, "[main] ImageSee spawned as pid=%d\n", img_pid) + serial.puts(&img_log) + sched.Scheduler.enable() # 破坏性 FAT32 测试已删除(write/rename/delete/mkdir/rmdir 会永久改坏 disk.img)。 @@ -263,7 +281,12 @@ class ViperKernel: # /dev/desk:explorer 与各 app 的共享内存通道(S3)。分配失败就让登记跳过 —— # explorer 的 open 会明确失败并打日志,不做静默兜底。 desk_shm: rawptr = mm.malloc(uint64(devfs.DESK_SHM_SIZE)) + sl: [160]char + viperlib.snprintf(&sl, 160, "[main] desk shm=0x%lx size=0x%lx\n", + uint64(desk_shm), uint64(devfs.DESK_SHM_SIZE)) + serial.puts(&sl) devfs.devfs_register_desk(desk_shm, uint64(devfs.DESK_SHM_SIZE)) + mm.check_lists(1) serial.puts("[main] sched started\n") diff --git a/VKernel/Kernel/mm/mm.vp b/VKernel/Kernel/mm/mm.vp index 8398ca8..0f4c359 100644 --- a/VKernel/Kernel/mm/mm.vp +++ b/VKernel/Kernel/mm/mm.vp @@ -56,6 +56,58 @@ class memory_block: # 块头魔数("BLK!") define BLOCK_MAGIC = 0x424C4B21 +# [DIAG] 监视某个空闲块头是否被越界写清零;配合 _last_user 定位"谁写到了这里"。 +static _watch_addr: uint64 = 0 +static _last_user: uint64 = 0 +static _last_size: uint64 = 0 +static _last_kind: int = 0 + +def _watch_check(where: int): + global _watch_addr + if _watch_addr == 0: return + if uint64((memory_block*(_watch_addr)).magic) == uint64(BLOCK_MAGIC): return + _wl: [192]char + viperlib.snprintf(&_wl, 192, "[mm] WATCH0 at %d: watch=0x%lx magic=0x%lx last=0x%lx size=0x%lx kind=%d\n", + where, _watch_addr, uint64(uint32((memory_block*(_watch_addr)).magic)), + _last_user, _last_size, _last_kind) + serial.puts(&_wl) + _watch_addr = 0 + +# ---- 真正的 4KiB 匿名页分配器 ---- +# mmap 的匿名/文件页不再走 malloc(4096)(那会占 8KiB:4KiB 头页 + 4KiB 数据), +# 而是直接向 buddy 要 order-0 块,整页交给用户。代价是页首的 memory_block 头会 +# 被用户数据覆盖 ⇒ free 时必须按"裸页"重建头再 merge;同时用位图区分"裸页"和 +# 普通块,避免 merge 把已分配裸页的脏头误判成坏块。 +define MAX_RAW_PAGES = 262144 # 1GiB / 4KiB +define RAW_BITS_BYTES = 32768 # MAX_RAW_PAGES / 8 +static _raw_heap_start: uint64 = 0 +static _raw_page_count: uint64 = 0 +static _raw_bits: [RAW_BITS_BYTES]uint8 + +def _raw_index(addr: uint64) -> uint64: + if addr < _raw_heap_start: return uint64(0xFFFFFFFFFFFFFFFF) + off: uint64 = addr - _raw_heap_start + idx: uint64 = off >> 12 + if idx >= _raw_page_count: return uint64(0xFFFFFFFFFFFFFFFF) + return idx + +def _raw_is_used(addr: uint64) -> int: + idx: uint64 = _raw_index(addr) + if idx == uint64(0xFFFFFFFFFFFFFFFF): return 0 + return (int(_raw_bits[idx >> 3]) >> int(idx & 7)) & 1 + +def _raw_set(addr: uint64, used: int): + idx: uint64 = _raw_index(addr) + if idx == uint64(0xFFFFFFFFFFFFFFFF): return + byte_i: uint64 = idx >> 3 + bit_i: int = int(idx & 7) + b: int = int(_raw_bits[byte_i]) + if used != 0: + b = b | (1 << bit_i) + else: + b = b & (255 - (1 << bit_i)) + _raw_bits[byte_i] = uint8(b & 0xFF) + # 伙伴系统结构 class __buddy_system: free_lists: [MAX_ORDER + 1]memory_block* @@ -91,11 +143,17 @@ class slab_page: def align_4k(addr: uint64) -> uint64: return (addr + 0xFFF) & ~0xFFF -# 计算阶数 +# 计算阶数:必须**向上取整**。旧实现用逐个右移(floor): +# size=0x4498+4096=0x5498? 或 _bb 的 1141760+4096 → 实际拿到 1MB 块, +# 但用户区从 block+4KB 起,写满 1.09MB 就**越过块尾 92KB**,踩坏后继块头 / +# 别人的数据(现场:[mm] BAD blk blk=0x21780000 sz=0 magic=0、随内存大小 +# 出现的随机崩溃、以及 Terminal 的 Text 缓冲被踩后键盘无响应)。 +# 向上取整后 1.14MB 请求拿 2MB 块,32MB 共享区拿 64MB 块(多一页头)。 static def get_order(size: uint64) -> int: order: int = 0 - while size > MIN_BLOCK_SIZE: - size >>= 1 + block: uint64 = uint64(MIN_BLOCK_SIZE) + while block < size and order < MAX_ORDER: + block = block << 1 order += 1 return order @@ -214,6 +272,10 @@ class _BuddySystemObject: # 检查伙伴块是否存在且空闲 if buddy >= memory_block*(self.buddy.end_addr): break # 伙伴块超出内存范围 + # 裸页(mmap 匿名/文件页)的块头已被用户数据覆盖:位图标记为已分配, + # 绝不能读它的 state/magic(会把用户数据当块头,误报 BAD / 误合并)。 + if _raw_is_used(uint64(buddy)) != 0: + break if int(buddy.state) != BLOCK_FREE or int(buddy.order) != order: break # 伙伴块不空闲或阶数不匹配 # 加固:摘链前先校验伙伴块头(magic / size-order 一致 / prev-next 在堆内且对齐)。 @@ -260,8 +322,31 @@ class _BuddySystemObject: # 保持空闲链原样 —— 否则紧接着的 `block.next.prev = None` 就会踩坏值。 _arc: int = self._check_block(block, BLOCK_FREE) if _arc != 0: - self._report_bad_block(1, _arc, block, found_order) - return None + # 现场:1GiB 内存布局下 order-16 空闲块头被整块清零(sz/st/ord/magic/ + # nxt/prv 全 0),块体本身未分配。若整头全 0,可按链表阶数无损重建头, + # 否则一次坏块会让之后所有分配永久失败(用户态 malloc 返回 NULL → + # 写 0 触发 [0E])。重建只针对"全零头",其他损坏仍按原逻辑拒绝。 + if _arc == 1: + _zeroed: int = 0 + if block.size == 0 and block.state == 0 and block.order == 0 and uint64(block.next) == 0 and uint64(block.prev) == 0: + _zeroed = 1 + if _zeroed == 1: + block.size = get_block_size(found_order) + block.state = BLOCK_FREE + block.order = uint8(found_order) + block.magic = uint32(BLOCK_MAGIC) + block.next = None + block.prev = None + _rbl: [128]char + viperlib.snprintf(&_rbl, 128, "[mm] repaired zeroed free block 0x%lx order=%d size=0x%lx\n", + uint64(block), found_order, block.size) + serial.puts(&_rbl) + else: + self._report_bad_block(1, _arc, block, found_order) + return None + else: + self._report_bad_block(1, _arc, block, found_order) + return None # 从空闲链表中移除块 if block.next: block.next.prev = None self.buddy.free_lists[found_order] = block.next @@ -502,6 +587,8 @@ SlabAllocator: _SlabAllocator # 内存分配函数 export def malloc(size: uint64) -> rawptr: + global _last_user, _last_size, _last_kind + _watch_check(1) if size == 0: return None if size <= SLAB_THRESHOLD: sc: int = _slab_size_class(size) @@ -512,6 +599,9 @@ export def malloc(size: uint64) -> rawptr: _mb: [96]char viperlib.snprintf(&_mb, 96, "[mm] malloc(%lu) slab sc=%d -> NULL\n", size, sc) serial.puts(&_mb) + _last_user = uint64(result) + _last_size = size + _last_kind = 1 return result hdr: uint64 = memory_block.__sizeof__() if size >= 4096: @@ -522,6 +612,9 @@ export def malloc(size: uint64) -> rawptr: block: memory_block* = BuddySystemObject.allocate_block(total) if not block: return None user_ptr: uint64 = (uint64(block) + hdr + 4095) & ~uint64(4095) + _last_user = user_ptr + _last_size = size + _last_kind = 3 real_hdr: uint64 = user_ptr - 8 __asm__(f"""mov rax, {asminp(block, ASM_DESCR.REG_ANY)} mov [{asminp(rawptr(real_hdr), ASM_DESCR.REG_ANY)}], rax""", @@ -533,6 +626,9 @@ export def malloc(size: uint64) -> rawptr: if order2 > MAX_ORDER: return None block2: memory_block* = BuddySystemObject.allocate_block(total2) if not block2: return None + _last_user = uint64(block2) + hdr + _last_size = size + _last_kind = 2 return rawptr(uint64(block2) + hdr) @@ -549,6 +645,7 @@ export def malloc_direct(size: uint64) -> rawptr: export def free(ptr: rawptr): + _watch_check(2) if ptr is None: return sp: slab_page* = _slab_find_page(ptr) if sp: @@ -575,6 +672,67 @@ export def free(ptr: rawptr): block2: memory_block* = rawptr(uint64(ptr) - memory_block.__sizeof__()) BuddySystemObject.free_block(block2) +# 真正的 4KiB 页分配:整页归调用者,页首不再保留块头。 +# ---- 4KiB 页池:mmap/brk/fork 的页不再逐页进出 buddy ---- +# 旧方案 alloc_page() 直接向 buddy 要 order-0 块:用户写满整页会覆盖页首块头, +# 每次 free 都得重建头再 merge;一旦 free-list 有重复/陈旧表头,就会出现 +# "页已经分给字体缓冲,同时还挂在空闲链上" —— 字体数据直接把块头写成 ASCII +# (BAD blk ... magic=0x574d6f74)。这里改成独立页池: +# · 每 2MiB 向 buddy 申请一个 order-9 块(首 4KiB 留给块头,不发给用户) +# · 剩余 511 页串成页内 free-list,alloc/free 只改页首 8 字节 +# · 页永远不逐页归还 buddy,因此页数据永远不会碰到 buddy 块头/空闲链 +# 代价:每 2MiB 浪费 4KiB(0.2%),且不再把空闲页合并回 buddy。 +define PAGE_POOL_CHUNK_SIZE = 0x200000 +define PAGE_POOL_RESERVE = 0x1000 +define PAGE_POOL_MAX = 134217728 # 128MiB +static _page_free_head: rawptr = None + +def _pg_next(p: rawptr) -> rawptr: + return rawptr(*(uint64*(p))) + +def _pg_set_next(p: rawptr, n: rawptr): + *(uint64*(p)) = uint64(n) + +# 启动时一次性预留一大块给页池,之后再不向 buddy 要块。这样页池的块永远不会 +# 出现在 buddy 空闲链上,页数据也不可能踩 buddy 块头;页分配/释放只操作页内 +# free-list(页首 8 字节)。预留块首 4KiB 保留给 memory_block 头。 +def init_page_pool(): + global _page_free_head + if _page_free_head is not None: return + b: memory_block* = BuddySystemObject.allocate_block(uint64(PAGE_POOL_MAX)) + if not b: + b = BuddySystemObject.allocate_block(uint64(67108864)) # 64MiB + if not b: + b = BuddySystemObject.allocate_block(uint64(33554432)) # 32MiB + if not b: + b = BuddySystemObject.allocate_block(uint64(16777216)) # 16MiB + if not b: return + base: uint64 = uint64(b) + end: uint64 = base + b.size + first: uint64 = base + uint64(PAGE_POOL_RESERVE) + p: uint64 = first + while p < end: + nxt: uint64 = 0 + if p + uint64(4096) < end: nxt = p + uint64(4096) + _pg_set_next(rawptr(p), rawptr(nxt)) + p = p + uint64(4096) + _page_free_head = rawptr(first) + +export def alloc_page() -> rawptr: + global _page_free_head + if _page_free_head is None: return None + p: rawptr = _page_free_head + _page_free_head = _pg_next(p) + return p + +# 页池释放:只把页压回页内 free-list,不碰 buddy。 +export def free_page(ptr: rawptr): + global _page_free_head + if ptr is None: return + a: uint64 = uint64(ptr) & ~uint64(0xFFF) + _pg_set_next(rawptr(a), _page_free_head) + _page_free_head = rawptr(a) + export def calloc(nmemb: uint64, size: uint64) -> rawptr: total_size: uint64 = nmemb * size ptr: rawptr = malloc(total_size) @@ -620,7 +778,7 @@ def free_memory_regions(): # 初始化内存管理系统 def init(MemmapAddr: uint64, MemmapSize: uint64, MemmapDescSize: uint64, FbAddr: uint64 = 0, FbSize: uint64 = 0, KernelAddr: uint64 = 0, KernelEnd: uint64 = 0): - global BuddySystemObject + global BuddySystemObject, _raw_heap_start, _raw_page_count start_addr: rawptr = None size: uint64 = 0 if MemmapAddr != 0: @@ -703,7 +861,35 @@ def init(MemmapAddr: uint64, MemmapSize: uint64, MemmapDescSize: uint64, FbAddr: if size < min_blk2: size = 33554432 BuddySystemObject.__init__(start_addr, size) + _raw_heap_start = uint64(start_addr) + _raw_page_count = size >> 12 + if _raw_page_count > uint64(MAX_RAW_PAGES): _raw_page_count = uint64(MAX_RAW_PAGES) + init_page_pool() SlabAllocator.__init__() + _hl: [128]char + viperlib.snprintf(&_hl, 128, "[mm] heap start=0x%lx end=0x%lx size=0x%lx\n", + uint64(start_addr), uint64(start_addr) + size, size) + serial.puts(&_hl) + check_lists(0) + +# 空闲链头一致性检查:tag 只用于日志定位调用阶段。 +export def check_lists(tag: int): + i: int = 0 + bad: int = 0 + while i <= MAX_ORDER: + b: memory_block* = BuddySystemObject.buddy.free_lists[i] + if b is not None: + if uint64(b.magic) != uint64(BLOCK_MAGIC): + _cl: [128]char + viperlib.snprintf(&_cl, 128, "[mm] check%d order=%d bad head=0x%lx magic=0x%lx\n", + tag, i, uint64(b), uint64(uint32(b.magic))) + serial.puts(&_cl) + bad += 1 + if bad >= 3: return + i += 1 + _ok: [64]char + viperlib.snprintf(&_ok, 64, "[mm] check%d free-list heads ok\n", tag) + serial.puts(&_ok) # 虚拟内存映射结构 diff --git a/VKernel/Kernel/paging/paging.vp b/VKernel/Kernel/paging/paging.vp index 907c43e..89812bb 100644 --- a/VKernel/Kernel/paging/paging.vp +++ b/VKernel/Kernel/paging/paging.vp @@ -1,5 +1,6 @@ import drivers.serial.uart.serial as serial import viperlib +import string define PTE_PRESENT = (1 << 0) @@ -11,6 +12,17 @@ define PTE_ACCESSED = (1 << 5) define PTE_DIRTY = (1 << 6) define PTE_PS = (1 << 7) define PTE_GLOBAL = (1 << 8) +# 4KB PTE bit 7 = PAT select (for large pages the same bit is PS). +define PTE_PAT = (1 << 7) + +# PAT entry 4 is reprogrammed to write-combining (WC) for the framebuffer. +# IA32_PAT is configured in main.vp after CPUID tells us PAT is available. +_pat_wc: int = 0 +def set_pat_wc(): + global _pat_wc + _pat_wc = 1 +def has_pat_wc() -> int: + return _pat_wc class _p: entries: [512]uint64 @@ -138,17 +150,45 @@ def _split_1gb_to_pd(pdp_pa: uint64, pdp_idx: uint64) -> int: if not (e & PTE_PRESENT): return 1 if not (e & PTE_PS): return 1 base: uint64 = e & ~uint64(0x3FFFFFFF) - inherit: uint64 = e & (PTE_PCD | PTE_PWT | PTE_USER | uint64(0x8000000000000000)) + # 只继承缓存/执行属性,**绝不继承 PTE_USER**:这些 2MB 条目是 identity 映射, + # 用户态只能通过 MapPage 为具体 4KB 页显式加 USER。若把固件 1GB 条目的 U=1 + # 带进 2MB 叶条目,应用一旦越界写到尚未映射的 VA(例如 heap 0x21780000), + # 就会命中 identity 叶页、直接改物理内存里的 buddy 块头(BAD blk 源头)。 + inherit: uint64 = e & (PTE_PCD | PTE_PWT | uint64(0x8000000000000000)) new_pd: _p* = AllocPage() if not new_pd: return 0 for si in range(512): new_pd.entries[si] = (base + uint64(si) * uint64(0x200000)) | PTE_PRESENT | PTE_WRITABLE | PTE_PS | inherit - # 新 PDPTE 是**非叶条目**:只带 PRESENT|WRITABLE|USER,**绝不带 bit63(NX)** —— - # NX 生效位置是叶条目,已由上面 512 个 2MB 条目继承;非叶条目带 bit63 会让后续所有 - # `entry & ~0xFFF` 派生出的表指针变成非规范地址(正是刚修的 #GP 根因)。 - pdp_t.entries[pdp_idx] = (uint64(new_pd) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE | (inherit & PTE_USER) + # 新 PDPTE 是**非叶条目**:只带 PRESENT|WRITABLE;USER 由 MapPage 在真正建立 + # 用户页时逐级补上。非叶条目也不能带 bit63(NX)(见下)。 + pdp_t.entries[pdp_idx] = (uint64(new_pd) & ~uint64(0xFFF)) | PTE_PRESENT | PTE_WRITABLE return 1 +# 把 1GiB 范围内"物理地址 == 虚拟地址"的 identity 叶条目的 USER 位清掉。 +# 参数 gb_base 必须是 1GiB 对齐的虚拟基址(pdp_idx << 30)。 +def _sanitize_identity_pd(pd: _p*, gb_base: uint64): + i: int = 0 + while i < 512: + e: uint64 = pd.entries[i] + if (e & PTE_PRESENT) == 0: + i += 1 + continue + va2m: uint64 = gb_base + uint64(i) * uint64(0x200000) + if (e & PTE_PS) != 0: + if (e & ~uint64(0x1FFFFF)) == va2m and (e & PTE_USER) != 0: + pd.entries[i] = e & ~PTE_USER + else: + pt_s: _p* = _p*(e & ~uint64(0xFFF)) + j: int = 0 + while j < 512: + ee: uint64 = pt_s.entries[j] + if (ee & PTE_PRESENT) != 0 and (ee & PTE_USER) != 0: + va4k: uint64 = va2m + uint64(j) * uint64(0x1000) + if (ee & ~uint64(0xFFF)) == va4k: + pt_s.entries[j] = ee & ~PTE_USER + j += 1 + i += 1 + def MapPage(VirtAddr: uint64, PhysAddr: uint64, flags: uint64): global pml4, pdp, pd, pt if VirtAddr == 0 or PhysAddr == 0: return @@ -274,6 +314,68 @@ def EnablePaging(): global pml4, pdp, pd, pt __asm__(f"mov cr3, {asminp(uint64(pml4), ASM_DESCR.REG_ANY)}") +# Physical address backing a 4KB user page, or 0 if unmapped / a huge page. +# munmap() needs this before UnMapPage() clears the PTE, so it can hand the +# frame back to the buddy allocator. +# (Local helper: _table_phys is defined later in this module and Viper does +# not allow same-module forward references.) +def _entry_table(e: uint64) -> uint64: + if (e & PTE_PRESENT) == 0: return 0 + if (e & uint64(0x7FF0000000000000)) != 0: return 0 + return e & uint64(0x000FFFFFFFFFF000) + +def GetPagePhys(VirtAddr: uint64) -> uint64: + if VirtAddr == 0: return 0 + p4: _p* = pml4 + if not p4: return 0 + va: uint64 = VirtAddr & ~uint64(0xFFF) + i4: uint64 = (va >> 39) & 0x1FF + pdp_pa: uint64 = _entry_table(p4.entries[i4]) + if pdp_pa == 0: return 0 + p3: _p* = _p*(pdp_pa) + i3: uint64 = (va >> 30) & 0x1FF + if p3.entries[i3] & PTE_PS: return 0 + pd_pa: uint64 = _entry_table(p3.entries[i3]) + if pd_pa == 0: return 0 + p2: _p* = _p*(pd_pa) + i2: uint64 = (va >> 21) & 0x1FF + if p2.entries[i2] & PTE_PS: return 0 + pt_pa: uint64 = _entry_table(p2.entries[i2]) + if pt_pa == 0: return 0 + p1: _p* = _p*(pt_pa) + i1: uint64 = (va >> 12) & 0x1FF + e: uint64 = p1.entries[i1] + if not (e & PTE_PRESENT): return 0 + return e & uint64(0x000FFFFFFFFFF000) + +# Rewrite the protection bits of an existing 4KB mapping, keeping the frame. +# Returns 1 on success, 0 if the page is not a present 4KB page. +def SetPageFlags(VirtAddr: uint64, flags: uint64) -> int: + if VirtAddr == 0: return 0 + p4: _p* = pml4 + if not p4: return 0 + va: uint64 = VirtAddr & ~uint64(0xFFF) + i4: uint64 = (va >> 39) & 0x1FF + pdp_pa: uint64 = _entry_table(p4.entries[i4]) + if pdp_pa == 0: return 0 + p3: _p* = _p*(pdp_pa) + i3: uint64 = (va >> 30) & 0x1FF + if p3.entries[i3] & PTE_PS: return 0 + pd_pa: uint64 = _entry_table(p3.entries[i3]) + if pd_pa == 0: return 0 + p2: _p* = _p*(pd_pa) + i2: uint64 = (va >> 21) & 0x1FF + if p2.entries[i2] & PTE_PS: return 0 + pt_pa: uint64 = _entry_table(p2.entries[i2]) + if pt_pa == 0: return 0 + p1: _p* = _p*(pt_pa) + i1: uint64 = (va >> 12) & 0x1FF + e: uint64 = p1.entries[i1] + if not (e & PTE_PRESENT): return 0 + p1.entries[i1] = (e & uint64(0x000FFFFFFFFFF000)) | flags | PTE_PRESENT + __asm__(f"invlpg [{asminp(va, ASM_DESCR.REG_ANY)}]") + return 1 + # Extract the physical address of a non-leaf page-table entry. # Bits 12..51 hold the address; bits 52..62 are reserved and must be zero. # Bit 63 (NX) is ignored for non-leaf entries. Returns 0 for unusable entries. @@ -505,7 +607,11 @@ def clone_for_process(isolate_vaddr: uint64) -> uint64: if not new_pdp_page: return uint64(new_pml4_page) for i in range(512): new_pdp_page.entries[i] = kernel_pdp.entries[i] - new_pml4_page.entries[pml4_idx] = (uint64(new_pdp_page) & ~uint64(0xFFF)) | (pml4.entries[pml4_idx] & uint64(0xFFF)) + # 固件 identity 大页可能带 U=1:克隆到进程地址空间后必须降为 supervisor。 + # 用户页的 USER 位由后续 MapPage 在具体 4KB PTE 上显式添加。 + if (new_pdp_page.entries[i] & PTE_PRESENT) != 0 and (new_pdp_page.entries[i] & PTE_PS) != 0: + new_pdp_page.entries[i] = new_pdp_page.entries[i] & ~PTE_USER + new_pml4_page.entries[pml4_idx] = (uint64(new_pdp_page) & ~uint64(0xFFF)) | (pml4.entries[pml4_idx] & uint64(0xFFF) & ~PTE_USER) pdp_idx: uint64 = (isolate_vaddr >> 30) & 0x1FF if not (kernel_pdp.entries[pdp_idx] & PTE_PRESENT): return uint64(new_pml4_page) # 1GB 大页(PDPTE.PS=1):源端没有下层 PD。先把它拆成常规 PD(语义不变:512 个 2MB @@ -515,11 +621,19 @@ def clone_for_process(isolate_vaddr: uint64) -> uint64: if kernel_pdp.entries[pdp_idx] & PTE_PS: if _split_1gb_to_pd(uint64(kernel_pdp), pdp_idx) == 0: return uint64(new_pml4_page) kernel_pd: _p* = _p*(kernel_pdp.entries[pdp_idx] & ~uint64(0xFFF)) + # 固件 identity 映射可能把 4KB/2MB 叶条目的 U 置 1。只要 U=1,用户态一旦 + # 写到尚未显式映射的 VA(应用越界写),就会直接落到**同数值物理页**上—— + # buddy 块头、页表、别人的数据全会被踩(现场:1GiB 下 0x21780000 的 + # order-16 空闲块头被清零)。这里把该 1GiB 内"物理地址 == 虚拟地址"的 + # identity 叶条目降为 supervisor;真正的用户页(phys != va)保持原样。 + _sanitize_identity_pd(kernel_pd, isolate_vaddr & ~uint64(0x3FFFFFFF)) new_pd_page: _p* = AllocPage() if not new_pd_page: return uint64(new_pml4_page) for i in range(512): new_pd_page.entries[i] = kernel_pd.entries[i] - new_pdp_page.entries[pdp_idx] = (uint64(new_pd_page) & ~uint64(0xFFF)) | (kernel_pdp.entries[pdp_idx] & uint64(0xFFF)) + if (new_pd_page.entries[i] & PTE_PRESENT) != 0 and (new_pd_page.entries[i] & PTE_PS) != 0: + new_pd_page.entries[i] = new_pd_page.entries[i] & ~PTE_USER + new_pdp_page.entries[pdp_idx] = (uint64(new_pd_page) & ~uint64(0xFFF)) | (kernel_pdp.entries[pdp_idx] & uint64(0xFFF) & ~PTE_USER) pd_idx: uint64 = (isolate_vaddr >> 21) & 0x1FF if not (kernel_pd.entries[pd_idx] & PTE_PRESENT): return uint64(new_pml4_page) if kernel_pd.entries[pd_idx] & PTE_PS: return uint64(new_pml4_page) @@ -527,10 +641,88 @@ def clone_for_process(isolate_vaddr: uint64) -> uint64: new_pt_page: _p* = AllocPage() if not new_pt_page: return uint64(new_pml4_page) for i in range(512): - new_pt_page.entries[i] = kernel_pt.entries[i] - new_pd_page.entries[pd_idx] = (uint64(new_pt_page) & ~uint64(0xFFF)) | (kernel_pd.entries[pd_idx] & uint64(0xFFF)) + new_pt_page.entries[i] = kernel_pt.entries[i] & ~PTE_USER + new_pd_page.entries[pd_idx] = (uint64(new_pt_page) & ~uint64(0xFFF)) | (kernel_pd.entries[pd_idx] & uint64(0xFFF) & ~PTE_USER) return uint64(new_pml4_page) +# Deep-copy every USER mapping from src into dst (dst must already contain the +# kernel mappings, e.g. from create_process_page_table()). Data frames are +# allocated through alloc_fn(4096); page-table pages come from the paging pool. +# +# A table is only duplicated when its subtree really contains user pages; +# otherwise the parent's table is shared. Without that check a single user +# page at 0x400000 drags the whole kernel identity map (one PT page per 2MB +# region) into the new address space and exhausts the page-table pool. +def _alloc_user_frame(alloc_fn: rawptr) -> rawptr: + newp: rawptr = None + __asm__(f"""mov r11, {asminp(alloc_fn, ASM_DESCR.REG_ANY)} + push rbp + mov rbp, rsp + and rsp, -16 + mov rdi, 4096 + call r11 + mov rsp, rbp + pop rbp + mov {asmout(newp, ASM_DESCR.OUTPUT_REG)}, rax""", + op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX, + ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX, + ASM_DESCR.CLOBBER_RSI, ASM_DESCR.CLOBBER_RDI, + ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, + ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) + return newp + +# level: 1 = PT, 2 = PD, 3 = PDPT (4 = PML4 only used as the entry point). +def _table_has_user(t: _p*, level: int) -> int: + i: int = 0 + while i < 512: + e: uint64 = t.entries[i] + if (e & PTE_PRESENT) != 0: + if level == 1: + if (e & PTE_USER) != 0: return 1 + elif (e & PTE_PS) != 0: + if (e & PTE_USER) != 0: return 1 + elif (e & PTE_USER) != 0: + if _table_has_user(_p*(e & ~uint64(0xFFF)), level - 1) != 0: return 1 + i += 1 + return 0 + +def _copy_user_table(src: _p*, dst: _p*, level: int, alloc_fn: rawptr) -> int: + i: int = 0 + while i < 512: + e: uint64 = src.entries[i] + if (e & PTE_PRESENT) == 0: + dst.entries[i] = e + elif level == 1: + if (e & PTE_USER) == 0: + dst.entries[i] = e + else: + src_phys: uint64 = e & uint64(0x000FFFFFFFFFF000) + newp: rawptr = _alloc_user_frame(alloc_fn) + if newp is None: return -1 + string.memcpy(newp, rawptr(src_phys), 4096) + dst.entries[i] = (uint64(newp) & ~uint64(0xFFF)) | (e & uint64(0xFFF)) + elif (e & PTE_PS) != 0: + dst.entries[i] = e + elif (e & PTE_USER) == 0: + dst.entries[i] = e + else: + csrc: _p* = _p*(e & ~uint64(0xFFF)) + if _table_has_user(csrc, level - 1) == 0: + dst.entries[i] = e + else: + np: _p* = AllocPage() + if not np: return -1 + dst.entries[i] = (uint64(np) & ~uint64(0xFFF)) | (e & uint64(0xFFF)) + if _copy_user_table(csrc, np, level - 1, alloc_fn) != 0: return -1 + i += 1 + return 0 + +def CopyUserSpace(src_pa: uint64, dst_pa: uint64, alloc_fn: rawptr) -> int: + if src_pa == 0 or dst_pa == 0: return -1 + s4: _p* = _p*(src_pa) + d4: _p* = _p*(dst_pa) + return _copy_user_table(s4, d4, 4, alloc_fn) + def save_pt_range(vaddr_start: uint64, vaddr_end: uint64, save_buf: uint64*, save_count: uint32*) -> int: global pml4 if not pml4: return -1 diff --git a/VKernel/Kernel/sched/process.vp b/VKernel/Kernel/sched/process.vp index ca5143d..03bdf7e 100644 --- a/VKernel/Kernel/sched/process.vp +++ b/VKernel/Kernel/sched/process.vp @@ -12,15 +12,21 @@ define PROC_READY = 0 define PROC_RUNNING = 1 define PROC_BLOCKED = 2 define PROC_DONE = 3 -define MAX_PROCESSES = 8 +define MAX_PROCESSES = 32 define MAX_PROC_THREADS = 8 define PROC_MAX_FDS = 16 # fd 种类(process.fd_kind[])。定义放这里、由 intr/syscall.vp 以 process.FD_KIND_* 引用, # 这样 process.vp 不必 import devfs —— devfs → keyboard/mouse → intr.idt → sched → process # 会形成环,不能让 process 反向依赖 devfs。 define FD_KIND_FAT32 = 0 -define FD_KIND_DEVFS = 1 +define FD_KIND_DEVFS = 1 +define FD_KIND_DIR = 2 +define FD_KIND_PIPE = 3 define PROC_MAX_DIRS = 8 +define MAX_VMAS = 64 +define VMA_KIND_ANON = 1 +define VMA_KIND_DEV = 2 +define VMA_KIND_FILE = 3 define KERN_STACK_SIZE = 8192 define USER_STACK_SIZE = 65536 # 用户栈虚拟区:主线程栈顶 = USER_STACK_VTOP(见 execrunner/elf.vp 的映射), @@ -42,17 +48,39 @@ class Process: heap_start: uint64 heap_size: uint64 name: [32]char + cwd: [256]char fd_table: [PROC_MAX_FDS]rawptr fd_used: [PROC_MAX_FDS]uint8 # 0 = FAT32 文件(fd_table[i] 里是 fileobj*);1 = devfs 设备(fd_table[i] 里是 # devfs_fd+1,+1 只为避开"devfs fd 0"与空指针的歧义)。READ/WRITE/CLOSE/IOCTL 与 # Process.exit 都必须按它分派 —— 把 devfs fd 当 fileobj* 解引用会直接跑飞。 fd_kind: [PROC_MAX_FDS]uint8 + fd_flags: [PROC_MAX_FDS]uint32 + fd_shared: [PROC_MAX_FDS]uint8 dir_table: [PROC_MAX_DIRS]rawptr dir_used: [PROC_MAX_DIRS]uint8 dir_pool: [PROC_MAX_DIRS]fat32_types.dirobj kern_stack: uint64 user_stack: uint64 + # Virtual memory areas backing mmap()/mprotect()/munmap()/mremap(). + vma_base: [MAX_VMAS]uint64 + vma_size: [MAX_VMAS]uint64 + vma_prot: [MAX_VMAS]uint32 + vma_kind: [MAX_VMAS]uint8 + vma_flags: [MAX_VMAS]uint32 + # brk() region (separate from the mmap bump allocator). + brk_start: uint64 + brk_end: uint64 + # Linux process metadata. + entry_rsp: uint64 + parent_pid: int + exit_code: int + # Signals (per process; delivery happens on syscall return). + sig_handler: [65]uint64 + sig_actflags: [65]uint64 + sig_restorer: [65]uint64 + sig_mask: uint64 + sig_pending: uint64 def addThread(self, tid: int) -> int: if self.thread_count >= MAX_PROC_THREADS: return -1 @@ -69,11 +97,12 @@ class Process: # 当指针解引用而跑飞。devfs 的 fd 表是独立的另一张;正规退出路径本来就不 # 回收 fd(见 intr/syscall.vp 的 EXIT 不调 Process.exit),这里只清槽位, # app 自己调 CLOSE 时才走 devfs_close。 - if fp and self.fd_kind[i] == FD_KIND_FAT32: + if fp and self.fd_kind[i] == FD_KIND_FAT32 and self.fd_shared[i] == 0: fat32.close(fp) self.fd_used[i] = 0 self.fd_table[i] = None self.fd_kind[i] = 0 + self.fd_shared[i] = 0 for i in range(PROC_MAX_DIRS): if self.dir_used[i]: dp: rawptr = self.dir_table[i] @@ -125,6 +154,7 @@ class ProcessManager: p.name[i] = ch i += 1 p.name[i] = 0 + string.strcpy(&p.cwd[0], "/") j: int for j in range(MAX_PROC_THREADS): p.threads[j] = -1 @@ -132,9 +162,28 @@ class ProcessManager: p.fd_used[j] = 0 p.fd_table[j] = None p.fd_kind[j] = 0 + p.fd_flags[j] = 0 + p.fd_shared[j] = 0 for j in range(PROC_MAX_DIRS): p.dir_used[j] = 0 p.dir_table[j] = None + for j in range(MAX_VMAS): + p.vma_base[j] = 0 + p.vma_size[j] = 0 + p.vma_prot[j] = 0 + p.vma_kind[j] = 0 + p.vma_flags[j] = 0 + p.brk_start = 0 + p.brk_end = 0 + p.entry_rsp = 0 + p.parent_pid = 0 + p.exit_code = 0 + p.sig_mask = 0 + p.sig_pending = 0 + for j in range(65): + p.sig_handler[j] = 0 + p.sig_actflags[j] = 0 + p.sig_restorer[j] = 0 __asm__(f"""mov {asmout(p.pml4_root, ASM_DESCR.OUTPUT_REG)}, cr3""", op=[ASM_DESCR.CLOBBER_MEMORY]) @@ -171,7 +220,10 @@ class ProcessManager: @staticmethod def create_process(name: str, entry: rawptr) -> Process*: m: ProcessManager* = _proc_mgr_ptr - if m.count >= MAX_PROCESSES: return None + if m.count >= MAX_PROCESSES: + # 启动时已有 1(内核)+7(app) 个槽;旧上限 8 导致 run/runb 永远失败。 + serial.puts("[process] create_process: pid table full\n") + return None pid: int = m.count m.count += 1 p: Process* = &m.processes[pid] @@ -189,17 +241,38 @@ class ProcessManager: p.name[i] = ch i += 1 p.name[i] = 0 + string.strcpy(&p.cwd[0], "/") for j in range(MAX_PROC_THREADS): p.threads[j] = -1 for j in range(PROC_MAX_FDS): p.fd_used[j] = 0 p.fd_table[j] = None p.fd_kind[j] = 0 + p.fd_flags[j] = 0 + p.fd_shared[j] = 0 for j in range(PROC_MAX_DIRS): p.dir_used[j] = 0 p.dir_table[j] = None + for j in range(MAX_VMAS): + p.vma_base[j] = 0 + p.vma_size[j] = 0 + p.vma_prot[j] = 0 + p.vma_kind[j] = 0 + p.vma_flags[j] = 0 + p.brk_start = 0 + p.brk_end = 0 + p.entry_rsp = 0 + p.parent_pid = 0 + p.exit_code = 0 + p.sig_mask = 0 + p.sig_pending = 0 + for j in range(65): + p.sig_handler[j] = 0 + p.sig_actflags[j] = 0 + p.sig_restorer[j] = 0 ks: rawptr = mm.malloc(KERN_STACK_SIZE) if ks is None: + serial.puts("[process] kernel stack alloc failed\n") m.count = m.count - 1 return None ks_size: uint64 = KERN_STACK_SIZE @@ -302,6 +375,60 @@ def drop_to_user_mode(entry: rawptr, user_rsp: uint64, kern_rsp: uint64, arg: ra ASM_DESCR.CLOBBER_R9, ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_RBP]) +# Resume context used by fork/clone children: they start user mode at the +# parent's post-syscall RIP with a full register set and RAX=0. +export _resume_ctx: [20]uint64 + +def set_resume(rip: uint64, rsp: uint64, flags: uint64, rax: uint64, regs: uint64*): + _resume_ctx[0] = rip + _resume_ctx[1] = rsp + _resume_ctx[2] = flags + _resume_ctx[3] = rax + i: int = 0 + while i < 12: + _resume_ctx[4 + i] = regs[i] + i += 1 + +def drop_to_user_mode_ctx(kern_rsp: uint64): + __asm__(f"""cli + mov rsp, {asminp(kern_rsp, ASM_DESCR.REG_ANY)} + mov ecx, 0xC0000101 + rdmsr + shl rdx, 32 + or rax, rdx + test rax, rax + jz 1f + swapgs + 1: + lea rbx, [rip + _resume_ctx] + mov r15, [rbx+32] + mov r14, [rbx+40] + mov r13, [rbx+48] + mov r12, [rbx+56] + mov r10, [rbx+64] + mov r9, [rbx+72] + mov r8, [rbx+80] + mov rbp, [rbx+88] + mov rdi, [rbx+96] + mov rsi, [rbx+104] + mov rdx, [rbx+112] + mov rcx, [rbx+0] + mov r11, [rbx+16] + mov rax, [rbx+24] + push 0x23 + push qword ptr [rbx+8] + push r11 + push 0x2B + push rcx + mov rbx, [rbx+120] + iretq""", + op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX, + ASM_DESCR.CLOBBER_RBX, ASM_DESCR.CLOBBER_RCX, + ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, + ASM_DESCR.CLOBBER_RDI, ASM_DESCR.CLOBBER_R8, + ASM_DESCR.CLOBBER_R9, ASM_DESCR.CLOBBER_R10, + ASM_DESCR.CLOBBER_RBP]) + # ============================================================ # 用户态线程(THREAD_CREATE)上下文 diff --git a/VKernel/Kernel/sched/sched.vp b/VKernel/Kernel/sched/sched.vp index 53f4a90..0873216 100644 --- a/VKernel/Kernel/sched/sched.vp +++ b/VKernel/Kernel/sched/sched.vp @@ -10,7 +10,7 @@ define THREAD_READY = 0 define THREAD_RUNNING = 1 define THREAD_BLOCKED = 2 define THREAD_DONE = 3 -define MAX_THREADS = 16 +define MAX_THREADS = 64 define THREAD_STACK_SIZE = 65536 define SCHED_QUANTUM = 10 @@ -21,6 +21,34 @@ export _isr_scratch_tbl: [MAX_THREADS * 6]uint64 export _switch_old_tid: int = 0 export _switch_new_tid: int = 0 +# Per-thread IA32_FS_BASE (glibc TLS). Kept outside Thread: Thread has a +# hard-coded 600-byte layout checked by _thread_entry. +define IA32_FS_BASE = 0xC0000100 +_fs_base_tbl: [MAX_THREADS]uint64 +_syscall_frame_tbl: [MAX_THREADS]uint64 +_clear_child_tid: [MAX_THREADS]uint64 + +def _msr_read(msr: uint32) -> uint64: + lo: uint32 + hi: uint32 + __asm__(f"""mov ecx, {asminp(msr, ASM_DESCR.REG_ANY)} + rdmsr + mov {asmout(lo, ASM_DESCR.OUTPUT_REG)}, eax + mov {asmout(hi, ASM_DESCR.OUTPUT_REG)}, edx""", + out=[asmout(lo, ASM_DESCR.OUTPUT_REG), asmout(hi, ASM_DESCR.OUTPUT_REG)], + op=[ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RDX]) + return uint64(lo) | (uint64(hi) << 32) + +def _msr_write(msr: uint32, val: uint64): + lo: uint32 = uint32(val & uint64(0xFFFFFFFF)) + hi: uint32 = uint32(val >> 32) + __asm__(f"""mov ecx, {asminp(msr, ASM_DESCR.REG_ANY)} + mov eax, {asminp(lo, ASM_DESCR.REG_ANY)} + mov edx, {asminp(hi, ASM_DESCR.REG_ANY)} + wrmsr""", + op=[ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RDX]) + + class Thread: rsp: uint64 @@ -103,12 +131,12 @@ class Scheduler: __asm__(f"""mov [{asminp(main_rsp_ptr, ASM_DESCR.REG_ANY)}], rsp""", op=[ASM_DESCR.CLOBBER_MEMORY]) # fail-fast:_thread_entry 的 asm 以硬编码偏移访问线程表(sizeof(Thread)=600、 - # current_tid=+9600=16*600)。增删 Thread 字段会让它们失配 ⇒ 线程入口会按错误的 + # current_tid=+38400=64*600)。增删 Thread 字段会让它们失配 ⇒ 线程入口会按错误的 # 偏移取 func/arg 并 call 任意地址。这里直接核对,不符立即停机暴露。 thr0_addr: uint64 = uint64(&s.threads[0]) curt_addr: uint64 = uint64(&s.current_tid) - if Thread.__sizeof__() != 600 or (curt_addr - thr0_addr) != 9600: - serial.puts("[sched] FATAL: Thread 布局与 _thread_entry 硬编码偏移(600/9600)不一致\n") + if Thread.__sizeof__() != 600 or (curt_addr - thr0_addr) != 38400: + serial.puts("[sched] FATAL: Thread 布局与 _thread_entry 硬编码偏移(600/38400)不一致\n") while True: asm.sti() asm.hlt() @@ -117,7 +145,8 @@ class Scheduler: def _yield(): global _switch_old_rsp, _switch_new_rsp, _yield_cnt, _yield_lock global _switch_old_tid, _switch_new_tid - global _switch_old_fpu, _switch_new_fpu + global _switch_old_fpu, _switch_new_fpu + global _fs_base_tbl _if_saved: int = 0 __asm__(f"""pushfq pop rax @@ -176,6 +205,9 @@ class Scheduler: cpu.set_kernel_rsp0(next_ks) _switch_old_tid = old_tid _switch_new_tid = next_tid + # Per-thread TLS: save the outgoing FS base, load the incoming one. + _fs_base_tbl[old_tid] = _msr_read(uint32(IA32_FS_BASE)) + _msr_write(uint32(IA32_FS_BASE), _fs_base_tbl[next_tid]) _yield_lock = 0 _do_switch() if _if_saved: @@ -306,8 +338,14 @@ class Scheduler: 现场 [0E] rip=0x0 err=0x15(用户态取指缺页)cr2=0x0 rsp=0x7fffffde0000,tid 为该 app 线程, 紧跟在 [syscall] exit called 之后。 """ + global _clear_child_tid s: Scheduler* = _sched_ptr tid: int = s.current_tid + ct: uint64 = _clear_child_tid[tid] + if ct != 0: + cw: uint32* = uint32*(ct) + cw[0] = uint32(0) + _clear_child_tid[tid] = 0 if tid > 0 and tid < MAX_THREADS: s.threads[tid].state = THREAD_DONE proc.free_user_thread(proc.find_user_slot_by_tid(tid)) @@ -318,6 +356,33 @@ class Scheduler: asm.hlt() Scheduler._yield() + @staticmethod + def exit_process(pid: int): + """Linux exit_group: retire every thread of `pid`, release the process, never return.""" + global _clear_child_tid + s: Scheduler* = _sched_ptr + i: int = 0 + while i < s.thread_count: + if s.threads[i].pid == pid: + ct: uint64 = _clear_child_tid[i] + if ct != 0: + cw: uint32* = uint32*(ct) + cw[0] = uint32(0) + _clear_child_tid[i] = 0 + s.threads[i].state = THREAD_DONE + slot: int = proc.find_user_slot_by_tid(i) + if slot >= 0: + proc.free_user_thread(slot) + i += 1 + p: proc.Process* = proc.ProcessManager.get_process(pid) + if p is not None: + p.exit() + Scheduler._yield() + while True: + asm.sti() + asm.hlt() + Scheduler._yield() + _sched_storage: Scheduler = Scheduler() export _sched_ptr: Scheduler* = &_sched_storage @@ -326,6 +391,34 @@ export _switch_new_rsp: rawptr export _switch_old_fpu: rawptr export _switch_new_fpu: rawptr +def set_fs_base(base: uint64): + global _fs_base_tbl + s: Scheduler* = _sched_ptr + _fs_base_tbl[s.current_tid] = base + _msr_write(uint32(IA32_FS_BASE), base) + +def set_thread_fs_base(tid: int, base: uint64): + global _fs_base_tbl + if tid >= 0 and tid < MAX_THREADS: + _fs_base_tbl[tid] = base + +def get_fs_base() -> uint64: + return _msr_read(uint32(IA32_FS_BASE)) + +def set_syscall_frame(f: uint64): + global _syscall_frame_tbl + s: Scheduler* = _sched_ptr + _syscall_frame_tbl[s.current_tid] = f + +def get_syscall_frame() -> uint64: + s: Scheduler* = _sched_ptr + return _syscall_frame_tbl[s.current_tid] + +def set_clear_child_tid(tid: int, addr: uint64): + global _clear_child_tid + if tid >= 0 and tid < MAX_THREADS: + _clear_child_tid[tid] = addr + @__attribute__(naked) def _do_switch(): __asm__(f"""push rax @@ -394,20 +487,20 @@ def _do_switch(): op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX]) # Thread 布局:rsp0/stack8/stack_size16/func24/arg32/state40/tid44/pid48/kern_stack56/fpu_area64/fpu_ptr592 -# ⇒ sizeof(Thread) = 600,Scheduler.threads[16] 占 9600 字节 ⇒ current_tid 在 +9600。 -# 增删 Thread 字段时必须同步下面 asm 里的 9600 / 600(以及 +24/+32/+40 三个字段偏移)。 +# ⇒ sizeof(Thread) = 600,Scheduler.threads[64] 占 38400 字节 ⇒ current_tid 在 +38400。 +# 增删 Thread 字段时必须同步下面 asm 里的 38400 / 600(以及 +24/+32/+40 三个字段偏移)。 @__attribute__(naked) def _thread_entry(): __asm__(f"""sub rsp, 8 mov rax, [rip + _sched_ptr] - mov ecx, dword ptr [rax + 9600] + mov ecx, dword ptr [rax + 38400] movsxd rcx, ecx imul rcx, rcx, 600 mov rdi, [rax + rcx + 32] mov rax, [rax + rcx + 24] call rax mov rax, [rip + _sched_ptr] - mov ecx, dword ptr [rax + 9600] + mov ecx, dword ptr [rax + 38400] movsxd rcx, ecx imul rcx, rcx, 600 mov dword ptr [rax + rcx + 40], 3 diff --git a/VKernel/project.vpj b/VKernel/project.vpj index 46519f9..47cc60c 100644 --- a/VKernel/project.vpj +++ b/VKernel/project.vpj @@ -23,7 +23,9 @@ "output": "kernel.elf" }, "includes": [ - "../../../includes" + "../..", + "../../../includes", + "../" ], "target": { "triple": "x86_64-none-elf", diff --git a/abi/__init__.vp b/abi/__init__.vp new file mode 100644 index 0000000..3f7e353 --- /dev/null +++ b/abi/__init__.vp @@ -0,0 +1,2 @@ +# Package marker for the shared Linux ABI module. +define ABI_VERSION = 1 diff --git a/abi/linux.vp b/abi/linux.vp new file mode 100644 index 0000000..ee3d0f5 --- /dev/null +++ b/abi/linux.vp @@ -0,0 +1,206 @@ +# Linux x86-64 ABI constants and structures shared by the kernel and vpsdk. +# +# The byte layouts must match Linux exactly: glibc reads these structs +# directly and never goes through any ViperOS wrapper. +# +# This module is imported by both sides: +# VKernel/Kernel/intr/syscall.vp (kernel project gains "../.." include) +# vpsdk/syscall.vp / vpsdk/fs.vp (apps already include the repo root) + +# ---------------------------------------------------------------- errno ---- +define EPERM = 1 +define ENOENT = 2 +define ESRCH = 3 +define EINTR = 4 +define EIO = 5 +define ENXIO = 6 +define E2BIG = 7 +define ENOEXEC = 8 +define EBADF = 9 +define ECHILD = 10 +define EAGAIN = 11 +define ENOMEM = 12 +define EACCES = 13 +define EFAULT = 14 +define ENOTBLK = 15 +define EBUSY = 16 +define EEXIST = 17 +define EXDEV = 18 +define ENODEV = 19 +define ENOTDIR = 20 +define EISDIR = 21 +define EINVAL = 22 +define ENFILE = 23 +define EMFILE = 24 +define ENOTTY = 25 +define ETXTBSY = 26 +define EFBIG = 27 +define ENOSPC = 28 +define ESPIPE = 29 +define EROFS = 30 +define EMLINK = 31 +define EPIPE = 32 +define EDOM = 33 +define ERANGE = 34 +define EDEADLK = 35 +define ENAMETOOLONG = 36 +define ENOLCK = 37 +define ENOSYS = 38 +define ENOTEMPTY = 39 +define ELOOP = 40 +define EOVERFLOW = 75 +define ENOTSUP = 95 +define EOPNOTSUPP = 95 +define ETIMEDOUT = 110 + +# ------------------------------------------------------------ open flags ---- +# Values are the Linux x86-64 ones (octal in the man page). +define O_RDONLY = 0 +define O_WRONLY = 1 +define O_RDWR = 2 +define O_ACCMODE = 3 +define O_CREAT = 64 +define O_EXCL = 128 +define O_NOCTTY = 256 +define O_TRUNC = 512 +define O_APPEND = 1024 +define O_NONBLOCK = 2048 +define O_DSYNC = 4096 +define O_DIRECT = 16384 +define O_DIRECTORY = 65536 +define O_NOFOLLOW = 131072 +define O_CLOEXEC = 524288 + +# --------------------------------------------------------------- fcntl ---- +define F_DUPFD = 0 +define F_GETFD = 1 +define F_SETFD = 2 +define F_GETFL = 3 +define F_SETFL = 4 +define F_DUPFD_CLOEXEC = 1030 +define FD_CLOEXEC = 1 + +# ----------------------------------------------------------- at / dirent ---- +define AT_FDCWD = -100 +define AT_SYMLINK_NOFOLLOW = 256 +define AT_REMOVEDIR = 512 +define AT_EACCESS = 512 +define AT_EMPTY_PATH = 4096 + +define DT_UNKNOWN = 0 +define DT_FIFO = 1 +define DT_CHR = 2 +define DT_DIR = 4 +define DT_BLK = 6 +define DT_REG = 8 +define DT_LNK = 10 +define DT_SOCK = 12 + +# ------------------------------------------------------------ mmap/mprot ---- +define PROT_NONE = 0 +define PROT_READ = 1 +define PROT_WRITE = 2 +define PROT_EXEC = 4 +define MAP_SHARED = 1 +define MAP_PRIVATE = 2 +define MAP_FIXED = 16 +define MAP_ANONYMOUS = 32 +define MAP_GROWSDOWN = 256 +define MAP_NORESERVE = 16384 +define MAP_POPULATE = 32768 +define MAP_FAILED: uint64 = 0xFFFFFFFFFFFFFFFF + +# ---------------------------------------------------------------- mremap ---- +define MREMAP_MAYMOVE = 1 +define MREMAP_FIXED = 2 + +# ------------------------------------------------------------- arch_prctl ---- +define ARCH_SET_GS = 0x1001 +define ARCH_SET_FS = 0x1002 +define ARCH_GET_FS = 0x1003 +define ARCH_GET_GS = 0x1004 + +# ---------------------------------------------------------------- rlimit ---- +define RLIMIT_CPU = 0 +define RLIMIT_FSIZE = 1 +define RLIMIT_DATA = 2 +define RLIMIT_STACK = 3 +define RLIMIT_CORE = 4 +define RLIMIT_NOFILE = 7 +define RLIM_INFINITY: uint64 = 0xFFFFFFFFFFFFFFFF + +# ------------------------------------------------------------- file modes ---- +define S_IFMT = 61440 +define S_IFDIR = 16384 +define S_IFCHR = 8192 +define S_IFIFO = 4096 +define S_IFREG = 32768 +define MODE_FILE = 33188 # 0100644 +define MODE_DIR = 16877 # 040755 +define MODE_CHAR = 8630 # 020666 +define MODE_FIFO = 4534 # 010666 + +define SEEK_SET = 0 +define SEEK_CUR = 1 +define SEEK_END = 2 + +define PATH_MAX = 256 +define SIZEOF_LINUX_STAT = 144 +define SIZEOF_UTSNAME = 390 +define SIZEOF_DIRENT64 = 19 + +# ---------------------------------------------------------------- structs ---- +# struct stat, x86-64 Linux layout (144 bytes). +@__attribute__(packed) +class linux_stat: + st_dev: uint64 + st_ino: uint64 + st_nlink: uint64 + st_mode: uint32 + st_uid: uint32 + st_gid: uint32 + pad0: uint32 + st_rdev: uint64 + st_size: uint64 + st_blksize: uint64 + st_blocks: uint64 + st_atime_sec: uint64 + st_atime_nsec: uint64 + st_mtime_sec: uint64 + st_mtime_nsec: uint64 + st_ctime_sec: uint64 + st_ctime_nsec: uint64 + res0: uint64 + res1: uint64 + res2: uint64 + +# struct linux_dirent64 header; the NUL-terminated name follows immediately. +@__attribute__(packed) +class linux_dirent64: + d_ino: uint64 + d_off: uint64 + d_reclen: uint16 + d_type: uint8 + +@__attribute__(packed) +class linux_timespec: + tv_sec: uint64 + tv_nsec: uint64 + +@__attribute__(packed) +class linux_utsname: + sysname: [65]char + nodename: [65]char + release: [65]char + version: [65]char + machine: [65]char + domainname: [65]char + +@__attribute__(packed) +class linux_rlimit: + cur: uint64 + max: uint64 + +class iovec: + base: rawptr + len: uint64 diff --git a/build.py b/build.py index 636d206..f3777b6 100644 --- a/build.py +++ b/build.py @@ -7,7 +7,7 @@ import subprocess # ============================================================================ # viperc 编译器路径 —— 唯一来源,改这里即可 # ============================================================================ -VIPERC = r'..\..\ViperC\__vpcbuild__\output\viperc.exe' +VIPERC = r'..\..\ViperC\__vpcbuild6__\output\viperc.exe' # 解析参数 @@ -17,6 +17,18 @@ noapp = '--noapp' in sys.argv bootloader = '--bootloader' in sys.argv # --onlyrun:跳过全部编译/打包,直接用现有 disk.img 启动 QEMU only_run = '--onlyrun' in sys.argv +# --headless:QEMU 不开窗口(-display none),串口仍写入 serial.log。 +# 自动化调试/无人值守验证时使用;需要看画面时不要带这个参数。 +headless = '--headless' in sys.argv +# --runsecs=N:启动 N 秒后自动结束 QEMU(0=一直运行到客户机退出)。 +# 与 --headless 搭配可用于无人值守的冒烟测试。 +run_secs = 0 +for _arg in sys.argv: + if _arg.startswith('--runsecs='): + try: + run_secs = int(_arg[len('--runsecs='):]) + except ValueError: + run_secs = 0 # --onlyapp:只编译库与应用(保留兼容;现在这已是默认行为) only_app = '--onlyapp' in sys.argv # --kernel:显式重编内核。不带 --app 时**默认就编内核**(见下方 app_filter 解析后的修正); @@ -45,6 +57,17 @@ if kernel_only and nokernel: sys.exit(1) +def SyncSyscallTable(): + """同步 syscall 表:Scripts/syscall_table.py 是唯一权威来源。 + + VKernel 与 vpsdk 各自持有一份 enum(vpsdk 不能依赖内核),构建前统一 + 重新生成,避免两份表手工漂移。生成器失败时保留现有文件继续构建。 + """ + rc = os.system(f'"{sys.executable}" Scripts/syscall_table.py --write') + if rc != 0: + print("[build] 警告: syscall 表同步失败,继续使用现有文件") + + def BuildKernel(): """编译内核:链接器一次产出带符号 kernel.elf,再由 objcopy 二次处理成裸镜像 kernel.bin""" cmd = rf'{VIPERC} --project ./VKernel/project.vpj --clear-cache --rebuild-includes' @@ -74,6 +97,7 @@ APP_LIST = [ ("scene3d", "Apps/Scene3D", "scene3d.elf", "scene3d.elf"), ("explorer", "Apps/Explorer", "explorer.elf", "explorer.elf"), ("fontdemo", "Apps/FontDemo", "fontdemo.elf", "fontdemo.elf"), + ("imagesee", "Apps/ImageSee", "imagesee.elf", "imagesee.elf"), ] # 动态库表:(工程目录, 产物文件名) LIB_LIST = [ @@ -97,6 +121,26 @@ def CopyFonts(): shutil.copy(src, "./disk/fonts/font.ttf") +def CopyAssets(): + """Copy image demo assets (Assets/images) to /IMAGES on the disk.""" + src_dir = "Assets/images" + if not os.path.isdir(src_dir): + print("[build] 警告: 缺少 Assets/images,ImageSee 将没有演示图片") + return + os.makedirs("./disk/images", exist_ok=True) + for name in os.listdir(src_dir): + low = name.lower() + if low.endswith(".png") or low.endswith(".jpg") or low.endswith(".jpeg"): + shutil.copy(os.path.join(src_dir, name), os.path.join("./disk/images", name)) + print(f"[build] asset {name} -> disk/images/{name}") + # Desktop wallpaper used by Explorer (separate from the ImageSee gallery). + bg = "Assets/background.jpg" + if os.path.exists(bg): + os.makedirs("./disk/sys/images/screen", exist_ok=True) + shutil.copy(bg, "./disk/sys/images/screen/background.jpg") + print("[build] asset background.jpg -> disk/sys/images/screen/background.jpg") + + def BuildLibs(): """编译 Libs/ 下的动态库(应用运行期经 LOAD_SO 加载)。""" os.makedirs("./disk/libs", exist_ok=True) @@ -222,11 +266,25 @@ def RunQemu(): '-serial', f'file:{serial_log}', # '-no-reboot' ] + if headless: + qemu_cmd += ['-display', 'none'] print(f"[build] QEMU cmd: {' '.join(qemu_cmd)}") proc = subprocess.Popen(qemu_cmd) - proc.wait() + if run_secs > 0: + try: + proc.wait(timeout=run_secs) + except subprocess.TimeoutExpired: + print(f"[build] --runsecs={run_secs}: 自动结束 QEMU") + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + else: + proc.wait() print(f"[build] serial_log path: {serial_log}") print(f"[build] file exists: {os.path.exists(serial_log)}") @@ -242,6 +300,7 @@ def RunQemu(): if only_run: print("[build] --onlyrun: 跳过编译/打包,直接用现有 disk.img 启动 QEMU") else: + SyncSyscallTable() if kernel_only: print("[build] --kernel-only: 只编译内核") BuildKernel() @@ -251,8 +310,10 @@ else: else: print("[build] 跳过内核编译(--app 或 --nokernel)") BuildApps() + CopyAssets() BuildBootloader() CopyFonts() BuildDisk() -RunQemu() \ No newline at end of file +if "--norun" not in sys.argv: + RunQemu() \ No newline at end of file diff --git a/docs/imagesee.md b/docs/imagesee.md new file mode 100644 index 0000000..0dc9b35 --- /dev/null +++ b/docs/imagesee.md @@ -0,0 +1,114 @@ +# ImageSee 图像解码库 + +`vpsdk/imagesee` 是一个纯用户态的 PNG / JPEG 解码库,配套示例应用 +`Apps/ImageSee` 在窗口中并排显示解码结果。 + +## 1. 目录与模块 + +```text +vpsdk/imagesee/ +├── imagesee.vp 公共 API:load / decode_memory / checksum +├── image.vp Image 类型与像素打包(0xAARRGGBB) +├── inflate.vp zlib / DEFLATE(stored / fixed / dynamic Huffman) +├── png.vp PNG:全部颜色类型、1/2/4/8/16 位、Adam7 +└── jpeg.vp JPEG:baseline/extended sequential、4:4:4/4:2:2/4:2:0 + +Apps/ImageSee/ 示例应用(窗口 + 两块 FBCanvas) +Assets/images/ 示例图片(由 Scripts/gen_imagesee_assets.py 生成) +``` + +## 2. 公共 API + +```python +import vpsdk.imagesee.imagesee as imagesee + +img = imagesee.load("/IMAGES/SAMPLE.PNG") # 失败返回 None +if img is not None: + w = img.width + h = img.height + c = img.pixels[y * w + x] # 0xAARRGGBB + r = (c >> 16) & 0xFF + g = (c >> 8) & 0xFF + b = c & 0xFF + +img2 = imagesee.decode_memory(buf, size) # 已有字节流时使用 +sum32 = imagesee.checksum(img) # FNV-1a,用于回归比对 +name = imagesee.format_name(img) # "png" / "jpeg" +``` + +像素格式与 ViperOS 窗口 / `FBCanvas` 一致:`0xAARRGGBB`,alpha 字节目前由 +合成层忽略,解码器一律输出 255(PNG 的透明像素保留 alpha=0 于像素字中)。 + +## 3. PNG 支持 + +- 签名、IHDR、PLTE、tRNS、IDAT(可多块)、IEND +- 颜色类型:0(灰度)、2(RGB)、3(调色板)、4(灰度+alpha)、6(RGBA) +- 位深:1/2/4/8/16(按 PNG 规则缩小到 8 位输出) +- 过滤:None / Sub / Up / Average / Paeth +- Adam7 隔行扫描 +- 解码后输出 RGBA;调色板 tRNS、灰度和 RGB 的透明色都会转成 alpha=0 + +## 4. JPEG 支持 + +- Baseline / extended sequential Huffman(SOF0 / SOF1) +- 灰度与 YCbCr,色度子采样 4:4:4 / 4:2:2 / 4:2:0(以及任意 H/V 因子) +- DQT(8/16 位)、DHT、DRI + RSTn 重启标记 +- 组件按 ID 识别(1=Y、2=Cb、3=Cr),也兼容 Adobe APP14 的 RGB 变换 +- 8×8 浮点 IDCT,YCbCr → RGB 后输出 + +**限制**:渐进式 JPEG(SOF2)目前明确返回 `None`(不做“解一半”的错误输出); +CMYK/YCCK 返回 `None`;不支持 EXIF 方向、缩略图、算术编码。 + +## 5. 示例应用 + +`/APPS/IMAGESEE.ELF` 启动后: + +1. 依次解码并打印 `/IMAGES/SAMPLE.PNG`、`PHOTO.JPG`、`GRAY.JPG`、 + `PAL.PNG`、`INTER.PNG` 的尺寸、FNV 校验和与 3 个采样像素; +2. 创建窗口,左画 PNG、右画 JPEG(等比缩放居中); +3. 失败时对应面板显示品红/深蓝棋盘,串口打印 `FAILED stage=N`。 + +`build.py` 会: + +- 编译 `Apps/ImageSee` 到 `disk/apps/imagesee.elf` +- 把 `Assets/images/*.png|jpg` 复制到 `disk/images/` +- 内核 `main.vp` 在 Explorer/FontDemo 之后自动 spawn ImageSee(pid=7) + +重建示例图片: + +```powershell +python Scripts/gen_imagesee_assets.py # 需要 Pillow,仅生成资源时使用 +``` + +## 6. 已验证 + +2026-10-28,QEMU 512MB,无头启动: + +| 文件 | 类型 | 结果 | +|---|---|---| +| sample.png | RGBA,非隔行 | 解码像素/校验和与 PIL 完全一致 | +| pal.png | 调色板 + tRNS | 完全一致 | +| inter.png | RGBA,Adam7,alpha 渐变 | 完全一致 | +| photo.jpg | baseline 4:2:0 | 尺寸/采样点与 PIL 在 ±11 内(IDCT/上采样差异) | +| gray.jpg | 灰度 baseline | 采样点完全一致 | + +## 7. Explorer 桌面壁纸 + +Explorer 启动时调用 `imagesee.load("/sys/images/screen/background.jpg")`: + +- 成功:按“cover”等比裁切铺满 framebuffer,任务栏/窗口叠加在壁纸上 +- 失败(文件不存在/解码失败):重试一次;仍失败则保持原来的深色纯色背景 +- 构建:`build.py` 把 `Assets/background.jpg` 复制到 + `disk/sys/images/screen/background.jpg` +- 重新生成壁纸:`python Scripts/gen_imagesee_assets.py`(960×540 baseline JPEG) + +## 8. 内存 + +解码使用 `stdlib.malloc`(mmap 后端,`free` 为空操作)。一次 `load` 会保留: + +- 压缩文件字节 +- DEFLATE 解压后的扫描线缓冲(PNG) +- 最终 `w*h*4` 像素 +- JPEG 的量化系数与分量平面 + +因此适合中小图片(示例 320×220)。超大图片(>4096²)不建议在当前堆上尝试。 diff --git a/docs/syscall.md b/docs/syscall.md new file mode 100644 index 0000000..85c783f --- /dev/null +++ b/docs/syscall.md @@ -0,0 +1,382 @@ +# ViperOS syscall 与 Linux x86-64 ABI 对齐计划 + +状态:**P0–P6 完成**(含明确受限项;内核/应用构建通过,`fork`、`wait4`、信号投递已在 QEMU 冒烟验证) +最后更新:2026-10-28 + +目标:让 ViperOS 的 syscall 面逐步收敛到 Linux x86-64 ABI,最终能够**直接运行静态/动态链接的 glibc 程序**。 + +非目标(至少本计划内不做): + +- 网络协议栈相关 syscall(socket/connect/…) +- ptrace / seccomp / cgroup / namespace +- 完整 VFS(符号链接、硬链接、inode 语义) +- 与 Linux 内核模块、io_uring、epoll 的兼容 + +--- + +## 1. 现状 + +### 1.1 入口与调用约定 + +| 项目 | 现状 | +|---|---| +| 指令 | `syscall`(MSR `STAR`/`LSTAR`/`FMASK`),另注册了 DPL=3 的 `int 0x80` 遗留门 | +| 调用号 | `rax` | +| 参数 | `rdi`, `rsi`, `rdx`, `r10`, `r8`, `r9`(与 Linux 一致) | +| 返回 | `rax`;目前正负混用 | +| 错误 | 无 `errno`、无 TLS;部分返回 `-1`,部分返回 FAT32 `FRESULT` **正数**,未知调用号返回 `0` | +| 表 | `VKernel/Kernel/intr/syscall.vp` 与 `vpsdk/syscall.vp` 两份手工镜像 | + +### 1.2 已实现的调用 + +权威列表见 `Scripts/syscall_table.py`(`build.py` 构建前自动同步内核与 SDK)。 +截至 P6,Linux 号段已覆盖: + +```text +文件 open/openat/close/read/write/pread/pwrite/lseek/stat/fstat/newfstatat/ + getdents64/dup/dup2/dup3/fcntl/mkdir/unlink/rename/rmdir/getcwd/chdir/ + access/faccessat/readlink/readlinkat +内存 brk/mmap/munmap/mprotect/mremap/madvise +进程 getpid/getppid/gettid/exit/exit_group/wait4/clone/fork/execve +线程 arch_prctl(FS)/set_tid_address/set_robust_list/futex/sched_yield +时间 clock_gettime/clock_getres/gettimeofday/nanosleep +信号 rt_sigaction/rt_sigprocmask/rt_sigreturn/kill +IPC pipe/pipe2/readv/writev/poll(超时桩) +系统 uname/getuid/geteuid/getgid/getegid/prlimit64/getrandom/reboot +``` + +私有段仅剩兼容别名(`LOAD_SO`/`SO_CALL1`/`SPAWN_PROC`/`THREAD_CREATE_OLD`/ +`REBOOT`/`SHUTDOWN`)。 + +### 1.3 已知偏差 + +- `execve(59)` 通过“加载新进程 + 退出调用者”模拟,PID 会改变 +- `fork(57)` / `clone(56)` 会复制/共享地址空间;`CLONE_VM` 无 `CLONE_THREAD` + 的 vfork 场景按同进程线程近似 +- 信号只在 **syscall 返回** 时投递(没有中断返回钩子);`ucontext_t` 为自定义 + 紧凑布局,`siglongjmp` 不可用 +- `futex` 为“查值 + yield”轮询;`poll` 恒返回 0(仅超时语义) +- `exit_group(231)` 终止进程内所有线程;进程退出不回收管道/文件引用计数 +- fork/exec 继承的非管道 fd 标记为两侧共享:close 只清槽位不释放对象(避免 + 交叉释放;代价是泄漏) +- 资源上限:8 进程 / 每进程 8 线程 / 16 fd / 8 目录句柄 / 64 VMA / 8 管道 +- P1 已补:`openat` / `fstat` / `newfstatat` / `getdents64` / `dup` / `fcntl`, + 私有目录调用已删除 +- P2 已补:`brk` / `mprotect` / `munmap` / `mremap` / `madvise`,文件映射 +- P3–P6 已补:启动栈/auxv、`PT_INTERP`/ET_DYN、`execve`、`wait4`、Linux + `clone`/`fork`、TLS、时间、管道、信号投递、`reboot(169)` + +### 1.4 维护风险 + +两份 `enum Syscall` 靠人工同步。P0 起改为**生成**: + +```text +Scripts/syscall_table.py ← 唯一权威表 + │ + ├─ VKernel/Kernel/intr/syscall.vp [BEGIN/END GENERATED] + └─ vpsdk/syscall.vp [BEGIN/END GENERATED] +``` + +`build.py` 在编译前运行生成器;两边不一致会直接覆盖,不再依赖记忆。 + +--- + +## 2. 目标 ABI 契约 + +### 2.1 调用与返回 + +- `rax` = 调用号,参数 `rdi/rsi/rdx/r10/r8/r9` +- 成功:返回非负值(字节数、fd、地址、0…) +- 失败:返回 `-errno`(`-ENOENT`、`-EINVAL`…),**不再返回 `FRESULT` 正数** +- 未实现:`-ENOSYS` +- 指针参数越界/为空:`-EFAULT` +- 用户态 `errno` 由 glibc 从返回值设置,内核不维护 `errno` + +### 2.2 数据结构 + +内核新增 `abi/linux.vp`(内核工程 include 增加仓库根目录),字段布局按 x86-64 Linux; +SDK 侧需要结构体时可直接 `import abi.linux`。两者共用同一份定义,避免布局漂移。 + +| 类型 | 大小 | 用途 | +|---|---:|---| +| `abi.linux_stat` | 144 | `stat/fstat/newfstatat` | +| `abi.linux_dirent64` | 19+name | `getdents64` | +| `abi.linux_timespec` | 16 | 时间 | +| `abi.linux_utsname` | 390 | `uname` | +| `abi.linux_rlimit` | 16 | `prlimit64` | +| `abi.iovec` | 16 | `readv/writev` | + +### 2.3 启动栈(P3) + +glibc 不读 `_start(argc, argv)`,它要求内核按 Linux 方式布置用户栈: + +```text +[argc][argv0..argvN][NULL][envp...][NULL][auxv pairs][AT_NULL] +``` + +必须提供的 auxv:`AT_PHDR`、`AT_PHENT`、`AT_PHNUM`、`AT_ENTRY`、`AT_BASE`、 +`AT_PAGESZ`、`AT_RANDOM`、`AT_UID/EUID/GID/EGID`、`AT_SECURE=0`、 +`AT_EXECFN`、`AT_SYSINFO_EHDR`(可选,无 vDSO 时省略)。 + +动态链接:`PT_INTERP` 指向的解释器按 ET_DYN 基址(`0x70000000`)加载, +主程序 PIE 基址 `0x60000000`,`AT_BASE` 指向解释器,`AT_PHDR/AT_ENTRY` +仍指向主程序。系统镜像中需要实际存在该解释器与 libc。 + +### 2.4 TLS 与线程(P4) + +- `arch_prctl(ARCH_SET_FS)`:设置每线程 `IA32_FS_BASE` +- 线程切换时保存/恢复 `FS_BASE` +- `set_tid_address`、`set_robust_list`、`rseq`(可为 `-ENOSYS`) +- futex 至少覆盖 `FUTEX_WAIT/FUTEX_WAKE`(glibc pthread 必需) + +### 2.5 兼容口径 + +- `uname().sysname` 报 `"Linux"`,`release` 报一个 Linux 风格版本号;真实产品名放在 `version` 字段。这样大量脚本/构建系统不会因 `sysname != Linux` 走特殊分支。 +- 文件权限位使用固定值:普通文件 `0644`、目录 `0755`;FAT32 没有 uid/gid。 + +--- + +## 3. 阶段计划 + +### P0 — ABI 契约与基础设施(本次完成,待编译验证) + +- [x] `abi/linux.vp`:errno、`O_*`、`AT_*`、`DT_*`、`PROT_*`、`MAP_*`、`ARCH_*`、结构体 +- [x] 统一错误模型:Linux 号段调用返回 `-errno`;未知调用 `-ENOSYS` + (私有 0x1000+ 段在 P6 删除前保留旧返回值) +- [x] `open` 采用 Linux `O_*`,内核翻译到 FAT32 `FA_*` +- [x] `lseek` 返回新偏移(而不是状态码),支持 64 位 offset(FAT32 上限内) +- [x] 新调用:`uname` `getuid/geteuid/getgid/getegid` `getppid` `gettid` + `getcwd` `chdir` `access` `faccessat` `readlink` `readlinkat` + `set_tid_address` `set_robust_list` `rseq` `prlimit64` `getrandom` +- [x] `Process.cwd`(默认 `/`) +- [x] syscall 表由 `Scripts/syscall_table.py` 生成,`build.py` 自动同步 +- [x] `vpsdk` 增加对应包装与常量 + +验收:现有应用行为不回退;所有失败路径可观察到负 errno;`Scripts/syscall_table.py --check` 通过。 + +### P1 — 文件系统 ABI(本次完成,待编译验证) + +- [x] `openat(257)`(`AT_FDCWD` + 绝对路径 + cwd 相对路径) +- [x] `fstat(5)`、`newfstatat(262)`、`statx(332)`(返回 `-ENOSYS`,glibc 回退) +- [x] `getdents64(217)` + 目录 fd(`O_DIRECTORY`) +- [x] `dup(32)` / `dup2(33)` / `dup3(292)`、`fcntl(72)`(`F_GETFL/F_SETFL/F_DUPFD`) +- [x] `pread64(17)` / `pwrite64(18)` +- [x] `rename(82)`、`rmdir(84)`、`unlinkat(263)` +- [x] 删除私有 `OPENDIR/READDIR/CLOSEDIR` +- [x] SDK 的 `opendir/readdir/closedir` 改由 `openat + getdents64 + close` 实现, + Terminal 等调用方无需改动 + +验收:`ls` 走 `openat + getdents64`;SDK 的 `open` 也统一经 `openat(AT_FDCWD)`; +`ld.so` 的 `openat/read/fstat/getdents64` 路径可用(文件映射属于 P2)。 + +P1 已知限制: + +- `getdents64` 每次只返回一条记录(glibc 合法,只是多一次 syscall) +- `d_ino` 固定 1、`d_off` 固定 0;`telldir/seekdir/rewinddir` 暂不保证 +- `dirfd != AT_FDCWD` 的相对路径查找返回 `-ENOTSUP`(P3/P6 再补目录 fd 反查) +- `statx` 返回 `-ENOSYS`,glibc 回退到 `newfstatat` +- `rename` 只支持同一 FAT32 卷内;`dup` 共享文件偏移,最后一次 `close` 才释放对象 + +### P2 — 内存管理(本次完成,待编译验证) + +- [x] `brk(12)`:独立 brk 区(`0x40000000` 起),按页增长/收缩 +- [x] `mmap` 真语义:`MAP_FIXED`、`PROT_*`、失败返回 `MAP_FAILED`(-1) +- [x] FAT32 文件映射:把文件内容复制进新页(`MAP_PRIVATE` 语义) +- [x] `munmap(11)` 真正解映射并把物理页归还 buddy +- [x] `mprotect(10)` 修改 PTE 权限(支持 VMA 拆分) +- [x] `mremap(25)`(`MREMAP_MAYMOVE` / `MREMAP_FIXED`)、`madvise(28)` +- [x] 每进程 VMA 表(64 项),记录 base/size/prot/kind/flags +- [x] SDK 的 `mmap` 把 `MAP_FAILED` 归一化为 0,既有 Explorer/devfs 调用方无需修改 + +验收:glibc 的 `malloc` 能使用 brk 主 arena;共享库文件可 mmap 后按段 mprotect; +Explorer 的 framebuffer 映射不回退。 + +P2 已知限制: + +- 文件映射是“读入页”,不支持 `MAP_SHARED` 回写/`msync`(ld.so 用 `MAP_PRIVATE`) +- `PROT_NONE` 仍映射为 present(可读),没有 NX;`PROT_EXEC` 仅记录 +- `munmap` 只归还数据页,页表页仍留在 paging 池 +- `brk` 区不在 VMA 表里(glibc 不会对它 munmap) +- `mremap` 增长必须带 `MREMAP_MAYMOVE`,通过“新建+拷贝+解映射”实现 + +### P3 — 进程、启动栈、解释器(本次完成) + +- [x] 用户栈按 Linux 布局写入 `argc/argv/envp/auxv` + (AT_PHDR/PHENT/PHNUM/PAGESZ/ENTRY/BASE/RANDOM/EXECFN/UID/EUID/GID/EGID 等) +- [x] `execve(59)` 原型 `(path, argv, envp)`;SDK 保留 `spawn` 走私有 `SPAWN_PROC` +- [x] `exit_group(231)` 终止进程内所有线程;`exit(60)` 只退当前线程 +- [x] `wait4(61)`(`WNOHANG`、状态字、子进程回收)、`getppid` +- [x] `clone(56)`:`(flags, stack, ptid, ctid, tls)`;CLONE_VM 建线程, + 不带 CLONE_VM 时复制地址空间 +- [x] `fork(57)`:复制用户页 + 启动在父进程 syscall 返回点(RAX=0) +- [x] `PT_INTERP` + ET_DYN 基址加载;`AT_BASE` 指向解释器 +- [x] 旧的 ViperOS `thread_create(func,arg)` 移到私有 `THREAD_CREATE_OLD` + +验收:静态非 PIE、动态 PIE(安装 `ld.so` 与 libc)都可从正确的启动栈进入; +fork 子进程在父进程同一返回点继续执行,父进程得到子 pid。 + +### P4 — TLS 与线程(本次完成) + +- [x] `arch_prctl(158)`:`SET_FS/GET_FS`(`SET_GS/GET_GS` 返回 `-ENOSYS`) +- [x] 线程切换保存/恢复 `IA32_FS_BASE`(独立表,不改 `Thread` 的 600B 布局) +- [x] `rseq(334)` 返回 `-ENOSYS`;`set_robust_list` 记录 +- [x] `futex(202)`:`WAIT/WAKE` 采用“检查值 + sched_yield”轮询实现 +- [x] `clone(CLONE_VM|CLONE_SETTLS)`:子线程从 post-syscall RIP 开始,FS=tls +- [x] `CLONE_CHILD_CLEARTID`:线程退出时写 0,配合 futex 唤醒 join + +验收:`pthread_create/join` 走 Linux clone 路径;TLS 随线程切换。 + +### P5 — 时间、信号、IPC(本次完成) + +- [x] `clock_gettime(228)` / `clock_getres(229)`(RTC 秒 + 1ms tick) +- [x] `gettimeofday(96)`、`nanosleep(35)` +- [x] `readv(19)` / `writev(20)` +- [x] `pipe(22)` / `pipe2(293)`(8 个管道 × 4KB,轮询读写) +- [x] `poll(7)`(当前固定返回 0 = 超时) +- [x] `rt_sigaction(13)` / `rt_sigprocmask(14)` / `kill(62)`:完整登记 +- [x] 信号投递:syscall 返回前重写用户上下文,进入 handler; + `rt_sigreturn(15)` 恢复原上下文与 syscall 返回值 +- [ ] `ppoll` / `select` / `epoll` + +验收:`signal()`/`sigaction()` 注册的 handler 能在下一次 syscall 返回时执行; +`raise()`/`kill(getpid(), sig)` 可触发。 + +### P6 — 清理私有段(部分完成) + +- [x] `reboot(169)`:校验 Linux magic,支持 RESTART / POWER_OFF +- [x] SDK 的 `process.reboot/shutdown` 改用 `reboot(169)` +- [~] 私有 `LOAD_SO/SO_CALL1` 保留(内核启动仍加载 `slog.so`,用户态链接器未实现) +- [~] 私有 `SPAWN_PROC` / `THREAD_CREATE_OLD` 保留(Terminal 与旧 SDK) +- [~] 私有 `REBOOT/SHUTDOWN` 保留作为兼容别名 +- [ ] 用户态动态链接器 + 0x1000+ 段清空 + +验收:Linux `reboot` 可用;私有段尚未清零(受用户态加载器缺失限制)。 + +--- + +## 4. 调用迁移表 + +`P0` = 本次;`—` 表示尚未实现(内核统一返回 `-ENOSYS`)。 + +| 号 | Linux | ViperOS | 状态 | 备注 | +|---:|---|---|---|---| +| 0 | read | READ | P0 | 字节数 / `-errno` | +| 1 | write | WRITE | P0 | 同上 | +| 2 | open | OPEN | P0 | Linux `O_*`,内核翻译 `FA_*` | +| 3 | close | CLOSE | P0 | 0 / `-EBADF` | +| 4 | stat | STAT | P0 | 保留私有 `fileinfo`(兼容旧 SDK) | +| 5 | fstat | FSTAT | P1 | `struct stat` | +| 7 | poll | POLL | P5 | 固定返回 0(超时) | +| 8 | lseek | SEEK | P0 | 返回新偏移 | +| 9 | mmap | MMAP | P2 | 匿名/文件/设备三种;失败 `MAP_FAILED` | +| 10 | mprotect | MPROTECT | P2 | | +| 11 | munmap | MUNMAP | P2 | | +| 12 | brk | BRK | P2 | | +| 13 | rt_sigaction | RT_SIGACTION | P5 | 登记 + 投递 | +| 14 | rt_sigprocmask | RT_SIGPROCMASK | P5 | 支持 block/unblock/setmask | +| 15 | rt_sigreturn | RT_SIGRETURN | P5 | 恢复原上下文 | +| 16 | ioctl | IOCTL | P0 | devfs 子集 | +| 17 | pread64 | PREAD64 | P1 | | +| 18 | pwrite64 | PWRITE64 | P1 | | +| 19 | readv | READV | P5 | | +| 20 | writev | WRITEV | P5 | | +| 21 | access | ACCESS | P0 | | +| 22 | pipe | PIPE | P5 | 8 × 4KB,轮询 | +| 24 | sched_yield | YIELD | P0 | | +| 25 | mremap | MREMAP | P2 | 增长需 `MREMAP_MAYMOVE` | +| 28 | madvise | MADVISE | P2 | 空操作 | +| 32 | dup | DUP | P1 | 末次 close 才释放对象 | +| 33 | dup2 | DUP2 | P1 | | +| 35 | nanosleep | NANOSLEEP | P5 | 1ms 精度 | +| 39 | getpid | GETPID | P0 | | +| 56 | clone | CLONE | P3 | Linux clone;CLONE_VM 建线程 | +| 57 | fork | FORK | P3 | 复制用户地址空间 | +| 59 | execve | SPAWN | P3 | 新进程 + 退出调用者 | +| 60 | exit | THREAD_EXIT | P0 | | +| 61 | wait4 | WAIT4 | P3 | 支持 WNOHANG | +| 62 | kill | KILL | P5 | 待下次 syscall 投递 | +| 63 | uname | UNAME | P0 | | +| 72 | fcntl | FCNTL | P1 | | +| 79 | getcwd | GETCWD | P0 | | +| 80 | chdir | CHDIR | P0 | | +| 82 | rename | RENAME | P1 | | +| 83 | mkdir | MKDIR | P0 | | +| 84 | rmdir | RMDIR | P1 | | +| 87 | unlink | REMOVE | P0 | | +| 89 | readlink | READLINK | P0 | 当前恒 `-EINVAL` | +| 96 | gettimeofday | GETTIMEOFDAY | P5 | RTC 秒 | +| 102 | getuid | GETUID | P0 | 恒 0 | +| 104 | getgid | GETGID | P0 | 恒 0 | +| 107 | geteuid | GETEUID | P0 | 恒 0 | +| 108 | getegid | GETEGID | P0 | 恒 0 | +| 110 | getppid | GETPPID | P0 | 恒 0 | +| 158 | arch_prctl | ARCH_PRCTL | P4 | SET_FS/GET_FS | +| 169 | reboot | REBOOT_LINUX | P6 | magic 校验;RESTART/POWER_OFF | +| 186 | gettid | GETTID | P0 | 返回线程 tid | +| 202 | futex | FUTEX | P4 | 轮询 WAIT/WAKE | +| 217 | getdents64 | GETDENTS64 | P1 | 每次返回一条记录 | +| 218 | set_tid_address | SET_TID_ADDRESS | P0 | | +| 228 | clock_gettime | CLOCK_GETTIME | P5 | RTC/1ms tick | +| 229 | clock_getres | CLOCK_GETRES | P5 | 1ms | +| 231 | exit_group | EXIT | P3 | 终止进程内所有线程 | +| 257 | openat | OPENAT | P1 | | +| 262 | newfstatat | NEWFSTATAT | P1 | | +| 263 | unlinkat | UNLINKAT | P1 | | +| 267 | readlinkat | READLINKAT | P0 | 当前恒 `-EINVAL` | +| 269 | faccessat | FACCESSAT | P0 | | +| 273 | set_robust_list | SET_ROBUST_LIST | P0 | 仅记录 | +| 292 | dup3 | DUP3 | P1 | flags 忽略 | +| 293 | pipe2 | PIPE2 | P5 | flags 仅记录 | +| 302 | prlimit64 | PRLIMIT64 | P0 | 只读限制 | +| 318 | getrandom | GETRANDOM | P0 | 软件 DRNG | +| 332 | statx | — | P1 | `-ENOSYS`,glibc 回退 | +| 334 | rseq | RSEQ | P0 | `-ENOSYS` | +| 0x1006 | — | LOAD_SO | P6 删除 | | +| 0x1007 | — | SO_CALL1 | P6 删除 | | +| 0x1008 | — | REBOOT | P6 保留别名 | | +| 0x1009 | — | SHUTDOWN | P6 保留别名 | | +| 0x100A | — | SPAWN_PROC | P6 保留 | Terminal `run` 用 | +| 0x100B | — | THREAD_CREATE_OLD | P6 保留 | 旧 `thread_create(func,arg)` | + +--- + +## 5. 测试与验收 + +1. **ABI 单元测试**(用户态小程序) + - 每个调用至少覆盖成功 + 一个错误路径,断言返回值为 `-errno` + - 结构体偏移/大小与 Linux 对照(编译期 `__sizeof__`) +2. **`Scripts/syscall_table.py --check`** + - CI/本地构建前检查两份表是否与权威表一致 +3. **glibc 里程碑** + - M1:静态 `hello`(write/exit/brk/mmap/arch_prctl) + - M2:动态 `hello`(`ld.so`、`openat`、`mmap`、`mprotect`) + - M3:`pthread` + `malloc` 压力测试 + - M4:`ls`/`cat`/`sh` 等 coreutils +4. **回归** + - 现有 7 个应用(Terminal / Explorer / Scene3D / Gargantua / FontDemo / HelloWorld / ImageSee)必须继续运行 + +### 5.1 已完成的冒烟验证(2026-10-28,QEMU) + +- 7 个内置应用全部启动,进入主循环,无异常 +- ImageSee 在同一启动中解码 PNG/JPEG 并在窗口显示(见 `docs/imagesee.md`) +- 临时测试程序调用 `fork()`:父进程得到子 pid,子进程在同一返回点继续运行 + (`getpid/getppid` 正确),`wait4` 返回子 pid 且状态为 `0x700`(exit code 7) +- 信号:`rt_sigaction(SIGUSR1)` + `kill(self)` → handler 在 `kill` 返回时执行, + `kill` 仍返回 0;`SIGUSR2` 被 `rt_sigprocmask` 阻塞时不投递,解除阻塞后的下一次 + syscall 返回时投递 +- 无人值守运行:`python build.py --headless --runsecs=45`(`-display none`, + 45 秒后自动结束 QEMU,串口仍写入 `serial.log`) + +--- + +## 6. 风险与已定决策 + +| 风险 | 处理 | +|---|---| +| 错误码改造影响旧应用 | SDK 已按 `<0` 判断;`FRESULT` 正数路径一并改掉,旧应用需回归测试 | +| 两份表再次漂移 | 生成器 + `build.py` 前缀同步;禁止手改生成段 | +| FAT32 无 inode/权限/符号链接 | `struct stat` 用固定值;`readlink` 一律 `-EINVAL`;`chmod` 后置 | +| `mprotect` 未实现时 RELRO 失效 | P2 前在文档标注为“安全降级”,不假装成功 | +| `getrandom` 无硬件熵 | 软件 xorshift + 启动期混合,仅用于 canary/非密码学场景 | +| `uname` 报 Linux | 提升兼容性;`version` 字段保留 ViperOS 标识 | +| 用户态栈布局改动大 | P3 单独提交,先静态后动态,避免与内存阶段交叉 | diff --git a/vpsdk/fs.vp b/vpsdk/fs.vp index 60836bf..adcc624 100644 --- a/vpsdk/fs.vp +++ b/vpsdk/fs.vp @@ -1,23 +1,31 @@ from . import syscall as syscall +import string -define O_RDONLY = 0x01 -define O_WRONLY = 0x02 -define O_RDWR = 0x03 -define O_CREAT = 0x04 -define O_TRUNC = 0x08 -define O_APPEND = 0x30 +# Linux x86-64 open flags. The kernel translates these to FatFs FA_* modes; +# never pass FatFs values from user space. +define O_RDONLY = 0 +define O_WRONLY = 1 +define O_RDWR = 2 +define O_CREAT = 64 +define O_EXCL = 128 +define O_TRUNC = 512 +define O_APPEND = 1024 +define O_DIRECTORY = 65536 +define O_CLOEXEC = 524288 define SEEK_SET = 0 define SEEK_CUR = 1 define SEEK_END = 2 define DT_UNKNOWN = 0 -define DT_FILE = 1 -define DT_DIR = 2 +define DT_REG = 8 +define DT_DIR = 4 +define DT_FILE = 8 # legacy alias def open(path: const str, flags: uint32) -> int: - return int(syscall._syscall3(uint64(syscall.Syscall.OPEN), uint64(path), uint64(flags), 0)) + """SDK open() goes through openat(AT_FDCWD) so relative paths honor cwd.""" + return syscall.openat(syscall.AT_FDCWD, path, flags, 0) def close(fd: int) -> int: return int(syscall._syscall1(uint64(syscall.Syscall.CLOSE), uint64(fd))) @@ -29,20 +37,19 @@ def write(fd: int, buf: rawptr, count: uint32) -> int: return int(syscall._syscall3(uint64(syscall.Syscall.WRITE), uint64(fd), uint64(buf), uint64(count))) def seek(fd: int, offset: uint32, whence: uint32 = 0) -> int: + """lseek: returns the new offset (>= 0) or -errno.""" return int(syscall._syscall3(uint64(syscall.Syscall.SEEK), uint64(fd), uint64(offset), uint64(whence))) -def tell(fd: int) -> uint32: - """当前位置 = lseek(fd, 0, SEEK_CUR)(TELL 私有 syscall 已删除)。""" - return uint32(syscall._syscall3(uint64(syscall.Syscall.SEEK), uint64(fd), 0, uint64(SEEK_CUR))) +def tell(fd: int) -> int: + """当前偏移 = lseek(fd, 0, SEEK_CUR)。""" + return int(syscall._syscall3(uint64(syscall.Syscall.SEEK), uint64(fd), 0, uint64(SEEK_CUR))) -def fsize(fd: int) -> uint32: - """文件大小:SIZE 私有 syscall 已删除,改用 lseek 往返(不改变最终位置)。 - - 三次 lseek:记住当前位置 → 跳到末尾拿到大小 → 复位。多两次 syscall,换掉一个私有号。 - """ - cur: uint32 = uint32(syscall._syscall3(uint64(syscall.Syscall.SEEK), uint64(fd), 0, uint64(SEEK_CUR))) - end: uint32 = uint32(syscall._syscall3(uint64(syscall.Syscall.SEEK), uint64(fd), 0, uint64(SEEK_END))) - syscall._syscall3(uint64(syscall.Syscall.SEEK), uint64(fd), uint64(cur), uint64(SEEK_SET)) +def fsize(fd: int) -> int: + """文件大小 = lseek(fd, 0, SEEK_END),随后恢复原位置。""" + cur: int = tell(fd) + end: int = int(syscall._syscall3(uint64(syscall.Syscall.SEEK), uint64(fd), 0, uint64(SEEK_END))) + if cur >= 0: + syscall._syscall3(uint64(syscall.Syscall.SEEK), uint64(fd), uint64(cur), uint64(SEEK_SET)) return end def mkdir(path: const str) -> int: @@ -52,13 +59,46 @@ def remove(path: const str) -> int: return int(syscall._syscall3(uint64(syscall.Syscall.REMOVE), uint64(path), 0, 0)) def opendir(path: const str) -> int: - return int(syscall._syscall3(uint64(syscall.Syscall.OPENDIR), uint64(path), 0, 0)) + """Linux opendir: openat(O_RDONLY|O_DIRECTORY), returns a fd.""" + return syscall.openat(syscall.AT_FDCWD, path, O_RDONLY | O_DIRECTORY, 0) -def readdir(dd: int, info: rawptr) -> int: - return int(syscall._syscall3(uint64(syscall.Syscall.READDIR), uint64(dd), uint64(info), 0)) +def readdir(fd: int, info: rawptr) -> int: + """Fill the legacy fileinfo layout from one getdents64 record. -def closedir(dd: int) -> int: - return int(syscall._syscall1(uint64(syscall.Syscall.CLOSEDIR), uint64(dd))) + Returns 0 with an entry, -ENOENT at end-of-directory, or another -errno. + """ + if info is None: return -22 # -EINVAL + buf: [512]char + n: int = syscall.getdents64(fd, &buf[0], uint64(512)) + if n < 0: return n + if n == 0: return -2 # -ENOENT: end of directory + string.memset(info, 0, 288) # sizeof(fileinfo) + dt: int = int(uint8(buf[18])) + attr: uint8 = 0x20 + if dt == DT_DIR: attr = 0x10 + ap: uint8* = uint8*(uint64(info) + 4) + ap[0] = attr + dst: char* = char*(uint64(info) + 16) + string.strcpy(dst, &buf[19]) + return 0 + +def closedir(fd: int) -> int: + return close(fd) + +def openat(dirfd: int, path: const str, flags: uint32, mode: uint32 = 0) -> int: + return syscall.openat(dirfd, path, flags, mode) + +def rename(old_path: const str, new_path: const str) -> int: + return syscall.rename(old_path, new_path) + +def rmdir(path: const str) -> int: + return syscall.rmdir(path) + +def unlinkat(dirfd: int, path: const str, flags: uint32 = 0) -> int: + return syscall.unlinkat(dirfd, path, flags) + +def fcntl(fd: int, cmd: uint32, arg: uint64 = 0) -> int: + return syscall.fcntl(fd, cmd, arg) def stat(path: const str, info: rawptr) -> int: return int(syscall._syscall3(uint64(syscall.Syscall.STAT), uint64(path), uint64(info), 0)) @@ -78,7 +118,9 @@ class FileIO: self._fd = fd self._flags = flags self._pos = 0 - self._size = fsize(fd) + sz: int = fsize(fd) + if sz < 0: sz = 0 + self._size = uint32(sz) self._closed = 0 def read(self, buf: rawptr, count: uint32) -> int: @@ -100,8 +142,8 @@ class FileIO: def seek(self, offset: uint32) -> int: if self._closed: return -1 res: int = seek(self._fd, offset) - if res == 0: - self._pos = offset + if res >= 0: + self._pos = uint32(res) return res def tell(self) -> uint32: diff --git a/vpsdk/imagesee/image.vp b/vpsdk/imagesee/image.vp new file mode 100644 index 0000000..e98bbea --- /dev/null +++ b/vpsdk/imagesee/image.vp @@ -0,0 +1,57 @@ +# ImageSee shared types. + +import vpsdk.stdlib as stdlib +from stdint import * + +define IMAGE_FMT_UNKNOWN = 0 +define IMAGE_FMT_PNG = 1 +define IMAGE_FMT_JPEG = 2 + +class Image: + width: int + height: int + format: int + # Packed 0xAARRGGBB, matching the window/FBCanvas pixel format. + pixels: uint32* + +def new_image(w: int, h: int, fmt: int) -> Image*: + if w <= 0 or h <= 0: return None + if w > 16384 or h > 16384: return None + img: Image* = Image*(stdlib.malloc(uint64(Image.__sizeof__()))) + if img is None: return None + px: uint32* = uint32*(stdlib.malloc(uint64(w * h * 4))) + if px is None: return None + img.width = w + img.height = h + img.format = fmt + img.pixels = px + return img + +def free_image(img: Image*): + # stdlib.free() is a no-op over the bump mmap heap; keep the API for callers. + if img is None: return + img.pixels = None + img.width = 0 + img.height = 0 + +def pixel_at(img: Image*, x: int, y: int) -> uint32: + if img is None: return uint32(0) + if x < 0 or y < 0 or x >= img.width or y >= img.height: return uint32(0) + return img.pixels[y * img.width + x] + +def red_of(c: uint32) -> int: + return int((c >> 16) & uint32(0xFF)) + +def green_of(c: uint32) -> int: + return int((c >> 8) & uint32(0xFF)) + +def blue_of(c: uint32) -> int: + return int(c & uint32(0xFF)) + +def alpha_of(c: uint32) -> int: + return int((c >> 24) & uint32(0xFF)) + +def make_pixel(r: int, g: int, b: int, a: int = 255) -> uint32: + v: uint32 = (uint32(a & 0xFF) << 24) | (uint32(r & 0xFF) << 16) + v = v | (uint32(g & 0xFF) << 8) | uint32(b & 0xFF) + return v diff --git a/vpsdk/imagesee/imagesee.vp b/vpsdk/imagesee/imagesee.vp new file mode 100644 index 0000000..d41c686 --- /dev/null +++ b/vpsdk/imagesee/imagesee.vp @@ -0,0 +1,67 @@ +# ImageSee: small PNG/JPEG decoder library for ViperOS user space. +# +# img = imagesee.load("/IMAGES/SAMPLE.PNG") +# if img is not None: +# ... img.pixels[y * img.width + x] is 0xAARRGGBB ... +# +# `load` reads the whole file into memory and sniffs the container format; +# `decode_memory` is available when the caller already has the bytes. + +import vpsdk.imagesee.image as image +import vpsdk.imagesee.png as png +import vpsdk.imagesee.jpeg as jpeg +import vpsdk.fs as fs +import vpsdk.stdlib as stdlib +from stdint import * + +define IMAGESEE_MAX_FILE = 64 * 1024 * 1024 + +def decode_memory(data: uint8*, n: uint64) -> image.Image*: + if data is None or n < 8: return None + if int(data[0]) == 0x89 and int(data[1]) == 0x50: + if int(data[2]) == 0x4E and int(data[3]) == 0x47: + return png.decode(data, n) + if int(data[0]) == 0xFF and int(data[1]) == 0xD8: + return jpeg.decode(data, n) + return None + +def load(path: const str) -> image.Image*: + fd: int = fs.open(path, fs.O_RDONLY) + if fd < 0: return None + sz: int = fs.fsize(fd) + if sz <= 0 or uint64(sz) > uint64(IMAGESEE_MAX_FILE): + fs.close(fd) + return None + buf: uint8* = uint8*(stdlib.malloc(uint64(sz))) + if buf is None: + fs.close(fd) + return None + got: uint64 = 0 + while got < uint64(sz): + n: int = fs.read(fd, rawptr(uint64(buf) + got), uint32(uint64(sz) - got)) + if n <= 0: break + got = got + uint64(n) + fs.close(fd) + if got != uint64(sz): return None + return decode_memory(buf, uint64(sz)) + +def last_error() -> int: + """Last JPEG decoder failure stage (0 = not started; 7 = render done).""" + return jpeg.last_error() + +def format_name(img: image.Image*) -> const str: + if img is None: return "none" + if img.format == image.IMAGE_FMT_PNG: return "png" + if img.format == image.IMAGE_FMT_JPEG: return "jpeg" + return "unknown" + +# 32-bit FNV-1a over packed pixels; handy for regression checks. +def checksum(img: image.Image*) -> uint32: + if img is None: return uint32(0) + h: uint32 = uint32(2166136261) + n: int = img.width * img.height + i: int = 0 + while i < n: + h = (h ^ img.pixels[i]) * uint32(16777619) + i = i + 1 + return h diff --git a/vpsdk/imagesee/inflate.vp b/vpsdk/imagesee/inflate.vp new file mode 100644 index 0000000..d478d50 --- /dev/null +++ b/vpsdk/imagesee/inflate.vp @@ -0,0 +1,261 @@ +# DEFLATE / zlib decompression (RFC 1950 + RFC 1951). +# +# Written for PNG's IDAT stream: self-contained, no allocation inside the +# decoder (the caller provides the output buffer), canonical Huffman tables +# built the same way as the Brotli decoder in this SDK. + +define INF_MAXBITS = 15 +define INF_MAXCODES = 288 +define INF_MAXDIST = 32 + +class InfHuff: + count: [INF_MAXBITS + 1]int + symbol: [INF_MAXCODES]int + +# Length codes 257..285 and distance codes 0..29. +_lbase: [29]int = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258] +_lextra: [29]int = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0] +_dbase: [30]int = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577] +_dextra: [30]int = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13] +_clorder: [19]int = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15] + +_inf_src: uint8* = None +_inf_len: uint64 = 0 +_inf_pos: uint64 = 0 +_inf_bitbuf: uint32 = 0 +_inf_bitcnt: int = 0 +_inf_out: uint8* = None +_inf_cap: uint64 = 0 +_inf_got: uint64 = 0 +_inf_err: int = 0 + +_inf_lens: [320]uint8 +_inf_lit: InfHuff +_inf_dist: InfHuff +_inf_clen: InfHuff + +def _inf_need(n: int) -> uint32: + global _inf_bitbuf, _inf_bitcnt, _inf_pos, _inf_err + while _inf_bitcnt < n: + if _inf_pos >= _inf_len: + _inf_err = 1 + return uint32(0) + _inf_bitbuf = _inf_bitbuf | (uint32(_inf_src[_inf_pos]) << _inf_bitcnt) + _inf_pos = _inf_pos + 1 + _inf_bitcnt = _inf_bitcnt + 8 + v: uint32 = _inf_bitbuf & ((uint32(1) << uint32(n)) - uint32(1)) + _inf_bitbuf = _inf_bitbuf >> uint32(n) + _inf_bitcnt = _inf_bitcnt - n + return v + +def _inf_decode(h: InfHuff*) -> int: + global _inf_err + code: int = 0 + first: int = 0 + index: int = 0 + ln: int = 1 + while ln <= INF_MAXBITS: + code = code | int(_inf_need(1)) + cnt: int = h.count[ln] + if code - first < cnt: + return h.symbol[index + (code - first)] + index = index + cnt + first = (first + cnt) << 1 + code = code << 1 + ln = ln + 1 + _inf_err = 1 + return -1 + +def _inf_build(h: InfHuff*, lens: uint8*, n: int) -> int: + i: int = 0 + while i <= INF_MAXBITS: + h.count[i] = 0 + i = i + 1 + i = 0 + while i < n: + ln8: uint8 = lens[i] + if ln8 != 0: + if int(ln8) > INF_MAXBITS: return -1 + h.count[int(ln8)] = h.count[int(ln8)] + 1 + i = i + 1 + offs: [INF_MAXBITS + 2]int + offs[1] = 0 + ln: int = 1 + while ln <= INF_MAXBITS: + offs[ln + 1] = offs[ln] + h.count[ln] + ln = ln + 1 + i = 0 + while i < n: + ln = int(lens[i]) + if ln != 0: + h.symbol[offs[ln]] = i + offs[ln] = offs[ln] + 1 + i = i + 1 + return 0 + +def _inf_put(b: int) -> int: + global _inf_got, _inf_err + if _inf_got >= _inf_cap: + _inf_err = 1 + return -1 + _inf_out[_inf_got] = uint8(b & 0xFF) + _inf_got = _inf_got + 1 + return 0 + +def _inf_stored() -> int: + global _inf_bitbuf, _inf_bitcnt, _inf_pos + # Drop the partial byte and read LEN/NLEN directly from the byte stream. + _inf_bitbuf = uint32(0) + _inf_bitcnt = 0 + if _inf_pos + 4 > _inf_len: return -1 + lenv: int = int(_inf_src[_inf_pos]) | (int(_inf_src[_inf_pos + 1]) << 8) + nlen: int = int(_inf_src[_inf_pos + 2]) | (int(_inf_src[_inf_pos + 3]) << 8) + _inf_pos = _inf_pos + 4 + if (lenv ^ 0xFFFF) != nlen: return -1 + if _inf_pos + uint64(lenv) > _inf_len: return -1 + i: int = 0 + while i < lenv: + if _inf_put(int(_inf_src[_inf_pos])) != 0: return -1 + _inf_pos = _inf_pos + 1 + i = i + 1 + return 0 + +def _inf_fixed() -> int: + i: int = 0 + while i < 144: + _inf_lens[i] = uint8(8) + i = i + 1 + while i < 256: + _inf_lens[i] = uint8(9) + i = i + 1 + while i < 280: + _inf_lens[i] = uint8(7) + i = i + 1 + while i < 288: + _inf_lens[i] = uint8(8) + i = i + 1 + if _inf_build(&_inf_lit, &_inf_lens[0], 288) != 0: return -1 + i = 0 + while i < 32: + _inf_lens[i] = uint8(5) + i = i + 1 + if _inf_build(&_inf_dist, &_inf_lens[0], 32) != 0: return -1 + return 0 + +def _inf_dynamic() -> int: + hlit: int = int(_inf_need(5)) + 257 + hdist: int = int(_inf_need(5)) + 1 + hclen: int = int(_inf_need(4)) + 4 + if hlit > 286 or hdist > 30: return -1 + i: int = 0 + while i < 320: + _inf_lens[i] = uint8(0) + i = i + 1 + i = 0 + while i < hclen: + _inf_lens[_clorder[i]] = uint8(_inf_need(3)) + i = i + 1 + if _inf_build(&_inf_clen, &_inf_lens[0], 19) != 0: return -1 + n: int = hlit + hdist + i = 0 + while i < n: + sym: int = _inf_decode(&_inf_clen) + if _inf_err != 0: return -1 + if sym < 16: + _inf_lens[i] = uint8(sym) + i = i + 1 + elif sym == 16: + if i == 0: return -1 + rep: int = 3 + int(_inf_need(2)) + prev: uint8 = _inf_lens[i - 1] + while rep > 0 and i < n: + _inf_lens[i] = prev + i = i + 1 + rep = rep - 1 + elif sym == 17: + rep: int = 3 + int(_inf_need(3)) + while rep > 0 and i < n: + _inf_lens[i] = uint8(0) + i = i + 1 + rep = rep - 1 + elif sym == 18: + rep: int = 11 + int(_inf_need(7)) + while rep > 0 and i < n: + _inf_lens[i] = uint8(0) + i = i + 1 + rep = rep - 1 + else: + return -1 + if _inf_build(&_inf_lit, &_inf_lens[0], hlit) != 0: return -1 + if _inf_build(&_inf_dist, &_inf_lens[hlit], hdist) != 0: return -1 + return 0 + +def _inf_codes() -> int: + while True: + sym: int = _inf_decode(&_inf_lit) + if _inf_err != 0: return -1 + if sym < 256: + if _inf_put(sym) != 0: return -1 + elif sym == 256: + return 0 + elif sym >= 257 and sym <= 285: + li: int = sym - 257 + length: int = _lbase[li] + int(_inf_need(_lextra[li])) + dsym: int = _inf_decode(&_inf_dist) + if _inf_err != 0: return -1 + if dsym < 0 or dsym >= 30: return -1 + dist: int = _dbase[dsym] + int(_inf_need(_dextra[dsym])) + if dist <= 0 or uint64(dist) > _inf_got: return -1 + i: int = 0 + while i < length: + src_i: uint64 = _inf_got - uint64(dist) + if _inf_put(int(_inf_out[src_i])) != 0: return -1 + i = i + 1 + else: + return -1 + +def _inf_blocks() -> int: + while True: + final: int = int(_inf_need(1)) + btype: int = int(_inf_need(2)) + if _inf_err != 0: return -1 + if btype == 0: + if _inf_stored() != 0: return -1 + elif btype == 1: + if _inf_fixed() != 0: return -1 + if _inf_codes() != 0: return -1 + elif btype == 2: + if _inf_dynamic() != 0: return -1 + if _inf_codes() != 0: return -1 + else: + return -1 + if final != 0: + return 0 + +# Inflate a zlib stream (skips the 2-byte header). Returns bytes written +# into `dst`, or -1 on malformed input / output overflow. +def zlib_decompress(src: uint8*, src_len: uint64, dst: uint8*, dst_cap: uint64) -> int: + global _inf_src, _inf_len, _inf_pos, _inf_bitbuf, _inf_bitcnt + global _inf_out, _inf_cap, _inf_got, _inf_err + if src is None or dst is None or src_len < 6: return -1 + cmf: int = int(src[0]) + flg: int = int(src[1]) + if (cmf & 0x0F) != 8: return -1 + if (cmf * 256 + flg) % 31 != 0: return -1 + if (flg & 0x20) != 0: return -1 # FDICT unsupported + _inf_src = src + _inf_len = src_len + _inf_pos = 2 + _inf_bitbuf = uint32(0) + _inf_bitcnt = 0 + _inf_out = dst + _inf_cap = dst_cap + _inf_got = 0 + _inf_err = 0 + if _inf_blocks() != 0: return -1 + return int(_inf_got) diff --git a/vpsdk/imagesee/jpeg.vp b/vpsdk/imagesee/jpeg.vp new file mode 100644 index 0000000..0ec7469 --- /dev/null +++ b/vpsdk/imagesee/jpeg.vp @@ -0,0 +1,781 @@ +# JPEG decoder: baseline/extended sequential (SOF0/SOF1) and progressive +# (SOF2), Huffman entropy coding, restart markers, 4:4:4 / 4:2:2 / 4:2:0 +# chroma subsampling, 8-bit samples. Output is packed 0xAARRGGBB. +# +# Coefficient arrays are kept in natural (de-zigzagged) order so the IDCT is +# a straight 8x8 pass. Progressive scans refine the same arrays. + +import vpsdk.imagesee.image as im +import vpsdk.stdlib as stdlib +import string +import math +from stdint import * + +define J_MAX_COMPS = 4 + +class JHuff: + count: [17]int + symbol: [256]int + +class JComp: + id: int + hs: int + vs: int + tq: int + td: int + ta: int + bw: int + bh: int + coef: int16* + plane: uint8* + +_j_src: uint8* = None +_j_len: uint64 = 0 +_j_pos: uint64 = 0 +_j_bitbuf: uint32 = 0 +_j_bitcnt: int = 0 +_j_marker: int = -1 +_j_err: int = 0 + +_j_w: int = 0 +_j_h: int = 0 +_j_ncomp: int = 0 +_j_maxh: int = 1 +_j_maxv: int = 1 +_j_mcu_cols: int = 0 +_j_mcu_rows: int = 0 +_j_progressive: int = 0 +_j_rgb_mode: int = 0 +_j_restart: int = 0 + +_j_scan_ns: int = 0 +_j_sc: [4]int +_j_sctd: [4]int +_j_scta: [4]int +_j_ss: int = 0 +_j_se: int = 0 +_j_ah: int = 0 +_j_al: int = 0 +_j_eobrun: int = 0 +_j_dcpred: [4]int + +_j_qt: [4 * 64]uint16 +_j_qt_used: [4]int +_j_dc: [4]JHuff +_j_ac: [4]JHuff +_j_comps: [4]JComp +_j_planes_ready: int = 0 +_j_stage: int = 0 +_j_scan_no: int = 0 +_j_cur_mcu: int = 0 + +_j_cs: [64]float +_j_cw: [8]float +_j_trig_ready: int = 0 +_j_g: [64]float + +_zigzag: [64]int = [ + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63 +] + +def _j_u8(o: uint64) -> int: + return int(_j_src[o]) + +def last_error() -> int: + return _j_stage + +def _j_u16(o: uint64) -> int: + return (int(_j_src[o]) << 8) | int(_j_src[o + 1]) + +# ---------------------------------------------------------------- bit reader +# Returns the next entropy byte with 0xFF00 unstuffing; on a marker it stores +# the marker id in _j_marker and returns -1. +def _j_byte() -> int: + global _j_pos, _j_marker + if _j_pos >= _j_len: return -1 + b: int = int(_j_src[_j_pos]) + _j_pos = _j_pos + 1 + if b == 0xFF: + while _j_pos < _j_len and int(_j_src[_j_pos]) == 0xFF: + _j_pos = _j_pos + 1 + if _j_pos >= _j_len: return -1 + m: int = int(_j_src[_j_pos]) + _j_pos = _j_pos + 1 + if m == 0x00: return 0xFF + _j_marker = m + return -1 + return b + +def _j_bits(n: int) -> uint32: + global _j_bitbuf, _j_bitcnt, _j_err + if n <= 0: return uint32(0) + while _j_bitcnt < n: + b: int = _j_byte() + if b < 0: + _j_err = 1 + return uint32(0) + _j_bitbuf = (_j_bitbuf << 8) | uint32(b) + _j_bitcnt = _j_bitcnt + 8 + _j_bitcnt = _j_bitcnt - n + v: uint32 = (_j_bitbuf >> uint32(_j_bitcnt)) & ((uint32(1) << uint32(n)) - uint32(1)) + return v + +def _j_get_bit() -> int: + return int(_j_bits(1)) + +def _j_receive(s: int) -> int: + if s == 0: return 0 + v: int = int(_j_bits(s)) + if v < (1 << (s - 1)): + v = v - ((1 << s) - 1) + return v + +def _j_huff_build(h: JHuff*, counts: uint8*, vals: uint8*, n: int) -> int: + total: int = 0 + i: int = 1 + while i <= 16: + c: int = int(counts[i - 1]) + h.count[i] = c + total = total + c + i = i + 1 + if total != n: return -1 + # canonical offsets + offs: [18]int + offs[1] = 0 + i = 1 + while i <= 16: + offs[i + 1] = offs[i] + h.count[i] + i = i + 1 + # `vals` is already ordered by code length (JPEG DHT order). + i = 0 + ln: int = 1 + vi: int = 0 + while ln <= 16: + c = h.count[ln] + k: int = 0 + while k < c: + h.symbol[offs[ln] + k] = int(vals[vi]) + vi = vi + 1 + k = k + 1 + ln = ln + 1 + return 0 + +def _j_hdecode(h: JHuff*) -> int: + global _j_err + code: int = 0 + first: int = 0 + index: int = 0 + ln: int = 1 + while ln <= 16: + code = code | _j_get_bit() + cnt: int = h.count[ln] + if code - first < cnt: + return h.symbol[index + (code - first)] + index = index + cnt + first = (first + cnt) << 1 + code = code << 1 + ln = ln + 1 + _j_err = 1 + return 0 + +# --------------------------------------------------------------- markers +def _j_next_marker() -> int: + global _j_pos, _j_marker + if _j_marker >= 0: + m: int = _j_marker + _j_marker = -1 + return m + while _j_pos < _j_len: + b: int = int(_j_src[_j_pos]) + _j_pos = _j_pos + 1 + if b != 0xFF: continue + while _j_pos < _j_len and int(_j_src[_j_pos]) == 0xFF: + _j_pos = _j_pos + 1 + if _j_pos >= _j_len: return -1 + m = int(_j_src[_j_pos]) + _j_pos = _j_pos + 1 + if m == 0x00: continue + return m + return -1 + +def _j_parse_dqt(p: uint64, pend: uint64) -> int: + while p < pend: + pq: int = int(_j_src[p]) >> 4 + tq: int = int(_j_src[p]) & 15 + p = p + 1 + if tq >= 4: return -1 + i: int = 0 + while i < 64: + if pq != 0: + if p + 1 >= pend: return -1 + v: int = (int(_j_src[p]) << 8) | int(_j_src[p + 1]) + p = p + 2 + else: + if p >= pend: return -1 + v = int(_j_src[p]) + p = p + 1 + _j_qt[tq * 64 + _zigzag[i]] = uint16(v) + i = i + 1 + _j_qt_used[tq] = 1 + return 0 + +def _j_parse_dht(p: uint64, pend: uint64) -> int: + while p < pend: + if p + 17 > pend: return -1 + tc: int = int(_j_src[p]) >> 4 + th: int = int(_j_src[p]) & 15 + p = p + 1 + if tc > 1 or th > 3: return -1 + counts: [16]uint8 + total: int = 0 + i: int = 0 + while i < 16: + counts[i] = _j_src[p + i] + total = total + int(counts[i]) + i = i + 1 + p = p + 16 + if total <= 0 or total > 256 or p + uint64(total) > pend: return -1 + vals: [256]uint8 + i = 0 + while i < total: + vals[i] = _j_src[p + i] + i = i + 1 + p = p + uint64(total) + if tc == 0: + if _j_huff_build(&_j_dc[th], &counts[0], &vals[0], total) != 0: return -1 + else: + if _j_huff_build(&_j_ac[th], &counts[0], &vals[0], total) != 0: return -1 + return 0 + +def _j_parse_sof(p: uint64, pend: uint64, prog: int) -> int: + global _j_w, _j_h, _j_ncomp, _j_progressive, _j_maxh, _j_maxv, _j_mcu_cols, _j_mcu_rows, _j_planes_ready + if p + 6 > pend: return -1 + prec: int = int(_j_src[p]) + h: int = _j_u16(p + 1) + w: int = _j_u16(p + 3) + nf: int = int(_j_src[p + 5]) + p = p + 6 + if prec != 8 or w <= 0 or h <= 0 or nf < 1 or nf > J_MAX_COMPS: return -1 + if w > 16384 or h > 16384: return -1 + if p + uint64(nf * 3) > pend: return -1 + _j_w = w + _j_h = h + _j_ncomp = nf + _j_progressive = prog + _j_maxh = 1 + _j_maxv = 1 + i: int = 0 + while i < nf: + cid: int = int(_j_src[p + i * 3]) + hv: int = int(_j_src[p + i * 3 + 1]) + tq: int = int(_j_src[p + i * 3 + 2]) + hs: int = hv >> 4 + vs: int = hv & 15 + if hs < 1 or hs > 4 or vs < 1 or vs > 4 or tq > 3: return -1 + _j_comps[i].id = cid + _j_comps[i].hs = hs + _j_comps[i].vs = vs + _j_comps[i].tq = tq + _j_comps[i].td = 0 + _j_comps[i].ta = 0 + if hs > _j_maxh: _j_maxh = hs + if vs > _j_maxv: _j_maxv = vs + i = i + 1 + _j_mcu_cols = (w + 8 * _j_maxh - 1) / (8 * _j_maxh) + _j_mcu_rows = (h + 8 * _j_maxv - 1) / (8 * _j_maxv) + _j_planes_ready = 0 + return 0 + +def _j_prepare() -> int: + global _j_planes_ready + if _j_planes_ready != 0: return 0 + i: int = 0 + while i < _j_ncomp: + comp: JComp* = &_j_comps[i] + comp.bw = _j_mcu_cols * comp.hs + comp.bh = _j_mcu_rows * comp.vs + if comp.bw <= 0 or comp.bh <= 0: return -1 + if _j_progressive != 0: + comp.coef = int16*(stdlib.malloc(uint64(comp.bw * comp.bh * 64 * 2))) + if comp.coef is None: return -1 + string.memset(rawptr(comp.coef), 0, uint64(comp.bw * comp.bh * 64 * 2)) + else: + comp.coef = int16*(stdlib.malloc(uint64(comp.bw * comp.bh * 64 * 2))) + if comp.coef is None: return -1 + string.memset(rawptr(comp.coef), 0, uint64(comp.bw * comp.bh * 64 * 2)) + i = i + 1 + _j_planes_ready = 1 + return 0 + +# ------------------------------------------------------------------- blocks +def _j_block_seq(c: int, blk: int): + comp: JComp* = &_j_comps[c] + base: int = blk * 64 + t: int = _j_hdecode(&_j_dc[comp.td]) + s: int = t & 15 + _j_dcpred[c] = _j_dcpred[c] + _j_receive(s) + comp.coef[base] = int16(_j_dcpred[c]) + k: int = 1 + while k < 64: + rs: int = _j_hdecode(&_j_ac[comp.ta]) + if _j_err != 0: break + s = rs & 15 + r: int = rs >> 4 + if s == 0: + if r == 15: + k = k + 16 + continue + break + k = k + r + if k > 63: break + v: int = _j_receive(s) + comp.coef[base + _zigzag[k]] = int16(v) + k = k + 1 + +def _j_block_dc_first(c: int, blk: int): + comp: JComp* = &_j_comps[c] + t: int = _j_hdecode(&_j_dc[comp.td]) + s: int = t & 15 + _j_dcpred[c] = _j_dcpred[c] + _j_receive(s) + comp.coef[blk * 64] = int16(_j_dcpred[c] << _j_al) + +def _j_block_dc_refine(c: int, blk: int): + comp: JComp* = &_j_comps[c] + if _j_get_bit() != 0: + comp.coef[blk * 64] = int16(int(comp.coef[blk * 64]) | (1 << _j_al)) + +def _j_block_ac_first(c: int, blk: int): + global _j_eobrun + if _j_eobrun > 0: + _j_eobrun = _j_eobrun - 1 + return + comp: JComp* = &_j_comps[c] + base: int = blk * 64 + k: int = _j_ss + while k <= _j_se: + rs: int = _j_hdecode(&_j_ac[comp.ta]) + if _j_err != 0: break + s: int = rs & 15 + r: int = rs >> 4 + if s == 0: + if r < 15: + _j_eobrun = (1 << r) + int(_j_bits(r)) - 1 + break + k = k + 16 + else: + k = k + r + if k > _j_se: break + v: int = _j_receive(s) + comp.coef[base + _zigzag[k]] = int16(v << _j_al) + k = k + 1 + +def _j_block_ac_refine(c: int, blk: int): + global _j_eobrun, _j_err + comp: JComp* = &_j_comps[c] + base: int = blk * 64 + p1: int = 1 << _j_al + m1: int = (0 - 1) << _j_al + k: int = _j_ss + if _j_eobrun == 0: + while k <= _j_se: + rs: int = _j_hdecode(&_j_ac[comp.ta]) + if _j_err != 0: break + s: int = rs & 15 + r: int = rs >> 4 + newv: int = 0 + zrl: int = 0 + if s == 0: + if r < 15: + _j_eobrun = (1 << r) + int(_j_bits(r)) - 1 + break + zrl = 1 + else: + if s != 1: + _j_err = 1 + break + if _j_get_bit() != 0: newv = p1 + else: newv = m1 + # Advance over r zero coefficients, refining non-zero ones. + while k <= _j_se: + idx: int = base + _zigzag[k] + cv: int = int(comp.coef[idx]) + if cv != 0: + if _j_get_bit() != 0 and (cv & p1) == 0: + if cv > 0: comp.coef[idx] = int16(cv + p1) + else: comp.coef[idx] = int16(cv + m1) + else: + if r == 0: break + r = r - 1 + k = k + 1 + if newv != 0 and k <= _j_se: + comp.coef[base + _zigzag[k]] = int16(newv) + k = k + 1 + elif zrl != 0 and k <= _j_se: + # ZRL skips 16 zero coefficients; the inner loop leaves us + # parked on the 16th one (r == 0), so step past it. + k = k + 1 + else: + while k <= _j_se: + idx: int = base + _zigzag[k] + cv: int = int(comp.coef[idx]) + if cv != 0: + if _j_get_bit() != 0 and (cv & p1) == 0: + if cv > 0: comp.coef[idx] = int16(cv + p1) + else: comp.coef[idx] = int16(cv + m1) + k = k + 1 + _j_eobrun = _j_eobrun - 1 + +# --------------------------------------------------------------------- scan +def _j_restart_scan() -> int: + global _j_bitbuf, _j_bitcnt, _j_err, _j_eobrun, _j_marker, _j_pos + _j_bitbuf = uint32(0) + _j_bitcnt = 0 + _j_err = 0 + _j_eobrun = 0 + i: int = 0 + while i < J_MAX_COMPS: + _j_dcpred[i] = 0 + i = i + 1 + if _j_marker >= 0: + m: int = _j_marker + _j_marker = -1 + if m >= 0xD0 and m <= 0xD7: return 0 + _j_marker = m + return -1 + while _j_pos < _j_len: + b: int = int(_j_src[_j_pos]) + _j_pos = _j_pos + 1 + if b != 0xFF: continue + while _j_pos < _j_len and int(_j_src[_j_pos]) == 0xFF: + _j_pos = _j_pos + 1 + if _j_pos >= _j_len: return -1 + m = int(_j_src[_j_pos]) + _j_pos = _j_pos + 1 + if m == 0x00: continue + if m >= 0xD0 and m <= 0xD7: return 0 + _j_marker = m + return -1 + return -1 + +def _j_decode_block(c: int, blk: int): + if _j_progressive == 0: + _j_block_seq(c, blk) + else: + if _j_ss == 0: + if _j_ah == 0: _j_block_dc_first(c, blk) + else: _j_block_dc_refine(c, blk) + else: + if _j_ah == 0: _j_block_ac_first(c, blk) + else: _j_block_ac_refine(c, blk) + +def _j_scan() -> int: + global _j_bitbuf, _j_bitcnt, _j_err, _j_eobrun, _j_cur_mcu, _j_stage + _j_bitbuf = uint32(0) + _j_bitcnt = 0 + _j_err = 0 + _j_eobrun = 0 + i: int = 0 + while i < J_MAX_COMPS: + _j_dcpred[i] = 0 + i = i + 1 + if _j_scan_ns < 1: return -1 + if _j_scan_ns > 1: + total: int = _j_mcu_cols * _j_mcu_rows + mcu: int = 0 + while mcu < total: + _j_cur_mcu = mcu + if _j_restart > 0 and mcu > 0 and (mcu % _j_restart) == 0: + if _j_restart_scan() != 0: return -1 + si: int = 0 + while si < _j_scan_ns: + c: int = _j_sc[si] + comp: JComp* = &_j_comps[c] + vy: int = 0 + while vy < comp.vs: + hx: int = 0 + while hx < comp.hs: + bx: int = (mcu % _j_mcu_cols) * comp.hs + hx + by: int = (mcu / _j_mcu_cols) * comp.vs + vy + _j_decode_block(c, by * comp.bw + bx) + if _j_err != 0: + _j_stage = 1000 + mcu + return -1 + hx = hx + 1 + vy = vy + 1 + si = si + 1 + mcu = mcu + 1 + else: + c = _j_sc[0] + comp = &_j_comps[c] + cw: int = (_j_w * comp.hs + _j_maxh - 1) / _j_maxh + ch: int = (_j_h * comp.vs + _j_maxv - 1) / _j_maxv + bwc: int = (cw + 7) / 8 + bhc: int = (ch + 7) / 8 + total = bwc * bhc + k: int = 0 + while k < total: + _j_cur_mcu = k + if _j_restart > 0 and k > 0 and (k % _j_restart) == 0: + if _j_restart_scan() != 0: return -1 + by: int = k / bwc + bx: int = k % bwc + _j_decode_block(c, by * comp.bw + bx) + if _j_err != 0: + _j_stage = 2000 + k + return -1 + k = k + 1 + _j_bitbuf = uint32(0) + _j_bitcnt = 0 + return 0 + +def _j_parse_sos(p: uint64, pend: uint64) -> int: + global _j_scan_ns, _j_ss, _j_se, _j_ah, _j_al + if p + 4 > pend: return -1 + ns: int = int(_j_src[p]) + if ns < 1 or ns > J_MAX_COMPS or p + uint64(3 + ns * 2) > pend: return -1 + p = p + 1 + i: int = 0 + while i < ns: + cs: int = int(_j_src[p]) + td: int = int(_j_src[p + 1]) >> 4 + ta: int = int(_j_src[p + 1]) & 15 + ci: int = -1 + j: int = 0 + while j < _j_ncomp: + if _j_comps[j].id == cs: ci = j + j = j + 1 + if ci < 0: return -1 + _j_sc[i] = ci + _j_sctd[i] = td + _j_scta[i] = ta + _j_comps[ci].td = td + _j_comps[ci].ta = ta + p = p + 2 + i = i + 1 + ss: int = int(_j_src[p]) + se: int = int(_j_src[p + 1]) + ahal: int = int(_j_src[p + 2]) + _j_scan_ns = ns + _j_ss = ss + _j_se = se + _j_ah = ahal >> 4 + _j_al = ahal & 15 + if se > 63 or ss > se: return -1 + return 0 + +# --------------------------------------------------------------------- IDCT +def _j_trig(): + global _j_trig_ready + if _j_trig_ready != 0: return + x: int = 0 + while x < 8: + u: int = 0 + while u < 8: + _j_cs[x * 8 + u] = float(math.cos(float((2 * x + 1) * u) * 3.141592653589793 / 16.0)) + u = u + 1 + x = x + 1 + _j_cw[0] = float(0.7071067811865476) + u = 1 + while u < 8: + _j_cw[u] = float(1.0) + u = u + 1 + _j_trig_ready = 1 + +def _j_idct_block(comp: JComp*, bx: int, by: int): + qbase: int = comp.tq * 64 + cbase: int = (by * comp.bw + bx) * 64 + v: int = 0 + while v < 8: + x: int = 0 + while x < 8: + s: float = float(0.0) + u: int = 0 + while u < 8: + s = s + _j_cw[u] * (float(int(comp.coef[cbase + v * 8 + u])) * float(int(_j_qt[qbase + v * 8 + u]))) * _j_cs[x * 8 + u] + u = u + 1 + _j_g[v * 8 + x] = s + x = x + 1 + v = v + 1 + y: int = 0 + pw: int = comp.bw * 8 + while y < 8: + x = 0 + while x < 8: + s: float = float(0.0) + v = 0 + while v < 8: + s = s + _j_cw[v] * _j_g[v * 8 + x] * _j_cs[y * 8 + v] + v = v + 1 + val: int = int(s * float(0.25) + float(128.5)) + if val < 0: val = 0 + if val > 255: val = 255 + comp.plane[(by * 8 + y) * pw + bx * 8 + x] = uint8(val) + x = x + 1 + y = y + 1 + +def _j_render() -> im.Image*: + global _j_stage + _j_trig() + i: int = 0 + while i < _j_ncomp: + comp: JComp* = &_j_comps[i] + pw: int = comp.bw * 8 + ph: int = comp.bh * 8 + comp.plane = uint8*(stdlib.malloc(uint64(pw * ph))) + if comp.plane is None: + _j_stage = 8 + return None + by: int = 0 + while by < comp.bh: + bx: int = 0 + while bx < comp.bw: + _j_idct_block(comp, bx, by) + bx = bx + 1 + by = by + 1 + i = i + 1 + img: im.Image* = im.new_image(_j_w, _j_h, im.IMAGE_FMT_JPEG) + if img is None: + _j_stage = 9 + return None + ci_y: int = 0 + ci_cb: int = 1 + ci_cr: int = 2 + i = 0 + while i < _j_ncomp: + if _j_comps[i].id == 1: ci_y = i + elif _j_comps[i].id == 2: ci_cb = i + elif _j_comps[i].id == 3: ci_cr = i + i = i + 1 + c0: JComp* = &_j_comps[ci_y] + pw0: int = c0.bw * 8 + y: int = 0 + while y < _j_h: + x: int = 0 + while x < _j_w: + if _j_ncomp == 1: + cy: int = (y * c0.vs) / _j_maxv + cx: int = (x * c0.hs) / _j_maxh + if cy >= c0.bh * 8: cy = c0.bh * 8 - 1 + if cx >= pw0: cx = pw0 - 1 + g: int = int(c0.plane[cy * pw0 + cx]) + img.pixels[y * _j_w + x] = im.make_pixel(g, g, g, 255) + else: + cr: JComp* = &_j_comps[ci_cr] + cb: JComp* = &_j_comps[ci_cb] + yv: int = int(c0.plane[((y * c0.vs) / _j_maxv) * pw0 + ((x * c0.hs) / _j_maxh)]) + crv: int = int(cr.plane[((y * cr.vs) / _j_maxv) * (cr.bw * 8) + ((x * cr.hs) / _j_maxh)]) + cbv: int = int(cb.plane[((y * cb.vs) / _j_maxv) * (cb.bw * 8) + ((x * cb.hs) / _j_maxh)]) + if _j_rgb_mode != 0: + img.pixels[y * _j_w + x] = im.make_pixel(yv, crv, cbv, 255) + else: + r: int = yv + int(float(1.402) * float(crv - 128)) + g: int = yv - int(float(0.344136) * float(cbv - 128)) - int(float(0.714136) * float(crv - 128)) + b: int = yv + int(float(1.772) * float(cbv - 128)) + if r < 0: r = 0 + if r > 255: r = 255 + if g < 0: g = 0 + if g > 255: g = 255 + if b < 0: b = 0 + if b > 255: b = 255 + img.pixels[y * _j_w + x] = im.make_pixel(r, g, b, 255) + x = x + 1 + y = y + 1 + return img + +# --------------------------------------------------------------------- main +def decode(src: uint8*, src_len: uint64) -> im.Image*: + global _j_src, _j_len, _j_pos, _j_marker, _j_err, _j_w, _j_h, _j_ncomp, _j_restart, _j_rgb_mode, _j_planes_ready + global _j_stage, _j_scan_no + _j_stage = 0 + _j_scan_no = 0 + if src is None or src_len < 4: return None + if int(src[0]) != 0xFF or int(src[1]) != 0xD8: return None + _j_stage = 1 + _j_src = src + _j_len = src_len + _j_pos = 2 + _j_marker = -1 + _j_err = 0 + _j_w = 0 + _j_h = 0 + _j_ncomp = 0 + _j_restart = 0 + _j_rgb_mode = 0 + _j_planes_ready = 0 + i: int = 0 + while i < 4: + _j_qt_used[i] = 0 + i = i + 1 + while True: + m: int = _j_next_marker() + if m < 0 or m == 0xD9: break + if m == 0xD8: continue + if m >= 0xD0 and m <= 0xD7: continue + if m == 0x01: continue + if _j_pos + 2 > _j_len: break + seclen: int = _j_u16(_j_pos) + if seclen < 2: break + pend: uint64 = _j_pos + uint64(seclen) + if pend > _j_len: break + p: uint64 = _j_pos + 2 + if m == 0xDB: + if _j_parse_dqt(p, pend) != 0: return None + _j_pos = pend + elif m == 0xC4: + if _j_parse_dht(p, pend) != 0: return None + _j_pos = pend + elif m == 0xC0 or m == 0xC1: + if _j_parse_sof(p, pend, 0) != 0: return None + _j_pos = pend + elif m == 0xC2: + # Progressive JPEG (SOF2) is parsed by the coefficient machinery + # below but successive-approximation refinement is not trusted yet: + # reject it instead of returning a partially-wrong image. + _j_stage = 15 + return None + elif m == 0xDD: + if p + 2 <= pend: _j_restart = _j_u16(p) + _j_pos = pend + elif m == 0xEE: + # Adobe APP14: transform 0 means the components are RGB. + if pend - p >= 12: + tr: int = int(_j_src[p + 11]) + if tr == 0: _j_rgb_mode = 1 + _j_pos = pend + elif m == 0xDA: + if _j_parse_sos(p, pend) != 0: + _j_stage = 12 + return None + _j_stage = 3 + if _j_prepare() != 0: + _j_stage = 13 + return None + _j_stage = 4 + kind: int = 0 + if _j_ss == 0: + if _j_ah != 0: kind = 1 + else: + if _j_ah != 0: kind = 3 + else: kind = 2 + _j_stage = 100 + _j_scan_no * 10 + kind + _j_pos = pend + if _j_scan() != 0: + return None + _j_scan_no = _j_scan_no + 1 + _j_stage = 5 + else: + _j_pos = pend + if _j_w <= 0 or _j_h <= 0 or _j_ncomp < 1: + _j_stage = 10 + return None + if _j_ncomp == 4: + _j_stage = 11 + return None + _j_stage = 6 + img: im.Image* = _j_render() + if img is None: return None + _j_stage = 7 + return img diff --git a/vpsdk/imagesee/png.vp b/vpsdk/imagesee/png.vp new file mode 100644 index 0000000..702aac1 --- /dev/null +++ b/vpsdk/imagesee/png.vp @@ -0,0 +1,303 @@ +# PNG decoder (RFC 2083): all standard color types, bit depths 1/2/4/8/16, +# plus Adam7 interlacing. Output is packed 0xAARRGGBB in `Image.pixels`. + +import vpsdk.imagesee.inflate as inflate +import vpsdk.imagesee.image as im +import vpsdk.stdlib as stdlib +import string +from stdint import * + +define PNG_SIG_0 = 0x89 +define PNG_SIG_1 = 0x50 +define PNG_SIG_2 = 0x4E +define PNG_SIG_3 = 0x47 + +_png_src: uint8* = None +_png_len: uint64 = 0 +_png_idat: uint8* = None +_png_idat_len: uint64 = 0 +_png_w: int = 0 +_png_h: int = 0 +_png_depth: int = 0 +_png_ctype: int = 0 +_png_interlace: int = 0 +_png_channels: int = 0 +_png_raw: uint8* = None +_png_plte: [768]uint8 +_png_plte_n: int = 0 +_png_trns: [256]uint8 +_png_trns_n: int = 0 +_png_err: int = 0 +_png_raw_off: uint64 = 0 + +# Adam7 pass origins / steps. +_ax0: [7]int = [0, 4, 0, 2, 0, 1, 0] +_ay0: [7]int = [0, 0, 4, 0, 2, 0, 1] +_adx: [7]int = [8, 8, 4, 4, 2, 2, 1] +_ady: [7]int = [8, 8, 8, 4, 4, 2, 2] + +def _be32(d: uint8*, o: uint64) -> uint32: + v: uint32 = (uint32(d[o]) << 24) | (uint32(d[o + 1]) << 16) + v = v | (uint32(d[o + 2]) << 8) | uint32(d[o + 3]) + return v + +def _sig_ok(d: uint8*) -> int: + if d[0] != uint8(PNG_SIG_0): return 0 + if d[1] != uint8(PNG_SIG_1): return 0 + if d[2] != uint8(PNG_SIG_2): return 0 + if d[3] != uint8(PNG_SIG_3): return 0 + return 1 + +def _channels_of(ct: int) -> int: + if ct == 0: return 1 + if ct == 2: return 3 + if ct == 3: return 1 + if ct == 4: return 2 + if ct == 6: return 4 + return 0 + +def _depth_ok(ct: int, bd: int) -> int: + if bd != 1 and bd != 2 and bd != 4 and bd != 8 and bd != 16: return 0 + if ct == 0: return 1 + if ct == 3: return 1 if bd <= 8 else 0 + if ct == 2 or ct == 4 or ct == 6: return 1 if bd == 8 or bd == 16 else 0 + return 0 + +def _rowbytes(w: int) -> int: + bits: int = w * _png_channels * _png_depth + return (bits + 7) / 8 + +def _pass_w(w: int, p: int) -> int: + x0: int = _ax0[p] + if w <= x0: return 0 + return (w - x0 + _adx[p] - 1) / _adx[p] + +def _pass_h(h: int, p: int) -> int: + y0: int = _ay0[p] + if h <= y0: return 0 + return (h - y0 + _ady[p] - 1) / _ady[p] + +def _raw_size() -> uint64: + if _png_interlace == 0: + return uint64(_png_h * (1 + _rowbytes(_png_w))) + total: uint64 = 0 + p: int = 0 + while p < 7: + pw: int = _pass_w(_png_w, p) + ph: int = _pass_h(_png_h, p) + if pw > 0 and ph > 0: + total = total + uint64(ph * (1 + _rowbytes(pw))) + p = p + 1 + return total + +def _sample(line: uint8*, idx: int, depth: int) -> int: + if depth == 8: + return int(line[idx]) + if depth == 16: + return (int(line[idx * 2]) << 8) | int(line[idx * 2 + 1]) + bitpos: int = idx * depth + byte: int = int(line[bitpos >> 3]) + shift: int = 8 - depth - (bitpos & 7) + return (byte >> shift) & ((1 << depth) - 1) + +def _paeth(a: int, b: int, c: int) -> int: + p: int = a + b - c + pa: int = p - a + if pa < 0: pa = -pa + pb: int = p - b + if pb < 0: pb = -pb + pc: int = p - c + if pc < 0: pc = -pc + if pa <= pb and pa <= pc: return a + if pb <= pc: return b + return c + +def _unfilter(ft: int, cur: uint8*, prev: uint8*, n: int, bpp: int): + i: int = 0 + while i < n: + a: int = 0 + c: int = 0 + if i >= bpp: + a = int(cur[i - bpp]) + c = int(prev[i - bpp]) + b: int = int(prev[i]) + x: int = int(cur[i]) + if ft == 1: + x = x + a + elif ft == 2: + x = x + b + elif ft == 3: + x = x + ((a + b) >> 1) + elif ft == 4: + x = x + _paeth(a, b, c) + cur[i] = uint8(x & 0xFF) + i = i + 1 + +def _scale(v: int, depth: int) -> int: + if depth == 8: return v + if depth == 16: return v >> 8 + mx: int = (1 << depth) - 1 + return (v * 255 + (mx / 2)) / mx + +def _pixel_argb(line: uint8*, idx: int) -> uint32: + ct: int = _png_ctype + bd: int = _png_depth + if ct == 0: + raw: int = _sample(line, idx, bd) + g: int = _scale(raw, bd) + a: int = 255 + if _png_trns_n >= 2: + tv: int = (int(_png_trns[0]) << 8) | int(_png_trns[1]) + if raw == tv: a = 0 + return im.make_pixel(g, g, g, a) + if ct == 2: + r: int = _sample(line, idx * 3, bd) + g: int = _sample(line, idx * 3 + 1, bd) + b: int = _sample(line, idx * 3 + 2, bd) + a: int = 255 + if _png_trns_n >= 6: + tr: int = (int(_png_trns[0]) << 8) | int(_png_trns[1]) + tg: int = (int(_png_trns[2]) << 8) | int(_png_trns[3]) + tb: int = (int(_png_trns[4]) << 8) | int(_png_trns[5]) + if r == tr and g == tg and b == tb: a = 0 + return im.make_pixel(_scale(r, bd), _scale(g, bd), _scale(b, bd), a) + if ct == 3: + pi: int = _sample(line, idx, bd) + if pi * 3 + 2 >= 768: return im.make_pixel(0, 0, 0, 255) + r: int = int(_png_plte[pi * 3]) + g: int = int(_png_plte[pi * 3 + 1]) + b: int = int(_png_plte[pi * 3 + 2]) + a: int = 255 + if pi < _png_trns_n: a = int(_png_trns[pi]) + return im.make_pixel(r, g, b, a) + if ct == 4: + g: int = _sample(line, idx * 2, bd) + a: int = _sample(line, idx * 2 + 1, bd) + return im.make_pixel(_scale(g, bd), _scale(g, bd), _scale(g, bd), _scale(a, bd)) + if ct == 6: + r: int = _sample(line, idx * 4, bd) + g: int = _sample(line, idx * 4 + 1, bd) + b: int = _sample(line, idx * 4 + 2, bd) + a: int = _sample(line, idx * 4 + 3, bd) + return im.make_pixel(_scale(r, bd), _scale(g, bd), _scale(b, bd), _scale(a, bd)) + return im.make_pixel(0, 0, 0, 255) + +def _decode_pass(out: uint32*, p: int): + global _png_raw_off + pw: int = _pass_w(_png_w, p) + ph: int = _pass_h(_png_h, p) + px0: int = _ax0[p] + py0: int = _ay0[p] + pdx: int = _adx[p] + pdy: int = _ady[p] + if _png_interlace == 0: + # Non-interlaced images are a single full-size pass, not Adam7 pass 0. + pw = _png_w + ph = _png_h + px0 = 0 + py0 = 0 + pdx = 1 + pdy = 1 + if pw <= 0 or ph <= 0: return + rb: int = _rowbytes(pw) + bpp: int = (_png_channels * _png_depth + 7) / 8 + if bpp < 1: bpp = 1 + cur: uint8* = uint8*(stdlib.malloc(uint64(rb))) + prev: uint8* = uint8*(stdlib.malloc(uint64(rb))) + if cur is None or prev is None: return + string.memset(rawptr(cur), 0, uint64(rb)) + string.memset(rawptr(prev), 0, uint64(rb)) + off: uint64 = _png_raw_off + ry: int = 0 + while ry < ph: + ft: int = int(_png_raw[off]) + off = off + 1 + string.memcpy(rawptr(cur), rawptr(uint64(_png_raw) + off), uint64(rb)) + off = off + uint64(rb) + _unfilter(ft, cur, prev, rb, bpp) + y: int = py0 + ry * pdy + rx: int = 0 + while rx < pw: + x: int = px0 + rx * pdx + if x < _png_w and y < _png_h: + out[y * _png_w + x] = _pixel_argb(cur, rx) + rx = rx + 1 + string.memcpy(rawptr(prev), rawptr(cur), uint64(rb)) + ry = ry + 1 + _png_raw_off = off + +def decode(src: uint8*, src_len: uint64) -> im.Image*: + global _png_src, _png_len, _png_w, _png_h, _png_depth, _png_ctype + global _png_interlace, _png_channels, _png_raw, _png_plte_n, _png_trns_n + global _png_idat, _png_idat_len, _png_err, _png_raw_off + if src is None or src_len < 33: return None + if _sig_ok(src) == 0: return None + _png_src = src + _png_len = src_len + _png_w = 0 + _png_h = 0 + _png_plte_n = 0 + _png_trns_n = 0 + _png_err = 0 + _png_raw_off = 0 + _png_idat = uint8*(stdlib.malloc(src_len)) + if _png_idat is None: return None + _png_idat_len = 0 + pos: uint64 = 8 + got_ihdr: int = 0 + while pos + 12 <= src_len: + clen: uint64 = uint64(_be32(src, pos)) + if pos + 12 + clen > src_len: return None + typ0: int = int(src[pos + 4]) + typ1: int = int(src[pos + 5]) + typ2: int = int(src[pos + 6]) + typ3: int = int(src[pos + 7]) + dpos: uint64 = pos + 8 + if typ0 == 73 and typ1 == 72 and typ2 == 68 and typ3 == 82: # IHDR + if clen < 13: return None + _png_w = int(_be32(src, dpos)) + _png_h = int(_be32(src, dpos + 4)) + _png_depth = int(src[dpos + 8]) + _png_ctype = int(src[dpos + 9]) + _png_interlace = int(src[dpos + 12]) + _png_channels = _channels_of(_png_ctype) + if _png_w <= 0 or _png_h <= 0 or _png_w > 16384 or _png_h > 16384: return None + if _png_channels == 0 or _depth_ok(_png_ctype, _png_depth) == 0: return None + if int(src[dpos + 10]) != 0 or int(src[dpos + 11]) != 0: return None + got_ihdr = 1 + elif typ0 == 80 and typ1 == 76 and typ2 == 84 and typ3 == 69: # PLTE + n: uint64 = clen + if n > 768: n = 768 + string.memcpy(rawptr(&_png_plte[0]), rawptr(uint64(src) + dpos), n) + _png_plte_n = int(n / 3) + elif typ0 == 116 and typ1 == 82 and typ2 == 78 and typ3 == 83: # tRNS + n: uint64 = clen + if n > 256: n = 256 + string.memcpy(rawptr(&_png_trns[0]), rawptr(uint64(src) + dpos), n) + _png_trns_n = int(n) + elif typ0 == 73 and typ1 == 68 and typ2 == 65 and typ3 == 84: # IDAT + string.memcpy(rawptr(uint64(_png_idat) + _png_idat_len), + rawptr(uint64(src) + dpos), clen) + _png_idat_len = _png_idat_len + clen + elif typ0 == 73 and typ1 == 69 and typ2 == 78 and typ3 == 68: # IEND + break + pos = pos + 12 + clen + if got_ihdr == 0 or _png_idat_len == 0: return None + + rawsz: uint64 = _raw_size() + if rawsz == 0: return None + _png_raw = uint8*(stdlib.malloc(rawsz)) + if _png_raw is None: return None + got_raw: int = inflate.zlib_decompress(_png_idat, _png_idat_len, _png_raw, rawsz) + if got_raw < 0 or uint64(got_raw) != rawsz: return None + + img: im.Image* = im.new_image(_png_w, _png_h, im.IMAGE_FMT_PNG) + if img is None: return None + if _png_interlace == 0: + _decode_pass(img.pixels, 0) + else: + p: int = 0 + while p < 7: + _decode_pass(img.pixels, p) + p = p + 1 + return img diff --git a/vpsdk/process.vp b/vpsdk/process.vp index 1b7f106..064ccd3 100644 --- a/vpsdk/process.vp +++ b/vpsdk/process.vp @@ -5,12 +5,12 @@ def exit(code: int): syscall._syscall1(uint64(syscall.Syscall.EXIT), uint64(code)) def reboot(): - """Reset the machine.""" - syscall._syscall0(uint64(syscall.Syscall.REBOOT)) + """Reset the machine via Linux reboot(169, LINUX_REBOOT_CMD_RESTART).""" + syscall.reboot_linux(uint64(0x01234567)) def shutdown(): - """Power off the machine via ACPI S5.""" - syscall._syscall0(uint64(syscall.Syscall.SHUTDOWN)) + """Power off the machine via Linux reboot(169, LINUX_REBOOT_CMD_POWER_OFF).""" + syscall.reboot_linux(uint64(0x4321FEDC)) def thread_exit(): """当前用户线程显式退出:内核回收其用户栈并标记线程结束(不返回)。""" @@ -25,11 +25,29 @@ def get_pid() -> int: """当前进程 pid(getpid)。/dev/desk 的事件按 pid 过滤,建窗前就要拿到它。""" return int(syscall._syscall0(uint64(syscall.Syscall.GETPID))) +def get_ppid() -> int: + return int(syscall._syscall0(uint64(syscall.Syscall.GETPPID))) + def spawn(path: const str, blocking: int = 0) -> int: - return int(syscall._syscall2(uint64(syscall.Syscall.SPAWN), uint64(path), uint64(blocking))) + """ViperOS launcher: start a new child and keep the caller alive.""" + return int(syscall._syscall2(uint64(syscall.Syscall.SPAWN_PROC), uint64(path), uint64(blocking))) + +def execve(path: const str, argv: rawptr, envp: rawptr) -> int: + """Linux execve: replace the current process (returns only on failure).""" + return int(syscall._syscall3(uint64(syscall.Syscall.SPAWN), uint64(path), uint64(argv), uint64(envp))) + +def fork() -> int: + return int(syscall._syscall0(uint64(syscall.Syscall.FORK))) + +def clone(flags: uint64, stack: uint64, ptid: uint64 = 0, ctid: uint64 = 0, tls: uint64 = 0) -> int: + return int(syscall._syscall5(uint64(syscall.Syscall.CLONE), flags, stack, ptid, ctid, tls)) + +def wait4(pid: int, status: rawptr, options: uint32 = 0) -> int: + return int(syscall._syscall4(uint64(syscall.Syscall.WAIT4), uint64(pid), + uint64(status), uint64(options), 0)) def thread_create(func: rawptr, arg: rawptr = None) -> int: - return int(syscall._syscall2(uint64(syscall.Syscall.THREAD_CREATE), uint64(func), uint64(arg))) + return int(syscall._syscall2(uint64(syscall.Syscall.THREAD_CREATE_OLD), uint64(func), uint64(arg))) class Thread: diff --git a/vpsdk/syscall.vp b/vpsdk/syscall.vp index 0a909dc..dab1509 100644 --- a/vpsdk/syscall.vp +++ b/vpsdk/syscall.vp @@ -1,39 +1,89 @@ # ============================================================================ -# 系统调用号表(SDK 侧镜像)。**与 VKernel/Kernel/intr/syscall.vp 的同名 enum 逐条对应**: -# vpsdk 不允许依赖 VKernel,所以这里是一份必须手工同步的副本 —— 增删/改号/改名两边一起改。 -# 编号口径:与 Linux x86-64 ABI 同名同号;Linux 有而我们未实现的留空不占号; -# 我们独有的能力不再新增 syscall,最终走 devfs 设备文件或下沉到用户层(见 0x1000+ 段的标注)。 +# 系统调用号表(SDK 侧镜像,由 Scripts/syscall_table.py 生成,禁止手改)。 +# 编号口径:与 Linux x86-64 ABI 同名同号;Linux 有而尚未实现的调用暂不占号。 # ============================================================================ +# BEGIN GENERATED SYSCALL TABLE enum Syscall: - # ---- Linux 对齐段(号 = Linux x86-64 的号)---- - READ = 0 # read(fd, buf, count) - WRITE = 1 # write(fd, buf, count) - OPEN = 2 # open(path, flags) - CLOSE = 3 # close(fd) - STAT = 4 # stat(path, info)(结构体仍是本系统的 fileinfo) - SEEK = 8 # lseek(fd, offset, whence)(whence 目前恒 0 = SEEK_SET) - MMAP = 9 # mmap(size) → VA(ABI 待向 Linux 的 addr/len/prot/flags/fd/off 靠拢) - MUNMAP = 11 # munmap(ptr, size)(目前空实现) - IOCTL = 16 # ioctl(fd, request, arg):设备控制(当前用于 /dev/fb0 查几何) - YIELD = 24 # sched_yield() - GETPID = 39 # getpid():返回当前进程 pid(app 用它过滤 /dev/desk 事件) - THREAD_CREATE = 56 # clone():建用户线程,返回 tid - SPAWN = 59 # execve():加载并运行新 ELF,返回 pid - THREAD_EXIT = 60 # exit():退役当前线程 - MKDIR = 83 # mkdir(path) - REMOVE = 87 # unlink(path) - EXIT = 231 # exit_group():退役当前线程(窗口由 explorer 侧回收) - # ---- 临时私有段(0x1000+):Linux 无对应,且**逐条都会被消掉** ---- - # 目标形态:私有能力不占 syscall 号 → 改 devfs 设备文件。 - # S3e 删 UI 11 个;S4 删 SERIAL_PUTS(改 /dev/serial0);S5 删 TELL/SIZE - # (改 lseek 的 SEEK_CUR / SEEK_END,见 vpsdk/fs.vp 的 tell/fsize)。 - OPENDIR = 0x1003 # TODO(linux):openat(O_DIRECTORY) + getdents64 - READDIR = 0x1004 # TODO(linux):getdents64 - CLOSEDIR = 0x1005 # TODO(linux):close - LOAD_SO = 0x1006 # TODO(userspace): dynamic library loading - SO_CALL1 = 0x1007 # TODO(userspace): dynamic library call - REBOOT = 0x1008 - SHUTDOWN = 0x1009 + # ---- Linux 对齐段(号 = Linux x86-64 的号) ---- + READ = 0 # read(fd, buf, count) + WRITE = 1 # write(fd, buf, count) + OPEN = 2 # open(path, flags) - Linux O_* + CLOSE = 3 # close(fd) + STAT = 4 # stat(path, fileinfo*) - legacy struct + FSTAT = 5 # fstat(fd, struct stat*) + POLL = 7 # poll(fds, nfds, timeout) - stub 0 + SEEK = 8 # lseek(fd, offset, whence) -> new offset + MMAP = 9 # mmap(addr, len, prot, flags, fd, off) + MPROTECT = 10 # mprotect(addr, len, prot) + MUNMAP = 11 # munmap(addr, len) + BRK = 12 # brk(addr) -> new break + RT_SIGACTION = 13 # rt_sigaction() - bookkeeping + RT_SIGPROCMASK = 14 # rt_sigprocmask() - bookkeeping + RT_SIGRETURN = 15 # rt_sigreturn() + IOCTL = 16 # ioctl(fd, request, arg) + PREAD64 = 17 # pread64(fd, buf, count, offset) + PWRITE64 = 18 # pwrite64(fd, buf, count, offset) + READV = 19 # readv(fd, iov, iovcnt) + WRITEV = 20 # writev(fd, iov, iovcnt) + ACCESS = 21 # access(path, mode) + PIPE = 22 # pipe(int[2]) + YIELD = 24 # sched_yield() + MREMAP = 25 # mremap(old, oldsz, newsz, flags, newaddr) + MADVISE = 28 # madvise(addr, len, advice) + DUP = 32 # dup(fd) + DUP2 = 33 # dup2(oldfd, newfd) + NANOSLEEP = 35 # nanosleep(req, rem) + GETPID = 39 # getpid() + CLONE = 56 # clone(flags, stack, ptid, ctid, tls) + FORK = 57 # fork() - copy address space + SPAWN = 59 # execve(path, argv, envp) + THREAD_EXIT = 60 # exit(): retire current thread + WAIT4 = 61 # wait4(pid, status, options, rusage) + KILL = 62 # kill(pid, sig) - bookkeeping + UNAME = 63 # uname(utsname*) + FCNTL = 72 # fcntl(fd, cmd, arg) + GETCWD = 79 # getcwd(buf, size) + CHDIR = 80 # chdir(path) + RENAME = 82 # rename(old, new) + MKDIR = 83 # mkdir(path) + RMDIR = 84 # rmdir(path) + REMOVE = 87 # unlink(path) + READLINK = 89 # readlink(path, buf, size) - no symlinks + GETUID = 102 # getuid() + GETGID = 104 # getgid() + GETEUID = 107 # geteuid() + GETEGID = 108 # getegid() + GETTIMEOFDAY = 96 # gettimeofday(tv, tz) + GETPPID = 110 # getppid() + ARCH_PRCTL = 158 # arch_prctl(code, addr) + REBOOT_LINUX = 169 # reboot(magic1, magic2, cmd, arg) + GETTID = 186 # gettid() + FUTEX = 202 # futex(uaddr, op, val, timeout) + GETDENTS64 = 217 # getdents64(fd, buf, count) + SET_TID_ADDRESS = 218 # set_tid_address(tidptr) + CLOCK_GETTIME = 228 # clock_gettime(clockid, timespec*) + CLOCK_GETRES = 229 # clock_getres(clockid, timespec*) + EXIT = 231 # exit_group(): terminate all threads + OPENAT = 257 # openat(dirfd, path, flags, mode) + NEWFSTATAT = 262 # newfstatat(dirfd, path, stat*, flags) + UNLINKAT = 263 # unlinkat(dirfd, path, flags) + READLINKAT = 267 # readlinkat(dirfd, path, buf, size) + FACCESSAT = 269 # faccessat(dirfd, path, mode, flags) + SET_ROBUST_LIST = 273 # set_robust_list(head, len) + DUP3 = 292 # dup3(oldfd, newfd, flags) + PIPE2 = 293 # pipe2(int[2], flags) + PRLIMIT64 = 302 # prlimit64(pid, resource, new, old) + GETRANDOM = 318 # getrandom(buf, len, flags) + STATX = 332 # statx(...) - -ENOSYS, glibc falls back + RSEQ = 334 # rseq(rseq, len, flags) - -ENOSYS + # ---- 临时私有段(0x1000+):Linux 无对应,逐条迁出 ---- + LOAD_SO = 0x1006 # TODO(userspace): dynamic library loading + SO_CALL1 = 0x1007 # TODO(userspace): dynamic library call + REBOOT = 0x1008 # TODO(linux): reboot(169) + SHUTDOWN = 0x1009 # TODO(linux): reboot(RB_POWER_OFF) + SPAWN_PROC = 0x100A # TODO(userspace): Terminal run helper + THREAD_CREATE_OLD = 0x100B # TODO(userspace): ViperOS thread_create +# END GENERATED SYSCALL TABLE # ---- mmap 的 prot / flags(与内核 VKernel/intr/syscall.vp 及各 Linux 同名常量一致)---- define PROT_NONE = 0x0 @@ -46,6 +96,39 @@ define MAP_FIXED = 0x10 define MAP_ANONYMOUS = 0x20 # fd = -1(Linux 匿名映射约定) define MMAP_FD_ANON: uint64 = 0xFFFFFFFFFFFFFFFF +define MAP_FAILED: uint64 = 0xFFFFFFFFFFFFFFFF +define MREMAP_MAYMOVE = 1 +define MREMAP_FIXED = 2 + +# ---- AT_* / fcntl(Linux 值)---- +define AT_FDCWD = -100 +define AT_SYMLINK_NOFOLLOW = 256 +define AT_REMOVEDIR = 512 +define AT_EMPTY_PATH = 4096 + +define F_DUPFD = 0 +define F_GETFD = 1 +define F_SETFD = 2 +define F_GETFL = 3 +define F_SETFL = 4 +define F_DUPFD_CLOEXEC = 1030 +define FD_CLOEXEC = 1 + +# ---- TLS / futex / time / wait / reboot ---- +define ARCH_SET_GS = 0x1001 +define ARCH_SET_FS = 0x1002 +define ARCH_GET_FS = 0x1003 +define ARCH_GET_GS = 0x1004 +define FUTEX_WAIT = 0 +define FUTEX_WAKE = 1 +define FUTEX_PRIVATE_FLAG = 128 +define CLOCK_REALTIME = 0 +define CLOCK_MONOTONIC = 1 +define WNOHANG = 1 +define LINUX_REBOOT_MAGIC1 = 0xFEE1DEAD +define LINUX_REBOOT_MAGIC2 = 672274793 +define LINUX_REBOOT_CMD_RESTART = 0x01234567 +define LINUX_REBOOT_CMD_POWER_OFF = 0x4321FEDC def _syscall0(num: uint64) -> uint64: ret: uint64 @@ -91,6 +174,21 @@ def _syscall3(num: uint64, a1: uint64, a2: uint64, a3: uint64) -> uint64: ASM_DESCR.CLOBBER_RSI, ASM_DESCR.CLOBBER_RDX]) return ret +def _syscall4(num: uint64, a1: uint64, a2: uint64, a3: uint64, a4: uint64) -> uint64: + ret: uint64 + __asm__(f"""mov rdi, {asminp(a1, ASM_DESCR.REG_ANY)} + mov rsi, {asminp(a2, ASM_DESCR.REG_ANY)} + mov rdx, {asminp(a3, ASM_DESCR.REG_ANY)} + mov r10, {asminp(a4, ASM_DESCR.REG_ANY)} + mov rax, {asminp(num, ASM_DESCR.REG_ANY)} + syscall""", + out=[asmout(ret, ASM_DESCR.OUTPUT_REG)], + op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RCX, + ASM_DESCR.CLOBBER_R11, ASM_DESCR.CLOBBER_RDI, + ASM_DESCR.CLOBBER_RSI, ASM_DESCR.CLOBBER_RDX, + ASM_DESCR.CLOBBER_R10]) + return ret + def _syscall5(num: uint64, a1: uint64, a2: uint64, a3: uint64, a4: uint64, a5: uint64) -> uint64: ret: uint64 __asm__(f"""mov rdi, {asminp(a1, ASM_DESCR.REG_ANY)} @@ -126,10 +224,185 @@ def _syscall6(num: uint64, a1: uint64, a2: uint64, a3: uint64, a4: uint64, a5: u return ret def mmap(addr: uint64, length: uint64, prot: uint64, flags: uint64, fd: uint64, offset: uint64) -> uint64: - """mmap(addr, length, prot, flags, fd, offset):返回映射起始 VA,失败返回 0。 + """mmap(addr, length, prot, flags, fd, offset):返回映射起始 VA。 - 匿名映射:fd = MMAP_FD_ANON(-1) 且 flags 带 MAP_ANONYMOUS; - 映射设备(如 /dev/fb0):fd 传该设备的 fd。addr / MAP_FIXED 由内核挑地址。 - (定义放在 _syscall6 之后,避免同模块内前向引用。) + 内核遵循 Linux 语义(失败返回 MAP_FAILED = -1);SDK 为兼容既有调用方 + 把它归一化为 0(C 代码里判 `== 0`)。 """ - return _syscall6(uint64(Syscall.MMAP), addr, length, prot, flags, fd, offset) + va: uint64 = _syscall6(uint64(Syscall.MMAP), addr, length, prot, flags, fd, offset) + if va == MAP_FAILED: return uint64(0) + return va + +def munmap(addr: uint64, length: uint64) -> int: + return int(_syscall2(uint64(Syscall.MUNMAP), addr, length)) + +def mprotect(addr: uint64, length: uint64, prot: uint64) -> int: + return int(_syscall3(uint64(Syscall.MPROTECT), addr, length, prot)) + +def brk(addr: uint64) -> uint64: + return _syscall1(uint64(Syscall.BRK), addr) + +def mremap(old_addr: uint64, old_size: uint64, new_size: uint64, flags: uint64, new_addr: uint64 = 0) -> uint64: + va: uint64 = _syscall5(uint64(Syscall.MREMAP), old_addr, old_size, new_size, flags, new_addr) + if va == MAP_FAILED: return uint64(0) + return va + +def madvise(addr: uint64, length: uint64, advice: uint32) -> int: + return int(_syscall3(uint64(Syscall.MADVISE), addr, length, uint64(advice))) + +# ============================================================================ +# P0 flat wrappers (Linux ABI). Higher level helpers live in vpsdk.fs / process. +# ============================================================================ + +def uname(buf: rawptr) -> int: + return int(_syscall1(uint64(Syscall.UNAME), uint64(buf))) + +def getuid() -> int: + return int(_syscall0(uint64(Syscall.GETUID))) + +def geteuid() -> int: + return int(_syscall0(uint64(Syscall.GETEUID))) + +def getgid() -> int: + return int(_syscall0(uint64(Syscall.GETGID))) + +def getegid() -> int: + return int(_syscall0(uint64(Syscall.GETEGID))) + +def getppid() -> int: + return int(_syscall0(uint64(Syscall.GETPPID))) + +def gettid() -> int: + return int(_syscall0(uint64(Syscall.GETTID))) + +def getcwd(buf: rawptr, size: uint64) -> int: + return int(_syscall2(uint64(Syscall.GETCWD), uint64(buf), size)) + +def chdir(path: const str) -> int: + return int(_syscall1(uint64(Syscall.CHDIR), uint64(path))) + +def access(path: const str, mode: uint32) -> int: + return int(_syscall2(uint64(Syscall.ACCESS), uint64(path), uint64(mode))) + +def faccessat(dirfd: int, path: const str, mode: uint32, flags: uint32) -> int: + return int(_syscall4(uint64(Syscall.FACCESSAT), uint64(dirfd), uint64(path), + uint64(mode), uint64(flags))) + +def readlink(path: const str, buf: rawptr, size: uint64) -> int: + return int(_syscall3(uint64(Syscall.READLINK), uint64(path), uint64(buf), size)) + +def readlinkat(dirfd: int, path: const str, buf: rawptr, size: uint64) -> int: + return int(_syscall4(uint64(Syscall.READLINKAT), uint64(dirfd), uint64(path), + uint64(buf), size)) + +def set_tid_address(tidptr: rawptr) -> int: return int(_syscall1(uint64(Syscall.SET_TID_ADDRESS), uint64(tidptr))) + +def set_robust_list(head: rawptr, length: uint64) -> int: + return int(_syscall2(uint64(Syscall.SET_ROBUST_LIST), uint64(head), length)) + +def rseq(rseq_ptr: rawptr, length: uint32, flags: uint32) -> int: + return int(_syscall3(uint64(Syscall.RSEQ), uint64(rseq_ptr), + uint64(length), uint64(flags))) + +def prlimit64(pid: int, resource: uint32, new_limit: rawptr, old_limit: rawptr) -> int: + return int(_syscall4(uint64(Syscall.PRLIMIT64), uint64(pid), uint64(resource), + uint64(new_limit), uint64(old_limit))) + +def getrandom(buf: rawptr, length: uint64, flags: uint32) -> int: + return int(_syscall3(uint64(Syscall.GETRANDOM), uint64(buf), length, uint64(flags))) + +# ---------------------------------------------------------------- P1 ---- + +def openat(dirfd: int, path: const str, flags: uint32, mode: uint32 = 0) -> int: + return int(_syscall4(uint64(Syscall.OPENAT), uint64(dirfd), uint64(path), + uint64(flags), uint64(mode))) + +def fstat(fd: int, st: rawptr) -> int: + return int(_syscall2(uint64(Syscall.FSTAT), uint64(fd), uint64(st))) + +def newfstatat(dirfd: int, path: const str, st: rawptr, flags: uint32) -> int: + return int(_syscall4(uint64(Syscall.NEWFSTATAT), uint64(dirfd), uint64(path), + uint64(st), uint64(flags))) + +def getdents64(fd: int, buf: rawptr, count: uint64) -> int: + return int(_syscall3(uint64(Syscall.GETDENTS64), uint64(fd), uint64(buf), count)) + +def dup(fd: int) -> int: + return int(_syscall1(uint64(Syscall.DUP), uint64(fd))) + +def dup2(oldfd: int, newfd: int) -> int: + return int(_syscall2(uint64(Syscall.DUP2), uint64(oldfd), uint64(newfd))) + +def dup3(oldfd: int, newfd: int, flags: uint32) -> int: + return int(_syscall3(uint64(Syscall.DUP3), uint64(oldfd), uint64(newfd), uint64(flags))) + +def fcntl(fd: int, cmd: uint32, arg: uint64) -> int: + return int(_syscall3(uint64(Syscall.FCNTL), uint64(fd), uint64(cmd), arg)) + +def pread64(fd: int, buf: rawptr, count: uint32, offset: uint64) -> int: + return int(_syscall4(uint64(Syscall.PREAD64), uint64(fd), uint64(buf), + uint64(count), offset)) + +def pwrite64(fd: int, buf: rawptr, count: uint32, offset: uint64) -> int: + return int(_syscall4(uint64(Syscall.PWRITE64), uint64(fd), uint64(buf), + uint64(count), offset)) + +def rename(old_path: const str, new_path: const str) -> int: + return int(_syscall2(uint64(Syscall.RENAME), uint64(old_path), uint64(new_path))) + +def rmdir(path: const str) -> int: + return int(_syscall1(uint64(Syscall.RMDIR), uint64(path))) + +def unlinkat(dirfd: int, path: const str, flags: uint32) -> int: + return int(_syscall3(uint64(Syscall.UNLINKAT), uint64(dirfd), uint64(path), + uint64(flags))) + +# ---------------------------------------------------------------- P4/P5/P6 ---- + +def arch_prctl(code: int, addr: uint64) -> int: + return int(_syscall2(uint64(Syscall.ARCH_PRCTL), uint64(code), addr)) + +def futex(uaddr: uint64, op: uint32, val: uint32, timeout: uint64 = 0) -> int: + return int(_syscall4(uint64(Syscall.FUTEX), uaddr, uint64(op), uint64(val), timeout)) + +def clock_gettime(clockid: int, ts: rawptr) -> int: + return int(_syscall2(uint64(Syscall.CLOCK_GETTIME), uint64(clockid), uint64(ts))) + +def clock_getres(clockid: int, ts: rawptr) -> int: + return int(_syscall2(uint64(Syscall.CLOCK_GETRES), uint64(clockid), uint64(ts))) + +def gettimeofday(tv: rawptr, tz: rawptr = None) -> int: + return int(_syscall2(uint64(Syscall.GETTIMEOFDAY), uint64(tv), uint64(tz))) + +def nanosleep(req: rawptr, rem: rawptr = None) -> int: + return int(_syscall2(uint64(Syscall.NANOSLEEP), uint64(req), uint64(rem))) + +def rt_sigaction(sig: int, act: rawptr, oldact: rawptr, sigsetsize: uint64 = 8) -> int: + return int(_syscall4(uint64(Syscall.RT_SIGACTION), uint64(sig), uint64(act), + uint64(oldact), sigsetsize)) + +def rt_sigprocmask(how: int, setp: rawptr, oldset: rawptr, sigsetsize: uint64 = 8) -> int: + return int(_syscall4(uint64(Syscall.RT_SIGPROCMASK), uint64(how), uint64(setp), + uint64(oldset), sigsetsize)) + +def kill(pid: int, sig: int) -> int: + return int(_syscall2(uint64(Syscall.KILL), uint64(pid), uint64(sig))) + +def readv(fd: int, iov: rawptr, iovcnt: int) -> int: + return int(_syscall3(uint64(Syscall.READV), uint64(fd), uint64(iov), uint64(iovcnt))) + +def writev(fd: int, iov: rawptr, iovcnt: int) -> int: + return int(_syscall3(uint64(Syscall.WRITEV), uint64(fd), uint64(iov), uint64(iovcnt))) + +def poll(fds: rawptr, nfds: uint64, timeout: int) -> int: + return int(_syscall3(uint64(Syscall.POLL), uint64(fds), nfds, uint64(timeout))) + +def pipe2(fds: rawptr, flags: uint32 = 0) -> int: + return int(_syscall2(uint64(Syscall.PIPE2), uint64(fds), uint64(flags))) + +def pipe(fds: rawptr) -> int: + return int(_syscall1(uint64(Syscall.PIPE), uint64(fds))) + +def reboot_linux(cmd: uint64) -> int: + return int(_syscall4(uint64(Syscall.REBOOT_LINUX), uint64(LINUX_REBOOT_MAGIC1), + uint64(LINUX_REBOOT_MAGIC2), cmd, uint64(0))) diff --git a/vpsdk/vpui.vp b/vpsdk/vpui.vp index 5f8efaa..f0f4baf 100644 --- a/vpsdk/vpui.vp +++ b/vpsdk/vpui.vp @@ -1,2074 +1,2099 @@ -import vpsdk.window as window -import vpsdk.process as process -import vpsdk.devfs as devfs -import vpsdk.stdlib as stdlib -import vpsdk.fonttools.fonttools as fonttools -import string - -define TITLE_H = 32 -define MAX_WIDGETS = 24 -define CHAR_W = 8 -define CHAR_H = 16 - -define TEXT_MODE_DIRECT = 0 -define TEXT_MODE_MANAGED = 1 -define TEXT_QUEUE_MAX = 256 -define TEXT_CHUNK = 28 - -define WIDGET_LABEL = 1 -define WIDGET_BUTTON = 2 -define WIDGET_ENTRY = 3 -define WIDGET_TEXT = 4 -define WIDGET_SCROLLBAR = 5 -define WIDGET_FBCANVAS = 6 -define WIDGET_FRAME = 7 - -define VERTICAL = 1 -define HORIZONTAL = 2 -define TEXT_MAX_LINES = 200 - -define PACK_TOP = 1 -define PACK_BOTTOM = 2 -define PACK_LEFT = 3 -define PACK_RIGHT = 4 - -define FILL_NONE = 0 -define FILL_X = 1 -define FILL_Y = 2 -define FILL_BOTH = 3 - -define STICKY_NONE = 0 -define STICKY_W = 1 -define STICKY_E = 2 -define STICKY_N = 4 -define STICKY_S = 8 -define STICKY_NSEW = 15 - -_g_wl_type: [MAX_WIDGETS]int -_g_wl_side: [MAX_WIDGETS]int -_g_wl_fill: [MAX_WIDGETS]int -_g_wl_expand: [MAX_WIDGETS]int -_g_wl_padx: [MAX_WIDGETS]int -_g_wl_pady: [MAX_WIDGETS]int -_g_wl_row: [MAX_WIDGETS]int -_g_wl_col: [MAX_WIDGETS]int -_g_wl_rowspan: [MAX_WIDGETS]int -_g_wl_colspan: [MAX_WIDGETS]int -_g_wl_sticky: [MAX_WIDGETS]int -_g_layout_init: int - -_hex_tbl: [17]char = "0123456789ABCDEF" -_hex_buf: [20]char = "0000000000000000000" - -def _serial_puts(s: const str): - # S4:SERIAL_PUTS 私有 syscall 已删除,改走 /dev/serial0 设备 - devfs.serial_puts(s) - -def _ptr_to_hex(val: uint64, buf: rawptr): - buf[0] = 48 - buf[1] = 120 - i: int = 15 - while i >= 0: - nibble: int = int(val & uint64(0xF)) - buf[2 + i] = _hex_tbl[nibble] - val = val >> 4 - i -= 1 - buf[18] = 0 - -def _fill_rect(fb: uint32*, fw: int, fh: int, x1: int, y1: int, w: int, h: int, color: uint32): - if not fb: return - if x1 < 0: w += x1; x1 = 0 - if y1 < 0: h += y1; y1 = 0 - if x1 + w > fw: w = fw - x1 - if y1 + h > fh: h = fh - y1 - if w <= 0 or h <= 0: return - # 逐行批量填充(string.memset32 → rep stosd):整窗填充从"每个像素一次带边界判断的写" - # 降成"每行一次 rep stosd"。app 每帧都要把整窗填一遍背景色,逐像素在 QEMU 下是主要热点。 - py: int = y1 - while py < y1 + h: - string.memset32(rawptr(uint64(fb) + uint64((py * fw + x1) * 4)), color, uint64(w)) - py += 1 - -def _draw_hline(fb: uint32*, fw: int, fh: int, x1: int, y: int, w: int, color: uint32): - if not fb: return - if y < 0 or y >= fh: return - for px in range(x1, x1 + w): - if px >= 0 and px < fw: - fb[y * fw + px] = color - -def _draw_vline(fb: uint32*, fw: int, fh: int, x: int, y1: int, h: int, color: uint32): - if not fb: return - if x < 0 or x >= fw: return - for py in range(y1, y1 + h): - if py >= 0 and py < fh: - fb[py * fw + x] = color - -def _lighten(color: uint32, amount: int) -> uint32: - r: int = int(color & 0xFF) - g: int = int((color >> 8) & 0xFF) - b: int = int((color >> 16) & 0xFF) - r = r + ((255 - r) * amount) / 256 - g = g + ((255 - g) * amount) / 256 - b = b + ((255 - b) * amount) / 256 - return uint32(r | (g << 8) | (b << 16)) - -def _darken(color: uint32, amount: int) -> uint32: - r: int = int(color & 0xFF) - g: int = int((color >> 8) & 0xFF) - b: int = int((color >> 16) & 0xFF) - r = r - (r * amount) / 256 - g = g - (g * amount) / 256 - b = b - (b * amount) / 256 - return uint32(r | (g << 8) | (b << 16)) - - -font: [256][16]uint8 = [ - [0x00,0x00,0x00,0x00,0x00,0x40,0x55,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x00,0x00,0x55,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x00,0x02,0x56,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x00,0x42,0x56,0x42,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x55,0x55,0x57,0x71,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x57,0x54,0x57,0x71,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x57,0x54,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x57,0x51,0x52,0x72,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x57,0x55,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x57,0x55,0x57,0x71,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x56,0x51,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x54,0x54,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x50,0x50,0x57,0x74,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x51,0x51,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x57,0x55,0x57,0x74,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x53,0x54,0x56,0x74,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x37,0x15,0x15,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x32,0x16,0x12,0x12,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x20,0x6E,0x22,0x2E,0x28,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x37,0x11,0x13,0x11,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x35,0x15,0x17,0x11,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x37,0x14,0x17,0x11,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x37,0x14,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x37,0x11,0x12,0x12,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x37,0x15,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x37,0x15,0x17,0x11,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x36,0x11,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x34,0x14,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x30,0x10,0x17,0x14,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x31,0x11,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x37,0x15,0x17,0x14,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x10,0x33,0x14,0x16,0x14,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x18,0x18,0x00,0x00,0x00], - [0x6C,0x6C,0x6C,0x6C,0x48,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x36,0x36,0x36,0x7F,0x7F,0x36,0x36,0x36,0x7F,0x7F,0x36,0x36,0x36,0x00,0x00,0x00], - [0x0C,0x0C,0x3E,0x7F,0x68,0x68,0x3E,0x0B,0x0B,0x7F,0x3E,0x18,0x18,0x00,0x00,0x00], - [0x60,0x60,0x66,0x06,0x0C,0x0C,0x18,0x30,0x30,0x60,0x66,0x06,0x06,0x00,0x00,0x00], - [0x38,0x6C,0x6C,0x6C,0x6C,0x6C,0x38,0x6C,0x6D,0x66,0x66,0x6F,0x3B,0x00,0x00,0x00], - [0x18,0x18,0x18,0x18,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x1C,0x3C,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x3C,0x1C,0x00,0x00], - [0x38,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x38,0x00,0x00], - [0x00,0x00,0x18,0x18,0x7E,0x3C,0x3C,0x3C,0x7E,0x18,0x18,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x18,0x18,0x18,0x7E,0x7E,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x18,0x30,0x00], - [0x00,0x00,0x00,0x00,0x00,0x00,0x7E,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00], - [0x00,0x00,0x06,0x06,0x0C,0x0C,0x18,0x30,0x30,0x60,0x60,0x00,0x00,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x66,0x6E,0x6E,0x7E,0x76,0x76,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x18,0x38,0x78,0x78,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x06,0x06,0x0E,0x1C,0x38,0x70,0x60,0x60,0x7E,0x7E,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x06,0x06,0x1C,0x1C,0x06,0x06,0x06,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x0C,0x0C,0x1C,0x1C,0x3C,0x2C,0x6C,0x7E,0x7E,0x0C,0x0C,0x0C,0x0C,0x00,0x00,0x00], - [0x7E,0x7E,0x60,0x60,0x7C,0x7E,0x06,0x06,0x06,0x06,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x60,0x60,0x7C,0x7E,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x7E,0x7E,0x06,0x06,0x0C,0x0C,0x18,0x18,0x30,0x30,0x30,0x30,0x30,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x66,0x66,0x7E,0x3C,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x66,0x66,0x7E,0x3E,0x06,0x06,0x06,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x18,0x18,0x30,0x00], - [0x00,0x02,0x06,0x0C,0x18,0x30,0x60,0x60,0x30,0x18,0x0C,0x06,0x02,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x7E,0x7E,0x00,0x00,0x7E,0x7E,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x40,0x60,0x30,0x18,0x0C,0x06,0x06,0x0C,0x18,0x30,0x60,0x40,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x06,0x0C,0x0C,0x18,0x18,0x18,0x00,0x00,0x18,0x18,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x66,0x6E,0x6A,0x6A,0x6A,0x6E,0x60,0x60,0x7C,0x3C,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x66,0x66,0x7E,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], - [0x7C,0x7E,0x66,0x66,0x66,0x7C,0x7C,0x66,0x66,0x66,0x66,0x7E,0x7C,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x78,0x7C,0x6C,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x6C,0x7C,0x78,0x00,0x00,0x00], - [0x7E,0x7E,0x60,0x60,0x60,0x7C,0x7C,0x60,0x60,0x60,0x60,0x7E,0x7E,0x00,0x00,0x00], - [0x7E,0x7E,0x60,0x60,0x60,0x7C,0x7C,0x60,0x60,0x60,0x60,0x60,0x60,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x60,0x60,0x60,0x6E,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x66,0x66,0x66,0x66,0x66,0x7E,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], - [0x7E,0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,0x00,0x00], - [0x3E,0x3E,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x6C,0x7C,0x38,0x00,0x00,0x00], - [0x66,0x66,0x66,0x6C,0x6C,0x78,0x78,0x78,0x6C,0x6C,0x66,0x66,0x66,0x00,0x00,0x00], - [0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x7E,0x00,0x00,0x00], - [0x63,0x77,0x7F,0x7F,0x6B,0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x00,0x00,0x00], - [0x66,0x66,0x66,0x66,0x76,0x76,0x7E,0x6E,0x6E,0x66,0x66,0x66,0x66,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x7C,0x7E,0x66,0x66,0x66,0x7E,0x7C,0x60,0x60,0x60,0x60,0x60,0x60,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x6A,0x6A,0x6C,0x7E,0x36,0x00,0x00,0x00], - [0x7C,0x7E,0x66,0x66,0x66,0x7E,0x7C,0x6C,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], - [0x3C,0x7E,0x66,0x60,0x60,0x78,0x3C,0x0E,0x06,0x06,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x7E,0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00], - [0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x3C,0x18,0x18,0x00,0x00,0x00], - [0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x6B,0x6B,0x7F,0x77,0x77,0x22,0x00,0x00,0x00], - [0x66,0x66,0x66,0x66,0x3C,0x3C,0x18,0x3C,0x3C,0x66,0x66,0x66,0x66,0x00,0x00,0x00], - [0x66,0x66,0x66,0x66,0x7E,0x3C,0x3C,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00], - [0x7E,0x7E,0x06,0x06,0x0C,0x0C,0x18,0x30,0x30,0x60,0x60,0x7E,0x7E,0x00,0x00,0x00], - [0x78,0x78,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x78,0x78,0x00,0x00], - [0xC0,0xC0,0x60,0x60,0x30,0x30,0x18,0x0C,0x0C,0x06,0x06,0x03,0x03,0x00,0x00,0x00], - [0x1E,0x1E,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x1E,0x1E,0x00,0x00], - [0x3C,0x7E,0x66,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00], - [0x30,0x38,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x3C,0x3E,0x06,0x3E,0x7E,0x66,0x66,0x7E,0x3E,0x00,0x00,0x00], - [0x60,0x60,0x60,0x60,0x7C,0x7E,0x66,0x66,0x66,0x66,0x66,0x7E,0x7C,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x3C,0x7E,0x66,0x60,0x60,0x60,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x06,0x06,0x06,0x06,0x3E,0x7E,0x66,0x66,0x66,0x66,0x66,0x7E,0x3E,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x3C,0x7E,0x66,0x7E,0x7E,0x60,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x1C,0x3E,0x30,0x30,0x30,0x7C,0x7C,0x30,0x30,0x30,0x30,0x30,0x30,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x3E,0x7E,0x66,0x66,0x66,0x7E,0x3E,0x06,0x06,0x3E,0x3C,0x00], - [0x60,0x60,0x60,0x60,0x7C,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], - [0x18,0x18,0x00,0x00,0x38,0x38,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,0x00,0x00], - [0x0C,0x0C,0x00,0x00,0x3C,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x7C,0x78,0x00], - [0x60,0x60,0x60,0x60,0x66,0x66,0x6C,0x78,0x78,0x6C,0x6C,0x66,0x66,0x00,0x00,0x00], - [0x38,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x36,0x7F,0x6B,0x6B,0x63,0x63,0x63,0x63,0x63,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x7C,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x3C,0x7E,0x66,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x7C,0x7E,0x66,0x66,0x66,0x7E,0x7C,0x60,0x60,0x60,0x60,0x00], - [0x00,0x00,0x00,0x00,0x3E,0x7E,0x66,0x66,0x66,0x7E,0x3E,0x06,0x06,0x07,0x07,0x00], - [0x00,0x00,0x00,0x00,0x6C,0x7E,0x76,0x60,0x60,0x60,0x60,0x60,0x60,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x3C,0x7E,0x62,0x70,0x3C,0x0E,0x46,0x7E,0x3C,0x00,0x00,0x00], - [0x30,0x30,0x30,0x30,0x7C,0x7C,0x30,0x30,0x30,0x30,0x30,0x3C,0x1C,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x7E,0x3E,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x66,0x66,0x66,0x66,0x66,0x3C,0x3C,0x18,0x18,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x63,0x63,0x63,0x63,0x6B,0x6B,0x7F,0x7F,0x36,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x66,0x66,0x3C,0x3C,0x18,0x3C,0x3C,0x66,0x66,0x00,0x00,0x00], - [0x00,0x00,0x00,0x00,0x66,0x66,0x66,0x66,0x66,0x7E,0x3E,0x06,0x06,0x7E,0x7C,0x00], - [0x00,0x00,0x00,0x00,0x7E,0x7E,0x0C,0x0C,0x18,0x30,0x30,0x7E,0x7E,0x00,0x00,0x00], - [0x0E,0x1E,0x18,0x18,0x18,0x18,0x30,0x30,0x18,0x18,0x18,0x18,0x1E,0x0E,0x00,0x00], - [0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00], - [0x70,0x78,0x18,0x18,0x18,0x18,0x0C,0x0C,0x18,0x18,0x18,0x18,0x78,0x70,0x00,0x00], - [0x31,0x79,0x6B,0x4F,0x46,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], - [0x00,0x00,0x70,0x13,0x24,0x26,0x24,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], -] - -class TextCmd: - x: int - y: int - color: uint32 - s: [32]char - -_text_mode: int = TEXT_MODE_MANAGED -_ttf_font: fonttools.Font* = None -_ttf_atlas: uint8* = None -_ttf_state: int = 0 -_text_queue: [TEXT_QUEUE_MAX]TextCmd -_text_qn: int = 0 - -def set_text_mode(mode: int): - global _text_mode - _text_mode = mode - -def _ensure_ttf(): - global _ttf_font, _ttf_atlas, _ttf_state - if _ttf_state != 0: return - f: fonttools.Font* = fonttools.load("/FONTS/FONT.TTF") - if f is None: - _ttf_state = -1 - return - a: uint8* = fonttools.build_ascii_atlas(f, CHAR_W, CHAR_H) - if a is None: - _ttf_state = -1 - return - _ttf_font = f - _ttf_atlas = a - _ttf_state = 1 - -def _queue_text(x: int, y: int, s: const str, color: uint32): - global _text_qn - if _text_mode != TEXT_MODE_MANAGED: return - cx: int = x - cy: int = y - i: int = 0 - while s[i] != 0: - if s[i] == 10: - cx = x - cy += CHAR_H - i += 1 - continue - if _text_qn >= TEXT_QUEUE_MAX: return - cmd: TextCmd* = &_text_queue[_text_qn] - cmd.x = cx - cmd.y = cy - cmd.color = color - n: int = 0 - while n < TEXT_CHUNK and s[i] != 0 and s[i] != 10: - cmd.s[n] = s[i] - n += 1 - i += 1 - cmd.s[n] = 0 - _text_qn += 1 - cx += n * CHAR_W - -def _flush_text(win_id: int): - global _text_qn - if _text_qn == 0: return - for i in range(_text_qn): - cmd: TextCmd* = &_text_queue[i] - window.text(win_id, cmd.x, cmd.y, cmd.color, &cmd.s[0]) - _text_qn = 0 - -def _draw_char(buf: uint32*, bw: int, bh: int, x: int, y: int, ch: char, color: uint32): - if not buf: return - if bw <= 0 or bh <= 0: return - ci: int = int(uint8(ch)) - if _text_mode == TEXT_MODE_DIRECT: - _ensure_ttf() - if _ttf_state == 1 and ci < 128: - fy: int = 0 - while fy < CHAR_H: - py: int = y + fy - if py >= 0 and py < bh: - fx: int = 0 - while fx < CHAR_W: - a: int = int(_ttf_atlas[(ci * CHAR_H + fy) * CHAR_W + fx]) - if a != 0: - px: int = x + fx - if px >= 0 and px < bw: - idx: int = py * bw + px - if a >= 255: - buf[idx] = color - else: - dst: uint32 = buf[idx] - dr: int = int(dst & 0xFF) - dg: int = int((dst >> 8) & 0xFF) - db: int = int((dst >> 16) & 0xFF) - sr: int = int(color & 0xFF) - sg: int = int((color >> 8) & 0xFF) - sb: int = int((color >> 16) & 0xFF) - r: int = (sr * a + dr * (255 - a)) / 255 - g: int = (sg * a + dg * (255 - a)) / 255 - b: int = (sb * a + db * (255 - a)) / 255 - buf[idx] = uint32(r) | (uint32(g) << 8) | (uint32(b) << 16) - fx += 1 - fy += 1 - return - if ci >= 128: ci = 0 - fy: int = 0 - while fy < CHAR_H: - if y + fy < 0 or y + fy >= bh: - fy += 1 - continue - row: uint8 = font[ci][fy] - if row == 0: - fy += 1 - continue - for fx in range(CHAR_W): - if row & (0x80 >> fx): - px: int = x + fx - if px >= 0 and px < bw: - buf[(y + fy) * bw + px] = color - fy += 1 - -def _draw_text(buf: uint32*, bw: int, bh: int, x: int, y: int, s: const str, color: uint32): - if not buf: return - if bw <= 0 or bh <= 0: return - if _text_mode == TEXT_MODE_MANAGED: - _queue_text(x, y, s, color) - return - cx: int = x - cy: int = y - i: int = 0 - while True: - ch: char = s[i] - if ch == 0: break - if ch == 10: - cy += CHAR_H - cx = x - else: - _draw_char(buf, bw, bh, cx, cy, ch, color) - cx += CHAR_W - i += 1 - -# ============================================================================ -# tkinter 风格的抽象基类层次(仿 tkinter 原始抽象) -# -# Misc(mixin:master/tk 公共状态 + winfo_master/toplevel、bind 等公共方法) -# ├─ Tk(Wm, Misc) 主窗口类(Wm 声明 WM 协议面,Tk 提供落地实现) -# └─ BaseWidget(Misc) -# └─ Widget(BaseWidget) 所有普通控件的抽象基类(Button/Label/Entry… 都继承 Widget) -# -# 字段模型(与 Test/MultiInheritTest 一致):父类字段被**展平**进子类,子类自己的方法可 -# 直接访问全部展平字段;字段偏移按"定义该方法的那一层"自己的展平布局计算(该测试里 -# ColoredCircle(ShapeM, ColorM) = (ptr, i32, f64):偏移 0 是唯一的共享 vptr,各基类字段依次 -# 跟在后面,虚表被合并)。由此有三条硬约束: -# ① Misc 的字段必须在所有派生类里落到同一偏移 —— 偏移 0 被共享 vptr 占用,Misc 的字段 -# 从 8 起,只要它前面没有"带字段的基类"就成立; -# ② 因此 `Wm`(Tk 的最左基类)必须**无字段**,否则会把 Misc 的字段整体后移,而 Misc 自己 -# 的方法仍按自身布局取偏移 ⇒ 静默错位; -# ③ 基类方法里**不要写 `self.<虚拟方法>()` 的自我分派**:多继承下非最左基类的方法在派生 -# 类的合并虚表里整体后移,而方法体内按"定义类自己的虚表"取索引 ⇒ 会指到别的槽位。 -# 所以 winfo_*/title 这类"需要看具体对象状态"的方法一律在 Misc / Wm 里只做声明,由 -# Tk / Widget 覆写实现;外部按具体类型调用即可正常走虚表。 -# ============================================================================ - -# ---- bind 回调注册表:按 (对象, 事件类型) 记录回调。 -# tkinter 用 这类字符串作为 sequence,这里改用 window.EVENT_TYPE_* 整型 -# (本运行时的事件类型就是这些整型,比较字符串在 VPC 模型下既慢又易错)。 -define MAX_BINDINGS = 64 - -_bind_obj: [MAX_BINDINGS]Misc* = [None] -_bind_seq: [MAX_BINDINGS]int = [0] -_bind_cb: [MAX_BINDINGS]rawptr = [None] - -def _bind_add(obj: Misc*, seq: int, func: rawptr) -> int: - i: int = 0 - while i < MAX_BINDINGS: - if _bind_obj[i] is None: - _bind_obj[i] = obj - _bind_seq[i] = seq - _bind_cb[i] = func - return i - i += 1 - return -1 - -def _bind_remove(obj: Misc*, seq: int) -> int: - i: int = 0 - while i < MAX_BINDINGS: - if _bind_obj[i] is obj and _bind_seq[i] == seq: - _bind_obj[i] = None - _bind_cb[i] = None - return 0 - i += 1 - return -1 - -def _bind_invoke(obj: Misc*, seq: int, evt: window.InputEvent*) -> int: - i: int = 0 - while i < MAX_BINDINGS: - if _bind_obj[i] is obj and _bind_seq[i] == seq: - cb: rawptr = _bind_cb[i] - if cb is None: return 0 - # 与 Button 回调同一约定:rawptr 是 VPC 生成的闭包对象 {fn, env}, - # fn 期望 rdi = env ⇒ 必须取 [cb+8] 传 rdi、再由 [cb] 调用。 - __asm__(f"""push r12 -push r13 -mov r13, {asminp(cb, ASM_DESCR.REG_ANY)} -mov rdi, qword ptr [r13 + 8] -mov r12, rsp -and rsp, -16 -call qword ptr [r13] -mov rsp, r12 -pop r13 -pop r12""", - op=[ASM_DESCR.CLOBBER_MEMORY, - ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, - ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, - ASM_DESCR.CLOBBER_RDI, - ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, - ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) - return 1 - i += 1 - return 0 - -# ---- Misc:Tk 与所有控件的公共 mixin ---- -# 只放"所有 Tk 对象都有的东西":公共状态(master/tk)、公共方法(winfo_master/toplevel、bind), -# 以及需要各具体类自己回答的查询(winfo_x/… 这里只声明,由 Tk / Widget 覆写)。 -# 约束:这里**不做 `self.<虚拟方法>()` 的自我分派**。多继承下非最左基类的方法在派生类的 -# 合并虚表里会整体后移,而方法体内按"定义类自己的虚表"取索引 ⇒ 会指到别的槽位。 -# 因此查询一律由具体类覆写、由外部按具体类型调用;Misc 自身只用自己声明的字段。 -virtual class Misc: - master: Misc* # 父容器;主窗口为 None - tk: Tk* # 所属顶层窗口;主窗口指向自身 - - # 公共状态初始化(各具体类的 __init__ 调用;不依赖基类 __init__ 链) - def _misc_setup(self, master: Misc* = None): - self.master = master - self.tk = None - if master is not None: - self.tk = master.tk - - # ---- winfo 系(tkinter Misc.winfo_* 的对应物)---- - def winfo_master(self) -> Misc*: - return self.master - - def winfo_toplevel(self) -> Tk*: - return self.tk - - # ---- bind 系(回调在 BaseWidget.dispatch_event 里派发)---- - def bind(self, seq: int, func: rawptr) -> int: - return _bind_add(Misc*(self), seq, func) - - def unbind(self, seq: int) -> int: - return _bind_remove(Misc*(self), seq) - - def _invoke_binding(self, seq: int, evt: window.InputEvent*) -> int: - return _bind_invoke(Misc*(self), seq, evt) - - # ---- 查询/生命周期:抽象,由 Tk / Widget 覆写 ---- - def winfo_exists(self) -> int: - return 0 - - def winfo_ismapped(self) -> int: - return 0 - - def winfo_x(self) -> int: - return 0 - - def winfo_y(self) -> int: - return 0 - - def winfo_width(self) -> int: - return 0 - - def winfo_height(self) -> int: - return 0 - - def destroy(self): - pass - - def update(self): - pass - -# ---- Wm:顶层窗口的"窗口管理器协议"(tkinter Wm 的对应物)---- -# 无字段(见文件头的布局约束),这里只**声明**协议面,落地实现由持有窗口状态的 Tk 覆写。 -# 同样不做自我分派(理由见 Misc 上方注释)。 -virtual class Wm: - def title(self, text: str): - pass - - def geometry(self, x: int, y: int, w: int, h: int): - pass - - def resizable(self, rw: int, rh: int): - pass - -# ---- BaseWidget:控件公共部分(tkinter BaseWidget 的对应物)---- -virtual class BaseWidget(Misc): - # 事件派发:先跑 bind 注册的回调,再交给控件自己的 handle_event。 - # 放在基类是为了让 Tk 的事件循环只调这一个入口,各控件无需关心 bind。 - def dispatch_event(self, evt: window.InputEvent*) -> int: - hit: int = self._invoke_binding(int(evt.type), evt) - handled: int = self.handle_event(evt) - if hit or handled: - return 1 - return 0 - - # 控件自身的事件处理(抽象,由具体控件覆写) - def handle_event(self, evt: window.InputEvent*) -> int: - return 0 - - # 抽象:具体控件在 Widget 层覆写(visible 是 Widget 的字段,不能在这里访问) - def destroy(self): - pass - -def _register_widget(master: Misc*, widget: Widget*) -> int: - tk: Tk* = master.tk - if tk is None: return -1 - idx: int = tk.widget_count - if idx < MAX_WIDGETS: - tk.widgets[idx] = widget - tk.widget_count = idx + 1 - return idx - -virtual class Widget(BaseWidget): - x: int - y: int - w: int - h: int - visible: int - widget_type: int - focused: int - hovered: int - - def destroy(self): - self.visible = 0 - - # ---- Misc 查询的覆写:控件用自己的几何信息回答 winfo_* ---- - def winfo_exists(self) -> int: - return self.visible - - def winfo_ismapped(self) -> int: - return self.visible - - def winfo_x(self) -> int: - return self.x - - def winfo_y(self) -> int: - return self.y - - def winfo_width(self) -> int: - return self.w - - def winfo_height(self) -> int: - return self.h - - def draw(self, fb: uint32*, fw: int, fh: int): - pass - - def handle_event(self, evt: window.InputEvent*) -> int: - return 0 - - def contains(self, px: int, py: int) -> int: - if px >= self.x and px < self.x + self.w and py >= self.y and py < self.y + self.h: - return 1 - return 0 - - def place(self, x: int = 0, y: int = 0, w: int = 0, h: int = 0): - self.x = x - self.y = y - if w > 0: - self.w = w - if h > 0: - self.h = h - -# 容器中坐标叠加:master 不是顶层窗口(即 Frame 这类容器)时,子控件先按 Tk 的口径算位置, -# 再叠加容器自身原点 ⇒ 视觉上落在容器内部。本运行时的 pack/grid 是 Tk 级、按全局下标走, -# 所以这里只做"原点叠加"这一层,不做真正的嵌套布局。 -def _container_offset(root: Tk*, w: Widget*): - if w.master is None: return - if uint64(w.master) == uint64(root): return - pw: Widget* = Widget*(w.master) - if pw.widget_type == WIDGET_FRAME: - w.x += pw.x - w.y += pw.y - -# ---- Frame:容器控件(tkinter Frame 的对应物)---- -# 可作 master 使用:子控件经 _misc_setup 继承它的 tk,照常注册进顶层窗口;位置再叠加本 -# 容器原点(见 _container_offset)。 -virtual class Frame(Widget): - fill_color: uint32 - border_color: uint32 - border_w: int - - def __init__(self, master: Misc*, w: int, h: int): - self.x = 0 - self.y = 0 - self.w = w - self.h = h - self.visible = 1 - self._misc_setup(master) - self.widget_type = WIDGET_FRAME - self.focused = 0 - self.hovered = 0 - self.fill_color = uint32(0xFF2A2A3A) - self.border_color = uint32(0xFF505070) - self.border_w = 1 - _register_widget(master, Widget*(self)) - - def draw(self, fb: uint32*, fw: int, fh: int): - _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, self.fill_color) - if self.border_w > 0: - _draw_hline(fb, fw, fh, self.x, self.y, self.w, self.border_color) - _draw_hline(fb, fw, fh, self.x, self.y + self.h - 1, self.w, self.border_color) - _draw_vline(fb, fw, fh, self.x, self.y, self.h, self.border_color) - _draw_vline(fb, fw, fh, self.x + self.w - 1, self.y, self.h, self.border_color) - - def handle_event(self, evt: window.InputEvent*) -> int: - return 0 - - def config(self, fill_color: uint32 = None, border_color: uint32 = None): - if fill_color is not None: - self.fill_color = fill_color - if border_color is not None: - self.border_color = border_color - -virtual class Label(Widget): - text: [64]char - color: uint32 - - def __init__(self, master: Misc*, text: str, color: uint32): - self.x = 0 - self.y = 0 - self.w = 0 - self.h = 0 - self.visible = 1 - self._misc_setup(master) - self.widget_type = WIDGET_LABEL - self.focused = 0 - self.hovered = 0 - self.color = color - i: int = 0 - for ch in text: - if i >= 63: break - self.text[i] = ch - i += 1 - self.text[i] = 0 - # Label 没有显式 w/h 参数:自然尺寸必须由文本推出。否则 pack 拿到的请求尺寸是 - # 0×0 ⇒ 不占布局空间,且 PACK_BOTTOM/非 fill 时会被贴到可视区域之外(文字被裁掉)。 - self._fit_natural() - _register_widget(master, Widget*(self)) - - # 按当前 text 计算自然尺寸:宽取最长行,高取行数(多行 Label 也成立)。 - def _fit_natural(self): - line_chars: int = 0 - max_chars: int = 0 - lines: int = 1 - i: int = 0 - while i < 64: - ch: char = self.text[i] - if ch == 0: break - if ch == 10: - if line_chars > max_chars: max_chars = line_chars - line_chars = 0 - lines += 1 - else: - line_chars += 1 - i += 1 - if line_chars > max_chars: max_chars = line_chars - self.w = max_chars * CHAR_W - self.h = lines * CHAR_H - - def draw(self, fb: uint32*, fw: int, fh: int): - _draw_text(fb, fw, fh, self.x, self.y, &self.text[0], self.color) - - def handle_event(self, evt: window.InputEvent*) -> int: - return 0 - - def config(self, text: const str = None, color: uint32 = None): - if text is not None: - i: int = 0 - while i < 63: - ch: char = *(text + i) - self.text[i] = ch - if ch == 0: break - i += 1 - self.text[i] = 0 - self._fit_natural() - if color is not None: - self.color = color - -virtual class Button(Widget): - text: [32]char - fill_color: uint32 - text_color: uint32 - pressed: int - callback: rawptr - - def __init__(self, master: Misc*, w: int, h: int, text: str, callback: rawptr): - self.x = 0 - self.y = 0 - self.w = w - self.h = h - self.visible = 1 - self._misc_setup(master) - self.widget_type = WIDGET_BUTTON - self.focused = 0 - self.hovered = 0 - self.fill_color = uint32(0x3C3C5A) - self.text_color = uint32(0xFFFFFFFF) - self.pressed = 0 - self.callback = callback - i: int = 0 - for ch in text: - if i >= 31: break - self.text[i] = ch - i += 1 - self.text[i] = 0 - _register_widget(master, Widget*(self)) - - def draw(self, fb: uint32*, fw: int, fh: int): - fill: uint32 = self.fill_color - border: uint32 = uint32(0x8080B0) - if self.pressed: - fill = _lighten(self.fill_color, 60) - border = uint32(0xA0A0D0) - elif self.hovered: - fill = _lighten(self.fill_color, 30) - border = uint32(0xA0A0D0) - _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, fill) - top_left: uint32 = _lighten(fill, 80) - bottom_right: uint32 = _darken(fill, 80) - if self.pressed: - top_left = _darken(fill, 60) - bottom_right = _lighten(fill, 40) - _draw_hline(fb, fw, fh, self.x, self.y, self.w, top_left) - _draw_hline(fb, fw, fh, self.x, self.y + 1, self.w, top_left) - _draw_vline(fb, fw, fh, self.x, self.y, self.h, top_left) - _draw_vline(fb, fw, fh, self.x + 1, self.y, self.h, top_left) - _draw_hline(fb, fw, fh, self.x, self.y + self.h - 1, self.w, bottom_right) - _draw_hline(fb, fw, fh, self.x, self.y + self.h - 2, self.w, bottom_right) - _draw_vline(fb, fw, fh, self.x + self.w - 1, self.y, self.h, bottom_right) - _draw_vline(fb, fw, fh, self.x + self.w - 2, self.y, self.h, bottom_right) - _draw_hline(fb, fw, fh, self.x - 1, self.y - 1, self.w + 2, border) - _draw_hline(fb, fw, fh, self.x - 1, self.y + self.h, self.w + 2, border) - _draw_vline(fb, fw, fh, self.x - 1, self.y - 1, self.h + 2, border) - _draw_vline(fb, fw, fh, self.x + self.w, self.y - 1, self.h + 2, border) - tl: int = int(string.strlen(&self.text[0])) - tx: int = self.x + (self.w - 8 * tl) / 2 - ty: int = self.y + (self.h - CHAR_H) / 2 - _draw_text(fb, fw, fh, tx, ty, &self.text[0], self.text_color) - - def handle_event(self, evt: window.InputEvent*) -> int: - etype: uint32 = evt.type - ex: int = evt.x - ey: int = evt.y - if etype == window.EVENT_TYPE_HOVER: - if self.contains(ex, ey): - if not self.hovered: - self.hovered = 1 - return 1 - else: - if self.hovered and not self.pressed: - self.hovered = 0 - return 1 - elif etype == window.EVENT_TYPE_HOVER_LEAVE: - if self.hovered: - self.hovered = 0 - return 1 - elif etype == window.EVENT_TYPE_CLICK: - if self.contains(ex, ey): - self.pressed = 1 - return 1 - elif etype == window.EVENT_TYPE_RELEASE: - if self.pressed: - self.pressed = 0 - if self.contains(ex, ey) and self.callback is not None: - # 闭包调用约定(VPC 生成):callback 是堆上的 {fn, env} 对象,fn 期望 rdi = env。 - # 之前直接 `call r13`(r13 = 闭包对象地址)会跳到堆上执行 - # (现场:rip=0x2040400a 在堆区、cr2=0x1、err=0x5)。 - __asm__(f"""push r12 -push r13 -mov r13, {asminp(self.callback, ASM_DESCR.REG_ANY)} -mov rdi, qword ptr [r13 + 8] -mov r12, rsp -and rsp, -16 -call qword ptr [r13] -mov rsp, r12 -pop r13 -pop r12""", - op=[ASM_DESCR.CLOBBER_MEMORY, - ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, - ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, - ASM_DESCR.CLOBBER_RDI, - ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, - ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) - return 1 - return 0 - - def config(self, text: const str = None, fill_color: uint32 = None, text_color: uint32 = None, callback: rawptr = None): - if text is not None: - i: int = 0 - while i < 31: - ch: char = *(text + i) - self.text[i] = ch - if ch == 0: break - i += 1 - self.text[i] = 0 - if fill_color is not None: - self.fill_color = fill_color - if text_color is not None: - self.text_color = text_color - if callback is not None: - self.callback = callback - -virtual class Entry(Widget): - text: [64]char - text_len: int - cursor_pos: int - max_len: int - bg_color: uint32 - border_color: uint32 - text_color: uint32 - cursor_color: uint32 - - def __init__(self, master: Misc*, w: int, h: int): - self.x = 0 - self.y = 0 - self.w = w - self.h = h - self.visible = 1 - self._misc_setup(master) - self.widget_type = WIDGET_ENTRY - self.focused = 0 - self.hovered = 0 - self.text_len = 0 - self.cursor_pos = 0 - self.max_len = 30 - self.bg_color = uint32(0xFF231919) - self.border_color = uint32(0x606080) - self.text_color = uint32(0x000000) - self.cursor_color = uint32(0x000000) - for i in range(64): - self.text[i] = 0 - _register_widget(master, Widget*(self)) - - def draw(self, fb: uint32*, fw: int, fh: int): - bg: uint32 = self.bg_color - bdr: uint32 = self.border_color - if self.focused: - bg = uint32(0xFFFFFF) - bdr = uint32(0x4488FF) - elif self.hovered: - bdr = uint32(0x8080B0) - _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, bg) - _draw_hline(fb, fw, fh, self.x, self.y, self.w, bdr) - _draw_hline(fb, fw, fh, self.x, self.y + self.h - 1, self.w, bdr) - _draw_vline(fb, fw, fh, self.x, self.y, self.h, bdr) - _draw_vline(fb, fw, fh, self.x + self.w - 1, self.y, self.h, bdr) - if self.text_len > 0: - tc: uint32 = self.text_color - if not self.focused: - tc = uint32(0xCCCCCC) - _draw_text(fb, fw, fh, self.x + 6, self.y + 7, &self.text[0], tc) - elif not self.focused: - _draw_text(fb, fw, fh, self.x + 6, self.y + 7, "Type here...", uint32(0x888888)) - if self.focused: - cx_pos: int = self.x + 6 + self.cursor_pos * CHAR_W - if cx_pos < self.x + self.w - 2: - _draw_vline(fb, fw, fh, cx_pos, self.y + 5, self.h - 10, self.cursor_color) - - def handle_event(self, evt: window.InputEvent*) -> int: - etype: uint32 = evt.type - ex: int = evt.x - ey: int = evt.y - if etype == window.EVENT_TYPE_HOVER: - if self.contains(ex, ey): - if not self.hovered: - self.hovered = 1 - return 1 - else: - if self.hovered: - self.hovered = 0 - return 1 - elif etype == window.EVENT_TYPE_HOVER_LEAVE: - if self.hovered: - self.hovered = 0 - return 1 - elif etype == window.EVENT_TYPE_CLICK: - if self.contains(ex, ey): - if not self.focused: - self.focused = 1 - tk4: Tk* = self.tk - tk4.focused_widget = Widget*(self) - rel_x: int = ex - self.x - 6 - if rel_x < 0: rel_x = 0 - new_pos: int = (rel_x + CHAR_W / 2) / CHAR_W - if new_pos > self.text_len: new_pos = self.text_len - self.cursor_pos = new_pos - return 1 - else: - if self.focused: - self.focused = 0 - tk5: Tk* = self.tk - tk5.focused_widget = None - return 1 - elif etype == window.EVENT_TYPE_KEY: - if self.focused: - ebtn: uint8 = evt.button - if ebtn == window.KEY_STATE_RELEASE: - return 0 - ch: int = ex - if ch == 8: - if self.cursor_pos > 0: - for i in range(self.cursor_pos, self.text_len): - self.text[i - 1] = self.text[i] - self.text_len -= 1 - self.cursor_pos -= 1 - self.text[self.text_len] = 0 - return 1 - elif ch == window.VK_LEFT: - if self.cursor_pos > 0: - self.cursor_pos -= 1 - return 1 - elif ch == window.VK_RIGHT: - if self.cursor_pos < self.text_len: - self.cursor_pos += 1 - return 1 - elif ch == window.VK_HOME: - self.cursor_pos = 0 - return 1 - elif ch == window.VK_END: - self.cursor_pos = self.text_len - return 1 - elif ch == window.VK_DELETE: - if self.cursor_pos < self.text_len: - for i in range(self.cursor_pos, self.text_len - 1): - self.text[i] = self.text[i + 1] - self.text_len -= 1 - self.text[self.text_len] = 0 - return 1 - elif ch >= 32 and ch < 127: - if self.text_len < self.max_len: - i: int = self.text_len - while i > self.cursor_pos: - self.text[i] = self.text[i - 1] - i -= 1 - self.text[self.cursor_pos] = char(ch) - self.text_len += 1 - self.cursor_pos += 1 - self.text[self.text_len] = 0 - return 1 - return 0 - - def get(self) -> const str: - return &self.text[0] - - def delete(self, start: int, end: int): - self.text_len = 0 - for i in range(64): - self.text[i] = 0 - - def config(self, bg_color: uint32 = None, text_color: uint32 = None, border_color: uint32 = None): - if bg_color is not None: - self.bg_color = bg_color - if text_color is not None: - self.text_color = text_color - if border_color is not None: - self.border_color = border_color - -virtual class Scrollbar(Widget): - orient: int - command: rawptr - slider_first: int - slider_last: int - action: int - action_value: int - pressed: int - dragging: int - drag_start_y: int - drag_start_first: int - trough_color: uint32 - slider_color: uint32 - arrow_color: uint32 - slider_hover: int - - def __init__(self, master: Misc*, w: int, h: int, orient: int): - self.x = 0 - self.y = 0 - self.w = w - self.h = h - self.visible = 1 - self._misc_setup(master) - self.widget_type = WIDGET_SCROLLBAR - self.focused = 0 - self.hovered = 0 - self.orient = orient - self.command = None - self.slider_first = 0 - self.slider_last = 1000 - self.action = 0 - self.action_value = 0 - self.pressed = 0 - self.dragging = 0 - self.drag_start_y = 0 - self.drag_start_first = 0 - self.trough_color = uint32(0x1E1E2E) - self.slider_color = uint32(0x4A4A6A) - self.arrow_color = uint32(0xAAAAAA) - self.slider_hover = 0 - _register_widget(master, Widget*(self)) - - def draw(self, fb: uint32*, fw: int, fh: int): - _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, self.trough_color) - if self.orient != VERTICAL: return - arrow_sz: int = self.w - if arrow_sz > 16: arrow_sz = 16 - self._draw_arrow_btn(fb, fw, fh, self.x, self.y, self.w, arrow_sz, 0, 1 if self.pressed == 1 else 0) - self._draw_arrow_btn(fb, fw, fh, self.x, self.y + self.h - arrow_sz, self.w, arrow_sz, 1, 1 if self.pressed == 2 else 0) - trough_y: int = self.y + arrow_sz - trough_h: int = self.h - 2 * arrow_sz - if trough_h <= 0: return - if self.slider_last > self.slider_first: - sl_h: int = (self.slider_last - self.slider_first) * trough_h / 1000 - if sl_h < 8: sl_h = 8 - sl_y: int = trough_y + self.slider_first * trough_h / 1000 - if sl_y + sl_h > trough_y + trough_h: - sl_y = trough_y + trough_h - sl_h - if sl_y < trough_y: sl_y = trough_y - color: uint32 = self.slider_color - if self.dragging: - color = _lighten(self.slider_color, 40) - elif self.slider_hover: - color = _lighten(self.slider_color, 20) - _fill_rect(fb, fw, fh, self.x + 1, sl_y, self.w - 2, sl_h, color) - top_c: uint32 = _lighten(color, 60) - bot_c: uint32 = _darken(color, 60) - _draw_hline(fb, fw, fh, self.x + 1, sl_y, self.w - 2, top_c) - _draw_hline(fb, fw, fh, self.x + 1, sl_y + sl_h - 1, self.w - 2, bot_c) - _draw_vline(fb, fw, fh, self.x + 1, sl_y, sl_h, top_c) - _draw_vline(fb, fw, fh, self.x + self.w - 2, sl_y, sl_h, bot_c) - - def _draw_arrow_btn(self, fb: uint32*, fw: int, fh: int, bx: int, by: int, bw: int, bh: int, direction: int, is_pressed: int): - color: uint32 = self.slider_color - if is_pressed: - color = _lighten(self.slider_color, 30) - _fill_rect(fb, fw, fh, bx, by, bw, bh, color) - top_c: uint32 = _lighten(color, 60) - bot_c: uint32 = _darken(color, 60) - if is_pressed: - top_c = _darken(color, 40) - bot_c = _lighten(color, 30) - _draw_hline(fb, fw, fh, bx, by, bw, top_c) - _draw_hline(fb, fw, fh, bx, by + bh - 1, bw, bot_c) - _draw_vline(fb, fw, fh, bx, by, bh, top_c) - _draw_vline(fb, fw, fh, bx + bw - 1, by, bh, bot_c) - cx: int = bx + bw / 2 - cy: int = by + bh / 2 - if direction == 0: - _fill_rect(fb, fw, fh, cx - 1, cy - 3, 3, 1, self.arrow_color) - _fill_rect(fb, fw, fh, cx - 2, cy - 2, 5, 1, self.arrow_color) - _fill_rect(fb, fw, fh, cx - 3, cy - 1, 7, 1, self.arrow_color) - else: - _fill_rect(fb, fw, fh, cx - 3, cy - 1, 7, 1, self.arrow_color) - _fill_rect(fb, fw, fh, cx - 2, cy, 5, 1, self.arrow_color) - _fill_rect(fb, fw, fh, cx - 1, cy + 1, 3, 1, self.arrow_color) - - def handle_event(self, evt: window.InputEvent*) -> int: - etype: uint32 = evt.type - ex: int = evt.x - ey: int = evt.y - if etype == window.EVENT_TYPE_HOVER: - if self.dragging: - self._handle_drag(ey) - return 1 - if self.contains(ex, ey): - if not self.slider_hover: - self.slider_hover = 1 - return 1 - else: - if self.slider_hover and self.pressed == 0: - self.slider_hover = 0 - return 1 - return 0 - elif etype == window.EVENT_TYPE_HOVER_LEAVE: - if self.slider_hover: - self.slider_hover = 0 - if not self.dragging: - self.pressed = 0 - return 1 - return 0 - elif etype == window.EVENT_TYPE_CLICK: - if self.contains(ex, ey): - if self.orient == VERTICAL: - arrow_sz: int = self.w - if arrow_sz > 16: arrow_sz = 16 - if ey < self.y + arrow_sz: - self.pressed = 1 - self.action = 1 - self._fire_command() - return 1 - elif ey >= self.y + self.h - arrow_sz: - self.pressed = 2 - self.action = 2 - self._fire_command() - return 1 - else: - trough_y: int = self.y + arrow_sz - trough_h: int = self.h - 2 * arrow_sz - if trough_h <= 0: return 1 - sl_h: int = (self.slider_last - self.slider_first) * trough_h / 1000 - if sl_h < 8: sl_h = 8 - sl_y: int = trough_y + self.slider_first * trough_h / 1000 - if sl_y + sl_h > trough_y + trough_h: - sl_y = trough_y + trough_h - sl_h - if ey >= sl_y and ey < sl_y + sl_h: - self.pressed = 4 - self.dragging = 1 - self.drag_start_y = ey - self.drag_start_first = self.slider_first - else: - self.pressed = 3 - if ey < sl_y: - self.action = 3 - else: - self.action = 4 - self._fire_command() - return 1 - return 0 - elif etype == window.EVENT_TYPE_RELEASE: - if self.pressed: - self.pressed = 0 - self.dragging = 0 - return 1 - return 0 - return 0 - - def _handle_drag(self, ey: int): - arrow_sz: int = self.w - if arrow_sz > 16: arrow_sz = 16 - trough_y: int = self.y + arrow_sz - trough_h: int = self.h - 2 * arrow_sz - if trough_h <= 0: return - delta_y: int = ey - self.drag_start_y - delta_first: int = delta_y * 1000 / trough_h - new_first: int = self.drag_start_first + delta_first - slider_size: int = self.slider_last - self.slider_first - if slider_size <= 0: slider_size = 1 - if new_first < 0: new_first = 0 - if new_first + slider_size > 1000: new_first = 1000 - slider_size - if new_first != self.slider_first: - self.slider_first = new_first - self.slider_last = new_first + slider_size - self.action = 5 - self.action_value = new_first - self._fire_command() - - def _fire_command(self): - if self.command is not None: - # 同 Button.handle_event:command 是闭包 {fn, env},必须 rdi = env 后 call fn。 - __asm__(f"""push r12 -push r13 -mov r13, {asminp(self.command, ASM_DESCR.REG_ANY)} -mov rdi, qword ptr [r13 + 8] -mov r12, rsp -and rsp, -16 -call qword ptr [r13] -mov rsp, r12 -pop r13 -pop r12""", - op=[ASM_DESCR.CLOBBER_MEMORY, - ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, - ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, - ASM_DESCR.CLOBBER_RDI, - ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, - ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) - - def set(self, first: int, last: int): - self.slider_first = first - self.slider_last = last - -virtual class FBCanvas(Widget): - _fb: uint32* - _cw: int - _ch: int - - def __init__(self, master: Misc*, w: int, h: int): - self.x = 0 - self.y = 0 - self.w = w - self.h = h - self.visible = 1 - self._misc_setup(master) - self.widget_type = WIDGET_FBCANVAS - self.focused = 0 - self.hovered = 0 - self._fb = None - self._cw = w - self._ch = h - _register_widget(master, Widget*(self)) - - def draw(self, fb: uint32*, fw: int, fh: int): - if not self._fb: return - if self._cw <= 0 or self._ch <= 0: return - copy_w: int = self._cw - if copy_w > self.w: copy_w = self.w - copy_h: int = self._ch - if copy_h > self.h: copy_h = self.h - for py in range(copy_h): - ty: int = self.y + py - if ty < 0 or ty >= fh: continue - for px in range(copy_w): - tx: int = self.x + px - if tx < 0 or tx >= fw: continue - fb[ty * fw + tx] = self._fb[py * self._cw + px] - - def handle_event(self, evt: window.InputEvent*) -> int: - return 0 - - def set_fb(self, fb: uint32*, cw: int, ch: int): - self._fb = fb - self._cw = cw - self._ch = ch - - def get_fb(self) -> uint32*: - return self._fb - - def get_w(self) -> int: - return self._cw - - def get_h(self) -> int: - return self._ch - -virtual class Text(Widget): - lines: [TEXT_MAX_LINES][80]char - line_count: int - input_buf: [80]char - input_len: int - cursor_pos: int - prompt: [80]char - bg_color: uint32 - text_color: uint32 - prompt_color: uint32 - cursor_color: uint32 - border_color: uint32 - on_command: rawptr - history: [16][80]char - history_count: int - history_idx: int - scroll_offset: int - max_lines: int - yscrollbar: rawptr - - def __init__(self, master: Misc*, w: int, h: int): - self.x = 0 - self.y = 0 - self.w = w - self.h = h - self.visible = 1 - self._misc_setup(master) - self.widget_type = WIDGET_TEXT - self.focused = 0 - self.hovered = 0 - self.line_count = 0 - self.input_len = 0 - self.cursor_pos = 0 - self.prompt[0] = '>' - self.prompt[1] = ' ' - self.prompt[2] = 0 - self.bg_color = uint32(0x0C0C0C) - self.text_color = uint32(0xCCCCCC) - self.prompt_color = uint32(0xCCCCCC) - self.cursor_color = uint32(0xCCCCCC) - self.border_color = uint32(0x333333) - self.on_command = None - self.history_count = 0 - self.history_idx = 0 - self.scroll_offset = 0 - self.max_lines = TEXT_MAX_LINES - self.yscrollbar = None - for i in range(TEXT_MAX_LINES): - for j in range(79 + 1): - self.lines[i][j] = 0 - for i in range(80): - self.input_buf[i] = 0 - _register_widget(master, Widget*(self)) - - def draw(self, fb: uint32*, fw: int, fh: int): - self._update_scrollbar() - _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, self.bg_color) - _fill_rect(fb, fw, fh, self.x, self.y + self.h - 2, self.w, 2, self.border_color) - visible: int = self._visible_lines() - for i in range(visible): - line_idx: int = self.scroll_offset + i - if line_idx < self.line_count: - _draw_text(fb, fw, fh, self.x + 4, self.y + i * CHAR_H, &self.lines[line_idx][0], self.text_color) - # 输入行不是一个固定在底部的控件,而是紧跟在最后一条输出之后的一行; - # 输出增多时它随之下移,超出可视区时由 scroll_offset 把它保持在最后一行。 - prompt_row: int = self.line_count - self.scroll_offset - if prompt_row >= 0 and prompt_row < visible: - prompt_y: int = self.y + prompt_row * CHAR_H - plen: int = int(string.strlen(&self.prompt[0])) - _draw_text(fb, fw, fh, self.x + 4, prompt_y, &self.prompt[0], self.prompt_color) - if self.input_len > 0: - _draw_text(fb, fw, fh, self.x + 4 + plen * CHAR_W, prompt_y, - &self.input_buf[0], self.text_color) - cx_pos: int = self.x + 4 + (plen + self.cursor_pos) * CHAR_W - if cx_pos < self.x + self.w - 4: - _fill_rect(fb, fw, fh, cx_pos, prompt_y, CHAR_W // 2, CHAR_H, self.cursor_color) - - def handle_event(self, evt: window.InputEvent*) -> int: - etype: uint32 = evt.type - ex: int = evt.x - ey: int = evt.y - if etype == window.EVENT_TYPE_CLICK: - if self.contains(ex, ey): - if not self.focused: - self.focused = 1 - tk: Tk* = self.tk - tk.focused_widget = Widget*(self) - prompt_row: int = self.line_count - self.scroll_offset - prompt_y: int = self.y + prompt_row * CHAR_H - if prompt_row >= 0 and prompt_row < self._visible_lines() and ey >= prompt_y and ey < prompt_y + CHAR_H: - plen: int = int(string.strlen(&self.prompt[0])) - rel_x: int = ex - self.x - 4 - plen * CHAR_W - if rel_x < 0: rel_x = 0 - new_pos: int = (rel_x + CHAR_W / 2) / CHAR_W - if new_pos > self.input_len: new_pos = self.input_len - self.cursor_pos = new_pos - return 1 - else: - if self.focused: - self.focused = 0 - tk2: Tk* = self.tk - tk2.focused_widget = None - return 1 - elif etype == window.EVENT_TYPE_WHEEL: - if self.contains(ex, ey): - w: int = int(evt.button) - if w >= 128: w = w - 256 - vis: int = self._visible_lines() - self.scroll_offset = self.scroll_offset - w * 3 - max_off: int = self.line_count + 1 - vis - if max_off < 0: max_off = 0 - if self.scroll_offset < 0: self.scroll_offset = 0 - if self.scroll_offset > max_off: self.scroll_offset = max_off - self._update_scrollbar() - return 1 - return 0 - elif etype == window.EVENT_TYPE_KEY: - if self.focused: - ebtn2: uint8 = evt.button - if ebtn2 == window.KEY_STATE_RELEASE: - return 0 - ch: int = ex - if ch == 8: - if self.cursor_pos > 0: - i: int = self.cursor_pos - while i < self.input_len: - self.input_buf[i - 1] = self.input_buf[i] - i += 1 - self.input_len -= 1 - self.cursor_pos -= 1 - self.input_buf[self.input_len] = 0 - return 1 - elif ch == 13 or ch == 10: - self._exec_input() - self.input_len = 0 - self.cursor_pos = 0 - for i in range(80): - self.input_buf[i] = 0 - return 1 - elif ch == window.VK_LEFT: - if self.cursor_pos > 0: - self.cursor_pos -= 1 - return 1 - elif ch == window.VK_RIGHT: - if self.cursor_pos < self.input_len: - self.cursor_pos += 1 - return 1 - elif ch == window.VK_UP: - if self.history_count > 0: - if self.history_idx < self.history_count: - self.history_idx += 1 - hi: int = self.history_count - self.history_idx - if hi < 0: hi = 0 - for j in range(80): - self.input_buf[j] = self.history[hi][j] - self.input_len = 0 - while self.input_len < 79 and self.input_buf[self.input_len] != 0: - self.input_len += 1 - self.cursor_pos = self.input_len - return 1 - elif ch == window.VK_DOWN: - if self.history_count > 0: - if self.history_idx > 0: - self.history_idx -= 1 - if self.history_idx == 0: - self.input_len = 0 - self.cursor_pos = 0 - for j2 in range(80): - self.input_buf[j2] = 0 - else: - hi2: int = self.history_count - self.history_idx - for j3 in range(80): - self.input_buf[j3] = self.history[hi2][j3] - self.input_len = 0 - while self.input_len < 79 and self.input_buf[self.input_len] != 0: - self.input_len += 1 - self.cursor_pos = self.input_len - return 1 - elif ch == window.VK_HOME: - self.cursor_pos = 0 - return 1 - elif ch == window.VK_END: - self.cursor_pos = self.input_len - return 1 - elif ch == window.VK_DELETE: - if self.cursor_pos < self.input_len: - i: int = self.cursor_pos - while i < self.input_len - 1: - self.input_buf[i] = self.input_buf[i + 1] - i += 1 - self.input_len -= 1 - self.input_buf[self.input_len] = 0 - return 1 - elif ch == window.VK_PRIOR: - if self.scroll_offset > 0: - vis: int = self._visible_lines() - self.scroll_offset -= vis - if self.scroll_offset < 0: self.scroll_offset = 0 - self._update_scrollbar() - return 1 - elif ch == window.VK_NEXT: - vis2: int = self._visible_lines() - max_off: int = self.line_count + 1 - vis2 - if max_off < 0: max_off = 0 - if self.scroll_offset < max_off: - self.scroll_offset += vis2 - if self.scroll_offset > max_off: self.scroll_offset = max_off - self._update_scrollbar() - return 1 - elif ch >= 32 and ch < 127: - if self.input_len < 79: - i: int = self.input_len - while i > self.cursor_pos: - self.input_buf[i] = self.input_buf[i - 1] - i -= 1 - self.input_buf[self.cursor_pos] = char(ch) - self.input_len += 1 - self.cursor_pos += 1 - self.input_buf[self.input_len] = 0 - return 1 - return 0 - - def _exec_input(self): - self.insert(&self.prompt[0]) - plen: int = int(string.strlen(&self.prompt[0])) - if self.input_len > 0: - last: int = self.line_count - 1 - j: int = 0 - while j < self.input_len and plen + j < 79: - self.lines[last][plen + j] = self.input_buf[j] - j += 1 - self.lines[last][plen + j] = 0 - if self.history_count < 16: - for k in range(80): - self.history[self.history_count][k] = self.input_buf[k] - self.history_count += 1 - else: - for i in range(15): - for k2 in range(80): - self.history[i][k2] = self.history[i + 1][k2] - for k3 in range(80): - self.history[15][k3] = self.input_buf[k3] - self.history_idx = 0 - - cmd: rawptr = self.on_command - before_lines: int = self.line_count - if cmd is not None: - # 同 Button.handle_event:on_command 是闭包 {fn, env},必须 rdi = env 后 call fn。 - __asm__(f"""push r12 -push r13 -mov r13, {asminp(cmd, ASM_DESCR.REG_ANY)} -mov rdi, qword ptr [r13 + 8] -mov r12, rsp -and rsp, -16 -call qword ptr [r13] -mov rsp, r12 -pop r13 -pop r12""", - op=[ASM_DESCR.CLOBBER_MEMORY, - ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, - ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, - ASM_DESCR.CLOBBER_RDI, - ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, - ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) - else: - self.insert("(no callback)") - - # 本命令确实产生了输出时,在日志与下一个输入行之间留一个空行。 - # 这样提示符不会紧贴最后一行日志(用实际写入的行数判断,clear 不会被补空行)。 - if self.line_count > before_lines: - self.insert("\n") - - def insert(self, text: str): - start: int = 0 - pos: int = 0 - while True: - ch: char = *(text + pos) - if ch == 0: - if pos > start: - self._insert_substring(text, start, pos) - break - if ch == 10: - self._insert_substring(text, start, pos) - start = pos + 1 - pos += 1 - - def insert_ptr(self, text: const str): - start: int = 0 - pos: int = 0 - while True: - ch: char = *(text + pos) - if ch == 0: - if pos > start: - self._insert_substring(text, start, pos) - break - if ch == 10: - self._insert_substring(text, start, pos) - start = pos + 1 - pos += 1 - - def _insert_substring(self, text: const str, start: int, end: int): - if self.line_count >= self.max_lines: - for i in range(self.max_lines - 1): - for j in range(79 + 1): - self.lines[i][j] = self.lines[i + 1][j] - self.line_count -= 1 - if self.scroll_offset > 0: - self.scroll_offset -= 1 - idx: int = self.line_count - j: int = 0 - k: int = start - while k < end and j < 79: - ch: char = *(text + k) - if ch == 9: - tab_stop: int = 8 - (j % 8) - while tab_stop > 0 and j < 79: - self.lines[idx][j] = 32 - j += 1 - tab_stop -= 1 - elif ch == 13: - pass - else: - self.lines[idx][j] = ch - j += 1 - k += 1 - self.lines[idx][j] = 0 - self.line_count += 1 - vis: int = self._visible_lines() - total: int = self.line_count + 1 # 输出行 + 输入行 - if total > vis: - self.scroll_offset = total - vis - else: - self.scroll_offset = 0 - self._update_scrollbar() - - def delete(self, start: int, end: int): - self.line_count = 0 - self.scroll_offset = 0 - self._update_scrollbar() - - def get(self) -> const str: - return &self.input_buf[0] - - def set_prompt(self, text: const str): - i: int = 0 - while i < 79: - ch: char = *(text + i) - self.prompt[i] = ch - if ch == 0: break - i += 1 - self.prompt[i] = 0 - - def config(self, text_color: uint32 = None, bg_color: uint32 = None, prompt_color: uint32 = None, cursor_color: uint32 = None, border_color: uint32 = None): - if text_color is not None: - self.text_color = text_color - if bg_color is not None: - self.bg_color = bg_color - if prompt_color is not None: - self.prompt_color = prompt_color - if cursor_color is not None: - self.cursor_color = cursor_color - if border_color is not None: - self.border_color = border_color - - def _visible_lines(self) -> int: - return (self.h - 2) / CHAR_H - - def _update_scrollbar(self): - vis: int = self._visible_lines() - total: int = self.line_count + 1 # 输出行 + 输入行 - if total <= vis: - self.scroll_offset = 0 - else: - max_off: int = total - vis - if self.scroll_offset > max_off: - self.scroll_offset = max_off - if self.yscrollbar is not None: - sb: Scrollbar* = Scrollbar*(self.yscrollbar) - if total <= vis: - sb.set(0, 1000) - else: - first: int = self.scroll_offset * 1000 / total - last: int = (self.scroll_offset + vis) * 1000 / total - sb.set(first, last) - -# ---- Tk:主窗口类 = Wm(窗口管理器协议,无字段)+ Misc(公共状态/方法)---- -virtual class Tk(Wm, Misc): - win_id: int - buf: uint32* - bw: int - bh: int - style: int - bg_color: uint32 - widgets: [MAX_WIDGETS]Widget* - widget_count: int - focused_widget: Widget* - hovered_widget: Widget* - _win_x: int - _win_y: int - _title: [32]char - resizable_w: int - resizable_h: int - _grid_rows: int - _grid_cols: int - _bb: uint32* # 后备缓冲:整帧画完再一次性拷进共享 arena,避免合成器读到半成品帧 - _bb_px: int # 后备缓冲容量(像素数),窗口变大时才重新分配 - _ansi_state: int # ESC 序列解析状态:0=普通 1=已收 ESC 2=已收 ESC[ - _ansi_param: int # CSI 数值参数(如 ESC[5~ 的 5) - - def __init__(self): - global _g_layout_init - # Misc 的公共状态:主窗口没有父容器,tk 指向自身(控件经 _misc_setup 继承它) - self.master = None - self.tk = Tk*(self) - self.widget_count = 0 - self.focused_widget = None - self.hovered_widget = None - self.win_id = -1 - self._win_x = 0 - self._win_y = 0 - self.style = 0 - self.bg_color = uint32(0xFF231919) - self.resizable_w = 1 - self.resizable_h = 1 - self._grid_rows = 0 - self._grid_cols = 0 - if _g_layout_init == 0: - for i in range(MAX_WIDGETS): - _g_wl_type[i] = 0 - _g_wl_side[i] = 0 - _g_wl_fill[i] = 0 - _g_wl_expand[i] = 0 - _g_wl_padx[i] = 0 - _g_wl_pady[i] = 0 - _g_wl_row[i] = 0 - _g_wl_col[i] = 0 - _g_wl_rowspan[i] = 0 - _g_wl_colspan[i] = 1 - _g_wl_sticky[i] = 0 - _g_layout_init = 1 - self._bb = None - self._bb_px = 0 - self._ansi_state = 0 - self._ansi_param = 0 - for j in range(32): - self._title[j] = 0 - - def pack(self, idx: int, side: int, fill: int, expand: int, padx: int, pady: int): - _g_wl_type[idx] = 2 - _g_wl_side[idx] = side - _g_wl_fill[idx] = fill - _g_wl_expand[idx] = expand - _g_wl_padx[idx] = padx - _g_wl_pady[idx] = pady - - def grid(self, idx: int, row: int, col: int, rowspan: int, colspan: int, sticky: int): - _g_wl_type[idx] = 3 - _g_wl_row[idx] = row - _g_wl_col[idx] = col - _g_wl_rowspan[idx] = rowspan - _g_wl_colspan[idx] = colspan - _g_wl_sticky[idx] = sticky - - # ---- Wm 协议的落地实现(Tk 持有窗口状态,覆写 Wm 声明的协议面)---- - def title(self, text: str): - i: int = 0 - while i < 31: - ch: char = text[i] - if ch == 0: break - self._title[i] = ch - i += 1 - self._title[i] = 0 - if self.win_id >= 0: - window.set_title(self.win_id, &self._title[0]) - - def set_style(self, s: int): - self.style = s - - def resizable(self, rw: int, rh: int): - self.resizable_w = rw - self.resizable_h = rh - if self.win_id >= 0: - window.set_resizable(self.win_id, rw, rh) - - def geometry(self, x: int, y: int, w: int, h: int): - self._win_x = x - self._win_y = y - self.bw = w - self.bh = h - TITLE_H - 2 - if self.win_id < 0: - self.win_id = window.create_window(x, y, w, h, &self._title[0], self.style) - self.buf = window.get_winbuf(self.win_id) - window.set_resizable(self.win_id, self.resizable_w, self.resizable_h) - else: - window.set_geometry(self.win_id, x, y, w, h) - self.buf = window.get_winbuf(self.win_id) - - def setup(self, x: int, y: int, w: int, h: int, title: const str, style: int): - self._win_x = x - self._win_y = y - self.win_id = window.create_window(x, y, w, h, title, style) - self.buf = window.get_winbuf(self.win_id) - self.bw = w - self.bh = h - TITLE_H - 2 - self.style = style - self.bg_color = uint32(0xFF231919) - - def redraw(self): - global _text_qn - # 窗口槽已回收(用户点了关闭):立即返回,不再触碰共享 arena / flush。 - if self.win_id < 0: return - if window.is_active(self.win_id) == 0: return - self.buf = window.get_winbuf(self.win_id) - sz: int = window.get_win_size(self.win_id) - if sz != 0: - self.bw = sz >> 16 - self.bh = sz & 0xFFFF - if not self.buf: return - if self.bw <= 0 or self.bh <= 0: return - # 先刷新尺寸再布局:窗口 resize 后若沿用上一帧的 bw/bh,布局会滞后一帧。 - self._do_layout() - _text_qn = 0 - # 双缓冲:整帧先画在**进程本地**的后备缓冲里,画完一次性拷进共享 arena,再 flush。 - # 为什么必须这么做:arena 正是 explorer 合成时读的那块内存(render_window_rows - # 直接从它逐行 blit 上屏)。若直接往 arena 上画,合成器会在"整窗已填成背景色、 - # 控件还没画"的中途采样 ⇒ 窗口闪烁;光标移动让 explorer 重画窗口所在行时同样撞上。 - px: int = self.bw * self.bh - if self._bb_px < px: - if self._bb: stdlib.free(rawptr(self._bb)) - self._bb = uint32*(stdlib.malloc(uint64(px * 4))) - self._bb_px = px - tgt: uint32* = self._bb - if not tgt: - tgt = self.buf - _fill_rect(tgt, self.bw, self.bh, 0, 0, self.bw, self.bh, self.bg_color) - for i in range(self.widget_count): - w: Widget* = self.widgets[i] - if w.visible: - w.draw(tgt, self.bw, self.bh) - if tgt != self.buf: - string.memcpy(self.buf, tgt, uint64(px * 4)) - _flush_text(self.win_id) - window.flush(self.win_id) - - def _do_layout(self): - self._grid_rows = 0 - self._grid_cols = 0 - cav_x: int = 0 - cav_y: int = 0 - cav_w: int = self.bw - cav_h: int = self.bh - for i in range(self.widget_count): - w: Widget* = self.widgets[i] - lt: int = _g_wl_type[i] - if lt == 2: - side: int = _g_wl_side[i] - fill: int = _g_wl_fill[i] - expand: int = _g_wl_expand[i] - px: int = _g_wl_padx[i] - py: int = _g_wl_pady[i] - ww: int = w.w - wh: int = w.h - # 交叉轴上未 fill 时按 Tk 默认 anchor=center 居中;沿 pack 方向仍贴腔体边缘 + - # padding(与 tkinter 一致)。此前一律贴左上,导致不 fill 的控件全部挤在左边缘。 - if side == PACK_TOP: - if fill == FILL_X or fill == FILL_BOTH: - ww = cav_w - 2 * px - w.x = cav_x + px - else: - w.x = cav_x + px + (cav_w - 2 * px - ww) / 2 - if expand: - if fill == FILL_Y or fill == FILL_BOTH: - wh = cav_h - 2 * py - w.y = cav_y + py - w.w = ww - w.h = wh - cav_y += wh + 2 * py - cav_h -= wh + 2 * py - elif side == PACK_BOTTOM: - if fill == FILL_X or fill == FILL_BOTH: - ww = cav_w - 2 * px - w.x = cav_x + px - else: - w.x = cav_x + px + (cav_w - 2 * px - ww) / 2 - if expand: - if fill == FILL_Y or fill == FILL_BOTH: - wh = cav_h - 2 * py - w.y = cav_y + cav_h - wh - py - w.w = ww - w.h = wh - cav_h -= wh + 2 * py - elif side == PACK_LEFT: - if fill == FILL_Y or fill == FILL_BOTH: - wh = cav_h - 2 * py - w.y = cav_y + py - else: - w.y = cav_y + py + (cav_h - 2 * py - wh) / 2 - if expand: - if fill == FILL_X or fill == FILL_BOTH: - ww = cav_w - 2 * px - w.x = cav_x + px - w.w = ww - w.h = wh - cav_x += ww + 2 * px - cav_w -= ww + 2 * px - elif side == PACK_RIGHT: - if fill == FILL_Y or fill == FILL_BOTH: - wh = cav_h - 2 * py - w.y = cav_y + py - else: - w.y = cav_y + py + (cav_h - 2 * py - wh) / 2 - if expand: - if fill == FILL_X or fill == FILL_BOTH: - ww = cav_w - 2 * px - w.x = cav_x + cav_w - ww - px - w.w = ww - w.h = wh - cav_w -= ww + 2 * px - _container_offset(self, w) - elif lt == 3: - r: int = _g_wl_row[i] + _g_wl_rowspan[i] - c: int = _g_wl_col[i] + _g_wl_colspan[i] - if r > self._grid_rows: self._grid_rows = r - if c > self._grid_cols: self._grid_cols = c - for i in range(self.widget_count): - w: Widget* = self.widgets[i] - if _g_wl_type[i] == 3: - if self._grid_rows <= 0: self._grid_rows = 1 - if self._grid_cols <= 0: self._grid_cols = 1 - cell_w: int = self.bw / self._grid_cols - cell_h: int = self.bh / self._grid_rows - wx: int = _g_wl_col[i] * cell_w - wy: int = _g_wl_row[i] * cell_h - ww: int = _g_wl_colspan[i] * cell_w - wh: int = _g_wl_rowspan[i] * cell_h - sticky: int = _g_wl_sticky[i] - if sticky == 0: - ox: int = (ww - w.w) / 2 - oy: int = (wh - w.h) / 2 - w.x = wx + ox - w.y = wy + oy - else: - w.x = wx - w.y = wy - if (sticky & 1) != 0: - w.w = ww - if (sticky & 2) != 0: - w.h = wh - _container_offset(self, w) - - def update(self): - self.redraw() - - def winfo_exists(self) -> int: - if self.win_id < 0: return 0 - return window.is_active(self.win_id) - - # ---- Misc 查询的覆写:主窗口用自己的窗口状态回答 winfo_* ---- - def winfo_ismapped(self) -> int: - if self.win_id < 0: return 0 - return window.is_active(self.win_id) - - def winfo_x(self) -> int: - return self._win_x - - def winfo_y(self) -> int: - return self._win_y - - def winfo_width(self) -> int: - return self.bw - - def winfo_height(self) -> int: - return self.bh - - # 无显式焦点控件时,按注册顺序取第一个可见文本控件(Text/Entry) - def _first_text_widget(self) -> Widget*: - for i in range(self.widget_count): - w: Widget* = self.widgets[i] - if w.visible != 0: - if w.widget_type == WIDGET_TEXT or w.widget_type == WIDGET_ENTRY: - return w - return None - - # 逐字节投递的按键流 -> ANSI CSI 状态机;返回 0=本字节被吞掉,1=可分发 - def _fold_ansi_key(self, evt: window.InputEvent*) -> int: - ch: int = evt.x - if self._ansi_state == 0: - if ch == 27: - self._ansi_state = 1 - self._ansi_param = 0 - return 0 - return 1 - if self._ansi_state == 1: - if ch == 91: - self._ansi_state = 2 - return 0 - # 孤立 ESC:只丢弃 ESC,当前字节照常分发 —— 否则会连带吞掉下一个键入字符。 - self._ansi_state = 0 - return 1 - if ch >= 48 and ch <= 57: - self._ansi_param = self._ansi_param * 10 + (ch - 48) - return 0 - self._ansi_state = 0 - vk: int = 0 - if ch == 65: vk = window.VK_UP - elif ch == 66: vk = window.VK_DOWN - elif ch == 67: vk = window.VK_RIGHT - elif ch == 68: vk = window.VK_LEFT - elif ch == 72: vk = window.VK_HOME - elif ch == 70: vk = window.VK_END - elif ch == 126: - if self._ansi_param == 3: vk = window.VK_DELETE - elif self._ansi_param == 5: vk = window.VK_PRIOR - elif self._ansi_param == 6: vk = window.VK_NEXT - if vk == 0: return 0 - evt.x = vk - return 1 - - # 指针下的最上层控件决定光标形状:Entry/Text → I 形,Button → 手指。 - # 与控件自己的 hover 视觉解耦(后者仍由 dispatch_event 维护)。 - def _update_cursor_for_hover(self, ex: int, ey: int): - hw: Widget* = None - i: int = self.widget_count - 1 - while i >= 0: - w: Widget* = self.widgets[i] - if w.visible and w.contains(ex, ey): - hw = w - break - i -= 1 - if hw == self.hovered_widget: return - self.hovered_widget = hw - ct: int = window.CURSOR_DEFAULT - if hw is not None: - if hw.widget_type == WIDGET_ENTRY or hw.widget_type == WIDGET_TEXT: - ct = window.CURSOR_IBEAM - elif hw.widget_type == WIDGET_BUTTON: - ct = window.CURSOR_HAND - window.set_cursor_type(self.win_id, ct) - - def mainloop(self): - self.redraw() - evt: window.InputEvent - while window.is_active(self.win_id): - result: int = window.poll_event(&evt) - if result != 0: - if evt.win_id == self.win_id: - need_redraw: int = 0 - etype: uint32 = evt.type - if etype == window.EVENT_TYPE_PING: - window.flush(self.win_id) - elif etype == window.EVENT_TYPE_RESIZE: - self.buf = window.get_winbuf(self.win_id) - sz: int = window.get_win_size(self.win_id) - self.bw = sz >> 16 - self.bh = sz & 0xFFFF - need_redraw = 1 - elif etype == window.EVENT_TYPE_KEY or etype == window.EVENT_TYPE_KEY_RELEASE: - folded: int = 1 - if etype == window.EVENT_TYPE_KEY: - folded = self._fold_ansi_key(&evt) - if folded != 0: - fw: Widget* = self.focused_widget - if fw is None: - fw = self._first_text_widget() - if fw is not None: - fw.focused = 1 - self.focused_widget = fw - if fw is not None: - handled: int = fw.dispatch_event(&evt) - if handled: need_redraw = 1 - else: - if etype == window.EVENT_TYPE_HOVER: - self._update_cursor_for_hover(evt.x, evt.y) - i: int = self.widget_count - 1 - while i >= 0: - w: Widget* = self.widgets[i] - if w.visible: - handled2: int = w.dispatch_event(&evt) - if handled2: - need_redraw = 1 - break - i -= 1 - else: - for i in range(self.widget_count): - w: Widget* = self.widgets[i] - if w.visible: - handled2: int = w.dispatch_event(&evt) - if handled2: need_redraw = 1 - if need_redraw: - self.redraw() - else: - process.yield_cpu() +import vpsdk.window as window +import vpsdk.process as process +import vpsdk.devfs as devfs +import vpsdk.stdlib as stdlib +import vpsdk.fonttools.fonttools as fonttools +import string + +define TITLE_H = 32 +define MAX_WIDGETS = 24 +define CHAR_W = 8 +define CHAR_H = 16 + +define TEXT_MODE_DIRECT = 0 +define TEXT_MODE_MANAGED = 1 +define TEXT_QUEUE_MAX = 256 +define TEXT_CHUNK = 28 + +define WIDGET_LABEL = 1 +define WIDGET_BUTTON = 2 +define WIDGET_ENTRY = 3 +define WIDGET_TEXT = 4 +define WIDGET_SCROLLBAR = 5 +define WIDGET_FBCANVAS = 6 +define WIDGET_FRAME = 7 + +define VERTICAL = 1 +define HORIZONTAL = 2 +define TEXT_MAX_LINES = 200 + +define PACK_TOP = 1 +define PACK_BOTTOM = 2 +define PACK_LEFT = 3 +define PACK_RIGHT = 4 + +define FILL_NONE = 0 +define FILL_X = 1 +define FILL_Y = 2 +define FILL_BOTH = 3 + +define STICKY_NONE = 0 +define STICKY_W = 1 +define STICKY_E = 2 +define STICKY_N = 4 +define STICKY_S = 8 +define STICKY_NSEW = 15 + +_g_wl_type: [MAX_WIDGETS]int +_g_wl_side: [MAX_WIDGETS]int +_g_wl_fill: [MAX_WIDGETS]int +_g_wl_expand: [MAX_WIDGETS]int +_g_wl_padx: [MAX_WIDGETS]int +_g_wl_pady: [MAX_WIDGETS]int +_g_wl_row: [MAX_WIDGETS]int +_g_wl_col: [MAX_WIDGETS]int +_g_wl_rowspan: [MAX_WIDGETS]int +_g_wl_colspan: [MAX_WIDGETS]int +_g_wl_sticky: [MAX_WIDGETS]int +_g_layout_init: int + +_hex_tbl: [17]char = "0123456789ABCDEF" +_hex_buf: [20]char = "0000000000000000000" + +def _serial_puts(s: const str): + # S4:SERIAL_PUTS 私有 syscall 已删除,改走 /dev/serial0 设备 + devfs.serial_puts(s) + +def _ptr_to_hex(val: uint64, buf: rawptr): + buf[0] = 48 + buf[1] = 120 + i: int = 15 + while i >= 0: + nibble: int = int(val & uint64(0xF)) + buf[2 + i] = _hex_tbl[nibble] + val = val >> 4 + i -= 1 + buf[18] = 0 + +def _fill_rect(fb: uint32*, fw: int, fh: int, x1: int, y1: int, w: int, h: int, color: uint32): + if not fb: return + if x1 < 0: w += x1; x1 = 0 + if y1 < 0: h += y1; y1 = 0 + if x1 + w > fw: w = fw - x1 + if y1 + h > fh: h = fh - y1 + if w <= 0 or h <= 0: return + # 逐行批量填充(string.memset32 → rep stosd):整窗填充从"每个像素一次带边界判断的写" + # 降成"每行一次 rep stosd"。app 每帧都要把整窗填一遍背景色,逐像素在 QEMU 下是主要热点。 + py: int = y1 + while py < y1 + h: + string.memset32(rawptr(uint64(fb) + uint64((py * fw + x1) * 4)), color, uint64(w)) + py += 1 + +def _draw_hline(fb: uint32*, fw: int, fh: int, x1: int, y: int, w: int, color: uint32): + if not fb: return + if y < 0 or y >= fh: return + for px in range(x1, x1 + w): + if px >= 0 and px < fw: + fb[y * fw + px] = color + +def _draw_vline(fb: uint32*, fw: int, fh: int, x: int, y1: int, h: int, color: uint32): + if not fb: return + if x < 0 or x >= fw: return + for py in range(y1, y1 + h): + if py >= 0 and py < fh: + fb[py * fw + x] = color + +def _lighten(color: uint32, amount: int) -> uint32: + r: int = int(color & 0xFF) + g: int = int((color >> 8) & 0xFF) + b: int = int((color >> 16) & 0xFF) + r = r + ((255 - r) * amount) / 256 + g = g + ((255 - g) * amount) / 256 + b = b + ((255 - b) * amount) / 256 + return uint32(r | (g << 8) | (b << 16)) + +def _darken(color: uint32, amount: int) -> uint32: + r: int = int(color & 0xFF) + g: int = int((color >> 8) & 0xFF) + b: int = int((color >> 16) & 0xFF) + r = r - (r * amount) / 256 + g = g - (g * amount) / 256 + b = b - (b * amount) / 256 + return uint32(r | (g << 8) | (b << 16)) + + +font: [256][16]uint8 = [ + [0x00,0x00,0x00,0x00,0x00,0x40,0x55,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x00,0x00,0x55,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x00,0x02,0x56,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x00,0x42,0x56,0x42,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x55,0x55,0x57,0x71,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x57,0x54,0x57,0x71,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x57,0x54,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x57,0x51,0x52,0x72,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x57,0x55,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x57,0x55,0x57,0x71,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x56,0x51,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x54,0x54,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x50,0x50,0x57,0x74,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x51,0x51,0x57,0x75,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x57,0x55,0x57,0x74,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x53,0x54,0x56,0x74,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x37,0x15,0x15,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x32,0x16,0x12,0x12,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x20,0x6E,0x22,0x2E,0x28,0x0E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x37,0x11,0x13,0x11,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x35,0x15,0x17,0x11,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x37,0x14,0x17,0x11,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x37,0x14,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x37,0x11,0x12,0x12,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x37,0x15,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x37,0x15,0x17,0x11,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x36,0x11,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x34,0x14,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x30,0x10,0x17,0x14,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x31,0x11,0x17,0x15,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x37,0x15,0x17,0x14,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x10,0x33,0x14,0x16,0x14,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x18,0x18,0x00,0x00,0x00], + [0x6C,0x6C,0x6C,0x6C,0x48,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x36,0x36,0x36,0x7F,0x7F,0x36,0x36,0x36,0x7F,0x7F,0x36,0x36,0x36,0x00,0x00,0x00], + [0x0C,0x0C,0x3E,0x7F,0x68,0x68,0x3E,0x0B,0x0B,0x7F,0x3E,0x18,0x18,0x00,0x00,0x00], + [0x60,0x60,0x66,0x06,0x0C,0x0C,0x18,0x30,0x30,0x60,0x66,0x06,0x06,0x00,0x00,0x00], + [0x38,0x6C,0x6C,0x6C,0x6C,0x6C,0x38,0x6C,0x6D,0x66,0x66,0x6F,0x3B,0x00,0x00,0x00], + [0x18,0x18,0x18,0x18,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x1C,0x3C,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x3C,0x1C,0x00,0x00], + [0x38,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x38,0x00,0x00], + [0x00,0x00,0x18,0x18,0x7E,0x3C,0x3C,0x3C,0x7E,0x18,0x18,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x18,0x18,0x18,0x7E,0x7E,0x18,0x18,0x18,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x18,0x30,0x00], + [0x00,0x00,0x00,0x00,0x00,0x00,0x7E,0x7E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00], + [0x00,0x00,0x06,0x06,0x0C,0x0C,0x18,0x30,0x30,0x60,0x60,0x00,0x00,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x66,0x6E,0x6E,0x7E,0x76,0x76,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x18,0x38,0x78,0x78,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x06,0x06,0x0E,0x1C,0x38,0x70,0x60,0x60,0x7E,0x7E,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x06,0x06,0x1C,0x1C,0x06,0x06,0x06,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x0C,0x0C,0x1C,0x1C,0x3C,0x2C,0x6C,0x7E,0x7E,0x0C,0x0C,0x0C,0x0C,0x00,0x00,0x00], + [0x7E,0x7E,0x60,0x60,0x7C,0x7E,0x06,0x06,0x06,0x06,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x60,0x60,0x7C,0x7E,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x7E,0x7E,0x06,0x06,0x0C,0x0C,0x18,0x18,0x30,0x30,0x30,0x30,0x30,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x66,0x66,0x7E,0x3C,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x66,0x66,0x7E,0x3E,0x06,0x06,0x06,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x18,0x18,0x18,0x00,0x00,0x00,0x18,0x18,0x18,0x18,0x30,0x00], + [0x00,0x02,0x06,0x0C,0x18,0x30,0x60,0x60,0x30,0x18,0x0C,0x06,0x02,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x7E,0x7E,0x00,0x00,0x7E,0x7E,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x40,0x60,0x30,0x18,0x0C,0x06,0x06,0x0C,0x18,0x30,0x60,0x40,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x06,0x0C,0x0C,0x18,0x18,0x18,0x00,0x00,0x18,0x18,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x66,0x6E,0x6A,0x6A,0x6A,0x6E,0x60,0x60,0x7C,0x3C,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x66,0x66,0x7E,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], + [0x7C,0x7E,0x66,0x66,0x66,0x7C,0x7C,0x66,0x66,0x66,0x66,0x7E,0x7C,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x78,0x7C,0x6C,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x6C,0x7C,0x78,0x00,0x00,0x00], + [0x7E,0x7E,0x60,0x60,0x60,0x7C,0x7C,0x60,0x60,0x60,0x60,0x7E,0x7E,0x00,0x00,0x00], + [0x7E,0x7E,0x60,0x60,0x60,0x7C,0x7C,0x60,0x60,0x60,0x60,0x60,0x60,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x60,0x60,0x60,0x6E,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x66,0x66,0x66,0x66,0x66,0x7E,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], + [0x7E,0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,0x00,0x00], + [0x3E,0x3E,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x6C,0x7C,0x38,0x00,0x00,0x00], + [0x66,0x66,0x66,0x6C,0x6C,0x78,0x78,0x78,0x6C,0x6C,0x66,0x66,0x66,0x00,0x00,0x00], + [0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x7E,0x00,0x00,0x00], + [0x63,0x77,0x7F,0x7F,0x6B,0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x00,0x00,0x00], + [0x66,0x66,0x66,0x66,0x76,0x76,0x7E,0x6E,0x6E,0x66,0x66,0x66,0x66,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x7C,0x7E,0x66,0x66,0x66,0x7E,0x7C,0x60,0x60,0x60,0x60,0x60,0x60,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x6A,0x6A,0x6C,0x7E,0x36,0x00,0x00,0x00], + [0x7C,0x7E,0x66,0x66,0x66,0x7E,0x7C,0x6C,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], + [0x3C,0x7E,0x66,0x60,0x60,0x78,0x3C,0x0E,0x06,0x06,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x7E,0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00], + [0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x3C,0x18,0x18,0x00,0x00,0x00], + [0x63,0x63,0x63,0x63,0x63,0x63,0x63,0x6B,0x6B,0x7F,0x77,0x77,0x22,0x00,0x00,0x00], + [0x66,0x66,0x66,0x66,0x3C,0x3C,0x18,0x3C,0x3C,0x66,0x66,0x66,0x66,0x00,0x00,0x00], + [0x66,0x66,0x66,0x66,0x7E,0x3C,0x3C,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00,0x00], + [0x7E,0x7E,0x06,0x06,0x0C,0x0C,0x18,0x30,0x30,0x60,0x60,0x7E,0x7E,0x00,0x00,0x00], + [0x78,0x78,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x78,0x78,0x00,0x00], + [0xC0,0xC0,0x60,0x60,0x30,0x30,0x18,0x0C,0x0C,0x06,0x06,0x03,0x03,0x00,0x00,0x00], + [0x1E,0x1E,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x1E,0x1E,0x00,0x00], + [0x3C,0x7E,0x66,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0x00], + [0x30,0x38,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x3C,0x3E,0x06,0x3E,0x7E,0x66,0x66,0x7E,0x3E,0x00,0x00,0x00], + [0x60,0x60,0x60,0x60,0x7C,0x7E,0x66,0x66,0x66,0x66,0x66,0x7E,0x7C,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x3C,0x7E,0x66,0x60,0x60,0x60,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x06,0x06,0x06,0x06,0x3E,0x7E,0x66,0x66,0x66,0x66,0x66,0x7E,0x3E,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x3C,0x7E,0x66,0x7E,0x7E,0x60,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x1C,0x3E,0x30,0x30,0x30,0x7C,0x7C,0x30,0x30,0x30,0x30,0x30,0x30,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x3E,0x7E,0x66,0x66,0x66,0x7E,0x3E,0x06,0x06,0x3E,0x3C,0x00], + [0x60,0x60,0x60,0x60,0x7C,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], + [0x18,0x18,0x00,0x00,0x38,0x38,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,0x00,0x00], + [0x0C,0x0C,0x00,0x00,0x3C,0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x7C,0x78,0x00], + [0x60,0x60,0x60,0x60,0x66,0x66,0x6C,0x78,0x78,0x6C,0x6C,0x66,0x66,0x00,0x00,0x00], + [0x38,0x38,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x7E,0x7E,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x36,0x7F,0x6B,0x6B,0x63,0x63,0x63,0x63,0x63,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x7C,0x7E,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x3C,0x7E,0x66,0x66,0x66,0x66,0x66,0x7E,0x3C,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x7C,0x7E,0x66,0x66,0x66,0x7E,0x7C,0x60,0x60,0x60,0x60,0x00], + [0x00,0x00,0x00,0x00,0x3E,0x7E,0x66,0x66,0x66,0x7E,0x3E,0x06,0x06,0x07,0x07,0x00], + [0x00,0x00,0x00,0x00,0x6C,0x7E,0x76,0x60,0x60,0x60,0x60,0x60,0x60,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x3C,0x7E,0x62,0x70,0x3C,0x0E,0x46,0x7E,0x3C,0x00,0x00,0x00], + [0x30,0x30,0x30,0x30,0x7C,0x7C,0x30,0x30,0x30,0x30,0x30,0x3C,0x1C,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x66,0x66,0x66,0x66,0x66,0x66,0x66,0x7E,0x3E,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x66,0x66,0x66,0x66,0x66,0x3C,0x3C,0x18,0x18,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x63,0x63,0x63,0x63,0x6B,0x6B,0x7F,0x7F,0x36,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x66,0x66,0x3C,0x3C,0x18,0x3C,0x3C,0x66,0x66,0x00,0x00,0x00], + [0x00,0x00,0x00,0x00,0x66,0x66,0x66,0x66,0x66,0x7E,0x3E,0x06,0x06,0x7E,0x7C,0x00], + [0x00,0x00,0x00,0x00,0x7E,0x7E,0x0C,0x0C,0x18,0x30,0x30,0x7E,0x7E,0x00,0x00,0x00], + [0x0E,0x1E,0x18,0x18,0x18,0x18,0x30,0x30,0x18,0x18,0x18,0x18,0x1E,0x0E,0x00,0x00], + [0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00,0x00], + [0x70,0x78,0x18,0x18,0x18,0x18,0x0C,0x0C,0x18,0x18,0x18,0x18,0x78,0x70,0x00,0x00], + [0x31,0x79,0x6B,0x4F,0x46,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], + [0x00,0x00,0x70,0x13,0x24,0x26,0x24,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], +] + +class TextCmd: + x: int + y: int + color: uint32 + s: [32]char + +_text_mode: int = TEXT_MODE_MANAGED +_ttf_font: fonttools.Font* = None +_ttf_atlas: uint8* = None +_ttf_state: int = 0 +_text_queue: [TEXT_QUEUE_MAX]TextCmd +_text_qn: int = 0 + +def set_text_mode(mode: int): + global _text_mode + _text_mode = mode + +def _ensure_ttf(): + global _ttf_font, _ttf_atlas, _ttf_state + if _ttf_state != 0: return + f: fonttools.Font* = fonttools.load("/FONTS/FONT.TTF") + if f is None: + _ttf_state = -1 + return + a: uint8* = fonttools.build_ascii_atlas(f, CHAR_W, CHAR_H) + if a is None: + _ttf_state = -1 + return + _ttf_font = f + _ttf_atlas = a + _ttf_state = 1 + +def _queue_text(x: int, y: int, s: const str, color: uint32): + global _text_qn + if _text_mode != TEXT_MODE_MANAGED: return + cx: int = x + cy: int = y + i: int = 0 + while s[i] != 0: + if s[i] == 10: + cx = x + cy += CHAR_H + i += 1 + continue + if _text_qn >= TEXT_QUEUE_MAX: return + cmd: TextCmd* = &_text_queue[_text_qn] + cmd.x = cx + cmd.y = cy + cmd.color = color + n: int = 0 + while n < TEXT_CHUNK and s[i] != 0 and s[i] != 10: + cmd.s[n] = s[i] + n += 1 + i += 1 + cmd.s[n] = 0 + _text_qn += 1 + cx += n * CHAR_W + +def _flush_text(win_id: int): + global _text_qn + if _text_qn == 0: return + for i in range(_text_qn): + cmd: TextCmd* = &_text_queue[i] + window.text(win_id, cmd.x, cmd.y, cmd.color, &cmd.s[0]) + _text_qn = 0 + +def _draw_char(buf: uint32*, bw: int, bh: int, x: int, y: int, ch: char, color: uint32): + if not buf: return + if bw <= 0 or bh <= 0: return + ci: int = int(uint8(ch)) + if _text_mode == TEXT_MODE_DIRECT: + _ensure_ttf() + if _ttf_state == 1 and ci < 128: + fy: int = 0 + while fy < CHAR_H: + py: int = y + fy + if py >= 0 and py < bh: + fx: int = 0 + while fx < CHAR_W: + a: int = int(_ttf_atlas[(ci * CHAR_H + fy) * CHAR_W + fx]) + if a != 0: + px: int = x + fx + if px >= 0 and px < bw: + idx: int = py * bw + px + if a >= 255: + buf[idx] = color + else: + dst: uint32 = buf[idx] + dr: int = int(dst & 0xFF) + dg: int = int((dst >> 8) & 0xFF) + db: int = int((dst >> 16) & 0xFF) + sr: int = int(color & 0xFF) + sg: int = int((color >> 8) & 0xFF) + sb: int = int((color >> 16) & 0xFF) + r: int = (sr * a + dr * (255 - a)) / 255 + g: int = (sg * a + dg * (255 - a)) / 255 + b: int = (sb * a + db * (255 - a)) / 255 + buf[idx] = uint32(r) | (uint32(g) << 8) | (uint32(b) << 16) + fx += 1 + fy += 1 + return + if ci >= 128: ci = 0 + fy: int = 0 + while fy < CHAR_H: + if y + fy < 0 or y + fy >= bh: + fy += 1 + continue + row: uint8 = font[ci][fy] + if row == 0: + fy += 1 + continue + for fx in range(CHAR_W): + if row & (0x80 >> fx): + px: int = x + fx + if px >= 0 and px < bw: + buf[(y + fy) * bw + px] = color + fy += 1 + +def _draw_text(buf: uint32*, bw: int, bh: int, x: int, y: int, s: const str, color: uint32): + if not buf: return + if bw <= 0 or bh <= 0: return + if _text_mode == TEXT_MODE_MANAGED: + _queue_text(x, y, s, color) + return + cx: int = x + cy: int = y + i: int = 0 + while True: + ch: char = s[i] + if ch == 0: break + if ch == 10: + cy += CHAR_H + cx = x + else: + _draw_char(buf, bw, bh, cx, cy, ch, color) + cx += CHAR_W + i += 1 + +# ============================================================================ +# tkinter 风格的抽象基类层次(仿 tkinter 原始抽象) +# +# Misc(mixin:master/tk 公共状态 + winfo_master/toplevel、bind 等公共方法) +# ├─ Tk(Wm, Misc) 主窗口类(Wm 声明 WM 协议面,Tk 提供落地实现) +# └─ BaseWidget(Misc) +# └─ Widget(BaseWidget) 所有普通控件的抽象基类(Button/Label/Entry… 都继承 Widget) +# +# 字段模型(与 Test/MultiInheritTest 一致):父类字段被**展平**进子类,子类自己的方法可 +# 直接访问全部展平字段;字段偏移按"定义该方法的那一层"自己的展平布局计算(该测试里 +# ColoredCircle(ShapeM, ColorM) = (ptr, i32, f64):偏移 0 是唯一的共享 vptr,各基类字段依次 +# 跟在后面,虚表被合并)。由此有三条硬约束: +# ① Misc 的字段必须在所有派生类里落到同一偏移 —— 偏移 0 被共享 vptr 占用,Misc 的字段 +# 从 8 起,只要它前面没有"带字段的基类"就成立; +# ② 因此 `Wm`(Tk 的最左基类)必须**无字段**,否则会把 Misc 的字段整体后移,而 Misc 自己 +# 的方法仍按自身布局取偏移 ⇒ 静默错位; +# ③ 基类方法里**不要写 `self.<虚拟方法>()` 的自我分派**:多继承下非最左基类的方法在派生 +# 类的合并虚表里整体后移,而方法体内按"定义类自己的虚表"取索引 ⇒ 会指到别的槽位。 +# 所以 winfo_*/title 这类"需要看具体对象状态"的方法一律在 Misc / Wm 里只做声明,由 +# Tk / Widget 覆写实现;外部按具体类型调用即可正常走虚表。 +# ============================================================================ + +# ---- bind 回调注册表:按 (对象, 事件类型) 记录回调。 +# tkinter 用 这类字符串作为 sequence,这里改用 window.EVENT_TYPE_* 整型 +# (本运行时的事件类型就是这些整型,比较字符串在 VPC 模型下既慢又易错)。 +define MAX_BINDINGS = 64 + +_bind_obj: [MAX_BINDINGS]Misc* = [None] +_bind_seq: [MAX_BINDINGS]int = [0] +_bind_cb: [MAX_BINDINGS]rawptr = [None] + +def _bind_add(obj: Misc*, seq: int, func: rawptr) -> int: + i: int = 0 + while i < MAX_BINDINGS: + if _bind_obj[i] is None: + _bind_obj[i] = obj + _bind_seq[i] = seq + _bind_cb[i] = func + return i + i += 1 + return -1 + +def _bind_remove(obj: Misc*, seq: int) -> int: + i: int = 0 + while i < MAX_BINDINGS: + if _bind_obj[i] is obj and _bind_seq[i] == seq: + _bind_obj[i] = None + _bind_cb[i] = None + return 0 + i += 1 + return -1 + +def _bind_invoke(obj: Misc*, seq: int, evt: window.InputEvent*) -> int: + i: int = 0 + while i < MAX_BINDINGS: + if _bind_obj[i] is obj and _bind_seq[i] == seq: + cb: rawptr = _bind_cb[i] + if cb is None: return 0 + # 与 Button 回调同一约定:rawptr 是 VPC 生成的闭包对象 {fn, env}, + # fn 期望 rdi = env ⇒ 必须取 [cb+8] 传 rdi、再由 [cb] 调用。 + __asm__(f"""push r12 +push r13 +mov r13, {asminp(cb, ASM_DESCR.REG_ANY)} +mov rdi, qword ptr [r13 + 8] +mov r12, rsp +and rsp, -16 +call qword ptr [r13] +mov rsp, r12 +pop r13 +pop r12""", + op=[ASM_DESCR.CLOBBER_MEMORY, + ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, + ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, + ASM_DESCR.CLOBBER_RDI, + ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, + ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) + return 1 + i += 1 + return 0 + +# ---- Misc:Tk 与所有控件的公共 mixin ---- +# 只放"所有 Tk 对象都有的东西":公共状态(master/tk)、公共方法(winfo_master/toplevel、bind), +# 以及需要各具体类自己回答的查询(winfo_x/… 这里只声明,由 Tk / Widget 覆写)。 +# 约束:这里**不做 `self.<虚拟方法>()` 的自我分派**。多继承下非最左基类的方法在派生类的 +# 合并虚表里会整体后移,而方法体内按"定义类自己的虚表"取索引 ⇒ 会指到别的槽位。 +# 因此查询一律由具体类覆写、由外部按具体类型调用;Misc 自身只用自己声明的字段。 +virtual class Misc: + master: Misc* # 父容器;主窗口为 None + tk: Tk* # 所属顶层窗口;主窗口指向自身 + + # 公共状态初始化(各具体类的 __init__ 调用;不依赖基类 __init__ 链) + def _misc_setup(self, master: Misc* = None): + self.master = master + self.tk = None + if master is not None: + self.tk = master.tk + + # ---- winfo 系(tkinter Misc.winfo_* 的对应物)---- + def winfo_master(self) -> Misc*: + return self.master + + def winfo_toplevel(self) -> Tk*: + return self.tk + + # ---- bind 系(回调在 BaseWidget.dispatch_event 里派发)---- + def bind(self, seq: int, func: rawptr) -> int: + return _bind_add(Misc*(self), seq, func) + + def unbind(self, seq: int) -> int: + return _bind_remove(Misc*(self), seq) + + def _invoke_binding(self, seq: int, evt: window.InputEvent*) -> int: + return _bind_invoke(Misc*(self), seq, evt) + + # ---- 查询/生命周期:抽象,由 Tk / Widget 覆写 ---- + def winfo_exists(self) -> int: + return 0 + + def winfo_ismapped(self) -> int: + return 0 + + def winfo_x(self) -> int: + return 0 + + def winfo_y(self) -> int: + return 0 + + def winfo_width(self) -> int: + return 0 + + def winfo_height(self) -> int: + return 0 + + def destroy(self): + pass + + def update(self): + pass + +# ---- Wm:顶层窗口的"窗口管理器协议"(tkinter Wm 的对应物)---- +# 无字段(见文件头的布局约束),这里只**声明**协议面,落地实现由持有窗口状态的 Tk 覆写。 +# 同样不做自我分派(理由见 Misc 上方注释)。 +virtual class Wm: + def title(self, text: str): + pass + + def geometry(self, x: int, y: int, w: int, h: int): + pass + + def resizable(self, rw: int, rh: int): + pass + +# ---- BaseWidget:控件公共部分(tkinter BaseWidget 的对应物)---- +virtual class BaseWidget(Misc): + # 事件派发:先跑 bind 注册的回调,再交给控件自己的 handle_event。 + # 放在基类是为了让 Tk 的事件循环只调这一个入口,各控件无需关心 bind。 + def dispatch_event(self, evt: window.InputEvent*) -> int: + hit: int = self._invoke_binding(int(evt.type), evt) + handled: int = self.handle_event(evt) + if hit or handled: + return 1 + return 0 + + # 控件自身的事件处理(抽象,由具体控件覆写) + def handle_event(self, evt: window.InputEvent*) -> int: + return 0 + + # 抽象:具体控件在 Widget 层覆写(visible 是 Widget 的字段,不能在这里访问) + def destroy(self): + pass + +def _register_widget(master: Misc*, widget: Widget*) -> int: + tk: Tk* = master.tk + if tk is None: return -1 + idx: int = tk.widget_count + if idx < 0 or idx >= MAX_WIDGETS: + return -1 + tk.widgets[idx] = widget + tk.widget_count = idx + 1 + return idx + +virtual class Widget(BaseWidget): + x: int + y: int + w: int + h: int + visible: int + widget_type: int + focused: int + hovered: int + + def destroy(self): + self.visible = 0 + + # ---- Misc 查询的覆写:控件用自己的几何信息回答 winfo_* ---- + def winfo_exists(self) -> int: + return self.visible + + def winfo_ismapped(self) -> int: + return self.visible + + def winfo_x(self) -> int: + return self.x + + def winfo_y(self) -> int: + return self.y + + def winfo_width(self) -> int: + return self.w + + def winfo_height(self) -> int: + return self.h + + def draw(self, fb: uint32*, fw: int, fh: int): + pass + + def handle_event(self, evt: window.InputEvent*) -> int: + return 0 + + def contains(self, px: int, py: int) -> int: + if px >= self.x and px < self.x + self.w and py >= self.y and py < self.y + self.h: + return 1 + return 0 + + def place(self, x: int = 0, y: int = 0, w: int = 0, h: int = 0): + self.x = x + self.y = y + if w > 0: + self.w = w + if h > 0: + self.h = h + +# 容器中坐标叠加:master 不是顶层窗口(即 Frame 这类容器)时,子控件先按 Tk 的口径算位置, +# 再叠加容器自身原点 ⇒ 视觉上落在容器内部。本运行时的 pack/grid 是 Tk 级、按全局下标走, +# 所以这里只做"原点叠加"这一层,不做真正的嵌套布局。 +def _container_offset(root: Tk*, w: Widget*): + if w.master is None: return + if uint64(w.master) == uint64(root): return + pw: Widget* = Widget*(w.master) + if pw.widget_type == WIDGET_FRAME: + w.x += pw.x + w.y += pw.y + +# ---- Frame:容器控件(tkinter Frame 的对应物)---- +# 可作 master 使用:子控件经 _misc_setup 继承它的 tk,照常注册进顶层窗口;位置再叠加本 +# 容器原点(见 _container_offset)。 +virtual class Frame(Widget): + fill_color: uint32 + border_color: uint32 + border_w: int + + def __init__(self, master: Misc*, w: int, h: int): + self.x = 0 + self.y = 0 + self.w = w + self.h = h + self.visible = 1 + self._misc_setup(master) + self.widget_type = WIDGET_FRAME + self.focused = 0 + self.hovered = 0 + self.fill_color = uint32(0xFF2A2A3A) + self.border_color = uint32(0xFF505070) + self.border_w = 1 + _register_widget(master, Widget*(self)) + + def draw(self, fb: uint32*, fw: int, fh: int): + _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, self.fill_color) + if self.border_w > 0: + _draw_hline(fb, fw, fh, self.x, self.y, self.w, self.border_color) + _draw_hline(fb, fw, fh, self.x, self.y + self.h - 1, self.w, self.border_color) + _draw_vline(fb, fw, fh, self.x, self.y, self.h, self.border_color) + _draw_vline(fb, fw, fh, self.x + self.w - 1, self.y, self.h, self.border_color) + + def handle_event(self, evt: window.InputEvent*) -> int: + return 0 + + def config(self, fill_color: uint32 = None, border_color: uint32 = None): + if fill_color is not None: + self.fill_color = fill_color + if border_color is not None: + self.border_color = border_color + +virtual class Label(Widget): + text: [64]char + color: uint32 + + def __init__(self, master: Misc*, text: str, color: uint32): + self.x = 0 + self.y = 0 + self.w = 0 + self.h = 0 + self.visible = 1 + self._misc_setup(master) + self.widget_type = WIDGET_LABEL + self.focused = 0 + self.hovered = 0 + self.color = color + i: int = 0 + for ch in text: + if i >= 63: break + self.text[i] = ch + i += 1 + self.text[i] = 0 + # Label 没有显式 w/h 参数:自然尺寸必须由文本推出。否则 pack 拿到的请求尺寸是 + # 0×0 ⇒ 不占布局空间,且 PACK_BOTTOM/非 fill 时会被贴到可视区域之外(文字被裁掉)。 + self._fit_natural() + _register_widget(master, Widget*(self)) + + # 按当前 text 计算自然尺寸:宽取最长行,高取行数(多行 Label 也成立)。 + def _fit_natural(self): + line_chars: int = 0 + max_chars: int = 0 + lines: int = 1 + i: int = 0 + while i < 64: + ch: char = self.text[i] + if ch == 0: break + if ch == 10: + if line_chars > max_chars: max_chars = line_chars + line_chars = 0 + lines += 1 + else: + line_chars += 1 + i += 1 + if line_chars > max_chars: max_chars = line_chars + self.w = max_chars * CHAR_W + self.h = lines * CHAR_H + + def draw(self, fb: uint32*, fw: int, fh: int): + _draw_text(fb, fw, fh, self.x, self.y, &self.text[0], self.color) + + def handle_event(self, evt: window.InputEvent*) -> int: + return 0 + + def config(self, text: const str = None, color: uint32 = None): + if text is not None: + i: int = 0 + while i < 63: + ch: char = *(text + i) + self.text[i] = ch + if ch == 0: break + i += 1 + self.text[i] = 0 + self._fit_natural() + if color is not None: + self.color = color + +virtual class Button(Widget): + text: [32]char + fill_color: uint32 + text_color: uint32 + pressed: int + callback: rawptr + + def __init__(self, master: Misc*, w: int, h: int, text: str, callback: rawptr): + self.x = 0 + self.y = 0 + self.w = w + self.h = h + self.visible = 1 + self._misc_setup(master) + self.widget_type = WIDGET_BUTTON + self.focused = 0 + self.hovered = 0 + self.fill_color = uint32(0x3C3C5A) + self.text_color = uint32(0xFFFFFFFF) + self.pressed = 0 + self.callback = callback + i: int = 0 + for ch in text: + if i >= 31: break + self.text[i] = ch + i += 1 + self.text[i] = 0 + _register_widget(master, Widget*(self)) + + def draw(self, fb: uint32*, fw: int, fh: int): + fill: uint32 = self.fill_color + border: uint32 = uint32(0x8080B0) + if self.pressed: + fill = _lighten(self.fill_color, 60) + border = uint32(0xA0A0D0) + elif self.hovered: + fill = _lighten(self.fill_color, 30) + border = uint32(0xA0A0D0) + _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, fill) + top_left: uint32 = _lighten(fill, 80) + bottom_right: uint32 = _darken(fill, 80) + if self.pressed: + top_left = _darken(fill, 60) + bottom_right = _lighten(fill, 40) + _draw_hline(fb, fw, fh, self.x, self.y, self.w, top_left) + _draw_hline(fb, fw, fh, self.x, self.y + 1, self.w, top_left) + _draw_vline(fb, fw, fh, self.x, self.y, self.h, top_left) + _draw_vline(fb, fw, fh, self.x + 1, self.y, self.h, top_left) + _draw_hline(fb, fw, fh, self.x, self.y + self.h - 1, self.w, bottom_right) + _draw_hline(fb, fw, fh, self.x, self.y + self.h - 2, self.w, bottom_right) + _draw_vline(fb, fw, fh, self.x + self.w - 1, self.y, self.h, bottom_right) + _draw_vline(fb, fw, fh, self.x + self.w - 2, self.y, self.h, bottom_right) + _draw_hline(fb, fw, fh, self.x - 1, self.y - 1, self.w + 2, border) + _draw_hline(fb, fw, fh, self.x - 1, self.y + self.h, self.w + 2, border) + _draw_vline(fb, fw, fh, self.x - 1, self.y - 1, self.h + 2, border) + _draw_vline(fb, fw, fh, self.x + self.w, self.y - 1, self.h + 2, border) + tl: int = int(string.strlen(&self.text[0])) + tx: int = self.x + (self.w - 8 * tl) / 2 + ty: int = self.y + (self.h - CHAR_H) / 2 + _draw_text(fb, fw, fh, tx, ty, &self.text[0], self.text_color) + + def handle_event(self, evt: window.InputEvent*) -> int: + etype: uint32 = evt.type + ex: int = evt.x + ey: int = evt.y + if etype == window.EVENT_TYPE_HOVER: + if self.contains(ex, ey): + if not self.hovered: + self.hovered = 1 + return 1 + else: + if self.hovered and not self.pressed: + self.hovered = 0 + return 1 + elif etype == window.EVENT_TYPE_HOVER_LEAVE: + if self.hovered: + self.hovered = 0 + return 1 + elif etype == window.EVENT_TYPE_CLICK: + if self.contains(ex, ey): + self.pressed = 1 + return 1 + elif etype == window.EVENT_TYPE_RELEASE: + if self.pressed: + self.pressed = 0 + if self.contains(ex, ey) and self.callback is not None: + # 闭包调用约定(VPC 生成):callback 是堆上的 {fn, env} 对象,fn 期望 rdi = env。 + # 之前直接 `call r13`(r13 = 闭包对象地址)会跳到堆上执行 + # (现场:rip=0x2040400a 在堆区、cr2=0x1、err=0x5)。 + __asm__(f"""push r12 +push r13 +mov r13, {asminp(self.callback, ASM_DESCR.REG_ANY)} +mov rdi, qword ptr [r13 + 8] +mov r12, rsp +and rsp, -16 +call qword ptr [r13] +mov rsp, r12 +pop r13 +pop r12""", + op=[ASM_DESCR.CLOBBER_MEMORY, + ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, + ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, + ASM_DESCR.CLOBBER_RDI, + ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, + ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) + return 1 + return 0 + + def config(self, text: const str = None, fill_color: uint32 = None, text_color: uint32 = None, callback: rawptr = None): + if text is not None: + i: int = 0 + while i < 31: + ch: char = *(text + i) + self.text[i] = ch + if ch == 0: break + i += 1 + self.text[i] = 0 + if fill_color is not None: + self.fill_color = fill_color + if text_color is not None: + self.text_color = text_color + if callback is not None: + self.callback = callback + +virtual class Entry(Widget): + text: [64]char + text_len: int + cursor_pos: int + max_len: int + bg_color: uint32 + border_color: uint32 + text_color: uint32 + cursor_color: uint32 + + def __init__(self, master: Misc*, w: int, h: int): + self.x = 0 + self.y = 0 + self.w = w + self.h = h + self.visible = 1 + self._misc_setup(master) + self.widget_type = WIDGET_ENTRY + self.focused = 0 + self.hovered = 0 + self.text_len = 0 + self.cursor_pos = 0 + self.max_len = 30 + self.bg_color = uint32(0xFF231919) + self.border_color = uint32(0x606080) + self.text_color = uint32(0x000000) + self.cursor_color = uint32(0x000000) + for i in range(64): + self.text[i] = 0 + _register_widget(master, Widget*(self)) + + def draw(self, fb: uint32*, fw: int, fh: int): + bg: uint32 = self.bg_color + bdr: uint32 = self.border_color + if self.focused: + bg = uint32(0xFFFFFF) + bdr = uint32(0x4488FF) + elif self.hovered: + bdr = uint32(0x8080B0) + _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, bg) + _draw_hline(fb, fw, fh, self.x, self.y, self.w, bdr) + _draw_hline(fb, fw, fh, self.x, self.y + self.h - 1, self.w, bdr) + _draw_vline(fb, fw, fh, self.x, self.y, self.h, bdr) + _draw_vline(fb, fw, fh, self.x + self.w - 1, self.y, self.h, bdr) + if self.text_len > 0: + tc: uint32 = self.text_color + if not self.focused: + tc = uint32(0xCCCCCC) + _draw_text(fb, fw, fh, self.x + 6, self.y + 7, &self.text[0], tc) + elif not self.focused: + _draw_text(fb, fw, fh, self.x + 6, self.y + 7, "Type here...", uint32(0x888888)) + if self.focused: + cx_pos: int = self.x + 6 + self.cursor_pos * CHAR_W + if cx_pos < self.x + self.w - 2: + _draw_vline(fb, fw, fh, cx_pos, self.y + 5, self.h - 10, self.cursor_color) + + def handle_event(self, evt: window.InputEvent*) -> int: + etype: uint32 = evt.type + ex: int = evt.x + ey: int = evt.y + if etype == window.EVENT_TYPE_HOVER: + if self.contains(ex, ey): + if not self.hovered: + self.hovered = 1 + return 1 + else: + if self.hovered: + self.hovered = 0 + return 1 + elif etype == window.EVENT_TYPE_HOVER_LEAVE: + if self.hovered: + self.hovered = 0 + return 1 + elif etype == window.EVENT_TYPE_CLICK: + if self.contains(ex, ey): + if not self.focused: + self.focused = 1 + tk4: Tk* = self.tk + tk4.focused_widget = Widget*(self) + rel_x: int = ex - self.x - 6 + if rel_x < 0: rel_x = 0 + new_pos: int = (rel_x + CHAR_W / 2) / CHAR_W + if new_pos > self.text_len: new_pos = self.text_len + self.cursor_pos = new_pos + return 1 + else: + if self.focused: + self.focused = 0 + tk5: Tk* = self.tk + tk5.focused_widget = None + return 1 + elif etype == window.EVENT_TYPE_KEY: + if self.focused: + ebtn: uint8 = evt.button + if ebtn == window.KEY_STATE_RELEASE: + return 0 + ch: int = ex + if ch == 8: + if self.cursor_pos > 0: + for i in range(self.cursor_pos, self.text_len): + self.text[i - 1] = self.text[i] + self.text_len -= 1 + self.cursor_pos -= 1 + self.text[self.text_len] = 0 + return 1 + elif ch == window.VK_LEFT: + if self.cursor_pos > 0: + self.cursor_pos -= 1 + return 1 + elif ch == window.VK_RIGHT: + if self.cursor_pos < self.text_len: + self.cursor_pos += 1 + return 1 + elif ch == window.VK_HOME: + self.cursor_pos = 0 + return 1 + elif ch == window.VK_END: + self.cursor_pos = self.text_len + return 1 + elif ch == window.VK_DELETE: + if self.cursor_pos < self.text_len: + for i in range(self.cursor_pos, self.text_len - 1): + self.text[i] = self.text[i + 1] + self.text_len -= 1 + self.text[self.text_len] = 0 + return 1 + elif ch >= 32 and ch < 127: + if self.text_len < self.max_len: + i: int = self.text_len + while i > self.cursor_pos: + self.text[i] = self.text[i - 1] + i -= 1 + self.text[self.cursor_pos] = char(ch) + self.text_len += 1 + self.cursor_pos += 1 + self.text[self.text_len] = 0 + return 1 + return 0 + + def get(self) -> const str: + return &self.text[0] + + def delete(self, start: int, end: int): + self.text_len = 0 + for i in range(64): + self.text[i] = 0 + + def config(self, bg_color: uint32 = None, text_color: uint32 = None, border_color: uint32 = None): + if bg_color is not None: + self.bg_color = bg_color + if text_color is not None: + self.text_color = text_color + if border_color is not None: + self.border_color = border_color + +virtual class Scrollbar(Widget): + orient: int + command: rawptr + slider_first: int + slider_last: int + action: int + action_value: int + pressed: int + dragging: int + drag_start_y: int + drag_start_first: int + trough_color: uint32 + slider_color: uint32 + arrow_color: uint32 + slider_hover: int + + def __init__(self, master: Misc*, w: int, h: int, orient: int): + self.x = 0 + self.y = 0 + self.w = w + self.h = h + self.visible = 1 + self._misc_setup(master) + self.widget_type = WIDGET_SCROLLBAR + self.focused = 0 + self.hovered = 0 + self.orient = orient + self.command = None + self.slider_first = 0 + self.slider_last = 1000 + self.action = 0 + self.action_value = 0 + self.pressed = 0 + self.dragging = 0 + self.drag_start_y = 0 + self.drag_start_first = 0 + self.trough_color = uint32(0x1E1E2E) + self.slider_color = uint32(0x4A4A6A) + self.arrow_color = uint32(0xAAAAAA) + self.slider_hover = 0 + _register_widget(master, Widget*(self)) + + def draw(self, fb: uint32*, fw: int, fh: int): + _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, self.trough_color) + if self.orient != VERTICAL: return + arrow_sz: int = self.w + if arrow_sz > 16: arrow_sz = 16 + self._draw_arrow_btn(fb, fw, fh, self.x, self.y, self.w, arrow_sz, 0, 1 if self.pressed == 1 else 0) + self._draw_arrow_btn(fb, fw, fh, self.x, self.y + self.h - arrow_sz, self.w, arrow_sz, 1, 1 if self.pressed == 2 else 0) + trough_y: int = self.y + arrow_sz + trough_h: int = self.h - 2 * arrow_sz + if trough_h <= 0: return + if self.slider_last > self.slider_first: + sl_h: int = (self.slider_last - self.slider_first) * trough_h / 1000 + if sl_h < 8: sl_h = 8 + sl_y: int = trough_y + self.slider_first * trough_h / 1000 + if sl_y + sl_h > trough_y + trough_h: + sl_y = trough_y + trough_h - sl_h + if sl_y < trough_y: sl_y = trough_y + color: uint32 = self.slider_color + if self.dragging: + color = _lighten(self.slider_color, 40) + elif self.slider_hover: + color = _lighten(self.slider_color, 20) + _fill_rect(fb, fw, fh, self.x + 1, sl_y, self.w - 2, sl_h, color) + top_c: uint32 = _lighten(color, 60) + bot_c: uint32 = _darken(color, 60) + _draw_hline(fb, fw, fh, self.x + 1, sl_y, self.w - 2, top_c) + _draw_hline(fb, fw, fh, self.x + 1, sl_y + sl_h - 1, self.w - 2, bot_c) + _draw_vline(fb, fw, fh, self.x + 1, sl_y, sl_h, top_c) + _draw_vline(fb, fw, fh, self.x + self.w - 2, sl_y, sl_h, bot_c) + + def _draw_arrow_btn(self, fb: uint32*, fw: int, fh: int, bx: int, by: int, bw: int, bh: int, direction: int, is_pressed: int): + color: uint32 = self.slider_color + if is_pressed: + color = _lighten(self.slider_color, 30) + _fill_rect(fb, fw, fh, bx, by, bw, bh, color) + top_c: uint32 = _lighten(color, 60) + bot_c: uint32 = _darken(color, 60) + if is_pressed: + top_c = _darken(color, 40) + bot_c = _lighten(color, 30) + _draw_hline(fb, fw, fh, bx, by, bw, top_c) + _draw_hline(fb, fw, fh, bx, by + bh - 1, bw, bot_c) + _draw_vline(fb, fw, fh, bx, by, bh, top_c) + _draw_vline(fb, fw, fh, bx + bw - 1, by, bh, bot_c) + cx: int = bx + bw / 2 + cy: int = by + bh / 2 + if direction == 0: + _fill_rect(fb, fw, fh, cx - 1, cy - 3, 3, 1, self.arrow_color) + _fill_rect(fb, fw, fh, cx - 2, cy - 2, 5, 1, self.arrow_color) + _fill_rect(fb, fw, fh, cx - 3, cy - 1, 7, 1, self.arrow_color) + else: + _fill_rect(fb, fw, fh, cx - 3, cy - 1, 7, 1, self.arrow_color) + _fill_rect(fb, fw, fh, cx - 2, cy, 5, 1, self.arrow_color) + _fill_rect(fb, fw, fh, cx - 1, cy + 1, 3, 1, self.arrow_color) + + def handle_event(self, evt: window.InputEvent*) -> int: + etype: uint32 = evt.type + ex: int = evt.x + ey: int = evt.y + if etype == window.EVENT_TYPE_HOVER: + if self.dragging: + self._handle_drag(ey) + return 1 + if self.contains(ex, ey): + if not self.slider_hover: + self.slider_hover = 1 + return 1 + else: + if self.slider_hover and self.pressed == 0: + self.slider_hover = 0 + return 1 + return 0 + elif etype == window.EVENT_TYPE_HOVER_LEAVE: + if self.slider_hover: + self.slider_hover = 0 + if not self.dragging: + self.pressed = 0 + return 1 + return 0 + elif etype == window.EVENT_TYPE_CLICK: + if self.contains(ex, ey): + if self.orient == VERTICAL: + arrow_sz: int = self.w + if arrow_sz > 16: arrow_sz = 16 + if ey < self.y + arrow_sz: + self.pressed = 1 + self.action = 1 + self._fire_command() + return 1 + elif ey >= self.y + self.h - arrow_sz: + self.pressed = 2 + self.action = 2 + self._fire_command() + return 1 + else: + trough_y: int = self.y + arrow_sz + trough_h: int = self.h - 2 * arrow_sz + if trough_h <= 0: return 1 + sl_h: int = (self.slider_last - self.slider_first) * trough_h / 1000 + if sl_h < 8: sl_h = 8 + sl_y: int = trough_y + self.slider_first * trough_h / 1000 + if sl_y + sl_h > trough_y + trough_h: + sl_y = trough_y + trough_h - sl_h + if ey >= sl_y and ey < sl_y + sl_h: + self.pressed = 4 + self.dragging = 1 + self.drag_start_y = ey + self.drag_start_first = self.slider_first + else: + self.pressed = 3 + if ey < sl_y: + self.action = 3 + else: + self.action = 4 + self._fire_command() + return 1 + return 0 + elif etype == window.EVENT_TYPE_RELEASE: + if self.pressed: + self.pressed = 0 + self.dragging = 0 + return 1 + return 0 + return 0 + + def _handle_drag(self, ey: int): + arrow_sz: int = self.w + if arrow_sz > 16: arrow_sz = 16 + trough_y: int = self.y + arrow_sz + trough_h: int = self.h - 2 * arrow_sz + if trough_h <= 0: return + delta_y: int = ey - self.drag_start_y + delta_first: int = delta_y * 1000 / trough_h + new_first: int = self.drag_start_first + delta_first + slider_size: int = self.slider_last - self.slider_first + if slider_size <= 0: slider_size = 1 + if new_first < 0: new_first = 0 + if new_first + slider_size > 1000: new_first = 1000 - slider_size + if new_first != self.slider_first: + self.slider_first = new_first + self.slider_last = new_first + slider_size + self.action = 5 + self.action_value = new_first + self._fire_command() + + def _fire_command(self): + if self.command is not None: + # 同 Button.handle_event:command 是闭包 {fn, env},必须 rdi = env 后 call fn。 + __asm__(f"""push r12 +push r13 +mov r13, {asminp(self.command, ASM_DESCR.REG_ANY)} +mov rdi, qword ptr [r13 + 8] +mov r12, rsp +and rsp, -16 +call qword ptr [r13] +mov rsp, r12 +pop r13 +pop r12""", + op=[ASM_DESCR.CLOBBER_MEMORY, + ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, + ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, + ASM_DESCR.CLOBBER_RDI, + ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, + ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) + + def set(self, first: int, last: int): + self.slider_first = first + self.slider_last = last + +virtual class FBCanvas(Widget): + _fb: uint32* + _cw: int + _ch: int + + def __init__(self, master: Misc*, w: int, h: int): + self.x = 0 + self.y = 0 + self.w = w + self.h = h + self.visible = 1 + self._misc_setup(master) + self.widget_type = WIDGET_FBCANVAS + self.focused = 0 + self.hovered = 0 + self._fb = None + self._cw = w + self._ch = h + _register_widget(master, Widget*(self)) + + def draw(self, fb: uint32*, fw: int, fh: int): + if not self._fb: return + if self._cw <= 0 or self._ch <= 0: return + copy_w: int = self._cw + if copy_w > self.w: copy_w = self.w + copy_h: int = self._ch + if copy_h > self.h: copy_h = self.h + for py in range(copy_h): + ty: int = self.y + py + if ty < 0 or ty >= fh: continue + for px in range(copy_w): + tx: int = self.x + px + if tx < 0 or tx >= fw: continue + fb[ty * fw + tx] = self._fb[py * self._cw + px] + + def handle_event(self, evt: window.InputEvent*) -> int: + return 0 + + def set_fb(self, fb: uint32*, cw: int, ch: int): + self._fb = fb + self._cw = cw + self._ch = ch + + def get_fb(self) -> uint32*: + return self._fb + + def get_w(self) -> int: + return self._cw + + def get_h(self) -> int: + return self._ch + +virtual class Text(Widget): + lines: [TEXT_MAX_LINES][80]char + line_count: int + input_buf: [80]char + input_len: int + cursor_pos: int + prompt: [80]char + bg_color: uint32 + text_color: uint32 + prompt_color: uint32 + cursor_color: uint32 + border_color: uint32 + on_command: rawptr + history: [16][80]char + history_count: int + history_idx: int + scroll_offset: int + max_lines: int + yscrollbar: rawptr + + def __init__(self, master: Misc*, w: int, h: int): + self.x = 0 + self.y = 0 + self.w = w + self.h = h + self.visible = 1 + self._misc_setup(master) + self.widget_type = WIDGET_TEXT + self.focused = 0 + self.hovered = 0 + self.line_count = 0 + self.input_len = 0 + self.cursor_pos = 0 + self.prompt[0] = '>' + self.prompt[1] = ' ' + self.prompt[2] = 0 + self.bg_color = uint32(0x0C0C0C) + self.text_color = uint32(0xCCCCCC) + self.prompt_color = uint32(0xCCCCCC) + self.cursor_color = uint32(0xCCCCCC) + self.border_color = uint32(0x333333) + self.on_command = None + self.history_count = 0 + self.history_idx = 0 + self.scroll_offset = 0 + self.max_lines = TEXT_MAX_LINES + self.yscrollbar = None + for i in range(TEXT_MAX_LINES): + for j in range(79 + 1): + self.lines[i][j] = 0 + for i in range(80): + self.input_buf[i] = 0 + _register_widget(master, Widget*(self)) + + def draw(self, fb: uint32*, fw: int, fh: int): + self._update_scrollbar() + _fill_rect(fb, fw, fh, self.x, self.y, self.w, self.h, self.bg_color) + _fill_rect(fb, fw, fh, self.x, self.y + self.h - 2, self.w, 2, self.border_color) + visible: int = self._visible_lines() + for i in range(visible): + line_idx: int = self.scroll_offset + i + if line_idx < self.line_count: + _draw_text(fb, fw, fh, self.x + 4, self.y + i * CHAR_H, &self.lines[line_idx][0], self.text_color) + # 输入行不是一个固定在底部的控件,而是紧跟在最后一条输出之后的一行; + # 输出增多时它随之下移,超出可视区时由 scroll_offset 把它保持在最后一行。 + prompt_row: int = self.line_count - self.scroll_offset + if prompt_row >= 0 and prompt_row < visible: + prompt_y: int = self.y + prompt_row * CHAR_H + plen: int = int(string.strlen(&self.prompt[0])) + _draw_text(fb, fw, fh, self.x + 4, prompt_y, &self.prompt[0], self.prompt_color) + if self.input_len > 0: + _draw_text(fb, fw, fh, self.x + 4 + plen * CHAR_W, prompt_y, + &self.input_buf[0], self.text_color) + cx_pos: int = self.x + 4 + (plen + self.cursor_pos) * CHAR_W + if cx_pos < self.x + self.w - 4: + _fill_rect(fb, fw, fh, cx_pos, prompt_y, CHAR_W // 2, CHAR_H, self.cursor_color) + + def handle_event(self, evt: window.InputEvent*) -> int: + etype: uint32 = evt.type + ex: int = evt.x + ey: int = evt.y + if etype == window.EVENT_TYPE_CLICK: + if self.contains(ex, ey): + if not self.focused: + self.focused = 1 + tk: Tk* = self.tk + tk.focused_widget = Widget*(self) + prompt_row: int = self.line_count - self.scroll_offset + prompt_y: int = self.y + prompt_row * CHAR_H + if prompt_row >= 0 and prompt_row < self._visible_lines() and ey >= prompt_y and ey < prompt_y + CHAR_H: + plen: int = int(string.strlen(&self.prompt[0])) + rel_x: int = ex - self.x - 4 - plen * CHAR_W + if rel_x < 0: rel_x = 0 + new_pos: int = (rel_x + CHAR_W / 2) / CHAR_W + if new_pos > self.input_len: new_pos = self.input_len + self.cursor_pos = new_pos + return 1 + else: + if self.focused: + self.focused = 0 + tk2: Tk* = self.tk + tk2.focused_widget = None + return 1 + elif etype == window.EVENT_TYPE_WHEEL: + if self.contains(ex, ey): + w: int = int(evt.button) + if w >= 128: w = w - 256 + vis: int = self._visible_lines() + self.scroll_offset = self.scroll_offset - w * 3 + max_off: int = self.line_count + 1 - vis + if max_off < 0: max_off = 0 + if self.scroll_offset < 0: self.scroll_offset = 0 + if self.scroll_offset > max_off: self.scroll_offset = max_off + self._update_scrollbar() + return 1 + return 0 + elif etype == window.EVENT_TYPE_KEY: + if self.focused: + ebtn2: uint8 = evt.button + if ebtn2 == window.KEY_STATE_RELEASE: + return 0 + ch: int = ex + if ch == 8: + if self.cursor_pos > 0: + i: int = self.cursor_pos + while i < self.input_len: + self.input_buf[i - 1] = self.input_buf[i] + i += 1 + self.input_len -= 1 + self.cursor_pos -= 1 + self.input_buf[self.input_len] = 0 + return 1 + elif ch == 13 or ch == 10: + self._exec_input() + self.input_len = 0 + self.cursor_pos = 0 + for i in range(80): + self.input_buf[i] = 0 + return 1 + elif ch == window.VK_LEFT: + if self.cursor_pos > 0: + self.cursor_pos -= 1 + return 1 + elif ch == window.VK_RIGHT: + if self.cursor_pos < self.input_len: + self.cursor_pos += 1 + return 1 + elif ch == window.VK_UP: + if self.history_count > 0: + if self.history_idx < self.history_count: + self.history_idx += 1 + hi: int = self.history_count - self.history_idx + if hi < 0: hi = 0 + for j in range(80): + self.input_buf[j] = self.history[hi][j] + self.input_len = 0 + while self.input_len < 79 and self.input_buf[self.input_len] != 0: + self.input_len += 1 + self.cursor_pos = self.input_len + return 1 + elif ch == window.VK_DOWN: + if self.history_count > 0: + if self.history_idx > 0: + self.history_idx -= 1 + if self.history_idx == 0: + self.input_len = 0 + self.cursor_pos = 0 + for j2 in range(80): + self.input_buf[j2] = 0 + else: + hi2: int = self.history_count - self.history_idx + for j3 in range(80): + self.input_buf[j3] = self.history[hi2][j3] + self.input_len = 0 + while self.input_len < 79 and self.input_buf[self.input_len] != 0: + self.input_len += 1 + self.cursor_pos = self.input_len + return 1 + elif ch == window.VK_HOME: + self.cursor_pos = 0 + return 1 + elif ch == window.VK_END: + self.cursor_pos = self.input_len + return 1 + elif ch == window.VK_DELETE: + if self.cursor_pos < self.input_len: + i: int = self.cursor_pos + while i < self.input_len - 1: + self.input_buf[i] = self.input_buf[i + 1] + i += 1 + self.input_len -= 1 + self.input_buf[self.input_len] = 0 + return 1 + elif ch == window.VK_PRIOR: + if self.scroll_offset > 0: + vis: int = self._visible_lines() + self.scroll_offset -= vis + if self.scroll_offset < 0: self.scroll_offset = 0 + self._update_scrollbar() + return 1 + elif ch == window.VK_NEXT: + vis2: int = self._visible_lines() + max_off: int = self.line_count + 1 - vis2 + if max_off < 0: max_off = 0 + if self.scroll_offset < max_off: + self.scroll_offset += vis2 + if self.scroll_offset > max_off: self.scroll_offset = max_off + self._update_scrollbar() + return 1 + elif ch >= 32 and ch < 127: + if self.input_len < 79: + i: int = self.input_len + while i > self.cursor_pos: + self.input_buf[i] = self.input_buf[i - 1] + i -= 1 + self.input_buf[self.cursor_pos] = char(ch) + self.input_len += 1 + self.cursor_pos += 1 + self.input_buf[self.input_len] = 0 + return 1 + return 0 + + def _exec_input(self): + self.insert(&self.prompt[0]) + plen: int = int(string.strlen(&self.prompt[0])) + if self.input_len > 0: + last: int = self.line_count - 1 + j: int = 0 + while j < self.input_len and plen + j < 79: + self.lines[last][plen + j] = self.input_buf[j] + j += 1 + self.lines[last][plen + j] = 0 + if self.history_count < 16: + for k in range(80): + self.history[self.history_count][k] = self.input_buf[k] + self.history_count += 1 + else: + for i in range(15): + for k2 in range(80): + self.history[i][k2] = self.history[i + 1][k2] + for k3 in range(80): + self.history[15][k3] = self.input_buf[k3] + self.history_idx = 0 + + cmd: rawptr = self.on_command + before_lines: int = self.line_count + if cmd is not None: + # 同 Button.handle_event:on_command 是闭包 {fn, env},必须 rdi = env 后 call fn。 + __asm__(f"""push r12 +push r13 +mov r13, {asminp(cmd, ASM_DESCR.REG_ANY)} +mov rdi, qword ptr [r13 + 8] +mov r12, rsp +and rsp, -16 +call qword ptr [r13] +mov rsp, r12 +pop r13 +pop r12""", + op=[ASM_DESCR.CLOBBER_MEMORY, + ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RCX, + ASM_DESCR.CLOBBER_RDX, ASM_DESCR.CLOBBER_RSI, + ASM_DESCR.CLOBBER_RDI, + ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9, + ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11]) + else: + self.insert("(no callback)") + + # 本命令确实产生了输出时,在日志与下一个输入行之间留一个空行。 + # 这样提示符不会紧贴最后一行日志(用实际写入的行数判断,clear 不会被补空行)。 + if self.line_count > before_lines: + self.insert("\n") + + def insert(self, text: str): + start: int = 0 + pos: int = 0 + while True: + ch: char = *(text + pos) + if ch == 0: + if pos > start: + self._insert_substring(text, start, pos) + break + if ch == 10: + self._insert_substring(text, start, pos) + start = pos + 1 + pos += 1 + + def insert_ptr(self, text: const str): + start: int = 0 + pos: int = 0 + while True: + ch: char = *(text + pos) + if ch == 0: + if pos > start: + self._insert_substring(text, start, pos) + break + if ch == 10: + self._insert_substring(text, start, pos) + start = pos + 1 + pos += 1 + + def _insert_substring(self, text: const str, start: int, end: int): + if self.line_count >= self.max_lines: + for i in range(self.max_lines - 1): + for j in range(79 + 1): + self.lines[i][j] = self.lines[i + 1][j] + self.line_count -= 1 + if self.scroll_offset > 0: + self.scroll_offset -= 1 + idx: int = self.line_count + j: int = 0 + k: int = start + while k < end and j < 79: + ch: char = *(text + k) + if ch == 9: + tab_stop: int = 8 - (j % 8) + while tab_stop > 0 and j < 79: + self.lines[idx][j] = 32 + j += 1 + tab_stop -= 1 + elif ch == 13: + pass + else: + self.lines[idx][j] = ch + j += 1 + k += 1 + self.lines[idx][j] = 0 + self.line_count += 1 + vis: int = self._visible_lines() + total: int = self.line_count + 1 # 输出行 + 输入行 + if total > vis: + self.scroll_offset = total - vis + else: + self.scroll_offset = 0 + self._update_scrollbar() + + def delete(self, start: int, end: int): + self.line_count = 0 + self.scroll_offset = 0 + self._update_scrollbar() + + def get(self) -> const str: + return &self.input_buf[0] + + def set_prompt(self, text: const str): + i: int = 0 + while i < 79: + ch: char = *(text + i) + self.prompt[i] = ch + if ch == 0: break + i += 1 + self.prompt[i] = 0 + + def config(self, text_color: uint32 = None, bg_color: uint32 = None, prompt_color: uint32 = None, cursor_color: uint32 = None, border_color: uint32 = None): + if text_color is not None: + self.text_color = text_color + if bg_color is not None: + self.bg_color = bg_color + if prompt_color is not None: + self.prompt_color = prompt_color + if cursor_color is not None: + self.cursor_color = cursor_color + if border_color is not None: + self.border_color = border_color + + def _visible_lines(self) -> int: + return (self.h - 2) / CHAR_H + + def _update_scrollbar(self): + vis: int = self._visible_lines() + total: int = self.line_count + 1 # 输出行 + 输入行 + if total <= vis: + self.scroll_offset = 0 + else: + max_off: int = total - vis + if self.scroll_offset > max_off: + self.scroll_offset = max_off + if self.yscrollbar is not None: + sb: Scrollbar* = Scrollbar*(self.yscrollbar) + if total <= vis: + sb.set(0, 1000) + else: + first: int = self.scroll_offset * 1000 / total + last: int = (self.scroll_offset + vis) * 1000 / total + sb.set(first, last) + +# ---- Tk:主窗口类 = Wm(窗口管理器协议,无字段)+ Misc(公共状态/方法)---- +virtual class Tk(Wm, Misc): + win_id: int + buf: uint32* + bw: int + bh: int + style: int + bg_color: uint32 + widgets: [MAX_WIDGETS]Widget* + widget_count: int + focused_widget: Widget* + hovered_widget: Widget* + _win_x: int + _win_y: int + _title: [32]char + resizable_w: int + resizable_h: int + _grid_rows: int + _grid_cols: int + _bb: uint32* # 后备缓冲:整帧画完再一次性拷进共享 arena,避免合成器读到半成品帧 + _bb_px: int # 后备缓冲容量(像素数),窗口变大时才重新分配 + _ansi_state: int # ESC 序列解析状态:0=普通 1=已收 ESC 2=已收 ESC[ + _ansi_param: int # CSI 数值参数(如 ESC[5~ 的 5) + + def __init__(self): + global _g_layout_init, _text_mode + # 运行时显式保持 MANAGED:文字交给 explorer 统一用 TTF 渲染, + # 避免每个 Tk 应用各自加载 13MB 字体。 + _text_mode = TEXT_MODE_MANAGED + # Misc 的公共状态:主窗口没有父容器,tk 指向自身(控件经 _misc_setup 继承它) + self.master = None + self.tk = Tk*(self) + self.widget_count = 0 + for i in range(MAX_WIDGETS): + self.widgets[i] = None + self.focused_widget = None + self.hovered_widget = None + self.win_id = -1 + self._win_x = 0 + self._win_y = 0 + self.style = 0 + self.bg_color = uint32(0xFF231919) + self.resizable_w = 1 + self.resizable_h = 1 + self._grid_rows = 0 + self._grid_cols = 0 + if _g_layout_init == 0: + for i in range(MAX_WIDGETS): + _g_wl_type[i] = 0 + _g_wl_side[i] = 0 + _g_wl_fill[i] = 0 + _g_wl_expand[i] = 0 + _g_wl_padx[i] = 0 + _g_wl_pady[i] = 0 + _g_wl_row[i] = 0 + _g_wl_col[i] = 0 + _g_wl_rowspan[i] = 0 + _g_wl_colspan[i] = 1 + _g_wl_sticky[i] = 0 + _g_layout_init = 1 + self._bb = None + self._bb_px = 0 + self._ansi_state = 0 + self._ansi_param = 0 + for j in range(32): + self._title[j] = 0 + + def widget_limit(self) -> int: + c: int = self.widget_count + if c < 0: return 0 + if c > MAX_WIDGETS: return MAX_WIDGETS + return c + + def pack(self, idx: int, side: int, fill: int, expand: int, padx: int, pady: int): + if idx < 0 or idx >= MAX_WIDGETS: return + _g_wl_type[idx] = 2 + _g_wl_side[idx] = side + _g_wl_fill[idx] = fill + _g_wl_expand[idx] = expand + _g_wl_padx[idx] = padx + _g_wl_pady[idx] = pady + + def grid(self, idx: int, row: int, col: int, rowspan: int, colspan: int, sticky: int): + if idx < 0 or idx >= MAX_WIDGETS: return + _g_wl_type[idx] = 3 + _g_wl_row[idx] = row + _g_wl_col[idx] = col + _g_wl_rowspan[idx] = rowspan + _g_wl_colspan[idx] = colspan + _g_wl_sticky[idx] = sticky + + # ---- Wm 协议的落地实现(Tk 持有窗口状态,覆写 Wm 声明的协议面)---- + def title(self, text: str): + i: int = 0 + while i < 31: + ch: char = text[i] + if ch == 0: break + self._title[i] = ch + i += 1 + self._title[i] = 0 + if self.win_id >= 0: + window.set_title(self.win_id, &self._title[0]) + + def set_style(self, s: int): + self.style = s + + def resizable(self, rw: int, rh: int): + self.resizable_w = rw + self.resizable_h = rh + if self.win_id >= 0: + window.set_resizable(self.win_id, rw, rh) + + def geometry(self, x: int, y: int, w: int, h: int): + self._win_x = x + self._win_y = y + self.bw = w + self.bh = h - TITLE_H - 2 + if self.win_id < 0: + self.win_id = window.create_window(x, y, w, h, &self._title[0], self.style) + self.buf = window.get_winbuf(self.win_id) + window.set_resizable(self.win_id, self.resizable_w, self.resizable_h) + else: + window.set_geometry(self.win_id, x, y, w, h) + self.buf = window.get_winbuf(self.win_id) + + def setup(self, x: int, y: int, w: int, h: int, title: const str, style: int): + self._win_x = x + self._win_y = y + self.win_id = window.create_window(x, y, w, h, title, style) + self.buf = window.get_winbuf(self.win_id) + self.bw = w + self.bh = h - TITLE_H - 2 + self.style = style + self.bg_color = uint32(0xFF231919) + + def redraw(self): + global _text_qn + # 窗口槽已回收(用户点了关闭):立即返回,不再触碰共享 arena / flush。 + if self.win_id < 0: return + if window.is_active(self.win_id) == 0: return + self.buf = window.get_winbuf(self.win_id) + sz: int = window.get_win_size(self.win_id) + if sz != 0: + self.bw = sz >> 16 + self.bh = sz & 0xFFFF + if not self.buf: return + if self.bw <= 0 or self.bh <= 0: return + # 先刷新尺寸再布局:窗口 resize 后若沿用上一帧的 bw/bh,布局会滞后一帧。 + self._do_layout() + _text_qn = 0 + # 双缓冲:整帧先画在**进程本地**的后备缓冲里,画完一次性拷进共享 arena,再 flush。 + # 为什么必须这么做:arena 正是 explorer 合成时读的那块内存(render_window_rows + # 直接从它逐行 blit 上屏)。若直接往 arena 上画,合成器会在"整窗已填成背景色、 + # 控件还没画"的中途采样 ⇒ 窗口闪烁;光标移动让 explorer 重画窗口所在行时同样撞上。 + px: int = self.bw * self.bh + if self._bb_px < px: + if self._bb: stdlib.free(rawptr(self._bb)) + self._bb = uint32*(stdlib.malloc(uint64(px * 4))) + self._bb_px = px + tgt: uint32* = self._bb + if not tgt: + tgt = self.buf + _fill_rect(tgt, self.bw, self.bh, 0, 0, self.bw, self.bh, self.bg_color) + for i in range(self.widget_limit()): + w: Widget* = self.widgets[i] + if w is None: continue + if w.visible: + w.draw(tgt, self.bw, self.bh) + if tgt != self.buf: + string.memcpy(self.buf, tgt, uint64(px * 4)) + _flush_text(self.win_id) + window.flush(self.win_id) + + def _do_layout(self): + self._grid_rows = 0 + self._grid_cols = 0 + cav_x: int = 0 + cav_y: int = 0 + cav_w: int = self.bw + cav_h: int = self.bh + for i in range(self.widget_limit()): + w: Widget* = self.widgets[i] + if w is None: continue + lt: int = _g_wl_type[i] + if lt == 2: + side: int = _g_wl_side[i] + fill: int = _g_wl_fill[i] + expand: int = _g_wl_expand[i] + px: int = _g_wl_padx[i] + py: int = _g_wl_pady[i] + ww: int = w.w + wh: int = w.h + # 交叉轴上未 fill 时按 Tk 默认 anchor=center 居中;沿 pack 方向仍贴腔体边缘 + + # padding(与 tkinter 一致)。此前一律贴左上,导致不 fill 的控件全部挤在左边缘。 + if side == PACK_TOP: + if fill == FILL_X or fill == FILL_BOTH: + ww = cav_w - 2 * px + w.x = cav_x + px + else: + w.x = cav_x + px + (cav_w - 2 * px - ww) / 2 + if expand: + if fill == FILL_Y or fill == FILL_BOTH: + wh = cav_h - 2 * py + w.y = cav_y + py + w.w = ww + w.h = wh + cav_y += wh + 2 * py + cav_h -= wh + 2 * py + elif side == PACK_BOTTOM: + if fill == FILL_X or fill == FILL_BOTH: + ww = cav_w - 2 * px + w.x = cav_x + px + else: + w.x = cav_x + px + (cav_w - 2 * px - ww) / 2 + if expand: + if fill == FILL_Y or fill == FILL_BOTH: + wh = cav_h - 2 * py + w.y = cav_y + cav_h - wh - py + w.w = ww + w.h = wh + cav_h -= wh + 2 * py + elif side == PACK_LEFT: + if fill == FILL_Y or fill == FILL_BOTH: + wh = cav_h - 2 * py + w.y = cav_y + py + else: + w.y = cav_y + py + (cav_h - 2 * py - wh) / 2 + if expand: + if fill == FILL_X or fill == FILL_BOTH: + ww = cav_w - 2 * px + w.x = cav_x + px + w.w = ww + w.h = wh + cav_x += ww + 2 * px + cav_w -= ww + 2 * px + elif side == PACK_RIGHT: + if fill == FILL_Y or fill == FILL_BOTH: + wh = cav_h - 2 * py + w.y = cav_y + py + else: + w.y = cav_y + py + (cav_h - 2 * py - wh) / 2 + if expand: + if fill == FILL_X or fill == FILL_BOTH: + ww = cav_w - 2 * px + w.x = cav_x + cav_w - ww - px + w.w = ww + w.h = wh + cav_w -= ww + 2 * px + _container_offset(self, w) + elif lt == 3: + r: int = _g_wl_row[i] + _g_wl_rowspan[i] + c: int = _g_wl_col[i] + _g_wl_colspan[i] + if r > self._grid_rows: self._grid_rows = r + if c > self._grid_cols: self._grid_cols = c + for i in range(self.widget_limit()): + w: Widget* = self.widgets[i] + if w is None: continue + if _g_wl_type[i] == 3: + if self._grid_rows <= 0: self._grid_rows = 1 + if self._grid_cols <= 0: self._grid_cols = 1 + cell_w: int = self.bw / self._grid_cols + cell_h: int = self.bh / self._grid_rows + wx: int = _g_wl_col[i] * cell_w + wy: int = _g_wl_row[i] * cell_h + ww: int = _g_wl_colspan[i] * cell_w + wh: int = _g_wl_rowspan[i] * cell_h + sticky: int = _g_wl_sticky[i] + if sticky == 0: + ox: int = (ww - w.w) / 2 + oy: int = (wh - w.h) / 2 + w.x = wx + ox + w.y = wy + oy + else: + w.x = wx + w.y = wy + if (sticky & 1) != 0: + w.w = ww + if (sticky & 2) != 0: + w.h = wh + _container_offset(self, w) + + def update(self): + self.redraw() + + def winfo_exists(self) -> int: + if self.win_id < 0: return 0 + return window.is_active(self.win_id) + + # ---- Misc 查询的覆写:主窗口用自己的窗口状态回答 winfo_* ---- + def winfo_ismapped(self) -> int: + if self.win_id < 0: return 0 + return window.is_active(self.win_id) + + def winfo_x(self) -> int: + return self._win_x + + def winfo_y(self) -> int: + return self._win_y + + def winfo_width(self) -> int: + return self.bw + + def winfo_height(self) -> int: + return self.bh + + # 无显式焦点控件时,按注册顺序取第一个可见文本控件(Text/Entry) + def _first_text_widget(self) -> Widget*: + for i in range(self.widget_limit()): + w: Widget* = self.widgets[i] + if w is None: continue + if w.visible != 0: + if w.widget_type == WIDGET_TEXT or w.widget_type == WIDGET_ENTRY: + return w + return None + + # 逐字节投递的按键流 -> ANSI CSI 状态机;返回 0=本字节被吞掉,1=可分发 + def _fold_ansi_key(self, evt: window.InputEvent*) -> int: + ch: int = evt.x + if self._ansi_state == 0: + if ch == 27: + self._ansi_state = 1 + self._ansi_param = 0 + return 0 + return 1 + if self._ansi_state == 1: + if ch == 91: + self._ansi_state = 2 + return 0 + # 孤立 ESC:只丢弃 ESC,当前字节照常分发 —— 否则会连带吞掉下一个键入字符。 + self._ansi_state = 0 + return 1 + if ch >= 48 and ch <= 57: + self._ansi_param = self._ansi_param * 10 + (ch - 48) + return 0 + self._ansi_state = 0 + vk: int = 0 + if ch == 65: vk = window.VK_UP + elif ch == 66: vk = window.VK_DOWN + elif ch == 67: vk = window.VK_RIGHT + elif ch == 68: vk = window.VK_LEFT + elif ch == 72: vk = window.VK_HOME + elif ch == 70: vk = window.VK_END + elif ch == 126: + if self._ansi_param == 3: vk = window.VK_DELETE + elif self._ansi_param == 5: vk = window.VK_PRIOR + elif self._ansi_param == 6: vk = window.VK_NEXT + if vk == 0: return 0 + evt.x = vk + return 1 + + # 指针下的最上层控件决定光标形状:Entry/Text → I 形,Button → 手指。 + # 与控件自己的 hover 视觉解耦(后者仍由 dispatch_event 维护)。 + def _update_cursor_for_hover(self, ex: int, ey: int): + hw: Widget* = None + i: int = self.widget_limit() - 1 + while i >= 0: + w: Widget* = self.widgets[i] + if w is None: + i -= 1 + continue + if w.visible and w.contains(ex, ey): + hw = w + break + i -= 1 + if hw == self.hovered_widget: return + self.hovered_widget = hw + ct: int = window.CURSOR_DEFAULT + if hw is not None: + if hw.widget_type == WIDGET_ENTRY or hw.widget_type == WIDGET_TEXT: + ct = window.CURSOR_IBEAM + elif hw.widget_type == WIDGET_BUTTON: + ct = window.CURSOR_HAND + window.set_cursor_type(self.win_id, ct) + + def mainloop(self): + self.redraw() + evt: window.InputEvent + while window.is_active(self.win_id): + result: int = window.poll_event(&evt) + if result != 0: + if evt.win_id == self.win_id: + need_redraw: int = 0 + etype: uint32 = evt.type + if etype == window.EVENT_TYPE_PING: + window.flush(self.win_id) + elif etype == window.EVENT_TYPE_RESIZE: + self.buf = window.get_winbuf(self.win_id) + sz: int = window.get_win_size(self.win_id) + self.bw = sz >> 16 + self.bh = sz & 0xFFFF + need_redraw = 1 + elif etype == window.EVENT_TYPE_KEY or etype == window.EVENT_TYPE_KEY_RELEASE: + folded: int = 1 + if etype == window.EVENT_TYPE_KEY: + folded = self._fold_ansi_key(&evt) + if folded != 0: + fw: Widget* = self.focused_widget + if fw is None: + fw = self._first_text_widget() + if fw is not None: + fw.focused = 1 + self.focused_widget = fw + if fw is not None: + handled: int = fw.dispatch_event(&evt) + if handled: need_redraw = 1 + else: + if etype == window.EVENT_TYPE_HOVER: + self._update_cursor_for_hover(evt.x, evt.y) + i: int = self.widget_limit() - 1 + while i >= 0: + w: Widget* = self.widgets[i] + if w is None: + i -= 1 + continue + if w.visible: + handled2: int = w.dispatch_event(&evt) + if handled2: + need_redraw = 1 + break + i -= 1 + else: + for i in range(self.widget_limit()): + w: Widget* = self.widgets[i] + if w is None: continue + if w.visible: + handled2: int = w.dispatch_event(&evt) + if handled2: need_redraw = 1 + if need_redraw: + self.redraw() + else: + process.yield_cpu()