aboutsummaryrefslogtreecommitdiffstats
path: root/surfaces/display.c
blob: 9207963b0ea407714d74aa814e4772d83dc1094a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
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);
}