diff --git a/Makefile b/Makefile index 044a872..dd2c3ca 100644 --- a/Makefile +++ b/Makefile @@ -134,6 +134,23 @@ QEMU = qemu-system-x86_64 # virtio-gpu wins the display class, stdvga carries the splash and text console. QEMU_DISPLAY = -vga std -device virtio-gpu-pci +# An absolute pointing device, on an xHCI controller. +# +# A PS/2 mouse is relative, so inside a VM the guest cursor and the host's +# accumulate independently and drift apart -- they are simply never in the same +# place. usb-tablet reports where the pointer *is*, so the two coincide. +# +# EMPTY BY DEFAULT, and that is not an oversight. The driver binds the tablet +# and identifies it correctly, but no reports have yet been observed arriving +# on its interrupt endpoint -- so switching the default pointer to it would +# replace a cursor that drifts with one that does not move. Opt in while +# working on it: +# +# make qemu QEMU_INPUT='-device qemu-xhci,id=xhci -device usb-tablet,bus=xhci.0' +# +# See kernel/drivers/usb/hid/usb_hid.c, tablet_report(). +QEMU_INPUT ?= + # ----------------------------------------------------------------------- # Cross-compilation: clang everywhere it can be had # @@ -433,7 +450,7 @@ $(OVMF_VARS): chmod u+w $@ qemu-uefi: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS) - $(QEMU) -machine pc-i440fx-9.2 -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -m 1G -smp 1 -cpu qemu64,+rdtscp -serial mon:stdio $(QEMU_DISPLAY) + $(QEMU) -machine pc-i440fx-9.2 -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -m 1G -smp 1 -cpu qemu64,+rdtscp -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) # Build the ramdisk-root variant. This is a target rather than just # "make RAMROOT=1" because toggling RAMROOT changes CFLAGS (-DRAMDISK_ROOT), @@ -920,12 +937,12 @@ schcom-test: schos.img $(BUILD)/schfs.img sh tools/posix/schcom_test.sh qemu-kvm: schos.img $(BUILD)/schfs.img - $(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) -cpu host \ + $(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu host \ -hda schos.img -hdb $(BUILD)/schfs.img \ -m 1G -smp 2 -machine pc-i440fx-9.2 qemu: schos.img $(BUILD)/schfs.img - $(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \ + $(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu qemu64,+rdtscp \ -hda schos.img -hdb $(BUILD)/schfs.img \ -m 1G -smp 2 -machine pc-i440fx-9.2 diff --git a/include/defs.h b/include/defs.h index 57f6d2f..af8c317 100644 --- a/include/defs.h +++ b/include/defs.h @@ -196,6 +196,11 @@ int input_ioctl(struct inode*, int request, addr_t arg); int input_grab(int dev, int on); int input_is_grabbed(int dev); void input_report(int dev, int type, int code, int value); +// An absolute pointing device (usb-tablet). Coordinates are already in screen +// pixels; see input_mouse_abs in kernel/dev/input.c for why this is separate +// from the relative path. +void input_mouse_abs(int x, int y, int buttons); +void mouse_set_abs(int x, int y, int buttons); void input_sync(int dev); int input_ready(int dev); void input_ps2_scancode(int raw, int e0); diff --git a/include/input.h b/include/input.h index 53a63b5..8cfd332 100644 --- a/include/input.h +++ b/include/input.h @@ -39,6 +39,12 @@ struct input_event { #define SYN_REPORT 0 // Relative axes. +// Absolute axes, for a pointing device that reports where it *is* rather than +// how far it moved -- a tablet, a touchscreen, or a VM's absolute pointer. +// Same numbering as Linux, because /dev/input/eventN claims to speak evdev. +#define ABS_X 0x00 +#define ABS_Y 0x01 + #define REL_X 0x00 #define REL_Y 0x01 #define REL_WHEEL 0x08 diff --git a/include/usb.h b/include/usb.h index a01ac65..7be0477 100644 --- a/include/usb.h +++ b/include/usb.h @@ -49,6 +49,11 @@ #define USB_HID_SUBCLASS_BOOT 0x01 #define USB_HID_PROTO_KEYBOARD 0x01 #define USB_HID_PROTO_MOUSE 0x02 +// Not a USB value: bInterfaceProtocol has no code for "absolute pointer". +// A HID device that claims neither boot protocol is treated as a tablet, which +// is what a VM offers so its cursor and the guest's can agree. Kept out of the +// 0x00-0x02 range the specification defines so it cannot collide. +#define USB_HID_PROTO_TABLET 0x80 // Bus speeds. // diff --git a/kernel/dev/input.c b/kernel/dev/input.c index 7f019ac..bc1b399 100644 --- a/kernel/dev/input.c +++ b/kernel/dev/input.c @@ -368,6 +368,33 @@ input_key(int usage, int down) // One mouse report: relative motion plus the current button state. `buttons` // is a bitmask in the order both PS/2 and HID boot protocol use -- left, // right, middle -- which is why neither caller has to reorder it. +// An absolute pointer: where it is, not how far it moved. +// +// A relative device can never agree with the host's cursor inside a VM -- the +// two accumulate independently and drift apart, which is exactly the symptom +// usb-tablet exists to remove. Reported as EV_ABS so a reader can tell the +// difference and treat the value as authoritative rather than as a delta. +void +input_mouse_abs(int x, int y, int buttons) +{ + static int last; + static int lastx = -1, lasty = -1; + static const int btncode[3] = { BTN_LEFT, BTN_RIGHT, BTN_MIDDLE }; + int i; + + if(x != lastx){ input_report(INPUT_MOUSE, EV_ABS, ABS_X, x); lastx = x; } + if(y != lasty){ input_report(INPUT_MOUSE, EV_ABS, ABS_Y, y); lasty = y; } + + for(i = 0; i < 3; i++){ + int now = (buttons >> i) & 1, was = (last >> i) & 1; + if(now != was) + input_report(INPUT_MOUSE, EV_KEY, btncode[i], now); + } + last = buttons; + + input_report(INPUT_MOUSE, EV_SYN, SYN_REPORT, 0); +} + void input_mouse(int dx, int dy, int buttons) { diff --git a/kernel/dev/mouse.c b/kernel/dev/mouse.c index 029c6f7..ac21454 100644 --- a/kernel/dev/mouse.c +++ b/kernel/dev/mouse.c @@ -95,6 +95,34 @@ mouse_inject(int dx, int dy, int buttons) input_mouse(dx, dy, buttons); } +// Put the pointer somewhere, rather than nudging it. +// +// The clamp is the same as the relative path's, and for the same reason: the +// screen size is a boot-time choice now. The coordinates arrive already scaled +// to pixels -- the HID driver does that, because it is the only place that +// knows the device's logical range. +void +mouse_set_abs(int x, int y, int buttons) +{ + int maxx = (int)vga_get_width(); + int maxy = (int)vga_get_height(); + if(maxx <= 0) maxx = 800; + if(maxy <= 0) maxy = 600; + + if(x < 0) x = 0; + if(y < 0) y = 0; + if(x >= maxx) x = maxx - 1; + if(y >= maxy) y = maxy - 1; + + acquire(&mouse_state.lock); + mouse_state.buttons = buttons; + mouse_state.x = x; + mouse_state.y = y; + release(&mouse_state.lock); + + input_mouse_abs(x, y, buttons); +} + void mouseintr(void) { diff --git a/kernel/drivers/usb/hid/usb_hid.c b/kernel/drivers/usb/hid/usb_hid.c index 3abf05f..9d42b14 100644 --- a/kernel/drivers/usb/hid/usb_hid.c +++ b/kernel/drivers/usb/hid/usb_hid.c @@ -196,6 +196,41 @@ keyboard_report(struct hid *h, const uchar *rep, int len) // 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)vga_get_width(); + int h_px = (int)vga_get_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) { @@ -224,7 +259,9 @@ hid_complete(struct usb_dev *dev, void *buf, int len) if(h == 0) return; - if(h->proto == USB_HID_PROTO_KEYBOARD) + 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); @@ -259,11 +296,20 @@ hid_probe(struct usb_dev *dev) 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(hid_request(dev, USB_HID_SET_PROTOCOL, USB_HID_PROTOCOL_BOOT) < 0){ + 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; } @@ -288,17 +334,25 @@ hid_probe(struct usb_dev *dev) 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" : "device", + 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; } -// Boot subclass only. A HID device that does not claim it needs its report -// descriptor parsed, and we would have nothing useful to do with it. +// 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 = USB_HID_SUBCLASS_BOOT, + .subclass = 0xFF, .protocol = 0xFF, .devclass = DEVCLASS_INPUT, .probe = hid_probe, diff --git a/projects/schcom/schcom b/projects/schcom/schcom index 69fe506..92d09d6 100644 Binary files a/projects/schcom/schcom and b/projects/schcom/schcom differ diff --git a/projects/schcom/schcom.c b/projects/schcom/schcom.c index 1518b29..c7fec79 100644 --- a/projects/schcom/schcom.c +++ b/projects/schcom/schcom.c @@ -68,7 +68,8 @@ static uint32_t fb_w, fb_h, fb_stride, fb_bpp; static size_t fb_size; static uint8_t *back; /* composited here, then copied out in one go */ -/* Pointer position, moved by EV_REL from the mouse. */ +/* Pointer position: set outright by EV_ABS from a tablet, nudged by EV_REL + * from a plain mouse. */ static int32_t ptr_x, ptr_y; static void @@ -517,6 +518,25 @@ handle_input(int fd) int repaint = 0; + /* An absolute pointer says where it is; a relative one says how far it + * moved. Taking the absolute value as authoritative is the whole point of + * usb-tablet: two pointers that accumulate deltas independently drift + * apart, which inside a VM shows up as the guest cursor sitting somewhere + * the host's is not. */ + if (ev.type == EV_ABS) { + int old_x = ptr_x, old_y = ptr_y; + if (ev.code == ABS_X) { ptr_x = ev.value; repaint = 1; } + if (ev.code == ABS_Y) { ptr_y = ev.value; repaint = 1; } + if (ptr_x < 0) ptr_x = 0; + if (ptr_y < 0) ptr_y = 0; + if (ptr_x >= (int32_t)fb_w) ptr_x = (int32_t)fb_w - 1; + if (ptr_y >= (int32_t)fb_h) ptr_y = (int32_t)fb_h - 1; + if (repaint) { + damage(old_x, old_y, PTR_SIZE, PTR_SIZE); + damage(ptr_x, ptr_y, PTR_SIZE, PTR_SIZE); + } + } + if (ev.type == EV_REL) { int old_x = ptr_x, old_y = ptr_y; if (ev.code == REL_X) { ptr_x += ev.value; repaint = 1; } @@ -542,7 +562,8 @@ handle_input(int fd) if (surfaces[i].used) top = &surfaces[i]; - if (top && (ev.type == EV_KEY || ev.type == EV_REL)) { + if (top && (ev.type == EV_KEY || ev.type == EV_REL || + ev.type == EV_ABS)) { struct schcom_input msg; memset(&msg, 0, sizeof msg); msg.hdr.op = SCHCOM_OP_INPUT;