diff --git a/apps/starry/wayland/README.md b/apps/starry/wayland/README.md new file mode 100644 index 0000000000..e75b2eb7a2 --- /dev/null +++ b/apps/starry/wayland/README.md @@ -0,0 +1,314 @@ +# Starry Wayland/Weston App + +This app case runs Weston, the reference Wayland compositor, on StarryOS with +QEMU virtio GPU and input devices. The automated test proves the compositor can +start on the DRM backend and accept a Wayland client connection. The manual +flow below starts the same stack with a VNC display so `gtk4-demo` can be used +interactively. + +## Host Prerequisites + +- QEMU with the target system emulators you want to run. +- Rust nightly and the normal repository build prerequisites. +- `debugfs` from e2fsprogs. On macOS with Homebrew: + +```bash +brew install e2fsprogs +export PATH="/opt/homebrew/opt/e2fsprogs/sbin:$PATH" +``` + +Run all commands from the repository root. + +## Automated Test + +Run the Starry app test through `xtask`: + +```bash +cargo xtask starry app qemu -t wayland --arch riscv64 +cargo xtask starry app qemu -t wayland --arch x86_64 +cargo xtask starry app qemu -t wayland --arch aarch64 +cargo xtask starry app qemu -t wayland --arch loongarch64 +``` + +The successful output contains both markers: + +```text +WAYLAND_TEST_RESULT PASSED +WAYLAND_TEST_PASSED +``` + +The app prebuild step downloads the heavy `llvm21-libs` APK dependency closure +on the host and injects those APKs into the rootfs overlay. The guest script is +[`wayland-test.sh`](wayland-test.sh). It installs any prefetched APKs first, +then installs `weston`, `weston-backend-drm`, and `weston-shell-desktop` from +Alpine apk with bounded HTTP mirror fallback, checks that `/dev/dri/card0` is +present, checks for `/dev/input/event*`, starts Weston with the DRM/pixman +backend, waits for `/tmp/wayland-*`, connects a client when `weston-info` is +available, scans the Weston log for obvious startup errors, and shuts the +compositor down cleanly. + +The automated test exercises these kernel paths: + +| Subsystem | Device / Syscall | Notes | +|-----------|------------------|-------| +| DRM/KMS | `/dev/dri/card0` | Dumb buffers, modeset, page flip | +| Input | `/dev/input/event*` | evdev protocol, libinput probe | +| memfd | `memfd_create` | Wayland SHM buffer backing storage | +| eventfd | `eventfd` | Compositor event loop signalling | +| Unix sockets | `bind` / `sendmsg` / `SCM_RIGHTS` | Wayland socket and fd passing | + +## Manual Reproduction with VNC + +The manual flow intentionally avoids the app test's `shell_init_cmd`; it boots +the same kernel and Alpine rootfs directly so you can type commands at the +StarryOS shell and interact with GTK through VNC. The guest-side Weston and GTK +commands are the same for the direct riscv64 and x86_64 flows. Only the +host-side QEMU launch command differs. For aarch64, use the helper documented +in the aarch64 note below. + +### Step 1: Build the Kernel and Provisioned Rootfs + +Run the automated test once for the architecture you want to reproduce. This +creates the kernel image and rootfs image used by the direct QEMU commands. It +also installs the prefetched Weston dependency closure into that rootfs, so the +manual session does not need guest networking. + +```bash +export PATH="/opt/homebrew/opt/e2fsprogs/sbin:$PATH" +ARCH=riscv64 # or: x86_64 +cargo xtask starry app qemu -t wayland --arch "$ARCH" +``` + +### Step 2: Copy the Rootfs for the Manual Session + +Use a copy so package installation and manual experiments do not dirty the +rootfs used by the app runner. + +```bash +mkdir -p tmp/wayland-manual +cp "tmp/axbuild/rootfs/rootfs-${ARCH}-alpine.img" "tmp/wayland-manual/${ARCH}.img" +``` + +### Step 3: Start QEMU with a VNC Display + +Choose the launch command that matches `ARCH`. The differences are QEMU binary, +machine type, and kernel image path. Choose any free VNC display number; the +TCP port is `5900 + VNC_DISPLAY`. + +```bash +VNC_DISPLAY=30 # example; use any free QEMU VNC display number +VNC_PORT=$((5900 + VNC_DISPLAY)) +``` + +For RISC-V: + +```bash +qemu-system-riscv64 \ + -machine virt \ + -kernel target/riscv64gc-unknown-none-elf/release/starryos.bin \ + -m 1G \ + -cpu rv64 \ + -serial stdio \ + -monitor none \ + -vnc "127.0.0.1:${VNC_DISPLAY}" \ + -device virtio-gpu-pci \ + -device virtio-keyboard-pci \ + -device virtio-mouse-pci \ + -device virtio-blk-pci,drive=disk0 \ + -drive id=disk0,if=none,format=raw,file=tmp/wayland-manual/riscv64.img +``` + +For x86_64, use `xtask` to launch QEMU because the current dynamic x86_64 +platform boots through generated OVMF/ESP artifacts rather than direct +`qemu-system-x86_64 -kernel starryos`. Create a manual VNC QEMU config, replacing +`` with `VNC_DISPLAY`: + +```toml +args = [ + "-m", "1G", + "-serial", "stdio", + "-monitor", "none", + "-vnc", "127.0.0.1:", + "-machine", "q35", + "-device", "virtio-gpu-pci", + "-device", "virtio-keyboard-pci", + "-device", "virtio-mouse-pci", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/wayland-manual/x86_64.img", +] +uefi = true +to_bin = true +timeout = 900 +fail_regex = ["(?i)\\bpanic(?:ked)?\\b"] +``` + +Then launch it: + +```bash +cargo xtask starry app qemu \ + -t wayland \ + --arch x86_64 \ + --qemu-config tmp/wayland-manual/qemu-x86_64-vnc.toml +``` + +Wait for the serial console to print the `root@starry:` prompt. + +### Step 4: Open the VNC Viewer + +Open the display from the host: + +```bash +open "vnc://127.0.0.1::${VNC_PORT}" +``` + +Some VNC clients prefer `127.0.0.1:${VNC_PORT}` when entering the address +manually. The double-colon form is the explicit TCP-port form used by many +command-line VNC tools. + +### Step 5: Verify User-Space Packages + +If Step 1 completed successfully, the copied rootfs already contains Weston and +its runtime dependencies. At the `root@starry:` prompt: + +```sh +command -v weston +ls /usr/lib/libweston-*/drm-backend.so +ls /usr/lib/weston/desktop-shell.so +``` + +Install `gtk4-demo` only if it is not already present in the manual image: + +```sh +if ! command -v gtk4-demo >/dev/null 2>&1; then +apk_branch="$(sed -n 's#.*/\(v[0-9][0-9.]*\)/main#\1#p' /etc/apk/repositories | head -1)" +[ -n "$apk_branch" ] || apk_branch=v3.22 + +for mirror in \ + http://mirrors.huaweicloud.com/alpine \ + http://dl-cdn.alpinelinux.org/alpine \ + http://mirrors.aliyun.com/alpine \ + http://mirrors.tuna.tsinghua.edu.cn/alpine \ + http://mirrors.cernet.edu.cn/alpine +do + printf '%s/%s/main\n%s/%s/community\n' \ + "$mirror" "$apk_branch" "$mirror" "$apk_branch" >/etc/apk/repositories + apk add --no-cache weston weston-backend-drm weston-shell-desktop gtk4.0-demo font-dejavu && break +done +fi +``` + +The install loop is only for a rootfs that does not already contain the manual +GTK demo packages and a kernel/QEMU launch that has working guest networking. +The standard Wayland app build is intentionally offline and does not attach a +virtio-net device. The loop installs Weston, the DRM backend plugin, the +desktop shell plugin, GTK4, Mesa, libdrm, libinput, a usable GTK font, and their +runtime dependencies. +The HTTP mirrors intentionally avoid guest TLS certificate trust failures when +the emulated RTC starts at an invalid date. If one mirror stalls or returns a +truncated package, rerun the loop; the next mirror will be tried. + +### Step 6: Start Weston + +Still inside StarryOS: + +```sh +export XDG_RUNTIME_DIR=/tmp +chmod 0700 /tmp +export LIBSEAT_BACKEND=noop +rm -f /tmp/wayland-* + +weston \ + --backend=drm-backend.so \ + --renderer=pixman \ + --no-config \ + --idle-time=0 \ + --log=/tmp/weston.log & +``` + +Expected evidence in `/tmp/weston.log` includes a `Virtual-1` DRM head and an +enabled output. Confirm that the Wayland socket exists: + +```sh +ls -l /tmp/wayland-* +``` + +### Step 7: Start GTK4 Demo + +```sh +export WAYLAND_DISPLAY="$(basename "$(ls /tmp/wayland-* | head -1)")" +export GDK_BACKEND=wayland +export GSK_RENDERER=cairo +gtk4-demo & +ps | grep gtk4-demo +``` + +The GTK4 demo window should appear in the VNC viewer. Use the VNC mouse and +keyboard to click widgets, open demo rows, scroll lists, and close or reopen +demo windows. For additional compositor evidence: + +```sh +tail -100 /tmp/weston.log +``` + +### Step 8: Optional SHM Client Check + +If `weston-simple-shm` is present in the image, it can be used as a small SHM +rendering client: + +```sh +weston-simple-shm & +``` + +### Step 9: Shut Down + +```sh +pkill gtk4-demo || true +pkill weston || true +poweroff +``` + +## aarch64 Note + +The automated Wayland app test does not require guest networking: the heavy +APK dependency closure is prefetched on the host and injected into the rootfs +overlay before boot. With that offline setup the aarch64 app test follows the +same `cargo xtask starry app qemu -t wayland --arch aarch64` flow as the other +architectures. + +The Cocoa/VNC helper is kept at [`run-hvf.sh`](run-hvf.sh). It uses the same +host-prefetched APK cache approach for Weston and `gtk4-demo`, expands the +manual rootfs, provisions it offline on first run, and then reuses the +provisioned image on later runs: + +```bash +./apps/starry/wayland/run-hvf.sh --no-build --provision-only +STARRY_VNC=9 ./apps/starry/wayland/run-hvf.sh --no-build --vnc-only +``` + +Use `--reprovision` to discard and recreate +`tmp/axbuild/rootfs/rootfs-aarch64-wayland.img`. Set +`STARRY_WAYLAND_ROOTFS_MB` if the default 4096 MiB manual image is not suitable. +The helper requires host `debugfs`, `e2fsck`, `resize2fs`, `python3`, and +`qemu-system-aarch64`; on macOS with Homebrew, the script adds the usual +Homebrew paths automatically. +The normal helper path does not attach a guest virtio-net device because the +Wayland build config is intentionally offline and does not enable network +drivers. +Use `--vnc-only` when you want to drive the serial console from the terminal and +view the GUI through VNC without Cocoa taking terminal focus. + +## Kernel-Side Dependencies + +This app requires: + +- DRM `/dev/dri/card0` support with dumb buffer allocation. +- virtio GPU, keyboard, mouse, and block devices in the QEMU config. +- evdev `/dev/input/event*` support for libinput. +- `memfd_create` and file-descriptor passing over Unix sockets for Wayland SHM. +- `eventfd` for the compositor event loop. +- udev seed data under `/run/udev/data/` for libinput device discovery. +- `starry-kernel/input` and `ax-feat/display` in the app build config. + +The optional manual package-install flow additionally needs a kernel/QEMU launch +with working guest networking if the packages are not already present in the +copied rootfs. diff --git a/apps/starry/wayland/README_CN.md b/apps/starry/wayland/README_CN.md new file mode 100644 index 0000000000..e3c90c0c9e --- /dev/null +++ b/apps/starry/wayland/README_CN.md @@ -0,0 +1,294 @@ +# Starry Wayland/Weston 应用 + +本应用用例在 StarryOS 上通过 QEMU virtio GPU 和输入设备运行 Weston +(Wayland 参考合成器)。自动化测试用于证明合成器可以通过 DRM 后端启动并 +接受 Wayland 客户端连接。下面的手动流程会用 VNC 暴露同一套图形栈,便于交互式 +运行 `gtk4-demo`。 + +## 宿主机前置条件 + +- 安装需要使用的 QEMU system emulator。 +- 安装 Rust nightly 和本仓库常规构建依赖。 +- 安装 e2fsprogs 的 `debugfs`。macOS + Homebrew 可使用: + +```bash +brew install e2fsprogs +export PATH="/opt/homebrew/opt/e2fsprogs/sbin:$PATH" +``` + +以下命令都在仓库根目录执行。 + +## 自动化测试 + +通过 `xtask` 运行 Starry app 测试: + +```bash +cargo xtask starry app qemu -t wayland --arch riscv64 +cargo xtask starry app qemu -t wayland --arch x86_64 +cargo xtask starry app qemu -t wayland --arch aarch64 +cargo xtask starry app qemu -t wayland --arch loongarch64 +``` + +成功输出会包含两个标记: + +```text +WAYLAND_TEST_RESULT PASSED +WAYLAND_TEST_PASSED +``` + +app 的 prebuild 步骤会先在宿主机下载较大的 `llvm21-libs` APK 依赖闭包,并把这些 +APK 注入 rootfs overlay。客体内脚本是 [`wayland-test.sh`](wayland-test.sh)。它会 +先安装预取的 APK,然后通过带超时和 HTTP 镜像 fallback 的 Alpine apk 安装 +`weston`、`weston-backend-drm` 和 `weston-shell-desktop`,检查 `/dev/dri/card0` +是否存在,检查 `/dev/input/event*`,用 DRM/pixman 后端启动 Weston,等待 +`/tmp/wayland-*` socket 出现,在 `weston-info` 可用时连接一个客户端,扫描 Weston +日志中的明显启动错误,然后干净关闭合成器。 + +自动化测试覆盖的内核路径: + +| 子系统 | 设备 / 系统调用 | 说明 | +|--------|------------------|------| +| DRM/KMS | `/dev/dri/card0` | Dumb buffer、modeset、page flip | +| 输入 | `/dev/input/event*` | evdev 协议、libinput 探测 | +| memfd | `memfd_create` | Wayland SHM buffer 后端存储 | +| eventfd | `eventfd` | 合成器事件循环信号 | +| Unix sockets | `bind` / `sendmsg` / `SCM_RIGHTS` | Wayland socket 和 fd 传递 | + +## 使用 VNC 手动复现 + +手动流程刻意绕过 app 测试里的 `shell_init_cmd`,直接启动同一个内核和 Alpine +rootfs。这样可以在 StarryOS shell 中手动输入命令,并通过 VNC 与 GTK 窗口交互。 +直接启动的 riscv64 和 x86_64 流程进入客体后的 Weston 和 GTK 命令完全相同,只有 +宿主机侧的 QEMU 启动命令不同。aarch64 请使用后文 aarch64 说明中的辅助脚本。 + +### 第一步:构建内核和已经 provision 的 rootfs + +先对要复现的架构运行一次自动化测试。该步骤会生成后续直接启动 QEMU 所需的内核镜像 +和 rootfs 镜像,并把宿主机预取的 Weston 依赖闭包安装进该 rootfs,所以手动会话 +不需要客体内网络。 + +```bash +export PATH="/opt/homebrew/opt/e2fsprogs/sbin:$PATH" +ARCH=riscv64 # 或:x86_64 +cargo xtask starry app qemu -t wayland --arch "$ARCH" +``` + +### 第二步:复制手动会话使用的 rootfs + +使用副本,避免手动安装包和调试操作污染 app runner 使用的 rootfs。 + +```bash +mkdir -p tmp/wayland-manual +cp "tmp/axbuild/rootfs/rootfs-${ARCH}-alpine.img" "tmp/wayland-manual/${ARCH}.img" +``` + +### 第三步:用 VNC 显示启动 QEMU + +选择与 `ARCH` 对应的启动命令。这里的差异只有 QEMU binary、machine type 和 kernel +镜像路径。VNC display number 可以任选一个空闲值;对应 TCP 端口是 +`5900 + VNC_DISPLAY`。 + +```bash +VNC_DISPLAY=30 # 示例;可改成任意空闲的 QEMU VNC display number +VNC_PORT=$((5900 + VNC_DISPLAY)) +``` + +RISC-V: + +```bash +qemu-system-riscv64 \ + -machine virt \ + -kernel target/riscv64gc-unknown-none-elf/release/starryos.bin \ + -m 1G \ + -cpu rv64 \ + -serial stdio \ + -monitor none \ + -vnc "127.0.0.1:${VNC_DISPLAY}" \ + -device virtio-gpu-pci \ + -device virtio-keyboard-pci \ + -device virtio-mouse-pci \ + -device virtio-blk-pci,drive=disk0 \ + -drive id=disk0,if=none,format=raw,file=tmp/wayland-manual/riscv64.img +``` + +x86_64 需要通过 `xtask` 启动 QEMU,因为当前 dynamic x86_64 平台通过生成的 +OVMF/ESP 启动,不能再直接使用 `qemu-system-x86_64 -kernel starryos`。创建一个 +手动 VNC QEMU 配置,并把 `` 替换成 `VNC_DISPLAY`: + +```toml +args = [ + "-m", "1G", + "-serial", "stdio", + "-monitor", "none", + "-vnc", "127.0.0.1:", + "-machine", "q35", + "-device", "virtio-gpu-pci", + "-device", "virtio-keyboard-pci", + "-device", "virtio-mouse-pci", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/wayland-manual/x86_64.img", +] +uefi = true +to_bin = true +timeout = 900 +fail_regex = ["(?i)\\bpanic(?:ked)?\\b"] +``` + +然后启动: + +```bash +cargo xtask starry app qemu \ + -t wayland \ + --arch x86_64 \ + --qemu-config tmp/wayland-manual/qemu-x86_64-vnc.toml +``` + +等待串口控制台出现 `root@starry:` 提示符。 + +### 第四步:打开 VNC 查看器 + +在宿主机上打开显示: + +```bash +open "vnc://127.0.0.1::${VNC_PORT}" +``` + +有些 VNC 客户端手动输入地址时更喜欢 `127.0.0.1:${VNC_PORT}`。双冒号形式表示显式 +TCP 端口,许多命令行 VNC 工具使用这种写法。 + +### 第五步:确认用户态包 + +如果第一步已经成功完成,复制出的 rootfs 已经包含 Weston 及其运行时依赖。在 +`root@starry:` 提示符下执行: + +```sh +command -v weston +ls /usr/lib/libweston-*/drm-backend.so +ls /usr/lib/weston/desktop-shell.so +``` + +只有在手动镜像里还没有 `gtk4-demo` 时才安装: + +```sh +if ! command -v gtk4-demo >/dev/null 2>&1; then +apk_branch="$(sed -n 's#.*/\(v[0-9][0-9.]*\)/main#\1#p' /etc/apk/repositories | head -1)" +[ -n "$apk_branch" ] || apk_branch=v3.22 + +for mirror in \ + http://mirrors.huaweicloud.com/alpine \ + http://dl-cdn.alpinelinux.org/alpine \ + http://mirrors.aliyun.com/alpine \ + http://mirrors.tuna.tsinghua.edu.cn/alpine \ + http://mirrors.cernet.edu.cn/alpine +do + printf '%s/%s/main\n%s/%s/community\n' \ + "$mirror" "$apk_branch" "$mirror" "$apk_branch" >/etc/apk/repositories + apk add --no-cache weston weston-backend-drm weston-shell-desktop gtk4.0-demo font-dejavu && break +done +fi +``` + +这段安装循环只用于手动 rootfs 尚未包含 GTK demo 包、且内核/QEMU 启动方式提供可用 +客体网络的情况。标准 Wayland app build 刻意走离线流程,不挂载 virtio-net 设备。 +循环会安装 Weston、DRM 后端插件、desktop shell 插件、GTK4、Mesa、libdrm、 +libinput、可用的 GTK 字体以及相关运行时依赖。 +这里故意使用 HTTP 镜像,以避开客体 RTC 初始日期不正确时触发的 TLS 证书信任问题。 +如果某个镜像卡住或下载到截断的包,重新执行这段循环即可继续尝试下一个镜像。 + +### 第六步:启动 Weston + +继续在 StarryOS 内执行: + +```sh +export XDG_RUNTIME_DIR=/tmp +chmod 0700 /tmp +export LIBSEAT_BACKEND=noop +rm -f /tmp/wayland-* + +weston \ + --backend=drm-backend.so \ + --renderer=pixman \ + --no-config \ + --idle-time=0 \ + --log=/tmp/weston.log & +``` + +`/tmp/weston.log` 中应能看到 `Virtual-1` DRM head 和启用的输出。确认 Wayland +socket 已创建: + +```sh +ls -l /tmp/wayland-* +``` + +### 第七步:启动 GTK4 Demo + +```sh +export WAYLAND_DISPLAY="$(basename "$(ls /tmp/wayland-* | head -1)")" +export GDK_BACKEND=wayland +export GSK_RENDERER=cairo +gtk4-demo & +ps | grep gtk4-demo +``` + +GTK4 demo 窗口应出现在 VNC 查看器中。使用 VNC 的鼠标和键盘点击控件、打开 demo +条目、滚动列表、关闭或重新打开 demo 窗口。需要更多合成器证据时可查看: + +```sh +tail -100 /tmp/weston.log +``` + +### 第八步:可选的 SHM 客户端检查 + +如果镜像中存在 `weston-simple-shm`,可以用它作为很小的 SHM 渲染客户端: + +```sh +weston-simple-shm & +``` + +### 第九步:关闭 + +```sh +pkill gtk4-demo || true +pkill weston || true +poweroff +``` + +## aarch64 说明 + +自动化 Wayland app 测试不需要客体内网络:较大的 APK 依赖闭包会先在宿主机预取, +再注入 rootfs overlay。使用这个离线流程时,aarch64 app 测试和其他架构一样通过 +`cargo xtask starry app qemu -t wayland --arch aarch64` 运行。 + +Cocoa/VNC 辅助脚本保留在 [`run-hvf.sh`](run-hvf.sh)。它同样使用宿主机预取 +APK 的方式准备 Weston 和 `gtk4-demo`,会扩展手动 rootfs,首次运行时离线 +provision,之后复用已经 provision 好的镜像: + +```bash +./apps/starry/wayland/run-hvf.sh --no-build --provision-only +STARRY_VNC=9 ./apps/starry/wayland/run-hvf.sh --no-build --vnc-only +``` + +使用 `--reprovision` 可以丢弃并重新创建 +`tmp/axbuild/rootfs/rootfs-aarch64-wayland.img`。如果默认 4096 MiB 的手动镜像 +不合适,可以设置 `STARRY_WAYLAND_ROOTFS_MB`。辅助脚本需要宿主机提供 `debugfs`、 +`e2fsck`、`resize2fs`、`python3` 和 `qemu-system-aarch64`;在 macOS Homebrew +环境下,脚本会自动加入常见 Homebrew 路径。 +辅助脚本的常规路径不会挂载客体 virtio-net 设备,因为 Wayland build config +刻意走离线流程,也没有启用网络驱动。 +需要在终端里操作串口、同时通过 VNC 查看图形界面时,使用 `--vnc-only`,避免 +Cocoa 抢占终端焦点。 + +## 内核侧依赖 + +本应用需要: + +- 支持 dumb buffer 分配的 DRM `/dev/dri/card0`。 +- QEMU 配置中启用 virtio GPU、keyboard、mouse 和 block 设备。 +- 支持 libinput 枚举的 evdev `/dev/input/event*`。 +- Wayland SHM 所需的 `memfd_create` 和 Unix socket fd 传递。 +- 合成器事件循环使用的 `eventfd`。 +- libinput 设备发现所需的 `/run/udev/data/` udev seed。 +- app build config 中启用 `starry-kernel/input` 和 `ax-feat/display`。 + +如果复制出的 rootfs 中还没有这些用户态包,可选的手动安装包流程还需要启用了可用 +客体网络的内核/QEMU 启动方式。 diff --git a/apps/starry/wayland/build-aarch64-unknown-none-softfloat.toml b/apps/starry/wayland/build-aarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..75cd11f462 --- /dev/null +++ b/apps/starry/wayland/build-aarch64-unknown-none-softfloat.toml @@ -0,0 +1,13 @@ +target = "aarch64-unknown-none-softfloat" +env = {} +log = "Warn" +features = [ + "ax-feat/rtc", + "ax-feat/display", + "ax-driver/serial", + "ax-driver/virtio-blk", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "starry-kernel/input", +] +plat_dyn = true diff --git a/apps/starry/wayland/build-loongarch64-unknown-none-softfloat.toml b/apps/starry/wayland/build-loongarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..1c1f527ff9 --- /dev/null +++ b/apps/starry/wayland/build-loongarch64-unknown-none-softfloat.toml @@ -0,0 +1,13 @@ +target = "loongarch64-unknown-none-softfloat" +env = {} +log = "Warn" +features = [ + "ax-feat/rtc", + "ax-feat/display", + "ax-driver/serial", + "ax-driver/virtio-blk", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "starry-kernel/input", +] +plat_dyn = true diff --git a/apps/starry/wayland/build-riscv64gc-unknown-none-elf.toml b/apps/starry/wayland/build-riscv64gc-unknown-none-elf.toml new file mode 100644 index 0000000000..9f10267e28 --- /dev/null +++ b/apps/starry/wayland/build-riscv64gc-unknown-none-elf.toml @@ -0,0 +1,13 @@ +target = "riscv64gc-unknown-none-elf" +env = {} +log = "Warn" +features = [ + "ax-feat/rtc", + "ax-feat/display", + "ax-driver/serial", + "ax-driver/virtio-blk", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "starry-kernel/input", +] +plat_dyn = true diff --git a/apps/starry/wayland/build-x86_64-unknown-none.toml b/apps/starry/wayland/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..9a36609b73 --- /dev/null +++ b/apps/starry/wayland/build-x86_64-unknown-none.toml @@ -0,0 +1,13 @@ +target = "x86_64-unknown-none" +env = {} +log = "Warn" +features = [ + "ax-feat/rtc", + "ax-feat/display", + "ax-driver/serial", + "ax-driver/virtio-blk", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "starry-kernel/input", +] +plat_dyn = true diff --git a/apps/starry/wayland/prebuild.sh b/apps/starry/wayland/prebuild.sh new file mode 100755 index 0000000000..a5ff5f8d40 --- /dev/null +++ b/apps/starry/wayland/prebuild.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env bash +set -euo pipefail + +app_dir="${STARRY_APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +workspace="${STARRY_WORKSPACE:-$(cd "$app_dir/../../.." && pwd)}" +arch="${STARRY_ARCH:-}" +rootfs="${STARRY_ROOTFS:-}" +overlay_dir="${STARRY_OVERLAY_DIR:-}" + +require_env() { + local name="$1" + local value="$2" + if [[ -z "$value" ]]; then + echo "error: $name is required" >&2 + exit 1 + fi +} + +ensure_host_tools() { + local missing=() + command -v debugfs >/dev/null 2>&1 || missing+=(e2fsprogs) + if [[ ${#missing[@]} -gt 0 ]]; then + echo "error: missing required host packages: ${missing[*]}" >&2 + exit 1 + fi +} + +copy_base_text_file_to_overlay() { + local guest_path="$1" + local target="$overlay_dir$guest_path" + mkdir -p "$(dirname "$target")" + if ! debugfs -R "cat $guest_path" "$rootfs" >"$target" 2>/dev/null; then + rm -f "$target" + return + fi + chmod 0644 "$target" +} + +prefetch_wayland_apks() { + local apk_arch branch cache_dir guest_cache_dir + + case "$arch" in + x86_64 | riscv64 | aarch64 | loongarch64) + apk_arch="$arch" + ;; + *) + echo "warning: unsupported apk arch for Wayland APK prefetch: $arch" >&2 + return 0 + ;; + esac + + branch="$(sed -n 's#.*/\(v[0-9][0-9.]*\)/main#\1#p' "$overlay_dir/etc/apk/repositories" 2>/dev/null | head -1)" + if [[ -z "$branch" ]]; then + branch="v3.23" + fi + + cache_dir="$workspace/target/wayland-apks/$branch/$apk_arch" + guest_cache_dir="$overlay_dir/usr/local/wayland-apks" + mkdir -p "$cache_dir" "$guest_cache_dir" + + if ! command -v python3 >/dev/null 2>&1; then + echo "warning: python3 not found; skipping host APK prefetch" >&2 + return 0 + fi + + python3 - "$apk_arch" "$branch" "$cache_dir" "$guest_cache_dir" <<'PY' +import io +import os +import re +import shutil +import sys +import tarfile +import urllib.request + +apk_arch, branch, cache_dir, guest_cache_dir = sys.argv[1:] +mirrors = [ + "http://mirrors.huaweicloud.com/alpine", + "http://mirrors.aliyun.com/alpine", + "http://mirrors.tuna.tsinghua.edu.cn/alpine", + "http://mirrors.cernet.edu.cn/alpine", + "http://dl-cdn.alpinelinux.org/alpine", +] +repos = ["main", "community"] +extra_roots = os.environ.get("STARRY_WAYLAND_EXTRA_APKS", "").split() +roots = ["weston", "weston-backend-drm", "weston-shell-desktop", *extra_roots] +installed_names = set(os.environ.get("STARRY_WAYLAND_INSTALLED_PACKAGES", "").split()) +write_install_list = os.environ.get("STARRY_WAYLAND_WRITE_INSTALL_LIST") == "1" + + +def log(message, stream=sys.stdout): + print(message, file=stream, flush=True) + + +def dep_key(value): + value = value.strip() + if not value or value.startswith("!"): + return None + return re.split(r"[<>=]", value, maxsplit=1)[0] + + +def fetch_bytes(path): + last_error = None + for mirror in mirrors: + url = f"{mirror}/{branch}/{path}" + try: + with urllib.request.urlopen(url, timeout=120) as resp: + return resp.read(), mirror + except Exception as exc: + last_error = exc + log(f"warning: failed to fetch {url}: {exc}", sys.stderr) + raise RuntimeError(f"all mirrors failed for {path}: {last_error}") + + +def fetch_file(path, target_path): + last_error = None + filename = os.path.basename(target_path) + for mirror in mirrors: + url = f"{mirror}/{branch}/{path}" + tmp = target_path + ".tmp" + try: + with urllib.request.urlopen(url, timeout=120) as resp, open(tmp, "wb") as out: + total_header = resp.headers.get("Content-Length") + total = int(total_header) if total_header and total_header.isdigit() else 0 + downloaded = 0 + next_report = 2 * 1024 * 1024 + log(f"WAYLAND_PREFETCH downloading {filename} from {mirror}") + while True: + chunk = resp.read(1024 * 1024) + if not chunk: + break + out.write(chunk) + downloaded += len(chunk) + if downloaded >= next_report: + if total: + log( + f"WAYLAND_PREFETCH downloading {filename} " + f"{downloaded // (1024 * 1024)}MiB/{total // (1024 * 1024)}MiB" + ) + else: + log( + f"WAYLAND_PREFETCH downloading {filename} " + f"{downloaded // (1024 * 1024)}MiB" + ) + next_report += 2 * 1024 * 1024 + os.replace(tmp, target_path) + return mirror + except Exception as exc: + last_error = exc + try: + os.unlink(tmp) + except FileNotFoundError: + pass + log(f"warning: failed to fetch {url}: {exc}", sys.stderr) + raise RuntimeError(f"all mirrors failed for {path}: {last_error}") + + +packages = {} +providers = {} +for repo in repos: + log(f"WAYLAND_PREFETCH fetching index {repo}/{apk_arch}") + data, _ = fetch_bytes(f"{repo}/{apk_arch}/APKINDEX.tar.gz") + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: + index = archive.extractfile("APKINDEX").read().decode() + for block in index.strip().split("\n\n"): + fields = {} + for line in block.splitlines(): + if len(line) > 2 and line[1] == ":": + fields.setdefault(line[0], []).append(line[2:]) + name = fields.get("P", [None])[0] + version = fields.get("V", [None])[0] + if not name or not version: + continue + deps = [] + for dep_line in fields.get("D", []): + deps.extend(filter(None, (dep_key(dep) for dep in dep_line.split()))) + provides = [name] + for provide_line in fields.get("p", []): + provides.extend(filter(None, (dep_key(item) for item in provide_line.split()))) + packages[name] = { + "name": name, + "version": version, + "repo": repo, + "deps": deps, + } + for provide in provides: + providers.setdefault(provide, name) + +resolved = [] +seen = set() +queue = list(roots) +while queue: + request = queue.pop(0) + name = request if request in packages else providers.get(request) + if not name or name in seen: + continue + seen.add(name) + pkg = packages[name] + resolved.append(pkg) + for dep in pkg["deps"]: + dep_name = dep if dep in packages else providers.get(dep) + if dep_name and dep_name not in seen: + queue.append(dep_name) + +os.makedirs(cache_dir, exist_ok=True) +os.makedirs(guest_cache_dir, exist_ok=True) +log(f"WAYLAND_PREFETCH resolved {len(resolved)} apk(s) for {apk_arch}") +for pkg in resolved: + filename = f"{pkg['name']}-{pkg['version']}.apk" + rel = f"{pkg['repo']}/{apk_arch}/{filename}" + cached = os.path.join(cache_dir, filename) + if not os.path.exists(cached) or os.path.getsize(cached) == 0: + mirror = fetch_file(rel, cached) + log(f"WAYLAND_PREFETCH downloaded {filename} from {mirror}") + else: + log(f"WAYLAND_PREFETCH cached {filename}") + shutil.copy2(cached, os.path.join(guest_cache_dir, filename)) + +if write_install_list: + install_list = os.path.join(guest_cache_dir, "install.list") + with open(install_list, "w", encoding="utf-8") as out: + for pkg in resolved: + if pkg["name"] not in installed_names: + filename = f"{pkg['name']}-{pkg['version']}.apk" + out.write(f"/usr/local/wayland-apks/{filename}\n") + +log(f"WAYLAND_PREFETCH prepared {len(resolved)} apk(s) for {apk_arch}") +PY +} + +populate_overlay() { + mkdir -p "$overlay_dir/usr/bin" + cp "$app_dir/wayland-test.sh" "$overlay_dir/usr/bin/wayland-test.sh" + chmod 0755 "$overlay_dir/usr/bin/wayland-test.sh" + + copy_base_text_file_to_overlay /etc/apk/repositories + copy_base_text_file_to_overlay /etc/resolv.conf + prefetch_wayland_apks +} + +require_env STARRY_ARCH "$arch" +require_env STARRY_ROOTFS "$rootfs" +require_env STARRY_OVERLAY_DIR "$overlay_dir" + +ensure_host_tools +populate_overlay diff --git a/apps/starry/wayland/qemu-aarch64.toml b/apps/starry/wayland/qemu-aarch64.toml new file mode 100644 index 0000000000..d54d7faec5 --- /dev/null +++ b/apps/starry/wayland/qemu-aarch64.toml @@ -0,0 +1,27 @@ +args = [ + "-nographic", + "-m", + "1G", + "-cpu", + "cortex-a53", + "-device", + "virtio-gpu-pci", + "-device", + "virtio-keyboard-pci", + "-device", + "virtio-mouse-pci", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/wayland-test.sh" +success_regex = ["(?m)^WAYLAND_TEST_PASSED\\s*$"] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + "(?m)^WAYLAND_TEST_FAILED(?:[: ].*)?$", +] +timeout = 900 diff --git a/apps/starry/wayland/qemu-loongarch64.toml b/apps/starry/wayland/qemu-loongarch64.toml new file mode 100644 index 0000000000..4abb0cf6f7 --- /dev/null +++ b/apps/starry/wayland/qemu-loongarch64.toml @@ -0,0 +1,29 @@ +args = [ + "-machine", + "virt", + "-cpu", + "la464", + "-nographic", + "-m", + "1G", + "-device", + "virtio-gpu-pci", + "-device", + "virtio-keyboard-pci", + "-device", + "virtio-mouse-pci", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-loongarch64-alpine.img", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/wayland-test.sh" +success_regex = ["(?m)^WAYLAND_TEST_PASSED\\s*$"] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + "(?m)^WAYLAND_TEST_FAILED(?:[: ].*)?$", +] +timeout = 900 diff --git a/apps/starry/wayland/qemu-riscv64.toml b/apps/starry/wayland/qemu-riscv64.toml new file mode 100644 index 0000000000..a00e293707 --- /dev/null +++ b/apps/starry/wayland/qemu-riscv64.toml @@ -0,0 +1,27 @@ +args = [ + "-nographic", + "-m", + "1G", + "-cpu", + "rv64", + "-device", + "virtio-gpu-pci", + "-device", + "virtio-keyboard-pci", + "-device", + "virtio-mouse-pci", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/wayland-test.sh" +success_regex = ["(?m)^WAYLAND_TEST_PASSED\\s*$"] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + "(?m)^WAYLAND_TEST_FAILED(?:[: ].*)?$", +] +timeout = 600 diff --git a/apps/starry/wayland/qemu-x86_64.toml b/apps/starry/wayland/qemu-x86_64.toml new file mode 100644 index 0000000000..16a9b84770 --- /dev/null +++ b/apps/starry/wayland/qemu-x86_64.toml @@ -0,0 +1,25 @@ +args = [ + "-nographic", + "-m", + "1G", + "-device", + "virtio-gpu-pci", + "-device", + "virtio-keyboard-pci", + "-device", + "virtio-mouse-pci", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", +] +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/wayland-test.sh" +success_regex = ["(?m)^WAYLAND_TEST_PASSED\\s*$"] +fail_regex = [ + '(?i)\bpanic(?:ked)?\b', + "(?m)^WAYLAND_TEST_FAILED(?:[: ].*)?$", +] +timeout = 900 diff --git a/apps/starry/wayland/run-hvf.sh b/apps/starry/wayland/run-hvf.sh new file mode 100755 index 0000000000..f532b86301 --- /dev/null +++ b/apps/starry/wayland/run-hvf.sh @@ -0,0 +1,342 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE="$(cd "$SCRIPT_DIR/../../.." && pwd)" +ARCH="aarch64" +TARGET="aarch64-unknown-none-softfloat" + +ROOTFS_BASE="$WORKSPACE/tmp/axbuild/rootfs/rootfs-${ARCH}-alpine.img" +ROOTFS_APP="$WORKSPACE/tmp/axbuild/rootfs/rootfs-${ARCH}-wayland.img" +KERNEL="$WORKSPACE/target/${TARGET}/release/starryos.bin" +BUILD_CONFIG="$SCRIPT_DIR/build-${TARGET}.toml" +PROVISION_MARKER=".wayland-provisioned" +WAYLAND_ROOTFS_MB="${STARRY_WAYLAND_ROOTFS_MB:-4096}" + +NO_BUILD=false +REPROVISION=false +USE_HVF=false +PROVISION_ONLY=false +USE_COCOA="${STARRY_COCOA:-true}" + +for arg in "$@"; do + case "$arg" in + --no-build) NO_BUILD=true ;; + --reprovision) REPROVISION=true ;; + --hvf) USE_HVF=true ;; + --provision-only) PROVISION_ONLY=true ;; + --vnc-only) USE_COCOA=false ;; + *) echo "unknown: $arg" >&2; exit 1 ;; + esac +done + +DBSBIN="/opt/homebrew/opt/e2fsprogs/sbin" +export PATH="$DBSBIN:/opt/homebrew/bin:/usr/local/bin:$PATH" + +require_tool() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "error: missing required host tool: $1" >&2 + exit 1 + fi +} + +run_provision_qemu() { + local provision_log="$1" + local input_fifo + local pipeline_pid + local deadline + local saw_done=false + local qemu_status=0 + + input_fifo="$(mktemp -u "${TMPDIR:-/tmp}/wayland-qemu-input.XXXXXX")" + mkfifo "$input_fifo" + + ( + qemu-system-aarch64 \ + -machine virt \ + -cpu cortex-a53 \ + -smp 4 -m 2048M \ + -nographic \ + -device virtio-blk-pci,drive=disk0 \ + -drive "id=disk0,if=none,format=raw,file=$ROOTFS_APP" \ + -append "root=/dev/sda console=ttyS0" \ + -kernel "$KERNEL" \ + <"$input_fifo" 2>&1 | tee "$provision_log" + ) & + pipeline_pid=$! + + exec 3>"$input_fifo" + rm -f "$input_fifo" + + sleep 25 + printf 'sh /usr/bin/provision-wayland.sh\n' >&3 + + deadline=$((SECONDS + 900)) + while kill -0 "$pipeline_pid" >/dev/null 2>&1; do + if grep -q "PROVISION_DONE" "$provision_log" 2>/dev/null; then + saw_done=true + printf '\001x' >&3 || true + break + fi + if grep -q "PROVISION_NO_PREFETCHED_APKS\\|PROVISION_FAILED\\|panic" "$provision_log" 2>/dev/null; then + break + fi + if [ "$SECONDS" -ge "$deadline" ]; then + echo "error: provisioning timed out after 900s" >&2 + break + fi + sleep 2 + done + + exec 3>&- + + for _ in 1 2 3 4 5 6 7 8 9 10; do + if ! kill -0 "$pipeline_pid" >/dev/null 2>&1; then + break + fi + sleep 1 + done + if kill -0 "$pipeline_pid" >/dev/null 2>&1; then + kill "$pipeline_pid" >/dev/null 2>&1 || true + fi + wait "$pipeline_pid" >/dev/null 2>&1 || qemu_status=$? + + if [ "$saw_done" = true ]; then + return 0 + fi + return "${qemu_status:-1}" +} + +inject_overlay() { + local overlay_dir="$1" + local rootfs_img="$2" + local commands + local log + + commands="$(mktemp "${TMPDIR:-/tmp}/wayland-overlay.XXXXXX")" + log="${rootfs_img}.debugfs-overlay.log" + { + ( + cd "$overlay_dir" + find . -type d | LC_ALL=C sort + ) | while IFS= read -r rel; do + [ "$rel" = "." ] && continue + printf 'mkdir /%s\n' "${rel#./}" + done + + ( + cd "$overlay_dir" + find . -type f | LC_ALL=C sort + ) | while IFS= read -r rel; do + local host_path="$overlay_dir/${rel#./}" + local guest_path="/${rel#./}" + local mode + mode="$(stat -f '%Lp' "$host_path" 2>/dev/null || stat -c '%a' "$host_path")" + printf 'rm %s\n' "$guest_path" + printf 'write %s %s\n' "$host_path" "$guest_path" + printf 'sif %s mode 0100%s\n' "$guest_path" "$mode" + done + } >"$commands" + + if ! debugfs -w -f "$commands" "$rootfs_img" 2>&1 | tee "$log"; then + rm -f "$commands" + echo "error: debugfs overlay injection failed; see $log" >&2 + exit 1 + fi + if grep -E "Could not allocate block|No space left|write:|error:" "$log" >/dev/null 2>&1; then + rm -f "$commands" + echo "error: debugfs reported overlay injection errors; see $log" >&2 + exit 1 + fi + rm -f "$commands" +} + +resize_rootfs() { + local rootfs_img="$1" + local target_mb="$2" + local current_bytes + local fsck_status + local target_bytes + + current_bytes="$(stat -f '%z' "$rootfs_img" 2>/dev/null || stat -c '%s' "$rootfs_img")" + target_bytes=$((target_mb * 1024 * 1024)) + if [ "$current_bytes" -ge "$target_bytes" ]; then + return + fi + + echo "==> Expanding manual rootfs to ${target_mb} MiB..." + set +e + e2fsck -fy "$rootfs_img" >/dev/null + fsck_status=$? + set -e + if [ "$fsck_status" -gt 1 ]; then + echo "error: e2fsck failed for $rootfs_img with status $fsck_status" >&2 + exit "$fsck_status" + fi + if command -v truncate >/dev/null 2>&1; then + truncate -s "${target_mb}M" "$rootfs_img" + else + dd if=/dev/zero bs=1m count=0 seek="$target_mb" of="$rootfs_img" 2>/dev/null + fi + resize2fs "$rootfs_img" >/dev/null +} + +rootfs_path_exists() { + local guest_path="$1" + local stat_output + + stat_output="$(debugfs -R "stat $guest_path" "$ROOTFS_APP" 2>&1 || true)" + printf '%s\n' "$stat_output" | grep -q '^Inode:' \ + && ! printf '%s\n' "$stat_output" | grep -q 'File not found' +} + +marker_exists() { + rootfs_path_exists "/$PROVISION_MARKER" +} + +installed_packages() { + debugfs -R "cat /lib/apk/db/installed" "$ROOTFS_APP" 2>/dev/null \ + | sed -n 's/^P://p' \ + | tr '\n' ' ' +} + +require_tool debugfs +require_tool e2fsck +require_tool resize2fs +require_tool python3 +require_tool qemu-system-aarch64 + +# ---- Build ---- +if [ "$NO_BUILD" = false ]; then + echo "==> Building StarryOS for $ARCH..." + cd "$WORKSPACE" + cargo xtask starry build --arch "$ARCH" --config "$BUILD_CONFIG" +fi +[ -f "$KERNEL" ] || { echo "error: kernel not found at $KERNEL" >&2; exit 1; } + +# ---- Rootfs ---- +mkdir -p "$(dirname "$ROOTFS_APP")" + +if [ ! -f "$ROOTFS_BASE" ]; then + echo "==> Downloading rootfs..." + cd "$WORKSPACE" + cargo xtask starry rootfs --arch "$ARCH" +fi + +if [ ! -f "$ROOTFS_APP" ] || [ "$REPROVISION" = true ]; then + echo "==> Creating wayland rootfs from base..." + cp "$ROOTFS_BASE" "$ROOTFS_APP" + chmod 0644 "$ROOTFS_APP" + resize_rootfs "$ROOTFS_APP" "$WAYLAND_ROOTFS_MB" + echo "nameserver 10.0.2.3" | \ + debugfs -w "$ROOTFS_APP" -R "cd /etc; rm resolv.conf; write /dev/stdin resolv.conf" 2>/dev/null || true +fi + +if ! marker_exists || [ "$REPROVISION" = true ]; then + resize_rootfs "$ROOTFS_APP" "$WAYLAND_ROOTFS_MB" + + echo "==> Injecting offline Wayland/GTK APK overlay..." + OVERLAY_DIR="$WORKSPACE/tmp/axbuild/starry-app/wayland-manual-overlay" + rm -rf "$OVERLAY_DIR" + mkdir -p "$OVERLAY_DIR/usr/bin" + export STARRY_APP_DIR="$SCRIPT_DIR" + export STARRY_WORKSPACE="$WORKSPACE" + export STARRY_ARCH="$ARCH" + export STARRY_ROOTFS="$ROOTFS_APP" + export STARRY_OVERLAY_DIR="$OVERLAY_DIR" + export STARRY_WAYLAND_EXTRA_APKS="gtk4.0-demo font-dejavu" + export STARRY_WAYLAND_INSTALLED_PACKAGES="$(installed_packages)" + export STARRY_WAYLAND_WRITE_INSTALL_LIST=1 + bash "$SCRIPT_DIR/prebuild.sh" + + cat >"$OVERLAY_DIR/usr/bin/provision-wayland.sh" <<'SCRIPT' +#!/bin/sh +set -eu +echo "PROVISION_BEGIN" +apk_list=/usr/local/wayland-apks/install.list +if [ ! -s "$apk_list" ]; then + echo "PROVISION_NO_PREFETCHED_APKS" + exit 1 +fi +xargs apk add --allow-untrusted --no-network < "$apk_list" +echo "PROVISION_PACKAGES_DONE" +touch /.wayland-provisioned +echo "PROVISION_DONE" +SCRIPT + chmod 0755 "$OVERLAY_DIR/usr/bin/provision-wayland.sh" + + inject_overlay "$OVERLAY_DIR" "$ROOTFS_APP" +fi + +if ! marker_exists || [ "$REPROVISION" = true ]; then + echo "==> Provisioning: installing prefetched Weston + GTK4 demo APKs (~3-5 min)..." + + # Headless boot: send the provision command once the shell is ready. + # AArch64 TCG boot is slow — wait 25s before sending. + provision_log="$WORKSPACE/tmp/axbuild/rootfs/provision-${ARCH}-wayland.log" + echo " (booting headless, aarch64 TCG takes ~20s to reach shell...)" + set +e + run_provision_qemu "$provision_log" + qemu_status=$? + set -e + + if [ "$qemu_status" -ne 0 ]; then + echo "==> WARNING: provisioning QEMU exited with status $qemu_status." + echo " See $provision_log" + fi + + # Verify marker was written + if marker_exists; then + echo "==> Provision complete." + else + echo "error: provision failed; marker was not written." >&2 + echo " See $provision_log" >&2 + exit 1 + fi +fi + +if [ "$PROVISION_ONLY" = true ]; then + echo "==> Provision-only requested; not launching graphical QEMU." + exit 0 +fi + +# ---- Launch ---- +CPU="cortex-a53" +ACCEL="TCG" +DISPLAY_NAME="Cocoa + VNC" +DISPLAY_ARGS=(-display cocoa,show-cursor=on -vnc ":${STARRY_VNC:-0}") +if [ "$USE_HVF" = true ]; then + CPU="max" + ACCEL="HVF" +fi +if [ "$USE_COCOA" = false ]; then + DISPLAY_NAME="VNC" + DISPLAY_ARGS=(-nographic -vnc ":${STARRY_VNC:-0}") +fi + +echo "" +echo "==> Launching StarryOS ($ACCEL, $DISPLAY_NAME display)..." +echo " Login as root, then:" +echo " weston --backend=drm-backend.so --renderer=pixman &" +echo " gtk4-demo" +echo "" + +QEMU_ARGS=(-machine virt -cpu "$CPU") +if [ "$USE_HVF" = true ]; then + QEMU_ARGS+=(-accel hvf) +fi +QEMU_ARGS+=(-smp 4 -m 2048M) +QEMU_ARGS+=("${DISPLAY_ARGS[@]}") + QEMU_ARGS+=( + -device virtio-gpu-pci + -device virtio-keyboard-pci + -device virtio-mouse-pci + -device virtio-blk-pci,drive=disk0 + -drive "id=disk0,if=none,format=raw,file=$ROOTFS_APP" + -append "root=/dev/sda console=ttyS0" +) +if [ "$USE_COCOA" != false ]; then + QEMU_ARGS+=(-serial stdio) +fi +QEMU_ARGS+=(-kernel "$KERNEL") +exec qemu-system-aarch64 "${QEMU_ARGS[@]}" diff --git a/apps/starry/wayland/wayland-test.sh b/apps/starry/wayland/wayland-test.sh new file mode 100755 index 0000000000..e80574e2bc --- /dev/null +++ b/apps/starry/wayland/wayland-test.sh @@ -0,0 +1,247 @@ +#!/bin/sh +set -eu + +# Wayland/Weston smoke test for StarryOS. +# Installs weston via Alpine apk, starts the compositor with the DRM backend, +# and verifies the Wayland socket appears and a simple client connects. + +green="$(printf '\033[32m')" +red="$(printf '\033[31m')" +bold="$(printf '\033[1m')" +reset="$(printf '\033[0m')" + +weston_pid="" +test_done=0 +failed=0 + +fail() { + printf "%sWAYLAND_TEST_FAILED: %s%s\n" "$red" "$*" "$reset" + echo "WAYLAND_TEST_FAILED" + failed=1 + exit 1 +} + +run_with_timeout() { + timeout_secs="$1" + shift + + if command -v timeout >/dev/null 2>&1; then + timeout "$timeout_secs" "$@" + return $? + fi + + "$@" & + cmd_pid=$! + elapsed=0 + while kill -0 "$cmd_pid" >/dev/null 2>&1; do + if [ "$elapsed" -ge "$timeout_secs" ]; then + echo "WAYLAND_PREP command timed out after ${timeout_secs}s: $*" + kill "$cmd_pid" >/dev/null 2>&1 || true + sleep 1 + kill -9 "$cmd_pid" >/dev/null 2>&1 || true + wait "$cmd_pid" >/dev/null 2>&1 || true + return 124 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + + wait "$cmd_pid" +} + +write_apk_repositories() { + mirror="$1" + branch="$(sed -n 's#.*/\(v[0-9][0-9.]*\)/main#\1#p' /etc/apk/repositories 2>/dev/null | head -1)" + if [ -z "$branch" ]; then + branch="v3.22" + fi + + cat >/etc/apk/repositories </dev/null || true + sleep 2 + done + done + + return 1 +} + +cleanup() { + if [ -n "$weston_pid" ]; then + kill "$weston_pid" >/dev/null 2>&1 || true + wait "$weston_pid" >/dev/null 2>&1 || true + weston_pid="" + fi + rm -f /tmp/wayland-* 2>/dev/null || true +} + +on_exit() { + rc=$? + cleanup + if [ "$test_done" -ne 1 ] && [ "$failed" -ne 1 ]; then + printf "%sWAYLAND_TEST_RESULT FAILED%s\n" "$red" "$reset" + echo "WAYLAND_TEST_FAILED" + fi + exit "$rc" +} +trap on_exit EXIT + +# ---- Install weston ---- +echo "WAYLAND_PREP installing weston..." +install_weston_packages 2>&1 || fail "apk add weston packages failed after mirror retries" + +# Debug: list available backends and shells +echo "WAYLAND_PREP available backends:" +ls -la /usr/lib/libweston-14/ 2>&1 || true +echo "WAYLAND_PREP available shells (/usr/lib/weston/):" +ls -la /usr/lib/weston/ 2>&1 || true +echo "WAYLAND_PREP searching for desktop-shell.so:" +find /usr/lib -name "desktop-shell.so" 2>/dev/null || echo " not found via find" + +# Verify the DRM device exists +if [ ! -e /dev/dri/card0 ]; then + fail "/dev/dri/card0 not found — DRM kernel driver missing" +fi +echo "WAYLAND_PREP /dev/dri/card0 present" + +# Verify input devices exist +input_count=$(ls /dev/input/event* 2>/dev/null | wc -l) +if [ "$input_count" -lt 1 ]; then + echo "WAYLAND_PREP warning: no /dev/input/event* devices found" +else + echo "WAYLAND_PREP found $input_count input device(s)" +fi + +# ---- Start Weston ---- +export XDG_RUNTIME_DIR=/tmp +chmod 0700 /tmp +rm -f /tmp/wayland-* 2>/dev/null + +# Skip libseat — our kernel doesn't run seatd +export LIBSEAT_BACKEND=noop + +echo "WAYLAND_STAGE starting weston with DRM backend..." +/usr/bin/weston \ + --backend=drm-backend.so \ + --renderer=pixman \ + --no-config \ + --idle-time=0 \ + --log=/tmp/weston.log & +weston_pid=$! + +# ---- Wait for Wayland socket ---- +socket_ready=0 +for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do + sleep 1 + if ! kill -0 "$weston_pid" >/dev/null 2>&1; then + echo "WAYLAND_STAGE weston exited prematurely (pid=$weston_pid)" + tail -30 /tmp/weston.log + fail "weston exited before creating Wayland socket" + fi + disp=$(ls /tmp/ 2>/dev/null | grep '^wayland-[0-9]*$' | head -1) + if [ -n "$disp" ]; then + socket_ready=1 + echo "WAYLAND_STAGE Wayland socket ready: /tmp/$disp" + break + fi +done + +if [ "$socket_ready" -ne 1 ]; then + tail -30 /tmp/weston.log + fail "weston did not create a Wayland socket within 15s" +fi + +export WAYLAND_DISPLAY="$disp" + +# ---- Verify compositor is responsive ---- +# weston-info queries the compositor for global interfaces. +echo "WAYLAND_STAGE connecting client..." +if command -v weston-info >/dev/null 2>&1; then + if weston-info >/tmp/weston-info.out 2>&1; then + echo "WAYLAND_STAGE weston-info connected successfully" + grep -q 'wl_compositor' /tmp/weston-info.out && echo "WAYLAND_STAGE wl_compositor interface present" + else + echo "WAYLAND_STAGE weston-info exited non-zero (may be normal)" + fi +else + # If weston-info is not available, verify the socket is still there + # and weston is still alive as a minimal smoke check. + echo "WAYLAND_STAGE weston-info not available, checking socket liveness..." + if [ -S "/tmp/$WAYLAND_DISPLAY" ]; then + echo "WAYLAND_STAGE socket /tmp/$WAYLAND_DISPLAY is alive" + else + fail "Wayland socket disappeared" + fi +fi + +# ---- Check weston log for obvious errors ---- +if grep -iE "failed to open|no such file|permission denied" /tmp/weston.log >/tmp/weston-errors.out 2>&1; then + echo "WAYLAND_STAGE weston log contains errors:" + cat /tmp/weston-errors.out + # Don't fail — some "errors" are benign (e.g. missing optional backends) +else + echo "WAYLAND_STAGE no obvious errors in weston log" +fi + +# Let weston run briefly, then shut down +sleep 2 + +# ---- Shutdown ---- +echo "WAYLAND_STAGE shutting down weston..." +kill "$weston_pid" 2>/dev/null || true +# Wait up to 5s for graceful exit, then force-kill +for i in 1 2 3 4 5; do + if ! kill -0 "$weston_pid" 2>/dev/null; then break; fi + sleep 1 +done +kill -9 "$weston_pid" 2>/dev/null || true +wait "$weston_pid" 2>/dev/null || true +weston_pid="" + +test_done=1 +trap - EXIT +cleanup + +printf "%sWAYLAND_TEST_RESULT PASSED%s\n" "$green" "$reset" +echo "WAYLAND_TEST_PASSED" diff --git a/components/axpoll/src/lib.rs b/components/axpoll/src/lib.rs index 879eb74810..8c789be5ef 100644 --- a/components/axpoll/src/lib.rs +++ b/components/axpoll/src/lib.rs @@ -14,7 +14,7 @@ use core::{ use ax_kspin::SpinNoIrq; use bitflags::bitflags; use linux_raw_sys::general::*; -use spin::LazyLock; +use spin::Once; bitflags! { /// I/O events. @@ -152,7 +152,7 @@ impl Drop for Inner { } /// A data structure for waking up tasks that are waiting for I/O events. -pub struct PollSet(LazyLock>); +pub struct PollSet(Once>); impl Default for PollSet { fn default() -> Self { @@ -163,7 +163,7 @@ impl Default for PollSet { impl PollSet { /// Creates a new empty [`PollSet`]. pub const fn new() -> Self { - Self(LazyLock::new(|| SpinNoIrq::new(Inner::new()))) + Self(Once::new()) } /// Registers a waker for the requested I/O events. @@ -174,7 +174,12 @@ impl PollSet { /// from hard IRQ, NMI, or trap callbacks, and must not hold locks that may /// be re-entered by the registered waker or by poll wakeup paths. pub unsafe fn register(&self, waker: &Waker, interests: IoEvents) { - let replaced = { self.0.lock().register(waker, interests) }; + let replaced = { + self.0 + .call_once(|| SpinNoIrq::new(Inner::new())) + .lock() + .register(waker, interests) + }; if let Some(entry) = replaced { entry.wake(); } @@ -190,9 +195,12 @@ impl PollSet { /// must not hold locks that may be re-entered by waker execution or poll /// wakeup paths. pub unsafe fn wake(&self, ready: IoEvents) -> usize { + let Some(inner) = self.0.get() else { + return 0; + }; let mut ready_entries = Vec::with_capacity(POLL_SET_CAPACITY); { - self.0.lock().drain_ready(ready, &mut ready_entries); + inner.lock().drain_ready(ready, &mut ready_entries); } let woke = ready_entries.len(); for entry in ready_entries { @@ -200,6 +208,46 @@ impl PollSet { } woke } + + /// Wakes up registered wakers whose interests intersect `ready` from IRQ context. + /// + /// Unlike [`wake`](Self::wake), this does not allocate a replacement + /// waiter buffer. It drains the already-initialized entries in place, so + /// device IRQ handlers can acknowledge the device and then wake matching + /// poll waiters without allocating in hard IRQ context. + pub fn wake_from_irq(&self, ready: IoEvents) -> usize { + let Some(inner) = self.0.get() else { + return 0; + }; + let mut ready_entries = [const { MaybeUninit::::uninit() }; POLL_SET_CAPACITY]; + let ready_len = { + let mut inner = inner.lock(); + let len = inner.len(); + if len == 0 { + return 0; + } + + let mut ready_len = 0; + let mut keep_len = 0; + for i in 0..len { + let entry = unsafe { inner.entries[i].assume_init_read() }; + if entry.interests.intersects(ready) { + ready_entries[ready_len].write(entry); + ready_len += 1; + } else { + inner.entries[keep_len].write(entry); + keep_len += 1; + } + } + inner.cursor = keep_len; + ready_len + }; + + for entry in ready_entries.iter_mut().take(ready_len) { + unsafe { entry.assume_init_read() }.wake(); + } + ready_len + } } impl Drop for PollSet { diff --git a/components/axpoll/tests/tests.rs b/components/axpoll/tests/tests.rs index fda9781b93..4ec16e3b14 100644 --- a/components/axpoll/tests/tests.rs +++ b/components/axpoll/tests/tests.rs @@ -259,6 +259,21 @@ fn concurrent_deferred_wakes_partition_by_mask() { assert_eq!(unsafe { ps.wake(IoEvents::IN | IoEvents::OUT) }, 0); } +#[test] +fn irq_wake_drains_without_double_wake() { + let ps = PollSet::new(); + assert_eq!(ps.wake_from_irq(IoEvents::IN), 0); + + let counter = Counter::new(); + let w = Waker::from(counter.clone()); + unsafe { ps.register(&w, IoEvents::IN) }; + + assert_eq!(ps.wake_from_irq(IoEvents::IN), 1); + assert_eq!(counter.count(), 1); + assert_eq!(ps.wake_from_irq(IoEvents::IN), 0); + assert_eq!(counter.count(), 1); +} + #[test] fn full_capacity() { let ps = PollSet::new(); diff --git a/docs/docs/components/crates/axpoll.md b/docs/docs/components/crates/axpoll.md index a7a85ea5c0..ea5def2028 100644 --- a/docs/docs/components/crates/axpoll.md +++ b/docs/docs/components/crates/axpoll.md @@ -33,7 +33,7 @@ | --- | --- | | `IoEvents` | 基于 `bitflags` 封装 Linux `POLL*` 事件位 | | `Pollable` | 约定对象如何查询当前事件,以及如何注册等待者 | -| `Inner` | `PollSet` 的内部 ring buffer,保存 `Waker` | +| `Inner` | `PollSet` 的内部 ring buffer,保存 `Waker` 与订阅的事件位 | | `PollSet` | 对外暴露的等待者集合,可注册与批量唤醒 | ### 1.3 `IoEvents`:readiness 位图协议 @@ -52,11 +52,12 @@ `PollSet` 看起来像一个等待者集合,但它不是无界队列,而是一个固定容量为 64 的 ring buffer。当前实现有几条必须写进文档的行为约束: -- `register()` 会把 waker 写入循环缓冲区 +- `register()` 会把 waker 和它订阅的 `IoEvents` 写入循环缓冲区 - 超过 64 个等待者后,新注册会覆盖最旧槽位 - 被覆盖掉的旧 waker 若与新 waker 不是同一个,会被立即唤醒 -- `wake()` 会把旧 `Inner` 整体换出,然后依靠旧 `Inner` 的 `Drop` 逐个唤醒 -- `PollSet` 自身 `Drop` 时会再触发一次 `wake()`,避免等待者永远悬挂 +- `wake(ready)` 只唤醒订阅事件与 `ready` 相交的等待者,未就绪的等待者会留在集合中 +- `wake_from_irq(ready)` 面向设备中断路径,不分配新的等待队列存储,也同样按事件位过滤 +- `PollSet` 自身 `Drop` 时会再触发一次 `wake(IoEvents::all())`,避免等待者永远悬挂 因此,`PollSet` 的真实语义更接近“有限容量的唤醒集合”,而不是严格意义上的公平等待队列。 @@ -69,7 +70,7 @@ 3. 若返回 `WouldBlock` 且非 nonblocking 模式,则调用 `pollable.register(cx, events)` 4. 事件成立后,由对象自身通过 `PollSet::wake()` 或自定义注册逻辑唤醒等待任务 -同一文件里还有 `register_irq_waker()`,说明 `axpoll` 不只服务文件/网络对象,也被用来桥接 IRQ 事件与任务等待。 +IRQ 驱动对象也按同一模型工作:中断处理路径确认设备状态后调用对象自己的 `PollSet::wake_from_irq(IoEvents::...)`,把硬件事件转成等待任务可见的 I/O 就绪状态,同时避免在硬中断上下文分配新的等待队列存储。 ## 核心功能 @@ -78,7 +79,7 @@ - 用统一位图表达可读、可写、挂断、错误等事件 - 为任意内核对象定义 `poll()` / `register()` 契约 - 提供可复用的 `PollSet`,让对象能够保存等待者并在状态变化时批量唤醒 -- 通过实现 `Wake`,让 `PollSet` 能直接接入 Rust waker 生态 +- 保存 Rust `Waker`,让内核对象能在状态变化时接入任务唤醒链路 ### 2.2 仓库里的真实使用者 @@ -146,7 +147,7 @@ axpoll = { workspace = true } - `PollSet` 的 64 项容量是实现边界,不可误当成无限等待列表 - 覆盖旧 waker 时会主动唤醒旧者,这会影响高并发下的重试频率和公平性 -- `wake()` 依赖旧 `Inner` 的 `Drop` 完成真正逐个唤醒,改这条路径极易产生丢唤醒 +- `wake(ready)` / `wake_from_irq(ready)` 必须先从集合里移除已就绪等待者,再在释放锁后执行 waker,改这条路径极易产生丢唤醒或重入死锁 - `IoEvents` 与 Linux 常量必须保持稳定对应关系,否则上层兼容性会直接出问题 ## 测试 diff --git a/drivers/ax-driver/src/virtio/display.rs b/drivers/ax-driver/src/virtio/display.rs index 682af33a14..50175e1a7c 100644 --- a/drivers/ax-driver/src/virtio/display.rs +++ b/drivers/ax-driver/src/virtio/display.rs @@ -2,11 +2,15 @@ extern crate alloc; use alloc::format; -use rdif_display::{DisplayError, DisplayInfo, FrameBuffer, PixelFormat}; +use rdif_display::{DisplayError, DisplayInfo, Event, FrameBuffer, PixelFormat}; use rdrive::{DriverGeneric, PlatformDevice, probe::OnProbeError}; #[cfg(all(feature = "pci", any(plat_static, plat_dyn)))] use virtio_drivers::transport::DeviceType; -use virtio_drivers::{Error as VirtIoError, device::gpu::VirtIOGpu, transport::Transport}; +use virtio_drivers::{ + Error as VirtIoError, + device::gpu::VirtIOGpu, + transport::{InterruptStatus, Transport}, +}; use crate::{BindingInfo, display::PlatformDeviceDisplay, virtio::VirtIoHalImpl}; #[cfg(all(feature = "pci", any(plat_static, plat_dyn)))] @@ -42,7 +46,8 @@ pub fn register_transport_with_info( transport: T, info: BindingInfo, ) -> Result<(), OnProbeError> { - let dev = VirtIoDisplay::new(transport) + let irq_num = info.irq_num(); + let dev = VirtIoDisplay::new(transport, irq_num) .map_err(|err| OnProbeError::other(format!("failed to initialize virtio-gpu: {err:?}")))?; let irq = plat_dev.register_display_with_info(dev, info); log::info!("registered virtio GPU device irq={irq:?}"); @@ -53,12 +58,14 @@ struct VirtIoDisplay { raw: VirtIOGpu, info: DisplayInfo, fb_base: *mut u8, + irq_num: Option, + irq_enabled: bool, } unsafe impl Send for VirtIoDisplay {} impl VirtIoDisplay { - fn new(transport: T) -> Result { + fn new(transport: T, irq_num: Option) -> Result { let mut raw = VirtIOGpu::new(transport)?; let framebuffer = raw.setup_framebuffer()?; let fb_base = framebuffer.as_mut_ptr(); @@ -71,7 +78,14 @@ impl VirtIoDisplay { format: PixelFormat::Xrgb8888, fb_size, }; - Ok(Self { raw, info, fb_base }) + let _ = raw.ack_interrupt(); + Ok(Self { + raw, + info, + fb_base, + irq_num, + irq_enabled: false, + }) } } @@ -90,6 +104,10 @@ impl rdif_display::Interface for VirtIoDisplay { Ok(unsafe { FrameBuffer::from_raw_parts_mut(self.fb_base, self.info.fb_size) }) } + fn irq_num(&self) -> Option { + self.irq_num + } + fn need_flush(&self) -> bool { true } @@ -97,6 +115,33 @@ impl rdif_display::Interface for VirtIoDisplay { fn flush(&mut self) -> Result<(), DisplayError> { self.raw.flush().map_err(map_display_err) } + + fn enable_irq(&mut self) { + self.irq_enabled = true; + } + + fn disable_irq(&mut self) { + self.irq_enabled = false; + } + + fn is_irq_enabled(&self) -> bool { + self.irq_enabled + } + + fn handle_irq(&mut self) -> Event { + let status = self.raw.ack_interrupt(); + display_irq_event(self.irq_enabled, status) + } +} + +fn display_irq_event(irq_enabled: bool, status: InterruptStatus) -> Event { + if !irq_enabled { + return Event::none(); + } + Event { + handled: !status.is_empty(), + changed: status.contains(InterruptStatus::DEVICE_CONFIGURATION_INTERRUPT), + } } fn map_display_err(err: VirtIoError) -> DisplayError { @@ -106,3 +151,46 @@ fn map_display_err(err: VirtIoError) -> DisplayError { _ => DisplayError::Other(alloc::boxed::Box::new(err)), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_irq_is_ignored_until_driver_enables_it() { + let status = + InterruptStatus::QUEUE_INTERRUPT | InterruptStatus::DEVICE_CONFIGURATION_INTERRUPT; + + assert_eq!(display_irq_event(false, status), Event::none()); + } + + #[test] + fn display_irq_reports_configuration_changes() { + assert_eq!( + display_irq_event(true, InterruptStatus::DEVICE_CONFIGURATION_INTERRUPT), + Event { + handled: true, + changed: true, + } + ); + } + + #[test] + fn display_irq_reports_non_configuration_interrupt_as_handled_only() { + assert_eq!( + display_irq_event(true, InterruptStatus::QUEUE_INTERRUPT), + Event { + handled: true, + changed: false, + } + ); + } + + #[test] + fn display_irq_empty_status_is_not_claimed() { + assert_eq!( + display_irq_event(true, InterruptStatus::empty()), + Event::none() + ); + } +} diff --git a/drivers/ax-driver/src/virtio/input.rs b/drivers/ax-driver/src/virtio/input.rs index 28b892575b..d7e5266666 100644 --- a/drivers/ax-driver/src/virtio/input.rs +++ b/drivers/ax-driver/src/virtio/input.rs @@ -2,14 +2,14 @@ extern crate alloc; use alloc::{borrow::ToOwned, format, string::String}; -use rdif_input::{AbsInfo, EventType, InputDeviceId, InputError, InputEvent}; +use rdif_input::{AbsInfo, Event, EventType, InputDeviceId, InputError, InputEvent}; use rdrive::{DriverGeneric, PlatformDevice, probe::OnProbeError}; #[cfg(all(feature = "pci", any(plat_static, plat_dyn)))] use virtio_drivers::transport::DeviceType; use virtio_drivers::{ Error as VirtIoError, device::input::{InputConfigSelect, VirtIOInput}, - transport::Transport, + transport::{InterruptStatus, Transport}, }; use crate::{BindingInfo, input::PlatformDeviceInput, virtio::VirtIoHalImpl}; @@ -46,7 +46,8 @@ pub fn register_transport_with_info( transport: T, info: BindingInfo, ) -> Result<(), OnProbeError> { - let dev = VirtIoInputDevice::new(transport).map_err(|err| { + let irq_num = info.irq_num(); + let dev = VirtIoInputDevice::new(transport, irq_num).map_err(|err| { OnProbeError::other(format!("failed to initialize virtio-input: {err:?}")) })?; let irq = plat_dev.register_input_with_info(dev, info); @@ -60,12 +61,14 @@ struct VirtIoInputDevice { name: String, physical_location: String, unique_id: String, + irq_num: Option, + irq_enabled: bool, } unsafe impl Send for VirtIoInputDevice {} impl VirtIoInputDevice { - fn new(transport: T) -> Result { + fn new(transport: T, irq_num: Option) -> Result { let mut raw = VirtIOInput::new(transport)?; let name = raw.name().unwrap_or_else(|_| "".to_owned()); let id = raw.ids()?; @@ -83,12 +86,20 @@ impl VirtIoInputDevice { "virtio-{:04x}-{:04x}-{:04x}-{:04x}-{}", device_id.bus_type, device_id.vendor, device_id.product, device_id.version, name ); + // Creating the event queue can raise an interrupt before the + // OS-specific evdev layer has installed its shared IRQ action. + // Acknowledge only the transport ISR here; queued input events remain + // in the virtqueue for the first reader to drain. + let _ = raw.ack_interrupt(); + Ok(Self { raw, device_id, name, physical_location, unique_id, + irq_num, + irq_enabled: false, }) } } @@ -112,6 +123,10 @@ impl rdif_input::Interface for VirtIoInputDevice { &self.unique_id } + fn irq_num(&self) -> Option { + self.irq_num + } + fn get_event_bits(&mut self, ty: EventType, out: &mut [u8]) -> Result { self.raw .query_config_select(InputConfigSelect::EvBits, ty as u8, out) @@ -146,6 +161,33 @@ impl rdif_input::Interface for VirtIoInputDevice { res: info.res as i32, }) } + + fn enable_irq(&mut self) { + self.irq_enabled = true; + } + + fn disable_irq(&mut self) { + self.irq_enabled = false; + } + + fn is_irq_enabled(&self) -> bool { + self.irq_enabled + } + + fn handle_irq(&mut self) -> Event { + let status = self.raw.ack_interrupt(); + input_irq_event(self.irq_enabled, status) + } +} + +fn input_irq_event(irq_enabled: bool, status: InterruptStatus) -> Event { + if !irq_enabled { + return Event::none(); + } + Event { + handled: !status.is_empty(), + input_ready: status.contains(InterruptStatus::QUEUE_INTERRUPT), + } } fn map_input_err(err: VirtIoError) -> InputError { @@ -155,3 +197,46 @@ fn map_input_err(err: VirtIoError) -> InputError { _ => InputError::Other(alloc::boxed::Box::new(err)), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn input_irq_is_ignored_until_driver_enables_it() { + let status = + InterruptStatus::QUEUE_INTERRUPT | InterruptStatus::DEVICE_CONFIGURATION_INTERRUPT; + + assert_eq!(input_irq_event(false, status), Event::none()); + } + + #[test] + fn input_irq_queue_interrupt_makes_input_ready() { + assert_eq!( + input_irq_event(true, InterruptStatus::QUEUE_INTERRUPT), + Event { + handled: true, + input_ready: true, + } + ); + } + + #[test] + fn input_irq_configuration_interrupt_is_claimed_without_input_ready() { + assert_eq!( + input_irq_event(true, InterruptStatus::DEVICE_CONFIGURATION_INTERRUPT), + Event { + handled: true, + input_ready: false, + } + ); + } + + #[test] + fn input_irq_empty_status_is_not_claimed() { + assert_eq!( + input_irq_event(true, InterruptStatus::empty()), + Event::none() + ); + } +} diff --git a/drivers/interface/rdif-display/src/interface.rs b/drivers/interface/rdif-display/src/interface.rs index 494fb06e8f..b39f72a6d9 100644 --- a/drivers/interface/rdif-display/src/interface.rs +++ b/drivers/interface/rdif-display/src/interface.rs @@ -2,12 +2,16 @@ use crate::{DisplayError, DisplayInfo, DriverGeneric, FrameBuffer}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Event { + pub handled: bool, pub changed: bool, } impl Event { pub const fn none() -> Self { - Self { changed: false } + Self { + handled: false, + changed: false, + } } } @@ -16,6 +20,10 @@ pub trait Interface: DriverGeneric { fn framebuffer(&mut self) -> Result, DisplayError>; + fn irq_num(&self) -> Option { + None + } + fn need_flush(&self) -> bool { false } diff --git a/drivers/interface/rdif-input/src/interface.rs b/drivers/interface/rdif-input/src/interface.rs index 2da4d20976..9add0b9e67 100644 --- a/drivers/interface/rdif-input/src/interface.rs +++ b/drivers/interface/rdif-input/src/interface.rs @@ -2,12 +2,16 @@ use crate::{AbsInfo, DriverGeneric, EventType, InputDeviceId, InputError, InputE #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct Event { + pub handled: bool, pub input_ready: bool, } impl Event { pub const fn none() -> Self { - Self { input_ready: false } + Self { + handled: false, + input_ready: false, + } } } @@ -18,6 +22,10 @@ pub trait Interface: DriverGeneric { fn unique_id(&self) -> &str; + fn irq_num(&self) -> Option { + None + } + fn get_event_bits(&mut self, ty: EventType, out: &mut [u8]) -> Result; fn read_event(&mut self) -> Result; diff --git a/drivers/interface/rdif-input/src/lib.rs b/drivers/interface/rdif-input/src/lib.rs index b09ae9ade7..622affc002 100644 --- a/drivers/interface/rdif-input/src/lib.rs +++ b/drivers/interface/rdif-input/src/lib.rs @@ -84,6 +84,12 @@ mod tests { assert_eq!(event.code, 30); assert_eq!(event.value, -1); assert_eq!(input.get_abs_info(0).unwrap().min, -100); - assert_eq!(input.handle_irq(), Event { input_ready: false }); + assert_eq!( + input.handle_irq(), + Event { + handled: false, + input_ready: false + } + ); } } diff --git a/drivers/net/rd-net/src/lib.rs b/drivers/net/rd-net/src/lib.rs index e6008db616..740b460bc6 100644 --- a/drivers/net/rd-net/src/lib.rs +++ b/drivers/net/rd-net/src/lib.rs @@ -280,6 +280,21 @@ impl IrqHandler { self.inner.rx_wakers.wake(id); } } + + pub fn enable_irq(&self) { + let iface = unsafe { &mut **self.inner.interface.get() }; + iface.enable_irq(); + } + + pub fn disable_irq(&self) { + let iface = unsafe { &mut **self.inner.interface.get() }; + iface.disable_irq(); + } + + pub fn is_irq_enabled(&self) -> bool { + let iface = unsafe { &mut **self.inner.interface.get() }; + iface.is_irq_enabled() + } } pub struct TxQueue { diff --git a/drivers/rdrive/src/probe/acpi.rs b/drivers/rdrive/src/probe/acpi.rs index c011227837..af5cbff6d3 100644 --- a/drivers/rdrive/src/probe/acpi.rs +++ b/drivers/rdrive/src/probe/acpi.rs @@ -287,8 +287,8 @@ mod tests { AcpiGsiController, AcpiHandler, AcpiId, AcpiIoApic, AcpiIrqPolarity, AcpiIrqTrigger, AcpiIsaIrqOverride, AcpiPchPic, AcpiResourceRange, AcpiRoot, AcpiRouting, LinkIrqResource, LinkIrqResourceKind, PciLinkAllocator, System, irq_descriptor_gsi, - is_buffer_field_to_field_unit_store_gap, pci_link_irq_field_candidates, - route_with_irq_descriptor_flags, select_pci_link_irq, + is_buffer_field_to_field_unit_store_gap, pci_irq_descriptor_gsi, + pci_link_irq_field_candidates, route_with_irq_descriptor_flags, select_pci_link_irq, }; use crate::register::{DriverRegister, ProbeKind, ProbeLevel, ProbePriority}; @@ -535,6 +535,26 @@ mod tests { assert_eq!(route.controller_input, 10); } + #[test] + fn pci_link_descriptor_reports_selected_power_of_two_gsi_directly() { + let resource = LinkIrqResource { + kind: LinkIrqResourceKind::SmallIrq, + descriptor: IrqDescriptor { + is_consumer: false, + trigger: InterruptTrigger::Level, + polarity: InterruptPolarity::ActiveLow, + is_shared: true, + is_wake_capable: false, + irq: 1 << 4, + }, + irqs: alloc::vec![4], + }; + + let descriptor = resource.descriptor_for_irq(4); + assert_eq!(descriptor.irq, 4); + assert_eq!(pci_irq_descriptor_gsi(&descriptor), Some(4)); + } + #[test] fn pch_pic_routes_map_acpi_gsi_to_controller_input() { let mut routing = AcpiRouting::new(); @@ -1115,7 +1135,7 @@ impl System { let Some(irq) = self.resolve_endpoint_gsi(info.address, intx_route)? else { return Ok(None); }; - let Some(gsi) = irq_descriptor_gsi(&irq) else { + let Some(gsi) = pci_irq_descriptor_gsi(&irq) else { return Err(OnProbeError::other(format!( "ACPI PCI endpoint {} pin {} returned an invalid IRQ descriptor: {:?}", info.address, intx_route.root_pin, irq @@ -2372,10 +2392,7 @@ fn build_link_srs_buffer(resource: &LinkIrqResource, irq: u32) -> Result impl LinkIrqResource { fn descriptor_for_irq(&self, irq: u32) -> IrqDescriptor { let mut descriptor = self.descriptor.clone(); - descriptor.irq = match self.kind { - LinkIrqResourceKind::SmallIrq if irq < 16 => 1u32 << irq, - _ => irq, - }; + descriptor.irq = irq; descriptor } } @@ -2438,6 +2455,10 @@ fn irq_descriptor_gsi(descriptor: &IrqDescriptor) -> Option { } } +fn pci_irq_descriptor_gsi(descriptor: &IrqDescriptor) -> Option { + Some(descriptor.irq) +} + fn route_with_irq_descriptor_flags( route: AcpiGsiRoute, descriptor: &IrqDescriptor, diff --git a/os/StarryOS/kernel/src/mm/loader.rs b/os/StarryOS/kernel/src/mm/loader.rs index e93c317e2e..81bc5161e0 100644 --- a/os/StarryOS/kernel/src/mm/loader.rs +++ b/os/StarryOS/kernel/src/mm/loader.rs @@ -1,7 +1,7 @@ //! User address space management. -use alloc::{borrow::ToOwned, string::String, vec, vec::Vec}; -use core::{ffi::CStr, iter}; +use alloc::{borrow::ToOwned, collections::VecDeque, string::String, vec, vec::Vec}; +use core::{ffi::CStr, iter, mem::size_of}; use ax_errno::{AxError, AxResult}; use ax_fs_ng::vfs::{CachedFile, FS_CONTEXT, FileBackend}; @@ -12,11 +12,10 @@ use ax_runtime::hal::{ }; use ax_sync::Mutex; use axfs_ng_vfs::Location; -use kernel_elf_parser::{ - AuxEntry, AuxType, ELFHeaders, ELFHeadersBuilder, ELFParser, app_stack_region, -}; +use kernel_elf_parser::{AuxEntry, AuxType, ELFHeaders, ELFHeadersBuilder, ELFParser}; use ouroboros::self_referencing; use uluru::LRUCache; +use zerocopy::IntoBytes; use crate::{ config::{USER_SPACE_BASE, USER_SPACE_SIZE}, @@ -26,7 +25,7 @@ use crate::{ #[cfg(target_arch = "riscv64")] const RISCV_COMPAT_HWCAP_IMAFDC: usize = (1 << (b'I' - b'A')) | (1 << (b'M' - b'A')) - | (1 << (b'A' - b'A')) + | (1 << 0) | (1 << (b'F' - b'A')) | (1 << (b'D' - b'A')) | (1 << (b'C' - b'A')); @@ -91,6 +90,69 @@ fn mapping_flags(flags: xmas_elf::program::Flags) -> MappingFlags { mapping_flags } +fn app_stack_region(args: &[String], envs: &[String], auxv: &[AuxEntry], sp: usize) -> Vec { + let mut data = VecDeque::new(); + let mut push = |src: &[u8]| -> usize { + data.extend(src.iter().copied()); + data.rotate_right(src.len()); + sp - data.len() + }; + + let random_str_pos = push(b"0123456789abcdef"); + let envs_slice: Vec<_> = envs + .iter() + .map(|env| { + push(b"\0"); + push(env.as_bytes()) + }) + .collect(); + let argv_slice: Vec<_> = args + .iter() + .map(|arg| { + push(b"\0"); + push(arg.as_bytes()) + }) + .collect(); + let padding_null = "\0".repeat(size_of::()); + let sp = push(padding_null.as_bytes()); + + push(&b"\0".repeat(sp % 16)); + + if (envs.len() + args.len() + 3) & 1 != 0 { + push(padding_null.as_bytes()); + } + + let has_random = auxv.iter().any(|entry| entry.get_type() == AuxType::RANDOM); + let has_execfn = auxv.iter().any(|entry| entry.get_type() == AuxType::EXECFN); + + // `push` prepends bytes to the stack image. Push the terminator first so + // user memory presents auxv as: supplied entries, AT_RANDOM, AT_EXECFN, + // AT_NULL. Without AT_NULL, musl keeps parsing argv/env padding as auxv + // and can falsely enable AT_SECURE. + push(AuxEntry::new(AuxType::NULL, 0).as_bytes()); + if !has_execfn { + push(AuxEntry::new(AuxType::EXECFN, argv_slice[0]).as_bytes()); + } + if !has_random { + push(AuxEntry::new(AuxType::RANDOM, random_str_pos).as_bytes()); + } + push(auxv.as_bytes()); + + push(padding_null.as_bytes()); + push(envs_slice.as_bytes()); + push(padding_null.as_bytes()); + push(argv_slice.as_bytes()); + let sp = push(args.len().as_bytes()); + + assert!(sp % 16 == 0); + + let mut result = Vec::with_capacity(data.len()); + let (first, second) = data.as_slices(); + result.extend_from_slice(first); + result.extend_from_slice(second); + result +} + /// Map the elf file to the user address space. /// /// # Arguments @@ -605,7 +667,11 @@ impl ElfLoader { .aux_vector(PAGE_SIZE_4K, ldso.map(|elf| elf.base())) .collect::>(); auxv.push(AuxEntry::new(AuxType::HWCAP, hwcap_value())); - auxv.push(AuxEntry::new(AuxType::NULL, 0)); + auxv.push(AuxEntry::new(AuxType::UID, 0)); + auxv.push(AuxEntry::new(AuxType::EUID, 0)); + auxv.push(AuxEntry::new(AuxType::GID, 0)); + auxv.push(AuxEntry::new(AuxType::EGID, 0)); + auxv.push(AuxEntry::new(AuxType::SECURE, 0)); debug!( "loader: entry={:#x} auxv_len={} has_ldso={} auxv_last_type={}", diff --git a/os/StarryOS/kernel/src/pseudofs/dev/card0.rs b/os/StarryOS/kernel/src/pseudofs/dev/card0.rs index bebf45ba27..31c2c68de8 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/card0.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/card0.rs @@ -36,6 +36,7 @@ use alloc::{ }; use core::{ any::Any, + ptr::NonNull, sync::atomic::{AtomicU32, AtomicU64, Ordering}, task::Context, }; @@ -70,10 +71,10 @@ use super::drm::{ DRM_MODE_PROP_IMMUTABLE, DRM_MODE_PROP_OBJECT, DRM_MODE_PROP_RANGE, DRM_PLANE_TYPE_PRIMARY, DRM_PROP_NAME_LEN, DrmAuth, DrmEvent, DrmEventVblank, DrmGetCap, DrmModeAtomic, DrmModeCardRes, DrmModeCreateBlob, DrmModeCreateDumb, DrmModeCrtc, DrmModeCrtcPageFlip, DrmModeDestroyBlob, - DrmModeDestroyDumb, DrmModeFbCmd2, DrmModeGetBlob, DrmModeGetConnector, DrmModeGetEncoder, - DrmModeGetPlane, DrmModeGetPlaneRes, DrmModeGetProperty, DrmModeMapDumb, DrmModeModeInfo, - DrmModeObjGetProperties, DrmModePropertyEnum, DrmPrimeHandle, DrmSetClientCap, DrmSetVersion, - DrmUnique, DrmVersion, DrmWaitVblank, + DrmModeDestroyDumb, DrmModeDirtyFB, DrmModeFbCmd2, DrmModeGetBlob, DrmModeGetConnector, + DrmModeGetEncoder, DrmModeGetPlane, DrmModeGetPlaneRes, DrmModeGetProperty, DrmModeMapDumb, + DrmModeModeInfo, DrmModeObjGetProperties, DrmModePropertyEnum, DrmPrimeHandle, DrmSetClientCap, + DrmSetVersion, DrmUnique, DrmVersion, DrmWaitVblank, }; use crate::pseudofs::{DeviceMmap, DeviceOps}; @@ -290,11 +291,13 @@ pub struct Card0 { /// Serializes the lazy initialization of `in_formats_blob` so /// only one allocation lands in `system_blobs`. system_blobs_init: Mutex<()>, + /// Registered virtio-gpu IRQ action, when the display backend advertises one. + irq_handle: spin::Once, } impl Card0 { pub fn new() -> Arc { - Arc::new(Self { + let card = Arc::new(Self { events: Mutex::new(VecDeque::with_capacity(MAX_EVENTS)), poll_rx: PollSet::new(), sequence: AtomicU32::new(0), @@ -314,7 +317,40 @@ impl Card0 { system_blobs: Mutex::new(BTreeMap::new()), in_formats_blob: AtomicU32::new(0), system_blobs_init: Mutex::new(()), - }) + irq_handle: spin::Once::new(), + }); + card.register_irq(); + card + } + + fn register_irq(self: &Arc) { + if !ax_display::has_display() { + return; + } + let Some(irq) = ax_display::framebuffer_irq_num() else { + return; + }; + + let data = NonNull::from(self.as_ref()).cast(); + let request = ax_runtime::hal::irq::IrqRequest::new(card0_irq_handler, data) + .share_mode(ax_runtime::hal::irq::ShareMode::Shared) + .auto_enable(ax_runtime::hal::irq::AutoEnable::No); + match ax_runtime::hal::irq::request_irq(irq, request) { + Ok(handle) => { + self.irq_handle.call_once(|| handle); + ax_display::framebuffer_enable_irq(); + if let Some(handle) = self.irq_handle.get().copied() + && let Err(err) = ax_runtime::hal::irq::enable_irq(handle) + { + warn!("failed to enable display irq handler for irq {irq}: {err:?}"); + ax_display::framebuffer_disable_irq(); + } + } + Err(err) => { + warn!("failed to register display irq handler for irq {irq}: {err:?}"); + ax_display::framebuffer_disable_irq(); + } + } } /// Lazily construct the `IN_FORMATS` blob the first time a caller @@ -490,7 +526,7 @@ impl DeviceOps for Card0 { DRM_IOCTL_GET_MAGIC => handle_get_magic(arg), DRM_IOCTL_AUTH_MAGIC => handle_auth_magic(arg), - DRM_IOCTL_MODE_DIRTYFB => handle_dirty_fb(arg), + DRM_IOCTL_MODE_DIRTYFB => self.handle_dirty_fb(arg), DRM_IOCTL_PRIME_HANDLE_TO_FD => handle_prime_handle_to_fd(arg), DRM_IOCTL_PRIME_FD_TO_HANDLE => handle_prime_fd_to_handle(arg), @@ -530,6 +566,17 @@ impl DeviceOps for Card0 { } } +unsafe fn card0_irq_handler( + _ctx: ax_runtime::hal::irq::IrqContext, + _data: NonNull<()>, +) -> ax_runtime::hal::irq::IrqReturn { + if ax_display::framebuffer_handle_irq() { + ax_runtime::hal::irq::IrqReturn::Handled + } else { + ax_runtime::hal::irq::IrqReturn::Unhandled + } +} + impl Pollable for Card0 { fn poll(&self) -> IoEvents { let mut events = IoEvents::empty(); @@ -739,10 +786,6 @@ fn handle_auth_magic(_arg: usize) -> VfsResult { Ok(0) } -fn handle_dirty_fb(_arg: usize) -> VfsResult { - Ok(0) -} - fn handle_prime_handle_to_fd(arg: usize) -> VfsResult { let ptr = arg as *mut DrmPrimeHandle; let mut req: DrmPrimeHandle = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; @@ -1221,6 +1264,16 @@ fn range_u32(name: &'static str, atomic: u32) -> PropMeta { } impl Card0 { + fn handle_dirty_fb(&self, arg: usize) -> VfsResult { + let ptr = arg as *const DrmModeDirtyFB; + let dirty: DrmModeDirtyFB = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; + if !self.fbs.lock().contains_key(&dirty.fb_id) { + return Err(VfsError::InvalidInput); + } + self.present_fb(dirty.fb_id); + Ok(0) + } + fn handle_page_flip(&self, arg: usize) -> VfsResult { let ptr = arg as *const DrmModeCrtcPageFlip; let f: DrmModeCrtcPageFlip = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; diff --git a/os/StarryOS/kernel/src/pseudofs/dev/event.rs b/os/StarryOS/kernel/src/pseudofs/dev/event.rs index 853fb8dffb..569dee2ae8 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/event.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/event.rs @@ -1,6 +1,7 @@ -use alloc::{collections::VecDeque, format, sync::Arc}; +use alloc::{collections::VecDeque, format, string::ToString, sync::Arc, vec, vec::Vec}; use core::{ any::Any, + ptr::NonNull, sync::atomic::{AtomicU32, Ordering}, task::Context, time::Duration, @@ -19,9 +20,9 @@ pub fn input_device_count() -> u32 { use ax_errno::{AxError, AxResult}; use ax_input::{ErasedInputDevice, Event, EventType, InputDevice, InputDeviceId, InputError}; use ax_runtime::hal::time::wall_time; -use ax_sync::Mutex; +use ax_sync::spin::SpinNoIrq as Mutex; use axfs_ng_vfs::{DeviceId, NodeFlags, NodeType, VfsResult}; -use axpoll::{IoEvents, Pollable}; +use axpoll::{IoEvents, PollSet, Pollable}; use bitmaps::Bitmap; use linux_raw_sys::{ general::{__kernel_old_time_t, __kernel_suseconds_t}, @@ -116,11 +117,10 @@ const ABS_MAX: usize = 0x40; pub struct EventDev { inner: Mutex, - /// IRQ line the underlying driver delivers buffered events on, when - /// the driver advertises one. `Pollable::register` wires the caller's - /// waker to this IRQ so virtio-input wakes its consumer in - /// microseconds rather than waiting for the next safety-net tick. + waiters: PollSet, + /// IRQ line the underlying driver advertises, when available. irq: Option, + irq_handle: spin::Once, ev_bits: Bitmap<{ EventType::COUNT as usize }>, /// Cached `EVIOCGPROP` bitmap. Computed once at probe from the driver's /// raw bits with a synthesized `INPUT_PROP_POINTER` for absolute or @@ -179,7 +179,9 @@ impl EventDev { read_ahead: VecDeque::with_capacity(READ_AHEAD_CAP), key_state: Bitmap::new(), }), + waiters: PollSet::new(), irq, + irq_handle: spin::Once::new(), ev_bits, prop_bits, abs_bits, @@ -195,25 +197,83 @@ impl EventDev { } fn get_event_bits(&self, arg: usize, size: usize, ty: u8) -> AxResult { - let bits = UserPtr::::from(arg).get_as_mut_slice(size)?; if ty == 0 { + let bits = UserPtr::::from(arg).get_as_mut_slice(size)?; Ok(copy_bytes(self.ev_bits.as_bytes(), bits)) } else { let ty = EventType::from_repr(ty).ok_or(AxError::InvalidInput)?; - match self.inner.lock().device.get_event_bits(ty, bits) { - Ok(true) => {} - Ok(false) => { - debug!("No events for {ty:?}"); + let mut kernel_bits = vec![0; size]; + { + let mut inner = self.inner.lock(); + match inner.device.get_event_bits(ty, &mut kernel_bits) { + Ok(true) => {} + Ok(false) => { + debug!("No events for {ty:?}"); + } + Err(err) => { + warn!("Failed to get event bits: {err:?}"); + } } - Err(err) => { - warn!("Failed to get event bits: {err:?}"); + } + let bytes = size.min(ty.bits_count().div_ceil(8)); + let bits = UserPtr::::from(arg).get_as_mut_slice(size)?; + bits[..bytes].copy_from_slice(&kernel_bits[..bytes]); + Ok(bytes) + } + } + + fn register_irq(self: &Arc) { + let Some(irq) = self.irq else { + return; + }; + + let data = NonNull::from(self.as_ref()).cast(); + let request = ax_runtime::hal::irq::IrqRequest::new(event_dev_irq_handler, data) + .share_mode(ax_runtime::hal::irq::ShareMode::Shared) + .auto_enable(ax_runtime::hal::irq::AutoEnable::No); + match ax_runtime::hal::irq::request_irq(irq, request) { + Ok(handle) => { + self.irq_handle.call_once(|| handle); + self.inner.lock().device.enable_irq(); + if let Some(handle) = self.irq_handle.get().copied() + && let Err(err) = ax_runtime::hal::irq::enable_irq(handle) + { + warn!("failed to enable evdev irq handler for irq {irq}: {err:?}"); + self.inner.lock().device.disable_irq(); } } - Ok(bits.len().min(ty.bits_count().div_ceil(8))) + Err(err) => { + warn!("failed to register evdev irq handler for irq {irq}: {err:?}"); + self.inner.lock().device.disable_irq(); + } } } } +unsafe fn event_dev_irq_handler( + _ctx: ax_runtime::hal::irq::IrqContext, + data: NonNull<()>, +) -> ax_runtime::hal::irq::IrqReturn { + let event_dev = unsafe { data.cast::().as_ref() }; + // Use `lock()` rather than `try_lock()` so the virtio ISR is always + // acknowledged. `SpinNoIrq` guarantees the holder has local IRQs + // disabled, so this IRQ can only fire on a different CPU. Without the + // ack, a level-triggered shared IRQ line stays asserted and can starve + // other devices on the same line. + let mut inner = event_dev.inner.lock(); + let event = inner.device.handle_irq(); + if event.input_ready && inner.drain_into_queue() { + drop(inner); + event_dev.waiters.wake_from_irq(IoEvents::IN); + return ax_runtime::hal::irq::IrqReturn::Wake; + } + if event.handled { + ax_runtime::hal::irq::IrqReturn::Handled + } else { + ax_runtime::hal::irq::IrqReturn::Unhandled + } +} + fn copy_bytes(src: &[u8], dst: &mut [u8]) -> usize { let len = src.len().min(dst.len()); dst[..len].copy_from_slice(&src[..len]); @@ -325,8 +385,8 @@ impl DeviceOps for EventDev { Ok(0) } EVIOCGID => { - *UserPtr::::from(arg).get_as_mut()? = - self.inner.lock().device.device_id(); + let device_id = self.inner.lock().device.device_id(); + *UserPtr::::from(arg).get_as_mut()? = device_id; Ok(0) } EVIOCGRAB => Ok(0), @@ -355,19 +415,19 @@ impl DeviceOps for EventDev { match nr { // EVIOCGNAME 0x06 => { - return return_str(arg, size, self.inner.lock().device.name()); + let name = self.inner.lock().device.name().to_string(); + return return_str(arg, size, &name); } // EVIOCGPHYS 0x07 => { - return return_str( - arg, - size, - self.inner.lock().device.physical_location(), - ); + let location = + self.inner.lock().device.physical_location().to_string(); + return return_str(arg, size, &location); } // EVIOCGUNIQ 0x08 => { - return return_str(arg, size, self.inner.lock().device.unique_id()); + let unique_id = self.inner.lock().device.unique_id().to_string(); + return return_str(arg, size, &unique_id); } // EVIOCGPROP — device property bitmap. libinput // uses INPUT_PROP_POINTER to keep the cursor @@ -380,11 +440,15 @@ impl DeviceOps for EventDev { } // EVIOCGKEY 0x18 => { + let key_state = { + let inner = self.inner.lock(); + let bytes = inner.key_state.as_bytes(); + let mut key_state = Vec::with_capacity(bytes.len()); + key_state.extend_from_slice(bytes); + key_state + }; let bits = UserPtr::::from(arg).get_as_mut_slice(size)?; - return Ok(copy_bytes( - self.inner.lock().key_state.as_bytes(), - bits, - )); + return Ok(copy_bytes(&key_state, bits)); } // EVIOCGLED 0x19 => { @@ -461,21 +525,8 @@ impl Pollable for EventDev { if !events.contains(IoEvents::IN) { return; } - // If the driver advertises an IRQ, route the caller's waker - // through the per-IRQ waker list so the next virtio-input - // notification wakes the consumer directly. The unconditional - // wake the previous implementation issued here turned epoll - // (level-triggered) into a register → wake → consume-empty → - // re-register loop spinning at ~500 Hz; that hot loop is what - // libinput saw as continuous activity. - if let Some(irq) = self.irq { - ax_task::future::register_irq_waker(irq, context.waker()); - } - // No IRQ advertised: fall back to an immediate wake so the - // caller doesn't sleep forever on devices that never deliver - // an IRQ at all (observed for QEMU virtio-keyboard-pci on - // aarch64 HVF). For these the consumer effectively polls. - else if self.inner.lock().has_event() { + unsafe { self.waiters.register(context.waker(), IoEvents::IN) }; + if self.inner.lock().has_event() { context.waker().wake_by_ref(); } } @@ -494,6 +545,7 @@ pub fn input_devices(fs: Arc) -> DirMapping { let is_mouse = keys[BTN_MOUSE / 8] & (1 << (BTN_MOUSE % 8)) != 0; let event_dev = Arc::new(EventDev::new(device)); + event_dev.register_irq(); let dev = Device::new( fs.clone(), NodeType::CharacterDevice, diff --git a/os/StarryOS/kernel/src/syscall/net/cmsg.rs b/os/StarryOS/kernel/src/syscall/net/cmsg.rs index 4c4f37fcb0..29420965be 100644 --- a/os/StarryOS/kernel/src/syscall/net/cmsg.rs +++ b/os/StarryOS/kernel/src/syscall/net/cmsg.rs @@ -8,6 +8,20 @@ use crate::{ mm::{UserConstPtr, UserPtr}, }; +fn cmsg_align(len: usize) -> usize { + let align = size_of::(); + (len + align - 1) & !(align - 1) +} + +fn cmsg_align_down(len: usize) -> usize { + let align = size_of::(); + len & !(align - 1) +} + +pub fn cmsg_space(len: usize) -> Option { + size_of::().checked_add(len).map(cmsg_align) +} + pub enum CMsg { Rights { fds: Vec> }, } @@ -65,7 +79,10 @@ impl<'a> CMsgBuilder<'a> { ty: u32, body: impl FnOnce(&mut [u8]) -> AxResult, ) -> AxResult { - let Some(body_capacity) = (self.capacity - *self.len).checked_sub(size_of::()) + let Some(body_capacity) = self + .capacity + .checked_sub(*self.len) + .and_then(|remaining| cmsg_align_down(remaining).checked_sub(size_of::())) else { return Ok(false); }; @@ -78,10 +95,13 @@ impl<'a> CMsgBuilder<'a> { .get_as_mut_slice(body_capacity)?; let body_len = body(data)?; - let cmsg_len = size_of::() + body_len; + let Some(cmsg_len) = size_of::().checked_add(body_len) else { + return Err(AxError::InvalidInput); + }; hdr.cmsg_len = cmsg_len; - self.hdr = UserPtr::from(hdr as *const _ as usize + cmsg_len); - *self.len += cmsg_len; + let cmsg_space = cmsg_align(cmsg_len); + self.hdr = UserPtr::from(hdr as *const _ as usize + cmsg_space); + *self.len += cmsg_space; Ok(true) } } diff --git a/os/StarryOS/kernel/src/syscall/net/io.rs b/os/StarryOS/kernel/src/syscall/net/io.rs index 1f875e63f3..b9bc5a0104 100644 --- a/os/StarryOS/kernel/src/syscall/net/io.rs +++ b/os/StarryOS/kernel/src/syscall/net/io.rs @@ -19,7 +19,7 @@ use super::addr::{ use crate::{ file::{FileLike, PacketSocket, Socket, add_file_like, get_file_like, netlink::NetlinkSocket}, mm::{IoVec, IoVectorBuf, UserConstPtr, UserPtr, VmBytes, VmBytesMut}, - syscall::net::{CMsg, CMsgBuilder}, + syscall::net::{CMsg, CMsgBuilder, cmsg_space}, time::TimeValueLike, }; @@ -54,8 +54,14 @@ fn parse_send_cmsgs(control_ptr: usize, control_len: usize) -> AxResult()) + .and_then(|space| ptr.checked_add(space)) + else { + return Err(AxError::InvalidInput); + }; + cmsg.push(Box::new(CMsg::parse(hdr)?) as CMsgData); - ptr += hdr.cmsg_len; + ptr = next_ptr; } Ok(cmsg) diff --git a/os/StarryOS/kernel/src/syscall/task/clone.rs b/os/StarryOS/kernel/src/syscall/task/clone.rs index 217f77fc73..d478dca498 100644 --- a/os/StarryOS/kernel/src/syscall/task/clone.rs +++ b/os/StarryOS/kernel/src/syscall/task/clone.rs @@ -192,6 +192,13 @@ impl CloneArgs { new_uctx.set_tls(tls); } new_uctx.set_retval(0); + #[cfg(target_arch = "riscv64")] + let child_fp_fs = match uctx.sstatus.fs() { + riscv::register::sstatus::FS::Dirty => riscv::register::sstatus::FS::Clean, + fs => fs, + }; + #[cfg(target_arch = "riscv64")] + new_uctx.sstatus.set_fs(child_fp_fs); let set_child_tid = if flags.contains(CloneFlags::CHILD_SETTID) { child_tid @@ -204,6 +211,13 @@ impl CloneArgs { let old_proc_data = &curr_thread.proc_data; let mut new_task = new_user_task(&curr.name(), new_uctx, set_child_tid); + #[cfg(target_arch = "riscv64")] + { + let mut fp_state = ax_cpu::FpState::default(); + fp_state.save(); + fp_state.fs = child_fp_fs; + new_task.ctx_mut().fp_state = fp_state; + } let tid = new_task.id().as_u64() as Pid; if flags.contains(CloneFlags::PARENT_SETTID) && parent_tid != 0 { diff --git a/os/arceos/modules/axdisplay/src/device.rs b/os/arceos/modules/axdisplay/src/device.rs index 15261818a5..0b22632ae2 100644 --- a/os/arceos/modules/axdisplay/src/device.rs +++ b/os/arceos/modules/axdisplay/src/device.rs @@ -19,6 +19,22 @@ pub trait DisplayDevice: Send { fn info(&self) -> DisplayInfo; fn flush(&mut self) -> DisplayResult; + + fn irq_num(&self) -> Option { + None + } + + fn enable_irq(&mut self) {} + + fn disable_irq(&mut self) {} + + fn is_irq_enabled(&self) -> bool { + false + } + + fn handle_irq(&mut self) -> bool { + false + } } pub struct ErasedDisplayDevice { @@ -52,4 +68,24 @@ impl DisplayDevice for ErasedDisplayDevice { fn flush(&mut self) -> DisplayResult { self.inner.flush() } + + fn irq_num(&self) -> Option { + self.inner.irq_num() + } + + fn enable_irq(&mut self) { + self.inner.enable_irq(); + } + + fn disable_irq(&mut self) { + self.inner.disable_irq(); + } + + fn is_irq_enabled(&self) -> bool { + self.inner.is_irq_enabled() + } + + fn handle_irq(&mut self) -> bool { + self.inner.handle_irq() + } } diff --git a/os/arceos/modules/axdisplay/src/lib.rs b/os/arceos/modules/axdisplay/src/lib.rs index e01ff83d6d..a9e9f428bc 100644 --- a/os/arceos/modules/axdisplay/src/lib.rs +++ b/os/arceos/modules/axdisplay/src/lib.rs @@ -11,7 +11,7 @@ pub mod rdif; mod types; use ax_lazyinit::LazyInit; -use ax_sync::Mutex; +use ax_sync::spin::SpinNoIrq as Mutex; pub use device::{DisplayDevice, DisplayError, DisplayResult, ErasedDisplayDevice}; pub use types::{DisplayInfo, PixelFormat}; @@ -43,3 +43,24 @@ pub fn framebuffer_info() -> DisplayInfo { pub fn framebuffer_flush() -> bool { MAIN_DISPLAY.lock().flush().is_ok() } + +/// Returns the main display IRQ line, if the driver advertises one. +pub fn framebuffer_irq_num() -> Option { + MAIN_DISPLAY.lock().irq_num() +} + +/// Enables IRQ handling in the main display driver. +pub fn framebuffer_enable_irq() { + MAIN_DISPLAY.lock().enable_irq(); +} + +/// Disables IRQ handling in the main display driver. +pub fn framebuffer_disable_irq() { + MAIN_DISPLAY.lock().disable_irq(); +} + +/// Acknowledges the main display IRQ source. +pub fn framebuffer_handle_irq() -> bool { + let mut display = MAIN_DISPLAY.lock(); + display.is_irq_enabled() && display.handle_irq() +} diff --git a/os/arceos/modules/axdisplay/src/rdif.rs b/os/arceos/modules/axdisplay/src/rdif.rs index c3950ae783..43baefee48 100644 --- a/os/arceos/modules/axdisplay/src/rdif.rs +++ b/os/arceos/modules/axdisplay/src/rdif.rs @@ -56,6 +56,26 @@ impl DisplayDevice for RdifDisplayDevice { } Ok(()) } + + fn irq_num(&self) -> Option { + self.device.irq_num() + } + + fn enable_irq(&mut self) { + self.device.enable_irq(); + } + + fn disable_irq(&mut self) { + self.device.disable_irq(); + } + + fn is_irq_enabled(&self) -> bool { + self.device.is_irq_enabled() + } + + fn handle_irq(&mut self) -> bool { + self.device.handle_irq().handled + } } impl From for PixelFormat { diff --git a/os/arceos/modules/axinput/src/device.rs b/os/arceos/modules/axinput/src/device.rs index 672a9d26ac..d493bfb445 100644 --- a/os/arceos/modules/axinput/src/device.rs +++ b/os/arceos/modules/axinput/src/device.rs @@ -16,6 +16,21 @@ pub enum InputError { Unsupported, } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct InputIrqEvent { + pub handled: bool, + pub input_ready: bool, +} + +impl InputIrqEvent { + pub const fn none() -> Self { + Self { + handled: false, + input_ready: false, + } + } +} + /// Domain boundary consumed by evdev and upper input services. pub trait InputDevice: Send { fn name(&self) -> &str; @@ -41,6 +56,18 @@ pub trait InputDevice: Send { fn get_abs_info(&mut self, _axis: u8) -> InputResult { Err(InputError::Unsupported) } + + fn enable_irq(&mut self) {} + + fn disable_irq(&mut self) {} + + fn is_irq_enabled(&self) -> bool { + false + } + + fn handle_irq(&mut self) -> InputIrqEvent { + InputIrqEvent::none() + } } pub struct ErasedInputDevice { @@ -98,4 +125,20 @@ impl InputDevice for ErasedInputDevice { fn get_abs_info(&mut self, axis: u8) -> InputResult { self.inner.get_abs_info(axis) } + + fn enable_irq(&mut self) { + self.inner.enable_irq(); + } + + fn disable_irq(&mut self) { + self.inner.disable_irq(); + } + + fn is_irq_enabled(&self) -> bool { + self.inner.is_irq_enabled() + } + + fn handle_irq(&mut self) -> InputIrqEvent { + self.inner.handle_irq() + } } diff --git a/os/arceos/modules/axinput/src/lib.rs b/os/arceos/modules/axinput/src/lib.rs index ee5893db7d..3a4c12a960 100644 --- a/os/arceos/modules/axinput/src/lib.rs +++ b/os/arceos/modules/axinput/src/lib.rs @@ -14,7 +14,7 @@ use core::mem; use ax_lazyinit::LazyInit; use ax_sync::Mutex; -pub use device::{ErasedInputDevice, InputDevice, InputError, InputResult}; +pub use device::{ErasedInputDevice, InputDevice, InputError, InputIrqEvent, InputResult}; pub use event::{AbsInfo, Event, EventType}; pub use id::InputDeviceId; diff --git a/os/arceos/modules/axinput/src/rdif.rs b/os/arceos/modules/axinput/src/rdif.rs index 7dbd8414f8..b396671972 100644 --- a/os/arceos/modules/axinput/src/rdif.rs +++ b/os/arceos/modules/axinput/src/rdif.rs @@ -2,7 +2,9 @@ use alloc::{boxed::Box, string::String}; use rdif_input::{InputError as RdifInputError, Interface}; -use crate::{AbsInfo, Event, EventType, InputDevice, InputDeviceId, InputError, InputResult}; +use crate::{ + AbsInfo, Event, EventType, InputDevice, InputDeviceId, InputError, InputIrqEvent, InputResult, +}; pub struct RdifInputDevice { name: String, @@ -37,6 +39,10 @@ impl InputDevice for RdifInputDevice { self.device.unique_id() } + fn irq_num(&self) -> Option { + self.device.irq_num() + } + fn get_event_bits(&mut self, ty: EventType, out: &mut [u8]) -> InputResult { self.device .get_event_bits(ty.into(), out) @@ -60,6 +66,26 @@ impl InputDevice for RdifInputDevice { .map(Into::into) .map_err(map_input_error) } + + fn enable_irq(&mut self) { + self.device.enable_irq(); + } + + fn disable_irq(&mut self) { + self.device.disable_irq(); + } + + fn is_irq_enabled(&self) -> bool { + self.device.is_irq_enabled() + } + + fn handle_irq(&mut self) -> InputIrqEvent { + let event = self.device.handle_irq(); + InputIrqEvent { + handled: event.handled, + input_ready: event.input_ready, + } + } } impl From for InputDeviceId { diff --git a/platforms/ax-plat-loongarch64-qemu-virt/src/irq/eiointc.rs b/platforms/ax-plat-loongarch64-qemu-virt/src/irq/eiointc.rs index 3290f874d4..26d2c9a11f 100644 --- a/platforms/ax-plat-loongarch64-qemu-virt/src/irq/eiointc.rs +++ b/platforms/ax-plat-loongarch64-qemu-virt/src/irq/eiointc.rs @@ -16,6 +16,14 @@ const VEC_REG_COUNT: usize = 4; const VEC_COUNT_PER_REG: usize = 64; const VEC_COUNT: usize = VEC_REG_COUNT * VEC_COUNT_PER_REG; +const fn init_word_count() -> usize { + VEC_COUNT / 32 +} + +const fn init_word_offset(index: usize) -> usize { + index * 4 +} + pub fn init() { // TODO: support smp let misc = iocsr_read_d(LOONGARCH_IOCSR_MISC_FUNC); @@ -23,9 +31,9 @@ pub fn init() { let index = 0; - for i in 0..(VEC_COUNT / 32) { + for i in 0..init_word_count() { let data = ((1 << (i * 2 + 1)) << 16) | (1 << (i * 2)); - iocsr_write_w(EIOINTC_REG_NODEMAP + i * 4, data); + iocsr_write_w(EIOINTC_REG_NODEMAP + init_word_offset(i), data); } for i in 0..(VEC_COUNT / 32 / 4) { let bit = 1 << (1 + index); @@ -37,8 +45,10 @@ pub fn init() { let data = bit | (bit << 8) | (bit << 16) | (bit << 24); iocsr_write_w(EIOINTC_REG_ROUTE + i * 4, data); } - for i in 0..(VEC_COUNT / 32) { - iocsr_write_w(EIOINTC_REG_BOUNCE + i * 4, u32::MAX); + for i in 0..init_word_count() { + let offset = init_word_offset(i); + iocsr_write_w(EIOINTC_REG_BOUNCE + offset, u32::MAX); + iocsr_write_w(EIOINTC_REG_ENABLE + offset, 0); } } @@ -72,3 +82,23 @@ pub fn complete_irq(irq: usize) { let (offset, bit) = split_bit(irq); iocsr_write_d(EIOINTC_REG_ISR + offset, bit); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn eiointc_init_covers_all_32_bit_enable_words() { + assert_eq!(init_word_count(), 8); + assert_eq!(init_word_offset(0), 0); + assert_eq!(init_word_offset(init_word_count() - 1), 28); + } + + #[test] + fn eiointc_irq_bits_split_on_64_bit_registers() { + assert_eq!(split_bit(0), (0, 1)); + assert_eq!(split_bit(63), (0, 1u64 << 63)); + assert_eq!(split_bit(64), (8, 1)); + assert_eq!(split_bit(255), (24, 1u64 << 63)); + } +} diff --git a/platforms/ax-plat-loongarch64-qemu-virt/src/irq/pch_pic.rs b/platforms/ax-plat-loongarch64-qemu-virt/src/irq/pch_pic.rs index ba4124a784..5925730bc0 100644 --- a/platforms/ax-plat-loongarch64-qemu-virt/src/irq/pch_pic.rs +++ b/platforms/ax-plat-loongarch64-qemu-virt/src/irq/pch_pic.rs @@ -4,6 +4,7 @@ use crate::config::{devices::PCH_PIC_PADDR, plat::PHYS_VIRT_OFFSET}; const PIC_COUNT_PER_REG: usize = 32; const PIC_REG_COUNT: usize = 2; +const PIC_IRQ_COUNT: usize = PIC_COUNT_PER_REG * PIC_REG_COUNT; const PCH_PIC_MASK: usize = 0x20; const PCH_PIC_EDGE: usize = 0x60; @@ -24,9 +25,11 @@ fn write_w(addr: usize, val: u32) { pub fn init() { // High level triggered - for _ in 0..PIC_REG_COUNT { - write_w(PCH_PIC_EDGE, 0); - write_w(PCH_PIC_POL, 0); + for i in 0..PIC_REG_COUNT { + let offset = i * 4; + write_w(PCH_PIC_MASK + offset, u32::MAX); + write_w(PCH_PIC_EDGE + offset, 0); + write_w(PCH_PIC_POL + offset, 0); } } @@ -34,7 +37,14 @@ fn split_bit(irq: usize) -> (usize, u32) { (irq / PIC_COUNT_PER_REG * 4, 1 << (irq % PIC_COUNT_PER_REG)) } +const fn valid_irq(irq: usize) -> bool { + irq < PIC_IRQ_COUNT +} + pub fn enable_irq(irq: usize) { + if !valid_irq(irq) { + return; + } let (offset, bit) = split_bit(irq); let addr = PCH_PIC_MASK + offset; @@ -47,7 +57,30 @@ pub fn enable_irq(irq: usize) { } pub fn disable_irq(irq: usize) { + if !valid_irq(irq) { + return; + } let (offset, bit) = split_bit(irq); let addr = PCH_PIC_MASK + offset; write_w(addr, read_w(addr) | bit); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pch_pic_accepts_only_two_mask_registers_worth_of_irqs() { + assert!(valid_irq(0)); + assert!(valid_irq(63)); + assert!(!valid_irq(64)); + } + + #[test] + fn pch_pic_irq_bits_split_on_32_bit_mask_registers() { + assert_eq!(split_bit(0), (0, 1)); + assert_eq!(split_bit(31), (0, 1u32 << 31)); + assert_eq!(split_bit(32), (4, 1)); + assert_eq!(split_bit(63), (4, 1u32 << 31)); + } +} diff --git a/scripts/axbuild/src/starry/mod.rs b/scripts/axbuild/src/starry/mod.rs index 7b21faf7e2..02fdfed16d 100644 --- a/scripts/axbuild/src/starry/mod.rs +++ b/scripts/axbuild/src/starry/mod.rs @@ -562,6 +562,9 @@ impl Starry { qemu.args.push("-snapshot".to_string()); } qemu::apply_dynamic_platform_qemu_boot(&mut qemu, &cargo); + if qemu.uefi { + qemu::apply_drive_snapshot_without_global_snapshot(&mut qemu); + } println!(" prepare assets: 0ns (pipeline=plain, cache=miss)"); println!( " qemu config: {} (timeout={})", diff --git a/scripts/axbuild/src/test/qemu.rs b/scripts/axbuild/src/test/qemu.rs index c1a9252e3a..bf8a3b95fb 100644 --- a/scripts/axbuild/src/test/qemu.rs +++ b/scripts/axbuild/src/test/qemu.rs @@ -1195,7 +1195,7 @@ fn disable_qemu_cpu_feature(cpu: &mut String, feature: &str) { cpu.push_str(&disabled_feature); } -fn apply_drive_snapshot_without_global_snapshot(qemu: &mut QemuConfig) { +pub(crate) fn apply_drive_snapshot_without_global_snapshot(qemu: &mut QemuConfig) { let mut global_snapshot = false; qemu.args.retain(|arg| { let keep = arg != "-snapshot"; diff --git a/test-suit/starryos/qemu-smp1/system/test-auxv/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/test-auxv/CMakeLists.txt new file mode 100644 index 0000000000..1540da720c --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-auxv/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.20) +project(test-auxv C) +set(APP_NAME test-auxv) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(${APP_NAME} src/main.c) +target_include_directories(${APP_NAME} PRIVATE src ../common) +target_compile_options(${APP_NAME} PRIVATE -Wall -Wextra -Werror) +install(TARGETS ${APP_NAME} RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/test-auxv/src/main.c b/test-suit/starryos/qemu-smp1/system/test-auxv/src/main.c new file mode 100644 index 0000000000..1c1b08c154 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-auxv/src/main.c @@ -0,0 +1,116 @@ +#include "test_framework.h" + +#include +#include +#include +#include +#include +#include + +#ifndef AT_SECURE +#define AT_SECURE 23 +#endif + +#ifndef AT_UID +#define AT_UID 11 +#endif + +#ifndef AT_EUID +#define AT_EUID 12 +#endif + +#ifndef AT_GID +#define AT_GID 13 +#endif + +#ifndef AT_EGID +#define AT_EGID 14 +#endif + +extern char **environ; + +static const Elf64_auxv_t *initial_auxv(void) +{ + char **envp = environ; + while (*envp != NULL) { + envp++; + } + return (const Elf64_auxv_t *)(envp + 1); +} + +static int find_auxv_value(unsigned long key, unsigned long *value) +{ + const Elf64_auxv_t *auxv = initial_auxv(); + + for (size_t i = 0; i < 128; i++) { + if (auxv[i].a_type == AT_NULL) { + return 0; + } + if (auxv[i].a_type == key) { + *value = auxv[i].a_un.a_val; + return 1; + } + } + + return -1; +} + +static void check_auxv_terminator(void) +{ + const Elf64_auxv_t *auxv = initial_auxv(); + int found = 0; + + for (size_t i = 0; i < 128; i++) { + if (auxv[i].a_type == AT_NULL) { + found = 1; + break; + } + } + + CHECK(found, "initial auxv contains AT_NULL terminator within 128 entries"); +} + +static void check_getauxval_entry(unsigned long key, unsigned long expected, + const char *msg) +{ + errno = 0; + unsigned long value = getauxval(key); + CHECK(errno == 0, "getauxval reports existing auxv entry"); + CHECK(value == expected, msg); +} + +int main(void) +{ + TEST_START("ELF auxiliary vector process ABI"); + + check_auxv_terminator(); + + unsigned long secure = 1; + int secure_found = find_auxv_value(AT_SECURE, &secure); + CHECK(secure_found == 1, "AT_SECURE is present in initial auxv"); + if (secure_found == 1) { + CHECK(secure == 0, "normal non-setuid exec has AT_SECURE=0"); + check_getauxval_entry(AT_SECURE, 0, "getauxval(AT_SECURE) == 0"); + } + + unsigned long uid = 0; + unsigned long euid = 0; + unsigned long gid = 0; + unsigned long egid = 0; + CHECK(find_auxv_value(AT_UID, &uid) == 1, "AT_UID is present"); + CHECK(find_auxv_value(AT_EUID, &euid) == 1, "AT_EUID is present"); + CHECK(find_auxv_value(AT_GID, &gid) == 1, "AT_GID is present"); + CHECK(find_auxv_value(AT_EGID, &egid) == 1, "AT_EGID is present"); + + CHECK(uid == (unsigned long)getuid(), "AT_UID matches getuid()"); + CHECK(euid == (unsigned long)geteuid(), "AT_EUID matches geteuid()"); + CHECK(gid == (unsigned long)getgid(), "AT_GID matches getgid()"); + CHECK(egid == (unsigned long)getegid(), "AT_EGID matches getegid()"); + + check_getauxval_entry(AT_UID, (unsigned long)getuid(), "getauxval(AT_UID) matches getuid()"); + check_getauxval_entry(AT_EUID, (unsigned long)geteuid(), "getauxval(AT_EUID) matches geteuid()"); + check_getauxval_entry(AT_GID, (unsigned long)getgid(), "getauxval(AT_GID) matches getgid()"); + check_getauxval_entry(AT_EGID, (unsigned long)getegid(), "getauxval(AT_EGID) matches getegid()"); + + TEST_DONE(); +} diff --git a/test-suit/starryos/qemu-smp1/system/test-clone-fp-state/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/test-clone-fp-state/CMakeLists.txt new file mode 100644 index 0000000000..19be9d0033 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-clone-fp-state/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.20) +project(test-clone-fp-state C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS ON) + +add_executable(test-clone-fp-state src/main.c) +target_compile_options(test-clone-fp-state PRIVATE -Wall -Wextra -Werror) + +install(TARGETS test-clone-fp-state RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/test-clone-fp-state/src/main.c b/test-suit/starryos/qemu-smp1/system/test-clone-fp-state/src/main.c new file mode 100644 index 0000000000..de00fb5b91 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-clone-fp-state/src/main.c @@ -0,0 +1,86 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +#ifndef __riscv +int main(void) +{ + puts("test-clone-fp-state is riscv64-only"); + return 0; +} +#else + +static int pass_count; +static int fail_count; + +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + pass_count++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + fail_count++; \ + } \ +} while (0) + +static long raw_clone_sigchld(void) +{ + register long a0 asm("a0") = SIGCHLD; + register long a1 asm("a1") = 0; + register long a2 asm("a2") = 0; + register long a3 asm("a3") = 0; + register long a4 asm("a4") = 0; + register long a7 asm("a7") = SYS_clone; + + asm volatile( + "ecall" + : "+r"(a0) + : "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a7) + : "memory"); + + return a0; +} + +int main(void) +{ + puts("================================================"); + puts(" TEST: riscv64 clone inherits floating-point state"); + printf(" FILE: %s\n", __FILE__); + puts("================================================"); + + const double expected = 8192.5; + asm volatile("fld ft0, %0" :: "m"(expected) : "ft0", "memory"); + + long pid = raw_clone_sigchld(); + if (pid == 0) { + double observed = 0.0; + asm volatile("fsd ft0, %0" : "=m"(observed) :: "memory"); + _exit(observed == expected ? 0 : 77); + } + + CHECK(pid > 0, "raw clone(SIGCHLD, NULL) creates a child"); + if (pid > 0) { + int status = 0; + pid_t waited = waitpid((pid_t)pid, &status, 0); + CHECK(waited == (pid_t)pid, "waitpid returns cloned child"); + CHECK(WIFEXITED(status), "cloned child exits normally"); + if (WIFEXITED(status)) { + CHECK(WEXITSTATUS(status) == 0, + "child observes inherited ft0 value at clone return"); + } + } + + puts("------------------------------------------------"); + printf(" DONE: %d pass, %d fail\n", pass_count, fail_count); + puts("================================================"); + return fail_count > 0 ? 1 : 0; +} + +#endif diff --git a/test-suit/starryos/qemu-smp1/system/test-desktop-virtio-devices/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/test-desktop-virtio-devices/CMakeLists.txt new file mode 100644 index 0000000000..a07af090b3 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-desktop-virtio-devices/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +project(test-desktop-virtio-devices C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +add_executable(test-desktop-virtio-devices src/main.c) +target_include_directories(test-desktop-virtio-devices PRIVATE src ../common) +target_compile_options(test-desktop-virtio-devices PRIVATE -Wall -Wextra -Werror) + +install(TARGETS test-desktop-virtio-devices RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/test-desktop-virtio-devices/src/main.c b/test-suit/starryos/qemu-smp1/system/test-desktop-virtio-devices/src/main.c new file mode 100644 index 0000000000..45bff59350 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/test-desktop-virtio-devices/src/main.c @@ -0,0 +1,298 @@ +/* + * test-desktop-virtio-devices + * + * Boots the same class of devices used by a basic graphical desktop and + * verifies the public userspace contracts they must expose together: + * + * - /dev/dri/card0 is a usable KMS node with a connected mode. + * - a dumb framebuffer can be created, mapped, written, and destroyed. + * - /dev/input/event* exposes both a keyboard-like device and a + * pointer-like device through standard evdev ioctls. + * + * The test intentionally avoids depending on driver internals. It is a + * normal desktop-device behavior test, not a patch-shape regression test. + */ + +#define _GNU_SOURCE +#include "test_framework.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct drm_mode_create_dumb { + uint32_t height; + uint32_t width; + uint32_t bpp; + uint32_t flags; + uint32_t handle; + uint32_t pitch; + uint64_t size; +}; +struct drm_mode_map_dumb { + uint32_t handle; + uint32_t pad; + uint64_t offset; +}; +struct drm_mode_destroy_dumb { + uint32_t handle; +}; +struct drm_mode_mode_info { + uint32_t clock; + uint16_t hdisplay, hsync_start, hsync_end, htotal, hskew; + uint16_t vdisplay, vsync_start, vsync_end, vtotal, vscan; + uint32_t vrefresh, flags, kind; + char name[32]; +}; +struct drm_mode_card_res { + uint64_t fb_id_ptr, crtc_id_ptr, connector_id_ptr, encoder_id_ptr; + uint32_t count_fbs, count_crtcs, count_connectors, count_encoders; + uint32_t min_width, max_width, min_height, max_height; +}; +struct drm_mode_get_connector { + uint64_t encoders_ptr, modes_ptr, props_ptr, prop_values_ptr; + uint32_t count_modes, count_props, count_encoders; + uint32_t encoder_id, connector_id, connector_type, connector_type_id; + uint32_t connection, mm_width, mm_height, subpixel, pad; +}; + +#define DRM_IOCTL_MODE_GETRESOURCES _IOWR('d', 0xA0, struct drm_mode_card_res) +#define DRM_IOCTL_MODE_GETCONNECTOR _IOWR('d', 0xA7, struct drm_mode_get_connector) +#define DRM_IOCTL_MODE_CREATE_DUMB _IOWR('d', 0xB2, struct drm_mode_create_dumb) +#define DRM_IOCTL_MODE_MAP_DUMB _IOWR('d', 0xB3, struct drm_mode_map_dumb) +#define DRM_IOCTL_MODE_DESTROY_DUMB _IOWR('d', 0xB4, struct drm_mode_destroy_dumb) + +#define EV_KEY 0x01 +#define EV_REL 0x02 +#define EV_ABS 0x03 +#define EV_MAX 0x1f +#define KEY_ENTER 28 +#define KEY_A 30 +#define REL_X 0x00 +#define REL_Y 0x01 +#define ABS_X 0x00 +#define ABS_Y 0x01 +#define KEY_MAX 0x2ff +#define REL_MAX 0x0f +#define ABS_MAX 0x3f +#define INPUT_PROP_POINTER 0x00 + +#ifndef EVIOCGNAME +#define EVIOCGNAME(len) _IOC(_IOC_READ, 'E', 0x06, len) +#endif +#ifndef EVIOCGID +struct input_id { + uint16_t bustype; + uint16_t vendor; + uint16_t product; + uint16_t version; +}; +#define EVIOCGID _IOR('E', 0x02, struct input_id) +#endif +#ifndef EVIOCGBIT +#define EVIOCGBIT(ev, len) _IOC(_IOC_READ, 'E', 0x20 + (ev), len) +#endif +#ifndef EVIOCGPROP +#define EVIOCGPROP(len) _IOC(_IOC_READ, 'E', 0x09, len) +#endif + +static int has_bit(const unsigned char *bits, size_t nbytes, size_t bit) +{ + if (bit / 8 >= nbytes) { + return 0; + } + return (bits[bit / 8] >> (bit % 8)) & 1; +} + +static void test_drm_device(void) +{ + int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC | O_NONBLOCK); + CHECK(fd >= 0, "open /dev/dri/card0"); + if (fd < 0) { + return; + } + + struct stat st; + CHECK_RET(fstat(fd, &st), 0, "fstat /dev/dri/card0"); + CHECK((st.st_mode & S_IFMT) == S_IFCHR, "/dev/dri/card0 is a character device"); + + uint32_t crtcs[4] = {0}; + uint32_t connectors[4] = {0}; + struct drm_mode_card_res res = { + .crtc_id_ptr = (uint64_t)(uintptr_t)crtcs, + .connector_id_ptr = (uint64_t)(uintptr_t)connectors, + .count_crtcs = 4, + .count_connectors = 4, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res), 0, "DRM GETRESOURCES"); + CHECK(res.count_crtcs >= 1, "DRM reports at least one CRTC"); + CHECK(res.count_connectors >= 1, "DRM reports at least one connector"); + + struct drm_mode_mode_info mode = {0}; + if (res.count_connectors > 0) { + struct drm_mode_get_connector conn = { + .connector_id = connectors[0], + .count_modes = 1, + .modes_ptr = (uint64_t)(uintptr_t)&mode, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETCONNECTOR, &conn), 0, "DRM GETCONNECTOR"); + CHECK(conn.count_modes >= 1, "DRM connector has a mode"); + CHECK(mode.hdisplay > 0 && mode.vdisplay > 0, "DRM mode has nonzero size"); + } + + uint32_t width = mode.hdisplay ? mode.hdisplay : 64; + uint32_t height = mode.vdisplay ? mode.vdisplay : 64; + struct drm_mode_create_dumb dumb = { + .width = width, + .height = height, + .bpp = 32, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &dumb), 0, "DRM CREATE_DUMB"); + CHECK(dumb.handle != 0, "DRM dumb buffer handle is nonzero"); + CHECK(dumb.pitch >= width * 4 && dumb.size >= (uint64_t)dumb.pitch * height, + "DRM dumb buffer pitch and size cover the mode"); + + struct drm_mode_map_dumb map = { .handle = dumb.handle }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map), 0, "DRM MAP_DUMB"); + + uint8_t *pixels = MAP_FAILED; + if (dumb.size > 0) { + pixels = mmap(NULL, dumb.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map.offset); + CHECK(pixels != MAP_FAILED, "mmap DRM dumb buffer"); + } + if (pixels != MAP_FAILED) { + pixels[0] = 0x12; + pixels[1] = 0x34; + pixels[dumb.size - 1] = 0x56; + CHECK(pixels[0] == 0x12 && pixels[1] == 0x34 && pixels[dumb.size - 1] == 0x56, + "mapped DRM dumb buffer stores user writes"); + munmap(pixels, dumb.size); + } + + struct drm_mode_destroy_dumb destroy = { .handle = dumb.handle }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy), 0, "DRM DESTROY_DUMB"); + close(fd); +} + +static int classify_event_device(const char *path) +{ + int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) { + printf(" INFO | %s | open failed errno=%d (%s)\n", path, errno, strerror(errno)); + return 0; + } + + struct stat st; + CHECK_RET(fstat(fd, &st), 0, "fstat event device"); + CHECK((st.st_mode & S_IFMT) == S_IFCHR, "event device is a character device"); + CHECK(major(st.st_rdev) == 13 && minor(st.st_rdev) >= 64, + "event device uses Linux evdev major/minor range"); + + char name[128] = {0}; + int rc = ioctl(fd, EVIOCGNAME(sizeof(name)), name); + CHECK(rc >= 0, "EVIOCGNAME succeeds"); + if (rc >= 0) { + printf(" INFO | %s | name=%s\n", path, name); + } + + struct input_id id = {0}; + CHECK_RET(ioctl(fd, EVIOCGID, &id), 0, "EVIOCGID succeeds"); + + unsigned char ev_bits[(EV_MAX + 8) / 8] = {0}; + rc = ioctl(fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits); + CHECK(rc >= 0, "EVIOCGBIT(0) succeeds"); + int has_keys = rc >= 0 && has_bit(ev_bits, sizeof(ev_bits), EV_KEY); + int has_rel = rc >= 0 && has_bit(ev_bits, sizeof(ev_bits), EV_REL); + int has_abs = rc >= 0 && has_bit(ev_bits, sizeof(ev_bits), EV_ABS); + + int is_keyboard = 0; + if (has_keys) { + unsigned char key_bits[(KEY_MAX + 8) / 8] = {0}; + rc = ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), key_bits); + CHECK(rc >= 0, "EVIOCGBIT(EV_KEY) succeeds"); + is_keyboard = rc >= 0 + && (has_bit(key_bits, sizeof(key_bits), KEY_A) + || has_bit(key_bits, sizeof(key_bits), KEY_ENTER)); + } + + int has_pointer_axes = 0; + if (has_rel) { + unsigned char rel_bits[(REL_MAX + 8) / 8] = {0}; + rc = ioctl(fd, EVIOCGBIT(EV_REL, sizeof(rel_bits)), rel_bits); + CHECK(rc >= 0, "EVIOCGBIT(EV_REL) succeeds"); + has_pointer_axes = rc >= 0 + && has_bit(rel_bits, sizeof(rel_bits), REL_X) + && has_bit(rel_bits, sizeof(rel_bits), REL_Y); + } + if (!has_pointer_axes && has_abs) { + unsigned char abs_bits[(ABS_MAX + 8) / 8] = {0}; + rc = ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(abs_bits)), abs_bits); + CHECK(rc >= 0, "EVIOCGBIT(EV_ABS) succeeds"); + has_pointer_axes = rc >= 0 + && has_bit(abs_bits, sizeof(abs_bits), ABS_X) + && has_bit(abs_bits, sizeof(abs_bits), ABS_Y); + } + + int is_pointer = 0; + if (has_pointer_axes) { + unsigned char prop_bits[8] = {0}; + rc = ioctl(fd, EVIOCGPROP(sizeof(prop_bits)), prop_bits); + CHECK(rc >= 0, "EVIOCGPROP succeeds on pointer candidate"); + is_pointer = rc >= 0 && has_bit(prop_bits, sizeof(prop_bits), INPUT_PROP_POINTER); + } + + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + rc = poll(&pfd, 1, 0); + CHECK(rc >= 0, "poll(event device, timeout=0) succeeds"); + + close(fd); + return (is_keyboard ? 1 : 0) | (is_pointer ? 2 : 0); +} + +static void test_evdev_devices(void) +{ + DIR *dir = opendir("/dev/input"); + CHECK(dir != NULL, "open /dev/input"); + if (!dir) { + return; + } + + int event_devices = 0; + int keyboard_devices = 0; + int pointer_devices = 0; + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (strncmp(ent->d_name, "event", 5) != 0) { + continue; + } + char path[64]; + snprintf(path, sizeof(path), "/dev/input/%s", ent->d_name); + int kind = classify_event_device(path); + event_devices++; + if (kind & 1) { + keyboard_devices++; + } + if (kind & 2) { + pointer_devices++; + } + } + closedir(dir); + + CHECK(event_devices >= 2, "at least two /dev/input/event* devices are present"); + CHECK(keyboard_devices >= 1, "at least one keyboard-like evdev device is present"); + CHECK(pointer_devices >= 1, "at least one pointer-like evdev device is present"); +} + +int main(void) +{ + TEST_START("desktop virtio devices"); + test_drm_device(); + test_evdev_devices(); + TEST_DONE(); +} diff --git a/test-suit/starryos/qemu-smp1/system/test-drm-perbuf-dumb/src/main.c b/test-suit/starryos/qemu-smp1/system/test-drm-perbuf-dumb/src/main.c index 3418395fee..af0fcbb896 100644 --- a/test-suit/starryos/qemu-smp1/system/test-drm-perbuf-dumb/src/main.c +++ b/test-suit/starryos/qemu-smp1/system/test-drm-perbuf-dumb/src/main.c @@ -35,6 +35,13 @@ struct drm_mode_map_dumb { struct drm_mode_destroy_dumb { uint32_t handle; }; +struct drm_clip_rect { + uint16_t x1, y1, x2, y2; +}; +struct drm_mode_dirtyfb { + uint32_t fb_id, flags, color, num_clips; + uint64_t clips_ptr; +}; struct drm_mode_fb_cmd2 { uint32_t fb_id, width, height, pixel_format, flags; uint32_t handles[4], pitches[4], offsets[4]; @@ -81,6 +88,7 @@ struct drm_mode_create_blob { #define DRM_IOCTL_MODE_GETRESOURCES _IOWR('d', 0xA0, struct drm_mode_card_res) #define DRM_IOCTL_MODE_GETCONNECTOR _IOWR('d', 0xA7, struct drm_mode_get_connector) #define DRM_IOCTL_MODE_GETPROPERTY _IOWR('d', 0xAA, struct drm_mode_get_property) +#define DRM_IOCTL_MODE_DIRTYFB _IOWR('d', 0xB1, struct drm_mode_dirtyfb) #define DRM_IOCTL_MODE_CREATE_DUMB _IOWR('d', 0xB2, struct drm_mode_create_dumb) #define DRM_IOCTL_MODE_MAP_DUMB _IOWR('d', 0xB3, struct drm_mode_map_dumb) #define DRM_IOCTL_MODE_DESTROY_DUMB _IOWR('d', 0xB4, struct drm_mode_destroy_dumb) @@ -251,6 +259,20 @@ int main(void) { }; CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_ATOMIC, &atom), 0, "atomic commit fb1"); + struct drm_clip_rect clip = { + .x1 = 0, + .y1 = 0, + .x2 = w < 32 ? (uint16_t)w : 32, + .y2 = h < 32 ? (uint16_t)h : 32, + }; + struct drm_mode_dirtyfb dirty = { + .fb_id = fb1.fb_id, + .num_clips = 1, + .clips_ptr = (uint64_t)(uintptr_t)&clip, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_DIRTYFB, &dirty), 0, + "DIRTYFB accepts valid clipped framebuffer damage"); + /* 翻到 fb2 — 这是双缓冲翻页的核心路径 */ values[3] = fb2.fb_id; CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_ATOMIC, &atom), 0, "atomic commit fb2"); diff --git a/test-suit/starryos/qemu-smp1/system/test-unix-scm-rights/src/main.c b/test-suit/starryos/qemu-smp1/system/test-unix-scm-rights/src/main.c index bcaa1deddb..ef40e22bba 100644 --- a/test-suit/starryos/qemu-smp1/system/test-unix-scm-rights/src/main.c +++ b/test-suit/starryos/qemu-smp1/system/test-unix-scm-rights/src/main.c @@ -13,6 +13,40 @@ #include #include +static size_t cmsg_aligned_len(size_t len) +{ + size_t align = sizeof(size_t) - 1; + return (len + align) & ~align; +} + +static struct cmsghdr *next_cmsg_checked(const struct msghdr *msg, + const struct cmsghdr *cmsg) +{ + const unsigned char *control = (const unsigned char *)msg->msg_control; + size_t controllen = msg->msg_controllen; + const unsigned char *next = (const unsigned char *)cmsg + cmsg_aligned_len(cmsg->cmsg_len); + size_t offset = (size_t)(next - control); + + if (offset > controllen || controllen - offset < sizeof(struct cmsghdr)) { + return NULL; + } + + struct cmsghdr *candidate = (struct cmsghdr *)(void *)next; + if (candidate->cmsg_len < CMSG_LEN(0) || candidate->cmsg_len > controllen - offset) { + return NULL; + } + return candidate; +} + +static int read_marker_from_fd(int fd, char expected, const char *msg) +{ + char got = 0; + ssize_t ret = read(fd, &got, 1); + CHECK_RET(ret, 1, msg); + CHECK(got == expected, "received fd reads expected marker"); + return ret == 1 && got == expected; +} + int main(void) { TEST_START("AF_UNIX SOCK_STREAM SCM_RIGHTS round-trip"); @@ -40,7 +74,7 @@ int main(void) cmh->cmsg_type = SCM_RIGHTS; cmh->cmsg_len = CMSG_LEN(sizeof(int)); memcpy(CMSG_DATA(cmh), &p[0], sizeof(int)); - mh.msg_controllen = cmh->cmsg_len; + mh.msg_controllen = sizeof(cbuf); ssize_t s = sendmsg(sv[0], &mh, 0); CHECK_RET(s, 1, "sendmsg with SCM_RIGHTS"); @@ -73,10 +107,7 @@ int main(void) /* Read the marker byte through the dup'd pipe end on the receive * side. If SCM_RIGHTS didn't actually pass the fd, this will fail. */ if (got_fd >= 0) { - char m = 0; - ssize_t rr = read(got_fd, &m, 1); - CHECK_RET(rr, 1, "read 1 byte from received pipe fd"); - CHECK(m == marker, "received pipe carries the same marker byte"); + read_marker_from_fd(got_fd, marker, "read 1 byte from received pipe fd"); close(got_fd); } @@ -85,5 +116,85 @@ int main(void) close(sv[0]); close(sv[1]); + CHECK_RET(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0, "socketpair for two cmsgs"); + + int p1[2]; + int p2[2]; + CHECK_RET(pipe(p1), 0, "pipe #1"); + CHECK_RET(pipe(p2), 0, "pipe #2"); + CHECK_RET(write(p1[1], "L", 1), 1, "write marker L"); + CHECK_RET(write(p2[1], "R", 1), 1, "write marker R"); + + char payload2 = 'B'; + struct iovec iov2 = { .iov_base = &payload2, .iov_len = 1 }; + char cbuf2[CMSG_SPACE(sizeof(int)) * 2]; + memset(cbuf2, 0, sizeof(cbuf2)); + struct msghdr mh2 = {0}; + mh2.msg_iov = &iov2; + mh2.msg_iovlen = 1; + mh2.msg_control = cbuf2; + mh2.msg_controllen = sizeof(cbuf2); + + struct cmsghdr *first = CMSG_FIRSTHDR(&mh2); + CHECK(first != NULL, "first sender cmsg available"); + first->cmsg_level = SOL_SOCKET; + first->cmsg_type = SCM_RIGHTS; + first->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(first), &p1[0], sizeof(int)); + + struct cmsghdr *second = + (struct cmsghdr *)((char *)first + CMSG_SPACE(sizeof(int))); + second->cmsg_level = SOL_SOCKET; + second->cmsg_type = SCM_RIGHTS; + second->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(second), &p2[0], sizeof(int)); + + mh2.msg_controllen = CMSG_SPACE(sizeof(int)) + CMSG_LEN(sizeof(int)); + CHECK_RET(sendmsg(sv[0], &mh2, 0), 1, "sendmsg with two aligned SCM_RIGHTS cmsgs"); + + char rxbuf2 = 0; + struct iovec riov2 = { .iov_base = &rxbuf2, .iov_len = 1 }; + char rcbuf2[CMSG_SPACE(sizeof(int) * 2) + CMSG_SPACE(sizeof(int))]; + memset(rcbuf2, 0, sizeof(rcbuf2)); + struct msghdr rmh2 = {0}; + rmh2.msg_iov = &riov2; + rmh2.msg_iovlen = 1; + rmh2.msg_control = rcbuf2; + rmh2.msg_controllen = sizeof(rcbuf2); + + CHECK_RET(recvmsg(sv[1], &rmh2, 0), 1, "recvmsg with two SCM_RIGHTS cmsgs"); + CHECK(rxbuf2 == payload2, "payload byte for two-cmsg message matches"); + + int received[4] = {-1, -1, -1, -1}; + int received_count = 0; + for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&rmh2); + cmsg != NULL; + cmsg = next_cmsg_checked(&rmh2, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) { + continue; + } + size_t data_len = cmsg->cmsg_len - CMSG_LEN(0); + int fd_count = (int)(data_len / sizeof(int)); + int *fds = (int *)CMSG_DATA(cmsg); + for (int i = 0; i < fd_count && received_count < 4; i++) { + received[received_count++] = fds[i]; + } + } + CHECK(received_count == 2, "received exactly two SCM_RIGHTS fds"); + if (received_count == 2) { + int left_ok = read_marker_from_fd(received[0], 'L', "read marker from first received fd"); + int right_ok = read_marker_from_fd(received[1], 'R', "read marker from second received fd"); + CHECK(left_ok && right_ok, "two aligned cmsgs preserve fd order and contents"); + } + for (int i = 0; i < received_count; i++) { + close(received[i]); + } + close(p1[0]); + close(p1[1]); + close(p2[0]); + close(p2[1]); + close(sv[0]); + close(sv[1]); + TEST_DONE(); }