diff --git a/repos/os/recipes/src/virtio_fb_drv/content.mk b/repos/os/recipes/src/virtio_fb_drv/content.mk new file mode 100644 index 0000000000..44917f3a41 --- /dev/null +++ b/repos/os/recipes/src/virtio_fb_drv/content.mk @@ -0,0 +1,2 @@ +SRC_DIR = src/drivers/framebuffer/virtio +include $(GENODE_DIR)/repos/base/recipes/src/content.inc diff --git a/repos/os/recipes/src/virtio_fb_drv/hash b/repos/os/recipes/src/virtio_fb_drv/hash new file mode 100644 index 0000000000..e236df5623 --- /dev/null +++ b/repos/os/recipes/src/virtio_fb_drv/hash @@ -0,0 +1 @@ +2021-09-21 505752c4facfcd13ceb75f90b7c9f43209586513 diff --git a/repos/os/recipes/src/virtio_fb_drv/used_apis b/repos/os/recipes/src/virtio_fb_drv/used_apis new file mode 100644 index 0000000000..51a066a8c4 --- /dev/null +++ b/repos/os/recipes/src/virtio_fb_drv/used_apis @@ -0,0 +1,7 @@ +base +blit +capture_session +event_session +os +platform_session +virtio diff --git a/repos/os/src/drivers/framebuffer/virtio/component.h b/repos/os/src/drivers/framebuffer/virtio/component.h new file mode 100644 index 0000000000..d36f7bd803 --- /dev/null +++ b/repos/os/src/drivers/framebuffer/virtio/component.h @@ -0,0 +1,563 @@ +/* + * \brief VirtIO MMIO Framebuffer component + * \author Piotr Tworek + * \date 2020-02-14 + */ + +/* + * Copyright (C) 2020 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace Virtio_fb { + using namespace Genode; + class Driver; +} + +/* + * This driver is based on Virtual I/O Device specification, Version 1.1, chapter 5.7 + * "GPU Device". This document can be found at: + * https://docs.oasis-open.org/virtio/virtio/v1.1/csprd01/virtio-v1.1-csprd01.html + */ +class Virtio_fb::Driver +{ + private: + + /* + * Noncopyable + */ + Driver(Root const &) = delete; + Driver &operator = (Driver const &) = delete; + + struct Device_init_failed : Exception { }; + struct Unsupported_version : Exception { }; + struct Features_init_failed : Exception { }; + struct Queue_init_failed : Exception { }; + struct Display_init_failed : Exception { }; + struct Display_deinit_failed : Exception { }; + + enum : uint16_t { CONTROL_VQ = 0, CURSOR_VQ = 1 }; + + struct Features : Register<64> + { + struct VIRGL : Bitfield<0, 1> { }; + struct EDID : Bitfield<1, 1> { }; + struct VERSION_1 : Bitfield<32, 1> { }; + }; + + struct Control_header + { + enum Type : uint32_t + { + /* 2D commands */ + CMD_GET_DISPLAY_INFO = 0x0100, + CMD_RESOURCE_CREATE_2D, + CMD_RESOURCE_UNREF, + CMD_RESOURCE_SET_SCANOUT, + CMD_RESOURCE_FLUSH, + CMD_RESOURCE_TRANSFER_TO_HOST, + CMD_RESOURCE_ATTACH_BACKING, + CMD_RESOURCE_DETACH_BACKING, + + /* Success responses */ + RESP_OK_NODATA = 0x1100, + RESP_OK_DISPLAY_INFO, + RESP_OK_CAPSET_INFO, + RESP_OK_CAPSET, + RESP_OK_EDID, + + /* Error responses */ + RESP_ERROR_UNSPECIFIED = 0x1200, + RESP_ERROR_OUT_OF_MEMORY, + RESP_ERROR_INVALID_SCANOUT_ID, + RESP_ERROR_INVALID_RESOURCE_ID, + RESP_ERROR_INVALID_CONTEXT_ID, + RESP_ERROR_INVALID_PARAMETER_ID, + }; + + Type type; + uint32_t flags = 0; + uint64_t fence_id = 0; + uint32_t ctx_id = 0; + uint32_t const padding = 0; + }; + + enum { MAX_SCANOUTS = 16 }; + enum : uint32_t { EVENT_DISPLAY = (1 << 0) }; + + enum Config : uint8_t + { + EVENTS_READ = 0, + EVENTS_CLEAR = 4, + NUM_SCANOUTS = 8, + }; + + struct Rect + { + uint32_t x; + uint32_t y; + uint32_t width; + uint32_t height; + }; + + struct Display_info + { + Control_header hdr; + struct + { + Rect rect; + uint32_t enabled; + uint32_t flags; + } modes[MAX_SCANOUTS]; + }; + + enum class Resource_Id : uint32_t { FRAMEBUFFER = 1 }; + + enum class Format : uint32_t { B8G8R8X8 = 2 }; + + struct Resource_create_2d + { + Resource_Id const resource_id = Resource_Id::FRAMEBUFFER; + Format const format = Format::B8G8R8X8; + uint32_t width; + uint32_t height; + }; + + struct Resource_destroy_2d + { + Resource_Id const resource_id = Resource_Id::FRAMEBUFFER; + uint32_t const padding = 0; + }; + + struct Attach_backing + { + Resource_Id const resource_id = Resource_Id::FRAMEBUFFER; + uint32_t const nr_entries = 1; + uint64_t addr; + uint32_t length; + uint32_t const padding = 0; + }; + + using Detach_backing = Resource_destroy_2d; + + struct Set_scanout + { + Rect rect; + uint32_t scanout_id; + Resource_Id const resource_id = Resource_Id::FRAMEBUFFER; + }; + + struct Transfer_to_host_2d + { + Rect rect; + uint64_t offset; + Resource_Id const resource_id = Resource_Id::FRAMEBUFFER; + uint32_t const paddiing = 0; + }; + + struct Resource_flush + { + Rect rect; + Resource_Id const resource_id = Resource_Id::FRAMEBUFFER; + uint32_t const padding = 0; + }; + + struct Control_queue_traits + { + static const bool device_write_only = false; + static const bool has_data_payload = true; + }; + + typedef Virtio::Queue Control_queue; + + class Fb_memory_resource + { + private: + + /* + * Noncopyable + */ + Fb_memory_resource(Fb_memory_resource const &) = delete; + Fb_memory_resource &operator = (Fb_memory_resource const &) = delete; + + Platform::Connection &_platform; + Attached_dataspace _attachement; + + static size_t _fb_size(Capture::Area const &area) { + return area.w() * area.h() * 4; } + + public: + + addr_t phys_addr() + { + Dataspace_client client(_attachement.cap()); + return client.phys_addr(); + } + + Capture::Pixel *local_addr() { return _attachement.local_addr(); } + size_t size() const { return _attachement.size(); }; + + Fb_memory_resource(Region_map &rm, + Platform::Connection &platform, + Capture::Area const &area) + : _platform(platform) + , _attachement(rm, _platform.alloc_dma_buffer(_fb_size(area), UNCACHED)) { } + + ~Fb_memory_resource() + { + _platform.free_dma_buffer(static_cap_cast(_attachement.cap())); + _attachement.invalidate(); + } + }; + + Env &_env; + Platform::Connection &_platform; + Virtio::Device &_device; + Control_queue _ctrl_vq { _env.ram(), _env.rm(), 4, 512 }; + uint32_t const _num_scanouts; + Signal_handler _irq_handler { _env.ep(), *this, &Driver::_handle_irq }; + + /* + * Capture + */ + uint32_t _selected_scanout_id { 0 }; + Capture::Area _display_area { 0, 0 }; + Capture::Connection _capture { _env }; + Constructible _captured_screen { }; + Timer::Connection _capture_timer { _env }; + Constructible _fb_res { }; + + Signal_handler _capture_timer_handler { + _env.ep(), *this, &Driver::_handle_capture_timer }; + + + static uint32_t _read_num_scanouts(Virtio::Device &device) + { + uint32_t num_scanouts; + uint32_t before = 0, after = 0; + do { + num_scanouts = device.read_config( + Config::NUM_SCANOUTS, Virtio::Device::ACCESS_32BIT); + } while (after != before); + return num_scanouts; + } + + uint32_t _read_pending_events() + { + uint32_t events; + uint32_t before = 0, after = 0; + do { + events = _device.read_config( + Config::EVENTS_READ, Virtio::Device::ACCESS_32BIT); + } while (after != before); + do { + _device.write_config( + Config::EVENTS_CLEAR, Virtio::Device::ACCESS_32BIT, events); + } while (after != before); + return events; + } + + static uint32_t _init_device(Virtio::Device &device, + Virtio::Queue_description const &ctrl_vq_desc) + { + using Status = Virtio::Device::Status; + + if (!device.set_status(Status::RESET)) { + error("Failed to reset the device!"); + throw Device_init_failed(); + } + + if (!device.set_status(Status::ACKNOWLEDGE)) { + error("Failed to acknowledge the device!"); + throw Device_init_failed(); + } + + if (!device.set_status(Status::DRIVER)) { + device.set_status(Status::FAILED); + error("Device initialization failed!"); + throw Device_init_failed(); + } + + const uint32_t low = device.get_features(0); + const uint32_t high = device.get_features(1); + const Features::access_t device_features = ((uint64_t)high << 32) | low; + Features::access_t driver_features = 0; + + /* This driver does not support legacy VirtIO versions. */ + if (!Features::VERSION_1::get(device_features)) { + error("Unsupprted VirtIO device version!"); + throw Unsupported_version(); + } + Features::VERSION_1::set(driver_features); + + device.set_features(0, (uint32_t)driver_features); + device.set_features(1, (uint32_t)(driver_features >> 32)); + + if (!device.set_status(Status::FEATURES_OK)) { + device.set_status(Status::FAILED); + error("Device feature negotiation failed!"); + throw Features_init_failed(); + } + + if (!device.configure_queue(CONTROL_VQ, ctrl_vq_desc)) { + error("Failed to initialize \"control\" VirtIO queue!"); + throw Queue_init_failed(); + } + + using Status = Virtio::Device::Status; + if (!device.set_status(Status::DRIVER_OK)) { + device.set_status(Status::FAILED); + error("Failed to initialize VirtIO queues!"); + throw Queue_init_failed(); + } + + auto num_scanouts = _read_num_scanouts(device); + if (num_scanouts > MAX_SCANOUTS) { + error("Invalid scanout number!"); + throw Device_init_failed(); + } + + return num_scanouts; + } + + void _configure_display() { + Control_header res2d_cmd { Control_header::CMD_RESOURCE_CREATE_2D }; + Resource_create_2d res2d_data { + .width = _display_area.w(), + .height = _display_area.h(), + }; + + if (!_exec_cmd(res2d_cmd, res2d_data)) { + error("Failed to create 2D resource!"); + throw Display_init_failed(); + } + + try { + _fb_res.construct(_env.rm(), _platform, _display_area); + } catch (...) { + error("Failed to allocate framebuffer!"); + throw; + } + + { + Control_header attach_cmd { Control_header::CMD_RESOURCE_ATTACH_BACKING }; + Attach_backing attach_data { + .addr = _fb_res->phys_addr(), + .length = static_cast(_fb_res->size()) + }; + + if (!_exec_cmd(attach_cmd, attach_data)) { + error("Failed to attach framebuffer backing!"); + throw Display_init_failed(); + } + } + + Control_header scanout_cmd { Control_header::CMD_RESOURCE_SET_SCANOUT }; + Set_scanout scanout_data { + .rect = { + .x = 0, + .y = 0, + .width = _display_area.w(), + .height = _display_area.h(), + }, + .scanout_id = _selected_scanout_id, + }; + + if (!_exec_cmd(scanout_cmd, scanout_data)) { + error("Failed to assign framebuffer to a scanout!"); + throw Display_init_failed(); + } + + _captured_screen.construct(_capture, _env.rm(), _display_area); + } + + void _shutdown_display() + { + Control_header detach_cmd { Control_header::CMD_RESOURCE_DETACH_BACKING }; + Detach_backing detach_data; + + if (!_exec_cmd(detach_cmd, detach_data)) { + error("Failed to detatch framebuffer backing!"); + throw Display_deinit_failed(); + } + + Control_header unref_cmd { Control_header::CMD_RESOURCE_UNREF }; + Resource_destroy_2d unref_data; + + if (!_exec_cmd(unref_cmd, unref_data)) { + error("Failed to unref framebuffer resource!"); + throw Display_deinit_failed(); + } + + _captured_screen.destruct(); + _fb_res.destruct(); + } + + void _reconfigure_display() + { + if (!_update_display_info(true)) { + error("Failed to update display info!"); + throw Display_deinit_failed(); + } + + _shutdown_display(); + _configure_display(); + } + + void _handle_irq() + { + const uint32_t reasons = _device.read_isr(); + + enum { IRQ_USED_RING_UPDATE = 1, IRQ_CONFIG_CHANGE = 2 }; + + /* + * This driver does not request interrupts when dealing with control + * queue. Some older pre 6.x Qemu versions do signal ctrl ring update + * when display size is changed. Just ACK and otherwise ignore such + * bogus updates. + */ + if ((reasons & IRQ_USED_RING_UPDATE) && _ctrl_vq.has_used_buffers()) + _ctrl_vq.ack_all_transfers(); + + if (reasons & IRQ_CONFIG_CHANGE) { + auto const events = _read_pending_events(); + if (events & EVENT_DISPLAY) + _reconfigure_display(); + } + + _device.irq_ack(); + } + + void _handle_capture_timer() + { + using Pixel = Capture::Pixel; + + if (!_captured_screen.constructed()) + return; + + Capture::Surface surface(_fb_res->local_addr(), _display_area); + _captured_screen->apply_to_surface(surface); + + _update_fb(); + } + + void _flush_ctrl_vq() + { + _device.notify_buffers_available(CONTROL_VQ); + while (!_ctrl_vq.has_used_buffers()); + } + + template + bool _exec_cmd(Control_header const &cmd, + CMD_DATA_TYPE const &cmd_data) + { + return _ctrl_vq.write_data_read_reply( + cmd, (char const *)&cmd_data, sizeof(CMD_DATA_TYPE), + [this]() { _flush_ctrl_vq(); }, + [](Control_header const &response) { + return response.type == Control_header::RESP_OK_NODATA; }); + } + + bool _update_display_info(bool use_current_scanout) + { + Control_header cmd { Control_header::CMD_GET_DISPLAY_INFO }; + + auto display_info_cb = [&](Display_info const &info) { + for (size_t i = 0; i < _num_scanouts; ++i) { + if (info.modes[i].enabled) { + if (use_current_scanout && (_selected_scanout_id != i)) + continue; + auto const &r = info.modes[i].rect; + _display_area = Capture::Area{ r.width, r.height }; + _selected_scanout_id = i; + return true; + } + } + return false; + }; + + if (!_ctrl_vq.write_data_read_reply( + cmd, [this] { _flush_ctrl_vq(); }, display_info_cb)) { + error("Failed to request display info!"); + return false; + } + + return true; + } + + void _init_display() { + if (!_update_display_info(false)) + throw Display_init_failed(); + _configure_display(); + } + + void _update_fb() + { + Control_header transfer_cmd { Control_header::CMD_RESOURCE_TRANSFER_TO_HOST }; + Transfer_to_host_2d transfer_data { + .rect = { + .x = 0, + .y = 0, + .width = _display_area.w(), + .height = _display_area.h(), + }, + .offset = 0, + }; + if (!_exec_cmd(transfer_cmd, transfer_data)) { + error("Failed to send transfer 2D resource to host command!"); + return; + } + + Control_header flush_cmd { Control_header::CMD_RESOURCE_FLUSH }; + Resource_flush flush_data { + .rect = { + .x = 0, + .y = 0, + .width = _display_area.w(), + .height = _display_area.h(), + }, + }; + if (!_exec_cmd(flush_cmd, flush_data)) { + error("Failed to send flush resource command!"); + return; + } + } + + public: + + Driver(Env &env, + Platform::Connection &platform, + Virtio::Device &device) + : _env(env), + _platform(platform), + _device(device), + _num_scanouts(_init_device(_device, _ctrl_vq.description())) + { + try { + _init_display(); + } catch (...) { + device.set_status(Virtio::Device::Status::RESET); + throw; + } + + _device.irq_sigh(_irq_handler); + _device.irq_ack(); + _capture_timer.sigh(_capture_timer_handler); + _capture_timer.trigger_periodic(10*1000); + } + + ~Driver() + { + _device.set_status(Virtio::Device::Status::RESET); + } +}; diff --git a/repos/os/src/drivers/framebuffer/virtio/mmio_device.cc b/repos/os/src/drivers/framebuffer/virtio/mmio_device.cc new file mode 100644 index 0000000000..0852d0635f --- /dev/null +++ b/repos/os/src/drivers/framebuffer/virtio/mmio_device.cc @@ -0,0 +1,42 @@ +/* + * \brief VirtIO MMIO Framebuffer driver + * \author Piotr Tworek + * \date 2020-02-14 + */ + +/* + * Copyright (C) 2020 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU Affero General Public License version 3. + */ + +#include +#include +#include + +#include "component.h" + +namespace Virtio_mmio_fb { + using namespace Genode; + struct Main; +} + +struct Virtio_mmio_fb::Main +{ + Genode::Env &env; + Platform::Connection platform { env }; + Platform::Device platform_device { platform, + Platform::Device::Type { "gpu" } }; + Virtio::Device virtio_device { platform_device }; + Virtio_fb::Driver driver { env, platform, virtio_device }; + + Main(Env &env) + try : env(env) { log("--- VirtIO MMIO Framebuffer driver started ---"); } + catch (...) { env.parent().exit(-1); } +}; + +void Component::construct(Genode::Env &env) +{ + static Virtio_mmio_fb::Main main(env); +} diff --git a/repos/os/src/drivers/framebuffer/virtio/spec/arm/target.mk b/repos/os/src/drivers/framebuffer/virtio/spec/arm/target.mk new file mode 100644 index 0000000000..deb31aef49 --- /dev/null +++ b/repos/os/src/drivers/framebuffer/virtio/spec/arm/target.mk @@ -0,0 +1,3 @@ +REQUIRES = arm + +include $(REP_DIR)/src/drivers/framebuffer/virtio/target_mmio.inc diff --git a/repos/os/src/drivers/framebuffer/virtio/spec/arm_64/target.mk b/repos/os/src/drivers/framebuffer/virtio/spec/arm_64/target.mk new file mode 100644 index 0000000000..488c889735 --- /dev/null +++ b/repos/os/src/drivers/framebuffer/virtio/spec/arm_64/target.mk @@ -0,0 +1,3 @@ +REQUIRES = arm_64 + +include $(REP_DIR)/src/drivers/framebuffer/virtio/target_mmio.inc diff --git a/repos/os/src/drivers/framebuffer/virtio/target_mmio.inc b/repos/os/src/drivers/framebuffer/virtio/target_mmio.inc new file mode 100644 index 0000000000..36c892b677 --- /dev/null +++ b/repos/os/src/drivers/framebuffer/virtio/target_mmio.inc @@ -0,0 +1,6 @@ +TARGET = virtio_mmio_fb_drv +SRC_CC = mmio_device.cc +LIBS = base blit +INC_DIR = $(REP_DIR)/src/drivers/framebuffer/virtio + +vpath % $(REP_DIR)/src/drivers/framebuffer/virtio