Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 161 additions & 2 deletions kernel/drivers/graphics/gfxfb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>(args.width) * args.height * sizeof(uint32_t);
auto* copy = static_cast<uint32_t*>(heap::ualloc(bytes));
if (!copy) {
return fs::ERR_NOMEM;
}

rc = mm::uaccess::copy_from_user(copy, reinterpret_cast<const void*>(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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<void*>(info_addr), &info, sizeof(info));
Expand Down Expand Up @@ -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<const void*>(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<const void*>(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**
*/
Expand Down
112 changes: 89 additions & 23 deletions kernel/drivers/graphics/gfxfb.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
};

/**
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading