From 46b7bf4fbc8b694214e8486ad759dfa07df61bee Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Tue, 21 Apr 2026 22:22:31 +0800 Subject: [PATCH] =?UTF-8?q?Wine=E9=80=82=E9=85=8D1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/syscall.md | 14 +- wine/README.md | 14 +- .../__pycache__/cli.cpython-313.pyc | Bin 2956 -> 6968 bytes .../__pycache__/constants.cpython-313.pyc | Bin 3938 -> 4020 bytes .../__pycache__/fb_window.cpython-313.pyc | Bin 0 -> 7261 bytes .../__pycache__/runner.cpython-313.pyc | Bin 80042 -> 103432 bytes wine/cleonos_wine_lib/cli.py | 72 ++- wine/cleonos_wine_lib/constants.py | 3 + wine/cleonos_wine_lib/fb_window.py | 143 +++++ wine/cleonos_wine_lib/runner.py | 585 +++++++++++++++++- wine/requirements.txt | 1 + 11 files changed, 813 insertions(+), 19 deletions(-) create mode 100644 wine/cleonos_wine_lib/__pycache__/fb_window.cpython-313.pyc create mode 100644 wine/cleonos_wine_lib/fb_window.py diff --git a/docs/syscall.md b/docs/syscall.md index c87c33f..32a1a85 100644 --- a/docs/syscall.md +++ b/docs/syscall.md @@ -697,8 +697,16 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2); ## 7. Wine 兼容说明 -- `wine/cleonos_wine_lib/runner.py` 当前已覆盖到 `0..80`(含 `stats/fd/exec_pathv_io`)。 -- `DL_*`(`77..79`)在 Wine 中当前为占位实现:`DL_OPEN=-1`、`DL_CLOSE=0`、`DL_SYM=0`。 -- framebuffer 相关 syscall(`81..83`)尚未在 Wine 中实现(会返回未支持路径)。 +- `wine/cleonos_wine_lib/runner.py` 当前已覆盖到 `0..83`(含 `DL_*`、`FB_*`)。 +- `DL_*`(`77..79`)在 Wine 中为“可运行兼容”实现: +- `DL_OPEN`:加载 guest ELF 到当前 Unicorn 地址空间,返回稳定 `handle`,并做引用计数。 +- `DL_SYM`:解析 ELF `SYMTAB/DYNSYM` 并返回 guest 可调用地址。 +- `DL_CLOSE`:引用计数归零后释放句柄。 +- `DL_*` 兼容限制:未实现完整动态链接器语义(例如完整重定位/依赖库链),但对 CLeonOS 现有用户态库调用场景可工作。 +- framebuffer syscall(`81..83`)在 Wine 中已实现兼容: +- `FB_INFO` 返回 framebuffer 参数(默认 `1280x800x32`,可用环境变量 `CLEONOS_WINE_FB_WIDTH/HEIGHT` 调整)。 +- `FB_BLIT` 实现内核同类参数校验并支持 `scale>=1` 绘制。 +- 配合 Wine 参数 `--fb-window` 可将 framebuffer 实时显示到主机窗口(pygame 后端);未启用时保持内存缓冲模式。 +- `FB_CLEAR` 支持清屏颜色写入。 - Wine 在运行时崩溃场景下会生成与内核一致格式的“信号编码退出状态”,可通过 `WAITPID` 读取。 - Wine 当前音频 syscall 为占位实现:`AUDIO_AVAILABLE=0`,`AUDIO_PLAY_TONE=0`,`AUDIO_STOP=1`。 diff --git a/wine/README.md b/wine/README.md index 0aaaff7..4f51dc8 100644 --- a/wine/README.md +++ b/wine/README.md @@ -13,7 +13,7 @@ CLeonOS-Wine 现在改为自研运行器:基于 Python + Unicorn,直接运 - `wine/cleonos_wine_lib/input_pump.py`:主机键盘输入线程 - `wine/cleonos_wine_lib/constants.py`:常量与 syscall ID - `wine/cleonos_wine_lib/platform.py`:Unicorn 导入与平台适配 -- `wine/requirements.txt`:Python 依赖(Unicorn) +- `wine/requirements.txt`:Python 依赖(Unicorn + pygame) ## 安装 @@ -26,6 +26,7 @@ pip install -r wine/requirements.txt ```bash python wine/cleonos_wine.py /hello.elf --rootfs build/x86_64/ramdisk_root python wine/cleonos_wine.py /shell/shell.elf --rootfs build/x86_64/ramdisk_root +python wine/cleonos_wine.py /shell/qrcode.elf --rootfs build/x86_64/ramdisk_root --fb-window -- --ecc H "hello wine" ``` 也支持直接传宿主路径: @@ -37,7 +38,7 @@ python wine/cleonos_wine.py build/x86_64/ramdisk_root/shell/shell.elf --rootfs b ## 支持 - ELF64 (x86_64) PT_LOAD 段装载 -- CLeonOS `int 0x80` syscall 0..80(含 `FD_*`、`PROC_*`、`STATS_*`、`EXEC_PATHV_IO`) +- CLeonOS `int 0x80` syscall 0..83(含 `FD_*`、`DL_*`、`FB_*`、`PROC_*`、`STATS_*`、`EXEC_PATHV_IO`) - TTY 输出与键盘输入队列 - rootfs 文件/目录访问(`FS_*`) - `/temp` 写入限制(`FS_MKDIR/WRITE/APPEND/REMOVE`) @@ -48,11 +49,20 @@ python wine/cleonos_wine.py build/x86_64/ramdisk_root/shell/shell.elf --rootfs b - 进程枚举与快照(`PROC_COUNT/PROC_PID_AT/PROC_SNAPSHOT/PROC_KILL`) - syscall 统计(`STATS_TOTAL/STATS_ID_COUNT/STATS_RECENT_*`) - 文件描述符(`FD_OPEN/FD_READ/FD_WRITE/FD_CLOSE/FD_DUP`) +- 动态库兼容加载(`DL_OPEN/DL_CLOSE/DL_SYM`,基于 ELF 符号解析) +- framebuffer 兼容(`FB_INFO/FB_BLIT/FB_CLEAR`,支持内存缓冲与窗口显示) - 异常退出状态编码与故障元信息(`PROC_LAST_SIGNAL/PROC_FAULT_*`) ## 参数 - `--no-kbd`:关闭输入线程 +- `--fb-window`:启用 framebuffer 窗口显示(pygame) +- `--fb-scale N`:窗口缩放倍数(默认 `2`) +- `--fb-max-fps N`:窗口刷新上限(默认 `60`) +- `--fb-hold-ms N`:程序退出后窗口保留毫秒数(默认 `2500`,静态图更容易看清) +- `--argv-line "..."`:直接指定 guest 参数行(等价于 shell 参数字符串) +- `--cwd PATH`:写入命令上下文中的工作目录(默认 `/`) +- `--` 之后内容:作为 guest argv 透传(推荐) - `--max-exec-depth N`:设置 exec 嵌套深度上限 - `--verbose`:打印更多日志 diff --git a/wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc index f83dbe1fac7f28ce57b0d101f64b4c0ac6414f81..57692939eccaad6db87da1aed83818a8c2dda53c 100644 GIT binary patch literal 6968 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 A$^ZZW delta 1232 zcmah|&2JM&6rb6h{m!lvC&tc(U^@woERm!T9E1dwsNza-)S!ADa8X1vOR4Y4?UK29b=Qd7?@-CjWjXAjcs1T66=_G zc2zh#M2~S>tPBpqdx$+Wzh*yL3kwI}dlgh7z=)H2Da1+f)cR6BTmy814W^2$ozgI7 zms5oi7neB~q{G+ENf*`9Y;Q21g;Zj3h-W3ZHpsph9vy#oCb`%`^TG!hvMc-#Lvh8U z80Ab7^=iWExMB@x|hWjWV_IL^vkrxjnFcj1{iBLe^ z^NC$@M3p)A%dk`?mq<%=IR6r9;wHK%=1~)utd*OV_vO+`<+@j?)h^er7x++;=Gwad z9G;BXnMIotj;@r{p6ahR>zj?}yPizj?Wh^D&CDYED>I%woFJG)dIA)TE*)T}j!kY) z0@Ogk$o9mI#ukMV=|sGxoq)nCaQ~!1TVenHIGpLNjvq$-h@!u>N(*3>`HjG*J(2oR z2oVd1LUV9THX3UIJ;Nb*4NqIVc{}K-k-2?gXK|mfi`gk*UuS=0?`r=HX_mb6+5QOo zIrk|($6n<=n*J|@=f5*kA!wKAm3eX38mr8TyR#Cn zqX*ejKZ4+;q^Dl`0eWxCQmE~vo;sGw=L)_G!uZZx~61$JyYo zdwyX&Rx|P)p10wF_ursso^LQ`>dwNuQ(d-OHlA7 z{z0#>JPj;uYrV({CB~%!A+^N_6aDoy;oHtC>Kwk~*_&nzm7l5k;T}ZrO|upwXR=#! zY|-*M9l&MFAjs=aRS`IPVWr72Yzc?aQyI&X^Jk)}hV6Pd=Is06m!2)j>kByzCa+=K z^-sbn*e8AX@Hmr;q0P%gYdE~q_p;qEDMYK!CAJWVOGjCvHL6Zs>Dd$&N$hNjd|_uV zybCsW^RL77?atQj_QrmYN`HMWKPc)wYASjpYBG24k6yGp&Aw;9#CP71&GpQYVf7)Ko>ED%#9rUYwfzz&ung%!<*`;dd(NvW!! z33*5hR|N(c;SR`Ug{xJx_}(XPAD6v|ZdtaYJEm#4`M9 zn{frxw0eHia=aiKP75tGDH8W#m6o1Y(D?KJqIECKSh+w#4eX&=O}m+aJL2? r7Wk)#_lk#z=9uTPfB&*rw@a;>v4mYp@Jly0I@{`->@E*A6wdtuY-Wf; diff --git a/wine/cleonos_wine_lib/__pycache__/fb_window.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/fb_window.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95fa9d0fb21e504cfbdec732b782d6dd6c7f8dea GIT binary patch literal 7261 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%0DO}HPY;Xr>Py`H2G(4&zRm8k}oX#XKIMLc9 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 JB3if}{{aA!{Y3x( delta 22138 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 bytes: + if size <= 0: + return b"" + + data = text.encode("utf-8", errors="replace") + if len(data) >= size: + data = data[: size - 1] + return data + (b"\x00" * (size - len(data))) + + +def _normalize_guest_cwd(cwd: str) -> str: + value = (cwd or "").strip().replace("\\", "/") + if not value: + return "/" + if not value.startswith("/"): + value = "/" + value + return value + + +def _write_command_context(rootfs: Path, guest_path: str, guest_args: List[str], cwd: str) -> None: + name = Path(guest_path).name + cmd = name[:-4] if name.lower().endswith(".elf") else name + arg = " ".join(guest_args) + ctx_payload = b"".join( + ( + _encode_cstr(cmd, 32), + _encode_cstr(arg, 160), + _encode_cstr(_normalize_guest_cwd(cwd), 192), + ) + ) + ctx_path = rootfs / "temp" / ".ush_cmd_ctx.bin" + ctx_path.parent.mkdir(parents=True, exist_ok=True) + ctx_path.write_bytes(ctx_payload) + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="CLeonOS-Wine: run CLeonOS ELF with Unicorn.") parser.add_argument("elf", help="Target ELF path. Supports /guest/path or host file path.") parser.add_argument("--rootfs", help="Rootfs directory (default: build/x86_64/ramdisk_root).") parser.add_argument("--no-kbd", action="store_true", help="Disable host keyboard input pump.") + parser.add_argument("--fb-window", action="store_true", help="Enable host framebuffer window (pygame backend).") + parser.add_argument("--fb-scale", type=int, default=2, help="Framebuffer window scale factor (default: 2).") + parser.add_argument("--fb-max-fps", type=int, default=60, help="Framebuffer present FPS limit (default: 60).") + parser.add_argument("--fb-hold-ms", type=int, default=2500, help="Keep fb window visible after app exits (ms).") + parser.add_argument("--argv-line", default="", help="Guest argv as one line (whitespace-separated).") + parser.add_argument("--cwd", default="/", help="Guest cwd for command-context apps.") + parser.add_argument("guest_args", nargs="*", help="Guest args (for dash-prefixed args use '--').") parser.add_argument("--max-exec-depth", type=int, default=DEFAULT_MAX_EXEC_DEPTH, help="Nested exec depth guard.") parser.add_argument("--verbose", action="store_true", help="Enable verbose runner output.") return parser.parse_args() @@ -20,6 +65,8 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() + guest_args = list(args.guest_args or []) + argv_items: List[str] try: rootfs = resolve_rootfs(args.rootfs) @@ -28,6 +75,24 @@ def main() -> int: print(f"[WINE][ERROR] {exc}", file=sys.stderr) return 2 + if len(guest_args) > 0 and guest_args[0] == "--": + guest_args = guest_args[1:] + + if (args.argv_line or "").strip(): + try: + guest_args = shlex.split(args.argv_line) + except Exception: + guest_args = (args.argv_line or "").split() + + argv_items = [guest_path] + argv_items.extend(guest_args) + + if guest_args: + try: + _write_command_context(rootfs, guest_path, guest_args, args.cwd) + except Exception as exc: + print(f"[WINE][WARN] failed to write command context: {exc}", file=sys.stderr) + if args.verbose: print("[WINE] backend=unicorn", file=sys.stderr) print(f"[WINE] rootfs={rootfs}", file=sys.stderr) @@ -44,6 +109,11 @@ def main() -> int: no_kbd=args.no_kbd, verbose=args.verbose, top_level=True, + fb_window=args.fb_window, + fb_scale=max(1, args.fb_scale), + fb_max_fps=max(1, args.fb_max_fps), + fb_hold_ms=max(0, args.fb_hold_ms), + argv_items=argv_items, ) ret = runner.run() if ret is None: @@ -51,4 +121,4 @@ def main() -> int: if args.verbose: print(f"\n[WINE] exit=0x{ret:016X}", file=sys.stderr) - return int(ret & 0xFF) \ No newline at end of file + return int(ret & 0xFF) diff --git a/wine/cleonos_wine_lib/constants.py b/wine/cleonos_wine_lib/constants.py index 310aa0f..5c5be38 100644 --- a/wine/cleonos_wine_lib/constants.py +++ b/wine/cleonos_wine_lib/constants.py @@ -89,6 +89,9 @@ SYS_DL_OPEN = 77 SYS_DL_CLOSE = 78 SYS_DL_SYM = 79 SYS_EXEC_PATHV_IO = 80 +SYS_FB_INFO = 81 +SYS_FB_BLIT = 82 +SYS_FB_CLEAR = 83 # proc states (from cleonos/c/include/cleonos_syscall.h) PROC_STATE_UNUSED = 0 diff --git a/wine/cleonos_wine_lib/fb_window.py b/wine/cleonos_wine_lib/fb_window.py new file mode 100644 index 0000000..57c9865 --- /dev/null +++ b/wine/cleonos_wine_lib/fb_window.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import sys +import time +from typing import Optional + +from .state import SharedKernelState + + +CLEONOS_KEY_LEFT = 0x01 +CLEONOS_KEY_RIGHT = 0x02 +CLEONOS_KEY_UP = 0x03 +CLEONOS_KEY_DOWN = 0x04 + + +class FBWindow: + def __init__(self, pygame_mod, width: int, height: int, scale: int, max_fps: int, *, verbose: bool = False) -> None: + self._pygame = pygame_mod + self.width = int(width) + self.height = int(height) + self.scale = max(1, int(scale)) + self._verbose = bool(verbose) + self._closed = False + self._last_present_ns = 0 + self._frame_interval_ns = int(1_000_000_000 / max_fps) if int(max_fps) > 0 else 0 + + window_w = self.width * self.scale + window_h = self.height * self.scale + + self._pygame.display.set_caption("CLeonOS Wine Framebuffer") + self._screen = self._pygame.display.set_mode((window_w, window_h)) + + @classmethod + def create( + cls, + width: int, + height: int, + scale: int, + max_fps: int, + *, + verbose: bool = False, + ) -> Optional["FBWindow"]: + try: + import pygame # type: ignore + except Exception as exc: + print(f"[WINE][WARN] framebuffer window disabled: pygame import failed ({exc})", file=sys.stderr) + return None + + try: + pygame.init() + pygame.display.init() + return cls(pygame, width, height, scale, max_fps, verbose=verbose) + except Exception as exc: + print(f"[WINE][WARN] framebuffer window disabled: unable to init display ({exc})", file=sys.stderr) + try: + pygame.quit() + except Exception: + pass + return None + + def close(self) -> None: + if self._closed: + return + + self._closed = True + try: + self._pygame.display.quit() + except Exception: + pass + try: + self._pygame.quit() + except Exception: + pass + + def is_closed(self) -> bool: + return self._closed + + def pump_input(self, state: SharedKernelState) -> None: + if self._closed: + return + + try: + events = self._pygame.event.get() + except Exception: + self.close() + return + + for event in events: + if event.type == self._pygame.QUIT: + self.close() + continue + + if event.type != self._pygame.KEYDOWN: + continue + + key = event.key + ch = 0 + + if key == self._pygame.K_LEFT: + ch = CLEONOS_KEY_LEFT + elif key == self._pygame.K_RIGHT: + ch = CLEONOS_KEY_RIGHT + elif key == self._pygame.K_UP: + ch = CLEONOS_KEY_UP + elif key == self._pygame.K_DOWN: + ch = CLEONOS_KEY_DOWN + elif key in (self._pygame.K_RETURN, self._pygame.K_KP_ENTER): + ch = ord("\n") + elif key == self._pygame.K_BACKSPACE: + ch = 8 + elif key == self._pygame.K_ESCAPE: + ch = 27 + elif key == self._pygame.K_TAB: + ch = ord("\t") + else: + text = getattr(event, "unicode", "") + if isinstance(text, str) and len(text) == 1: + code = ord(text) + if 32 <= code <= 126: + ch = code + + if ch != 0: + state.push_key(ch) + + def present(self, pixels_bgra: bytearray, *, force: bool = False) -> bool: + if self._closed: + return False + + now_ns = time.monotonic_ns() + if not force and self._frame_interval_ns > 0 and (now_ns - self._last_present_ns) < self._frame_interval_ns: + return False + + try: + src = self._pygame.image.frombuffer(pixels_bgra, (self.width, self.height), "BGRA") + if self.scale != 1: + src = self._pygame.transform.scale(src, (self.width * self.scale, self.height * self.scale)) + self._screen.blit(src, (0, 0)) + self._pygame.display.flip() + self._last_present_ns = now_ns + return True + except Exception: + self.close() + return False diff --git a/wine/cleonos_wine_lib/runner.py b/wine/cleonos_wine_lib/runner.py index 57b1384..cffb1de 100644 --- a/wine/cleonos_wine_lib/runner.py +++ b/wine/cleonos_wine_lib/runner.py @@ -4,7 +4,7 @@ import os import struct import sys import time -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -48,6 +48,9 @@ from .constants import ( SYS_FD_OPEN, SYS_FD_READ, SYS_FD_WRITE, + SYS_FB_BLIT, + SYS_FB_CLEAR, + SYS_FB_INFO, SYS_FS_APPEND, SYS_FS_CHILD_COUNT, SYS_FS_GET_CHILD_NAME, @@ -112,6 +115,7 @@ from .constants import ( u64, u64_neg1, ) +from .fb_window import FBWindow from .input_pump import InputPump from .platform import ( Uc, @@ -165,6 +169,19 @@ class FDEntry: tty_index: int = 0 +@dataclass +class DLImage: + handle: int + guest_path: str + host_path: str + owner_pid: int + ref_count: int + map_start: int + map_end: int + load_bias: int + symbols: Dict[str, int] = field(default_factory=dict) + + EXEC_PATH_MAX = 192 EXEC_ARG_LINE_MAX = 256 EXEC_ENV_LINE_MAX = 512 @@ -174,6 +191,14 @@ EXEC_ITEM_MAX = 128 EXEC_STATUS_SIGNAL_FLAG = 1 << 63 PROC_PATH_MAX = 192 FD_MAX = 64 +DL_MAX_NAME = 192 +DL_MAX_SYMBOL = 128 +DL_BASE_START = 0x0000000100000000 +DL_BASE_GAP = 0x0000000000100000 +FB_DEFAULT_WIDTH = 1280 +FB_DEFAULT_HEIGHT = 800 +FB_MAX_DIM = 4096 +FB_MAX_UPLOAD_BYTES = 64 * 1024 * 1024 class CLeonOSWineNative: @@ -194,6 +219,10 @@ class CLeonOSWineNative: argv_items: Optional[List[str]] = None, env_items: Optional[List[str]] = None, inherited_fds: Optional[Dict[int, FDEntry]] = None, + fb_window: bool = False, + fb_scale: int = 2, + fb_max_fps: int = 60, + fb_hold_ms: int = 2500, ) -> None: self.elf_path = elf_path self.rootfs = rootfs @@ -206,6 +235,10 @@ class CLeonOSWineNative: self.top_level = top_level self.pid = int(pid) self.ppid = int(ppid) + self.fb_window = bool(fb_window) + self.fb_scale = max(1, int(fb_scale)) + self.fb_max_fps = max(1, int(fb_max_fps)) + self.fb_hold_ms = max(0, int(fb_hold_ms)) self.argv_items = list(argv_items) if argv_items is not None else [] self.env_items = list(env_items) if env_items is not None else [] self._exit_requested = False @@ -222,6 +255,20 @@ class CLeonOSWineNative: self._tty_index = int(self.state.tty_active) self._fds: Dict[int, FDEntry] = {} self._fd_inherited = inherited_fds if inherited_fds is not None else {} + self._dl_images: Dict[int, DLImage] = {} + self._dl_path_to_handle: Dict[str, int] = {} + self._dl_next_handle = 1 + self._dl_next_base = DL_BASE_START + + self._fb_width = self._bounded_env_int("CLEONOS_WINE_FB_WIDTH", FB_DEFAULT_WIDTH, 64, FB_MAX_DIM) + self._fb_height = self._bounded_env_int("CLEONOS_WINE_FB_HEIGHT", FB_DEFAULT_HEIGHT, 64, FB_MAX_DIM) + self._fb_bpp = 32 + self._fb_pitch = self._fb_width * 4 + self._fb_pixels = bytearray(self._fb_pitch * self._fb_height) + self._fb_window: Optional[FBWindow] = None + self._fb_window_failed = False + self._fb_dirty = False + self._fb_presented_once = False default_path = self._normalize_guest_path(self.guest_path_hint or f"/{self.elf_path.name}") self.argv_items, self.env_items = self._prepare_exec_items(default_path, self.argv_items, self.env_items) @@ -250,6 +297,89 @@ class CLeonOSWineNative: mode = cls._fd_access_mode(flags) return mode in (O_WRONLY, O_RDWR) + @staticmethod + def _bounded_env_int(name: str, default: int, min_value: int, max_value: int) -> int: + raw = os.environ.get(name) + value = default + + if raw is not None: + try: + value = int(raw.strip(), 10) + except Exception: + value = default + + if value < min_value: + value = min_value + if value > max_value: + value = max_value + return value + + def _ensure_fb_window(self) -> None: + if not self.fb_window: + return + + if self._fb_window is not None or self._fb_window_failed: + return + + self._fb_window = FBWindow.create( + self._fb_width, + self._fb_height, + self.fb_scale, + self.fb_max_fps, + verbose=self.verbose, + ) + + if self._fb_window is None: + self._fb_window_failed = True + + def _fb_poll_window(self) -> None: + self._ensure_fb_window() + if self._fb_window is not None: + self._fb_window.pump_input(self.state) + if self._fb_window.is_closed(): + self._fb_window = None + + def _fb_present(self, *, force: bool = False) -> None: + self._ensure_fb_window() + if self._fb_window is None: + return + + self._fb_window.pump_input(self.state) + did_present = self._fb_window.present(self._fb_pixels, force=force) + if did_present: + self._fb_dirty = False + self._fb_presented_once = True + + if self._fb_window.is_closed(): + self._fb_window = None + + def _fb_mark_dirty(self) -> None: + self._fb_dirty = True + self._fb_present(force=False) + + def _fb_hold_after_exit(self) -> None: + end_ns: int + + if self._fb_window is None: + return + + if self.fb_hold_ms <= 0 or self._fb_presented_once is False: + return + + end_ns = time.monotonic_ns() + (self.fb_hold_ms * 1_000_000) + + while time.monotonic_ns() < end_ns: + if self._fb_window is None: + return + + self._fb_window.pump_input(self.state) + if self._fb_window.is_closed(): + self._fb_window = None + return + + self._fb_window.present(self._fb_pixels, force=True) + time.sleep(0.016) + def _init_default_fds(self) -> None: self._fds = { 0: FDEntry(kind="tty", flags=O_RDONLY, offset=0, tty_index=self._tty_index), @@ -325,6 +455,8 @@ class CLeonOSWineNative: self._install_hooks(uc) self._load_segments(uc) self._prepare_stack_and_return(uc) + self._ensure_fb_window() + self._fb_present(force=True) if self.top_level and not self.no_kbd: self._input_pump = InputPump(self.state) @@ -346,6 +478,11 @@ class CLeonOSWineNative: finally: if self.top_level and self._input_pump is not None: self._input_pump.stop() + if self._fb_window is not None: + self._fb_hold_after_exit() + if self._fb_window is not None: + self._fb_window.close() + self._fb_window = None if interrupted: self.state.mark_exited(self.pid, u64_neg1()) @@ -391,6 +528,8 @@ class CLeonOSWineNative: self._reg_write(uc, UC_X86_REG_RAX, u64(ret)) def _dispatch_syscall(self, uc: Uc, sid: int, arg0: int, arg1: int, arg2: int) -> int: + self._fb_poll_window() + if sid == SYS_LOG_WRITE: data = self._read_guest_bytes(uc, arg0, arg1) text = data.decode("utf-8", errors="replace") @@ -563,11 +702,17 @@ class CLeonOSWineNative: if sid == SYS_FD_DUP: return self._fd_dup(arg0) if sid == SYS_DL_OPEN: - return u64_neg1() + return self._dl_open(uc, arg0) if sid == SYS_DL_CLOSE: - return 0 + return self._dl_close(arg0) if sid == SYS_DL_SYM: - return 0 + return self._dl_sym(uc, arg0, arg1) + if sid == SYS_FB_INFO: + return self._fb_info(uc, arg0) + if sid == SYS_FB_BLIT: + return self._fb_blit(uc, arg0) + if sid == SYS_FB_CLEAR: + return self._fb_clear(arg0) return u64_neg1() @@ -643,6 +788,12 @@ class CLeonOSWineNative: return True return False + def _range_overlaps_mapped(self, start: int, end: int) -> bool: + for ms, me in self._mapped_ranges: + if start < me and end > ms: + return True + return False + @staticmethod def _reg_read(uc: Uc, reg: int) -> int: return int(uc.reg_read(reg)) @@ -715,24 +866,23 @@ class CLeonOSWineNative: return False @staticmethod - def _parse_elf(path: Path) -> ELFImage: - data = path.read_bytes() + def _parse_elf_image_from_blob(data: bytes, *, require_entry: bool) -> ELFImage: if len(data) < 64: - raise RuntimeError(f"ELF too small: {path}") + raise RuntimeError("ELF too small") if data[0:4] != b"\x7fELF": - raise RuntimeError(f"invalid ELF magic: {path}") + raise RuntimeError("invalid ELF magic") if data[4] != 2 or data[5] != 1: - raise RuntimeError(f"unsupported ELF class/endianness: {path}") + raise RuntimeError("unsupported ELF class/endianness") entry = struct.unpack_from(" ELFImage: + data = path.read_bytes() + try: + return CLeonOSWineNative._parse_elf_image_from_blob(data, require_entry=True) + except RuntimeError as exc: + raise RuntimeError(f"{exc}: {path}") from exc + def _fs_node_count(self) -> int: count = 1 for _root, dirs, files in os.walk(self.rootfs): @@ -1087,6 +1245,10 @@ class CLeonOSWineNative: argv_items=argv_items, env_items=env_items, inherited_fds=child_stdio, + fb_window=self.fb_window, + fb_scale=self.fb_scale, + fb_max_fps=self.fb_max_fps, + fb_hold_ms=self.fb_hold_ms, ) child_ret = child.run() @@ -1445,6 +1607,403 @@ class CLeonOSWineNative: self._fds[slot] = self._clone_fd_entry(src) return slot + def _dl_alloc_handle(self) -> int: + handle = int(u64(self._dl_next_handle)) + if handle == 0: + handle = 1 + + while handle in self._dl_images or handle == 0: + handle = int(u64(handle + 1)) + if handle == 0: + handle = 1 + + self._dl_next_handle = int(u64(handle + 1)) + if self._dl_next_handle == 0: + self._dl_next_handle = 1 + + return handle + + def _dl_pick_base(self, min_vaddr: int, max_vaddr: int) -> Tuple[int, int, int]: + old_base = page_floor(min_vaddr) + span = page_ceil(max_vaddr - old_base) + candidate = page_ceil(self._dl_next_base) + retries = 0 + + if span <= 0: + return 0, 0, 0 + + while retries < 1024: + start = candidate + end = start + span + + if not self._range_overlaps_mapped(start, end): + self._dl_next_base = end + DL_BASE_GAP + return start, end, start - old_base + + candidate = end + DL_BASE_GAP + retries += 1 + + return 0, 0, 0 + + @staticmethod + def _dl_rebase_non_exec_segment(data: bytes, old_base: int, old_end: int, delta: int) -> bytes: + if not data or delta == 0: + return data + + patched = bytearray(data) + limit = len(patched) - (len(patched) % 8) + + for off in range(0, limit, 8): + value = struct.unpack_from(" str: + if start < 0 or end <= start or start >= len(blob): + return "" + + limit = min(end, len(blob)) + cur = start + while cur < limit and blob[cur] != 0: + cur += 1 + + if cur <= start: + return "" + + return blob[start:cur].decode("utf-8", errors="ignore") + + @classmethod + def _dl_extract_symbols(cls, blob: bytes, load_bias: int) -> Dict[str, int]: + symbols: Dict[str, int] = {} + sections: List[Tuple[int, int, int, int, int, int, int, int, int, int]] = [] + + if len(blob) < 0x40: + return symbols + + try: + shoff = struct.unpack_from(" len(blob): + return symbols + + for idx in range(shnum): + off = shoff + (idx * shentsize) + try: + sh = struct.unpack_from("= len(sections): + continue + + if sh_entsize < 24: + sh_entsize = 24 + + if sh_offset < 0 or sh_size <= 0 or sh_offset >= len(blob): + continue + + sym_end = min(len(blob), sh_offset + sh_size) + if sym_end <= sh_offset: + continue + + strtab = sections[sh_link] + str_off = int(strtab[4]) + str_size = int(strtab[5]) + + if str_size <= 0 or str_off < 0 or str_off >= len(blob): + continue + + str_end = min(len(blob), str_off + str_size) + count = (sym_end - sh_offset) // sh_entsize + + for i in range(count): + ent_off = sh_offset + (i * sh_entsize) + if ent_off + 24 > len(blob): + break + + try: + st_name, _st_info, _st_other, st_shndx, st_value, _st_size = struct.unpack_from( + " int: + guest_path = self._normalize_guest_path(self._read_guest_cstring(uc, path_ptr, DL_MAX_NAME)) + cached_handle = self._dl_path_to_handle.get(guest_path) + host_path: Optional[Path] + file_blob: bytes + image: ELFImage + e_type: int + min_vaddr: int + max_vaddr: int + map_start: int + map_end: int + load_bias: int + handle: int + owner_pid: int + + if not guest_path.startswith("/") or guest_path == "/": + return u64_neg1() + if len(guest_path.encode("utf-8", errors="replace")) >= DL_MAX_NAME: + return u64_neg1() + + if cached_handle is not None: + cached = self._dl_images.get(cached_handle) + if cached is not None: + cached.ref_count += 1 + return cached.handle + + host_path = self._guest_to_host(guest_path, must_exist=True) + if host_path is None or not host_path.is_file(): + return u64_neg1() + + try: + file_blob = host_path.read_bytes() + image = self._parse_elf_image_from_blob(file_blob, require_entry=False) + e_type = struct.unpack_from(" int: + key = int(handle) + image = self._dl_images.get(key) + + if key == 0 or image is None: + return u64_neg1() + + if image.ref_count > 1: + image.ref_count -= 1 + return 0 + + del self._dl_images[key] + if self._dl_path_to_handle.get(image.guest_path) == key: + del self._dl_path_to_handle[image.guest_path] + return 0 + + def _dl_sym(self, uc: Uc, handle: int, symbol_ptr: int) -> int: + symbol = self._read_guest_cstring(uc, symbol_ptr, DL_MAX_SYMBOL) + image = self._dl_images.get(int(handle)) + addr: Optional[int] + + if image is None or not symbol: + return 0 + + addr = image.symbols.get(symbol) + if addr is None and not symbol.startswith("_"): + addr = image.symbols.get(f"_{symbol}") + + return int(u64(addr)) if addr is not None else 0 + + def _fb_info(self, uc: Uc, out_ptr: int) -> int: + if out_ptr == 0: + return 0 + + self._ensure_fb_window() + + payload = struct.pack( + " int: + if len(self._fb_pixels) == 0: + return 0 + + pixel = struct.pack(" int: + req_blob = self._read_guest_bytes_exact(uc, int(req_ptr), 56) + pixels_ptr: int + src_width: int + src_height: int + src_pitch_bytes: int + dst_x: int + dst_y: int + scale: int + total_src_bytes: int + src_blob: Optional[bytes] + max_src_w: int + max_src_h: int + copy_w: int + copy_h: int + + if req_ptr == 0 or req_blob is None or len(req_blob) != 56: + return 0 + + pixels_ptr, src_width, src_height, src_pitch_bytes, dst_x, dst_y, scale = struct.unpack(" 4096 or src_height > 4096 or scale > 8: + return 0 + + if src_pitch_bytes == 0: + src_pitch_bytes = src_width * 4 + + if src_pitch_bytes < (src_width * 4): + return 0 + + if src_height > (u64_neg1() // src_pitch_bytes): + return 0 + + if dst_x >= self._fb_width or dst_y >= self._fb_height: + return 0 + + total_src_bytes = src_pitch_bytes * src_height + if total_src_bytes == 0 or total_src_bytes > FB_MAX_UPLOAD_BYTES: + return 0 + + src_blob = self._read_guest_bytes_exact(uc, pixels_ptr, total_src_bytes) + if src_blob is None: + return 0 + + max_src_w = (self._fb_width - dst_x + scale - 1) // scale + max_src_h = (self._fb_height - dst_y + scale - 1) // scale + copy_w = min(src_width, max_src_w) + copy_h = min(src_height, max_src_h) + + if copy_w <= 0 or copy_h <= 0: + return 0 + + if scale == 1: + row_bytes = copy_w * 4 + for y in range(copy_h): + src_off = y * src_pitch_bytes + dst_off = ((dst_y + y) * self._fb_width + dst_x) * 4 + self._fb_pixels[dst_off : dst_off + row_bytes] = src_blob[src_off : src_off + row_bytes] + else: + draw_row_pixels = min(self._fb_width - dst_x, copy_w * scale) + draw_row_bytes = draw_row_pixels * 4 + + for y in range(copy_h): + src_row_off = y * src_pitch_bytes + src_row = memoryview(src_blob)[src_row_off : src_row_off + (copy_w * 4)] + expanded = bytearray(copy_w * scale * 4) + write_off = 0 + + for x in range(copy_w): + pixel = bytes(src_row[x * 4 : (x + 1) * 4]) + expanded[write_off : write_off + (scale * 4)] = pixel * scale + write_off += scale * 4 + + draw_y = dst_y + (y * scale) + repeat_h = min(scale, self._fb_height - draw_y) + row_data = expanded[:draw_row_bytes] + + for sy in range(repeat_h): + dst_off = ((draw_y + sy) * self._fb_width + dst_x) * 4 + self._fb_pixels[dst_off : dst_off + draw_row_bytes] = row_data + + self._fb_mark_dirty() + + return 1 + @staticmethod def _truncate_item_text(text: str, max_bytes: int = EXEC_ITEM_MAX) -> str: if max_bytes <= 1: diff --git a/wine/requirements.txt b/wine/requirements.txt index 3873be6..c501798 100644 --- a/wine/requirements.txt +++ b/wine/requirements.txt @@ -1 +1,2 @@ unicorn>=2.0.1 +pygame>=2.5.2