Paint with the mouse
Hold the left button and move the pointer over the Screen to paint. Turn the wheel to change colour, shown in the square at the top-left. Thirty seconds without a pointer event ends the program.
This combines the framebuffer drawing from the earlier examples with the input-event loop from keyboard control. Three pieces are new:
- Find the Screen pointer by asking input devices whether they report absolute positions.
- Convert those positions into pixel coordinates, then keep the whole brush on the Screen.
- Wait for input with
poll, read the queued events, and paint when a complete report arrives.
In the listing, find_tablet finds the device, to_pixel converts one coordinate, and paint
positions the brush. wait_for_events and read_event form the input loop. The framebuffer setup
and fill_rect serve the same roles as before.
; @screen width=320 height=240
default rel
%include "sys/syscall.inc"
%include "fcntl.inc"
%include "sys/mman.inc"
%include "poll.inc"
%include "linux/fb.inc"
%include "linux/input.inc"
BRUSH equ 6 ; the brush's width and height
QUIET equ 30000 ; ms without an event before the end
PAPER equ 0x00f4f0e6
section .rodata
fb_path: db "/dev/fb0", 0
colours: dd 0x00202020, 0x00e04040, 0x00f0a020, 0x0040b040, 0x002080e0, 0x008040c0
COLOURS equ ($ - colours) / 4 ; how many there are: 6
section .data
event_path: db "/dev/input/event0", 0
colour: dd 1 ; the colour the wheel chose, 0 to COLOURS - 1
x: dd 0 ; the pointer, in the tablet's units
y: dd 0
pressed: db 0 ; 1 while the left button is down
section .bss
var: resb fb_var_screeninfo_size
fix: resb fb_fix_screeninfo_size
types: resb 4 ; one bit for each type of event a device sends
xinfo: resb input_absinfo_size ; the range of each axis
yinfo: resb input_absinfo_size
event: resb input_event_size
waiter: resb pollfd_size
section .text
global _start
; fill_rect: a rectangle of colour r8d, edx pixels wide and ecx lines high, whose
; top-left corner is at column edi of line esi.
; Reads rbx (the top-left pixel) and r13 (bytes a line); changes rax, rcx, rsi,
; rdi, r9 and r10.
fill_rect:
test edx, edx
jle .done
test ecx, ecx
jle .done
mov r9d, ecx
imul rsi, r13
lea r10, [rbx + rsi]
lea r10, [r10 + rdi*4]
mov eax, r8d
.line:
mov rdi, r10
mov ecx, edx
rep stosd
add r10, r13
dec r9d
jnz .line
.done:
ret
; to_pixel: the pixel of the position eax on an axis of edx pixels whose range
; ends at ecx: (eax × edx + ecx - 1) ÷ ecx, into eax. Changes edx.
to_pixel:
imul eax, edx
lea eax, [rax + rcx - 1]
xor edx, edx
div ecx
ret
; paint: a BRUSH by BRUSH square of the chosen colour around the pointer, kept on
; the Screen. Changes the registers fill_rect changes, and rdx and r8.
paint:
mov eax, [x]
mov edx, [var + fb_var_screeninfo.xres]
mov ecx, [xinfo + input_absinfo.maximum]
call to_pixel
sub eax, BRUSH / 2 ; the brush's left edge
jns .left_inside
xor eax, eax
.left_inside:
mov ecx, [var + fb_var_screeninfo.xres]
sub ecx, BRUSH
cmp eax, ecx
jle .right_inside
mov eax, ecx
.right_inside:
mov edi, eax
mov eax, [y] ; the same down the Screen
mov edx, [var + fb_var_screeninfo.yres]
mov ecx, [yinfo + input_absinfo.maximum]
call to_pixel
sub eax, BRUSH / 2
jns .top_inside
xor eax, eax
.top_inside:
mov ecx, [var + fb_var_screeninfo.yres]
sub ecx, BRUSH
cmp eax, ecx
jle .bottom_inside
mov eax, ecx
.bottom_inside:
mov esi, eax
mov eax, [colour]
lea r8, [colours]
mov r8d, [r8 + rax*4]
mov edx, BRUSH
mov ecx, BRUSH
jmp fill_rect ; its ret returns from paint
; show_colour: the chosen colour in a framed square at the top-left corner.
show_colour:
mov edi, 2
mov esi, 2
mov edx, 20
mov ecx, 20
xor r8d, r8d ; the frame, black
call fill_rect
mov eax, [colour]
lea r8, [colours]
mov r8d, [r8 + rax*4]
mov edi, 4
mov esi, 4
mov edx, 16
mov ecx, 16
jmp fill_rect
_start:
mov eax, SYS_open
lea rdi, [fb_path]
mov esi, O_RDWR
syscall
test rax, rax
js fail
mov r12, rax
mov eax, SYS_ioctl
mov rdi, r12
mov esi, FBIOGET_VSCREENINFO
lea rdx, [var]
syscall
mov eax, SYS_ioctl
mov rdi, r12
mov esi, FBIOGET_FSCREENINFO
lea rdx, [fix]
syscall
mov eax, SYS_mmap
xor edi, edi
mov esi, [fix + fb_fix_screeninfo.smem_len]
mov edx, PROT_READ | PROT_WRITE
mov r10d, MAP_SHARED
mov r8, r12
xor r9d, r9d
syscall
cmp rax, -4095
jae fail
mov rbx, rax ; rbx: the top-left pixel
mov r13d, [fix + fb_fix_screeninfo.line_length]
xor edi, edi ; a clean sheet of paper
xor esi, esi
mov edx, [var + fb_var_screeninfo.xres]
mov ecx, [var + fb_var_screeninfo.yres]
mov r8d, PAPER
call fill_rect
call show_colour
; find the tablet: the first of event0 to event9 that reports a position
xor r14d, r14d ; r14: the number to try
find_tablet:
lea eax, [r14 + '0']
mov [event_path + 16], al ; the digit of /dev/input/event0
mov eax, SYS_open
lea rdi, [event_path]
mov esi, O_RDONLY | O_NONBLOCK
syscall
test rax, rax
js next_device ; no such device
mov r12, rax
mov eax, SYS_ioctl
mov rdi, r12
mov esi, EVIOCGBIT(0, 4) ; which types of event it sends
lea rdx, [types]
syscall
test dword [types], 1 << EV_ABS
jnz found_tablet ; positions: this is the tablet
mov eax, SYS_close
mov rdi, r12
syscall
next_device:
inc r14d
cmp r14d, 10
jb find_tablet
jmp fail
found_tablet: ; r12: the tablet
mov eax, SYS_ioctl
mov rdi, r12
mov esi, EVIOCGABS(ABS_X) ; x's range, and where the pointer is now
lea rdx, [xinfo]
syscall
mov eax, SYS_ioctl
mov rdi, r12
mov esi, EVIOCGABS(ABS_Y)
lea rdx, [yinfo]
syscall
mov eax, [xinfo + input_absinfo.value]
mov [x], eax
mov eax, [yinfo + input_absinfo.value]
mov [y], eax
wait_for_events:
mov [waiter + pollfd.fd], r12d
mov word [waiter + pollfd.events], POLLIN
mov eax, SYS_poll
lea rdi, [waiter]
mov esi, 1 ; one descriptor
mov edx, QUIET ; for at most QUIET ms
syscall
test rax, rax
jle done ; 0: nothing for QUIET ms
read_event:
mov eax, SYS_read
mov rdi, r12
lea rsi, [event]
mov edx, input_event_size
syscall
cmp rax, input_event_size
jne wait_for_events ; -EAGAIN: all read
movzx eax, word [event + input_event.type]
movzx ecx, word [event + input_event.code]
mov edx, [event + input_event.value]
cmp eax, EV_ABS ; a move: where the pointer is now
jne not_move
cmp ecx, ABS_X
jne not_x
mov [x], edx
not_x:
cmp ecx, ABS_Y
jne read_event
mov [y], edx
jmp read_event
not_move:
cmp eax, EV_KEY ; a button: 1 down, 0 up
jne not_button
cmp ecx, BTN_LEFT
jne read_event
mov [pressed], dl
jmp read_event
not_button:
cmp eax, EV_REL ; the wheel: 1 a notch away from you, -1 toward
jne not_wheel
cmp ecx, REL_WHEEL
jne read_event ; skip REL_WHEEL_HI_RES, the same turn finer
add edx, [colour]
jns not_below
add edx, COLOURS ; before the first colour: the last
not_below:
cmp edx, COLOURS
jb not_above
sub edx, COLOURS ; after the last: the first
not_above:
mov [colour], edx
call show_colour
jmp read_event
not_wheel:
cmp eax, EV_SYN ; the end of a report: paint if the button is down
jne read_event
cmp ecx, SYN_REPORT
jne read_event
cmp byte [pressed], 0
je read_event
call paint
jmp read_event
done:
mov eax, SYS_exit
xor edi, edi
syscall
fail:
mov eax, SYS_exit
mov edi, 1
syscall
Finding the tablet
The Screen pointer appears as a USB tablet: a device that reports where the pointer is,
rather than how far it moved. Its /dev/input/eventN number can change, so find_tablet tries
event0 through event9 and asks each open device which event types it supports.
In the string /dev/input/event0, the digit is byte 16, counting from zero. r14d holds a number
from 0 through 9. Adding '0' produces that digit's character code, and writing al to
[event_path + 16] changes just the digit. A failed open moves on to the next number.
EVIOCGBIT(0, 4) asks for four bytes of event-type bits in types. Event type number n has
bit n: EV_ABS is 3, so its mask is 1 << 3, or binary 1000. The instruction
test dword [types], 1 << EV_ABS checks that bit without changing the stored bits. jnz takes
the device when the bit is set. Otherwise the program closes it and continues searching. This
identifies the pointer among this Screen's devices, not every possible Linux input device.
EVIOCGBIT(0, 4) and EVIOCGABS(ABS_X) are NASM macros supplied by the include file.
Their arguments determine the request number at assembly time.
From the tablet's units to pixels
This Screen's tablet has a range of 0 through 32767 on each axis, independent of the Screen's
size. Other tablets can have different ranges. EVIOCGABS fills input_absinfo: minimum
and maximum describe the range, and value is the current position. The listing reads both
structures but uses only their maximum and value fields: it assumes the zero minimum
reported by this Screen.
The Screen starts with a whole pixel coordinate and sends an integer in the tablet's range. For a
320-pixel-wide Screen, pixel 160 becomes 160 × 32767 ÷ 320, rounded down: 16383. The program
reverses that scaling by rounding up:
column = (value × xres + maximum - 1) ÷ maximum
All divisions here discard the remainder. Adding maximum - 1 makes that integer division
round up instead. For 16383, the result is (16383 × 320 + 32766) ÷ 32767 = 160.
Using xres, rather than xres - 1, reverses the Screen's own encoding, which also uses the
full width. Treating 16383 directly as a column would address column 16383 on a Screen whose
last column is 319.
For a complete bounded conversion with this zero-based range, the formula is:
value = min(maximum, max(minimum, value))
column = min(xres - 1, (value × xres + maximum - 1) ÷ maximum)
min selects the smaller number and max the larger. At value = maximum, the quotient is
xres, so the final min brings it back to the last valid pixel, xres - 1. This formula
still assumes minimum = 0; another device's nonzero minimum needs a different conversion.
The listing's to_pixel performs the division without those clamps. That is sufficient for
this Screen: its input values come from valid whole pixels, so they never reach 32767. A drag
outside the Screen stays at an edge pixel. The multiply also fits in 32 bits at the Screen sizes
used here.
paint then subtracts BRUSH / 2 from each converted coordinate to centre the brush. It keeps
the left edge between 0 and xres - BRUSH, and the top edge between 0 and yres - BRUSH.
These bounds keep the entire rectangle inside the framebuffer, including near an edge.
Waiting, reading, and painting
waiter is one pollfd structure. Its fd field is the tablet descriptor, and events is
POLLIN, requesting notification when input can be read. poll fills revents with what
happened. The syscall receives the structure's address, a count of one descriptor, and the
QUIET timeout in milliseconds.
A positive poll result means a descriptor has reported something. Zero means the timeout
expired, and a negative raw result means an error. This example ends on either zero or an error
with jle done. It assumes the found tablet remains available: it does not inspect revents
for device errors.
After a positive result, read_event reads one 24-byte input_event at a time. Opening with
O_NONBLOCK lets the program drain all queued events: once the queue is empty, the raw read
returns -EAGAIN instead of waiting. The branch back to wait_for_events handles that case.
It also takes that branch for any other result that is not a complete event, rather than
handling read errors separately. Waiting again avoids repeatedly reading an empty queue.
As in keyboard control, type, code, and value identify each event:
EV_ABSwithABS_XorABS_Yreplaces the corresponding saved position.EV_KEYwithBTN_LEFTsaves 1 for down or 0 for up inpressed.EV_RELwithREL_WHEELchanges the colour by one for each notch. The matchingREL_WHEEL_HI_RESevent describes the same turn in finer units and is ignored.
A movement's events form a report, ending with EV_SYN and SYN_REPORT. A diagonal move
can update both axes before that marker. Painting only at SYN_REPORT uses both new coordinates,
instead of drawing once with the old y and again with the new y. The button flag must be nonzero
to paint. A repeat value of 2 would also count as held, though this Screen sends button presses
and releases without repeats.
paint ends with jmp fill_rect. Unlike call, that jump adds no return address: fill_rect's
ret uses the address saved by the caller of paint. show_colour uses the same arrangement
for its final rectangle. The indicator shares the canvas, so painting over it is allowed.
Turning the wheel draws it again.
Supplementary C version
// @screen width=320 height=240
#include <fcntl.h>
#include <poll.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <linux/fb.h>
#include <linux/input.h>
enum { BRUSH = 6, QUIET = 30000, PAPER = 0x00f4f0e6 };
static const uint32_t colours[] = { 0x00202020, 0x00e04040, 0x00f0a020,
0x0040b040, 0x002080e0, 0x008040c0 };
enum { COLOURS = sizeof colours / sizeof colours[0] };
static uint8_t *pixels;
static unsigned line_length;
static void fill_rect(int x, int y, int width, int height, uint32_t colour) {
for (int line = y; line < y + height; line++) {
uint32_t *row = (uint32_t *)(pixels + line * line_length);
for (int column = x; column < x + width; column++)
row[column] = colour;
}
}
/* The first input device that reports a position. */
static int open_tablet(void) {
for (int number = 0; number < 32; number++) {
char path[32];
snprintf(path, sizeof path, "/dev/input/event%d", number);
int device = open(path, O_RDONLY | O_NONBLOCK);
if (device < 0) continue;
unsigned char types[EV_MAX / 8 + 1] = { 0 };
ioctl(device, EVIOCGBIT(0, sizeof types), types);
if (types[EV_ABS / 8] >> (EV_ABS % 8) & 1) return device;
close(device);
}
return -1;
}
/* The pixel of a position on an axis of size pixels. */
static int to_pixel(int value, int size, const struct input_absinfo *axis) {
return ((long)value * size + axis->maximum - 1) / axis->maximum;
}
static int clamp(int value, int low, int high) {
return value < low ? low : value > high ? high : value;
}
static void show_colour(unsigned colour) {
fill_rect(2, 2, 20, 20, 0);
fill_rect(4, 4, 16, 16, colours[colour]);
}
int main(void) {
int fb = open("/dev/fb0", O_RDWR);
int tablet = open_tablet();
if (fb < 0 || tablet < 0) return 1;
struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;
ioctl(fb, FBIOGET_VSCREENINFO, &var);
ioctl(fb, FBIOGET_FSCREENINFO, &fix);
pixels = mmap(NULL, fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fb, 0);
if (pixels == MAP_FAILED) return 1;
line_length = fix.line_length;
fill_rect(0, 0, var.xres, var.yres, PAPER);
struct input_absinfo xinfo, yinfo;
ioctl(tablet, EVIOCGABS(ABS_X), &xinfo);
ioctl(tablet, EVIOCGABS(ABS_Y), &yinfo);
int x = xinfo.value, y = yinfo.value, pressed = 0;
unsigned colour = 1;
show_colour(colour);
struct pollfd waiter = { .fd = tablet, .events = POLLIN };
while (poll(&waiter, 1, QUIET) > 0) {
struct input_event event;
while (read(tablet, &event, sizeof event) == sizeof event) {
if (event.type == EV_ABS && event.code == ABS_X) x = event.value;
if (event.type == EV_ABS && event.code == ABS_Y) y = event.value;
if (event.type == EV_KEY && event.code == BTN_LEFT) pressed = event.value;
if (event.type == EV_REL && event.code == REL_WHEEL) {
colour = (colour + COLOURS + event.value) % COLOURS;
show_colour(colour);
}
if (event.type == EV_SYN && event.code == SYN_REPORT && pressed) {
int left = to_pixel(x, var.xres, &xinfo) - BRUSH / 2;
int top = to_pixel(y, var.yres, &yinfo) - BRUSH / 2;
fill_rect(clamp(left, 0, var.xres - BRUSH), clamp(top, 0, var.yres - BRUSH),
BRUSH, BRUSH, colours[colour]);
}
}
}
return 0;
}The C version uses the same drawing and report-based painting steps. Its open_tablet builds
paths with snprintf and searches through event31. Its byte array of type bits uses byte
n / 8, bit n % 8, for event type n, instead of NASM's single four-byte bitmask.
Try it
- Increase
BRUSHfrom 6 to 12. Keep it positive and no larger than either Screen dimension, so the rectangle bounds inpaintremain valid. - Make the right button erase, with erasing taking priority when both buttons are held.
Add a byte such as
erasingbesidepressed. In theEV_KEYbranch, saveBTN_RIGHT's value there as the code savesBTN_LEFTinpressed. Treat any nonzero value as held, including a repeat value of 2. AtSYN_REPORT, callpaintwhen either flag is nonzero. Insidepaint, where it currently loadsr8dfromcolours, choosePAPERinstead whenerasingis nonzero. Leave the wheel's chosen colour intact for the next left-button stroke. - Add white,
0x00ffffff, tocolours.COLOURSfollows, since NASM counts the table's bytes. - Change
@screentowidth=640 height=480and paint again. The conversion now uses twice as many pixels on each axis, and the brush still lands under the pointer.
Instructions in this lesson
testjlemovimulleastosdadddecjneretxordivcallsubjnscmpjmpsyscalljsjaeincjbmovzxje