From cfa58e54da4def3f36869e30240c1b6a54252c33 Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Mon, 5 Oct 2026 21:38:28 -0700 Subject: [PATCH 1/4] fix(drivers): verified virtio MSI-X vectors and guarded reset before mapping --- kernel/drivers/net/virtio_net.cpp | 11 +++++++--- kernel/drivers/virtio/virtio_pci_driver.cpp | 23 +++++++++++++++++++-- kernel/drivers/virtio/virtio_pci_driver.h | 9 +++++--- kernel/drivers/virtio/virtqueue.h | 5 +++++ 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/kernel/drivers/net/virtio_net.cpp b/kernel/drivers/net/virtio_net.cpp index 7b314f81..d141b654 100644 --- a/kernel/drivers/net/virtio_net.cpp +++ b/kernel/drivers/net/virtio_net.cpp @@ -137,9 +137,14 @@ int32_t virtio_net_driver::attach() { // Assign MSI-X vectors to queues if (m_dev->get_msi_state().mode == pci::MSI_MODE_MSIX) { - route_config_msix(0); - route_queue_msix(VIRTIO_NET_QUEUE_RX, 0); - route_queue_msix(VIRTIO_NET_QUEUE_TX, (m_dev->get_msi_state().vector_count > 1) ? 1 : 0); + uint16_t tx_vector = (m_dev->get_msi_state().vector_count > 1) ? 1 : 0; + + if (route_config_msix(0) != 0 || + route_queue_msix(VIRTIO_NET_QUEUE_RX, 0) != 0 || + route_queue_msix(VIRTIO_NET_QUEUE_TX, tx_vector) != 0) { + set_failed(); + return -1; + } } set_driver_ok(); diff --git a/kernel/drivers/virtio/virtio_pci_driver.cpp b/kernel/drivers/virtio/virtio_pci_driver.cpp index 1c384aa0..3bb7786d 100644 --- a/kernel/drivers/virtio/virtio_pci_driver.cpp +++ b/kernel/drivers/virtio/virtio_pci_driver.cpp @@ -260,16 +260,31 @@ int32_t virtio_pci_driver::setup_queue(uint16_t index, virtqueue& queue, uintptr return 0; } -void virtio_pci_driver::route_queue_msix(uint16_t index, uint16_t vector) { +int32_t virtio_pci_driver::route_queue_msix(uint16_t index, uint16_t vector) { m_common_cfg->queue_select = index; sync::atomic_fence_seq_cst(); m_common_cfg->queue_msix_vector = vector; sync::atomic_fence_seq_cst(); + + // The device reads back NO_VECTOR in place of a vector it cannot use + if (m_common_cfg->queue_msix_vector != vector) { + log::error("%s: queue %u refused MSI-X vector %u", m_log_name, index, vector); + return -1; + } + + return 0; } -void virtio_pci_driver::route_config_msix(uint16_t vector) { +int32_t virtio_pci_driver::route_config_msix(uint16_t vector) { m_common_cfg->msix_config = vector; sync::atomic_fence_seq_cst(); + + if (m_common_cfg->msix_config != vector) { + log::error("%s: config changes refused MSI-X vector %u", m_log_name, vector); + return -1; + } + + return 0; } void virtio_pci_driver::set_driver_ok() { @@ -281,6 +296,10 @@ void virtio_pci_driver::set_failed() { } void virtio_pci_driver::reset_device() { + if (!m_common_cfg) { + return; + } + write_status(0); while (read_status() != 0) { cpu::relax(); diff --git a/kernel/drivers/virtio/virtio_pci_driver.h b/kernel/drivers/virtio/virtio_pci_driver.h index d8db319b..6b62610a 100644 --- a/kernel/drivers/virtio/virtio_pci_driver.h +++ b/kernel/drivers/virtio/virtio_pci_driver.h @@ -44,13 +44,16 @@ class virtio_pci_driver : public pci_driver { */ int32_t setup_queue(uint16_t index, virtio::virtqueue& queue, uintptr_t* out_notify); - // Route a queue's interrupts or config change interrupts to an MSI-X vector - void route_queue_msix(uint16_t index, uint16_t vector); - void route_config_msix(uint16_t vector); + // Route a queue's interrupts or config change interrupts to an MSI-X vector. + // Both return negative when the device cannot use the vector. + int32_t route_queue_msix(uint16_t index, uint16_t vector); + int32_t route_config_msix(uint16_t vector); // Queues may carry buffers only after the driver reports itself ready void set_driver_ok(); void set_failed(); + + // Does nothing until start_transport() maps the device, so a failed attach can still detach void reset_device(); // Reading the ISR status acknowledges a legacy or MSI interrupt diff --git a/kernel/drivers/virtio/virtqueue.h b/kernel/drivers/virtio/virtqueue.h index 205118c0..ac04d467 100644 --- a/kernel/drivers/virtio/virtqueue.h +++ b/kernel/drivers/virtio/virtqueue.h @@ -94,6 +94,11 @@ class virtqueue { */ bool has_used() const; + /** + * Number of free descriptors. `add_buf()` uses one and `add_buf_chain()` uses two. + */ + uint16_t free_count() const { return m_free_count; } + /** * Notify the device by writing to the doorbell. * @param notify_addr Virtual address of the notification register. From 3cfb44325c6e6d918e5924c91dc0e5544213fa74 Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Mon, 5 Oct 2026 22:03:42 -0700 Subject: [PATCH 2/4] feat(drivers): added a virtio-gpu display driver --- kernel/drivers/graphics/virtio_gpu.cpp | 421 +++++++++++++++++++++++++ kernel/drivers/graphics/virtio_gpu.h | 169 ++++++++++ kernel/drivers/pci_driver.h | 5 +- kernel/drivers/virtio/virtio_pci.h | 3 + 4 files changed, 595 insertions(+), 3 deletions(-) create mode 100644 kernel/drivers/graphics/virtio_gpu.cpp create mode 100644 kernel/drivers/graphics/virtio_gpu.h diff --git a/kernel/drivers/graphics/virtio_gpu.cpp b/kernel/drivers/graphics/virtio_gpu.cpp new file mode 100644 index 00000000..fdd61cca --- /dev/null +++ b/kernel/drivers/graphics/virtio_gpu.cpp @@ -0,0 +1,421 @@ +#include "drivers/graphics/virtio_gpu.h" +#include "clock/clock.h" +#include "common/logging.h" +#include "common/string.h" +#include "dynpriv/dynpriv.h" +#include "hw/cpu.h" +#include "mm/heap.h" +#include "mm/vmm.h" + +namespace drivers { + +using namespace virtio; + +constexpr int32_t ERR_DEVICE = -1; +constexpr int32_t ERR_BATCH_FULL = -2; + +constexpr uint32_t RESOURCE_ID = 1; + +constexpr uint16_t BITS_PER_PIXEL = 32; +constexpr uint32_t BYTES_PER_PIXEL = BITS_PER_PIXEL / 8; + +// A B8G8R8X8 pixel read as a little-endian 32-bit value +constexpr uint8_t RED_SHIFT = 16; +constexpr uint8_t GREEN_SHIFT = 8; +constexpr uint8_t BLUE_SHIFT = 0; + +// Used when the device reports no usable scanout +constexpr uint32_t DEFAULT_WIDTH = 1024; +constexpr uint32_t DEFAULT_HEIGHT = 768; + +// Bounds the backing allocation against a bad scanout size +constexpr uint32_t MAX_DIMENSION = 4096; + +// A polled batch slower than this marks the device dead +constexpr uint64_t POLL_TIMEOUT_NS = 1000000000ULL; + +// A request chained to a response +constexpr uint32_t DESCRIPTORS_PER_COMMAND = 2; + +// A transfer and a flush +constexpr uint32_t COMMANDS_PER_RECT = 2; + +// The alignment of the widest request or response field +constexpr uint32_t COMMAND_ALIGN = 8; + +static uint32_t align_command_bytes(uint32_t size) { + return (size + COMMAND_ALIGN - 1) & ~(COMMAND_ALIGN - 1); +} + +// Page bytes taken by a request and its response +static uint32_t command_footprint(uint32_t request_size, uint32_t response_size) { + return align_command_bytes(request_size) + align_command_bytes(response_size); +} + +// OK responses sit below the first error type +static bool is_ok_response(uint32_t type) { + return type >= VIRTIO_GPU_RESP_OK_NODATA && type < VIRTIO_GPU_RESP_ERR_UNSPEC; +} + +static bool is_usable_mode(const virtio_gpu_display_one& mode) { + return mode.enabled && mode.r.width > 0 && mode.r.height > 0 && + mode.r.width <= MAX_DIMENSION && mode.r.height <= MAX_DIMENSION; +} + +int32_t virtio_gpu_driver::allocate_command_page() { + int32_t rc = 0; + RUN_ELEVATED( + rc = vmm::alloc_contiguous( + 1, pmm::ZONE_DMA32, + paging::PAGE_READ | paging::PAGE_WRITE | paging::PAGE_USER | paging::PAGE_DMA, + vmm::ALLOC_ZERO, kva::tag::generic, + m_command_page_va, m_command_page_phys) + ); + + if (rc != vmm::OK) { + log::error("virtio-gpu: failed to allocate the command page"); + return ERR_DEVICE; + } + + return OK; +} + +bool virtio_gpu_driver::batch_has_room(uint32_t bytes, uint32_t commands) const { + return m_batch_bytes + bytes <= pmm::PAGE_SIZE && + m_batch_commands + commands <= MAX_BATCH_COMMANDS && + m_control_queue.free_count() >= commands * DESCRIPTORS_PER_COMMAND; +} + +int32_t virtio_gpu_driver::queue_command(const void* request, uint32_t request_size, + uint32_t response_size, uint32_t* out_response_offset) { + if (m_is_broken) { + return ERR_DEVICE; + } + + if (!batch_has_room(command_footprint(request_size, response_size), 1)) { + return ERR_BATCH_FULL; + } + + uint32_t request_offset = m_batch_bytes; + uint32_t response_offset = request_offset + align_command_bytes(request_size); + auto* page = reinterpret_cast(m_command_page_va); + string::memcpy(page + request_offset, request, request_size); + string::memset(page + response_offset, 0, response_size); + + int32_t head = m_control_queue.add_buf_chain( + m_command_page_phys + request_offset, request_size, + m_command_page_phys + response_offset, response_size, + 0, VRING_DESC_F_WRITE); + + if (head < 0) { + return ERR_BATCH_FULL; + } + + m_response_offsets[m_batch_commands++] = response_offset; + m_batch_bytes = response_offset + align_command_bytes(response_size); + + if (out_response_offset) { + *out_response_offset = response_offset; + } + + return OK; +} + +int32_t virtio_gpu_driver::wait_for_batch(wait_mode mode) { + uint32_t completed = 0; + uint64_t deadline = clock::now_ns() + POLL_TIMEOUT_NS; + + while (true) { + uint16_t head = 0; + uint32_t written = 0; + + while (m_control_queue.get_used(&head, &written)) { + m_control_queue.free_desc(head); + completed++; + } + + if (completed >= m_batch_commands) { + return OK; + } + + // Wait queues have no timeout, so only polling can detect a dead device + if (mode == wait_mode::sleep && m_has_completion_interrupt) { + wait_for_event(); + } else if (clock::now_ns() > deadline) { + return ERR_DEVICE; + } else { + cpu::relax(); + } + } +} + +int32_t virtio_gpu_driver::run_batch(wait_mode mode) { + if (m_is_broken) { + return ERR_DEVICE; + } + + if (m_batch_commands == 0) { + return OK; + } + + m_control_queue.kick(m_control_notify); + + if (wait_for_batch(mode) != OK) { + log::error("virtio-gpu: device stopped answering commands"); + m_is_broken = true; + return ERR_DEVICE; + } + + int32_t rc = OK; + auto* page = reinterpret_cast(m_command_page_va); + + for (uint32_t i = 0; i < m_batch_commands && rc == OK; i++) { + auto* response = reinterpret_cast(page + m_response_offsets[i]); + + if (!is_ok_response(response->type)) { + log::error("virtio-gpu: command failed with response 0x%x", response->type); + rc = ERR_DEVICE; + } + } + + m_batch_commands = 0; + m_batch_bytes = 0; + + return rc; +} + +int32_t virtio_gpu_driver::query_display(uint32_t* out_scanout, uint32_t* out_width, uint32_t* out_height) { + virtio_gpu_ctrl_hdr request = {}; + request.type = VIRTIO_GPU_CMD_GET_DISPLAY_INFO; + + uint32_t response_offset = 0; + int32_t rc = queue_command(&request, sizeof(request), + sizeof(virtio_gpu_resp_display_info), &response_offset); + + if (rc != OK) { + return rc; + } + + rc = run_batch(wait_mode::poll); + if (rc != OK) { + return rc; + } + + // The response stays in the page until the next command is queued + auto* display_info = reinterpret_cast( + m_command_page_va + response_offset); + + if (display_info->hdr.type != VIRTIO_GPU_RESP_OK_DISPLAY_INFO) { + log::error("virtio-gpu: display info came back as response 0x%x", display_info->hdr.type); + return ERR_DEVICE; + } + + for (uint32_t i = 0; i < VIRTIO_GPU_MAX_SCANOUTS; i++) { + virtio_gpu_display_one mode = display_info->pmodes[i]; + + if (is_usable_mode(mode)) { + *out_scanout = i; + *out_width = mode.r.width; + *out_height = mode.r.height; + return OK; + } + } + + log::warn("virtio-gpu: no usable scanout reported, using %ux%u", DEFAULT_WIDTH, DEFAULT_HEIGHT); + *out_scanout = 0; + *out_width = DEFAULT_WIDTH; + *out_height = DEFAULT_HEIGHT; + + return OK; +} + +int32_t virtio_gpu_driver::create_scanout_surface(uint32_t scanout, uint32_t width, uint32_t height) { + uint64_t pitch = static_cast(width) * BYTES_PER_PIXEL; + uint64_t size = pitch * height; + size_t pages = pmm::page_align_up(size) / pmm::PAGE_SIZE; + + pmm::phys_addr_t phys = 0; + int32_t rc = 0; + RUN_ELEVATED( + rc = vmm::alloc_contiguous( + pages, pmm::ZONE_ANY, paging::PAGE_KERNEL_RW, + vmm::ALLOC_ZERO, kva::tag::generic, + m_backing_va, phys) + ); + + if (rc != vmm::OK) { + log::error("virtio-gpu: failed to allocate a %lu byte backing", size); + return ERR_DEVICE; + } + + virtio_gpu_resource_create_2d create = {}; + create.hdr.type = VIRTIO_GPU_CMD_RESOURCE_CREATE_2D; + create.resource_id = RESOURCE_ID; + create.format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM; + create.width = width; + create.height = height; + + virtio_gpu_resource_attach_backing attach_backing = {}; + attach_backing.hdr.type = VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING; + attach_backing.resource_id = RESOURCE_ID; + attach_backing.nr_entries = 1; + attach_backing.entry.addr = phys; + attach_backing.entry.length = static_cast(size); + + virtio_gpu_set_scanout set_scanout = {}; + set_scanout.hdr.type = VIRTIO_GPU_CMD_SET_SCANOUT; + set_scanout.r = {0, 0, width, height}; + set_scanout.scanout_id = scanout; + set_scanout.resource_id = RESOURCE_ID; + + if (queue_command(create) != OK || + queue_command(attach_backing) != OK || + queue_command(set_scanout) != OK || + run_batch(wait_mode::poll) != OK) { + return ERR_DEVICE; + } + + m_surface = { + .phys = phys, + .width = width, + .height = height, + .pitch = pitch, + .bpp = BITS_PER_PIXEL, + .red_shift = RED_SHIFT, + .green_shift = GREEN_SHIFT, + .blue_shift = BLUE_SHIFT, + .cacheable = true, + }; + + return OK; +} + +// Runs the batch first when the rect does not fit +int32_t virtio_gpu_driver::queue_rect_update(const gfxfb::gfxfb_rect& rect) { + virtio_gpu_transfer_to_host_2d transfer = {}; + transfer.hdr.type = VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D; + transfer.r = {rect.x, rect.y, rect.width, rect.height}; + transfer.offset = rect.y * m_surface.pitch + static_cast(rect.x) * BYTES_PER_PIXEL; + transfer.resource_id = RESOURCE_ID; + + virtio_gpu_resource_flush show = {}; + show.hdr.type = VIRTIO_GPU_CMD_RESOURCE_FLUSH; + show.r = transfer.r; + show.resource_id = RESOURCE_ID; + + // Checked together so a rect is never copied without being shown + uint32_t bytes = command_footprint(sizeof(transfer), sizeof(virtio_gpu_ctrl_hdr)) + + command_footprint(sizeof(show), sizeof(virtio_gpu_ctrl_hdr)); + + if (!batch_has_room(bytes, COMMANDS_PER_RECT) && run_batch(wait_mode::sleep) != OK) { + return ERR_DEVICE; + } + + if (queue_command(transfer) != OK || queue_command(show) != OK) { + return ERR_DEVICE; + } + + return OK; +} + +int32_t virtio_gpu_driver::attach() { + log::info("virtio-gpu: attaching to %02x:%02x.%x", + m_dev->bus(), m_dev->slot(), m_dev->func()); + + uint64_t features = 0; + int32_t rc = start_transport(VIRTIO_F_VERSION_1, 0, &features); + if (rc != OK) { + return rc; + } + + if (setup_queue(VIRTIO_GPU_QUEUE_CONTROL, m_control_queue, &m_control_notify) != OK || + allocate_command_page() != OK) { + set_failed(); + return ERR_DEVICE; + } + + // Without MSI-X every batch is polled + if (setup_msix(1) == OK) { + m_has_completion_interrupt = route_queue_msix(VIRTIO_GPU_QUEUE_CONTROL, 0) == OK; + } + + if (!m_has_completion_interrupt) { + log::warn("virtio-gpu: no completion interrupt, polling instead"); + } + + set_driver_ok(); + + uint32_t scanout = 0; + uint32_t width = 0; + uint32_t height = 0; + + if (query_display(&scanout, &width, &height) != OK || + create_scanout_surface(scanout, width, height) != OK) { + log::error("virtio-gpu: display setup failed"); + set_failed(); + return ERR_DEVICE; + } + + RUN_ELEVATED(rc = gfxfb::register_display(m_surface, this)); + if (rc != gfxfb::OK) { + log::error("virtio-gpu: display registration failed"); + set_failed(); + return ERR_DEVICE; + } + + m_is_registered = true; + log::info("virtio-gpu: %ux%u on scanout %u, device is live", width, height, scanout); + + return OK; +} + +int32_t virtio_gpu_driver::detach() { + // gfxfb can still call flush(), which must fail once the page is gone + m_is_broken = true; + reset_device(); + + // gfxfb keeps a registered surface for the life of the kernel + if (!m_is_registered && m_backing_va) { + RUN_ELEVATED(vmm::free(m_backing_va)); + m_backing_va = 0; + } + + if (m_command_page_va) { + RUN_ELEVATED(vmm::free(m_command_page_va)); + m_command_page_va = 0; + } + + return pci_driver::detach(); +} + +void virtio_gpu_driver::run() { + // Batches complete in the flushing task, so this task has no work +} + +int32_t virtio_gpu_driver::flush(const gfxfb::gfxfb_rect* rects, uint32_t count) { + RUN_ELEVATED(sync::mutex_lock(m_batch_lock)); + + int32_t rc = OK; + for (uint32_t i = 0; i < count && rc == OK; i++) { + rc = queue_rect_update(rects[i]); + } + + if (rc == OK) { + rc = run_batch(wait_mode::sleep); + } + + RUN_ELEVATED(sync::mutex_unlock(m_batch_lock)); + + return rc == OK ? gfxfb::OK : gfxfb::ERR; +} + +__PRIVILEGED_CODE void virtio_gpu_driver::on_interrupt(uint32_t vector) { + (void)vector; + ack_interrupt(); +} + +REGISTER_PCI_DRIVER(virtio_gpu_driver, + PCI_MATCH(VIRTIO_VENDOR_ID, VIRTIO_DEV_GPU, PCI_MATCH_ANY_8, PCI_MATCH_ANY_8, PCI_MATCH_ANY_8), + PCI_DRIVER_FACTORY(virtio_gpu_driver)); + +} // namespace drivers diff --git a/kernel/drivers/graphics/virtio_gpu.h b/kernel/drivers/graphics/virtio_gpu.h new file mode 100644 index 00000000..1e30875d --- /dev/null +++ b/kernel/drivers/graphics/virtio_gpu.h @@ -0,0 +1,169 @@ +#ifndef STELLUX_DRIVERS_GRAPHICS_VIRTIO_GPU_H +#define STELLUX_DRIVERS_GRAPHICS_VIRTIO_GPU_H + +#include "drivers/virtio/virtio_pci_driver.h" +#include "drivers/graphics/gfxfb.h" +#include "sync/mutex.h" + +namespace drivers::virtio { + +constexpr uint16_t VIRTIO_DEV_GPU = VIRTIO_DEV_MODERN_BASE + 16; + +constexpr uint16_t VIRTIO_GPU_QUEUE_CONTROL = 0; + +constexpr uint32_t VIRTIO_GPU_CMD_GET_DISPLAY_INFO = 0x0100; +constexpr uint32_t VIRTIO_GPU_CMD_RESOURCE_CREATE_2D = 0x0101; +constexpr uint32_t VIRTIO_GPU_CMD_SET_SCANOUT = 0x0103; +constexpr uint32_t VIRTIO_GPU_CMD_RESOURCE_FLUSH = 0x0104; +constexpr uint32_t VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D = 0x0105; +constexpr uint32_t VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING = 0x0106; + +constexpr uint32_t VIRTIO_GPU_RESP_OK_NODATA = 0x1100; +constexpr uint32_t VIRTIO_GPU_RESP_OK_DISPLAY_INFO = 0x1101; +constexpr uint32_t VIRTIO_GPU_RESP_ERR_UNSPEC = 0x1200; + +constexpr uint32_t VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM = 2; + +constexpr uint32_t VIRTIO_GPU_MAX_SCANOUTS = 16; + +struct virtio_gpu_ctrl_hdr { + uint32_t type; + uint32_t flags; + uint64_t fence_id; + uint32_t ctx_id; + uint8_t ring_idx; + uint8_t padding[3]; +} __attribute__((packed)); + +struct virtio_gpu_rect { + uint32_t x; + uint32_t y; + uint32_t width; + uint32_t height; +} __attribute__((packed)); + +struct virtio_gpu_display_one { + virtio_gpu_rect r; + uint32_t enabled; + uint32_t flags; +} __attribute__((packed)); + +struct virtio_gpu_resp_display_info { + virtio_gpu_ctrl_hdr hdr; + virtio_gpu_display_one pmodes[VIRTIO_GPU_MAX_SCANOUTS]; +} __attribute__((packed)); + +struct virtio_gpu_resource_create_2d { + virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t format; + uint32_t width; + uint32_t height; +} __attribute__((packed)); + +struct virtio_gpu_mem_entry { + uint64_t addr; + uint32_t length; + uint32_t padding; +} __attribute__((packed)); + +// A single entry, enough for a physically contiguous backing +struct virtio_gpu_resource_attach_backing { + virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t nr_entries; + virtio_gpu_mem_entry entry; +} __attribute__((packed)); + +struct virtio_gpu_set_scanout { + virtio_gpu_ctrl_hdr hdr; + virtio_gpu_rect r; + uint32_t scanout_id; + uint32_t resource_id; +} __attribute__((packed)); + +struct virtio_gpu_transfer_to_host_2d { + virtio_gpu_ctrl_hdr hdr; + virtio_gpu_rect r; + uint64_t offset; + uint32_t resource_id; + uint32_t padding; +} __attribute__((packed)); + +struct virtio_gpu_resource_flush { + virtio_gpu_ctrl_hdr hdr; + virtio_gpu_rect r; + uint32_t resource_id; + uint32_t padding; +} __attribute__((packed)); + +} // namespace drivers::virtio + +namespace drivers { + +/** + * A virtio GPU used as a 2D display. Userland draws into the RAM backing its + * one resource, and `flush()` copies the changed rects into it and shows them. + */ +class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { +public: + virtio_gpu_driver(pci::device* dev) + : virtio_pci_driver("virtio_gpu", "virtio-gpu", dev) { + m_batch_lock.init(); + } + + int32_t attach() override; + int32_t detach() override; + void run() override; + + int32_t flush(const gfxfb::gfxfb_rect* rects, uint32_t count) override; + + /** @note Privilege: **required** */ + __PRIVILEGED_CODE void on_interrupt(uint32_t vector) override; + +private: + static constexpr uint32_t MAX_BATCH_COMMANDS = 64; + + // The boot thread cannot sleep, so attach polls for completions + enum class wait_mode : uint8_t { poll, sleep }; + + int32_t allocate_command_page(); + bool batch_has_room(uint32_t bytes, uint32_t commands) const; + int32_t queue_command(const void* request, uint32_t request_size, + uint32_t response_size, uint32_t* out_response_offset); + int32_t run_batch(wait_mode mode); + int32_t wait_for_batch(wait_mode mode); + + // For commands whose response is only a header + template + int32_t queue_command(const T& request) { + return queue_command(&request, sizeof(request), sizeof(virtio::virtio_gpu_ctrl_hdr), nullptr); + } + + int32_t query_display(uint32_t* out_scanout, uint32_t* out_width, uint32_t* out_height); + int32_t create_scanout_surface(uint32_t scanout, uint32_t width, uint32_t height); + int32_t queue_rect_update(const gfxfb::gfxfb_rect& rect); + + virtio::virtqueue m_control_queue; + uintptr_t m_control_notify = 0; + + // A batch's requests and responses share this DMA page + uintptr_t m_command_page_va = 0; + pmm::phys_addr_t m_command_page_phys = 0; + uint32_t m_batch_bytes = 0; + uint32_t m_batch_commands = 0; + uint32_t m_response_offsets[MAX_BATCH_COMMANDS] = {}; + + uintptr_t m_backing_va = 0; + gfxfb::surface m_surface = {}; + + // Held across a batch's completion wait + sync::mutex m_batch_lock; + bool m_has_completion_interrupt = false; + bool m_is_registered = false; + bool m_is_broken = false; // a batch timed out, so the device may still own the page +}; + +} // namespace drivers + +#endif // STELLUX_DRIVERS_GRAPHICS_VIRTIO_GPU_H diff --git a/kernel/drivers/pci_driver.h b/kernel/drivers/pci_driver.h index def10ddd..14164d4e 100644 --- a/kernel/drivers/pci_driver.h +++ b/kernel/drivers/pci_driver.h @@ -121,9 +121,8 @@ class pci_driver : public device_driver { int32_t setup_msix(uint32_t count); /** - * Block the driver task until an interrupt or other event wakes it. - * Multiple interrupts may coalesce into a single wake, the driver's - * run() loop must drain all pending device work on each return. + * Block until an interrupt arrives. One task may wait at a time, and it + * must drain all pending work on return since interrupts coalesce. */ void wait_for_event(); diff --git a/kernel/drivers/virtio/virtio_pci.h b/kernel/drivers/virtio/virtio_pci.h index b03b39b6..c274acb4 100644 --- a/kernel/drivers/virtio/virtio_pci.h +++ b/kernel/drivers/virtio/virtio_pci.h @@ -8,6 +8,9 @@ namespace drivers::virtio { // Virtio PCI vendor ID constexpr uint16_t VIRTIO_VENDOR_ID = 0x1AF4; +// Modern PCI device IDs are this base plus the virtio device type +constexpr uint16_t VIRTIO_DEV_MODERN_BASE = 0x1040; + // Virtio PCI capability types (vendor-specific cap, cap_vndr = 0x09) constexpr uint8_t VIRTIO_PCI_CAP_COMMON_CFG = 1; constexpr uint8_t VIRTIO_PCI_CAP_NOTIFY_CFG = 2; From 52a3049c8fc0f60c57aa53594f6eade120fa4918 Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Mon, 5 Oct 2026 22:18:47 -0700 Subject: [PATCH 3/4] feat(stlxdm): flushed presented damage to displays that need it --- userland/apps/stlxdm/src/presenter.cpp | 65 +++++++++++++++++++++++++- userland/apps/stlxdm/src/presenter.hpp | 5 +- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/userland/apps/stlxdm/src/presenter.cpp b/userland/apps/stlxdm/src/presenter.cpp index aa305e14..9ce011f7 100644 --- a/userland/apps/stlxdm/src/presenter.cpp +++ b/userland/apps/stlxdm/src/presenter.cpp @@ -1,5 +1,7 @@ #include "presenter.hpp" +#include +#include #include #include #include @@ -8,6 +10,9 @@ #include constexpr unsigned long GFXFB_GET_INFO = 0x4700; +constexpr unsigned long GFXFB_FLUSH = 0x4701; + +constexpr uint8_t GFXFB_INFO_NEEDS_FLUSH = 0x01; namespace { @@ -19,12 +24,31 @@ struct gfxfb_info { uint8_t red_shift; uint8_t green_shift; uint8_t blue_shift; - uint8_t padding[3]; + uint8_t flags; + uint8_t padding[2]; uint64_t size; }; +struct gfxfb_rect { + uint32_t x; + uint32_t y; + uint32_t width; + uint32_t height; +}; + +struct gfxfb_flush_args { + uint64_t rects; + uint32_t count; + uint32_t reserved; +}; + } // namespace +static gfxfb_rect to_gfxfb_rect(const damage_list::rect& r) { + return { static_cast(r.x), static_cast(r.y), + static_cast(r.w), static_cast(r.h) }; +} + int memcpy_presenter::init() { m_fd = open("/dev/gfxfb", O_RDWR); if (m_fd < 0) { @@ -38,7 +62,7 @@ int memcpy_presenter::init() { return -1; } - /* The scanout mapping is write-combining, reads from it are + /* The scanout mapping may be write-combining, where reads are * pathologically slow, so composition targets m_back instead */ void* mem = mmap(nullptr, info.size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); @@ -53,6 +77,7 @@ int memcpy_presenter::init() { m_pitch = static_cast(info.pitch); m_size = info.size; m_scanout = static_cast(mem); + m_needs_flush = (info.flags & GFXFB_INFO_NEEDS_FLUSH) != 0; m_back = static_cast( malloc(static_cast(m_width) * m_height * 4)); @@ -99,18 +124,54 @@ void memcpy_presenter::copy_rect(const damage_list::rect& r) { } } +void memcpy_presenter::flush_display(const damage_list::rect* rects, uint32_t count) { + if (!m_needs_flush || count == 0) { + return; + } + + gfxfb_rect changed[damage_list::MAX_RECTS]; + uint32_t changed_count = 0; + + /* A failed flush left its regions unshown, so the next one sends the whole screen */ + if (m_last_flush_failed) { + changed[changed_count++] = { 0, 0, m_width, m_height }; + } else { + for (uint32_t i = 0; i < count; i++) { + changed[changed_count++] = to_gfxfb_rect(rects[i]); + } + } + + gfxfb_flush_args args = {}; + args.rects = reinterpret_cast(changed); + args.count = changed_count; + + bool failed = ioctl(m_fd, GFXFB_FLUSH, &args) < 0; + if (failed && !m_last_flush_failed) { + printf("stlxdm: display flush failed: %s\r\n", strerror(errno)); + } + + m_last_flush_failed = failed; +} + void memcpy_presenter::present(const damage_list& damage) { if (damage.full()) { damage_list::rect whole = { 0, 0, static_cast(m_width), static_cast(m_height) }; copy_rect(whole); + flush_display(&whole, 1); return; } + damage_list::rect copied[damage_list::MAX_RECTS]; + uint32_t count = 0; + for (uint32_t i = 0; i < damage.count(); i++) { damage_list::rect r = damage.at(i); if (damage_list::clip(r, static_cast(m_width), static_cast(m_height))) { copy_rect(r); + copied[count++] = r; } } + + flush_display(copied, count); } diff --git a/userland/apps/stlxdm/src/presenter.hpp b/userland/apps/stlxdm/src/presenter.hpp index f412a696..c29666c0 100644 --- a/userland/apps/stlxdm/src/presenter.hpp +++ b/userland/apps/stlxdm/src/presenter.hpp @@ -35,7 +35,7 @@ class presenter { }; /* Composes into one persistent buffer and row-copies damage into the - * write-combining scanout mapping, the only mode today's hardware has. */ + * framebuffer mapping, flushing it on displays that need a flush. */ class memcpy_presenter final : public presenter { public: /* Opens and maps the framebuffer. Returns 0, or -1 without one. */ @@ -49,6 +49,7 @@ class memcpy_presenter final : public presenter { private: void copy_rect(const damage_list::rect& r); + void flush_display(const damage_list::rect* rects, uint32_t count); int m_fd = -1; uint8_t* m_scanout = nullptr; @@ -58,6 +59,8 @@ class memcpy_presenter final : public presenter { uint32_t m_pitch = 0; uint64_t m_size = 0; bool m_first_acquire = true; + bool m_needs_flush = false; + bool m_last_flush_failed = false; }; #endif From 786d7a4667262e4ab950985151d386345d8441ac Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Mon, 5 Oct 2026 22:23:59 -0700 Subject: [PATCH 4/4] feat(build): ran aarch64 QEMU on a 1920x1080 virtio GPU instead of ramfb --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7c7e8def..77d9f01e 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,9 @@ else $(error ACCEL must be tcg or hvf) endif +# The kernel drives a virtio GPU because the aarch64 firmware caps ramfb at 1024x768 +QEMU_AARCH64_DISPLAY := -device virtio-gpu-pci,xres=1920,yres=1080 + # QEMU_X2APIC=1 makes the firmware start Stellux in x2APIC mode. # The extra CPU at APIC ID 256 forces this because only x2APIC mode can address it. ifeq ($(QEMU_X2APIC),1) @@ -297,7 +300,7 @@ run-qemu-aarch64: check-image-aarch64 -smp $(QEMU_CPU_CORES) \ -bios $(QEMU_EFI_AARCH64) \ -drive format=raw,file=$(IMAGE_DIR)/stellux-aarch64.img \ - -device ramfb \ + $(QEMU_AARCH64_DISPLAY) \ -device qemu-xhci,id=xhci \ -device usb-hub,bus=xhci.0,port=1 \ -device usb-kbd,bus=xhci.0,port=1.1 \ @@ -419,7 +422,7 @@ run-qemu-aarch64-debug: check-image-aarch64 -smp $(QEMU_CPU_CORES) \ -bios $(QEMU_EFI_AARCH64) \ -drive format=raw,file=$(IMAGE_DIR)/stellux-aarch64.img \ - -device ramfb \ + $(QEMU_AARCH64_DISPLAY) \ -device qemu-xhci,id=xhci \ -device usb-hub,bus=xhci.0,port=1 \ -device usb-kbd,bus=xhci.0,port=1.1 \