From 89da982956599eea008b39819acfedfb13470a5f Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Mon, 5 Oct 2026 20:57:25 -0700 Subject: [PATCH 1/2] refactor(graphics): described the gfxfb framebuffer with a single surface struct --- kernel/drivers/graphics/gfxfb.cpp | 61 +++++++++++++++---------------- kernel/drivers/graphics/gfxfb.h | 15 ++++++++ 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/kernel/drivers/graphics/gfxfb.cpp b/kernel/drivers/graphics/gfxfb.cpp index bc62c4630..0cb12203d 100644 --- a/kernel/drivers/graphics/gfxfb.cpp +++ b/kernel/drivers/graphics/gfxfb.cpp @@ -18,14 +18,10 @@ namespace { class gfxfb_node : public fs::node { public: - gfxfb_node(fs::instance* fs, const char* name, - uint64_t phys, uint64_t w, uint64_t h, uint64_t p, - uint16_t bpp, uint8_t r, uint8_t g, uint8_t b) + gfxfb_node(fs::instance* fs, const char* name, const surface& fb) : fs::node(fs::node_type::char_device, fs, name) - , m_phys(phys), m_width(w), m_height(h), m_pitch(p) - , m_bpp(bpp), m_red_shift(r), m_green_shift(g), m_blue_shift(b) - , m_fb_size(p * h) { - m_size = m_fb_size; + , m_surface(fb) { + m_size = surface_size(); } int32_t ioctl(fs::file*, uint32_t cmd, uint64_t arg) override { @@ -38,14 +34,14 @@ class gfxfb_node : public fs::node { } gfxfb_info info{}; - info.width = m_width; - info.height = m_height; - info.pitch = m_pitch; - info.bpp = m_bpp; - info.red_shift = m_red_shift; - info.green_shift = m_green_shift; - info.blue_shift = m_blue_shift; - info.size = m_fb_size; + info.width = m_surface.width; + info.height = m_surface.height; + info.pitch = m_surface.pitch; + info.bpp = m_surface.bpp; + info.red_shift = m_surface.red_shift; + info.green_shift = m_surface.green_shift; + info.blue_shift = m_surface.blue_shift; + info.size = surface_size(); int32_t rc = mm::uaccess::copy_to_user( reinterpret_cast(arg), &info, sizeof(info)); @@ -70,7 +66,7 @@ class gfxfb_node : public fs::node { return mm::MM_CTX_ERR_INVALID_ARG; } - size_t aligned_fb_size = pmm::page_align_up(m_fb_size); + size_t aligned_fb_size = pmm::page_align_up(surface_size()); if (offset + aligned_len > aligned_fb_size) { log::error("gfxfb: mmap: offset+aligned_len=%lu > aligned_fb_size=%lu", offset + aligned_len, aligned_fb_size); @@ -78,11 +74,11 @@ class gfxfb_node : public fs::node { } log::info("gfxfb: mmap: phys=0x%lx len=%lu prot=%u cache=%u", - m_phys + offset, length, prot, paging::PAGE_WC); + m_surface.phys + offset, length, prot, paging::PAGE_WC); int32_t rc = mm::mm_context_map_device( mm_ctx, - m_phys + offset, + m_surface.phys + offset, length, prot, paging::PAGE_WC, @@ -97,15 +93,9 @@ class gfxfb_node : public fs::node { } private: - uint64_t m_phys; - uint64_t m_width; - uint64_t m_height; - uint64_t m_pitch; - uint16_t m_bpp; - uint8_t m_red_shift; - uint8_t m_green_shift; - uint8_t m_blue_shift; - uint64_t m_fb_size; + uint64_t surface_size() const { return m_surface.pitch * m_surface.height; } + + surface m_surface; }; } // namespace @@ -120,17 +110,24 @@ __PRIVILEGED_CODE int32_t init() { return OK; } + const surface boot_surface = { + .phys = fb.fb_phys, + .width = fb.width, + .height = fb.height, + .pitch = fb.pitch, + .bpp = fb.bpp, + .red_shift = fb.red_mask_shift, + .green_shift = fb.green_mask_shift, + .blue_shift = fb.blue_mask_shift, + }; + void* mem = heap::kzalloc(sizeof(gfxfb_node)); if (!mem) { log::error("gfxfb: failed to allocate gfxfb_node"); return ERR; } - auto* node = new (mem) gfxfb_node( - nullptr, "gfxfb", - fb.fb_phys, fb.width, fb.height, fb.pitch, - fb.bpp, fb.red_mask_shift, fb.green_mask_shift, fb.blue_mask_shift - ); + auto* node = new (mem) gfxfb_node(nullptr, "gfxfb", boot_surface); int32_t rc = devfs::add_char_device("gfxfb", node); if (rc != devfs::OK) { diff --git a/kernel/drivers/graphics/gfxfb.h b/kernel/drivers/graphics/gfxfb.h index dd3b82327..67691ff3c 100644 --- a/kernel/drivers/graphics/gfxfb.h +++ b/kernel/drivers/graphics/gfxfb.h @@ -2,6 +2,7 @@ #define STELLUX_DRIVERS_GRAPHICS_GFXFB_H #include "common/types.h" +#include "mm/pmm_types.h" namespace gfxfb { @@ -22,6 +23,20 @@ struct gfxfb_info { uint64_t size; }; +/** + * The pixel memory behind /dev/gfxfb and the layout userland draws in. + */ +struct surface { + pmm::phys_addr_t phys; // physically contiguous, pitch * height bytes + uint64_t width; + uint64_t height; + uint64_t pitch; + uint16_t bpp; + uint8_t red_shift; + uint8_t green_shift; + uint8_t blue_shift; +}; + /** * @brief Initialize the framebuffer device and register /dev/gfxfb. * No-op if no framebuffer is available from the bootloader. From 491413b9533f26beb8e087a9368f8ba02c7a4e4c Mon Sep 17 00:00:00 2001 From: Albert Slepak Date: Mon, 5 Oct 2026 21:16:44 -0700 Subject: [PATCH 2/2] feat(gfx): added driver-owned displays and a flush ioctl to /dev/gfxfb --- kernel/drivers/graphics/gfxfb.cpp | 303 ++++++++++++++++++++------- kernel/drivers/graphics/gfxfb.h | 55 ++++- kernel/tests/graphics/gfxfb.test.cpp | 239 +++++++++++++++++++++ 3 files changed, 522 insertions(+), 75 deletions(-) create mode 100644 kernel/tests/graphics/gfxfb.test.cpp diff --git a/kernel/drivers/graphics/gfxfb.cpp b/kernel/drivers/graphics/gfxfb.cpp index 0cb12203d..271af2cac 100644 --- a/kernel/drivers/graphics/gfxfb.cpp +++ b/kernel/drivers/graphics/gfxfb.cpp @@ -14,91 +14,228 @@ namespace gfxfb { +// Rects copied in and handed to the display per call, small enough for the kernel stack +constexpr uint32_t RECTS_PER_DISPLAY_FLUSH = 16; + namespace { class gfxfb_node : public fs::node { public: - gfxfb_node(fs::instance* fs, const char* name, const surface& fb) - : fs::node(fs::node_type::char_device, fs, name) - , m_surface(fb) { - m_size = surface_size(); + gfxfb_node(fs::instance* fs, const char* name, const surface& fb, display* driver_display) + : fs::node(fs::node_type::char_device, fs, name) { + set_surface(fb, driver_display); } - int32_t ioctl(fs::file*, uint32_t cmd, uint64_t arg) override { - if (cmd != GFXFB_GET_INFO) { - return fs::ERR_NOSYS; - } + void set_surface(const surface& fb, display* driver_display); + bool has_driver_display() const { return m_display != nullptr; } - if (arg == 0) { - return fs::ERR_INVAL; - } + int32_t ioctl(fs::file*, uint32_t cmd, uint64_t arg) override; + int32_t mmap(fs::file*, mm::mm_context* mm_ctx, uintptr_t addr, + size_t length, uint32_t prot, uint32_t map_flags, + uint64_t offset, uintptr_t* out_addr) override; - gfxfb_info info{}; - info.width = m_surface.width; - info.height = m_surface.height; - info.pitch = m_surface.pitch; - info.bpp = m_surface.bpp; - info.red_shift = m_surface.red_shift; - info.green_shift = m_surface.green_shift; - info.blue_shift = m_surface.blue_shift; - info.size = surface_size(); - - int32_t rc = mm::uaccess::copy_to_user( - reinterpret_cast(arg), &info, sizeof(info)); - if (rc != mm::uaccess::OK) { - return fs::ERR_INVAL; - } +private: + int32_t get_info(uint64_t info_addr); + int32_t flush(uint64_t args_addr); + uint64_t surface_size() const { return m_surface.pitch * m_surface.height; } + + surface m_surface = {}; + display* m_display = nullptr; +}; + +} // namespace + +__PRIVILEGED_BSS static gfxfb_node* g_node; +// Trims `rect` to `fb` and returns false when nothing of it remains +static bool clip_rect_to_surface(gfxfb_rect& rect, const surface& fb) { + if (rect.width == 0 || rect.height == 0 || rect.x >= fb.width || rect.y >= fb.height) { + return false; + } + + uint64_t max_width = fb.width - rect.x; + if (rect.width > max_width) { + rect.width = static_cast(max_width); + } + + uint64_t max_height = fb.height - rect.y; + if (rect.height > max_height) { + rect.height = static_cast(max_height); + } + + return true; +} + +void gfxfb_node::set_surface(const surface& fb, display* driver_display) { + m_surface = fb; + m_display = driver_display; + m_size = surface_size(); +} + +int32_t gfxfb_node::ioctl(fs::file*, uint32_t cmd, uint64_t arg) { + if (cmd == GFXFB_GET_INFO) { + return get_info(arg); + } + + if (cmd == GFXFB_FLUSH) { + return flush(arg); + } + + return fs::ERR_NOSYS; +} + +int32_t gfxfb_node::get_info(uint64_t info_addr) { + if (info_addr == 0) { + return fs::ERR_INVAL; + } + + gfxfb_info info{}; + info.width = m_surface.width; + info.height = m_surface.height; + info.pitch = m_surface.pitch; + info.bpp = m_surface.bpp; + info.red_shift = m_surface.red_shift; + info.green_shift = m_surface.green_shift; + info.blue_shift = m_surface.blue_shift; + info.flags = has_driver_display() ? GFXFB_INFO_NEEDS_FLUSH : 0; + info.size = surface_size(); + + int32_t rc = mm::uaccess::copy_to_user( + reinterpret_cast(info_addr), &info, sizeof(info)); + + if (rc != mm::uaccess::OK) { + return fs::ERR_INVAL; + } + + return fs::OK; +} + +int32_t gfxfb_node::flush(uint64_t args_addr) { + if (args_addr == 0) { + return fs::ERR_INVAL; + } + + gfxfb_flush_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; + } + + if (args.reserved != 0 || args.count > GFXFB_MAX_FLUSH_RECTS) { + return fs::ERR_INVAL; + } + + // The boot framebuffer is scanned out directly, so there is nothing to copy + if (!has_driver_display()) { return fs::OK; } - int32_t mmap(fs::file*, mm::mm_context* mm_ctx, uintptr_t addr, - size_t length, uint32_t prot, uint32_t map_flags, - uint64_t offset, uintptr_t* out_addr) override { - size_t aligned_len = pmm::page_align_up(length); - if (aligned_len < length) { - log::error("gfxfb: mmap: aligned_len < length"); - return mm::MM_CTX_ERR_INVALID_ARG; + gfxfb_rect batch[RECTS_PER_DISPLAY_FLUSH]; + uint32_t handled = 0; + + while (handled < args.count) { + uint32_t batch_count = args.count - handled; + if (batch_count > RECTS_PER_DISPLAY_FLUSH) { + batch_count = RECTS_PER_DISPLAY_FLUSH; } - if (offset + aligned_len < aligned_len) { - log::error("gfxfb: mmap: offset overflow"); - return mm::MM_CTX_ERR_INVALID_ARG; + uint64_t batch_addr = args.rects + static_cast(handled) * sizeof(gfxfb_rect); + rc = mm::uaccess::copy_from_user( + batch, reinterpret_cast(batch_addr), batch_count * sizeof(gfxfb_rect)); + + if (rc != mm::uaccess::OK) { + return fs::ERR_INVAL; } - size_t aligned_fb_size = pmm::page_align_up(surface_size()); - if (offset + aligned_len > aligned_fb_size) { - log::error("gfxfb: mmap: offset+aligned_len=%lu > aligned_fb_size=%lu", - offset + aligned_len, aligned_fb_size); - return mm::MM_CTX_ERR_INVALID_ARG; + uint32_t kept = 0; + for (uint32_t i = 0; i < batch_count; i++) { + gfxfb_rect rect = batch[i]; + + if (clip_rect_to_surface(rect, m_surface)) { + batch[kept++] = rect; + } } - log::info("gfxfb: mmap: phys=0x%lx len=%lu prot=%u cache=%u", - m_surface.phys + offset, length, prot, paging::PAGE_WC); - - int32_t rc = mm::mm_context_map_device( - mm_ctx, - m_surface.phys + offset, - length, - prot, - paging::PAGE_WC, - map_flags, - addr, - out_addr - ); - if (rc != mm::MM_CTX_OK) { - log::error("gfxfb: mmap: mm_context_map_device failed rc=%d", rc); + if (kept > 0 && m_display->flush(batch, kept) != OK) { + return fs::ERR_IO; } - return rc; + + handled += batch_count; } -private: - uint64_t surface_size() const { return m_surface.pitch * m_surface.height; } + return fs::OK; +} - surface m_surface; -}; +int32_t gfxfb_node::mmap(fs::file*, mm::mm_context* mm_ctx, uintptr_t addr, + size_t length, uint32_t prot, uint32_t map_flags, + uint64_t offset, uintptr_t* out_addr) { + size_t aligned_len = pmm::page_align_up(length); + if (aligned_len < length) { + log::error("gfxfb: mmap: aligned_len < length"); + return mm::MM_CTX_ERR_INVALID_ARG; + } -} // namespace + if (offset + aligned_len < aligned_len) { + log::error("gfxfb: mmap: offset overflow"); + return mm::MM_CTX_ERR_INVALID_ARG; + } + + size_t aligned_fb_size = pmm::page_align_up(surface_size()); + if (offset + aligned_len > aligned_fb_size) { + log::error("gfxfb: mmap: offset+aligned_len=%lu > aligned_fb_size=%lu", + offset + aligned_len, aligned_fb_size); + + return mm::MM_CTX_ERR_INVALID_ARG; + } + + uint32_t cache = m_surface.cacheable ? paging::PAGE_NORMAL : paging::PAGE_WC; + log::info("gfxfb: mmap: phys=0x%lx len=%lu prot=%u cache=%u", + m_surface.phys + offset, length, prot, cache); + + int32_t rc = mm::mm_context_map_device( + mm_ctx, + m_surface.phys + offset, + length, + prot, + cache, + map_flags, + addr, + out_addr + ); + + if (rc != mm::MM_CTX_OK) { + log::error("gfxfb: mmap: mm_context_map_device failed rc=%d", rc); + } + + return rc; +} + +/** + * @note Privilege: **required** + */ +__PRIVILEGED_CODE static int32_t add_node(const surface& fb, display* driver_display) { + void* mem = heap::kzalloc(sizeof(gfxfb_node)); + if (!mem) { + log::error("gfxfb: failed to allocate gfxfb_node"); + return ERR; + } + + auto* node = new (mem) gfxfb_node(nullptr, "gfxfb", fb, driver_display); + + int32_t rc = devfs::add_char_device("gfxfb", node); + if (rc != devfs::OK) { + log::error("gfxfb: failed to register /dev/gfxfb"); + node->~gfxfb_node(); + heap::kfree(mem); + return ERR; + } + + g_node = node; + + return OK; +} /** * @note Privilege: **required** @@ -119,27 +256,45 @@ __PRIVILEGED_CODE int32_t init() { .red_shift = fb.red_mask_shift, .green_shift = fb.green_mask_shift, .blue_shift = fb.blue_mask_shift, + .cacheable = false, }; - void* mem = heap::kzalloc(sizeof(gfxfb_node)); - if (!mem) { - log::error("gfxfb: failed to allocate gfxfb_node"); + if (add_node(boot_surface, nullptr) != OK) { return ERR; } - auto* node = new (mem) gfxfb_node(nullptr, "gfxfb", boot_surface); + log::info("gfxfb: %lux%lu %ubpp phys=0x%lx size=%lu, registered /dev/gfxfb", + fb.width, fb.height, static_cast(fb.bpp), + fb.fb_phys, fb.pitch * fb.height); + return OK; +} - int32_t rc = devfs::add_char_device("gfxfb", node); - if (rc != devfs::OK) { - log::error("gfxfb: failed to register /dev/gfxfb"); - node->~gfxfb_node(); - heap::kfree(mem); +/** + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t register_display(const surface& fb, display* driver_display) { + if (!driver_display) { return ERR; } - log::info("gfxfb: %lux%lu %ubpp phys=0x%lx size=%lu, registered /dev/gfxfb", - fb.width, fb.height, static_cast(fb.bpp), - fb.fb_phys, fb.pitch * fb.height); + if (g_node && g_node->has_driver_display()) { + log::warn("gfxfb: a driver display is already registered"); + return ERR; + } + + if (g_node) { + g_node->set_surface(fb, driver_display); + log::info("gfxfb: %lux%lu driver display replaced the boot framebuffer", + fb.width, fb.height); + return OK; + } + + if (add_node(fb, driver_display) != OK) { + return ERR; + } + + log::info("gfxfb: %lux%lu driver display, registered /dev/gfxfb", fb.width, fb.height); + return OK; } diff --git a/kernel/drivers/graphics/gfxfb.h b/kernel/drivers/graphics/gfxfb.h index 67691ff3c..d56358ffd 100644 --- a/kernel/drivers/graphics/gfxfb.h +++ b/kernel/drivers/graphics/gfxfb.h @@ -10,6 +10,11 @@ constexpr int32_t OK = 0; constexpr int32_t ERR = -1; constexpr uint32_t GFXFB_GET_INFO = 0x4700; +constexpr uint32_t GFXFB_FLUSH = 0x4701; + +constexpr uint8_t GFXFB_INFO_NEEDS_FLUSH = 0x01; // drawing reaches the screen only through GFXFB_FLUSH + +constexpr uint32_t GFXFB_MAX_FLUSH_RECTS = 256; struct gfxfb_info { uint64_t width; @@ -19,10 +24,25 @@ struct gfxfb_info { uint8_t red_shift; uint8_t green_shift; uint8_t blue_shift; - uint8_t padding[3]; + uint8_t flags; // GFXFB_INFO_* + uint8_t padding[2]; uint64_t size; }; +struct gfxfb_rect { + uint32_t x; + uint32_t y; + uint32_t width; + uint32_t height; +}; + +// GFXFB_FLUSH argument, naming the regions of the mapping that changed +struct gfxfb_flush_args { + uint64_t rects; // user address of `count` gfxfb_rect entries + uint32_t count; // at most GFXFB_MAX_FLUSH_RECTS + uint32_t reserved; // must be zero +}; + /** * The pixel memory behind /dev/gfxfb and the layout userland draws in. */ @@ -35,6 +55,27 @@ struct surface { uint8_t red_shift; uint8_t green_shift; uint8_t blue_shift; + bool cacheable; +}; + +/** + * A display that a device driver owns. Userland draws into its surface + * through /dev/gfxfb, then `GFXFB_FLUSH` hands the changed regions to + * `flush()` which copies them to the screen. + */ +class display { +public: + virtual ~display() = default; + + /** + * @brief Copy regions of the surface to the screen. Runs in the task + * that called `GFXFB_FLUSH` and may block it. Several tasks can call + * it at once. + * @param rects Regions inside the surface, none of them empty. + * @param count Number of regions, at least one. + * @return OK on success, ERR on failure. + */ + virtual int32_t flush(const gfxfb_rect* rects, uint32_t count) = 0; }; /** @@ -45,6 +86,18 @@ struct surface { */ __PRIVILEGED_CODE int32_t init(); +/** + * @brief Put a driver-owned display behind /dev/gfxfb in place of the boot + * framebuffer. Only the first driver display is accepted. It must be + * registered before userland starts, as from a driver's `attach()`. + * @param fb Surface userland maps and draws into. Its memory must never be freed. + * @param driver_display Display that flushes `fb`. It must never be freed. + * @return OK on success, ERR if a driver display already exists or + * registration fails. + * @note Privilege: **required** + */ +__PRIVILEGED_CODE int32_t register_display(const surface& fb, display* driver_display); + } // namespace gfxfb #endif // STELLUX_DRIVERS_GRAPHICS_GFXFB_H diff --git a/kernel/tests/graphics/gfxfb.test.cpp b/kernel/tests/graphics/gfxfb.test.cpp new file mode 100644 index 000000000..1dc66f765 --- /dev/null +++ b/kernel/tests/graphics/gfxfb.test.cpp @@ -0,0 +1,239 @@ +#define STLX_TEST_TIER TIER_SCHED + +#include "stlx_unit_test.h" +#include "helpers.h" +#include "drivers/graphics/gfxfb.h" +#include "fs/fs.h" +#include "fs/file.h" +#include "fs/fstypes.h" +#include "mm/mm.h" +#include "mm/paging.h" +#include "mm/vma.h" +#include "mm/vmm.h" + +using test_helpers::user_page; +using test_helpers::user_space_scope; + +TEST_SUITE(gfxfb); + +static constexpr uint64_t SURFACE_WIDTH = 64; +static constexpr uint64_t SURFACE_HEIGHT = 32; +static constexpr uint16_t SURFACE_BPP = 32; +static constexpr uint64_t SURFACE_PITCH = SURFACE_WIDTH * (SURFACE_BPP / 8); +static constexpr uint64_t SURFACE_SIZE = SURFACE_PITCH * SURFACE_HEIGHT; +static constexpr size_t SURFACE_PAGES = SURFACE_SIZE / pmm::PAGE_SIZE; +static constexpr uint8_t SURFACE_RED_SHIFT = 16; +static constexpr uint8_t SURFACE_GREEN_SHIFT = 8; +static constexpr uint8_t SURFACE_BLUE_SHIFT = 0; + +// Where the flush tests place their request and rect array in the user page +static constexpr size_t ARGS_OFFSET = 0; +static constexpr size_t RECTS_OFFSET = 64; + +// Records every rect the device hands to the display +class recording_display : public gfxfb::display { +public: + static constexpr uint32_t MAX_RECORDED = 64; + + gfxfb::gfxfb_rect recorded[MAX_RECORDED] = {}; + uint32_t recorded_count = 0; + + int32_t flush(const gfxfb::gfxfb_rect* rects, uint32_t count) override { + for (uint32_t i = 0; i < count && recorded_count < MAX_RECORDED; i++) { + recorded[recorded_count++] = rects[i]; + } + + return gfxfb::OK; + } +}; + +// Registration is permanent, so the display and its surface outlive the suite +static recording_display g_display; +static gfxfb::surface g_surface = {}; + +static int32_t register_test_display() { + uintptr_t kernel_va = 0; + pmm::phys_addr_t phys = 0; + int32_t rc = vmm::alloc_contiguous( + SURFACE_PAGES, pmm::ZONE_ANY, paging::PAGE_KERNEL_RW, + vmm::ALLOC_ZERO, kva::tag::generic, kernel_va, phys); + if (rc != vmm::OK) { + return rc; + } + + g_surface = { + .phys = phys, + .width = SURFACE_WIDTH, + .height = SURFACE_HEIGHT, + .pitch = SURFACE_PITCH, + .bpp = SURFACE_BPP, + .red_shift = SURFACE_RED_SHIFT, + .green_shift = SURFACE_GREEN_SHIFT, + .blue_shift = SURFACE_BLUE_SHIFT, + .cacheable = true, + }; + + return gfxfb::register_display(g_surface, &g_display); +} + +BEFORE_ALL(gfxfb, register_test_display); + +// Calls an ioctl on /dev/gfxfb inside the page's address space, as a syscall body would +static int32_t call_gfxfb_ioctl(user_page& page, uint32_t cmd, uint64_t arg) { + int32_t open_err = fs::OK; + fs::file* file = fs::open("/dev/gfxfb", fs::O_RDWR, &open_err); + if (!file) { + return open_err; + } + + int32_t rc = fs::OK; + { + user_space_scope scope(page.ctx); + rc = fs::ioctl(file, cmd, arg); + } + + fs::close(file); + + return rc; +} + +// Writes a flush request for `count` rects already placed at RECTS_OFFSET +static void write_flush_args(user_page& page, uint32_t count) { + auto* args = page.at(ARGS_OFFSET); + args->rects = page.addr + RECTS_OFFSET; + args->count = count; + args->reserved = 0; +} + +static bool rect_equals(const gfxfb::gfxfb_rect& rect, uint32_t x, uint32_t y, + uint32_t width, uint32_t height) { + return rect.x == x && rect.y == y && rect.width == width && rect.height == height; +} + +// GFXFB_GET_INFO describes the driver display and tells userland to flush +TEST(gfxfb, get_info_reports_the_driver_display) { + user_page page; + ASSERT_TRUE(page.ready()); + + ASSERT_EQ(call_gfxfb_ioctl(page, gfxfb::GFXFB_GET_INFO, page.addr), fs::OK); + + auto* info = page.at(0); + EXPECT_EQ(info->width, SURFACE_WIDTH); + EXPECT_EQ(info->height, SURFACE_HEIGHT); + EXPECT_EQ(info->pitch, SURFACE_PITCH); + EXPECT_EQ(info->bpp, SURFACE_BPP); + EXPECT_EQ(info->red_shift, SURFACE_RED_SHIFT); + EXPECT_EQ(info->green_shift, SURFACE_GREEN_SHIFT); + EXPECT_EQ(info->blue_shift, SURFACE_BLUE_SHIFT); + EXPECT_EQ(info->flags, gfxfb::GFXFB_INFO_NEEDS_FLUSH); + EXPECT_EQ(info->size, SURFACE_SIZE); +} + +// Rects crossing the display edge are trimmed, and empty or outside ones are dropped +TEST(gfxfb, flush_clips_rects_to_the_display) { + user_page page; + ASSERT_TRUE(page.ready()); + + auto* rects = page.at(RECTS_OFFSET); + rects[0] = {2, 3, 10, 4}; // inside, kept whole + rects[1] = {60, 30, 10, 10}; // crosses the corner, trimmed to 4x2 + rects[2] = {64, 0, 5, 5}; // right of the display, dropped + rects[3] = {0, 0, 0, 7}; // empty, dropped + rects[4] = {0xFFFFFFF0, 0, 0x20, 1}; // x + width overflows, dropped + rects[5] = {1, 1, 0xFFFFFFFF, 0xFFFFFFFF}; // oversized, trimmed to 63x31 + + write_flush_args(page, 6); + g_display.recorded_count = 0; + ASSERT_EQ(call_gfxfb_ioctl(page, gfxfb::GFXFB_FLUSH, page.addr + ARGS_OFFSET), fs::OK); + + ASSERT_EQ(g_display.recorded_count, static_cast(3)); + EXPECT_TRUE(rect_equals(g_display.recorded[0], 2, 3, 10, 4)); + EXPECT_TRUE(rect_equals(g_display.recorded[1], 60, 30, 4, 2)); + EXPECT_TRUE(rect_equals(g_display.recorded[2], 1, 1, 63, 31)); +} + +// A list longer than one internal batch reaches the display complete and in order +TEST(gfxfb, flush_delivers_every_rect_of_a_long_list) { + user_page page; + ASSERT_TRUE(page.ready()); + + constexpr uint32_t COUNT = 40; + auto* rects = page.at(RECTS_OFFSET); + for (uint32_t i = 0; i < COUNT; i++) { + rects[i] = {i, 0, 1, 1}; + } + + write_flush_args(page, COUNT); + g_display.recorded_count = 0; + ASSERT_EQ(call_gfxfb_ioctl(page, gfxfb::GFXFB_FLUSH, page.addr + ARGS_OFFSET), fs::OK); + + ASSERT_EQ(g_display.recorded_count, COUNT); + for (uint32_t i = 0; i < COUNT; i++) { + EXPECT_EQ(g_display.recorded[i].x, i); + } +} + +// Oversized lists, nonzero reserved fields and unreadable pointers fail before reaching the display +TEST(gfxfb, flush_rejects_malformed_requests) { + user_page page; + ASSERT_TRUE(page.ready()); + + auto* rects = page.at(RECTS_OFFSET); + rects[0] = {0, 0, 1, 1}; + g_display.recorded_count = 0; + + write_flush_args(page, gfxfb::GFXFB_MAX_FLUSH_RECTS + 1); + EXPECT_EQ(call_gfxfb_ioctl(page, gfxfb::GFXFB_FLUSH, page.addr + ARGS_OFFSET), fs::ERR_INVAL); + + auto* args = page.at(ARGS_OFFSET); + write_flush_args(page, 1); + args->reserved = 1; + EXPECT_EQ(call_gfxfb_ioctl(page, gfxfb::GFXFB_FLUSH, page.addr + ARGS_OFFSET), fs::ERR_INVAL); + + write_flush_args(page, 1); + args->rects = page.addr + pmm::PAGE_SIZE; + EXPECT_EQ(call_gfxfb_ioctl(page, gfxfb::GFXFB_FLUSH, page.addr + ARGS_OFFSET), fs::ERR_INVAL); + + EXPECT_EQ(call_gfxfb_ioctl(page, gfxfb::GFXFB_FLUSH, 0), fs::ERR_INVAL); + EXPECT_EQ(g_display.recorded_count, static_cast(0)); +} + +// Mapping /dev/gfxfb reaches the surface memory, write-back because it is RAM +TEST(gfxfb, mmap_maps_the_display_surface) { + mm::mm_context* ctx = mm::mm_context_create(); + ASSERT_NOT_NULL(ctx); + + fs::file* file = fs::open("/dev/gfxfb", fs::O_RDWR); + ASSERT_NOT_NULL(file); + + uintptr_t addr = 0; + uint32_t prot = mm::MM_PROT_READ | mm::MM_PROT_WRITE; + EXPECT_EQ(fs::mmap(file, ctx, 0, SURFACE_SIZE, prot, mm::MM_MAP_SHARED, 0, &addr), mm::MM_CTX_OK); + EXPECT_EQ(paging::get_physical(addr, ctx->pt_root), g_surface.phys); + EXPECT_EQ(paging::get_physical(addr + pmm::PAGE_SIZE, ctx->pt_root), g_surface.phys + pmm::PAGE_SIZE); + + paging::page_flags_t flags = paging::get_page_flags(addr, ctx->pt_root); + EXPECT_EQ(flags & paging::PAGE_TYPE_MASK, paging::PAGE_NORMAL); + + fs::close(file); + mm::mm_context_release(ctx); +} + +static recording_display g_rival_display; + +// Once a driver display owns /dev/gfxfb, a later one cannot take it over +TEST(gfxfb, second_driver_display_is_refused) { + EXPECT_EQ(gfxfb::register_display(g_surface, &g_rival_display), gfxfb::ERR); + + user_page page; + ASSERT_TRUE(page.ready()); + + auto* rects = page.at(RECTS_OFFSET); + rects[0] = {0, 0, 1, 1}; + write_flush_args(page, 1); + + g_display.recorded_count = 0; + ASSERT_EQ(call_gfxfb_ioctl(page, gfxfb::GFXFB_FLUSH, page.addr + ARGS_OFFSET), fs::OK); + EXPECT_EQ(g_display.recorded_count, static_cast(1)); + EXPECT_EQ(g_rival_display.recorded_count, static_cast(0)); +}