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 /surfaces/display.c | |
| 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.
Diffstat (limited to 'surfaces/display.c')
| -rw-r--r-- | surfaces/display.c | 264 |
1 files changed, 264 insertions, 0 deletions
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); +} |
