// SPDX-FileCopyrightText: © 2026 Vladimir Zorin // SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later // Licensed under OWL v1.0+. See LICENSE. /* term.drm_core — DRM/KMS console output for the gfx layer. * * Opens a DRM card node (/dev/dri/card0), becomes DRM master, picks the * first connected connector and its preferred mode, and drives the CRTC * with two 32bpp XRGB8888 dumb buffers: blits render into the back buffer * (integer nearest-neighbour upscaling, source alpha ignored — composite * before blitting), and flip() queues a vsynced page flip. The flip * completion arrives as a DRM_EVENT_FLIP_COMPLETE record readable on the * card fd — fd() exposes it so an event loop can wait cooperatively; * handle_event() drains it. The dumb-buffer mmap is ordinary cached RAM * with no deferred-IO shadowing, so writes never stall on a kernel flush. * * Raw stable-UAPI ioctls only — no libdrm. The prior CRTC configuration * is saved on open and restored on close; the driver's lastclose fbdev * restore is the safety net if the process dies uncleanly. * * Coordinates are 1-indexed at the Lua boundary to match term.gfx; blits * clip silently against the mode bounds. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "gfx_core.h" #define DRM_MT "term.drm" /* enum drm_connector_status "connected" — the enum itself is not part of * the UAPI headers (drm_mode_get_connector.connection just references it), * but the values are frozen ABI. */ #define DRM_CONN_CONNECTED 1 typedef struct { __u32 handle; /* dumb buffer GEM handle; 0 when not created */ __u32 fb_id; /* ADDFB framebuffer id; 0 when not added */ __u32 pitch; /* bytes per row, as returned by CREATE_DUMB (drivers pad) */ __u64 size; uint8_t *map; /* mmap'd buffer memory; NULL after close */ } drm_buf_t; typedef struct { int fd; /* -1 after close */ int is_master; __u32 crtc_id; __u32 connector_id; struct drm_mode_modeinfo mode; /* the chosen mode */ struct drm_mode_crtc saved; /* prior CRTC config, restored on close */ int saved_valid; drm_buf_t buf[2]; int back; /* index of the buffer blits land in */ int flip_pending; /* a queued flip whose event hasn't been drained */ int xres, yres; /* mode.hdisplay / mode.vdisplay */ uint8_t *rowbuf; /* xres * 4 scratch: one packed device row */ } drm_t; static drm_t *check_drm(lua_State *L, int idx) { return (drm_t *)luaL_checkudata(L, idx, DRM_MT); } static drm_t *check_open_drm(lua_State *L, int idx) { drm_t *d = check_drm(L, idx); if (d->fd < 0) luaL_error(L, "DRM device is closed"); return d; } /* DRM ioctls are interruptible; retry EINTR/EAGAIN (the libdrm drmIoctl * idiom). PAGE_FLIP is NOT routed through this — its EAGAIN/EBUSY are * real answers, not retry conditions. */ static int xioctl(int fd, unsigned long req, void *arg) { int r; do { r = ioctl(fd, req, arg); } while (r == -1 && (errno == EINTR || errno == EAGAIN)); return r; } /* Drain the card fd's event stream (non-blocking; the fd is O_NONBLOCK). * Returns 1 if a flip-complete record was seen. Records are parsed via * memcpy — never cast into the byte buffer — and the walk is bounded by * both the read length and each record's own length. */ static int drain_events(drm_t *d) { char ebuf[1024]; int completed = 0; for (;;) { ssize_t n = read(d->fd, ebuf, sizeof(ebuf)); if (n <= 0) break; size_t off = 0; while (off + sizeof(struct drm_event) <= (size_t)n) { struct drm_event e; memcpy(&e, ebuf + off, sizeof(e)); if (e.length < sizeof(struct drm_event) || off + e.length > (size_t)n) break; if (e.type == DRM_EVENT_FLIP_COMPLETE) { d->flip_pending = 0; completed = 1; } off += e.length; } } return completed; } /* Release everything, restoring the console CRTC first. Idempotent; pure * C with no event-loop dependency, so it is safe from __gc and from a * teardown that runs after the LEV loop has stopped. */ static void drm_release(drm_t *d) { if (d->fd >= 0) { /* SETCRTC/RMFB return EBUSY against an in-flight flip: give the * pending flip a bounded chance to complete, then proceed anyway. */ for (int i = 0; d->flip_pending && i < 3; i++) { struct pollfd p = {.fd = d->fd, .events = POLLIN}; poll(&p, 1, 50); drain_events(d); } d->flip_pending = 0; if (d->saved_valid) { struct drm_mode_crtc c = d->saved; if (c.fb_id != 0) { c.set_connectors_ptr = (__u64)(uintptr_t)&d->connector_id; c.count_connectors = 1; } else { /* The CRTC had no framebuffer (off): restore-as-disable. */ c.set_connectors_ptr = 0; c.count_connectors = 0; c.mode_valid = 0; } /* Best-effort: the driver's lastclose restore is the net. */ xioctl(d->fd, DRM_IOCTL_MODE_SETCRTC, &c); d->saved_valid = 0; } for (int i = 0; i < 2; i++) { drm_buf_t *b = &d->buf[i]; if (b->map) { munmap(b->map, (size_t)b->size); b->map = NULL; } if (b->fb_id) { unsigned int id = b->fb_id; xioctl(d->fd, DRM_IOCTL_MODE_RMFB, &id); b->fb_id = 0; } if (b->handle) { struct drm_mode_destroy_dumb dd; memset(&dd, 0, sizeof(dd)); dd.handle = b->handle; xioctl(d->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dd); b->handle = 0; } } if (d->is_master) { xioctl(d->fd, DRM_IOCTL_DROP_MASTER, NULL); d->is_master = 0; } close(d->fd); d->fd = -1; } free(d->rowbuf); d->rowbuf = NULL; } /* Pick the first connected connector with modes and resolve its mode and * CRTC. Fills d->connector_id, d->mode, d->crtc_id (and d->saved via * GETCRTC). Returns 0 on success; on failure returns -1 with *msg set to * a static description (errno left from the failing call, 0 when the * failure is a "nothing suitable found" verdict rather than an ioctl). */ static int pick_output(drm_t *d, const char **msg) { struct drm_mode_card_res res; __u32 *conn_ids = NULL, *crtc_ids = NULL; struct drm_mode_modeinfo *modes = NULL; __u32 *conn_encs = NULL; int ret = -1; /* GETRESOURCES two-call: counts first, arrays second; re-loop when a * hotplug grows the counts between the calls. */ for (int attempt = 0;; attempt++) { memset(&res, 0, sizeof(res)); if (xioctl(d->fd, DRM_IOCTL_MODE_GETRESOURCES, &res) == -1) { *msg = "GETRESOURCES failed"; goto out; } if (res.count_connectors < 1) { *msg = "no connectors"; errno = 0; goto out; } free(conn_ids); free(crtc_ids); conn_ids = (__u32 *)calloc(res.count_connectors, sizeof(__u32)); crtc_ids = (__u32 *)calloc(res.count_crtcs ? res.count_crtcs : 1, sizeof(__u32)); if (!conn_ids || !crtc_ids) { *msg = "out of memory"; goto out; } __u32 nconn = res.count_connectors, ncrtc = res.count_crtcs; res.connector_id_ptr = (__u64)(uintptr_t)conn_ids; res.crtc_id_ptr = (__u64)(uintptr_t)crtc_ids; res.encoder_id_ptr = 0; res.count_encoders = 0; res.fb_id_ptr = 0; res.count_fbs = 0; if (xioctl(d->fd, DRM_IOCTL_MODE_GETRESOURCES, &res) == -1) { *msg = "GETRESOURCES failed"; goto out; } if (res.count_connectors <= nconn && res.count_crtcs <= ncrtc) break; if (attempt >= 2) { *msg = "resource counts kept changing"; errno = 0; goto out; } } /* Scan connectors for the first connected one that carries modes. */ struct drm_mode_get_connector conn; __u32 want = 0; for (__u32 i = 0; i < res.count_connectors && !want; i++) { memset(&conn, 0, sizeof(conn)); conn.connector_id = conn_ids[i]; /* Counts-only call; count_modes == 0 also forces a fresh probe. */ if (xioctl(d->fd, DRM_IOCTL_MODE_GETCONNECTOR, &conn) == -1) continue; if (conn.connection != DRM_CONN_CONNECTED || conn.count_modes < 1) continue; __u32 nmodes = conn.count_modes, nencs = conn.count_encoders; free(modes); free(conn_encs); modes = (struct drm_mode_modeinfo *)calloc(nmodes, sizeof(*modes)); conn_encs = (__u32 *)calloc(nencs ? nencs : 1, sizeof(__u32)); if (!modes || !conn_encs) { *msg = "out of memory"; goto out; } memset(&conn, 0, sizeof(conn)); conn.connector_id = conn_ids[i]; conn.modes_ptr = (__u64)(uintptr_t)modes; conn.count_modes = nmodes; conn.encoders_ptr = (__u64)(uintptr_t)conn_encs; conn.count_encoders = nencs; if (xioctl(d->fd, DRM_IOCTL_MODE_GETCONNECTOR, &conn) == -1) continue; if (conn.connection != DRM_CONN_CONNECTED || conn.count_modes < 1) continue; want = conn_ids[i]; } if (!want) { *msg = "no connected connector with modes"; errno = 0; goto out; } d->connector_id = want; /* Preferred mode, else the first (the kernel sorts sensibly). */ d->mode = modes[0]; for (__u32 i = 0; i < conn.count_modes; i++) { if (modes[i].type & DRM_MODE_TYPE_PREFERRED) { d->mode = modes[i]; break; } } /* CRTC: the connector's live encoder chain first (the normal case at * a VT with fbcon), else any of its encoders' possible CRTCs. */ d->crtc_id = 0; if (conn.encoder_id) { struct drm_mode_get_encoder enc; memset(&enc, 0, sizeof(enc)); enc.encoder_id = conn.encoder_id; if (xioctl(d->fd, DRM_IOCTL_MODE_GETENCODER, &enc) == 0 && enc.crtc_id) d->crtc_id = enc.crtc_id; } for (__u32 i = 0; !d->crtc_id && i < conn.count_encoders; i++) { struct drm_mode_get_encoder enc; memset(&enc, 0, sizeof(enc)); enc.encoder_id = conn_encs[i]; if (xioctl(d->fd, DRM_IOCTL_MODE_GETENCODER, &enc) == -1) continue; for (__u32 c = 0; c < res.count_crtcs; c++) { if (enc.possible_crtcs & (1u << c)) { d->crtc_id = crtc_ids[c]; break; } } } if (!d->crtc_id) { *msg = "no usable CRTC"; errno = 0; goto out; } /* Save the prior CRTC configuration for restore on close. */ memset(&d->saved, 0, sizeof(d->saved)); d->saved.crtc_id = d->crtc_id; if (xioctl(d->fd, DRM_IOCTL_MODE_GETCRTC, &d->saved) == 0) d->saved_valid = 1; ret = 0; out: free(conn_ids); free(crtc_ids); free(modes); free(conn_encs); return ret; } /* drm_core.open(path?) -> drm | nil, err (default "/dev/dri/card0") */ static int l_drm_open(lua_State *L) { const char *path = luaL_optstring(L, 1, "/dev/dri/card0"); /* Create the userdata before acquiring resources so __gc owns every * later failure path (the gfx_core.surface idiom). */ drm_t *d = (drm_t *)lua_newuserdata(L, sizeof(drm_t)); memset(d, 0, sizeof(drm_t)); d->fd = -1; luaL_getmetatable(L, DRM_MT); lua_setmetatable(L, -2); /* O_NONBLOCK keeps the event read() non-blocking — blocking waits are * the event loop's job, on the fd this module exposes. */ d->fd = open(path, O_RDWR | O_CLOEXEC | O_NONBLOCK); if (d->fd == -1) { lua_pushnil(L); lua_pushfstring(L, "%s: %s", path, strerror(errno)); return 2; } if (xioctl(d->fd, DRM_IOCTL_SET_MASTER, NULL) == -1) { int e = errno; drm_release(d); lua_pushnil(L); if (e == ENOTTY || e == EINVAL) lua_pushfstring(L, "%s: not a DRM device (%s)", path, strerror(e)); else lua_pushfstring(L, "%s: cannot become DRM master (%s) — is a compositor running?", path, strerror(e)); return 2; } d->is_master = 1; const char *msg = NULL; if (pick_output(d, &msg) == -1) { int e = errno; drm_release(d); lua_pushnil(L); if (e) lua_pushfstring(L, "%s: %s (%s)", path, msg, strerror(e)); else lua_pushfstring(L, "%s: %s", path, msg); return 2; } d->xres = d->mode.hdisplay; d->yres = d->mode.vdisplay; if (d->xres < 1 || d->yres < 1) { drm_release(d); lua_pushnil(L); lua_pushfstring(L, "%s: inconsistent mode geometry", path); return 2; } /* Two dumb buffers: CREATE_DUMB -> ADDFB (bpp 32 / depth 24 is the * legacy-ioctl spelling of XRGB8888) -> MAP_DUMB + mmap. Both start * black so the first SETCRTC presents a clean screen. */ for (int i = 0; i < 2; i++) { drm_buf_t *b = &d->buf[i]; struct drm_mode_create_dumb cd; memset(&cd, 0, sizeof(cd)); cd.width = (__u32)d->xres; cd.height = (__u32)d->yres; cd.bpp = 32; if (xioctl(d->fd, DRM_IOCTL_MODE_CREATE_DUMB, &cd) == -1) { int e = errno; drm_release(d); lua_pushnil(L); lua_pushfstring(L, "%s: CREATE_DUMB failed (%s)", path, strerror(e)); return 2; } b->handle = cd.handle; b->pitch = cd.pitch; b->size = cd.size; struct drm_mode_fb_cmd fc; memset(&fc, 0, sizeof(fc)); fc.width = (__u32)d->xres; fc.height = (__u32)d->yres; fc.pitch = cd.pitch; fc.bpp = 32; fc.depth = 24; fc.handle = cd.handle; if (xioctl(d->fd, DRM_IOCTL_MODE_ADDFB, &fc) == -1) { int e = errno; drm_release(d); lua_pushnil(L); lua_pushfstring(L, "%s: ADDFB failed (%s)", path, strerror(e)); return 2; } b->fb_id = fc.fb_id; struct drm_mode_map_dumb md; memset(&md, 0, sizeof(md)); md.handle = cd.handle; if (xioctl(d->fd, DRM_IOCTL_MODE_MAP_DUMB, &md) == -1) { int e = errno; drm_release(d); lua_pushnil(L); lua_pushfstring(L, "%s: MAP_DUMB failed (%s)", path, strerror(e)); return 2; } b->map = (uint8_t *)mmap(NULL, (size_t)b->size, PROT_READ | PROT_WRITE, MAP_SHARED, d->fd, (off_t)md.offset); if (b->map == MAP_FAILED) { int e = errno; b->map = NULL; drm_release(d); lua_pushnil(L); lua_pushfstring(L, "%s: mmap failed (%s)", path, strerror(e)); return 2; } memset(b->map, 0, (size_t)b->size); } struct drm_mode_crtc crtc; memset(&crtc, 0, sizeof(crtc)); crtc.crtc_id = d->crtc_id; crtc.fb_id = d->buf[0].fb_id; crtc.set_connectors_ptr = (__u64)(uintptr_t)&d->connector_id; crtc.count_connectors = 1; crtc.mode = d->mode; crtc.mode_valid = 1; if (xioctl(d->fd, DRM_IOCTL_MODE_SETCRTC, &crtc) == -1) { int e = errno; drm_release(d); lua_pushnil(L); lua_pushfstring(L, "%s: SETCRTC failed (%s)", path, strerror(e)); return 2; } d->back = 1; d->flip_pending = 0; d->rowbuf = (uint8_t *)malloc((size_t)d->xres * 4); if (!d->rowbuf) { drm_release(d); lua_pushnil(L); lua_pushstring(L, "out of memory allocating row buffer"); return 2; } return 1; } /* drm:size() -> w, h */ static int l_drm_size(lua_State *L) { drm_t *d = check_open_drm(L, 1); lua_pushinteger(L, d->xres); lua_pushinteger(L, d->yres); return 2; } /* drm:fd() -> fd — pollable: flip-complete events land here. */ static int l_drm_fd(lua_State *L) { drm_t *d = check_open_drm(L, 1); lua_pushinteger(L, d->fd); return 1; } /* drm:info() -> table */ static int l_drm_info(lua_State *L) { drm_t *d = check_open_drm(L, 1); lua_createtable(L, 0, 7); lua_pushinteger(L, d->xres); lua_setfield(L, -2, "w"); lua_pushinteger(L, d->yres); lua_setfield(L, -2, "h"); lua_pushinteger(L, d->buf[0].pitch); lua_setfield(L, -2, "pitch"); lua_pushinteger(L, d->mode.vrefresh); lua_setfield(L, -2, "refresh"); lua_pushlstring(L, d->mode.name, strnlen(d->mode.name, sizeof(d->mode.name))); lua_setfield(L, -2, "mode"); lua_pushinteger(L, d->connector_id); lua_setfield(L, -2, "connector"); lua_pushinteger(L, d->crtc_id); lua_setfield(L, -2, "crtc"); return 1; } /* XRGB8888 little-endian: one packing for the whole module. */ static inline uint32_t pack_px(uint8_t r, uint8_t g, uint8_t b) { return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; } /* drm:fill(r, g, b) — flood the BACK buffer with one colour. */ static int l_drm_fill(lua_State *L) { drm_t *d = check_open_drm(L, 1); uint32_t v = pack_px((uint8_t)luaL_checkinteger(L, 2), (uint8_t)luaL_checkinteger(L, 3), (uint8_t)luaL_checkinteger(L, 4)); uint32_t *row = (uint32_t *)d->rowbuf; for (int x = 0; x < d->xres; x++) row[x] = v; drm_buf_t *b = &d->buf[d->back]; size_t row_bytes = (size_t)d->xres * 4; for (int y = 0; y < d->yres; y++) memcpy(b->map + (size_t)y * b->pitch, d->rowbuf, row_bytes); return 0; } /* drm:blit(surface, dx, dy, k) * * Blits a term.gfx surface into the BACK buffer with its top-left at * dx,dy (1-indexed), each source pixel expanded to a k×k block (k >= 1, * default 1). Source alpha is ignored. Each scaled row is packed once * into the row scratch and memcpy'd into the k device rows it covers; * the write is clipped to the mode bounds. */ static int l_drm_blit(lua_State *L) { drm_t *d = check_open_drm(L, 1); gfx_surface_t *s = (gfx_surface_t *)luaL_checkudata(L, 2, GFX_SURFACE_MT); int dx0 = (int)luaL_checkinteger(L, 3) - 1; int dy0 = (int)luaL_checkinteger(L, 4) - 1; int k = (int)luaL_optinteger(L, 5, 1); if (k < 1) return luaL_error(L, "invalid scale factor %d", k); if (!s->px) return 0; drm_buf_t *b = &d->buf[d->back]; /* horizontal clip, computed once: visible dest x range [vx0, vx1) */ int vx0 = dx0 < 0 ? 0 : dx0; int vx1 = dx0 + s->w * k; if (vx1 > d->xres) vx1 = d->xres; if (vx1 <= vx0) return 0; int vis_w = vx1 - vx0; size_t row_bytes = (size_t)vis_w * 4; for (int sy = 0; sy < s->h; sy++) { int fy0 = dy0 + sy * k; /* first device row of this source row's k-band */ if (fy0 + k <= 0) continue; if (fy0 >= d->yres) break; /* pack the visible span of this source row, k-replicated */ const uint8_t *sp = s->px + (size_t)sy * s->w * 4; int sc = (vx0 - dx0) / k; int rem = k - ((vx0 - dx0) % k); uint32_t *out = (uint32_t *)d->rowbuf; for (int x = 0; x < vis_w; x++) { const uint8_t *p = sp + (size_t)sc * 4; *out++ = pack_px(p[0], p[1], p[2]); if (--rem == 0) { sc++; rem = k; } } for (int r = 0; r < k; r++) { int fy = fy0 + r; if (fy < 0 || fy >= d->yres) continue; memcpy(b->map + (size_t)fy * b->pitch + (size_t)vx0 * 4, d->rowbuf, row_bytes); } } return 0; } /* drm:flip() -> true | nil, err * * Queue a vsynced page flip to the back buffer and swap roles. The old * front buffer keeps scanning out until vblank, so the caller must wait * for the completion event (fd readable -> handle_event()) before the * next blit into it. */ static int l_drm_flip(lua_State *L) { drm_t *d = check_open_drm(L, 1); if (d->flip_pending) { lua_pushnil(L); lua_pushstring(L, "flip pending"); return 2; } struct drm_mode_crtc_page_flip f; memset(&f, 0, sizeof(f)); f.crtc_id = d->crtc_id; f.fb_id = d->buf[d->back].fb_id; f.flags = DRM_MODE_PAGE_FLIP_EVENT; int r; do { r = ioctl(d->fd, DRM_IOCTL_MODE_PAGE_FLIP, &f); } while (r == -1 && errno == EINTR); if (r == -1) { lua_pushnil(L); lua_pushfstring(L, "page flip failed (%s)", strerror(errno)); return 2; } d->flip_pending = 1; d->back ^= 1; lua_pushboolean(L, 1); return 1; } /* drm:handle_event() -> bool — drain the event stream after the fd went * readable; true iff a flip completed (clears the pending state). */ static int l_drm_handle_event(lua_State *L) { drm_t *d = check_open_drm(L, 1); lua_pushboolean(L, drain_events(d)); return 1; } /* drm:pending() -> bool — a queued flip whose event hasn't been drained. */ static int l_drm_pending(lua_State *L) { drm_t *d = check_open_drm(L, 1); lua_pushboolean(L, d->flip_pending); return 1; } /* drm:close() — restore the prior CRTC, drop master, release everything; * idempotent (also runs from __gc). */ static int l_drm_close(lua_State *L) { drm_t *d = check_drm(L, 1); drm_release(d); return 0; } static const luaL_Reg drm_methods[] = { {"size", l_drm_size }, {"fd", l_drm_fd }, {"info", l_drm_info }, {"fill", l_drm_fill }, {"blit", l_drm_blit }, {"flip", l_drm_flip }, {"handle_event", l_drm_handle_event}, {"pending", l_drm_pending }, {"close", l_drm_close }, {NULL, NULL } }; static const luaL_Reg module_funcs[] = { {"open", l_drm_open}, {NULL, NULL } }; int luaopen_term_drm_core(lua_State *L) { luaL_newmetatable(L, DRM_MT); lua_newtable(L); luaL_register(L, NULL, drm_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_drm_close); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); luaL_newlib(L, module_funcs); return 1; }