diff --git a/.ci/test-vgpu.sh b/.ci/test-vgpu.sh new file mode 100755 index 00000000..88efd873 --- /dev/null +++ b/.ci/test-vgpu.sh @@ -0,0 +1,161 @@ +#!/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 + +# 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. +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/.ci/test-vinput.sh b/.ci/test-vinput.sh new file mode 100755 index 00000000..b1c1b51d --- /dev/null +++ b/.ci/test-vinput.sh @@ -0,0 +1,66 @@ +#!/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 + +# 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. +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/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..e9ae53aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,6 +73,18 @@ 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 + - name: virtio-input test + run: .ci/test-vinput.sh + shell: bash + timeout-minutes: 5 + env: + SDL_VIDEODRIVER: offscreen semu-macOS: runs-on: macos-latest @@ -126,6 +138,18 @@ 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 + - 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/.gitignore b/.gitignore index b56a9aa8..b65332e8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,19 @@ semu Image ext4.img rootfs.cpio +rootfs_full.cpio # intermediate riscv-harts.dtsi +.smp_stamp + +# Build directories +buildroot/ +linux/ +rootfs/ +directfb/ +extra_packages/ + +# DirectFB build +DirectFB2/ +DirectFB-examples/ diff --git a/Makefile b/Makefile index 0fcd4f27..b341713e 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,41 @@ 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 and virtio-input if SDL is not set + override ENABLE_VIRTIOGPU := 0 + override ENABLE_VIRTIOINPUT := 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) + +# 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/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 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 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/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/device.h b/device.h index 88baea83..bdc40f2e 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,114 @@ 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) */ + +/* 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 @@ -442,16 +553,23 @@ 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 +#if SEMU_HAS(VIRTIOINPUT) + virtio_input_state_t vkeyboard; + virtio_input_state_t vmouse; +#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..1b30eb22 100644 --- a/feature.h +++ b/feature.h @@ -22,5 +22,15 @@ #define SEMU_FEATURE_VIRTIOFS 1 #endif +/* virtio-gpu */ +#ifndef SEMU_FEATURE_VIRTIOGPU +#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 82e1433e..ee243ed6 100644 --- a/main.c +++ b/main.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -27,11 +28,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 +107,40 @@ 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 + +#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); @@ -197,6 +235,16 @@ 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 +#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 } } @@ -249,17 +297,33 @@ 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 +#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 } } @@ -315,26 +379,41 @@ 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 +#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 } } @@ -822,6 +901,20 @@ 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 +#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; @@ -1539,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; @@ -1552,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/minimal.dts b/minimal.dts index c2d412c0..0738066b 100644 --- a/minimal.dts +++ b/minimal.dts @@ -87,5 +87,27 @@ interrupts = <6>; }; #endif + +#if SEMU_FEATURE_VIRTIOGPU + gpu0: virtio@4900000 { + compatible = "virtio,mmio"; + reg = <0x4900000 0x200>; + 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/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 diff --git a/scripts/build-image.sh b/scripts/build-image.sh index 0b977a4e..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,10 +63,10 @@ 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 - + # 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 +76,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 +113,14 @@ function do_linux function show_help { cat << EOF -Usage: $0 [--buildroot] [--linux] [--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 + --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 +129,8 @@ EOF } BUILD_BUILDROOT=0 +BUILD_X11=0 +BUILD_EXTRA_PACKAGES=0 BUILD_LINUX=0 EXTERNAL_ROOT=0 CLEAN_BUILD=0 @@ -109,6 +140,12 @@ while [[ $# -gt 0 ]]; do --buildroot) BUILD_BUILDROOT=1 ;; + --x11) + BUILD_X11=1 + ;; + --extra-packages) + BUILD_EXTRA_PACKAGES=1 + ;; --linux) BUILD_LINUX=1 ;; @@ -133,11 +170,72 @@ 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 [[ $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 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} 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-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/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-events.c b/window-events.c new file mode 100644 index 00000000..4ca63c48 --- /dev/null +++ b/window-events.c @@ -0,0 +1,189 @@ +#include +#include + +#define SDL_COND_TIMEOUT 1 /* ms */ + +#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; +} + +bool handle_window_events(void) +{ + SDL_Event e; + int linux_key; + bool quit = false; + + while (SDL_WaitEventTimeout(&e, SDL_COND_TIMEOUT)) { + switch (e.type) { + case SDL_QUIT: + quit = true; + break; + 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; + } + } + + return quit; +} diff --git a/window-sw.c b/window-sw.c new file mode 100644 index 00000000..affe7fae --- /dev/null +++ b/window-sw.c @@ -0,0 +1,509 @@ +#include +#include + +#include "device.h" +#include "virtio-gpu.h" +#include "window.h" + +#define SDL_COND_TIMEOUT 1 /* ms */ + +enum primary_op { + PRIMARY_NONE, + PRIMARY_CLEAR, + PRIMARY_FLUSH, +}; + +enum cursor_op { + CURSOR_NONE, + CURSOR_CLEAR, + CURSOR_UPDATE, + CURSOR_MOVE, +}; + +struct display_info { + enum primary_op primary_pending; + enum cursor_op cursor_pending; + + /* 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_img; + SDL_Rect cursor_rect; /* Cursor size and position */ + SDL_Texture *cursor_texture; + + SDL_mutex *img_mtx; + SDL_cond *img_cond; + 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; + +/* Main loop runs on the main thread */ +static void window_main_loop_sw(void) +{ + if (headless_mode) + return; + + SDL_Surface *surface; + + while (!should_exit) { +#if SEMU_HAS(VIRTIOINPUT) + /* Handle SDL events */ + if (handle_window_events()) { + should_exit = true; + exit(0); + } +#endif + + /* 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); + + 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 = 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 both planes when any update is pending */ + if (any_update) { + 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); + } + SDL_UnlockMutex(display->img_mtx); + } + } + } +} + +static void window_init_sw(void) +{ + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + 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, + "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, + "window_init_sw(): failed to create condition variable for " + "display %d: %s\n", + i, SDL_GetError()); + SDL_DestroyMutex(displays[i].img_mtx); + exit(2); + } + + /* 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); + } + + /* 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) { + 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 (headless_mode) + return; + + 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)); + + /* 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 cursor plane rendering */ + display->cursor_pending = CURSOR_CLEAR; + 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 (headless_mode) + return; + + 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; + } + + /* 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; + + /* 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->cursor_pending = CURSOR_UPDATE; + 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 (headless_mode) + return; + + 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->cursor_pending = CURSOR_MOVE; + 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 (headless_mode) + return; + + 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->primary_pending = PRIMARY_CLEAR; + 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 (headless_mode) + return; + + 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. + * 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; + } + + /* 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->primary_pending = PRIMARY_FLUSH; + 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, + .window_main_loop = window_main_loop_sw, + .window_shutdown = window_shutdown_sw, +}; diff --git a/window.h b/window.h new file mode 100644 index 00000000..97fca861 --- /dev/null +++ b/window.h @@ -0,0 +1,33 @@ +#pragma once + +#if SEMU_HAS(VIRTIOGPU) +/* Cursor size is always 64*64 in VirtIO GPU */ +#define CURSOR_WIDTH 64 +#define CURSOR_HEIGHT 64 + +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); + /* 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) +bool handle_window_events(void); +#endif + +#endif