os: Add VirtIO framebuffer driver.

This implements the necessary bits to provide 2D framebuffer support on
top of VirtIO GPU device as implemented in Qemu. I don't know if any
other implementation of this specific device exists.

Compared to the ramfb driver which already exists in Genode Virtio FB driver
has one major benefit. It allows Qemu window to be dynamically resized at
runtime. The driver will treat this as resolution change and act accordingly.
Ramfb driver can currently only use the hardcoded 1024x768 screen size. Changing
screen resolution might not sound like a big deal, but it is rather useful to
run Genode on Qemu in full screen mode.
This commit is contained in:
Piotr Tworek 2021-01-13 09:41:30 +01:00 committed by Christian Helmuth
parent 2ec9e69fd4
commit 7c1888644a
8 changed files with 627 additions and 0 deletions

View File

@ -0,0 +1,2 @@
SRC_DIR = src/drivers/framebuffer/virtio
include $(GENODE_DIR)/repos/base/recipes/src/content.inc

View File

@ -0,0 +1 @@
2021-09-21 505752c4facfcd13ceb75f90b7c9f43209586513

View File

@ -0,0 +1,7 @@
base
blit
capture_session
event_session
os
platform_session
virtio

View File

@ -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 <base/component.h>
#include <base/signal.h>
#include <capture_session/connection.h>
#include <irq_session/client.h>
#include <timer_session/connection.h>
#include <util/register.h>
#include <virtio/queue.h>
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_header, Control_queue_traits> 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<Capture::Pixel>(); }
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<Ram_dataspace>(_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<Driver> _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<Capture::Connection::Screen> _captured_screen { };
Timer::Connection _capture_timer { _env };
Constructible<Fb_memory_resource> _fb_res { };
Signal_handler<Driver> _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<uint32_t>(_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<Pixel> 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 <typename CMD_DATA_TYPE>
bool _exec_cmd(Control_header const &cmd,
CMD_DATA_TYPE const &cmd_data)
{
return _ctrl_vq.write_data_read_reply<Control_header>(
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<Display_info>(
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);
}
};

View File

@ -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 <base/component.h>
#include <platform_session/connection.h>
#include <virtio/mmio_device.h>
#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);
}

View File

@ -0,0 +1,3 @@
REQUIRES = arm
include $(REP_DIR)/src/drivers/framebuffer/virtio/target_mmio.inc

View File

@ -0,0 +1,3 @@
REQUIRES = arm_64
include $(REP_DIR)/src/drivers/framebuffer/virtio/target_mmio.inc

View File

@ -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