From 9152699fe9040e05209b2d2836b383c2ae1b8ab0 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Sun, 22 Feb 2026 20:23:12 +0800 Subject: [PATCH 1/6] Add linux configs Update guest kernel configuration to enable virtio-input. Co-authored-by: Shengwen Cheng --- configs/linux.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configs/linux.config b/configs/linux.config index b54c82ce..3adeccda 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 # @@ -1053,7 +1053,7 @@ 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_VDPA is not set From 399ec534d6b0cf55f8e0ddb7707562e1c2adad70 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Fri, 20 Mar 2026 23:28:16 +0800 Subject: [PATCH 2/6] Implement virtio-input for keyboard and mouse Add virtio-input keyboard and mouse devices and connect them to the SDL frontend so host input can reach the guest. Introducing window-driven input requires more than just another virtio device. Once SDL owns the frontend, the emulator needs a threading model that keeps video handling on the process main thread, lets the emulator continue running while the window loop is active, and still allows a window-close request to wake a blocked SMP 'poll(-1)' path. The same shutdown path also needs to work in both normal execution and 'gdbstub'-driven execution. There are a few ways to structure this. One option is to keep the older execution model and bolt the SDL loop onto it. In practice that is a poor fit. The official SDL documentation [1] notes that most graphics backends are not thread-safe and that SDL video functions should only be called from the application's main thread. That matters especially on macOS, and a plain shared close flag is still not enough to unblock an idle emulator. The more workable structure is to reserve the original main thread for the window loop and move 'semu_run()' / 'semu_run_debug()' to a worker thread, with an explicit wakeup path from the frontend back into the emulator. This commit takes that approach. The main thread now stays in the window backend when one is present, while the emulator runs on a worker thread. Shutdown is completed with a wake pipe so closing the window can break a blocked SMP poll and let both sides unwind cleanly. At the device level, this adds virtio-input keyboard and mouse support, including MMIO state, configuration handling, event injection, and integration with the existing virtio-mmio framework. The advertised capabilities are tightened to match what the frontend can actually generate. The mouse is modeled as a relative pointing device with 'REL_X'/'REL_Y' and wheel axes rather than an absolute pointer. On the frontend side, SDL input is translated into Linux input events for keyboard, mouse buttons, relative mouse motion, and wheel scrolling. Repeated SDL keydown events are dropped once 'EV_REP' is advertised so the guest remains responsible for key-repeat timing. Mouse clicks also enter SDL relative mouse mode, and 'Ctrl+Alt+G' releases that grab again so continuous relative motion can be delivered to the guest. At this stage the SDL thread still calls the virtio-input update helpers directly, so shared device state remains protected by 'virtio_input_mutex'. A later commit moves that ownership boundary onto the emulator thread. The guest-side enumeration path is covered by a new virtio-input smoke test in CI, and manual testing still confirms end-to-end event delivery through the guest evdev interfaces. [1]: https://wiki.libsdl.org/SDL3/FAQDevelopment Co-authored-by: Shengwen Cheng --- .ci/test-vinput.sh | 73 +++ .github/actions/setup-semu/action.yml | 5 +- .github/workflows/main.yml | 8 + Makefile | 23 + README.md | 4 +- device.h | 81 +++ feature.h | 5 + main.c | 253 +++++++- minimal.dts | 14 + virtio-input-codes.h | 193 ++++++ virtio-input-event.c | 239 +++++++ virtio-input-event.h | 35 + virtio-input.c | 882 ++++++++++++++++++++++++++ virtio.h | 6 + window-sw.c | 160 +++++ window.h | 43 ++ 16 files changed, 1995 insertions(+), 29 deletions(-) create mode 100755 .ci/test-vinput.sh create mode 100644 virtio-input-codes.h create mode 100644 virtio-input-event.c create mode 100644 virtio-input-event.h create mode 100644 virtio-input.c create mode 100644 window-sw.c create mode 100644 window.h diff --git a/.ci/test-vinput.sh b/.ci/test-vinput.sh new file mode 100755 index 00000000..7c0dae15 --- /dev/null +++ b/.ci/test-vinput.sh @@ -0,0 +1,73 @@ +#!/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=1200 + ;; +esac + +cleanup +trap cleanup EXIT + +# Feature toggles are passed through environment variables, which do not +# participate in make's normal dependency tracking. Force a rebuild here so +# one-feature-at-a-time test runs never reuse a stale semu binary. +make -B semu minimal.dtb + +# 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. +# Print a concrete status marker that is not present in the echoed command text. +expect "# " { send "if ls /dev/input/event* >/dev/null 2>&1; then status=OK; else status=BAD; fi; printf \"__EVT_%s__\\n\" \"\$status\"\r" } +expect { + -exact "__EVT_OK__" {} + -exact "__EVT_BAD__" { exit 3 } + timeout { exit 3 } +} + +expect "# " { send "cat /proc/bus/input/devices | head -20\r" } +# Emit a shell-expanded status marker so expect cannot match the echoed command. +expect "# " { send "if grep -qi virtio /proc/bus/input/devices; then status=OK; else status=BAD; fi; printf \"__VPROC_%s__\\n\" \"\$status\"\r" } +expect { + -exact "__VPROC_OK__" {} + -exact "__VPROC_BAD__" { exit 3 } + 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..8ee56604 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 b36afa68..dc05f36c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,6 +73,10 @@ jobs: run: .ci/test-sound.sh shell: bash timeout-minutes: 5 + - name: virtio-input test + run: .ci/test-vinput.sh + shell: bash + timeout-minutes: 5 semu-macOS: runs-on: macos-latest @@ -126,6 +130,10 @@ jobs: run: .ci/test-sound.sh shell: bash timeout-minutes: 20 + - name: virtio-input test + run: .ci/test-vinput.sh + shell: bash + timeout-minutes: 20 coding_style: runs-on: ubuntu-24.04 diff --git a/Makefile b/Makefile index ea2a3491..00ba4c51 100644 --- a/Makefile +++ b/Makefile @@ -158,6 +158,29 @@ LDFLAGS += -lm # after git submodule. .DEFAULT_GOAL := all +# 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-input if SDL is not set + override ENABLE_VIRTIOINPUT := 0 +endif + +# virtio-input +ENABLE_VIRTIOINPUT ?= 1 +$(call set-feature, VIRTIOINPUT) +ifeq ($(call has, VIRTIOINPUT), 1) + OBJS_EXTRA += virtio-input-event.o + OBJS_EXTRA += virtio-input.o + OBJS_EXTRA += window-sw.o +endif + BIN = semu all: $(BIN) minimal.dtb diff --git a/README.md b/README.md index c773bcf4..4ed3c674 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A minimalist RISC-V system emulator capable of running Linux the kernel and corr - UART: 8250/16550 - PLIC (platform-level interrupt controller): 32 interrupts, no priority - Standard SBI, with the timer extension -- Three types of I/O support using VirtIO standard: +- Four types of I/O support using VirtIO standard: - virtio-blk acquires disk image from the host. - virtio-net is mapped as TAP interface. - virtio-snd uses [PortAudio](https://github.com/PortAudio/portaudio) for sound playback on the host with one limitations: @@ -18,6 +18,8 @@ A minimalist RISC-V system emulator capable of running Linux the kernel and corr the program cannot write the PCM frames into guest OS ALSA stack. - For instance, the following buffer/period size settings on `aplay` has been tested with broken and stutter effects yet complete with no any errors: `aplay --buffer-size=32768 --period-size=4096 /usr/share/sounds/alsa/Front_Center.wav`. + - virtio-input exposes SDL-backed keyboard and mouse devices to the guest. + - You can exit the SDL window by pressing Ctrl+A+G ## Prerequisites diff --git a/device.h b/device.h index 62cf66b6..6cb17e8d 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, @@ -229,6 +232,66 @@ void virtio_rng_write(hart_t *vm, void virtio_rng_init(void); #endif /* SEMU_HAS(VIRTIORNG) */ +/* VirtIO Input */ + +#if SEMU_HAS(VIRTIOINPUT) + +#define IRQ_VINPUT_KEYBOARD 7 +#define IRQ_VINPUT_KEYBOARD_BIT (1 << IRQ_VINPUT_KEYBOARD) + +#define IRQ_VINPUT_MOUSE 8 +#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 ev_value); +void virtio_input_update_mouse_button_state(uint32_t button, bool pressed); +void virtio_input_update_mouse_motion(int32_t dx, int32_t dy); +void virtio_input_update_scroll(int32_t dx, int32_t dy); + +/* Returns true if the device has a pending interrupt. Safe to call from + * the emulator thread without holding any lock internally. + */ +bool virtio_input_irq_pending(virtio_input_state_t *vinput); +#endif /* SEMU_HAS(VIRTIOINPUT) */ + /* ACLINT MTIMER */ typedef struct { /* A MTIMER device has two separate base addresses: one for the MTIME @@ -433,6 +496,7 @@ bool virtio_fs_init(virtio_fs_state_t *vfs, char *mtag, char *dir); /* memory mapping */ typedef struct { + int exit_code; bool debug; bool stopped; uint32_t *ram; @@ -459,6 +523,23 @@ typedef struct { #if SEMU_HAS(VIRTIOFS) virtio_fs_state_t vfs; #endif +#if SEMU_HAS(VIRTIOINPUT) + virtio_input_state_t vkeyboard; + virtio_input_state_t vmouse; + /* Use self-pipe trick to unblock the emulator loop when the + * window closes. When all harts are idle, semu_run() calls + * poll(-1) and blocks indefinitely waiting for timer or UART + * events. window_shutdown_sw() runs on the window-event thread + * and has no way to wake that blocked poll() other than writing + * to a file descriptor it is watching. + * + * wake_fd[0] (read end) is added to pfds[] so poll() monitors it. + * wake_fd[1] (write end) is handed to window_shutdown_sw(), which + * writes one byte on window close to make wake_fd[0] readable and + * return poll() immediately. + */ + int wake_fd[2]; +#endif uint32_t peripheral_update_ctr; diff --git a/feature.h b/feature.h index 850ef89f..b2cda1ed 100644 --- a/feature.h +++ b/feature.h @@ -22,5 +22,10 @@ #define SEMU_FEATURE_VIRTIOFS 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 2d745de2..b528d838 100644 --- a/main.c +++ b/main.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,9 @@ #include "coro.h" #include "device.h" #include "mini-gdbstub/include/gdbstub.h" +#if SEMU_HAS(VIRTIOINPUT) +#include "window.h" +#endif #include "riscv.h" #include "riscv_private.h" #define PRIV(x) ((emu_state_t *) x->priv) @@ -112,6 +116,28 @@ static void emu_update_vrng_interrupts(vm_t *vm) } #endif +#if SEMU_HAS(VIRTIOINPUT) +static void emu_update_vinput_keyboard_interrupts(vm_t *vm) +{ + emu_state_t *data = PRIV(vm->hart[0]); + if (virtio_input_irq_pending(&data->vkeyboard)) + 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 (virtio_input_irq_pending(&data->vmouse)) + 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); @@ -207,6 +233,17 @@ 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(VIRTIOINPUT) + if (virtio_input_irq_pending(&emu->vkeyboard)) + emu_update_vinput_keyboard_interrupts(vm); + + if (virtio_input_irq_pending(&emu->vmouse)) + emu_update_vinput_mouse_interrupts(vm); + + /* A closed window is treated like a frontend shutdown request. */ + if (g_window.window_is_closed()) + emu->stopped = true; #endif } } @@ -270,6 +307,18 @@ static void mem_load(hart_t *hart, case 0x48: /* virtio-fs */ virtio_fs_read(hart, &data->vfs, addr & 0xFFFFF, width, value); return; +#endif +#if SEMU_HAS(VIRTIOINPUT) + case 0x49: /* virtio-input keyboard */ + virtio_input_read(hart, &data->vkeyboard, addr & 0xFFFFF, width, + value); + emu_update_vinput_keyboard_interrupts(hart->vm); + return; + case 0x4A: /* virtio-input mouse */ + virtio_input_read(hart, &data->vmouse, addr & 0xFFFFF, width, + value); + emu_update_vinput_mouse_interrupts(hart->vm); + return; #endif } } @@ -345,6 +394,18 @@ static void mem_store(hart_t *hart, virtio_fs_write(hart, &data->vfs, addr & 0xFFFFF, width, value); emu_update_vfs_interrupts(hart->vm); return; +#endif +#if SEMU_HAS(VIRTIOINPUT) + case 0x49: /* virtio-input keyboard */ + virtio_input_write(hart, &data->vkeyboard, addr & 0xFFFFF, width, + value); + emu_update_vinput_keyboard_interrupts(hart->vm); + return; + case 0x4A: /* virtio-input mouse */ + virtio_input_write(hart, &data->vmouse, addr & 0xFFFFF, width, + value); + emu_update_vinput_mouse_interrupts(hart->vm); + return; #endif } } @@ -845,6 +906,29 @@ static int semu_init(emu_state_t *emu, int argc, char **argv) fprintf(stderr, "No virtio-fs functioned\n"); #endif +#if SEMU_HAS(VIRTIOINPUT) + g_window.window_init(); + + emu->vkeyboard.ram = emu->ram; + virtio_input_init(&(emu->vkeyboard)); + + emu->vmouse.ram = emu->ram; + virtio_input_init(&(emu->vmouse)); + + emu->wake_fd[0] = emu->wake_fd[1] = -1; + if (vm->n_hart > 1 && g_window.window_main_loop) { + if (pipe(emu->wake_fd) < 0) { + perror("pipe"); + return 2; + } + /* Make the write end non-blocking so window_shutdown_sw() never + * stalls. Single-hart mode never blocks in poll(-1), so it does not + * need the wake pipe at all. + */ + fcntl(emu->wake_fd[1], F_SETFL, O_NONBLOCK); + } +#endif + emu->peripheral_update_ctr = 0; emu->debug = debug; @@ -1127,7 +1211,7 @@ static void print_mmu_cache_stats(vm_t *vm) } #endif -static int semu_run(emu_state_t *emu) +static void semu_run(emu_state_t *emu) { int ret; vm_t *vm = &emu->vm; @@ -1159,7 +1243,8 @@ static int semu_run(emu_state_t *emu) int kq = kqueue(); if (kq < 0) { perror("kqueue"); - return -1; + emu->exit_code = -1; + return; } struct kevent kev_timer; @@ -1167,7 +1252,8 @@ static int semu_run(emu_state_t *emu) if (kevent(kq, &kev_timer, 1, NULL, 0, NULL) < 0) { perror("kevent timer setup"); close(kq); - return -1; + emu->exit_code = -1; + return; } if (isatty(emu->uart.in_fd)) { @@ -1177,14 +1263,16 @@ static int semu_run(emu_state_t *emu) if (kevent(kq, &kev_uart, 1, NULL, 0, NULL) < 0) { perror("kevent uart setup"); close(kq); - return -1; + emu->exit_code = -1; + return; } } #else int wfi_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); if (wfi_timer_fd < 0) { perror("timerfd_create"); - return -1; + emu->exit_code = -1; + return; } struct itimerspec its = { @@ -1194,7 +1282,8 @@ static int semu_run(emu_state_t *emu) if (timerfd_settime(wfi_timer_fd, 0, &its, NULL) < 0) { perror("timerfd_settime"); close(wfi_timer_fd); - return -1; + emu->exit_code = -1; + return; } #endif @@ -1213,8 +1302,14 @@ static int semu_run(emu_state_t *emu) if (signal_received) break; #endif - /* Only need fds for timer and UART (no coroutine I/O) */ + /* Only need fds for timer and UART (no coroutine I/O), + * plus an optional wake pipe when VIRTIOINPUT is enabled. + */ size_t needed = 2; +#if SEMU_HAS(VIRTIOINPUT) + if (emu->wake_fd[0] >= 0) + needed++; +#endif /* Grow buffer if needed (amortized realloc) */ if (needed > poll_capacity) { @@ -1227,7 +1322,8 @@ static int semu_run(emu_state_t *emu) #else close(wfi_timer_fd); #endif - return -1; + emu->exit_code = -1; + return; } pfds = new_pfds; poll_capacity = needed; @@ -1311,6 +1407,18 @@ static int semu_run(emu_state_t *emu) pfd_count++; } +#if SEMU_HAS(VIRTIOINPUT) + /* Always watch the wake pipe so that closing the SDL window + * unblocks poll(-1) immediately. + */ + int wake_pfd_index = -1; + if (emu->wake_fd[0] >= 0 && pfd_count < poll_capacity) { + pfds[pfd_count] = (struct pollfd) {emu->wake_fd[0], POLLIN, 0}; + wake_pfd_index = (int) pfd_count; + pfd_count++; + } +#endif + /* Set poll timeout based on current idle state (adaptive timeout). * Three-tier strategy: * 1. Blocking (-1): All harts idle + have fds → wait for events @@ -1365,6 +1473,21 @@ static int semu_run(emu_state_t *emu) perror("poll"); } +#if SEMU_HAS(VIRTIOINPUT) + /* Drain one wake byte if the pipe fired. At this point the wake + * pipe only signals shutdown/close requests, so one byte is + * enough to make emu_tick_peripherals() observe + * g_window.window_is_closed() and stop the emulator. + */ + if (wake_pfd_index >= 0 && + (pfds[wake_pfd_index].revents & POLLIN)) { + char wake_byte; + ssize_t bytes_drained = + read(emu->wake_fd[0], &wake_byte, sizeof(wake_byte)); + (void) bytes_drained; + } +#endif + /* Resume all hart coroutines (round-robin scheduling). * Each hart executes a batch of instructions, then yields back. * Harts in WFI will have their in_wfi flag cleared by interrupt @@ -1391,11 +1514,26 @@ static int semu_run(emu_state_t *emu) #else close(wfi_timer_fd); #endif +#if SEMU_HAS(VIRTIOINPUT) + if (emu->wake_fd[0] >= 0) + close(emu->wake_fd[0]); + if (emu->wake_fd[1] >= 0) + close(emu->wake_fd[1]); +#endif + /* A closed window is a normal user action, not an error. */ +#if SEMU_HAS(VIRTIOINPUT) + if (emu->stopped && !g_window.window_is_closed()) +#else if (emu->stopped) - return 1; +#endif + { + emu->exit_code = 1; + return; + } - return 0; + emu->exit_code = 0; + return; } /* Single-hart mode: use original scheduling */ @@ -1432,20 +1570,24 @@ static int semu_run(emu_state_t *emu) MIN(SEMU_SLIRP_SLICE_STEPS, SLIRP_POLL_INTERVAL - i); ret = semu_run_chunk(emu, steps); - if (ret) - return ret; + if (ret) { + emu->exit_code = ret; + return; + } } } else #endif { ret = semu_run_chunk(emu, SEMU_SINGLE_SLICE_STEPS); - if (ret) - return ret; + if (ret) { + emu->exit_code = ret; + return; + } } } /* unreachable */ - return 0; + emu->exit_code = 0; } static inline bool semu_is_interrupt(emu_state_t *emu) @@ -1486,7 +1628,11 @@ static int semu_read_mem(void *args, size_t addr, size_t len, void *val) static gdb_action_t semu_cont(void *args) { emu_state_t *emu = (emu_state_t *) args; +#if SEMU_HAS(VIRTIOINPUT) + while (!semu_is_interrupt(emu) && !g_window.window_is_closed()) { +#else while (!semu_is_interrupt(emu)) { +#endif #ifdef MMU_CACHE_STATS /* Check if signal received (SIGINT/SIGTERM). * Break to return control to gdbstub, stats printed at end of main(). @@ -1500,6 +1646,11 @@ static gdb_action_t semu_cont(void *args) /* Clear the interrupt if it's pending */ __atomic_store_n(&emu->is_interrupted, false, __ATOMIC_RELAXED); +#if SEMU_HAS(VIRTIOINPUT) + /* Tell gdbstub_run() to exit cleanly when the window is closed. */ + if (g_window.window_is_closed()) + return ACT_SHUTDOWN; +#endif return ACT_RESUME; } @@ -1529,7 +1680,7 @@ static void semu_set_cpu(void *args, int cpuid) emu->curr_cpuid = cpuid; } -static int semu_run_debug(emu_state_t *emu) +static void semu_run_debug(emu_state_t *emu) { vm_t *vm = &emu->vm; @@ -1558,18 +1709,37 @@ static int semu_run_debug(emu_state_t *emu) .target_desc = TARGET_RV32, }, "127.0.0.1:1234")) { - return 1; + emu->exit_code = 1; + return; } - emu->is_interrupted = false; - if (!gdbstub_run(&gdbstub, (void *) emu)) - return 1; + __atomic_store_n(&emu->is_interrupted, false, __ATOMIC_RELAXED); + bool ok = gdbstub_run(&gdbstub, (void *) emu); gdbstub_close(&gdbstub); - return 0; + emu->exit_code = ok ? 0 : 1; } +#if SEMU_HAS(VIRTIOINPUT) +/* Thread wrapper for running emulator in background thread */ +static void *emu_thread_func(void *arg) +{ + emu_state_t *emu = (emu_state_t *) arg; + + if (emu->debug) + semu_run_debug(emu); + else + 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 NULL; +} +#endif + int main(int argc, char **argv) { int ret; @@ -1583,13 +1753,44 @@ 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(VIRTIOINPUT) + /* 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; + + if (emu.wake_fd[1] >= 0) + g_window.window_set_wake_fd(emu.wake_fd[1]); + + 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(); + + /* window_main_loop() returns either because the user closed the window + * (SDL_QUIT) or because the emulator called window_shutdown(). + * emu_tick_peripherals() picks up g_window.window_is_closed() and + * sets emu->stopped, so no direct write to emu.stopped is needed + * here. + */ + + /* Wait for emulator thread to finish. */ + pthread_join(emu_thread, NULL); + } else +#endif + { + if (emu.debug) + semu_run_debug(&emu); + else + semu_run(&emu); + } #ifdef MMU_CACHE_STATS print_mmu_cache_stats(&emu.vm); #endif - return ret; + return emu.exit_code; } diff --git a/minimal.dts b/minimal.dts index c2d412c0..c1806ba3 100644 --- a/minimal.dts +++ b/minimal.dts @@ -87,5 +87,19 @@ interrupts = <6>; }; #endif + +#if SEMU_FEATURE_VIRTIOINPUT + keyboard0: virtio@4900000 { + compatible = "virtio,mmio"; + reg = <0x4900000 0x200>; + interrupts = <7>; + }; + + mouse0: virtio@4a00000 { + compatible = "virtio,mmio"; + reg = <0x4a00000 0x200>; + interrupts = <8>; + }; +#endif }; }; diff --git a/virtio-input-codes.h b/virtio-input-codes.h new file mode 100644 index 00000000..d1fff60a --- /dev/null +++ b/virtio-input-codes.h @@ -0,0 +1,193 @@ +#pragma once + +/* + * 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_REL 0x02 +#define SEMU_EV_ABS 0x03 +#define SEMU_EV_LED 0x11 +#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 + +/* + * Relative axis identifiers (used for pointer motion and scroll reporting) + * + * REL_X / REL_Y are standard evdev mouse deltas. REL_WHEEL positive = scroll + * up, REL_HWHEEL positive = scroll right. + */ +#define SEMU_REL_X 0x00 +#define SEMU_REL_Y 0x01 +#define SEMU_REL_HWHEEL 0x06 +#define SEMU_REL_WHEEL 0x08 + +/* + * Absolute axis identifiers (used for pointer position reporting) + */ +#define SEMU_ABS_X 0x00 +#define SEMU_ABS_Y 0x01 + +/* + * LED codes (used for Num Lock / Caps Lock / Scroll Lock state feedback + * from driver to device via VIRTIO_INPUT_STATUSQ) + */ +#define SEMU_LED_NUML 0x00 +#define SEMU_LED_CAPSL 0x01 +#define SEMU_LED_SCROLLL 0x02 + +/* + * 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-event.c b/virtio-input-event.c new file mode 100644 index 00000000..c8289e4b --- /dev/null +++ b/virtio-input-event.c @@ -0,0 +1,239 @@ +#include +#include + +#include "device.h" +#include "virtio-input-codes.h" +#include "virtio-input-event.h" +#include "window.h" + +#define VINPUT_SDL_EVENT_WAIT_TIMEOUT_MS 1 /* ms */ + +#define DEF_KEY_MAP(_sdl_scancode, _linux_key) \ + {.sdl_scancode = _sdl_scancode, .linux_key = _linux_key} + +struct vinput_key_map_entry { + int sdl_scancode; + int linux_key; +}; + +static struct vinput_key_map_entry vinput_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 vinput_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 vinput_sdl_scancode_to_linux_key(int sdl_scancode) +{ + unsigned long key_cnt = + sizeof(vinput_key_map) / sizeof(struct vinput_key_map_entry); + for (unsigned long i = 0; i < key_cnt; i++) + if (sdl_scancode == vinput_key_map[i].sdl_scancode) + return vinput_key_map[i].linux_key; + + return -1; +} + +bool vinput_handle_events(void) +{ + SDL_Event e; + int linux_key; + bool quit = false; + + while (SDL_WaitEventTimeout(&e, VINPUT_SDL_EVENT_WAIT_TIMEOUT_MS)) { + switch (e.type) { + case SDL_QUIT: + quit = true; + break; + case SDL_WINDOWEVENT: + if (e.window.event == SDL_WINDOWEVENT_FOCUS_LOST) + g_window.window_set_mouse_grab(false); + break; + case SDL_KEYDOWN: + if (g_window.window_is_mouse_grabbed() && + e.key.keysym.scancode == SDL_SCANCODE_G && + (e.key.keysym.mod & KMOD_CTRL) && + (e.key.keysym.mod & KMOD_ALT)) { + g_window.window_set_mouse_grab(false); + break; + } + /* EV_REP is advertised, so the guest kernel drives key repeat. + * Drop host autorepeat events to avoid double repeat. + */ + if (e.key.repeat) + break; + linux_key = vinput_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 = vinput_sdl_scancode_to_linux_key(e.key.keysym.scancode); + if (linux_key >= 0) + virtio_input_update_key(linux_key, 0); + break; + case SDL_MOUSEBUTTONDOWN: + g_window.window_set_mouse_grab(true); + linux_key = vinput_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 = vinput_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: + if (!g_window.window_is_mouse_grabbed() || + (e.motion.xrel == 0 && e.motion.yrel == 0)) + break; + virtio_input_update_mouse_motion(e.motion.xrel, e.motion.yrel); + break; + case SDL_MOUSEWHEEL: { + int dx = e.wheel.x; + int dy = e.wheel.y; + /* SDL_MOUSEWHEEL_FLIPPED means natural/reversed scrolling — + * negate to get standard evdev REL_WHEEL convention. + */ + if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) { + dx = -dx; + dy = -dy; + } + virtio_input_update_scroll(dx, dy); + break; + } + } + } + + return quit; +} + +int virtio_input_fill_ev_key_bitmap(uint8_t *bitmap, size_t bitmap_size) +{ + unsigned long key_cnt = + sizeof(vinput_key_map) / sizeof(struct vinput_key_map_entry); + int max_byte = 0; + for (unsigned long i = 0; i < key_cnt; i++) { + int code = vinput_key_map[i].linux_key; + int byte_idx = code / 8; + if ((size_t) byte_idx >= bitmap_size) + continue; + bitmap[byte_idx] |= (uint8_t) (1U << (code % 8)); + if (byte_idx > max_byte) + max_byte = byte_idx; + } + return max_byte + 1; +} diff --git a/virtio-input-event.h b/virtio-input-event.h new file mode 100644 index 00000000..01f6f4d1 --- /dev/null +++ b/virtio-input-event.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include "feature.h" + +#if SEMU_HAS(VIRTIOINPUT) +/* Per virtio-input spec, config string/bitmap payloads are 128 bytes. */ +#define VIRTIO_INPUT_CFG_PAYLOAD_SIZE 128 + +/* Per-device identifier shared between the window backend and the + * virtio-input device model. + */ +enum { + VINPUT_KEYBOARD_ID = 0, + VINPUT_MOUSE_ID = 1, + VINPUT_DEV_CNT, +}; + +/* Poll and translate pending SDL events on the main thread. Returns true if a + * quit/close request was observed, which tells the caller to shut down the + * frontend loop. + */ +bool vinput_handle_events(void); + +/* Fill bitmap[] with exactly the key codes this backend can generate. + * Per the virtio-input spec, only advertise key codes this device will + * actually generate. Returns the minimum byte count needed (index of the + * highest set byte + 1), matching the virtio-input config "size" field. + * bitmap_size must be >= VIRTIO_INPUT_CFG_PAYLOAD_SIZE. + */ +int virtio_input_fill_ev_key_bitmap(uint8_t *bitmap, size_t bitmap_size); +#endif /* SEMU_HAS(VIRTIOINPUT) */ diff --git a/virtio-input.c b/virtio-input.c new file mode 100644 index 00000000..0979655f --- /dev/null +++ b/virtio-input.c @@ -0,0 +1,882 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "device.h" +#include "riscv.h" +#include "riscv_private.h" +#include "utils.h" +#include "virtio-input-codes.h" +#include "virtio-input-event.h" +#include "virtio.h" + +#define BUS_VIRTUAL 0x06 + +#define VINPUT_KEYBOARD_NAME "VirtIO Keyboard" +#define VINPUT_MOUSE_NAME "VirtIO Mouse" + +#define VINPUT_SERIAL "None" + +#define VIRTIO_INPUT_FEATURES_0 0 +#define VIRTIO_INPUT_FEATURES_1 1 /* VIRTIO_F_VERSION_1 */ + +#define VIRTIO_INPUT_QUEUE_NUM_MAX 1024 +#define VIRTIO_INPUT_QUEUE (vinput->queues[vinput->QueueSel]) + +#define PRIV(x) ((struct vinput_data *) (x)->priv) + +enum { + VIRTIO_INPUT_EVENTQ = 0, + VIRTIO_INPUT_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[VIRTIO_INPUT_CFG_PAYLOAD_SIZE]; + uint8_t bitmap[VIRTIO_INPUT_CFG_PAYLOAD_SIZE]; + 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 vinput_data { + virtio_input_state_t *vinput; + struct virtio_input_config cfg; + int type; /* VINPUT_KEYBOARD_ID or VINPUT_MOUSE_ID */ +}; + +static pthread_mutex_t vinput_mutex = PTHREAD_MUTEX_INITIALIZER; +static struct vinput_data vinput_dev[VINPUT_DEV_CNT]; +static const char *vinput_dev_name[VINPUT_DEV_CNT] = { + VINPUT_KEYBOARD_NAME, + VINPUT_MOUSE_NAME, +}; + +static inline void vinput_bitmap_set_bit(uint8_t *map, unsigned long bit) +{ + map[bit / 8] |= (uint8_t) (1U << (bit % 8)); +} + +/* Return the number of bytes the driver needs to read from the config bitmap, + * defined by the virtio input spec as "highest set byte index + 1". + */ +static inline unsigned long vinput_bitmap_get_size(const uint8_t *bitmap, + unsigned long max_bytes) +{ + while (max_bytes > 0 && bitmap[max_bytes - 1] == 0) + max_bytes--; + return max_bytes; +} + +static inline 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 virtio_input_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 + access_size > end) + return false; + return true; +} + +static inline uint32_t virtio_input_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; +} + +/* Consume all pending buffers from the status queue and return them to the + * used ring. The guest driver uses statusq to send EV_LED events (Caps Lock, + * Num Lock, etc.) and acknowledges each buffer so the queue never stalls. + * SDL has no portable LED-control API, so LED state is not applied to the host + * keyboard here. Must be called with vinput_mutex held. + */ +static void virtio_input_drain_statusq(virtio_input_state_t *vinput) +{ + virtio_input_queue_t *queue = &vinput->queues[VIRTIO_INPUT_STATUSQ]; + uint32_t *ram = vinput->ram; + + if (!(vinput->Status & VIRTIO_STATUS__DRIVER_OK) || !queue->ready) + return; + + uint16_t new_avail = ram[queue->QueueAvail] >> 16; + uint16_t avail_delta = (uint16_t) (new_avail - queue->last_avail); + uint16_t new_used = ram[queue->QueueUsed] >> 16; + bool consumed = false; + + if (avail_delta > (uint16_t) queue->QueueNum) { + virtio_input_set_fail(vinput); + return; + } + + const uint32_t event_size = (uint32_t) sizeof(struct virtio_input_event); + + while (queue->last_avail != new_avail) { + 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; + } + + uint32_t *desc = &ram[queue->QueueDesc + buffer_idx * 4]; + uint32_t desc_addr = desc[0]; + uint32_t desc_addr_high = desc[1]; + uint32_t desc_len = desc[2]; + uint16_t desc_flags = desc[3] & 0xFFFF; + + if (desc_addr_high != 0 || (desc_flags & VIRTIO_DESC_F_WRITE) || + desc_len < event_size || desc_addr > RAM_SIZE - event_size) { + virtio_input_set_fail(vinput); + return; + } + + /* Device is read-only on this queue, so no bytes are written into + * the device-writable portion of the buffer. used.len must be 0. + */ + uint32_t vq_used_addr = + queue->QueueUsed + 1 + (new_used % queue->QueueNum) * 2; + ram[vq_used_addr] = buffer_idx; + ram[vq_used_addr + 1] = 0; + new_used++; + queue->last_avail++; + consumed = true; + } + + if (consumed) { + uint16_t *used_hdr = (uint16_t *) &ram[queue->QueueUsed]; + used_hdr[0] = 0; + used_hdr[1] = new_used; + if (!(ram[queue->QueueAvail] & 1)) + vinput->InterruptStatus |= VIRTIO_INT__USED_RING; + } +} + +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 = &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. Compare the + * start address against the last valid event-sized window in RAM so + * guest-controlled addr cannot wrap past UINT32_MAX during validation. + */ + const uint32_t event_size = + (uint32_t) sizeof(struct virtio_input_event); + if (addr_high != 0 || !(vq_desc.flags & VIRTIO_DESC_F_WRITE) || + vq_desc.len < event_size || vq_desc.addr > RAM_SIZE - event_size) { + virtio_input_set_fail(vinput); + return false; + } + + /* Write event into guest buffer directly */ + ev = (struct virtio_input_event *) ((uintptr_t) 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 *) &ram[queue->QueueUsed]; + used_hdr[0] = 0; /* virtq_used.flags */ + used_hdr[1] = new_used; /* virtq_used.idx */ + + return true; +} + +static void virtio_input_update_eventq(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 = VIRTIO_INPUT_EVENTQ; + + /* Start of the critical section */ + pthread_mutex_lock(&vinput_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; + uint16_t avail_delta = (uint16_t) (new_avail - queue->last_avail); + if (avail_delta > (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(&vinput_mutex); +} + +void virtio_input_update_key(uint32_t key, uint32_t ev_value) +{ + /* ev_value follows Linux evdev: 0=release, 1=press, 2=repeat */ + struct virtio_input_event input_ev[] = { + {.type = SEMU_EV_KEY, .code = key, .value = ev_value}, + {.type = SEMU_EV_SYN, .code = SEMU_SYN_REPORT, .value = 0}, + }; + + size_t ev_cnt = ARRAY_SIZE(input_ev); + virtio_input_update_eventq(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_input_update_eventq(VINPUT_MOUSE_ID, input_ev, ev_cnt); +} + +void virtio_input_update_mouse_motion(int32_t dx, int32_t dy) +{ + struct virtio_input_event input_ev[3]; + uint32_t ev_cnt = 0; + + if (dx) + input_ev[ev_cnt++] = (struct virtio_input_event) { + .type = SEMU_EV_REL, .code = SEMU_REL_X, .value = (uint32_t) dx}; + if (dy) + input_ev[ev_cnt++] = (struct virtio_input_event) { + .type = SEMU_EV_REL, .code = SEMU_REL_Y, .value = (uint32_t) dy}; + if (!ev_cnt) + return; + + input_ev[ev_cnt++] = (struct virtio_input_event) { + .type = SEMU_EV_SYN, .code = SEMU_SYN_REPORT, .value = 0}; + + virtio_input_update_eventq(VINPUT_MOUSE_ID, input_ev, ev_cnt); +} + +void virtio_input_update_scroll(int32_t dx, int32_t dy) +{ + /* Build only the non-zero axis events and always terminate with SYN_REPORT. + * dx > 0: scroll right, dy > 0: scroll up (matches Linux evdev convention). + */ + struct virtio_input_event input_ev[3]; + uint32_t ev_cnt = 0; + + if (dx) + input_ev[ev_cnt++] = + (struct virtio_input_event) {.type = SEMU_EV_REL, + .code = SEMU_REL_HWHEEL, + .value = (uint32_t) dx}; + if (dy) + input_ev[ev_cnt++] = + (struct virtio_input_event) {.type = SEMU_EV_REL, + .code = SEMU_REL_WHEEL, + .value = (uint32_t) dy}; + if (!ev_cnt) + return; + + input_ev[ev_cnt++] = (struct virtio_input_event) { + .type = SEMU_EV_SYN, .code = SEMU_SYN_REPORT, .value = 0}; + + virtio_input_update_eventq(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, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + + switch (dev_id) { + case VINPUT_KEYBOARD_ID: + cfg->size = 0; + break; + case VINPUT_MOUSE_ID: + /* INPUT_PROP_POINTER marks this as a pointer device. */ + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_INPUT_PROP_POINTER); + cfg->size = (uint8_t) vinput_bitmap_get_size( + cfg->u.bitmap, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + break; + } +} + +static void virtio_input_fill_keyboard_ev_bits(int dev_id, uint8_t event) +{ + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + memset(cfg->u.bitmap, 0, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + + switch (event) { + case SEMU_EV_KEY: + /* Only advertise key codes that key_map[] actually generates. */ + cfg->size = (uint8_t) virtio_input_fill_ev_key_bitmap( + cfg->u.bitmap, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + break; + case SEMU_EV_LED: + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_LED_NUML); + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_LED_CAPSL); + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_LED_SCROLLL); + cfg->size = (uint8_t) vinput_bitmap_get_size( + cfg->u.bitmap, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + break; + case SEMU_EV_REP: + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_REP_DELAY); + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_REP_PERIOD); + cfg->size = (uint8_t) vinput_bitmap_get_size( + cfg->u.bitmap, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + break; + default: + cfg->size = 0; + } +} + +static void virtio_input_fill_mouse_ev_bits(int dev_id, uint8_t event) +{ + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + memset(cfg->u.bitmap, 0, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + + switch (event) { + case SEMU_EV_KEY: + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_BTN_LEFT); + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_BTN_RIGHT); + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_BTN_MIDDLE); + cfg->size = (uint8_t) vinput_bitmap_get_size( + cfg->u.bitmap, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + break; + case SEMU_EV_REL: + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_REL_X); + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_REL_Y); + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_REL_HWHEEL); + vinput_bitmap_set_bit(cfg->u.bitmap, SEMU_REL_WHEEL); + cfg->size = (uint8_t) vinput_bitmap_get_size( + cfg->u.bitmap, VIRTIO_INPUT_CFG_PAYLOAD_SIZE); + break; + default: + cfg->size = 0; + } +} + +static void virtio_input_fill_ev_bits(int dev_id, uint8_t event) +{ + switch (dev_id) { + case VINPUT_KEYBOARD_ID: + virtio_input_fill_keyboard_ev_bits(dev_id, event); + break; + case VINPUT_MOUSE_ID: + virtio_input_fill_mouse_ev_bits(dev_id, event); + break; + } +} + +static void virtio_input_fill_abs_info(int dev_id, uint8_t code) +{ + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + (void) code; + + /* The current pointing device is a relative mouse, so no ABS axes or + * ABS_INFO ranges are exposed. + */ + cfg->size = 0; +} + +static void virtio_input_cfg_read(int dev_id) +{ + struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; + memset(&cfg->u, 0, sizeof(cfg->u)); + cfg->size = 0; + + switch (cfg->select) { + case VIRTIO_INPUT_CFG_UNSET: + return; + case VIRTIO_INPUT_CFG_ID_NAME: + strcpy(cfg->u.string, vinput_dev_name[dev_id]); + cfg->size = strlen(vinput_dev_name[dev_id]); + return; + case VIRTIO_INPUT_CFG_ID_SERIAL: + strcpy(cfg->u.string, VINPUT_SERIAL); + cfg->size = strlen(VINPUT_SERIAL); + return; + 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; + case VIRTIO_INPUT_CFG_PROP_BITS: + virtio_input_properties(dev_id); + return; + case VIRTIO_INPUT_CFG_EV_BITS: + virtio_input_fill_ev_bits(dev_id, cfg->subsel); + return; + case VIRTIO_INPUT_CFG_ABS_INFO: + virtio_input_fill_abs_info(dev_id, cfg->subsel); + return; + default: + return; + } +} + +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 + ? VIRTIO_INPUT_FEATURES_0 + : (vinput->DeviceFeaturesSel == 1 ? VIRTIO_INPUT_FEATURES_1 + : 0); + return true; + case _(QueueNumMax): + *value = VIRTIO_INPUT_QUEUE_NUM_MAX; + return true; + case _(QueueReady): + *value = VIRTIO_INPUT_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: + virtio_input_cfg_read(PRIV(vinput)->type); + *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 */ + uint32_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 <= VIRTIO_INPUT_QUEUE_NUM_MAX) + VIRTIO_INPUT_QUEUE.QueueNum = value; + else + virtio_input_set_fail(vinput); + return true; + case _(QueueReady): + VIRTIO_INPUT_QUEUE.ready = value & 1; + if (VIRTIO_INPUT_QUEUE.ready) { + uint32_t qnum = VIRTIO_INPUT_QUEUE.QueueNum; + uint32_t ram_words = RAM_SIZE / 4; + + /* Validate that the entire avail ring, desc table, and used ring + * fit within guest RAM. virtio_input_preprocess() only checks the + * base address of each ring — without this check a guest could + * place a ring near the end of RAM and cause out-of-bounds host + * accesses when the ring entries are subsequently dereferenced. + * + * Max words accessed per ring: + * avail: QueueAvail + 1 + (qnum-1)/2 + * desc: QueueDesc + qnum*4 - 1 + * used: QueueUsed + qnum*2 (vq_used_addr+1) + */ + if (qnum == 0 || + VIRTIO_INPUT_QUEUE.QueueAvail + 1 + (qnum - 1) / 2 >= + ram_words || + VIRTIO_INPUT_QUEUE.QueueDesc + qnum * 4 > ram_words || + VIRTIO_INPUT_QUEUE.QueueUsed + qnum * 2 >= ram_words) { + virtio_input_set_fail(vinput); + return true; + } + + VIRTIO_INPUT_QUEUE.last_avail = + vinput->ram[VIRTIO_INPUT_QUEUE.QueueAvail] >> 16; + } + return true; + case _(QueueDescLow): + VIRTIO_INPUT_QUEUE.QueueDesc = virtio_input_preprocess(vinput, value); + return true; + case _(QueueDescHigh): + if (value) + virtio_input_set_fail(vinput); + return true; + case _(QueueDriverLow): + VIRTIO_INPUT_QUEUE.QueueAvail = virtio_input_preprocess(vinput, value); + return true; + case _(QueueDriverHigh): + if (value) + virtio_input_set_fail(vinput); + return true; + case _(QueueDeviceLow): + VIRTIO_INPUT_QUEUE.QueueUsed = virtio_input_preprocess(vinput, value); + return true; + case _(QueueDeviceHigh): + if (value) + virtio_input_set_fail(vinput); + return true; + case _(QueueNotify): + if (value >= ARRAY_SIZE(vinput->queues)) { + virtio_input_set_fail(vinput); + return true; + } + /* EVENTQ: actual buffer availability is checked lazily in + * virtio_input_update_eventq() when the next event arrives. + * STATUSQ: drain LED-state buffers from the guest immediately so + * the driver's status queue never runs out of available entries. + */ + if (value == VIRTIO_INPUT_STATUSQ) + virtio_input_drain_statusq(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(&vinput_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 = virtio_input_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(&vinput_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(&vinput_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 = virtio_input_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(&vinput_mutex); +} + +bool virtio_input_irq_pending(virtio_input_state_t *vinput) +{ + pthread_mutex_lock(&vinput_mutex); + bool pending = vinput->InterruptStatus != 0; + pthread_mutex_unlock(&vinput_mutex); + return pending; +} + +void virtio_input_init(virtio_input_state_t *vinput) +{ + static int vinput_dev_cnt = 0; + 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-sw.c b/window-sw.c new file mode 100644 index 00000000..940c7f3d --- /dev/null +++ b/window-sw.c @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include + +#include "device.h" +#include "feature.h" +#include "virtio-input-event.h" +#include "window.h" + +static SDL_Window *sdl_window; +static int wake_write_fd = -1; +static bool headless_mode = false; +static bool mouse_grabbed = false; +static bool should_exit = false; + +/* The backend only needs the pipe's write end. The emulator owns the read end + * and drains it after poll() returns. + */ +static void window_set_wake_fd_sw(int fd) +{ + wake_write_fd = fd; +} + +static void window_wake_backend_sw(void) +{ + if (wake_write_fd >= 0) { + char byte = 1; + /* Best-effort wakeup: the pipe is non-blocking, and the byte value has + * no meaning beyond making the read end readable. + */ + ssize_t bytes_written = write(wake_write_fd, &byte, 1); + (void) bytes_written; + } +} + +static inline void window_shutdown_sw(void) +{ + /* Both user-driven close and emulator-driven shutdown funnel through the + * same flag so the main thread and emulator thread observe one exit state. + */ + __atomic_store_n(&should_exit, true, __ATOMIC_RELAXED); + /* Unblock any poll(-1) in the SMP emulator loop immediately. */ + window_wake_backend_sw(); +} + +static bool window_is_closed_sw(void) +{ + return __atomic_load_n(&should_exit, __ATOMIC_RELAXED); +} + +/* Main-thread-only helper for relative-pointer devices. SDL's grab and + * relative mouse APIs are part of the windowing backend, so callers use this + * to switch between normal host-pointer mode and guest-directed mouse mode. + */ +static void window_set_mouse_grab_sw(bool grabbed) +{ + if (headless_mode || !sdl_window) { + mouse_grabbed = false; + return; + } + + if (mouse_grabbed == grabbed) + return; + + if (grabbed) { + if (SDL_SetRelativeMouseMode(SDL_TRUE) < 0) { + fprintf(stderr, + "window_set_mouse_grab_sw(): failed to enable relative " + "mouse mode: %s\n", + SDL_GetError()); + return; + } + SDL_SetWindowGrab(sdl_window, SDL_TRUE); + SDL_ShowCursor(SDL_DISABLE); + } else { + SDL_SetWindowGrab(sdl_window, SDL_FALSE); + SDL_SetRelativeMouseMode(SDL_FALSE); + SDL_ShowCursor(SDL_ENABLE); + } + + mouse_grabbed = grabbed; +} + +static bool window_is_mouse_grabbed_sw(void) +{ + return mouse_grabbed; +} + +/* Main loop runs on the main thread */ +static void window_main_loop_sw(void) +{ + if (headless_mode) { + /* Block until the emulator calls window_shutdown_sw(), so main() can + * proceed to pthread_join() rather than stopping the emulator + * immediately. There is no SDL event loop in this mode, so the main + * thread just polls the shared close flag. + */ + while (!window_is_closed_sw()) + usleep(10000); + return; + } + + /* relaxed ordering is sufficient: the only consequence of reading a stale + * false is a few extra loop iterations (each blocked up to 1 ms inside + * SDL_WaitEventTimeout). Ordering with the emulator thread is provided by + * pthread_join(), not by this flag. + */ + while (!window_is_closed_sw()) { + if (vinput_handle_events()) { + /* User closed the window. Set the flag so window_shutdown_sw() + * (called from the emulator thread) does not race with us, then + * return normally so main() can pthread_join the emulator thread + * and collect its exit code. + */ + window_shutdown_sw(); + return; + } + } +} + +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; + } + + sdl_window = SDL_CreateWindow("semu", SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, + SCREEN_HEIGHT, SDL_WINDOW_SHOWN); + if (!sdl_window) { + fprintf(stderr, + "window_init_sw(): failed to create SDL window: %s\n" + "Running in headless mode.\n", + SDL_GetError()); + headless_mode = true; + return; + } + + fprintf(stderr, + "semu: click window to capture mouse, Ctrl+Alt+G to " + "release\n"); +} + +const struct window_backend g_window = { + .window_init = window_init_sw, + .window_main_loop = window_main_loop_sw, + .window_shutdown = window_shutdown_sw, + .window_is_closed = window_is_closed_sw, + .window_set_wake_fd = window_set_wake_fd_sw, + .window_wake_backend = window_wake_backend_sw, + .window_set_mouse_grab = window_set_mouse_grab_sw, + .window_is_mouse_grabbed = window_is_mouse_grabbed_sw, +}; diff --git a/window.h b/window.h new file mode 100644 index 00000000..7dbe634b --- /dev/null +++ b/window.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "feature.h" + +#if SEMU_HAS(VIRTIOINPUT) +struct window_backend { + void (*window_init)(void); + /* 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); + /* Returns true once the window has been closed (or SDL failed to + * initialize). Safe to call from any thread. + */ + bool (*window_is_closed)(void); + /* Register the write end of a pipe to be written when the window shuts + * down. Must be called before window_main_loop(). + */ + void (*window_set_wake_fd)(int fd); + /* Best-effort wakeup hook for the backend self-pipe. */ + void (*window_wake_backend)(void); + /* Enable or disable SDL's relative mouse mode for the frontend window. + * When this returns with grab enabled, pointer motion is reported as + * relative deltas, the host cursor is hidden, and SDL confines the + * pointer to the semu window until the grab is released again. + */ + void (*window_set_mouse_grab)(bool grabbed); + /* Returns true once the frontend window currently owns the host mouse + * grab. Safe to call from the main thread while translating SDL events. + */ + bool (*window_is_mouse_grabbed)(void); +}; + +extern const struct window_backend g_window; +#endif From 6b8b0e1dfc3923a98c4bdbf22741d36df303bf01 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Wed, 1 Apr 2026 04:54:39 +0800 Subject: [PATCH 3/6] Ignore build outputs and working directories Exclude the SMP stamp file, full root filesystem image, and the buildroot, linux, and rootfs working directories so they do not appear as untracked noise. --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index b56a9aa8..00f1dfe7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,10 @@ rootfs.cpio # intermediate riscv-harts.dtsi +.smp_stamp +rootfs_full.cpio + +# Build directories +buildroot/ +linux/ +rootfs/ From a467a7b56b0bcc644d83ec5785fb2d7941e5f6c9 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Wed, 1 Apr 2026 05:02:56 +0800 Subject: [PATCH 4/6] Enable SDL offscreen mode explicitly in CI Set SDL_VIDEODRIVER=offscreen in common.sh so that SDL initialises without a display server, which is required for headless CI runs. --- .ci/common.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ci/common.sh b/.ci/common.sh index e6f88cf3..0aab566e 100755 --- a/.ci/common.sh +++ b/.ci/common.sh @@ -7,6 +7,9 @@ set -euo pipefail MACHINE_TYPE="$(uname -m)" OS_TYPE="$(uname -s)" +# Enable SDL headless mode explicitly. +export SDL_VIDEODRIVER=offscreen + # Cleanup function - kills all semu processes cleanup() { sleep 1 From 023bba4e7364c4b15d75759c0e51542d5430b6d4 Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Sat, 21 Mar 2026 18:13:09 +0800 Subject: [PATCH 5/6] Add input event debug logging Manual testing of virtio-input currently relies on guest-side tools such as 'hexdump /dev/input/eventX'. That confirms that input reaches the guest, but it is not very convenient when checking how host input is turned into virtio-input events in real time. Relative mouse motion and wheel input are especially tedious to inspect from the raw evdev stream alone. Add a build-time input debug switch for the virtio-input event injection path. When 'ENABLE_INPUT_DEBUG=1' is set in the Makefile, the build defines 'SEMU_INPUT_DEBUG' and enables verbose logging in the virtio-input update helpers. With the flag enabled, the virtio-input update path prints the events that are about to be injected into the guest. The output includes: - keyboard events, including the Linux input key code and evdev value (release, press, or repeat) - mouse button events, including the Linux input button code and pressed state - relative mouse motion events, including dx/dy deltas - mouse wheel events, including dx/dy deltas - drop messages when an input event cannot be delivered because the guest has not supplied buffers to 'EVENTQ' --- Makefile | 2 ++ virtio-input.c | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Makefile b/Makefile index 00ba4c51..8943203d 100644 --- a/Makefile +++ b/Makefile @@ -174,6 +174,8 @@ endif # virtio-input ENABLE_VIRTIOINPUT ?= 1 +ENABLE_INPUT_DEBUG ?= 0 +CFLAGS += -DSEMU_INPUT_DEBUG=$(ENABLE_INPUT_DEBUG) $(call set-feature, VIRTIOINPUT) ifeq ($(call has, VIRTIOINPUT), 1) OBJS_EXTRA += virtio-input-event.o diff --git a/virtio-input.c b/virtio-input.c index 0979655f..7700e0a4 100644 --- a/virtio-input.c +++ b/virtio-input.c @@ -17,6 +17,8 @@ #define BUS_VIRTUAL 0x06 +#define VINPUT_DEBUG_PREFIX "[SEMU vinput-log]: " + #define VINPUT_KEYBOARD_NAME "VirtIO Keyboard" #define VINPUT_MOUSE_NAME "VirtIO Mouse" @@ -346,6 +348,10 @@ static void virtio_input_update_eventq(int dev_id, /* No buffers available - drop event or handle later */ if (queue->last_avail == new_avail) { +#if SEMU_INPUT_DEBUG + fprintf(stderr, VINPUT_DEBUG_PREFIX "drop dev=%d (no guest buffers)\n", + dev_id); +#endif /* TODO: Consider buffering events instead of dropping them */ goto out; } @@ -372,6 +378,10 @@ static void virtio_input_update_eventq(int dev_id, void virtio_input_update_key(uint32_t key, uint32_t ev_value) { +#if SEMU_INPUT_DEBUG + fprintf(stderr, VINPUT_DEBUG_PREFIX "key code=%u value=%u\n", key, + ev_value); +#endif /* ev_value follows Linux evdev: 0=release, 1=press, 2=repeat */ struct virtio_input_event input_ev[] = { {.type = SEMU_EV_KEY, .code = key, .value = ev_value}, @@ -384,6 +394,10 @@ void virtio_input_update_key(uint32_t key, uint32_t ev_value) void virtio_input_update_mouse_button_state(uint32_t button, bool pressed) { +#if SEMU_INPUT_DEBUG + fprintf(stderr, VINPUT_DEBUG_PREFIX "button code=%u pressed=%u\n", button, + pressed); +#endif struct virtio_input_event input_ev[] = { {.type = SEMU_EV_KEY, .code = button, .value = pressed}, {.type = SEMU_EV_SYN, .code = SEMU_SYN_REPORT, .value = 0}, @@ -395,6 +409,9 @@ void virtio_input_update_mouse_button_state(uint32_t button, bool pressed) void virtio_input_update_mouse_motion(int32_t dx, int32_t dy) { +#if SEMU_INPUT_DEBUG + fprintf(stderr, VINPUT_DEBUG_PREFIX "motion dx=%d dy=%d\n", dx, dy); +#endif struct virtio_input_event input_ev[3]; uint32_t ev_cnt = 0; @@ -415,6 +432,9 @@ void virtio_input_update_mouse_motion(int32_t dx, int32_t dy) void virtio_input_update_scroll(int32_t dx, int32_t dy) { +#if SEMU_INPUT_DEBUG + fprintf(stderr, VINPUT_DEBUG_PREFIX "scroll dx=%d dy=%d\n", dx, dy); +#endif /* Build only the non-zero axis events and always terminate with SYN_REPORT. * dx > 0: scroll right, dy > 0: scroll up (matches Linux evdev convention). */ From 5c24de0373a7b6cae74d6d532ee3813305536e0a Mon Sep 17 00:00:00 2001 From: Mes0903 Date: Fri, 20 Mar 2026 23:30:48 +0800 Subject: [PATCH 6/6] Introduce SPSC queue to handle virtio-input event The current virtio-input path gives the SDL thread direct access to guest-visible virtio-input state. SDL input is translated on the main thread and then immediately forwarded into the virtio-input update helpers, which write EVENTQ, advance the used ring, and set 'InterruptStatus'. At the same time, the emulator thread polls 'virtio_input_irq_pending()' from 'emu_tick_peripherals()', which puts this shared state on a hot path. That ownership model creates an awkward tradeoff. Without synchronization, the SDL thread and the emulator thread can race on the same 'virtio_input_state_t'. With 'virtio_input_mutex', the hot path has to pay for cross-thread locking just to observe whether an interrupt is pending. This commit moves the ownership boundary earlier. SDL input is still collected and translated on the main thread, but it is now stored as bounded host event records instead of updating guest memory, virtqueues, or virtio registers directly. 'emu_tick_peripherals()' drains those records on the emulator thread and dispatches them into the existing 'virtio_input_update_*()' helpers. Guest-visible virtio-input state is therefore owned by the emulator thread, while the SDL thread only owns host event collection. This change also simplifies the interrupt pending path. 'virtio_input_irq_pending()' now only checks 'InterruptStatus', and the vinput-internal mutex is removed because descriptor processing, queue updates, status queue draining, MMIO accesses, and other guest-visible state transitions all run on the emulator thread. At the interface level, the new event queue carries keyboard, mouse button, mouse motion, and mouse wheel input in a host-centric form. This keeps the split between the window side and the emulator side explicit while preserving the existing internal virtio-input event injection helpers. --- device.h | 24 ++--- main.c | 23 +++-- virtio-input-event.c | 211 +++++++++++++++++++++++++++++++++++++++---- virtio-input-event.h | 60 ++++++++++++ virtio-input.c | 133 ++++++++++++++++++--------- 5 files changed, 373 insertions(+), 78 deletions(-) diff --git a/device.h b/device.h index 6cb17e8d..9d8d4cfc 100644 --- a/device.h +++ b/device.h @@ -281,10 +281,11 @@ void virtio_input_write(hart_t *vm, uint32_t value); void virtio_input_init(virtio_input_state_t *vinput); -void virtio_input_update_key(uint32_t key, uint32_t ev_value); -void virtio_input_update_mouse_button_state(uint32_t button, bool pressed); -void virtio_input_update_mouse_motion(int32_t dx, int32_t dy); -void virtio_input_update_scroll(int32_t dx, int32_t dy); + +/* Drain translated host window events and update guest-visible virtio-input + * device state. Must be called from the emulator thread. + */ +void virtio_input_drain_host_events(void); /* Returns true if the device has a pending interrupt. Safe to call from * the emulator thread without holding any lock internally. @@ -527,16 +528,17 @@ typedef struct { virtio_input_state_t vkeyboard; virtio_input_state_t vmouse; /* Use self-pipe trick to unblock the emulator loop when the - * window closes. When all harts are idle, semu_run() calls + * window backend has queued work, such as input events or + * window shutdown. When all harts are idle, semu_run() calls * poll(-1) and blocks indefinitely waiting for timer or UART - * events. window_shutdown_sw() runs on the window-event thread - * and has no way to wake that blocked poll() other than writing - * to a file descriptor it is watching. + * events. The window-event thread has no way to wake that + * blocked poll() other than writing to a file descriptor it is + * watching. * * wake_fd[0] (read end) is added to pfds[] so poll() monitors it. - * wake_fd[1] (write end) is handed to window_shutdown_sw(), which - * writes one byte on window close to make wake_fd[0] readable and - * return poll() immediately. + * wake_fd[1] (write end) is handed to the window backend, which + * writes one byte when backend work arrives to make wake_fd[0] + * readable and return poll() immediately. */ int wake_fd[2]; #endif diff --git a/main.c b/main.c index b528d838..cd15e86e 100644 --- a/main.c +++ b/main.c @@ -27,6 +27,7 @@ #include "device.h" #include "mini-gdbstub/include/gdbstub.h" #if SEMU_HAS(VIRTIOINPUT) +#include "virtio-input-event.h" #include "window.h" #endif #include "riscv.h" @@ -235,6 +236,13 @@ static inline void emu_tick_peripherals(emu_state_t *emu) emu_update_vfs_interrupts(vm); #endif #if SEMU_HAS(VIRTIOINPUT) + /* The empty path is common during CI and boot workloads, so only + * drain the host-side queue after the window thread has published + * pending work for the emulator thread. + */ + if (vinput_may_have_pending_cmds()) + virtio_input_drain_host_events(); + if (virtio_input_irq_pending(&emu->vkeyboard)) emu_update_vinput_keyboard_interrupts(vm); @@ -1408,8 +1416,8 @@ static void semu_run(emu_state_t *emu) } #if SEMU_HAS(VIRTIOINPUT) - /* Always watch the wake pipe so that closing the SDL window - * unblocks poll(-1) immediately. + /* Always watch the wake pipe so that backend work such as input + * events or SDL window close unblocks poll(-1) immediately. */ int wake_pfd_index = -1; if (emu->wake_fd[0] >= 0 && pfd_count < poll_capacity) { @@ -1474,10 +1482,13 @@ static void semu_run(emu_state_t *emu) } #if SEMU_HAS(VIRTIOINPUT) - /* Drain one wake byte if the pipe fired. At this point the wake - * pipe only signals shutdown/close requests, so one byte is - * enough to make emu_tick_peripherals() observe - * g_window.window_is_closed() and stop the emulator. + /* Drain one wake byte if the pipe fired. The virtio-input path + * coalesces backend wakeups behind a bool gate, so it contributes + * at most one queued notification byte before the emulator thread + * drains pending work. Extra shutdown wake bytes do not need to be + * fully consumed here because the first one is enough to make + * emu_tick_peripherals() observe g_window.window_is_closed() and + * stop the emulator. */ if (wake_pfd_index >= 0 && (pfds[wake_pfd_index].revents & POLLIN)) { diff --git a/virtio-input-event.c b/virtio-input-event.c index c8289e4b..2073c2d6 100644 --- a/virtio-input-event.c +++ b/virtio-input-event.c @@ -6,7 +6,11 @@ #include "virtio-input-event.h" #include "window.h" +#define VINPUT_CMD_QUEUE_SIZE 1024U +#define VINPUT_CMD_QUEUE_MASK (VINPUT_CMD_QUEUE_SIZE - 1U) + #define VINPUT_SDL_EVENT_WAIT_TIMEOUT_MS 1 /* ms */ +#define VINPUT_SDL_EVENT_BURST_LIMIT 64U #define DEF_KEY_MAP(_sdl_scancode, _linux_key) \ {.sdl_scancode = _sdl_scancode, .linux_key = _linux_key} @@ -16,6 +20,24 @@ struct vinput_key_map_entry { int linux_key; }; +/* Per-device SPSC queue. The queue stays entirely on the host side so SDL + * never touches guest-facing virtio-input state directly. Each virtio-input + * device gets its own queue so that resetting one device (on guest Status=0) + * does not drop pending events destined for the other device. + */ +struct vinput_cmd_queue { + struct vinput_cmd entries[VINPUT_CMD_QUEUE_SIZE]; + uint32_t head; + uint32_t tail; +}; + +static struct vinput_cmd_queue vinput_cmd_queues[VINPUT_DEV_CNT]; + +/* Single wake gate across all device queues. The emulator drains every queue + * after one pipe wake-up, so coalescing through a single gate is enough. + */ +static bool vinput_cmd_wake_pending; + static struct vinput_key_map_entry vinput_key_map[] = { /* Keyboard */ DEF_KEY_MAP(SDL_SCANCODE_ESCAPE, SEMU_KEY_ESC), @@ -119,6 +141,52 @@ static struct vinput_key_map_entry vinput_key_map[] = { DEF_KEY_MAP(SDL_SCANCODE_DELETE, SEMU_KEY_DELETE), }; +static bool vinput_push_cmd(int dev_id, const struct vinput_cmd *event) +{ + struct vinput_cmd_queue *queue = &vinput_cmd_queues[dev_id]; + uint32_t head = __atomic_load_n(&queue->head, __ATOMIC_RELAXED); + uint32_t tail = __atomic_load_n(&queue->tail, __ATOMIC_ACQUIRE); + uint32_t next = (head + 1U) & VINPUT_CMD_QUEUE_MASK; + + /* Keep the producer non-blocking. If the queue is full, the newest event + * is dropped. This remains intentionally lossy even for key/button events, + * which means a sustained overflow can lose a release edge. We keep that + * tradeoff explicit here rather than synthesizing corrective events. + */ + if (next == tail) + return false; + + queue->entries[head] = *event; + __atomic_store_n(&queue->head, next, __ATOMIC_RELEASE); + + /* Coalesce wakeups across a whole drain batch. The producer only writes to + * the wake pipe when transitioning wake_pending false -> true and the + * consumer clears it after draining queued events and rechecks for races. + * + * SEQ_CST on this exchange pairs with the SEQ_CST store in + * vinput_rearm_cmd_wake(). The total order guarantees that if this + * exchange reads the stale "true", the consumer's later reads of the + * queue head/tail will observe the store above. Without it, weakly- + * ordered architectures can lose a wake-up. + */ + if (!__atomic_exchange_n(&vinput_cmd_wake_pending, true, __ATOMIC_SEQ_CST)) + g_window.window_wake_backend(); + + return true; +} + +static bool vinput_all_queues_empty(void) +{ + for (int i = 0; i < VINPUT_DEV_CNT; i++) { + struct vinput_cmd_queue *queue = &vinput_cmd_queues[i]; + uint32_t tail = __atomic_load_n(&queue->tail, __ATOMIC_RELAXED); + uint32_t head = __atomic_load_n(&queue->head, __ATOMIC_ACQUIRE); + if (tail != head) + return false; + } + return true; +} + /* Mouse button mapping uses SDL button IDs, not scancodes */ static int vinput_sdl_button_to_linux_key(int sdl_button) { @@ -148,17 +216,93 @@ static int vinput_sdl_scancode_to_linux_key(int sdl_scancode) return -1; } +bool vinput_pop_cmd(int dev_id, struct vinput_cmd *event) +{ + /* Consumer-side dequeue. Called from the emulator thread after poll() + * wakes, and also from the periodic peripheral tick while work remains. + */ + struct vinput_cmd_queue *queue = &vinput_cmd_queues[dev_id]; + uint32_t tail = __atomic_load_n(&queue->tail, __ATOMIC_RELAXED); + uint32_t head = __atomic_load_n(&queue->head, __ATOMIC_ACQUIRE); + + if (tail == head) + return false; + + *event = queue->entries[tail]; + tail = (tail + 1U) & VINPUT_CMD_QUEUE_MASK; + __atomic_store_n(&queue->tail, tail, __ATOMIC_RELEASE); + + return true; +} + +bool vinput_rearm_cmd_wake(void) +{ + /* Clear wake_pending only after the current batch has been drained. If the + * producer published while wake_pending was still true, one of the queues + * will be non-empty here and the consumer must keep draining instead of + * returning to poll(). + * + * SEQ_CST pairs with the SEQ_CST exchange in vinput_push_cmd(). See the + * note there: without a total order, the producer can read a stale "true" + * while this thread reads stale empty queues, losing the wake-up. + */ + __atomic_store_n(&vinput_cmd_wake_pending, false, __ATOMIC_SEQ_CST); + return vinput_all_queues_empty(); +} + +bool vinput_may_have_pending_cmds(void) +{ + return __atomic_load_n(&vinput_cmd_wake_pending, __ATOMIC_RELAXED); +} + +void vinput_reset_host_events(int dev_id) +{ + /* Drop every pending event for this device only. The other device's queue + * is left intact. + */ + struct vinput_cmd event; + while (vinput_pop_cmd(dev_id, &event)) + ; + + /* Restore the wake-gate invariant: wake_pending true means a pipe byte is + * in flight, or the consumer has not rearmed yet. + * + * Reset can run on the emulator thread between main.c consuming the pipe + * byte and the next emu_tick_peripherals() drain. If we left + * wake_pending=true with no backing pipe byte and no events for this + * device to process, a later producer push would see wake_pending=true and + * skip its pipe write, and the emulator could block in poll(-1) + * indefinitely. + * + * Mirror the producer's rearm idiom: clear the gate, then if the other + * device still has work, re-arm the gate with a fresh pipe byte so the + * consumer is guaranteed to be woken and drain it next tick. + */ + __atomic_store_n(&vinput_cmd_wake_pending, false, __ATOMIC_SEQ_CST); + if (!vinput_all_queues_empty() && + !__atomic_exchange_n(&vinput_cmd_wake_pending, true, + __ATOMIC_SEQ_CST)) { + g_window.window_wake_backend(); + } +} + bool vinput_handle_events(void) { SDL_Event e; + uint32_t processed = 0; int linux_key; - bool quit = false; - while (SDL_WaitEventTimeout(&e, VINPUT_SDL_EVENT_WAIT_TIMEOUT_MS)) { + /* SDL stays on the main thread. Wait for one event, then drain a bounded + * burst so the window loop can still return to GPU display work under + * continuous input traffic. + */ + if (!SDL_WaitEventTimeout(&e, VINPUT_SDL_EVENT_WAIT_TIMEOUT_MS)) + return false; + + do { switch (e.type) { case SDL_QUIT: - quit = true; - break; + return true; case SDL_WINDOWEVENT: if (e.window.event == SDL_WINDOWEVENT_FOCUS_LOST) g_window.window_set_mouse_grab(false); @@ -177,31 +321,57 @@ bool vinput_handle_events(void) if (e.key.repeat) break; linux_key = vinput_sdl_scancode_to_linux_key(e.key.keysym.scancode); - if (linux_key >= 0) - virtio_input_update_key(linux_key, 1); + if (linux_key >= 0) { + struct vinput_cmd event = { + .type = VINPUT_CMD_KEYBOARD_KEY, + .u.keyboard_key = {.key = (uint32_t) linux_key, .value = 1}, + }; + vinput_push_cmd(VINPUT_KEYBOARD_ID, &event); + } break; case SDL_KEYUP: linux_key = vinput_sdl_scancode_to_linux_key(e.key.keysym.scancode); - if (linux_key >= 0) - virtio_input_update_key(linux_key, 0); + if (linux_key >= 0) { + struct vinput_cmd event = { + .type = VINPUT_CMD_KEYBOARD_KEY, + .u.keyboard_key = {.key = (uint32_t) linux_key, .value = 0}, + }; + vinput_push_cmd(VINPUT_KEYBOARD_ID, &event); + } break; case SDL_MOUSEBUTTONDOWN: g_window.window_set_mouse_grab(true); linux_key = vinput_sdl_button_to_linux_key(e.button.button); - if (linux_key >= 0) - virtio_input_update_mouse_button_state(linux_key, true); + if (linux_key >= 0) { + struct vinput_cmd event = { + .type = VINPUT_CMD_MOUSE_BUTTON, + .u.mouse_button = {.button = (uint32_t) linux_key, + .pressed = true}, + }; + vinput_push_cmd(VINPUT_MOUSE_ID, &event); + } break; case SDL_MOUSEBUTTONUP: linux_key = vinput_sdl_button_to_linux_key(e.button.button); - if (linux_key >= 0) - virtio_input_update_mouse_button_state(linux_key, false); + if (linux_key >= 0) { + struct vinput_cmd event = { + .type = VINPUT_CMD_MOUSE_BUTTON, + .u.mouse_button = {.button = (uint32_t) linux_key, + .pressed = false}, + }; + vinput_push_cmd(VINPUT_MOUSE_ID, &event); + } break; - case SDL_MOUSEMOTION: + case SDL_MOUSEMOTION: { if (!g_window.window_is_mouse_grabbed() || (e.motion.xrel == 0 && e.motion.yrel == 0)) break; - virtio_input_update_mouse_motion(e.motion.xrel, e.motion.yrel); - break; + struct vinput_cmd event = { + .type = VINPUT_CMD_MOUSE_MOTION, + .u.mouse_motion = {.dx = e.motion.xrel, .dy = e.motion.yrel}, + }; + vinput_push_cmd(VINPUT_MOUSE_ID, &event); + } break; case SDL_MOUSEWHEEL: { int dx = e.wheel.x; int dy = e.wheel.y; @@ -212,13 +382,18 @@ bool vinput_handle_events(void) dx = -dx; dy = -dy; } - virtio_input_update_scroll(dx, dy); + struct vinput_cmd event = { + .type = VINPUT_CMD_MOUSE_WHEEL, + .u.mouse_wheel = {.dx = dx, .dy = dy}, + }; + vinput_push_cmd(VINPUT_MOUSE_ID, &event); break; } } - } + processed++; + } while (processed < VINPUT_SDL_EVENT_BURST_LIMIT && SDL_PollEvent(&e)); - return quit; + return false; } int virtio_input_fill_ev_key_bitmap(uint8_t *bitmap, size_t bitmap_size) diff --git a/virtio-input-event.h b/virtio-input-event.h index 01f6f4d1..264b8757 100644 --- a/virtio-input-event.h +++ b/virtio-input-event.h @@ -19,12 +19,72 @@ enum { VINPUT_DEV_CNT, }; +/* Host-side input commands produced by the window backend. The SDL/main + * thread translates platform input into this neutral form. The emulator + * thread consumes it and updates the virtio-input device state. + */ +enum vinput_cmd_type { + VINPUT_CMD_KEYBOARD_KEY = 0, + VINPUT_CMD_MOUSE_BUTTON, + VINPUT_CMD_MOUSE_MOTION, + VINPUT_CMD_MOUSE_WHEEL, +}; + +/* Input command of the queued backend. Used to make producer does not need to + * touch virtio queues, guest RAM, or heap allocation. + */ +struct vinput_cmd { + enum vinput_cmd_type type; + union { + struct { + uint32_t key; + uint32_t value; + } keyboard_key; + struct { + uint32_t button; + bool pressed; + } mouse_button; + struct { + int32_t dx; + int32_t dy; + } mouse_motion; + struct { + int32_t dx; + int32_t dy; + } mouse_wheel; + } u; +}; + /* Poll and translate pending SDL events on the main thread. Returns true if a * quit/close request was observed, which tells the caller to shut down the * frontend loop. */ bool vinput_handle_events(void); +/* Pop one translated backend input event from the per-device queue. Called by + * the emulator thread while draining work that arrived from the SDL/main + * thread. dev_id selects which device's queue to read. + */ +bool vinput_pop_cmd(int dev_id, struct vinput_cmd *cmd); + +/* Reopen the producer wake gate after the emulator thread drains the current + * batch of queued input events across all device queues. Returns true if every + * queue is empty across the rearm, or false if the producer raced and more + * events are already pending. + */ +bool vinput_rearm_cmd_wake(void); + +/* Returns true once the backend has published input work for the emulator + * thread. This is a cheap fast-path check used to skip queue-drain bookkeeping + * when no translated input events are pending. + */ +bool vinput_may_have_pending_cmds(void); + +/* Drop all pending events for one virtio-input device. Called when the guest + * resets that device; the other device's queue is left untouched. + */ +void vinput_reset_host_events(int dev_id); + /* Fill bitmap[] with exactly the key codes this backend can generate. * Per the virtio-input spec, only advertise key codes this device will * actually generate. Returns the minimum byte count needed (index of the diff --git a/virtio-input.c b/virtio-input.c index 7700e0a4..664be79d 100644 --- a/virtio-input.c +++ b/virtio-input.c @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -15,6 +14,25 @@ #include "virtio-input-event.h" #include "virtio.h" +/* Threading invariant: every function in this file that reads or writes + * guest-visible virtio-input state (descriptors, virtqueues, Status, + * InterruptStatus, the per-device config union) runs exclusively on the + * emulator thread. + * + * The SDL/main thread produces host input through the SPSC queue in + * virtio-input-event.c; the emulator thread consumes that queue in + * virtio_input_drain_host_events() and then calls into this file. Guest MMIO + * accesses arrive via virtio_input_read()/virtio_input_write() from + * mem_load()/mem_store(), which is also the emulator thread. + * + * The only cross-thread touch point is virtio_input_irq_pending(), which reads + * InterruptStatus from the PLIC polling path on the same emulator thread. + * + * No vinput-internal mutex is required as long as this invariant holds. If a + * future change reintroduces SDL-thread writes into virtio-input device state, + * add a lock back at the same time. + */ + #define BUS_VIRTUAL 0x06 #define VINPUT_DEBUG_PREFIX "[SEMU vinput-log]: " @@ -93,7 +111,6 @@ struct vinput_data { int type; /* VINPUT_KEYBOARD_ID or VINPUT_MOUSE_ID */ }; -static pthread_mutex_t vinput_mutex = PTHREAD_MUTEX_INITIALIZER; static struct vinput_data vinput_dev[VINPUT_DEV_CNT]; static const char *vinput_dev_name[VINPUT_DEV_CNT] = { VINPUT_KEYBOARD_NAME, @@ -152,7 +169,7 @@ static inline uint32_t virtio_input_preprocess(virtio_input_state_t *vinput, * used ring. The guest driver uses statusq to send EV_LED events (Caps Lock, * Num Lock, etc.) and acknowledges each buffer so the queue never stalls. * SDL has no portable LED-control API, so LED state is not applied to the host - * keyboard here. Must be called with vinput_mutex held. + * keyboard here. */ static void virtio_input_drain_statusq(virtio_input_state_t *vinput) { @@ -227,6 +244,8 @@ static void virtio_input_update_status(virtio_input_state_t *vinput, /* Reset */ uint32_t *ram = vinput->ram; void *priv = vinput->priv; + int dev_id = PRIV(vinput)->type; + vinput_reset_host_events(dev_id); memset(vinput, 0, sizeof(*vinput)); vinput->ram = ram; vinput->priv = priv; @@ -325,25 +344,23 @@ static void virtio_input_update_eventq(int dev_id, int index = VIRTIO_INPUT_EVENTQ; - /* Start of the critical section */ - pthread_mutex_lock(&vinput_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; + return; if (!((vinput->Status & VIRTIO_STATUS__DRIVER_OK) && queue->ready)) - goto out; + return; /* Check for new buffers */ uint16_t new_avail = ram[queue->QueueAvail] >> 16; uint16_t avail_delta = (uint16_t) (new_avail - queue->last_avail); if (avail_delta > (uint16_t) queue->QueueNum) { fprintf(stderr, "%s(): size check failed\n", __func__); - goto fail; + virtio_input_set_fail(vinput); + return; } /* No buffers available - drop event or handle later */ @@ -353,7 +370,7 @@ static void virtio_input_update_eventq(int dev_id, dev_id); #endif /* TODO: Consider buffering events instead of dropping them */ - goto out; + return; } /* Try to write events to used ring */ @@ -365,18 +382,9 @@ static void virtio_input_update_eventq(int dev_id, */ 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(&vinput_mutex); } -void virtio_input_update_key(uint32_t key, uint32_t ev_value) +static void virtio_input_update_key(uint32_t key, uint32_t ev_value) { #if SEMU_INPUT_DEBUG fprintf(stderr, VINPUT_DEBUG_PREFIX "key code=%u value=%u\n", key, @@ -392,7 +400,8 @@ void virtio_input_update_key(uint32_t key, uint32_t ev_value) virtio_input_update_eventq(VINPUT_KEYBOARD_ID, input_ev, ev_cnt); } -void virtio_input_update_mouse_button_state(uint32_t button, bool pressed) +static void virtio_input_update_mouse_button_state(uint32_t button, + bool pressed) { #if SEMU_INPUT_DEBUG fprintf(stderr, VINPUT_DEBUG_PREFIX "button code=%u pressed=%u\n", button, @@ -407,7 +416,7 @@ void virtio_input_update_mouse_button_state(uint32_t button, bool pressed) virtio_input_update_eventq(VINPUT_MOUSE_ID, input_ev, ev_cnt); } -void virtio_input_update_mouse_motion(int32_t dx, int32_t dy) +static void virtio_input_update_mouse_motion(int32_t dx, int32_t dy) { #if SEMU_INPUT_DEBUG fprintf(stderr, VINPUT_DEBUG_PREFIX "motion dx=%d dy=%d\n", dx, dy); @@ -430,7 +439,7 @@ void virtio_input_update_mouse_motion(int32_t dx, int32_t dy) virtio_input_update_eventq(VINPUT_MOUSE_ID, input_ev, ev_cnt); } -void virtio_input_update_scroll(int32_t dx, int32_t dy) +static void virtio_input_update_scroll(int32_t dx, int32_t dy) { #if SEMU_INPUT_DEBUG fprintf(stderr, VINPUT_DEBUG_PREFIX "scroll dx=%d dy=%d\n", dx, dy); @@ -460,6 +469,54 @@ void virtio_input_update_scroll(int32_t dx, int32_t dy) virtio_input_update_eventq(VINPUT_MOUSE_ID, input_ev, ev_cnt); } +void virtio_input_drain_host_events(void) +{ + for (;;) { + struct vinput_cmd event; + + /* Drain per-device queues on the emulator thread so SDL never touches + * guest-visible virtio-input state directly. + * + * We intentionally drain the whole keyboard queue before touching the + * mouse queue, rather than round-robining between them. The guest- + * visible cross-device order is decided by PLIC arbitration when it + * picks between the pending IRQs, not by the order we drain the queues + * here. Round-robining between queues would not change it. + * + * If a future change raises interrupts mid-drain, adds host-side + * timestamps to virtio_input_event, or otherwise starts to rely on + * sub-tick cross-device ordering, revisit this loop. + */ + while (vinput_pop_cmd(VINPUT_KEYBOARD_ID, &event)) { + if (event.type == VINPUT_CMD_KEYBOARD_KEY) + virtio_input_update_key(event.u.keyboard_key.key, + event.u.keyboard_key.value); + } + + while (vinput_pop_cmd(VINPUT_MOUSE_ID, &event)) { + switch (event.type) { + case VINPUT_CMD_MOUSE_BUTTON: + virtio_input_update_mouse_button_state( + event.u.mouse_button.button, event.u.mouse_button.pressed); + break; + case VINPUT_CMD_MOUSE_MOTION: + virtio_input_update_mouse_motion(event.u.mouse_motion.dx, + event.u.mouse_motion.dy); + break; + case VINPUT_CMD_MOUSE_WHEEL: + virtio_input_update_scroll(event.u.mouse_wheel.dx, + event.u.mouse_wheel.dy); + break; + default: + break; + } + } + + if (vinput_rearm_cmd_wake()) + break; + } +} + static void virtio_input_properties(int dev_id) { struct virtio_input_config *cfg = &vinput_dev[dev_id].cfg; @@ -779,8 +836,6 @@ void virtio_input_read(hart_t *vm, size_t access_size = 0; bool is_cfg = false; - pthread_mutex_lock(&vinput_mutex); - switch (width) { case RV_MEM_LW: access_size = 4; @@ -795,7 +850,7 @@ void virtio_input_read(hart_t *vm, break; default: vm_set_exception(vm, RV_EXC_ILLEGAL_INSN, 0); - goto out; + return; } is_cfg = virtio_input_is_config_access(addr, access_size); @@ -808,20 +863,17 @@ void virtio_input_read(hart_t *vm, if (!is_cfg) { if (access_size != 4 || (addr & 0x3)) { vm_set_exception(vm, RV_EXC_LOAD_MISALIGN, vm->exc_val); - goto out; + return; } } else { if (addr & (access_size - 1)) { vm_set_exception(vm, RV_EXC_LOAD_MISALIGN, vm->exc_val); - goto out; + return; } } 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(&vinput_mutex); } void virtio_input_write(hart_t *vm, @@ -833,8 +885,6 @@ void virtio_input_write(hart_t *vm, size_t access_size = 0; bool is_cfg = false; - pthread_mutex_lock(&vinput_mutex); - switch (width) { case RV_MEM_SW: access_size = 4; @@ -847,7 +897,7 @@ void virtio_input_write(hart_t *vm, break; default: vm_set_exception(vm, RV_EXC_ILLEGAL_INSN, 0); - goto out; + return; } is_cfg = virtio_input_is_config_access(addr, access_size); @@ -861,28 +911,25 @@ void virtio_input_write(hart_t *vm, if (!is_cfg) { if (access_size != 4 || (addr & 0x3)) { vm_set_exception(vm, RV_EXC_STORE_MISALIGN, vm->exc_val); - goto out; + return; } } else { if (addr & (access_size - 1)) { vm_set_exception(vm, RV_EXC_STORE_MISALIGN, vm->exc_val); - goto out; + return; } } if (!virtio_input_reg_write(vinput, addr, value)) vm_set_exception(vm, RV_EXC_STORE_FAULT, vm->exc_val); - -out: - pthread_mutex_unlock(&vinput_mutex); } bool virtio_input_irq_pending(virtio_input_state_t *vinput) { - pthread_mutex_lock(&vinput_mutex); - bool pending = vinput->InterruptStatus != 0; - pthread_mutex_unlock(&vinput_mutex); - return pending; + /* Called from the emulator thread after draining queued window events; see + * the threading invariant at the top of this file. + */ + return vinput->InterruptStatus != 0; } void virtio_input_init(virtio_input_state_t *vinput)