SchOS/kernel/drivers/usb/hid/usb_hid.c
Nexoniarz 817d666068 gfx: the console and the crash screen stop drawing on a driver by name
Same disease as the network stack, one subsystem over. kernel/dev/vga.c
was three things wearing one name: the owner of the framebuffer, a
bitmap drawing library, and the text console. Only the first is a
driver, and because they shared a file they shared a prefix -- so
panic.c drew the crash screen with vga_fill_rect, console.c printed with
vga_putc, /dev/fb0 handed out vga_fb_phys, and every one of those files
named a display driver it had no business knowing about.

Split three ways:

  - kernel/drivers/gpu/vga/vga_fb.c owns the VBE framebuffer -- the mode
    the loader programmed and handed over, the aperture, and the display
    class vtable. It is a driver and now lives with the driver that
    finds the card on the bus.
  - kernel/dev/gfx.c draws: pixels, shapes, glyphs, the cursor, scroll,
    the ANSI erase operations. It names no driver. The framebuffer
    arrives through gfx_attach_scanout(), so a driver publishes what it
    owns and this file draws on it without knowing what it is.
  - include/gfx.h is the interface, and says why it is deliberately not
    the display class: that one is about frames -- acquire, present,
    vblank -- and the crash screen has to work when nothing else does.

main() no longer calls a display driver. platform_register_devices()
takes the framebuffer address and brings up whatever owns it, which is
where driver names belong; kernel/drivers/registry.c is the file whose
entire job is naming every driver in the kernel.

virtio-gpu got the e1000 treatment as well: instances in an array, the
instance hung on dev->priv by an `attached` hook, and every op reading
it rather than a file-static. It refused the second adapter outright
before, which is not hypothetical -- integrated plus discrete is an
ordinary laptop. The VGA driver deliberately has no such hook, and says
so: there is one framebuffer the loader programmed however many
VGA-class controllers the bus turns up, and a second one publishing the
same scanout is the truth rather than a bug.

Private headers moved next to their drivers: include/virtio.h and
include/xhci.h each had exactly one includer. include/ is for headers
the rest of the kernel needs.

Deleted rather than moved: consoleinit() programmed the sixteen ANSI
colours into the VGA DAC and disabled text-mode blink, by writing ports
0x3C0-0x3C9 from the console. The comment three lines above the colour
table already said this does nothing in a 32bpp linear mode -- there is
no palette to index -- so it was both dead and in the wrong file.

Verified, and this is a refactor so the evidence is that nothing moved:
  - bootcheck -Fb -Net: 59 [ok], no failures. That covers the console
    through every command, /dev/fb0's geometry, writing pixels through
    write() and mapping the framebuffer through mmap.
  - make crashshot: the crash screen renders exactly as before --
    shapes, glyphs, logo and QR code -- which is the whole of gfx.c
    exercised at once, in a panic, with the scanout attached by a driver.
  - make schcom-test: five pixel assertions from the host oracle still
    pass, so the display class and the compositor above it are unmoved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:04:10 +02:00

360 lines
11 KiB
C

