From ffc5798707db6a49e1b8502715e19dc8a64e37a8 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Sun, 22 Feb 2026 17:55:59 +0800 Subject: [PATCH 1/9] Add DirectFB2 cross-compilation support Add DirectFB2 cross-compilation support for RISC-V guest userspace testing ('scripts/build-image.sh', 'configs/riscv-cross-file'). Co-authored-by: Shengwen Cheng --- .gitignore | 12 +++++++ scripts/build-image.sh | 71 ++++++++++++++++++++++++++++++++++++++++-- scripts/rootfs_ext4.sh | 9 +++++- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b56a9aa8..2fd19910 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,15 @@ rootfs.cpio # intermediate riscv-harts.dtsi +.smp_stamp + +# Build directories +buildroot/ +linux/ +rootfs/ +directfb/ +extra_packages/ + +# DirectFB build +DirectFB2/ +DirectFB-examples/ diff --git a/scripts/build-image.sh b/scripts/build-image.sh index 0b977a4e..0f918f35 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -43,7 +43,7 @@ function do_buildroot safe_copy configs/buildroot.config buildroot/.config safe_copy configs/busybox.config buildroot/busybox.config cp -f target/init buildroot/fs/cpio/init - + # Otherwise, the error below raises: # You seem to have the current working directory in your # LD_LIBRARY_PATH environment variable. This doesn't work. @@ -53,6 +53,10 @@ function do_buildroot ASSERT make $PARALLEL popd + if [[ $BUILD_EXTRA_PACKAGES -eq 1 ]]; then + do_extra_packages + fi + if [[ $EXTERNAL_ROOT -eq 1 ]]; then echo "Copying rootfs.cpio to rootfs_full.cpio (external root mode)" cp -f buildroot/output/images/rootfs.cpio ./rootfs_full.cpio @@ -86,12 +90,13 @@ function do_linux function show_help { cat << EOF -Usage: $0 [--buildroot] [--linux] [--all] [--external-root] [--clean-build] [--help] +Usage: $0 [--buildroot] [--linux] [--extra-packages] [--all] [--external-root] [--clean-build] [--help] Options: --buildroot Build Buildroot rootfs + --extra-packages Build extra packages along with Buildroot --linux Build Linux kernel - --all Build both Buildroot and Linux + --all Build both Buildroot and Linux kernel --external-root Use external rootfs instead of initramfs --clean-build Remove entire buildroot/ and/or linux/ directories before build --help Show this message @@ -100,6 +105,7 @@ EOF } BUILD_BUILDROOT=0 +BUILD_EXTRA_PACKAGES=0 BUILD_LINUX=0 EXTERNAL_ROOT=0 CLEAN_BUILD=0 @@ -109,6 +115,9 @@ while [[ $# -gt 0 ]]; do --buildroot) BUILD_BUILDROOT=1 ;; + --extra-packages) + BUILD_EXTRA_PACKAGES=1 + ;; --linux) BUILD_LINUX=1 ;; @@ -133,11 +142,67 @@ while [[ $# -gt 0 ]]; do shift done +function do_directfb +{ + export PATH=$PATH:$PWD/buildroot/output/host/bin + export BUILDROOT_OUT=$PWD/buildroot/output/ + mkdir -p directfb + + # Build DirectFB2 + if [ ! -d DirectFB2 ]; then + echo "Cloning DirectFB2..." + ASSERT git clone https://github.com/directfb2/DirectFB2.git + else + echo "DirectFB2 already exists, skipping clone..." + fi + pushd DirectFB2 + cp ../configs/riscv-cross-file . + ASSERT meson -Ddrmkms=true --cross-file riscv-cross-file build/riscv + ASSERT meson compile -C build/riscv + DESTDIR=$BUILDROOT_OUT/host/riscv32-buildroot-linux-gnu/sysroot meson install -C build/riscv + DESTDIR=../../../directfb meson install -C build/riscv + popd + + # Build DirectFB2 examples + if [ ! -d DirectFB-examples ]; then + echo "Cloning DirectFB-examples..." + ASSERT git clone https://github.com/directfb2/DirectFB-examples.git + else + echo "DirectFB-examples already exists, skipping clone..." + fi + pushd DirectFB-examples/ + cp ../configs/riscv-cross-file . + ASSERT meson --cross-file riscv-cross-file build/riscv + ASSERT meson compile -C build/riscv + DESTDIR=../../../directfb meson install -C build/riscv + popd +} + +function do_extra_packages +{ + export PATH="$PWD/buildroot/output/host/bin:$PATH" + export CROSS_COMPILE=riscv32-buildroot-linux-gnu- + + rm -rf extra_packages + mkdir -p extra_packages + mkdir -p extra_packages/root + + do_directfb && OK + + cp -r directfb/* extra_packages + cp target/run.sh extra_packages/root/ +} + if [[ $BUILD_BUILDROOT -eq 0 && $BUILD_LINUX -eq 0 ]]; then echo "Error: No build target specified. Use --buildroot, --linux, or --all." show_help fi +if [[ $BUILD_EXTRA_PACKAGES -eq 1 && $BUILD_BUILDROOT -eq 0 ]]; then + echo "Error: --extra-packages requires --buildroot to be specified." + show_help +fi + if [[ $CLEAN_BUILD -eq 1 && $BUILD_BUILDROOT -eq 1 && -d buildroot ]]; then echo "Removing buildroot/ for clean build..." rm -rf buildroot diff --git a/scripts/rootfs_ext4.sh b/scripts/rootfs_ext4.sh index 0fb91de3..1cfd607c 100755 --- a/scripts/rootfs_ext4.sh +++ b/scripts/rootfs_ext4.sh @@ -13,9 +13,16 @@ mkdir -p $DIR echo "[*] Extract CPIO" pushd $DIR -cpio -idmv < ../$ROOTFS_CPIO +cpio -idmv < ../$ROOTFS_CPIO popd +if [ -d extra_packages ] && [ -n "$(ls -A extra_packages 2>/dev/null)" ]; then + echo "[*] Copying extra packages..." + cp -r extra_packages/. $DIR/ +else + echo "[*] No extra_packages directory or directory is empty, skipping..." +fi + echo "[*] Create empty image" dd if=/dev/zero of=${IMG} bs=4k count=${IMG_SIZE_BLOCKS} From 9b1d92b19bcb192307fe209a67791853e8b9ac07 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Sun, 22 Feb 2026 17:55:31 +0800 Subject: [PATCH 2/9] Implement virtio-gpu 2D device Add a minimal virtio-gpu device model in 2D mode, backed by a software renderer and SDL2 host window, to reach a baseline sufficient for DRM dumb buffer scanout. 3D/blob/virgl acceleration is explicitly out of scope. There are multiple ways to provide display output: 1. Implement the full virtio-gpu 3D/blob path (virgl integration), which requires a substantially larger surface area and additional host dependencies. 2. Implement virtio-gpu 2D only, backed by a software renderer and a host window system, which is enough for DRM dumb buffer style usage and basic compositing. 3. Provide a non-virtio framebuffer-like device, which would not match the intended virtio workflow and would not exercise guest virtio drivers. This commit adopts (2) to provide the smallest virtio-conformant baseline that still enables real guest drivers and guest userspace stacks. virtio-gpu (2D): - Add a core virtio-gpu device model with MMIO register handling ('virtio-gpu.c'). - Add a software backend for 2D resources and transfers ('virtio-gpu-sw.c'). - Add a host window path via SDL2 ('window-sw.c') to present the scanout surface. - Generate EDID data so the guest can probe display identification and modes. Supported GPU commands (2D path): - 'GET_DISPLAY_INFO' - 'RESOURCE_CREATE_2D', 'RESOURCE_UNREF' - 'SET_SCANOUT' - 'TRANSFER_TO_HOST_2D' - 'RESOURCE_FLUSH' - 'RESOURCE_ATTACH_BACKING', 'RESOURCE_DETACH_BACKING' - 'GET_EDID' - 'UPDATE_CURSOR', 'MOVE_CURSOR' Explicitly not implemented (3D/blob related): - 'GET_CAPSET_INFO', 'GET_CAPSET', 'RESOURCE_ASSIGN_UUID' - 'RESOURCE_CREATE_BLOB', 'SET_SCANOUT_BLOB' The device model is intentionally limited to the 2D command set needed for a basic DRM/KMS-based flow: create 2D resources, attach guest backing memory, transfer to host, flush to scanout, and optionally update/move the cursor. MMIO access rule: virtio-mmio common registers are restricted to aligned 32-bit accesses. The device-specific configuration region starts at Config (offset 0x100) and permits byte/halfword accesses as required by certain virtio devices. QEMU uses a shared virtio-mmio layer to handle sub-32-bit accesses in the device-specific configuration region and to avoid duplicating MMIO read/write boilerplate across each virtio device. In contrast, this project keeps the device model intentionally minimal, so each virtio device currently implements its own MMIO read/write handlers instead of introducing a unified MMIO framework. CI test: Currently, on Linux we use 'SDL_VIDEODRIVER=offscreen' to help validate the correctness of the vGPU functionality in CI. However, since this flag does not work properly on macOS, we apply headless mode in macOS CI test for now. With headless mode enabled, we can still verify the following: - The Virtio-GPU driver loads in the guest kernel. - The '/dev/dri/card0' DRM device node appears. - The 'virtio_gpu' driver binds correctly. - GPU commands work (create resource, attach backing, transfer data). - DirectFB/graphics libraries can initialize via DRM/KMS. - The protocol implementation is correct. References: - VirtIO Spec v1.3 (virtio-mmio, virtio-gpu) - QEMU: - hw/display/virtio-gpu.c - Linux: - drivers/virtio/virtio_ring.c - drivers/virtio/virtio_mmio.c - drivers/gpu/drm/virtio/virtgpu_drv.c - drivers/gpu/drm/virtio/virtgpu_display.c - drivers/gpu/drm/virtio/virtgpu_ioctl.c - drivers/gpu/drm/virtio/virtgpu_vq.c - drivers/gpu/drm/virtio/virtgpu_object.c - drivers/gpu/drm/virtio/virtgpu_gem.c - drivers/gpu/drm/virtio/virtgpu_prime.c - drivers/gpu/drm/virtio/virtgpu_plane.c Co-authored-by: Shengwen Cheng --- .ci/test-vgpu.sh | 157 +++++ .github/actions/setup-semu/action.yml | 5 +- .github/workflows/main.yml | 12 + .gitignore | 1 + Makefile | 30 + configs/riscv-cross-file | 18 + device.h | 65 +- feature.h | 5 + main.c | 42 +- minimal.dts | 8 + target/run.sh | 4 + utils.h | 7 + virtio-gpu-sw.c | 662 ++++++++++++++++++++ virtio-gpu.c | 828 ++++++++++++++++++++++++++ virtio-gpu.h | 362 +++++++++++ virtio.h | 6 + window-sw.c | 424 +++++++++++++ window.h | 22 + 18 files changed, 2647 insertions(+), 11 deletions(-) create mode 100755 .ci/test-vgpu.sh create mode 100644 configs/riscv-cross-file create mode 100755 target/run.sh create mode 100644 virtio-gpu-sw.c create mode 100644 virtio-gpu.c create mode 100644 virtio-gpu.h create mode 100644 window-sw.c create mode 100644 window.h diff --git a/.ci/test-vgpu.sh b/.ci/test-vgpu.sh new file mode 100755 index 00000000..87c90d1e --- /dev/null +++ b/.ci/test-vgpu.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "${SCRIPT_DIR}/common.sh" + +# Override timeout and sleep duration for macOS - emulation is significantly slower +case "${OS_TYPE}" in + Darwin) + TIMEOUT=10800 + DFB_SLEEP=180 + ;; + *) + DFB_SLEEP=5 + ;; +esac +export DFB_SLEEP + +cleanup +trap cleanup EXIT + +# Download pre-built ext4.img with DirectFB if not present +EXT4_URL="https://github.com/Mes0903/semu/raw/blob/ext4.img.bz2" +EXT4_SHA1="83ed49c16d341bdf3210141d5f6d5842b77a6adc" + +if [ ! -f "ext4.img.bz2" ]; then + echo "Downloading ext4.img.bz2 for DirectFB testing..." + curl --progress-bar -O -L "${EXT4_URL}" + echo "${EXT4_SHA1} ext4.img.bz2" | shasum -a 1 -c - +fi + +echo "Decompressing ext4.img.bz2 for DirectFB testing..." +rm -f ext4.img +bunzip2 -kf ext4.img.bz2 + +# NOTE: We want to capture the expect exit code and map +# it to our MESSAGES array for meaningful error output. +# Temporarily disable errexit for the expect call. +set +e +expect <<'DONE' +set timeout $env(TIMEOUT) +spawn make check + +# Boot and login +expect "buildroot login:" { send "root\r" } timeout { exit 1 } +expect "# " { send "uname -a\r" } timeout { exit 2 } +expect "riscv32 GNU/Linux" {} + +# ---------------- vgpu basic checks ---------------- +expect "# " { send "ls -la /dev/dri/ 2>/dev/null || true\r" } +expect "# " { send "test -c /dev/dri/card0 && echo __VGPU_DRM_OK__ || echo __VGPU_DRM_MISSING__\r" } timeout { exit 3 } +expect { + -re "\r?\n__VGPU_DRM_OK__" {} + -re "\r?\n__VGPU_DRM_MISSING__" { exit 3 } + timeout { exit 3 } +} + +# virtio transport may be virtio-mmio, binding check should look at virtio_gpu driver directory +expect "# " { + send "sh -lc 'ls /sys/bus/virtio/drivers/virtio_gpu/virtio* >/dev/null 2>&1 && echo __VGPU_BIND_OK__ || echo __VGPU_BIND_BAD__'\r" +} timeout { exit 3 } +expect { + -re "\r?\n__VGPU_BIND_OK__" {} + -re "\r?\n__VGPU_BIND_BAD__" { + send "ls -l /sys/bus/virtio/drivers/virtio_gpu/ 2>/dev/null || true\r" + # emit literal $d via \u0024 to avoid Tcl variable substitution + send "sh -lc 'for d in /sys/bus/virtio/devices/virtio*; do echo \u0024d; ls -l \u0024d/driver 2>/dev/null || true; done'\r" + exit 3 + } + timeout { exit 3 } +} + +# Useful logs (non-fatal) +expect "# " { send "dmesg | grep -Ei 'virtio.*gpu|drm.*virtio|scanout|number of scanouts' | tail -n 80 || true\r" } + +# ---------------- DirectFB2 ---------------- +# Strategy: +# 1) Stop X11 if running (it holds the DRM device) +# 2) Check run.sh exists at /root/run.sh +# 3) cd to /root and source run.sh to set PATH/LD_LIBRARY_PATH +# 4) Verify df_drivertest is in PATH +# 5) Run df_drivertest and check for DirectFB init messages +# +# NOTE: df_drivertest may segfault when killed due to a race condition in DirectFB2's +# fusion module (libfusion) during signal handling. When SIGTERM is sent, the signal +# handler starts cleanup while the "Fusion Dispatch" thread may still be accessing +# shared state, leading to a use-after-free crash. The test passes if DirectFB init +# messages appear, even if the program crashes afterward during cleanup. + +# Step 0: Stop X11 to release DRM device (it holds /dev/dri/card0) +# Use pidof with fallback to ps/grep if pidof unavailable +expect "# " { + send "sh -lc '\ + if command -v pidof >/dev/null 2>&1; then \ + pidof Xorg >/dev/null 2>&1 && kill \u0024(pidof Xorg) 2>/dev/null || true; \ + else \ + ps | grep Xorg | grep -v grep | awk \"{print \u00241}\" | xargs kill 2>/dev/null || true; \ + fi; \ + sleep 1; echo __X11_STOPPED__'\r" +} +expect "__X11_STOPPED__" {} + +# Step 1: Check run.sh exists +expect "# " { send "test -f /root/run.sh && echo __RUNSH_OK__ || echo __DFB_RUNSH_MISSING__\r" } +expect { + -re "\r?\n__RUNSH_OK__" {} + -re "\r?\n__DFB_RUNSH_MISSING__" { exit 4 } + timeout { exit 4 } +} + +# Step 2: cd to /root and source run.sh +expect "# " { send "cd /root && . ./run.sh >/dev/null 2>&1; echo __SRC_DONE__\r" } +expect "__SRC_DONE__" {} + +# Step 3: Verify df_drivertest is available +expect "# " { send "command -v df_drivertest && echo __APP_OK__ || echo __APP_MISS__\r" } +expect { + -re "\r?\n__APP_OK__" {} + -re "\r?\n__APP_MISS__" { exit 4 } + timeout { exit 4 } +} + +# Step 4: Run df_drivertest and check output (run in background, kill after delay) +expect "# " { send "df_drivertest >/tmp/dfb.log 2>&1 & sleep $env(DFB_SLEEP); kill \u0024! 2>/dev/null; head -30 /tmp/dfb.log\r" } +# Check for DRMKMS init message +expect "# " { send "grep -qi 'DRMKMS/System' /tmp/dfb.log && echo __DFB_OK__ || echo __DFB_FAIL__\r" } +expect { + -re "\r?\n__DFB_OK__" {} + -re "\r?\n__DFB_FAIL__" { exit 4 } + timeout { exit 4 } +} +DONE + +ret="$?" +set -e # Re-enable errexit after capturing expect's return code + +MESSAGES=( + "PASS: headless vgpu + DirectFB2 checks" + "FAIL: boot/login prompt not found" + "FAIL: shell prompt not found" + "FAIL: virtio-gpu basic checks failed (/dev/dri/card0 or virtio_gpu binding)" + "FAIL: DirectFB2 check failed (run.sh/df_drivertest missing or no DRMKMS init messages)" +) + +# Clean up pre-built ext4.img so other tests can use their own +if [ -f "ext4.img.bz2" ]; then + rm -f ext4.img +fi + +if [[ "${ret}" -eq 0 ]]; then + print_success "${MESSAGES[0]}" + exit 0 +fi + +print_error "${MESSAGES[${ret}]:-FAIL: unknown error (exit code ${ret})}" +exit "${ret}" diff --git a/.github/actions/setup-semu/action.yml b/.github/actions/setup-semu/action.yml index 6c581ccd..07baf5e6 100644 --- a/.github/actions/setup-semu/action.yml +++ b/.github/actions/setup-semu/action.yml @@ -14,7 +14,8 @@ runs: device-tree-compiler \ expect \ libasound2-dev \ - libudev-dev + libudev-dev \ + libsdl2-dev \ - name: Install dependencies (macOS) if: runner.os == 'macOS' @@ -23,4 +24,4 @@ runs: HOMEBREW_NO_AUTO_UPDATE: 1 HOMEBREW_NO_ANALYTICS: 1 run: | - brew install make dtc expect e2fsprogs + brew install make dtc expect e2fsprogs sdl2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a091bc22..b7bf21a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,6 +73,12 @@ jobs: run: .ci/test-sound.sh shell: bash timeout-minutes: 5 + - name: virtio-gpu test + run: .ci/test-vgpu.sh + shell: bash + timeout-minutes: 5 + env: + SDL_VIDEODRIVER: offscreen semu-macOS: runs-on: macos-latest @@ -126,6 +132,12 @@ jobs: run: .ci/test-sound.sh shell: bash timeout-minutes: 20 + - name: virtio-gpu test + run: .ci/test-vgpu.sh + shell: bash + timeout-minutes: 120 + env: + SDL_VIDEODRIVER: offscreen coding_style: runs-on: ubuntu-24.04 diff --git a/.gitignore b/.gitignore index 2fd19910..b65332e8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ semu Image ext4.img rootfs.cpio +rootfs_full.cpio # intermediate riscv-harts.dtsi diff --git a/Makefile b/Makefile index 0fcd4f27..c105010e 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ include mk/check-libs.mk CC ?= gcc CFLAGS := -O2 -g -Wall -Wextra CFLAGS += -include common.h +LDFLAGS := # clock frequency CLOCK_FREQ ?= 65000000 @@ -158,6 +159,35 @@ LDFLAGS += -lm # after git submodule. .DEFAULT_GOAL := all +# virtio-gpu +ENABLE_VIRTIOGPU ?= 1 + +# SDL2 +ENABLE_SDL ?= 1 +ifeq (, $(shell which sdl2-config)) + $(warning No sdl2-config in $$PATH. Check SDL2 installation in advance) + override ENABLE_SDL := 0 +endif +ifeq ($(ENABLE_SDL),1) + CFLAGS += $(shell sdl2-config --cflags) + LDFLAGS += $(shell sdl2-config --libs) +else + # Disable virtio-gpu if SDL is not set + override ENABLE_VIRTIOGPU := 0 +endif + +# virtio-gpu +ifneq ($(UNAME_S),Linux) + ENABLE_VIRTIOGPU := 0 +endif +ifeq ($(ENABLE_VIRTIOGPU),1) + OBJS_EXTRA += virtio-gpu.o + OBJS_EXTRA += virtio-gpu-sw.o + OBJS_EXTRA += window-sw.o +endif + +$(call set-feature, VIRTIOGPU) + BIN = semu all: $(BIN) minimal.dtb diff --git a/configs/riscv-cross-file b/configs/riscv-cross-file new file mode 100644 index 00000000..668f91f5 --- /dev/null +++ b/configs/riscv-cross-file @@ -0,0 +1,18 @@ +[binaries] + c = 'riscv32-buildroot-linux-gnu-gcc' + strip = 'riscv32-buildroot-linux-gnu-strip' + pkgconfig = 'pkg-config' + python = '/usr/bin/python3' + +[properties] + pkg_config_libdir = ['@GLOBAL_SOURCE_ROOT@' / '../buildroot/output/host/riscv32-buildroot-linux-gnu/sysroot/usr/local/lib/pkgconfig', + '@GLOBAL_SOURCE_ROOT@' / '../buildroot/output/host/riscv32-buildroot-linux-gnu/sysroot/usr/share/pkgconfig/', + '@GLOBAL_SOURCE_ROOT@' / '../buildroot/output/host/riscv32-buildroot-linux-gnu/sysroot/usr/lib/pkgconfig/' + ] + sys_root = '@GLOBAL_SOURCE_ROOT@' / '../buildroot/output/host/riscv32-buildroot-linux-gnu/sysroot' + +[host_machine] + system = 'linux' + cpu_family = 'riscv32' + cpu = 'riscv32-ima' + endian = 'little' diff --git a/device.h b/device.h index 88baea83..b3d9adff 100644 --- a/device.h +++ b/device.h @@ -12,6 +12,9 @@ #define DTB_SIZE (1 * 1024 * 1024) #define INITRD_SIZE (8 * 1024 * 1024) +#define SCREEN_WIDTH 1024 +#define SCREEN_HEIGHT 768 + void ram_read(hart_t *core, uint32_t *mem, const uint32_t addr, @@ -225,6 +228,57 @@ void virtio_rng_write(hart_t *vm, void virtio_rng_init(void); #endif /* SEMU_HAS(VIRTIORNG) */ +/* VirtIO-GPU */ + +#if SEMU_HAS(VIRTIOGPU) + +#define IRQ_VGPU 7 +#define IRQ_VGPU_BIT (1 << IRQ_VGPU) + +typedef struct { + uint32_t QueueNum; + uint32_t QueueDesc; + uint32_t QueueAvail; + uint32_t QueueUsed; + uint16_t last_avail; + bool ready; +} virtio_gpu_queue_t; + +typedef struct { + /* feature negotiation */ + uint32_t DeviceFeaturesSel; + uint32_t DriverFeatures; + uint32_t DriverFeaturesSel; + /* queue config */ + uint32_t QueueSel; + virtio_gpu_queue_t queues[2]; + /* status */ + uint32_t Status; + uint32_t InterruptStatus; + /* supplied by environment */ + uint32_t *ram; + /* implementation-specific */ + void *priv; +} virtio_gpu_state_t; + +void virtio_gpu_read(hart_t *vm, + virtio_gpu_state_t *vgpu, + uint32_t addr, + uint8_t width, + uint32_t *value); + +void virtio_gpu_write(hart_t *vm, + virtio_gpu_state_t *vgpu, + uint32_t addr, + uint8_t width, + uint32_t value); + +void virtio_gpu_init(virtio_gpu_state_t *vgpu); +void virtio_gpu_add_scanout(virtio_gpu_state_t *vgpu, + uint32_t width, + uint32_t height); +#endif /* SEMU_HAS(VIRTIOGPU) */ + /* ACLINT MTIMER */ typedef struct { /* A MTIMER device has two separate base addresses: one for the MTIME @@ -442,16 +496,19 @@ typedef struct { #if SEMU_HAS(VIRTIORNG) virtio_rng_state_t vrng; #endif - /* ACLINT */ - mtimer_state_t mtimer; - mswi_state_t mswi; - sswi_state_t sswi; #if SEMU_HAS(VIRTIOSND) virtio_snd_state_t vsnd; #endif #if SEMU_HAS(VIRTIOFS) virtio_fs_state_t vfs; #endif +#if SEMU_HAS(VIRTIOGPU) + virtio_gpu_state_t vgpu; +#endif + /* ACLINT */ + mtimer_state_t mtimer; + mswi_state_t mswi; + sswi_state_t sswi; uint32_t peripheral_update_ctr; diff --git a/feature.h b/feature.h index 850ef89f..351d2408 100644 --- a/feature.h +++ b/feature.h @@ -22,5 +22,10 @@ #define SEMU_FEATURE_VIRTIOFS 1 #endif +/* virtio-gpu */ +#ifndef SEMU_FEATURE_VIRTIOGPU +#define SEMU_FEATURE_VIRTIOGPU 1 +#endif + /* Feature test macro */ #define SEMU_HAS(x) SEMU_FEATURE_##x diff --git a/main.c b/main.c index 82e1433e..141ac38f 100644 --- a/main.c +++ b/main.c @@ -27,11 +27,14 @@ #include "mini-gdbstub/include/gdbstub.h" #include "riscv.h" #include "riscv_private.h" +#include "window.h" + #define PRIV(x) ((emu_state_t *) x->priv) /* Forward declarations for coroutine support */ static void wfi_handler(hart_t *hart); static void hart_exec_loop(void *arg); +extern const struct window_backend g_window; /* Define fetch separately since it is simpler (fixed width, already checked * alignment, only main RAM is executable). @@ -103,6 +106,18 @@ static void emu_update_vrng_interrupts(vm_t *vm) } #endif +#if SEMU_HAS(VIRTIOGPU) +static void emu_update_vgpu_interrupts(vm_t *vm) +{ + emu_state_t *data = PRIV(vm->hart[0]); + if (data->vgpu.InterruptStatus) + data->plic.active |= IRQ_VGPU_BIT; + else + data->plic.active &= ~IRQ_VGPU_BIT; + plic_update_interrupts(vm, &data->plic); +} +#endif + static void emu_update_timer_interrupt(hart_t *hart) { emu_state_t *data = PRIV(hart); @@ -197,6 +212,10 @@ static inline void emu_tick_peripherals(emu_state_t *emu) #if SEMU_HAS(VIRTIOFS) if (emu->vfs.InterruptStatus) emu_update_vfs_interrupts(vm); +#endif +#if SEMU_HAS(VIRTIOGPU) + if (emu->vgpu.InterruptStatus) + emu_update_vgpu_interrupts(vm); #endif } } @@ -249,17 +268,21 @@ static void mem_load(hart_t *hart, virtio_rng_read(hart, &data->vrng, addr & 0xFFFFF, width, value); return; #endif - #if SEMU_HAS(VIRTIOSND) case 0x47: /* virtio-snd */ virtio_snd_read(hart, &data->vsnd, addr & 0xFFFFF, width, value); return; #endif - #if SEMU_HAS(VIRTIOFS) case 0x48: /* virtio-fs */ virtio_fs_read(hart, &data->vfs, addr & 0xFFFFF, width, value); return; +#endif +#if SEMU_HAS(VIRTIOGPU) + case 0x49: /* virtio-gpu */ + virtio_gpu_read(hart, &data->vgpu, addr & 0xFFFFF, width, value); + emu_update_vgpu_interrupts(hart->vm); + return; #endif } } @@ -315,26 +338,29 @@ static void mem_store(hart_t *hart, aclint_sswi_write(hart, &data->sswi, addr & 0xFFFFF, width, value); aclint_sswi_update_interrupts(hart, &data->sswi); return; - #if SEMU_HAS(VIRTIORNG) case 0x46: /* virtio-rng */ virtio_rng_write(hart, &data->vrng, addr & 0xFFFFF, width, value); emu_update_vrng_interrupts(hart->vm); return; #endif - #if SEMU_HAS(VIRTIOSND) case 0x47: /* virtio-snd */ virtio_snd_write(hart, &data->vsnd, addr & 0xFFFFF, width, value); emu_update_vsnd_interrupts(hart->vm); return; #endif - #if SEMU_HAS(VIRTIOFS) case 0x48: /* virtio-fs */ virtio_fs_write(hart, &data->vfs, addr & 0xFFFFF, width, value); emu_update_vfs_interrupts(hart->vm); return; +#endif +#if SEMU_HAS(VIRTIOGPU) + case 0x49: /* virtio-gpu */ + virtio_gpu_write(hart, &data->vgpu, addr & 0xFFFFF, width, value); + emu_update_vgpu_interrupts(hart->vm); + return; #endif } } @@ -822,7 +848,13 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) if (!virtio_fs_init(&(emu->vfs), "myfs", shared_dir)) fprintf(stderr, "No virtio-fs functioned\n"); #endif +#if SEMU_HAS(VIRTIOGPU) + emu->vgpu.ram = emu->ram; + virtio_gpu_init(&(emu->vgpu)); + virtio_gpu_add_scanout(&(emu->vgpu), SCREEN_WIDTH, SCREEN_HEIGHT); + g_window.window_init(); +#endif emu->peripheral_update_ctr = 0; emu->debug = debug; diff --git a/minimal.dts b/minimal.dts index c2d412c0..90b9239f 100644 --- a/minimal.dts +++ b/minimal.dts @@ -87,5 +87,13 @@ interrupts = <6>; }; #endif + +#if SEMU_FEATURE_VIRTIOGPU + gpu0: virtio@4900000 { + compatible = "virtio,mmio"; + reg = <0x4900000 0x200>; + interrupts = <7>; + }; +#endif }; }; diff --git a/target/run.sh b/target/run.sh new file mode 100755 index 00000000..673dbae1 --- /dev/null +++ b/target/run.sh @@ -0,0 +1,4 @@ +#!/usr/bin/bash + +export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib +export PATH=$PATH:/usr/local/bin/ diff --git a/utils.h b/utils.h index b6c872e2..b232fc0a 100644 --- a/utils.h +++ b/utils.h @@ -79,6 +79,13 @@ static inline void list_del_init(struct list_head *node) INIT_LIST_HEAD(node); } +#define LIST_HEAD_INIT(name) \ + { \ + .prev = (&name), .next = (&name) \ + } + +#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name) + #ifndef container_of #define container_of(ptr, type, member) \ __extension__({ \ diff --git a/virtio-gpu-sw.c b/virtio-gpu-sw.c new file mode 100644 index 00000000..97b5c23a --- /dev/null +++ b/virtio-gpu-sw.c @@ -0,0 +1,662 @@ +#include +#include +#include + +#include "device.h" +#include "virtio-gpu.h" +#include "virtio.h" +#include "window.h" + +extern const struct window_backend g_window; + +static void virtio_gpu_resource_create_2d_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Read request */ + if (vq_desc[0].len < sizeof(struct vgpu_res_create_2d)) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_res_create_2d *request = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_res_create_2d)); + if (!request) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Resource ID should not be zero */ + if (request->resource_id == 0) { + fprintf(stderr, "%s(): resource id should not be 0\n", __func__); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID); + return; + } + + /* Create 2D resource */ + struct vgpu_resource_2d *res_2d = + vgpu_create_resource_2d(request->resource_id); + if (!res_2d) { + fprintf(stderr, "%s(): failed to allocate new resource\n", __func__); + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Select image formats */ + uint32_t bits_per_pixel; + + switch (request->format) { + case VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM: + case VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM: + case VIRTIO_GPU_FORMAT_A8R8G8B8_UNORM: + case VIRTIO_GPU_FORMAT_X8R8G8B8_UNORM: + case VIRTIO_GPU_FORMAT_R8G8B8A8_UNORM: + case VIRTIO_GPU_FORMAT_X8B8G8R8_UNORM: + case VIRTIO_GPU_FORMAT_A8B8G8R8_UNORM: + case VIRTIO_GPU_FORMAT_R8G8B8X8_UNORM: + bits_per_pixel = 32; + break; + default: + fprintf(stderr, "%s(): unsupported format %d\n", __func__, + request->format); + vgpu_destroy_resource_2d(request->resource_id); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER); + return; + } + + /* Set 2D resource */ + res_2d->width = request->width; + res_2d->height = request->height; + res_2d->format = request->format; + res_2d->bits_per_pixel = bits_per_pixel; + res_2d->stride = + ((request->width * bits_per_pixel + 0x1f) >> 5) * sizeof(uint32_t); + + /* Guard against integer overflow in image buffer allocation. + * Both stride and height are guest-controlled uint32_t values whose + * product can silently wrap around in 32-bit arithmetic, resulting in + * an undersized malloc while later transfers write to the full extent. */ + size_t image_size = (size_t) res_2d->stride * request->height; + if (request->height && image_size / request->height != res_2d->stride) { + fprintf(stderr, "%s(): image size overflow (%u x %u)\n", __func__, + request->width, request->height); + vgpu_destroy_resource_2d(request->resource_id); + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_OUT_OF_MEMORY); + return; + } + res_2d->image = malloc(image_size); + + /* Failed to create image buffer */ + if (!res_2d->image) { + fprintf(stderr, "%s(): Failed to allocate image buffer\n", __func__); + vgpu_destroy_resource_2d(request->resource_id); + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Write response */ + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_OK_NODATA); + + /* Handle fencing flag */ + virtio_gpu_set_response_fencing(vgpu, &request->hdr, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len); +} + +static void virtio_gpu_cmd_resource_unref_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Read request */ + if (vq_desc[0].len < sizeof(struct vgpu_res_unref)) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_res_unref *request = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_res_unref)); + if (!request) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Destroy 2D resource */ + int result = vgpu_destroy_resource_2d(request->resource_id); + if (result) { + fprintf(stderr, "%s(): failed to destroy resource %d\n", __func__, + request->resource_id); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID); + return; + } + + /* Response OK */ + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_OK_NODATA); +} + +static void virtio_gpu_cmd_set_scanout_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Read request */ + if (vq_desc[0].len < sizeof(struct vgpu_set_scanout)) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_set_scanout *request = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_set_scanout)); + if (!request) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Validate scanout ID */ + if (request->scanout_id >= VIRTIO_GPU_MAX_SCANOUTS) { + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID); + return; + } + + /* Resource ID 0 to stop displaying */ + if (request->resource_id == 0) { + g_window.window_clear(request->scanout_id); + goto leave; + } + + /* Retrieve 2D resource */ + struct vgpu_resource_2d *res_2d = + vgpu_get_resource_2d(request->resource_id); + if (!res_2d) { + fprintf(stderr, "%s(): invalid resource id %d\n", __func__, + request->resource_id); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID); + return; + } + + /* Bind scanout with resource */ + res_2d->scanout_id = request->scanout_id; + +leave: + /* Response OK */ + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_OK_NODATA); +} + +static void virtio_gpu_cmd_resource_flush_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Read request */ + if (vq_desc[0].len < sizeof(struct vgpu_res_flush)) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_res_flush *request = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_res_flush)); + if (!request) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Retrieve 2D resource */ + struct vgpu_resource_2d *res_2d = + vgpu_get_resource_2d(request->resource_id); + if (!res_2d) { + fprintf(stderr, "%s(): invalid resource id %d\n", __func__, + request->resource_id); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID); + return; + } + + /* Flush display context */ + g_window.window_flush(res_2d->scanout_id, request->resource_id); + + /* Response OK */ + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_OK_NODATA); +} + +static void virtio_gpu_copy_image_from_pages(struct vgpu_trans_to_host_2d *req, + struct vgpu_resource_2d *res_2d) +{ + uint32_t stride = res_2d->stride; + uint32_t bpp = res_2d->bits_per_pixel / 8; /* Bytes per pixel */ + uint32_t width = + (req->r.width < res_2d->width) ? req->r.width : res_2d->width; + uint32_t height = + (req->r.height < res_2d->height) ? req->r.height : res_2d->height; + void *img_data = (void *) res_2d->image; + + /* Copy image by row */ + for (uint32_t h = 0; h < height; h++) { + /* Note that source offset is in the image coordinate. The address to + * copy from is the page base address plus with the offset + */ + size_t src_offset = req->offset + stride * h; + size_t dest_offset = (req->r.y + h) * stride + (req->r.x * bpp); + void *dest = (void *) ((uintptr_t) img_data + dest_offset); + size_t total = width * bpp; + + iov_to_buf(res_2d->iovec, res_2d->page_cnt, src_offset, dest, total); + } +} + +static void virtio_gpu_cursor_image_copy(struct vgpu_resource_2d *res_2d) +{ + iov_to_buf(res_2d->iovec, res_2d->page_cnt, 0, res_2d->image, + (size_t) res_2d->stride * res_2d->height); +} + +static void virtio_gpu_cmd_transfer_to_host_2d_handler( + virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Read request */ + if (vq_desc[0].len < sizeof(struct vgpu_trans_to_host_2d)) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_trans_to_host_2d *req = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_trans_to_host_2d)); + if (!req) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Retrieve 2D resource */ + struct vgpu_resource_2d *res_2d = vgpu_get_resource_2d(req->resource_id); + if (!res_2d) { + fprintf(stderr, "%s(): invalid resource id %d\n", __func__, + req->resource_id); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID); + return; + } + + /* Check if backing has been attached */ + if (!res_2d->iovec) { + fprintf(stderr, "%s(): backing not attached for resource %d\n", + __func__, req->resource_id); + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_UNSPEC); + return; + } + + /* Check image boundary */ + if (req->r.x > res_2d->width || req->r.y > res_2d->height || + req->r.width > res_2d->width || req->r.height > res_2d->height || + req->r.x + req->r.width > res_2d->width || + req->r.y + req->r.height > res_2d->height) { + fprintf(stderr, "%s(): invalid image size\n", __func__); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER); + return; + } + + uint32_t width = + (req->r.width < res_2d->width) ? req->r.width : res_2d->width; + uint32_t height = + (req->r.height < res_2d->height) ? req->r.height : res_2d->height; + + /* Transfer frame data from guest to host. + * + * TODO: The cursor detection heuristic based on 64x64 dimensions is + * fragile. A regular resource transferring a 64x64 sub-region would + * incorrectly take the cursor fast-path. Consider tagging cursor resources + * explicitly if this becomes an issue. + */ + if (width == CURSOR_WIDTH && height == CURSOR_HEIGHT) + virtio_gpu_cursor_image_copy(res_2d); + else + virtio_gpu_copy_image_from_pages(req, res_2d); + + /* Response OK */ + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_OK_NODATA); + + /* Handle fencing flag */ + virtio_gpu_set_response_fencing(vgpu, &req->hdr, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len); +} + +static void virtio_gpu_cmd_resource_attach_backing_handler( + virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Read request */ + if (vq_desc[0].len < sizeof(struct vgpu_res_attach_backing)) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_res_attach_backing *backing_info = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_res_attach_backing)); + if (!backing_info) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + if (vq_desc[1].len < + sizeof(struct vgpu_mem_entry) * backing_info->nr_entries) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_mem_entry *pages = vgpu_mem_guest_to_host( + vgpu, vq_desc[1].addr, + sizeof(struct vgpu_mem_entry) * backing_info->nr_entries); + if (!pages) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Retrieve 2D resource */ + struct vgpu_resource_2d *res_2d = + vgpu_get_resource_2d(backing_info->resource_id); + if (!res_2d) { + fprintf(stderr, "%s(): invalid resource id %d\n", __func__, + backing_info->resource_id); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID); + return; + } + + /* Check if backing is already attached */ + if (res_2d->iovec) { + fprintf(stderr, "%s(): backing already attached for resource %d\n", + __func__, backing_info->resource_id); + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_UNSPEC); + return; + } + + /* Dispatch page memories to the 2D resource */ + res_2d->page_cnt = backing_info->nr_entries; + res_2d->iovec = malloc(sizeof(struct iovec) * backing_info->nr_entries); + if (!res_2d->iovec) { + fprintf(stderr, "%s(): failed to allocate io vector\n", __func__); + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_mem_entry *mem_entries = (struct vgpu_mem_entry *) pages; + for (size_t i = 0; i < backing_info->nr_entries; i++) { + /* Attach address and length of i-th page to the 2D resource */ + res_2d->iovec[i].iov_base = vgpu_mem_guest_to_host( + vgpu, mem_entries[i].addr, mem_entries[i].length); + res_2d->iovec[i].iov_len = mem_entries[i].length; + + /* Corrupted page address */ + if (!res_2d->iovec[i].iov_base) { + fprintf(stderr, "%s(): invalid page address\n", __func__); + free(res_2d->iovec); + res_2d->iovec = NULL; + res_2d->page_cnt = 0; + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_UNSPEC); + return; + } + } + + /* Response OK */ + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_OK_NODATA); + + /* Handle fencing flag */ + virtio_gpu_set_response_fencing(vgpu, &backing_info->hdr, + vq_desc[resp_idx].addr, + vq_desc[resp_idx].len); +} + +static void virtio_gpu_cmd_resource_detach_backing_handler( + virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Read request */ + if (vq_desc[0].len < sizeof(struct vgpu_res_detach_backing)) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_res_detach_backing *request = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_res_detach_backing)); + if (!request) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + /* Retrieve 2D resource */ + struct vgpu_resource_2d *res_2d = + vgpu_get_resource_2d(request->resource_id); + + if (!res_2d) { + fprintf(stderr, "%s(): invalid resource id %d\n", __func__, + request->resource_id); + *plen = virtio_gpu_write_response( + vgpu, vq_desc[resp_idx].addr, vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID); + return; + } + + /* Check if backing exists */ + if (!res_2d->iovec) { + fprintf(stderr, "%s(): no backing for resource %d\n", __func__, + request->resource_id); + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_ERR_UNSPEC); + return; + } + + /* Detach backing - free iovec array */ + free(res_2d->iovec); + res_2d->iovec = NULL; + res_2d->page_cnt = 0; + + /* Response OK */ + *plen = virtio_gpu_write_response(vgpu, vq_desc[resp_idx].addr, + vq_desc[resp_idx].len, + VIRTIO_GPU_RESP_OK_NODATA); +} + +static void virtio_gpu_cmd_update_cursor_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Read request */ + if (vq_desc[0].len < sizeof(struct virtio_gpu_update_cursor)) { + *plen = 0; + return; + } + + struct virtio_gpu_update_cursor *cursor = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct virtio_gpu_update_cursor)); + if (!cursor) { + *plen = 0; + return; + } + + /* Validate scanout ID */ + if (cursor->pos.scanout_id >= VIRTIO_GPU_MAX_SCANOUTS) { + *plen = 0; + return; + } + + /* Update cursor image */ + struct vgpu_resource_2d *res_2d = vgpu_get_resource_2d(cursor->resource_id); + + if (res_2d) { + g_window.cursor_update(cursor->pos.scanout_id, cursor->resource_id, + cursor->pos.x, cursor->pos.y); + } else if (cursor->resource_id == 0) { + g_window.cursor_clear(cursor->pos.scanout_id); + } else { + fprintf(stderr, "%s(): invalid resource id %d\n", __func__, + cursor->resource_id); + } + + /* Cursor commands use only 1 descriptor and do NOT have a response + * descriptor. The device should return with len=0. + */ + *plen = 0; +} + +static void virtio_gpu_cmd_move_cursor_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Read request */ + if (vq_desc[0].len < sizeof(struct virtio_gpu_update_cursor)) { + *plen = 0; + return; + } + + struct virtio_gpu_update_cursor *cursor = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct virtio_gpu_update_cursor)); + if (!cursor) { + *plen = 0; + return; + } + + /* Validate scanout ID */ + if (cursor->pos.scanout_id >= VIRTIO_GPU_MAX_SCANOUTS) { + *plen = 0; + return; + } + + /* Move cursor to new position */ + g_window.cursor_move(cursor->pos.scanout_id, cursor->pos.x, cursor->pos.y); + + /* Cursor commands use only 1 descriptor and do NOT have a response + * descriptor. The device should return with len=0. + */ + *plen = 0; +} + +const struct vgpu_cmd_backend g_vgpu_backend = { + .get_display_info = virtio_gpu_get_display_info_handler, + .resource_create_2d = virtio_gpu_resource_create_2d_handler, + .resource_unref = virtio_gpu_cmd_resource_unref_handler, + .set_scanout = virtio_gpu_cmd_set_scanout_handler, + .resource_flush = virtio_gpu_cmd_resource_flush_handler, + .transfer_to_host_2d = virtio_gpu_cmd_transfer_to_host_2d_handler, + .resource_attach_backing = virtio_gpu_cmd_resource_attach_backing_handler, + .resource_detach_backing = virtio_gpu_cmd_resource_detach_backing_handler, + .get_capset_info = VGPU_CMD_UNDEF, + .get_capset = VGPU_CMD_UNDEF, + .get_edid = virtio_gpu_get_edid_handler, + .resource_assign_uuid = VGPU_CMD_UNDEF, + .resource_create_blob = VGPU_CMD_UNDEF, + .set_scanout_blob = VGPU_CMD_UNDEF, + .ctx_create = VGPU_CMD_UNDEF, + .ctx_destroy = VGPU_CMD_UNDEF, + .ctx_attach_resource = VGPU_CMD_UNDEF, + .ctx_detach_resource = VGPU_CMD_UNDEF, + .resource_create_3d = VGPU_CMD_UNDEF, + .transfer_to_host_3d = VGPU_CMD_UNDEF, + .transfer_from_host_3d = VGPU_CMD_UNDEF, + .submit_3d = VGPU_CMD_UNDEF, + .resource_map_blob = VGPU_CMD_UNDEF, + .resource_unmap_blob = VGPU_CMD_UNDEF, + .update_cursor = virtio_gpu_cmd_update_cursor_handler, + .move_cursor = virtio_gpu_cmd_move_cursor_handler, +}; diff --git a/virtio-gpu.c b/virtio-gpu.c new file mode 100644 index 00000000..555e49d2 --- /dev/null +++ b/virtio-gpu.c @@ -0,0 +1,828 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "device.h" +#include "riscv.h" +#include "riscv_private.h" +#include "utils.h" +#include "virtio-gpu.h" +#include "virtio.h" +#include "window.h" + +#define VGPU_CMD_TRACE_ENABLED 0 + +#define VIRTIO_F_VERSION_1 1 + +#define VIRTIO_GPU_EVENT_DISPLAY (1 << 0) +#define VIRTIO_GPU_F_EDID (1 << 1) +#define VIRTIO_GPU_F_CONTEXT_INIT (1 << 4) +#define VIRTIO_GPU_FLAG_FENCE (1 << 0) + +#define VGPU_QUEUE_NUM_MAX 1024 +#define VGPU_QUEUE (vgpu->queues[vgpu->QueueSel]) + +#define PRIV(x) ((virtio_gpu_data_t *) x->priv) + +#if VGPU_CMD_TRACE_ENABLED +#define VGPU_CMD(cmd, fn) \ + case VIRTIO_GPU_CMD_##cmd: \ + printf("(*) semu/virtio-gpu: %s\n", "VIRTIO_GPU_CMD_" #cmd); \ + g_vgpu_backend.fn(vgpu, vq_desc, plen); \ + break; +#else +#define VGPU_CMD(cmd, fn) \ + case VIRTIO_GPU_CMD_##cmd: \ + g_vgpu_backend.fn(vgpu, vq_desc, plen); \ + break; +#endif + +extern const struct vgpu_cmd_backend g_vgpu_backend; +extern const struct window_backend g_window; + +static virtio_gpu_data_t virtio_gpu_data; +static struct vgpu_config vgpu_configs; +static LIST_HEAD(vgpu_res_2d_list); + +size_t iov_to_buf(const struct iovec *iov, + const unsigned int iov_cnt, + size_t offset, + void *buf, + size_t bytes) +{ + size_t done = 0; + + for (unsigned int i = 0; i < iov_cnt; i++) { + /* Skip empty pages */ + if (iov[i].iov_base == 0 || iov[i].iov_len == 0) + continue; + + if (offset < iov[i].iov_len) { + /* Take as much as data of current page can provide */ + size_t remained = bytes - done; + size_t page_avail = iov[i].iov_len - offset; + size_t len = (remained < page_avail) ? remained : page_avail; + + /* Copy to buffer */ + void *src = (void *) ((uintptr_t) iov[i].iov_base + offset); + void *dest = (void *) ((uintptr_t) buf + done); + memcpy(dest, src, len); + + /* If there is still data left to read, but current page is + * exhausted, we need to read from the beginning of the next + * page, where its offset should be 0 */ + offset = 0; + + /* Count the total received bytes so far */ + done += len; + + /* Data transfering of current scanline is complete */ + if (done >= bytes) + break; + } else { + offset -= iov[i].iov_len; + } + } + + return done; +} + +struct vgpu_resource_2d *vgpu_create_resource_2d(int resource_id) +{ + struct vgpu_resource_2d *res = calloc(1, sizeof(struct vgpu_resource_2d)); + if (!res) + return NULL; + res->resource_id = resource_id; + list_push(&res->list, &vgpu_res_2d_list); + return res; +} + +struct vgpu_resource_2d *vgpu_get_resource_2d(uint32_t resource_id) +{ + struct vgpu_resource_2d *res_2d; + list_for_each_entry (res_2d, &vgpu_res_2d_list, list) { + if (res_2d->resource_id == resource_id) + return res_2d; + } + + return NULL; +} + +int vgpu_destroy_resource_2d(uint32_t resource_id) +{ + struct vgpu_resource_2d *res_2d = vgpu_get_resource_2d(resource_id); + + /* Failed to find the resource */ + if (!res_2d) + return -1; + + /* Release the resource */ + free(res_2d->image); + list_del(&res_2d->list); + free(res_2d->iovec); + free(res_2d); + + return 0; +} + +void *vgpu_mem_guest_to_host(virtio_gpu_state_t *vgpu, + uint32_t addr, + uint32_t size) +{ + if (addr >= RAM_SIZE || size > RAM_SIZE || addr + size > RAM_SIZE) { + fprintf(stderr, + "virtio-gpu: guest address 0x%x size 0x%x out of bounds\n", + addr, size); + return NULL; + } + return (void *) ((uintptr_t) vgpu->ram + addr); +} + +uint32_t virtio_gpu_write_response(virtio_gpu_state_t *vgpu, + uint64_t addr, + uint32_t desc_len, + uint32_t type) +{ + if (desc_len < sizeof(struct vgpu_ctrl_hdr)) + return 0; + + struct vgpu_ctrl_hdr *response = + vgpu_mem_guest_to_host(vgpu, addr, sizeof(struct vgpu_ctrl_hdr)); + if (!response) + return 0; + + memset(response, 0, sizeof(*response)); + response->type = type; + + return sizeof(*response); +} + +void virtio_gpu_set_fail(virtio_gpu_state_t *vgpu) +{ + vgpu->Status |= VIRTIO_STATUS__DEVICE_NEEDS_RESET; + if (vgpu->Status & VIRTIO_STATUS__DRIVER_OK) + vgpu->InterruptStatus |= VIRTIO_INT__CONF_CHANGE; +} + +/* Find the response descriptor index (first descriptor with WRITE flag). + * Returns -1 if no writable descriptor is found. + */ +int virtio_gpu_find_response_desc(struct virtq_desc *vq_desc, int max_desc) +{ + for (int i = 0; i < max_desc; i++) { + if (vq_desc[i].flags & VIRTIO_DESC_F_WRITE) + return i; + } + return -1; +} + +static inline uint32_t vgpu_preprocess(virtio_gpu_state_t *vgpu, uint32_t addr) +{ + if ((addr >= RAM_SIZE) || (addr & 0b11)) + return virtio_gpu_set_fail(vgpu), 0; + + return addr >> 2; +} + +static void virtio_gpu_update_status(virtio_gpu_state_t *vgpu, uint32_t status) +{ + vgpu->Status |= status; + if (status) + return; + + /* Reset VirtIO device state (feature negotiation, queue descriptors, + * avail/used rings, status and interrupt registers). 'ram' and 'priv' are + * infrastructure pointers provided by the host, not device state, so + * they are saved and restored across the memset. + * + * 'vgpu->priv' (virtio_gpu_data_t) is intentionally NOT reset here. + * It holds only host-configured scanout info (display dimensions / + * enabled flags) set up before the guest driver probes the device. + * The guest re-queries this via CMD_GET_DISPLAY_INFO after each + * reset, so it must survive. Runtime state (resource-to-scanout + * mapping) lives in vgpu_res_2d_list and is freed below. + * + * If runtime per-scanout state is ever added to virtio_gpu_data_t, + * revisit this. + */ + uint32_t *ram = vgpu->ram; + void *priv = vgpu->priv; + memset(vgpu, 0, sizeof(*vgpu)); + vgpu->ram = ram; + vgpu->priv = priv; + + /* Release all 2D resources */ + struct list_head *curr, *next; + list_for_each_safe (curr, next, &vgpu_res_2d_list) { + struct vgpu_resource_2d *res_2d = + list_entry(curr, struct vgpu_resource_2d, list); + + list_del(&res_2d->list); + free(res_2d->image); + free(res_2d->iovec); + free(res_2d); + } +} + + +void virtio_gpu_set_response_fencing(virtio_gpu_state_t *vgpu, + struct vgpu_ctrl_hdr *request, + uint64_t addr, + uint32_t desc_len) +{ + if (desc_len < sizeof(struct vgpu_ctrl_hdr)) + return; + + /* Guest is riscv32, upper 32 bits of addr are always 0. */ + struct vgpu_ctrl_hdr *response = vgpu_mem_guest_to_host( + vgpu, (uint32_t) addr, sizeof(struct vgpu_ctrl_hdr)); + if (!response) + return; + + if (request->flags & VIRTIO_GPU_FLAG_FENCE) { + response->flags = VIRTIO_GPU_FLAG_FENCE; + response->fence_id = request->fence_id; + } +} + +void virtio_gpu_get_display_info_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Write display information */ + if (vq_desc[resp_idx].len < sizeof(struct vgpu_resp_disp_info)) { + *plen = 0; + return; + } + + struct vgpu_resp_disp_info *response = vgpu_mem_guest_to_host( + vgpu, vq_desc[resp_idx].addr, sizeof(struct vgpu_resp_disp_info)); + if (!response) { + *plen = 0; + return; + } + + memset(response, 0, sizeof(*response)); + response->hdr.type = VIRTIO_GPU_RESP_OK_DISPLAY_INFO; + + int scanout_num = vgpu_configs.num_scanouts; + for (int i = 0; i < scanout_num; i++) { + response->pmodes[i].r.width = PRIV(vgpu)->scanouts[i].width; + response->pmodes[i].r.height = PRIV(vgpu)->scanouts[i].height; + response->pmodes[i].enabled = PRIV(vgpu)->scanouts[i].enabled; + } + + /* Update write length */ + *plen = sizeof(*response); +} + +static uint8_t virtio_gpu_generate_edid_checksum(uint8_t *edid, size_t size) +{ + uint8_t sum = 0; + + for (size_t i = 0; i < size; i++) + sum += edid[i]; + + return 0x100 - sum; +} + +static void virtio_gpu_generate_edid(uint8_t *edid, int width_cm, int height_cm) +{ + /* Check: + * "VESA ENHANCED EXTENDED DISPLAY IDENTIFICATION DATA STANDARD" + * (Defines EDID Structure Version 1, Revision 4) + */ + + memset(edid, 0, 128); + + /* EDID header */ + edid[0] = 0x00; + edid[1] = 0xff; + edid[2] = 0xff; + edid[3] = 0xff; + edid[4] = 0xff; + edid[5] = 0xff; + edid[6] = 0xff; + edid[7] = 0x00; + + /* ISA (Industry Standard Architecture) + * Plug and Play Device Identifier (PNPID) */ + char manufacture[3] = {'T', 'W', 'N'}; + + /* Vendor ID uses 2 bytes to store 3 characters, where 'A' starts as 1 */ + uint16_t vendor_id = ((((manufacture[0] - '@') & 0b11111) << 10) | + (((manufacture[1] - '@') & 0b11111) << 5) | + (((manufacture[2] - '@') & 0b11111) << 0)); + /* Convert vendor ID to big-endian order */ + edid[8] = vendor_id >> 8; + edid[9] = vendor_id & 0xff; + + /* Product code (all zeros if unused) */ + memset(&edid[10], 0, 6); + + /* Week of manufacture (1-54) */ + edid[16] = 0; + /* Year of manufacture (starts from 1990) */ + edid[17] = 2023 - 1990; + + /* EDID 1.4 (Version 1, Revision 4) */ + edid[18] = 1; /* Version number */ + edid[19] = 4; /* Revision number */ + + /* Video input definition */ + uint8_t signal_interface = 0b1 << 7; /* digital */ + uint8_t color_bit_depth = 0b010 << 4; /* 8 bits per primary color */ + uint8_t interface_type = 0b101; /* DisplayPort is supported */ + edid[20] = signal_interface | color_bit_depth | interface_type; + + /* Screen size or aspect ratio */ + edid[21] = width_cm; /* Horizontal screen size (1cm - 255cm) */ + edid[22] = height_cm; /* Vertical screen size (1cm - 255cm) */ + + /* Gamma value */ + edid[23] = 1; /* Assigned with the minimum value */ + + /* Feature support */ + uint8_t power_management = 0 << 4; /* standby, suspend and active-off + * modes are not supported */ + uint8_t color_type = 0 << 2; /* ignored as it is for the analog display */ + uint8_t other_flags = 0b110; /* [2]: sRGB as default color space + * [1]: Prefered timing mode with native format + * [0]: Non-continuys frequency */ + edid[24] = power_management | color_type | other_flags; + + /* Established timmings: These are the default timmings defined by the + * VESA. Each bit represents 1 configuration. For now, we enable the + * timming configurations of 1024x768@60Hz only */ + edid[35] = 0b00000000; + edid[36] = 0b00001000; + edid[37] = 0b00000000; + + /* Standard timmings: 16 bytes data start from edid[38] to edid[54] as + * additional timming configurations with 2 bytes for each to define + * the horizontal pixel number, aspect ratio, and refresh rate. */ + + /* Extension block count number */ + edid[126] = 0; /* No other extension blocks are defined */ + + /* Checksum of the first (and the only) extension block */ + edid[127] = virtio_gpu_generate_edid_checksum(edid, 127); +} + +void virtio_gpu_get_edid_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + /* Check if there's a writable response descriptor */ + int resp_idx = virtio_gpu_find_response_desc(vq_desc, 3); + if (resp_idx < 0) { + *plen = 0; + return; + } + + /* Generate the display EDID */ + struct vgpu_resp_edid edid = { + .hdr = {.type = VIRTIO_GPU_RESP_OK_EDID}, + .size = 128 /* One EDID extension block only */ + }; + virtio_gpu_generate_edid((uint8_t *) edid.edid, 0, 0); + + /* Write EDID response */ + if (vq_desc[resp_idx].len < sizeof(struct vgpu_resp_edid)) { + *plen = 0; + return; + } + + struct vgpu_resp_edid *response = vgpu_mem_guest_to_host( + vgpu, vq_desc[resp_idx].addr, sizeof(struct vgpu_resp_edid)); + if (!response) { + *plen = 0; + return; + } + + memcpy(response, &edid, sizeof(struct vgpu_resp_edid)); + + /* return write length */ + *plen = sizeof(*response); +} + +void virtio_gpu_cmd_undefined_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen) +{ + if (vq_desc[0].len < sizeof(struct vgpu_ctrl_hdr)) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + struct vgpu_ctrl_hdr *header = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_ctrl_hdr)); + if (!header) { + virtio_gpu_set_fail(vgpu); + *plen = 0; + return; + } + + fprintf(stderr, "%s(): unsupported VirtIO-GPU command %d.", __func__, + header->type); + + virtio_gpu_set_fail(vgpu); + *plen = 0; +} + +static int virtio_gpu_desc_handler(virtio_gpu_state_t *vgpu, + const virtio_gpu_queue_t *queue, + uint32_t desc_idx, + uint32_t *plen) +{ + /* virtio-gpu uses 3 virtqueue descriptors at most */ + struct virtq_desc vq_desc[3] = {0}; + + /* Collect descriptors */ + for (int i = 0; i < 3; i++) { + if (desc_idx >= queue->QueueNum) { + virtio_gpu_set_fail(vgpu); + return -1; + } + + /* The size of the `struct virtq_desc` is 4 words */ + uint32_t desc_offset = queue->QueueDesc + desc_idx * 4; + if (desc_offset + 3 >= RAM_SIZE / 4) { + virtio_gpu_set_fail(vgpu); + return -1; + } + uint32_t *desc = &vgpu->ram[desc_offset]; + + /* Retrieve the fields of current descriptor */ + vq_desc[i].addr = desc[0]; + vq_desc[i].len = desc[2]; + vq_desc[i].flags = desc[3]; + desc_idx = desc[3] >> 16; /* vq_desc[desc_cnt].next */ + + /* Leave the loop if next-flag is not set */ + if (!(vq_desc[i].flags & VIRTIO_DESC_F_NEXT)) + break; + } + + /* Process the header */ + if (vq_desc[0].len < sizeof(struct vgpu_ctrl_hdr)) { + virtio_gpu_set_fail(vgpu); + return -1; + } + + struct vgpu_ctrl_hdr *header = vgpu_mem_guest_to_host( + vgpu, vq_desc[0].addr, sizeof(struct vgpu_ctrl_hdr)); + if (!header) { + virtio_gpu_set_fail(vgpu); + return -1; + } + + /* Process the command */ + switch (header->type) { + /* 2D commands */ + VGPU_CMD(GET_DISPLAY_INFO, get_display_info) + VGPU_CMD(RESOURCE_CREATE_2D, resource_create_2d) + VGPU_CMD(RESOURCE_UNREF, resource_unref) + VGPU_CMD(SET_SCANOUT, set_scanout) + VGPU_CMD(RESOURCE_FLUSH, resource_flush) + VGPU_CMD(TRANSFER_TO_HOST_2D, transfer_to_host_2d) + VGPU_CMD(RESOURCE_ATTACH_BACKING, resource_attach_backing) + VGPU_CMD(RESOURCE_DETACH_BACKING, resource_detach_backing) + VGPU_CMD(GET_CAPSET_INFO, get_capset_info) + VGPU_CMD(GET_CAPSET, get_capset) + VGPU_CMD(GET_EDID, get_edid) + VGPU_CMD(RESOURCE_ASSIGN_UUID, resource_assign_uuid) + VGPU_CMD(RESOURCE_CREATE_BLOB, resource_create_blob) + VGPU_CMD(SET_SCANOUT_BLOB, set_scanout_blob) + /* 3D commands */ + VGPU_CMD(CTX_CREATE, ctx_create) + VGPU_CMD(CTX_DESTROY, ctx_destroy) + VGPU_CMD(CTX_ATTACH_RESOURCE, ctx_attach_resource) + VGPU_CMD(CTX_DETACH_RESOURCE, ctx_detach_resource) + VGPU_CMD(RESOURCE_CREATE_3D, resource_create_3d) + VGPU_CMD(TRANSFER_TO_HOST_3D, transfer_to_host_3d) + VGPU_CMD(TRANSFER_FROM_HOST_3D, transfer_from_host_3d) + VGPU_CMD(SUBMIT_3D, submit_3d) + VGPU_CMD(RESOURCE_MAP_BLOB, resource_map_blob) + VGPU_CMD(RESOURCE_UNMAP_BLOB, resource_unmap_blob) + VGPU_CMD(UPDATE_CURSOR, update_cursor) + VGPU_CMD(MOVE_CURSOR, move_cursor) + default: + virtio_gpu_cmd_undefined_handler(vgpu, vq_desc, plen); + return -1; + } + + return 0; +} + +static void virtio_queue_notify_handler(virtio_gpu_state_t *vgpu, int index) +{ + uint32_t *ram = vgpu->ram; + virtio_gpu_queue_t *queue = &vgpu->queues[index]; + if (vgpu->Status & VIRTIO_STATUS__DEVICE_NEEDS_RESET) + return; + + if (!((vgpu->Status & VIRTIO_STATUS__DRIVER_OK) && queue->ready)) + return virtio_gpu_set_fail(vgpu); + + /* Check for new buffers */ + uint16_t new_avail = ram[queue->QueueAvail] >> 16; + if (new_avail - queue->last_avail > (uint16_t) queue->QueueNum) + return (fprintf(stderr, "%s(): size check failed\n", __func__), + virtio_gpu_set_fail(vgpu)); + + if (queue->last_avail == new_avail) + return; + + /* Process them */ + uint16_t new_used = ram[queue->QueueUsed] >> 16; /* virtq_used.idx (le16) */ + while (queue->last_avail != new_avail) { + /* Obtain the index in the ring buffer */ + uint16_t queue_idx = queue->last_avail % queue->QueueNum; + + /* Since each buffer index occupies 2 bytes but the memory is aligned + * with 4 bytes, and the first element of the available queue is stored + * at ram[queue->QueueAvail + 1], to acquire the buffer index, it + * requires the following array index calculation and bit shifting. + * Check also the `struct virtq_avail` on the spec. + */ + uint16_t buffer_idx = ram[queue->QueueAvail + 1 + queue_idx / 2] >> + (16 * (queue_idx % 2)); + + /* Consume request from the available queue and process the data in the + * descriptor list. + */ + uint32_t len = 0; + int result = virtio_gpu_desc_handler(vgpu, queue, buffer_idx, &len); + if (result != 0) + return; + + /* Write used element information (`struct virtq_used_elem`) to the used + * queue */ + uint32_t vq_used_addr = + queue->QueueUsed + 1 + (new_used % queue->QueueNum) * 2; + ram[vq_used_addr] = buffer_idx; /* virtq_used_elem.id (le32) */ + ram[vq_used_addr + 1] = len; /* virtq_used_elem.len (le32) */ + queue->last_avail++; + new_used++; + } + + /* Update virtq_used.idx (keep `virtq_used.flags` in low 16 bits) */ + vgpu->ram[queue->QueueUsed] &= MASK(16); /* clear high 16 bits (idx) */ + vgpu->ram[queue->QueueUsed] |= ((uint32_t) new_used) << 16; /* set idx */ + + /* Send interrupt, unless VIRTQ_AVAIL_F_NO_INTERRUPT is set */ + if (!(ram[queue->QueueAvail] & 1)) + vgpu->InterruptStatus |= VIRTIO_INT__USED_RING; +} + +static bool virtio_gpu_reg_read(virtio_gpu_state_t *vgpu, + uint32_t addr, + uint32_t *value) +{ +#define _(reg) VIRTIO_##reg + switch (addr) { + case _(MagicValue): + *value = 0x74726976; + return true; + case _(Version): + *value = 2; + return true; + case _(DeviceID): + *value = 16; + return true; + case _(VendorID): + *value = VIRTIO_VENDOR_ID; + return true; + case _(DeviceFeatures): + if (vgpu->DeviceFeaturesSel) { /* [63:32] */ + *value = VIRTIO_F_VERSION_1; + } else { /* [31:0] */ + *value = VIRTIO_GPU_F_EDID; + } + return true; + case _(QueueNumMax): + *value = VGPU_QUEUE_NUM_MAX; + return true; + case _(QueueReady): + *value = VGPU_QUEUE.ready ? 1 : 0; + return true; + case _(InterruptStatus): + *value = vgpu->InterruptStatus; + return true; + case _(Status): + *value = vgpu->Status; + return true; + case _(SHMLenLow): + case _(SHMLenHigh): + /* shared memory is unimplemented */ + *value = -1; + return true; + case _(SHMBaseLow): + *value = 0; + return true; + case _(SHMBaseHigh): + *value = 0; + return true; + case _(ConfigGeneration): + *value = 0; + return true; + default: + /* Invalid address which exceeded the range */ + if (!RANGE_CHECK(addr, _(Config), sizeof(struct vgpu_config))) + return false; + + /* Read configuration from the corresponding register */ + uint32_t offset = (addr - _(Config)) << 2; + switch (offset) { + case offsetof(struct vgpu_config, events_read): { + *value = 0; /* No event is implemented currently */ + return true; + } + case offsetof(struct vgpu_config, num_scanouts): { + *value = vgpu_configs.num_scanouts; + return true; + } + case offsetof(struct vgpu_config, num_capsets): { + *value = 0; + return true; + } + default: + return false; + } + } +#undef _ +} + +static bool virtio_gpu_reg_write(virtio_gpu_state_t *vgpu, + uint32_t addr, + uint32_t value) +{ +#define _(reg) VIRTIO_##reg + switch (addr) { + case _(DeviceFeaturesSel): + vgpu->DeviceFeaturesSel = value; + return true; + case _(DriverFeatures): + if (vgpu->DriverFeaturesSel == 0) + vgpu->DriverFeatures = value; + return true; + case _(DriverFeaturesSel): + vgpu->DriverFeaturesSel = value; + return true; + case _(QueueSel): + if (value < ARRAY_SIZE(vgpu->queues)) + vgpu->QueueSel = value; + else + virtio_gpu_set_fail(vgpu); + return true; + case _(QueueNum): + if (value > 0 && value <= VGPU_QUEUE_NUM_MAX) + VGPU_QUEUE.QueueNum = value; + else + virtio_gpu_set_fail(vgpu); + return true; + case _(QueueReady): + VGPU_QUEUE.ready = value & 1; + if (value & 1) + VGPU_QUEUE.last_avail = vgpu->ram[VGPU_QUEUE.QueueAvail] >> 16; + return true; + case _(QueueDescLow): + VGPU_QUEUE.QueueDesc = vgpu_preprocess(vgpu, value); + return true; + case _(QueueDescHigh): + if (value) + virtio_gpu_set_fail(vgpu); + return true; + case _(QueueDriverLow): + VGPU_QUEUE.QueueAvail = vgpu_preprocess(vgpu, value); + return true; + case _(QueueDriverHigh): + if (value) + virtio_gpu_set_fail(vgpu); + return true; + case _(QueueDeviceLow): + VGPU_QUEUE.QueueUsed = vgpu_preprocess(vgpu, value); + return true; + case _(QueueDeviceHigh): + if (value) + virtio_gpu_set_fail(vgpu); + return true; + case _(QueueNotify): + if (value < ARRAY_SIZE(vgpu->queues)) + virtio_queue_notify_handler(vgpu, value); + else + virtio_gpu_set_fail(vgpu); + return true; + case _(InterruptACK): + vgpu->InterruptStatus &= ~value; + return true; + case _(Status): + virtio_gpu_update_status(vgpu, value); + return true; + case _(SHMSel): + return true; + default: + /* Invalid address which exceeded the range */ + if (!RANGE_CHECK(addr, _(Config), sizeof(struct vgpu_config))) + return false; + + /* Write configuration to the corresponding register */ + uint32_t offset = (addr - _(Config)) << 2; + switch (offset) { + case offsetof(struct vgpu_config, events_clear): { + /* Ignored, no event is implemented currently */ + return true; + } + default: + return false; + } + } +#undef _ +} + +void virtio_gpu_read(hart_t *vm, + virtio_gpu_state_t *vgpu, + uint32_t addr, + uint8_t width, + uint32_t *value) +{ + switch (width) { + case RV_MEM_LW: + if (!virtio_gpu_reg_read(vgpu, addr >> 2, value)) + vm_set_exception(vm, RV_EXC_LOAD_FAULT, vm->exc_val); + break; + case RV_MEM_LBU: + case RV_MEM_LB: + case RV_MEM_LHU: + case RV_MEM_LH: + vm_set_exception(vm, RV_EXC_LOAD_MISALIGN, vm->exc_val); + return; + default: + vm_set_exception(vm, RV_EXC_ILLEGAL_INSN, 0); + return; + } +} + +void virtio_gpu_write(hart_t *vm, + virtio_gpu_state_t *vgpu, + uint32_t addr, + uint8_t width, + uint32_t value) +{ + switch (width) { + case RV_MEM_SW: + if (!virtio_gpu_reg_write(vgpu, addr >> 2, value)) + vm_set_exception(vm, RV_EXC_STORE_FAULT, vm->exc_val); + break; + case RV_MEM_SB: + case RV_MEM_SH: + vm_set_exception(vm, RV_EXC_STORE_MISALIGN, vm->exc_val); + return; + default: + vm_set_exception(vm, RV_EXC_ILLEGAL_INSN, 0); + return; + } +} + +void virtio_gpu_init(virtio_gpu_state_t *vgpu) +{ + vgpu->priv = &virtio_gpu_data; +} + +void virtio_gpu_add_scanout(virtio_gpu_state_t *vgpu, + uint32_t width, + uint32_t height) +{ + int scanout_num = vgpu_configs.num_scanouts; + + if (scanout_num >= VIRTIO_GPU_MAX_SCANOUTS) { + fprintf(stderr, "%s(): exceeded scanout maximum number\n", __func__); + exit(2); + } + + PRIV(vgpu)->scanouts[scanout_num].width = width; + PRIV(vgpu)->scanouts[scanout_num].height = height; + PRIV(vgpu)->scanouts[scanout_num].enabled = 1; + + g_window.window_add(width, height); + + vgpu_configs.num_scanouts++; +} diff --git a/virtio-gpu.h b/virtio-gpu.h new file mode 100644 index 00000000..02e38abf --- /dev/null +++ b/virtio-gpu.h @@ -0,0 +1,362 @@ +#pragma once + +#if SEMU_HAS(VIRTIOGPU) + +#include "virtio.h" + +#define VIRTIO_GPU_MAX_SCANOUTS 16 +#define VGPU_CMD_UNDEF virtio_gpu_cmd_undefined_handler + +struct vgpu_scanout_info { + uint32_t width; + uint32_t height; + uint32_t enabled; +}; + +typedef struct { + struct vgpu_scanout_info scanouts[VIRTIO_GPU_MAX_SCANOUTS]; +} virtio_gpu_data_t; + +struct vgpu_resource_2d { + uint32_t scanout_id; + uint32_t resource_id; + uint32_t format; + uint32_t width; + uint32_t height; + uint32_t stride; + uint32_t bits_per_pixel; + uint32_t *image; + size_t page_cnt; + struct iovec *iovec; + struct list_head list; +}; + +PACKED(struct vgpu_config { + uint32_t events_read; + uint32_t events_clear; + uint32_t num_scanouts; + uint32_t num_capsets; +}); + +PACKED(struct vgpu_ctrl_hdr { + uint32_t type; + uint32_t flags; + uint64_t fence_id; + uint32_t ctx_id; + uint8_t ring_idx; + uint8_t padding[3]; +}); + +PACKED(struct vgpu_rect { + uint32_t x; + uint32_t y; + uint32_t width; + uint32_t height; +}); + +PACKED(struct vgpu_resp_disp_info { + struct vgpu_ctrl_hdr hdr; + struct virtio_gpu_display_one { + struct vgpu_rect r; + uint32_t enabled; + uint32_t flags; + } pmodes[VIRTIO_GPU_MAX_SCANOUTS]; +}); + +PACKED(struct vgpu_res_create_2d { + struct vgpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t format; + uint32_t width; + uint32_t height; +}); + +PACKED(struct vgpu_res_unref { + struct vgpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t padding; +}); + +PACKED(struct vgpu_set_scanout { + struct vgpu_ctrl_hdr hdr; + struct vgpu_rect r; + uint32_t scanout_id; + uint32_t resource_id; +}); + +PACKED(struct vgpu_res_flush { + struct vgpu_ctrl_hdr hdr; + struct vgpu_rect r; + uint32_t resource_id; + uint32_t padding; +}); + +PACKED(struct vgpu_trans_to_host_2d { + struct vgpu_ctrl_hdr hdr; + struct vgpu_rect r; + uint64_t offset; + uint32_t resource_id; + uint32_t padding; +}); + +PACKED(struct vgpu_res_attach_backing { + struct vgpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t nr_entries; +}); + +PACKED(struct vgpu_res_detach_backing { + struct vgpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t padding; +}); + +PACKED(struct vgpu_mem_entry { + uint64_t addr; + uint32_t length; + uint32_t padding; +}); + +PACKED(struct vgpu_resp_edid { + struct vgpu_ctrl_hdr hdr; + uint32_t size; + uint32_t padding; + char edid[1024]; +}); + +PACKED(struct vgpu_get_capset_info { + struct vgpu_ctrl_hdr hdr; + uint32_t capset_index; + uint32_t padding; +}); + +PACKED(struct vgpu_resp_capset_info { + struct vgpu_ctrl_hdr hdr; + uint32_t capset_id; + uint32_t capset_max_version; + uint32_t capset_max_size; + uint32_t padding; +}); + +PACKED(struct vgpu_get_capset { + struct vgpu_ctrl_hdr hdr; + uint32_t capset_id; + uint32_t capset_version; +}); + +PACKED(struct virtio_gpu_resp_capset { + struct vgpu_ctrl_hdr hdr; + uint8_t capset_data[]; +}); + +PACKED(struct virtio_gpu_ctx_create { + struct vgpu_ctrl_hdr hdr; + uint32_t nlen; + uint32_t context_init; + char debug_name[64]; +}); + +PACKED(struct virtio_gpu_cursor_pos { + uint32_t scanout_id; + uint32_t x; + uint32_t y; + uint32_t padding; +}); + +PACKED(struct virtio_gpu_update_cursor { + struct vgpu_ctrl_hdr hdr; + struct virtio_gpu_cursor_pos pos; + uint32_t resource_id; + uint32_t hot_x; + uint32_t hot_y; + uint32_t padding; +}); + +/* clang-format off */ +PACKED(struct virtio_gpu_ctx_destroy { + struct vgpu_ctrl_hdr hdr; +}); +/* clang-format on */ + +PACKED(struct virtio_gpu_resource_create_3d { + struct vgpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t target; + uint32_t format; + uint32_t bind; + uint32_t width; + uint32_t height; + uint32_t depth; + uint32_t array_size; + uint32_t last_level; + uint32_t nr_samples; + uint32_t flags; + uint32_t padding; +}); + +PACKED(struct virtio_gpu_ctx_resource { + struct vgpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t padding; +}); + +PACKED(struct virtio_gpu_box { + uint32_t x; + uint32_t y; + uint32_t z; + uint32_t w; + uint32_t h; + uint32_t d; +}); + +PACKED(struct virtio_gpu_transfer_host_3d { + struct vgpu_ctrl_hdr hdr; + struct virtio_gpu_box box; + uint64_t offset; + uint32_t resource_id; + uint32_t level; + uint32_t stride; + uint32_t layer_stride; +}); + +PACKED(struct virtio_gpu_cmd_submit { + struct vgpu_ctrl_hdr hdr; + uint32_t size; + uint32_t num_in_fences; +}); + +PACKED(struct virtio_gpu_resp_map_info { + struct vgpu_ctrl_hdr hdr; + uint32_t map_info; + uint32_t padding; +}); + +enum virtio_gpu_ctrl_type { + /* 2D commands */ + VIRTIO_GPU_CMD_GET_DISPLAY_INFO = 0x0100, + VIRTIO_GPU_CMD_RESOURCE_CREATE_2D, + VIRTIO_GPU_CMD_RESOURCE_UNREF, + VIRTIO_GPU_CMD_SET_SCANOUT, + VIRTIO_GPU_CMD_RESOURCE_FLUSH, + VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D, + VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING, + VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING, + VIRTIO_GPU_CMD_GET_CAPSET_INFO, + VIRTIO_GPU_CMD_GET_CAPSET, + VIRTIO_GPU_CMD_GET_EDID, + VIRTIO_GPU_CMD_RESOURCE_ASSIGN_UUID, + VIRTIO_GPU_CMD_RESOURCE_CREATE_BLOB, + VIRTIO_GPU_CMD_SET_SCANOUT_BLOB, + + /* 3D commands */ + VIRTIO_GPU_CMD_CTX_CREATE = 0x0200, + VIRTIO_GPU_CMD_CTX_DESTROY, + VIRTIO_GPU_CMD_CTX_ATTACH_RESOURCE, + VIRTIO_GPU_CMD_CTX_DETACH_RESOURCE, + VIRTIO_GPU_CMD_RESOURCE_CREATE_3D, + VIRTIO_GPU_CMD_TRANSFER_TO_HOST_3D, + VIRTIO_GPU_CMD_TRANSFER_FROM_HOST_3D, + VIRTIO_GPU_CMD_SUBMIT_3D, + VIRTIO_GPU_CMD_RESOURCE_MAP_BLOB, + VIRTIO_GPU_CMD_RESOURCE_UNMAP_BLOB, + + /* Cursor commands */ + VIRTIO_GPU_CMD_UPDATE_CURSOR = 0x0300, + VIRTIO_GPU_CMD_MOVE_CURSOR, + + /* Success responses */ + VIRTIO_GPU_RESP_OK_NODATA = 0x1100, + VIRTIO_GPU_RESP_OK_DISPLAY_INFO, + VIRTIO_GPU_RESP_OK_CAPSET_INFO, + VIRTIO_GPU_RESP_OK_CAPSET, + VIRTIO_GPU_RESP_OK_EDID, + + /* Error responses */ + VIRTIO_GPU_RESP_ERR_UNSPEC = 0x1200, + VIRTIO_GPU_RESP_ERR_OUT_OF_MEMORY, + VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID, + VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID, + VIRTIO_GPU_RESP_ERR_INVALID_CONTEXT_ID, + VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER, +}; + +enum virtio_gpu_formats { + VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM = 1, + VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM = 2, + VIRTIO_GPU_FORMAT_A8R8G8B8_UNORM = 3, + VIRTIO_GPU_FORMAT_X8R8G8B8_UNORM = 4, + VIRTIO_GPU_FORMAT_R8G8B8A8_UNORM = 67, + VIRTIO_GPU_FORMAT_X8B8G8R8_UNORM = 68, + VIRTIO_GPU_FORMAT_A8B8G8R8_UNORM = 121, + VIRTIO_GPU_FORMAT_R8G8B8X8_UNORM = 134 +}; + +typedef void (*vgpu_cmd_func)(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen); + +struct vgpu_cmd_backend { + /* 2D commands */ + vgpu_cmd_func get_display_info; + vgpu_cmd_func resource_create_2d; + vgpu_cmd_func resource_unref; + vgpu_cmd_func set_scanout; + vgpu_cmd_func resource_flush; + vgpu_cmd_func transfer_to_host_2d; + vgpu_cmd_func resource_attach_backing; + vgpu_cmd_func resource_detach_backing; + vgpu_cmd_func get_capset_info; + vgpu_cmd_func get_capset; + vgpu_cmd_func get_edid; + vgpu_cmd_func resource_assign_uuid; + vgpu_cmd_func resource_create_blob; + vgpu_cmd_func set_scanout_blob; + /* 3D commands */ + vgpu_cmd_func ctx_create; + vgpu_cmd_func ctx_destroy; + vgpu_cmd_func ctx_attach_resource; + vgpu_cmd_func ctx_detach_resource; + vgpu_cmd_func resource_create_3d; + vgpu_cmd_func transfer_to_host_3d; + vgpu_cmd_func transfer_from_host_3d; + vgpu_cmd_func submit_3d; + vgpu_cmd_func resource_map_blob; + vgpu_cmd_func resource_unmap_blob; + /* Cursor commands */ + vgpu_cmd_func update_cursor; + vgpu_cmd_func move_cursor; +}; + +size_t iov_to_buf(const struct iovec *iov, + const unsigned int iov_cnt, + size_t offset, + void *buf, + size_t bytes); + +struct vgpu_resource_2d *vgpu_create_resource_2d(int resource_id); +struct vgpu_resource_2d *vgpu_get_resource_2d(uint32_t resource_id); +int vgpu_destroy_resource_2d(uint32_t resource_id); + +void *vgpu_mem_guest_to_host(virtio_gpu_state_t *vgpu, + uint32_t addr, + uint32_t size); +uint32_t virtio_gpu_write_response(virtio_gpu_state_t *vgpu, + uint64_t addr, + uint32_t desc_len, + uint32_t type); +void virtio_gpu_set_fail(virtio_gpu_state_t *vgpu); +void virtio_gpu_set_response_fencing(virtio_gpu_state_t *vgpu, + struct vgpu_ctrl_hdr *request, + uint64_t addr, + uint32_t desc_len); +void virtio_gpu_get_display_info_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen); +void virtio_gpu_get_edid_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen); +void virtio_gpu_cmd_undefined_handler(virtio_gpu_state_t *vgpu, + struct virtq_desc *vq_desc, + uint32_t *plen); +int virtio_gpu_find_response_desc(struct virtq_desc *vq_desc, int max_desc); +#endif diff --git a/virtio.h b/virtio.h index 95357d63..af9f965c 100644 --- a/virtio.h +++ b/virtio.h @@ -96,6 +96,12 @@ _(QueueDeviceLow, 0x0a0) /* W */ \ _(QueueDeviceHigh, 0x0a4) /* W */ \ _(ConfigGeneration, 0x0fc) /* R */ \ + _(SHMSel, 0x0ac) /* W */ \ + _(SHMLenLow, 0x0b0) /* R */ \ + _(SHMLenHigh, 0x0b4) /* R */ \ + _(SHMBaseLow, 0x0b8) /* R */ \ + _(SHMBaseHigh, 0x0bc) /* R */ \ + _(QueueReset, 0x0c0) /* RW */ \ _(Config, 0x100) /* RW */ enum { diff --git a/window-sw.c b/window-sw.c new file mode 100644 index 00000000..876b29da --- /dev/null +++ b/window-sw.c @@ -0,0 +1,424 @@ +#include +#include + +#include "device.h" +#include "virtio-gpu.h" +#include "window.h" + +#define SDL_COND_TIMEOUT 1 /* ms */ + +enum { + CLEAR_PRIMARY_PLANE, + FLUSH_PRIMARY_PLANE, + UPDATE_CURSOR_PLANE, + CLEAR_CURSOR_PLANE, + MOVE_CURSOR_PLANE, +}; + +struct display_info { + /* Request type: primary or cursor */ + int render_type; + + /* Primary plane */ + struct vgpu_resource_2d resource; + uint32_t primary_sdl_format; + uint32_t *primary_img; + SDL_Texture *primary_texture; + + /* Cursor plane */ + struct vgpu_resource_2d cursor; + uint32_t cursor_sdl_format; + uint32_t *cursor_img; + SDL_Rect cursor_rect; /* Cursor size and position */ + SDL_Texture *cursor_texture; + + SDL_mutex *img_mtx; + SDL_cond *img_cond; + SDL_Thread *win_thread; + SDL_Window *window; + SDL_Renderer *renderer; +}; + +static struct display_info displays[VIRTIO_GPU_MAX_SCANOUTS]; +static int display_cnt; + +static int window_thread(void *data) +{ + struct display_info *display = (struct display_info *) data; + struct vgpu_resource_2d *resource = &display->resource; + struct vgpu_resource_2d *cursor = &display->cursor; + + /* Create SDL window */ + display->window = SDL_CreateWindow("semu", SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, resource->width, + resource->height, SDL_WINDOW_SHOWN); + + if (!display->window) { + fprintf(stderr, "%s(): failed to create window\n", __func__); + exit(2); + } + + /* Create SDL render */ + display->renderer = + SDL_CreateRenderer(display->window, -1, SDL_RENDERER_ACCELERATED); + + if (!display->renderer) { + fprintf(stderr, "%s(): failed to create renderer\n", __func__); + exit(2); + } + + /* Render the whole screen with black color */ + SDL_SetRenderDrawColor(display->renderer, 0, 0, 0, 255); + SDL_RenderClear(display->renderer); + SDL_RenderPresent(display->renderer); + + SDL_Surface *surface; + + while (1) { + /* Mutex lock */ + SDL_LockMutex(display->img_mtx); + + /* Wait until the image is arrived */ + while (SDL_CondWaitTimeout(display->img_cond, display->img_mtx, + SDL_COND_TIMEOUT)) + ; + + if (display->render_type == CLEAR_PRIMARY_PLANE) { + /* FIXME */ + /* Set color for clearing */ + SDL_SetRenderDrawColor(display->renderer, 0, 0, 0, 255); + } else if (display->render_type == FLUSH_PRIMARY_PLANE) { + /* Generate primary plane texture */ + surface = SDL_CreateRGBSurfaceWithFormatFrom( + resource->image, resource->width, resource->height, + resource->bits_per_pixel, resource->stride, + display->primary_sdl_format); + + if (surface) { + SDL_DestroyTexture(display->primary_texture); + display->primary_texture = + SDL_CreateTextureFromSurface(display->renderer, surface); + SDL_FreeSurface(surface); + } else { + fprintf(stderr, "Failed to create primary plane surface\n"); + } + } else if (display->render_type == UPDATE_CURSOR_PLANE) { + /* Generate cursor plane texture */ + surface = SDL_CreateRGBSurfaceWithFormatFrom( + cursor->image, cursor->width, cursor->height, CURSOR_BPP, + CURSOR_STRIDE, SDL_PIXELFORMAT_ARGB8888); + + if (surface) { + SDL_DestroyTexture(display->cursor_texture); + display->cursor_texture = + SDL_CreateTextureFromSurface(display->renderer, surface); + SDL_FreeSurface(surface); + } else { + fprintf(stderr, "Failed to create cursor plane surface\n"); + } + } else if (display->render_type == CLEAR_CURSOR_PLANE) { + SDL_DestroyTexture(display->cursor_texture); + display->cursor_texture = NULL; + } + + /* Render primary and cursor planes */ + SDL_RenderClear(display->renderer); + + if (display->primary_texture) + SDL_RenderCopy(display->renderer, display->primary_texture, NULL, + NULL); + + if (display->cursor_texture) + SDL_RenderCopy(display->renderer, display->cursor_texture, NULL, + &display->cursor_rect); + + SDL_RenderPresent(display->renderer); + + /* Mutex unlock */ + SDL_UnlockMutex(display->img_mtx); + } +} + +void window_init_sw(void) +{ + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + fprintf(stderr, "%s(): failed to initialize SDL\n", __func__); + exit(2); + } + + for (int i = 0; i < display_cnt; i++) { + displays[i].img_mtx = SDL_CreateMutex(); + if (!displays[i].img_mtx) { + fprintf(stderr, "%s(): failed to create mutex: %s\n", __func__, + SDL_GetError()); + exit(2); + } + + displays[i].img_cond = SDL_CreateCond(); + if (!displays[i].img_cond) { + fprintf(stderr, "%s(): failed to create condition variable: %s\n", + __func__, SDL_GetError()); + SDL_DestroyMutex(displays[i].img_mtx); + exit(2); + } + + displays[i].win_thread = + SDL_CreateThread(window_thread, NULL, (void *) &displays[i]); + if (!displays[i].win_thread) { + fprintf(stderr, "%s(): failed to create window thread\n", __func__); + exit(2); + } + SDL_DetachThread(displays[i].win_thread); + } +} + +static void window_add_sw(uint32_t width, uint32_t height) +{ + if (display_cnt >= VIRTIO_GPU_MAX_SCANOUTS) { + fprintf(stderr, "%s(): display count exceeds maximum\n", __func__); + exit(2); + } + + displays[display_cnt].resource.width = width; + displays[display_cnt].resource.height = height; + display_cnt++; +} + +static bool virtio_gpu_to_sdl_format(uint32_t virtio_gpu_format, + uint32_t *sdl_format) +{ + switch (virtio_gpu_format) { + case VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM: + *sdl_format = SDL_PIXELFORMAT_ARGB8888; + return true; + case VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM: + *sdl_format = SDL_PIXELFORMAT_XRGB8888; + return true; + case VIRTIO_GPU_FORMAT_A8R8G8B8_UNORM: + *sdl_format = SDL_PIXELFORMAT_BGRA8888; + return true; + case VIRTIO_GPU_FORMAT_X8R8G8B8_UNORM: + *sdl_format = SDL_PIXELFORMAT_BGRX8888; + return true; + case VIRTIO_GPU_FORMAT_R8G8B8A8_UNORM: + *sdl_format = SDL_PIXELFORMAT_ABGR8888; + return true; + case VIRTIO_GPU_FORMAT_X8B8G8R8_UNORM: + *sdl_format = SDL_PIXELFORMAT_RGBX8888; + return true; + case VIRTIO_GPU_FORMAT_A8B8G8R8_UNORM: + *sdl_format = SDL_PIXELFORMAT_RGBA8888; + return true; + case VIRTIO_GPU_FORMAT_R8G8B8X8_UNORM: + *sdl_format = SDL_PIXELFORMAT_XBGR8888; + return true; + default: + return false; + } +} + +static void cursor_clear_sw(int scanout_id) +{ + if (scanout_id >= display_cnt) + return; + + struct display_info *display = &displays[scanout_id]; + + /* Start of the critical section */ + if (SDL_LockMutex(displays[scanout_id].img_mtx) != 0) { + fprintf(stderr, "%s(): failed to lock mutex: %s\n", __func__, + SDL_GetError()); + return; + } + + /* Reset cursor information */ + memset(&display->cursor_rect, 0, sizeof(SDL_Rect)); + display->cursor_sdl_format = 0; + + /* Reset cursor resource */ + memset(&display->cursor, 0, sizeof(struct vgpu_resource_2d)); + free(display->cursor_img); + display->cursor_img = NULL; + display->cursor.image = NULL; + + /* Trigger plane rendering */ + display->render_type = CLEAR_CURSOR_PLANE; + SDL_CondSignal(display->img_cond); + + /* End of the critical section */ + SDL_UnlockMutex(displays[scanout_id].img_mtx); +} + +static void cursor_update_sw(int scanout_id, int res_id, int x, int y) +{ + if (scanout_id >= display_cnt) + return; + + struct vgpu_resource_2d *resource = vgpu_get_resource_2d(res_id); + if (!resource) { + fprintf(stderr, "%s(): invalid resource id %d\n", __func__, res_id); + return; + } + + /* Convert virtio-gpu resource format to SDL format */ + uint32_t sdl_format; + bool legal_format = virtio_gpu_to_sdl_format(resource->format, &sdl_format); + + if (!legal_format) { + fprintf(stderr, "%s(): invalid resource format\n", __func__); + return; + } + + /* Start of the critical section */ + if (SDL_LockMutex(displays[scanout_id].img_mtx) != 0) { + fprintf(stderr, "%s(): failed to lock mutex: %s\n", __func__, + SDL_GetError()); + return; + } + + /* Update cursor information */ + struct display_info *display = &displays[scanout_id]; + display->cursor_rect.x = x; + display->cursor_rect.y = y; + display->cursor_rect.w = resource->width; + display->cursor_rect.h = resource->height; + display->cursor_sdl_format = sdl_format; + + /* Cursor resource update */ + memcpy(&display->cursor, resource, sizeof(struct vgpu_resource_2d)); + size_t pixels_size = (size_t) resource->stride * resource->height; + free(display->cursor_img); + display->cursor_img = malloc(pixels_size); + if (!display->cursor_img) { + fprintf(stderr, "%s(): failed to allocate cursor image\n", __func__); + SDL_UnlockMutex(displays[scanout_id].img_mtx); + return; + } + + display->cursor.image = display->cursor_img; + memcpy(display->cursor_img, resource->image, pixels_size); + + /* Trigger cursor rendering */ + display->render_type = UPDATE_CURSOR_PLANE; + SDL_CondSignal(display->img_cond); + + /* End of the critical section */ + SDL_UnlockMutex(displays[scanout_id].img_mtx); +} + +static void cursor_move_sw(int scanout_id, int x, int y) +{ + if (scanout_id >= display_cnt) + return; + + struct display_info *display = &displays[scanout_id]; + + /* Start of the critical section */ + if (SDL_LockMutex(displays[scanout_id].img_mtx) != 0) { + fprintf(stderr, "%s(): failed to lock mutex: %s\n", __func__, + SDL_GetError()); + return; + } + + /* Update cursor position */ + display->cursor_rect.x = x; + display->cursor_rect.y = y; + + /* Trigger cursor rendering */ + display->render_type = MOVE_CURSOR_PLANE; + SDL_CondSignal(display->img_cond); + + /* End of the critical section */ + SDL_UnlockMutex(displays[scanout_id].img_mtx); +} + +static void window_clear_sw(int scanout_id) +{ + if (scanout_id >= display_cnt) + return; + + struct display_info *display = &displays[scanout_id]; + + /* Start of the critical section */ + if (SDL_LockMutex(displays[scanout_id].img_mtx) != 0) { + fprintf(stderr, "%s(): failed to lock mutex: %s\n", __func__, + SDL_GetError()); + return; + } + + /* Reset primary plane resource */ + memset(&display->resource, 0, sizeof(struct vgpu_resource_2d)); + free(display->primary_img); + display->primary_img = NULL; + display->resource.image = NULL; + + /* Trigger primary plane rendering */ + display->render_type = CLEAR_PRIMARY_PLANE; + SDL_CondSignal(display->img_cond); + + /* End of the critical section */ + SDL_UnlockMutex(displays[scanout_id].img_mtx); +} + +static void window_flush_sw(int scanout_id, int res_id) +{ + if (scanout_id >= display_cnt) + return; + + struct display_info *display = &displays[scanout_id]; + struct vgpu_resource_2d *resource = vgpu_get_resource_2d(res_id); + if (!resource) { + fprintf(stderr, "%s(): invalid resource id %d\n", __func__, res_id); + return; + } + + /* Convert virtio-gpu resource format to SDL format */ + uint32_t sdl_format; + bool legal_format = virtio_gpu_to_sdl_format(resource->format, &sdl_format); + + if (!legal_format) { + fprintf(stderr, "%s(): invalid resource format\n", __func__); + return; + } + + /* Start of the critical section */ + if (SDL_LockMutex(displays[scanout_id].img_mtx) != 0) { + fprintf(stderr, "%s(): failed to lock mutex: %s\n", __func__, + SDL_GetError()); + return; + } + + /* Update primary plane resource */ + display->primary_sdl_format = sdl_format; + memcpy(&display->resource, resource, sizeof(struct vgpu_resource_2d)); + + /* Deep copy pixel data to decouple from the original resource buffer */ + size_t pixels_size = (size_t) resource->stride * resource->height; + uint32_t *new_img = realloc(display->primary_img, pixels_size); + if (!new_img) { + fprintf(stderr, "%s(): failed to allocate primary image\n", __func__); + SDL_UnlockMutex(displays[scanout_id].img_mtx); + return; + } + display->primary_img = new_img; + display->resource.image = new_img; + memcpy(new_img, resource->image, pixels_size); + + /* Trigger primary plane flushing */ + display->render_type = FLUSH_PRIMARY_PLANE; + SDL_CondSignal(display->img_cond); + + /* End of the critical section */ + SDL_UnlockMutex(displays[scanout_id].img_mtx); +} + +const struct window_backend g_window = { + .window_init = window_init_sw, + .window_add = window_add_sw, + .window_set_scanout = NULL, + .window_clear = window_clear_sw, + .window_flush = window_flush_sw, + .cursor_clear = cursor_clear_sw, + .cursor_update = cursor_update_sw, + .cursor_move = cursor_move_sw, +}; diff --git a/window.h b/window.h new file mode 100644 index 00000000..e0c85ea0 --- /dev/null +++ b/window.h @@ -0,0 +1,22 @@ +#pragma once + +#if SEMU_HAS(VIRTIOGPU) +/* Cursor size is always 64*64 in VirtIO GPU */ +#define CURSOR_WIDTH 64 +#define CURSOR_HEIGHT 64 + +#define CURSOR_BPP 4 /* Bytes per pixel, using ARGB */ +#define CURSOR_STRIDE (CURSOR_WIDTH * CURSOR_BPP) + +struct window_backend { + void (*window_init)(void); + void (*window_add)(uint32_t width, uint32_t height); + void (*window_set_scanout)(int scanout_id, uint32_t texture_id); + void (*window_clear)(int scanout_id); + void (*window_flush)(int scanout_id, int res_id); + void (*cursor_clear)(int scanout_id); + void (*cursor_update)(int scanout_id, int res_id, int x, int y); + void (*cursor_move)(int scanout_id, int x, int y); +}; + +#endif From 4469b916d236b5a9b1c806b13f834d45aa28b17b Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Sun, 22 Feb 2026 13:32:26 +0800 Subject: [PATCH 3/9] Implement virtio-input for keyboard and mouse Add virtio-input device emulation to deliver keyboard and mouse input from the host SDL2 window to the guest. This enables interactive control of the guest userspace through the host window. virtio-input: - Add virtio-input emulation for keyboard and mouse ('virtio-input.c'). - Provide SDL-to-evdev event mapping ('window-events.c') and queue update logic with synchronization to avoid concurrent ring corruption. - Implement device configuration queries via 'struct virtio_input_config' and 'VIRTIO_INPUT_CFG_*' selectors. Supported configuration selectors: - 'VIRTIO_INPUT_CFG_ID_NAME' - 'VIRTIO_INPUT_CFG_ID_SERIAL' - 'VIRTIO_INPUT_CFG_ID_DEVIDS' - 'VIRTIO_INPUT_CFG_PROP_BITS' - 'VIRTIO_INPUT_CFG_EV_BITS' - 'VIRTIO_INPUT_CFG_ABS_INFO' MMIO access rule: virtio-mmio common registers are restricted to aligned 32-bit accesses. The device-specific configuration region starts at Config (offset 0x100) and permits byte/halfword accesses as required by virtio-input ('select', 'subsel', and byte-sized fields). This prevents guest drivers from faulting when performing per-byte configuration accesses, while keeping the common register model strict. Build and configuration: - Guard virtio-input code with SEMU_HAS(VIRTIOINPUT) so that the virtio-gpu device can be built independently without virtio-input. - Move 'window-events.o' to the VIRTIOINPUT build section, since it depends on virtio-input symbols. Testing notes: - Boot a guest kernel with virtio-gpu and virtio-input enabled. - Verify that the guest detects an input device (virtio-input). - Start a userspace UI stack (e.g., DirectFB2 on top of DRM/KMS) and confirm: - keyboard and mouse events are delivered to the guest userspace - More specifically, execute 'source ./run.sh', then run DirectFB2 test programs to verify. References: - VirtIO Spec v1.3 (virtio-mmio, virtio-input) - Linux: - drivers/virtio/virtio_ring.c - drivers/virtio/virtio_mmio.c Co-authored-by: Shengwen Cheng --- .ci/test-vinput.sh | 62 +++ .github/workflows/main.yml | 12 + Makefile | 12 +- device.h | 61 +++ feature.h | 5 + main.c | 60 +++ minimal.dts | 14 + virtio-input-codes.h | 185 +++++++++ virtio-input.c | 754 +++++++++++++++++++++++++++++++++++++ window-events.c | 188 +++++++++ window-sw.c | 7 + window.h | 4 + 12 files changed, 1363 insertions(+), 1 deletion(-) create mode 100755 .ci/test-vinput.sh create mode 100644 virtio-input-codes.h create mode 100644 virtio-input.c create mode 100644 window-events.c diff --git a/.ci/test-vinput.sh b/.ci/test-vinput.sh new file mode 100755 index 00000000..de6fb0c6 --- /dev/null +++ b/.ci/test-vinput.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "${SCRIPT_DIR}/common.sh" + +# Override timeout for macOS - emulation is significantly slower +case "${OS_TYPE}" in + Darwin) + TIMEOUT=10800 + ;; +esac + +cleanup +trap cleanup EXIT + +# NOTE: We want to capture the expect exit code and map +# it to our MESSAGES array for meaningful error output. +# Temporarily disable errexit for the expect call. +set +e +expect <<'DONE' +set timeout $env(TIMEOUT) +spawn make check + +# Boot and login +expect "buildroot login:" { send "root\r" } timeout { exit 1 } +expect "# " { send "uname -a\r" } timeout { exit 2 } +expect "riscv32 GNU/Linux" {} + +# ---------------- virtio-input ---------------- +# Require actual event* nodes, not just /dev/input directory existence +expect "# " { send "ls /dev/input/event* >/dev/null 2>&1 && echo __EVT_OK__ || echo __EVT_BAD__\r" } +expect { + -re "\r?\n__EVT_OK__" {} + -re "\r?\n__EVT_BAD__" { exit 3 } + timeout { exit 3 } +} + +expect "# " { send "cat /proc/bus/input/devices | head -20\r" } +expect "# " { send "grep -qi virtio /proc/bus/input/devices && echo __VPROC_OK__ || echo __VPROC_WARN__\r" } +expect -re "__VPROC_(OK|WARN)__" {} timeout { exit 3 } +DONE + +ret="$?" +set -e # Re-enable errexit after capturing expect's return code + +MESSAGES=( + "PASS: headless virtio-input checks" + "FAIL: boot/login prompt not found" + "FAIL: shell prompt not found" + "FAIL: virtio-input basic checks failed (/dev/input/event* or /proc/bus/input/devices)" + "FAIL: virtio-input event stream did not produce bytes (needs host->virtio-input injection path)" +) + +if [[ "${ret}" -eq 0 ]]; then + print_success "${MESSAGES[0]}" + exit 0 +fi + +print_error "${MESSAGES[${ret}]:-FAIL: unknown error (exit code ${ret})}" +exit "${ret}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b7bf21a3..e9ae53aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,6 +79,12 @@ jobs: timeout-minutes: 5 env: SDL_VIDEODRIVER: offscreen + - name: virtio-input test + run: .ci/test-vinput.sh + shell: bash + timeout-minutes: 5 + env: + SDL_VIDEODRIVER: offscreen semu-macOS: runs-on: macos-latest @@ -138,6 +144,12 @@ jobs: timeout-minutes: 120 env: SDL_VIDEODRIVER: offscreen + - name: virtio-input test + run: .ci/test-vinput.sh + shell: bash + timeout-minutes: 20 + env: + SDL_VIDEODRIVER: offscreen coding_style: runs-on: ubuntu-24.04 diff --git a/Makefile b/Makefile index c105010e..c965c8db 100644 --- a/Makefile +++ b/Makefile @@ -172,8 +172,9 @@ ifeq ($(ENABLE_SDL),1) CFLAGS += $(shell sdl2-config --cflags) LDFLAGS += $(shell sdl2-config --libs) else - # Disable virtio-gpu if SDL is not set + # Disable virtio-gpu and virtio-input if SDL is not set override ENABLE_VIRTIOGPU := 0 + override ENABLE_VIRTIOINPUT := 0 endif # virtio-gpu @@ -188,6 +189,15 @@ endif $(call set-feature, VIRTIOGPU) +# virtio-input +ENABLE_VIRTIOINPUT ?= 1 +ifeq ($(ENABLE_VIRTIOINPUT),1) + OBJS_EXTRA += virtio-input.o + OBJS_EXTRA += window-events.o +endif + +$(call set-feature, VIRTIOINPUT) + BIN = semu all: $(BIN) minimal.dtb diff --git a/device.h b/device.h index b3d9adff..bdc40f2e 100644 --- a/device.h +++ b/device.h @@ -279,6 +279,63 @@ void virtio_gpu_add_scanout(virtio_gpu_state_t *vgpu, uint32_t height); #endif /* SEMU_HAS(VIRTIOGPU) */ +/* VirtIO Input */ + +#if SEMU_HAS(VIRTIOINPUT) + +#define IRQ_VINPUT_KEYBOARD 8 +#define IRQ_VINPUT_KEYBOARD_BIT (1 << IRQ_VINPUT_KEYBOARD) + +#define IRQ_VINPUT_MOUSE 9 +#define IRQ_VINPUT_MOUSE_BIT (1 << IRQ_VINPUT_MOUSE) + +typedef struct { + uint32_t QueueNum; + uint32_t QueueDesc; + uint32_t QueueAvail; + uint32_t QueueUsed; + uint16_t last_avail; + bool ready; +} virtio_input_queue_t; + +typedef struct { + /* feature negotiation */ + uint32_t DeviceFeaturesSel; + uint32_t DriverFeatures; + uint32_t DriverFeaturesSel; + /* queue config */ + uint32_t QueueSel; + virtio_input_queue_t queues[2]; + /* status */ + uint32_t Status; + uint32_t InterruptStatus; + /* supplied by environment */ + uint32_t *ram; + /* implementation-specific */ + void *priv; +} virtio_input_state_t; + +void virtio_input_read(hart_t *vm, + virtio_input_state_t *vinput, + uint32_t addr, + uint8_t width, + uint32_t *value); + +void virtio_input_write(hart_t *vm, + virtio_input_state_t *vinput, + uint32_t addr, + uint8_t width, + uint32_t value); + +void virtio_input_init(virtio_input_state_t *vinput); + +void virtio_input_update_key(uint32_t key, uint32_t state); + +void virtio_input_update_mouse_button_state(uint32_t button, bool pressed); + +void virtio_input_update_cursor(uint32_t x, uint32_t y); +#endif /* SEMU_HAS(VIRTIOINPUT) */ + /* ACLINT MTIMER */ typedef struct { /* A MTIMER device has two separate base addresses: one for the MTIME @@ -504,6 +561,10 @@ typedef struct { #endif #if SEMU_HAS(VIRTIOGPU) virtio_gpu_state_t vgpu; +#endif +#if SEMU_HAS(VIRTIOINPUT) + virtio_input_state_t vkeyboard; + virtio_input_state_t vmouse; #endif /* ACLINT */ mtimer_state_t mtimer; diff --git a/feature.h b/feature.h index 351d2408..1b30eb22 100644 --- a/feature.h +++ b/feature.h @@ -27,5 +27,10 @@ #define SEMU_FEATURE_VIRTIOGPU 1 #endif +/* virtio-input */ +#ifndef SEMU_FEATURE_VIRTIOINPUT +#define SEMU_FEATURE_VIRTIOINPUT 1 +#endif + /* Feature test macro */ #define SEMU_HAS(x) SEMU_FEATURE_##x diff --git a/main.c b/main.c index 141ac38f..876896e2 100644 --- a/main.c +++ b/main.c @@ -118,6 +118,28 @@ static void emu_update_vgpu_interrupts(vm_t *vm) } #endif +#if SEMU_HAS(VIRTIOINPUT) +static void emu_update_vinput_keyboard_interrupts(vm_t *vm) +{ + emu_state_t *data = PRIV(vm->hart[0]); + if (data->vkeyboard.InterruptStatus) + data->plic.active |= IRQ_VINPUT_KEYBOARD_BIT; + else + data->plic.active &= ~IRQ_VINPUT_KEYBOARD_BIT; + plic_update_interrupts(vm, &data->plic); +} + +static void emu_update_vinput_mouse_interrupts(vm_t *vm) +{ + emu_state_t *data = PRIV(vm->hart[0]); + if (data->vmouse.InterruptStatus) + data->plic.active |= IRQ_VINPUT_MOUSE_BIT; + else + data->plic.active &= ~IRQ_VINPUT_MOUSE_BIT; + plic_update_interrupts(vm, &data->plic); +} +#endif + static void emu_update_timer_interrupt(hart_t *hart) { emu_state_t *data = PRIV(hart); @@ -216,6 +238,12 @@ static inline void emu_tick_peripherals(emu_state_t *emu) #if SEMU_HAS(VIRTIOGPU) if (emu->vgpu.InterruptStatus) emu_update_vgpu_interrupts(vm); +#endif +#if SEMU_HAS(VIRTIOINPUT) + if (emu->vkeyboard.InterruptStatus) + emu_update_vinput_keyboard_interrupts(vm); + if (emu->vmouse.InterruptStatus) + emu_update_vinput_mouse_interrupts(vm); #endif } } @@ -283,6 +311,18 @@ static void mem_load(hart_t *hart, virtio_gpu_read(hart, &data->vgpu, addr & 0xFFFFF, width, value); emu_update_vgpu_interrupts(hart->vm); return; +#endif +#if SEMU_HAS(VIRTIOINPUT) + case 0x50: /* virtio-input keyboard */ + virtio_input_read(hart, &data->vkeyboard, addr & 0xFFFFF, width, + value); + emu_update_vinput_keyboard_interrupts(hart->vm); + return; + case 0x51: /* virtio-input mouse */ + virtio_input_read(hart, &data->vmouse, addr & 0xFFFFF, width, + value); + emu_update_vinput_mouse_interrupts(hart->vm); + return; #endif } } @@ -361,6 +401,18 @@ static void mem_store(hart_t *hart, virtio_gpu_write(hart, &data->vgpu, addr & 0xFFFFF, width, value); emu_update_vgpu_interrupts(hart->vm); return; +#endif +#if SEMU_HAS(VIRTIOINPUT) + case 0x50: /* virtio-input keyboard */ + virtio_input_write(hart, &data->vkeyboard, addr & 0xFFFFF, width, + value); + emu_update_vinput_keyboard_interrupts(hart->vm); + return; + case 0x51: /* virtio-input mouse */ + virtio_input_write(hart, &data->vmouse, addr & 0xFFFFF, width, + value); + emu_update_vinput_mouse_interrupts(hart->vm); + return; #endif } } @@ -855,6 +907,14 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) g_window.window_init(); #endif +#if SEMU_HAS(VIRTIOINPUT) + emu->vkeyboard.ram = emu->ram; + virtio_input_init(&(emu->vkeyboard)); + + emu->vmouse.ram = emu->ram; + virtio_input_init(&(emu->vmouse)); +#endif + emu->peripheral_update_ctr = 0; emu->debug = debug; diff --git a/minimal.dts b/minimal.dts index 90b9239f..0738066b 100644 --- a/minimal.dts +++ b/minimal.dts @@ -95,5 +95,19 @@ interrupts = <7>; }; #endif + +#if SEMU_FEATURE_VIRTIOINPUT + keyboard0: virtio@5000000 { + compatible = "virtio,mmio"; + reg = <0x5000000 0x200>; + interrupts = <8>; + }; + + mouse0: virtio@5100000 { + compatible = "virtio,mmio"; + reg = <0x5100000 0x200>; + interrupts = <9>; + }; +#endif }; }; diff --git a/virtio-input-codes.h b/virtio-input-codes.h new file mode 100644 index 00000000..fb0088fb --- /dev/null +++ b/virtio-input-codes.h @@ -0,0 +1,185 @@ +#pragma once + +/* + * SEMU Input Event Codes + * + * Input event type and code definitions used by the SEMU virtual input + * subsystem. The numeric values are chosen to match the Linux evdev ABI so + * that a guest kernel can consume them directly without translation. + * + * Reference: Linux kernel include/uapi/linux/input-event-codes.h + * https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git + * + * SPDX-License-Identifier: MIT + */ + +/* + * Event types + * + * Each input event carries a type that identifies the class of data it + * reports: synchronization boundaries, key/button state changes, or + * absolute axis positions. + */ + +#define SEMU_EV_SYN 0x00 +#define SEMU_EV_KEY 0x01 +#define SEMU_EV_ABS 0x03 +#define SEMU_EV_REP 0x14 + +/* + * Synchronization codes + * + * A SYN_REPORT event marks the end of a coherent group of input events + * (e.g. one key press, or one pointer movement with its axis values). + */ + +#define SEMU_SYN_REPORT 0 + +/* + * Keyboard scancodes + * + * Standard PC/AT keyboard codes covering the main block, function keys, + * navigation cluster, and numeric keypad. + */ + +/* Escape and number row */ +#define SEMU_KEY_ESC 1 +#define SEMU_KEY_1 2 +#define SEMU_KEY_2 3 +#define SEMU_KEY_3 4 +#define SEMU_KEY_4 5 +#define SEMU_KEY_5 6 +#define SEMU_KEY_6 7 +#define SEMU_KEY_7 8 +#define SEMU_KEY_8 9 +#define SEMU_KEY_9 10 +#define SEMU_KEY_0 11 +#define SEMU_KEY_MINUS 12 +#define SEMU_KEY_EQUAL 13 +#define SEMU_KEY_BACKSPACE 14 + +/* Top letter row */ +#define SEMU_KEY_TAB 15 +#define SEMU_KEY_Q 16 +#define SEMU_KEY_W 17 +#define SEMU_KEY_E 18 +#define SEMU_KEY_R 19 +#define SEMU_KEY_T 20 +#define SEMU_KEY_Y 21 +#define SEMU_KEY_U 22 +#define SEMU_KEY_I 23 +#define SEMU_KEY_O 24 +#define SEMU_KEY_P 25 +#define SEMU_KEY_LEFTBRACE 26 +#define SEMU_KEY_RIGHTBRACE 27 +#define SEMU_KEY_ENTER 28 + +/* Home row */ +#define SEMU_KEY_LEFTCTRL 29 +#define SEMU_KEY_A 30 +#define SEMU_KEY_S 31 +#define SEMU_KEY_D 32 +#define SEMU_KEY_F 33 +#define SEMU_KEY_G 34 +#define SEMU_KEY_H 35 +#define SEMU_KEY_J 36 +#define SEMU_KEY_K 37 +#define SEMU_KEY_L 38 +#define SEMU_KEY_SEMICOLON 39 +#define SEMU_KEY_APOSTROPHE 40 +#define SEMU_KEY_GRAVE 41 + +/* Bottom letter row */ +#define SEMU_KEY_LEFTSHIFT 42 +#define SEMU_KEY_BACKSLASH 43 +#define SEMU_KEY_Z 44 +#define SEMU_KEY_X 45 +#define SEMU_KEY_C 46 +#define SEMU_KEY_V 47 +#define SEMU_KEY_B 48 +#define SEMU_KEY_N 49 +#define SEMU_KEY_M 50 +#define SEMU_KEY_COMMA 51 +#define SEMU_KEY_DOT 52 +#define SEMU_KEY_SLASH 53 +#define SEMU_KEY_RIGHTSHIFT 54 + +/* Modifier and space row */ +#define SEMU_KEY_LEFTALT 56 +#define SEMU_KEY_SPACE 57 +#define SEMU_KEY_CAPSLOCK 58 + +/* Function keys */ +#define SEMU_KEY_F1 59 +#define SEMU_KEY_F2 60 +#define SEMU_KEY_F3 61 +#define SEMU_KEY_F4 62 +#define SEMU_KEY_F5 63 +#define SEMU_KEY_F6 64 +#define SEMU_KEY_F7 65 +#define SEMU_KEY_F8 66 +#define SEMU_KEY_F9 67 +#define SEMU_KEY_F10 68 +#define SEMU_KEY_F11 87 +#define SEMU_KEY_F12 88 + +/* Numeric keypad */ +#define SEMU_KEY_NUMLOCK 69 +#define SEMU_KEY_SCROLLLOCK 70 +#define SEMU_KEY_KP7 71 +#define SEMU_KEY_KP8 72 +#define SEMU_KEY_KP9 73 +#define SEMU_KEY_KPASTERISK 55 +#define SEMU_KEY_KPMINUS 74 +#define SEMU_KEY_KP4 75 +#define SEMU_KEY_KP5 76 +#define SEMU_KEY_KP6 77 +#define SEMU_KEY_KPPLUS 78 +#define SEMU_KEY_KP1 79 +#define SEMU_KEY_KP2 80 +#define SEMU_KEY_KP3 81 +#define SEMU_KEY_KP0 82 +#define SEMU_KEY_KPDOT 83 +#define SEMU_KEY_KPENTER 96 +#define SEMU_KEY_KPSLASH 98 + +/* Right-side modifiers */ +#define SEMU_KEY_RIGHTCTRL 97 +#define SEMU_KEY_RIGHTALT 100 + +/* Navigation cluster */ +#define SEMU_KEY_HOME 102 +#define SEMU_KEY_UP 103 +#define SEMU_KEY_PAGEUP 104 +#define SEMU_KEY_LEFT 105 +#define SEMU_KEY_RIGHT 106 +#define SEMU_KEY_END 107 +#define SEMU_KEY_DOWN 108 +#define SEMU_KEY_PAGEDOWN 109 +#define SEMU_KEY_INSERT 110 +#define SEMU_KEY_DELETE 111 + +/* + * Mouse button codes + */ +#define SEMU_BTN_LEFT 0x110 +#define SEMU_BTN_RIGHT 0x111 +#define SEMU_BTN_MIDDLE 0x112 + +/* + * Absolute axis identifiers (used for pointer position reporting) + */ +#define SEMU_ABS_X 0x00 +#define SEMU_ABS_Y 0x01 + +/* + * Key-repeat configuration codes + */ +#define SEMU_REP_DELAY 0x00 +#define SEMU_REP_PERIOD 0x01 + +/* + * Device property flags (reported via VIRTIO_INPUT_CFG_PROP_BITS) + */ +#define SEMU_INPUT_PROP_POINTER 0x00 +#define SEMU_INPUT_PROP_DIRECT 0x01 diff --git a/virtio-input.c b/virtio-input.c new file mode 100644 index 00000000..48ee34cd --- /dev/null +++ b/virtio-input.c @@ -0,0 +1,754 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "device.h" +#include "riscv.h" +#include "riscv_private.h" +#include "utils.h" +#include "virtio-input-codes.h" +#include "virtio.h" + +#define BUS_VIRTUAL 0x06 /* Definition from the Linux kernel */ + +#define VINPUT_KEYBOARD_NAME "VirtIO Keyboard" +#define VINPUT_MOUSE_NAME "VirtIO Mouse" + +#define VIRTIO_INPUT_SERIAL "None" + +#define VIRTIO_F_VERSION_1 1 + +#define VINPUT_FEATURES_0 0 +#define VINPUT_FEATURES_1 1 /* VIRTIO_F_VERSION_1 */ + +#define VINPUT_QUEUE_NUM_MAX 1024 +#define VINPUT_QUEUE (vinput->queues[vinput->QueueSel]) + +#define PRIV(x) ((struct virio_input_data *) (x)->priv) + +enum { + VINPUT_KEYBOARD_ID = 0, + VINPUT_MOUSE_ID = 1, + VINPUT_DEV_CNT, +}; + +enum { + EVENTQ = 0, + STATUSQ = 1, +}; + +enum { + VIRTIO_INPUT_REG_SELECT = 0x100, + VIRTIO_INPUT_REG_SUBSEL = 0x101, + VIRTIO_INPUT_REG_SIZE = 0x102, +}; + +enum virtio_input_config_select { + VIRTIO_INPUT_CFG_UNSET = 0x00, + VIRTIO_INPUT_CFG_ID_NAME = 0x01, + VIRTIO_INPUT_CFG_ID_SERIAL = 0x02, + VIRTIO_INPUT_CFG_ID_DEVIDS = 0x03, + VIRTIO_INPUT_CFG_PROP_BITS = 0x10, + VIRTIO_INPUT_CFG_EV_BITS = 0x11, + VIRTIO_INPUT_CFG_ABS_INFO = 0x12, +}; + +PACKED(struct virtio_input_absinfo { + uint32_t min; + uint32_t max; + uint32_t fuzz; + uint32_t flat; + uint32_t res; +}); + +PACKED(struct virtio_input_devids { + uint16_t bustype; + uint16_t vendor; + uint16_t product; + uint16_t version; +}); + +PACKED(struct virtio_input_config { + uint8_t select; + uint8_t subsel; + uint8_t size; + uint8_t reserved[5]; + union { + char string[128]; + uint8_t bitmap[128]; + struct virtio_input_absinfo abs; + struct virtio_input_devids ids; + } u; +}); + +PACKED(struct virtio_input_event { + uint16_t type; + uint16_t code; + uint32_t value; +}); + +struct virio_input_data { + virtio_input_state_t *vinput; + struct virtio_input_config cfg; + int type; /* VINPUT_KEYBOARD_ID or VINPUT_MOUSE_ID */ +}; + +static pthread_mutex_t virtio_input_mutex = PTHREAD_MUTEX_INITIALIZER; + +static struct virio_input_data vinput_dev[VINPUT_DEV_CNT]; +static int vinput_dev_cnt; + +static char *vinput_dev_name[VINPUT_DEV_CNT] = { + VINPUT_KEYBOARD_NAME, + VINPUT_MOUSE_NAME, +}; + +static inline void bitmap_set_bit(uint8_t *map, unsigned long bit) +{ + /* Each byte holds 8 bits. Index into the byte with bit/8, + * then set the corresponding bit within that byte with 1 << (bit%8). + */ + map[bit / 8] |= (uint8_t) (1U << (bit % 8)); +} + +static void virtio_input_set_fail(virtio_input_state_t *vinput) +{ + vinput->Status |= VIRTIO_STATUS__DEVICE_NEEDS_RESET; + if (vinput->Status & VIRTIO_STATUS__DRIVER_OK) + vinput->InterruptStatus |= VIRTIO_INT__CONF_CHANGE; +} + +static inline bool vinput_is_config_access(uint32_t addr, size_t access_size) +{ + const uint32_t base = VIRTIO_Config << 2; + const uint32_t end = base + (uint32_t) sizeof(struct virtio_input_config); + + /* [base, end) */ + if (access_size == 0) + return false; + if (addr < base) + return false; + if (addr >= end) + return false; + if (addr + access_size > end) + return false; + return true; +} + +static inline uint32_t vinput_preprocess(virtio_input_state_t *vinput, + uint32_t addr) +{ + if ((addr >= RAM_SIZE) || (addr & 0b11)) + return virtio_input_set_fail(vinput), 0; + + return addr >> 2; +} + +static void virtio_input_update_status(virtio_input_state_t *vinput, + uint32_t status) +{ + vinput->Status |= status; + if (status) + return; + + /* Reset */ + uint32_t *ram = vinput->ram; + void *priv = vinput->priv; + memset(vinput, 0, sizeof(*vinput)); + vinput->ram = ram; + vinput->priv = priv; +} + +/* Returns true if any events were written to used ring, false otherwise */ +static bool virtio_input_desc_handler(virtio_input_state_t *vinput, + struct virtio_input_event *input_ev, + uint32_t ev_cnt, + virtio_input_queue_t *queue) +{ + uint32_t *desc; + struct virtq_desc vq_desc; + struct virtio_input_event *ev; + + uint32_t *ram = vinput->ram; + uint16_t new_avail = + ram[queue->QueueAvail] >> 16; /* virtq_avail.idx (le16) */ + uint16_t new_used = ram[queue->QueueUsed] >> 16; /* virtq_used.idx (le16) */ + + /* For checking if the event buffer has enough space to write */ + uint32_t end = queue->last_avail + ev_cnt; + uint32_t flattened_avail_idx = new_avail; + + /* Handle if the available index has overflowed and returned to the + * beginning */ + if (new_avail < queue->last_avail) + flattened_avail_idx += (1U << 16); + + /* Check if need to wait until the driver supplies new buffers */ + if (flattened_avail_idx < end) + return false; + + for (uint32_t i = 0; i < ev_cnt; i++) { + /* Obtain the available ring index */ + uint16_t queue_idx = queue->last_avail % queue->QueueNum; + uint16_t buffer_idx = ram[queue->QueueAvail + 1 + queue_idx / 2] >> + (16 * (queue_idx % 2)); + + if (buffer_idx >= queue->QueueNum) { + virtio_input_set_fail(vinput); + return false; + } + + desc = &vinput->ram[queue->QueueDesc + buffer_idx * 4]; + vq_desc.addr = desc[0]; + uint32_t addr_high = desc[1]; + vq_desc.len = desc[2]; + vq_desc.flags = desc[3] & 0xFFFF; + + /* Validate descriptor: 32-bit addressing only, WRITE flag set, + * buffer large enough, and address within RAM bounds. + */ + if (addr_high != 0 || !(vq_desc.flags & VIRTIO_DESC_F_WRITE) || + vq_desc.len < sizeof(struct virtio_input_event) || + vq_desc.addr + sizeof(struct virtio_input_event) > RAM_SIZE) { + virtio_input_set_fail(vinput); + return false; + } + + /* Write event into guest buffer directly */ + ev = (struct virtio_input_event *) ((uintptr_t) vinput->ram + + vq_desc.addr); + ev->type = input_ev[i].type; + ev->code = input_ev[i].code; + ev->value = input_ev[i].value; + + /* Update used ring */ + uint32_t vq_used_addr = + queue->QueueUsed + 1 + (new_used % queue->QueueNum) * 2; + ram[vq_used_addr] = buffer_idx; + ram[vq_used_addr + 1] = sizeof(struct virtio_input_event); + + new_used++; + queue->last_avail++; + } + + /* Update used ring header */ + uint16_t *used_hdr = (uint16_t *) &vinput->ram[queue->QueueUsed]; + used_hdr[0] = 0; /* virtq_used.flags */ + used_hdr[1] = new_used; /* virtq_used.idx */ + + return true; +} + +static void virtio_queue_event_update(int dev_id, + struct virtio_input_event *input_ev, + uint32_t ev_cnt) +{ + virtio_input_state_t *vinput = vinput_dev[dev_id].vinput; + if (!vinput) + return; + + int index = EVENTQ; + + /* Start of the critical section */ + pthread_mutex_lock(&virtio_input_mutex); + + uint32_t *ram = vinput->ram; + virtio_input_queue_t *queue = &vinput->queues[index]; + + /* Check device status */ + if (vinput->Status & VIRTIO_STATUS__DEVICE_NEEDS_RESET) + goto out; + + if (!((vinput->Status & VIRTIO_STATUS__DRIVER_OK) && queue->ready)) + goto out; + + /* Check for new buffers */ + uint16_t new_avail = ram[queue->QueueAvail] >> 16; + if (new_avail - queue->last_avail > (uint16_t) queue->QueueNum) { + fprintf(stderr, "%s(): size check failed\n", __func__); + goto fail; + } + + /* No buffers available - drop event or handle later */ + if (queue->last_avail == new_avail) { + /* TODO: Consider buffering events instead of dropping them */ + goto out; + } + + /* Try to write events to used ring */ + bool wrote_events = + virtio_input_desc_handler(vinput, input_ev, ev_cnt, queue); + + /* Send interrupt only if we actually wrote events, unless + * VIRTQ_AVAIL_F_NO_INTERRUPT is set */ + if (wrote_events && !(ram[queue->QueueAvail] & 1)) + vinput->InterruptStatus |= VIRTIO_INT__USED_RING; + + goto out; + +fail: + virtio_input_set_fail(vinput); + +out: + /* End of the critical section */ + pthread_mutex_unlock(&virtio_input_mutex); +} + +void virtio_input_update_key(uint32_t key, uint32_t state) +{ + struct virtio_input_event input_ev[] = { + {.type = SEMU_EV_KEY, .code = key, .value = state}, + {.type = SEMU_EV_SYN, .code = SEMU_SYN_REPORT, .value = 0}, + }; + + size_t ev_cnt = ARRAY_SIZE(input_ev); + virtio_queue_event_update(VINPUT_KEYBOARD_ID, input_ev, ev_cnt); +} + +void virtio_input_update_mouse_button_state(uint32_t button, bool pressed) +{ + struct virtio_input_event input_ev[] = { + {.type = SEMU_EV_KEY, .code = button, .value = pressed}, + {.type = SEMU_EV_SYN, .code = SEMU_SYN_REPORT, .value = 0}, + }; + + size_t ev_cnt = ARRAY_SIZE(input_ev); + virtio_queue_event_update(VINPUT_MOUSE_ID, input_ev, ev_cnt); +} + +void virtio_input_update_cursor(uint32_t x, uint32_t y) +{ + struct virtio_input_event input_ev[] = { + {.type = SEMU_EV_ABS, .code = SEMU_ABS_X, .value = x}, + {.type = SEMU_EV_ABS, .code = SEMU_ABS_Y, .value = y}, + {.type = SEMU_EV_SYN, .code = SEMU_SYN_REPORT, .value = 0}, + }; + + size_t ev_cnt = ARRAY_SIZE(input_ev); + virtio_queue_event_update(VINPUT_MOUSE_ID, input_ev, ev_cnt); +} + +static void virtio_input_properties(int dev_id) +{ + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + + memset(cfg->u.bitmap, 0, 128); + + switch (dev_id) { + case VINPUT_KEYBOARD_ID: + cfg->size = 0; + break; + case VINPUT_MOUSE_ID: + bitmap_set_bit(cfg->u.bitmap, SEMU_INPUT_PROP_POINTER); + bitmap_set_bit(cfg->u.bitmap, SEMU_INPUT_PROP_DIRECT); + cfg->size = 128; + break; + } +} + +static void virtio_keyboard_support_events(int dev_id, uint8_t event) +{ + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + + memset(cfg->u.bitmap, 0, 128); + + switch (event) { + case SEMU_EV_KEY: + memset(cfg->u.bitmap, 0xff, 128); + cfg->size = 128; + break; + case SEMU_EV_REP: + bitmap_set_bit(cfg->u.bitmap, SEMU_REP_DELAY); + bitmap_set_bit(cfg->u.bitmap, SEMU_REP_PERIOD); + cfg->size = 128; + break; + default: + cfg->size = 0; + } +} + +static void virtio_mouse_support_events(int dev_id, uint8_t event) +{ + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + + memset(cfg->u.bitmap, 0, 128); + + switch (event) { + case SEMU_EV_KEY: + bitmap_set_bit(cfg->u.bitmap, SEMU_BTN_LEFT); + bitmap_set_bit(cfg->u.bitmap, SEMU_BTN_RIGHT); + bitmap_set_bit(cfg->u.bitmap, SEMU_BTN_MIDDLE); + cfg->size = 128; + break; + case SEMU_EV_ABS: + bitmap_set_bit(cfg->u.bitmap, SEMU_ABS_X); + bitmap_set_bit(cfg->u.bitmap, SEMU_ABS_Y); + cfg->size = 128; + break; + default: + cfg->size = 0; + } +} + +static void virtio_input_support_events(int dev_id, uint8_t event) +{ + switch (dev_id) { + case VINPUT_KEYBOARD_ID: + virtio_keyboard_support_events(dev_id, event); + break; + case VINPUT_MOUSE_ID: + virtio_mouse_support_events(dev_id, event); + break; + } +} + +static void virtio_input_abs_range(int dev_id, uint8_t code) +{ + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + + switch (code) { + case SEMU_ABS_X: + /* [abs.min, abs.max] is [0, 1023] */ + cfg->u.abs.min = 0; + cfg->u.abs.max = SCREEN_WIDTH - 1; + cfg->u.abs.fuzz = 0; + cfg->u.abs.flat = 0; + cfg->u.abs.res = 1; + cfg->size = sizeof(struct virtio_input_absinfo); + break; + case SEMU_ABS_Y: + /* [abs.min, abs.max] is [0, 767] */ + cfg->u.abs.min = 0; + cfg->u.abs.max = SCREEN_HEIGHT - 1; + cfg->u.abs.fuzz = 0; + cfg->u.abs.flat = 0; + cfg->u.abs.res = 1; + cfg->size = sizeof(struct virtio_input_absinfo); + break; + default: + cfg->size = 0; + } +} + +static bool virtio_input_cfg_read(int dev_id) +{ + uint8_t select = vinput_dev[dev_id].cfg.select; + uint8_t subsel = vinput_dev[dev_id].cfg.subsel; + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + + switch (select) { + case VIRTIO_INPUT_CFG_UNSET: + cfg->size = 0; + return true; + case VIRTIO_INPUT_CFG_ID_NAME: + strcpy(cfg->u.string, vinput_dev_name[dev_id]); + cfg->size = strlen(vinput_dev_name[dev_id]); + return true; + case VIRTIO_INPUT_CFG_ID_SERIAL: + strcpy(cfg->u.string, VIRTIO_INPUT_SERIAL); + cfg->size = strlen(VIRTIO_INPUT_SERIAL); + return true; + case VIRTIO_INPUT_CFG_ID_DEVIDS: + cfg->u.ids.bustype = BUS_VIRTUAL; + cfg->u.ids.vendor = 0; + cfg->u.ids.product = 0; + cfg->u.ids.version = 1; + cfg->size = sizeof(struct virtio_input_devids); + return true; + case VIRTIO_INPUT_CFG_PROP_BITS: + virtio_input_properties(dev_id); + return true; + case VIRTIO_INPUT_CFG_EV_BITS: + virtio_input_support_events(dev_id, subsel); + return true; + case VIRTIO_INPUT_CFG_ABS_INFO: + virtio_input_abs_range(dev_id, subsel); + return true; + default: + fprintf(stderr, + "virtio-input: Unknown value written to select register.\n"); + return false; + } +} + +static bool virtio_input_reg_read(virtio_input_state_t *vinput, + uint32_t addr, + uint32_t *value, + size_t size) +{ +#define _(reg) (VIRTIO_##reg << 2) + switch (addr) { + case _(MagicValue): + *value = 0x74726976; + return true; + case _(Version): + *value = 2; + return true; + case _(DeviceID): + *value = 18; + return true; + case _(VendorID): + *value = VIRTIO_VENDOR_ID; + return true; + case _(DeviceFeatures): + *value = vinput->DeviceFeaturesSel == 0 + ? VINPUT_FEATURES_0 + : (vinput->DeviceFeaturesSel == 1 ? VINPUT_FEATURES_1 : 0); + return true; + case _(QueueNumMax): + *value = VINPUT_QUEUE_NUM_MAX; + return true; + case _(QueueReady): + *value = VINPUT_QUEUE.ready ? 1 : 0; + return true; + case _(InterruptStatus): + *value = vinput->InterruptStatus; + return true; + case _(Status): + *value = vinput->Status; + return true; + case _(ConfigGeneration): + *value = 0; + return true; + case VIRTIO_INPUT_REG_SIZE: + if (!virtio_input_cfg_read(PRIV(vinput)->type)) + return false; + *value = PRIV(vinput)->cfg.size; + return true; + default: + /* Invalid address which exceeded the range */ + if (!RANGE_CHECK(addr, _(Config), sizeof(struct virtio_input_config))) + return false; + + /* Read virtio-input specific registers */ + off_t offset = addr - VIRTIO_INPUT_REG_SELECT; + uint8_t *reg = (uint8_t *) ((uintptr_t) &PRIV(vinput)->cfg + offset); + + /* Clear value first to avoid returning dirty high bits on partial reads + */ + *value = 0; + memcpy(value, reg, size); + + return true; + } +#undef _ +} + +static bool virtio_input_reg_write(virtio_input_state_t *vinput, + uint32_t addr, + uint32_t value) +{ +#define _(reg) (VIRTIO_##reg << 2) + switch (addr) { + case _(DeviceFeaturesSel): + vinput->DeviceFeaturesSel = value; + return true; + case _(DriverFeatures): + if (vinput->DriverFeaturesSel == 0) + vinput->DriverFeatures = value; + return true; + case _(DriverFeaturesSel): + vinput->DriverFeaturesSel = value; + return true; + case _(QueueSel): + if (value < ARRAY_SIZE(vinput->queues)) + vinput->QueueSel = value; + else + virtio_input_set_fail(vinput); + return true; + case _(QueueNum): + if (value > 0 && value <= VINPUT_QUEUE_NUM_MAX) + VINPUT_QUEUE.QueueNum = value; + else + virtio_input_set_fail(vinput); + return true; + case _(QueueReady): + VINPUT_QUEUE.ready = value & 1; + if (value & 1) + VINPUT_QUEUE.last_avail = + vinput->ram[VINPUT_QUEUE.QueueAvail] >> 16; + return true; + case _(QueueDescLow): + VINPUT_QUEUE.QueueDesc = vinput_preprocess(vinput, value); + return true; + case _(QueueDescHigh): + if (value) + virtio_input_set_fail(vinput); + return true; + case _(QueueDriverLow): + VINPUT_QUEUE.QueueAvail = vinput_preprocess(vinput, value); + return true; + case _(QueueDriverHigh): + if (value) + virtio_input_set_fail(vinput); + return true; + case _(QueueDeviceLow): + VINPUT_QUEUE.QueueUsed = vinput_preprocess(vinput, value); + return true; + case _(QueueDeviceHigh): + if (value) + virtio_input_set_fail(vinput); + return true; + case _(QueueNotify): + if (value < ARRAY_SIZE(vinput->queues)) { + /* QueueNotify is just a "kick" signal - actual buffer availability + * is checked via avail.idx in virtio_queue_event_update() */ + /* No action needed here for event queue in this minimal + * implementation */ + } else { + virtio_input_set_fail(vinput); + } + return true; + case _(InterruptACK): + vinput->InterruptStatus &= ~value; + return true; + case _(Status): + virtio_input_update_status(vinput, value); + return true; + case _(SHMSel): + return true; + case VIRTIO_INPUT_REG_SELECT: + PRIV(vinput)->cfg.select = value; + return true; + case VIRTIO_INPUT_REG_SUBSEL: + PRIV(vinput)->cfg.subsel = value; + return true; + default: + /* No other writable registers */ + return false; + } +#undef _ +} + +void virtio_input_read(hart_t *vm, + virtio_input_state_t *vinput, + uint32_t addr, + uint8_t width, + uint32_t *value) +{ + size_t access_size = 0; + bool is_cfg = false; + + pthread_mutex_lock(&virtio_input_mutex); + + switch (width) { + case RV_MEM_LW: + access_size = 4; + break; + case RV_MEM_LBU: + case RV_MEM_LB: + access_size = 1; + break; + case RV_MEM_LHU: + case RV_MEM_LH: + access_size = 2; + break; + default: + vm_set_exception(vm, RV_EXC_ILLEGAL_INSN, 0); + goto out; + } + + is_cfg = vinput_is_config_access(addr, access_size); + + /* + * Common registers (before Config): only allow aligned 32-bit LW. + * Device-specific config (Config and after): allow 8/16/32-bit with + * natural alignment. + */ + if (!is_cfg) { + if (access_size != 4 || (addr & 0x3)) { + vm_set_exception(vm, RV_EXC_LOAD_MISALIGN, vm->exc_val); + goto out; + } + } else { + if (addr & (access_size - 1)) { + vm_set_exception(vm, RV_EXC_LOAD_MISALIGN, vm->exc_val); + goto out; + } + } + + if (!virtio_input_reg_read(vinput, addr, value, access_size)) + vm_set_exception(vm, RV_EXC_LOAD_FAULT, vm->exc_val); + +out: + pthread_mutex_unlock(&virtio_input_mutex); +} + +void virtio_input_write(hart_t *vm, + virtio_input_state_t *vinput, + uint32_t addr, + uint8_t width, + uint32_t value) +{ + size_t access_size = 0; + bool is_cfg = false; + + pthread_mutex_lock(&virtio_input_mutex); + + switch (width) { + case RV_MEM_SW: + access_size = 4; + break; + case RV_MEM_SB: + access_size = 1; + break; + case RV_MEM_SH: + access_size = 2; + break; + default: + vm_set_exception(vm, RV_EXC_ILLEGAL_INSN, 0); + goto out; + } + + is_cfg = vinput_is_config_access(addr, access_size); + + /* + * Common registers (before Config): only allow aligned 32-bit SW. + * Device-specific config (Config and after): allow 8/16/32-bit with + * natural alignment. Note: only select/subsel are writable; others + * will return false and be reported as STORE_FAULT below. + */ + if (!is_cfg) { + if (access_size != 4 || (addr & 0x3)) { + vm_set_exception(vm, RV_EXC_STORE_MISALIGN, vm->exc_val); + goto out; + } + } else { + if (addr & (access_size - 1)) { + vm_set_exception(vm, RV_EXC_STORE_MISALIGN, vm->exc_val); + goto out; + } + } + + if (!virtio_input_reg_write(vinput, addr, value)) + vm_set_exception(vm, RV_EXC_STORE_FAULT, vm->exc_val); + +out: + pthread_mutex_unlock(&virtio_input_mutex); +} + +void virtio_input_init(virtio_input_state_t *vinput) +{ + if (vinput_dev_cnt >= VINPUT_DEV_CNT) { + fprintf(stderr, + "Exceeded the number of virtio-input devices that can be " + "allocated.\n"); + exit(2); + } + + vinput->priv = &vinput_dev[vinput_dev_cnt]; + PRIV(vinput)->type = vinput_dev_cnt; + PRIV(vinput)->vinput = vinput; + vinput_dev_cnt++; +} diff --git a/window-events.c b/window-events.c new file mode 100644 index 00000000..5bdb6225 --- /dev/null +++ b/window-events.c @@ -0,0 +1,188 @@ +#include +#include + +#include "device.h" +#include "virtio-input-codes.h" +#include "window.h" + +#define DEF_KEY_MAP(_sdl_scancode, _linux_key) \ + { \ + .sdl_scancode = _sdl_scancode, .linux_key = _linux_key \ + } + +struct key_map_entry { + int sdl_scancode; + int linux_key; +}; + +static struct key_map_entry key_map[] = { + /* Keyboard */ + DEF_KEY_MAP(SDL_SCANCODE_ESCAPE, SEMU_KEY_ESC), + DEF_KEY_MAP(SDL_SCANCODE_1, SEMU_KEY_1), + DEF_KEY_MAP(SDL_SCANCODE_2, SEMU_KEY_2), + DEF_KEY_MAP(SDL_SCANCODE_3, SEMU_KEY_3), + DEF_KEY_MAP(SDL_SCANCODE_4, SEMU_KEY_4), + DEF_KEY_MAP(SDL_SCANCODE_5, SEMU_KEY_5), + DEF_KEY_MAP(SDL_SCANCODE_6, SEMU_KEY_6), + DEF_KEY_MAP(SDL_SCANCODE_7, SEMU_KEY_7), + DEF_KEY_MAP(SDL_SCANCODE_8, SEMU_KEY_8), + DEF_KEY_MAP(SDL_SCANCODE_9, SEMU_KEY_9), + DEF_KEY_MAP(SDL_SCANCODE_0, SEMU_KEY_0), + DEF_KEY_MAP(SDL_SCANCODE_MINUS, SEMU_KEY_MINUS), + DEF_KEY_MAP(SDL_SCANCODE_EQUALS, SEMU_KEY_EQUAL), + DEF_KEY_MAP(SDL_SCANCODE_BACKSPACE, SEMU_KEY_BACKSPACE), + DEF_KEY_MAP(SDL_SCANCODE_TAB, SEMU_KEY_TAB), + DEF_KEY_MAP(SDL_SCANCODE_Q, SEMU_KEY_Q), + DEF_KEY_MAP(SDL_SCANCODE_W, SEMU_KEY_W), + DEF_KEY_MAP(SDL_SCANCODE_E, SEMU_KEY_E), + DEF_KEY_MAP(SDL_SCANCODE_R, SEMU_KEY_R), + DEF_KEY_MAP(SDL_SCANCODE_T, SEMU_KEY_T), + DEF_KEY_MAP(SDL_SCANCODE_Y, SEMU_KEY_Y), + DEF_KEY_MAP(SDL_SCANCODE_U, SEMU_KEY_U), + DEF_KEY_MAP(SDL_SCANCODE_I, SEMU_KEY_I), + DEF_KEY_MAP(SDL_SCANCODE_O, SEMU_KEY_O), + DEF_KEY_MAP(SDL_SCANCODE_P, SEMU_KEY_P), + DEF_KEY_MAP(SDL_SCANCODE_LEFTBRACKET, SEMU_KEY_LEFTBRACE), + DEF_KEY_MAP(SDL_SCANCODE_RIGHTBRACKET, SEMU_KEY_RIGHTBRACE), + DEF_KEY_MAP(SDL_SCANCODE_RETURN, SEMU_KEY_ENTER), + DEF_KEY_MAP(SDL_SCANCODE_LCTRL, SEMU_KEY_LEFTCTRL), + DEF_KEY_MAP(SDL_SCANCODE_A, SEMU_KEY_A), + DEF_KEY_MAP(SDL_SCANCODE_S, SEMU_KEY_S), + DEF_KEY_MAP(SDL_SCANCODE_D, SEMU_KEY_D), + DEF_KEY_MAP(SDL_SCANCODE_F, SEMU_KEY_F), + DEF_KEY_MAP(SDL_SCANCODE_G, SEMU_KEY_G), + DEF_KEY_MAP(SDL_SCANCODE_H, SEMU_KEY_H), + DEF_KEY_MAP(SDL_SCANCODE_J, SEMU_KEY_J), + DEF_KEY_MAP(SDL_SCANCODE_K, SEMU_KEY_K), + DEF_KEY_MAP(SDL_SCANCODE_L, SEMU_KEY_L), + DEF_KEY_MAP(SDL_SCANCODE_SEMICOLON, SEMU_KEY_SEMICOLON), + DEF_KEY_MAP(SDL_SCANCODE_APOSTROPHE, SEMU_KEY_APOSTROPHE), + DEF_KEY_MAP(SDL_SCANCODE_GRAVE, SEMU_KEY_GRAVE), + DEF_KEY_MAP(SDL_SCANCODE_LSHIFT, SEMU_KEY_LEFTSHIFT), + DEF_KEY_MAP(SDL_SCANCODE_BACKSLASH, SEMU_KEY_BACKSLASH), + DEF_KEY_MAP(SDL_SCANCODE_Z, SEMU_KEY_Z), + DEF_KEY_MAP(SDL_SCANCODE_X, SEMU_KEY_X), + DEF_KEY_MAP(SDL_SCANCODE_C, SEMU_KEY_C), + DEF_KEY_MAP(SDL_SCANCODE_V, SEMU_KEY_V), + DEF_KEY_MAP(SDL_SCANCODE_B, SEMU_KEY_B), + DEF_KEY_MAP(SDL_SCANCODE_N, SEMU_KEY_N), + DEF_KEY_MAP(SDL_SCANCODE_M, SEMU_KEY_M), + DEF_KEY_MAP(SDL_SCANCODE_COMMA, SEMU_KEY_COMMA), + DEF_KEY_MAP(SDL_SCANCODE_PERIOD, SEMU_KEY_DOT), + DEF_KEY_MAP(SDL_SCANCODE_SLASH, SEMU_KEY_SLASH), + DEF_KEY_MAP(SDL_SCANCODE_RSHIFT, SEMU_KEY_RIGHTSHIFT), + DEF_KEY_MAP(SDL_SCANCODE_LALT, SEMU_KEY_LEFTALT), + DEF_KEY_MAP(SDL_SCANCODE_SPACE, SEMU_KEY_SPACE), + DEF_KEY_MAP(SDL_SCANCODE_CAPSLOCK, SEMU_KEY_CAPSLOCK), + DEF_KEY_MAP(SDL_SCANCODE_F1, SEMU_KEY_F1), + DEF_KEY_MAP(SDL_SCANCODE_F2, SEMU_KEY_F2), + DEF_KEY_MAP(SDL_SCANCODE_F3, SEMU_KEY_F3), + DEF_KEY_MAP(SDL_SCANCODE_F4, SEMU_KEY_F4), + DEF_KEY_MAP(SDL_SCANCODE_F5, SEMU_KEY_F5), + DEF_KEY_MAP(SDL_SCANCODE_F6, SEMU_KEY_F6), + DEF_KEY_MAP(SDL_SCANCODE_F7, SEMU_KEY_F7), + DEF_KEY_MAP(SDL_SCANCODE_F8, SEMU_KEY_F8), + DEF_KEY_MAP(SDL_SCANCODE_F9, SEMU_KEY_F9), + DEF_KEY_MAP(SDL_SCANCODE_F10, SEMU_KEY_F10), + DEF_KEY_MAP(SDL_SCANCODE_NUMLOCKCLEAR, SEMU_KEY_NUMLOCK), + DEF_KEY_MAP(SDL_SCANCODE_SCROLLLOCK, SEMU_KEY_SCROLLLOCK), + DEF_KEY_MAP(SDL_SCANCODE_KP_7, SEMU_KEY_KP7), + DEF_KEY_MAP(SDL_SCANCODE_KP_8, SEMU_KEY_KP8), + DEF_KEY_MAP(SDL_SCANCODE_KP_9, SEMU_KEY_KP9), + DEF_KEY_MAP(SDL_SCANCODE_KP_MULTIPLY, SEMU_KEY_KPASTERISK), + DEF_KEY_MAP(SDL_SCANCODE_KP_MINUS, SEMU_KEY_KPMINUS), + DEF_KEY_MAP(SDL_SCANCODE_KP_4, SEMU_KEY_KP4), + DEF_KEY_MAP(SDL_SCANCODE_KP_5, SEMU_KEY_KP5), + DEF_KEY_MAP(SDL_SCANCODE_KP_6, SEMU_KEY_KP6), + DEF_KEY_MAP(SDL_SCANCODE_KP_PLUS, SEMU_KEY_KPPLUS), + DEF_KEY_MAP(SDL_SCANCODE_KP_1, SEMU_KEY_KP1), + DEF_KEY_MAP(SDL_SCANCODE_KP_2, SEMU_KEY_KP2), + DEF_KEY_MAP(SDL_SCANCODE_KP_3, SEMU_KEY_KP3), + DEF_KEY_MAP(SDL_SCANCODE_KP_0, SEMU_KEY_KP0), + DEF_KEY_MAP(SDL_SCANCODE_KP_PERIOD, SEMU_KEY_KPDOT), + DEF_KEY_MAP(SDL_SCANCODE_F11, SEMU_KEY_F11), + DEF_KEY_MAP(SDL_SCANCODE_F12, SEMU_KEY_F12), + DEF_KEY_MAP(SDL_SCANCODE_KP_ENTER, SEMU_KEY_KPENTER), + DEF_KEY_MAP(SDL_SCANCODE_KP_DIVIDE, SEMU_KEY_KPSLASH), + DEF_KEY_MAP(SDL_SCANCODE_RCTRL, SEMU_KEY_RIGHTCTRL), + DEF_KEY_MAP(SDL_SCANCODE_RALT, SEMU_KEY_RIGHTALT), + DEF_KEY_MAP(SDL_SCANCODE_HOME, SEMU_KEY_HOME), + DEF_KEY_MAP(SDL_SCANCODE_UP, SEMU_KEY_UP), + DEF_KEY_MAP(SDL_SCANCODE_PAGEUP, SEMU_KEY_PAGEUP), + DEF_KEY_MAP(SDL_SCANCODE_LEFT, SEMU_KEY_LEFT), + DEF_KEY_MAP(SDL_SCANCODE_RIGHT, SEMU_KEY_RIGHT), + DEF_KEY_MAP(SDL_SCANCODE_END, SEMU_KEY_END), + DEF_KEY_MAP(SDL_SCANCODE_DOWN, SEMU_KEY_DOWN), + DEF_KEY_MAP(SDL_SCANCODE_PAGEDOWN, SEMU_KEY_PAGEDOWN), + DEF_KEY_MAP(SDL_SCANCODE_INSERT, SEMU_KEY_INSERT), + DEF_KEY_MAP(SDL_SCANCODE_DELETE, SEMU_KEY_DELETE), +}; + +/* Mouse button mapping uses SDL button IDs, not scancodes */ +static int sdl_button_to_linux_key(int sdl_button) +{ + switch (sdl_button) { + case SDL_BUTTON_LEFT: + return SEMU_BTN_LEFT; + case SDL_BUTTON_RIGHT: + return SEMU_BTN_RIGHT; + case SDL_BUTTON_MIDDLE: + return SEMU_BTN_MIDDLE; + default: + return -1; + } +} + +/* TODO: The current implementation has an O(n) time complexity, which should be + * optimizable using a hash table or some lookup table. + */ +static int sdl_scancode_to_linux_key(int sdl_scancode) +{ + unsigned long key_cnt = sizeof(key_map) / sizeof(struct key_map_entry); + for (unsigned long i = 0; i < key_cnt; i++) + if (sdl_scancode == key_map[i].sdl_scancode) + return key_map[i].linux_key; + + return -1; +} + +int window_events_thread(void *data) +{ + (void) data; + + int linux_key; + + while (1) { + SDL_Event e; + if (!SDL_WaitEvent(&e)) + continue; + + switch (e.type) { + case SDL_QUIT: + exit(0); + case SDL_KEYDOWN: + linux_key = sdl_scancode_to_linux_key(e.key.keysym.scancode); + if (linux_key >= 0) + virtio_input_update_key(linux_key, 1); + break; + case SDL_KEYUP: + linux_key = sdl_scancode_to_linux_key(e.key.keysym.scancode); + if (linux_key >= 0) + virtio_input_update_key(linux_key, 0); + break; + case SDL_MOUSEBUTTONDOWN: + linux_key = sdl_button_to_linux_key(e.button.button); + if (linux_key >= 0) + virtio_input_update_mouse_button_state(linux_key, true); + break; + case SDL_MOUSEBUTTONUP: + linux_key = sdl_button_to_linux_key(e.button.button); + if (linux_key >= 0) + virtio_input_update_mouse_button_state(linux_key, false); + break; + case SDL_MOUSEMOTION: + virtio_input_update_cursor(e.motion.x, e.motion.y); + break; + } + } +} diff --git a/window-sw.c b/window-sw.c index 876b29da..98ae854c 100644 --- a/window-sw.c +++ b/window-sw.c @@ -35,6 +35,7 @@ struct display_info { SDL_mutex *img_mtx; SDL_cond *img_cond; SDL_Thread *win_thread; + SDL_Thread *ev_thread; SDL_Window *window; SDL_Renderer *renderer; }; @@ -72,6 +73,12 @@ static int window_thread(void *data) SDL_RenderClear(display->renderer); SDL_RenderPresent(display->renderer); +#if SEMU_HAS(VIRTIOINPUT) + /* Create event handling thread */ + ((struct display_info *) data)->ev_thread = + SDL_CreateThread(window_events_thread, NULL, data); +#endif + SDL_Surface *surface; while (1) { diff --git a/window.h b/window.h index e0c85ea0..8e2c38a8 100644 --- a/window.h +++ b/window.h @@ -19,4 +19,8 @@ struct window_backend { void (*cursor_move)(int scanout_id, int x, int y); }; +#if SEMU_HAS(VIRTIOINPUT) +int window_events_thread(void *data); +#endif + #endif From d4b318a77926020f561362a02f912babf3e73291 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Sun, 22 Feb 2026 20:21:35 +0800 Subject: [PATCH 4/9] Add buildroot config Update Buildroot configuration to include dependencies required for basic DRM userspace (e.g., 'libdrm') and for the host window path (SDL2). Co-authored-by: Shengwen Cheng --- configs/buildroot.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/configs/buildroot.config b/configs/buildroot.config index 315fccf5..feea8283 100644 --- a/configs/buildroot.config +++ b/configs/buildroot.config @@ -39,6 +39,8 @@ BR2_FORTIFY_SOURCE_1=y BR2_PACKAGE_ALSA_UTILS=y BR2_PACKAGE_ALSA_UTILS_APLAY=y BR2_PACKAGE_ALSA_UTILS_SPEAKER_TEST=y +BR2_PACKAGE_LIBDRM=y +BR2_PACKAGE_LIBDRM_INSTALL_TESTS=y # BR2_PACKAGE_URANDOM_SCRIPTS is not set BR2_TARGET_ROOTFS_CPIO=y BR2_TARGET_ROOTFS_CPIO_FULL=y From 7de08660246725fe271c1d46d2b1c4217897916a Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Sun, 22 Feb 2026 20:23:12 +0800 Subject: [PATCH 5/9] Add linux configs Update guest kernel configuration to enable virtio-gpu and virtio-input. Co-authored-by: Shengwen Cheng --- configs/linux.config | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/configs/linux.config b/configs/linux.config index b54c82ce..0a32ab3c 100644 --- a/configs/linux.config +++ b/configs/linux.config @@ -759,7 +759,7 @@ CONFIG_INPUT=y # # CONFIG_INPUT_MOUSEDEV is not set # CONFIG_INPUT_JOYDEV is not set -# CONFIG_INPUT_EVDEV is not set +CONFIG_INPUT_EVDEV=y # CONFIG_INPUT_EVBUG is not set # @@ -911,13 +911,19 @@ CONFIG_MFD_SYSCON=y # # Graphics support # -# CONFIG_DRM is not set +CONFIG_DRM=y +CONFIG_DRM_KMS_HELPER=y # CONFIG_DRM_DEBUG_MODESET_LOCK is not set # # ARM devices # # end of ARM devices +CONFIG_DRM_VIRTIO_GPU=y +CONFIG_DRM_VIRTIO_GPU_KMS=y +CONFIG_DRM_PANEL=y +CONFIG_DRM_BRIDGE=y +CONFIG_DRM_PANEL_BRIDGE=y # # Frame buffer Devices @@ -1053,9 +1059,10 @@ CONFIG_VIRTIO_ANCHOR=y CONFIG_VIRTIO=y CONFIG_VIRTIO_MENU=y # CONFIG_VIRTIO_BALLOON is not set -# CONFIG_VIRTIO_INPUT is not set +CONFIG_VIRTIO_INPUT=y CONFIG_VIRTIO_MMIO=y # CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES is not set +CONFIG_VIRTIO_DMA_SHARED_BUFFER=y # CONFIG_VDPA is not set # CONFIG_VHOST_MENU is not set From c385e0263134d59e5aedc8073992f75d727ec5e8 Mon Sep 17 00:00:00 2001 From: Mes Date: Fri, 13 Feb 2026 22:23:34 +0800 Subject: [PATCH 6/9] Move SDL event loop to main-thread for macOS On macOS, GUI-related code must run on the main thread. Previously, we called 'SDL_CreateThread()' inside 'window_init_sw()' in 'window-sw.c' to spawn a new thread running 'window_thread()', and 'window_thread()' then called 'SDL_CreateWindow()'. This fails on macOS because 'SDL_CreateWindow()' must be called from the main thread. In addition, the official SDL documentation [1] notes that most graphics backends are not thread-safe, and that SDL video functions should only be called from the application's main thread. This is especially important on macOS. In QEMU, the vCPU execution thread(s) and the emulator main loop thread are separated. Typically, SDL handling is placed in the emulator main loop (managed via timers and executed sequentially). However, to support different UI backends such as Cocoa, QEMU also provides an option to run the emulator main loop in a background thread, while keeping the UI event loop on the main thread. This commit follows QEMU's approach: it spawns a 'pthread' thread from 'main()' to run the emulator main loop 'semu_run', and starts the SDL-related event loop in the original main thread with a few minor changes to ensure the code runs correctly. Finally, since some environments (such as CI) may not be able to create an SDL window, this commit added a headless mode so that vGPU functionality can still be tested without a GUI using test programs such as DirectFB2. [1]: https://wiki.libsdl.org/SDL3/FAQDevelopment --- Makefile | 4 - main.c | 50 ++++++++- window-events.c | 21 ++-- window-sw.c | 282 ++++++++++++++++++++++++++++++------------------ window.h | 12 ++- 5 files changed, 244 insertions(+), 125 deletions(-) diff --git a/Makefile b/Makefile index c965c8db..b341713e 100644 --- a/Makefile +++ b/Makefile @@ -177,10 +177,6 @@ else override ENABLE_VIRTIOINPUT := 0 endif -# virtio-gpu -ifneq ($(UNAME_S),Linux) - ENABLE_VIRTIOGPU := 0 -endif ifeq ($(ENABLE_VIRTIOGPU),1) OBJS_EXTRA += virtio-gpu.o OBJS_EXTRA += virtio-gpu-sw.o diff --git a/main.c b/main.c index 876896e2..ee243ed6 100644 --- a/main.c +++ b/main.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -1631,6 +1632,24 @@ static int semu_run_debug(emu_state_t *emu) return 0; } +/* Thread wrapper for running emulator in background thread */ +static void *emu_thread_func(void *arg) +{ + emu_state_t *emu = (emu_state_t *) arg; + int ret; + + if (emu->debug) + ret = semu_run_debug(emu); + else + ret = semu_run(emu); + + /* Unblock window_main_loop() on the main thread so it can return */ + if (g_window.window_shutdown) + g_window.window_shutdown(); + + return (void *) (intptr_t) ret; +} + int main(int argc, char **argv) { int ret; @@ -1644,10 +1663,33 @@ int main(int argc, char **argv) signal(SIGTERM, signal_handler_stats); #endif - if (emu.debug) - ret = semu_run_debug(&emu); - else - ret = semu_run(&emu); +#if SEMU_HAS(VIRTIOGPU) + /* If window backend has a main loop function, run emulator in background + * thread and use main thread for window events (required for macOS SDL2). + */ + if (g_window.window_main_loop) { + pthread_t emu_thread; + void *thread_ret; + + if (pthread_create(&emu_thread, NULL, emu_thread_func, &emu) != 0) { + fprintf(stderr, "Failed to create emulator thread\n"); + return 1; + } + + /* Main thread runs window event loop (required for macOS) */ + g_window.window_main_loop(); + + /* Wait for emulator thread to finish */ + pthread_join(emu_thread, &thread_ret); + ret = (int) (intptr_t) thread_ret; + } else +#endif + { + if (emu.debug) + ret = semu_run_debug(&emu); + else + ret = semu_run(&emu); + } #ifdef MMU_CACHE_STATS print_mmu_cache_stats(&emu.vm); diff --git a/window-events.c b/window-events.c index 5bdb6225..4ca63c48 100644 --- a/window-events.c +++ b/window-events.c @@ -1,5 +1,7 @@ #include -#include +#include + +#define SDL_COND_TIMEOUT 1 /* ms */ #include "device.h" #include "virtio-input-codes.h" @@ -146,20 +148,17 @@ static int sdl_scancode_to_linux_key(int sdl_scancode) return -1; } -int window_events_thread(void *data) +bool handle_window_events(void) { - (void) data; - + SDL_Event e; int linux_key; + bool quit = false; - while (1) { - SDL_Event e; - if (!SDL_WaitEvent(&e)) - continue; - + while (SDL_WaitEventTimeout(&e, SDL_COND_TIMEOUT)) { switch (e.type) { case SDL_QUIT: - exit(0); + quit = true; + break; case SDL_KEYDOWN: linux_key = sdl_scancode_to_linux_key(e.key.keysym.scancode); if (linux_key >= 0) @@ -185,4 +184,6 @@ int window_events_thread(void *data) break; } } + + return quit; } diff --git a/window-sw.c b/window-sw.c index 98ae854c..76b74ec4 100644 --- a/window-sw.c +++ b/window-sw.c @@ -18,6 +18,7 @@ enum { struct display_info { /* Request type: primary or cursor */ int render_type; + bool render_pending; /* Primary plane */ struct vgpu_resource_2d resource; @@ -34,151 +35,198 @@ struct display_info { SDL_mutex *img_mtx; SDL_cond *img_cond; - SDL_Thread *win_thread; - SDL_Thread *ev_thread; SDL_Window *window; SDL_Renderer *renderer; }; static struct display_info displays[VIRTIO_GPU_MAX_SCANOUTS]; static int display_cnt; +static bool headless_mode = false; +static bool should_exit = false; -static int window_thread(void *data) +/* Main loop runs on the main thread */ +static void window_main_loop_sw(void) { - struct display_info *display = (struct display_info *) data; - struct vgpu_resource_2d *resource = &display->resource; - struct vgpu_resource_2d *cursor = &display->cursor; - - /* Create SDL window */ - display->window = SDL_CreateWindow("semu", SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, resource->width, - resource->height, SDL_WINDOW_SHOWN); - - if (!display->window) { - fprintf(stderr, "%s(): failed to create window\n", __func__); - exit(2); - } - - /* Create SDL render */ - display->renderer = - SDL_CreateRenderer(display->window, -1, SDL_RENDERER_ACCELERATED); - - if (!display->renderer) { - fprintf(stderr, "%s(): failed to create renderer\n", __func__); - exit(2); - } + if (headless_mode) + return; - /* Render the whole screen with black color */ - SDL_SetRenderDrawColor(display->renderer, 0, 0, 0, 255); - SDL_RenderClear(display->renderer); - SDL_RenderPresent(display->renderer); + SDL_Surface *surface; + while (!should_exit) { #if SEMU_HAS(VIRTIOINPUT) - /* Create event handling thread */ - ((struct display_info *) data)->ev_thread = - SDL_CreateThread(window_events_thread, NULL, data); + /* Handle SDL events */ + if (handle_window_events()) { + should_exit = true; + exit(0); + } #endif - SDL_Surface *surface; - - while (1) { - /* Mutex lock */ - SDL_LockMutex(display->img_mtx); - - /* Wait until the image is arrived */ - while (SDL_CondWaitTimeout(display->img_cond, display->img_mtx, - SDL_COND_TIMEOUT)) - ; - - if (display->render_type == CLEAR_PRIMARY_PLANE) { - /* FIXME */ - /* Set color for clearing */ - SDL_SetRenderDrawColor(display->renderer, 0, 0, 0, 255); - } else if (display->render_type == FLUSH_PRIMARY_PLANE) { - /* Generate primary plane texture */ - surface = SDL_CreateRGBSurfaceWithFormatFrom( - resource->image, resource->width, resource->height, - resource->bits_per_pixel, resource->stride, - display->primary_sdl_format); - - if (surface) { - SDL_DestroyTexture(display->primary_texture); - display->primary_texture = - SDL_CreateTextureFromSurface(display->renderer, surface); - SDL_FreeSurface(surface); - } else { - fprintf(stderr, "Failed to create primary plane surface\n"); - } - } else if (display->render_type == UPDATE_CURSOR_PLANE) { - /* Generate cursor plane texture */ - surface = SDL_CreateRGBSurfaceWithFormatFrom( - cursor->image, cursor->width, cursor->height, CURSOR_BPP, - CURSOR_STRIDE, SDL_PIXELFORMAT_ARGB8888); - - if (surface) { - SDL_DestroyTexture(display->cursor_texture); - display->cursor_texture = - SDL_CreateTextureFromSurface(display->renderer, surface); - SDL_FreeSurface(surface); - } else { - fprintf(stderr, "Failed to create cursor plane surface\n"); + /* Check each display for pending render requests */ + for (int i = 0; i < display_cnt; i++) { + struct display_info *display = &displays[i]; + struct vgpu_resource_2d *resource = &display->resource; + struct vgpu_resource_2d *cursor = &display->cursor; + + /* Mutex lock */ + if (SDL_LockMutex(display->img_mtx) == 0) { + /* Wait until the image is arrived */ + SDL_CondWaitTimeout(display->img_cond, display->img_mtx, + SDL_COND_TIMEOUT); + + if (display->render_pending) { + if (display->render_type == CLEAR_PRIMARY_PLANE) { + /* FIXME */ + /* Set color for clearing */ + SDL_SetRenderDrawColor(display->renderer, 0, 0, 0, 255); + } else if (display->render_type == FLUSH_PRIMARY_PLANE) { + /* Generate primary plane texture */ + surface = SDL_CreateRGBSurfaceWithFormatFrom( + resource->image, resource->width, resource->height, + resource->bits_per_pixel, resource->stride, + display->primary_sdl_format); + + if (surface) { + SDL_DestroyTexture(display->primary_texture); + display->primary_texture = + SDL_CreateTextureFromSurface(display->renderer, + surface); + SDL_FreeSurface(surface); + } else { + fprintf(stderr, + "Failed to create primary plane surface: " + "%s\n", + SDL_GetError()); + } + } else if (display->render_type == UPDATE_CURSOR_PLANE) { + /* Generate cursor plane texture */ + surface = SDL_CreateRGBSurfaceWithFormatFrom( + cursor->image, cursor->width, cursor->height, + CURSOR_BPP * 8, CURSOR_STRIDE, + SDL_PIXELFORMAT_ARGB8888); + + if (surface) { + SDL_DestroyTexture(display->cursor_texture); + display->cursor_texture = + SDL_CreateTextureFromSurface(display->renderer, + surface); + SDL_FreeSurface(surface); + } else { + fprintf(stderr, + "Failed to create cursor plane surface: " + "%s\n", + SDL_GetError()); + } + } else if (display->render_type == CLEAR_CURSOR_PLANE) { + SDL_DestroyTexture(display->cursor_texture); + display->cursor_texture = NULL; + } + + /* Render primary and cursor planes */ + SDL_RenderClear(display->renderer); + + if (display->primary_texture) + SDL_RenderCopy(display->renderer, + display->primary_texture, NULL, NULL); + + if (display->cursor_texture) + SDL_RenderCopy(display->renderer, + display->cursor_texture, NULL, + &display->cursor_rect); + + SDL_RenderPresent(display->renderer); + display->render_pending = false; + } + SDL_UnlockMutex(display->img_mtx); } - } else if (display->render_type == CLEAR_CURSOR_PLANE) { - SDL_DestroyTexture(display->cursor_texture); - display->cursor_texture = NULL; } - - /* Render primary and cursor planes */ - SDL_RenderClear(display->renderer); - - if (display->primary_texture) - SDL_RenderCopy(display->renderer, display->primary_texture, NULL, - NULL); - - if (display->cursor_texture) - SDL_RenderCopy(display->renderer, display->cursor_texture, NULL, - &display->cursor_rect); - - SDL_RenderPresent(display->renderer); - - /* Mutex unlock */ - SDL_UnlockMutex(display->img_mtx); } } -void window_init_sw(void) +static void window_init_sw(void) { if (SDL_Init(SDL_INIT_VIDEO) < 0) { - fprintf(stderr, "%s(): failed to initialize SDL\n", __func__); - exit(2); + fprintf(stderr, + "window_init_sw(): failed to initialize SDL: %s\n" + "Running in headless mode.\n", + SDL_GetError()); + headless_mode = true; + return; } + /* Create windows and renderers on main thread */ for (int i = 0; i < display_cnt; i++) { displays[i].img_mtx = SDL_CreateMutex(); if (!displays[i].img_mtx) { - fprintf(stderr, "%s(): failed to create mutex: %s\n", __func__, - SDL_GetError()); + fprintf(stderr, + "window_init_sw(): failed to create mutex for display %d: " + "%s\n", + i, SDL_GetError()); exit(2); } displays[i].img_cond = SDL_CreateCond(); if (!displays[i].img_cond) { - fprintf(stderr, "%s(): failed to create condition variable: %s\n", - __func__, SDL_GetError()); + fprintf(stderr, + "window_init_sw(): failed to create condition variable for " + "display %d: %s\n", + i, SDL_GetError()); SDL_DestroyMutex(displays[i].img_mtx); exit(2); } - displays[i].win_thread = - SDL_CreateThread(window_thread, NULL, (void *) &displays[i]); - if (!displays[i].win_thread) { - fprintf(stderr, "%s(): failed to create window thread\n", __func__); + /* Create window on main thread (required for macOS) */ + displays[i].window = SDL_CreateWindow( + "semu", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + displays[i].resource.width, displays[i].resource.height, + SDL_WINDOW_SHOWN); + + if (!displays[i].window) { + fprintf(stderr, + "window_init_sw(): failed to create SDL window for display " + "%d: %s\n" + "Possible causes:\n" + " - No display available (headless environment)\n" + " - SDL video driver not supported\n" + " - Insufficient permissions\n" + "Running in headless mode.\n", + i, SDL_GetError()); + headless_mode = true; + return; + } + + /* Create renderer (try accelerated first, fall back to software) */ + displays[i].renderer = SDL_CreateRenderer(displays[i].window, -1, + SDL_RENDERER_ACCELERATED); + + if (!displays[i].renderer) { + fprintf(stderr, + "window_init_sw(): accelerated renderer not available, " + "trying software renderer: %s\n", + SDL_GetError()); + displays[i].renderer = SDL_CreateRenderer(displays[i].window, -1, + SDL_RENDERER_SOFTWARE); + } + + if (!displays[i].renderer) { + fprintf(stderr, + "window_init_sw(): failed to create renderer for display " + "%d: %s\n", + i, SDL_GetError()); exit(2); } - SDL_DetachThread(displays[i].win_thread); + + /* Initialize with black screen */ + SDL_SetRenderDrawColor(displays[i].renderer, 0, 0, 0, 255); + SDL_RenderClear(displays[i].renderer); + SDL_RenderPresent(displays[i].renderer); } } +static void window_shutdown_sw(void) +{ + should_exit = true; +} + static void window_add_sw(uint32_t width, uint32_t height) { if (display_cnt >= VIRTIO_GPU_MAX_SCANOUTS) { @@ -226,6 +274,9 @@ static bool virtio_gpu_to_sdl_format(uint32_t virtio_gpu_format, static void cursor_clear_sw(int scanout_id) { + if (headless_mode) + return; + if (scanout_id >= display_cnt) return; @@ -250,6 +301,7 @@ static void cursor_clear_sw(int scanout_id) /* Trigger plane rendering */ display->render_type = CLEAR_CURSOR_PLANE; + display->render_pending = true; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -258,6 +310,9 @@ static void cursor_clear_sw(int scanout_id) static void cursor_update_sw(int scanout_id, int res_id, int x, int y) { + if (headless_mode) + return; + if (scanout_id >= display_cnt) return; @@ -307,6 +362,7 @@ static void cursor_update_sw(int scanout_id, int res_id, int x, int y) /* Trigger cursor rendering */ display->render_type = UPDATE_CURSOR_PLANE; + display->render_pending = true; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -315,6 +371,9 @@ static void cursor_update_sw(int scanout_id, int res_id, int x, int y) static void cursor_move_sw(int scanout_id, int x, int y) { + if (headless_mode) + return; + if (scanout_id >= display_cnt) return; @@ -333,6 +392,7 @@ static void cursor_move_sw(int scanout_id, int x, int y) /* Trigger cursor rendering */ display->render_type = MOVE_CURSOR_PLANE; + display->render_pending = true; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -341,6 +401,9 @@ static void cursor_move_sw(int scanout_id, int x, int y) static void window_clear_sw(int scanout_id) { + if (headless_mode) + return; + if (scanout_id >= display_cnt) return; @@ -361,6 +424,7 @@ static void window_clear_sw(int scanout_id) /* Trigger primary plane rendering */ display->render_type = CLEAR_PRIMARY_PLANE; + display->render_pending = true; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -369,6 +433,9 @@ static void window_clear_sw(int scanout_id) static void window_flush_sw(int scanout_id, int res_id) { + if (headless_mode) + return; + if (scanout_id >= display_cnt) return; @@ -413,6 +480,7 @@ static void window_flush_sw(int scanout_id, int res_id) /* Trigger primary plane flushing */ display->render_type = FLUSH_PRIMARY_PLANE; + display->render_pending = true; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -428,4 +496,6 @@ const struct window_backend g_window = { .cursor_clear = cursor_clear_sw, .cursor_update = cursor_update_sw, .cursor_move = cursor_move_sw, + .window_main_loop = window_main_loop_sw, + .window_shutdown = window_shutdown_sw, }; diff --git a/window.h b/window.h index 8e2c38a8..2f4e8e9f 100644 --- a/window.h +++ b/window.h @@ -17,10 +17,20 @@ struct window_backend { void (*cursor_clear)(int scanout_id); void (*cursor_update)(int scanout_id, int res_id, int x, int y); void (*cursor_move)(int scanout_id, int x, int y); + /* Main loop function that runs on the main thread (for macOS SDL2). + * If non-NULL, the emulator runs in a background thread while this + * function handles window events on the main thread. + * Returns when the emulator should exit. + */ + void (*window_main_loop)(void); + /* Called from the emulator thread when semu_run() returns, to unblock + * window_main_loop() so the main thread can proceed to pthread_join. + */ + void (*window_shutdown)(void); }; #if SEMU_HAS(VIRTIOINPUT) -int window_events_thread(void *data); +bool handle_window_events(void); #endif #endif From d2da92de0de2b8089059760f676ddd77a4a2c40a Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Thu, 26 Feb 2026 18:24:19 +0800 Subject: [PATCH 7/9] Track primary and cursor plane ops independently The single 'render_type'/'render_pending' pair in struct 'display_info' can lose updates: if the emulator thread signals a 'PRIMARY_FLUSH' and then a CURSOR_MOVE before the main loop wakes up, the second assignment silently overwrites 'render_type', causing the flush to be dropped and the screen to stall. Replace the shared fields with two independent pending state fields: - enum primary_op: primary_pending (NONE / CLEAR / FLUSH) - enum cursor_op: cursor_pending (NONE / CLEAR / UPDATE / MOVE) The main loop now handles both planes in separate if-blocks within the same iteration, calling 'SDL_RenderPresent' once whenever either field is non-NONE. This ensures that a concurrent flush and cursor update are both applied without either being lost. --- window-sw.c | 170 +++++++++++++++++++++++++++------------------------- window.h | 3 - 2 files changed, 89 insertions(+), 84 deletions(-) diff --git a/window-sw.c b/window-sw.c index 76b74ec4..affe7fae 100644 --- a/window-sw.c +++ b/window-sw.c @@ -7,18 +7,22 @@ #define SDL_COND_TIMEOUT 1 /* ms */ -enum { - CLEAR_PRIMARY_PLANE, - FLUSH_PRIMARY_PLANE, - UPDATE_CURSOR_PLANE, - CLEAR_CURSOR_PLANE, - MOVE_CURSOR_PLANE, +enum primary_op { + PRIMARY_NONE, + PRIMARY_CLEAR, + PRIMARY_FLUSH, +}; + +enum cursor_op { + CURSOR_NONE, + CURSOR_CLEAR, + CURSOR_UPDATE, + CURSOR_MOVE, }; struct display_info { - /* Request type: primary or cursor */ - int render_type; - bool render_pending; + enum primary_op primary_pending; + enum cursor_op cursor_pending; /* Primary plane */ struct vgpu_resource_2d resource; @@ -28,7 +32,6 @@ struct display_info { /* Cursor plane */ struct vgpu_resource_2d cursor; - uint32_t cursor_sdl_format; uint32_t *cursor_img; SDL_Rect cursor_rect; /* Cursor size and position */ SDL_Texture *cursor_texture; @@ -73,55 +76,75 @@ static void window_main_loop_sw(void) SDL_CondWaitTimeout(display->img_cond, display->img_mtx, SDL_COND_TIMEOUT); - if (display->render_pending) { - if (display->render_type == CLEAR_PRIMARY_PLANE) { - /* FIXME */ - /* Set color for clearing */ - SDL_SetRenderDrawColor(display->renderer, 0, 0, 0, 255); - } else if (display->render_type == FLUSH_PRIMARY_PLANE) { - /* Generate primary plane texture */ - surface = SDL_CreateRGBSurfaceWithFormatFrom( - resource->image, resource->width, resource->height, - resource->bits_per_pixel, resource->stride, - display->primary_sdl_format); - - if (surface) { - SDL_DestroyTexture(display->primary_texture); - display->primary_texture = - SDL_CreateTextureFromSurface(display->renderer, - surface); - SDL_FreeSurface(surface); - } else { - fprintf(stderr, - "Failed to create primary plane surface: " - "%s\n", - SDL_GetError()); - } - } else if (display->render_type == UPDATE_CURSOR_PLANE) { - /* Generate cursor plane texture */ - surface = SDL_CreateRGBSurfaceWithFormatFrom( - cursor->image, cursor->width, cursor->height, - CURSOR_BPP * 8, CURSOR_STRIDE, - SDL_PIXELFORMAT_ARGB8888); - - if (surface) { - SDL_DestroyTexture(display->cursor_texture); - display->cursor_texture = - SDL_CreateTextureFromSurface(display->renderer, - surface); - SDL_FreeSurface(surface); - } else { - fprintf(stderr, - "Failed to create cursor plane surface: " - "%s\n", - SDL_GetError()); - } - } else if (display->render_type == CLEAR_CURSOR_PLANE) { + bool any_update = display->primary_pending != PRIMARY_NONE || + display->cursor_pending != CURSOR_NONE; + + /* Handle primary plane update */ + if (display->primary_pending == PRIMARY_CLEAR) { + SDL_DestroyTexture(display->primary_texture); + display->primary_texture = NULL; + display->primary_pending = PRIMARY_NONE; + } else if (display->primary_pending == PRIMARY_FLUSH) { + /* Generate primary plane texture */ + surface = SDL_CreateRGBSurfaceWithFormatFrom( + resource->image, resource->width, resource->height, + resource->bits_per_pixel, resource->stride, + display->primary_sdl_format); + + if (surface) { + SDL_DestroyTexture(display->primary_texture); + display->primary_texture = SDL_CreateTextureFromSurface( + display->renderer, surface); + SDL_FreeSurface(surface); + } else { + fprintf(stderr, + "Failed to create primary plane surface: " + "%s\n", + SDL_GetError()); + } + display->primary_pending = PRIMARY_NONE; + } + + /* Handle cursor plane update */ + if (display->cursor_pending == CURSOR_UPDATE) { + /* Cursor data is always treated as ARGB8888 regardless of + * the resource format reported by the guest, following the + * same approach as QEMU (see QEMUCursor: "data format is + * 32bit RGBA"). + * + * In practice the Linux virtio-gpu driver creates dumb + * buffers with B8G8R8X8_UNORM and uses the X byte as alpha + * (0 = transparent), so following the X format would + * discard cursor transparency. + */ + surface = SDL_CreateRGBSurfaceWithFormatFrom( + cursor->image, cursor->width, cursor->height, + cursor->bits_per_pixel, cursor->stride, + SDL_PIXELFORMAT_ARGB8888); + + if (surface) { SDL_DestroyTexture(display->cursor_texture); - display->cursor_texture = NULL; + display->cursor_texture = SDL_CreateTextureFromSurface( + display->renderer, surface); + SDL_FreeSurface(surface); + } else { + fprintf(stderr, + "Failed to create cursor plane surface: " + "%s\n", + SDL_GetError()); } + display->cursor_pending = CURSOR_NONE; + } else if (display->cursor_pending == CURSOR_CLEAR) { + SDL_DestroyTexture(display->cursor_texture); + display->cursor_texture = NULL; + display->cursor_pending = CURSOR_NONE; + } else if (display->cursor_pending == CURSOR_MOVE) { + /* cursor_rect already updated by caller */ + display->cursor_pending = CURSOR_NONE; + } - /* Render primary and cursor planes */ + /* Render both planes when any update is pending */ + if (any_update) { SDL_RenderClear(display->renderer); if (display->primary_texture) @@ -134,7 +157,6 @@ static void window_main_loop_sw(void) &display->cursor_rect); SDL_RenderPresent(display->renderer); - display->render_pending = false; } SDL_UnlockMutex(display->img_mtx); } @@ -291,7 +313,6 @@ static void cursor_clear_sw(int scanout_id) /* Reset cursor information */ memset(&display->cursor_rect, 0, sizeof(SDL_Rect)); - display->cursor_sdl_format = 0; /* Reset cursor resource */ memset(&display->cursor, 0, sizeof(struct vgpu_resource_2d)); @@ -299,9 +320,8 @@ static void cursor_clear_sw(int scanout_id) display->cursor_img = NULL; display->cursor.image = NULL; - /* Trigger plane rendering */ - display->render_type = CLEAR_CURSOR_PLANE; - display->render_pending = true; + /* Trigger cursor plane rendering */ + display->cursor_pending = CURSOR_CLEAR; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -322,15 +342,6 @@ static void cursor_update_sw(int scanout_id, int res_id, int x, int y) return; } - /* Convert virtio-gpu resource format to SDL format */ - uint32_t sdl_format; - bool legal_format = virtio_gpu_to_sdl_format(resource->format, &sdl_format); - - if (!legal_format) { - fprintf(stderr, "%s(): invalid resource format\n", __func__); - return; - } - /* Start of the critical section */ if (SDL_LockMutex(displays[scanout_id].img_mtx) != 0) { fprintf(stderr, "%s(): failed to lock mutex: %s\n", __func__, @@ -344,7 +355,6 @@ static void cursor_update_sw(int scanout_id, int res_id, int x, int y) display->cursor_rect.y = y; display->cursor_rect.w = resource->width; display->cursor_rect.h = resource->height; - display->cursor_sdl_format = sdl_format; /* Cursor resource update */ memcpy(&display->cursor, resource, sizeof(struct vgpu_resource_2d)); @@ -361,8 +371,7 @@ static void cursor_update_sw(int scanout_id, int res_id, int x, int y) memcpy(display->cursor_img, resource->image, pixels_size); /* Trigger cursor rendering */ - display->render_type = UPDATE_CURSOR_PLANE; - display->render_pending = true; + display->cursor_pending = CURSOR_UPDATE; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -391,8 +400,7 @@ static void cursor_move_sw(int scanout_id, int x, int y) display->cursor_rect.y = y; /* Trigger cursor rendering */ - display->render_type = MOVE_CURSOR_PLANE; - display->render_pending = true; + display->cursor_pending = CURSOR_MOVE; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -423,8 +431,7 @@ static void window_clear_sw(int scanout_id) display->resource.image = NULL; /* Trigger primary plane rendering */ - display->render_type = CLEAR_PRIMARY_PLANE; - display->render_pending = true; + display->primary_pending = PRIMARY_CLEAR; SDL_CondSignal(display->img_cond); /* End of the critical section */ @@ -446,10 +453,12 @@ static void window_flush_sw(int scanout_id, int res_id) return; } - /* Convert virtio-gpu resource format to SDL format */ + /* Convert virtio-gpu resource format to SDL format. + * Only the primary plane negotiates format at runtime; the cursor plane + * always renders as ARGB8888 (see cursor_update_sw). + */ uint32_t sdl_format; bool legal_format = virtio_gpu_to_sdl_format(resource->format, &sdl_format); - if (!legal_format) { fprintf(stderr, "%s(): invalid resource format\n", __func__); return; @@ -479,8 +488,7 @@ static void window_flush_sw(int scanout_id, int res_id) memcpy(new_img, resource->image, pixels_size); /* Trigger primary plane flushing */ - display->render_type = FLUSH_PRIMARY_PLANE; - display->render_pending = true; + display->primary_pending = PRIMARY_FLUSH; SDL_CondSignal(display->img_cond); /* End of the critical section */ diff --git a/window.h b/window.h index 2f4e8e9f..97fca861 100644 --- a/window.h +++ b/window.h @@ -5,9 +5,6 @@ #define CURSOR_WIDTH 64 #define CURSOR_HEIGHT 64 -#define CURSOR_BPP 4 /* Bytes per pixel, using ARGB */ -#define CURSOR_STRIDE (CURSOR_WIDTH * CURSOR_BPP) - struct window_backend { void (*window_init)(void); void (*window_add)(uint32_t width, uint32_t height); From abf6e8177e84787abcfad5e26f6edbbb8d8943ae Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Thu, 29 Jan 2026 16:51:40 +0800 Subject: [PATCH 8/9] DO NOT MERGE: Add X11 package build support Add optional X11 support for Buildroot image building: - Add configs/x11.config with X11/Xorg related packages (mesa3d, xorg7, xserver, xterm, etc.) - Add --x11 flag to build-image.sh to enable X11 package merging - Add copy_buildroot_config function to handle config merging with X11 This allows building a Buildroot image with X11 support using: './scripts/build-image.sh --buildroot --x11' Also, the Xserver can be started by the 'startx' command. The X11 packages are optional and not required for basic virtio-gpu functionality with DirectFB2 or other non-X11 display backends. --- configs/x11.config | 35 +++++++++++++++++++++++++++++++++++ scripts/build-image.sh | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 configs/x11.config diff --git a/configs/x11.config b/configs/x11.config new file mode 100644 index 00000000..3cab4aac --- /dev/null +++ b/configs/x11.config @@ -0,0 +1,35 @@ +BR2_TOOLCHAIN_BUILDROOT_CXX=y +BR2_INSTALL_LIBSTDCPP=y +BR2_PACKAGE_GLMARK2=y +BR2_PACKAGE_KMSCUBE=y +BR2_PACKAGE_MESA3D_DEMOS=y +BR2_PACKAGE_MESA3D=y +BR2_PACKAGE_MESA3D_GALLIUM_DRIVER=y +BR2_PACKAGE_MESA3D_DRIVER=y +BR2_PACKAGE_MESA3D_NEEDS_X11=y +BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_SWRAST=y +BR2_PACKAGE_MESA3D_GALLIUM_DRIVER_VIRGL=n +BR2_PACKAGE_MESA3D_GBM=y +BR2_PACKAGE_MESA3D_OPENGL_GLX=y +BR2_PACKAGE_MESA3D_OPENGL_EGL=y +BR2_PACKAGE_MESA3D_OPENGL_ES=y +BR2_PACKAGE_PROVIDES_LIBGBM="mesa3d" +BR2_PACKAGE_XORG7=y +BR2_PACKAGE_XSERVER_XORG_SERVER=y +BR2_PACKAGE_XSERVER_XORG_SERVER_MODULAR=y +BR2_PACKAGE_XLIB_LIBX11=y +BR2_PACKAGE_XAPP_TWM=y +BR2_PACKAGE_XAPP_XAUTH=y +BR2_PACKAGE_XAPP_XCLOCK=y +BR2_PACKAGE_XAPP_XINIT=y +BR2_PACKAGE_XDRIVER_XF86_INPUT_LIBINPUT=y +BR2_PACKAGE_XTERM=y +BR2_PACKAGE_EUDEV=y +BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y +BR2_PACKAGE_PROVIDES_UDEV="eudev" +BR2_PACKAGE_HAS_UDEV=y +BR2_PACKAGE_LIBGLEW=y +BR2_PACKAGE_HAS_LIBGBM=y +BR2_PACKAGE_HAS_LIBGLES=y +BR2_PACKAGE_LIBINPUT=y +BR2_PACKAGE_LIBDRI2=y diff --git a/scripts/build-image.sh b/scripts/build-image.sh index 0f918f35..9bf7db2d 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -31,6 +31,29 @@ function safe_copy { fi } +function copy_buildroot_config +{ + local buildroot_config="configs/buildroot.config" + local x11_config="configs/x11.config" + local output_config="buildroot/.config" + local merge_tool="buildroot/support/kconfig/merge_config.sh" + + if [ ! -f "$output_config" ]; then + echo "Preparing initial Buildroot config..." + + # Check X11 option + if [[ $BUILD_X11 -eq 1 ]]; then + # Compile Buildroot with X11 + "$merge_tool" -m -r -O buildroot "$buildroot_config" "$x11_config" + else + # Compile Buildroot without X11 + cp -f "$buildroot_config" "$output_config" + fi + else + echo "$output_config already exists, skipping copy" + fi +} + function do_buildroot { if [ ! -d buildroot ]; then @@ -40,7 +63,7 @@ function do_buildroot echo "buildroot/ already exists, skipping clone" fi - safe_copy configs/buildroot.config buildroot/.config + copy_buildroot_config safe_copy configs/busybox.config buildroot/busybox.config cp -f target/init buildroot/fs/cpio/init @@ -90,10 +113,11 @@ function do_linux function show_help { cat << EOF -Usage: $0 [--buildroot] [--linux] [--extra-packages] [--all] [--external-root] [--clean-build] [--help] +Usage: $0 [--buildroot] [--x11] [--linux] [--extra-packages] [--all] [--external-root] [--clean-build] [--help] Options: --buildroot Build Buildroot rootfs + --x11 Build Buildroot with X11 --extra-packages Build extra packages along with Buildroot --linux Build Linux kernel --all Build both Buildroot and Linux kernel @@ -105,6 +129,7 @@ EOF } BUILD_BUILDROOT=0 +BUILD_X11=0 BUILD_EXTRA_PACKAGES=0 BUILD_LINUX=0 EXTERNAL_ROOT=0 @@ -115,6 +140,9 @@ while [[ $# -gt 0 ]]; do --buildroot) BUILD_BUILDROOT=1 ;; + --x11) + BUILD_X11=1 + ;; --extra-packages) BUILD_EXTRA_PACKAGES=1 ;; @@ -203,6 +231,11 @@ if [[ $BUILD_EXTRA_PACKAGES -eq 1 && $BUILD_BUILDROOT -eq 0 ]]; then show_help fi +if [[ $BUILD_X11 -eq 1 && $BUILD_BUILDROOT -eq 0 ]]; then + echo "Error: --x11 requires --buildroot to be specified." + show_help +fi + if [[ $CLEAN_BUILD -eq 1 && $BUILD_BUILDROOT -eq 1 && -d buildroot ]]; then echo "Removing buildroot/ for clean build..." rm -rf buildroot From f5478f4b84805cdc4c2c73bebd714418db2ebd84 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Thu, 5 Feb 2026 17:56:58 +0800 Subject: [PATCH 9/9] DO NOT MERGE: Add Image for CI testing Since the pull request has not been finalized yet, the corresponding 'Image' and 'ext4.img' have not been uploaded to the blob. Therefore, this commit is added to allow the CI tests to run. In addition, due to GitHub Actions caching behavior, we found that the 'Image' must be deleted and re-downloaded during testing for the new 'Image' to take effect. To address this, this commit adds an extra step in the script to remove the 'Image' and download it again. This commit will be removed once the code review is completed. --- .ci/test-vgpu.sh | 4 ++++ .ci/test-vinput.sh | 4 ++++ mk/external.mk | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.ci/test-vgpu.sh b/.ci/test-vgpu.sh index 87c90d1e..88efd873 100755 --- a/.ci/test-vgpu.sh +++ b/.ci/test-vgpu.sh @@ -34,6 +34,10 @@ echo "Decompressing ext4.img.bz2 for DirectFB testing..." rm -f ext4.img bunzip2 -kf ext4.img.bz2 +# Force fresh download of Image and rootfs.cpio to avoid stale cache +rm -f Image rootfs.cpio +make Image rootfs.cpio + # NOTE: We want to capture the expect exit code and map # it to our MESSAGES array for meaningful error output. # Temporarily disable errexit for the expect call. diff --git a/.ci/test-vinput.sh b/.ci/test-vinput.sh index de6fb0c6..b1c1b51d 100755 --- a/.ci/test-vinput.sh +++ b/.ci/test-vinput.sh @@ -15,6 +15,10 @@ esac cleanup trap cleanup EXIT +# Force fresh download of Image and rootfs.cpio to avoid stale cache +rm -f Image rootfs.cpio +make Image rootfs.cpio + # NOTE: We want to capture the expect exit code and map # it to our MESSAGES array for meaningful error output. # Temporarily disable errexit for the expect call. diff --git a/mk/external.mk b/mk/external.mk index c2db02ec..bc27d8f1 100644 --- a/mk/external.mk +++ b/mk/external.mk @@ -3,12 +3,12 @@ # _DATA : the file to be read by specific executable. # _DATA_SHA1 : the checksum of the content in _DATA -COMMON_URL = https://github.com/sysprog21/semu/raw/blob +COMMON_URL = https://github.com/Mes0903/semu/raw/blob # kernel KERNEL_DATA_URL = $(COMMON_URL)/Image.bz2 KERNEL_DATA = Image -KERNEL_DATA_SHA1 = 3c0dcfbae504444a7decfaed97b31cbf3dfa2fef +KERNEL_DATA_SHA1 = 0666dbb915cdb2275c73183caa45524b57e5ea7d # initrd INITRD_DATA_URL = $(COMMON_URL)/rootfs.cpio.bz2