From 0192977783fd065989373ff6a3077b5fc85df25a Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Sun, 25 Oct 2026 06:38:18 -0700 Subject: [PATCH] feat(graphics): drove virtio-gpu's hardware cursor virtio-gpu reported no cursor, so moving a pointer meant presenting a frame. Each picture is padded into one 64 by 64 resource, the only size the device draws, and every display only records a hidden cursor's move. --- kernel/drivers/graphics/gfxfb.h | 19 +- kernel/drivers/graphics/gfxfb_node.h | 10 +- kernel/drivers/graphics/virtio_gpu.cpp | 229 +++++++++++++++++++++---- kernel/drivers/graphics/virtio_gpu.h | 88 ++++++++-- 4 files changed, 287 insertions(+), 59 deletions(-) diff --git a/kernel/drivers/graphics/gfxfb.h b/kernel/drivers/graphics/gfxfb.h index 76502b66..c1d1dabc 100644 --- a/kernel/drivers/graphics/gfxfb.h +++ b/kernel/drivers/graphics/gfxfb.h @@ -258,18 +258,19 @@ class display { /** * @brief Place the hotspot of an output's cursor at (`x`, `y`) in the - * output's pixels, even off the output. A move before any picture only - * records the position, and the cursor stays hidden. The call may wait - * only for the display's own lock and for the hardware to take the - * position, within GFXFB_PRESENT_TIMEOUT_NS of giving it, never for a - * vertical blank or for its driver's task. + * output's pixels, even off the output. A move while the cursor is hidden, + * before any picture or after a hide, only records the position, and the + * cursor stays hidden. The call may wait only for the display's own lock + * and for the hardware to take the position, within + * GFXFB_PRESENT_TIMEOUT_NS of giving it, never for a vertical blank or for + * its driver's task. * @param output Index of the output, below the count. * @param x Column of the hotspot, negative left of the output. * @param y Row of the hotspot, negative above the output. - * @return OK once the hardware was given the position or, before any - * picture, once the display recorded it. Otherwise the first that holds of - * ERR_UNSUPPORTED on an output without a hardware cursor, ERR_DEVICE once - * the hardware has failed, ERR_DARK when the output is not lit, and + * @return OK once the hardware was given the position or, while the cursor + * is hidden, once the display recorded it. Otherwise the first that holds + * of ERR_UNSUPPORTED on an output without a hardware cursor, ERR_DEVICE + * once the hardware has failed, ERR_DARK when the output is not lit, and * ERR_REFUSED when the hardware refused the move. */ virtual int32_t move_cursor(uint32_t output, int32_t x, int32_t y) = 0; diff --git a/kernel/drivers/graphics/gfxfb_node.h b/kernel/drivers/graphics/gfxfb_node.h index f985cd81..84437c6c 100644 --- a/kernel/drivers/graphics/gfxfb_node.h +++ b/kernel/drivers/graphics/gfxfb_node.h @@ -207,10 +207,10 @@ class boot_display : public display { * failing with `fs::ERR_BUSY` while another client controls the buffers or * reserves control, then fails with `fs::ERR_IO` once the display has failed, * and otherwise returns the display's result. The cursor calls for one output - * reach the display one at a time, never waiting for a present there. A - * closing client that controls the buffers hides each output's cursor before - * its control ends, unless the display has failed, and an output whose hide - * the display refuses keeps showing its cursor. + * reach the display one at a time, and gfxfb never makes one wait for a + * present there. A closing client that controls the buffers hides each + * output's cursor before its control ends, unless the display has failed, and + * an output whose hide the display refuses keeps showing its cursor. */ class gfxfb_node : public fs::node { public: @@ -323,7 +323,7 @@ class gfxfb_node : public fs::node { uint32_t m_controller_calls = 0; // admitted calls of `m_controller` that have not ended sync::mutex m_present_locks[GFXFB_OUTPUTS_MAX]; // so presents for one output reach the display one at a time - sync::mutex m_cursor_locks[GFXFB_OUTPUTS_MAX]; // likewise for cursor calls, which never wait for a present + sync::mutex m_cursor_locks[GFXFB_OUTPUTS_MAX]; // likewise for cursor calls, which never wait for a present lock sync::spinlock m_outputs_lock; // over the members below, taken with interrupts disabled output_records m_outputs[GFXFB_OUTPUTS_MAX]; // written unlocked only by `show()`, before the node is opened bool m_display_failed = false; // set for good by a display call's ERR_DEVICE or a failed end diff --git a/kernel/drivers/graphics/virtio_gpu.cpp b/kernel/drivers/graphics/virtio_gpu.cpp index 7b68d0b9..caf8b964 100644 --- a/kernel/drivers/graphics/virtio_gpu.cpp +++ b/kernel/drivers/graphics/virtio_gpu.cpp @@ -20,7 +20,7 @@ constexpr int32_t ERR_ATTACH_UNANSWERED = -3; // The device had no memory left for a command. A client's requests for buffers reach that limit on a healthy device constexpr int32_t ERR_OUT_OF_DEVICE_MEMORY = -4; -// A B8G8R8X8 pixel, which a driver display's buffers hold as a little-endian 32-bit value +// A B8G8R8X8 pixel, which a driver display's buffers hold as a little-endian 32-bit value, or a cursor's B8G8R8A8 pixel constexpr uint32_t BYTES_PER_PIXEL = 4; // Used when the device reports no usable scanout @@ -37,6 +37,10 @@ constexpr uint64_t REFRESH_PERIOD_NS = 1000000000000ULL / REFRESH_MILLIHERTZ; // Bounds a buffer's RAM against a bad scanout size or a client's request constexpr uint32_t MAX_DIMENSION = 4096; +// The device draws a cursor only from a resource of exactly this width and height +constexpr uint32_t CURSOR_DIMENSION = 64; +static_assert(CURSOR_DIMENSION <= gfxfb::CURSOR_DIMENSION_MAX, "a cursor maximum gfxfb accepts"); + // A batch still unanswered this long after its submission marks the device dead constexpr uint64_t BATCH_TIMEOUT_NS = 1000000000ULL; @@ -44,9 +48,9 @@ constexpr uint64_t BATCH_TIMEOUT_NS = 1000000000ULL; // then ends within GFXFB_PRESENT_TIMEOUT_NS even counted from the lock, its latch coming at most a refresh period later constexpr uint64_t PRESENT_BATCH_TIMEOUT_NS = gfxfb::GFXFB_PRESENT_TIMEOUT_NS - REFRESH_PERIOD_NS; -// A batch counts as unanswered no sooner than this long after its submission, even past its deadline, so a task that -// waited for the CPU before submitting fails no device that answers within this long -constexpr uint64_t MIN_BATCH_WAIT_NS = REFRESH_PERIOD_NS; +// A batch or a cursor command counts as unanswered no sooner than this long after its submission, even past its +// deadline, so a task that waited for the CPU before submitting fails no device that answers within this long +constexpr uint64_t MIN_ANSWER_WAIT_NS = REFRESH_PERIOD_NS; // A request chained to a response constexpr uint32_t DESCRIPTORS_PER_COMMAND = 2; @@ -81,6 +85,24 @@ static bool is_usable_mode(const virtio_gpu_display_one& mode) { mode.r.width <= MAX_DIMENSION && mode.r.height <= MAX_DIMENSION; } +// The device places a cursor by its picture's top left corner. A corner CURSOR_DIMENSION or more past the scanout's +// left or top edge shows nothing, so it is held there, which also keeps it a signed 32-bit value for the device +static uint32_t cursor_corner(int32_t hotspot_position, uint32_t hotspot) { + int64_t corner = static_cast(hotspot_position) - hotspot; + int64_t farthest = -static_cast(CURSOR_DIMENSION); + if (corner < farthest) { + corner = farthest; + } + + return static_cast(corner); +} + +// When a request submitted just now counts as unanswered: at `deadline_ns`, or MIN_ANSWER_WAIT_NS from now when later +static uint64_t answer_deadline(uint64_t deadline_ns) { + uint64_t earliest_failure_ns = clock::now_ns() + MIN_ANSWER_WAIT_NS; + return deadline_ns > earliest_failure_ns ? deadline_ns : earliest_failure_ns; +} + // The caller must then keep the buffer's RAM allocated for the life of the kernel static void log_kept_ram(const virtio_gpu_buffer& buf) { log::error( @@ -151,19 +173,20 @@ int32_t virtio_gpu_driver::queue_command(const void* request, uint32_t request_s return OK; } -int32_t virtio_gpu_driver::wait_for_batch(wait_mode mode, uint64_t deadline_ns) { +// Waits until the device hands back `count` requests on `queue`, freeing their descriptors, failing at `deadline_ns` +int32_t virtio_gpu_driver::wait_for_used(virtqueue& queue, uint32_t count, wait_mode mode, uint64_t deadline_ns) { uint32_t completed = 0; while (true) { uint16_t head = 0; uint32_t written = 0; - while (m_control_queue.get_used(&head, &written)) { - m_control_queue.free_desc(head); + while (queue.get_used(&head, &written)) { + queue.free_desc(head); completed++; } - if (completed >= m_batch_commands) { + if (completed >= count) { return OK; } @@ -195,10 +218,7 @@ int32_t virtio_gpu_driver::run_batch_until(wait_mode mode, uint64_t deadline_ns) m_control_queue.kick(m_control_notify); - uint64_t earliest_failure_ns = clock::now_ns() + MIN_BATCH_WAIT_NS; - uint64_t answer_deadline_ns = deadline_ns > earliest_failure_ns ? deadline_ns : earliest_failure_ns; - - if (wait_for_batch(mode, answer_deadline_ns) != OK) { + if (wait_for_used(m_control_queue, m_batch_commands, mode, answer_deadline(deadline_ns)) != OK) { log::error("virtio-gpu: device stopped answering commands"); m_is_broken = true; return ERR_IO; @@ -281,8 +301,14 @@ int32_t virtio_gpu_driver::query_display(uint32_t* out_scanout, uint32_t* out_wi return OK; } -// Allocates the RAM for a width by height picture and fills the buffer's layout from it -int32_t virtio_gpu_driver::allocate_backing(virtio_gpu_buffer& buf, uint32_t width, uint32_t height) { +// Allocates the RAM for a width by height picture and fills the buffer's layout from it. `access` is +// `paging::PAGE_USER` for RAM the driver writes while lowered, and 0 otherwise +int32_t virtio_gpu_driver::allocate_backing( + virtio_gpu_buffer& buf, + uint32_t width, + uint32_t height, + paging::page_flags_t access +) { uint64_t pitch = static_cast(width) * BYTES_PER_PIXEL; size_t pages = pmm::page_align_up(pitch * height) / pmm::PAGE_SIZE; @@ -292,7 +318,7 @@ int32_t virtio_gpu_driver::allocate_backing(virtio_gpu_buffer& buf, uint32_t wid dma_profile(BACKING_ADDRESS_BITS), pages, dma::caching::cached_when_coherent, - 0, + access, buf.backing ) ); @@ -311,15 +337,15 @@ int32_t virtio_gpu_driver::allocate_backing(virtio_gpu_buffer& buf, uint32_t wid return OK; } -// Runs under `m_batch_lock` once the driver is attached. On any failure but `ERR_ATTACH_UNANSWERED` the device holds -// none of the RAM -int32_t virtio_gpu_driver::create_resource(virtio_gpu_buffer& buf, wait_mode mode) { +// Runs under `m_batch_lock` once the driver is attached. Gives the device a resource of `format` pixels backed by the +// buffer's RAM. On any failure but `ERR_ATTACH_UNANSWERED` the device holds none of the RAM +int32_t virtio_gpu_driver::create_resource(virtio_gpu_buffer& buf, uint32_t format, wait_mode mode) { buf.resource_id = m_next_resource_id++; virtio_gpu_resource_create_2d create = {}; create.hdr.type = VIRTIO_GPU_CMD_RESOURCE_CREATE_2D; create.resource_id = buf.resource_id; - create.format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM; + create.format = format; create.width = buf.width; create.height = buf.height; @@ -390,12 +416,12 @@ int32_t virtio_gpu_driver::queue_set_scanout(uint32_t scanout, const virtio_gpu_ } int32_t virtio_gpu_driver::light_scanout(uint32_t scanout, uint32_t width, uint32_t height) { - if (allocate_backing(m_base_buffer, width, height) != OK) { + if (allocate_backing(m_base_buffer, width, height, 0) != OK) { log::error("virtio-gpu: failed to allocate the RAM for a %ux%u scanout", width, height); return ERR_IO; } - if (create_resource(m_base_buffer, wait_mode::poll) != OK) { + if (create_resource(m_base_buffer, VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM, wait_mode::poll) != OK) { return ERR_IO; } @@ -410,6 +436,100 @@ int32_t virtio_gpu_driver::light_scanout(uint32_t scanout, uint32_t width, uint3 return OK; } +// Creates the resource every scanout's cursor is drawn from, a picture of B8G8R8A8 pixels, which as little-endian +// 32-bit values hold alpha, red, green and blue from the top byte down, as gfxfb's cursor pixels do +int32_t virtio_gpu_driver::create_cursor_resource() { + if (allocate_backing(m_cursor_buffer, CURSOR_DIMENSION, CURSOR_DIMENSION, paging::PAGE_USER) != OK) { + log::error("virtio-gpu: failed to allocate the RAM for the cursor"); + return ERR_IO; + } + + return create_resource(m_cursor_buffer, VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM, wait_mode::poll); +} + +// Runs under `m_batch_lock` between batches, sending `command` from the start of the command page. A command the device +// has not handed back by `answer_deadline(deadline_ns)` marks the device dead, as an unanswered batch does +int32_t virtio_gpu_driver::run_cursor_command(const virtio_gpu_update_cursor& command, uint64_t deadline_ns) { + if (m_is_broken) { + return ERR_IO; + } + + string::memcpy(reinterpret_cast(m_command_page.virt), &command, sizeof(command)); + if (m_cursor_queue.add_buf(m_command_page.phys, sizeof(command), 0) < 0) { + return ERR_IO; + } + + m_cursor_queue.kick(m_cursor_notify); + + if (wait_for_used(m_cursor_queue, 1, wait_mode::sleep, answer_deadline(deadline_ns)) != OK) { + log::error("virtio-gpu: device stopped answering cursor commands"); + m_is_broken = true; + return ERR_IO; + } + + return OK; +} + +// A cursor command of `type` for the lit scanout, with a picture's hotspot at the hotspot's recorded position. It names +// the cursor resource even for a move, since the device hides the cursor on a move naming none +virtio_gpu_update_cursor virtio_gpu_driver::cursor_command(uint32_t type, uint32_t hot_x, uint32_t hot_y) const { + virtio_gpu_update_cursor command = {}; + command.hdr.type = type; + command.pos.scanout_id = m_scanout; + command.pos.x = cursor_corner(m_cursor_x, hot_x); + command.pos.y = cursor_corner(m_cursor_y, hot_y); + command.resource_id = m_cursor_buffer.resource_id; + command.hot_x = hot_x; + command.hot_y = hot_y; + + return command; +} + +// Runs under `m_batch_lock`. Shows `image` as the lit scanout's cursor, or hides it at a width of 0, the cursor command +// due by `deadline_ns`. The device reads the resource as it runs the command, so the copy is confirmed first +int32_t virtio_gpu_driver::update_cursor(const gfxfb::cursor_image& image, uint64_t deadline_ns) { + if (image.width == 0) { + virtio_gpu_update_cursor hide = cursor_command(VIRTIO_GPU_CMD_UPDATE_CURSOR, 0, 0); + hide.resource_id = 0; + + int32_t rc = run_cursor_command(hide, deadline_ns); + if (rc == OK) { + m_is_cursor_shown = false; + } + + return rc; + } + + auto* pixels = reinterpret_cast(m_cursor_buffer.backing.virt); + string::memset(pixels, 0, m_cursor_buffer.size); + for (uint32_t row = 0; row < image.height; row++) { + uint32_t* destination = pixels + row * CURSOR_DIMENSION; + const uint32_t* source = image.pixels + row * image.width; + string::memcpy(destination, source, image.width * BYTES_PER_PIXEL); + } + + virtio_gpu_transfer_to_host_2d transfer = {}; + transfer.hdr.type = VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D; + transfer.r = {0, 0, CURSOR_DIMENSION, CURSOR_DIMENSION}; + transfer.resource_id = m_cursor_buffer.resource_id; + + // A copy the device answers by its deadline leaves the cursor command its minimum wait before `deadline_ns` + uint64_t copy_deadline_ns = deadline_ns - MIN_ANSWER_WAIT_NS; + if (queue_command(transfer) != OK || run_batch_until(wait_mode::sleep, copy_deadline_ns) != OK) { + return ERR_IO; + } + + virtio_gpu_update_cursor show = cursor_command(VIRTIO_GPU_CMD_UPDATE_CURSOR, image.hot_x, image.hot_y); + int32_t rc = run_cursor_command(show, deadline_ns); + if (rc == OK) { + m_cursor_hot_x = image.hot_x; + m_cursor_hot_y = image.hot_y; + m_is_cursor_shown = true; + } + + return rc; +} + // Queues the copy of `rect` from `buf`'s RAM into its resource and the flush that shows it on any scanout showing that // resource, first running the batch with `deadline_ns` when the pair does not fit int32_t virtio_gpu_driver::queue_rect_update( @@ -456,14 +576,17 @@ int32_t virtio_gpu_driver::attach() { } if (setup_queue(VIRTIO_GPU_QUEUE_CONTROL, m_control_queue, &m_control_notify) != OK || + setup_queue(VIRTIO_GPU_QUEUE_CURSOR, m_cursor_queue, &m_cursor_notify) != OK || allocate_command_page() != OK) { set_failed(); return ERR_IO; } - // Without MSI-X every batch is polled + // Without MSI-X, or when the device refuses the vector for either queue, every wait polls. The queues share one + // vector, so a wait must sleep on it only under `m_batch_lock` if (setup_msix(1) == OK) { - m_has_completion_interrupt = route_queue_msix(VIRTIO_GPU_QUEUE_CONTROL, 0) == OK; + m_has_completion_interrupt = route_queue_msix(VIRTIO_GPU_QUEUE_CONTROL, 0) == OK && + route_queue_msix(VIRTIO_GPU_QUEUE_CURSOR, 0) == OK; } if (!m_has_completion_interrupt) { @@ -479,7 +602,8 @@ int32_t virtio_gpu_driver::attach() { uint32_t height = 0; if (query_display(&scanout, &width, &height) != OK || - light_scanout(scanout, width, height) != OK) { + light_scanout(scanout, width, height) != OK || + create_cursor_resource() != OK) { log::error("virtio-gpu: display setup failed"); set_failed(); return ERR_IO; @@ -508,6 +632,7 @@ int32_t virtio_gpu_driver::detach() { RUN_ELEVATED(dma::free_pages(m_base_buffer.backing)); } + RUN_ELEVATED(dma::free_pages(m_cursor_buffer.backing)); RUN_ELEVATED(dma::free_pages(m_command_page)); return pci_driver::detach(); @@ -562,13 +687,13 @@ int32_t virtio_gpu_driver::create_buffer(uint32_t width, uint32_t height, uint32 return gfxfb::ERR_NO_MEMORY; } - if (allocate_backing(*buf, width, height) != OK) { + if (allocate_backing(*buf, width, height, 0) != OK) { heap::ufree_delete(buf); return gfxfb::ERR_NO_MEMORY; } RUN_ELEVATED(sync::mutex_lock(m_batch_lock)); - int32_t rc = create_resource(*buf, wait_mode::sleep); + int32_t rc = create_resource(*buf, VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM, wait_mode::sleep); bool is_broken = m_is_broken; RUN_ELEVATED(sync::mutex_unlock(m_batch_lock)); @@ -696,6 +821,52 @@ int32_t virtio_gpu_driver::present( return rc; } +int32_t virtio_gpu_driver::set_cursor(uint32_t output, const gfxfb::cursor_image& image) { + bool fits = image.width <= CURSOR_DIMENSION && image.height <= CURSOR_DIMENSION; + + RUN_ELEVATED(sync::mutex_lock(m_batch_lock)); + uint64_t deadline_ns = clock::now_ns() + gfxfb::GFXFB_PRESENT_TIMEOUT_NS; + + int32_t rc = gfxfb::OK; + if (m_is_broken) { + rc = gfxfb::ERR_DEVICE; + } else if (output != m_scanout) { + rc = gfxfb::ERR_DARK; + } else if (!fits) { + rc = gfxfb::ERR_INVALID_ARG; + } else if (update_cursor(image, deadline_ns) != OK) { + rc = m_is_broken ? gfxfb::ERR_DEVICE : gfxfb::ERR_REFUSED; + } + + RUN_ELEVATED(sync::mutex_unlock(m_batch_lock)); + + return rc; +} + +int32_t virtio_gpu_driver::move_cursor(uint32_t output, int32_t x, int32_t y) { + RUN_ELEVATED(sync::mutex_lock(m_batch_lock)); + uint64_t deadline_ns = clock::now_ns() + gfxfb::GFXFB_PRESENT_TIMEOUT_NS; + + int32_t rc = gfxfb::OK; + if (m_is_broken) { + rc = gfxfb::ERR_DEVICE; + } else if (output != m_scanout) { + rc = gfxfb::ERR_DARK; + } else { + m_cursor_x = x; + m_cursor_y = y; + + virtio_gpu_update_cursor move = cursor_command(VIRTIO_GPU_CMD_MOVE_CURSOR, m_cursor_hot_x, m_cursor_hot_y); + if (m_is_cursor_shown && run_cursor_command(move, deadline_ns) != OK) { + rc = m_is_broken ? gfxfb::ERR_DEVICE : gfxfb::ERR_REFUSED; + } + } + + RUN_ELEVATED(sync::mutex_unlock(m_batch_lock)); + + return rc; +} + void virtio_gpu_driver::release_buffer(gfxfb::buffer* buf) { auto* released = static_cast(buf); @@ -716,9 +887,9 @@ int32_t virtio_gpu_driver::read_output(uint32_t output, gfxfb::output_state& out out.lit = output == m_scanout; out.boot_output = false; - // The driver leaves the device's cursor queue unused - out.cursor_max_width = 0; - out.cursor_max_height = 0; + // Every scanout has a cursor, drawn from the cursor resource + out.cursor_max_width = CURSOR_DIMENSION; + out.cursor_max_height = CURSOR_DIMENSION; if (out.lit) { out.current = { diff --git a/kernel/drivers/graphics/virtio_gpu.h b/kernel/drivers/graphics/virtio_gpu.h index 108df595..475f6786 100644 --- a/kernel/drivers/graphics/virtio_gpu.h +++ b/kernel/drivers/graphics/virtio_gpu.h @@ -15,6 +15,7 @@ namespace drivers::virtio { constexpr uint16_t VIRTIO_DEV_GPU = VIRTIO_DEV_MODERN_BASE + 16; constexpr uint16_t VIRTIO_GPU_QUEUE_CONTROL = 0; +constexpr uint16_t VIRTIO_GPU_QUEUE_CURSOR = 1; constexpr uint32_t VIRTIO_GPU_CMD_GET_DISPLAY_INFO = 0x0100; constexpr uint32_t VIRTIO_GPU_CMD_RESOURCE_CREATE_2D = 0x0101; @@ -25,11 +26,15 @@ 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_CMD_RESOURCE_DETACH_BACKING = 0x0107; +constexpr uint32_t VIRTIO_GPU_CMD_UPDATE_CURSOR = 0x0300; +constexpr uint32_t VIRTIO_GPU_CMD_MOVE_CURSOR = 0x0301; + 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_RESP_ERR_OUT_OF_MEMORY = 0x1201; +constexpr uint32_t VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM = 1; constexpr uint32_t VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM = 2; constexpr uint32_t VIRTIO_GPU_MAX_SCANOUTS = 16; @@ -127,6 +132,24 @@ struct virtio_gpu_resource_flush { uint32_t padding; } __attribute__((packed)); +// The cursor picture's top left corner on a scanout, each coordinate a signed 32-bit value in an unsigned field +struct virtio_gpu_cursor_pos { + uint32_t scanout_id; + uint32_t x; + uint32_t y; + uint32_t padding; +} __attribute__((packed)); + +// Both cursor queue commands, which the device answers by handing them back with no response +struct virtio_gpu_update_cursor { + virtio_gpu_ctrl_hdr hdr; + virtio_gpu_cursor_pos pos; + uint32_t resource_id; // 0 hides the cursor + uint32_t hot_x; + uint32_t hot_y; + uint32_t padding; +} __attribute__((packed)); + } // namespace drivers::virtio namespace drivers { @@ -162,14 +185,31 @@ struct virtio_gpu_buffer : gfxfb::buffer { * after its submission, so `present()` can return past that deadline by up to * a refresh period for each batch that goes out within a refresh period of it * or later, as when its task waited for the CPU, instead of failing a device - * that answers within a refresh period. Once a batch goes unanswered, which - * the driver logs once, or the driver detaches, every call that sends a - * command fails with ERR_DEVICE, but a present whose batch went unanswered - * returns OK and ends with GFXFB_PRESENT_FAILED, since the device may still - * read its buffer. Before that, a command the device refuses fails its call - * with ERR_REFUSED, logged unless the device ran out of memory, or with - * ERR_NO_MEMORY when the device has no memory left for a new buffer's + * that answers within a refresh period. Once a batch or a cursor command goes + * unanswered, which the driver logs once, or the driver detaches, every call + * that sends a command fails with ERR_DEVICE, but a present whose batch went + * unanswered returns OK and ends with GFXFB_PRESENT_FAILED, since the device + * may still read its buffer. Before that, a command the device refuses fails + * its call with ERR_REFUSED, logged unless the device ran out of memory, or + * with ERR_NO_MEMORY when the device has no memory left for a new buffer's * resource. + * + * Every scanout has a hardware cursor of up to 64 by 64 pixels, drawn from the + * one cursor resource, which the driver shows on the lit scanout through + * commands on the device's cursor queue. `set_cursor()` pads the picture with + * transparent pixels into the resource's RAM, waits for the device to confirm + * the copy into the resource, then sends the cursor command. A hide sends only + * a cursor command naming no resource. `move_cursor()` sends the new position, + * only recording it while the cursor is hidden. The device answers a cursor + * command by handing it back with no response, so it can refuse only the + * picture's copy. The cursor command is due GFXFB_PRESENT_TIMEOUT_NS after the + * call takes the driver's lock and the copy a refresh period sooner, as a + * present's batches are, but each of the two counts as unanswered no sooner + * than a refresh period after its submission. So a cursor call returns within + * GFXFB_PRESENT_TIMEOUT_NS of taking the lock, or a refresh period after its + * last submission when its task waited for the CPU. Either way + * `move_cursor()`, whose one submission follows the lock, returns within + * GFXFB_PRESENT_TIMEOUT_NS of handing the device its command. */ class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { public: @@ -199,9 +239,8 @@ class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { uint32_t region_count ) override; - // The driver leaves every scanout's hardware cursor unused, as `read_output()` reports - int32_t set_cursor(uint32_t, const gfxfb::cursor_image&) override { return gfxfb::ERR_UNSUPPORTED; } - int32_t move_cursor(uint32_t, int32_t, int32_t) override { return gfxfb::ERR_UNSUPPORTED; } + int32_t set_cursor(uint32_t output, const gfxfb::cursor_image& image) override; + int32_t move_cursor(uint32_t output, int32_t x, int32_t y) override; void release_buffer(gfxfb::buffer* buf) override; @@ -222,7 +261,7 @@ class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { uint32_t response_size, uint32_t* out_response_offset); int32_t run_batch(wait_mode mode); int32_t run_batch_until(wait_mode mode, uint64_t deadline_ns); - int32_t wait_for_batch(wait_mode mode, uint64_t deadline_ns); + int32_t wait_for_used(virtio::virtqueue& queue, uint32_t count, wait_mode mode, uint64_t deadline_ns); // For commands whose response is only a header template @@ -232,11 +271,15 @@ class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { uint32_t read_scanout_count(); int32_t query_display(uint32_t* out_scanout, uint32_t* out_width, uint32_t* out_height); - int32_t allocate_backing(virtio_gpu_buffer& buf, uint32_t width, uint32_t height); - int32_t create_resource(virtio_gpu_buffer& buf, wait_mode mode); + int32_t allocate_backing(virtio_gpu_buffer& buf, uint32_t width, uint32_t height, paging::page_flags_t access); + int32_t create_resource(virtio_gpu_buffer& buf, uint32_t format, wait_mode mode); int32_t destroy_resource(const virtio_gpu_buffer& buf); int32_t queue_set_scanout(uint32_t scanout, const virtio_gpu_buffer& buf); int32_t light_scanout(uint32_t scanout, uint32_t width, uint32_t height); + int32_t create_cursor_resource(); + int32_t run_cursor_command(const virtio::virtio_gpu_update_cursor& command, uint64_t deadline_ns); + int32_t update_cursor(const gfxfb::cursor_image& image, uint64_t deadline_ns); + virtio::virtio_gpu_update_cursor cursor_command(uint32_t type, uint32_t hot_x, uint32_t hot_y) const; int32_t queue_rect_update(const virtio_gpu_buffer& buf, const gfxfb::gfxfb_rect& rect, uint64_t deadline_ns); int32_t show_buffer(const virtio_gpu_buffer& buf, const gfxfb::gfxfb_rect* regions, uint32_t count); void set_latch_timer(); @@ -245,8 +288,10 @@ class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { virtio::virtqueue m_control_queue; uintptr_t m_control_notify = 0; + virtio::virtqueue m_cursor_queue; + uintptr_t m_cursor_notify = 0; - // A batch's requests and responses share this DMA page + // A batch's requests and responses share this DMA page, and a cursor command is staged at its start between batches dma::buffer m_command_page = {}; uint32_t m_batch_bytes = 0; uint32_t m_batch_commands = 0; @@ -258,11 +303,11 @@ class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { uint64_t m_lit_ns = 0; // when the scanout was lit, the first of its refresh boundaries virtio_gpu_buffer m_base_buffer = {}; - // Held across a batch's completion wait + // Held across a batch's or a cursor command'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 + bool m_is_broken = false; // a batch or a cursor command timed out, so the device may still own the page // Read and advanced under `m_batch_lock` once the driver is attached uint32_t m_next_resource_id = 1; @@ -270,6 +315,17 @@ class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { // The resource the lit scanout shows, read and written under `m_batch_lock` once the driver is attached uint32_t m_shown_resource_id = 0; + // The cursor resource, created at attach, whose RAM holds the last picture `set_cursor()` padded into it + virtio_gpu_buffer m_cursor_buffer = {}; + + // The lit scanout's cursor, read and written under `m_batch_lock`: the hotspot's last position in the scanout's + // pixels, and the hotspot in the picture shown while `m_is_cursor_shown` + int32_t m_cursor_x = 0; + int32_t m_cursor_y = 0; + uint32_t m_cursor_hot_x = 0; + uint32_t m_cursor_hot_y = 0; + bool m_is_cursor_shown = false; + // The latch of the last present the device confirmed, which `m_latch_timer` reports. Its callback reads them with // no lock, since they are set before the timer and gfxfb sends the scanout no present until their present has ended timer::deadline_timer m_latch_timer;