schcom: repaint what changed, and stop rendering frames nobody sees

Moving the mouse repainted the whole screen. At 1280x800 that is a
million pixels of desktop fill, every surface re-blitted, and a 4 MB copy
to the framebuffer -- per event. Under emulation the pointer lagged the
mouse by whole seconds, and that was not the emulation's fault: moving a
10x10 cursor does not require touching four megabytes.

Two changes, and the second matters as much as the first.

Damage tracking: one rectangle, unioned from whatever actually changed,
and only those rows are redrawn and copied out. A pointer move damages
where it was and where it is; a commit damages that surface; a client
going away damages what it had. One rectangle rather than a list, because
in practice the changes are a cursor and the window under it, and a list
costs more bookkeeping than it saves.

Draining input: a single mouse movement produces a burst of EV_REL ending
in EV_SYN, and the loop handled one event per poll and repainted. So the
compositor rendered every intermediate position of a cursor that was
never seen at those positions. Both queues are now drained before
anything is painted. handle_input grew a distinct "queue empty" return
for it -- EV_SYN changes nothing but is not the end of the burst, and a
drain that stopped there would leave the rest queued.

Also cuts the loader's resolution prompt from four seconds to one and a
half, on both the BIOS and UEFI paths. Four was mine and it was too long:
it is paid on every unattended boot, and adding four seconds to every
start to offer a menu nobody usually wants makes the common case worse to
serve the rare one.

schcom-test passes, including two overlapping clients and stacking order.
bootcheck 108 [ok].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-10 05:54:06 +02:00
commit bf9ac843f7
4 changed files with 130 additions and 37 deletions

View file

@ -337,8 +337,10 @@ reqh:
# path, so whatever is chosen here is what the framebuffer, the console and the # path, so whatever is chosen here is what the framebuffer, the console and the
# compositor all get. # compositor all get.
# #
# Four seconds, then it boots on its own. A loader that waits for a keypress is # A second and a half, then it boots on its own. This was four seconds and that
# a machine that does not come back up after a power cut. # was simply too long: it is paid on every unattended boot, and a loader that
# adds four seconds to every start to offer a menu nobody usually wants has made
# the common case worse to serve the rare one.
# #
# In: CX = default mode. Out: CX = chosen mode. Clobbers AX, BX, DX, SI. # In: CX = default mode. Out: CX = chosen mode. Clobbers AX, BX, DX, SI.
res_menu: res_menu:
@ -346,9 +348,9 @@ res_menu:
movw $resprompt, %si movw $resprompt, %si
call print16 call print16
# Poll for ~4s. INT 1Ah tick count runs at 18.2 Hz, so 72 ticks is four # Poll for ~1.5s. INT 1Ah tick count runs at 18.2 Hz, so 27 ticks is a second
# seconds and needs no calibration against CPU speed -- a delay loop would be # and a half and needs no calibration against CPU speed -- a delay loop would
# a different length on every machine. # be a different length on every machine.
movb $0x00, %ah movb $0x00, %ah
int $0x1A # CX:DX = ticks since midnight int $0x1A # CX:DX = ticks since midnight
movw %dx, %bx # BX = starting tick, low word movw %dx, %bx # BX = starting tick, low word
@ -358,7 +360,7 @@ res_menu:
movb $0x00, %ah movb $0x00, %ah
int $0x1A int $0x1A
subw %bx, %dx subw %bx, %dx
cmpw $72, %dx cmpw $27, %dx
jb .Lrwait jb .Lrwait
movw $restimeout, %si movw $restimeout, %si
call print16 call print16
@ -467,7 +469,7 @@ resmodes:
.word 0x0145 # 1280x1024 verified .word 0x0145 # 1280x1024 verified
resprompt: resprompt:
.asciz "Press R within 4s to choose a resolution... " .asciz "Press R for resolution... "
restimeout: restimeout:
.asciz "keeping 800x600\r\n" .asciz "keeping 800x600\r\n"
resmenu: resmenu:

View file

