diff options
| -rw-r--r-- | Makefile | 15 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | surfaces/input.c | 1 | ||||
| -rw-r--r-- | surfaces/main.c | 125 | ||||
| -rw-r--r-- | surfaces/vega.h | 4 | ||||
| -rw-r--r-- | tests/check.py | 84 |
6 files changed, 222 insertions, 19 deletions
@@ -15,7 +15,7 @@ SURFACES = surfaces/screen.c surfaces/draw.c surfaces/width.c surfaces/web.c sur surfaces/input.c surfaces/main.c .PHONY: all check FORCE -all: $(BUILD)/vega $(BUILD)/vega-symbols +all: $(BUILD)/vega $(BUILD)/vega-symbols $(BUILD)/vega-surface.so $(BUILD): mkdir -p $@ @@ -24,7 +24,16 @@ $(LIBPURE): FORCE $(MAKE) -C $(PURE) build/libpure.a $(BUILD)/vega: $(SURFACES) surfaces/vega.h symbols/symbols.c symbols/symbols.h $(LIBPURE) | $(BUILD) - $(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE)/core -Isymbols $(SURFACES) symbols/symbols.c $(LIBPURE) $(LDFLAGS) -lm $(LDLIBS) -o $@ + $(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE)/core -I$(PURE)/serve -Isymbols $(SURFACES) symbols/symbols.c $(LIBPURE) $(LDFLAGS) -lm $(LDLIBS) -o $@ + +# The same host as a surface a Pure node maps (pure-serve --surface, Pure's +# serve/surface.h): the node's display. It calls the app model pure-serve +# exports, so it links no libpure.a of its own; it exports pure_surface_run +# alone, and what only the vega command uses is left out. +$(BUILD)/vega-surface.so: $(SURFACES) surfaces/vega.h symbols/symbols.c symbols/symbols.h | $(BUILD) + $(CC) $(CPPFLAGS) $(CFLAGS) -Wno-unused-function -fPIC -shared -fvisibility=hidden -ffunction-sections \ + -fdata-sections -DVEGA_SURFACE -I$(PURE)/core -I$(PURE)/serve -Isymbols $(SURFACES) symbols/symbols.c \ + $(LDFLAGS) -Wl,--gc-sections -lm $(LDLIBS) -o $@ # Building symbol files reads fonts through FreeType; reading them does not. FREETYPE_CFLAGS ?= $(shell pkg-config --cflags freetype2) @@ -36,7 +45,7 @@ $(BUILD)/vega-symbols: symbols/symbols-tool.c $(SYMBOLS) symbols/symbols.h symbo $(CC) $(CPPFLAGS) $(CFLAGS) $(FREETYPE_CFLAGS) symbols/symbols-tool.c $(SYMBOLS) $(LDFLAGS) \ $(FREETYPE_LIBS) $(GL_LIBS) -lm $(LDLIBS) -o $@ -check: $(BUILD)/vega $(BUILD)/vega-symbols +check: $(BUILD)/vega $(BUILD)/vega-symbols $(BUILD)/vega-surface.so $(MAKE) -C $(AUP) build/aup $(MAKE) -C $(PURE) build/pure-serve python3 tests/check.py --vega $(BUILD)/vega --symbols $(BUILD)/vega-symbols --aup $(AUP)/build/aup --serve $(PURE)/build/pure-serve @@ -33,7 +33,15 @@ curl -s -X POST -H 'Content-Type: application/json' -d '{"title": "ship it", "la curl -s 'localhost:8767/views/cards_list?from=0&count=20' ``` -한 저장소는 한 번에 한 호스트가 연다(상태의 소유자는 하나다). +한 저장소는 한 번에 한 호스트가 연다(상태의 소유자는 하나다). 사람과 프로그램이 동시에 쓰려면 Vega를 노드 안에서 +돌린다. `pure-serve`가 `build/vega-surface.so`를 매핑해 제 스레드에서 돌린다([Pure 설계 010](/srv/git/pure.git): +`docs/design/010-surfaces.md`). 디스플레이는 노드의 마지막 저장을 제자리에서 읽어 그리고, 사람이 치는 키와 글자는 HTTP +요청과 같은 줄로 노드의 소유자에게 간다. 그 사이에 HTTP나 JSON이 없다. + +```sh +../pure/build/pure-serve --module board.pmod --store ~/.board \ + --surface build/vega-surface.so -- display --symbols ui.vsym --live # 디스플레이와 HTTP가 한 노드를 쓴다 +``` 글자와 아이콘은 하나의 바이너리 형식(심볼 파일)으로 그린다. 폰트와 SVG는 빌드할 때 한 번만 해석한다 ([설계 002](docs/design/002-symbol-format.md)). @@ -50,7 +58,7 @@ build/vega-symbols render ui.vsym 0xE000 48 icon.pgm | 디렉터리 | 무엇 | 만드는 것 | |---|---|---| | `lib/` | aup 라이브러리(`use vega`): 목록·표·포커스·톤 같은 인터페이스 어휘 | aup가 읽는다 | -| `surfaces/` | 모듈을 사람에게 보이는 표면: TUI, CLI, 문장(NUI), 웹, 픽셀, 디스플레이(KMS). Pure의 `libpure.a`를 링크한다 | `build/vega` | +| `surfaces/` | 모듈을 사람에게 보이는 표면: TUI, CLI, 문장(NUI), 웹, 픽셀, 디스플레이(KMS). Pure의 `libpure.a`를 링크한다. 같은 호스트가 Pure 노드 안의 디스플레이도 된다(`pure-serve --surface`, 노드가 내보내는 앱 모델을 부른다) | `build/vega`, `build/vega-surface.so` | | `symbols/` | 글리프와 아이콘을 한 형식으로, CPU와 GPU로 그리기([설계 002](docs/design/002-symbol-format.md)) | `build/vega-symbols` | Pure와 같은 규칙이다([Pure 009](/srv/git/pure.git): `docs/design/009-layout.md`). 최상위 디렉터리가 역할별 부품이고, diff --git a/surfaces/input.c b/surfaces/input.c index b358d47..9403a75 100644 --- a/surfaces/input.c +++ b/surfaces/input.c @@ -84,6 +84,7 @@ VegaInput *vega_input_open(const char *const *paths, unsigned count, unsigned wi VegaInput *input = calloc(1, sizeof(*input)); if (!input) return NULL; input->width = width, input->height = height, input->x = (int)width / 2, input->y = (int)height / 2; + if (!paths) return input; /* none */ if (count) { /* files of recorded events, positions already in screen pixels */ for (unsigned i = 0; i < count; ++i) { int fd = open(paths[i], O_RDONLY | O_CLOEXEC); diff --git a/surfaces/main.c b/surfaces/main.c index 26aa4eb..a7488ab 100644 --- a/surfaces/main.c +++ b/surfaces/main.c @@ -2,6 +2,7 @@ #include "vega.h" #include "module-store.h" #include "store.h" +#include "surface.h" #include <poll.h> #include <signal.h> #include <stdlib.h> @@ -42,11 +43,19 @@ * (0 first) of a list or table; click is a mouse click on the last shown TUI * frame (1 first, as terminals report it); time sets the time the next * events see (live surfaces use the clock) and tick ticks a timer (live - * surfaces tick them on time); swap replaces the module as --watch does. */ + * surfaces tick them on time); swap replaces the module as --watch does. + * + * Built with VEGA_SURFACE the same host is a library a Pure node maps + * (pure-serve --surface vega-surface.so -- display ARGUMENTS, Pure's + * serve/surface.h): the display shows the node's app as of each save, read + * where it is, and what people type goes to the node's owner. */ static PureModule modules[2]; /* the running one (app.module) and the next */ static unsigned current; static PureApp app; +/* In a node: the node, whose owner applies the events; app then shows its + * last save (a reader) and applies none itself. */ +static const PureSurface *node; static VegaScreen screen; static VegaGrid *drawn; /* the TUI's last frame, which a click lands on */ static char entry[VEGA_TEXT]; @@ -153,10 +162,16 @@ static int key_input(const char *key) { return vega_key_input(app.module, key); /* A key press and release; false when the app has no such key or the press failed. */ static bool press(const char *key, int *input) { *input = key_input(key); + if (*input >= 0 && node) return node->press(node->host, (uint32_t)*input) == PURE_OK; return *input >= 0 && vega_press(&app, key); } -static bool commit(const char *text, size_t length) { return vega_commit(&app, &screen, text, length); } +static bool commit(const char *text, size_t length) { + if (node) + return screen.prompt >= 0 && + node->type(node->host, (uint32_t)screen.prompt, (PureSpan){(const unsigned char *)text, length}) == PURE_OK; + return vega_commit(&app, &screen, text, length); +} static const VegaPart *part_named(const char *name) { for (unsigned i = 0; i < screen.count; ++i) @@ -179,7 +194,7 @@ static bool mouse(unsigned button, unsigned line) { item = button == 64 ? focus - 1 : focus + 1; } } else if (button) return false; - return vega_point(&app, part, item); + return !node && vega_point(&app, part, item); /* a node takes keys and text, not pointing yet */ } static int hex(char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; } @@ -552,23 +567,29 @@ static void show_pointer(uint32_t *out, size_t stride, int x, int y, unsigned wi } /* Draw, show, then wait for input or the next tick, until a signal (or the - * end of recorded events: the last frame is then printed as text). */ + * end of recorded events: the last frame is then printed as text). In a + * node: draw its last save, wait for input or its next save, until it stops; + * its owner runs the timers, and the signals are its own. */ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded) { VegaGrid grid = {0}; if (!vega_grid(&grid, columns, rows)) return 2; drawn = &grid; - signal(SIGINT, on_stop); - signal(SIGTERM, on_stop); + if (!node) { + signal(SIGINT, on_stop); + signal(SIGTERM, on_stop); + } VegaTimers timers; vega_timers_start(&timers, app.module, vega_now()); char path[512] = ""; unsigned long differing = 0; bool open_input = true; int status = 0; - while (!stopping) { + while (!stopping && !(node && node->stopping(node->host))) { bool ticked = false; - long due = vega_timers_run(&timers, &app, vega_now(), &ticked); - vega_draw(&grid, &app, &screen, entry, entry_length, typing); + long due = node ? -1 : vega_timers_run(&timers, &app, vega_now(), &ticked); + PureSnapshot *snapshot = node ? node->read(node->host, &app) : NULL; + if (!node || snapshot) vega_draw(&grid, &app, &screen, entry, entry_length, typing); + if (snapshot) node->release(node->host, snapshot); unsigned width, height; size_t stride; uint32_t *out = vega_display_buffer(display, &width, &height, &stride); @@ -580,18 +601,25 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded if (pointer) show_pointer(out, stride, x, y, width, height); if (!show_frame(&grid, path, &differing, false)) { fputs("vega: the display did not take the frame\n", stderr); status = 1; break; } if (!open_input) break; - struct pollfd waits[32]; + struct pollfd waits[33]; unsigned count = vega_input_fds(input, waits, 32); - int timeout = screen.ticks ? 16 : -1; + if (node) waits[count++] = (struct pollfd){node->changes, POLLIN, 0}; /* the node saved: draw again */ + int timeout = screen.ticks && !node ? 16 : -1; if (due >= 0 && (timeout < 0 || due < timeout)) timeout = (int)due; if (poll(waits, count, timeout) <= 0) { + if (node) continue; app.now = vega_now(); if (screen.ticks) pure_app_frame(&app); continue; } - app.now = vega_now(); + if (!node) app.now = vega_now(); for (unsigned i = 0; i < count; ++i) { if (!(waits[i].revents & (POLLIN | POLLHUP | POLLERR))) continue; + if (node && waits[i].fd == node->changes) { + uint64_t saves; + if (read(node->changes, &saves, sizeof(saves)) < 0) { /* cleared already */ } + continue; + } VegaEvent events[64]; bool ended; unsigned n = vega_input_read(input, waits[i].fd, events, 64, &ended); @@ -610,6 +638,7 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded dump(&grid); if (path[0]) printf("frame %s differing %lu grid %ux%u cell %dx%d\n", path, differing, grid.width, grid.height, face.cell_width, face.cell_height); + fflush(stdout); } drawn = NULL; vega_grid_free(&grid); @@ -746,6 +775,77 @@ static bool open_face(const char *path, float size) { return ok; /* the bytes stay: the face reads them */ } +#ifdef VEGA_SURFACE +/* Vega in a Pure node (serve/surface.h): pure-serve ... --surface + * vega-surface.so -- display --symbols FONT.vsym [--size PX] [--card PATH] + * [--writeback WIDTHxHEIGHT --out PREFIX] [--live | --input EVENTS]. The + * display shows the node's app as of each save; keys and text go to its + * owner. Recorded events are read to their end, then the last frame is + * printed as text (on the node's standard output); live, it runs until the + * node stops. */ +__attribute__((visibility("default"))) int pure_surface_run(const PureSurface *surface, int argc, char **argv) { + if (surface->version != PURE_SURFACE_VERSION || surface->app_size != sizeof(PureApp) || + surface->module_size != sizeof(PureModule)) { + fputs("vega: this surface was built for another Pure\n", stderr); + return 2; + } + const char *symbols_path = NULL, *card = "/dev/dri/card1", *recorded = NULL; + unsigned width = 0, height = 0; + float size = 16; + bool writeback = false, live_input = false, usage = argc < 2 || strcmp(argv[1], "display"); + for (int i = 2; !usage && i < argc; i += 2) { + if (!strcmp(argv[i], "--live")) { + live_input = true; + --i; + continue; + } + if (i + 1 >= argc) usage = true; + else if (!strcmp(argv[i], "--symbols")) symbols_path = argv[i + 1]; + else if (!strcmp(argv[i], "--size") && sscanf(argv[i + 1], "%f", &size) == 1) {} + else if (!strcmp(argv[i], "--card")) card = argv[i + 1]; + else if (!strcmp(argv[i], "--input")) recorded = argv[i + 1]; + else if (!strcmp(argv[i], "--out")) frames_prefix = argv[i + 1]; + else if (!strcmp(argv[i], "--writeback") && sscanf(argv[i + 1], "%ux%u", &width, &height) == 2 && width && height) + writeback = true; + else usage = true; + } + if (usage || !symbols_path || (writeback && !frames_prefix) || (live_input && recorded)) { + fputs("vega-surface.so display --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WIDTHxHEIGHT --out PREFIX]\n" + " [--live | --input EVENTS]\n", stderr); + return 2; + } + if (!open_face(symbols_path, size)) return 2; + char error[256]; + if (!(display = vega_display_open(card, writeback, writeback ? width : 0, writeback ? height : 0, error, sizeof(error)))) { + fprintf(stderr, "vega: %s\n", error); + return 2; + } + unsigned pixels_across, pixels_down; + size_t stride; + vega_display_buffer(display, &pixels_across, &pixels_down, &stride); + node = surface; + vega_screen(&screen, surface->module); + typing = screen.prompt >= 0; + const char *paths[1] = {recorded}; + /* Without --live or --input, no devices: the node's saves alone draw it. */ + VegaInput *input = vega_input_open(live_input || recorded ? paths : NULL, recorded ? 1 : 0, pixels_across, pixels_down); + int status = 2; + if (!input) fprintf(stderr, "vega: cannot read %s\n", recorded ? recorded : "input devices"); + else { + PureSnapshot *first = node->read(node->host, &app); /* app shows the node's states from here on */ + if (first) node->release(node->host, first); + status = first ? live(input, pixels_across / (unsigned)face.cell_width, pixels_down / (unsigned)face.cell_height, + recorded != NULL) + : 2; + vega_input_close(input); + } + pure_app_stop(&app); + vega_display_close(display); + display = NULL; + node = NULL; + return status; +} +#else int main(int argc, char **argv) { unsigned width = 0, height = 0, port = 8080; const char *symbols_path = NULL, *card = "/dev/dri/card1"; @@ -841,3 +941,4 @@ int main(int argc, char **argv) { vega_display_close(display); return status; } +#endif diff --git a/surfaces/vega.h b/surfaces/vega.h index ba7b61f..fd8780b 100644 --- a/surfaces/vega.h +++ b/surfaces/vega.h @@ -155,8 +155,8 @@ void vega_display_close(VegaDisplay *); /* Input from evdev: keyboards (key names and the text they type on a US * layout), mice and touch screens (presses at a pixel of a width x height - * screen). With paths, recorded events are read from those files instead, - * positions in screen pixels. */ + * screen). With count paths, recorded events are read from those files + * instead, positions in screen pixels; with paths NULL, there is none. */ typedef enum { VEGA_EVENT_KEY, VEGA_EVENT_TEXT, VEGA_EVENT_PRESS, VEGA_EVENT_WHEEL } VegaEventKind; typedef struct { VegaEventKind kind; diff --git a/tests/check.py b/tests/check.py index 0e5c3bb..c0633c3 100644 --- a/tests/check.py +++ b/tests/check.py @@ -1160,6 +1160,89 @@ def live_display(vega, module, work): return {'events': len(events), 'lines': len(got)} +def node_display(vega, serve, module, work): + """Vega in a Pure node (vega-surface.so run by pure-serve --surface): the + node's app, which an HTTP client gave an item, is on its display, and + recorded keys typed there (an item, Enter, Tab, Space) go to the node's + owner. The last frame is the one the vega command draws for the same + state and keys, it came back from the display controller as drawn, and + the node's HTTP views show what was typed.""" + import http.client + import select + import shutil + import signal + import struct + import time + card, face, library = Path('/dev/dri/card1'), work / 'ui.vsym', vega.parent / 'vega-surface.so' + if not card.exists() or not face.exists() or not library.exists(): + return None + store = work / 'node-store' + + def start(*extra): + """The node, its port, and what it printed after the line that tells it.""" + process = subprocess.Popen([str(a) for a in (serve, '--module', module, '--store', store, '--port', 0, *extra)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + printed, deadline = '', time.time() + 10 + while '\n' not in printed and time.time() < deadline: + if select.select([process.stdout], [], [], 1)[0]: + printed += os.read(process.stdout.fileno(), 65536).decode() + assert '\n' in printed, 'node did not start' + first, rest = printed.split('\n', 1) + return process, json.loads(first)['port'], rest + + def post(port, path, body): + connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10) + connection.request('POST', path, body) + status = connection.getresponse().status + connection.close() + return status + + node, port, _ = start() + try: + assert post(port, '/inputs/add', b'eggs') == 200 + finally: + node.send_signal(signal.SIGTERM) + node.communicate(timeout=30) + before = work / 'node-store-before' + shutil.copytree(store, before) + codes = {'m': 50, 'i': 23, 'l': 38, 'k': 37, 'enter': 28, 'tab': 15, 'space': 57} + events = [] # (type, code, value): EV_KEY 1, EV_SYN 0 + for name in ('m', 'i', 'l', 'k', 'enter', 'tab', 'space'): + events.extend([(1, codes[name], 1), (0, 0, 0), (1, codes[name], 0), (0, 0, 0)]) + recording = work / 'node-events.bin' + recording.write_bytes(b''.join(struct.pack('llHHi', 0, 0, *event) for event in events)) + node, port, printed = start('--surface', library, '--', 'display', '--symbols', face, '--size', 18, '--card', card, + '--writeback', '640x480', '--out', work / 'node', '--input', recording) + lines = [] + try: + deadline = time.time() + 30 + while '\nframe ' not in '\n' + printed and time.time() < deadline: + ready, _, _ = select.select([node.stdout], [], [], 1) + if ready: + printed += os.read(node.stdout.fileno(), 65536).decode() + lines = printed.splitlines() + assert any(line.startswith('frame ') for line in lines), lines + left = http.client.HTTPConnection('127.0.0.1', port, timeout=10) + left.request('GET', '/views/left') + shown = left.getresponse().read().decode() + left.close() + finally: + node.send_signal(signal.SIGTERM) + _, error = node.communicate(timeout=30) + if node.returncode == 1 and b'writeback' in error: + return None + assert node.returncode == 0 and b'"returned":0' in error, error[-800:] + assert shown == '1 of 2', shown # eggs to do, milk done + frame = next(line.split() for line in lines if line.startswith('frame ')) + assert frame[3] == '0', frame # came back as drawn + script = f'type {"milk".encode().hex()}\nenter\ntab\nkey Space\nshow\n' + tui = run(vega, 'tui', module, '--store', before, '--dump', frame[5], stdin=script) + expected = [line for line in tui.stdout.splitlines() if not line.startswith('cells ')] + got = [line for line in lines if not line.startswith('frame ')] + assert got == expected, (got, expected) + return {'events': len(events), 'lines': len(got)} + + def width_of(character): return width(character) or 0 @@ -1205,6 +1288,7 @@ def main(): report['pixels'] = pixels(vega, args.symbols.resolve(), modules['todo'], work) report['display'] = display(vega, modules['todo'], work) report['live_display'] = live_display(vega, modules['todo'], work) + report['node_display'] = node_display(vega, args.serve.resolve(), modules['todo'], work) print(json.dumps(report)) |
