/* * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. * * Authors: * Imre Deak */ #include "config.h" #include "igt.h" #include #include #include #include #include #include #include #include "intel_bufmgr.h" #define MAX_CONNECTORS 10 #define MAX_CRTCS 6 /* max combinations with repetitions */ #define MAX_COMBINATION_ELEMS MAX_CRTCS static int drm_fd; static drmModeRes *drm_resources; static int filter_test_id; static bool dry_run; const drmModeModeInfo mode_640_480 = { .name = "640x480", .vrefresh = 60, .clock = 25200, .hdisplay = 640, .hsync_start = 656, .hsync_end = 752, .htotal = 800, .vdisplay = 480, .vsync_start = 490, .vsync_end = 492, .vtotal = 525, .flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC, }; enum test_flags { TEST_INVALID = 0x01, TEST_CLONE = 0x02, TEST_SINGLE_CRTC_CLONE = 0x04, TEST_EXCLUSIVE_CRTC_CLONE = 0x08, TEST_STEALING = 0x10, TEST_TIMINGS = 0x20, }; struct test_config { const char *name; enum test_flags flags; drmModeRes *resources; }; struct connector_config { drmModeConnector *connector; int crtc_idx; drmModeModeInfo default_mode; }; struct crtc_config { int crtc_idx; int crtc_id; int pipe_id; int connector_count; struct connector_config *cconfs; struct igt_fb fb_info; drmModeModeInfo mode; }; static bool drm_mode_equal(drmModeModeInfo *m1, drmModeModeInfo *m2) { #define COMP(x) do { if (m1->x != m2->x) return false; } while (0) COMP(vrefresh); COMP(clock); COMP(hdisplay); COMP(hsync_start); COMP(hsync_end); COMP(htotal); COMP(vdisplay); COMP(vsync_start); COMP(vsync_end); COMP(vtotal); COMP(flags); return true; } static bool connector_supports_mode(drmModeConnector *connector, drmModeModeInfo *mode) { int i; for (i = 0; i < connector->count_modes; i++) if (drm_mode_equal(&connector->modes[i], mode)) return true; return false; } static bool crtc_supports_mode(struct crtc_config *crtc, drmModeModeInfo *mode) { int i; for (i = 0; i < crtc->connector_count; i++) { if (!connector_supports_mode(crtc->cconfs[i].connector, mode)) return false; } return true; } static int paint_fb(struct igt_fb *fb, const char *test_name, const char **crtc_str, int crtc_count, int current_crtc_idx) { double x, y; cairo_t *cr; int i; cr = igt_get_cairo_ctx(drm_fd, fb); cairo_move_to(cr, fb->width / 2, fb->height / 2); cairo_set_font_size(cr, 24); igt_cairo_printf_line(cr, align_hcenter, 40, "%s", test_name); cairo_get_current_point(cr, &x, &y); cairo_move_to(cr, 60, y); for (i = 0; i < crtc_count; i++) { if (i == current_crtc_idx) { cairo_get_current_point(cr, &x, &y); cairo_move_to(cr, x - 20, y); igt_cairo_printf_line(cr, align_right, 20, "X"); cairo_move_to(cr, x, y); } igt_cairo_printf_line(cr, align_left, 20, "%s", crtc_str[i]); } igt_put_cairo_ctx(drm_fd, fb, cr); return 0; } static void create_fb_for_crtc(struct crtc_config *crtc, struct igt_fb *fb_info) { int bpp; int depth; int fb_id; bpp = 32; depth = 24; fb_id = igt_create_pattern_fb(drm_fd, crtc->mode.hdisplay, crtc->mode.vdisplay, igt_bpp_depth_to_drm_format(bpp, depth), LOCAL_DRM_FORMAT_MOD_NONE, fb_info); igt_assert_lt(0, fb_id); } static void get_mode_for_crtc(struct crtc_config *crtc, drmModeModeInfo *mode_ret) { drmModeModeInfo mode; int i; /* * First try to select a default mode that is supported by all * connectors. */ for (i = 0; i < crtc->connector_count; i++) { mode = crtc->cconfs[i].default_mode; if (crtc_supports_mode(crtc, &mode)) goto found; } /* * Then just fall back to find any that is supported by all * connectors. */ for (i = 0; i < crtc->cconfs[0].connector->count_modes; i++) { mode = crtc->cconfs[0].connector->modes[i]; if (crtc_supports_mode(crtc, &mode)) goto found; } /* * If none is found then just pick the default mode of the first * connector and hope the other connectors can support it by scaling * etc. */ mode = crtc->cconfs[0].default_mode; found: *mode_ret = mode; } static int get_encoder_idx(drmModeRes *resources, drmModeEncoder *encoder) { int i; for (i = 0; i < resources->count_encoders; i++) if (resources->encoders[i] == encoder->encoder_id) return i; igt_assert(0); } static void get_crtc_config_str(struct crtc_config *crtc, char *buf, size_t buf_size) { int pos; int i; pos = snprintf(buf, buf_size, "CRTC[%d] [Pipe %s] Mode: %s@%dHz Connectors: ", crtc->crtc_id, kmstest_pipe_name(crtc->pipe_id), crtc->mode.name, crtc->mode.vrefresh); if (pos > buf_size) return; for (i = 0; i < crtc->connector_count; i++) { drmModeConnector *connector = crtc->cconfs[i].connector; pos += snprintf(&buf[pos], buf_size - pos, "%s%s-%d[%d]", i ? ", " : "", kmstest_connector_type_str(connector->connector_type), connector->connector_type_id, connector->connector_id); if (pos > buf_size) return; } } static void setup_crtcs(drmModeRes *resources, struct connector_config *cconf, int connector_count, struct crtc_config *crtcs, int *crtc_count_ret, bool *config_valid_ret) { struct crtc_config *crtc; int crtc_count; bool config_valid; int i; int encoder_usage_count[resources->count_encoders]; kmstest_unset_all_crtcs(drm_fd, resources); i = 0; crtc_count = 0; crtc = crtcs; config_valid = true; while (i < connector_count) { drmModeCrtc *drm_crtc; unsigned long encoder_mask; int j; igt_assert_lt(crtc_count, MAX_CRTCS); crtc->crtc_idx = cconf[i].crtc_idx; drm_crtc = drmModeGetCrtc(drm_fd, resources->crtcs[crtc->crtc_idx]); crtc->crtc_id = drm_crtc->crtc_id; drmModeFreeCrtc(drm_crtc); crtc->pipe_id = kmstest_get_pipe_from_crtc_id(drm_fd, crtc->crtc_id); crtc->connector_count = 1; for (j = i + 1; j < connector_count; j++) if (cconf[j].crtc_idx == crtc->crtc_idx) crtc->connector_count++; crtc->cconfs = malloc(sizeof(*crtc->cconfs) * crtc->connector_count); igt_assert(crtc->cconfs); encoder_mask = 0; for (j = 0; j < crtc->connector_count; j++) { drmModeConnector *connector; drmModeEncoder *encoder; crtc->cconfs[j] = cconf[i + j]; connector = cconf[i + j].connector; /* Intel connectors have only a single encoder */ if (connector->count_encoders == 1) { encoder = drmModeGetEncoder(drm_fd, connector->encoders[0]); } else { igt_assert_eq(connector->connector_type, DRM_MODE_CONNECTOR_DisplayPort); igt_assert(connector->count_encoders >= crtc->crtc_idx); encoder = drmModeGetEncoder(drm_fd, connector->encoders[crtc_count]); } igt_assert(encoder); config_valid &= !!(encoder->possible_crtcs & (1 << crtc->crtc_idx)); encoder_mask |= 1 << get_encoder_idx(resources, encoder); config_valid &= !(encoder_mask & ~encoder->possible_clones); drmModeFreeEncoder(encoder); } get_mode_for_crtc(crtc, &crtc->mode); create_fb_for_crtc(crtc, &crtc->fb_info); i += crtc->connector_count; crtc_count++; crtc++; } memset(encoder_usage_count, 0, sizeof(encoder_usage_count)); for (i = 0; i < connector_count; i++) { drmModeConnector *connector = cconf[i].connector; drmModeEncoder *encoder; int idx = 0; /* DP MST configs are presumed valid */ if (connector->count_encoders > 1) idx = cconf[i].crtc_idx; encoder = drmModeGetEncoder(drm_fd, connector->encoders[idx]); encoder_usage_count[get_encoder_idx(resources, encoder)]++; drmModeFreeEncoder(encoder); } for (i = 0; i < resources->count_encoders; i++) if (encoder_usage_count[i] > 1) config_valid = false; *crtc_count_ret = crtc_count; *config_valid_ret = config_valid; } static void cleanup_crtcs(struct crtc_config *crtcs, int crtc_count) { int i; for (i = 0; i < crtc_count; i++) { igt_remove_fb(drm_fd, &crtcs[i].fb_info); drmModeSetCrtc(drm_fd, crtcs[i].crtc_id, 0, 0, 0, NULL, 0, NULL); free(crtcs[i].cconfs); } } static uint32_t *get_connector_ids(struct crtc_config *crtc) { uint32_t *ids; int i; ids = malloc(sizeof(*ids) * crtc->connector_count); igt_assert(ids); for (i = 0; i < crtc->connector_count; i++) ids[i] = crtc->cconfs[i].connector->connector_id; return ids; } static int test_stealing(int fd, struct crtc_config *crtc, uint32_t *ids) { int i, ret = 0; if (!crtc->connector_count) return drmModeSetCrtc(fd, crtc->crtc_id, crtc->fb_info.fb_id, 0, 0, ids, crtc->connector_count, &crtc->mode); for (i = 0; i < crtc->connector_count; ++i) { ret = drmModeSetCrtc(fd, crtc->crtc_id, crtc->fb_info.fb_id, 0, 0, &ids[i], 1, &crtc->mode); igt_assert_eq(ret, 0); ret = drmModeSetCrtc(fd, crtc->crtc_id, crtc->fb_info.fb_id, 0, 0, ids, crtc->connector_count, &crtc->mode); /* This should fail with -EINVAL */ if (!ret) return 0; } return ret; } static double frame_time(const drmModeModeInfo *kmode) { return 1000.0 * kmode->htotal * kmode->vtotal / kmode->clock; } static double line_time(const drmModeModeInfo *kmode) { return 1000.0 * kmode->htotal / kmode->clock; } static void check_timings(int crtc_idx, const drmModeModeInfo *kmode) { #define CALIBRATE_TS_STEPS 120 /* ~2s has to be less than 128! */ drmVBlank wait; igt_stats_t stats; uint32_t last_seq; uint64_t last_timestamp; double expected; double accuracy; double mean; double stddev; int n; memset(&wait, 0, sizeof(wait)); wait.request.type = kmstest_get_vbl_flag(crtc_idx); wait.request.type |= DRM_VBLANK_RELATIVE | DRM_VBLANK_NEXTONMISS; do_or_die(drmWaitVBlank(drm_fd, &wait)); last_seq = wait.reply.sequence; last_timestamp = wait.reply.tval_sec; last_timestamp *= 1000000; last_timestamp += wait.reply.tval_usec; memset(&wait, 0, sizeof(wait)); wait.request.type = kmstest_get_vbl_flag(crtc_idx); wait.request.type |= DRM_VBLANK_ABSOLUTE | DRM_VBLANK_EVENT; wait.request.sequence = last_seq; for (n = 0; n < CALIBRATE_TS_STEPS; n++) { drmVBlank check = {}; ++wait.request.sequence; do_or_die(drmWaitVBlank(drm_fd, &wait)); /* Double check that haven't already missed the vblank */ check.request.type = kmstest_get_vbl_flag(crtc_idx); check.request.type |= DRM_VBLANK_RELATIVE; do_or_die(drmWaitVBlank(drm_fd, &check)); igt_assert(!igt_vblank_after(check.reply.sequence, wait.request.sequence)); } igt_stats_init_with_size(&stats, CALIBRATE_TS_STEPS); for (n = 0; n < CALIBRATE_TS_STEPS; n++) { struct drm_event_vblank ev; uint64_t now; igt_assert(read(drm_fd, &ev, sizeof(ev)) == sizeof(ev)); igt_assert_eq(ev.sequence, last_seq + 1); now = ev.tv_sec; now *= 1000000; now += ev.tv_usec; igt_stats_push(&stats, now - last_timestamp); last_timestamp = now; last_seq = ev.sequence; } expected = frame_time(kmode); mean = igt_stats_get_mean(&stats); stddev = igt_stats_get_std_deviation(&stats); /* 99.7% samples fall within `accuracy` on both sides of mean in normal * distribution if `accuracy = 3 * sigma`. * https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule * * The value of 99.7% was chosen to suit requirements of test cases * which depend on timing, giving the lowest acceptable MTBF of 5.6s * for 60Hz sampling rate. */ accuracy = 3. * stddev; igt_info("Expected frametime: %.0fus; measured %.1fus +- %.3fus accuracy %.2f%% [%.2f scanlines]\n", expected, mean, stddev, 100 * accuracy / mean, accuracy / line_time(kmode)); /* 99.7% samples within one scanline on each side of mean */ igt_assert_f(accuracy < line_time(kmode), "vblank accuracy (%.3fus, %.1f%%) worse than a scanline (%.3fus)\n", accuracy, 100 * accuracy / mean, line_time(kmode)); /* At least 90% of frame times fall within the one scanline on each * side of expected mean. * * Expected scanline duration: * (expected - accuracy, expected + accuracy). * Assuming maximum difference allowed: * expected = mean + n * sigma * the scanline duration becomes: * (mean - accuracy + n * sigma, mean + accuracy + n * sigma) * The expected scanline captures the following number of samples * from each side of expected: * (erf(abs(-(accuracy/sigma) + n) / sqrt(2)) * + erf((accuracy/sigma) + n) / sqrt(2))) / 2 * = samples * * Solving for samples = 0.9: * n = 1.718 * * See: * https://en.wikipedia.org/wiki/Standard_deviation#Rules_for_normally_distributed_data */ igt_assert_f(fabs(mean - expected) < 1.718 * stddev, "vblank interval differs from modeline! expected %.1fus, measured %1.fus +- %.3fus, difference %.1fus (%.1f sigma)\n", expected, mean, stddev, fabs(mean - expected), fabs(mean - expected) / stddev); } static void test_crtc_config(const struct test_config *tconf, struct crtc_config *crtcs, int crtc_count) { char str_buf[MAX_CRTCS][1024]; const char *crtc_strs[MAX_CRTCS]; struct crtc_config *crtc; static int test_id; bool config_failed = false; int ret = 0; int i; test_id++; if (filter_test_id && filter_test_id != test_id) return; igt_info(" Test id#%d CRTC count %d\n", test_id, crtc_count); for (i = 0; i < crtc_count; i++) { get_crtc_config_str(&crtcs[i], str_buf[i], sizeof(str_buf[i])); crtc_strs[i] = &str_buf[i][0]; } if (dry_run) { for (i = 0; i < crtc_count; i++) igt_info(" %s\n", crtc_strs[i]); return; } for (i = 0; i < crtc_count; i++) { uint32_t *ids; crtc = &crtcs[i]; igt_info(" %s\n", crtc_strs[i]); create_fb_for_crtc(crtc, &crtc->fb_info); paint_fb(&crtc->fb_info, tconf->name, crtc_strs, crtc_count, i); ids = get_connector_ids(crtc); if (tconf->flags & TEST_STEALING) ret = test_stealing(drm_fd, crtc, ids); else ret = drmModeSetCrtc(drm_fd, crtc->crtc_id, crtc->fb_info.fb_id, 0, 0, ids, crtc->connector_count, &crtc->mode); free(ids); if (ret < 0) { igt_assert_eq(errno, EINVAL); config_failed = true; } } igt_assert(config_failed == !!(tconf->flags & TEST_INVALID)); if (ret == 0 && tconf->flags & TEST_TIMINGS) check_timings(crtcs[0].crtc_idx, &crtcs[0].mode); return; } static void test_one_combination(const struct test_config *tconf, struct connector_config *cconfs, int connector_count) { struct crtc_config crtcs[MAX_CRTCS]; int crtc_count; bool config_valid; setup_crtcs(tconf->resources, cconfs, connector_count, crtcs, &crtc_count, &config_valid); if (config_valid == !(tconf->flags & TEST_INVALID)) test_crtc_config(tconf, crtcs, crtc_count); cleanup_crtcs(crtcs, crtc_count); } static int assign_crtc_to_connectors(const struct test_config *tconf, int *crtc_idxs, int connector_count, struct connector_config *cconfs) { unsigned long crtc_idx_mask; int i; crtc_idx_mask = 0; for (i = 0; i < connector_count; i++) { int crtc_idx = crtc_idxs[i]; if ((tconf->flags & TEST_SINGLE_CRTC_CLONE) && crtc_idx_mask & ~(1 << crtc_idx)) return -1; if ((tconf->flags & TEST_EXCLUSIVE_CRTC_CLONE) && crtc_idx_mask & (1 << crtc_idx)) return -1; crtc_idx_mask |= 1 << crtc_idx; cconfs[i].crtc_idx = crtc_idx; } return 0; } static int get_one_connector(drmModeRes *resources, int connector_id, struct connector_config *cconf) { drmModeConnector *connector; connector = drmModeGetConnectorCurrent(drm_fd, connector_id); igt_assert(connector); cconf->connector = connector; if (connector->connection != DRM_MODE_CONNECTED) { drmModeFreeConnector(connector); return -1; } if (!kmstest_get_connector_default_mode(drm_fd, connector, &cconf->default_mode)) { drmModeFreeConnector(connector); return -1; } return 0; } static int get_connectors(drmModeRes *resources, int *connector_idxs, int connector_count, struct connector_config *cconfs) { int i; for (i = 0; i < connector_count; i++) { int connector_idx; int connector_id; connector_idx = connector_idxs[i]; igt_assert_lt(connector_idx, resources->count_connectors); connector_id = resources->connectors[connector_idx]; if (get_one_connector(resources, connector_id, &cconfs[i]) < 0) goto err; } return 0; err: while (i--) drmModeFreeConnector(cconfs[i].connector); return -1; } static void free_connectors(struct connector_config *cconfs, int connector_count) { int i; for (i = 0; i < connector_count; i++) drmModeFreeConnector(cconfs[i].connector); } struct combination { int elems[MAX_COMBINATION_ELEMS]; }; struct combination_set { int count; int capacity; struct combination *items; }; /* * Get all possible selection of k elements from n elements with or without * repetitions. */ static void iterate_combinations(int n, int k, bool allow_repetitions, int depth, int base, struct combination *comb, struct combination_set *set) { int v; if (!k) { igt_assert(set->count < set->capacity); set->items[set->count++] = *comb; return; } for (v = base; v < n; v++) { comb->elems[depth] = v; iterate_combinations(n, k - 1, allow_repetitions, depth + 1, allow_repetitions ? 0 : v + 1, comb, set); } } static void get_combinations(int n, int k, bool allow_repetitions, struct combination_set *set) { struct combination comb; igt_assert(k <= ARRAY_SIZE(set->items[0].elems)); set->count = 0; iterate_combinations(n, k, allow_repetitions, 0, 0, &comb, set); } static void test_combinations(const struct test_config *tconf, int connector_count) { struct combination_set connector_combs; struct combination_set crtc_combs; struct connector_config *cconfs; int i; if (connector_count > 2 && (tconf->flags & TEST_STEALING)) return; igt_assert(tconf->resources); connector_combs.capacity = pow(tconf->resources->count_connectors, tconf->resources->count_crtcs + 1); crtc_combs.capacity = pow(tconf->resources->count_crtcs, tconf->resources->count_crtcs + 1); connector_combs.items = malloc(connector_combs.capacity * sizeof(struct combination)); crtc_combs.items = malloc(crtc_combs.capacity * sizeof(struct combination)); get_combinations(tconf->resources->count_connectors, connector_count, false, &connector_combs); get_combinations(tconf->resources->count_crtcs, connector_count, true, &crtc_combs); igt_info("Testing: %s %d connector combinations\n", tconf->name, connector_count); for (i = 0; i < connector_combs.count; i++) { int *connector_idxs; int ret; int j; cconfs = malloc(sizeof(*cconfs) * connector_count); igt_assert(cconfs); connector_idxs = &connector_combs.items[i].elems[0]; ret = get_connectors(tconf->resources, connector_idxs, connector_count, cconfs); if (ret < 0) goto free_cconfs; for (j = 0; j < crtc_combs.count; j++) { int *crtc_idxs = &crtc_combs.items[j].elems[0]; ret = assign_crtc_to_connectors(tconf, crtc_idxs, connector_count, cconfs); if (ret < 0) continue; test_one_combination(tconf, cconfs, connector_count); } free_connectors(cconfs, connector_count); free_cconfs: free(cconfs); } free(connector_combs.items); free(crtc_combs.items); } static void run_test(const struct test_config *tconf) { int connector_num; connector_num = tconf->flags & TEST_CLONE ? 2 : 1; for (; connector_num <= tconf->resources->count_crtcs; connector_num++) test_combinations(tconf, connector_num); } static int opt_handler(int opt, int opt_index, void *data) { switch (opt) { case 'd': dry_run = true; break; case 't': filter_test_id = atoi(optarg); break; default: igt_assert(0); } return 0; } int main(int argc, char **argv) { const struct { enum test_flags flags; const char *name; } tests[] = { { TEST_TIMINGS, "basic" }, { TEST_CLONE | TEST_SINGLE_CRTC_CLONE, "basic-clone-single-crtc" }, { TEST_INVALID | TEST_CLONE | TEST_SINGLE_CRTC_CLONE, "invalid-clone-single-crtc" }, { TEST_INVALID | TEST_CLONE | TEST_EXCLUSIVE_CRTC_CLONE, "invalid-clone-exclusive-crtc" }, { TEST_CLONE | TEST_EXCLUSIVE_CRTC_CLONE, "clone-exclusive-crtc" }, { TEST_INVALID | TEST_CLONE | TEST_SINGLE_CRTC_CLONE | TEST_STEALING, "invalid-clone-single-crtc-stealing" } }; const char *help_str = " -d\t\tDon't run any test, only print what would be done. (still needs DRM access)\n" " -t \tRun only the test with this id."; int i; int ret; ret = igt_subtest_init_parse_opts(&argc, argv, "dt:", NULL, help_str, opt_handler, NULL); if (ret < 0) return ret == -1 ? 0 : ret; igt_skip_on_simulation(); igt_assert_f(!(dry_run && filter_test_id), "only one of -d and -t is accepted\n"); igt_fixture { drm_fd = drm_open_driver_master(DRIVER_ANY); if (!dry_run) kmstest_set_vt_graphics_mode(); drm_resources = drmModeGetResources(drm_fd); igt_require(drm_resources); } for (i = 0; i < ARRAY_SIZE(tests); i++) { igt_subtest(tests[i].name) { struct test_config tconf = { .flags = tests[i].flags, .name = tests[i].name, .resources = drm_resources, }; run_test(&tconf); } } igt_fixture { drmModeFreeResources(drm_resources); close(drm_fd); } igt_exit(); }