├── .builds ├── alpine.yml └── freebsd.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── alloc.c ├── device.c ├── doc └── compositor.md ├── example ├── common.c ├── common.h ├── compositor.c ├── dynamic.c ├── meson.build ├── multi-output.c └── simple.c ├── include ├── libliftoff.h ├── list.h ├── log.h └── private.h ├── layer.c ├── list.c ├── log.c ├── meson.build ├── output.c ├── plane.c └── test ├── bench.c ├── check_ndebug.c ├── libdrm_mock.c ├── libdrm_mock.h ├── meson.build ├── test_alloc.c ├── test_dynamic.c ├── test_priority.c └── test_prop.c /.builds/alpine.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | packages: 3 | - gcc 4 | - clang 5 | - meson 6 | - libdrm-dev 7 | - gcovr 8 | - py3-setuptools # https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/21003 9 | sources: 10 | - https://github.com/emersion/libliftoff 11 | artifacts: 12 | - coveragereport.tar.gz 13 | tasks: 14 | - setup: | 15 | cd libliftoff 16 | CC=gcc meson build-gcc/ -Db_coverage=true 17 | CC=clang meson build-clang/ 18 | - build-gcc: | 19 | cd libliftoff 20 | ninja -C build-gcc/ 21 | - build-clang: | 22 | cd libliftoff 23 | ninja -C build-clang/ 24 | - test-gcc: | 25 | cd libliftoff 26 | ninja -C build-gcc/ test 27 | - test-clang: | 28 | cd libliftoff 29 | ninja -C build-clang/ test 30 | - coverage: | 31 | cd libliftoff 32 | echo "exclude-directories = test/" >>gcovr.cfg 33 | echo "exclude-directories = example/" >>gcovr.cfg 34 | ninja -C build-gcc/ coverage-html 35 | cd build-gcc/meson-logs/ 36 | tar -czvf ~/coveragereport.tar.gz coveragereport 37 | -------------------------------------------------------------------------------- /.builds/freebsd.yml: -------------------------------------------------------------------------------- 1 | image: freebsd/latest 2 | packages: 3 | - devel/pkgconf 4 | - graphics/libdrm 5 | - devel/meson 6 | sources: 7 | - https://github.com/emersion/libliftoff 8 | tasks: 9 | - setup: | 10 | cd libliftoff 11 | meson build/ -Dauto_features=enabled 12 | - build: | 13 | cd libliftoff 14 | ninja -C build/ 15 | - test: | 16 | cd libliftoff 17 | ninja -C build/ test 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 8 10 | max_line_length = 80 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /build-* 3 | *.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Simon Ser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libliftoff 2 | 3 | [![builds.sr.ht status](https://builds.sr.ht/~emersion/libliftoff.svg)](https://builds.sr.ht/~emersion/libliftoff) 4 | 5 | Lightweight KMS plane library. 6 | 7 | > ### This project has moved 8 | > 9 | > This project has [moved to gitlab.freedesktop.org](https://gitlab.freedesktop.org/emersion/libliftoff). 10 | 11 | libliftoff eases the use of KMS planes from userspace without standing in your 12 | way. Users create "virtual planes" called layers, set KMS properties on them, 13 | and libliftoff will pick planes for these layers if possible. 14 | 15 | Resources: 16 | 17 | * [Blog post introducing the project][intro-post] 18 | * [FOSDEM 2020 talk][fosdem-2020] 19 | 20 | ## Building 21 | 22 | Depends on libdrm. Requires universal planes and atomic. 23 | 24 | meson build/ 25 | ninja -C build/ 26 | 27 | ## Usage 28 | 29 | See [`liftoff.h`][liftoff.h] and the [`example/`][example] directory. See 30 | [`doc/compositor.md`][doc/compositor] for compositor guidelines. 31 | 32 | Here's the general idea: 33 | 34 | ```c 35 | struct liftoff_device *device; 36 | struct liftoff_output *output; 37 | struct liftoff_layer *layer; 38 | drmModeAtomicReq *req; 39 | int ret; 40 | 41 | device = liftoff_device_create(drm_fd); 42 | output = liftoff_output_create(device, crtc_id); 43 | 44 | liftoff_device_register_all_planes(device); 45 | 46 | layer = liftoff_layer_create(output); 47 | liftoff_layer_set_property(layer, "FB_ID", fb_id); 48 | /* Probably setup more properties and more layers */ 49 | 50 | req = drmModeAtomicAlloc(); 51 | ret = liftoff_output_apply(output, req); 52 | if (ret < 0) { 53 | perror("liftoff_output_apply"); 54 | exit(1); 55 | } 56 | 57 | ret = drmModeAtomicCommit(drm_fd, req, DRM_MODE_ATOMIC_NONBLOCK, NULL); 58 | if (ret < 0) { 59 | perror("drmModeAtomicCommit"); 60 | exit(1); 61 | } 62 | drmModeAtomicFree(req); 63 | ``` 64 | 65 | ## Contributing 66 | 67 | Report bugs and send pull requests on [GitHub][github]. 68 | 69 | We use the Wayland/Weston style and contribution guidelines, see [Weston's 70 | contributing document][weston-contributing]. 71 | 72 | ## License 73 | 74 | MIT 75 | 76 | [liftoff.h]: https://github.com/emersion/libliftoff/blob/master/include/libliftoff.h 77 | [example]: https://github.com/emersion/libliftoff/tree/master/example 78 | [doc/compositor]: https://github.com/emersion/libliftoff/blob/master/doc/compositor.md 79 | [intro-post]: https://emersion.fr/blog/2019/xdc2019-wrap-up/#libliftoff 80 | [fosdem-2020]: https://fosdem.org/2020/schedule/event/kms_planes/ 81 | [github]: https://github.com/emersion/libliftoff 82 | [weston-contributing]: https://gitlab.freedesktop.org/wayland/weston/-/blob/master/CONTRIBUTING.md 83 | -------------------------------------------------------------------------------- /alloc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "log.h" 9 | #include "private.h" 10 | 11 | /* Plane allocation algorithm 12 | * 13 | * Goal: KMS exposes a set of hardware planes, user submitted a set of layers. 14 | * We want to map as many layers as possible to planes. 15 | * 16 | * However, all layers can't be mapped to any plane. There are constraints, 17 | * sometimes depending on driver-specific limitations or the configuration of 18 | * other planes. 19 | * 20 | * The only way to discover driver-specific limitations is via an atomic test 21 | * commit: we submit a plane configuration, and KMS replies whether it's 22 | * supported or not. Thus we need to incrementally build a valid configuration. 23 | * 24 | * Let's take an example with 2 planes and 3 layers. Plane 1 is only compatible 25 | * with layer 2 and plane 2 is only compatible with layer 3. Our algorithm will 26 | * discover the solution by building the mapping one plane at a time. It first 27 | * starts with plane 1: an atomic commit assigning layer 1 to plane 1 is 28 | * submitted. It fails, because this isn't supported by the driver. Then layer 29 | * 2 is assigned to plane 1 and the atomic test succeeds. We can go on and 30 | * repeat the operation with plane 2. After exploring the whole tree, we end up 31 | * with a valid allocation. 32 | * 33 | * 34 | * layer 1 layer 1 35 | * +---------> failure +---------> failure 36 | * | | 37 | * | | 38 | * | | 39 | * +---------+ | +---------+ | 40 | * | | | layer 2 | | | layer 3 final allocation: 41 | * | plane 1 +------------>+ plane 2 +--+---------> plane 1 → layer 2 42 | * | | | | | plane 2 → layer 3 43 | * +---------+ | +---------+ 44 | * | 45 | * | 46 | * | layer 3 47 | * +---------> failure 48 | * 49 | * 50 | * Note how layer 2 isn't considered for plane 2: it's already mapped to plane 51 | * 1. Also note that branches are pruned as soon as an atomic test fails. 52 | * 53 | * In practice, the primary plane is treated separately. This is where layers 54 | * that can't be mapped to any plane (e.g. layer 1 in our example) will be 55 | * composited. The primary plane is the first that will be allocated, because 56 | * some drivers require it to be enabled in order to light up any other plane. 57 | * Then all other planes will be allocated, from the topmost one to the 58 | * bottommost one. 59 | * 60 | * The "zpos" property (which defines ordering between layers/planes) is handled 61 | * as a special case. If it's set on layers, it adds additional constraints on 62 | * their relative ordering. If two layers intersect, their relative zpos needs 63 | * to be preserved during plane allocation. 64 | * 65 | * Implementation-wise, the output_choose_layers function is called at each node 66 | * of the tree. It iterates over layers, check constraints, performs an atomic 67 | * test commit and calls itself recursively on the next plane. 68 | */ 69 | 70 | /* Global data for the allocation algorithm */ 71 | struct alloc_result { 72 | drmModeAtomicReq *req; 73 | uint32_t flags; 74 | size_t planes_len; 75 | 76 | struct liftoff_layer **best; 77 | int best_score; 78 | 79 | /* per-output */ 80 | bool has_composition_layer; 81 | size_t non_composition_layers_len; 82 | }; 83 | 84 | /* Transient data, arguments for each step */ 85 | struct alloc_step { 86 | struct liftoff_list *plane_link; /* liftoff_plane.link */ 87 | size_t plane_idx; 88 | 89 | struct liftoff_layer **alloc; /* only items up to plane_idx are valid */ 90 | int score; /* number of allocated layers */ 91 | int last_layer_zpos; 92 | 93 | bool composited; /* per-output */ 94 | 95 | char log_prefix[64]; 96 | }; 97 | 98 | static void 99 | plane_step_init_next(struct alloc_step *step, struct alloc_step *prev, 100 | struct liftoff_layer *layer) 101 | { 102 | struct liftoff_plane *plane; 103 | struct liftoff_layer_property *zpos_prop; 104 | size_t len; 105 | 106 | plane = liftoff_container_of(prev->plane_link, plane, link); 107 | 108 | step->plane_link = prev->plane_link->next; 109 | step->plane_idx = prev->plane_idx + 1; 110 | step->alloc = prev->alloc; 111 | step->alloc[prev->plane_idx] = layer; 112 | 113 | if (layer != NULL && layer == layer->output->composition_layer) { 114 | assert(!prev->composited); 115 | step->composited = true; 116 | } else { 117 | step->composited = prev->composited; 118 | } 119 | 120 | if (layer != NULL && layer != layer->output->composition_layer) { 121 | step->score = prev->score + 1; 122 | } else { 123 | step->score = prev->score; 124 | } 125 | 126 | zpos_prop = NULL; 127 | if (layer != NULL) { 128 | zpos_prop = layer_get_property(layer, "zpos"); 129 | } 130 | if (zpos_prop != NULL && plane->type != DRM_PLANE_TYPE_PRIMARY) { 131 | step->last_layer_zpos = zpos_prop->value; 132 | } else { 133 | step->last_layer_zpos = prev->last_layer_zpos; 134 | } 135 | 136 | if (layer != NULL) { 137 | len = strlen(prev->log_prefix) + 2; 138 | if (len > sizeof(step->log_prefix) - 1) { 139 | len = sizeof(step->log_prefix) - 1; 140 | } 141 | memset(step->log_prefix, ' ', len); 142 | step->log_prefix[len] = '\0'; 143 | } else { 144 | memcpy(step->log_prefix, prev->log_prefix, 145 | sizeof(step->log_prefix)); 146 | } 147 | } 148 | 149 | static bool 150 | is_layer_allocated(struct alloc_step *step, struct liftoff_layer *layer) 151 | { 152 | size_t i; 153 | 154 | /* TODO: speed this up with an array of bools indicating whether a layer 155 | * has been allocated */ 156 | for (i = 0; i < step->plane_idx; i++) { 157 | if (step->alloc[i] == layer) { 158 | return true; 159 | } 160 | } 161 | return false; 162 | } 163 | 164 | static bool 165 | has_composited_layer_over(struct liftoff_output *output, 166 | struct alloc_step *step, struct liftoff_layer *layer) 167 | { 168 | struct liftoff_layer *other_layer; 169 | struct liftoff_layer_property *zpos_prop, *other_zpos_prop; 170 | 171 | zpos_prop = layer_get_property(layer, "zpos"); 172 | if (zpos_prop == NULL) { 173 | return false; 174 | } 175 | 176 | liftoff_list_for_each(other_layer, &output->layers, link) { 177 | if (is_layer_allocated(step, other_layer)) { 178 | continue; 179 | } 180 | 181 | other_zpos_prop = layer_get_property(other_layer, "zpos"); 182 | if (other_zpos_prop == NULL) { 183 | continue; 184 | } 185 | 186 | if (layer_intersects(layer, other_layer) && 187 | other_zpos_prop->value > zpos_prop->value) { 188 | return true; 189 | } 190 | } 191 | 192 | return false; 193 | } 194 | 195 | static bool 196 | has_allocated_layer_over(struct liftoff_output *output, struct alloc_step *step, 197 | struct liftoff_layer *layer) 198 | { 199 | ssize_t i; 200 | struct liftoff_plane *other_plane; 201 | struct liftoff_layer *other_layer; 202 | struct liftoff_layer_property *zpos_prop, *other_zpos_prop; 203 | 204 | zpos_prop = layer_get_property(layer, "zpos"); 205 | if (zpos_prop == NULL) { 206 | return false; 207 | } 208 | 209 | i = -1; 210 | liftoff_list_for_each(other_plane, &output->device->planes, link) { 211 | i++; 212 | if (i >= (ssize_t)step->plane_idx) { 213 | break; 214 | } 215 | if (other_plane->type == DRM_PLANE_TYPE_PRIMARY) { 216 | continue; 217 | } 218 | 219 | other_layer = step->alloc[i]; 220 | if (other_layer == NULL) { 221 | continue; 222 | } 223 | 224 | other_zpos_prop = layer_get_property(other_layer, "zpos"); 225 | if (other_zpos_prop == NULL) { 226 | continue; 227 | } 228 | 229 | /* Since plane zpos is descending, this means the other layer is 230 | * supposed to be under but is mapped to a plane over the 231 | * current one. */ 232 | if (zpos_prop->value > other_zpos_prop->value && 233 | layer_intersects(layer, other_layer)) { 234 | return true; 235 | } 236 | } 237 | 238 | return false; 239 | } 240 | 241 | static bool 242 | has_allocated_plane_under(struct liftoff_output *output, 243 | struct alloc_step *step, struct liftoff_layer *layer) 244 | { 245 | struct liftoff_plane *plane, *other_plane; 246 | ssize_t i; 247 | 248 | plane = liftoff_container_of(step->plane_link, plane, link); 249 | 250 | i = -1; 251 | liftoff_list_for_each(other_plane, &output->device->planes, link) { 252 | i++; 253 | if (i >= (ssize_t)step->plane_idx) { 254 | break; 255 | } 256 | if (other_plane->type == DRM_PLANE_TYPE_PRIMARY) { 257 | continue; 258 | } 259 | if (step->alloc[i] == NULL) { 260 | continue; 261 | } 262 | 263 | if (plane->zpos >= other_plane->zpos && 264 | layer_intersects(layer, step->alloc[i])) { 265 | return true; 266 | } 267 | } 268 | 269 | return false; 270 | } 271 | 272 | static bool 273 | check_layer_plane_compatible(struct alloc_step *step, 274 | struct liftoff_layer *layer, 275 | struct liftoff_plane *plane) 276 | { 277 | struct liftoff_output *output; 278 | struct liftoff_layer_property *zpos_prop; 279 | 280 | output = layer->output; 281 | 282 | /* Skip this layer if already allocated */ 283 | if (is_layer_allocated(step, layer)) { 284 | return false; 285 | } 286 | 287 | zpos_prop = layer_get_property(layer, "zpos"); 288 | if (zpos_prop != NULL) { 289 | if ((int)zpos_prop->value > step->last_layer_zpos && 290 | has_allocated_layer_over(output, step, layer)) { 291 | /* This layer needs to be on top of the last 292 | * allocated one */ 293 | liftoff_log(LIFTOFF_DEBUG, 294 | "%s Layer %p -> plane %"PRIu32": " 295 | "layer zpos invalid", 296 | step->log_prefix, (void *)layer, plane->id); 297 | return false; 298 | } 299 | if ((int)zpos_prop->value < step->last_layer_zpos && 300 | has_allocated_plane_under(output, step, layer)) { 301 | /* This layer needs to be under the last 302 | * allocated one, but this plane isn't under the 303 | * last one (in practice, since planes are 304 | * sorted by zpos it means it has the same zpos, 305 | * ie. undefined ordering). */ 306 | liftoff_log(LIFTOFF_DEBUG, 307 | "%s Layer %p -> plane %"PRIu32": " 308 | "plane zpos invalid", 309 | step->log_prefix, (void *)layer, plane->id); 310 | return false; 311 | } 312 | } 313 | 314 | if (plane->type != DRM_PLANE_TYPE_PRIMARY && 315 | has_composited_layer_over(output, step, layer)) { 316 | liftoff_log(LIFTOFF_DEBUG, 317 | "%s Layer %p -> plane %"PRIu32": " 318 | "has composited layer on top", 319 | step->log_prefix, (void *)layer, plane->id); 320 | return false; 321 | } 322 | 323 | if (plane->type != DRM_PLANE_TYPE_PRIMARY && 324 | layer == layer->output->composition_layer) { 325 | liftoff_log(LIFTOFF_DEBUG, 326 | "%s Layer %p -> plane %"PRIu32": " 327 | "cannot put composition layer on " 328 | "non-primary plane", 329 | step->log_prefix, (void *)layer, plane->id); 330 | return false; 331 | } 332 | 333 | return true; 334 | } 335 | 336 | static bool 337 | check_alloc_valid(struct alloc_result *result, struct alloc_step *step) 338 | { 339 | /* If composition isn't used, we need to have allocated all 340 | * layers. */ 341 | /* TODO: find a way to fail earlier, e.g. when the number of 342 | * layers exceeds the number of planes. */ 343 | if (result->has_composition_layer && !step->composited && 344 | step->score != (int)result->non_composition_layers_len) { 345 | liftoff_log(LIFTOFF_DEBUG, 346 | "%sCannot skip composition: some layers " 347 | "are missing a plane", step->log_prefix); 348 | return false; 349 | } 350 | /* On the other hand, if we manage to allocate all layers, we 351 | * don't want to use composition. We don't want to use the 352 | * composition layer at all. */ 353 | if (step->composited && 354 | step->score == (int)result->non_composition_layers_len) { 355 | liftoff_log(LIFTOFF_DEBUG, 356 | "%sRefusing to use composition: all layers " 357 | "have been put in a plane", step->log_prefix); 358 | return false; 359 | } 360 | 361 | /* TODO: check allocation isn't empty */ 362 | 363 | return true; 364 | } 365 | 366 | static int 367 | output_choose_layers(struct liftoff_output *output, struct alloc_result *result, 368 | struct alloc_step *step) 369 | { 370 | struct liftoff_device *device; 371 | struct liftoff_plane *plane; 372 | struct liftoff_layer *layer; 373 | int cursor, ret; 374 | size_t remaining_planes; 375 | struct alloc_step next_step = {0}; 376 | 377 | device = output->device; 378 | 379 | if (step->plane_link == &device->planes) { /* Allocation finished */ 380 | if (step->score > result->best_score && 381 | check_alloc_valid(result, step)) { 382 | /* We found a better allocation */ 383 | liftoff_log(LIFTOFF_DEBUG, 384 | "%sFound a better allocation with score=%d", 385 | step->log_prefix, step->score); 386 | result->best_score = step->score; 387 | memcpy(result->best, step->alloc, 388 | result->planes_len * sizeof(struct liftoff_layer *)); 389 | } 390 | return 0; 391 | } 392 | 393 | plane = liftoff_container_of(step->plane_link, plane, link); 394 | 395 | remaining_planes = result->planes_len - step->plane_idx; 396 | if (result->best_score >= step->score + (int)remaining_planes) { 397 | /* Even if we find a layer for all remaining planes, we won't 398 | * find a better allocation. Give up. */ 399 | /* TODO: change remaining_planes to only count those whose 400 | * possible CRTC match and which aren't allocated */ 401 | return 0; 402 | } 403 | 404 | cursor = drmModeAtomicGetCursor(result->req); 405 | 406 | if (plane->layer != NULL) { 407 | goto skip; 408 | } 409 | if ((plane->possible_crtcs & (1 << output->crtc_index)) == 0) { 410 | goto skip; 411 | } 412 | 413 | liftoff_log(LIFTOFF_DEBUG, 414 | "%sPerforming allocation for plane %"PRIu32" (%zu/%zu)", 415 | step->log_prefix, plane->id, step->plane_idx + 1, result->planes_len); 416 | 417 | liftoff_list_for_each(layer, &output->layers, link) { 418 | if (layer->plane != NULL || layer->force_composition) { 419 | continue; 420 | } 421 | if (!layer_is_visible(layer)) { 422 | continue; 423 | } 424 | if (!check_layer_plane_compatible(step, layer, plane)) { 425 | continue; 426 | } 427 | 428 | /* Try to use this layer for the current plane */ 429 | ret = plane_apply(plane, layer, result->req); 430 | if (ret == -EINVAL) { 431 | liftoff_log(LIFTOFF_DEBUG, 432 | "%s Layer %p -> plane %"PRIu32": " 433 | "incompatible properties", 434 | step->log_prefix, (void *)layer, plane->id); 435 | continue; 436 | } else if (ret != 0) { 437 | return ret; 438 | } 439 | 440 | ret = device_test_commit(device, result->req, result->flags); 441 | if (ret == 0) { 442 | liftoff_log(LIFTOFF_DEBUG, 443 | "%s Layer %p -> plane %"PRIu32": success", 444 | step->log_prefix, (void *)layer, plane->id); 445 | /* Continue with the next plane */ 446 | plane_step_init_next(&next_step, step, layer); 447 | ret = output_choose_layers(output, result, &next_step); 448 | if (ret != 0) { 449 | return ret; 450 | } 451 | } else if (ret != -EINVAL && ret != -ERANGE && ret != -ENOSPC) { 452 | return ret; 453 | } else { 454 | liftoff_log(LIFTOFF_DEBUG, 455 | "%s Layer %p -> plane %"PRIu32": " 456 | "test-only commit failed (%s)", 457 | step->log_prefix, (void *)layer, plane->id, 458 | strerror(-ret)); 459 | } 460 | 461 | drmModeAtomicSetCursor(result->req, cursor); 462 | } 463 | 464 | skip: 465 | /* Try not to use the current plane */ 466 | plane_step_init_next(&next_step, step, NULL); 467 | ret = output_choose_layers(output, result, &next_step); 468 | if (ret != 0) { 469 | return ret; 470 | } 471 | drmModeAtomicSetCursor(result->req, cursor); 472 | 473 | return 0; 474 | } 475 | 476 | static int 477 | apply_current(struct liftoff_device *device, drmModeAtomicReq *req) 478 | { 479 | struct liftoff_plane *plane; 480 | int cursor, ret; 481 | 482 | cursor = drmModeAtomicGetCursor(req); 483 | 484 | liftoff_list_for_each(plane, &device->planes, link) { 485 | ret = plane_apply(plane, plane->layer, req); 486 | assert(ret != -EINVAL); 487 | if (ret != 0) { 488 | drmModeAtomicSetCursor(req, cursor); 489 | return ret; 490 | } 491 | } 492 | 493 | return 0; 494 | } 495 | 496 | static bool 497 | layer_needs_realloc(struct liftoff_layer *layer) 498 | { 499 | size_t i; 500 | struct liftoff_layer_property *prop; 501 | 502 | if (layer->changed) { 503 | return true; 504 | } 505 | 506 | for (i = 0; i < layer->props_len; i++) { 507 | prop = &layer->props[i]; 508 | if (prop->value == prop->prev_value) { 509 | continue; 510 | } 511 | 512 | /* If FB_ID changes from non-zero to zero, we don't need to 513 | * display this layer anymore, so we may be able to re-use its 514 | * plane for another layer. If FB_ID changes from zero to 515 | * non-zero, we might be able to find a plane for this layer. 516 | * If FB_ID changes from non-zero to non-zero, we can try to 517 | * re-use the previous allocation. */ 518 | if (strcmp(prop->name, "FB_ID") == 0) { 519 | if (prop->value == 0 || prop->prev_value == 0) { 520 | return true; 521 | } 522 | /* TODO: check format/modifier is the same? */ 523 | continue; 524 | } 525 | 526 | /* If the layer was or becomes completely transparent or 527 | * completely opaque, we might be able to find a better 528 | * allocation. Otherwise, we can keep the current one. */ 529 | if (strcmp(prop->name, "alpha") == 0) { 530 | if (prop->value == 0 || prop->prev_value == 0 || 531 | prop->value == 0xFFFF || prop->prev_value == 0xFFFF) { 532 | return true; 533 | } 534 | continue; 535 | } 536 | 537 | /* We should never need a re-alloc when IN_FENCE_FD or 538 | * FB_DAMAGE_CLIPS changes. */ 539 | if (strcmp(prop->name, "IN_FENCE_FD") == 0 || 540 | strcmp(prop->name, "FB_DAMAGE_CLIPS") == 0) { 541 | continue; 542 | } 543 | 544 | /* TODO: if CRTC_{X,Y,W,H} changed but intersection with other 545 | * layers hasn't changed, don't realloc */ 546 | return true; 547 | } 548 | 549 | return false; 550 | } 551 | 552 | static int 553 | reuse_previous_alloc(struct liftoff_output *output, drmModeAtomicReq *req, 554 | uint32_t flags) 555 | { 556 | struct liftoff_device *device; 557 | struct liftoff_layer *layer; 558 | int cursor, ret; 559 | 560 | device = output->device; 561 | 562 | if (output->layers_changed) { 563 | return -EINVAL; 564 | } 565 | 566 | liftoff_list_for_each(layer, &output->layers, link) { 567 | if (layer_needs_realloc(layer)) { 568 | return -EINVAL; 569 | } 570 | } 571 | 572 | cursor = drmModeAtomicGetCursor(req); 573 | 574 | ret = apply_current(device, req); 575 | if (ret != 0) { 576 | return ret; 577 | } 578 | 579 | ret = device_test_commit(device, req, flags); 580 | if (ret != 0) { 581 | drmModeAtomicSetCursor(req, cursor); 582 | } 583 | return ret; 584 | } 585 | 586 | static void 587 | mark_layers_clean(struct liftoff_output *output) 588 | { 589 | struct liftoff_layer *layer; 590 | 591 | output->layers_changed = false; 592 | 593 | liftoff_list_for_each(layer, &output->layers, link) { 594 | layer_mark_clean(layer); 595 | } 596 | } 597 | 598 | static void 599 | update_layers_priority(struct liftoff_device *device) 600 | { 601 | struct liftoff_output *output; 602 | struct liftoff_layer *layer; 603 | 604 | device->page_flip_counter++; 605 | bool period_elapsed = 606 | device->page_flip_counter >= LIFTOFF_PRIORITY_PERIOD; 607 | if (period_elapsed) { 608 | device->page_flip_counter = 0; 609 | } 610 | 611 | liftoff_list_for_each(output, &device->outputs, link) { 612 | liftoff_list_for_each(layer, &output->layers, link) { 613 | layer_update_priority(layer, period_elapsed); 614 | } 615 | } 616 | } 617 | 618 | static void 619 | log_reuse(struct liftoff_output *output) 620 | { 621 | if (output->alloc_reused_counter == 0) { 622 | liftoff_log(LIFTOFF_DEBUG, 623 | "Reusing previous plane allocation on output %p", 624 | (void *)output); 625 | } 626 | output->alloc_reused_counter++; 627 | } 628 | 629 | static void 630 | log_no_reuse(struct liftoff_output *output) 631 | { 632 | liftoff_log(LIFTOFF_DEBUG, "Computing plane allocation on output %p", 633 | (void *)output); 634 | 635 | if (output->alloc_reused_counter != 0) { 636 | liftoff_log(LIFTOFF_DEBUG, 637 | "Stopped reusing previous plane allocation on " 638 | "output %p (had reused it %d times)", 639 | (void *)output, output->alloc_reused_counter); 640 | output->alloc_reused_counter = 0; 641 | } 642 | } 643 | 644 | static size_t 645 | non_composition_layers_length(struct liftoff_output *output) 646 | { 647 | struct liftoff_layer *layer; 648 | size_t n; 649 | 650 | n = 0; 651 | liftoff_list_for_each(layer, &output->layers, link) { 652 | if (layer_is_visible(layer) && 653 | output->composition_layer != layer) { 654 | n++; 655 | } 656 | } 657 | 658 | return n; 659 | } 660 | 661 | int 662 | liftoff_output_apply(struct liftoff_output *output, drmModeAtomicReq *req, 663 | uint32_t flags) 664 | { 665 | struct liftoff_device *device; 666 | struct liftoff_plane *plane; 667 | struct liftoff_layer *layer; 668 | struct alloc_result result = {0}; 669 | struct alloc_step step = {0}; 670 | size_t i, candidate_planes; 671 | int ret; 672 | 673 | device = output->device; 674 | 675 | update_layers_priority(device); 676 | 677 | ret = reuse_previous_alloc(output, req, flags); 678 | if (ret == 0) { 679 | log_reuse(output); 680 | return 0; 681 | } 682 | log_no_reuse(output); 683 | 684 | device->test_commit_counter = 0; 685 | output_log_layers(output); 686 | 687 | /* Unset all existing plane and layer mappings. */ 688 | liftoff_list_for_each(plane, &device->planes, link) { 689 | if (plane->layer != NULL && plane->layer->output == output) { 690 | plane->layer->plane = NULL; 691 | plane->layer = NULL; 692 | } 693 | } 694 | 695 | /* Disable all planes we might use. Do it before building mappings to 696 | * make sure not to hit bandwidth limits because too many planes are 697 | * enabled. */ 698 | candidate_planes = 0; 699 | liftoff_list_for_each(plane, &device->planes, link) { 700 | if (plane->layer == NULL) { 701 | candidate_planes++; 702 | liftoff_log(LIFTOFF_DEBUG, 703 | "Disabling plane %"PRIu32, plane->id); 704 | ret = plane_apply(plane, NULL, req); 705 | assert(ret != -EINVAL); 706 | if (ret != 0) { 707 | return ret; 708 | } 709 | } 710 | } 711 | 712 | result.req = req; 713 | result.flags = flags; 714 | result.planes_len = liftoff_list_length(&device->planes); 715 | 716 | step.alloc = malloc(result.planes_len * sizeof(*step.alloc)); 717 | result.best = malloc(result.planes_len * sizeof(*result.best)); 718 | if (step.alloc == NULL || result.best == NULL) { 719 | liftoff_log_errno(LIFTOFF_ERROR, "malloc"); 720 | return -ENOMEM; 721 | } 722 | 723 | /* For each plane, try to find a layer. Don't do it the other 724 | * way around (ie. for each layer, try to find a plane) because 725 | * some drivers want user-space to enable the primary plane 726 | * before any other plane. */ 727 | 728 | result.best_score = -1; 729 | memset(result.best, 0, result.planes_len * sizeof(*result.best)); 730 | result.has_composition_layer = output->composition_layer != NULL; 731 | result.non_composition_layers_len = 732 | non_composition_layers_length(output); 733 | step.plane_link = device->planes.next; 734 | step.plane_idx = 0; 735 | step.score = 0; 736 | step.last_layer_zpos = INT_MAX; 737 | step.composited = false; 738 | ret = output_choose_layers(output, &result, &step); 739 | if (ret != 0) { 740 | return ret; 741 | } 742 | 743 | liftoff_log(LIFTOFF_DEBUG, 744 | "Found plane allocation for output %p (score: %d, candidate planes: %zu, tests: %d):", 745 | (void *)output, result.best_score, candidate_planes, 746 | device->test_commit_counter); 747 | 748 | /* Apply the best allocation */ 749 | i = 0; 750 | liftoff_list_for_each(plane, &device->planes, link) { 751 | layer = result.best[i]; 752 | i++; 753 | if (layer == NULL) { 754 | continue; 755 | } 756 | 757 | liftoff_log(LIFTOFF_DEBUG, " Layer %p -> plane %"PRIu32, 758 | (void *)layer, plane->id); 759 | 760 | assert(plane->layer == NULL); 761 | assert(layer->plane == NULL); 762 | plane->layer = layer; 763 | layer->plane = plane; 764 | } 765 | if (i == 0) { 766 | liftoff_log(LIFTOFF_DEBUG, " (No layer has a plane)"); 767 | } 768 | 769 | ret = apply_current(device, req); 770 | if (ret != 0) { 771 | return ret; 772 | } 773 | 774 | free(step.alloc); 775 | free(result.best); 776 | 777 | mark_layers_clean(output); 778 | 779 | return 0; 780 | } 781 | -------------------------------------------------------------------------------- /device.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "log.h" 6 | #include "private.h" 7 | 8 | struct liftoff_device * 9 | liftoff_device_create(int drm_fd) 10 | { 11 | struct liftoff_device *device; 12 | drmModeRes *drm_res; 13 | 14 | device = calloc(1, sizeof(*device)); 15 | if (device == NULL) { 16 | liftoff_log_errno(LIFTOFF_ERROR, "calloc"); 17 | return NULL; 18 | } 19 | 20 | liftoff_list_init(&device->planes); 21 | liftoff_list_init(&device->outputs); 22 | 23 | device->drm_fd = dup(drm_fd); 24 | if (device->drm_fd < 0) { 25 | liftoff_log_errno(LIFTOFF_ERROR, "dup"); 26 | liftoff_device_destroy(device); 27 | return NULL; 28 | } 29 | 30 | drm_res = drmModeGetResources(drm_fd); 31 | if (drm_res == NULL) { 32 | liftoff_log_errno(LIFTOFF_ERROR, "drmModeGetResources"); 33 | liftoff_device_destroy(device); 34 | return NULL; 35 | } 36 | 37 | device->crtcs = malloc(drm_res->count_crtcs * sizeof(uint32_t)); 38 | if (device->crtcs == NULL) { 39 | liftoff_log_errno(LIFTOFF_ERROR, "malloc"); 40 | drmModeFreeResources(drm_res); 41 | liftoff_device_destroy(device); 42 | return NULL; 43 | } 44 | device->crtcs_len = drm_res->count_crtcs; 45 | memcpy(device->crtcs, drm_res->crtcs, 46 | drm_res->count_crtcs * sizeof(uint32_t)); 47 | 48 | drmModeFreeResources(drm_res); 49 | 50 | return device; 51 | } 52 | 53 | void 54 | liftoff_device_destroy(struct liftoff_device *device) 55 | { 56 | struct liftoff_plane *plane, *tmp; 57 | 58 | if (device == NULL) { 59 | return; 60 | } 61 | 62 | close(device->drm_fd); 63 | liftoff_list_for_each_safe(plane, tmp, &device->planes, link) { 64 | liftoff_plane_destroy(plane); 65 | } 66 | free(device->crtcs); 67 | free(device); 68 | } 69 | 70 | int 71 | liftoff_device_register_all_planes(struct liftoff_device *device) 72 | { 73 | drmModePlaneRes *drm_plane_res; 74 | uint32_t i; 75 | 76 | drm_plane_res = drmModeGetPlaneResources(device->drm_fd); 77 | if (drm_plane_res == NULL) { 78 | liftoff_log_errno(LIFTOFF_ERROR, "drmModeGetPlaneResources"); 79 | return -errno; 80 | } 81 | 82 | for (i = 0; i < drm_plane_res->count_planes; i++) { 83 | if (liftoff_plane_create(device, drm_plane_res->planes[i]) == NULL) { 84 | return -errno; 85 | } 86 | } 87 | drmModeFreePlaneResources(drm_plane_res); 88 | 89 | return 0; 90 | } 91 | 92 | int 93 | device_test_commit(struct liftoff_device *device, drmModeAtomicReq *req, 94 | uint32_t flags) 95 | { 96 | int ret; 97 | 98 | device->test_commit_counter++; 99 | 100 | flags &= ~DRM_MODE_PAGE_FLIP_EVENT; 101 | do { 102 | ret = drmModeAtomicCommit(device->drm_fd, req, 103 | DRM_MODE_ATOMIC_TEST_ONLY | flags, 104 | NULL); 105 | } while (ret == -EINTR || ret == -EAGAIN); 106 | 107 | /* The kernel will return -EINVAL for invalid configuration, -ERANGE for 108 | * CRTC coords overflow, and -ENOSPC for invalid SRC coords. */ 109 | if (ret != 0 && ret != -EINVAL && ret != -ERANGE && ret != -ENOSPC) { 110 | liftoff_log(LIFTOFF_ERROR, "drmModeAtomicCommit: %s", 111 | strerror(-ret)); 112 | } 113 | 114 | return ret; 115 | } 116 | -------------------------------------------------------------------------------- /doc/compositor.md: -------------------------------------------------------------------------------- 1 | # Using libliftoff for a compositor 2 | 3 | This document explains how to use libliftoff for a full-blown compositor. 4 | 5 | A compositor receives buffers coming from clients and displays them on screen. 6 | Client buffers are attached to surfaces. For instance a Wayland compositor 7 | receives shared-memory buffers via `wl_shm` and DMA-BUFs via `wp_linux_dmabuf`. 8 | Shared memory buffers are stored in main memory, DMA-BUFs are stored directly 9 | on the GPU. 10 | 11 | Compositors which can't take advantage of hardware planes will always perform 12 | composition for all client buffers. In other words, such compositors will copy 13 | client buffers to their own buffer, then display their own buffer on screen. 14 | Typically the copying is done via OpenGL. 15 | 16 | Compositors which can take advantage of hardware planes will sometimes avoid 17 | composition (because the hardware can directly scan-out client buffers), but 18 | sometimes they'll need to fallback to composition (because of hardware 19 | limitations). libliftoff can help with this decision-making. 20 | 21 | ## Setting up layers 22 | 23 | A compositor will want to create one layer per surface. That way, in the 24 | best-case scenario, all client buffers will make it into a hardware plane and 25 | composition won't be necessary. The layers effectively describe the compositor's 26 | scene-graph. 27 | 28 | The compositor will need to set standard plane properties on the buffer, in 29 | particular the `CRTC_*`, `SRC_*` and `zpos` properties. These properties define 30 | the position and size of the layer. 31 | 32 | ### DMA-BUFs 33 | 34 | Each time a client submits a new DMA-BUF for a surface, the compositor should 35 | import the buffer and update the layer's `FB_ID`. libliftoff will try to put 36 | this layer in a plane. 37 | 38 | ### Buffers that can't be directly scanned out 39 | 40 | Buffers using shared memory can't be directly scanned out by the hardware. 41 | 42 | These will need to be composited, the compositor should call 43 | `liftoff_layer_set_fb_composited`. libliftoff will never try to put this layer 44 | in a plane. 45 | 46 | ### Elements drawn by the compositor 47 | 48 | Sometimes compositors want to draw UI elements themselves, for instance window 49 | decorations. The compositor could allocate buffers for these UI elements, but 50 | sometimes it may want not to. libliftoff needs to know about this to avoid 51 | putting a layer which is underneath in a hardware plane. 52 | 53 | For each of those elements, the compositor can create a layer, set the `CRTC_*` 54 | and `zpos` properties to indicate the geometry of the element, and call 55 | `liftoff_layer_set_fb_composited` to indicate that this region needs to be 56 | composited. 57 | 58 | ## Composition layer 59 | 60 | A compositor must always be prepared to fallback to composition. There are a lot 61 | of potential reasons why composition is needed, for instance: 62 | 63 | - The client uses a buffer which can't be directly scanned out, or the 64 | compositor wants to draw a UI element itself 65 | - The format or modifier used by the client isn't compatible with any of the 66 | hardware planes 67 | - The client buffer isn't compatible with any of the planes, e.g. because of a 68 | format or modifier mismatch 69 | - Putting a client buffer into a hardware plane would exceed the hardware 70 | bandwidth limits 71 | 72 | The compositor needs to prepare a special buffer where client buffers will be 73 | copied to if composition is necessary. Because it's not possible to know in 74 | advance whether composition will be necessary, the compositor needs to setup 75 | this "composition buffer" before libliftoff does its job. 76 | 77 | The composition buffer should be allocated with a format and modifier compatible 78 | with the primary plane. 79 | 80 | The compositor should create a special layer, the "composition layer". The layer 81 | should cover the whole CRTC. `liftoff_output_set_composition_layer` should be 82 | used to tell libliftoff that composition will happen on this special layer. 83 | 84 | If all regular layers can be put into a plane, the composition layer won't be 85 | used. Otherwise, the compositor needs to perform the composition prior to 86 | performing a page-flip. Each layer that didn't make it into a hardware plane 87 | (ie. `liftoff_layer_needs_composition` returns true) needs to be composited. 88 | 89 | ## Disabling layers 90 | 91 | When the compositor needs to hide a surface, it's not very handy to destroy the 92 | layer (and re-create it when the surface is visible again). Instead, the 93 | compositor can disable a layer by setting `FB_ID` to zero. 94 | 95 | The layer will be ignored by libliftoff. 96 | -------------------------------------------------------------------------------- /example/common.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "common.h" 8 | 9 | drmModeConnector * 10 | pick_connector(int drm_fd, drmModeRes *drm_res) 11 | { 12 | int i; 13 | drmModeConnector *connector; 14 | 15 | for (i = 0; i < drm_res->count_connectors; i++) { 16 | connector = drmModeGetConnector(drm_fd, drm_res->connectors[i]); 17 | if (connector->connection == DRM_MODE_CONNECTED) { 18 | return connector; 19 | } 20 | drmModeFreeConnector(connector); 21 | } 22 | 23 | return NULL; 24 | } 25 | 26 | drmModeCrtc * 27 | pick_crtc(int drm_fd, drmModeRes *drm_res, drmModeConnector *connector) 28 | { 29 | drmModeEncoder *enc; 30 | uint32_t crtc_id; 31 | int i; 32 | int j; 33 | bool found; 34 | 35 | enc = drmModeGetEncoder(drm_fd, connector->encoder_id); 36 | 37 | if (enc) { 38 | /* Current CRTC happens to be usable on the selected connector */ 39 | crtc_id = enc->crtc_id; 40 | drmModeFreeEncoder(enc); 41 | return drmModeGetCrtc(drm_fd, crtc_id); 42 | } else { 43 | /* Current CRTC used by this encoder can't drive the selected connector. 44 | * Search all of them for a valid combination. */ 45 | for (i = 0, found = false; !found && i < connector->count_encoders; i++) { 46 | enc = drmModeGetEncoder(drm_fd, connector->encoders[i]); 47 | 48 | if (!enc) { 49 | continue; 50 | } 51 | 52 | for (j = 0; !found && j < drm_res->count_crtcs; j++) { 53 | /* Can the CRTC drive the connector? */ 54 | if (enc->possible_crtcs & (1 << j)) { 55 | crtc_id = drm_res->crtcs[j]; 56 | found = true; 57 | } 58 | } 59 | drmModeFreeEncoder(enc); 60 | } 61 | 62 | if (found) { 63 | return drmModeGetCrtc(drm_fd, crtc_id); 64 | } else { 65 | return NULL; 66 | } 67 | } 68 | } 69 | 70 | void 71 | disable_all_crtcs_except(int drm_fd, drmModeRes *drm_res, uint32_t crtc_id) 72 | { 73 | int i; 74 | 75 | for (i = 0; i < drm_res->count_crtcs; i++) { 76 | if (drm_res->crtcs[i] == crtc_id) { 77 | continue; 78 | } 79 | drmModeSetCrtc(drm_fd, drm_res->crtcs[i], 80 | 0, 0, 0, NULL, 0, NULL); 81 | } 82 | } 83 | 84 | bool 85 | dumb_fb_init(struct dumb_fb *fb, int drm_fd, uint32_t format, uint32_t width, 86 | uint32_t height) 87 | { 88 | int ret; 89 | uint32_t fb_id; 90 | 91 | assert(format == DRM_FORMAT_ARGB8888 || format == DRM_FORMAT_XRGB8888); 92 | 93 | struct drm_mode_create_dumb create = { 94 | .width = width, 95 | .height = height, 96 | .bpp = 32, 97 | .flags = 0, 98 | }; 99 | ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create); 100 | if (ret < 0) { 101 | return false; 102 | } 103 | 104 | uint32_t handles[4] = { create.handle }; 105 | uint32_t strides[4] = { create.pitch }; 106 | uint32_t offsets[4] = { 0 }; 107 | ret = drmModeAddFB2(drm_fd, width, height, format, handles, strides, 108 | offsets, &fb_id, 0); 109 | if (ret < 0) { 110 | return false; 111 | } 112 | 113 | fb->width = width; 114 | fb->height = height; 115 | fb->stride = create.pitch; 116 | fb->size = create.size; 117 | fb->handle = create.handle; 118 | fb->id = fb_id; 119 | return true; 120 | } 121 | 122 | void * 123 | dumb_fb_map(struct dumb_fb *fb, int drm_fd) 124 | { 125 | int ret; 126 | 127 | struct drm_mode_map_dumb map = { .handle = fb->handle }; 128 | ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_MAP_DUMB, &map); 129 | if (ret < 0) { 130 | return MAP_FAILED; 131 | } 132 | 133 | return mmap(0, fb->size, PROT_READ | PROT_WRITE, MAP_SHARED, drm_fd, 134 | map.offset); 135 | } 136 | 137 | void 138 | dumb_fb_fill(struct dumb_fb *fb, int drm_fd, uint32_t color) 139 | { 140 | uint32_t *data; 141 | size_t i; 142 | 143 | data = dumb_fb_map(fb, drm_fd); 144 | if (data == MAP_FAILED) { 145 | return; 146 | } 147 | 148 | for (i = 0; i < fb->size / sizeof(uint32_t); i++) { 149 | data[i] = color; 150 | } 151 | 152 | munmap(data, fb->size); 153 | } 154 | -------------------------------------------------------------------------------- /example/common.h: -------------------------------------------------------------------------------- 1 | #ifndef EXAMPLE_COMMON_H 2 | #define EXAMPLE_COMMON_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct dumb_fb { 9 | uint32_t format; 10 | uint32_t width, height, stride, size; 11 | uint32_t handle; 12 | uint32_t id; 13 | }; 14 | 15 | drmModeConnector * 16 | pick_connector(int drm_fd, drmModeRes *drm_res); 17 | 18 | drmModeCrtc * 19 | pick_crtc(int drm_fd, drmModeRes *drm_res, drmModeConnector *connector); 20 | 21 | void 22 | disable_all_crtcs_except(int drm_fd, drmModeRes *drm_res, uint32_t crtc_id); 23 | 24 | bool 25 | dumb_fb_init(struct dumb_fb *fb, int drm_fd, uint32_t format, 26 | uint32_t width, uint32_t height); 27 | 28 | void * 29 | dumb_fb_map(struct dumb_fb *fb, int drm_fd); 30 | 31 | void 32 | dumb_fb_fill(struct dumb_fb *fb, int drm_fd, uint32_t color); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /example/compositor.c: -------------------------------------------------------------------------------- 1 | /* Compositor: create a few layers, display as many of them as possible on a 2 | * plane. Iterate over layers that didn't make it into a plane, and fallback to 3 | * composition if necessary. */ 4 | 5 | #define _POSIX_C_SOURCE 200809L 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "common.h" 17 | 18 | #define MAX_LAYERS_LEN 16 19 | 20 | /* ARGB 8:8:8:8 */ 21 | static const uint32_t colors[] = { 22 | 0xFFFF0000, /* red */ 23 | 0xFF00FF00, /* green */ 24 | 0xFF0000FF, /* blue */ 25 | 0xFFFFFF00, /* yellow */ 26 | }; 27 | 28 | static struct liftoff_layer * 29 | add_layer(int drm_fd, struct liftoff_output *output, int x, int y, int width, 30 | int height, bool with_alpha, bool white, struct dumb_fb *fb) 31 | { 32 | static size_t color_idx = 0; 33 | uint32_t color; 34 | struct liftoff_layer *layer; 35 | 36 | uint32_t format = with_alpha ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888; 37 | if (!dumb_fb_init(fb, drm_fd, format, width, height)) { 38 | fprintf(stderr, "failed to create framebuffer\n"); 39 | return NULL; 40 | } 41 | printf("Created FB %d with size %dx%d\n", fb->id, width, height); 42 | 43 | if (white) { 44 | color = 0xFFFFFFFF; 45 | } else { 46 | color = colors[color_idx]; 47 | color_idx = (color_idx + 1) % (sizeof(colors) / sizeof(colors[0])); 48 | } 49 | 50 | dumb_fb_fill(fb, drm_fd, color); 51 | 52 | layer = liftoff_layer_create(output); 53 | liftoff_layer_set_property(layer, "FB_ID", fb->id); 54 | liftoff_layer_set_property(layer, "CRTC_X", x); 55 | liftoff_layer_set_property(layer, "CRTC_Y", y); 56 | liftoff_layer_set_property(layer, "CRTC_W", width); 57 | liftoff_layer_set_property(layer, "CRTC_H", height); 58 | liftoff_layer_set_property(layer, "SRC_X", 0); 59 | liftoff_layer_set_property(layer, "SRC_Y", 0); 60 | liftoff_layer_set_property(layer, "SRC_W", width << 16); 61 | liftoff_layer_set_property(layer, "SRC_H", height << 16); 62 | 63 | return layer; 64 | } 65 | 66 | /* Naive compositor for opaque buffers */ 67 | static void 68 | composite(int drm_fd, struct dumb_fb *dst_fb, struct dumb_fb *src_fb, int dst_x, 69 | int dst_y) 70 | { 71 | uint8_t *dst, *src; 72 | int i, y, src_width; 73 | 74 | dst = dumb_fb_map(dst_fb, drm_fd); 75 | src = dumb_fb_map(src_fb, drm_fd); 76 | 77 | src_width = src_fb->width; 78 | if (dst_x < 0) { 79 | dst_x = 0; 80 | } 81 | if (dst_x + src_width > (int)dst_fb->width) { 82 | src_width = dst_fb->width - dst_x; 83 | } 84 | 85 | for (i = 0; i < (int)src_fb->height; i++) { 86 | y = dst_y + i; 87 | if (y < 0 || y >= (int)dst_fb->height) { 88 | continue; 89 | } 90 | memcpy(dst + dst_fb->stride * y + 91 | dst_x * sizeof(uint32_t), 92 | src + src_fb->stride * i, 93 | src_width * sizeof(uint32_t)); 94 | } 95 | 96 | munmap(dst, dst_fb->size); 97 | munmap(src, src_fb->size); 98 | } 99 | 100 | int 101 | main(int argc, char *argv[]) 102 | { 103 | int opt; 104 | size_t layers_len; 105 | int drm_fd; 106 | struct liftoff_device *device; 107 | drmModeRes *drm_res; 108 | drmModeCrtc *crtc; 109 | drmModeConnector *connector; 110 | struct liftoff_output *output; 111 | struct dumb_fb composition_fb = {0}; 112 | struct liftoff_layer *composition_layer; 113 | struct dumb_fb fbs[MAX_LAYERS_LEN] = {0}; 114 | struct liftoff_layer *layers[MAX_LAYERS_LEN]; 115 | struct liftoff_plane *plane; 116 | drmModeAtomicReq *req; 117 | int ret; 118 | size_t i; 119 | uint32_t flags; 120 | 121 | layers_len = 6; 122 | while ((opt = getopt(argc, argv, "l:h")) != -1) { 123 | switch (opt) { 124 | case 'l': 125 | layers_len = atoi(optarg); 126 | break; 127 | default: 128 | fprintf(stderr, 129 | "usage: %s [options...]\n" 130 | " -h Display help message\n" 131 | " -l Number of layers (default: 6)\n", 132 | argv[0]); 133 | return opt == 'h' ? 0 : 1; 134 | } 135 | } 136 | if (layers_len <= 0 || layers_len > MAX_LAYERS_LEN) { 137 | fprintf(stderr, "invalid -l value\n"); 138 | return 1; 139 | } 140 | 141 | drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); 142 | if (drm_fd < 0) { 143 | perror("open"); 144 | return 1; 145 | } 146 | 147 | if (drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1) < 0) { 148 | perror("drmSetClientCap(ATOMIC)"); 149 | return 1; 150 | } 151 | 152 | device = liftoff_device_create(drm_fd); 153 | if (device == NULL) { 154 | perror("liftoff_device_create"); 155 | return 1; 156 | } 157 | 158 | liftoff_device_register_all_planes(device); 159 | 160 | drm_res = drmModeGetResources(drm_fd); 161 | connector = pick_connector(drm_fd, drm_res); 162 | crtc = pick_crtc(drm_fd, drm_res, connector); 163 | disable_all_crtcs_except(drm_fd, drm_res, crtc->crtc_id); 164 | output = liftoff_output_create(device, crtc->crtc_id); 165 | drmModeFreeResources(drm_res); 166 | 167 | if (connector == NULL) { 168 | fprintf(stderr, "no connector found\n"); 169 | return 1; 170 | } 171 | if (crtc == NULL || !crtc->mode_valid) { 172 | fprintf(stderr, "no CRTC found\n"); 173 | return 1; 174 | } 175 | 176 | printf("Using connector %d, CRTC %d\n", connector->connector_id, 177 | crtc->crtc_id); 178 | 179 | composition_layer = add_layer(drm_fd, output, 0, 0, crtc->mode.hdisplay, 180 | crtc->mode.vdisplay, false, true, 181 | &composition_fb); 182 | layers[0] = add_layer(drm_fd, output, 0, 0, crtc->mode.hdisplay, 183 | crtc->mode.vdisplay, false, true, &fbs[0]); 184 | for (i = 1; i < layers_len; i++) { 185 | layers[i] = add_layer(drm_fd, output, 100 * i, 100 * i, 186 | 256, 256, i % 2, false, &fbs[i]); 187 | } 188 | 189 | liftoff_layer_set_property(composition_layer, "zpos", 0); 190 | for (i = 0; i < layers_len; i++) { 191 | liftoff_layer_set_property(layers[i], "zpos", i); 192 | } 193 | 194 | liftoff_output_set_composition_layer(output, composition_layer); 195 | 196 | flags = DRM_MODE_ATOMIC_NONBLOCK; 197 | req = drmModeAtomicAlloc(); 198 | ret = liftoff_output_apply(output, req, flags); 199 | if (ret != 0) { 200 | perror("liftoff_output_apply"); 201 | return 1; 202 | } 203 | 204 | /* Composite layers that didn't make it into a plane */ 205 | for (i = 1; i < layers_len; i++) { 206 | if (liftoff_layer_needs_composition(layers[i])) { 207 | composite(drm_fd, &composition_fb, &fbs[i], 208 | i * 100, i * 100); 209 | } 210 | } 211 | 212 | ret = drmModeAtomicCommit(drm_fd, req, flags, NULL); 213 | if (ret < 0) { 214 | perror("drmModeAtomicCommit"); 215 | return 1; 216 | } 217 | 218 | plane = liftoff_layer_get_plane(composition_layer); 219 | printf("Composition layer got assigned to plane %u\n", 220 | plane ? liftoff_plane_get_id(plane) : 0); 221 | for (i = 0; i < layers_len; i++) { 222 | plane = liftoff_layer_get_plane(layers[i]); 223 | printf("Layer %zu got assigned to plane %u\n", i, 224 | plane ? liftoff_plane_get_id(plane) : 0); 225 | } 226 | 227 | sleep(1); 228 | 229 | drmModeAtomicFree(req); 230 | liftoff_layer_destroy(composition_layer); 231 | for (i = 0; i < layers_len; i++) { 232 | liftoff_layer_destroy(layers[i]); 233 | } 234 | liftoff_output_destroy(output); 235 | drmModeFreeCrtc(crtc); 236 | drmModeFreeConnector(connector); 237 | liftoff_device_destroy(device); 238 | return 0; 239 | } 240 | -------------------------------------------------------------------------------- /example/dynamic.c: -------------------------------------------------------------------------------- 1 | /* Dynamic: create a few layers, setup a rendering loop for one of them. The 2 | * result is a rectangle updating its color while all other layers that make it 3 | * into a plane are static. */ 4 | 5 | #define _POSIX_C_SOURCE 200809L 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "common.h" 16 | 17 | #define LAYERS_LEN 4 18 | 19 | struct example_layer { 20 | float color[3]; 21 | int dec; 22 | int x, y; 23 | 24 | struct dumb_fb fbs[2]; 25 | size_t front_fb; 26 | 27 | struct liftoff_layer *layer; 28 | }; 29 | 30 | static int drm_fd = -1; 31 | static struct liftoff_device *device = NULL; 32 | static struct liftoff_output *output = NULL; 33 | static struct example_layer layers[LAYERS_LEN] = {0}; 34 | static size_t active_layer_idx = 2; 35 | 36 | static bool 37 | init_layer(int drm_fd, struct example_layer *layer, 38 | struct liftoff_output *output, int width, int height, 39 | bool with_alpha) 40 | { 41 | static size_t color_idx = 0; 42 | static float color_value = 1.0; 43 | uint32_t format; 44 | 45 | format = with_alpha ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888; 46 | if (!dumb_fb_init(&layer->fbs[0], drm_fd, format, width, height) || 47 | !dumb_fb_init(&layer->fbs[1], drm_fd, format, width, height)) { 48 | fprintf(stderr, "failed to create framebuffer\n"); 49 | return false; 50 | } 51 | 52 | layer->layer = liftoff_layer_create(output); 53 | liftoff_layer_set_property(layer->layer, "CRTC_W", width); 54 | liftoff_layer_set_property(layer->layer, "CRTC_H", height); 55 | liftoff_layer_set_property(layer->layer, "SRC_X", 0); 56 | liftoff_layer_set_property(layer->layer, "SRC_Y", 0); 57 | liftoff_layer_set_property(layer->layer, "SRC_W", width << 16); 58 | liftoff_layer_set_property(layer->layer, "SRC_H", height << 16); 59 | 60 | layer->color[color_idx % 3] = color_value; 61 | color_idx++; 62 | if (color_idx % 3 == 0) { 63 | color_value -= 0.1; 64 | } 65 | 66 | return true; 67 | } 68 | 69 | static void 70 | draw_layer(int drm_fd, struct example_layer *layer) 71 | { 72 | uint32_t color; 73 | struct dumb_fb *fb; 74 | 75 | layer->front_fb = (layer->front_fb + 1) % 2; 76 | fb = &layer->fbs[layer->front_fb]; 77 | 78 | color = ((uint32_t)0xFF << 24) | 79 | ((uint32_t)(layer->color[0] * 0xFF) << 16) | 80 | ((uint32_t)(layer->color[1] * 0xFF) << 8) | 81 | (uint32_t)(layer->color[2] * 0xFF); 82 | 83 | dumb_fb_fill(fb, drm_fd, color); 84 | 85 | liftoff_layer_set_property(layer->layer, "FB_ID", fb->id); 86 | liftoff_layer_set_property(layer->layer, "CRTC_X", layer->x); 87 | liftoff_layer_set_property(layer->layer, "CRTC_Y", layer->y); 88 | } 89 | 90 | static bool 91 | draw(void) 92 | { 93 | struct example_layer *active_layer; 94 | drmModeAtomicReq *req; 95 | int ret, inc; 96 | size_t i; 97 | uint32_t flags; 98 | struct liftoff_plane *plane; 99 | 100 | active_layer = &layers[active_layer_idx]; 101 | 102 | inc = (active_layer->dec + 1) % 3; 103 | 104 | active_layer->color[inc] += 0.05; 105 | active_layer->color[active_layer->dec] -= 0.05; 106 | 107 | if (active_layer->color[active_layer->dec] < 0.0f) { 108 | active_layer->color[inc] = 1.0f; 109 | active_layer->color[active_layer->dec] = 0.0f; 110 | active_layer->dec = inc; 111 | } 112 | 113 | draw_layer(drm_fd, active_layer); 114 | 115 | flags = DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT; 116 | req = drmModeAtomicAlloc(); 117 | ret = liftoff_output_apply(output, req, flags); 118 | if (ret != 0) { 119 | perror("liftoff_output_apply"); 120 | return false; 121 | } 122 | 123 | ret = drmModeAtomicCommit(drm_fd, req, flags, NULL); 124 | if (ret < 0) { 125 | perror("drmModeAtomicCommit"); 126 | return false; 127 | } 128 | 129 | drmModeAtomicFree(req); 130 | 131 | for (i = 0; i < sizeof(layers) / sizeof(layers[0]); i++) { 132 | plane = liftoff_layer_get_plane(layers[i].layer); 133 | if (plane != NULL) { 134 | printf("Layer %zu got assigned to plane %u\n", i, 135 | liftoff_plane_get_id(plane)); 136 | } else { 137 | printf("Layer %zu has no plane assigned\n", i); 138 | } 139 | } 140 | 141 | return true; 142 | } 143 | 144 | static void 145 | page_flip_handler(int fd, unsigned seq, unsigned tv_sec, unsigned tv_usec, 146 | unsigned crtc_id, void *data) 147 | { 148 | draw(); 149 | } 150 | 151 | int 152 | main(int argc, char *argv[]) 153 | { 154 | drmModeRes *drm_res; 155 | drmModeCrtc *crtc; 156 | drmModeConnector *connector; 157 | size_t i; 158 | int ret; 159 | 160 | drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); 161 | if (drm_fd < 0) { 162 | perror("open"); 163 | return 1; 164 | } 165 | 166 | if (drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1) < 0) { 167 | perror("drmSetClientCap(ATOMIC)"); 168 | return 1; 169 | } 170 | 171 | device = liftoff_device_create(drm_fd); 172 | if (device == NULL) { 173 | perror("liftoff_device_create"); 174 | return 1; 175 | } 176 | 177 | liftoff_device_register_all_planes(device); 178 | 179 | drm_res = drmModeGetResources(drm_fd); 180 | connector = pick_connector(drm_fd, drm_res); 181 | crtc = pick_crtc(drm_fd, drm_res, connector); 182 | disable_all_crtcs_except(drm_fd, drm_res, crtc->crtc_id); 183 | output = liftoff_output_create(device, crtc->crtc_id); 184 | drmModeFreeResources(drm_res); 185 | 186 | if (connector == NULL) { 187 | fprintf(stderr, "no connector found\n"); 188 | return 1; 189 | } 190 | if (crtc == NULL || !crtc->mode_valid) { 191 | fprintf(stderr, "no CRTC found\n"); 192 | return 1; 193 | } 194 | 195 | printf("Using connector %d, CRTC %d\n", connector->connector_id, 196 | crtc->crtc_id); 197 | 198 | init_layer(drm_fd, &layers[0], output, crtc->mode.hdisplay, 199 | crtc->mode.vdisplay, false); 200 | for (i = 1; i < LAYERS_LEN; i++) { 201 | init_layer(drm_fd, &layers[i], output, 100, 100, i % 2); 202 | layers[i].x = 100 * i; 203 | layers[i].y = 100 * i; 204 | } 205 | 206 | for (i = 0; i < LAYERS_LEN; i++) { 207 | liftoff_layer_set_property(layers[i].layer, "zpos", i); 208 | 209 | draw_layer(drm_fd, &layers[i]); 210 | } 211 | 212 | draw(); 213 | 214 | for (i = 0; i < 120; i++) { 215 | drmEventContext drm_event = { 216 | .version = 3, 217 | .page_flip_handler2 = page_flip_handler, 218 | }; 219 | struct pollfd pfd = { 220 | .fd = drm_fd, 221 | .events = POLLIN, 222 | }; 223 | 224 | ret = poll(&pfd, 1, 1000); 225 | if (ret != 1) { 226 | perror("poll"); 227 | return 1; 228 | } 229 | 230 | drmHandleEvent(drm_fd, &drm_event); 231 | } 232 | 233 | for (i = 0; i < sizeof(layers) / sizeof(layers[0]); i++) { 234 | liftoff_layer_destroy(layers[i].layer); 235 | } 236 | liftoff_output_destroy(output); 237 | drmModeFreeCrtc(crtc); 238 | drmModeFreeConnector(connector); 239 | liftoff_device_destroy(device); 240 | return 0; 241 | } 242 | -------------------------------------------------------------------------------- /example/meson.build: -------------------------------------------------------------------------------- 1 | executable( 2 | 'simple', 3 | files('common.c', 'simple.c'), 4 | dependencies: [liftoff], 5 | ) 6 | 7 | executable( 8 | 'compositor', 9 | files('common.c', 'compositor.c'), 10 | dependencies: [liftoff], 11 | ) 12 | 13 | executable( 14 | 'dynamic', 15 | files('common.c', 'dynamic.c'), 16 | dependencies: [liftoff], 17 | ) 18 | 19 | executable( 20 | 'multi-output', 21 | files('common.c', 'multi-output.c'), 22 | dependencies: [liftoff], 23 | ) 24 | -------------------------------------------------------------------------------- /example/multi-output.c: -------------------------------------------------------------------------------- 1 | /* Multiple outputs: create a few layers on each output and display as many of 2 | * them as possible. Layers that don't make it into a plane won't be dispayed. 3 | * Demonstrates how the library distributes planes across CRTCs. */ 4 | 5 | #define _POSIX_C_SOURCE 200809L 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "common.h" 15 | 16 | #define MAX_OUTPUTS 32 17 | #define LAYERS_PER_OUTPUT 4 18 | 19 | /* ARGB 8:8:8:8 */ 20 | static const uint32_t colors[] = { 21 | 0xFFFF0000, /* red */ 22 | 0xFF00FF00, /* green */ 23 | 0xFF0000FF, /* blue */ 24 | 0xFFFFFF00, /* yellow */ 25 | }; 26 | 27 | static struct liftoff_layer * 28 | add_layer(int drm_fd, struct liftoff_output *output, int x, int y, int width, 29 | int height, bool with_alpha) 30 | { 31 | static bool first = true; 32 | static size_t color_idx = 0; 33 | struct dumb_fb fb = {0}; 34 | uint32_t color; 35 | struct liftoff_layer *layer; 36 | 37 | uint32_t format = with_alpha ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888; 38 | if (!dumb_fb_init(&fb, drm_fd, format, width, height)) { 39 | fprintf(stderr, "failed to create framebuffer\n"); 40 | return NULL; 41 | } 42 | printf("Created FB %d with size %dx%d\n", fb.id, width, height); 43 | 44 | if (first) { 45 | color = 0xFFFFFFFF; 46 | first = false; 47 | } else { 48 | color = colors[color_idx]; 49 | color_idx = (color_idx + 1) % (sizeof(colors) / sizeof(colors[0])); 50 | } 51 | 52 | dumb_fb_fill(&fb, drm_fd, color); 53 | 54 | layer = liftoff_layer_create(output); 55 | liftoff_layer_set_property(layer, "FB_ID", fb.id); 56 | liftoff_layer_set_property(layer, "CRTC_X", x); 57 | liftoff_layer_set_property(layer, "CRTC_Y", y); 58 | liftoff_layer_set_property(layer, "CRTC_W", width); 59 | liftoff_layer_set_property(layer, "CRTC_H", height); 60 | liftoff_layer_set_property(layer, "SRC_X", 0); 61 | liftoff_layer_set_property(layer, "SRC_Y", 0); 62 | liftoff_layer_set_property(layer, "SRC_W", width << 16); 63 | liftoff_layer_set_property(layer, "SRC_H", height << 16); 64 | 65 | return layer; 66 | } 67 | 68 | int 69 | main(int argc, char *argv[]) 70 | { 71 | int drm_fd; 72 | struct liftoff_device *device; 73 | drmModeRes *drm_res; 74 | drmModeConnector *connector; 75 | drmModeCrtc *crtcs[MAX_OUTPUTS], *crtc; 76 | struct liftoff_output *outputs[MAX_OUTPUTS], *output; 77 | struct liftoff_layer *layers[MAX_OUTPUTS * LAYERS_PER_OUTPUT]; 78 | struct liftoff_plane *plane; 79 | size_t outputs_len, layers_len; 80 | drmModeAtomicReq *req; 81 | int ret; 82 | size_t i, j; 83 | uint32_t flags; 84 | 85 | drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); 86 | if (drm_fd < 0) { 87 | perror("open"); 88 | return 1; 89 | } 90 | 91 | if (drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1) < 0) { 92 | perror("drmSetClientCap(ATOMIC)"); 93 | return 1; 94 | } 95 | 96 | device = liftoff_device_create(drm_fd); 97 | if (device == NULL) { 98 | perror("liftoff_device_create"); 99 | return 1; 100 | } 101 | 102 | liftoff_device_register_all_planes(device); 103 | 104 | drm_res = drmModeGetResources(drm_fd); 105 | 106 | outputs_len = 0; 107 | for (i = 0; i < (size_t)drm_res->count_connectors; i++) { 108 | connector = drmModeGetConnector(drm_fd, drm_res->connectors[i]); 109 | if (connector->connection != DRM_MODE_CONNECTED) { 110 | drmModeFreeConnector(connector); 111 | continue; 112 | } 113 | 114 | crtc = pick_crtc(drm_fd, drm_res, connector); 115 | if (crtc == NULL || !crtc->mode_valid) { 116 | drmModeFreeConnector(connector); 117 | continue; 118 | } 119 | 120 | output = liftoff_output_create(device, crtc->crtc_id); 121 | 122 | printf("Using connector %d, CRTC %d\n", connector->connector_id, 123 | crtc->crtc_id); 124 | 125 | drmModeFreeConnector(connector); 126 | 127 | crtcs[outputs_len] = crtc; 128 | outputs[outputs_len] = output; 129 | outputs_len++; 130 | } 131 | drmModeFreeResources(drm_res); 132 | 133 | if (outputs_len == 0) { 134 | fprintf(stderr, "no connector found\n"); 135 | return 1; 136 | } 137 | 138 | layers_len = 0; 139 | for (i = 0; i < outputs_len; i++) { 140 | output = outputs[i]; 141 | crtc = crtcs[i]; 142 | 143 | layers[layers_len++] = add_layer(drm_fd, output, 0, 0, 144 | crtc->mode.hdisplay, 145 | crtc->mode.vdisplay, false); 146 | for (j = 1; j < LAYERS_PER_OUTPUT; j++) { 147 | layers[layers_len++] = add_layer(drm_fd, output, 148 | 100 * j, 100 * j, 149 | 256, 256, j % 2); 150 | } 151 | } 152 | 153 | for (i = 0; i < layers_len; i++) { 154 | liftoff_layer_set_property(layers[i], "zpos", i); 155 | } 156 | 157 | flags = DRM_MODE_ATOMIC_NONBLOCK; 158 | req = drmModeAtomicAlloc(); 159 | for (i = 0; i < outputs_len; i++) { 160 | ret = liftoff_output_apply(outputs[i], req, flags); 161 | if (ret != 0) { 162 | perror("liftoff_output_apply"); 163 | return 1; 164 | } 165 | } 166 | 167 | ret = drmModeAtomicCommit(drm_fd, req, flags, NULL); 168 | if (ret < 0) { 169 | perror("drmModeAtomicCommit"); 170 | return 1; 171 | } 172 | 173 | for (i = 0; i < layers_len; i++) { 174 | plane = liftoff_layer_get_plane(layers[i]); 175 | if (plane != NULL) { 176 | printf("Layer %zu got assigned to plane %u\n", i, 177 | liftoff_plane_get_id(plane)); 178 | } else { 179 | printf("Layer %zu has no plane assigned\n", i); 180 | } 181 | } 182 | 183 | sleep(1); 184 | 185 | drmModeAtomicFree(req); 186 | for (i = 0; i < layers_len; i++) { 187 | liftoff_layer_destroy(layers[i]); 188 | } 189 | for (i = 0; i < outputs_len; i++) { 190 | liftoff_output_destroy(outputs[i]); 191 | drmModeFreeCrtc(crtcs[i]); 192 | } 193 | liftoff_device_destroy(device); 194 | return 0; 195 | } 196 | -------------------------------------------------------------------------------- /example/simple.c: -------------------------------------------------------------------------------- 1 | /* Minimalistic example: create a few layers and display as many of them as 2 | * possible. Layers that don't make it into a plane won't be displayed. */ 3 | 4 | #define _POSIX_C_SOURCE 200809L 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "common.h" 14 | 15 | #define LAYERS_LEN 4 16 | 17 | /* ARGB 8:8:8:8 */ 18 | static const uint32_t colors[] = { 19 | 0xFFFF0000, /* red */ 20 | 0xFF00FF00, /* green */ 21 | 0xFF0000FF, /* blue */ 22 | 0xFFFFFF00, /* yellow */ 23 | }; 24 | 25 | static struct liftoff_layer * 26 | add_layer(int drm_fd, struct liftoff_output *output, int x, int y, int width, 27 | int height, bool with_alpha) 28 | { 29 | static bool first = true; 30 | static size_t color_idx = 0; 31 | struct dumb_fb fb = {0}; 32 | uint32_t color; 33 | struct liftoff_layer *layer; 34 | 35 | uint32_t format = with_alpha ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888; 36 | if (!dumb_fb_init(&fb, drm_fd, format, width, height)) { 37 | fprintf(stderr, "failed to create framebuffer\n"); 38 | return NULL; 39 | } 40 | printf("Created FB %d with size %dx%d\n", fb.id, width, height); 41 | 42 | if (first) { 43 | color = 0xFFFFFFFF; 44 | first = false; 45 | } else { 46 | color = colors[color_idx]; 47 | color_idx = (color_idx + 1) % (sizeof(colors) / sizeof(colors[0])); 48 | } 49 | 50 | dumb_fb_fill(&fb, drm_fd, color); 51 | 52 | layer = liftoff_layer_create(output); 53 | liftoff_layer_set_property(layer, "FB_ID", fb.id); 54 | liftoff_layer_set_property(layer, "CRTC_X", x); 55 | liftoff_layer_set_property(layer, "CRTC_Y", y); 56 | liftoff_layer_set_property(layer, "CRTC_W", width); 57 | liftoff_layer_set_property(layer, "CRTC_H", height); 58 | liftoff_layer_set_property(layer, "SRC_X", 0); 59 | liftoff_layer_set_property(layer, "SRC_Y", 0); 60 | liftoff_layer_set_property(layer, "SRC_W", width << 16); 61 | liftoff_layer_set_property(layer, "SRC_H", height << 16); 62 | 63 | return layer; 64 | } 65 | 66 | int 67 | main(int argc, char *argv[]) 68 | { 69 | int drm_fd; 70 | struct liftoff_device *device; 71 | drmModeRes *drm_res; 72 | drmModeCrtc *crtc; 73 | drmModeConnector *connector; 74 | struct liftoff_output *output; 75 | struct liftoff_layer *layers[LAYERS_LEN]; 76 | struct liftoff_plane *plane; 77 | drmModeAtomicReq *req; 78 | int ret; 79 | size_t i; 80 | uint32_t flags; 81 | 82 | drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); 83 | if (drm_fd < 0) { 84 | perror("open"); 85 | return 1; 86 | } 87 | 88 | if (drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1) < 0) { 89 | perror("drmSetClientCap(ATOMIC)"); 90 | return 1; 91 | } 92 | 93 | device = liftoff_device_create(drm_fd); 94 | if (device == NULL) { 95 | perror("liftoff_device_create"); 96 | return 1; 97 | } 98 | 99 | liftoff_device_register_all_planes(device); 100 | 101 | drm_res = drmModeGetResources(drm_fd); 102 | connector = pick_connector(drm_fd, drm_res); 103 | crtc = pick_crtc(drm_fd, drm_res, connector); 104 | disable_all_crtcs_except(drm_fd, drm_res, crtc->crtc_id); 105 | output = liftoff_output_create(device, crtc->crtc_id); 106 | drmModeFreeResources(drm_res); 107 | 108 | if (connector == NULL) { 109 | fprintf(stderr, "no connector found\n"); 110 | return 1; 111 | } 112 | if (crtc == NULL || !crtc->mode_valid) { 113 | fprintf(stderr, "no CRTC found\n"); 114 | return 1; 115 | } 116 | 117 | printf("Using connector %d, CRTC %d\n", connector->connector_id, 118 | crtc->crtc_id); 119 | 120 | layers[0] = add_layer(drm_fd, output, 0, 0, crtc->mode.hdisplay, 121 | crtc->mode.vdisplay, false); 122 | for (i = 1; i < LAYERS_LEN; i++) { 123 | layers[i] = add_layer(drm_fd, output, 100 * i, 100 * i, 124 | 256, 256, i % 2); 125 | } 126 | 127 | for (i = 0; i < LAYERS_LEN; i++) { 128 | liftoff_layer_set_property(layers[i], "zpos", i); 129 | } 130 | 131 | flags = DRM_MODE_ATOMIC_NONBLOCK; 132 | req = drmModeAtomicAlloc(); 133 | ret = liftoff_output_apply(output, req, flags); 134 | if (ret != 0) { 135 | perror("liftoff_output_apply"); 136 | return 1; 137 | } 138 | 139 | ret = drmModeAtomicCommit(drm_fd, req, flags, NULL); 140 | if (ret < 0) { 141 | perror("drmModeAtomicCommit"); 142 | return 1; 143 | } 144 | 145 | for (i = 0; i < sizeof(layers) / sizeof(layers[0]); i++) { 146 | plane = liftoff_layer_get_plane(layers[i]); 147 | if (plane != NULL) { 148 | printf("Layer %zu got assigned to plane %u\n", i, 149 | liftoff_plane_get_id(plane)); 150 | } else { 151 | printf("Layer %zu has no plane assigned\n", i); 152 | } 153 | } 154 | 155 | sleep(1); 156 | 157 | drmModeAtomicFree(req); 158 | for (i = 0; i < sizeof(layers) / sizeof(layers[0]); i++) { 159 | liftoff_layer_destroy(layers[i]); 160 | } 161 | liftoff_output_destroy(output); 162 | drmModeFreeCrtc(crtc); 163 | drmModeFreeConnector(connector); 164 | liftoff_device_destroy(device); 165 | return 0; 166 | } 167 | -------------------------------------------------------------------------------- /include/libliftoff.h: -------------------------------------------------------------------------------- 1 | #ifndef LIFTOFF_H 2 | #define LIFTOFF_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | struct liftoff_device; 11 | struct liftoff_output; 12 | struct liftoff_layer; 13 | struct liftoff_plane; 14 | 15 | /** 16 | * Initialize libliftoff for a DRM node. 17 | * 18 | * The node is expected to have DRM_CLIENT_CAP_ATOMIC enabled. libliftoff takes 19 | * ownership of the file descriptor. 20 | */ 21 | struct liftoff_device * 22 | liftoff_device_create(int drm_fd); 23 | 24 | /** 25 | * Destroy a libliftoff device. 26 | * 27 | * The caller is expected to destroy the outputs and layers explicitly. 28 | */ 29 | void 30 | liftoff_device_destroy(struct liftoff_device *device); 31 | 32 | /** 33 | * Register all available hardware planes to be managed by the libliftoff 34 | * device. 35 | * 36 | * Users should call this function if they don't manually set any plane property 37 | * and instead use libliftoff layers. 38 | * 39 | * Zero is returned on success, negative errno on error. 40 | */ 41 | int 42 | liftoff_device_register_all_planes(struct liftoff_device *device); 43 | 44 | /** 45 | * Register a hardware plane to be managed by the libliftoff device. 46 | * 47 | * Users should call this function for each plane they don't want to manually 48 | * manage. Registering the same plane twice is an error. 49 | */ 50 | struct liftoff_plane * 51 | liftoff_plane_create(struct liftoff_device *device, uint32_t plane_id); 52 | 53 | /** 54 | * Unregister a hardware plane. 55 | */ 56 | void 57 | liftoff_plane_destroy(struct liftoff_plane *plane); 58 | 59 | /** 60 | * Obtain the object ID of the plane. 61 | */ 62 | uint32_t 63 | liftoff_plane_get_id(struct liftoff_plane *plane); 64 | 65 | /** 66 | * Build a layer to plane mapping and append the plane configuration to `req`. 67 | * 68 | * Callers are expected to commit `req` afterwards and can figure out which 69 | * layers need composition via `liftoff_layer_needs_composition`. 70 | * 71 | * `flags` is the atomic commit flags the caller intends to use. 72 | * 73 | * Zero is returned on success, negative errno on error. 74 | */ 75 | int 76 | liftoff_output_apply(struct liftoff_output *output, drmModeAtomicReq *req, 77 | uint32_t flags); 78 | 79 | /** 80 | * Make the device manage a CRTC's planes. 81 | * 82 | * The returned output allows callers to attach layers. 83 | */ 84 | struct liftoff_output * 85 | liftoff_output_create(struct liftoff_device *device, uint32_t crtc_id); 86 | 87 | /** 88 | * Destroy a libliftoff output. 89 | * 90 | * The caller is expected to destroy the output's layers explicitly. 91 | */ 92 | void 93 | liftoff_output_destroy(struct liftoff_output *output); 94 | 95 | /** 96 | * Indicate on which layer composition can take place. 97 | * 98 | * Users should be able to blend layers that haven't been mapped to a plane to 99 | * this layer. The composition layer won't be used if all other layers have been 100 | * mapped to a plane. There is at most one composition layer per output. 101 | */ 102 | void 103 | liftoff_output_set_composition_layer(struct liftoff_output *output, 104 | struct liftoff_layer *layer); 105 | 106 | /** 107 | * Check whether this output needs composition. 108 | * 109 | * An output doesn't need composition if all visible layers could be mapped to a 110 | * plane. In other words, if an output needs composition, at least one layer 111 | * will return true when `liftoff_layer_needs_composition` is called. 112 | */ 113 | bool 114 | liftoff_output_needs_composition(struct liftoff_output *output); 115 | 116 | /** 117 | * Create a new layer on an output. 118 | * 119 | * A layer is a virtual plane. Users can create as many layers as they want and 120 | * set any KMS property on them, without any constraint. libliftoff will try 121 | * to map layers to hardware planes on a best-effort basis. The user will need 122 | * to manually handle layers that couldn't be mapped to a plane. 123 | */ 124 | struct liftoff_layer * 125 | liftoff_layer_create(struct liftoff_output *output); 126 | 127 | /** 128 | * Destroy a layer. 129 | */ 130 | void 131 | liftoff_layer_destroy(struct liftoff_layer *layer); 132 | 133 | /** 134 | * Set a property on the layer. 135 | * 136 | * Any plane property can be set (except CRTC_ID). If none of the planes support 137 | * the property, the layer won't be mapped to any plane. 138 | * 139 | * Setting a zero FB_ID disables the layer. 140 | * 141 | * Zero is returned on success, negative errno on error. 142 | */ 143 | int 144 | liftoff_layer_set_property(struct liftoff_layer *layer, const char *name, 145 | uint64_t value); 146 | 147 | /** 148 | * Force composition on this layer. 149 | * 150 | * This unsets any previous FB_ID value. To switch back to direct scan-out, set 151 | * FB_ID again. 152 | * 153 | * This can be used when no KMS FB ID is available for this layer but it still 154 | * needs to be displayed (e.g. the buffer cannot be imported in KMS). 155 | */ 156 | void 157 | liftoff_layer_set_fb_composited(struct liftoff_layer *layer); 158 | 159 | /** 160 | * Check whether this layer needs composition. 161 | * 162 | * A layer needs composition if it's visible and if it couldn't be mapped to a 163 | * plane. 164 | */ 165 | bool 166 | liftoff_layer_needs_composition(struct liftoff_layer *layer); 167 | 168 | /** 169 | * Retrieve the plane mapped to this layer. 170 | * 171 | * NULL is returned if no plane is mapped. 172 | */ 173 | struct liftoff_plane * 174 | liftoff_layer_get_plane(struct liftoff_layer *layer); 175 | 176 | enum liftoff_log_priority { 177 | LIFTOFF_SILENT, 178 | LIFTOFF_ERROR, 179 | LIFTOFF_DEBUG, 180 | }; 181 | 182 | typedef void (*liftoff_log_handler)(enum liftoff_log_priority priority, 183 | const char *fmt, va_list args); 184 | 185 | /** 186 | * Set libliftoff's log priority. 187 | * 188 | * Only messages with a priority higher than the provided priority will be 189 | * logged. The default priority is LIFTOFF_ERROR. 190 | */ 191 | void 192 | liftoff_log_set_priority(enum liftoff_log_priority priority); 193 | 194 | /** 195 | * Set libliftoff's log handler. 196 | * 197 | * The default handler prints messages to stderr. NULL restores the default 198 | * handler. 199 | */ 200 | void 201 | liftoff_log_set_handler(liftoff_log_handler handler); 202 | 203 | #endif 204 | -------------------------------------------------------------------------------- /include/list.h: -------------------------------------------------------------------------------- 1 | #ifndef LIST_H 2 | #define LIST_H 3 | 4 | #include 5 | #include 6 | 7 | struct liftoff_list { 8 | struct liftoff_list *prev; 9 | struct liftoff_list *next; 10 | }; 11 | 12 | void 13 | liftoff_list_init(struct liftoff_list *list); 14 | 15 | void 16 | liftoff_list_insert(struct liftoff_list *list, struct liftoff_list *elm); 17 | 18 | void 19 | liftoff_list_remove(struct liftoff_list *elm); 20 | 21 | size_t 22 | liftoff_list_length(const struct liftoff_list *list); 23 | 24 | bool 25 | liftoff_list_empty(const struct liftoff_list *list); 26 | 27 | #define liftoff_container_of(ptr, sample, member) \ 28 | (__typeof__(sample))((char *)(ptr) - \ 29 | offsetof(__typeof__(*sample), member)) 30 | 31 | #define liftoff_list_for_each(pos, head, member) \ 32 | for (pos = liftoff_container_of((head)->next, pos, member); \ 33 | &pos->member != (head); \ 34 | pos = liftoff_container_of(pos->member.next, pos, member)) 35 | 36 | #define liftoff_list_for_each_safe(pos, tmp, head, member) \ 37 | for (pos = liftoff_container_of((head)->next, pos, member), \ 38 | tmp = liftoff_container_of(pos->member.next, tmp, member); \ 39 | &pos->member != (head); \ 40 | pos = tmp, \ 41 | tmp = liftoff_container_of(pos->member.next, tmp, member)) 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /include/log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #ifdef __GNUC__ 9 | #define _LIFTOFF_ATTRIB_PRINTF(start, end) __attribute__((format(printf, start, end))) 10 | #else 11 | #define _LIFTOFF_ATTRIB_PRINTF(start, end) 12 | #endif 13 | 14 | bool 15 | log_has(enum liftoff_log_priority priority); 16 | 17 | void 18 | liftoff_log(enum liftoff_log_priority priority, const char *format, ...) 19 | _LIFTOFF_ATTRIB_PRINTF(2, 3); 20 | 21 | void 22 | liftoff_log_errno(enum liftoff_log_priority priority, const char *msg); 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /include/private.h: -------------------------------------------------------------------------------- 1 | #ifndef PRIVATE_H 2 | #define PRIVATE_H 3 | 4 | #include 5 | #include "list.h" 6 | #include "log.h" 7 | 8 | /* Layer priority is assigned depending on the number of updates during a 9 | * given number of page-flips */ 10 | #define LIFTOFF_PRIORITY_PERIOD 60 11 | 12 | struct liftoff_device { 13 | int drm_fd; 14 | 15 | struct liftoff_list planes; /* liftoff_plane.link */ 16 | struct liftoff_list outputs; /* liftoff_output.link */ 17 | 18 | uint32_t *crtcs; 19 | size_t crtcs_len; 20 | 21 | int page_flip_counter; 22 | int test_commit_counter; 23 | }; 24 | 25 | struct liftoff_output { 26 | struct liftoff_device *device; 27 | uint32_t crtc_id; 28 | size_t crtc_index; 29 | struct liftoff_list link; /* liftoff_device.outputs */ 30 | 31 | struct liftoff_layer *composition_layer; 32 | 33 | struct liftoff_list layers; /* liftoff_layer.link */ 34 | /* layer added or removed, or composition layer changed */ 35 | bool layers_changed; 36 | 37 | int alloc_reused_counter; 38 | }; 39 | 40 | struct liftoff_layer { 41 | struct liftoff_output *output; 42 | struct liftoff_list link; /* liftoff_output.layers */ 43 | 44 | struct liftoff_layer_property *props; 45 | size_t props_len; 46 | 47 | bool force_composition; /* FB needs to be composited */ 48 | 49 | struct liftoff_plane *plane; 50 | 51 | int current_priority, pending_priority; 52 | /* prop added or force_composition changed */ 53 | bool changed; 54 | }; 55 | 56 | struct liftoff_layer_property { 57 | char name[DRM_PROP_NAME_LEN]; 58 | uint64_t value, prev_value; 59 | }; 60 | 61 | struct liftoff_plane { 62 | uint32_t id; 63 | uint32_t possible_crtcs; 64 | uint32_t type; 65 | int zpos; /* greater values mean closer to the eye */ 66 | /* TODO: formats */ 67 | struct liftoff_list link; /* liftoff_device.planes */ 68 | 69 | struct liftoff_plane_property *props; 70 | size_t props_len; 71 | 72 | struct liftoff_layer *layer; 73 | }; 74 | 75 | struct liftoff_plane_property { 76 | char name[DRM_PROP_NAME_LEN]; 77 | uint32_t id; 78 | }; 79 | 80 | struct liftoff_rect { 81 | int x, y; 82 | int width, height; 83 | }; 84 | 85 | int 86 | device_test_commit(struct liftoff_device *device, drmModeAtomicReq *req, 87 | uint32_t flags); 88 | 89 | struct liftoff_layer_property * 90 | layer_get_property(struct liftoff_layer *layer, const char *name); 91 | 92 | void 93 | layer_get_rect(struct liftoff_layer *layer, struct liftoff_rect *rect); 94 | 95 | bool 96 | layer_intersects(struct liftoff_layer *a, struct liftoff_layer *b); 97 | 98 | void 99 | layer_mark_clean(struct liftoff_layer *layer); 100 | 101 | void 102 | layer_update_priority(struct liftoff_layer *layer, bool make_current); 103 | 104 | bool 105 | layer_has_fb(struct liftoff_layer *layer); 106 | 107 | bool 108 | layer_is_visible(struct liftoff_layer *layer); 109 | 110 | int 111 | plane_apply(struct liftoff_plane *plane, struct liftoff_layer *layer, 112 | drmModeAtomicReq *req); 113 | 114 | void 115 | output_log_layers(struct liftoff_output *output); 116 | 117 | #endif 118 | -------------------------------------------------------------------------------- /layer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "private.h" 5 | 6 | struct liftoff_layer * 7 | liftoff_layer_create(struct liftoff_output *output) 8 | { 9 | struct liftoff_layer *layer; 10 | 11 | layer = calloc(1, sizeof(*layer)); 12 | if (layer == NULL) { 13 | liftoff_log_errno(LIFTOFF_ERROR, "calloc"); 14 | return NULL; 15 | } 16 | layer->output = output; 17 | liftoff_list_insert(output->layers.prev, &layer->link); 18 | output->layers_changed = true; 19 | return layer; 20 | } 21 | 22 | void 23 | liftoff_layer_destroy(struct liftoff_layer *layer) 24 | { 25 | if (layer == NULL) { 26 | return; 27 | } 28 | 29 | layer->output->layers_changed = true; 30 | if (layer->plane != NULL) { 31 | layer->plane->layer = NULL; 32 | } 33 | if (layer->output->composition_layer == layer) { 34 | layer->output->composition_layer = NULL; 35 | } 36 | free(layer->props); 37 | liftoff_list_remove(&layer->link); 38 | free(layer); 39 | } 40 | 41 | struct liftoff_layer_property * 42 | layer_get_property(struct liftoff_layer *layer, const char *name) 43 | { 44 | size_t i; 45 | 46 | for (i = 0; i < layer->props_len; i++) { 47 | if (strcmp(layer->props[i].name, name) == 0) { 48 | return &layer->props[i]; 49 | } 50 | } 51 | return NULL; 52 | } 53 | 54 | int 55 | liftoff_layer_set_property(struct liftoff_layer *layer, const char *name, 56 | uint64_t value) 57 | { 58 | struct liftoff_layer_property *props; 59 | struct liftoff_layer_property *prop; 60 | 61 | if (strcmp(name, "CRTC_ID") == 0) { 62 | liftoff_log(LIFTOFF_ERROR, 63 | "refusing to set a layer's CRTC_ID"); 64 | return -EINVAL; 65 | } 66 | 67 | prop = layer_get_property(layer, name); 68 | if (prop == NULL) { 69 | props = realloc(layer->props, (layer->props_len + 1) 70 | * sizeof(struct liftoff_layer_property)); 71 | if (props == NULL) { 72 | liftoff_log_errno(LIFTOFF_ERROR, "realloc"); 73 | return -ENOMEM; 74 | } 75 | layer->props = props; 76 | layer->props_len++; 77 | 78 | prop = &layer->props[layer->props_len - 1]; 79 | memset(prop, 0, sizeof(*prop)); 80 | strncpy(prop->name, name, sizeof(prop->name) - 1); 81 | 82 | layer->changed = true; 83 | } 84 | 85 | prop->value = value; 86 | 87 | if (strcmp(name, "FB_ID") == 0 && layer->force_composition) { 88 | layer->force_composition = false; 89 | layer->changed = true; 90 | } 91 | 92 | return 0; 93 | } 94 | 95 | void 96 | liftoff_layer_set_fb_composited(struct liftoff_layer *layer) 97 | { 98 | if (layer->force_composition) { 99 | return; 100 | } 101 | 102 | liftoff_layer_set_property(layer, "FB_ID", 0); 103 | 104 | layer->force_composition = true; 105 | layer->changed = true; 106 | } 107 | 108 | struct liftoff_plane * 109 | liftoff_layer_get_plane(struct liftoff_layer *layer) 110 | { 111 | return layer->plane; 112 | } 113 | 114 | bool 115 | liftoff_layer_needs_composition(struct liftoff_layer *layer) 116 | { 117 | if (!layer_is_visible(layer)) { 118 | return false; 119 | } 120 | return layer->plane == NULL; 121 | } 122 | 123 | void 124 | layer_get_rect(struct liftoff_layer *layer, struct liftoff_rect *rect) 125 | { 126 | struct liftoff_layer_property *x_prop, *y_prop, *w_prop, *h_prop; 127 | 128 | x_prop = layer_get_property(layer, "CRTC_X"); 129 | y_prop = layer_get_property(layer, "CRTC_Y"); 130 | w_prop = layer_get_property(layer, "CRTC_W"); 131 | h_prop = layer_get_property(layer, "CRTC_H"); 132 | 133 | rect->x = x_prop != NULL ? x_prop->value : 0; 134 | rect->y = y_prop != NULL ? y_prop->value : 0; 135 | rect->width = w_prop != NULL ? w_prop->value : 0; 136 | rect->height = h_prop != NULL ? h_prop->value : 0; 137 | } 138 | 139 | bool 140 | layer_intersects(struct liftoff_layer *a, struct liftoff_layer *b) 141 | { 142 | struct liftoff_rect ra, rb; 143 | 144 | layer_get_rect(a, &ra); 145 | layer_get_rect(b, &rb); 146 | 147 | return ra.x < rb.x + rb.width && ra.y < rb.y + rb.height && 148 | ra.x + ra.width > rb.x && ra.y + ra.height > rb.y; 149 | } 150 | 151 | void 152 | layer_mark_clean(struct liftoff_layer *layer) 153 | { 154 | size_t i; 155 | 156 | layer->changed = false; 157 | 158 | for (i = 0; i < layer->props_len; i++) { 159 | layer->props[i].prev_value = layer->props[i].value; 160 | } 161 | } 162 | 163 | static void 164 | log_priority(struct liftoff_layer *layer) 165 | { 166 | if (layer->current_priority == layer->pending_priority) { 167 | return; 168 | } 169 | 170 | liftoff_log(LIFTOFF_DEBUG, "Layer %p priority change: %d -> %d", 171 | (void *)layer, layer->current_priority, 172 | layer->pending_priority); 173 | } 174 | 175 | void 176 | layer_update_priority(struct liftoff_layer *layer, bool make_current) 177 | { 178 | struct liftoff_layer_property *prop; 179 | 180 | /* TODO: also bump priority when updating other properties */ 181 | prop = layer_get_property(layer, "FB_ID"); 182 | if (prop != NULL && prop->prev_value != prop->value) { 183 | layer->pending_priority++; 184 | } 185 | 186 | if (make_current) { 187 | log_priority(layer); 188 | layer->current_priority = layer->pending_priority; 189 | layer->pending_priority = 0; 190 | } 191 | } 192 | 193 | bool 194 | layer_has_fb(struct liftoff_layer *layer) 195 | { 196 | struct liftoff_layer_property *fb_id_prop; 197 | 198 | fb_id_prop = layer_get_property(layer, "FB_ID"); 199 | return fb_id_prop != NULL && fb_id_prop->value != 0; 200 | } 201 | 202 | bool 203 | layer_is_visible(struct liftoff_layer *layer) 204 | { 205 | struct liftoff_layer_property *alpha_prop; 206 | 207 | alpha_prop = layer_get_property(layer, "alpha"); 208 | if (alpha_prop != NULL && alpha_prop->value == 0) { 209 | return false; /* fully transparent */ 210 | } 211 | 212 | if (layer->force_composition) { 213 | return true; 214 | } else { 215 | return layer_has_fb(layer); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /list.c: -------------------------------------------------------------------------------- 1 | #include "list.h" 2 | 3 | void 4 | liftoff_list_init(struct liftoff_list *list) 5 | { 6 | list->prev = list; 7 | list->next = list; 8 | } 9 | 10 | void 11 | liftoff_list_insert(struct liftoff_list *list, struct liftoff_list *elm) 12 | { 13 | elm->prev = list; 14 | elm->next = list->next; 15 | list->next = elm; 16 | elm->next->prev = elm; 17 | } 18 | 19 | void 20 | liftoff_list_remove(struct liftoff_list *elm) 21 | { 22 | elm->prev->next = elm->next; 23 | elm->next->prev = elm->prev; 24 | elm->next = NULL; 25 | elm->prev = NULL; 26 | } 27 | 28 | size_t 29 | liftoff_list_length(const struct liftoff_list *list) 30 | { 31 | struct liftoff_list *e; 32 | size_t count; 33 | 34 | count = 0; 35 | e = list->next; 36 | while (e != list) { 37 | e = e->next; 38 | count++; 39 | } 40 | 41 | return count; 42 | } 43 | 44 | bool 45 | liftoff_list_empty(const struct liftoff_list *list) 46 | { 47 | return list->next == list; 48 | } 49 | -------------------------------------------------------------------------------- /log.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "log.h" 5 | 6 | static enum liftoff_log_priority log_priority = LIFTOFF_ERROR; 7 | 8 | static void 9 | log_stderr(enum liftoff_log_priority priority, const char *fmt, va_list args) 10 | { 11 | vfprintf(stderr, fmt, args); 12 | fprintf(stderr, "\n"); 13 | } 14 | 15 | static liftoff_log_handler log_handler = log_stderr; 16 | 17 | void 18 | liftoff_log_set_priority(enum liftoff_log_priority priority) 19 | { 20 | log_priority = priority; 21 | } 22 | 23 | void 24 | liftoff_log_set_handler(liftoff_log_handler handler) { 25 | if (handler) { 26 | log_handler = handler; 27 | } else { 28 | log_handler = log_stderr; 29 | } 30 | } 31 | 32 | bool 33 | log_has(enum liftoff_log_priority priority) 34 | { 35 | return priority <= log_priority; 36 | } 37 | 38 | void 39 | liftoff_log(enum liftoff_log_priority priority, const char *fmt, ...) 40 | { 41 | if (!log_has(priority)) { 42 | return; 43 | } 44 | 45 | va_list args; 46 | va_start(args, fmt); 47 | log_handler(priority, fmt, args); 48 | va_end(args); 49 | } 50 | 51 | void 52 | liftoff_log_errno(enum liftoff_log_priority priority, const char *msg) 53 | { 54 | // Ensure errno is still set to its original value when we return 55 | int prev_errno = errno; 56 | liftoff_log(priority, "%s: %s", msg, strerror(errno)); 57 | errno = prev_errno; 58 | } 59 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'libliftoff', 3 | 'c', 4 | version: '0.2.0', 5 | license: 'MIT', 6 | meson_version: '>=0.52.0', 7 | default_options: [ 8 | 'c_std=c99', 9 | 'warning_level=3', 10 | 'werror=true', 11 | ], 12 | ) 13 | 14 | cc = meson.get_compiler('c') 15 | 16 | add_project_arguments(cc.get_supported_arguments([ 17 | '-Wundef', 18 | '-Wmissing-prototypes', 19 | '-Walloca', 20 | 21 | '-Wno-missing-braces', 22 | '-Wno-unused-parameter', 23 | ]), language: 'c') 24 | 25 | liftoff_inc = include_directories('include') 26 | 27 | drm = dependency('libdrm', include_type: 'system') 28 | 29 | liftoff_deps = [drm] 30 | 31 | liftoff_lib = library( 32 | 'liftoff', 33 | files( 34 | 'alloc.c', 35 | 'device.c', 36 | 'layer.c', 37 | 'list.c', 38 | 'log.c', 39 | 'output.c', 40 | 'plane.c', 41 | ), 42 | include_directories: liftoff_inc, 43 | version: meson.project_version(), 44 | dependencies: liftoff_deps, 45 | install: true, 46 | ) 47 | 48 | liftoff = declare_dependency( 49 | link_with: liftoff_lib, 50 | include_directories: liftoff_inc, 51 | dependencies: liftoff_deps, 52 | ) 53 | 54 | install_headers('include/libliftoff.h') 55 | 56 | pkgconfig = import('pkgconfig') 57 | pkgconfig.generate( 58 | liftoff_lib, 59 | version: meson.project_version(), 60 | filebase: meson.project_name(), 61 | name: meson.project_name(), 62 | description: 'KMS plane library', 63 | ) 64 | 65 | subdir('example') 66 | subdir('test') 67 | -------------------------------------------------------------------------------- /output.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "private.h" 7 | 8 | struct liftoff_output * 9 | liftoff_output_create(struct liftoff_device *device, uint32_t crtc_id) 10 | { 11 | struct liftoff_output *output; 12 | ssize_t crtc_index; 13 | size_t i; 14 | 15 | crtc_index = -1; 16 | for (i = 0; i < device->crtcs_len; i++) { 17 | if (device->crtcs[i] == crtc_id) { 18 | crtc_index = i; 19 | break; 20 | } 21 | } 22 | if (crtc_index < 0) { 23 | return NULL; 24 | } 25 | 26 | output = calloc(1, sizeof(*output)); 27 | if (output == NULL) { 28 | return NULL; 29 | } 30 | output->device = device; 31 | output->crtc_id = crtc_id; 32 | output->crtc_index = crtc_index; 33 | liftoff_list_init(&output->layers); 34 | liftoff_list_insert(&device->outputs, &output->link); 35 | return output; 36 | } 37 | 38 | void 39 | liftoff_output_destroy(struct liftoff_output *output) 40 | { 41 | if (output == NULL) { 42 | return; 43 | } 44 | 45 | liftoff_list_remove(&output->link); 46 | free(output); 47 | } 48 | 49 | void 50 | liftoff_output_set_composition_layer(struct liftoff_output *output, 51 | struct liftoff_layer *layer) 52 | { 53 | assert(layer->output == output); 54 | if (layer != output->composition_layer) { 55 | output->layers_changed = true; 56 | } 57 | output->composition_layer = layer; 58 | } 59 | 60 | bool 61 | liftoff_output_needs_composition(struct liftoff_output *output) 62 | { 63 | struct liftoff_layer *layer; 64 | 65 | liftoff_list_for_each(layer, &output->layers, link) { 66 | if (liftoff_layer_needs_composition(layer)) { 67 | return true; 68 | } 69 | } 70 | 71 | return false; 72 | } 73 | 74 | static double 75 | fp16_to_double(uint64_t val) 76 | { 77 | return (double)(val >> 16) + (double)(val & 0xFFFF) / 0xFFFF; 78 | } 79 | 80 | void 81 | output_log_layers(struct liftoff_output *output) 82 | { 83 | struct liftoff_layer *layer; 84 | size_t i; 85 | bool is_composition_layer; 86 | 87 | if (!log_has(LIFTOFF_DEBUG)) { 88 | return; 89 | } 90 | 91 | liftoff_log(LIFTOFF_DEBUG, "Layers on CRTC %"PRIu32" (%zu total):", 92 | output->crtc_id, liftoff_list_length(&output->layers)); 93 | liftoff_list_for_each(layer, &output->layers, link) { 94 | if (layer->force_composition) { 95 | liftoff_log(LIFTOFF_DEBUG, " Layer %p " 96 | "(forced composition):", (void *)layer); 97 | } else { 98 | if (!layer_has_fb(layer)) { 99 | continue; 100 | } 101 | is_composition_layer = output->composition_layer == layer; 102 | liftoff_log(LIFTOFF_DEBUG, " Layer %p%s:", 103 | (void *)layer, is_composition_layer ? 104 | " (composition layer)" : ""); 105 | } 106 | 107 | for (i = 0; i < layer->props_len; i++) { 108 | char *name = layer->props[i].name; 109 | uint64_t value = layer->props[i].value; 110 | 111 | if (strcmp(name, "CRTC_X") == 0 || 112 | strcmp(name, "CRTC_Y") == 0) { 113 | liftoff_log(LIFTOFF_DEBUG, " %s = %+"PRIi32, 114 | name, (int32_t)value); 115 | } else if (strcmp(name, "SRC_X") == 0 || 116 | strcmp(name, "SRC_Y") == 0 || 117 | strcmp(name, "SRC_W") == 0 || 118 | strcmp(name, "SRC_H") == 0) { 119 | liftoff_log(LIFTOFF_DEBUG, " %s = %f", 120 | name, fp16_to_double(value)); 121 | } else { 122 | liftoff_log(LIFTOFF_DEBUG, " %s = %"PRIu64, 123 | name, value); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /plane.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "private.h" 6 | 7 | static int 8 | guess_plane_zpos_from_type(struct liftoff_device *device, uint32_t plane_id, 9 | uint32_t type) 10 | { 11 | struct liftoff_plane *primary; 12 | 13 | /* From far to close to the eye: primary, overlay, cursor. Unless 14 | * the overlay ID < primary ID. */ 15 | switch (type) { 16 | case DRM_PLANE_TYPE_PRIMARY: 17 | return 0; 18 | case DRM_PLANE_TYPE_CURSOR: 19 | return 2; 20 | case DRM_PLANE_TYPE_OVERLAY: 21 | if (liftoff_list_empty(&device->planes)) { 22 | return 0; /* No primary plane, shouldn't happen */ 23 | } 24 | primary = liftoff_container_of(device->planes.next, 25 | primary, link); 26 | if (plane_id < primary->id) { 27 | return -1; 28 | } else { 29 | return 1; 30 | } 31 | } 32 | return 0; 33 | } 34 | 35 | struct liftoff_plane * 36 | liftoff_plane_create(struct liftoff_device *device, uint32_t id) 37 | { 38 | struct liftoff_plane *plane, *cur; 39 | drmModePlane *drm_plane; 40 | drmModeObjectProperties *drm_props; 41 | uint32_t i; 42 | drmModePropertyRes *drm_prop; 43 | struct liftoff_plane_property *prop; 44 | uint64_t value; 45 | bool has_type = false, has_zpos = false; 46 | 47 | liftoff_list_for_each(plane, &device->planes, link) { 48 | if (plane->id == id) { 49 | liftoff_log(LIFTOFF_ERROR, "tried to register plane " 50 | "%"PRIu32" twice\n", id); 51 | errno = EEXIST; 52 | return NULL; 53 | } 54 | } 55 | 56 | plane = calloc(1, sizeof(*plane)); 57 | if (plane == NULL) { 58 | liftoff_log_errno(LIFTOFF_ERROR, "calloc"); 59 | return NULL; 60 | } 61 | 62 | drm_plane = drmModeGetPlane(device->drm_fd, id); 63 | if (drm_plane == NULL) { 64 | liftoff_log_errno(LIFTOFF_ERROR, "drmModeGetPlane"); 65 | return NULL; 66 | } 67 | plane->id = drm_plane->plane_id; 68 | plane->possible_crtcs = drm_plane->possible_crtcs; 69 | drmModeFreePlane(drm_plane); 70 | 71 | drm_props = drmModeObjectGetProperties(device->drm_fd, id, 72 | DRM_MODE_OBJECT_PLANE); 73 | if (drm_props == NULL) { 74 | liftoff_log_errno(LIFTOFF_ERROR, "drmModeObjectGetProperties"); 75 | return NULL; 76 | } 77 | plane->props = calloc(drm_props->count_props, 78 | sizeof(struct liftoff_plane_property)); 79 | if (plane->props == NULL) { 80 | liftoff_log_errno(LIFTOFF_ERROR, "calloc"); 81 | drmModeFreeObjectProperties(drm_props); 82 | return NULL; 83 | } 84 | for (i = 0; i < drm_props->count_props; i++) { 85 | drm_prop = drmModeGetProperty(device->drm_fd, 86 | drm_props->props[i]); 87 | if (drm_prop == NULL) { 88 | liftoff_log_errno(LIFTOFF_ERROR, "drmModeGetProperty"); 89 | drmModeFreeObjectProperties(drm_props); 90 | return NULL; 91 | } 92 | prop = &plane->props[i]; 93 | memcpy(prop->name, drm_prop->name, sizeof(prop->name)); 94 | prop->id = drm_prop->prop_id; 95 | drmModeFreeProperty(drm_prop); 96 | plane->props_len++; 97 | 98 | value = drm_props->prop_values[i]; 99 | if (strcmp(prop->name, "type") == 0) { 100 | plane->type = value; 101 | has_type = true; 102 | } else if (strcmp(prop->name, "zpos") == 0) { 103 | plane->zpos = value; 104 | has_zpos = true; 105 | } 106 | } 107 | drmModeFreeObjectProperties(drm_props); 108 | 109 | if (!has_type) { 110 | liftoff_log(LIFTOFF_ERROR, 111 | "plane %"PRIu32" is missing the 'type' property", 112 | plane->id); 113 | free(plane); 114 | errno = EINVAL; 115 | return NULL; 116 | } else if (!has_zpos) { 117 | plane->zpos = guess_plane_zpos_from_type(device, plane->id, 118 | plane->type); 119 | } 120 | 121 | /* During plane allocation, we will use the plane list order to fill 122 | * planes with FBs. Primary planes need to be filled first, then planes 123 | * far from the primary planes, then planes closer and closer to the 124 | * primary plane. */ 125 | if (plane->type == DRM_PLANE_TYPE_PRIMARY) { 126 | liftoff_list_insert(&device->planes, &plane->link); 127 | } else { 128 | liftoff_list_for_each(cur, &device->planes, link) { 129 | if (cur->type != DRM_PLANE_TYPE_PRIMARY && 130 | plane->zpos >= cur->zpos) { 131 | liftoff_list_insert(cur->link.prev, &plane->link); 132 | break; 133 | } 134 | } 135 | 136 | if (plane->link.next == NULL) { /* not inserted */ 137 | liftoff_list_insert(device->planes.prev, &plane->link); 138 | } 139 | } 140 | 141 | return plane; 142 | } 143 | 144 | void 145 | liftoff_plane_destroy(struct liftoff_plane *plane) 146 | { 147 | if (plane->layer != NULL) { 148 | plane->layer->plane = NULL; 149 | } 150 | liftoff_list_remove(&plane->link); 151 | free(plane->props); 152 | free(plane); 153 | } 154 | 155 | uint32_t 156 | liftoff_plane_get_id(struct liftoff_plane *plane) 157 | { 158 | return plane->id; 159 | } 160 | 161 | static struct liftoff_plane_property * 162 | plane_get_property(struct liftoff_plane *plane, const char *name) 163 | { 164 | size_t i; 165 | 166 | for (i = 0; i < plane->props_len; i++) { 167 | if (strcmp(plane->props[i].name, name) == 0) { 168 | return &plane->props[i]; 169 | } 170 | } 171 | return NULL; 172 | } 173 | 174 | static int 175 | plane_set_prop(struct liftoff_plane *plane, drmModeAtomicReq *req, 176 | struct liftoff_plane_property *prop, uint64_t value) 177 | { 178 | int ret; 179 | 180 | ret = drmModeAtomicAddProperty(req, plane->id, prop->id, value); 181 | if (ret < 0) { 182 | liftoff_log(LIFTOFF_ERROR, "drmModeAtomicAddProperty: %s", 183 | strerror(-ret)); 184 | return ret; 185 | } 186 | 187 | return 0; 188 | } 189 | 190 | static int 191 | set_plane_prop_str(struct liftoff_plane *plane, drmModeAtomicReq *req, 192 | const char *name, uint64_t value) 193 | { 194 | struct liftoff_plane_property *prop; 195 | 196 | prop = plane_get_property(plane, name); 197 | if (prop == NULL) { 198 | liftoff_log(LIFTOFF_DEBUG, 199 | "plane %"PRIu32" is missing the %s property", 200 | plane->id, name); 201 | return -EINVAL; 202 | } 203 | 204 | return plane_set_prop(plane, req, prop, value); 205 | } 206 | 207 | int 208 | plane_apply(struct liftoff_plane *plane, struct liftoff_layer *layer, 209 | drmModeAtomicReq *req) 210 | { 211 | int cursor, ret; 212 | size_t i; 213 | struct liftoff_layer_property *layer_prop; 214 | struct liftoff_plane_property *plane_prop; 215 | 216 | cursor = drmModeAtomicGetCursor(req); 217 | 218 | if (layer == NULL) { 219 | ret = set_plane_prop_str(plane, req, "FB_ID", 0); 220 | if (ret != 0) { 221 | return ret; 222 | } 223 | return set_plane_prop_str(plane, req, "CRTC_ID", 0); 224 | } 225 | 226 | ret = set_plane_prop_str(plane, req, "CRTC_ID", layer->output->crtc_id); 227 | if (ret != 0) { 228 | return ret; 229 | } 230 | 231 | for (i = 0; i < layer->props_len; i++) { 232 | layer_prop = &layer->props[i]; 233 | if (strcmp(layer_prop->name, "zpos") == 0) { 234 | /* We don't yet support setting the zpos property. We 235 | * only use it (read-only) during plane allocation. */ 236 | continue; 237 | } 238 | 239 | plane_prop = plane_get_property(plane, layer_prop->name); 240 | if (plane_prop == NULL) { 241 | if (strcmp(layer_prop->name, "alpha") == 0 && 242 | layer_prop->value == 0xFFFF) { 243 | continue; /* Layer is completely opaque */ 244 | } 245 | if (strcmp(layer_prop->name, "rotation") == 0 && 246 | layer_prop->value == DRM_MODE_ROTATE_0) { 247 | continue; /* Layer isn't rotated */ 248 | } 249 | drmModeAtomicSetCursor(req, cursor); 250 | return -EINVAL; 251 | } 252 | 253 | ret = plane_set_prop(plane, req, plane_prop, layer_prop->value); 254 | if (ret != 0) { 255 | drmModeAtomicSetCursor(req, cursor); 256 | return ret; 257 | } 258 | } 259 | 260 | return 0; 261 | } 262 | -------------------------------------------------------------------------------- /test/bench.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200112L 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "libdrm_mock.h" 9 | #include "log.h" 10 | 11 | #define MAX_PLANES 128 12 | #define MAX_LAYERS 128 13 | 14 | static struct liftoff_layer * 15 | add_layer(struct liftoff_output *output, int x, int y, int width, int height) 16 | { 17 | uint32_t fb_id; 18 | struct liftoff_layer *layer; 19 | 20 | layer = liftoff_layer_create(output); 21 | fb_id = liftoff_mock_drm_create_fb(layer); 22 | liftoff_layer_set_property(layer, "FB_ID", fb_id); 23 | liftoff_layer_set_property(layer, "CRTC_X", x); 24 | liftoff_layer_set_property(layer, "CRTC_Y", y); 25 | liftoff_layer_set_property(layer, "CRTC_W", width); 26 | liftoff_layer_set_property(layer, "CRTC_H", height); 27 | liftoff_layer_set_property(layer, "SRC_X", 0); 28 | liftoff_layer_set_property(layer, "SRC_Y", 0); 29 | liftoff_layer_set_property(layer, "SRC_W", width << 16); 30 | liftoff_layer_set_property(layer, "SRC_H", height << 16); 31 | 32 | return layer; 33 | } 34 | 35 | int 36 | main(int argc, char *argv[]) 37 | { 38 | int opt; 39 | size_t planes_len, layers_len; 40 | struct timespec start, end; 41 | struct liftoff_mock_plane *mock_planes[MAX_PLANES]; 42 | size_t i, j; 43 | int plane_type; 44 | int drm_fd; 45 | struct liftoff_device *device; 46 | struct liftoff_output *output; 47 | struct liftoff_layer *layers[MAX_LAYERS]; 48 | drmModeAtomicReq *req; 49 | int ret; 50 | 51 | planes_len = 5; 52 | layers_len = 10; 53 | while ((opt = getopt(argc, argv, "p:l:")) != -1) { 54 | switch (opt) { 55 | case 'p': 56 | planes_len = atoi(optarg); 57 | break; 58 | case 'l': 59 | layers_len = atoi(optarg); 60 | break; 61 | default: 62 | fprintf(stderr, "usage: %s [-p planes] [-l layers]\n", 63 | argv[0]); 64 | exit(EXIT_FAILURE); 65 | } 66 | } 67 | 68 | liftoff_log_set_priority(LIFTOFF_SILENT); 69 | 70 | for (i = 0; i < planes_len; i++) { 71 | plane_type = i == 0 ? DRM_PLANE_TYPE_PRIMARY : 72 | DRM_PLANE_TYPE_OVERLAY; 73 | mock_planes[i] = liftoff_mock_drm_create_plane(plane_type); 74 | } 75 | 76 | drm_fd = liftoff_mock_drm_open(); 77 | device = liftoff_device_create(drm_fd); 78 | assert(device != NULL); 79 | 80 | liftoff_device_register_all_planes(device); 81 | 82 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 83 | 84 | for (i = 0; i < layers_len; i++) { 85 | /* Planes don't intersect, so the library can arrange them in 86 | * any order. Testing all combinations takes more time. */ 87 | layers[i] = add_layer(output, i * 100, i * 100, 100, 100); 88 | for (j = 0; j < planes_len; j++) { 89 | if (j == 1) { 90 | /* Make the lowest plane above the primary plane 91 | * incompatible with all layers. A solution 92 | * using all planes won't be reached, so the 93 | * library will keep trying more combinations. 94 | */ 95 | continue; 96 | } 97 | liftoff_mock_plane_add_compatible_layer(mock_planes[j], 98 | layers[i]); 99 | } 100 | } 101 | 102 | clock_gettime(CLOCK_MONOTONIC, &start); 103 | 104 | req = drmModeAtomicAlloc(); 105 | ret = liftoff_output_apply(output, req, 0); 106 | assert(ret == 0); 107 | drmModeAtomicFree(req); 108 | 109 | clock_gettime(CLOCK_MONOTONIC, &end); 110 | 111 | double dur_ms = ((double)end.tv_sec - (double)start.tv_sec) * 1000 + 112 | ((double)end.tv_nsec - (double)start.tv_nsec) / 1000000; 113 | printf("Plane allocation took %fms\n", dur_ms); 114 | printf("Plane allocation performed %zu atomic test commits\n", 115 | liftoff_mock_commit_count); 116 | /* TODO: the mock libdrm library takes time to check atomic requests. 117 | * This benchmark doesn't account for time spent in the mock library. */ 118 | printf("With 20µs per atomic test commit, plane allocation would take " 119 | "%fms\n", dur_ms + liftoff_mock_commit_count * 0.02); 120 | 121 | liftoff_device_destroy(device); 122 | close(drm_fd); 123 | } 124 | -------------------------------------------------------------------------------- /test/check_ndebug.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int 4 | main(int argc, char *argv[]) 5 | { 6 | #ifdef NDEBUG 7 | fprintf(stderr, "NDEBUG is defined, cannot run tests\n"); 8 | return 1; 9 | #else 10 | return 0; 11 | #endif 12 | } 13 | -------------------------------------------------------------------------------- /test/libdrm_mock.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "libdrm_mock.h" 11 | 12 | #define MAX_PLANES 64 13 | #define MAX_LAYERS 512 14 | #define MAX_PLANE_PROPS 64 15 | #define MAX_REQ_PROPS 1024 16 | 17 | uint32_t liftoff_mock_drm_crtc_id = 0xCC000000; 18 | size_t liftoff_mock_commit_count = 0; 19 | bool liftoff_mock_require_primary_plane = false; 20 | 21 | struct liftoff_mock_plane { 22 | uint32_t id; 23 | struct liftoff_layer *compatible_layers[MAX_LAYERS]; 24 | bool enabled_props[MAX_PLANE_PROPS]; 25 | uint64_t prop_values[MAX_PLANE_PROPS]; 26 | }; 27 | 28 | struct liftoff_mock_prop { 29 | uint32_t obj_id, prop_id; 30 | uint64_t value; 31 | }; 32 | 33 | struct _drmModeAtomicReq { 34 | struct liftoff_mock_prop props[MAX_REQ_PROPS]; 35 | int cursor; 36 | }; 37 | 38 | static int mock_pipe[2] = {-1, -1}; 39 | static struct liftoff_mock_plane mock_planes[MAX_PLANES]; 40 | static struct liftoff_layer *mock_fbs[MAX_LAYERS]; 41 | 42 | enum plane_prop { 43 | PLANE_TYPE, 44 | PLANE_FB_ID, 45 | PLANE_CRTC_ID, 46 | }; 47 | 48 | static const char *basic_plane_props[] = { 49 | [PLANE_TYPE] = "type", 50 | [PLANE_FB_ID] = "FB_ID", 51 | [PLANE_CRTC_ID] = "CRTC_ID", 52 | "CRTC_X", 53 | "CRTC_Y", 54 | "CRTC_W", 55 | "CRTC_H", 56 | "SRC_X", 57 | "SRC_Y", 58 | "SRC_W", 59 | "SRC_H", 60 | }; 61 | 62 | static const size_t basic_plane_props_len = sizeof(basic_plane_props) / 63 | sizeof(basic_plane_props[0]); 64 | 65 | static drmModePropertyRes plane_props[MAX_PLANE_PROPS] = {0}; 66 | 67 | static size_t plane_props_len = 0; 68 | 69 | static void 70 | assert_drm_fd(int fd) 71 | { 72 | int ret; 73 | struct stat stat_got, stat_want; 74 | 75 | ret = fstat(mock_pipe[0], &stat_want); 76 | assert(ret == 0); 77 | ret = fstat(fd, &stat_got); 78 | assert(ret == 0); 79 | 80 | assert(stat_got.st_dev == stat_want.st_dev && 81 | stat_got.st_ino == stat_want.st_ino); 82 | } 83 | 84 | static uint32_t 85 | register_prop(const drmModePropertyRes *prop) 86 | { 87 | drmModePropertyRes *dst; 88 | 89 | assert(plane_props_len < MAX_PLANE_PROPS); 90 | dst = &plane_props[plane_props_len]; 91 | memcpy(dst, prop, sizeof(*dst)); 92 | dst->prop_id = 0xB0000000 + plane_props_len; 93 | plane_props_len++; 94 | 95 | return dst->prop_id; 96 | } 97 | 98 | static void 99 | init_basic_props(void) 100 | { 101 | size_t i; 102 | 103 | if (plane_props_len > 0) 104 | return; 105 | 106 | for (i = 0; i < basic_plane_props_len; i++) { 107 | drmModePropertyRes prop = {0}; 108 | strncpy(prop.name, basic_plane_props[i], sizeof(prop.name) - 1); 109 | /* TODO: fill flags */ 110 | register_prop(&prop); 111 | } 112 | } 113 | 114 | int 115 | liftoff_mock_drm_open(void) 116 | { 117 | int ret; 118 | 119 | assert(mock_pipe[0] < 0); 120 | ret = pipe(mock_pipe); 121 | assert(ret == 0); 122 | 123 | init_basic_props(); 124 | 125 | return mock_pipe[0]; 126 | } 127 | 128 | struct liftoff_mock_plane * 129 | liftoff_mock_drm_create_plane(int type) 130 | { 131 | struct liftoff_mock_plane *plane; 132 | size_t i; 133 | 134 | assert(mock_pipe[0] < 0); 135 | 136 | init_basic_props(); 137 | 138 | i = 0; 139 | plane = &mock_planes[0]; 140 | while (plane->id != 0) { 141 | plane++; 142 | i++; 143 | } 144 | 145 | plane->id = 0xEE000000 + i; 146 | plane->prop_values[PLANE_TYPE] = type; 147 | 148 | for (size_t i = 0; i < basic_plane_props_len; i++) { 149 | plane->enabled_props[i] = true; 150 | } 151 | 152 | return plane; 153 | } 154 | 155 | struct liftoff_mock_plane * 156 | liftoff_mock_drm_get_plane(uint32_t id) 157 | { 158 | struct liftoff_mock_plane *plane; 159 | 160 | plane = &mock_planes[0]; 161 | while (plane->id != 0) { 162 | if (plane->id == id) { 163 | return plane; 164 | } 165 | plane++; 166 | } 167 | 168 | abort(); // unreachable 169 | } 170 | 171 | void 172 | liftoff_mock_plane_add_compatible_layer(struct liftoff_mock_plane *plane, 173 | struct liftoff_layer *layer) 174 | { 175 | size_t i; 176 | 177 | for (i = 0; i < MAX_LAYERS; i++) { 178 | if (plane->compatible_layers[i] == NULL) { 179 | plane->compatible_layers[i] = layer; 180 | return; 181 | } 182 | } 183 | 184 | abort(); // unreachable 185 | } 186 | 187 | uint32_t 188 | liftoff_mock_drm_create_fb(struct liftoff_layer *layer) 189 | { 190 | size_t i; 191 | 192 | i = 0; 193 | while (mock_fbs[i] != 0) { 194 | i++; 195 | } 196 | 197 | mock_fbs[i] = layer; 198 | 199 | return 0xFB000000 + i; 200 | } 201 | 202 | static bool 203 | mock_atomic_req_get_property(drmModeAtomicReq *req, uint32_t obj_id, 204 | enum plane_prop prop, uint64_t *value) 205 | { 206 | ssize_t i; 207 | uint32_t prop_id; 208 | 209 | prop_id = 0xB0000000 + prop; 210 | for (i = req->cursor - 1; i >= 0; i--) { 211 | if (req->props[i].obj_id == obj_id && 212 | req->props[i].prop_id == prop_id) { 213 | *value = req->props[i].value; 214 | return true; 215 | } 216 | } 217 | 218 | return false; 219 | } 220 | 221 | static struct liftoff_layer * 222 | mock_fb_get_layer(uint32_t fb_id) 223 | { 224 | size_t i; 225 | 226 | if (fb_id == 0) { 227 | return NULL; 228 | } 229 | 230 | assert((fb_id & 0xFF000000) == 0xFB000000); 231 | 232 | i = fb_id & 0x00FFFFFF; 233 | assert(i < MAX_LAYERS); 234 | 235 | return mock_fbs[i]; 236 | } 237 | 238 | struct liftoff_layer * 239 | liftoff_mock_plane_get_layer(struct liftoff_mock_plane *plane) 240 | { 241 | return mock_fb_get_layer(plane->prop_values[PLANE_FB_ID]); 242 | } 243 | 244 | static size_t 245 | get_prop_index(uint32_t id) 246 | { 247 | size_t i; 248 | 249 | assert((id & 0xFF000000) == 0xB0000000); 250 | 251 | i = id & 0x00FFFFFF; 252 | assert(i < plane_props_len); 253 | 254 | return i; 255 | } 256 | 257 | uint32_t 258 | liftoff_mock_plane_add_property(struct liftoff_mock_plane *plane, 259 | const drmModePropertyRes *prop) 260 | { 261 | uint32_t prop_id; 262 | 263 | prop_id = register_prop(prop); 264 | plane->enabled_props[get_prop_index(prop_id)] = true; 265 | if (prop->count_values == 1) { 266 | plane->prop_values[get_prop_index(prop_id)] = prop->values[0]; 267 | } 268 | return prop_id; 269 | } 270 | 271 | static void 272 | apply_atomic_req(drmModeAtomicReq *req) 273 | { 274 | int i; 275 | size_t prop_index; 276 | struct liftoff_mock_prop *prop; 277 | struct liftoff_mock_plane *plane; 278 | 279 | for (i = 0; i < req->cursor; i++) { 280 | prop = &req->props[i]; 281 | plane = liftoff_mock_drm_get_plane(prop->obj_id); 282 | prop_index = get_prop_index(prop->prop_id); 283 | plane->prop_values[prop_index] = prop->value; 284 | fprintf(stderr, "libdrm_mock: plane %"PRIu32": " 285 | "setting %s = %"PRIu64"\n", plane->id, 286 | plane_props[prop_index].name, prop->value); 287 | } 288 | } 289 | 290 | int 291 | drmModeAtomicCommit(int fd, drmModeAtomicReq *req, uint32_t flags, 292 | void *user_data) 293 | { 294 | size_t i, j; 295 | struct liftoff_mock_plane *plane; 296 | uint64_t type, fb_id, crtc_id; 297 | bool has_fb, has_crtc, found; 298 | bool any_plane_enabled, primary_plane_enabled; 299 | struct liftoff_layer *layer; 300 | 301 | assert_drm_fd(fd); 302 | assert(flags == DRM_MODE_ATOMIC_TEST_ONLY || flags == 0); 303 | 304 | liftoff_mock_commit_count++; 305 | 306 | any_plane_enabled = false; 307 | primary_plane_enabled = false; 308 | for (i = 0; i < MAX_PLANES; i++) { 309 | plane = &mock_planes[i]; 310 | if (plane->id == 0) { 311 | break; 312 | } 313 | 314 | type = plane->prop_values[PLANE_TYPE]; 315 | fb_id = plane->prop_values[PLANE_FB_ID]; 316 | crtc_id = plane->prop_values[PLANE_CRTC_ID]; 317 | mock_atomic_req_get_property(req, plane->id, PLANE_FB_ID, 318 | &fb_id); 319 | mock_atomic_req_get_property(req, plane->id, PLANE_CRTC_ID, 320 | &crtc_id); 321 | 322 | has_fb = fb_id != 0; 323 | has_crtc = crtc_id != 0; 324 | 325 | if (has_fb != has_crtc) { 326 | fprintf(stderr, "libdrm_mock: plane %u: both FB_ID and " 327 | "CRTC_ID must be set or unset together " 328 | "(FB_ID = %"PRIu64", CRTC_ID = %"PRIu64")\n", 329 | plane->id, fb_id, crtc_id); 330 | return -EINVAL; 331 | } 332 | 333 | if (has_fb) { 334 | if (crtc_id != liftoff_mock_drm_crtc_id) { 335 | fprintf(stderr, "libdrm_mock: plane %u: " 336 | "invalid CRTC_ID\n", plane->id); 337 | return -EINVAL; 338 | } 339 | layer = mock_fb_get_layer(fb_id); 340 | if (layer == NULL) { 341 | fprintf(stderr, "libdrm_mock: plane %u: " 342 | "invalid FB_ID\n", plane->id); 343 | return -EINVAL; 344 | } 345 | found = false; 346 | for (j = 0; j < MAX_LAYERS; j++) { 347 | if (plane->compatible_layers[j] == layer) { 348 | found = true; 349 | break; 350 | } 351 | } 352 | if (!found) { 353 | fprintf(stderr, "libdrm_mock: plane %u: " 354 | "layer %p is not compatible\n", 355 | plane->id, (void *)layer); 356 | return -EINVAL; 357 | } 358 | 359 | any_plane_enabled = true; 360 | if (type == DRM_PLANE_TYPE_PRIMARY) { 361 | primary_plane_enabled = true; 362 | } 363 | } 364 | } 365 | 366 | if (liftoff_mock_require_primary_plane && any_plane_enabled && 367 | !primary_plane_enabled) { 368 | fprintf(stderr, "libdrm_mock: cannot light up CRTC without " 369 | "enabling the primary plane\n"); 370 | return -EINVAL; 371 | } 372 | 373 | if (!(flags & DRM_MODE_ATOMIC_TEST_ONLY)) { 374 | apply_atomic_req(req); 375 | } 376 | 377 | return 0; 378 | } 379 | 380 | drmModeRes * 381 | drmModeGetResources(int fd) 382 | { 383 | drmModeRes *res; 384 | 385 | assert_drm_fd(fd); 386 | 387 | res = calloc(1, sizeof(*res)); 388 | res->count_crtcs = 1; 389 | res->crtcs = &liftoff_mock_drm_crtc_id; 390 | return res; 391 | } 392 | 393 | void 394 | drmModeFreeResources(drmModeRes *res) 395 | { 396 | free(res); 397 | } 398 | 399 | drmModePlaneRes * 400 | drmModeGetPlaneResources(int fd) 401 | { 402 | static uint32_t plane_ids[MAX_PLANES]; 403 | drmModePlaneRes *res; 404 | size_t i; 405 | 406 | assert_drm_fd(fd); 407 | 408 | for (i = 0; i < MAX_PLANES; i++) { 409 | if (mock_planes[i].id == 0) { 410 | break; 411 | } 412 | plane_ids[i] = mock_planes[i].id; 413 | } 414 | 415 | res = calloc(1, sizeof(*res)); 416 | res->count_planes = i; 417 | res->planes = plane_ids; 418 | return res; 419 | } 420 | 421 | void 422 | drmModeFreePlaneResources(drmModePlaneRes *res) 423 | { 424 | free(res); 425 | } 426 | 427 | drmModePlane * 428 | drmModeGetPlane(int fd, uint32_t id) 429 | { 430 | drmModePlane *plane; 431 | 432 | assert_drm_fd(fd); 433 | 434 | plane = calloc(1, sizeof(*plane)); 435 | plane->plane_id = id; 436 | plane->possible_crtcs = 1 << 0; 437 | return plane; 438 | } 439 | 440 | void 441 | drmModeFreePlane(drmModePlane *plane) { 442 | free(plane); 443 | } 444 | 445 | drmModeObjectProperties * 446 | drmModeObjectGetProperties(int fd, uint32_t obj_id, uint32_t obj_type) 447 | { 448 | struct liftoff_mock_plane *plane; 449 | drmModeObjectProperties *props; 450 | size_t i; 451 | 452 | assert_drm_fd(fd); 453 | assert(obj_type == DRM_MODE_OBJECT_PLANE); 454 | 455 | plane = NULL; 456 | for (i = 0; i < MAX_PLANES; i++) { 457 | if (mock_planes[i].id == obj_id) { 458 | plane = &mock_planes[i]; 459 | break; 460 | } 461 | } 462 | assert(plane != NULL); 463 | 464 | props = calloc(1, sizeof(*props)); 465 | props->props = calloc(plane_props_len, sizeof(uint32_t)); 466 | props->prop_values = calloc(plane_props_len, sizeof(uint64_t)); 467 | for (i = 0; i < plane_props_len; i++) { 468 | if (!plane->enabled_props[i]) 469 | continue; 470 | props->props[props->count_props] = plane_props[i].prop_id; 471 | props->prop_values[props->count_props] = plane->prop_values[i]; 472 | props->count_props++; 473 | } 474 | return props; 475 | } 476 | 477 | void 478 | drmModeFreeObjectProperties(drmModeObjectProperties *props) { 479 | free(props->props); 480 | free(props->prop_values); 481 | free(props); 482 | } 483 | 484 | drmModePropertyRes * 485 | drmModeGetProperty(int fd, uint32_t id) 486 | { 487 | assert_drm_fd(fd); 488 | 489 | return &plane_props[get_prop_index(id)]; 490 | } 491 | 492 | void 493 | drmModeFreeProperty(drmModePropertyRes *prop) { 494 | /* Owned by plane_props */ 495 | } 496 | 497 | drmModeAtomicReq * 498 | drmModeAtomicAlloc(void) 499 | { 500 | return calloc(1, sizeof(drmModeAtomicReq)); 501 | } 502 | 503 | void 504 | drmModeAtomicFree(drmModeAtomicReq *req) 505 | { 506 | free(req); 507 | } 508 | 509 | int 510 | drmModeAtomicAddProperty(drmModeAtomicReq *req, uint32_t obj_id, 511 | uint32_t prop_id, uint64_t value) 512 | { 513 | assert((size_t)req->cursor < sizeof(req->props) / sizeof(req->props[0])); 514 | req->props[req->cursor].obj_id = obj_id; 515 | req->props[req->cursor].prop_id = prop_id; 516 | req->props[req->cursor].value = value; 517 | req->cursor++; 518 | return req->cursor; 519 | } 520 | 521 | int 522 | drmModeAtomicGetCursor(drmModeAtomicReq *req) 523 | { 524 | return req->cursor; 525 | } 526 | 527 | void 528 | drmModeAtomicSetCursor(drmModeAtomicReq *req, int cursor) 529 | { 530 | req->cursor = cursor; 531 | } 532 | -------------------------------------------------------------------------------- /test/libdrm_mock.h: -------------------------------------------------------------------------------- 1 | #ifndef LIFTOFF_LIBDRM_MOCK_H 2 | #define LIFTOFF_LIBDRM_MOCK_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | extern uint32_t liftoff_mock_drm_crtc_id; 9 | extern size_t liftoff_mock_commit_count; 10 | 11 | /** 12 | * Some drivers require the primary plane to be enabled in order to light up a 13 | * CRTC (e.g. i915). If this variable is set to true, this behavior is mimicked. 14 | */ 15 | extern bool liftoff_mock_require_primary_plane; 16 | 17 | struct liftoff_layer; 18 | 19 | int 20 | liftoff_mock_drm_open(void); 21 | 22 | uint32_t 23 | liftoff_mock_drm_create_fb(struct liftoff_layer *layer); 24 | 25 | struct liftoff_mock_plane * 26 | liftoff_mock_drm_create_plane(int type); 27 | 28 | struct liftoff_mock_plane * 29 | liftoff_mock_drm_get_plane(uint32_t id); 30 | 31 | void 32 | liftoff_mock_plane_add_compatible_layer(struct liftoff_mock_plane *plane, 33 | struct liftoff_layer *layer); 34 | struct liftoff_layer * 35 | liftoff_mock_plane_get_layer(struct liftoff_mock_plane *plane); 36 | 37 | uint32_t 38 | liftoff_mock_plane_add_property(struct liftoff_mock_plane *plane, 39 | const drmModePropertyRes *prop); 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /test/meson.build: -------------------------------------------------------------------------------- 1 | # This mock library will replace libdrm 2 | mock_drm_lib = shared_library( 3 | 'drm', 4 | files('libdrm_mock.c'), 5 | dependencies: drm.partial_dependency(compile_args: true), 6 | soversion: drm.version().split('.')[0], # TODO: get it from the real dep 7 | ) 8 | 9 | mock_liftoff = declare_dependency( 10 | link_with: [mock_drm_lib, liftoff_lib], 11 | include_directories: [liftoff_inc], 12 | dependencies: drm.partial_dependency(compile_args: true), 13 | ) 14 | 15 | test('check_ndebug', executable('check_ndebug', 'check_ndebug.c')) 16 | 17 | bench_exe = executable( 18 | 'bench', 19 | files('bench.c'), 20 | dependencies: mock_liftoff, 21 | ) 22 | 23 | tests = { 24 | 'alloc': [ 25 | 'basic', 26 | 'no-props-fail', 27 | 'zero-fb-id-fail', 28 | 'composition-zero-fb-id', 29 | 'empty', 30 | 'simple-1x', 31 | 'simple-1x-fail', 32 | 'simple-3x', 33 | 'zpos-3x', 34 | 'zpos-3x-intersect-fail', 35 | 'zpos-3x-intersect-partial', 36 | 'zpos-3x-disjoint-partial', 37 | 'zpos-3x-disjoint', 38 | 'zpos-4x-intersect-partial', 39 | 'zpos-4x-disjoint', 40 | 'zpos-4x-disjoint-alt', 41 | 'zpos-4x-domino-fail', 42 | 'zpos-4x-domino-partial', 43 | 'composition-3x', 44 | 'composition-3x-fail', 45 | 'composition-3x-partial', 46 | 'composition-3x-force', 47 | ], 48 | 'dynamic': [ 49 | 'same', 50 | 'change-fb', 51 | 'unset-fb', 52 | 'set-fb', 53 | 'add-layer', 54 | 'remove-layer', 55 | 'change-composition-layer', 56 | 'change-alpha', 57 | 'set-alpha-from-opaque', 58 | 'set-alpha-from-transparent', 59 | 'unset-alpha-to-opaque', 60 | 'unset-alpha-to-transparent', 61 | 'change-in-fence-fd', 62 | 'change-fb-damage-clips', 63 | ], 64 | 'priority': [ 65 | #'basic', 66 | ], 67 | 'prop': [ 68 | 'default-alpha', 69 | 'default-rotation', 70 | 'ignore-alpha', 71 | 'immutable-zpos', 72 | 'unmatched', 73 | ], 74 | } 75 | 76 | foreach test_name, subtests : tests 77 | test_exe = executable( 78 | 'test-' + test_name, 79 | files('test_' + test_name + '.c'), 80 | dependencies: mock_liftoff, 81 | ) 82 | foreach subtest_name : subtests 83 | test( 84 | test_name + '@' + subtest_name, 85 | test_exe, 86 | args: [subtest_name], 87 | ) 88 | endforeach 89 | endforeach 90 | -------------------------------------------------------------------------------- /test/test_alloc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "libdrm_mock.h" 8 | 9 | static struct liftoff_layer * 10 | add_layer(struct liftoff_output *output, int x, int y, int width, int height) 11 | { 12 | uint32_t fb_id; 13 | struct liftoff_layer *layer; 14 | 15 | layer = liftoff_layer_create(output); 16 | fb_id = liftoff_mock_drm_create_fb(layer); 17 | liftoff_layer_set_property(layer, "FB_ID", fb_id); 18 | liftoff_layer_set_property(layer, "CRTC_X", x); 19 | liftoff_layer_set_property(layer, "CRTC_Y", y); 20 | liftoff_layer_set_property(layer, "CRTC_W", width); 21 | liftoff_layer_set_property(layer, "CRTC_H", height); 22 | liftoff_layer_set_property(layer, "SRC_X", 0); 23 | liftoff_layer_set_property(layer, "SRC_Y", 0); 24 | liftoff_layer_set_property(layer, "SRC_W", width << 16); 25 | liftoff_layer_set_property(layer, "SRC_H", height << 16); 26 | 27 | return layer; 28 | } 29 | 30 | struct test_plane { 31 | int type; 32 | }; 33 | 34 | /* This structure describes a layer in a test case. The first block of fields 35 | * describe the layer properties: geometry, vertical ordering, etc. The `compat` 36 | * field describes which hardware planes the layer is compatible with. The 37 | * `result` field describes the expected hardware plane returned by libliftoff. 38 | */ 39 | struct test_layer { 40 | int x, y, width, height; 41 | int zpos; /* zero means unset */ 42 | bool composition; 43 | bool force_composited; 44 | 45 | struct test_plane *compat[64]; 46 | struct test_plane *result; 47 | }; 48 | 49 | struct test_case { 50 | const char *name; 51 | struct test_layer layers[64]; 52 | }; 53 | 54 | /* This array describes the hardware we're going to perform the tests with. Our 55 | * hardware has one primary plane at the bottom position, two overlay planes 56 | * at the middle position (with undefined ordering between themselves), and one 57 | * cursor plane at the top. 58 | */ 59 | static struct test_plane test_setup[] = { 60 | { .type = DRM_PLANE_TYPE_PRIMARY }, /* zpos = 0 */ 61 | { .type = DRM_PLANE_TYPE_CURSOR }, /* zpos = 2 */ 62 | { .type = DRM_PLANE_TYPE_OVERLAY }, /* zpos = 1 */ 63 | { .type = DRM_PLANE_TYPE_OVERLAY }, /* zpos = 1 */ 64 | }; 65 | 66 | static const size_t test_setup_len = sizeof(test_setup) / sizeof(test_setup[0]); 67 | 68 | #define PRIMARY_PLANE &test_setup[0] 69 | #define CURSOR_PLANE &test_setup[1] 70 | #define OVERLAY_PLANE &test_setup[2] 71 | 72 | /* non-primary planes */ 73 | #define FIRST_2_SECONDARY_PLANES { &test_setup[1], &test_setup[2] } 74 | #define FIRST_3_SECONDARY_PLANES { &test_setup[1], &test_setup[2], \ 75 | &test_setup[3] } 76 | 77 | static struct test_case tests[] = { 78 | { 79 | .name = "empty", 80 | }, 81 | { 82 | .name = "simple-1x-fail", 83 | .layers = { 84 | { 85 | .width = 1920, 86 | .height = 1080, 87 | .compat = { NULL }, 88 | .result = NULL, 89 | }, 90 | }, 91 | }, 92 | { 93 | .name = "simple-1x", 94 | .layers = { 95 | { 96 | .width = 1920, 97 | .height = 1080, 98 | .compat = { PRIMARY_PLANE }, 99 | .result = PRIMARY_PLANE, 100 | }, 101 | }, 102 | }, 103 | { 104 | .name = "simple-3x", 105 | .layers = { 106 | { 107 | .width = 1920, 108 | .height = 1080, 109 | .compat = { PRIMARY_PLANE }, 110 | .result = PRIMARY_PLANE, 111 | }, 112 | { 113 | .width = 100, 114 | .height = 100, 115 | .compat = { CURSOR_PLANE }, 116 | .result = CURSOR_PLANE, 117 | }, 118 | { 119 | .width = 100, 120 | .height = 100, 121 | .compat = { OVERLAY_PLANE }, 122 | .result = OVERLAY_PLANE, 123 | }, 124 | }, 125 | }, 126 | { 127 | .name = "zpos-3x", 128 | .layers = { 129 | { 130 | .width = 1920, 131 | .height = 1080, 132 | .zpos = 1, 133 | .compat = { PRIMARY_PLANE }, 134 | .result = PRIMARY_PLANE, 135 | }, 136 | { 137 | .width = 100, 138 | .height = 100, 139 | .zpos = 2, 140 | .compat = FIRST_2_SECONDARY_PLANES, 141 | .result = OVERLAY_PLANE, 142 | }, 143 | { 144 | .width = 100, 145 | .height = 100, 146 | .zpos = 3, 147 | .compat = FIRST_2_SECONDARY_PLANES, 148 | .result = CURSOR_PLANE, 149 | }, 150 | }, 151 | }, 152 | { 153 | .name = "zpos-3x-intersect-fail", 154 | /* Layer 1 is over layer 2 but falls back to composition. Since 155 | * they intersect, layer 2 needs to be composited too. */ 156 | .layers = { 157 | { 158 | .width = 1920, 159 | .height = 1080, 160 | .zpos = 1, 161 | .compat = { PRIMARY_PLANE }, 162 | .result = PRIMARY_PLANE, 163 | }, 164 | { 165 | .width = 100, 166 | .height = 100, 167 | .zpos = 3, 168 | .compat = { NULL }, 169 | .result = NULL, 170 | }, 171 | { 172 | .width = 100, 173 | .height = 100, 174 | .zpos = 2, 175 | .compat = FIRST_2_SECONDARY_PLANES, 176 | .result = NULL, 177 | }, 178 | }, 179 | }, 180 | { 181 | .name = "zpos-3x-intersect-partial", 182 | /* Layer 1 is only compatible with the cursor plane. Layer 2 is 183 | * only compatible with the overlay plane. Layer 2 is over layer 184 | * 1, but the cursor plane is over the overlay plane. There is a 185 | * zpos conflict, only one of these two layers can be mapped to 186 | * a plane. */ 187 | .layers = { 188 | { 189 | .width = 1920, 190 | .height = 1080, 191 | .zpos = 1, 192 | .compat = { PRIMARY_PLANE }, 193 | .result = PRIMARY_PLANE, 194 | }, 195 | { 196 | .width = 100, 197 | .height = 100, 198 | .zpos = 2, 199 | .compat = { CURSOR_PLANE }, 200 | .result = NULL, 201 | }, 202 | { 203 | .width = 100, 204 | .height = 100, 205 | .zpos = 3, 206 | .compat = { OVERLAY_PLANE }, 207 | .result = OVERLAY_PLANE, 208 | }, 209 | }, 210 | }, 211 | { 212 | .name = "zpos-3x-disjoint-partial", 213 | /* Layer 1 is over layer 2 and falls back to composition. Since 214 | * they don't intersect, layer 2 can be mapped to a plane. */ 215 | .layers = { 216 | { 217 | .width = 1920, 218 | .height = 1080, 219 | .zpos = 1, 220 | .compat = { PRIMARY_PLANE }, 221 | .result = PRIMARY_PLANE, 222 | }, 223 | { 224 | .width = 100, 225 | .height = 100, 226 | .zpos = 3, 227 | .compat = { NULL }, 228 | .result = NULL, 229 | }, 230 | { 231 | .x = 100, 232 | .y = 100, 233 | .width = 100, 234 | .height = 100, 235 | .zpos = 2, 236 | .compat = { CURSOR_PLANE }, 237 | .result = CURSOR_PLANE, 238 | }, 239 | }, 240 | }, 241 | { 242 | .name = "zpos-3x-disjoint", 243 | /* Layer 1 is only compatible with the cursor plane. Layer 2 is 244 | * only compatible with the overlay plane. Layer 2 is over layer 245 | * 1, but the cursor plane is over the overlay plane. There is a 246 | * zpos conflict, however since these two layers don't 247 | * intersect, we can still map them to planes. */ 248 | .layers = { 249 | { 250 | .width = 1920, 251 | .height = 1080, 252 | .zpos = 1, 253 | .compat = { PRIMARY_PLANE }, 254 | .result = PRIMARY_PLANE, 255 | }, 256 | { 257 | .width = 100, 258 | .height = 100, 259 | .zpos = 2, 260 | .compat = { CURSOR_PLANE }, 261 | .result = CURSOR_PLANE, 262 | }, 263 | { 264 | .x = 100, 265 | .y = 100, 266 | .width = 100, 267 | .height = 100, 268 | .zpos = 3, 269 | .compat = { OVERLAY_PLANE }, 270 | .result = OVERLAY_PLANE, 271 | }, 272 | }, 273 | }, 274 | { 275 | .name = "zpos-4x-intersect-partial", 276 | /* We have 4 layers and 4 planes. However since they all 277 | * intersect and the ordering between both overlay planes is 278 | * undefined, we can only use 3 planes. */ 279 | .layers = { 280 | { 281 | .width = 1920, 282 | .height = 1080, 283 | .zpos = 1, 284 | .compat = { PRIMARY_PLANE }, 285 | .result = PRIMARY_PLANE, 286 | }, 287 | { 288 | .width = 100, 289 | .height = 100, 290 | .zpos = 4, 291 | .compat = FIRST_3_SECONDARY_PLANES, 292 | .result = CURSOR_PLANE, 293 | }, 294 | { 295 | .width = 100, 296 | .height = 100, 297 | .zpos = 2, 298 | .compat = FIRST_3_SECONDARY_PLANES, 299 | .result = NULL, 300 | }, 301 | { 302 | .width = 100, 303 | .height = 100, 304 | .zpos = 3, 305 | .compat = FIRST_3_SECONDARY_PLANES, 306 | .result = &test_setup[3], 307 | }, 308 | }, 309 | }, 310 | { 311 | .name = "zpos-4x-disjoint", 312 | /* Ordering between the two overlay planes isn't defined, 313 | * however layers 2 and 3 don't intersect so they can be mapped 314 | * to these planes nonetheless. */ 315 | .layers = { 316 | { 317 | .width = 1920, 318 | .height = 1080, 319 | .zpos = 1, 320 | .compat = { PRIMARY_PLANE }, 321 | .result = PRIMARY_PLANE, 322 | }, 323 | { 324 | .width = 100, 325 | .height = 100, 326 | .zpos = 4, 327 | .compat = FIRST_3_SECONDARY_PLANES, 328 | .result = CURSOR_PLANE, 329 | }, 330 | { 331 | .width = 100, 332 | .height = 100, 333 | .zpos = 2, 334 | .compat = { &test_setup[3] }, 335 | .result = &test_setup[3], 336 | }, 337 | { 338 | .x = 100, 339 | .y = 100, 340 | .width = 100, 341 | .height = 100, 342 | .zpos = 3, 343 | .compat = { OVERLAY_PLANE }, 344 | .result = OVERLAY_PLANE, 345 | }, 346 | }, 347 | }, 348 | { 349 | .name = "zpos-4x-disjoint-alt", 350 | /* Same as zpos-4x-disjoint, but with the last two layers' 351 | * plane swapped. */ 352 | .layers = { 353 | { 354 | .width = 1920, 355 | .height = 1080, 356 | .zpos = 1, 357 | .compat = { PRIMARY_PLANE }, 358 | .result = PRIMARY_PLANE, 359 | }, 360 | { 361 | .width = 100, 362 | .height = 100, 363 | .zpos = 4, 364 | .compat = FIRST_3_SECONDARY_PLANES, 365 | .result = CURSOR_PLANE, 366 | }, 367 | { 368 | .width = 100, 369 | .height = 100, 370 | .zpos = 2, 371 | .compat = { OVERLAY_PLANE }, 372 | .result = OVERLAY_PLANE, 373 | }, 374 | { 375 | .x = 100, 376 | .y = 100, 377 | .width = 100, 378 | .height = 100, 379 | .zpos = 3, 380 | .compat = { &test_setup[3] }, 381 | .result = &test_setup[3], 382 | }, 383 | }, 384 | }, 385 | { 386 | .name = "zpos-4x-domino-fail", 387 | /* A layer on top falls back to composition. There is a layer at 388 | * zpos=2 which doesn't overlap and could be mapped to a plane, 389 | * however another layer at zpos=3 overlaps both and prevents 390 | * all layers from being mapped to a plane. */ 391 | .layers = { 392 | { 393 | .width = 1920, 394 | .height = 1080, 395 | .zpos = 1, 396 | .compat = { PRIMARY_PLANE }, 397 | .result = PRIMARY_PLANE, 398 | }, 399 | { 400 | .width = 100, 401 | .height = 100, 402 | .zpos = 4, 403 | .compat = { NULL }, 404 | .result = NULL, 405 | }, 406 | { 407 | .x = 100, 408 | .y = 100, 409 | .width = 100, 410 | .height = 100, 411 | .zpos = 2, 412 | .compat = FIRST_3_SECONDARY_PLANES, 413 | .result = NULL, 414 | }, 415 | { 416 | .x = 50, 417 | .y = 50, 418 | .width = 100, 419 | .height = 100, 420 | .zpos = 3, 421 | .compat = FIRST_3_SECONDARY_PLANES, 422 | .result = NULL, 423 | }, 424 | }, 425 | }, 426 | { 427 | .name = "zpos-4x-domino-partial", 428 | /* A layer on top falls back to composition. A layer at zpos=2 429 | * falls back to composition too because it's underneath. A 430 | * layer at zpos=3 doesn't intersect with the one at zpos=4 and 431 | * is over the one at zpos=2 so it can be mapped to a plane. */ 432 | .layers = { 433 | { 434 | .width = 1920, 435 | .height = 1080, 436 | .zpos = 1, 437 | .compat = { PRIMARY_PLANE }, 438 | .result = PRIMARY_PLANE, 439 | }, 440 | { 441 | .width = 100, 442 | .height = 100, 443 | .zpos = 4, 444 | .compat = { NULL }, 445 | .result = NULL, 446 | }, 447 | { 448 | .x = 100, 449 | .y = 100, 450 | .width = 100, 451 | .height = 100, 452 | .zpos = 3, 453 | .compat = FIRST_3_SECONDARY_PLANES, 454 | .result = CURSOR_PLANE, 455 | }, 456 | { 457 | .x = 50, 458 | .y = 50, 459 | .width = 100, 460 | .height = 100, 461 | .zpos = 2, 462 | .compat = FIRST_3_SECONDARY_PLANES, 463 | .result = NULL, 464 | }, 465 | }, 466 | }, 467 | { 468 | .name = "composition-3x", 469 | .layers = { 470 | { 471 | .width = 1920, 472 | .height = 1080, 473 | .zpos = 1, 474 | .composition = true, 475 | .compat = { PRIMARY_PLANE }, 476 | .result = NULL, 477 | }, 478 | { 479 | .width = 1920, 480 | .height = 1080, 481 | .zpos = 1, 482 | .compat = { PRIMARY_PLANE }, 483 | .result = PRIMARY_PLANE, 484 | }, 485 | { 486 | .width = 100, 487 | .height = 100, 488 | .zpos = 2, 489 | .compat = FIRST_2_SECONDARY_PLANES, 490 | .result = OVERLAY_PLANE, 491 | }, 492 | { 493 | .width = 100, 494 | .height = 100, 495 | .zpos = 3, 496 | .compat = FIRST_2_SECONDARY_PLANES, 497 | .result = CURSOR_PLANE, 498 | }, 499 | }, 500 | }, 501 | { 502 | .name = "composition-3x-fail", 503 | .layers = { 504 | { 505 | .width = 1920, 506 | .height = 1080, 507 | .zpos = 1, 508 | .composition = true, 509 | .compat = { PRIMARY_PLANE }, 510 | .result = PRIMARY_PLANE, 511 | }, 512 | { 513 | .width = 1920, 514 | .height = 1080, 515 | .zpos = 1, 516 | .compat = { PRIMARY_PLANE }, 517 | .result = NULL, 518 | }, 519 | { 520 | .width = 100, 521 | .height = 100, 522 | .zpos = 2, 523 | .compat = FIRST_2_SECONDARY_PLANES, 524 | .result = NULL, 525 | }, 526 | { 527 | .width = 100, 528 | .height = 100, 529 | .zpos = 3, 530 | .compat = { 0 }, 531 | .result = NULL, 532 | }, 533 | }, 534 | }, 535 | { 536 | .name = "composition-3x-partial", 537 | .layers = { 538 | { 539 | .width = 1920, 540 | .height = 1080, 541 | .zpos = 1, 542 | .composition = true, 543 | .compat = { PRIMARY_PLANE }, 544 | .result = PRIMARY_PLANE, 545 | }, 546 | { 547 | .width = 1920, 548 | .height = 1080, 549 | .zpos = 1, 550 | .compat = { PRIMARY_PLANE }, 551 | .result = NULL, 552 | }, 553 | { 554 | .width = 100, 555 | .height = 100, 556 | .zpos = 2, 557 | .compat = { 0 }, 558 | .result = NULL, 559 | }, 560 | { 561 | .width = 100, 562 | .height = 100, 563 | .zpos = 3, 564 | .compat = { CURSOR_PLANE }, 565 | .result = CURSOR_PLANE, 566 | }, 567 | }, 568 | }, 569 | { 570 | .name = "composition-3x-force", 571 | /* Layers at zpos=1 and zpos=2 could be put on a plane, but 572 | * FB composition is forced on the zpos=2 one. As a result, only 573 | * the layer at zpos=3 can be put into a plane. */ 574 | .layers = { 575 | { 576 | .width = 1920, 577 | .height = 1080, 578 | .zpos = 1, 579 | .composition = true, 580 | .compat = { PRIMARY_PLANE }, 581 | .result = PRIMARY_PLANE, 582 | }, 583 | { 584 | .width = 1920, 585 | .height = 1080, 586 | .zpos = 1, 587 | .compat = { PRIMARY_PLANE }, 588 | .result = NULL, 589 | }, 590 | { 591 | .width = 100, 592 | .height = 100, 593 | .zpos = 2, 594 | .force_composited = true, 595 | .compat = FIRST_2_SECONDARY_PLANES, 596 | .result = NULL, 597 | }, 598 | { 599 | .width = 100, 600 | .height = 100, 601 | .zpos = 3, 602 | .compat = { CURSOR_PLANE }, 603 | .result = CURSOR_PLANE, 604 | }, 605 | }, 606 | }, 607 | }; 608 | 609 | static bool 610 | test_needs_composition(struct test_layer *test_layers) 611 | { 612 | size_t i; 613 | 614 | for (i = 0; test_layers[i].width > 0; i++) { 615 | if (test_layers[i].result == NULL) { 616 | return true; 617 | } 618 | } 619 | 620 | return false; 621 | } 622 | 623 | static void 624 | run_test(struct test_layer *test_layers) 625 | { 626 | size_t i, j; 627 | ssize_t plane_index_got, plane_index_want; 628 | struct liftoff_mock_plane *mock_planes[64]; 629 | struct liftoff_mock_plane *mock_plane; 630 | struct test_layer *test_layer; 631 | int drm_fd; 632 | struct liftoff_device *device; 633 | struct liftoff_output *output; 634 | struct liftoff_layer *layers[64]; 635 | struct liftoff_plane *plane; 636 | drmModeAtomicReq *req; 637 | bool ok; 638 | int ret; 639 | uint32_t plane_id; 640 | 641 | liftoff_mock_require_primary_plane = true; 642 | 643 | for (i = 0; i < test_setup_len; i++) { 644 | mock_planes[i] = liftoff_mock_drm_create_plane(test_setup[i].type); 645 | } 646 | 647 | drm_fd = liftoff_mock_drm_open(); 648 | device = liftoff_device_create(drm_fd); 649 | assert(device != NULL); 650 | 651 | liftoff_device_register_all_planes(device); 652 | 653 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 654 | for (i = 0; test_layers[i].width > 0; i++) { 655 | test_layer = &test_layers[i]; 656 | layers[i] = add_layer(output, test_layer->x, test_layer->y, 657 | test_layer->width, test_layer->height); 658 | if (test_layer->zpos != 0) { 659 | liftoff_layer_set_property(layers[i], "zpos", 660 | test_layer->zpos); 661 | } 662 | if (test_layer->composition) { 663 | liftoff_output_set_composition_layer(output, layers[i]); 664 | } 665 | if (test_layer->force_composited) { 666 | liftoff_layer_set_fb_composited(layers[i]); 667 | } 668 | for (j = 0; test_layer->compat[j] != NULL; j++) { 669 | mock_plane = mock_planes[test_layer->compat[j] - 670 | test_setup]; 671 | liftoff_mock_plane_add_compatible_layer(mock_plane, 672 | layers[i]); 673 | } 674 | } 675 | 676 | req = drmModeAtomicAlloc(); 677 | ret = liftoff_output_apply(output, req, 0); 678 | assert(ret == 0); 679 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 680 | assert(ret == 0); 681 | drmModeAtomicFree(req); 682 | 683 | ok = true; 684 | for (i = 0; test_layers[i].width > 0; i++) { 685 | plane = liftoff_layer_get_plane(layers[i]); 686 | mock_plane = NULL; 687 | if (plane != NULL) { 688 | plane_id = liftoff_plane_get_id(plane); 689 | mock_plane = liftoff_mock_drm_get_plane(plane_id); 690 | } 691 | plane_index_got = -1; 692 | for (j = 0; j < test_setup_len; j++) { 693 | if (mock_planes[j] == mock_plane) { 694 | plane_index_got = j; 695 | break; 696 | } 697 | } 698 | assert(mock_plane == NULL || plane_index_got >= 0); 699 | 700 | fprintf(stderr, "layer %zu got assigned to plane %d\n", 701 | i, (int)plane_index_got); 702 | 703 | plane_index_want = -1; 704 | if (test_layers[i].result != NULL) { 705 | plane_index_want = test_layers[i].result - test_setup; 706 | } 707 | 708 | if (plane_index_got != plane_index_want) { 709 | fprintf(stderr, " ERROR: want plane %d\n", 710 | (int)plane_index_want); 711 | ok = false; 712 | } 713 | } 714 | assert(ok); 715 | 716 | assert(test_needs_composition(test_layers) == 717 | liftoff_output_needs_composition(output)); 718 | 719 | liftoff_output_destroy(output); 720 | liftoff_device_destroy(device); 721 | close(drm_fd); 722 | } 723 | 724 | static void 725 | test_basic(void) 726 | { 727 | struct liftoff_mock_plane *mock_plane; 728 | int drm_fd; 729 | struct liftoff_device *device; 730 | struct liftoff_output *output; 731 | struct liftoff_layer *layer; 732 | drmModeAtomicReq *req; 733 | int ret; 734 | 735 | mock_plane = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_PRIMARY); 736 | 737 | drm_fd = liftoff_mock_drm_open(); 738 | device = liftoff_device_create(drm_fd); 739 | assert(device != NULL); 740 | 741 | liftoff_device_register_all_planes(device); 742 | 743 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 744 | layer = add_layer(output, 0, 0, 1920, 1080); 745 | 746 | liftoff_mock_plane_add_compatible_layer(mock_plane, layer); 747 | 748 | req = drmModeAtomicAlloc(); 749 | ret = liftoff_output_apply(output, req, 0); 750 | assert(ret == 0); 751 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 752 | assert(ret == 0); 753 | assert(liftoff_mock_plane_get_layer(mock_plane) == layer); 754 | drmModeAtomicFree(req); 755 | 756 | liftoff_device_destroy(device); 757 | close(drm_fd); 758 | } 759 | 760 | /* Checks that the library doesn't allocate a plane for a layer without a 761 | * non-zero FB_ID set. */ 762 | static void 763 | test_no_fb_fail(bool zero_fb_id) 764 | { 765 | struct liftoff_mock_plane *mock_plane; 766 | int drm_fd; 767 | struct liftoff_device *device; 768 | struct liftoff_output *output; 769 | struct liftoff_layer *layer; 770 | drmModeAtomicReq *req; 771 | int ret; 772 | 773 | mock_plane = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_PRIMARY); 774 | 775 | drm_fd = liftoff_mock_drm_open(); 776 | device = liftoff_device_create(drm_fd); 777 | assert(device != NULL); 778 | 779 | liftoff_device_register_all_planes(device); 780 | 781 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 782 | layer = liftoff_layer_create(output); 783 | if (zero_fb_id) { 784 | liftoff_layer_set_property(layer, "FB_ID", 0); 785 | } 786 | 787 | liftoff_mock_plane_add_compatible_layer(mock_plane, layer); 788 | 789 | req = drmModeAtomicAlloc(); 790 | ret = liftoff_output_apply(output, req, 0); 791 | assert(ret == 0); 792 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 793 | assert(ret == 0); 794 | assert(liftoff_mock_plane_get_layer(mock_plane) == NULL); 795 | drmModeAtomicFree(req); 796 | 797 | liftoff_device_destroy(device); 798 | close(drm_fd); 799 | } 800 | 801 | /* Checks that the library doesn't fallback to composition when a layer doesn't 802 | * have a FB. */ 803 | static void 804 | test_composition_zero_fb(void) 805 | { 806 | struct liftoff_mock_plane *mock_plane; 807 | int drm_fd; 808 | struct liftoff_device *device; 809 | struct liftoff_output *output; 810 | struct liftoff_layer *composition_layer, *layer_with_fb, 811 | *layer_without_fb; 812 | drmModeAtomicReq *req; 813 | int ret; 814 | 815 | mock_plane = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_PRIMARY); 816 | 817 | drm_fd = liftoff_mock_drm_open(); 818 | device = liftoff_device_create(drm_fd); 819 | assert(device != NULL); 820 | 821 | liftoff_device_register_all_planes(device); 822 | 823 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 824 | composition_layer = add_layer(output, 0, 0, 1920, 1080); 825 | layer_with_fb = add_layer(output, 0, 0, 1920, 1080); 826 | layer_without_fb = liftoff_layer_create(output); 827 | (void)layer_with_fb; 828 | 829 | liftoff_output_set_composition_layer(output, composition_layer); 830 | 831 | liftoff_mock_plane_add_compatible_layer(mock_plane, composition_layer); 832 | liftoff_mock_plane_add_compatible_layer(mock_plane, layer_without_fb); 833 | liftoff_mock_plane_add_compatible_layer(mock_plane, layer_with_fb); 834 | 835 | req = drmModeAtomicAlloc(); 836 | ret = liftoff_output_apply(output, req, 0); 837 | assert(ret == 0); 838 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 839 | assert(ret == 0); 840 | assert(liftoff_mock_plane_get_layer(mock_plane) == layer_with_fb); 841 | drmModeAtomicFree(req); 842 | 843 | liftoff_device_destroy(device); 844 | close(drm_fd); 845 | } 846 | 847 | int 848 | main(int argc, char *argv[]) 849 | { 850 | const char *test_name; 851 | size_t i; 852 | 853 | liftoff_log_set_priority(LIFTOFF_DEBUG); 854 | 855 | if (argc != 2) { 856 | fprintf(stderr, "usage: %s \n", argv[0]); 857 | return 1; 858 | } 859 | test_name = argv[1]; 860 | 861 | if (strcmp(test_name, "basic") == 0) { 862 | test_basic(); 863 | return 0; 864 | } else if (strcmp(test_name, "no-props-fail") == 0) { 865 | test_no_fb_fail(false); 866 | return 0; 867 | } else if (strcmp(test_name, "zero-fb-id-fail") == 0) { 868 | test_no_fb_fail(true); 869 | return 0; 870 | } else if (strcmp(test_name, "composition-zero-fb-id") == 0) { 871 | test_composition_zero_fb(); 872 | return 0; 873 | } 874 | 875 | for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { 876 | if (strcmp(tests[i].name, test_name) == 0) { 877 | run_test(tests[i].layers); 878 | return 0; 879 | } 880 | } 881 | 882 | fprintf(stderr, "no such test: %s\n", test_name); 883 | return 1; 884 | } 885 | -------------------------------------------------------------------------------- /test/test_dynamic.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "libdrm_mock.h" 8 | 9 | struct context { 10 | int drm_fd; 11 | struct liftoff_output *output; 12 | struct liftoff_mock_plane *mock_plane; 13 | struct liftoff_layer *layer, *other_layer; 14 | size_t commit_count; 15 | }; 16 | 17 | struct test_case { 18 | const char *name; 19 | void (*run)(struct context *ctx); 20 | }; 21 | 22 | static struct liftoff_layer * 23 | add_layer(struct liftoff_output *output, int x, int y, int width, int height) 24 | { 25 | uint32_t fb_id; 26 | struct liftoff_layer *layer; 27 | 28 | layer = liftoff_layer_create(output); 29 | fb_id = liftoff_mock_drm_create_fb(layer); 30 | liftoff_layer_set_property(layer, "FB_ID", fb_id); 31 | liftoff_layer_set_property(layer, "CRTC_X", x); 32 | liftoff_layer_set_property(layer, "CRTC_Y", y); 33 | liftoff_layer_set_property(layer, "CRTC_W", width); 34 | liftoff_layer_set_property(layer, "CRTC_H", height); 35 | liftoff_layer_set_property(layer, "SRC_X", 0); 36 | liftoff_layer_set_property(layer, "SRC_Y", 0); 37 | liftoff_layer_set_property(layer, "SRC_W", width << 16); 38 | liftoff_layer_set_property(layer, "SRC_H", height << 16); 39 | 40 | return layer; 41 | } 42 | 43 | static void 44 | first_commit(struct context *ctx) 45 | { 46 | drmModeAtomicReq *req; 47 | int ret; 48 | 49 | assert(ctx->commit_count == 0); 50 | 51 | req = drmModeAtomicAlloc(); 52 | ret = liftoff_output_apply(ctx->output, req, 0); 53 | assert(ret == 0); 54 | ret = drmModeAtomicCommit(ctx->drm_fd, req, 0, NULL); 55 | assert(ret == 0); 56 | drmModeAtomicFree(req); 57 | 58 | ctx->commit_count = liftoff_mock_commit_count; 59 | /* We need to check whether the library can re-use old configurations 60 | * with a single atomic commit. If we don't have enough planes/layers, 61 | * the library will find a plane allocation in a single commit and we 62 | * won't be able to tell the difference between a re-use and a complete 63 | * run. */ 64 | assert(ctx->commit_count > 1); 65 | } 66 | 67 | static void 68 | second_commit(struct context *ctx, bool want_reuse_prev_alloc) 69 | { 70 | drmModeAtomicReq *req; 71 | int ret; 72 | 73 | req = drmModeAtomicAlloc(); 74 | ret = liftoff_output_apply(ctx->output, req, 0); 75 | assert(ret == 0); 76 | if (want_reuse_prev_alloc) { 77 | /* The library should perform only one TEST_ONLY commit with the 78 | * previous plane allocation. */ 79 | assert(liftoff_mock_commit_count == ctx->commit_count + 1); 80 | } else { 81 | /* Since there are at least two planes, the library should 82 | * perform more than one TEST_ONLY commit. */ 83 | assert(liftoff_mock_commit_count > ctx->commit_count + 1); 84 | } 85 | ret = drmModeAtomicCommit(ctx->drm_fd, req, 0, NULL); 86 | assert(ret == 0); 87 | drmModeAtomicFree(req); 88 | } 89 | 90 | static void 91 | run_same(struct context *ctx) 92 | { 93 | first_commit(ctx); 94 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 95 | 96 | second_commit(ctx, true); 97 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 98 | } 99 | 100 | static void 101 | run_change_fb(struct context *ctx) 102 | { 103 | first_commit(ctx); 104 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 105 | 106 | liftoff_layer_set_property(ctx->layer, "FB_ID", 107 | liftoff_mock_drm_create_fb(ctx->layer)); 108 | 109 | second_commit(ctx, true); 110 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 111 | } 112 | 113 | static void 114 | run_unset_fb(struct context *ctx) 115 | { 116 | first_commit(ctx); 117 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 118 | 119 | liftoff_layer_set_property(ctx->layer, "FB_ID", 0); 120 | 121 | second_commit(ctx, false); 122 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == NULL); 123 | } 124 | 125 | static void 126 | run_set_fb(struct context *ctx) 127 | { 128 | liftoff_layer_set_property(ctx->layer, "FB_ID", 0); 129 | first_commit(ctx); 130 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == NULL); 131 | 132 | liftoff_layer_set_property(ctx->layer, "FB_ID", 133 | liftoff_mock_drm_create_fb(ctx->layer)); 134 | 135 | second_commit(ctx, false); 136 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 137 | } 138 | 139 | static void 140 | run_add_layer(struct context *ctx) 141 | { 142 | first_commit(ctx); 143 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 144 | 145 | add_layer(ctx->output, 0, 0, 256, 256); 146 | 147 | second_commit(ctx, false); 148 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 149 | } 150 | 151 | static void 152 | run_remove_layer(struct context *ctx) 153 | { 154 | first_commit(ctx); 155 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 156 | 157 | liftoff_layer_destroy(ctx->other_layer); 158 | ctx->other_layer = NULL; 159 | 160 | second_commit(ctx, false); 161 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 162 | } 163 | 164 | static void 165 | run_change_composition_layer(struct context *ctx) 166 | { 167 | first_commit(ctx); 168 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 169 | 170 | liftoff_output_set_composition_layer(ctx->output, ctx->layer); 171 | 172 | second_commit(ctx, false); 173 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 174 | } 175 | 176 | static void 177 | run_change_alpha(struct context *ctx) 178 | { 179 | liftoff_layer_set_property(ctx->layer, "alpha", 42); 180 | 181 | first_commit(ctx); 182 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 183 | 184 | liftoff_layer_set_property(ctx->layer, "alpha", 43); 185 | 186 | second_commit(ctx, true); 187 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 188 | } 189 | 190 | static void 191 | run_set_alpha_from_opaque(struct context *ctx) 192 | { 193 | liftoff_layer_set_property(ctx->layer, "alpha", 0xFFFF); /* opaque */ 194 | 195 | first_commit(ctx); 196 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 197 | 198 | liftoff_layer_set_property(ctx->layer, "alpha", 42); 199 | 200 | second_commit(ctx, false); 201 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 202 | } 203 | 204 | static void 205 | run_set_alpha_from_transparent(struct context *ctx) 206 | { 207 | liftoff_layer_set_property(ctx->layer, "alpha", 0); /* transparent */ 208 | 209 | first_commit(ctx); 210 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == NULL); 211 | 212 | liftoff_layer_set_property(ctx->layer, "alpha", 42); 213 | 214 | second_commit(ctx, false); 215 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 216 | } 217 | 218 | static void 219 | run_unset_alpha_to_opaque(struct context *ctx) 220 | { 221 | liftoff_layer_set_property(ctx->layer, "alpha", 42); 222 | 223 | first_commit(ctx); 224 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 225 | 226 | liftoff_layer_set_property(ctx->layer, "alpha", 0xFFFF); /* opaque */ 227 | 228 | second_commit(ctx, false); 229 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 230 | } 231 | 232 | static void 233 | run_unset_alpha_to_transparent(struct context *ctx) 234 | { 235 | liftoff_layer_set_property(ctx->layer, "alpha", 42); 236 | 237 | first_commit(ctx); 238 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 239 | 240 | liftoff_layer_set_property(ctx->layer, "alpha", 0); /* transparent */ 241 | 242 | second_commit(ctx, false); 243 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == NULL); 244 | } 245 | 246 | static void 247 | run_change_in_fence_fd(struct context *ctx) 248 | { 249 | liftoff_layer_set_property(ctx->layer, "IN_FENCE_FD", 42); 250 | 251 | first_commit(ctx); 252 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 253 | 254 | liftoff_layer_set_property(ctx->layer, "IN_FENCE_FD", 43); 255 | 256 | second_commit(ctx, true); 257 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 258 | } 259 | 260 | static void 261 | run_change_fb_damage_clips(struct context *ctx) 262 | { 263 | liftoff_layer_set_property(ctx->layer, "FB_DAMAGE_CLIPS", 42); 264 | 265 | first_commit(ctx); 266 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 267 | 268 | liftoff_layer_set_property(ctx->layer, "FB_DAMAGE_CLIPS", 43); 269 | 270 | second_commit(ctx, true); 271 | assert(liftoff_mock_plane_get_layer(ctx->mock_plane) == ctx->layer); 272 | } 273 | 274 | static const struct test_case tests[] = { 275 | { .name = "same", .run = run_same }, 276 | { .name = "change-fb", .run = run_change_fb }, 277 | { .name = "unset-fb", .run = run_unset_fb }, 278 | { .name = "set-fb", .run = run_set_fb }, 279 | { .name = "add-layer", .run = run_add_layer }, 280 | { .name = "remove-layer", .run = run_remove_layer }, 281 | { .name = "change-composition-layer", .run = run_change_composition_layer }, 282 | { .name = "change-alpha", .run = run_change_alpha }, 283 | { .name = "set-alpha-from-opaque", .run = run_set_alpha_from_opaque }, 284 | { .name = "set-alpha-from-transparent", .run = run_set_alpha_from_transparent }, 285 | { .name = "unset-alpha-to-opaque", .run = run_unset_alpha_to_opaque }, 286 | { .name = "unset-alpha-to-transparent", .run = run_unset_alpha_to_transparent }, 287 | { .name = "change-in-fence-fd", .run = run_change_in_fence_fd }, 288 | { .name = "change-fb-damage-clips", .run = run_change_fb_damage_clips }, 289 | }; 290 | 291 | static void 292 | run(const struct test_case *test) 293 | { 294 | struct context ctx = {0}; 295 | struct liftoff_device *device; 296 | const char *prop_name; 297 | drmModePropertyRes prop; 298 | 299 | /* Always create two planes: a primary plane only compatible with 300 | * `layer`, and a cursor plane incompatible with any layer. Always 301 | * create 3 layers: `layer`, `other_layer`, and an unnamed third layer. 302 | */ 303 | 304 | ctx.mock_plane = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_PRIMARY); 305 | /* Plane incompatible with all layers */ 306 | liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_CURSOR); 307 | 308 | prop_name = "alpha"; 309 | prop = (drmModePropertyRes){0}; 310 | strncpy(prop.name, prop_name, sizeof(prop.name) - 1); 311 | liftoff_mock_plane_add_property(ctx.mock_plane, &prop); 312 | 313 | prop_name = "IN_FENCE_FD"; 314 | prop = (drmModePropertyRes){0}; 315 | strncpy(prop.name, prop_name, sizeof(prop.name) - 1); 316 | liftoff_mock_plane_add_property(ctx.mock_plane, &prop); 317 | 318 | prop_name = "FB_DAMAGE_CLIPS"; 319 | prop = (drmModePropertyRes){0}; 320 | strncpy(prop.name, prop_name, sizeof(prop.name) - 1); 321 | liftoff_mock_plane_add_property(ctx.mock_plane, &prop); 322 | 323 | ctx.drm_fd = liftoff_mock_drm_open(); 324 | device = liftoff_device_create(ctx.drm_fd); 325 | assert(device != NULL); 326 | 327 | liftoff_device_register_all_planes(device); 328 | 329 | ctx.output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 330 | ctx.layer = add_layer(ctx.output, 0, 0, 1920, 1080); 331 | /* Layers incompatible with all planes */ 332 | ctx.other_layer = add_layer(ctx.output, 0, 0, 256, 256); 333 | add_layer(ctx.output, 0, 0, 256, 256); 334 | 335 | liftoff_mock_plane_add_compatible_layer(ctx.mock_plane, ctx.layer); 336 | 337 | test->run(&ctx); 338 | 339 | liftoff_device_destroy(device); 340 | close(ctx.drm_fd); 341 | } 342 | 343 | int 344 | main(int argc, char *argv[]) 345 | { 346 | const char *test_name; 347 | 348 | liftoff_log_set_priority(LIFTOFF_DEBUG); 349 | 350 | if (argc != 2) { 351 | fprintf(stderr, "usage: %s \n", argv[0]); 352 | return 1; 353 | } 354 | test_name = argv[1]; 355 | 356 | for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { 357 | if (strcmp(test_name, tests[i].name) == 0) { 358 | run(&tests[i]); 359 | return 0; 360 | } 361 | } 362 | 363 | fprintf(stderr, "no such test: %s\n", test_name); 364 | return 1; 365 | } 366 | -------------------------------------------------------------------------------- /test/test_priority.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "libdrm_mock.h" 7 | 8 | /* Number of page-flips before the plane allocation has stabilized */ 9 | #define STABILIZE_PAGEFLIP_COUNT 600 /* 10s at 60FPS */ 10 | 11 | static struct liftoff_layer * 12 | add_layer(struct liftoff_output *output, int x, int y, int width, int height) 13 | { 14 | uint32_t fb_id; 15 | struct liftoff_layer *layer; 16 | 17 | layer = liftoff_layer_create(output); 18 | fb_id = liftoff_mock_drm_create_fb(layer); 19 | liftoff_layer_set_property(layer, "FB_ID", fb_id); 20 | liftoff_layer_set_property(layer, "CRTC_X", x); 21 | liftoff_layer_set_property(layer, "CRTC_Y", y); 22 | liftoff_layer_set_property(layer, "CRTC_W", width); 23 | liftoff_layer_set_property(layer, "CRTC_H", height); 24 | liftoff_layer_set_property(layer, "SRC_X", 0); 25 | liftoff_layer_set_property(layer, "SRC_Y", 0); 26 | liftoff_layer_set_property(layer, "SRC_W", width << 16); 27 | liftoff_layer_set_property(layer, "SRC_H", height << 16); 28 | 29 | return layer; 30 | } 31 | 32 | int 33 | main(int argc, char *argv[]) 34 | { 35 | struct liftoff_mock_plane *mock_plane; 36 | int drm_fd; 37 | struct liftoff_device *device; 38 | struct liftoff_output *output; 39 | struct liftoff_layer *layers[2], *layer; 40 | uint32_t fbs[2]; 41 | drmModeAtomicReq *req; 42 | int ret; 43 | 44 | liftoff_log_set_priority(LIFTOFF_SILENT); 45 | 46 | mock_plane = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_PRIMARY); 47 | /* Plane incompatible with all layers */ 48 | liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_CURSOR); 49 | 50 | drm_fd = liftoff_mock_drm_open(); 51 | device = liftoff_device_create(drm_fd); 52 | assert(device != NULL); 53 | 54 | liftoff_device_register_all_planes(device); 55 | 56 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 57 | layers[0] = add_layer(output, 0, 0, 1920, 1080); 58 | layers[1] = add_layer(output, 0, 0, 1920, 1080); 59 | 60 | /* All layers are compatible with the primary plane */ 61 | liftoff_mock_plane_add_compatible_layer(mock_plane, layers[0]); 62 | liftoff_mock_plane_add_compatible_layer(mock_plane, layers[1]); 63 | 64 | for (size_t i = 0; i < sizeof(layers) / sizeof(layers[0]); i++) { 65 | /* We will continuously update layers[i]. After some time we 66 | * want to see it get a plane. */ 67 | layer = layers[i]; 68 | fprintf(stderr, "Testing layer %zu\n", i); 69 | 70 | fbs[0] = liftoff_mock_drm_create_fb(layer); 71 | fbs[1] = liftoff_mock_drm_create_fb(layer); 72 | 73 | req = drmModeAtomicAlloc(); 74 | for (int j = 0; j < STABILIZE_PAGEFLIP_COUNT; j++) { 75 | drmModeAtomicSetCursor(req, 0); 76 | 77 | liftoff_layer_set_property(layer, "FB_ID", fbs[j % 2]); 78 | 79 | ret = liftoff_output_apply(output, req, 0); 80 | assert(ret == 0); 81 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 82 | assert(ret == 0); 83 | } 84 | 85 | assert(liftoff_mock_plane_get_layer(mock_plane) == layer); 86 | 87 | drmModeAtomicFree(req); 88 | } 89 | 90 | liftoff_device_destroy(device); 91 | close(drm_fd); 92 | 93 | return 0; 94 | } 95 | -------------------------------------------------------------------------------- /test/test_prop.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "libdrm_mock.h" 7 | 8 | static struct liftoff_layer * 9 | add_layer(struct liftoff_output *output, int x, int y, int width, int height) 10 | { 11 | uint32_t fb_id; 12 | struct liftoff_layer *layer; 13 | 14 | layer = liftoff_layer_create(output); 15 | fb_id = liftoff_mock_drm_create_fb(layer); 16 | liftoff_layer_set_property(layer, "FB_ID", fb_id); 17 | liftoff_layer_set_property(layer, "CRTC_X", x); 18 | liftoff_layer_set_property(layer, "CRTC_Y", y); 19 | liftoff_layer_set_property(layer, "CRTC_W", width); 20 | liftoff_layer_set_property(layer, "CRTC_H", height); 21 | liftoff_layer_set_property(layer, "SRC_X", 0); 22 | liftoff_layer_set_property(layer, "SRC_Y", 0); 23 | liftoff_layer_set_property(layer, "SRC_W", width << 16); 24 | liftoff_layer_set_property(layer, "SRC_H", height << 16); 25 | 26 | return layer; 27 | } 28 | 29 | static int 30 | test_prop_default(const char *prop_name) 31 | { 32 | struct liftoff_mock_plane *mock_plane_with_prop, 33 | *mock_plane_without_prop; 34 | int drm_fd; 35 | struct liftoff_device *device; 36 | struct liftoff_output *output; 37 | struct liftoff_layer *layer; 38 | drmModeAtomicReq *req; 39 | int ret; 40 | 41 | mock_plane_without_prop = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_OVERLAY); 42 | mock_plane_with_prop = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_OVERLAY); 43 | 44 | /* Value that requires the prop for the plane allocation to succeed */ 45 | uint64_t require_prop_value; 46 | /* Value that doesn't require the prop to be present */ 47 | uint64_t default_value; 48 | if (strcmp(prop_name, "alpha") == 0) { 49 | require_prop_value = (uint16_t)(0.5 * 0xFFFF); 50 | default_value = 0xFFFF; /* opaque */ 51 | } else if (strcmp(prop_name, "rotation") == 0) { 52 | require_prop_value = DRM_MODE_ROTATE_180; 53 | default_value = DRM_MODE_ROTATE_0; 54 | } else { 55 | fprintf(stderr, "no such test: default-%s\n", prop_name); 56 | return 1; 57 | } 58 | 59 | /* We need to setup mock plane properties before creating the liftoff 60 | * device */ 61 | drmModePropertyRes prop = {0}; 62 | strncpy(prop.name, prop_name, sizeof(prop.name) - 1); 63 | liftoff_mock_plane_add_property(mock_plane_with_prop, &prop); 64 | 65 | drm_fd = liftoff_mock_drm_open(); 66 | device = liftoff_device_create(drm_fd); 67 | assert(device != NULL); 68 | 69 | liftoff_device_register_all_planes(device); 70 | 71 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 72 | layer = add_layer(output, 0, 0, 1920, 1080); 73 | 74 | liftoff_mock_plane_add_compatible_layer(mock_plane_without_prop, layer); 75 | 76 | /* First test that the layer doesn't get assigned to the plane without 77 | * the prop when using a non-default value */ 78 | 79 | req = drmModeAtomicAlloc(); 80 | 81 | liftoff_layer_set_property(layer, prop.name, require_prop_value); 82 | 83 | ret = liftoff_output_apply(output, req, 0); 84 | assert(ret == 0); 85 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 86 | assert(ret == 0); 87 | assert(liftoff_layer_get_plane(layer) == NULL); 88 | drmModeAtomicFree(req); 89 | 90 | /* The layer should get assigned to the plane without the prop when 91 | * using the default value */ 92 | 93 | req = drmModeAtomicAlloc(); 94 | 95 | liftoff_layer_set_property(layer, prop.name, default_value); 96 | 97 | ret = liftoff_output_apply(output, req, 0); 98 | assert(ret == 0); 99 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 100 | assert(ret == 0); 101 | assert(liftoff_layer_get_plane(layer) != NULL); 102 | drmModeAtomicFree(req); 103 | 104 | /* The layer should get assigned to the plane with the prop when using 105 | * a non-default value */ 106 | 107 | liftoff_mock_plane_add_compatible_layer(mock_plane_with_prop, layer); 108 | 109 | req = drmModeAtomicAlloc(); 110 | 111 | liftoff_layer_set_property(layer, prop.name, require_prop_value); 112 | 113 | ret = liftoff_output_apply(output, req, 0); 114 | assert(ret == 0); 115 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 116 | assert(ret == 0); 117 | assert(liftoff_layer_get_plane(layer) != NULL); 118 | drmModeAtomicFree(req); 119 | 120 | liftoff_device_destroy(device); 121 | close(drm_fd); 122 | 123 | return 0; 124 | } 125 | 126 | /* Checks that a fully transparent layer is ignored. */ 127 | static int 128 | test_ignore_alpha(void) 129 | { 130 | struct liftoff_mock_plane *mock_plane; 131 | drmModePropertyRes prop = {0}; 132 | int drm_fd; 133 | struct liftoff_device *device; 134 | struct liftoff_output *output; 135 | struct liftoff_layer *layer; 136 | drmModeAtomicReq *req; 137 | int ret; 138 | 139 | mock_plane = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_PRIMARY); 140 | 141 | strncpy(prop.name, "alpha", sizeof(prop.name) - 1); 142 | liftoff_mock_plane_add_property(mock_plane, &prop); 143 | 144 | drm_fd = liftoff_mock_drm_open(); 145 | device = liftoff_device_create(drm_fd); 146 | assert(device != NULL); 147 | 148 | liftoff_device_register_all_planes(device); 149 | 150 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 151 | layer = add_layer(output, 0, 0, 1920, 1080); 152 | liftoff_layer_set_property(layer, "alpha", 0); /* fully transparent */ 153 | 154 | liftoff_mock_plane_add_compatible_layer(mock_plane, layer); 155 | 156 | req = drmModeAtomicAlloc(); 157 | ret = liftoff_output_apply(output, req, 0); 158 | assert(ret == 0); 159 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 160 | assert(ret == 0); 161 | assert(liftoff_mock_plane_get_layer(mock_plane) == NULL); 162 | assert(!liftoff_layer_needs_composition(layer)); 163 | drmModeAtomicFree(req); 164 | 165 | liftoff_device_destroy(device); 166 | close(drm_fd); 167 | 168 | return 0; 169 | } 170 | 171 | static int 172 | test_immutable_zpos(void) 173 | { 174 | struct liftoff_mock_plane *mock_plane1, *mock_plane2; 175 | drmModePropertyRes prop = {0}; 176 | uint64_t prop_value; 177 | int drm_fd; 178 | struct liftoff_device *device; 179 | struct liftoff_output *output; 180 | struct liftoff_layer *layer1, *layer2; 181 | drmModeAtomicReq *req; 182 | int ret; 183 | 184 | mock_plane1 = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_OVERLAY); 185 | mock_plane2 = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_OVERLAY); 186 | 187 | strncpy(prop.name, "zpos", sizeof(prop.name) - 1); 188 | prop.flags = DRM_MODE_PROP_IMMUTABLE; 189 | prop.count_values = 1; 190 | prop.values = &prop_value; 191 | 192 | /* Plane 2 is always on top of plane 1, and this is immutable */ 193 | prop_value = 1; 194 | liftoff_mock_plane_add_property(mock_plane1, &prop); 195 | prop_value = 2; 196 | liftoff_mock_plane_add_property(mock_plane2, &prop); 197 | 198 | drm_fd = liftoff_mock_drm_open(); 199 | device = liftoff_device_create(drm_fd); 200 | assert(device != NULL); 201 | 202 | liftoff_device_register_all_planes(device); 203 | 204 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 205 | layer1 = add_layer(output, 0, 0, 256, 256); 206 | layer2 = add_layer(output, 128, 128, 256, 256); 207 | 208 | /* All layers are compatible with all planes */ 209 | liftoff_mock_plane_add_compatible_layer(mock_plane1, layer1); 210 | liftoff_mock_plane_add_compatible_layer(mock_plane1, layer2); 211 | liftoff_mock_plane_add_compatible_layer(mock_plane2, layer1); 212 | liftoff_mock_plane_add_compatible_layer(mock_plane2, layer2); 213 | 214 | /* Layer 2 on top of layer 1 */ 215 | liftoff_layer_set_property(layer1, "zpos", 42); 216 | liftoff_layer_set_property(layer2, "zpos", 43); 217 | 218 | req = drmModeAtomicAlloc(); 219 | ret = liftoff_output_apply(output, req, 0); 220 | assert(ret == 0); 221 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 222 | assert(ret == 0); 223 | assert(liftoff_mock_plane_get_layer(mock_plane1) == layer1); 224 | assert(liftoff_mock_plane_get_layer(mock_plane2) == layer2); 225 | drmModeAtomicFree(req); 226 | 227 | /* Layer 1 on top of layer 2 */ 228 | liftoff_layer_set_property(layer1, "zpos", 43); 229 | liftoff_layer_set_property(layer2, "zpos", 42); 230 | 231 | req = drmModeAtomicAlloc(); 232 | ret = liftoff_output_apply(output, req, 0); 233 | assert(ret == 0); 234 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 235 | assert(ret == 0); 236 | assert(liftoff_mock_plane_get_layer(mock_plane1) == layer2); 237 | assert(liftoff_mock_plane_get_layer(mock_plane2) == layer1); 238 | drmModeAtomicFree(req); 239 | 240 | liftoff_device_destroy(device); 241 | close(drm_fd); 242 | 243 | return 0; 244 | } 245 | 246 | static int 247 | test_unmatched_prop(void) 248 | { 249 | struct liftoff_mock_plane *mock_plane; 250 | int drm_fd; 251 | struct liftoff_device *device; 252 | struct liftoff_output *output; 253 | struct liftoff_layer *layer; 254 | drmModeAtomicReq *req; 255 | int ret; 256 | 257 | mock_plane = liftoff_mock_drm_create_plane(DRM_PLANE_TYPE_PRIMARY); 258 | 259 | drm_fd = liftoff_mock_drm_open(); 260 | device = liftoff_device_create(drm_fd); 261 | assert(device != NULL); 262 | 263 | liftoff_device_register_all_planes(device); 264 | 265 | output = liftoff_output_create(device, liftoff_mock_drm_crtc_id); 266 | layer = add_layer(output, 0, 0, 1920, 1080); 267 | liftoff_layer_set_property(layer, "asdf", 0); /* doesn't exist */ 268 | 269 | liftoff_mock_plane_add_compatible_layer(mock_plane, layer); 270 | 271 | req = drmModeAtomicAlloc(); 272 | ret = liftoff_output_apply(output, req, 0); 273 | assert(ret == 0); 274 | ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); 275 | assert(ret == 0); 276 | assert(liftoff_mock_plane_get_layer(mock_plane) == NULL); 277 | drmModeAtomicFree(req); 278 | 279 | liftoff_device_destroy(device); 280 | close(drm_fd); 281 | 282 | return 0; 283 | } 284 | 285 | int 286 | main(int argc, char *argv[]) 287 | { 288 | const char *test_name; 289 | 290 | liftoff_log_set_priority(LIFTOFF_DEBUG); 291 | 292 | if (argc != 2) { 293 | fprintf(stderr, "usage: %s \n", argv[0]); 294 | return 1; 295 | } 296 | test_name = argv[1]; 297 | 298 | const char default_test_prefix[] = "default-"; 299 | if (strncmp(test_name, default_test_prefix, 300 | strlen(default_test_prefix)) == 0) { 301 | return test_prop_default(test_name + strlen(default_test_prefix)); 302 | } else if (strcmp(test_name, "ignore-alpha") == 0) { 303 | return test_ignore_alpha(); 304 | } else if (strcmp(test_name, "immutable-zpos") == 0) { 305 | return test_immutable_zpos(); 306 | } else if (strcmp(test_name, "unmatched") == 0) { 307 | return test_unmatched_prop(); 308 | } else { 309 | fprintf(stderr, "no such test: %s\n", test_name); 310 | return 1; 311 | } 312 | } 313 | --------------------------------------------------------------------------------