diff --git a/apps/starry/doom/README.md b/apps/starry/doom/README.md new file mode 100644 index 0000000000..9de5ff20c5 --- /dev/null +++ b/apps/starry/doom/README.md @@ -0,0 +1,128 @@ +# doomgeneric Wayland 集成测试 + +在 StarryOS 上运行 Weston (DRM backend + GL/llvmpipe 渲染器) 作为 Wayland +compositor,然后通过 doomgeneric (SDL2 Wayland 输出) 运行 Doom 游戏, +验证从内核 DRM 到用户态渲染的全链路连通性。 + +## 当前状态 + +| 组件 | 状态 | 备注 | +|---|---|---| +| Weston DRM + GL (llvmpipe) | ✅ 正常 | `--renderer=gl` 路径,214x120 分辨率 | +| doomgeneric + SDL Wayland | ✅ 正常 | SDL_RENDER_DRIVER=opengles2 + LIBGL_ALWAYS_SOFTWARE=1 | + +## 分辨率说明 + +测试使用 **214x120** 分辨率,与 ffplay 测试的 284x160 不同。 +这是为了验证 DRM 驱动支持任意分辨率的模式设置。 + +- QEMU 配置: `virtio-gpu-pci,xres=214,yres=120` +- Weston: 自动检测 DRM 分辨率 + +## 内核需求 + +- `/dev/dri/card0` — DRM 设备(CREATE_DUMB、MAP_DUMB、ADDFB2、GETCAP)、 + PRIME dma-buf 导出/导入(HANDLE_TO_FD / FD_TO_HANDLE) +- `/dev/input/event*` — evdev 输入(鼠标、键盘) +- `memfd_create` + seal 支持 +- AF_UNIX SCM_RIGHTS 文件描述符传递 +- sysfs 设备枚举 + +## 测试流程 + +1. 启动 Weston(`kiosk-shell`,DRM 后端,GL/llvmpipe 渲染器) +2. 等待 Wayland socket 就绪(最多 120 秒) +3. 启动 doomgeneric(SDL_VIDEODRIVER=wayland,SDL_RENDER_DRIVER=opengles2,LIBGL_ALWAYS_SOFTWARE=1) +4. 验证进程存活 35 秒 +5. 输出 `DOOMGENERIC_TEST_PASSED` 或 `DOOMGENERIC_TEST_FAILED` + +## 构建说明 + +doomgeneric 从源代码构建,首次构建时从 GitHub 下载: + +- 源码地址:`https://github.com/ozkl/doomgeneric/archive/refs/heads/master.tar.gz` +- 缓存路径:`target/doomgeneric-source.tar.gz`(已加入 `.gitignore`) + +### 构建流程 + +1. 从 GitHub 下载源码(已缓存则跳过) +2. 解压到临时目录,修改 `Makefile.sdl` 使用 `gcc` +3. 使用 `qemu-user` 运行 Alpine GCC,编译 doomgeneric(链接 Alpine musl) +4. 复制编译好的二进制文件到 rootfs overlay + +### 主机依赖 + +构建需要在主机上安装以下工具(Docker 镜像已预装): + +- `debugfs` (e2fsprogs) — 操作 rootfs 镜像 +- `install` (coreutils) — 复制文件 +- `readelf` (binutils) — 解析动态库依赖 +- `qemu-user-static` — 运行 Alpine 工具链 + +### 依赖的 Alpine 包 + +| 包 | 用途 | +|---|---| +| weston + weston-backend-drm | Wayland compositor + DRM 后端 | +| mesa + mesa-egl + mesa-gl + mesa-dri-gallium + mesa-gbm | Mesa GL 库和 DRI 驱动 | +| libinput + libxkbcommon + xkeyboard-config | 输入 + 键盘映射 | +| pixman | 软件渲染回退 | +| freedoom | Doom WAD 文件(开源版) | +| sdl2 + sdl2_mixer | SDL2 渲染库(Wayland 后端) | +| gcc + make + musl-dev + sdl2-dev + sdl2_mixer-dev | 编译工具链和头文件 | + +## 构建与运行 + +```bash +cargo xtask starry app qemu -t doom --arch x86_64 +``` + +这会依次: +- 构建 StarryOS 内核(含 DRM display + PRIME dma-buf 支持) +- 运行 `prebuild.sh` 构建 rootfs overlay(安装 Alpine 包、编译 doomgeneric、复制运行时库、复制 Freedoom WAD) +- 启动 QEMU(virtio-gpu-pci,VNC :0,4 核 2G,214x120 分辨率,TCG 加速) +- 等待测试结果(QEMU 进程超时 180 秒) + +## 查看画面 + +QEMU 使用 VNC 显示输出,需要通过 VNC 连接查看: + +```bash +vncviewer localhost:5900 +``` + +或浏览器打开 `http://localhost:6080/vnc.html`(需 noVNC)。 + +## Docker 内运行 + +```bash +docker run --rm \ + -v $(pwd):/workspace \ + -w /workspace \ + --network host \ + ghcr.io/rcore-os/tgoskits-container:latest \ + cargo xtask starry app qemu -t doom --arch x86_64 +``` + +VNC 端口 `:0` 映射到宿主机 `localhost:5900`,`--network host` 确保 VNC +连接可达。 + +## 与 ffplay 测试的区别 + +| 项目 | ffplay | doomgeneric | +|---|---|---| +| 分辨率 | 284x160 | 214x120 | +| Weston 渲染器 | GL (llvmpipe) | GL (llvmpipe) | +| 测试内容 | 视频播放 | 游戏运行 | +| 输入支持 | 无 | 鼠标 + 键盘(virtio-tablet + virtio-keyboard) | + +## 故障排除 + +### 无 DRM 设备 +确保 QEMU 配置包含 `virtio-gpu-pci` 设备。 + +### Weston 启动失败 +检查 weston 日志:`/tmp/weston.log` 和 `/tmp/weston-stderr.log` + +### doomgeneric 无响应 +检查 doomgeneric 日志:`/tmp/doomgeneric_stderr.log` diff --git a/apps/starry/doom/build-x86_64-unknown-none.toml b/apps/starry/doom/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..6a37fb365f --- /dev/null +++ b/apps/starry/doom/build-x86_64-unknown-none.toml @@ -0,0 +1,13 @@ +target = "x86_64-unknown-none" +env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2" } +log = "Warn" +features = [ + "ax-feat/display", + "axplat-dyn/efi", + "ax-driver/virtio-blk", + "ax-driver/virtio-net", + "ax-driver/virtio-input", + "ax-driver/virtio-gpu", + "ax-driver/virtio-socket", + "starry-kernel/input", +] diff --git a/apps/starry/doom/ci-run.sh b/apps/starry/doom/ci-run.sh new file mode 100644 index 0000000000..a6583e2148 --- /dev/null +++ b/apps/starry/doom/ci-run.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +export STARRY_APP_DIR=/workspace/apps/starry/doom +export STARRY_ARCH=x86_64 +export STARRY_ROOTFS=/tmp/.tgos-images/rootfs-x86_64-alpine.img/rootfs-x86_64-alpine.img +export STARRY_STAGING_ROOT=/workspace/tmp/doom-staging +export STARRY_OVERLAY_DIR=/workspace/tmp/doom-overlay +export STARRY_WORKSPACE=/workspace + +rm -rf "$STARRY_STAGING_ROOT" "$STARRY_OVERLAY_DIR" +mkdir -p "$STARRY_STAGING_ROOT" "$STARRY_OVERLAY_DIR" + +cd /workspace +bash /workspace/apps/starry/doom/prebuild.sh 2>&1 +echo "EXIT_CODE=$?" diff --git a/apps/starry/doom/prebuild.sh b/apps/starry/doom/prebuild.sh new file mode 100755 index 0000000000..eaca90a45a --- /dev/null +++ b/apps/starry/doom/prebuild.sh @@ -0,0 +1,381 @@ +#!/usr/bin/env bash +# doomgeneric prebuild — 模仿 ffplay 构建流程 +set -euo pipefail + +# Overridable defaults for Docker container environment +: "${STARRY_ROOTFS:=/tmp/.tgos-images/rootfs-x86_64-alpine.img/rootfs-x86_64-alpine.img}" +: "${STARRY_STAGING_ROOT:=/workspace/tmp/doom-staging}" +: "${STARRY_OVERLAY_DIR:=/workspace/tmp/doom-overlay}" +: "${STARRY_ARCH:=x86_64}" + +app_dir="${STARRY_APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +arch="$STARRY_ARCH" +base_rootfs="$STARRY_ROOTFS" +staging_root="$STARRY_STAGING_ROOT" +overlay_dir="$STARRY_OVERLAY_DIR" +workspace="${STARRY_WORKSPACE:-$(cd "$app_dir/../../.." && pwd)}" +apk_cache="$workspace/target/doomgeneric-apk-cache" + +ensure_host_packages() { + local missing=() + + command -v debugfs >/dev/null 2>&1 || missing+=(e2fsprogs) + command -v install >/dev/null 2>&1 || missing+=(coreutils) + command -v readelf >/dev/null 2>&1 || missing+=(binutils) + + case "$arch" in + x86_64) command -v qemu-x86_64-static >/dev/null 2>&1 || missing+=(qemu-user-static) ;; + aarch64) command -v qemu-aarch64-static >/dev/null 2>&1 || missing+=(qemu-user-static) ;; + esac + + if [[ ${#missing[@]} -gt 0 ]]; then + echo "error: missing required host packages: ${missing[*]}" >&2 + exit 1 + fi +} + +extract_base_rootfs() { + debugfs -R "rdump / $staging_root" "$base_rootfs" +} + +install_packages() { + local qemu_runner + case "$arch" in + x86_64) qemu_runner="qemu-x86_64-static" ;; + aarch64) qemu_runner="qemu-aarch64-static" ;; + *) echo "unsupported arch: $arch" >&2; exit 1 ;; + esac + + [[ -f /etc/resolv.conf ]] && cp /etc/resolv.conf "$staging_root/etc/resolv.conf" + mkdir -p "$apk_cache" + + cat > "$staging_root/etc/apk/repositories" <<'REPO' +https://mirrors.aliyun.com/alpine/edge/main +https://mirrors.aliyun.com/alpine/edge/community +REPO + + echo "[doom] installing packages via qemu-user apk..." + QEMU_LD_PREFIX="$staging_root" \ + LD_LIBRARY_PATH="$staging_root/lib:$staging_root/usr/lib" \ + "$qemu_runner" -L "$staging_root" \ + "$staging_root/sbin/apk" \ + --root "$staging_root" \ + --repositories-file "$staging_root/etc/apk/repositories" \ + --keys-dir "$staging_root/etc/apk/keys" \ + --cache-dir "$apk_cache" \ + --update-cache --no-progress --no-scripts \ + add weston weston-backend-drm mesa-gbm \ + mesa mesa-egl mesa-gl mesa-dri-gallium mesa-utils \ + libinput libxkbcommon pixman xkeyboard-config \ + freedoom sdl2 \ + gcc make musl-dev sdl2-dev sdl2_mixer-dev + + # debugfs rdump 可能丢失执行权限,修复关键二进制和动态链接器 + echo "[doom] fixing permissions in staging root..." + chmod -R 755 "$staging_root/lib/" 2>/dev/null || true + chmod -R 755 "$staging_root/usr/lib/" 2>/dev/null || true + chmod 755 "$staging_root/usr/bin/"* 2>/dev/null || true + chmod 755 "$staging_root/bin/"* 2>/dev/null || true + chmod 755 "$staging_root/sbin/"* 2>/dev/null || true +} + +build_doomgeneric() { + local doom_src_url="https://github.com/ozkl/doomgeneric/archive/refs/heads/master.tar.gz" + local doom_src_cache="$workspace/target/doomgeneric-source.tar.gz" + mkdir -p "$(dirname "$doom_src_cache")" + + echo "[doom] downloading doomgeneric source..." + if [[ ! -f "$doom_src_cache" ]]; then + wget -q --timeout=30 -O "$doom_src_cache" "$doom_src_url" || \ + curl -fsSL --max-time 30 -o "$doom_src_cache" "$doom_src_url" + fi + [[ -f "$doom_src_cache" ]] || { echo "error: failed to download doomgeneric from $doom_src_url" >&2; exit 1; } + + echo "[doom] extracting doomgeneric source..." + local src_dir="$staging_root/tmp/doomgeneric-src" + local host_src="/tmp/doomgeneric-src" + local doom_bin_out="$workspace/target/.doomgeneric" + # 清旧产物,防止静默沿用失败构建 + rm -rf "$src_dir" "$host_src" "$doom_bin_out" + mkdir -p "$src_dir" "$host_src" "$(dirname "$doom_bin_out")" /tmp/doom-extract + tar xzf "$doom_src_cache" -C /tmp/doom-extract + cp -r /tmp/doom-extract/doomgeneric-master/doomgeneric/* "$src_dir/" + # 也要复制到 host /tmp,因为 qemu-user 走 host 文件系统 + cp -r /tmp/doom-extract/doomgeneric-master/doomgeneric/* "$host_src/" + rm -rf /tmp/doom-extract + + # 替换 CC=clang → CC=gcc + sed -i 's/CC=clang/CC=gcc/' "$src_dir/Makefile.sdl" + sed -i 's/CC=clang/CC=gcc/' "$host_src/Makefile.sdl" + # 给 DG_Init 添加 renderer NULL 检查 + SDL_GetError 打印 + for s in "$src_dir/doomgeneric_sdl.c" "$host_src/doomgeneric_sdl.c"; do + sed -i '/^ renderer = SDL_CreateRenderer/a\ if (!renderer) {\n fprintf(stderr, "DG_Init FATAL: SDL_CreateRenderer failed: %s\\n", SDL_GetError());\n exit(1);\n }' "$s" + done + + local qemu_runner + case "$arch" in + x86_64) qemu_runner="qemu-x86_64-static" ;; + aarch64) qemu_runner="qemu-aarch64-static" ;; + *) echo "unsupported arch: $arch" >&2; exit 1 ;; + esac + + echo "[doom] compiling doomgeneric via qemu-user..." + # Makefile 用反引号执行 sdl2-config,但 qemu-user 下 shebang 脚本的 exec() + # 行为不稳定,且 sdl2-config 返回的 -I/usr/include/SDL2 是客户机路径, + # 在宿主机上不存在。因此显式覆盖 SDL_CFLAGS/SDL_LIBS,不走反引号。 + # --sysroot: 让 GCC/ld 把系统路径前缀拼上 staging root(qemu-user -L + # 只翻译 ELF 加载路径,不翻译 GCC/ld 的 open() 系统调用) + # -I/-L $staging_root: 显式传宿主机可见的绝对路径给 SDL2 + # --allow-multiple-definition: dummy.o 和 i_sdlmusic.o 都定义了函数 + "$qemu_runner" -L "$staging_root" \ + /bin/sh -c "cd /tmp/doomgeneric-src && make -f Makefile.sdl clean && make -f Makefile.sdl -j4 \ + CC=gcc \ + SDL_CFLAGS=\"\" \ + SDL_LIBS=\"-lSDL2_mixer -lSDL2\" \ + CFLAGS=\"--sysroot=$staging_root -I$staging_root/usr/include/SDL2 -D_REENTRANT\" \ + LDFLAGS=\"--sysroot=$staging_root -no-pie -Wl,--allow-multiple-definition \ + -Wl,--dynamic-linker=/lib/ld-musl-x86_64.so.1 \ + -L$staging_root/usr/lib -L$staging_root/lib\"" 2>&1 || true + + # 检查编译产物(明确失败) + if [[ ! -f "$src_dir/doomgeneric" && ! -f "$host_src/doomgeneric" ]]; then + echo "[doom] ERROR: build failed!" >&2 + exit 1 + fi + local doom_bin="" + [[ -f "$src_dir/doomgeneric" ]] && doom_bin="$src_dir/doomgeneric" + [[ -z "$doom_bin" && -f "$host_src/doomgeneric" ]] && doom_bin="$host_src/doomgeneric" + + cp "$doom_bin" "$doom_bin_out" + chmod +x "$doom_bin_out" + # 也复制到 staging root,便于 populate_overlay 解析 DT_NEEDED + mkdir -p "$staging_root/usr/bin" && cp "$doom_bin" "$staging_root/usr/bin/doomgeneric" && chmod +x "$staging_root/usr/bin/doomgeneric" + echo "[doom] doomgeneric built successfully (musl)" +} + +# ================================================================ +# Helper: copy a single file from staging root to overlay, +# resolving symlinks and handling missing files gracefully. +# ================================================================ +copy_file_to_overlay() { + local guest_path="$1" + local mode="$2" + local source="$staging_root${guest_path}" + local target="$overlay_dir${guest_path}" + + if [[ ! -e "$source" ]]; then + echo "warning: skipping missing file: $guest_path" >&2 + return 0 + fi + + if [[ -L "$source" ]]; then + source="$(readlink -f "$source")" + fi + + install -Dm"$mode" "$source" "$target" +} + +# ================================================================ +# Helper: find a shared library in standard library directories. +# ================================================================ +find_library_path() { + local library="$1" + local dir + + for dir in lib usr/lib usr/local/lib usr/lib/pulseaudio usr/lib/dri; do + if [[ -e "$staging_root/$dir/$library" ]]; then + printf '/%s/%s\n' "$dir" "$library" + return 0 + fi + done + + return 1 +} + +# ================================================================ +# Helper: recursively copy runtime DT_NEEDED dependencies for a +# binary or .so, transitively. +# ================================================================ +copy_runtime_dependencies() { + local pending=("$@") + local seen=" " + local guest_path library + + while [[ ${#pending[@]} -gt 0 ]]; do + guest_path="${pending[0]}" + pending=("${pending[@]:1}") + + if [[ "$seen" == *" $guest_path "* ]]; then + continue + fi + seen+="$guest_path " + + while IFS= read -r library; do + local library_path + if ! library_path="$(find_library_path "$library")"; then + continue + fi + copy_file_to_overlay "$library_path" 0644 + pending+=("$library_path") + done < <( + readelf -d "$staging_root$guest_path" 2>/dev/null | + sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p' + ) + done +} + +populate_overlay() { + echo "[doom] populating overlay..." + + # === Mesa shader cache 目录(预创建避免 "No space left" 警告) === + mkdir -p "$overlay_dir/root/.cache/mesa_shader_cache" + + # === Weston 二进制 + 运行时 .so 依赖(自动解析 DT_NEEDED 递归复制) === + copy_file_to_overlay /usr/bin/weston 0755 + copy_runtime_dependencies /usr/bin/weston + + # Weston backend/plugin 模块 (libweston-14/*.so, usr/lib/weston/*) + if [[ -d "$staging_root/usr/lib/libweston-14" ]]; then + mkdir -p "$overlay_dir/usr/lib/libweston-14" + find "$staging_root/usr/lib/libweston-14" -maxdepth 1 -type f | while read -r src; do + install -Dm0644 "$src" "$overlay_dir/usr/lib/libweston-14/$(basename "$src")" + done + fi + if [[ -d "$staging_root/usr/lib/weston" ]]; then + (cd "$staging_root" && find usr/lib/weston \( -type f -o -type l \) | while read -r rel; do + local src="$staging_root/$rel" + local target="$src" + if [[ -L "$src" ]]; then + target="$(readlink -f "$src")" + fi + install -Dm0644 "$target" "$overlay_dir/$rel" + done) + fi + # 解析 libweston-14 插件的 DT_NEEDED + if [[ -d "$overlay_dir/usr/lib/libweston-14" ]]; then + for so in "$overlay_dir/usr/lib/libweston-14"/*.so; do + [[ -f "$so" ]] || continue + local rel="${so#$overlay_dir}" + copy_runtime_dependencies "$rel" 2>/dev/null || true + done + fi + + # Weston example clients (weston-simple-shm, 等) + if [[ -d "$staging_root/usr/libexec/weston" ]]; then + mkdir -p "$overlay_dir/usr/bin" + find "$staging_root/usr/libexec/weston" -maxdepth 1 -type f | while read -r src; do + local name + name="$(basename "$src")" + install -Dm0755 "$src" "$overlay_dir/usr/bin/$name" + echo "[doom] weston example: $name" + done + fi + + # === Mesa DRI / GL 库 === + for d in usr/lib/dri usr/lib/gallium usr/lib/gbm; do + [[ -d "$staging_root/$d" ]] || continue + mkdir -p "$overlay_dir/$d" + cp -L "$staging_root/$d"/*.so "$overlay_dir/$d/" 2>/dev/null || true + done + # 解析 DRI 驱动的 DT_NEEDED(libgallium, libLLVM 等) + for dri_so in "$overlay_dir/usr/lib/dri/"*.so \ + "$overlay_dir/usr/lib/gbm/"*.so \ + "$overlay_dir/usr/lib/gallium/"*.so; do + [[ -f "$dri_so" ]] || continue + local rel="${dri_so#$overlay_dir}" + copy_runtime_dependencies "$rel" 2>/dev/null || true + done + + # === SDL2 dlopen'd 库全面扫描 === + # SDL2 --enable-wayland-shared: dlopen 加载 wayland/xkbcommon + # SDL2_mixer: dlopen 加载音频编解码库 (vorbis, ogg, mpg123, opus, flac 等) + # Mesa: dlopen 加载 DRI/EGL/GL 驱动 + for lib in libwayland-client.so.0 libwayland-cursor.so.0 \ + libwayland-egl.so.1 libxkbcommon.so.0 \ + libxkbcommon-x11.so.0; do + if lib_path=$(find_library_path "$lib"); then + copy_file_to_overlay "$lib_path" 0644 + copy_runtime_dependencies "$lib_path" 2>/dev/null || true + fi + done + # 全面扫描 usr/lib/ 和 lib/ 下所有与 SDL/Wayland/Mesa/音频相关的 .so + for pattern in libSDL2* libwayland-* libxkbcommon* \ + libEGL* libGL* libGLES* libgbm* libgallium* libglapi* \ + libdrm* libffi* libexpat* libz* libstdc++* \ + libvorbis* libogg* libmpg123* libopus* libFLAC* \ + libsndfile* libpulse* libasyncns* liblzma* \ + libpng* libjpeg* libtiff* libwebp* \ + libpthread* librt* libdl* libm* libxcrypt*; do + for dir in "$staging_root/usr/lib" "$staging_root/lib"; do + for f in "$dir"/$pattern; do + [[ -f "$f" ]] || continue + local base; base="$(basename "$f")" + local target_dir="$overlay_dir/usr/lib" + if [[ ! -f "$target_dir/$base" ]]; then + cp -L "$f" "$target_dir/$base" && chmod 0644 "$target_dir/$base" + fi + done + done + done + # 确保 SDL2_mixer 的 dlopen 加载器能找到音频插件 + for subdir in usr/lib/sdl2 usr/lib/alsa-lib usr/lib/pulseaudio; do + if [[ -d "$staging_root/$subdir" ]]; then + mkdir -p "$overlay_dir/$subdir" + cp -L "$staging_root/$subdir"/*.so "$overlay_dir/$subdir/" 2>/dev/null || true + fi + done + + # === 动态链接器 (musl) === + [[ -f "$staging_root/lib/ld-musl-x86_64.so.1" ]] && \ + install -Dm0644 "$staging_root/lib/ld-musl-x86_64.so.1" "$overlay_dir/lib/ld-musl-x86_64.so.1" + + # === doomgeneric === + local doom_bin_out="$workspace/target/.doomgeneric" + [[ -f "$doom_bin_out" ]] || { echo "error: doomgeneric binary missing" >&2; exit 1; } + install -Dm0755 "$doom_bin_out" "$overlay_dir/usr/bin/doomgeneric" + # 确保 staging root 里有 doomgeneric,以便解析 DT_NEEDED 复制 SDL2 等库 + if [[ ! -f "$staging_root/usr/bin/doomgeneric" ]]; then + mkdir -p "$staging_root/usr/bin" + cp "$doom_bin_out" "$staging_root/usr/bin/doomgeneric" + fi + copy_runtime_dependencies /usr/bin/doomgeneric + install -Dm0755 "$app_dir/test_doom.sh" "$overlay_dir/usr/bin/test_doom.sh" + + # === mesa-utils / weston-info === + for bin in eglinfo weston-info; do + [[ -f "$staging_root/usr/bin/$bin" ]] && install -Dm0755 "$staging_root/usr/bin/$bin" "$overlay_dir/usr/bin/$bin" + done + + # === XKB 数据 === + for share in usr/share/X11/xkb usr/share/X11/locale; do + [[ -d "$staging_root/$share" ]] || continue + (cd "$staging_root/$share" && find . -type f 2>/dev/null | while read -r f; do + install -Dm0644 "$staging_root/$share/$f" "$overlay_dir/$share/$f" + done) + done + + # === libinput 数据 === + if [[ -d "$staging_root/usr/share/libinput" ]]; then + mkdir -p "$overlay_dir/usr/share/libinput" + find "$staging_root/usr/share/libinput" -type f | while read -r f; do + install -Dm0644 "$f" "$overlay_dir/usr/share/libinput/$(basename "$f")" + done + fi + + # === Freedoom WAD === + if [[ -d "$staging_root/usr/share/games/doom" ]]; then + mkdir -p "$overlay_dir/usr/share/games/doom" + find "$staging_root/usr/share/games/doom" -name '*.wad' -type f | while read -r f; do + install -Dm0644 "$f" "$overlay_dir/usr/share/games/doom/$(basename "$f")" + done + fi + + echo "[doom] overlay ready" +} + +ensure_host_packages +extract_base_rootfs +install_packages +build_doomgeneric +populate_overlay +rm -f "$workspace/target/.doomgeneric" diff --git a/apps/starry/doom/qemu-x86_64.toml b/apps/starry/doom/qemu-x86_64.toml new file mode 100644 index 0000000000..58ac50a220 --- /dev/null +++ b/apps/starry/doom/qemu-x86_64.toml @@ -0,0 +1,41 @@ +args = [ + "-machine", + "q35", + "-serial", + "stdio", + "-vnc", + ":0", + "-vga", + "none", + "-m", + "2G", + "-smp", + "4", + "-device", + "virtio-gpu-pci,xres=214,yres=120", + "-device", + "virtio-tablet-pci", + "-device", + "virtio-keyboard-pci", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", + "-accel", "tcg", +] +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test_doom.sh" +success_regex = ["DOOMGENERIC_TEST_PASSED"] +fail_regex = [ + "(?i)\\bpanic(?:ked)?\\b", + "(?i)page fault", + "(?i)segmentation fault", + "DOOMGENERIC_TEST_FAILED", +] +timeout = 180 diff --git a/apps/starry/doom/test_doom.sh b/apps/starry/doom/test_doom.sh new file mode 100755 index 0000000000..1ac07fd8d9 --- /dev/null +++ b/apps/starry/doom/test_doom.sh @@ -0,0 +1,135 @@ +#!/bin/sh +set +e + +export PATH=/usr/bin:/bin:/sbin:/usr/sbin +export XDG_RUNTIME_DIR=/tmp +export LIBSEAT_BACKEND=noop +export DISPLAY= +export TERM=dumb +chmod 0700 /tmp +rm -f /tmp/wayland-* 2>/dev/null + +PASS=0 +FAIL=0 + +pass() { PASS=$((PASS+1)); printf "\033[32m[PASS]\033[0m %s\n" "$*"; } +fail() { FAIL=$((FAIL+1)); printf "\033[31m[FAIL]\033[0m %s\n" "$*"; } +info() { printf "\033[33m[INFO]\033[0m %s\n" "$*"; } +hdr() { printf "\n\033[1;33m=== %s ===\033[0m\n" "$*"; } + +printf '\033[?1000l\033[?1002l\033[?1003l\033[?1006l' 2>/dev/null || true + +hdr "L1: Weston compositor (GL renderer / llvmpipe, 214x120)" +[ ! -e /dev/dri/card0 ] && { fail "L1: no DRM device"; exit 1; } + +rm -f /tmp/weston.log /tmp/weston-stderr.log +LIBGL_DEBUG=verbose LIBGL_ALWAYS_SOFTWARE=1 /usr/bin/weston \ + --backend=drm-backend.so \ + --renderer=gl \ + --shell=kiosk-shell.so \ + --no-config \ + --idle-time=0 \ + --log=/tmp/weston.log \ + >/tmp/weston-stdout.log 2>/tmp/weston-stderr.log & +WESTON_PID=$! + +# GL 渲染器 llvmpipe 初始化较慢,给 120s 超时 +WAYLAND_TIMEOUT=120 +READY=0 +for i in $(seq 1 $WAYLAND_TIMEOUT); do + sleep 1 + DISP=$(ls /tmp/ 2>/dev/null | grep '^wayland-[0-9]*$' | head -1) + if [ -n "$DISP" ]; then READY=1; break; fi + if ! kill -0 "$WESTON_PID" 2>/dev/null; then + fail "L1: Weston exited during init (after ${i}s)" + echo "=== Weston log (last 30 lines) ===" + tail -30 /tmp/weston.log 2>&1 || true + echo "=== Weston stderr ===" + cat /tmp/weston-stderr.log 2>&1 || true + echo "DOOMGENERIC_TEST_FAILED"; exit 1 + fi + [ $((i % 10)) -eq 0 ] && info "L1: waiting for Wayland socket... ${i}s" +done +[ "$READY" -eq 1 ] && pass "L1: Wayland socket /tmp/$DISP" \ + || { fail "L1: no Wayland socket after ${WAYLAND_TIMEOUT}s"; cat /tmp/weston.log 2>&1; echo "DOOMGENERIC_TEST_FAILED"; exit 1; } + +export WAYLAND_DISPLAY="$DISP" + +hdr "L2: doomgeneric SDL2 Wayland (214x120, GL)" +IWAD="/usr/share/games/doom/freedoom2.wad" +if [ ! -f "$IWAD" ]; then + IWAD="/usr/share/games/doom/freedoom1.wad" +fi + +if [ -f "$IWAD" ]; then + echo "--- starting doomgeneric (SDL2 Wayland, GL llvmpipe) ---" + rm -f /tmp/doomgeneric_stdout.log /tmp/doomgeneric_stderr.log + + export DOOMWADPATH=/usr/share/games/doom + SDL_RENDER_DRIVER=opengles2 \ + SDL_VIDEODRIVER=wayland \ + SDL_AUDIODRIVER=dummy \ + LIBGL_ALWAYS_SOFTWARE=1 \ + SDL_RENDER_VSYNC=1 \ + /usr/bin/doomgeneric \ + -iwad "$IWAD" \ + -nosound \ + >/tmp/doomgeneric_stdout.log 2>/tmp/doomgeneric_stderr.log & + DOOM_PID=$! + + sleep 5 + + if kill -0 "$DOOM_PID" 2>/dev/null; then + pass "L2: doomgeneric process alive (pid=$DOOM_PID)" + else + wait "$DOOM_PID" 2>/dev/null + RC=$? + case $RC in + 139) fail "L2: doomgeneric SIGSEGV" ;; + *) fail "L2: doomgeneric exited early (exit=$RC)" ;; + esac + fi + + sleep 30 + + if kill -0 "$DOOM_PID" 2>/dev/null; then + pass "L2: doomgeneric survived 35s (pid=$DOOM_PID)" + kill "$DOOM_PID" 2>/dev/null || true + else + wait "$DOOM_PID" 2>/dev/null + RC=$? + case $RC in + 139) fail "L2: doomgeneric SIGSEGV" ;; + *) fail "L2: doomgeneric exited early (exit=$RC)" ;; + esac + fi +else + fail "L2: no freedoom IWAD found" + ls /usr/share/games/doom/ 2>&1 +fi + +hdr "L3: Diagnostic dump" +echo "--- weston log (last 50 lines) ---" +tail -50 /tmp/weston.log 2>&1 || true +echo "--- weston stderr ---" +cat /tmp/weston-stderr.log 2>&1 || true +echo "--- doomgeneric stderr (raw) ---" +cat /tmp/doomgeneric_stderr.log 2>&1 +echo "--- doomgeneric stdout (raw) ---" +cat /tmp/doomgeneric_stdout.log 2>&1 +echo "--- SDL/renderer info ---" +grep -iE 'renderer|error|failed' /tmp/doomgeneric_stderr.log 2>/dev/null || echo "(no renderer info found)" + +hdr "Cleanup" +kill "$DOOM_PID" 2>/dev/null || true +kill "$WESTON_PID" 2>/dev/null || true +for i in $(seq 1 5); do + kill -0 "$WESTON_PID" 2>/dev/null || break + sleep 1 +done +kill -9 "$WESTON_PID" 2>/dev/null || true +wait "$WESTON_PID" 2>/dev/null || true + +hdr "SUMMARY" +echo "Passed: $PASS Failed: $FAIL" +[ "$FAIL" -eq 0 ] && echo "DOOMGENERIC_TEST_PASSED" || echo "DOOMGENERIC_TEST_FAILED" diff --git a/os/StarryOS/kernel/src/pseudofs/dev/card0.rs b/os/StarryOS/kernel/src/pseudofs/dev/card0.rs index 807c302ddb..dcfb10ce99 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/card0.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/card0.rs @@ -220,6 +220,10 @@ struct DumbBuffer { struct Framebuffer { /// Total backing size in bytes. size: u64, + /// Row stride (pitch) in bytes — from ADDFB2.pitches[0]. + stride: u32, + /// Framebuffer height in pixels — from ADDFB2.height. + height: u32, /// Backing pages. Shared with the (now possibly removed) dumb /// buffer; refcount keeps them alive until both this fb and any /// user mappings have been dropped. @@ -681,25 +685,37 @@ impl Card0 { // the lock so `framebuffer_flush` doesn't run with the // map locked. Pages survive a concurrent DESTROY_DUMB because // the fb owns its own Arc clone. - let (pages, size) = match self.fbs.lock().get(&fb_id) { - Some(fb) => (fb.pages.clone(), fb.size), + let (pages, src_stride, rows, size) = match self.fbs.lock().get(&fb_id) { + Some(fb) => (fb.pages.clone(), fb.stride, fb.height as usize, fb.size), None => return, }; if !ax_display::has_display() { return; }; let info = ax_display::framebuffer_info(); - let copy = (size as usize).min(info.fb_size); - // SAFETY: `pages` owns the source pages; `info.fb_base_vaddr` - // is the axdisplay-owned scanout region; the two regions don't - // overlap because the per-buffer pages were allocated separately - // from the axdisplay framebuffer. - unsafe { - core::ptr::copy_nonoverlapping( - pages.start_vaddr().as_usize() as *const u8, - info.fb_base_vaddr as *mut u8, - copy, - ); + let src = pages.start_vaddr().as_usize() as *const u8; + let dst = info.fb_base_vaddr as *mut u8; + + if src_stride != 0 && info.stride != 0 && src_stride as usize != info.stride { + // Stride mismatch — copy row by row to avoid diagonal tearing. + let dst_limit = info.fb_size / info.stride.max(1); + let rows = rows.min(dst_limit); + let bytes_per_row = (src_stride as usize).min(info.stride); + for row in 0..rows { + unsafe { + core::ptr::copy_nonoverlapping( + src.add(row * src_stride as usize), + dst.add(row * info.stride), + bytes_per_row, + ); + } + } + } else { + // Strides match (or one is unknown) — flat copy. + let copy = (size as usize).min(info.fb_size); + unsafe { + core::ptr::copy_nonoverlapping(src, dst, copy); + } } let _ = ax_display::framebuffer_flush(); } @@ -1139,6 +1155,38 @@ impl Card0 { }; (b.pages.clone(), b.size) }; + // Use the plane stride from the ADDFB2 request (f.pitches[0]) + // rather than the dumb buffer's pitch. PRIME/import buffers may + // have a dumb pitch of 0 even when userspace supplies a valid + // stride in the ADDFB2 call. + let fb_stride = f.pitches[0]; + let fb_width = f.width; + let fb_height = f.height; + let fb_pixel_format = f.pixel_format; + + let bpp = match fb_pixel_format { + DRM_FORMAT_XRGB8888 | DRM_FORMAT_ARGB8888 => 32u32, + _ => { + warn!("ADDFB2: unsupported pixel_format {:#x}", fb_pixel_format); + return Err(VfsError::InvalidInput); + } + }; + let visible_bytes = fb_width * (bpp / 8); + if fb_stride < visible_bytes { + warn!( + "ADDFB2: stride {} < visible bytes {} ({}bpp, {}px)", + fb_stride, visible_bytes, bpp, fb_width + ); + return Err(VfsError::InvalidInput); + } + let fb_total = fb_stride as u64 * fb_height as u64; + if size < fb_total { + warn!( + "ADDFB2: buffer size {} < fb_total {} ({}stride × {}height)", + size, fb_total, fb_stride, fb_height + ); + return Err(VfsError::InvalidInput); + } if f.flags & DRM_MODE_FB_MODIFIERS != 0 { for i in 0..4 { if f.handles[i] == 0 { @@ -1151,7 +1199,15 @@ impl Card0 { } } let fb_id = self.next_fb_id.fetch_add(1, Ordering::Relaxed); - self.fbs.lock().insert(fb_id, Framebuffer { size, pages }); + self.fbs.lock().insert( + fb_id, + Framebuffer { + size, + stride: fb_stride, + height: fb_height, + pages, + }, + ); f.fb_id = fb_id; ptr.vm_write(f).map_err(|_| VfsError::BadAddress)?; Ok(0)