2021-12-17 15:04:45 +01:00

459 lines
12 KiB
C++

/*
* \brief VirtIO based input driver
* \author Piotr Tworek
* \date 2020-02-01
*/
/*
* 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.
*/
/* Genode */
#include <base/attached_rom_dataspace.h>
#include <base/component.h>
#include <base/env.h>
#include <base/log.h>
#include <event_session/connection.h>
#include <input/keycodes.h>
#include <util/register.h>
#include <virtio/queue.h>
namespace Virtio_input {
using namespace Genode;
struct Main;
class Driver;
}
class Virtio_input::Driver
{
public:
struct Device_not_found : Exception { };
struct Device_init_failed : Exception { };
struct Queue_init_failed : Exception { };
private:
enum { VENDOR_QEMU = 0x0627 };
enum Product {
Any = 0x0,
Keyboard = 0x1,
Mouse = 0x2,
Tablet = 0x3,
};
enum Config : uint8_t {
SelectID = 0x00,
SelectSubID = 0x01,
Data_size = 0x02,
Data = 0x08,
};
enum Config_id : uint8_t {
Name = 0x01,
Serial = 0x02,
Device_id = 0x03,
Prop_bits = 0x10,
Event_bits = 0x11,
Abs_info = 0x12,
};
struct Device_id {
uint16_t bus_type = 0;
uint16_t vendor = 0;
uint16_t product = 0;
uint16_t version = 0;
};
struct Features : Register<64> { struct VERSION_1 : Bitfield<32, 1> { }; };
enum Queue_id : uint16_t { EVENTS_VQ = 0, STATUS_VQ = 1 };
struct Abs_config {
struct {
uint32_t min;
uint32_t max;
} x, y;
uint32_t width;
uint32_t height;
};
struct Event {
enum Type {
Syn = 0x00,
Key = 0x01,
Rel = 0x02,
Abs = 0x03,
Rep = 0x14,
};
enum Code {
Rel_x = 0x00,
Rel_y = 0x01,
Rel_wheel = 0x08,
Abs_x = 0x00,
Abs_y = 0x01,
};
uint16_t type;
uint16_t code;
uint32_t value;
bool operator == (Event const &other) const {
return other.type == this->type &&
other.code == this->code &&
other.value == this->value;
}
};
struct Events_queue_traits {
static const bool device_write_only = true;
static const bool has_data_payload = false;
};
struct Status_queue_traits {
static const bool device_write_only = false;
static const bool has_data_payload = false;
};
enum { QUEUE_SIZE = 64, QUEUE_ELM_SIZE = sizeof(Event) };
typedef Virtio::Queue<Event, Events_queue_traits> Events_virtqueue;
typedef Virtio::Queue<Event, Status_queue_traits> Status_virtqueue;
Driver(Driver const &);
Driver &operator = (Driver const &);
Env &_env;
::Event::Connection _event_session { _env };
Virtio::Device &_device;
Event _last_sent_key_event { 0, 0, 0 };
Input::Relative_motion _rel_motion { 0, 0 };
Input::Absolute_motion _abs_motion { -1, -1 };
Abs_config _abs_config { { 0, 0 }, { 0, 0 }, 0, 0 };
Signal_handler<Driver> _irq_handler {_env.ep(), *this, &Driver::_handle_irq};
Events_virtqueue _events_vq { _env.ram(), _env.rm(),
QUEUE_SIZE, QUEUE_ELM_SIZE };
Status_virtqueue _status_vq { _env.ram(), _env.rm(),
QUEUE_SIZE, QUEUE_ELM_SIZE };
void _handle_event(::Event::Session_client::Batch &batch, const Event &evt)
{
switch (evt.type) {
case Event::Type::Syn:
{
if (_rel_motion.x != 0 || _rel_motion.y != 0) {
batch.submit(_rel_motion);
_rel_motion = Input::Relative_motion{0, 0};
}
if (_abs_motion.x >= 0 || _abs_motion.y >= 0) {
batch.submit(_abs_motion);
_abs_motion = Input::Absolute_motion{-1, -1};
}
break;
}
case Event::Type::Rel:
{
switch (evt.code) {
case Event::Code::Rel_x: _rel_motion.x = evt.value; break;
case Event::Code::Rel_y: _rel_motion.y = evt.value; break;
case Event::Code::Rel_wheel:
batch.submit(Input::Wheel{0, (int)evt.value});
break;
default:
warning("Unhandled relative event code: ", Hex(evt.code));
break;
}
break;
}
case Event::Type::Key:
{
/* filter out auto-repeat keypress events */
if (_last_sent_key_event == evt)
break;
/* Genode keyboard event codes mirror linux evdev ones */
Input::Keycode keycode = static_cast<Input::Keycode>(evt.code);
/*
* Some key events apparently don't send both press and
* release values. Fake both press and release to make
* nitpicker happy.
*/
if ((keycode == Input::BTN_GEAR_UP ||
keycode == Input::BTN_GEAR_DOWN) && !evt.value)
batch.submit(Input::Press{keycode});
switch (evt.value) {
case 0: batch.submit(Input::Release{keycode}); break;
case 1: batch.submit(Input::Press{keycode}); break;
default:
warning("Unhandled key event value: ", evt.value);
break;
}
_last_sent_key_event = evt;
break;
}
case Event::Type::Abs:
{
switch (evt.code) {
case Event::Code::Abs_x:
_abs_motion.x = (_abs_config.width * evt.value / _abs_config.x.max);
_abs_motion.y = max(0, _abs_motion.y);
break;
case Event::Code::Abs_y:
_abs_motion.x = max(0, _abs_motion.x);
_abs_motion.y = (_abs_config.height * evt.value / _abs_config.y.max);
break;
default:
warning("Unhandled absolute event code: ", Hex(evt.code));
break;
}
break;
}
default:
warning("Unhandled event type: ", Hex(evt.type));
break;
}
}
static Product _match_product(Xml_node const &config)
{
auto product_string = config.attribute_value("match_product", String<10>("any"));
if (product_string == "keyboard") {
return Product::Keyboard;
} else if (product_string == "mouse") {
return Product::Mouse;
} else if (product_string == "tablet") {
return Product::Tablet;
} else if (product_string == "any") {
return Product::Any;
}
error("Invalid product name: ", product_string);
throw Device_init_failed();
}
static size_t _cfg_select(Virtio::Device &device, Config_id sel, uint8_t subsel)
{
device.write_config(Config::SelectID, Virtio::Device::ACCESS_8BIT, sel);
device.write_config(Config::SelectSubID, Virtio::Device::ACCESS_8BIT, subsel);
return device.read_config(Config::Data_size, Virtio::Device::ACCESS_8BIT);
}
static Abs_config _read_abs_config(Virtio::Device &device,
Xml_node const &config)
{
Abs_config cfg { {0, ~0U}, {0, ~0U}, 0, 0};
auto size = _cfg_select(device, Config_id::Abs_info, Event::Code::Abs_x);
if (size >= sizeof(cfg.x)) {
cfg.x.min = device.read_config(Config::Data, Virtio::Device::ACCESS_32BIT);
cfg.x.max = device.read_config(Config::Data + 4, Virtio::Device::ACCESS_32BIT);
}
size = _cfg_select(device, Config_id::Abs_info, Event::Code::Abs_y);
if (size >= sizeof(cfg.y)) {
cfg.y.min = device.read_config(Config::Data, Virtio::Device::ACCESS_32BIT);
cfg.y.max = device.read_config(Config::Data + 4, Virtio::Device::ACCESS_32BIT);
}
cfg.width = config.attribute_value("width", cfg.x.max);
cfg.height = config.attribute_value("height", cfg.y.max);
return cfg;
}
static struct Device_id _read_device_id(Virtio::Device &device)
{
struct Device_id device_id;
auto size = _cfg_select(device, Config_id::Device_id, 0);
if (size != sizeof(device_id)) {
error("Invalid VirtIO input device ID size!");
throw Device_init_failed();
}
device_id.bus_type = (uint16_t)device.read_config(Config::Data + 0, Virtio::Device::ACCESS_16BIT);
device_id.vendor = (uint16_t)device.read_config(Config::Data + 2, Virtio::Device::ACCESS_16BIT);
device_id.product = (uint16_t)device.read_config(Config::Data + 4, Virtio::Device::ACCESS_16BIT);
device_id.version = (uint16_t)device.read_config(Config::Data + 6, Virtio::Device::ACCESS_16BIT);
return device_id;
}
template <size_t SZ>
static String<SZ> _read_device_name(Virtio::Device &device)
{
auto size = _cfg_select(device, Config_id::Name, 0);
size = min(size, SZ);
char buf[SZ];
memset(buf, 0, sizeof(buf));
for (unsigned i = 0; i < size; ++i)
buf[i] = (uint8_t)device.read_config((uint8_t)(Config::Data + i), Virtio::Device::ACCESS_8BIT);
return String<SZ>(buf);
}
static bool _probe_device(Virtio::Device &device, Product match_product)
{
using Status = Virtio::Device::Status;
if (!device.set_status(Status::RESET)) {
warning("Failed to reset the device!");
return false;
}
if (!device.set_status(Status::ACKNOWLEDGE)) {
warning("Failed to acknowledge the device!");
return false;
}
const auto dev_id = _read_device_id(device);
if (dev_id.vendor != VENDOR_QEMU) {
warning("Unsupported VirtIO input device vendor: ", Hex(dev_id.vendor));
}
if (match_product != Product::Any && match_product != dev_id.product)
return false;
return true;
}
static bool _init_features(Virtio::Device &device)
{
using Status = Virtio::Device::Status;
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;
if (!Features::VERSION_1::get(device_features)) {
warning("Unsupprted VirtIO device version!");
return false;
}
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!");
return false;
}
return true;
}
void _init_driver(Xml_node const &config)
{
using Status = Virtio::Device::Status;
const auto product = _match_product(config);
if (_probe_device(_device, product) && _init_features(_device)) {
if (!_device.set_status(Status::DRIVER)) {
_device.set_status(Status::FAILED);
warning("Device initialization failed!");
throw Device_init_failed();
}
return;
} else if (!_device.set_status(Status::RESET)) {
warning("Failed to reset the device!");
}
warning("No suitable VirtIO input device found!");
throw Device_not_found();
}
void _setup_virtio_queues()
{
if (!_device.configure_queue(EVENTS_VQ, _events_vq.description())) {
error("Failed to initialize events VirtIO queue!");
throw Queue_init_failed();
}
if (!_device.configure_queue(STATUS_VQ, _status_vq.description())) {
error("Failed to initialize status 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();
}
}
void _handle_irq()
{
enum { IRQ_USED_RING_UPDATE = 1, IRQ_CONFIG_CHANGE = 2 };
const uint32_t reasons = _device.read_isr();
if (reasons & IRQ_USED_RING_UPDATE) {
_event_session.with_batch([&] (::Event::Session_client::Batch &batch) {
while (_events_vq.has_used_buffers())
_handle_event(batch, _events_vq.read_data());
});
/* Reclaim all buffers processed by the device. */
if (_status_vq.has_used_buffers())
_status_vq.ack_all_transfers();
}
_device.irq_ack();
}
public:
Driver(Env &env,
Virtio::Device &device,
Xml_node const &config)
:
_env(env), _device(device)
{
_init_driver(config);
_abs_config = _read_abs_config(_device, config);
_setup_virtio_queues();
_device.irq_sigh(_irq_handler);
_device.irq_ack();
log("Using \"", _read_device_name<32>(_device), "\" device.");
}
};