diff options
| author | eunhhu <[email protected]> | 2026-09-27 14:59:54 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-27 14:59:54 +0100 |
| commit | 5313285bbfedf6154d2611c4923cff458d7ef271 (patch) | |
| tree | 81a9d1516f5666c351cf716b394523255d9a5183 | |
| parent | 993871c6dfa19cf4e0bd4a16c9aad26cb0025d70 (diff) | |
| download | vega-5313285bbfedf6154d2611c4923cff458d7ef271.tar.gz vega-5313285bbfedf6154d2611c4923cff458d7ef271.zip | |
Put the frames on a display through KMS, checked by writeback
vega display draws the same frames on a display with the kernel's mode
setting and no library between: a connected connector's mode on a CRTC, its
primary plane flipped between two dumb buffers by atomic commits, the grid
as many cells as fit. Frames are drawn in cached memory and copied over,
since blending over write-combined buffers read them back slowly (a 1080p
frame took 370 ms; now 20.6 ms with a vertical sync and the check below).
This Pi has no screen but its display controller has writeback connectors:
--writeback sets a mode on one and takes every frame back as the controller
put it out. Each came back exactly as drawn, and the new check finds it the
pixel surface's frame for the same grid, pixel for pixel.
| -rw-r--r-- | Makefile | 3 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | docs/design/003-pixel-surface.md | 19 | ||||
| -rw-r--r-- | surfaces/display.c | 264 | ||||
| -rw-r--r-- | surfaces/main.c | 89 | ||||
| -rw-r--r-- | surfaces/pixels.c | 4 | ||||
| -rw-r--r-- | surfaces/vega.h | 16 | ||||
| -rw-r--r-- | tests/check.py | 37 |
8 files changed, 421 insertions, 14 deletions
@@ -11,7 +11,8 @@ LIBPURE = $(PURE)/build/libpure.a # Parts: lib/ (the aup library: the interface vocabulary), surfaces/ (the vega # host: TUI, CLI, NUI sentences, web), symbols/ (glyphs and icons as one # format, drawn by CPU or GPU). -SURFACES = surfaces/screen.c surfaces/draw.c surfaces/width.c surfaces/web.c surfaces/pixels.c surfaces/main.c +SURFACES = surfaces/screen.c surfaces/draw.c surfaces/width.c surfaces/web.c surfaces/pixels.c surfaces/display.c \ + surfaces/main.c .PHONY: all check FORCE all: $(BUILD)/vega $(BUILD)/vega-symbols @@ -16,6 +16,7 @@ printf 'text 6d696c6b\nkey Space\nshow\n' | build/vega say todo.pmod build/vega web todo.pmod --watch # 브라우저(GUI). --watch: 모듈이 다시 빌드되면 상태를 이어 바꾼다 # (옆에서 AUP_PATH=lib aup watch examples/todo.aup -o todo.pmod) build/vega pixels todo.pmod --symbols ui.vsym --grid 48x12 --out frame < script # 픽셀: frame-N.ppm (설계 003) +build/vega display todo.pmod --symbols ui.vsym < script # 디스플레이(KMS). --writeback WxH --out: 화면 없이 되돌려 받기 make check # 참조 모델 대조, 프레임 검사, 대화형 pty 검사, 견고성 ``` @@ -48,7 +49,7 @@ build/vega-symbols render ui.vsym 0xE000 48 icon.pgm | 디렉터리 | 무엇 | 만드는 것 | |---|---|---| | `lib/` | aup 라이브러리(`use vega`): 목록·표·포커스·톤 같은 인터페이스 어휘 | aup가 읽는다 | -| `surfaces/` | 모듈을 사람에게 보이는 표면: TUI, CLI, 문장(NUI), 웹, 픽셀. Pure의 `libpure.a`를 링크한다 | `build/vega` | +| `surfaces/` | 모듈을 사람에게 보이는 표면: TUI, CLI, 문장(NUI), 웹, 픽셀, 디스플레이(KMS). Pure의 `libpure.a`를 링크한다 | `build/vega` | | `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/docs/design/003-pixel-surface.md b/docs/design/003-pixel-surface.md index f01406d..9d9f2b1 100644 --- a/docs/design/003-pixel-surface.md +++ b/docs/design/003-pixel-surface.md @@ -28,14 +28,29 @@ Raspberry Pi 5. todo 예제에 한글 항목 넷, 48×12칸, 20px: 실행 전체(3.6 MB 심볼 파일 열기, 글리프 약 40개 굽기, 그리기, PPM 쓰기)가 7 ms다. 192×45칸(1920×1350)은 프레임마다 12 ms다(그리기, 합성, 7.8 MB PPM을 `/dev/shm`에 쓰기). +## 디스플레이(KMS) + +`vega display MODULE --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WxH --out PREFIX] < script`: 같은 프레임을 +디스플레이에 낸다(`surfaces/display.c`). 라이브러리 없이 커널의 ioctl로 한다. 연결된 커넥터를 찾아 그 모드를 CRTC에 걸고, +주 평면(primary plane)에 덤 버퍼 둘 중 하나를 atomic 커밋으로 넘긴다. 격자는 화면에 드는 만큼의 칸이다. 프레임은 캐시되는 +메모리에 그린 뒤 덤 버퍼로 복사한다. 덤 버퍼는 write-combine 메모리여서 글리프를 섞으려고 그 메모리를 읽으면 느리다. 처음에는 +그 버퍼에 바로 그렸고, 1080p 한 장에 약 370 ms가 들었다. + +이 Pi(vc4)는 HDMI가 비어 있어도 writeback 커넥터가 둘 있다. 디스플레이 컨트롤러가 내보낸 프레임을 메모리로 되돌려 쓰는 +출력이다. `--writeback`은 그 커넥터에 모드를 걸고, 프레임마다 돌아온 그림을 그린 것과 대 보고 PPM으로 쓴다. 1920×1080에서 +프레임 하나가 20.6 ms다(합성, 복사, 커밋과 수직 동기 한 번(60 Hz, 16.7 ms), 캡처를 읽어 대 보고 6 MB PPM을 쓰기까지). 돌아온 +그림은 늘 그린 것과 같았다(다른 픽셀 0). + ## 확인 +`tests/check.py` 11번과 12번. 12번은 writeback 커넥터가 있으면 같은 스크립트를 디스플레이로 돌린다. 프레임마다 돌아온 그림이 +그린 것과 같고, 같은 격자의 픽셀 표면 그림과 픽셀 하나까지 같다(640×480에서 프레임 둘, 589,512 픽셀). + `tests/check.py` 11번. 같은 스크립트를 `tui --dump`와 `pixels`로 돌려 글이 같고, 그림에서 빈 칸은 그 행의 배경(포커스 행은 제 색)뿐이며 글자가 있는 칸에는 잉크가 있다(프레임 둘, 칸 1,024개). ## 다음 -- 화면에 내기: KMS 덤 버퍼(XRGB8888)에 바로 합성하고 페이지를 넘긴다. 이 Pi는 HDMI가 비어 있어도 writeback 커넥터가 있어 화면 - 없이 확인할 수 있다. 바뀐 칸만 다시 그린다(TUI가 이미 칸마다 보인 것을 기억한다). +- 바뀐 칸만 다시 그리고 복사한다(TUI가 이미 칸마다 보인 것을 기억한다). 커밋을 기다리지 않고 페이지 넘김 이벤트로 맞춘다. - 입력: evdev(키보드, 마우스, 터치). 칸의 격자가 있으니 클릭이 곧 행이다(`vega_hit`). - 비례 글꼴의 줄: 칸이 아닌 글자 폭으로 놓는 표와 문단. 합성은 GPU로(아틀라스, [002](002-symbol-format.md)). diff --git a/surfaces/display.c b/surfaces/display.c new file mode 100644 index 0000000..9207963 --- /dev/null +++ b/surfaces/display.c @@ -0,0 +1,264 @@ +#define _DEFAULT_SOURCE +#include "vega.h" +#include <drm/drm.h> +#include <drm/drm_fourcc.h> +#include <drm/drm_mode.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#include <unistd.h> + +/* A display through KMS (the kernel's mode setting), with no library between: + * a connector's mode on a CRTC, its primary plane showing one of two dumb + * buffers (0xRRGGBB pixels the surface draws into), flipped by atomic + * commits. On a writeback connector (a display controller's output written + * back to memory) each shown frame is captured, so the whole path can be + * checked with no screen attached. */ + +typedef struct { + uint32_t handle, id, pitch; /* dumb buffer, framebuffer */ + uint64_t size; + uint32_t *pixels; +} Buffer; + +struct VegaDisplay { + int fd; + uint32_t connector, crtc, plane, mode_blob; + struct drm_mode_modeinfo mode; + bool writeback, set; + Buffer buffers[2], capture; + unsigned back; + /* Frames are drawn here, in cached memory, then copied to the buffer + * shown: dumb buffers are write-combined, and blending reads what it + * draws over, which such memory makes slow. */ + uint32_t *shadow; + /* property ids */ + uint32_t connector_crtc, crtc_mode, crtc_active, plane_fb, plane_crtc, plane_src[4], plane_dst[4], writeback_fb, + writeback_fence; +}; + +enum { PRIMARY = 1 }; /* the value of a plane's "type" property for a primary plane */ + +static int io(int fd, unsigned long request, void *arg) { + int r; + do { r = ioctl(fd, request, arg); } while (r && (errno == EINTR || errno == EAGAIN)); + return r; +} + +/* The id of an object's property by name, and its value. */ +static uint32_t property(int fd, uint32_t object, uint32_t type, const char *name, uint64_t *value) { + struct drm_mode_obj_get_properties list = {.obj_id = object, .obj_type = type}; + if (io(fd, DRM_IOCTL_MODE_OBJ_GETPROPERTIES, &list) || !list.count_props) return 0; + uint32_t *ids = calloc(list.count_props, sizeof(*ids)); + uint64_t *values = calloc(list.count_props, sizeof(*values)); + uint32_t found = 0; + list.props_ptr = (uintptr_t)ids; + list.prop_values_ptr = (uintptr_t)values; + if (ids && values && !io(fd, DRM_IOCTL_MODE_OBJ_GETPROPERTIES, &list)) + for (uint32_t i = 0; i < list.count_props && !found; ++i) { + struct drm_mode_get_property p = {.prop_id = ids[i]}; + if (!io(fd, DRM_IOCTL_MODE_GETPROPERTY, &p) && !strcmp(p.name, name)) { + found = ids[i]; + if (value) *value = values[i]; + } + } + free(ids); + free(values); + return found; +} + +static bool make_buffer(int fd, unsigned width, unsigned height, Buffer *out) { + struct drm_mode_create_dumb create = {.width = width, .height = height, .bpp = 32}; + if (io(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create)) return false; + *out = (Buffer){.handle = create.handle, .pitch = create.pitch, .size = create.size}; + struct drm_mode_fb_cmd2 fb = {.width = width, .height = height, .pixel_format = DRM_FORMAT_XRGB8888, + .handles = {create.handle}, .pitches = {create.pitch}}; + struct drm_mode_map_dumb map = {.handle = create.handle}; + if (io(fd, DRM_IOCTL_MODE_ADDFB2, &fb) || io(fd, DRM_IOCTL_MODE_MAP_DUMB, &map)) return false; + out->id = fb.fb_id; + void *pixels = mmap(NULL, create.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, (off_t)map.offset); + if (pixels == MAP_FAILED) return false; + out->pixels = pixels; + return true; +} + +static void free_buffer(int fd, Buffer *buffer) { + if (buffer->pixels) munmap(buffer->pixels, buffer->size); + if (buffer->id) io(fd, DRM_IOCTL_MODE_RMFB, &buffer->id); + if (buffer->handle) { + struct drm_mode_destroy_dumb destroy = {.handle = buffer->handle}; + io(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); + } + *buffer = (Buffer){0}; +} + +#define FAIL(...) do { snprintf(error, error_size, __VA_ARGS__); vega_display_close(d); return NULL; } while (0) + +VegaDisplay *vega_display_open(const char *card, bool writeback, unsigned width, unsigned height, char *error, + size_t error_size) { + VegaDisplay *d = calloc(1, sizeof(*d)); + if (!d) { snprintf(error, error_size, "out of memory"); return NULL; } + d->fd = open(card, O_RDWR | O_CLOEXEC); + d->writeback = writeback; + if (d->fd < 0) FAIL("cannot open %s: %s", card, strerror(errno)); + struct drm_set_client_cap caps[] = {{DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1}, {DRM_CLIENT_CAP_ATOMIC, 1}, + {DRM_CLIENT_CAP_WRITEBACK_CONNECTORS, 1}}; + for (unsigned i = 0; i < (writeback ? 3u : 2u); ++i) + if (io(d->fd, DRM_IOCTL_SET_CLIENT_CAP, &caps[i])) FAIL("%s has no atomic mode setting", card); + struct drm_mode_card_res res = {0}; + if (io(d->fd, DRM_IOCTL_MODE_GETRESOURCES, &res)) FAIL("%s sets no modes", card); + uint32_t crtcs[16] = {0}, connectors[16] = {0}, encoders[16] = {0}; + if (res.count_crtcs > 16 || res.count_connectors > 16 || res.count_encoders > 16) FAIL("too many outputs"); + res = (struct drm_mode_card_res){.crtc_id_ptr = (uintptr_t)crtcs, .connector_id_ptr = (uintptr_t)connectors, + .encoder_id_ptr = (uintptr_t)encoders, .count_crtcs = res.count_crtcs, + .count_connectors = res.count_connectors, .count_encoders = res.count_encoders}; + if (io(d->fd, DRM_IOCTL_MODE_GETRESOURCES, &res)) FAIL("%s sets no modes", card); + /* The first connected connector of the kind asked for, its preferred (or + * asked) mode, and a CRTC its encoder can drive. */ + uint32_t possible = 0; + for (uint32_t i = 0; i < res.count_connectors && !d->connector; ++i) { + struct drm_mode_modeinfo modes[64]; + struct drm_mode_get_connector c = {.connector_id = connectors[i]}; + if (io(d->fd, DRM_IOCTL_MODE_GETCONNECTOR, &c)) continue; + bool back = c.connector_type == DRM_MODE_CONNECTOR_WRITEBACK; + if (back != writeback || c.connection != 1 || !c.count_modes || c.count_modes > 64) continue; + uint32_t encoder_ids[8] = {0}; + struct drm_mode_get_connector full = {.connector_id = connectors[i], .modes_ptr = (uintptr_t)modes, + .count_modes = c.count_modes, .encoders_ptr = (uintptr_t)encoder_ids, + .count_encoders = c.count_encoders < 8 ? c.count_encoders : 8}; + if (io(d->fd, DRM_IOCTL_MODE_GETCONNECTOR, &full) || !full.count_encoders) continue; + int chosen = -1; + for (uint32_t m = 0; m < full.count_modes && chosen < 0; ++m) + if (width ? modes[m].hdisplay == width && modes[m].vdisplay == height : (modes[m].type & DRM_MODE_TYPE_PREFERRED)) + chosen = (int)m; + if (chosen < 0 && !width) chosen = 0; + if (chosen < 0) continue; + struct drm_mode_get_encoder e = {.encoder_id = encoder_ids[0]}; + if (io(d->fd, DRM_IOCTL_MODE_GETENCODER, &e)) continue; + d->connector = connectors[i]; + d->mode = modes[chosen]; + possible = e.possible_crtcs; + } + if (!d->connector) FAIL(writeback ? "no writeback connector with a %ux%u mode" : "no display connected (%ux%u)", width, height); + for (uint32_t i = 0; i < res.count_crtcs && !d->crtc; ++i) + if (possible & (1u << i)) d->crtc = crtcs[i]; + /* The CRTC's primary plane. */ + struct drm_mode_get_plane_res planes = {0}; + uint32_t plane_ids[64] = {0}; + if (io(d->fd, DRM_IOCTL_MODE_GETPLANERESOURCES, &planes) || planes.count_planes > 64) FAIL("no planes"); + planes.plane_id_ptr = (uintptr_t)plane_ids; + if (io(d->fd, DRM_IOCTL_MODE_GETPLANERESOURCES, &planes)) FAIL("no planes"); + int crtc_index = -1; + for (uint32_t i = 0; i < res.count_crtcs; ++i) if (crtcs[i] == d->crtc) crtc_index = (int)i; + for (uint32_t i = 0; i < planes.count_planes && !d->plane && crtc_index >= 0; ++i) { + struct drm_mode_get_plane p = {.plane_id = plane_ids[i]}; + uint64_t type = 0; + if (io(d->fd, DRM_IOCTL_MODE_GETPLANE, &p) || !(p.possible_crtcs & (1u << crtc_index))) continue; + if (property(d->fd, plane_ids[i], DRM_MODE_OBJECT_PLANE, "type", &type) && type == PRIMARY) + d->plane = plane_ids[i]; + } + if (!d->crtc || !d->plane) FAIL("no CRTC and plane for the connector"); + const char *src[4] = {"SRC_X", "SRC_Y", "SRC_W", "SRC_H"}, *dst[4] = {"CRTC_X", "CRTC_Y", "CRTC_W", "CRTC_H"}; + d->connector_crtc = property(d->fd, d->connector, DRM_MODE_OBJECT_CONNECTOR, "CRTC_ID", NULL); + d->crtc_mode = property(d->fd, d->crtc, DRM_MODE_OBJECT_CRTC, "MODE_ID", NULL); + d->crtc_active = property(d->fd, d->crtc, DRM_MODE_OBJECT_CRTC, "ACTIVE", NULL); + d->plane_fb = property(d->fd, d->plane, DRM_MODE_OBJECT_PLANE, "FB_ID", NULL); + d->plane_crtc = property(d->fd, d->plane, DRM_MODE_OBJECT_PLANE, "CRTC_ID", NULL); + for (int i = 0; i < 4; ++i) { + d->plane_src[i] = property(d->fd, d->plane, DRM_MODE_OBJECT_PLANE, src[i], NULL); + d->plane_dst[i] = property(d->fd, d->plane, DRM_MODE_OBJECT_PLANE, dst[i], NULL); + } + if (writeback) { + d->writeback_fb = property(d->fd, d->connector, DRM_MODE_OBJECT_CONNECTOR, "WRITEBACK_FB_ID", NULL); + d->writeback_fence = property(d->fd, d->connector, DRM_MODE_OBJECT_CONNECTOR, "WRITEBACK_OUT_FENCE_PTR", NULL); + if (!d->writeback_fb || !d->writeback_fence) FAIL("the writeback connector lacks its properties"); + } + if (!d->connector_crtc || !d->crtc_mode || !d->crtc_active || !d->plane_fb || !d->plane_crtc) FAIL("missing properties"); + struct drm_mode_create_blob blob = {.data = (uintptr_t)&d->mode, .length = sizeof(d->mode)}; + if (io(d->fd, DRM_IOCTL_MODE_CREATEPROPBLOB, &blob)) FAIL("cannot make the mode"); + d->mode_blob = blob.blob_id; + for (int i = 0; i < 2; ++i) + if (!make_buffer(d->fd, d->mode.hdisplay, d->mode.vdisplay, &d->buffers[i])) FAIL("cannot make buffers"); + if (writeback && !make_buffer(d->fd, d->mode.hdisplay, d->mode.vdisplay, &d->capture)) FAIL("cannot make buffers"); + if (!(d->shadow = malloc((size_t)d->mode.hdisplay * d->mode.vdisplay * sizeof(*d->shadow)))) FAIL("out of memory"); + return d; +} + +uint32_t *vega_display_buffer(VegaDisplay *d, unsigned *width, unsigned *height, size_t *stride) { + *width = d->mode.hdisplay; + *height = d->mode.vdisplay; + *stride = d->mode.hdisplay; + return d->shadow; +} + +const uint32_t *vega_display_captured(const VegaDisplay *d, size_t *stride) { + *stride = d->capture.pitch / 4; + return d->writeback ? d->capture.pixels : NULL; +} + +/* One atomic commit: the back buffer on the plane (and, the first time, the + * mode on the CRTC and the CRTC on the connector); on writeback, the capture + * buffer and a fence for when it holds the frame. */ +bool vega_display_show(VegaDisplay *d) { + uint32_t objects[3], counts[3] = {0}, ids[24]; + uint64_t values[24]; + unsigned n = 0, o = 0; + int fence = -1; +#define SET(object, id, value) do { \ + if (!o || objects[o - 1] != (object)) objects[o++] = (object); \ + ++counts[o - 1]; ids[n] = (id); values[n++] = (value); } while (0) + Buffer *shown = &d->buffers[d->back]; + for (unsigned y = 0; y < d->mode.vdisplay; ++y) + memcpy((unsigned char *)shown->pixels + (size_t)y * shown->pitch, d->shadow + (size_t)y * d->mode.hdisplay, + (size_t)d->mode.hdisplay * sizeof(*d->shadow)); + if (!d->set || d->writeback) SET(d->connector, d->connector_crtc, d->crtc); + if (d->writeback) { + SET(d->connector, d->writeback_fb, d->capture.id); + SET(d->connector, d->writeback_fence, (uintptr_t)&fence); + } + if (!d->set) { + SET(d->crtc, d->crtc_mode, d->mode_blob); + SET(d->crtc, d->crtc_active, 1); + } + SET(d->plane, d->plane_fb, shown->id); + SET(d->plane, d->plane_crtc, d->crtc); + uint64_t size[4] = {0, 0, (uint64_t)d->mode.hdisplay << 16, (uint64_t)d->mode.vdisplay << 16}; + uint64_t place[4] = {0, 0, d->mode.hdisplay, d->mode.vdisplay}; + for (int i = 0; i < 4; ++i) { + if (d->plane_src[i]) SET(d->plane, d->plane_src[i], size[i]); + if (d->plane_dst[i]) SET(d->plane, d->plane_dst[i], place[i]); + } +#undef SET + struct drm_mode_atomic commit = {.flags = d->set ? 0 : DRM_MODE_ATOMIC_ALLOW_MODESET, .count_objs = o, + .objs_ptr = (uintptr_t)objects, .count_props_ptr = (uintptr_t)counts, + .props_ptr = (uintptr_t)ids, .prop_values_ptr = (uintptr_t)values}; + if (io(d->fd, DRM_IOCTL_MODE_ATOMIC, &commit)) return false; + d->set = true; + d->back ^= 1; + if (fence >= 0) { /* the frame is in the capture buffer when the fence signals */ + struct pollfd wait = {.fd = fence, .events = POLLIN}; + bool done = poll(&wait, 1, 1000) == 1; + close(fence); + if (!done) return false; + } + return true; +} + +void vega_display_close(VegaDisplay *d) { + if (!d) return; + if (d->fd >= 0) { + for (int i = 0; i < 2; ++i) free_buffer(d->fd, &d->buffers[i]); + free_buffer(d->fd, &d->capture); + free(d->shadow); + if (d->mode_blob) { + struct drm_mode_destroy_blob blob = {.blob_id = d->mode_blob}; + io(d->fd, DRM_IOCTL_MODE_DESTROYPROPBLOB, &blob); + } + close(d->fd); + } + free(d); +} diff --git a/surfaces/main.c b/surfaces/main.c index d00bca6..0d02cce 100644 --- a/surfaces/main.c +++ b/surfaces/main.c @@ -23,6 +23,11 @@ * pixels --symbols FONT.vsym [--size PX] --grid WxH --out PREFIX * the TUI's frames drawn with a font's glyphs, driven by a * script: each show writes PREFIX-N.ppm after the text + * display --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WxH --out PREFIX] + * the same on a display through KMS, as many cells as fit, + * driven by a script; with writeback each shown frame comes + * back from the display controller, is set against what was + * drawn and written to PREFIX-N.ppm * With --store the app's states live in that tree store: the app continues * where it was, whichever surface runs it. With --watch (tui, web) a module * written again replaces the running one and states keep their values by @@ -459,7 +464,7 @@ static int interactive(void) { /* ---- scripts -------------------------------------------------------- */ -typedef enum { SURFACE_CLI, SURFACE_SAY, SURFACE_TUI, SURFACE_WEB, SURFACE_PIXELS } Surface; +typedef enum { SURFACE_CLI, SURFACE_SAY, SURFACE_TUI, SURFACE_WEB, SURFACE_PIXELS, SURFACE_DISPLAY } Surface; /* The pixel surface's font and where its frames go. */ static VegaFace face; @@ -490,11 +495,48 @@ static bool write_frame(const VegaGrid *grid, char path[512]) { return ok; } +static VegaDisplay *display; + +/* A frame on the display; with writeback, what came back is set against it + * and written (differing: how many pixels are not what was drawn). */ +static bool show_frame(const VegaGrid *grid, char path[512], unsigned long *differing) { + unsigned width, height; + size_t stride, captured_stride; + uint32_t *out = vega_display_buffer(display, &width, &height, &stride); + vega_pixels_clear(out, stride, width, height); + vega_pixels(grid, &face, out, stride); + if (!vega_display_show(display)) return false; + const uint32_t *back = vega_display_captured(display, &captured_stride); + *differing = 0; + path[0] = 0; + if (!back || !frames_prefix) return true; + /* The capture read once, a row at a time (it is not cached memory either). */ + uint32_t *row = malloc((size_t)width * sizeof(*row)); + unsigned char *rgb = malloc((size_t)width * height * 3); + bool ok = row && rgb; + for (unsigned y = 0; ok && y < height; ++y) { + memcpy(row, back + (size_t)y * captured_stride, (size_t)width * sizeof(*row)); + for (unsigned x = 0; x < width; ++x) { + *differing += (row[x] & 0xffffff) != (out[(size_t)y * stride + x] & 0xffffff); + unsigned char *p = rgb + ((size_t)y * width + x) * 3; + p[0] = (unsigned char)(row[x] >> 16), p[1] = (unsigned char)(row[x] >> 8), p[2] = (unsigned char)row[x]; + } + } + snprintf(path, 512, "%s-%u.ppm", frames_prefix, ++frames); + FILE *f = ok ? fopen(path, "wb") : NULL; + ok = f && fprintf(f, "P6\n%u %u\n255\n", width, height) > 0 && fwrite(rgb, 3, (size_t)width * height, f) == (size_t)width * height; + if (f && fclose(f)) ok = false; + free(row); + free(rgb); + return ok; +} + + /* painting: show writes the terminal bytes of the frame, not the grid. */ static int script(Surface surface, unsigned width, unsigned height, bool painting) { static char line[1 << 16]; VegaGrid grid = {0}; - bool gridded = surface == SURFACE_TUI || surface == SURFACE_PIXELS; + bool gridded = surface == SURFACE_TUI || surface == SURFACE_PIXELS || surface == SURFACE_DISPLAY; if (gridded && !vega_grid(&grid, width, height)) return 2; bool shown = false; drawn = gridded ? &grid : NULL; @@ -572,9 +614,13 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin dump(&grid); printf("cells %lu\n", vega_evaluations() - counted); char path[512]; + unsigned long differing; if (surface == SURFACE_PIXELS) { if (!write_frame(&grid, path)) { fprintf(stderr, "vega: cannot write %s\n", path); return 1; } printf("frame %s %dx%d\n", path, (int)grid.width * face.cell_width, (int)grid.height * face.cell_height); + } else if (surface == SURFACE_DISPLAY) { + if (!show_frame(&grid, path, &differing)) { fputs("vega: the display did not take the frame\n", stderr); return 1; } + if (path[0]) printf("frame %s differing %lu grid %ux%u\n", path, differing, grid.width, grid.height); } } counted = vega_evaluations(); @@ -594,6 +640,9 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin return 0; } +/* Surfaces that draw glyphs. */ +static bool glyphed(const char *surface) { return !strcmp(surface, "pixels") || !strcmp(surface, "display"); } + /* The symbols a pixel surface draws with, read whole and kept. */ static bool open_face(const char *path, float size) { static VegaSymbols symbols; @@ -614,8 +663,9 @@ static bool open_face(const char *path, float size) { int main(int argc, char **argv) { unsigned width = 0, height = 0, port = 8080; - const char *symbols_path = NULL; + const char *symbols_path = NULL, *card = "/dev/dri/card1"; float size = 16; + bool writeback = false; bool dumped = false, painting = false, watch = false, usage = argc < 3; const char *store = NULL, *listen_on = "127.0.0.1"; for (int i = 3; !usage && i < argc; i += 2) { @@ -633,29 +683,47 @@ int main(int argc, char **argv) { (!strcmp(argv[i], "--grid") && !strcmp(argv[1], "pixels"))) && sscanf(argv[i + 1], "%ux%u", &width, &height) == 2 && width && height && width <= 1000 && height <= 1000) dumped = true, painting = !strcmp(argv[i], "--paint"); - else if (!strcmp(argv[i], "--symbols") && !strcmp(argv[1], "pixels")) symbols_path = argv[i + 1]; - else if (!strcmp(argv[i], "--out") && !strcmp(argv[1], "pixels")) frames_prefix = argv[i + 1]; - else if (!strcmp(argv[i], "--size") && !strcmp(argv[1], "pixels") && sscanf(argv[i + 1], "%f", &size) == 1) {} + else if (!strcmp(argv[i], "--writeback") && !strcmp(argv[1], "display") && + sscanf(argv[i + 1], "%ux%u", &width, &height) == 2 && width && height) + writeback = true; + else if (!strcmp(argv[i], "--card") && !strcmp(argv[1], "display")) card = argv[i + 1]; + else if (!strcmp(argv[i], "--symbols") && glyphed(argv[1])) symbols_path = argv[i + 1]; + else if (!strcmp(argv[i], "--out") && glyphed(argv[1])) frames_prefix = argv[i + 1]; + else if (!strcmp(argv[i], "--size") && glyphed(argv[1]) && sscanf(argv[i + 1], "%f", &size) == 1) {} else usage = true; } - bool pixels = argc > 1 && !strcmp(argv[1], "pixels"); + bool pixels = argc > 1 && !strcmp(argv[1], "pixels"), on_display = argc > 1 && !strcmp(argv[1], "display"); if (usage || (dumped && strcmp(argv[1], "tui") && !pixels) || (!strcmp(argv[1], "check") && argc != 3) || - (pixels && (!dumped || !symbols_path || !frames_prefix))) { + (pixels && (!dumped || !symbols_path || !frames_prefix)) || (on_display && (!symbols_path || (writeback && !frames_prefix)))) { fputs("vega tui|cli|say MODULE.pmod [--store PATH]\n" "vega tui MODULE.pmod [--store PATH] [--watch] | --dump|--paint WIDTHxHEIGHT < script\n" "vega web MODULE.pmod [--store PATH] [--listen ADDRESS] [--port N] [--watch]\n" "vega pixels MODULE.pmod [--store PATH] --symbols FONT.vsym [--size PX] --grid WIDTHxHEIGHT --out PREFIX < script\n" + "vega display MODULE.pmod [--store PATH] --symbols FONT.vsym [--size PX] [--card PATH]\n" + " [--writeback WIDTHxHEIGHT --out PREFIX] < script\n" "vega check MODULE.pmod\n", stderr); return 2; } - if (pixels && !open_face(symbols_path, size)) return 2; + if ((pixels || on_display) && !open_face(symbols_path, size)) return 2; + if (on_display) { + 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); + width = pixels_across / (unsigned)face.cell_width; + height = pixels_down / (unsigned)face.cell_height; + } if (!strcmp(argv[1], "check")) { if (!load_module(argv[2], &modules[0])) return 2; return vega_check(&modules[0], print_finding, argv[2]) ? 1 : 0; } Surface surface = !strcmp(argv[1], "cli") ? SURFACE_CLI : !strcmp(argv[1], "say") ? SURFACE_SAY : !strcmp(argv[1], "tui") ? SURFACE_TUI : !strcmp(argv[1], "web") ? SURFACE_WEB : - pixels ? SURFACE_PIXELS : (Surface)-1; + pixels ? SURFACE_PIXELS : on_display ? SURFACE_DISPLAY : (Surface)-1; if ((int)surface < 0) { fprintf(stderr, "vega: unknown surface %s\n", argv[1]); return 2; } if (!load(argv[2], store)) return 2; if (watch && !dumped) { @@ -667,5 +735,6 @@ int main(int argc, char **argv) { /* Frame changes are stored with events; keep the last ones too. */ if (pure_app_save(&app) != PURE_OK) { fputs("vega: cannot save\n", stderr); status = status ? status : 1; } pure_app_stop(&app); + vega_display_close(display); return status; } diff --git a/surfaces/pixels.c b/surfaces/pixels.c index 67e7bc5..f671143 100644 --- a/surfaces/pixels.c +++ b/surfaces/pixels.c @@ -67,6 +67,10 @@ static void missing(uint32_t *out, size_t stride, int x, int y, int width, int h for (int j = top; j <= bottom; ++j) out[(size_t)j * stride + (size_t)left] = out[(size_t)j * stride + (size_t)right] = color; } +void vega_pixels_clear(uint32_t *out, size_t stride, unsigned width, unsigned height) { + fill(out, stride, 0, 0, (int)width, (int)height, background); +} + void vega_pixels(const VegaGrid *grid, VegaFace *face, uint32_t *out, size_t stride) { int width = (int)grid->width * face->cell_width, height = (int)grid->height * face->cell_height; /* Backgrounds first: a wide glyph spans its neighbour's cell. */ diff --git a/surfaces/vega.h b/surfaces/vega.h index b8e0044..69eba0b 100644 --- a/surfaces/vega.h +++ b/surfaces/vega.h @@ -135,4 +135,20 @@ void vega_face_close(VegaFace *); /* The grid into out: 0xRRGGBB pixels, grid->width * cell_width across and * grid->height * cell_height down, stride pixels apart. */ void vega_pixels(const VegaGrid *, VegaFace *, uint32_t *out, size_t stride); +/* width x height of out in the background's colour. */ +void vega_pixels_clear(uint32_t *out, size_t stride, unsigned width, unsigned height); + +/* A display through KMS: the first connected connector (or, with writeback, + * a writeback connector: the frames come back to memory, so no screen is + * needed to check them), its preferred mode or width x height. */ +typedef struct VegaDisplay VegaDisplay; +VegaDisplay *vega_display_open(const char *card, bool writeback, unsigned width, unsigned height, char *error, + size_t error_size); +/* The buffer to draw the next frame into: 0xRRGGBB, stride pixels apart. */ +uint32_t *vega_display_buffer(VegaDisplay *, unsigned *width, unsigned *height, size_t *stride); +/* Show it (and, on writeback, wait until the frame has come back). */ +bool vega_display_show(VegaDisplay *); +/* Writeback: the last frame as the display controller put it out. */ +const uint32_t *vega_display_captured(const VegaDisplay *, size_t *stride); +void vega_display_close(VegaDisplay *); #endif diff --git a/tests/check.py b/tests/check.py index e21cfc8..119328a 100644 --- a/tests/check.py +++ b/tests/check.py @@ -33,6 +33,9 @@ (a Latin monospace font, then Korean from a CJK one): the same text, blank cells only background (a focused row's own colour), every other cell inked, so no glyph spills into its neighbour. +12. Display: the same frames through KMS on a writeback connector (when the + device has one) come back from the display controller exactly as drawn, + and are the pixel surface's frames for the same grid. 10. Symbols: SVG icons (arcs, compact flags, holes, no viewBox) become outlines of the right area, an icon asking for what symbols cannot hold is refused by name, damaged files are refused whole, and (when DejaVu Sans is @@ -1074,6 +1077,39 @@ def pixels(vega, tool, module, work): return {'frames': len(frames), 'cells': checked} +def display(vega, module, work): + """The same frames on a display through KMS, on a writeback connector: each + comes back from the display controller exactly as drawn, and it is the + pixel surface's frame for the same grid. None without such a device.""" + card, face = Path('/dev/dri/card1'), work / 'ui.vsym' + if not card.exists() or not face.exists(): + return None + items = ['우유 사기', 'Pure 런타임 문서 읽기', 'on the display'] + script = ''.join(f'text {item.encode().hex()}\n' for item in items) + 'key Space\nshow\nkey ArrowUp\nshow\n' + shown = run(vega, 'display', module, '--symbols', face, '--size', 18, '--card', card, '--writeback', '640x480', + '--out', work / 'display', stdin=script) + if shown.returncode == 2 and ('writeback' in shown.stderr or 'cannot open' in shown.stderr): + return None # no writeback connector, or no access to the card + assert shown.returncode == 0, shown.stderr + frames = [line.split() for line in shown.stdout.splitlines() if line.startswith('frame ')] + assert len(frames) == 2 and all(frame[3] == '0' for frame in frames), frames + grid = frames[0][5] + drawn = run(vega, 'pixels', module, '--symbols', face, '--size', 18, '--grid', grid, '--out', work / 'grid', stdin=script) + assert drawn.returncode == 0, drawn.stderr + assert [line for line in drawn.stdout.splitlines() if not line.startswith('frame ')] == \ + [line for line in shown.stdout.splitlines() if not line.startswith('frame ')] + compared = 0 + for (_, captured, *_), (_, pixel_path, size) in zip(frames, [l.split() for l in drawn.stdout.splitlines() if l.startswith('frame ')]): + header, image = Path(captured).read_bytes().split(b'255\n', 1) + full_width = int(header.split()[1]) + head, grid_image = Path(pixel_path).read_bytes().split(b'255\n', 1) + width, height = map(int, head.split()[1:3]) + for y in range(height): + assert image[3 * y * full_width:3 * (y * full_width + width)] == grid_image[3 * y * width:3 * (y + 1) * width], y + compared += width * height + return {'frames': len(frames), 'pixels': compared} + + def width_of(character): return width(character) or 0 @@ -1117,6 +1153,7 @@ def main(): if args.symbols: report['symbols'] = symbols(args.symbols.resolve(), work) report['pixels'] = pixels(vega, args.symbols.resolve(), modules['todo'], work) + report['display'] = display(vega, modules['todo'], work) print(json.dumps(report)) |
