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 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/.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/ diff --git a/Makefile b/Makefile index ea2a3491..8943203d 100644 --- a/Makefile +++ b/Makefile @@ -158,6 +158,31 @@ 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 +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 + 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/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 diff --git a/device.h b/device.h index 62cf66b6..9d8d4cfc 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,67 @@ 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); + +/* 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. + */ +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 +497,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 +524,24 @@ 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 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. 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 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 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..cd15e86e 100644 --- a/main.c +++ b/main.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,10 @@ #include "coro.h" #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" #include "riscv_private.h" #define PRIV(x) ((emu_state_t *) x->priv) @@ -112,6 +117,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 +234,24 @@ 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) + /* 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); + + 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 +315,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 +402,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 +914,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 +1219,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 +1251,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 +1260,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 +1271,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 +1290,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 +1310,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 +1330,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 +1415,18 @@ static int semu_run(emu_state_t *emu) pfd_count++; } +#if SEMU_HAS(VIRTIOINPUT) + /* 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) { + 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 +1481,24 @@ static int semu_run(emu_state_t *emu) perror("poll"); } +#if SEMU_HAS(VIRTIOINPUT) + /* 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)) { + 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 +1525,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 +1581,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 +1639,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 +1657,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 +1691,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 +1720,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 +1764,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..2073c2d6 --- /dev/null +++ b/virtio-input-event.c @@ -0,0 +1,414 @@ +#include +#include + +#include "device.h" +#include "virtio-input-codes.h" +#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} + +struct vinput_key_map_entry { + int sdl_scancode; + 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), + 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), +}; + +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) +{ + 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_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; + + /* 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: + return true; + 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) { + 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) { + 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) { + 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) { + 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: { + if (!g_window.window_is_mouse_grabbed() || + (e.motion.xrel == 0 && e.motion.yrel == 0)) + 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; + /* 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; + } + 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 false; +} + +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..264b8757 --- /dev/null +++ b/virtio-input-event.h @@ -0,0 +1,95 @@ +#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, +}; + +/* 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 + * 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..664be79d --- /dev/null +++ b/virtio-input.c @@ -0,0 +1,949 @@ +#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" + +/* 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]: " + +#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 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. + */ +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; + int dev_id = PRIV(vinput)->type; + vinput_reset_host_events(dev_id); + 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; + + uint32_t *ram = vinput->ram; + virtio_input_queue_t *queue = &vinput->queues[index]; + + /* Check device status */ + if (vinput->Status & VIRTIO_STATUS__DEVICE_NEEDS_RESET) + return; + + if (!((vinput->Status & VIRTIO_STATUS__DRIVER_OK) && queue->ready)) + 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__); + virtio_input_set_fail(vinput); + return; + } + + /* 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 */ + return; + } + + /* 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; +} + +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, + 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}, + {.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); +} + +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, + 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}, + }; + + size_t ev_cnt = ARRAY_SIZE(input_ev); + virtio_input_update_eventq(VINPUT_MOUSE_ID, input_ev, ev_cnt); +} + +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); +#endif + 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); +} + +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); +#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). + */ + 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); +} + +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; + 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; + + 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); + return; + } + + 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); + return; + } + } else { + if (addr & (access_size - 1)) { + vm_set_exception(vm, RV_EXC_LOAD_MISALIGN, vm->exc_val); + return; + } + } + + if (!virtio_input_reg_read(vinput, addr, value, access_size)) + vm_set_exception(vm, RV_EXC_LOAD_FAULT, vm->exc_val); +} + +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; + + 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); + return; + } + + 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); + return; + } + } else { + if (addr & (access_size - 1)) { + vm_set_exception(vm, RV_EXC_STORE_MISALIGN, vm->exc_val); + return; + } + } + + if (!virtio_input_reg_write(vinput, addr, value)) + vm_set_exception(vm, RV_EXC_STORE_FAULT, vm->exc_val); +} + +bool virtio_input_irq_pending(virtio_input_state_t *vinput) +{ + /* 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) +{ + 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