diff --git a/Makefile b/Makefile index 2da950f..07ac46f 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,13 @@ EXPORTS = \ -Wl,--export=output \ -Wl,--export=length \ -Wl,--export=measure \ + -Wl,--export=graphics_size \ + -Wl,--export=graphics_init \ + -Wl,--export=image_begin \ + -Wl,--export=image_commit \ + -Wl,--export=image_remove \ + -Wl,--export=graphics_capability \ + -Wl,--export=graphics_resize_prepare \ -Wl,--export=Clay_SetPointerState \ -Wl,--export=pointer_over_count \ -Wl,--export=pointer_over_id_string_length \ diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index e159935..370137d 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -906,7 +906,8 @@ This encoding has been extended incrementally (floating, clip, and scroll groups were added after the initial protocol) but has never been restructured. It is likely to remain stable in structure while continuing to grow. However, specific opcode values, mask definitions, and field layouts are implementation details -and are not locked down by this specification. +and are not locked down by this specification. The image element added one more +opcode on the same extension pattern (Graphics Specification §6.1). ### 12.2 Directive property groups @@ -985,6 +986,9 @@ The `text()` constructor accepts: `color`, `bg`, `fontSize`, `letterSpacing`, `lineHeight`, and attribute flags (`bold`, `italic`, `underline`, `strikethrough`). +The `img()` constructor accepts `image` (a pixel-surface registry id), `alt`, +`width`/`height` sizing axes, `variant`, and `bg` (Graphics Specification §6.1). + These property groups represent the current implementation surface. New groups and fields have been added incrementally and more may follow. @@ -1113,6 +1117,12 @@ the most recent `render()` call. Each error is a `ClayError` object with: single cell than the cell can store. The excess marks are truncated (see §13, Cell representation). Reported at most once per frame, on the first truncation. The `message` SHOULD identify the per-cell limit. + - `"IMAGE_NOT_FOUND"` — An `img` directive referenced a registry id that is + not live; the element rendered at the alt tier (Graphics Specification §13). + - `"IMAGE_PLACEMENTS_EXCEEDED"` — More kitty placements were required in one + frame than the placements record holds (Graphics Specification §5.4, §13). + - `"IMAGE_PLACEMENT_COLLISION"` — Two elements in one frame derived the same + placement id (Graphics Specification §9.2.1, §13). - `message`: a human-readable string describing the error in detail. Errors are collected per-render; each call to `render()` returns only the errors diff --git a/src/clayterm.c b/src/clayterm.c index 7e4e62e..5078991 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -35,6 +35,10 @@ struct Clayterm *ct_active_context = NULL; #define OP_TEXT 0x03 #define OP_CLOSE_ELEMENT 0x04 #define OP_END_LAYOUT 0x05 +/* OP_IMG (0x06): the void image directive (Graphics Specification §6.1). + * Decoded into a balanced open/close pair at walk time; pixel tiers attach + * the op's wire address as customData for the render-command walk. */ +#define OP_IMG 0x06 #define PROP_LAYOUT 0x01 #define PROP_BG_COLOR 0x02 @@ -55,6 +59,11 @@ struct Clayterm *ct_active_context = NULL; * Mirrored by ERROR_TYPES in term.ts. */ #define CLAYTERM_ERR_CLIP_DEPTH_EXCEEDED 9 #define CLAYTERM_ERR_COMBINING_MARKS_EXCEEDED 10 +#define CLAYTERM_ERR_IMAGE_NOT_FOUND 11 +#define CLAYTERM_ERR_IMAGE_PLACEMENTS_EXCEEDED 12 +#define CLAYTERM_ERR_IMAGE_PLACEMENT_COLLISION 13 + +#define MAX_ERROR_MESSAGE 96 #define CLAYTERM_STR_(x) #x #define CLAYTERM_STR(x) CLAYTERM_STR_(x) @@ -86,7 +95,15 @@ struct Clayterm { /* error collection */ Clay_ErrorData errors[MAX_ERRORS]; int error_count; + /* dynamic message pool for image errors (each message bounds to + * MAX_ERROR_MESSAGE bytes; Graphics Specification §13) */ + char msg_pool[MAX_ERRORS * MAX_ERROR_MESSAGE]; + int msg_used; int animating_count; + /* graphics substrate (Graphics Specification): the pixel-surface + * registry, tier engine, and placements record live in their own + * dimension-independent carve — set by init(), survives resize (§5.1) */ + struct Graphics *gfx; /* Caret state for hardware-cursor management. The renderer records the * first text node carrying a caret declaration per frame, precomputes the * caret's byte offset into that node's content, then places the cell as @@ -600,6 +617,227 @@ static Clay_SizingAxis decode_axis(uint32_t *buf, int len, int *i) { return axis; } +/* ── Image element (Graphics Specification §6.1) ──────────────────── */ + +/* OP_IMG wire layout (packed by ops.ts): + * [opcode][id_len][id bytes padded][image u32, 0 = omitted] + * [variant u8 | bg_flag u8 | pad u16][width axis 3 words] + * [height axis 3 words][alt_len][alt bytes padded][bg u32 when bg_flag] */ +struct ImgOp { + char *id_chars; + uint32_t id_len; + uint32_t image_id; + uint8_t variant; + uint8_t bg_flag; + uint32_t bg; + char *alt_chars; + uint32_t alt_len; + Clay_SizingAxis waxis; + Clay_SizingAxis haxis; +}; + +static void decode_img_op(uint32_t *buf, int len, int *i, struct ImgOp *o) { + uint32_t id_len = rd(buf, len, i); + int id_words = (id_len + 3) / 4; + o->id_chars = (char *)&buf[*i]; + o->id_len = id_len; + *i += id_words; + + o->image_id = rd(buf, len, i); + uint32_t cfg = rd(buf, len, i); + o->variant = cfg & 0xff; + o->bg_flag = (cfg >> 8) & 0xff; + + o->waxis = decode_axis(buf, len, i); + o->haxis = decode_axis(buf, len, i); + + uint32_t alt_len = rd(buf, len, i); + int alt_words = (alt_len + 3) / 4; + o->alt_chars = (char *)&buf[*i]; + o->alt_len = alt_len; + *i += alt_words; + + o->bg = o->bg_flag ? rd(buf, len, i) : 0; +} + +/* Dynamic image error messages land in the carve's message pool (§13). + * Hand-rolled formatting: no stdio in freestanding wasm. */ +static int img_msg_copy(char *dst, int *n, const char *s) { + while (*s != 0 && *n < MAX_ERROR_MESSAGE - 1) { + dst[(*n)++] = *s++; + } + return *n; +} + +static void report_img_error(struct Clayterm *ct, int code, struct ImgOp *o) { + if (ct->error_count >= MAX_ERRORS) { + return; + } + if (ct->msg_used + MAX_ERROR_MESSAGE > (int)sizeof(ct->msg_pool)) { + return; + } + char *msg = ct->msg_pool + ct->msg_used; + ct->msg_used += MAX_ERROR_MESSAGE; + int n = 0; + n += img_msg_copy(msg + n, &n, "image element '"); + uint32_t id_show = o->id_len; + for (uint32_t k = 0; k < id_show && n < MAX_ERROR_MESSAGE - 1; k++) { + msg[n++] = o->id_chars[k]; + } + n += img_msg_copy(msg + n, &n, "'"); + if (code == CLAYTERM_ERR_IMAGE_NOT_FOUND) { + n += img_msg_copy(msg + n, &n, " references unknown registry id "); + /* decimal registry id */ + char dec[10]; + int m = 0; + uint32_t v = o->image_id; + do { + dec[m++] = (char)('0' + (v % 10)); + v /= 10; + } while (v > 0); + for (int k = m - 1; k >= 0 && n < MAX_ERROR_MESSAGE - 1; k--) { + msg[n++] = dec[k]; + } + } else if (code == CLAYTERM_ERR_IMAGE_PLACEMENTS_EXCEEDED) { + n += img_msg_copy(msg + n, &n, " exceeds placement table capacity"); + } else { + n += img_msg_copy(msg + n, &n, " placement id collision"); + } + ct->errors[ct->error_count++] = (Clay_ErrorData){ + .errorType = (Clay_ErrorType)code, + .errorText = {.isStaticallyAllocated = false, .length = n, .chars = msg}, + .userData = ct, + }; +} + +/* Expand the img directive into its Clay element (§15 transfer note): + * a balanced open/close pair; the alt tier adds a wrapped text child, the + * pixel tiers attach the op's wire address as customData for the + * render-command walk. Tier resolution reads only inputs stable within a + * frame (variant, omission, registry liveness, evidence, line mode); the + * demotions that need layout/clip state happen later and are + * footprint-preserving (§8.5), so the sizing decided here stays valid. */ +static void decode_img_element(struct Clayterm *ct, uint32_t *buf, int len, + int *i, int op_start, int mode) { + struct ImgOp o; + decode_img_op(buf, len, i, &o); + + struct ImageEntry *entry = + o.image_id != 0 ? graphics_entry(ct->gfx, o.image_id) : NULL; + uint8_t tier = graphics_resolve_tier(ct, o.image_id, o.variant, mode); + if (o.image_id != 0 && (entry == NULL || !entry->live)) { + report_img_error(ct, CLAYTERM_ERR_IMAGE_NOT_FOUND, &o); + tier = IMG_TIER_ALT; + } + + Clay_String str = {.length = (int32_t)o.id_len, .chars = o.id_chars}; + Clay_ElementId eid = Clay__HashString(str, 0); + Clay__OpenElementWithId(eid); + + Clay_ElementDeclaration decl = {0}; + decl.layout.sizing.width = o.waxis; + decl.layout.sizing.height = o.haxis; + if (tier != IMG_TIER_ALT && entry != NULL) { + /* fit() resolves to the intrinsic cell size under the half-row pixel + * convention (§8.3): W columns × ceil(H/2) rows */ + if (o.waxis.type == CLAY__SIZING_TYPE_FIT) { + decl.layout.sizing.width.type = CLAY__SIZING_TYPE_FIXED; + decl.layout.sizing.width.size.minMax.min = (float)entry->width; + decl.layout.sizing.width.size.minMax.max = (float)entry->width; + } + if (o.haxis.type == CLAY__SIZING_TYPE_FIT) { + float rows = (float)((entry->height + 1) / 2); + decl.layout.sizing.height.type = CLAY__SIZING_TYPE_FIXED; + decl.layout.sizing.height.size.minMax.min = rows; + decl.layout.sizing.height.size.minMax.max = rows; + } + } + if (o.bg_flag != 0) { + /* cells cannot be transparent: the declared bg's RGB paints, alpha is + * dropped at the cell (the value space is open({bg})'s rgba()) */ + decl.backgroundColor = unpack_color(o.bg); + decl.backgroundColor.a = 255; + } + if (tier == IMG_TIER_ALT) { + /* the box clips its own alt-text child at the box height (§9.4) */ + decl.clip.vertical = 1; + } else { + decl.custom.customData = (void *)&buf[op_start]; + } + Clay__ConfigureOpenElement(decl); + + if (tier == IMG_TIER_ALT) { + Clay_String alt = {.length = (int32_t)o.alt_len, .chars = o.alt_chars}; + Clay_TextElementConfig config = {0}; + config.wrapMode = CLAY_TEXT_WRAP_WORDS; + Clay__OpenTextElement(alt, config); + } + Clay__CloseElement(); +} + +/* The render-command walk's img handler: demotions first (containment + * §8.5, collision §9.2.1, overlap §10.3, capacity §5.4 — every one + * footprint-preserving), then kitty record / ascii art. `op_start` is the + * opcode word's index in buf (the customData payload). */ +static void render_custom_img(struct Clayterm *ct, uint32_t *buf, int len, + int op_start, int x0, int y0, int x1, int y1, + int mode) { + uint32_t *op = buf + op_start; + struct ImgOp o; + int oi = 1; /* skip the opcode word */ + decode_img_op(op, len - op_start, &oi, &o); + + struct ImageEntry *entry = + o.image_id != 0 ? graphics_entry(ct->gfx, o.image_id) : NULL; + uint8_t tier = graphics_resolve_tier(ct, o.image_id, o.variant, mode); + if (entry == NULL || !entry->live) { + return; /* alt tier never reaches CUSTOM (no custom config) */ + } + + struct ImagePlacement fp = graphics_contain_fit(x0, y0, x1 - x0, y1 - y0, + entry->width, entry->height); + if (fp.w <= 0 || fp.h <= 0) { + return; /* empty footprint paints nothing (§8.2) */ + } + + if (tier == IMG_TIER_KITTY) { + /* containment gate (§8.5): the terminal cannot clip a placement */ + if (ct->clipping) { + int inside = fp.x >= ct->clipx && fp.y >= ct->clipy && + fp.x + fp.w <= ct->clipx + ct->clipw && + fp.y + fp.h <= ct->clipy + ct->cliph; + if (!inside) { + tier = IMG_TIER_ASCII; + } + } + uint32_t pid = 0; + if (tier == IMG_TIER_KITTY) { + pid = graphics_placement_id(o.id_chars, o.id_len, o.image_id); + if (graphics_placement_collision(ct, pid)) { + report_img_error(ct, CLAYTERM_ERR_IMAGE_PLACEMENT_COLLISION, &o); + tier = IMG_TIER_ASCII; + } + } + if (tier == IMG_TIER_KITTY) { + struct ImagePlacement *p = + graphics_add_placement(ct, o.image_id, pid, entry->data_version); + if (p == NULL) { + report_img_error(ct, CLAYTERM_ERR_IMAGE_PLACEMENTS_EXCEEDED, &o); + tier = IMG_TIER_ASCII; + } else { + p->x = fp.x; + p->y = fp.y; + p->w = fp.w; + p->h = fp.h; + p->blank_bg = o.bg_flag ? (o.bg & COLOR_MASK) : ATTR_DEFAULT; + } + } + } + if (tier == IMG_TIER_ASCII) { + graphics_paint_ascii(ct, entry, &fp); + } +} + /* ── Public API ───────────────────────────────────────────────────── */ static int align64(int n) { return (n + 63) & ~63; } @@ -625,11 +863,14 @@ int clayterm_size(int w, int h) { int cell_count = w * h; int cell_bytes = cell_count * (int)sizeof(Cell); int out_bytes = cell_count * OUT_BYTES_PER_CELL; + int msg_bytes = MAX_ERRORS * MAX_ERROR_MESSAGE; int clay_bytes = (int)Clay_MinMemorySize(); return align8((int)sizeof(struct Clayterm)) + align8(cell_bytes) /* front */ + align8(cell_bytes) /* back */ - + align8(out_bytes) /* output buffer */ - + align64(clay_bytes); /* Clay arena */ + + align8(out_bytes) /* output buffer */ + + align8(msg_bytes) /* dynamic error messages */ + + align64(clay_bytes) /* Clay arena */ + ; } static void clay_error(Clay_ErrorData err) { @@ -681,15 +922,17 @@ int error_message_ptr(struct Clayterm *ct, int index) { return (int)ct->errors[index].errorText.chars; } -struct Clayterm *init(void *mem, int w, int h) { +struct Clayterm *init(void *mem, int w, int h, struct Graphics *gfx) { set_clay_capacity(w, h); struct Clayterm *ct = (struct Clayterm *)mem; int cell_count = w * h; int cell_bytes = align8(cell_count * (int)sizeof(Cell)); int out_bytes = align8(cell_count * OUT_BYTES_PER_CELL); + int msg_bytes = align8(MAX_ERRORS * MAX_ERROR_MESSAGE); char *base = (char *)mem + align8((int)sizeof(struct Clayterm)); - char *clay_mem = base + cell_bytes * 2 + out_bytes; + char *msg_mem = base + cell_bytes * 2 + out_bytes; + char *clay_mem = msg_mem + msg_bytes; int clay_bytes = align64((int)Clay_MinMemorySize()); Clay_Arena arena = Clay_CreateArenaWithCapacityAndMemory(clay_bytes, clay_mem); @@ -706,6 +949,7 @@ struct Clayterm *init(void *mem, int w, int h) { .lastbg = 0xffffffff, .lastx = -1, .lasty = -1, + .gfx = gfx, }; // initialize back buffer with spaces and default fg/bg @@ -721,7 +965,10 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, int i = 0; ct_active_context = ct; ct->error_count = 0; + ct->msg_used = 0; ct->animating_count = 0; + ct->gfx->emitted_placements = 0; + graphics_frame_begin(ct->gfx); ct->caret_text_chars = NULL; ct->caret_text_length = 0; ct->caret_offset_bytes = 0; @@ -737,6 +984,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, while (i < len) { uint32_t op = rd(buf, len, &i); + int op_start = i - 1; switch (op) { case OP_OPEN_ELEMENT: { @@ -908,6 +1156,10 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, Clay__CloseElement(); break; + case OP_IMG: + decode_img_element(ct, buf, len, &i, op_start, mode); + break; + default: break; } @@ -946,6 +1198,16 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, case CLAY_RENDER_COMMAND_TYPE_BORDER: render_border(ct, x0, y0, x1, y1, cmd); break; + case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { + /* the img element's custom command (Graphics Specification): demote + * if needed, then record the kitty placement or paint ascii art */ + if (cmd->renderData.custom.customData != NULL) { + int op_start = + (int)((uint32_t *)cmd->renderData.custom.customData - buf); + render_custom_img(ct, buf, len, op_start, x0, y0, x1, y1, mode); + } + break; + } case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { /* intersect the child box with the current active rect (if any) */ int nx0 = x0, ny0 = y0, nx1 = x1, ny1 = y1; @@ -1024,10 +1286,33 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, } } + /* Graphics §9.2.6 frame ordering, around the cell-write phase: + * coverage pass → stale deletions + transmissions → cell writes → + * placements → cursor restore. Line mode (mode == 1) caps the ladder at + * ascii (§12), so it records no placements and emits no APC bytes: the + * stale/transmission phases are skipped and the placements tables are + * left untouched so the next cursor-update render can reconcile. */ + graphics_cover(ct); + if (mode == 0) { + graphics_emit_before_cells(ct); + } + if (mode == 1) { present_lines(ct); } else { present_cups(ct, row); + graphics_emit_placements(ct, row); + graphics_frame_end(ct->gfx); + if (ct->gfx->emitted_placements) { + /* A2 carve-out: restore the post-frame cursor state §7.6 defines + * (the placement CUPs moved the cursor after the cell writes) */ + if (ct->has_caret) { + emit_cursor(ct, ct->caret_x, ct->caret_y, row); + buf_str(&ct->out, "\x1b[?25h"); + } else if (ct->lastx >= 0 && ct->lasty >= 0) { + emit_cursor(ct, ct->lastx + 1, ct->lasty, row); + } + } } ct_active_context = NULL; diff --git a/src/clayterm.h b/src/clayterm.h index 8a24db4..df30237 100644 --- a/src/clayterm.h +++ b/src/clayterm.h @@ -6,12 +6,13 @@ #include #include "cell.h" +#include "graphics.h" struct Clayterm; /* WASM exports */ int clayterm_size(int w, int h); -struct Clayterm *init(void *mem, int w, int h); +struct Clayterm *init(void *mem, int w, int h, struct Graphics *gfx); void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row, float deltaTime); char *output(struct Clayterm *ct); diff --git a/src/graphics.c b/src/graphics.c new file mode 100644 index 0000000..ba915a6 --- /dev/null +++ b/src/graphics.c @@ -0,0 +1,952 @@ +/* graphics.c — pixel-surface registry and tier engine + * + * Implements the Graphics Specification's substrate: the surface registry + * (§5.3), the tier engine (§9), and the cell-buffer integration phases + * (§10). Included AFTER clayterm.c in module.c so the Clayterm struct + * definition is visible. + * + * No heap: every structure is fixed-capacity, carved once at Term creation + * and never grown, moved, or freed at runtime (§5.1). Reclamation of pixel + * bytes happens only by compaction of the pool. + */ + +#include "graphics.h" + +#include "buffer.h" +#include "cell.h" +#include "mem.h" +#include "shape_vectors.h" + +#include "../clay/clay.h" + +/* Contrast exponents (§9.3.4): baked calibration constants, pinned as + * integer powers of two so the enhancement is exact repeated squaring — + * bit-identical on every host, with no floating-point library. */ +#define GAMMA_D 4.0f +#define GAMMA_G 2.0f + +/* ── Wire helpers ─────────────────────────────────────────────────── */ + +static void buf_u32(Buffer *b, uint32_t v) { + char tmp[10]; + int n = 0; + do { + tmp[n++] = (char)('0' + (v % 10)); + v /= 10; + } while (v > 0); + char out[10]; + for (int i = 0; i < n; i++) { + out[i] = tmp[n - 1 - i]; + } + buf_put(b, out, n); +} + +static const char B64_ALPHABET[64] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', +}; + +/* Encode src[0..len) as base64 into dst; returns the character count + * (ceil(len/3)*4, '=' padded — RFC 4648). dst needs that much room. */ +static uint32_t b64_encode(const char *src, uint32_t len, char *dst) { + uint32_t o = 0; + uint32_t i = 0; + while (i + 3 <= len) { + uint32_t v = ((uint8_t)src[i] << 16) | ((uint8_t)src[i + 1] << 8) | + (uint8_t)src[i + 2]; + dst[o++] = B64_ALPHABET[(v >> 18) & 63]; + dst[o++] = B64_ALPHABET[(v >> 12) & 63]; + dst[o++] = B64_ALPHABET[(v >> 6) & 63]; + dst[o++] = B64_ALPHABET[v & 63]; + i += 3; + } + uint32_t rem = len - i; + if (rem == 1) { + uint32_t v = (uint8_t)src[i] << 16; + dst[o++] = B64_ALPHABET[(v >> 18) & 63]; + dst[o++] = B64_ALPHABET[(v >> 12) & 63]; + dst[o++] = '='; + dst[o++] = '='; + } else if (rem == 2) { + uint32_t v = ((uint8_t)src[i] << 16) | ((uint8_t)src[i + 1] << 8); + dst[o++] = B64_ALPHABET[(v >> 18) & 63]; + dst[o++] = B64_ALPHABET[(v >> 12) & 63]; + dst[o++] = B64_ALPHABET[(v >> 6) & 63]; + dst[o++] = '='; + } + return o; +} + +/* ── Kitty control-block emission (§9.2) ──────────────────────────── */ + +/* Every emitted control block sets q=2 (INV-I5): no acknowledgment and no + * failure response can reach the input parser. */ + +static void kitty_delete_image(struct Clayterm *ct, uint32_t wire_id) { + /* data-freeing form: deletes the image, all its placements, and the + * terminal's stored copy (Graphics Specification §7.2) */ + buf_str(&ct->out, "\x1b_Ga=d,d=I,i="); + buf_u32(&ct->out, wire_id); + buf_str(&ct->out, ",q=2\x1b\\"); +} + +static void kitty_delete_placement(struct Clayterm *ct, uint32_t wire_id, + uint32_t placement_id) { + /* scoped lowercase form: deletes one placement, retains stored data + * (Graphics Specification §9.2.5) */ + buf_str(&ct->out, "\x1b_Ga=d,d=i,i="); + buf_u32(&ct->out, wire_id); + buf_str(&ct->out, ",p="); + buf_u32(&ct->out, placement_id); + buf_str(&ct->out, ",q=2\x1b\\"); +} + +static void kitty_place(struct Clayterm *ct, uint32_t wire_id, + uint32_t placement_id, int cols, int rows) { + buf_str(&ct->out, "\x1b_Ga=p,i="); + buf_u32(&ct->out, wire_id); + buf_str(&ct->out, ",p="); + buf_u32(&ct->out, placement_id); + buf_str(&ct->out, ",c="); + buf_u32(&ct->out, (uint32_t)cols); + buf_str(&ct->out, ",r="); + buf_u32(&ct->out, (uint32_t)rows); + buf_str(&ct->out, ",C=1,q=2\x1b\\"); +} + +/* Transmit a surface's pixels: lazy (first placement), chunked per the + * protocol, delete-before-re-transmit (§9.2.2). */ +static void kitty_transmit(struct Clayterm *ct, struct ImageEntry *e) { + if (e->transmitted_version != 0) { + /* the image exists terminal-side: the protocol requires the existing + * image and all its placements to be deleted before re-transmission */ + kitty_delete_image(ct, e->id); + } + const char *src = ct->gfx->pool + e->pool_offset; + uint32_t len = e->byte_len; + char chunk[IMG_CHUNK_CHARS]; + uint32_t off = 0; + while (off < len) { + uint32_t take = len - off; + if (take > IMG_CHUNK_SRC) { + take = IMG_CHUNK_SRC; + } + uint32_t chars = b64_encode(src + off, take, chunk); + int last = (off + take >= len); + buf_str(&ct->out, "\x1b_G"); + if (off == 0) { + buf_str(&ct->out, "a=t,t=d,f=32,i="); + buf_u32(&ct->out, e->id); + buf_str(&ct->out, ",s="); + buf_u32(&ct->out, e->width); + buf_str(&ct->out, ",v="); + buf_u32(&ct->out, e->height); + } + buf_str(&ct->out, last ? ",m=0,q=2;" : ",m=1,q=2;"); + buf_put(&ct->out, chunk, (int)chars); + buf_str(&ct->out, "\x1b\\"); + off += take; + } + e->transmitted_version = e->data_version; +} + +/* ── Placement-id derivation (§9.2.1/§15; pinned, golden-locked) ──── */ + +uint32_t graphics_placement_id(const char *id_chars, uint32_t id_len, + uint32_t registry_id) { + uint32_t h = 2166136261u; + for (uint32_t i = 0; i < id_len; i++) { + h ^= (uint8_t)id_chars[i]; + h *= 16777619u; + } + h ^= 0x3A; /* ':' separator */ + h *= 16777619u; + char dec[10]; + int n = 0; + uint32_t v = registry_id; + do { + dec[n++] = (char)('0' + (v % 10)); + v /= 10; + } while (v > 0); + for (int i = 0; i < n; i++) { + h ^= (uint8_t)dec[n - 1 - i]; + h *= 16777619u; + } + return h == 0 ? 1u : h; +} + +/* ── Footprint math (§8.2) ────────────────────────────────────────── */ + +static int round_pos(float f) { return (int)(f + 0.5f); } + +/* Freestanding floor/ceil (no libm): exact for the small magnitudes the + * sampling math produces. */ +static int ifloor(float f) { + int i = (int)f; + return i > f ? i - 1 : i; +} + +static int iceil(float f) { + int i = (int)f; + return i < f ? i + 1 : i; +} + +/* Contain-fit region of a W×H source inside a boxW×boxH cell box under the + * half-row pixel convention (A = 2), centered both axes. Empty when the + * constrained solve rounds either dimension to zero. */ +struct ImagePlacement graphics_contain_fit(int boxX, int boxY, int boxW, + int boxH, uint32_t W, uint32_t H) { + struct ImagePlacement fp = {0}; + if (boxW <= 0 || boxH <= 0 || W == 0 || H == 0) { + return fp; + } + int64_t cols = (int64_t)boxH * 2 * (int64_t)W / (int64_t)H; + if (cols > boxW) { + cols = boxW; + } + if (cols < 0) { + cols = 0; + } + int64_t rows = ((int64_t)cols * (int64_t)H + (int64_t)W) / ((int64_t)2 * W); + if (rows > boxH) { + rows = boxH; + } + if (cols <= 0 || rows <= 0) { + return fp; /* empty footprint paints nothing (§8.2) */ + } + fp.x = boxX + (boxW - (int)cols) / 2; + fp.y = boxY + (boxH - (int)rows) / 2; + fp.w = (int)cols; + fp.h = (int)rows; + return fp; +} + +/* ── Carve (§5.2) ─────────────────────────────────────────────────── */ + +int graphics_size(int pool_bytes) { + if (pool_bytes <= 0) { + pool_bytes = 4194304; + } + int tables = 2 * PLACEMENT_CAP * (int)sizeof(struct ImagePlacement); + return align64((int)sizeof(struct Graphics)) + align64(pool_bytes) + + align8(tables); +} + +struct Graphics *graphics_init(void *mem, int pool_bytes) { + if (pool_bytes <= 0) { + pool_bytes = 4194304; + } + struct Graphics *g = (struct Graphics *)mem; + char *base = (char *)mem + align64((int)sizeof(struct Graphics)); + int table_bytes = PLACEMENT_CAP * (int)sizeof(struct ImagePlacement); + *g = (struct Graphics){ + .pool = base, + .pool_bytes = (uint32_t)pool_bytes, + .pool_bump = 0, + .placements_front = (struct ImagePlacement *)(base + align64(pool_bytes)), + .placements_back = + (struct ImagePlacement *)(base + align64(pool_bytes) + table_bytes), + }; + return g; +} + +/* ── Registry (§5.3) ──────────────────────────────────────────────── */ + +struct ImageEntry *graphics_entry(struct Graphics *g, uint32_t id) { + for (int i = 0; i < IMAGE_ENTRY_CAP; i++) { + struct ImageEntry *e = &g->entries[i]; + if (e->live && e->id == id) { + return e; + } + } + return NULL; +} + +static struct ImageEntry *entry_free_slot(struct Graphics *g) { + for (int i = 0; i < IMAGE_ENTRY_CAP; i++) { + if (!g->entries[i].live && g->entries[i].id == 0) { + return &g->entries[i]; + } + } + return NULL; +} + +/* Compact the pool: copy live payloads to the pool start in offset order, + * rewrite the entries' offsets, reset the bump pointer (§5.3). */ +static void pool_compact(struct Graphics *g) { + uint32_t o = 0; + for (int i = 0; i < IMAGE_ENTRY_CAP; i++) { + struct ImageEntry *e = &g->entries[i]; + if (!e->live) { + continue; + } + if (e->pool_offset != o) { + memmove(g->pool + o, g->pool + e->pool_offset, e->byte_len); + e->pool_offset = o; + } + o += e->byte_len; + } + g->pool_bump = o; +} + +/* Reserve extent for a (re)set; dead extents from replaced entries are + * reclaimed only by compaction (§5.3). Returns the offset, or a negative + * enum img_err code. */ +static int32_t pool_reserve(struct Graphics *g, uint32_t needed) { + if ((uint64_t)g->pool_bump + needed <= g->pool_bytes) { + int32_t off = (int32_t)g->pool_bump; + g->pool_bump += needed; + return off; + } + pool_compact(g); + if ((uint64_t)g->pool_bump + needed > g->pool_bytes) { + return -IMG_ERR_POOL; + } + int32_t off = (int32_t)g->pool_bump; + g->pool_bump += needed; + return off; +} + +int image_begin(struct Clayterm *ct, uint32_t id, uint32_t w, uint32_t h) { + struct Graphics *g = ct->gfx; + if (id == 0) { + return -IMG_ERR_ID; /* the protocol forbids zero image ids (§6.2) */ + } + if (w == 0 || h == 0) { + return -IMG_ERR_DIMS; + } + uint64_t needed = (uint64_t)w * h * 4; + if (needed > (uint64_t)g->pool_bytes) { + return -IMG_ERR_POOL; + } + struct ImageEntry *e = graphics_entry(g, id); + if (e == NULL) { + e = entry_free_slot(g); + if (e == NULL) { + return -IMG_ERR_TABLE; + } + e->id = id; + } + uint32_t rel; + if (e->live && needed <= e->byte_len) { + /* same-size (or shrinking) replace writes in place: the old data is + * being overwritten atomically with the caller's copy — no observer + * can run between begin and commit (single transaction, single + * thread). Video's same-size-per-frame path allocates nothing. */ + e->width = w; + e->height = h; + e->byte_len = (uint32_t)needed; + rel = e->pool_offset; + } else { + int32_t off = pool_reserve(g, (uint32_t)needed); + if (off < 0) { + return off; + } + e->width = w; + e->height = h; + e->byte_len = (uint32_t)needed; + e->pool_offset = (uint32_t)off; + e->live = 1; + rel = (uint32_t)off; + } + /* the host writes pixels directly: return the absolute address of the + * reserved extent (pool-relative offsets are a C-side detail) */ + return (int32_t)(int32_t *)(uintptr_t)(g->pool + rel); +} + +int image_commit(struct Clayterm *ct, uint32_t id) { + struct ImageEntry *e = graphics_entry(ct->gfx, id); + if (e == NULL || !e->live) { + return -IMG_ERR_UNKNOWN; + } + e->data_version++; + return IMG_OK; +} + +/* ── Blank-state reconciliation (§7.2/§10.1/§11.2) ────────────────── */ + +/* Rewrite a front-table record's cells to their covered-cell value: the + * state the terminal shows once the pixels are gone (the blank bytes + * written when coverage appeared). Whole-struct writes so stale combining + * marks cannot survive (#114's Cell carries them). */ +static void reconcile_blank(struct Clayterm *ct, struct ImagePlacement *p) { + for (int y = p->y; y < p->y + p->h; y++) { + for (int x = p->x; x < p->x + p->w; x++) { + if (x < 0 || x >= ct->w || y < 0 || y >= ct->h) { + continue; + } + Cell *c = cell_at(ct, ct->front, x, y); + /* the ATTR_DEFAULT sentinel keeps the cell's existing background — + * the blank write that appeared under the graphic omitted bg, so + * the pre-coverage background is the blank state (§10.1) */ + uint32_t bg = + (p->blank_bg & ATTR_DEFAULT) ? c->bg : (p->blank_bg & COLOR_MASK); + *c = (Cell){.ch = ' ', .fg = ATTR_DEFAULT, .bg = bg}; + } + } +} + +/* Dirty-mark a region: force the next diff to emit it (§10.4). */ +static void dirty_mark(struct Clayterm *ct, int x, int y, int w, int h) { + for (int yy = y; yy < y + h; yy++) { + for (int xx = x; xx < x + w; xx++) { + if (xx < 0 || xx >= ct->w || yy < 0 || yy >= ct->h) { + continue; + } + *cell_at(ct, ct->front, xx, yy) = (Cell){0}; + } + } +} + +/* ── Removal, denial, resize (§7.2/§11.2/§10.5) ───────────────────── */ + +/* Reconcile (and clear) the front-table records of one id — or every id + * when only_id is 0 — without emitting bytes. The terminal's cells revert + * to the blank state the moment the pixels are gone. */ +static void reconcile_and_clear(struct Clayterm *ct, uint32_t only_id) { + struct Graphics *g = ct->gfx; + for (int i = 0; i < PLACEMENT_CAP; i++) { + struct ImagePlacement *p = &g->placements_front[i]; + if (p->used && (only_id == 0 || p->registry_id == only_id)) { + reconcile_blank(ct, p); + p->used = 0; + } + g->placements_back[i].used = 0; + } + g->placement_count = 0; +} + +/* Deletion bytes for every image with live front placements (one d=I per + * registry id, deduplicated) plus the reconcile/clear above. The denial + * (§11.2) and resize (§10.5) paths. Returns bytes written. */ +static int emit_deletions_and_clear(struct Clayterm *ct, uint32_t only_id) { + struct Graphics *g = ct->gfx; + int wrote = 0; + int seen[IMAGE_ENTRY_CAP]; + int seen_count = 0; + for (int i = 0; i < PLACEMENT_CAP; i++) { + struct ImagePlacement *p = &g->placements_front[i]; + if (!p->used) { + continue; + } + if (only_id != 0 && p->registry_id != only_id) { + continue; + } + int seen_already = 0; + for (int k = 0; k < seen_count; k++) { + if (seen[k] == (int)p->registry_id) { + seen_already = 1; + break; + } + } + if (seen_already) { + continue; + } + if (seen_count < IMAGE_ENTRY_CAP) { + seen[seen_count++] = (int)p->registry_id; + } + kitty_delete_image(ct, p->registry_id); + struct ImageEntry *e = graphics_entry(g, p->registry_id); + if (e != NULL) { + e->transmitted_version = 0; /* never trust persisted data (§9.2.2) */ + } + wrote = 1; + } + reconcile_and_clear(ct, only_id); + return wrote; +} + +int image_remove(struct Clayterm *ct, uint32_t id) { + struct Graphics *g = ct->gfx; + struct ImageEntry *e = graphics_entry(g, id); + if (e == NULL || !e->live) { + return -IMG_ERR_UNKNOWN; + } + /* bytes whenever the id was live (§7.2) — a never-placed image's d=I is + * a harmless no-op; a placed image's must flow even when the evidence + * has since been denied (the terminal may still hold the pixels) */ + ct->out.length = 0; + kitty_delete_image(ct, id); + reconcile_and_clear(ct, id); + e->live = 0; + e->id = 0; /* frees the slot for entry_free_slot */ + return ct->out.length; +} + +int graphics_capability(struct Clayterm *ct, int kitty_graphics) { + struct Graphics *g = ct->gfx; + int was = g->kitty_graphics; + g->kitty_graphics = (uint8_t)(kitty_graphics != 0); + if (was && !g->kitty_graphics) { + /* denial (§11.2): immediate deletion bytes (TINV-5) */ + ct->out.length = 0; + emit_deletions_and_clear(ct, 0); + } + return ct->out.length; +} + +int graphics_resize_prepare(struct Clayterm *ct) { + ct->out.length = 0; + emit_deletions_and_clear(ct, 0); + return ct->out.length; +} + +/* ── Frame hooks (§5.4) ───────────────────────────────────────────── */ + +void graphics_frame_begin(struct Graphics *g) { + for (int i = 0; i < PLACEMENT_CAP; i++) { + g->placements_back[i].used = 0; + } + g->placement_count = 0; +} + +void graphics_frame_end(struct Graphics *g) { + struct ImagePlacement *tmp = g->placements_front; + g->placements_front = g->placements_back; + g->placements_back = tmp; +} + +/* ── Tier resolution (§9.1) ───────────────────────────────────────── */ + +/* Pre-layout resolution: everything it reads is stable within a frame. + * The containment/overlap/capacity demotions of §8.5/§10.3 happen later, + * at the render-command walk, and can only lower kitty→ascii — the same + * painted footprint — so the sizing decision made here stays valid. */ +uint8_t graphics_resolve_tier(struct Clayterm *ct, uint32_t registry_id, + uint8_t variant, int mode) { + if (registry_id == 0 || variant == IMG_VARIANT_ALT) { + return IMG_TIER_ALT; + } + struct ImageEntry *e = graphics_entry(ct->gfx, registry_id); + if (e == NULL || !e->live) { + return IMG_TIER_ALT; /* IMAGE_NOT_FOUND surfaced by the caller (§13) */ + } + if (variant == IMG_VARIANT_ASCII) { + return IMG_TIER_ASCII; + } + int kitty_ok = ct->gfx->kitty_graphics && mode == 0; + if (variant == IMG_VARIANT_AUTO || variant == IMG_VARIANT_KITTY) { + if (kitty_ok) { + return IMG_TIER_KITTY; + } + } + return IMG_TIER_ASCII; +} + +/* ── ASCII tier (§9.3) ────────────────────────────────────────────── */ + +/* Sampling-circle geometry: cell-relative fractions; calibration constants + * locked by golden tests (§9.3.2). Six internal circles in two staggered + * columns (left lowered, right raised, radii enlarged); ten external + * circles reaching into neighboring cells. The ordering must match + * tasks/gen-shape-vectors.ts and the §9.3.4 affecting table. */ +static const float CIRCLE_R = 0.34f; +static const float INTERNAL[6][2] = { + {0.30f, 0.24f}, {0.70f, 0.12f}, /* top pair */ + {0.30f, 0.56f}, {0.70f, 0.44f}, /* middle pair */ + {0.30f, 0.88f}, {0.70f, 0.76f}, /* bottom pair */ +}; +static const float EXTERNAL[10][2] = { + {0.40f, -0.28f}, {0.85f, -0.12f}, /* above */ + {-0.10f, 0.30f}, {1.10f, 0.18f}, /* upper sides */ + {-0.12f, 0.62f}, {1.12f, 0.50f}, /* lower sides */ + {-0.08f, 0.88f}, {1.08f, 0.80f}, /* bottom sides */ + {0.40f, 1.28f}, {0.85f, 1.16f}, /* below */ +}; +/* internal k ← externals (§9.3.4, closed) */ +static const uint8_t AFFECTING[6] = {4, 4, 3, 3, 4, 4}; +static const uint8_t AFFECTING_SET[6][4] = { + {0, 1, 2, 4}, {0, 1, 3, 5}, {2, 4, 6}, + {3, 5, 7}, {4, 6, 8, 9}, {5, 7, 8, 9}, +}; + +/* Luma of one pixel, [0,1]. */ +static float luma_px(const char *px) { + return (0.2126f * (uint8_t)px[0] + 0.7152f * (uint8_t)px[1] + + 0.0722f * (uint8_t)px[2]) / + 255.0f; +} + +/* Mean luma of the source pixels whose cell-space (u,v) falls inside the + * circle at (cu, cv). Pixels outside the source image are excluded. When + * no pixel center falls inside, the rectangle's mean lightness is the + * component (degenerate footprints; §9.3.1). */ +static float circle_luma(const struct Clayterm *ct, const struct ImageEntry *e, + int x0, int y0, int x1, int y1, float cu, float cv) { + float rw = (float)(x1 - x0); + float rh = (float)(y1 - y0); + float sum = 0.0f; + int count = 0; + float rect_sum = 0.0f; + int rect_count = 0; + /* iterate the circle's source-plane bounding box, clamped to the image */ + int lox = x0 + ifloor((cu - CIRCLE_R) * rw); + int hix = x0 + iceil((cu + CIRCLE_R) * rw); + int loy = y0 + ifloor((cv - CIRCLE_R) * rh); + int hiy = y0 + iceil((cv + CIRCLE_R) * rh); + if (lox < 0) { + lox = 0; + } + if (loy < 0) { + loy = 0; + } + if (hix > (int)e->width) { + hix = (int)e->width; + } + if (hiy > (int)e->height) { + hiy = (int)e->height; + } + const char *base = ct->gfx->pool + e->pool_offset; + for (int py = loy; py < hiy; py++) { + for (int px = lox; px < hix; px++) { + float u = (rw > 0) ? ((float)px + 0.5f - (float)x0) / rw : 0.5f; + float v = (rh > 0) ? ((float)py + 0.5f - (float)y0) / rh : 0.5f; + float dx = u - cu; + float dy = v - cv; + float y = luma_px(base + ((uint32_t)py * e->width + px) * 4); + rect_sum += y; + rect_count++; + if (dx * dx + dy * dy <= CIRCLE_R * CIRCLE_R) { + sum += y; + count++; + } + } + } + if (count > 0) { + return sum / (float)count; + } + return rect_count > 0 ? rect_sum / (float)rect_count : 0.0f; +} + +/* Contrast enhancement (§9.3.4): directional, then global. The exponents + * are integer powers of two, so (x)^γ is exact repeated squaring — no + * floating-point library, bit-identical everywhere. */ +static void contrast_enhance(float *v, const float *ext) { + for (int k = 0; k < 6; k++) { + float m = v[k]; + for (int j = 0; j < AFFECTING[k]; j++) { + if (ext[AFFECTING_SET[k][j]] > m) { + m = ext[AFFECTING_SET[k][j]]; + } + } + if (m > 0) { + float t = v[k] / m; + t *= t; /* ^2 */ + t *= t; /* ^4 = GAMMA_D */ + v[k] = t * m; + } else { + v[k] = 0; + } + } + float M = 0; + for (int k = 0; k < 6; k++) { + if (v[k] > M) { + M = v[k]; + } + } + if (M > 0) { + for (int k = 0; k < 6; k++) { + float t = v[k] / M; + t *= t; /* ^2 = GAMMA_G */ + v[k] = t * M; + } + } else { + for (int k = 0; k < 6; k++) { + v[k] = 0; + } + } +} + +/* Paint the footprint's cells as character art (§9.3). Ordinary cells: + * opaque cells carry the color sample as foreground and leave the + * background untouched; transparent cells (alpha sample < 128) are blank + * spaces keeping the background. */ +void graphics_paint_ascii(struct Clayterm *ct, const struct ImageEntry *e, + const struct ImagePlacement *fp) { + const char *base = ct->gfx->pool + e->pool_offset; + uint32_t W = e->width; + uint32_t H = e->height; + for (int cy = 0; cy < fp->h; cy++) { + for (int cx = 0; cx < fp->w; cx++) { + int x0 = (int)(round_pos((float)cx * (float)W / (float)fp->w)); + int y0 = (int)(round_pos((float)cy * (float)H / (float)fp->h)); + int x1 = (int)(round_pos((float)(cx + 1) * (float)W / (float)fp->w)); + int y1 = (int)(round_pos((float)(cy + 1) * (float)H / (float)fp->h)); + if (x1 <= x0 || y1 <= y0) { + /* empty rectangle: the single pixel at the start, clamped (§9.3.1) */ + x1 = x0 + 1; + y1 = y0 + 1; + if (x1 > (int)W) { + x1 = (int)W; + } + if (y1 > (int)H) { + y1 = (int)H; + } + } + float v[6]; + float ext[10]; + for (int k = 0; k < 6; k++) { + v[k] = + circle_luma(ct, e, x0, y0, x1, y1, INTERNAL[k][0], INTERNAL[k][1]); + } + for (int k = 0; k < 10; k++) { + ext[k] = + circle_luma(ct, e, x0, y0, x1, y1, EXTERNAL[k][0], EXTERNAL[k][1]); + } + contrast_enhance(v, ext); + + /* color + alpha samples: per-channel means over the rectangle */ + float mr = 0, mg = 0, mb = 0, ma = 0; + for (int py = y0; py < y1; py++) { + for (int px = x0; px < x1; px++) { + const char *px4 = base + ((uint32_t)py * W + px) * 4; + mr += (uint8_t)px4[0]; + mg += (uint8_t)px4[1]; + mb += (uint8_t)px4[2]; + ma += (uint8_t)px4[3]; + } + } + int n = (x1 - x0) * (y1 - y0); + mr /= n; + mg /= n; + mb /= n; + ma /= n; + + if (ma < 128.0f) { + setcell(ct, fp->x + cx, fp->y + cy, ' ', ATTR_DEFAULT, ATTR_DEFAULT); + continue; + } + float sv[6] = {v[0], v[1], v[2], v[3], v[4], v[5]}; + int best = 0; + float best_d = 1e30f; + for (int k = 0; k < 95; k++) { + float d = 0; + for (int c = 0; c < 6; c++) { + float diff = sv[c] - SHAPE_VECTORS[k][c]; + d += diff * diff; + } + if (d < best_d) { + best_d = d; + best = k; + } + } + uint32_t fg = ((uint32_t)(uint8_t)mr << 16) | + ((uint32_t)(uint8_t)mg << 8) | (uint32_t)(uint8_t)mb; + setcell(ct, fp->x + cx, fp->y + cy, (uint32_t)(0x20 + best), fg, + ATTR_DEFAULT); + } + } +} + +/* ── Frame phases (§9.2.6 ordering) ───────────────────────────────── */ + +/* Any covered cell differing front-vs-back? (§10.2's repair trigger.) */ +static int box_differs(struct Clayterm *ct, const struct ImagePlacement *bp) { + for (int y = bp->y; y < bp->y + bp->h; y++) { + for (int x = bp->x; x < bp->x + bp->w; x++) { + if (x < 0 || x >= ct->w || y < 0 || y >= ct->h) { + continue; + } + if (cell_cmp(cell_at(ct, ct->front, x, y), cell_at(ct, ct->back, x, y))) { + return 1; /* one write is enough to heal the whole box */ + } + } + } + return 0; +} + +/* Phase 0, before any emission: the repair precheck (§10.2) — a placement + * whose covered cells will receive writes (front ≠ back anywhere in its + * box) must be re-placed this frame; run before any dirty-marking mutates + * the front buffer, and before the diff itself. */ +static void graphics_precheck_repairs(struct Clayterm *ct) { + struct Graphics *g = ct->gfx; + for (int i = 0; i < g->placement_count; i++) { + struct ImagePlacement *bp = &g->placements_back[i]; + if (!bp->used || bp->tier != IMG_TIER_KITTY) { + continue; + } + bp->repair = (uint8_t)box_differs(ct, bp); + } +} + +/* The front-table record matching (registry_id, placement_id), or NULL. */ +static struct ImagePlacement *front_match(struct Graphics *g, uint32_t rid, + uint32_t pid) { + for (int i = 0; i < PLACEMENT_CAP; i++) { + struct ImagePlacement *fp = &g->placements_front[i]; + if (fp->used && fp->registry_id == rid && fp->placement_id == pid) { + return fp; + } + } + return NULL; +} + +/* Phase 1+2, before cell writes: stale-placement deletions and + * transmissions, in that order. */ +void graphics_emit_before_cells(struct Clayterm *ct) { + struct Graphics *g = ct->gfx; + graphics_precheck_repairs(ct); + /* stale placements: front entries this frame does not re-create, scoped + * lowercase deletions (data retained; §9.2.5) */ + for (int i = 0; i < PLACEMENT_CAP; i++) { + struct ImagePlacement *fp = &g->placements_front[i]; + if (!fp->used) { + continue; + } + int re_created = 0; + int version_changed = 0; + for (int k = 0; k < g->placement_count; k++) { + struct ImagePlacement *bp = &g->placements_back[k]; + if (!bp->used || bp->tier != IMG_TIER_KITTY) { + continue; + } + if (bp->registry_id != fp->registry_id || + bp->placement_id != fp->placement_id) { + continue; + } + re_created = 1; + struct ImageEntry *e = graphics_entry(g, bp->registry_id); + if (e != NULL && e->data_version != fp->data_version) { + version_changed = 1; + } + break; + } + if (!re_created) { + struct ImageEntry *e = graphics_entry(g, fp->registry_id); + uint32_t current = e != NULL ? e->data_version : 0; + if (current == fp->data_version) { + /* no transmission will clean this image; delete the placement + * here (§9.2.5) */ + kitty_delete_placement(ct, fp->registry_id, fp->placement_id); + } + /* the pixels are gone either way (scoped delete, or the + * data-freeing delete of the transmission path) */ + dirty_mark(ct, fp->x, fp->y, fp->w, fp->h); + } + } + /* unreferenced entries lose their transmission state (§9.2.2: never + * trust persisted data after losing the last placement) */ + for (int i = 0; i < IMAGE_ENTRY_CAP; i++) { + struct ImageEntry *e = &g->entries[i]; + if (!e->live || e->transmitted_version == 0) { + continue; + } + int referenced = 0; + for (int k = 0; k < g->placement_count; k++) { + if (g->placements_back[k].used && + g->placements_back[k].registry_id == e->id) { + referenced = 1; + break; + } + } + if (!referenced) { + e->transmitted_version = 0; + } + } + /* transmissions, in registry id order (§9.2.6) */ + for (int i = 0; i < IMAGE_ENTRY_CAP; i++) { + struct ImageEntry *e = &g->entries[i]; + if (!e->live) { + continue; + } + int referenced = 0; + for (int k = 0; k < g->placement_count; k++) { + if (g->placements_back[k].used && + g->placements_back[k].registry_id == e->id) { + referenced = 1; + break; + } + } + if (!referenced) { + continue; + } + if (e->transmitted_version != e->data_version) { + kitty_transmit(ct, e); + } + } +} + +/* Phase 4, after cell writes: the placements phase. A placement emits + * only when it is new, moved (the front record's box differs), or repaired + * (its covered cells receive writes, §10.2) — steady state is byte-silent + * (§8.5). */ +void graphics_emit_placements(struct Clayterm *ct, int row) { + struct Graphics *g = ct->gfx; + int emitted = 0; + for (int i = 0; i < g->placement_count; i++) { + struct ImagePlacement *p = &g->placements_back[i]; + if (!p->used || p->tier != IMG_TIER_KITTY) { + continue; + } + struct ImagePlacement *fp = front_match(g, p->registry_id, p->placement_id); + int moved = fp == NULL || fp->x != p->x || fp->y != p->y || fp->w != p->w || + fp->h != p->h; + /* a re-transmitted image lost its terminal placements (the protocol's + * delete-before-re-transmit) — the same frame must re-place it */ + int retransmitted = fp != NULL && fp->data_version != p->data_version; + if ((!moved && !retransmitted) && !p->repair) { + continue; + } + emit_cursor(ct, p->x, p->y, row); + kitty_place(ct, p->registry_id, p->placement_id, p->w, p->h); + emitted = 1; + } + ct->gfx->emitted_placements = emitted; +} + +/* Coverage pass (§10.3): after the render-command walk, pixel-tier coverage + * asserts its blanks over the back buffer, in back-table order — the + * later element wins shared cells. */ +void graphics_cover(struct Clayterm *ct) { + struct Graphics *g = ct->gfx; + for (int i = 0; i < g->placement_count; i++) { + struct ImagePlacement *p = &g->placements_back[i]; + if (!p->used || p->tier != IMG_TIER_KITTY) { + continue; + } + for (int y = p->y; y < p->y + p->h; y++) { + for (int x = p->x; x < p->x + p->w; x++) { + setcell(ct, x, y, ' ', ATTR_DEFAULT, p->blank_bg); + } + } + } +} + +/* ── Back-table helpers used by the render-command walk ───────────── */ + +/* Add a kitty placement record; returns the slot, or NULL at capacity + * (the caller demotes to ascii + error, §5.4). */ +struct ImagePlacement *graphics_add_placement(struct Clayterm *ct, + uint32_t registry_id, + uint32_t placement_id, + uint32_t data_version) { + struct Graphics *g = ct->gfx; + if (g->placement_count >= PLACEMENT_CAP) { + return NULL; + } + struct ImagePlacement *p = &g->placements_back[g->placement_count++]; + *p = (struct ImagePlacement){ + .registry_id = registry_id, + .placement_id = placement_id, + .data_version = data_version, + .tier = IMG_TIER_KITTY, + .used = 1, + }; + return p; +} + +/* Same-frame placement-id collision check (§9.2.1/§13). */ +int graphics_placement_collision(struct Clayterm *ct, uint32_t placement_id) { + struct Graphics *g = ct->gfx; + for (int i = 0; i < g->placement_count - 1; i++) { + if (g->placements_back[i].used && + g->placements_back[i].placement_id == placement_id) { + return 1; + } + } + return 0; +} diff --git a/src/graphics.h b/src/graphics.h new file mode 100644 index 0000000..0b7ffa4 --- /dev/null +++ b/src/graphics.h @@ -0,0 +1,181 @@ +/* graphics.h — pixel-surface registry and tier engine + * + * Implements the Graphics Specification: a per-Term registry of caller-owned + * decoded RGBA pixel surfaces, plus the engine that downsamples a surface + * onto the terminal through a fidelity ladder (kitty graphics protocol, + * shape-aware ASCII character art, alt text). + * + * Memory: the registry, pixel pool, and double-buffered placements tables + * live in their own dimension-independent carve (§5.1) — they survive resize + * unchanged. No heap: fixed capacities, compaction-only reclamation, closed + * failure codes. + */ + +#ifndef GRAPHICS_H +#define GRAPHICS_H + +#include + +struct Clayterm; +struct Buffer; + +/* ── Capacities (Graphics Specification §5.3/§5.4, closed) ────────── */ + +#define IMAGE_ENTRY_CAP 256 +#define PLACEMENT_CAP 64 +/* base64 chars per non-final chunk; 4096 % 4 == 0 as the protocol requires */ +#define IMG_CHUNK_CHARS 4096 +/* source bytes per maximum chunk: 4096 base64 chars = 3072 raw bytes */ +#define IMG_CHUNK_SRC 3072 + +/* ── Tiers ────────────────────────────────────────────────────────── */ + +enum img_tier { + IMG_TIER_KITTY = 1, + IMG_TIER_ASCII = 2, + IMG_TIER_ALT = 3, +}; + +/* variant property values on the wire (Graphics Specification §7.1) */ +enum img_variant { + IMG_VARIANT_AUTO = 0, + IMG_VARIANT_KITTY = 1, + IMG_VARIANT_ASCII = 2, + IMG_VARIANT_ALT = 3, +}; + +/* ── Registry entries (§5.3) ──────────────────────────────────────── */ + +struct ImageEntry { + uint32_t id; + uint32_t width, height; + uint32_t pool_offset; /* pool-relative; stable until compaction */ + uint32_t byte_len; + uint32_t data_version; /* bumped on every successful setImage */ + uint32_t transmitted_version; /* 0 = never sent (last version streamed) */ + uint8_t live; +}; + +/* ── Placements (double-buffered record; §5.4) ────────────────────── */ + +struct ImagePlacement { + uint32_t registry_id; + uint32_t placement_id; + uint32_t data_version; + int x, y, w, h; /* painted footprint, terminal cells */ + uint32_t blank_bg; /* blank-state background: ATTR_DEFAULT sentinel or ARGB */ + uint8_t tier; /* enum img_tier; only IMG_TIER_KITTY entries are placed */ + uint8_t used; + uint8_t repair; /* cells in the footprint changed this frame (§10.2) */ +}; + +/* ── Error codes returned by the native surface ───────────────────── */ + +enum img_err { + IMG_OK = 0, + IMG_ERR_ID = 1, /* id outside [1, 4294967295] */ + IMG_ERR_DIMS = 2, /* width/height not positive */ + IMG_ERR_LEN = 3, /* byte length != w*h*4 */ + IMG_ERR_TABLE = 4, /* entry table full */ + IMG_ERR_POOL = 5, /* pool cannot hold payload even after compaction */ + IMG_ERR_UNKNOWN = 6, /* commit/remove on unknown or dead id */ +}; + +struct Graphics { + /* capability mirror (terminfo-spec §4.2: the renderer's private + * capability state; folded in through the update path) */ + uint8_t kitty_graphics; + /* pixel pool: carved once, never grown or moved at runtime (§5.3) */ + char *pool; + uint32_t pool_bytes; + uint32_t pool_bump; /* high-water bump offset; dead extents below it */ + struct ImageEntry entries[IMAGE_ENTRY_CAP]; + struct ImagePlacement *placements_front; /* previous frame's placements */ + struct ImagePlacement *placements_back; /* frame being built */ + int placement_count; /* used slots in the back table */ + /* set during reduce(): the placements phase emitted at least one CUP */ + int emitted_placements; +}; + +/* ── Carve ────────────────────────────────────────────────────────── */ + +int graphics_size(int pool_bytes); +struct Graphics *graphics_init(void *mem, int pool_bytes); + +/* ── Registry surface (native, called outside render transactions) ── */ + +/* Reserve and validate. Returns the pool offset to write pixels at, or a + * negative `enum img_err` code. Updates the entry's dimensions and extent; + * the caller copies `w*h*4` bytes to [pool + offset] before commit. */ +int image_begin(struct Clayterm *ct, uint32_t id, uint32_t w, uint32_t h); + +/* Finalize a begun image: bumps the data version (Graphics Specification + * §4.3). Returns 0 or a negative `enum img_err` code. */ +int image_commit(struct Clayterm *ct, uint32_t id); + +/* Remove a surface: emits the data-freeing deletion block (§7.2) into the + * Term's output buffer (reset first), reconciles covered cells to their + * blank state, and frees the entry. Returns the byte length written (0 when + * nothing was emitted), or a negative `enum img_err` code. */ +int image_remove(struct Clayterm *ct, uint32_t id); + +/* Capability mirror (terminfo-spec §4.2). A true→false transition is a + * capability denial (§11.2): emits deletion bytes for every image with live + * placements into the output buffer (reset first), resets transmission + * state, clears the placements tables, and reconciles covered cells. + * Returns the byte length written (0 = nothing emitted). */ +int graphics_capability(struct Clayterm *ct, int kitty_graphics); + +/* Resize (§10.5): before the dimension-dependent state is discarded, emit + * one data-freeing deletion per image with live placements into the output + * buffer (reset first), reset transmission state, and clear the placements + * tables. Returns the byte length written. */ +int graphics_resize_prepare(struct Clayterm *ct); + +/* ── Frame hooks (called from reduce()) ───────────────────────────── */ + +void graphics_frame_begin(struct Graphics *g); +void graphics_frame_end(struct Graphics *g); /* swaps the tables */ + +/* ── Tier engine (called from clayterm.c's reduce()) ──────────────── */ + +/* Registry lookup; NULL when unknown or dead. */ +struct ImageEntry *graphics_entry(struct Graphics *g, uint32_t id); + +/* Pre-layout tier resolution (§9.1): variant/omission/liveness/evidence + + * the line-mode cap. The containment/overlap/capacity/collision demotions + * of §8.5/§10.3 happen later, at the render-command walk, and only lower + * kitty→ascii with the same footprint. */ +uint8_t graphics_resolve_tier(struct Clayterm *ct, uint32_t registry_id, + uint8_t variant, int mode); + +/* Contain-fit painted footprint (§8.2) inside the box, for a W×H source. */ +struct ImagePlacement graphics_contain_fit(int boxX, int boxY, int boxW, + int boxH, uint32_t W, uint32_t H); + +/* Pinned placement-id derivation (§9.2.1/§15). */ +uint32_t graphics_placement_id(const char *id_chars, uint32_t id_len, + uint32_t registry_id); + +/* Add a kitty placement record to the back table; NULL at capacity + * (caller demotes to ascii and reports IMAGE_PLACEMENTS_EXCEEDED, §5.4). */ +struct ImagePlacement *graphics_add_placement(struct Clayterm *ct, + uint32_t registry_id, + uint32_t placement_id, + uint32_t data_version); + +/* Same-frame placement-id collision check (§9.2.1/§13). */ +int graphics_placement_collision(struct Clayterm *ct, uint32_t placement_id); + +/* Paint a footprint as ascii character art (§9.3). */ +void graphics_paint_ascii(struct Clayterm *ct, const struct ImageEntry *e, + const struct ImagePlacement *fp); + +/* Frame phases in §9.2.6 order: the coverage pass (after the render-command + * walk), the pre-cell phases (stale deletions + transmissions; skipped in + * line mode by the caller), and the placements phase (after cell writes). */ +void graphics_cover(struct Clayterm *ct); +void graphics_emit_before_cells(struct Clayterm *ct); +void graphics_emit_placements(struct Clayterm *ct, int row); + +#endif diff --git a/src/input.c b/src/input.c index 5c16e96..425babd 100644 --- a/src/input.c +++ b/src/input.c @@ -1027,7 +1027,16 @@ static int parse_apc_response(struct InputState *st) { const char *payload = st->buf + 3; int plen = end - 3 - (st->buf[end - 1] == '\x07' ? 1 : 2); - emit_cap_bool(st, CAP_KITTY_GRAPHICS, payload_contains(payload, plen, ";OK")); + /* The reply's payload is the response shape `i=;OK` or + * `i=;ENOTSUPPORTED…` — it never carries an `a=` action key. Frames + * that do are commands (ours, quiet q=2, or another client's), not + * responses: the renderer's own emissions must not surface as events + * (Graphics Specification INV-I5: the input stream stays clean). */ + int is_command = plen >= 2 && payload[0] == 'a' && payload[1] == '='; + if (!is_command) { + emit_cap_bool(st, CAP_KITTY_GRAPHICS, + payload_contains(payload, plen, ";OK")); + } shift(st, end); return PARSE_OK; diff --git a/src/mem.c b/src/mem.c index 3a0895a..24ae930 100644 --- a/src/mem.c +++ b/src/mem.c @@ -10,6 +10,23 @@ void *memcpy(void *dst, const void *src, size_t n) { return dst; } +void *memmove(void *dst, const void *src, size_t n) { + unsigned char *d = (unsigned char *)dst; + const unsigned char *s = (const unsigned char *)src; + if (d == s || n == 0) + return dst; + if (d < s) { + while (n--) + *d++ = *s++; + } else { + d += n; + s += n; + while (n--) + *--d = *--s; + } + return dst; +} + void *memset(void *dst, int c, size_t n) { unsigned char *d = (unsigned char *)dst; while (n--) diff --git a/src/mem.h b/src/mem.h index 0a11d96..172c4cd 100644 --- a/src/mem.h +++ b/src/mem.h @@ -6,6 +6,7 @@ typedef __SIZE_TYPE__ size_t; void *memcpy(void *dst, const void *src, size_t n); +void *memmove(void *dst, const void *src, size_t n); void *memset(void *dst, int c, size_t n); size_t strlen(const char *s); int align8(int n); diff --git a/src/module.c b/src/module.c index 5438e13..d852e81 100644 --- a/src/module.c +++ b/src/module.c @@ -9,6 +9,7 @@ #include "utf8.c" #include "wcwidth.c" #include "clayterm.c" +#include "graphics.c" #include "transitions.c" #include "trie.c" #include "input.c" diff --git a/src/shape_vectors.h b/src/shape_vectors.h new file mode 100644 index 0000000..57cf620 --- /dev/null +++ b/src/shape_vectors.h @@ -0,0 +1,183 @@ +/* GENERATED by tasks/gen-shape-vectors.ts — do not edit. + * + * Shape vectors for the ascii art tier (Graphics Specification + * §9.3.2/§9.3.3): per-character ink fractions over the six internal + * sampling circles, computed from the public-domain 8x8 bitmap font + * (vendor/font8x8_basic.h), per-component max normalized. The circle + * geometry mirrors src/graphics.c's CIRCLE_R/INTERNAL; regeneration: + * deno task gen-shape (verify: deno task gen-shape:check). + */ + +#ifndef SHAPE_VECTORS_H +#define SHAPE_VECTORS_H + +#include + +#define SHAPE_VECTOR_COUNT 95 + +static const float SHAPE_VECTORS[95][6] = { + {0, 0, 0, 0, 0, 0}, /* U+0020 (space) */ + {0.573712, 0.765193, 0.408567, 0.532286, 0.210526, + 0.313924}, /* U+0021 (!) */ + {0.346359, 0.38674, 0.01318, 0.122164, 0, 0}, /* U+0022 (") */ + {0.941385, 0.839779, 1, 0.848168, 0.707895, 0.741772}, /* U+0023 (#) */ + {0.683837, 0.441989, 0.777595, 0.4363, 0.589474, 0.465823}, /* U+0024 ($) */ + {0.476021, 0.41989, 0.518946, 0.553229, 0.486842, + 0.443038}, /* U+0025 (%) */ + {0.761989, 0.660221, 0.881384, 0.724258, 0.613158, + 0.746835}, /* U+0026 (&) */ + {0.362345, 0.022099, 0.095552, 0, 0, 0}, /* U+0027 (') */ + {0.534636, 0.292818, 0.53542, 0.048866, 0.460526, + 0.240506}, /* U+0028 (() */ + {0.465364, 0.312155, 0.512356, 0.373473, 0.4, 0.286076}, /* U+0029 ()) */ + {0.648313, 0.627072, 0.718287, 0.914485, 0.289474, + 0.574684}, /* U+002a (*) */ + {0.559503, 0.28453, 0.617792, 0.34555, 0.271053, 0.260759}, /* U+002b (+) */ + {0, 0, 0.227348, 0.017452, 0.536842, 0.187342}, /* U+002c (,) */ + {0.278863, 0.104972, 0.285008, 0.188482, 0, 0.096203}, /* U+002d (-) */ + {0, 0, 0.214168, 0.017452, 0.347368, 0.187342}, /* U+002e (.) */ + {0.371226, 0.535912, 0.433278, 0.312391, 0.381579, + 0.002532}, /* U+002f (/) */ + {0.865009, 0.930939, 0.924217, 0.874346, 0.797368, + 0.807595}, /* U+0030 (0) */ + {0.568384, 0.292818, 0.649094, 0.240838, 0.739474, + 0.392405}, /* U+0031 (1) */ + {0.708703, 0.596685, 0.68369, 0.47644, 0.821053, 0.427848}, /* U+0032 (2) */ + {0.634103, 0.596685, 0.609555, 0.579407, 0.605263, + 0.518987}, /* U+0033 (3) */ + {0.730018, 0.859116, 0.736409, 0.736475, 0.407895, + 0.873418}, /* U+0034 (4) */ + {0.753108, 0.546961, 0.69028, 0.465969, 0.605263, + 0.577215}, /* U+0035 (5) */ + {0.722913, 0.243094, 0.92257, 0.352531, 0.663158, + 0.518987}, /* U+0036 (6) */ + {0.692718, 0.696133, 0.448105, 0.431065, 0.444737, + 0.286076}, /* U+0037 (7) */ + {0.865009, 0.596685, 0.869852, 0.579407, 0.663158, + 0.518987}, /* U+0038 (8) */ + {0.825933, 0.662983, 0.665568, 0.596859, 0.415789, + 0.516456}, /* U+0039 (9) */ + {0.21492, 0.179558, 0.329489, 0.095986, 0.347368, + 0.187342}, /* U+003a (:) */ + {0.21492, 0.179558, 0.342669, 0.095986, 0.536842, + 0.187342}, /* U+003b (;) */ + {0.523979, 0.292818, 0.53542, 0.038394, 0.460526, + 0.240506}, /* U+003c (<) */ + {0.316163, 0.251381, 0.489292, 0.30541, 0.413158, + 0.268354}, /* U+003d (=) */ + {0.412078, 0.375691, 0.46458, 0.373473, 0.4, 0.344304}, /* U+003e (>) */ + {0.634103, 0.596685, 0.339374, 0.403141, 0.271053, + 0.18481}, /* U+003f (?) */ + {0.877442, 1, 0.90939, 0.849913, 0.631579, 0.61519}, /* U+0040 (@) */ + {0.788632, 0.588398, 0.859967, 0.595113, 0.692105, + 0.63038}, /* U+0041 (A) */ + {0.841918, 0.80663, 0.808896, 0.712042, 0.771053, + 0.696203}, /* U+0042 (B) */ + {0.566607, 0.502762, 0.574959, 0.268761, 0.526316, + 0.417722}, /* U+0043 (C) */ + {0.797513, 0.70442, 0.759473, 0.614311, 0.826316, + 0.617722}, /* U+0044 (D) */ + {0.893428, 0.624309, 0.871499, 0.383944, 0.773684, + 0.513924}, /* U+0045 (E) */ + {0.893428, 0.624309, 0.855025, 0.308901, 0.668421, + 0.207595}, /* U+0046 (F) */ + {0.568384, 0.502762, 0.644152, 0.450262, 0.528947, + 0.751899}, /* U+0047 (G) */ + {0.85968, 0.651934, 0.850082, 0.663176, 0.594737, 0.56962}, /* U+0048 (H) */ + {0.630551, 0.375691, 0.621087, 0.230366, 0.634211, + 0.316456}, /* U+0049 (I) */ + {0.321492, 0.848066, 0.617792, 0.582897, 0.663158, + 0.577215}, /* U+004a (J) */ + {0.802842, 0.640884, 0.820428, 0.635253, 0.6, 0.556962}, /* U+004b (K) */ + {0.678508, 0.121547, 0.691928, 0.21466, 0.771053, 0.58481}, /* U+004c (L) */ + {1, 1, 0.883031, 1, 0.465789, 0.724051}, /* U+004d (M) */ + {0.797513, 0.834254, 0.817133, 0.881326, 0.434211, + 0.736709}, /* U+004e (N) */ + {0.626998, 0.70442, 0.642504, 0.600349, 0.581579, + 0.617722}, /* U+004f (O) */ + {0.841918, 0.80663, 0.78089, 0.452007, 0.665789, 0.192405}, /* U+0050 (P) */ + {0.824156, 0.660221, 0.802306, 0.593368, 0.613158, + 0.716456}, /* U+0051 (Q) */ + {0.843694, 0.80663, 0.805601, 0.705061, 0.6, 0.617722}, /* U+0052 (R) */ + {0.797513, 0.378453, 0.787479, 0.472949, 0.639474, + 0.549367}, /* U+0053 (S) */ + {0.749556, 0.574586, 0.621087, 0.303665, 0.634211, + 0.316456}, /* U+0054 (T) */ + {0.742451, 0.649171, 0.846787, 0.589878, 0.768421, + 0.653165}, /* U+0055 (U) */ + {0.742451, 0.649171, 0.795717, 0.534031, 0.568421, + 0.524051}, /* U+0056 (V) */ + {0.648313, 0.718232, 0.869852, 0.877836, 0.692105, + 0.853165}, /* U+0057 (W) */ + {0.664298, 0.618785, 0.728171, 0.619546, 0.555263, + 0.503797}, /* U+0058 (X) */ + {0.831261, 0.585635, 0.723229, 0.441536, 0.634211, + 0.349367}, /* U+0059 (Y) */ + {0.703375, 0.812155, 0.583196, 0.630017, 0.755263, + 0.683544}, /* U+005a (Z) */ + {0.671403, 0.20442, 0.665568, 0.013962, 0.665789, + 0.159494}, /* U+005b ([) */ + {0.445826, 0.118785, 0.294893, 0.418848, 0.002632, + 0.460759}, /* U+005c (\) */ + {0.57016, 0.566298, 0.561779, 0.471204, 0.544737, + 0.491139}, /* U+005d (]) */ + {0.520426, 0.593923, 0.263591, 0.308901, 0, 0.108861}, /* U+005e (^) */ + {0, 0, 0.01318, 0, 0.468421, 0.41519}, /* U+005f (_) */ + {0.332149, 0.372928, 0.072488, 0.129145, 0, 0}, /* U+0060 (`) */ + {0.390764, 0.254144, 0.813839, 0.530541, 0.668421, + 0.729114}, /* U+0061 (a) */ + {0.722913, 0.127072, 0.749588, 0.452007, 0.676316, + 0.686076}, /* U+0062 (b) */ + {0.445826, 0.254144, 0.830313, 0.340314, 0.660526, + 0.410127}, /* U+0063 (c) */ + {0.502664, 0.751381, 0.769357, 0.666667, 0.578947, + 0.668354}, /* U+0064 (d) */ + {0.513321, 0.254144, 0.950577, 0.404887, 0.694737, + 0.473418}, /* U+0065 (e) */ + {0.774423, 0.389503, 0.782537, 0.188482, 0.665789, + 0.098734}, /* U+0066 (f) */ + {0.390764, 0.370166, 0.782537, 0.556719, 0.957895, + 0.817722}, /* U+0067 (g) */ + {0.749556, 0.325967, 0.706755, 0.581152, 0.597368, + 0.648101}, /* U+0068 (h) */ + {0.48135, 0.18232, 0.668863, 0.212914, 0.634211, 0.316456}, /* U+0069 (i) */ + {0.168739, 0.466851, 0.431631, 0.478185, 0.9, 0.716456}, /* U+006a (j) */ + {0.701599, 0.323204, 0.85173, 0.492147, 0.7, 0.572152}, /* U+006b (k) */ + {0.579041, 0.292818, 0.621087, 0.230366, 0.634211, + 0.316456}, /* U+006c (l) */ + {0.584369, 0.339779, 0.932455, 0.760908, 0.610526, + 0.891139}, /* U+006d (m) */ + {0.511545, 0.254144, 0.843493, 0.465969, 0.594737, + 0.567089}, /* U+006e (n) */ + {0.447602, 0.254144, 0.899506, 0.455497, 0.663158, + 0.577215}, /* U+006f (o) */ + {0.456483, 0.370166, 0.749588, 0.56719, 1, 0.643038}, /* U+0070 (p) */ + {0.390764, 0.370166, 0.769357, 0.556719, 0.684211, 1}, /* U+0071 (q) */ + {0.509769, 0.372928, 0.749588, 0.500873, 0.665789, + 0.389873}, /* U+0072 (r) */ + {0.470693, 0.251381, 0.787479, 0.401396, 0.673684, + 0.455696}, /* U+0073 (s) */ + {0.586146, 0.464088, 0.579901, 0.408377, 0.444737, + 0.397468}, /* U+0074 (t) */ + {0.401421, 0.28453, 0.802306, 0.471204, 0.578947, + 0.665823}, /* U+0075 (u) */ + {0.401421, 0.28453, 0.792422, 0.411867, 0.568421, + 0.524051}, /* U+0076 (v) */ + {0.412078, 0.320442, 0.967051, 0.753927, 0.865789, + 0.941772}, /* U+0077 (w) */ + {0.374778, 0.301105, 0.685338, 0.490401, 0.555263, + 0.562025}, /* U+0078 (x) */ + {0.401421, 0.28453, 0.718287, 0.485166, 0.957895, + 0.817722}, /* U+0079 (y) */ + {0.53286, 0.290055, 0.802306, 0.425829, 0.755263, + 0.425316}, /* U+007a (z) */ + {0.566607, 0.461326, 0.573311, 0.188482, 0.455263, + 0.379747}, /* U+007b ({) */ + {0.344583, 0.516575, 0.339374, 0.356021, 0.355263, + 0.44557}, /* U+007c (|) */ + {0.564831, 0.29558, 0.581549, 0.335079, 0.555263, + 0.270886}, /* U+007d (}) */ + {0.39254, 0.59116, 0.003295, 0.143106, 0, 0}, /* U+007e (~) */ +}; + +#endif diff --git a/tasks/gen-shape-vectors.ts b/tasks/gen-shape-vectors.ts new file mode 100644 index 0000000..4898182 --- /dev/null +++ b/tasks/gen-shape-vectors.ts @@ -0,0 +1,174 @@ +// Generate src/shape_vectors.h: the build-time shape-vector table for the +// ascii art tier (Graphics Specification §9.3.2). +// +// For each of the 95 printable ASCII characters, the shape vector records +// the fraction of each internal sampling circle's area covered by the +// character's ink, sampled from the vendored 8x8 public-domain bitmap font +// (vendor/font8x8_basic.h). Components are then divided by the largest +// value of that component across all characters (per-component max +// normalization). +// +// The circle geometry here MUST match src/graphics.c's INTERNAL/CIRCLE_R — +// both are the §9.3.2 calibration constants, locked by golden tests. +// +// Regenerate: deno task gen-shape. Verify: deno task gen-shape:check. + +let CIRCLE_R = 0.34; +// two staggered columns: left lowered, right raised (§9.3.2) +let INTERNAL: Array<[number, number]> = [ + [0.3, 0.24], + [0.7, 0.12], // top pair + [0.3, 0.56], + [0.7, 0.44], // middle pair + [0.3, 0.88], + [0.7, 0.76], // bottom pair +]; + +// point-supersampling density per circle's bounding square +let SAMPLES = 32; + +let FONT_PATH = "vendor/font8x8_basic.h"; +let OUT_PATH = "src/shape_vectors.h"; + +function loadFont(): Uint8Array[] { + let text = new TextDecoder().decode(Deno.readFileSync(FONT_PATH)); + let glyphs: Uint8Array[] = []; + let glyph = /\{([^}]*)\}\s*,?\s*\/\/\s*U\+([0-9A-Fa-f]+)/g; + let rows = /0x([0-9A-Fa-f]{2})/g; + for (let match of text.matchAll(glyph)) { + let cp = parseInt(match[2], 16); + let bytes = new Uint8Array(8); + let i = 0; + for (let b of match[1].matchAll(rows)) { + if (i < 8) { + bytes[i++] = parseInt(b[1], 16); + } + } + while (glyphs.length < cp) { + glyphs.push(new Uint8Array(8)); + } + glyphs[cp] = bytes; + } + return glyphs; +} + +function ink(font: Uint8Array, u: number, v: number): boolean { + let col = Math.floor(u * 8); + let row = Math.floor(v * 8); + if (col < 0 || col > 7 || row < 0 || row > 7) { + return false; + } + return (font[row] & (1 << col)) !== 0; +} + +function shapeVector(font: Uint8Array): number[] { + let vector: number[] = []; + for (let [cu, cv] of INTERNAL) { + let inside = 0; + let inked = 0; + for (let sy = 0; sy < SAMPLES; sy++) { + for (let sx = 0; sx < SAMPLES; sx++) { + // jittered grid point in the circle's bounding square + let u = cu - CIRCLE_R + ((sx + 0.5) / SAMPLES) * 2 * CIRCLE_R; + let v = cv - CIRCLE_R + ((sy + 0.5) / SAMPLES) * 2 * CIRCLE_R; + let du = u - cu; + let dv = v - cv; + if (du * du + dv * dv > CIRCLE_R * CIRCLE_R) { + continue; + } + inside++; + if (ink(font, u, v)) { + inked++; + } + } + } + vector.push(inside > 0 ? inked / inside : 0); + } + return vector; +} + +function normalize(vectors: number[][]): number[][] { + let max = [0, 0, 0, 0, 0, 0]; + for (let vector of vectors) { + for (let k = 0; k < 6; k++) { + if (vector[k] > max[k]) { + max[k] = vector[k]; + } + } + } + return vectors.map((vector) => + vector.map((v, k) => (max[k] > 0 ? v / max[k] : 0)) + ); +} + +function render(vectors: number[][]): string { + let lines: string[] = []; + lines.push("/* GENERATED by tasks/gen-shape-vectors.ts — do not edit."); + lines.push(" *"); + lines.push(" * Shape vectors for the ascii art tier (Graphics Specification"); + lines.push( + " * §9.3.2/§9.3.3): per-character ink fractions over the six internal", + ); + lines.push( + " * sampling circles, computed from the public-domain 8x8 bitmap font", + ); + lines.push( + " * (vendor/font8x8_basic.h), per-component max normalized. The circle", + ); + lines.push( + " * geometry mirrors src/graphics.c's CIRCLE_R/INTERNAL; regeneration:", + ); + lines.push(" * deno task gen-shape (verify: deno task gen-shape:check)."); + lines.push(" */"); + lines.push(""); + lines.push("#ifndef SHAPE_VECTORS_H"); + lines.push("#define SHAPE_VECTORS_H"); + lines.push(""); + lines.push("#include "); + lines.push(""); + lines.push("#define SHAPE_VECTOR_COUNT 95"); + lines.push(""); + lines.push("static let float SHAPE_VECTORS[95][6] = {"); + for (let i = 0; i < 95; i++) { + let cp = 0x20 + i; + let label = cp === 0x20 ? "space" : String.fromCharCode(cp); + let vals = vectors[i] + .map(( + v, + ) => (v === 0 ? "0" : v.toFixed(6).replace(/0+$/, "").replace(/\.$/, ""))) + .join(", "); + lines.push( + ` { ${vals} }, /* U+${cp.toString(16).padStart(4, "0")} (${label}) */`, + ); + } + lines.push("};"); + lines.push(""); + lines.push("#endif"); + lines.push(""); + return lines.join("\n"); +} + +// Regenerate: deno task gen-shape. Verify: deno task gen-shape:check. + +let glyphs = loadFont(); +if (glyphs.length < 0x7f) { + console.error(`font parse failed: ${glyphs.length} glyphs`); + Deno.exit(1); +} +let vectors = normalize( + Array.from({ length: 95 }, (_, i) => shapeVector(glyphs[0x20 + i])), +); +let out = render(vectors); + +let check = Deno.args.includes("--check"); +if (check) { + let existing = new TextDecoder().decode(Deno.readFileSync(OUT_PATH)); + if (existing !== out) { + console.error(`${OUT_PATH} is stale; run deno task gen-shape`); + Deno.exit(1); + } + console.log(`gen-shape:check: ${OUT_PATH} current`); +} else { + Deno.writeTextFileSync(OUT_PATH, out); + console.log(`wrote ${OUT_PATH} (${vectors.length} vectors)`); +} diff --git a/term-native.ts b/term-native.ts index b54549d..6ab3c04 100644 --- a/term-native.ts +++ b/term-native.ts @@ -1,5 +1,9 @@ import { f32, offsets, struct } from "./typedef.ts"; +/* Graphics Specification §5.3: the image pixel pool's normative default. + * Exposed as the createTerm `imagePoolBytes` option by term.ts. */ +export const DEFAULT_IMAGE_POOL_BYTES = 4194304; + export interface BoundingBox { x: number; y: number; @@ -45,6 +49,16 @@ export interface Native { ): void; output(ct: number): number; length(ct: number): number; + /** The graphics carve's base (Graphics Specification §5.1); the pool and + * registry live here. Exposed for surface-address math (PR: zero-copy + * canvas views). */ + readonly gfxPtr: number; + /** Graphics substrate surface (native; the public surface lives on Term). */ + imageBegin(ct: number, id: number, w: number, h: number): number; + imageCommit(ct: number, id: number): number; + imageRemove(ct: number, id: number): number; + graphicsCapability(ct: number, kitty: number): number; + graphicsResizePrepare(ct: number): number; setPointer(x: number, y: number, down: boolean): void; getPointerOverIds(): string[]; getElementBounds(id: string): BoundingBox | undefined; @@ -59,6 +73,7 @@ import { compiled } from "./wasm.ts"; export async function createTermNative( w: number, h: number, + imagePoolBytes?: number, ): Promise { let memory = new WebAssembly.Memory({ initial: 2 }); let exports: Record = {}; @@ -91,7 +106,7 @@ export async function createTermNative( let ct = exports as unknown as { __heap_base: WebAssembly.Global; clayterm_size(w: number, h: number): number; - init(mem: number, w: number, h: number): number; + init(mem: number, w: number, h: number, gfx: number): number; reduce( ct: number, buf: number, @@ -102,6 +117,13 @@ export async function createTermNative( ): void; output(ct: number): number; length(ct: number): number; + graphics_size(poolBytes: number): number; + graphics_init(mem: number, poolBytes: number): number; + image_begin(ct: number, id: number, w: number, h: number): number; + image_commit(ct: number, id: number): number; + image_remove(ct: number, id: number): number; + graphics_capability(ct: number, kitty: number): number; + graphics_resize_prepare(ct: number): number; Clay_SetPointerState(vec: number, down: number): void; pointer_over_count(): number; pointer_over_id_string_length(index: number): number; @@ -122,22 +144,32 @@ export async function createTermNative( let statePtr!: number; let opsBuf = 0; + let gfxPtr = 0; + let gfxBytes = 0; - // Renderer state and the fixed transfer buffer share linear memory as - // [heap: state][opsBuf]; opsBuf moves when the state size changes. Memory is - // grown to fit but never reclaimed, so a downsize keeps the high-water mark - // (renderer-spec 7.7). + // Linear memory layout: [heap: gfx carve][clayterm carve][opsBuf]. + // The gfx carve (Graphics Specification §5.1) is dimension-independent: + // allocated once and never re-initialized, so the image registry, pixel + // pool, and placements tables survive resize unchanged (§10.5). The + // clayterm carve and opsBuf move on resize exactly as before + // (renderer-spec 7.7): memory is grown to fit but never reclaimed. function layout(lw: number, lh: number): void { let heap = ct.__heap_base.value as number; + if (gfxPtr === 0) { + gfxBytes = ct.graphics_size(imagePoolBytes ?? DEFAULT_IMAGE_POOL_BYTES); + gfxPtr = heap; + ct.graphics_init(gfxPtr, imagePoolBytes ?? DEFAULT_IMAGE_POOL_BYTES); + } let size = ct.clayterm_size(lw, lh); - let needed = heap + size + transferBytes; + let clayBase = gfxPtr + gfxBytes; + let needed = clayBase + size + transferBytes; let pages = Math.ceil(needed / WASM_PAGE_BYTES); let current = memory.buffer.byteLength / WASM_PAGE_BYTES; if (pages > current) { memory.grow(pages - current); } - statePtr = ct.init(heap, lw, lh); - opsBuf = (heap + size + 3) & ~3; + statePtr = ct.init(clayBase, lw, lh, gfxPtr); + opsBuf = (clayBase + size + 3) & ~3; } layout(w, h); @@ -146,6 +178,9 @@ export async function createTermNative( get statePtr() { return statePtr; }, + get gfxPtr() { + return gfxPtr; + }, get opsBuf() { return opsBuf; }, @@ -155,6 +190,11 @@ export async function createTermNative( reduce: ct.reduce, output: ct.output, length: ct.length, + imageBegin: ct.image_begin, + imageCommit: ct.image_commit, + imageRemove: ct.image_remove, + graphicsCapability: ct.graphics_capability, + graphicsResizePrepare: ct.graphics_resize_prepare, animating: ct.animating, setPointer(x: number, y: number, down: boolean) { let view = new DataView(memory.buffer); diff --git a/vendor/font8x8_basic.h b/vendor/font8x8_basic.h new file mode 100644 index 0000000..125cf16 --- /dev/null +++ b/vendor/font8x8_basic.h @@ -0,0 +1,152 @@ +/** + * 8x8 monochrome bitmap fonts for rendering + * Author: Daniel Hepper + * + * License: Public Domain + * + * Based on: + * // Summary: font8x8.h + * // 8x8 monochrome bitmap fonts for rendering + * // + * // Author: + * // Marcel Sondaar + * // International Business Machines (public domain VGA fonts) + * // + * // License: + * // Public Domain + * + * Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm + **/ + +// Constant: font8x8_basic +// Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin) +char font8x8_basic[128][8] = { + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019 + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space) + { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!) + { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (") + { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#) + { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($) + { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%) + { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&) + { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (') + { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (() + { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ()) + { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*) + { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,) + { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.) + { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/) + { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0) + { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1) + { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2) + { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3) + { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4) + { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5) + { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6) + { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7) + { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8) + { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9) + { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:) + { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;) + { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<) + { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=) + { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>) + { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?) + { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@) + { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A) + { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B) + { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C) + { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D) + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E) + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F) + { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G) + { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H) + { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I) + { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J) + { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K) + { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L) + { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M) + { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N) + { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O) + { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P) + { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q) + { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R) + { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S) + { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T) + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U) + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V) + { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W) + { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X) + { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y) + { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z) + { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([) + { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\) + { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (]) + { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_) + { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`) + { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a) + { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b) + { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c) + { 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d) + { 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e) + { 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f) + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g) + { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h) + { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i) + { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j) + { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k) + { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l) + { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m) + { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n) + { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o) + { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p) + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q) + { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r) + { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s) + { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v) + { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w) + { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y) + { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z) + { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({) + { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|) + { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (}) + { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F +};