diff --git a/kernel/drivers/graphics/gfxfb.cpp b/kernel/drivers/graphics/gfxfb.cpp index ec0abe35..de386edc 100644 --- a/kernel/drivers/graphics/gfxfb.cpp +++ b/kernel/drivers/graphics/gfxfb.cpp @@ -153,6 +153,49 @@ static int32_t to_fs_error(int32_t rc) { } } +// A cursor maximum as `read_output` reports it, held to CURSOR_DIMENSION_MAX so the pictures gfxfb copies stay bounded +// and GFXFB_GET_OUTPUT reports the largest picture GFXFB_SET_CURSOR takes +static uint32_t bounded_cursor_max(uint32_t reported) { + return reported < CURSOR_DIMENSION_MAX ? reported : CURSOR_DIMENSION_MAX; +} + +// Copies the picture `args` shows onto the unprivileged heap, as userland supplies it and it can fill 256 KiB, once its +// hotspot lies inside it and it fits the output's cursor maximum. On OK the caller frees `pixels` +static int32_t copy_cursor_picture(display& shown_display, const gfxfb_cursor_args& args, uint32_t*& pixels) { + bool hotspot_inside = args.hot_x < args.width && args.hot_y < args.height; + if (!hotspot_inside) { + return fs::ERR_INVAL; + } + + output_state state = {}; + int32_t rc = shown_display.read_output(args.output, state); + if (rc != OK) { + return to_fs_error(rc); + } + + bool fits_across = args.width <= bounded_cursor_max(state.cursor_max_width); + bool fits_down = args.height <= bounded_cursor_max(state.cursor_max_height); + if (!fits_across || !fits_down) { + return fs::ERR_INVAL; + } + + size_t bytes = static_cast(args.width) * args.height * sizeof(uint32_t); + auto* copy = static_cast(heap::ualloc(bytes)); + if (!copy) { + return fs::ERR_NOMEM; + } + + rc = mm::uaccess::copy_from_user(copy, reinterpret_cast(args.pixels), bytes); + if (rc != mm::uaccess::OK) { + heap::ufree(copy); + return fs::ERR_INVAL; + } + + pixels = copy; + + return fs::OK; +} + // The event reporting how the present of `handle` on `output` ended, with a frame and a time only for one that latched static gfxfb_event present_event(uint32_t output, uint32_t handle, const present_result& result) { bool latched = result.status == GFXFB_PRESENT_LATCHED; @@ -210,6 +253,8 @@ int32_t boot_display::read_output(uint32_t output, output_state& out) { out.lit = true; out.boot_output = true; + out.cursor_max_width = 0; + out.cursor_max_height = 0; // The boot framebuffer carries no refresh rate out.current = { @@ -247,6 +292,10 @@ void gfxfb_node::init_locks() { lock.init(); } + for (sync::mutex& lock : m_cursor_locks) { + lock.init(); + } + m_outputs_lock = sync::SPINLOCK_INIT; } @@ -378,8 +427,8 @@ void gfxfb_node::wait_for_unended_presents(client& owner) { sync::spin_unlock_irqrestore(m_outputs_lock, state); } -// Drops the events of `owner`, the controlling client, and presents each output's recorded base buffer where the output -// shows another buffer, naming no region. Waits for the display to end those it accepts +// Drops the events of `owner`, the controlling client, presents each output's recorded base buffer where the output +// shows another buffer, naming no region, and hides every cursor. Waits for the display to end the presents it accepts void gfxfb_node::give_outputs_back(client& owner) { // No file reads `owner`'s events any more, and `queue_present()` refuses an output whose event `owner` holds unread sync::irq_state state = sync::spin_lock_irqsave(owner.events_lock); @@ -399,6 +448,10 @@ void gfxfb_node::give_outputs_back(client& owner) { if (base) { queue_present(owner, output, 0, base, nullptr, 0); } + + // The hide's result is dropped, since a close reports nothing. When the display refuses it with ERR_REFUSED, + // the cursor stays until a later controller's GFXFB_SET_CURSOR on the output replaces it + give_cursor_image(output, {}); } wait_for_unended_presents(owner); @@ -475,6 +528,14 @@ int32_t gfxfb_node::ioctl(fs::file* f, uint32_t cmd, uint64_t arg) { return present(client_of(f), arg); } + if (cmd == GFXFB_SET_CURSOR) { + return set_cursor(client_of(f), arg); + } + + if (cmd == GFXFB_MOVE_CURSOR) { + return move_cursor(client_of(f), arg); + } + return fs::ERR_NOSYS; } @@ -540,6 +601,8 @@ int32_t gfxfb_node::get_output(uint64_t info_addr) { info.flags = (state.lit ? GFXFB_OUTPUT_LIT : 0) | (is_primary ? GFXFB_OUTPUT_PRIMARY : 0); info.reserved = 0; info.current = state.lit ? state.current : gfxfb_mode{}; + info.cursor_max_width = bounded_cursor_max(state.cursor_max_width); + info.cursor_max_height = bounded_cursor_max(state.cursor_max_height); rc = mm::uaccess::copy_to_user( reinterpret_cast(info_addr), &info, sizeof(info)); @@ -842,6 +905,102 @@ int32_t gfxfb_node::queue_present( return to_fs_error(rc); } +int32_t gfxfb_node::set_cursor(client& owner, uint64_t args_addr) { + gfxfb_cursor_args args{}; + int32_t rc = mm::uaccess::copy_from_user( + &args, reinterpret_cast(args_addr), sizeof(args)); + + if (rc != mm::uaccess::OK) { + return fs::ERR_INVAL; + } + + bool is_reserved_clear = args.reserved == 0; + bool names_output = args.output < clamped_output_count(*m_display); + if (!is_reserved_clear || !names_output) { + return fs::ERR_INVAL; + } + + // A width of 0 hides the cursor, so the picture's other fields go unread + uint32_t* pixels = nullptr; + cursor_image image = {}; + if (args.width > 0) { + rc = copy_cursor_picture(*m_display, args, pixels); + if (rc != fs::OK) { + return rc; + } + + image = { + .pixels = pixels, + .width = args.width, + .height = args.height, + .hot_x = args.hot_x, + .hot_y = args.hot_y, + }; + } + + rc = fs::ERR_BUSY; + if (begin_controlled_call(owner)) { + rc = give_cursor_image(args.output, image); + end_controlled_call(rc == fs::OK); + } + + if (pixels) { + heap::ufree(pixels); + } + + return rc; +} + +// Hands the display `image` for `output`'s cursor, a width of 0 hiding the cursor, unless the display failed +int32_t gfxfb_node::give_cursor_image(uint32_t output, const cursor_image& image) { + sync::mutex_lock(m_cursor_locks[output]); + + int32_t rc = fs::ERR_IO; + if (!has_display_failed()) { + int32_t display_rc = m_display->set_cursor(output, image); + mark_failed_on_device_error(display_rc); + rc = to_fs_error(display_rc); + } + + sync::mutex_unlock(m_cursor_locks[output]); + + return rc; +} + +int32_t gfxfb_node::move_cursor(client& owner, uint64_t position_addr) { + gfxfb_cursor_position position{}; + int32_t rc = mm::uaccess::copy_from_user( + &position, reinterpret_cast(position_addr), sizeof(position)); + + if (rc != mm::uaccess::OK) { + return fs::ERR_INVAL; + } + + bool is_reserved_clear = position.reserved == 0; + bool names_output = position.output < clamped_output_count(*m_display); + if (!is_reserved_clear || !names_output) { + return fs::ERR_INVAL; + } + + if (!begin_controlled_call(owner)) { + return fs::ERR_BUSY; + } + + sync::mutex_lock(m_cursor_locks[position.output]); + + rc = fs::ERR_IO; + if (!has_display_failed()) { + int32_t display_rc = m_display->move_cursor(position.output, position.x, position.y); + mark_failed_on_device_error(display_rc); + rc = to_fs_error(display_rc); + } + + sync::mutex_unlock(m_cursor_locks[position.output]); + end_controlled_call(rc == fs::OK); + + return rc; +} + /** * @note Privilege: **required** */ diff --git a/kernel/drivers/graphics/gfxfb.h b/kernel/drivers/graphics/gfxfb.h index 3f417458..76502b66 100644 --- a/kernel/drivers/graphics/gfxfb.h +++ b/kernel/drivers/graphics/gfxfb.h @@ -34,13 +34,17 @@ * `create_buffer()`, and presents buffers on outputs, which gfxfb hands to the * display's `present()`. The display reports to `present_done()` how each * present it accepted ended, and gfxfb queues the end as an event for the open - * file that presented. One open file at a time controls the buffers, from its - * first creation or present that succeeds until it closes. gfxfb keeps a - * reference-counted record of each buffer, every lit output's base buffer - * included, and hands the buffer back to the display's `release_buffer()` once - * no handle, output or mapping refers to it. Once a display call returns - * ERR_DEVICE or a present ends with `GFXFB_PRESENT_FAILED`, gfxfb fails every - * later `GFXFB_FLUSH` and `GFXFB_PRESENT` without calling the display. + * file that presented. Userland gives an output's hardware cursor a picture + * and moves it, which gfxfb hands to the display's `set_cursor()` and + * `move_cursor()`. One open file at a time controls the buffers, from its + * first creation, present or cursor call that succeeds until it closes. gfxfb + * keeps a reference-counted record of each buffer, every lit output's base + * buffer included, and hands the buffer back to the display's + * `release_buffer()` once no handle, output or mapping refers to it. Once a + * display call returns ERR_DEVICE or a present ends with + * `GFXFB_PRESENT_FAILED`, gfxfb fails every later `GFXFB_FLUSH`, + * `GFXFB_PRESENT`, `GFXFB_SET_CURSOR` and `GFXFB_MOVE_CURSOR` without handing + * it to the display. */ namespace gfxfb { @@ -51,16 +55,23 @@ constexpr int32_t ERR_INVALID_ARG = -2; constexpr int32_t ERR_DARK = -3; // the output is not lit constexpr int32_t ERR_DEVICE = -4; // the display's hardware failed for good constexpr int32_t ERR_NO_MEMORY = -5; -constexpr int32_t ERR_UNSUPPORTED = -6; // the display creates or presents no buffers +constexpr int32_t ERR_UNSUPPORTED = -6; // the display creates or presents no buffers, or the output has no cursor constexpr int32_t ERR_REFUSED = -7; // the hardware refused the call and stays usable +// The widest and tallest cursor picture a display may report for an output, which bounds the pictures gfxfb copies +constexpr uint32_t CURSOR_DIMENSION_MAX = 256; + /** - * One output's state as its display reports it. + * One output's state as its display reports it. Its cursor maximum is the + * largest picture its hardware cursor shows, at most CURSOR_DIMENSION_MAX in + * each dimension, or 0 by 0 for an output without a hardware cursor. */ struct output_state { bool lit; - bool boot_output; // the boot firmware lit this output - gfxfb_mode current; // the mode it is lit at, meaningful while lit + bool boot_output; // the boot firmware lit this output + gfxfb_mode current; // the mode it is lit at, meaningful while lit + uint32_t cursor_max_width; // the widest cursor picture it shows + uint32_t cursor_max_height; // the tallest }; /** @@ -92,23 +103,41 @@ struct present_result { uint64_t timestamp_ns; // when the latching vertical blank began, on the kernel's monotonic clock }; +/** + * A cursor picture as gfxfb hands it to `display::set_cursor()`: `width` by + * `height` pixels, row after row with no padding, and its hotspot (the pixel + * that sits at the cursor's position). Each pixel holds alpha in bits 31 to 24 + * and red, green and blue as a buffer's pixel does, none multiplied by alpha. + * A width of 0 hides the cursor, with every other field zero. + */ +struct cursor_image { + const uint32_t* pixels; // in gfxfb's memory, valid only during the call + uint32_t width; + uint32_t height; + uint32_t hot_x; + uint32_t hot_y; +}; + /** * A display behind /dev/gfxfb: one a device driver owns, or gfxfb's own boot * display. gfxfb calls `output_count()`, `read_output()` and `base_buffer()` * from `init()` and `register_display()`, on the boot thread, which cannot * sleep, so they must not wait. It calls `output_count()`, `read_output()`, - * `needs_flush()`, `flush()`, `create_buffer()` and `present()` from the tasks - * that use /dev/gfxfb, several tasks at once, and `flush()`, `create_buffer()` - * and `present()` may block the task that calls them. gfxfb never calls - * `present()` for one output from two tasks at once, nor while a present the - * display accepted there has not ended. It calls `release_buffer()` from the - * reaper, the kernel task that runs deferred cleanup, and `release_buffer()` - * must never wait, since the reaper runs no other cleanup until it returns. A - * display returns ERR_DEVICE only once its hardware has failed for good, and - * from then on from every call that needs the hardware, so a refusal that - * leaves the hardware usable is ERR_REFUSED, or ERR_NO_MEMORY when the - * hardware has no memory left for a buffer. It likewise ends a present with - * `GFXFB_PRESENT_FAILED` only once its hardware has failed for good. + * `needs_flush()`, `flush()`, `create_buffer()`, `present()`, `set_cursor()` + * and `move_cursor()` from the tasks that use /dev/gfxfb, several tasks at + * once, and `flush()`, `create_buffer()`, `present()` and the two cursor calls + * may block the task that calls them. gfxfb never calls `present()` for one + * output from two tasks at once, nor while a present the display accepted + * there has not ended, and never makes two cursor calls for one output at + * once, though a cursor call may run during a present for the same output. It + * calls `release_buffer()` from the reaper, the kernel task that runs deferred + * cleanup, and `release_buffer()` must never wait, since the reaper runs no + * other cleanup until it returns. A display returns ERR_DEVICE only once its + * hardware has failed for good, and from then on from every call that needs + * the hardware, so a refusal that leaves the hardware usable is ERR_REFUSED, + * or ERR_NO_MEMORY when the hardware has no memory left for a buffer. It + * likewise ends a present with `GFXFB_PRESENT_FAILED` only once its hardware + * has failed for good. */ class display { public: @@ -208,6 +237,43 @@ class display { */ virtual int32_t present(uint32_t output, buffer* buf, const gfxfb_rect* regions, uint32_t region_count) = 0; + /** + * @brief Give an output's hardware cursor `image`, or hide the cursor when + * `image.width` is 0. The display must copy the picture before returning, + * converting it if its hardware wants another form. The call may wait only + * for the display's own lock, for the hardware to take the picture and for + * the copy, never for a vertical blank or for its driver's task, and + * returns within a time the display states. + * @param output Index of the output, below the count. + * @param image The picture with its hotspot inside it, or a hide with a + * width of 0. gfxfb frees its pixels once the call returns. + * @return OK once the hardware was given the picture, or hid the cursor. + * Otherwise the display returns 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, ERR_INVALID_ARG for a + * picture wider or taller than the output's cursor maximum, and + * ERR_REFUSED when the hardware refused the call. + */ + virtual int32_t set_cursor(uint32_t output, const cursor_image& image) = 0; + + /** + * @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. + * @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 + * ERR_REFUSED when the hardware refused the move. + */ + virtual int32_t move_cursor(uint32_t output, int32_t x, int32_t y) = 0; + /** * @brief Take back a buffer from `create_buffer()` or `base_buffer()` once * nothing refers to it, freeing it unless the display keeps it for good, diff --git a/kernel/drivers/graphics/gfxfb_abi.h b/kernel/drivers/graphics/gfxfb_abi.h index edf97e47..4a115a28 100644 --- a/kernel/drivers/graphics/gfxfb_abi.h +++ b/kernel/drivers/graphics/gfxfb_abi.h @@ -44,10 +44,10 @@ * A program can also create buffers, each holding one picture of 32-bit * pixels. `GFXFB_CREATE_BUFFER` returns a new buffer's handle, its layout and * the mmap offset that maps it. An open file controls the buffers from its - * first `GFXFB_CREATE_BUFFER` or `GFXFB_PRESENT` that succeeds until it - * closes. A creation fails with EINVAL when the kernel cannot read its - * argument, when its width or height is zero or when its reserved field is - * set. Any other creation, and any present that passes the checks below, fails + * first creation, present or cursor call that succeeds until it closes. A + * creation fails with EINVAL when the kernel cannot read its argument, when + * its width or height is zero or when its reserved field is set. Any other + * creation, and any present or cursor call that passes the checks below, fails * with EBUSY while another open file controls the buffers or reserves control, * and otherwise reserves control for its file until the call returns, so two * files never control the buffers at once. A creation that reserves control @@ -120,12 +120,38 @@ * there are none. `poll` reports the file readable while it holds an event, * and never writable. A file that closes while a present of it has not ended * waits for that end, whose event then reaches no file. A closing file that - * controls the buffers then drops the events it holds and presents the base + * controls the buffers then drops the events it holds, presents the base * buffer of each output showing another buffer, as a present of handle 0 - * naming no region. It also waits for the ends of those the device accepts, - * and their events reach no file either. It keeps control through both waits. - * An output where the base buffer's present does not latch, as on a dark - * output or a failed device, keeps showing the buffer it showed. + * naming no region, and hides each output's cursor. It also waits for the ends + * of those presents the device accepts, and their events reach no file either. + * It keeps control through both waits. An output where the base buffer's + * present does not latch, as on a dark output or a failed device, keeps + * showing the buffer it showed. An output keeps showing its cursor when the + * device refuses the hide or the kernel has found the device failed. + * + * An output may have a hardware cursor, a small picture the device draws over + * what the output shows, so moving it changes no buffer. `GFXFB_GET_OUTPUT` + * reports the largest cursor picture an output shows, 0 by 0 for an output + * without a hardware cursor. Two cursor calls drive it. `GFXFB_SET_CURSOR` + * gives an output's cursor a picture and its hotspot (the picture's pixel that + * sits at the cursor's position), or hides the cursor when the picture's width + * is 0, reading none of the picture's other fields. Each pixel holds alpha in + * bits 31 to 24 and red, green and blue as a buffer's pixel does, none + * multiplied by alpha. The kernel copies the picture, so the program may + * change its pixels once the call returns. `GFXFB_MOVE_CURSOR` places the + * hotspot of an output's cursor at a position in the output's pixels, even one + * off the output, so the picture may lie partly or wholly off it. A cursor + * call fails with EINVAL when the kernel cannot read its argument, when its + * reserved field is set and for an output of `output_count` or more. A + * `GFXFB_SET_CURSOR` showing a picture then fails with EINVAL when its hotspot + * lies outside the picture or the picture is wider or taller than the output's + * largest, with ENOMEM when no memory is left to copy the picture, and + * otherwise with EINVAL when the kernel cannot read its pixels. Past those + * checks and control, a cursor call fails with EIO once the kernel has found + * the device failed, and otherwise with the first that applies of ENOTTY on an + * output without a hardware cursor, EIO on a device that failed, EAGAIN on a + * dark output, EINVAL for a picture wider or taller than the output's largest, + * and EIO when the device refused the call. * * The kernel and userland are built from one tree, so a structure may gain * fields when its call gains a feature. Each structure holds fixed-width @@ -142,6 +168,8 @@ constexpr uint32_t GFXFB_GET_OUTPUT = 0x4702; constexpr uint32_t GFXFB_CREATE_BUFFER = 0x4703; constexpr uint32_t GFXFB_DESTROY_BUFFER = 0x4704; // the argument is the handle itself constexpr uint32_t GFXFB_PRESENT = 0x4705; +constexpr uint32_t GFXFB_SET_CURSOR = 0x4706; +constexpr uint32_t GFXFB_MOVE_CURSOR = 0x4707; constexpr uint8_t GFXFB_INFO_NEEDS_FLUSH = 0x01; // drawing reaches the screen only through GFXFB_FLUSH @@ -204,11 +232,13 @@ struct gfxfb_mode { // GFXFB_GET_OUTPUT argument, filled with one output's state struct gfxfb_output_info { - uint32_t output; // in: the output's index, EINVAL unless below `output_count` - uint32_t output_count; // out: how many outputs the device drives - uint32_t flags; // out: GFXFB_OUTPUT_* - uint32_t reserved; // out: zero - gfxfb_mode current; // out: the mode while lit, zero while dark + uint32_t output; // in: the output's index, EINVAL unless below `output_count` + uint32_t output_count; // out: how many outputs the device drives + uint32_t flags; // out: GFXFB_OUTPUT_* + uint32_t reserved; // out: zero + gfxfb_mode current; // out: the mode while lit, zero while dark + uint32_t cursor_max_width; // out: the widest cursor picture the output shows, 0 without a hardware cursor + uint32_t cursor_max_height; // out: the tallest, 0 without a hardware cursor }; // GFXFB_CREATE_BUFFER argument, filled with the new buffer's handle, layout and mmap offset @@ -233,6 +263,25 @@ struct gfxfb_present_args { uint32_t reserved; // must be zero }; +// GFXFB_SET_CURSOR argument, giving an output's hardware cursor a picture and its hotspot, or hiding the cursor +struct gfxfb_cursor_args { + uint32_t output; // the output's index, EINVAL unless below `output_count` + uint32_t width; // at most the output's `cursor_max_width`, or 0 to hide the cursor + uint32_t height; // at most the output's `cursor_max_height` + uint32_t hot_x; // the hotspot's column in the picture, below `width` + uint32_t hot_y; // the hotspot's row in the picture, below `height` + uint32_t reserved; // must be zero + uint64_t pixels; // user address of `width` times `height` pixels, row after row with no padding +}; + +// GFXFB_MOVE_CURSOR argument, placing the hotspot of an output's cursor +struct gfxfb_cursor_position { + uint32_t output; // the output's index, EINVAL unless below `output_count` + int32_t x; // the hotspot's column in the output's pixels, negative left of the output + int32_t y; // the hotspot's row, negative above the output + uint32_t reserved; // must be zero +}; + // One event as `read` returns it struct gfxfb_event { uint32_t type; // GFXFB_EVENT_PRESENT @@ -247,9 +296,11 @@ static_assert(sizeof(gfxfb_info) == 40, "gfxfb_info layout"); static_assert(sizeof(gfxfb_rect) == 16, "gfxfb_rect layout"); static_assert(sizeof(gfxfb_flush_args) == 16, "gfxfb_flush_args layout"); static_assert(sizeof(gfxfb_mode) == 16, "gfxfb_mode layout"); -static_assert(sizeof(gfxfb_output_info) == 32, "gfxfb_output_info layout"); +static_assert(sizeof(gfxfb_output_info) == 40, "gfxfb_output_info layout"); static_assert(sizeof(gfxfb_buffer_args) == 40, "gfxfb_buffer_args layout"); static_assert(sizeof(gfxfb_present_args) == 24, "gfxfb_present_args layout"); +static_assert(sizeof(gfxfb_cursor_args) == 32, "gfxfb_cursor_args layout"); +static_assert(sizeof(gfxfb_cursor_position) == 16, "gfxfb_cursor_position layout"); static_assert(sizeof(gfxfb_event) == 32, "gfxfb_event layout"); } // namespace gfxfb diff --git a/kernel/drivers/graphics/gfxfb_node.h b/kernel/drivers/graphics/gfxfb_node.h index 44e5751a..f985cd81 100644 --- a/kernel/drivers/graphics/gfxfb_node.h +++ b/kernel/drivers/graphics/gfxfb_node.h @@ -107,8 +107,8 @@ struct output_records { * output, whose base buffer is the boot framebuffer. The output's mode is the * framebuffer's size with an unknown refresh rate. The display hardware scans * that memory out directly, so `flush()` has nothing to copy and returns `OK`. - * It creates and presents no buffers, and keeps the boot framebuffer when - * gfxfb hands it back. + * It creates and presents no buffers, has no hardware cursor, and keeps the + * boot framebuffer when gfxfb hands it back. */ class boot_display : public display { public: @@ -122,6 +122,8 @@ class boot_display : public display { int32_t flush(uint32_t output, const gfxfb_rect* rects, uint32_t count) override; int32_t create_buffer(uint32_t, uint32_t, uint32_t, buffer**) override { return ERR_UNSUPPORTED; } int32_t present(uint32_t, buffer*, const gfxfb_rect*, uint32_t) override { return ERR_UNSUPPORTED; } + int32_t set_cursor(uint32_t, const cursor_image&) override { return ERR_UNSUPPORTED; } + int32_t move_cursor(uint32_t, int32_t, int32_t) override { return ERR_UNSUPPORTED; } void release_buffer(buffer*) override {} const boot_framebuffer& framebuffer() const { return m_framebuffer; } @@ -145,8 +147,8 @@ class boot_display : public display { * offset 0 is refused, while `GFXFB_GET_OUTPUT` still reports every output. * Each open file is a client, whose buffers mmap maps at the offsets their * creation returned, above the primary output's base buffer at offset 0. A - * client controls the buffers from its first `GFXFB_CREATE_BUFFER` or - * `GFXFB_PRESENT` that succeeds until it closes. Before control is checked, a + * client controls the buffers from its first creation, present or cursor + * request that succeeds until the client closes. Before control is checked, a * creation request fails with `fs::ERR_INVAL` when gfxfb cannot read it, when * its width or height is zero or when its reserved field is set, and a present * request fails with `fs::ERR_INVAL` when gfxfb cannot read it, when its @@ -193,6 +195,22 @@ class boot_display : public display { * present of its base buffer, a `GFXFB_FLUSH` past its request checks returns * `fs::OK` without reading a rect or calling the display, and marks the base * buffer stale, so that buffer's next present passes the display no region. + * + * A cursor request fails with `fs::ERR_INVAL` when gfxfb cannot read it, when + * its reserved field is set or when it names an output of the display's output + * count or more, and a `GFXFB_SET_CURSOR` showing a picture then fails with + * `fs::ERR_INVAL` when its hotspot lies outside the picture or the picture is + * wider or taller than the output's cursor maximum or CURSOR_DIMENSION_MAX, + * with `fs::ERR_NOMEM` when no memory is left to copy the picture, and + * otherwise with `fs::ERR_INVAL` when gfxfb cannot read its pixels. A cursor + * request past those checks reserves and takes control as a creation does, + * 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. */ class gfxfb_node : public fs::node { public: @@ -229,11 +247,11 @@ class gfxfb_node : public fs::node { * the client that presented, waking the client's readers, pollers and closing task. On GFXFB_PRESENT_LATCHED the * output shows the present's buffer and drops the one it replaced, on GFXFB_PRESENT_DARK it drops the present's * buffer, and on GFXFB_PRESENT_FAILED it keeps both, since the failed hardware may still read either, and counts - * the display as failed, so every later `GFXFB_FLUSH` past its request checks and every later present request - * reaching an output fails with `fs::ERR_IO`. The display must report each present its `present()` accepted as - * `display::present()` requires, through `gfxfb::present_done()`, which calls this. The display must report - * GFXFB_PRESENT_LATCHED only once the hardware no longer reads the replaced buffer and GFXFB_PRESENT_DARK only - * once the hardware can no longer latch the present. The call never sleeps or calls the display. + * the display as failed, so every later `GFXFB_FLUSH` past its request checks, present request reaching an output + * and cursor request past control fails with `fs::ERR_IO`. The display must report each present its `present()` + * accepted as `display::present()` requires, through `gfxfb::present_done()`, which calls this. The display must + * report GFXFB_PRESENT_LATCHED only once the hardware no longer reads the replaced buffer and GFXFB_PRESENT_DARK + * only once the hardware can no longer latch the present. The call never sleeps or calls the display. * @param output Index of the output the present was for. The call drops the end, logging an error, when the * output holds no unended present, as once that present has ended or for an index of the display's output count * or more. @@ -279,6 +297,9 @@ class gfxfb_node : public fs::node { const gfxfb_rect* regions, uint32_t region_count ); + int32_t set_cursor(client& owner, uint64_t args_addr); + int32_t give_cursor_image(uint32_t output, const cursor_image& image); + int32_t move_cursor(client& owner, uint64_t position_addr); bool has_display_failed(); void mark_failed_on_device_error(int32_t rc); bool holds_unended_present(const client& owner) const; @@ -302,6 +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::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 6283f3c1..7b68d0b9 100644 --- a/kernel/drivers/graphics/virtio_gpu.cpp +++ b/kernel/drivers/graphics/virtio_gpu.cpp @@ -716,6 +716,10 @@ 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; + if (out.lit) { out.current = { .width = m_base_buffer.width, diff --git a/kernel/drivers/graphics/virtio_gpu.h b/kernel/drivers/graphics/virtio_gpu.h index 6f9362ed..108df595 100644 --- a/kernel/drivers/graphics/virtio_gpu.h +++ b/kernel/drivers/graphics/virtio_gpu.h @@ -198,6 +198,11 @@ class virtio_gpu_driver : public virtio_pci_driver, public gfxfb::display { const gfxfb::gfxfb_rect* regions, 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; } + void release_buffer(gfxfb::buffer* buf) override; /** @note Privilege: **required** */ diff --git a/kernel/tests/graphics/gfxfb.test.cpp b/kernel/tests/graphics/gfxfb.test.cpp index 8d242574..532104dc 100644 --- a/kernel/tests/graphics/gfxfb.test.cpp +++ b/kernel/tests/graphics/gfxfb.test.cpp @@ -85,8 +85,8 @@ static constexpr size_t MANY_REGIONS_PAGES = // refusal test static constexpr uint32_t WIDE_REGION_COUNT = 0x10001; -// What `get_output` fills the GFXFB_GET_OUTPUT reply's 32-bit fields and its refresh rate with first, so a field left -// unwritten fails its check +// What `get_output` fills the GFXFB_GET_OUTPUT reply's 32-bit fields and its refresh rate with first, as a test does an +// output state's cursor maximum before `read_output()`, so a field left unwritten fails its check static constexpr uint32_t UNWRITTEN_FIELD = 0xFFFFFFFF; static constexpr uint64_t UNWRITTEN_RATE = 0xFFFFFFFFFFFFFFFF; @@ -115,13 +115,45 @@ static constexpr uint32_t WIDE_FORMAT = 0x10000 + gfxfb::GFXFB_FORMAT_XRGB8888; static constexpr size_t NODE_NAME_BYTES = 24; static constexpr size_t NODE_PATH_BYTES = 48; -// A display with two outputs, dark until a test lights them, that records the first MAX_RECORDED rects of its flushes -// and regions of its last accepted present, creates write-combining buffers and counts buffers handed back +// Where the cursor tests place their move request and their pictures' pixels in the user page, apart from a +// GFXFB_SET_CURSOR request at ARGS_OFFSET +static constexpr size_t MOVE_OFFSET = 32; +static constexpr size_t PIXELS_OFFSET = 1024; + +// The cursor maximum the cursor tests' outputs report, unlike in its two dimensions, so a check reading the wrong one +// fails +static constexpr uint32_t CURSOR_MAX_WIDTH = 200; +static constexpr uint32_t CURSOR_MAX_HEIGHT = 40; + +// A cursor maximum above both CURSOR_DIMENSION_MAX and 0xFFFF, which no display keeping the contract reports, so a +// reply or a picture check that trusts it, or narrows it to 16 bits, fails +static constexpr uint32_t WIDE_CURSOR_MAX_WIDTH = 0x10000 + 16; +static constexpr uint32_t WIDE_CURSOR_MAX_HEIGHT = 0x10000 + 8; + +// A cursor picture's width or height whose low 16 bits alone make 1, so a size check narrowed to 16 bits passes it +static constexpr uint32_t WIDE_CURSOR_DIMENSION = 0x10001; + +// The pages `map_wide_cursor_pixels` maps, holding WIDE_CURSOR_DIMENSION pixels, so their copy cannot refuse a picture +// that wide or tall in the size check's place +static constexpr size_t WIDE_CURSOR_PAGES = + (WIDE_CURSOR_DIMENSION * sizeof(uint32_t) + pmm::PAGE_SIZE - 1) / pmm::PAGE_SIZE; + +// A hotspot column or row whose low 16 bits are 0, so a check narrowed to 16 bits finds it inside any picture +static constexpr uint32_t WIDE_HOTSPOT = 0x10000; + +// A cursor position off any output, one coordinate negative and one above 0xFFFF, so a coordinate narrowed or clamped +// on its way to the display fails +static constexpr int32_t OFF_OUTPUT_X = -70000; +static constexpr int32_t OFF_OUTPUT_Y = 70000; + +// A display with two outputs, dark until a test lights them, that records its cursor calls, the first MAX_RECORDED +// rects of its flushes and regions of its last accepted present, creates write-combining buffers and counts releases class recording_display : public gfxfb::display { public: static constexpr uint32_t OUTPUTS = 2; static constexpr uint32_t MAX_RECORDED = 64; static constexpr uint32_t MAX_CREATED = gfxfb::GFXFB_CLIENT_BUFFERS_MAX + 2; // a full file, then two refills + static constexpr uint32_t MAX_CURSOR_PIXELS = 8; gfxfb::output_state states[OUTPUTS] = {}; gfxfb::buffer bases[OUTPUTS] = {}; @@ -155,6 +187,17 @@ class recording_display : public gfxfb::display { gfxfb::gfxfb_rect presented_regions[MAX_RECORDED] = {}; // its first MAX_RECORDED regions gfxfb::gfxfb_rect last_presented_region = {}; // its last region, zeros for none + int32_t cursor_answer = gfxfb::OK; // what `set_cursor()` and `move_cursor()` return + void (*while_setting_cursor)() = nullptr; // run once by the next `set_cursor()`, before it returns + uint32_t set_cursor_calls = 0; + uint32_t move_cursor_calls = 0; + uint32_t cursor_output = OUTPUTS; // the output of the last cursor call, OUTPUTS before any + gfxfb::cursor_image cursor = {}; // the last picture `set_cursor()` got + uint32_t cursor_pixels[MAX_CURSOR_PIXELS] = {}; // that picture's first pixels, read during the call + bool hidden_on[OUTPUTS] = {}; // whether the last `set_cursor()` on each output hid the cursor + int32_t cursor_x = 0; // the last move's position + int32_t cursor_y = 0; + uint32_t output_count() const override { return count; } bool needs_flush() const override { return flush_needed; } @@ -187,6 +230,16 @@ class recording_display : public gfxfb::display { const gfxfb::gfxfb_rect* regions, uint32_t region_count ) override; + 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 { + move_cursor_calls++; + cursor_output = output; + cursor_x = x; + cursor_y = y; + + return cursor_answer; + } void release_buffer(gfxfb::buffer* buf) override { last_released = buf; @@ -223,6 +276,16 @@ struct task_create { sync::atomic done; }; +// A cursor move to the origin run on a task of its own: the file and page it moves through, the output whose cursor it +// moves, its result and the flag set once it returns +struct task_move { + fs::file* file; + user_page* page; + uint32_t output; + int32_t result; + sync::atomic done; +}; + // A base buffer's memory is never freed, so every test's buffers use this one allocation static pmm::phys_addr_t g_buffer_phys; @@ -335,6 +398,30 @@ int32_t recording_display::present( return gfxfb::OK; } +int32_t recording_display::set_cursor(uint32_t output, const gfxfb::cursor_image& image) { + set_cursor_calls++; + cursor_output = output; + cursor = image; + + // A close hiding past the output count fails the tests' call counts instead of writing past `hidden_on` + if (output < OUTPUTS) { + hidden_on[output] = image.width == 0; + } + + uint32_t readable_count = image.pixels ? image.width * image.height : 0; + for (uint32_t i = 0; i < readable_count && i < MAX_CURSOR_PIXELS; i++) { + cursor_pixels[i] = image.pixels[i]; + } + + if (while_setting_cursor) { + void (*hook)() = while_setting_cursor; + while_setting_cursor = nullptr; + hook(); + } + + return cursor_answer; +} + // Lights `output` of `fake`, the boot output when `is_boot_output` holds. Output 0's base buffer is cacheable at the // start of the shared memory, and output 1's is write-combining a page in, so a test can tell which one is mapped static void light_output(recording_display& fake, uint32_t output, bool is_boot_output) { @@ -350,13 +437,15 @@ static void light_output(recording_display& fake, uint32_t output, bool is_boot_ }; fake.states[output] = { - .lit = true, - .boot_output = is_boot_output, - .current = { + .lit = true, + .boot_output = is_boot_output, + .current = { .width = fake.bases[output].width, .height = fake.bases[output].height, .refresh_millihertz = is_first ? OUTPUT_0_REFRESH_MILLIHERTZ : OUTPUT_1_REFRESH_MILLIHERTZ, }, + .cursor_max_width = 0, + .cursor_max_height = 0, }; } @@ -796,11 +885,109 @@ static int32_t get_output(const char* path, user_page& page, uint32_t output) { .height = UNWRITTEN_FIELD, .refresh_millihertz = UNWRITTEN_RATE, }, + .cursor_max_width = UNWRITTEN_FIELD, + .cursor_max_height = UNWRITTEN_FIELD, }; return call_gfxfb_ioctl(path, page, gfxfb::GFXFB_GET_OUTPUT, page.addr); } +// Sends `args` through GFXFB_SET_CURSOR on `file`, writing them at the page's start +static int32_t send_cursor_args(fs::file* file, user_page& page, const gfxfb::gfxfb_cursor_args& args) { + *page.at(ARGS_OFFSET) = args; + + return file_ioctl(file, page, gfxfb::GFXFB_SET_CURSOR, page.addr + ARGS_OFFSET); +} + +// A GFXFB_SET_CURSOR request on `output` for a `width` by `height` picture whose pixels sit at PIXELS_OFFSET, with its +// hotspot on its last pixel, the largest `hot_x` and `hot_y` inside it +static gfxfb::gfxfb_cursor_args cursor_picture( + const user_page& page, + uint32_t output, + uint32_t width, + uint32_t height +) { + return { + .output = output, + .width = width, + .height = height, + .hot_x = width - 1, + .hot_y = height - 1, + .reserved = 0, + .pixels = page.addr + PIXELS_OFFSET, + }; +} + +// Hides `output`'s cursor through GFXFB_SET_CURSOR on `file` +static int32_t hide_cursor(fs::file* file, user_page& page, uint32_t output) { + gfxfb::gfxfb_cursor_args hide = { + .output = output, + .width = 0, + .height = 0, + .hot_x = 0, + .hot_y = 0, + .reserved = 0, + .pixels = 0, + }; + + return send_cursor_args(file, page, hide); +} + +// Pixel i of the pictures `write_cursor_pixels` writes, unlike every other and with alpha set +static uint32_t cursor_pixel(uint32_t i) { + return 0x80000000 + i * 0x00010203; +} + +// Writes `count` pixels at PIXELS_OFFSET, pixel i being `cursor_pixel(i)` +static void write_cursor_pixels(user_page& page, uint32_t count) { + for (uint32_t i = 0; i < count; i++) { + *page.at(PIXELS_OFFSET + i * sizeof(uint32_t)) = cursor_pixel(i); + } +} + +// Maps WIDE_CURSOR_PAGES readable pages into the page's address space and returns their user address, or 0 when the +// mapping fails +static uintptr_t map_wide_cursor_pixels(user_page& page) { + uintptr_t addr = 0; + uint32_t flags = mm::MM_MAP_PRIVATE | mm::MM_MAP_ANONYMOUS; + size_t length = WIDE_CURSOR_PAGES * pmm::PAGE_SIZE; + int32_t map_rc = mm::MM_CTX_OK; + RUN_ELEVATED(map_rc = mm::mm_context_map_anonymous(page.ctx, 0, length, mm::MM_PROT_READ, flags, &addr)); + + return map_rc == mm::MM_CTX_OK ? addr : 0; +} + +// Moves `output`'s cursor through GFXFB_MOVE_CURSOR on `file` so its hotspot sits at (`x`, `y`) +static int32_t move_cursor_to(fs::file* file, user_page& page, uint32_t output, int32_t x, int32_t y) { + *page.at(MOVE_OFFSET) = { + .output = output, + .x = x, + .y = y, + .reserved = 0, + }; + + return file_ioctl(file, page, gfxfb::GFXFB_MOVE_CURSOR, page.addr + MOVE_OFFSET); +} + +// Makes the cursor move of `arg`, a `task_move`, then records the result and sets its flag +static void move_on_a_task(void* arg) { + task_move* request = static_cast(arg); + request->result = move_cursor_to(request->file, *request->page, request->output, 0, 0); + request->done.store_release(1); + sched::exit(0); +} + +// Starts moving `output`'s cursor through `file` on a task of its own, which sets `request.done` once the move returns, +// or returns nullptr when the task cannot start +static sched::task* start_move(task_move& request, fs::file* file, user_page& page, uint32_t output) { + request.file = file; + request.page = &page; + request.output = output; + request.done.store_relaxed(0); + + return test_helpers::start_pinned_task(move_on_a_task, &request, "gfxfb_move"); +} + // True when `rect` holds exactly the given position and size static bool rect_equals(const gfxfb::gfxfb_rect& rect, uint32_t x, uint32_t y, uint32_t width, uint32_t height) { @@ -873,16 +1060,21 @@ TEST(gfxfb, get_info_reports_the_boot_display) { EXPECT_EQ(stat_size(test), BOOT_SIZE); } -// The boot display drives one output, lit and the boot output, whose base buffer is the boot framebuffer +// The boot display drives one output with no hardware cursor, lit and the boot output, whose base buffer is the boot +// framebuffer TEST(gfxfb, boot_display_has_one_lit_boot_output) { gfxfb::boot_display boot(test_boot_framebuffer()); gfxfb::output_state state = {}; + state.cursor_max_width = UNWRITTEN_FIELD; + state.cursor_max_height = UNWRITTEN_FIELD; EXPECT_EQ(boot.output_count(), static_cast(1)); EXPECT_FALSE(boot.needs_flush()); ASSERT_EQ(boot.read_output(0, state), gfxfb::OK); EXPECT_TRUE(state.lit); EXPECT_TRUE(state.boot_output); + EXPECT_EQ(state.cursor_max_width, static_cast(0)); + EXPECT_EQ(state.cursor_max_height, static_cast(0)); EXPECT_EQ(boot.read_output(1, state), gfxfb::ERR_INVALID_ARG); gfxfb::buffer* base = boot.base_buffer(0); @@ -900,7 +1092,11 @@ TEST(gfxfb, get_output_reports_each_output) { light_output(g_outputs_display, 0, false); g_outputs_display.states[0].current.width = WIDE_MODE_WIDTH; g_outputs_display.states[0].current.height = WIDE_MODE_HEIGHT; + g_outputs_display.states[0].cursor_max_width = WIDE_CURSOR_MAX_WIDTH; + g_outputs_display.states[0].cursor_max_height = WIDE_CURSOR_MAX_HEIGHT; g_outputs_display.states[1].current = {.width = 1, .height = 1, .refresh_millihertz = 1}; // stale while dark + g_outputs_display.states[1].cursor_max_width = CURSOR_MAX_WIDTH; + g_outputs_display.states[1].cursor_max_height = CURSOR_MAX_HEIGHT; gfxfb_test_node test = publish_driver_node(&g_outputs_display); ASSERT_NOT_NULL(test.node); @@ -917,6 +1113,8 @@ TEST(gfxfb, get_output_reports_each_output) { EXPECT_EQ(info->current.width, WIDE_MODE_WIDTH); EXPECT_EQ(info->current.height, WIDE_MODE_HEIGHT); EXPECT_EQ(info->current.refresh_millihertz, OUTPUT_0_REFRESH_MILLIHERTZ); + EXPECT_EQ(info->cursor_max_width, gfxfb::CURSOR_DIMENSION_MAX); + EXPECT_EQ(info->cursor_max_height, gfxfb::CURSOR_DIMENSION_MAX); ASSERT_EQ(get_output(test.path, page, 1), fs::OK); EXPECT_EQ(info->output, static_cast(1)); @@ -926,6 +1124,8 @@ TEST(gfxfb, get_output_reports_each_output) { EXPECT_EQ(info->current.width, static_cast(0)); EXPECT_EQ(info->current.height, static_cast(0)); EXPECT_EQ(info->current.refresh_millihertz, static_cast(0)); + EXPECT_EQ(info->cursor_max_width, CURSOR_MAX_WIDTH); + EXPECT_EQ(info->cursor_max_height, CURSOR_MAX_HEIGHT); EXPECT_EQ(get_output(test.path, page, recording_display::OUTPUTS), fs::ERR_INVAL); EXPECT_EQ(get_output(test.path, page, WIDE_OUTPUT_INDEX), fs::ERR_INVAL); @@ -947,7 +1147,8 @@ TEST(gfxfb, get_output_reports_each_output) { EXPECT_EQ(crossing_request->reserved, UNWRITTEN_FIELD); } -// The boot display's one output is primary and lit at the boot framebuffer's size, with an unknown refresh rate +// The boot display's one output is primary and lit at the boot framebuffer's size, with an unknown refresh rate and no +// hardware cursor TEST(gfxfb, get_output_reports_the_boot_display) { gfxfb_test_node test = publish_boot_node(); ASSERT_NOT_NULL(test.node); @@ -963,6 +1164,8 @@ TEST(gfxfb, get_output_reports_the_boot_display) { EXPECT_EQ(info->current.width, BOOT_WIDTH); EXPECT_EQ(info->current.height, BOOT_HEIGHT); EXPECT_EQ(info->current.refresh_millihertz, static_cast(0)); + EXPECT_EQ(info->cursor_max_width, static_cast(0)); + EXPECT_EQ(info->cursor_max_height, static_cast(0)); EXPECT_EQ(get_output(test.path, page, 1), fs::ERR_INVAL); } @@ -3855,6 +4058,496 @@ TEST(gfxfb, present_checks_its_regions_before_control) { ASSERT_TRUE(closes_before_deadline(refused)); } +static recording_display g_cursor_copy_display; + +// SET_CURSOR hands the display a copy of the picture with its size and hotspot, takes the largest picture in each +// dimension, and for a width of 0 hides the cursor, reading none of the picture's other fields +TEST(gfxfb, set_cursor_hands_the_display_a_copy_of_the_picture) { + light_output(g_cursor_copy_display, 0, false); + g_cursor_copy_display.states[1].cursor_max_width = CURSOR_MAX_WIDTH; + g_cursor_copy_display.states[1].cursor_max_height = CURSOR_MAX_HEIGHT; + gfxfb_test_node test = publish_driver_node(&g_cursor_copy_display); + ASSERT_NOT_NULL(test.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* file = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(file); + + constexpr uint32_t width = 3; + constexpr uint32_t height = 2; + const gfxfb::cursor_image& given = g_cursor_copy_display.cursor; + write_cursor_pixels(page, width * height); + ASSERT_EQ(send_cursor_args(file, page, cursor_picture(page, 1, width, height)), fs::OK); + EXPECT_EQ(g_cursor_copy_display.cursor_output, static_cast(1)); + EXPECT_EQ(given.width, width); + EXPECT_EQ(given.height, height); + EXPECT_EQ(given.hot_x, width - 1); + EXPECT_EQ(given.hot_y, height - 1); + EXPECT_NE(reinterpret_cast(given.pixels), page.addr + PIXELS_OFFSET); + for (uint32_t i = 0; i < width * height; i++) { + EXPECT_EQ(g_cursor_copy_display.cursor_pixels[i], cursor_pixel(i)); + } + + ASSERT_EQ(send_cursor_args(file, page, cursor_picture(page, 1, CURSOR_MAX_WIDTH, 1)), fs::OK); + EXPECT_EQ(given.width, CURSOR_MAX_WIDTH); + ASSERT_EQ(send_cursor_args(file, page, cursor_picture(page, 1, 1, CURSOR_MAX_HEIGHT)), fs::OK); + EXPECT_EQ(given.height, CURSOR_MAX_HEIGHT); + + // The hide's other fields hold what no picture may, and its pixels' address cannot be read + gfxfb::gfxfb_cursor_args hide = { + .output = 1, + .width = 0, + .height = CURSOR_MAX_HEIGHT + 1, + .hot_x = 1, + .hot_y = CURSOR_MAX_HEIGHT + 1, + .reserved = 0, + .pixels = page.addr + pmm::PAGE_SIZE, + }; + + ASSERT_EQ(send_cursor_args(file, page, hide), fs::OK); + EXPECT_TRUE(g_cursor_copy_display.hidden_on[1]); + EXPECT_EQ(given.height, static_cast(0)); + EXPECT_EQ(given.hot_x, static_cast(0)); + EXPECT_EQ(given.hot_y, static_cast(0)); + EXPECT_NULL(given.pixels); + EXPECT_EQ(g_cursor_copy_display.set_cursor_calls, static_cast(4)); + + ASSERT_TRUE(closes_before_deadline(file)); +} + +static recording_display g_cursor_refusal_display; + +// Cursor requests gfxfb refuses fail with ERR_INVAL before control, so a file another file's control keeps out sees +// them, while the largest pictures, held to CURSOR_DIMENSION_MAX past a wider maximum, and a hide reach control +TEST(gfxfb, cursor_calls_refuse_requests_before_control) { + light_output(g_cursor_refusal_display, 0, false); + g_cursor_refusal_display.states[0].cursor_max_width = CURSOR_MAX_WIDTH; + g_cursor_refusal_display.states[0].cursor_max_height = CURSOR_MAX_HEIGHT; + g_cursor_refusal_display.states[1].cursor_max_width = WIDE_CURSOR_MAX_WIDTH; + g_cursor_refusal_display.states[1].cursor_max_height = WIDE_CURSOR_MAX_HEIGHT; + gfxfb_test_node test = publish_driver_node(&g_cursor_refusal_display); + ASSERT_NOT_NULL(test.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* controller = fs::open(test.path, fs::O_RDWR); + fs::file* file = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(controller); + ASSERT_NOT_NULL(file); + ASSERT_NE(request_small_buffer(controller, page).handle, static_cast(0)); + + // A 2 by 2 picture with its hotspot one past either edge or at WIDE_HOTSPOT, or its second row unmapped + gfxfb::gfxfb_cursor_args wide_hotspot = cursor_picture(page, 0, 2, 2); + wide_hotspot.hot_x = 2; + gfxfb::gfxfb_cursor_args tall_hotspot = cursor_picture(page, 0, 2, 2); + tall_hotspot.hot_y = 2; + gfxfb::gfxfb_cursor_args far_hotspot_x = cursor_picture(page, 0, 2, 2); + far_hotspot_x.hot_x = WIDE_HOTSPOT; + gfxfb::gfxfb_cursor_args far_hotspot_y = cursor_picture(page, 0, 2, 2); + far_hotspot_y.hot_y = WIDE_HOTSPOT; + gfxfb::gfxfb_cursor_args unreadable = cursor_picture(page, 0, 2, 2); + unreadable.pixels = page.addr + pmm::PAGE_SIZE - 2 * sizeof(uint32_t); + + // Pictures WIDE_CURSOR_DIMENSION wide or tall on output 1, with every pixel readable + uintptr_t wide_pixels = map_wide_cursor_pixels(page); + ASSERT_NE(wide_pixels, static_cast(0)); + gfxfb::gfxfb_cursor_args wide_picture = cursor_picture(page, 1, WIDE_CURSOR_DIMENSION, 1); + wide_picture.pixels = wide_pixels; + gfxfb::gfxfb_cursor_args tall_picture = cursor_picture(page, 1, 1, WIDE_CURSOR_DIMENSION); + tall_picture.pixels = wide_pixels; + + uint32_t bound = gfxfb::CURSOR_DIMENSION_MAX; + EXPECT_EQ(file_ioctl(file, page, gfxfb::GFXFB_SET_CURSOR, 0), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, recording_display::OUTPUTS, 1, 1)), fs::ERR_INVAL); + EXPECT_EQ(hide_cursor(file, page, recording_display::OUTPUTS), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, WIDE_OUTPUT_INDEX, 1, 1)), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, wide_hotspot), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, tall_hotspot), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, far_hotspot_x), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, far_hotspot_y), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 0, CURSOR_MAX_WIDTH + 1, 1)), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 0, 1, CURSOR_MAX_HEIGHT + 1)), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 1, bound + 1, 1)), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 1, 1, bound + 1)), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, wide_picture), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, tall_picture), fs::ERR_INVAL); + EXPECT_EQ(send_cursor_args(file, page, unreadable), fs::ERR_INVAL); + + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 0, CURSOR_MAX_WIDTH, 1)), fs::ERR_BUSY); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 0, 1, CURSOR_MAX_HEIGHT)), fs::ERR_BUSY); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 1, bound, 1)), fs::ERR_BUSY); + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 1, 1, bound)), fs::ERR_BUSY); + EXPECT_EQ(hide_cursor(file, page, 0), fs::ERR_BUSY); + EXPECT_EQ(g_cursor_refusal_display.set_cursor_calls, static_cast(0)); + + // A reserved field with only its low or only its high bit set, in a picture and in a move + auto* position = page.at(MOVE_OFFSET); + const uint32_t reserved_values[] = {0x1, 0x80000000}; + for (uint32_t reserved : reserved_values) { + gfxfb::gfxfb_cursor_args picture = cursor_picture(page, 0, 2, 2); + picture.reserved = reserved; + EXPECT_EQ(send_cursor_args(file, page, picture), fs::ERR_INVAL); + + *position = {.output = 0, .x = 0, .y = 0, .reserved = reserved}; + EXPECT_EQ(file_ioctl(file, page, gfxfb::GFXFB_MOVE_CURSOR, page.addr + MOVE_OFFSET), fs::ERR_INVAL); + } + + EXPECT_EQ(file_ioctl(file, page, gfxfb::GFXFB_MOVE_CURSOR, 0), fs::ERR_INVAL); + EXPECT_EQ(move_cursor_to(file, page, recording_display::OUTPUTS, 0, 0), fs::ERR_INVAL); + EXPECT_EQ(move_cursor_to(file, page, WIDE_OUTPUT_INDEX, 0, 0), fs::ERR_INVAL); + EXPECT_EQ(move_cursor_to(file, page, 0, 0, 0), fs::ERR_BUSY); + EXPECT_EQ(g_cursor_refusal_display.move_cursor_calls, static_cast(0)); + + ASSERT_TRUE(closes_before_deadline(file)); + ASSERT_TRUE(closes_before_deadline(controller)); +} + +static recording_display g_cursor_move_display; + +// MOVE_CURSOR hands the display its output and a position off the output, even before any picture +TEST(gfxfb, move_cursor_hands_the_display_the_position) { + light_output(g_cursor_move_display, 0, false); + gfxfb_test_node test = publish_driver_node(&g_cursor_move_display); + ASSERT_NOT_NULL(test.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* file = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(file); + + ASSERT_EQ(move_cursor_to(file, page, 1, OFF_OUTPUT_X, OFF_OUTPUT_Y), fs::OK); + EXPECT_EQ(g_cursor_move_display.cursor_output, static_cast(1)); + EXPECT_EQ(g_cursor_move_display.cursor_x, OFF_OUTPUT_X); + EXPECT_EQ(g_cursor_move_display.cursor_y, OFF_OUTPUT_Y); + EXPECT_EQ(g_cursor_move_display.move_cursor_calls, static_cast(1)); + EXPECT_EQ(g_cursor_move_display.set_cursor_calls, static_cast(0)); + + ASSERT_TRUE(closes_before_deadline(file)); +} + +static recording_display g_cursor_answer_display; + +// Each refusal of a cursor call by the display returns as its file error and leaves the display usable, so the next +// call reaches it +TEST(gfxfb, cursor_calls_return_the_display_refusals_as_file_errors) { + light_output(g_cursor_answer_display, 0, false); + g_cursor_answer_display.states[0].cursor_max_width = CURSOR_MAX_WIDTH; + g_cursor_answer_display.states[0].cursor_max_height = CURSOR_MAX_HEIGHT; + gfxfb_test_node test = publish_driver_node(&g_cursor_answer_display); + ASSERT_NOT_NULL(test.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* file = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(file); + + g_cursor_answer_display.cursor_answer = gfxfb::ERR_DARK; + EXPECT_EQ(hide_cursor(file, page, 0), fs::ERR_AGAIN); + EXPECT_EQ(move_cursor_to(file, page, 0, 0, 0), fs::ERR_AGAIN); + g_cursor_answer_display.cursor_answer = gfxfb::ERR_UNSUPPORTED; + EXPECT_EQ(hide_cursor(file, page, 0), fs::ERR_NOSYS); + EXPECT_EQ(move_cursor_to(file, page, 0, 0, 0), fs::ERR_NOSYS); + g_cursor_answer_display.cursor_answer = gfxfb::ERR_INVALID_ARG; + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 0, 1, 1)), fs::ERR_INVAL); + g_cursor_answer_display.cursor_answer = gfxfb::ERR_REFUSED; + EXPECT_EQ(hide_cursor(file, page, 0), fs::ERR_IO); + EXPECT_EQ(move_cursor_to(file, page, 0, 0, 0), fs::ERR_IO); + + g_cursor_answer_display.cursor_answer = gfxfb::OK; + EXPECT_EQ(hide_cursor(file, page, 0), fs::OK); + EXPECT_EQ(move_cursor_to(file, page, 0, 0, 0), fs::OK); + EXPECT_EQ(g_cursor_answer_display.set_cursor_calls, static_cast(5)); + EXPECT_EQ(g_cursor_answer_display.move_cursor_calls, static_cast(4)); + + ASSERT_TRUE(closes_before_deadline(file)); +} + +static recording_display g_set_cursor_failure_display; +static recording_display g_move_cursor_failure_display; + +// Either cursor call's ERR_DEVICE counts the display as failed, so every later flush, and every present and cursor call +// that control admits, fails with ERR_IO without handing it to the display, and the controller's close hides no cursor +TEST(gfxfb, a_cursor_call_failing_the_display_fails_every_later_flush_present_and_cursor_call) { + recording_display* fakes[] = {&g_set_cursor_failure_display, &g_move_cursor_failure_display}; + for (recording_display* fake : fakes) { + light_output(*fake, 0, false); + gfxfb_test_node test = publish_driver_node(fake); + ASSERT_NOT_NULL(test.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* file = fs::open(test.path, fs::O_RDWR); + fs::file* rival = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(file); + ASSERT_NOT_NULL(rival); + ASSERT_EQ(move_cursor_to(file, page, 0, 0, 0), fs::OK); + + bool fails_by_moving = fake == &g_move_cursor_failure_display; + fake->cursor_answer = gfxfb::ERR_DEVICE; + int32_t failing_rc = fails_by_moving ? move_cursor_to(file, page, 0, 0, 0) : hide_cursor(file, page, 0); + EXPECT_EQ(failing_rc, fs::ERR_IO); + fake->cursor_answer = gfxfb::OK; + + EXPECT_EQ(hide_cursor(file, page, 0), fs::ERR_IO); + EXPECT_EQ(move_cursor_to(file, page, 0, 0, 0), fs::ERR_IO); + EXPECT_EQ(present_buffer(file, page, 0, 0), fs::ERR_IO); + EXPECT_EQ(flush_one_rect(test.path, page), fs::ERR_IO); + EXPECT_EQ(fake->set_cursor_calls + fake->move_cursor_calls, static_cast(2)); + EXPECT_EQ(fake->present_calls, static_cast(0)); + EXPECT_EQ(fake->recorded_count, static_cast(0)); + + // Control is checked before the display's failure, so the rival's calls fail with ERR_BUSY while `file` + // controls. Once it closes they fail with ERR_IO and leave no reservation against a file opened afterwards + EXPECT_EQ(hide_cursor(rival, page, 0), fs::ERR_BUSY); + EXPECT_EQ(move_cursor_to(rival, page, 0, 0, 0), fs::ERR_BUSY); + ASSERT_TRUE(closes_before_deadline(file)); + EXPECT_EQ(hide_cursor(rival, page, 0), fs::ERR_IO); + EXPECT_EQ(move_cursor_to(rival, page, 0, 0, 0), fs::ERR_IO); + fs::file* late = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(late); + EXPECT_EQ(move_cursor_to(late, page, 0, 0, 0), fs::ERR_IO); + EXPECT_EQ(fake->set_cursor_calls + fake->move_cursor_calls, static_cast(2)); + + ASSERT_TRUE(closes_before_deadline(late)); + ASSERT_TRUE(closes_before_deadline(rival)); + } +} + +static recording_display g_cursor_control_display; + +// The rival file the hook moves through, the page holding its request, the move its task makes, and what the hook saw +static fs::file* g_cursor_rival; +static user_page* g_cursor_page; +static task_move g_rival_move; +static sched::task* g_rival_mover; +static bool g_rival_moved_in_time; + +// Runs inside a `set_cursor()` display call: moves through the rival file on a task of its own and records whether the +// move returns before the deadline +static void move_through_the_rival() { + g_rival_mover = start_move(g_rival_move, g_cursor_rival, *g_cursor_page, 0); + g_rival_moved_in_time = g_rival_mover && spin_wait(g_rival_move.done); +} + +// A cursor call reserves control while in the display and takes it if it succeeds, as a creation does, so meanwhile and +// afterwards another file's calls fail with ERR_BUSY, while a call the display refuses takes none +TEST(gfxfb, a_cursor_call_takes_control_as_a_creation_does) { + light_output(g_cursor_control_display, 0, false); + gfxfb_test_node test = publish_driver_node(&g_cursor_control_display); + ASSERT_NOT_NULL(test.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* setter = fs::open(test.path, fs::O_RDWR); + fs::file* mover = fs::open(test.path, fs::O_RDWR); + fs::file* creator = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(setter); + ASSERT_NOT_NULL(mover); + ASSERT_NOT_NULL(creator); + + // The setter's hide reserves control against the mover while in the display, and the display's refusals of that + // hide and the mover's move leave no file in control, so the creator's creation succeeds + g_cursor_rival = mover; + g_cursor_page = &page; + g_rival_mover = nullptr; + g_rival_moved_in_time = false; + g_cursor_control_display.while_setting_cursor = move_through_the_rival; + g_cursor_control_display.cursor_answer = gfxfb::ERR_DARK; + EXPECT_EQ(hide_cursor(setter, page, 0), fs::ERR_AGAIN); + ASSERT_NOT_NULL(g_rival_mover); + EXPECT_TRUE(g_rival_moved_in_time); + ASSERT_TRUE(spin_wait(g_rival_move.done)); + EXPECT_EQ(g_rival_move.result, fs::ERR_BUSY); + unpin(g_rival_mover); + EXPECT_EQ(move_cursor_to(mover, page, 0, 0, 0), fs::ERR_AGAIN); + g_cursor_control_display.cursor_answer = gfxfb::OK; + ASSERT_NE(request_small_buffer(creator, page).handle, static_cast(0)); + ASSERT_TRUE(closes_before_deadline(creator)); + + // The setter's hide takes control, which its refused move keeps, so the mover's calls never reach the display + uint32_t format = gfxfb::GFXFB_FORMAT_XRGB8888; + ASSERT_EQ(hide_cursor(setter, page, 0), fs::OK); + g_cursor_control_display.cursor_answer = gfxfb::ERR_REFUSED; + EXPECT_EQ(move_cursor_to(setter, page, 0, 0, 0), fs::ERR_IO); + g_cursor_control_display.cursor_answer = gfxfb::OK; + EXPECT_EQ(move_cursor_to(mover, page, 0, 0, 0), fs::ERR_BUSY); + EXPECT_EQ(hide_cursor(mover, page, 0), fs::ERR_BUSY); + EXPECT_EQ(present_buffer(mover, page, 0, 0), fs::ERR_BUSY); + EXPECT_EQ(request_buffer(mover, page, SMALL_BUFFER_WIDTH, SMALL_BUFFER_HEIGHT, format), fs::ERR_BUSY); + EXPECT_EQ(g_cursor_control_display.move_cursor_calls, static_cast(2)); + + // Once the setter closes, the mover's move takes control, so a creation through a file opened afterwards fails + ASSERT_TRUE(closes_before_deadline(setter)); + ASSERT_EQ(move_cursor_to(mover, page, 0, 0, 0), fs::OK); + fs::file* late = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(late); + EXPECT_EQ(request_buffer(late, page, SMALL_BUFFER_WIDTH, SMALL_BUFFER_HEIGHT, format), fs::ERR_BUSY); + + ASSERT_TRUE(closes_before_deadline(late)); + ASSERT_TRUE(closes_before_deadline(mover)); +} + +static recording_display g_cursor_close_display; + +// The file the hook creates through, the page holding its request, and whether its creation returned ERR_BUSY in time +static fs::file* g_close_rival; +static user_page* g_close_page; +static bool g_rival_kept_out; + +// Runs inside the closing controller's first hide: records whether a creation through the rival file, which takes no +// cursor lock, fails with ERR_BUSY before the deadline +static void create_through_the_rival() { + g_rival_kept_out = creates_before_deadline(g_close_rival, *g_close_page, fs::ERR_BUSY); +} + +// A closing controller hides the cursor of each output, one it never gave a picture included, before its control ends, +// while a closing bystander hides none +TEST(gfxfb, a_closing_controller_hides_each_output_cursor) { + light_output(g_cursor_close_display, 0, false); + g_cursor_close_display.states[0].cursor_max_width = CURSOR_MAX_WIDTH; + g_cursor_close_display.states[0].cursor_max_height = CURSOR_MAX_HEIGHT; + gfxfb_test_node test = publish_driver_node(&g_cursor_close_display); + ASSERT_NOT_NULL(test.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* file = fs::open(test.path, fs::O_RDWR); + fs::file* bystander = fs::open(test.path, fs::O_RDWR); + fs::file* rival = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(file); + ASSERT_NOT_NULL(bystander); + ASSERT_NOT_NULL(rival); + + write_cursor_pixels(page, 1); + ASSERT_EQ(send_cursor_args(file, page, cursor_picture(page, 0, 1, 1)), fs::OK); + ASSERT_TRUE(closes_before_deadline(bystander)); + EXPECT_EQ(g_cursor_close_display.set_cursor_calls, static_cast(1)); + EXPECT_FALSE(g_cursor_close_display.hidden_on[0]); + + g_close_rival = rival; + g_close_page = &page; + g_rival_kept_out = false; + g_cursor_close_display.while_setting_cursor = create_through_the_rival; + ASSERT_TRUE(closes_before_deadline(file)); + EXPECT_EQ(g_cursor_close_display.set_cursor_calls, static_cast(3)); + EXPECT_TRUE(g_cursor_close_display.hidden_on[0]); + EXPECT_TRUE(g_cursor_close_display.hidden_on[1]); + EXPECT_TRUE(g_rival_kept_out); + + ASSERT_TRUE(closes_before_deadline(rival)); +} + +static recording_display g_serialized_cursor_display; +static gfxfb_test_node g_serialized_cursor_node; + +// The file and page the hooks' moves go through, the output whose cursor they move, the move their task makes, and what +// the hooks saw of it +static fs::file* g_serialized_cursor_file; +static user_page* g_serialized_cursor_page; +static uint32_t g_serialized_cursor_output; +static task_move g_second_move; +static sched::task* g_second_mover; +static bool g_second_mover_blocked; +static uint32_t g_move_calls_while_blocked; +static bool g_moved_while_presenting; + +// Starts the hooks' move on their output on a task of its own, or returns nullptr when the task cannot start +static sched::task* start_second_move() { + return start_move(g_second_move, g_serialized_cursor_file, *g_serialized_cursor_page, g_serialized_cursor_output); +} + +// Runs inside a `set_cursor()` display call: starts the hooks' move, then records whether it blocks and how many moves +// the display has taken meanwhile +static void move_while_setting_cursor() { + g_second_mover = start_second_move(); + g_second_mover_blocked = g_second_mover && test_helpers::blocks_before_deadline(g_second_mover); + g_move_calls_while_blocked = g_serialized_cursor_display.move_cursor_calls; +} + +// Runs inside a present's display call: starts the hooks' move and records whether it returns before the deadline +static void move_while_presenting() { + g_second_mover = start_second_move(); + g_moved_while_presenting = g_second_mover && spin_wait(g_second_move.done); +} + +// A cursor move waits for a `set_cursor()` display call for its output, so the display never runs two cursor calls for +// one output at once, but never waits for a present there +TEST(gfxfb, cursor_calls_for_one_output_reach_the_display_one_at_a_time) { + light_output(g_serialized_cursor_display, 0, false); + light_output(g_serialized_cursor_display, 1, false); + g_serialized_cursor_node = publish_driver_node(&g_serialized_cursor_display); + ASSERT_NOT_NULL(g_serialized_cursor_node.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* file = fs::open(g_serialized_cursor_node.path, fs::O_RDWR); + ASSERT_NOT_NULL(file); + + g_serialized_cursor_file = file; + g_serialized_cursor_page = &page; + + // Output 1 as well as output 0, the primary, since a call locking another output's cursor mutex would let the move + // reach the display during the hide + for (uint32_t output = 0; output < recording_display::OUTPUTS; output++) { + uint32_t moves_before = g_serialized_cursor_display.move_cursor_calls; + + g_serialized_cursor_output = output; + g_second_mover = nullptr; + g_second_mover_blocked = false; + g_move_calls_while_blocked = 0; + g_serialized_cursor_display.while_setting_cursor = move_while_setting_cursor; + + ASSERT_EQ(hide_cursor(file, page, output), fs::OK); + ASSERT_NOT_NULL(g_second_mover); + EXPECT_TRUE(g_second_mover_blocked); + EXPECT_EQ(g_move_calls_while_blocked, moves_before); + + ASSERT_TRUE(spin_wait(g_second_move.done)); + EXPECT_EQ(g_second_move.result, fs::OK); + EXPECT_EQ(g_serialized_cursor_display.move_cursor_calls, moves_before + 1); + EXPECT_EQ(g_serialized_cursor_display.cursor_output, output); + unpin(g_second_mover); + } + + g_serialized_cursor_output = 0; + g_second_mover = nullptr; + g_moved_while_presenting = false; + g_serialized_cursor_display.while_presenting = move_while_presenting; + + ASSERT_EQ(present_buffer(file, page, 0, 0), fs::OK); + ASSERT_NOT_NULL(g_second_mover); + EXPECT_TRUE(g_moved_while_presenting); + ASSERT_TRUE(spin_wait(g_second_move.done)); + EXPECT_EQ(g_second_move.result, fs::OK); + unpin(g_second_mover); + + end_present(g_serialized_cursor_node, 0, gfxfb::GFXFB_PRESENT_LATCHED); + ASSERT_TRUE(closes_before_deadline(file)); +} + +// The boot display has no hardware cursor, so a picture exceeds its cursor maximum of 0 by 0 and a hide and a move fail +// with ERR_NOSYS, none of them taking control +TEST(gfxfb, boot_display_has_no_cursor) { + gfxfb_test_node test = publish_boot_node(); + ASSERT_NOT_NULL(test.node); + + user_page page; + ASSERT_TRUE(page.ready()); + fs::file* file = fs::open(test.path, fs::O_RDWR); + fs::file* other = fs::open(test.path, fs::O_RDWR); + ASSERT_NOT_NULL(file); + ASSERT_NOT_NULL(other); + + EXPECT_EQ(send_cursor_args(file, page, cursor_picture(page, 0, 1, 1)), fs::ERR_INVAL); + EXPECT_EQ(hide_cursor(file, page, 0), fs::ERR_NOSYS); + EXPECT_EQ(move_cursor_to(file, page, 0, 0, 0), fs::ERR_NOSYS); + EXPECT_EQ(move_cursor_to(other, page, 0, 0, 0), fs::ERR_NOSYS); + + ASSERT_TRUE(closes_before_deadline(other)); + ASSERT_TRUE(closes_before_deadline(file)); +} + // Registration is permanent, so /dev/gfxfb shows `g_registered_display` for the rest of the run static recording_display g_registered_display; static recording_display g_refused_display;