// USB HID, boot protocol: keyboards and mice.
//
// Boot protocol is the reason a USB keyboard works in a BIOS setup screen
// before any operating system exists. A HID device normally describes its
// reports with a report descriptor -- a small stack language that has to be
// parsed to know which bit means which key -- but a device claiming the boot
// subclass also promises to support one fixed layout on request. Eight bytes
// for a keyboard, three for a mouse, defined in the HID specification and
// identical on every device.
//
// That is worth taking. A full report-descriptor parser is a real piece of
// work and buys nothing for a keyboard; it is what a game controller or a
// touchscreen will need, and it can arrive with them.
#include "types.h"
#include "defs.h"
#include "gfx.h"
#include "param.h"
#include "spinlock.h"
#include "dma.h"
#include "usb.h"
#include "input.h"
#define NHID 4
struct hid {
int in_use;
int proto; // USB_HID_PROTO_KEYBOARD or _MOUSE
uchar prev[8]; // the previous report, for edge detection
};
static struct hid hids[NHID];
// ---------------------------------------------------------------------------
// Keyboard
//
// The report is: modifiers, a reserved byte, then up to six usage IDs of keys
// currently held. It is a *state*, not an event -- a key that stays down
// appears in every report until it is released, so a character is emitted
// only for usages that were not in the previous report. Sending one per
// report instead would repeat every held key a few hundred times a second.
// ---------------------------------------------------------------------------
#define HID_MOD_CTRL 0x11 // left | right
#define HID_MOD_SHIFT 0x22
#define HID_MOD_ALT 0x44
// Usage IDs 0x00-0x38 of the Keyboard/Keypad page. 0 means "nothing to type".
static const uchar hid_unshifted[0x39] = {
0, 0, 0, 0,
'a','b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z',
'1','2','3','4','5','6','7','8','9','0',
'\n', 0x1B, '\b', '\t', ' ',
'-','=','[',']','\\','#',';','\'','`',',','.','/',
};
static const uchar hid_shifted[0x39] = {
0, 0, 0, 0,
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'!','@','#','$','%','^','&','*','(',')',
'\n', 0x1B, '\b', '\t', ' ',
'_','+','{','}','|','~',':','"','~','<','>','?',
};
// Characters decoded from a report, waiting to be handed to the console.
//
// The console pulls input through a getc function rather than being pushed
// to (consoleintr takes one), which is exactly the shape the PS/2 driver
// needs and exactly the wrong shape here -- a USB report arrives complete,
// with up to six keys in it. So the report is decoded into this queue and
// the console then drains it through the same path as any other keyboard,
// which means the entire line discipline is reused rather than reimplemented.
#define KQSIZE 64
static struct {
uchar buf[KQSIZE];
uint r, w;
} kq;
static void
kq_put(int c)
{
if(kq.w - kq.r < KQSIZE)
kq.buf[kq.w++ % KQSIZE] = c;
}
static int
usb_kbd_getc(void)
{
if(kq.r == kq.w)
return -1;
return kq.buf[kq.r++ % KQSIZE];
}
// Was this usage in that report?
static int
was_down(const uchar *prev, uchar usage)
{
int i;
for(i = 2; i < 8; i++)
if(prev[i] == usage)
return 1;
return 0;
}
// Turn the difference between two reports into press and release events for
// /dev/input/event0.
//
// The console path below cannot do this job: it emits a character per new key
// and nothing at all for a release, because a character stream has no way to
// say "released". A program that wants to know a key is being *held* -- which
// is every game and every compositor -- needs exactly the events this makes.
static void
keyboard_events(const uchar *prev, const uchar *rep)
{
int i, bit, n = 0;
for(bit = 0; bit < 8; bit++){
int now = (rep[0] >> bit) & 1;
int then = (prev[0] >> bit) & 1;
if(now != then){
input_report(INPUT_KBD, EV_KEY, input_hid_keycode(0xE0 + bit), now);
n++;
}
}
for(i = 2; i < 8; i++){
uchar u = rep[i];
if(u && !was_down(prev, u)){
int k = input_hid_keycode(u);
if(k){ input_report(INPUT_KBD, EV_KEY, k, 1); n++; }
}
}
for(i = 2; i < 8; i++){
uchar u = prev[i];
if(u && !was_down(rep, u)){
int k = input_hid_keycode(u);
if(k){ input_report(INPUT_KBD, EV_KEY, k, 0); n++; }
}
}
if(n)
input_sync(INPUT_KBD);
}
static void
keyboard_report(struct hid *h, const uchar *rep, int len)
{
uchar mods = rep[0];
int i, c;
if(len < 8)
return;
keyboard_events(h->prev, rep);
// Grabbed: the events above still went to /dev/input/event0, but nothing
// reaches the console. Note prev[] is updated before returning, or the next
// report would look like every held key had just been pressed again.
if(input_is_grabbed(INPUT_KBD)){
memmove(h->prev, rep, 8);
return;
}
for(i = 2; i < 8; i++){
uchar usage = rep[i];
if(usage == 0 || usage >= 0x39)
continue;
if(was_down(h->prev, usage))
continue; // still held from last time, not a new press
c = (mods & HID_MOD_SHIFT) ? hid_shifted[usage] : hid_unshifted[usage];
if(c == 0)
continue;
// Ctrl folds a letter down into its control character, which is how ^C
// and ^D reach the line discipline at all.
if(mods & HID_MOD_CTRL){
if(c >= 'a' && c <= 'z')
c = c - 'a' + 1;
else if(c >= 'A' && c <= 'Z')
c = c - 'A' + 1;
else
continue;
}
kq_put(c);
}
memmove(h->prev, rep, 8);
consoleintr(usb_kbd_getc);
}
// ---------------------------------------------------------------------------
// Mouse
// ---------------------------------------------------------------------------
// QEMU's usb-tablet, and absolute pointers generally.
//
// Report: one byte of buttons, then X and Y as 16-bit little-endian values
// over the device's logical range, then a wheel byte. The range is 0..0x7fff
// and is *not* read from the report descriptor -- parsing one is a real piece
// of work, and this decodes exactly the fixed layout the tablet declares.
// A device whose report is a different length is left alone rather than
// guessed at.
//
// Scaling happens here because this is the only place that knows the logical
// range; everything above deals in pixels.
#define TABLET_LOGICAL_MAX 0x7fff
static void
tablet_report(struct hid *h, const uchar *rep, int len)
{
(void)h;
if(len < 5)
return;
int rawx = rep[1] | (rep[2] << 8);
int rawy = rep[3] | (rep[4] << 8);
int w = (int)gfx_width();
int h_px = (int)gfx_height();
if(w <= 0) w = 800;
if(h_px <= 0) h_px = 600;
// Scale in 64-bit-free arithmetic: 0x7fff * 4096 still fits in an int.
int x = (int)(((long long)rawx * (w - 1)) / TABLET_LOGICAL_MAX);
int y = (int)(((long long)rawy * (h_px - 1)) / TABLET_LOGICAL_MAX);
mouse_set_abs(x, y, rep[0] & 0x07);
}
static void
mouse_report(struct hid *h, const uchar *rep, int len)
{
int dx, dy;
(void)h;
if(len < 3)
return;
// The deltas are signed bytes. Reading them as unsigned is the classic way
// to get a pointer that only travels right and down.
dx = (signed char)rep[1];
dy = (signed char)rep[2];
// HID's Y axis points down, and so does the screen's. The PS/2 protocol's
// points up, which is why mouseintr subtracts where this adds.
mouse_inject(dx, dy, rep[0] & 0x07);
}
// ---------------------------------------------------------------------------
static void
hid_complete(struct usb_dev *dev, void *buf, int len)
{
struct hid *h = dev->drvdata;
if(h == 0)
return;
if(h->proto == USB_HID_PROTO_TABLET)
tablet_report(h, buf, len);
else if(h->proto == USB_HID_PROTO_KEYBOARD)
keyboard_report(h, buf, len);
else
mouse_report(h, buf, len);
}
static int
hid_request(struct usb_dev *dev, int request, int value)
{
struct usb_setup s;
s.bmRequestType = USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
s.bRequest = request;
s.wValue = value;
s.wIndex = dev->iface.bInterfaceNumber;
s.wLength = 0;
return dev->ops->control(dev, &s, 0, 0);
}
static int
hid_probe(struct usb_dev *dev)
{
struct hid *h = 0;
int i, proto = dev->iface.bInterfaceProtocol;
if(!dev->has_intr_in){
cprintf("usb-hid: no interrupt IN endpoint; nothing to read\n");
return -1;
}
for(i = 0; i < NHID; i++)
if(!hids[i].in_use){ h = &hids[i]; break; }
if(h == 0)
return -1;
// A HID device that claims no boot protocol is taken to be an absolute
// pointer -- in practice a tablet, which is what a VM offers so that its
// cursor and the host's can agree. It must not be asked for boot protocol:
// it has none, the request fails, and refusing on that basis is what kept
// usb-tablet from binding at all.
if(proto != USB_HID_PROTO_KEYBOARD && proto != USB_HID_PROTO_MOUSE)
proto = USB_HID_PROTO_TABLET;
// Ask for the fixed layout explicitly. A device that supports the boot
// subclass still starts in report protocol, so without this the reports
// arrive in whatever shape its report descriptor declares -- which we have
// not read and cannot interpret.
if(proto != USB_HID_PROTO_TABLET &&
hid_request(dev, USB_HID_SET_PROTOCOL, USB_HID_PROTOCOL_BOOT) < 0){
cprintf("usb-hid: SET_PROTOCOL(boot) refused\n");
return -1;
}
// Idle rate 0 = "report only when something changes". The default is to
// resend the current state every few milliseconds forever, which for a
// keyboard means a constant stream of interrupts saying nothing.
hid_request(dev, USB_HID_SET_IDLE, 0);
if(dev->ops->open_ep(dev, &dev->intr_in) < 0)
return -1;
h->in_use = 1;
h->proto = proto;
dev->drvdata = h;
dev->complete = hid_complete;
// Arm the first transfer. Until a TRB is waiting on the endpoint the device
// has nowhere to put a report, so nothing would ever arrive.
if(dev->ops->queue_in(dev, dev->intr_in.bEndpointAddress, 0, 0) < 0)
return -1;
cprintf("usb-hid: %s on slot %d, endpoint 0x%x every %d ms\n",
proto == USB_HID_PROTO_KEYBOARD ? "keyboard" :
proto == USB_HID_PROTO_MOUSE ? "mouse" :
proto == USB_HID_PROTO_TABLET ? "tablet (absolute)" : "device",
dev->slot, dev->intr_in.bEndpointAddress, dev->intr_in.bInterval);
return 0;
}
// Any HID interface, not just the boot subclass.
//
// It was boot-only, on the reasoning that a device without boot protocol needs
// its report descriptor parsed and there would be nothing useful to do with it.
// That is right in general and wrong for the one device that matters here:
// usb-tablet declares subclass 0, so it was refused at the door -- and its
// report layout is fixed and known, so there is something useful to do with it
// after all. hid_probe decides what a given interface actually is; anything it
// cannot make sense of is still declined there.
const struct usb_driver usb_hid_driver = {
.name = "usb-hid",
.class = USB_CLASS_HID,
.subclass = 0xFF,
.protocol = 0xFF,
.devclass = DEVCLASS_INPUT,
.probe = hid_probe,
};