@ -261,8 +261,10 @@ load_elf(EFI_FILE_PROTOCOL *file)
// and which index means what varies by machine. So this walks them, and the // and which index means what varies by machine. So this walks them, and the
// menu is built from what is actually there rather than from a table. // menu is built from what is actually there rather than from a table.
// //
// Four seconds, then it boots on its own. A loader that waits for a keypress // A second and a half, then it boots on its own. This was four seconds and that
// is a machine that does not come back up after a power cut. // was too long: it is paid on every unattended boot, and adding four seconds to
// every start to offer a menu nobody usually wants makes the common case worse
// to serve the rare one.
static void static void
print_dec(UINT32 v) print_dec(UINT32 v)
@ -330,8 +332,8 @@ choose_mode(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
{ {
UINT32 current = gop->Mode->Mode; UINT32 current = gop->Mode->Mode;
print(L"Press R within 4s to choose a resolution... "); print(L"Press R for resolution... ");
CHAR16 c = wait_key(4000); CHAR16 c = wait_key(1500);
if (c != L'r' && c != L'R') { if (c != L'r' && c != L'R') {
print(L"keeping "); print(L"keeping ");
print_dec(gop->Mode->Info->HorizontalResolution); print(L"x"); print_dec(gop->Mode->Info->HorizontalResolution); print(L"x");

Binary file not shown.

View file

@ -120,14 +120,66 @@ open_framebuffer(void)
return fd; return fd;
} }
/* Damage: the rectangle that has to be redrawn and copied out.
*
* Without this the compositor repainted the whole screen for every event --
* at 1280x800 that is a million pixels of desktop fill plus a 4 MB copy to the
* framebuffer, per mouse movement. Under emulation the pointer lagged behind
* the mouse by whole seconds, and it was not the emulation's fault: moving a
* 10x10 cursor does not require touching four megabytes.
*
* One rectangle rather than a list. Two distant small changes then repaint the
* span between them, which is wasteful in theory; in practice the changes are
* a pointer and the window under it, and a list costs more bookkeeping than it
* saves.
*/
static int dmg_x0, dmg_y0, dmg_x1, dmg_y1; /* x1/y1 exclusive */
static int dirty;
static void
damage(int x, int y, int w, int h)
{
int x0 = x, y0 = y, x1 = x + w, y1 = y + h;
if (x0 < 0) x0 = 0;
if (y0 < 0) y0 = 0;
if (x1 > (int)fb_w) x1 = (int)fb_w;
if (y1 > (int)fb_h) y1 = (int)fb_h;
if (x0 >= x1 || y0 >= y1)
return;
if (!dirty) {
dmg_x0 = x0; dmg_y0 = y0; dmg_x1 = x1; dmg_y1 = y1;
dirty = 1;
return;
}
if (x0 < dmg_x0) dmg_x0 = x0;
if (y0 < dmg_y0) dmg_y0 = y0;
if (x1 > dmg_x1) dmg_x1 = x1;
if (y1 > dmg_y1) dmg_y1 = y1;
}
static void
damage_all(void)
{
damage(0, 0, (int)fb_w, (int)fb_h);
}
#define PTR_SIZE 10
static void static void
composite(void) composite(void)
{ {
if (!dirty)
return;
const int rx0 = dmg_x0, ry0 = dmg_y0, rx1 = dmg_x1, ry1 = dmg_y1;
/* Desktop. A flat colour rather than black, so that "nothing was drawn" /* Desktop. A flat colour rather than black, so that "nothing was drawn"
* and "the compositor is not running" cannot look the same. */ * and "the compositor is not running" cannot look the same. */
for (uint32_t y = 0; y < fb_h; y++) { for (int y = ry0; y < ry1; y++) {
uint32_t *row = (uint32_t *)(back + (size_t)y * fb_stride); uint32_t *row = (uint32_t *)(back + (size_t)y * fb_stride);
for (uint32_t x = 0; x < fb_w; x++) for (int x = rx0; x < rx1; x++)
row[x] = 0x00203040; row[x] = 0x00203040;
} }
@ -139,17 +191,18 @@ composite(void)
if (!s->used || !s->pixels) if (!s->used || !s->pixels)
continue; continue;
/* Only the part of this surface inside the damage rectangle. */
for (uint32_t sy = 0; sy < s->h; sy++) { for (uint32_t sy = 0; sy < s->h; sy++) {
int32_t dy = s->y + (int32_t)sy; int dy = s->y + (int)sy;
if (dy < 0 || dy >= (int32_t)fb_h) if (dy < ry0 || dy >= ry1)
continue; continue;
uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride); uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride);
uint32_t *src = (uint32_t *)((uint8_t *)s->pixels + (size_t)sy * s->stride); uint32_t *src = (uint32_t *)((uint8_t *)s->pixels + (size_t)sy * s->stride);
for (uint32_t sx = 0; sx < s->w; sx++) { for (uint32_t sx = 0; sx < s->w; sx++) {
int32_t dx = s->x + (int32_t)sx; int dx = s->x + (int)sx;
if (dx < 0 || dx >= (int32_t)fb_w) if (dx < rx0 || dx >= rx1)
continue; continue;
dst[dx] = src[sx]; dst[dx] = src[sx];
} }
@ -157,22 +210,29 @@ composite(void)
} }
/* A crude pointer, drawn last so it is never under a window. */ /* A crude pointer, drawn last so it is never under a window. */
for (int32_t py = 0; py < 10; py++) { for (int py = 0; py < PTR_SIZE; py++) {
int32_t dy = ptr_y + py; int dy = ptr_y + py;
if (dy < 0 || dy >= (int32_t)fb_h) if (dy < ry0 || dy >= ry1)
continue; continue;
uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride); uint32_t *dst = (uint32_t *)(back + (size_t)dy * fb_stride);
for (int32_t px = 0; px < 10 - py; px++) { for (int px = 0; px < PTR_SIZE - py; px++) {
int32_t dx = ptr_x + px; int dx = ptr_x + px;
if (dx >= 0 && dx < (int32_t)fb_w) if (dx >= rx0 && dx < rx1)
dst[dx] = 0x00FFFFFF; dst[dx] = 0x00FFFFFF;
} }
} }
/* One copy to the screen. Compositing straight into the framebuffer would /* Copy out only what changed, row by row. Compositing straight into the
* show every intermediate state -- the flicker would be the whole desktop * framebuffer would show every intermediate state -- the flicker would be
* being repainted under the user's eyes. */ * the whole desktop being repainted under the user's eyes -- but copying
memcpy(fb, back, fb_size); * the whole thing to move a cursor is what made the pointer lag. */
size_t bytes = (size_t)(rx1 - rx0) * 4;
for (int y = ry0; y < ry1; y++) {
size_t off = (size_t)y * fb_stride + (size_t)rx0 * 4;
memcpy(fb + off, back + off, bytes);
}
dirty = 0;
} }
/* ---------------------------------------------------------------- clients */ /* ---------------------------------------------------------------- clients */
@ -234,6 +294,7 @@ drop_client(int ci)
for (int i = 0; i < MAX_SURFACES; i++) { for (int i = 0; i < MAX_SURFACES; i++) {
struct surface *s = &surfaces[i]; struct surface *s = &surfaces[i];
if (s->used && s->client == ci) { if (s->used && s->client == ci) {
damage(s->x, s->y, (int)s->w, (int)s->h);
if (s->pixels) if (s->pixels)
shmdt(s->pixels); shmdt(s->pixels);
/* IPC_RMID once, here: the segment goes when the last attach does, /* IPC_RMID once, here: the segment goes when the last attach does,
@ -318,6 +379,7 @@ handle_create_surface(int ci, const struct schcom_create_surface *req)
drop_client(ci); drop_client(ci);
return; return;
} }
damage(s->x, s->y, (int)w, (int)h);
printf("schcom: surface %u %ux%u at %d,%d for client %d (shm %d)\n", printf("schcom: surface %u %ux%u at %d,%d for client %d (shm %d)\n",
s->id, w, h, s->x, s->y, ci, shmid); s->id, w, h, s->x, s->y, ci, shmid);
fflush(stdout); fflush(stdout);
@ -404,7 +466,10 @@ handle_message(int ci)
return 0; return 0;
} }
/* The commit is the whole point: the pixels were already there, but /* The commit is the whole point: the pixels were already there, but
* they were not allowed to be shown until now. */ * they were not allowed to be shown until now. The damage rectangle in
* the message is advisory -- repaint the whole surface, which is never
* wrong, only sometimes more than asked. */
damage(s->x, s->y, (int)s->w, (int)s->h);
struct schcom_header fr; struct schcom_header fr;
memset(&fr, 0, sizeof fr); memset(&fr, 0, sizeof fr);
fr.op = SCHCOM_OP_FRAME; fr.op = SCHCOM_OP_FRAME;
@ -420,6 +485,7 @@ handle_message(int ci)
case SCHCOM_OP_DESTROY: { case SCHCOM_OP_DESTROY: {
struct surface *s = find_surface(hdr.surface); struct surface *s = find_surface(hdr.surface);
if (s && s->client == ci) { if (s && s->client == ci) {
damage(s->x, s->y, (int)s->w, (int)s->h);
if (s->pixels) if (s->pixels)
shmdt(s->pixels); shmdt(s->pixels);
shmctl(s->shmid, IPC_RMID, NULL); shmctl(s->shmid, IPC_RMID, NULL);
@ -438,23 +504,32 @@ handle_message(int ci)
/* ----------------------------------------------------------------- input */ /* ----------------------------------------------------------------- input */
/* Returns 1 if the screen needs repainting, -1 to quit. */ /* 1 if the screen needs repainting, 0 if the event changed nothing, -1 to
* quit, -2 when the queue is empty. The last is separate from 0 on purpose:
* every burst of movement ends in an EV_SYN that changes nothing, and a
* drain loop that stopped there would leave the rest of the burst queued. */
static int static int
handle_input(int fd) handle_input(int fd)
{ {
struct input_event ev; struct input_event ev;
if (read(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) if (read(fd, &ev, sizeof ev) != (ssize_t)sizeof ev)
return 0; return -2;
int repaint = 0; int repaint = 0;
if (ev.type == EV_REL) { 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; } if (ev.code == REL_X) { ptr_x += ev.value; repaint = 1; }
if (ev.code == REL_Y) { ptr_y += ev.value; repaint = 1; } if (ev.code == REL_Y) { ptr_y += ev.value; repaint = 1; }
if (ptr_x < 0) ptr_x = 0; if (ptr_x < 0) ptr_x = 0;
if (ptr_y < 0) ptr_y = 0; if (ptr_y < 0) ptr_y = 0;
if (ptr_x >= (int32_t)fb_w) ptr_x = (int32_t)fb_w - 1; 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 (ptr_y >= (int32_t)fb_h) ptr_y = (int32_t)fb_h - 1;
if (repaint) {
/* Where it was and where it is. Everything between is untouched. */
damage(old_x, old_y, PTR_SIZE, PTR_SIZE);
damage(ptr_x, ptr_y, PTR_SIZE, PTR_SIZE);
}
} }
if (ev.type == EV_KEY && ev.value == 1 && ev.code == KEY_ESC) if (ev.type == EV_KEY && ev.value == 1 && ev.code == KEY_ESC)
@ -523,6 +598,7 @@ main(int argc, char **argv)
printf("schcom: listening on %s\n", SCHCOM_SOCKET_PATH); printf("schcom: listening on %s\n", SCHCOM_SOCKET_PATH);
fflush(stdout); fflush(stdout);
damage_all();
composite(); composite();
int frames = 1; int frames = 1;
@ -568,16 +644,29 @@ main(int argc, char **argv)
} }
} }
/* Drain both devices before painting anything. A single mouse
* movement produces a burst of EV_REL followed by EV_SYN, and
* repainting per event meant the compositor rendered every
* intermediate position of a cursor nobody saw at those positions --
* which is most of what "the mouse lags" was. */
int quit = 0;
if (kbd_slot >= 0 && (pfd[kbd_slot].revents & POLLIN)) { if (kbd_slot >= 0 && (pfd[kbd_slot].revents & POLLIN)) {
int r = handle_input(kbd); for (int n = 0; n < 256; n++) {
if (r < 0) break; int r = handle_input(kbd);
repaint |= r; if (r == -2) break;
if (r < 0) { quit = 1; break; }
if (r > 0) repaint = 1;
}
} }
if (mouse_slot >= 0 && (pfd[mouse_slot].revents & POLLIN)) { if (!quit && mouse_slot >= 0 && (pfd[mouse_slot].revents & POLLIN)) {
int r = handle_input(mouse); for (int n = 0; n < 256; n++) {
if (r < 0) break; int r = handle_input(mouse);
repaint |= r; if (r == -2) break;
if (r < 0) { quit = 1; break; }
if (r > 0) repaint = 1;
}
} }
if (quit) break;
/* Walk the client slots in the same order they were added to pfd. */ /* Walk the client slots in the same order they were added to pfd. */
int k = first_client; int k = first_client;