Display & Input

Framebuffer, SDL, simplefb, VGA, and input event routing

Data Flow

graph TD A[Guest writes to FB RAM] --> B[Dirty bits set] B --> C[sdl_refresh / simplefb_refresh] C --> D[SDL_BlitSurface + SDL_UpdateRect] E[Host Keyboard] --> F[SDL key event] F --> G[vm_send_key_event] G --> H[virtio_input_send_key_event] I[Host Mouse] --> J[SDL mouse event] J --> K[vm_send_mouse_event] K --> L[virtio_input_send_mouse_event] M[Console resize] --> N[virtio_console_resize_event]

Framebuffer Architecture

TinyEMU abstracts the display via FBDevice (machine.h). The device-specific code (simplefb or VGA) fills fb_data, width, height, and stride. The SDL frontend polls for dirty pages and redraws only changed regions.

struct FBDevice {
    int width;
    int height;
    int stride;
    uint8_t *fb_data;
    int fb_size;
    void *device_opaque;
    void (*refresh)(struct FBDevice *fb_dev,
                    SimpleFBDrawFunc *redraw_func, void *opaque);
};

Simple Framebuffer

simplefb.c registers a RAM region with DEVRAM_FLAG_DIRTY_BITS at FRAMEBUFFER_BASE_ADDR. The guest writes pixels directly into RAM. The host detects changes via the dirty-bit bitmap and calls the registered redraw_func.

SimpleFBState *simplefb_init(PhysMemoryMap *map, uint64_t phys_addr,
                             FBDevice *fb_dev, int width, int height)
{
    SimpleFBState *s = mallocz(sizeof(*s));
    fb_dev->width = width;
    fb_dev->height = height;
    fb_dev->stride = width * 4;
    fb_dev->fb_size = (height * fb_dev->stride + FB_ALLOC_ALIGN - 1)
                      & ~(FB_ALLOC_ALIGN - 1);
    s->fb_page_count = fb_dev->fb_size >> DEVRAM_PAGE_SIZE_LOG2;
    s->mem_range = cpu_register_ram(map, phys_addr, fb_dev->fb_size,
                                    DEVRAM_FLAG_DIRTY_BITS);
    fb_dev->fb_data = s->mem_range->phys_mem;
    fb_dev->device_opaque = s;
    fb_dev->refresh = simplefb_refresh1;
    return s;
}

SDL Frontend

sdl.c creates an SDL window and surface. During sdl_refresh() it calls fb_dev->refresh(), which invokes sdl_update() to blit dirty rectangles to the screen.

static void sdl_update(FBDevice *fb_dev, void *opaque,
                       int x, int y, int w, int h)
{
    SDL_Rect r;
    r.x = x; r.y = y; r.w = w; r.h = h;
    SDL_BlitSurface(fb_surface, &r, screen, &r);
    SDL_UpdateRect(screen, r.x, r.y, r.w, r.h);
}

void sdl_refresh(VirtMachine *m)
{
    if (m->fb_dev) {
        m->fb_dev->refresh(m->fb_dev, sdl_update, NULL);
    }
}

VGA / PCI Display

For x86 machines, TinyEMU includes a minimal VGA device (vga.c) and a PCI VGA card (pci.c). It exposes Bochs VBE registers and a linear framebuffer. The same FBDevice abstraction is used to forward updates to SDL.

VGAState *pci_vga_init(PCIBus *bus, FBDevice *fb_dev,
                       int width, int height,
                       const uint8_t *vga_rom_buf, int vga_rom_size)
{
    VGAState *s = mallocz(sizeof(*s));
    s->fb_dev = fb_dev;
    /* register PCI BARs, VBE IO ports, VGA memory */
    return s;
}

Input Event Routing

Keyboard and mouse events from SDL are routed through the machine class vtable to VirtIO input devices. For RISC-V, riscv_vm_send_key_event and riscv_vm_send_mouse_event forward to the respective VIRTIODevice.

static void riscv_vm_send_key_event(VirtMachine *s1, BOOL is_down,
                                    uint16_t key_code)
{
    RISCVMachine *s = (RISCVMachine *)s1;
    if (s->keyboard_dev) {
        virtio_input_send_key_event(s->keyboard_dev, is_down, key_code);
    }
}

static void riscv_vm_send_mouse_event(VirtMachine *s1, int dx, int dy, int dz,
                                      unsigned int buttons)
{
    RISCVMachine *s = (RISCVMachine *)s1;
    if (s->mouse_dev) {
        virtio_input_send_mouse_event(s->mouse_dev, dx, dy, dz, buttons);
    }
}

Console Resize

When the terminal window changes size, the STDIODevice sets resize_pending. The next iteration of virt_machine_run() reads the new dimensions and notifies the VirtIO console via virtio_console_resize_event().

if (s->resize_pending) {
    int width, height;
    console_get_size(s, &width, &height);
    virtio_console_resize_event(m->console_dev, width, height);
    s->resize_pending = FALSE;
}