// SPDX-FileCopyrightText: © 2026 Vladimir Zorin // SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later // Licensed under OWL v1.0+. See LICENSE. /* term.gfx_core — a raw RGBA pixel surface for the Kitty graphics layer. * * Holds an 8-bit-per-channel, non-premultiplied RGBA buffer (row-major, * 4 bytes per pixel) behind a Lua userdata. Everything the animation/sprite * layers do at 30–60 fps — fill, blit/compositing, and per-frame * serialization — runs here in C; the convenience drawing (line, circle) * stays in Lua on top of `pixel`. * * Coordinates are 1-indexed at the Lua boundary to match the rest of the * gfx module; they are converted to 0-indexed internally. All pixel/rect/ * blit operations clip silently — drawing partly or wholly off-surface is a * no-op for the out-of-bounds part, never an error. */ #include #include #include #include #include #include #include #include #include #include #include "gfx_core.h" #define RETURN_ERR(L) \ do { \ lua_pushnil(L); \ lua_pushstring(L, strerror(errno)); \ return 2; \ } while (0) #define SURFACE_MT GFX_SURFACE_MT /* Per-axis cap. 32767² × 4 bytes ≈ 4 GiB, well within the size_t overflow * guard below; the cap mostly rejects nonsense sizes early with a clear error. */ #define MAX_DIM 32767 /* blend modes */ #define BLEND_REPLACE 0 #define BLEND_ALPHA 1 #define BLEND_ADD 2 /* flip flags (bitmask: 1 = horizontal, 2 = vertical) */ #define FLIP_H 1 #define FLIP_V 2 /* ── helpers ───────────────────────────────────────────────────────────── */ static gfx_surface_t *check_surface(lua_State *L, int idx) { return (gfx_surface_t *)luaL_checkudata(L, idx, SURFACE_MT); } /* Validate dimensions and compute the byte size without overflow. Returns 0 * and sets *out on success, -1 on invalid/overflowing dimensions. */ static int surface_bytes(int w, int h, size_t *out) { if (w <= 0 || h <= 0 || w > MAX_DIM || h > MAX_DIM) return -1; size_t bytes = (size_t)w * (size_t)h; if (bytes > SIZE_MAX / 4) return -1; *out = bytes * 4; return 0; } static inline uint8_t clamp8(int v) { if (v < 0) return 0; if (v > 255) return 255; return (uint8_t)v; } /* ── construction ──────────────────────────────────────────────────────── */ /* gfx_core.surface(w, h) -> surface (zero-filled = fully transparent) */ static int l_surface_new(lua_State *L) { int w = (int)luaL_checkinteger(L, 1); int h = (int)luaL_checkinteger(L, 2); size_t bytes; if (surface_bytes(w, h, &bytes) < 0) return luaL_error(L, "invalid surface dimensions %dx%d", w, h); /* Create the userdata before allocating the buffer so __gc owns every * later failure path (lua_newuserdata can longjmp on OOM). */ gfx_surface_t *s = (gfx_surface_t *)lua_newuserdata(L, sizeof(gfx_surface_t)); s->w = w; s->h = h; s->px = NULL; luaL_getmetatable(L, SURFACE_MT); lua_setmetatable(L, -2); s->px = (uint8_t *)calloc(1, bytes); if (!s->px) { lua_pushnil(L); lua_pushstring(L, "out of memory allocating surface"); return 2; } return 1; } static int l_surface_gc(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); free(s->px); s->px = NULL; return 0; } /* ── geometry ──────────────────────────────────────────────────────────── */ static int l_surface_width(lua_State *L) { lua_pushinteger(L, check_surface(L, 1)->w); return 1; } static int l_surface_height(lua_State *L) { lua_pushinteger(L, check_surface(L, 1)->h); return 1; } /* s:resize(w, h) -> true | nil, err (preserves the top-left overlap) */ static int l_surface_resize(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); int nw = (int)luaL_checkinteger(L, 2); int nh = (int)luaL_checkinteger(L, 3); if (nw == s->w && nh == s->h) { lua_pushboolean(L, 1); return 1; } size_t bytes; if (surface_bytes(nw, nh, &bytes) < 0) return luaL_error(L, "invalid surface dimensions %dx%d", nw, nh); uint8_t *np = (uint8_t *)calloc(1, bytes); if (!np) { lua_pushnil(L); lua_pushstring(L, "out of memory resizing surface"); return 2; } int cw = nw < s->w ? nw : s->w; int ch = nh < s->h ? nh : s->h; for (int y = 0; y < ch; y++) memcpy(np + (size_t)y * nw * 4, s->px + (size_t)y * s->w * 4, (size_t)cw * 4); free(s->px); s->px = np; s->w = nw; s->h = nh; lua_pushboolean(L, 1); return 1; } /* ── solid fills ───────────────────────────────────────────────────────── */ /* s:fill(r, g, b, a) — flood the whole surface with one colour. */ static int l_surface_fill(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); uint8_t r = clamp8((int)luaL_checkinteger(L, 2)); uint8_t g = clamp8((int)luaL_checkinteger(L, 3)); uint8_t b = clamp8((int)luaL_checkinteger(L, 4)); uint8_t a = clamp8((int)luaL_optinteger(L, 5, 255)); size_t n = (size_t)s->w * s->h; uint8_t *p = s->px; for (size_t i = 0; i < n; i++) { *p++ = r; *p++ = g; *p++ = b; *p++ = a; } return 0; } /* s:clear() — fill fully transparent (memset 0). */ static int l_surface_clear(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); memset(s->px, 0, (size_t)s->w * s->h * 4); return 0; } /* ── single pixel ──────────────────────────────────────────────────────── */ /* s:pixel(x, y, r, g, b, a) — 1-indexed, clipped (off-surface is a no-op). */ static int l_surface_pixel(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); int x = (int)luaL_checkinteger(L, 2) - 1; int y = (int)luaL_checkinteger(L, 3) - 1; if (x < 0 || y < 0 || x >= s->w || y >= s->h) return 0; uint8_t *p = s->px + ((size_t)y * s->w + x) * 4; p[0] = clamp8((int)luaL_checkinteger(L, 4)); p[1] = clamp8((int)luaL_checkinteger(L, 5)); p[2] = clamp8((int)luaL_checkinteger(L, 6)); p[3] = clamp8((int)luaL_optinteger(L, 7, 255)); return 0; } /* s:get(x, y) -> r, g, b, a | nil (nil when out of bounds) */ static int l_surface_get(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); int x = (int)luaL_checkinteger(L, 2) - 1; int y = (int)luaL_checkinteger(L, 3) - 1; if (x < 0 || y < 0 || x >= s->w || y >= s->h) { lua_pushnil(L); return 1; } uint8_t *p = s->px + ((size_t)y * s->w + x) * 4; lua_pushinteger(L, p[0]); lua_pushinteger(L, p[1]); lua_pushinteger(L, p[2]); lua_pushinteger(L, p[3]); return 4; } /* ── rectangle ─────────────────────────────────────────────────────────── */ /* s:rect(x1, y1, x2, y2, r, g, b, a, filled) — 1-indexed inclusive corners, * clipped. `filled` true paints the interior; false draws the 1px outline. */ static int l_surface_rect(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); int x1 = (int)luaL_checkinteger(L, 2) - 1; int y1 = (int)luaL_checkinteger(L, 3) - 1; int x2 = (int)luaL_checkinteger(L, 4) - 1; int y2 = (int)luaL_checkinteger(L, 5) - 1; uint8_t r = clamp8((int)luaL_checkinteger(L, 6)); uint8_t g = clamp8((int)luaL_checkinteger(L, 7)); uint8_t b = clamp8((int)luaL_checkinteger(L, 8)); uint8_t a = clamp8((int)luaL_optinteger(L, 9, 255)); int filled = lua_toboolean(L, 10); if (x1 > x2) { int t = x1; x1 = x2; x2 = t; } if (y1 > y2) { int t = y1; y1 = y2; y2 = t; } for (int y = y1; y <= y2; y++) { if (y < 0 || y >= s->h) continue; int edge_row = (y == y1 || y == y2); for (int x = x1; x <= x2; x++) { if (x < 0 || x >= s->w) continue; if (!filled && !edge_row && x != x1 && x != x2) continue; uint8_t *p = s->px + ((size_t)y * s->w + x) * 4; p[0] = r; p[1] = g; p[2] = b; p[3] = a; } } return 0; } /* ── blit / compositing ────────────────────────────────────────────────── */ /* Composite one already-tinted source pixel onto dp. Straight * (non-premultiplied) alpha, integer math: out = (src*a + dst*(255-a)) / 255. * Callers skip the call entirely when sa <= 0 and blend != REPLACE. */ static inline void composite_px(uint8_t *dp, int sr, int sg, int sb, int sa, int blend) { if (blend == BLEND_REPLACE) { dp[0] = clamp8(sr); dp[1] = clamp8(sg); dp[2] = clamp8(sb); dp[3] = clamp8(sa); } else if (blend == BLEND_ADD) { dp[0] = clamp8(dp[0] + sr * sa / 255); dp[1] = clamp8(dp[1] + sg * sa / 255); dp[2] = clamp8(dp[2] + sb * sa / 255); /* additive leaves destination alpha as-is */ } else { /* BLEND_ALPHA */ int ia = 255 - sa; dp[0] = clamp8((sr * sa + dp[0] * ia) / 255); dp[1] = clamp8((sg * sa + dp[1] * ia) / 255); dp[2] = clamp8((sb * sa + dp[2] * ia) / 255); dp[3] = clamp8(sa + dp[3] * ia / 255); } } /* s:blit(src, dx, dy, sx, sy, sw, sh, flip, tr, tg, tb, ta, galpha, blend, * dw, dh, rot) * * Copies a `sw`x`sh` region of `src` (top-left at sx,sy) onto `s` with its * top-left at dx,dy. Everything is 1-indexed and clipped against both * surfaces. `flip` is a bitmask (1=h, 2=v). `tr,tg,tb,ta` multiply the source * channels (255 = unchanged). `galpha` is a global opacity (255 = opaque). * `blend` is 0=replace, 1=alpha (default), 2=add. Straight (non-premultiplied) * alpha, integer math: out = (src*a + dst*(255-a)) / 255. * `rot` (radians, default 0) rotates the scaled dw*dh rect around its centre * (positive = clockwise on screen, y-down): the destination bounding box is * walked and each pixel inverse-rotated back into the unrotated rect, then * nearest-neighbour sampled — so rot composes freely with dw/dh scaling, * flip, tint, and every blend mode. rot == 0 takes the bit-for-bit identical * axis-aligned fast path. */ static int l_surface_blit(lua_State *L) { gfx_surface_t *dst = check_surface(L, 1); gfx_surface_t *src = check_surface(L, 2); int dx = (int)luaL_checkinteger(L, 3) - 1; int dy = (int)luaL_checkinteger(L, 4) - 1; int sx0 = (int)luaL_optinteger(L, 5, 1) - 1; int sy0 = (int)luaL_optinteger(L, 6, 1) - 1; int sw = (int)luaL_optinteger(L, 7, src->w); int sh = (int)luaL_optinteger(L, 8, src->h); int flip = (int)luaL_optinteger(L, 9, 0); int tr = (int)luaL_optinteger(L, 10, 255); int tg = (int)luaL_optinteger(L, 11, 255); int tb = (int)luaL_optinteger(L, 12, 255); int ta = (int)luaL_optinteger(L, 13, 255); int galpha = (int)luaL_optinteger(L, 14, 255); int blend = (int)luaL_optinteger(L, 15, BLEND_ALPHA); /* Destination size: defaults to the source size (1:1, the historic * behaviour). When dw/dh differ, the source rect is nearest-neighbour * scaled into the dw*dh destination rect. dw==sw && dh==sh reduces to the * identity mapping, so the 1:1 path is bit-for-bit unchanged. */ int dw = (int)luaL_optinteger(L, 16, sw); int dh = (int)luaL_optinteger(L, 17, sh); double rot = (double)luaL_optnumber(L, 18, 0.0); if (dw < 1 || dh < 1 || sw < 1 || sh < 1) return 0; int flip_h = (flip & FLIP_H) != 0; int flip_v = (flip & FLIP_V) != 0; if (rot != 0.0) { /* Rotated path: walk the destination bounding box of the rotated * dw*dh rect and inverse-rotate each pixel centre back into the * unrotated rect (then the usual nearest-neighbour source sampling). * Sampling at pixel CENTRES (+0.5) keeps the mapping stable against * floating-point noise at exact right angles. */ double co = cos(rot), si_ = sin(rot); double cxd = (double)dx + dw * 0.5; /* centre of the unrotated rect */ double cyd = (double)dy + dh * 0.5; double hw = (dw * fabs(co) + dh * fabs(si_)) * 0.5; double hh = (dw * fabs(si_) + dh * fabs(co)) * 0.5; int bx0 = (int)floor(cxd - hw), bx1 = (int)ceil(cxd + hw); int by0 = (int)floor(cyd - hh), by1 = (int)ceil(cyd + hh); if (bx0 < 0) bx0 = 0; if (by0 < 0) by0 = 0; if (bx1 > dst->w - 1) bx1 = dst->w - 1; if (by1 > dst->h - 1) by1 = dst->h - 1; for (int dyy = by0; dyy <= by1; dyy++) { double py = ((double)dyy + 0.5) - cyd; for (int dxx = bx0; dxx <= bx1; dxx++) { double px_ = ((double)dxx + 0.5) - cxd; /* inverse rotation of the dest offset (y-down coords) */ double fi = (px_ * co + py * si_) + dw * 0.5; double fj = (-px_ * si_ + py * co) + dh * 0.5; if (fi < 0 || fi >= (double)dw || fj < 0 || fj >= (double)dh) continue; int i = (int)fi; int j = (int)fj; int sic = (i * sw) / dw; int sjc = (j * sh) / dh; int scol = sx0 + (flip_h ? (sw - 1 - sic) : sic); int srow = sy0 + (flip_v ? (sh - 1 - sjc) : sjc); if (scol < 0 || scol >= src->w || srow < 0 || srow >= src->h) continue; uint8_t *sp = src->px + ((size_t)srow * src->w + scol) * 4; int sr = sp[0] * tr / 255; int sg = sp[1] * tg / 255; int sb = sp[2] * tb / 255; int sa = sp[3] * ta / 255; sa = sa * galpha / 255; if (sa <= 0 && blend != BLEND_REPLACE) continue; composite_px(dst->px + ((size_t)dyy * dst->w + dxx) * 4, sr, sg, sb, sa, blend); } } return 0; } for (int j = 0; j < dh; j++) { int dyy = dy + j; if (dyy < 0 || dyy >= dst->h) continue; int sj = (j * sh) / dh; /* source row offset within [0, sh) */ int srow = sy0 + (flip_v ? (sh - 1 - sj) : sj); if (srow < 0 || srow >= src->h) continue; for (int i = 0; i < dw; i++) { int dxx = dx + i; if (dxx < 0 || dxx >= dst->w) continue; int si = (i * sw) / dw; /* source col offset within [0, sw) */ int scol = sx0 + (flip_h ? (sw - 1 - si) : si); if (scol < 0 || scol >= src->w) continue; uint8_t *sp = src->px + ((size_t)srow * src->w + scol) * 4; /* apply tint (channel multiply, 255 = identity) */ int sr = sp[0] * tr / 255; int sg = sp[1] * tg / 255; int sb = sp[2] * tb / 255; int sa = sp[3] * ta / 255; sa = sa * galpha / 255; /* global opacity folds into source alpha */ if (sa <= 0 && blend != BLEND_REPLACE) continue; composite_px(dst->px + ((size_t)dyy * dst->w + dxx) * 4, sr, sg, sb, sa, blend); } } return 0; } /* ── serialization / transport ─────────────────────────────────────────── */ /* s:tostring() -> raw RGBA bytes (w*h*4), ready for a Kitty f=32 transmit. */ static int l_surface_tostring(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); lua_pushlstring(L, (const char *)s->px, (size_t)s->w * s->h * 4); return 1; } /* s:write_shm(name) -> 0 | nil, err * * Creates a POSIX shared-memory object `name` sized to the surface and copies * the RGBA bytes straight in — no intermediate Lua string. The object is NOT * unlinked here: the terminal unlinks it after reading (Kitty shm transport * contract), exactly like std.create_shm. */ static int l_surface_write_shm(lua_State *L) { gfx_surface_t *s = check_surface(L, 1); const char *name = luaL_checkstring(L, 2); size_t len = (size_t)s->w * s->h * 4; int fd = shm_open(name, O_CREAT | O_RDWR | O_CLOEXEC, 0666); if (fd == -1) RETURN_ERR(L); if (ftruncate(fd, len) == -1) { close(fd); shm_unlink(name); RETURN_ERR(L); } void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (ptr == MAP_FAILED) { close(fd); shm_unlink(name); RETURN_ERR(L); } memcpy(ptr, s->px, len); munmap(ptr, len); close(fd); lua_pushinteger(L, 0); return 1; } /* ── registration ──────────────────────────────────────────────────────── */ static const luaL_Reg surface_methods[] = { {"width", l_surface_width }, {"height", l_surface_height }, {"resize", l_surface_resize }, {"fill", l_surface_fill }, {"clear", l_surface_clear }, {"pixel", l_surface_pixel }, {"get", l_surface_get }, {"rect", l_surface_rect }, {"blit", l_surface_blit }, {"tostring", l_surface_tostring }, {"write_shm", l_surface_write_shm}, {NULL, NULL } }; static const luaL_Reg module_funcs[] = { {"surface", l_surface_new}, {NULL, NULL } }; int luaopen_term_gfx_core(lua_State *L) { luaL_newmetatable(L, SURFACE_MT); lua_newtable(L); luaL_register(L, NULL, surface_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_surface_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); luaL_newlib(L, module_funcs); return 1; }