Files
lightcraft/docs/control-protocol.md
T
Brandon ThomasandClaude Opus 5.5 78e50b5fc5 M5.6a: memory report (library.memory, ui.inspect memory) and opt-in DHAT heap profile
Per-cache bytes: decoded thumbnail/preview/full sources, rendered previews,
GPU device buffers (allocated/pooled/retired), loupe stage caches (CPU and
GPU) and textures. lightcraft-cli --features dhat-heap adds live/peak heap and
writes dhat-heap.json.

Baseline (14 raws, loupe, 12 steps): peak RSS 1.9-2.3 GB, footprint 2.5-2.6 GB;
heap peak 634 MB after import (probes decode every raw), 930 MB during grid
thumbnails (parallel decodes); GPU free pool grows to 2 GB (exact-size reuse).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:28:13 -04:00

3.9 KiB
Raw Blame History

Control protocol

lightcraft --control 7980 (or LIGHTCRAFT_CONTROL_PORT=7980) starts a JSON-lines server on 127.0.0.1:7980 (loopback only). One request per line, one reply per line, in order:

→ {"id": 1, "method": "engine.execute", "params": {"command": "photo.rate", "params": {"rating": 4}}}
← {"id": 1, "ok": true, "result": null}
← {"id": 2, "ok": false, "error": "unknown command `nope`"}

Requests are answered on the UI thread between frames (timeout 60 s). The MCP server's connect mode (mcp.md) is a thin layer over this channel. Implementation: crates/ui-egui/src/control.rs (methods) and apps/lightcraft/src/control_server.rs (transport).

Methods

Method Params Result
engine.execute (alias ui.menu.invoke) {command, params?} Run any engine or UI command (see engine.commands)
engine.commands — Engine + UI commands: id, label, menu, shortcut, params doc, enabled
ui.menu.list — Menu entries
ui.inspect — UI state, window, canvas/image rects, active photo, selection, perf, status, memory (bytes per cache, see library.memory; plus stage caches and textures)
ui.widgets {filter?} On-screen widgets {id, rect: [x, y, w, h]} (screen points)
ui.clickWidget / ui.dragWidget {id, count?, fx?, fy?} / {id, toX?, toY?, dx?, dy?, steps?} Real egui input on a widget
ui.move / ui.click / ui.drag {x, y, count?, button?} / {x, y, toX, toY, steps?} Raw pointer input, screen points
ui.pointer {events: [{kind: down|drag|up, x, y}], alt?, shift?, cmd?} Gesture in normalized image coordinates (Detail view)
ui.key {key, cmd?, shift?, alt?, ctrl?} Key press
ui.text {text} Text input
ui.scroll {dx, dy} Mouse wheel
ui.set partial UI state, e.g. {"view": "detail"} Resulting UI state
ui.dialog.confirm / ui.dialog.cancel — Close the open dialog
ui.resize {width, height} Resize the window
ui.screenshot {path?, headless?} {path, width, height} once the frame (with finished renders) is captured. headless: true draws the UI on the CPU (no compositor needed); a windowed capture that gets no frame within 2 s falls back to headless automatically
ui.render {id?, size?, path?} Render a photo (PNG to path), {width, height}
app.quit — Close the app

Headless rendering (no window, no GPU)

The egui UI can be rasterized on the CPU (crates/ui-egui/src/softpaint.rs, driven by crates/ui-egui/src/headless.rs): same tessellation, gamma-space premultiplied blending and scissor clipping as egui's GPU backends, so the image matches the window (minus GPU dithering).

  • In the running app: {"method": "ui.screenshot", "params": {"path": "a.png", "headless": true}}. The UI is drawn into an offscreen context from the app's logic tick, which keeps running when the window is occluded or the display sleeps/locks. Windowed screenshots fall back to this after 2 s.
  • Without any app window: lightcraft-cli snapshot runs a whole app session headlessly and answers the same control requests (same handler) from a JSON-lines script:
lightcraft-cli snapshot --demo -o grid.png --size 1600x1000 [--scale 2]
lightcraft-cli snapshot --library DIR --script tour.jsonl -o shot.png

tour.jsonl holds one request per line (# comments allowed), e.g. {"method": "ui.set", "params": {"view": "detail", "right": "edit", "openSections": ["optics"]}} then {"method": "ui.screenshot"}. Replies are printed to stdout. A ui.screenshot without path writes -o (then OUT-2.png, OUT-3.png, …); ui.settle {timeoutMs?} waits until no renders are in flight. Each request runs frames until it is answered and its injected input (clicks, keys, drags) has played out. Widget ids for ui.clickWidget come from ui.widgets (e.g. button:upright-auto). A 1600×1000 demo snapshot takes ~1–2 s (debug build).