summaryrefslogtreecommitdiff
path: root/tools/src/display.c
diff options
context:
space:
mode:
Diffstat (limited to 'tools/src/display.c')
-rw-r--r--tools/src/display.c514
1 files changed, 514 insertions, 0 deletions
diff --git a/tools/src/display.c b/tools/src/display.c
new file mode 100644
index 0000000..3dbf49e
--- /dev/null
+++ b/tools/src/display.c
@@ -0,0 +1,514 @@
+/* SPDX-License-Identifier: MIT */
+
+#include "display.h"
+#include "adt.h"
+#include "assert.h"
+#include "dcp.h"
+#include "dcp_iboot.h"
+#include "fb.h"
+#include "memory.h"
+#include "string.h"
+#include "utils.h"
+#include "xnuboot.h"
+
+#define DISPLAY_STATUS_DELAY 100
+#define DISPLAY_STATUS_RETRIES 20
+
+#define COMPARE(a, b) \
+ if ((a) > (b)) { \
+ *best = modes[i]; \
+ continue; \
+ } else if ((a) < (b)) { \
+ continue; \
+ }
+
+static dcp_dev_t *dcp;
+static dcp_iboot_if_t *iboot;
+static u64 fb_dva;
+static u64 fb_size;
+bool display_is_external;
+
+#define abs(x) ((x) >= 0 ? (x) : -(x))
+
+u64 display_mode_fb_size(dcp_timing_mode_t *mode)
+{
+ // assume 4 byte per pixel (either BGRA x2r10b10g10)
+ return mode->width * mode->height * 4;
+}
+
+static void display_choose_timing_mode(dcp_timing_mode_t *modes, int cnt, dcp_timing_mode_t *best,
+ dcp_timing_mode_t *want)
+{
+ *best = modes[0];
+
+ for (int i = 1; i < cnt; i++) {
+ COMPARE(modes[i].valid, best->valid);
+ if (want && want->valid) {
+ COMPARE(modes[i].width == want->width && modes[i].height == want->height,
+ best->width == want->width && best->height == want->height);
+ COMPARE(-abs((long)modes[i].fps - (long)want->fps),
+ -abs((long)best->fps - (long)want->fps));
+ } else {
+ COMPARE(display_mode_fb_size(&modes[i]) <= fb_size,
+ display_mode_fb_size(best) <= fb_size);
+ }
+
+ COMPARE(modes[i].width <= 1920, best->width <= 1920);
+ COMPARE(modes[i].height <= 1200, best->height <= 1200);
+ COMPARE(modes[i].fps <= 60 << 16, best->fps <= 60 << 16);
+ COMPARE(modes[i].width, best->width);
+ COMPARE(modes[i].height, best->height);
+ COMPARE(modes[i].fps, best->fps);
+ }
+
+ printf("display: timing mode: valid=%d %dx%d %d.%02d Hz\n", best->valid, best->width,
+ best->height, best->fps >> 16, ((best->fps & 0xffff) * 100 + 0x7fff) >> 16);
+}
+
+static void display_choose_color_mode(dcp_color_mode_t *modes, int cnt, dcp_color_mode_t *best)
+{
+ *best = modes[0];
+
+ for (int i = 1; i < cnt; i++) {
+ COMPARE(modes[i].valid, best->valid);
+ COMPARE(modes[i].bpp <= 32, best->bpp <= 32);
+ COMPARE(modes[i].bpp, best->bpp);
+ COMPARE(-modes[i].colorimetry, -best->colorimetry);
+ COMPARE(-modes[i].encoding, -best->encoding);
+ COMPARE(-modes[i].eotf, -best->eotf);
+ }
+
+ printf("display: color mode: valid=%d colorimetry=%d eotf=%d encoding=%d bpp=%d\n", best->valid,
+ best->colorimetry, best->eotf, best->encoding, best->bpp);
+}
+
+int display_get_vram(u64 *paddr, u64 *size)
+{
+ int ret = 0;
+ int adt_path[4];
+ int node = adt_path_offset_trace(adt, "/vram", adt_path);
+
+ if (node < 0) {
+ printf("display: '/vram' not found\n");
+ return -1;
+ }
+
+ int pp = 0;
+ while (adt_path[pp])
+ pp++;
+ adt_path[pp + 1] = 0;
+
+ ret = adt_get_reg(adt, adt_path, "reg", 0, paddr, size);
+ if (ret < 0) {
+ printf("display: failed to read /vram/reg\n");
+ return -1;
+ }
+
+ if (*paddr != cur_boot_args.video.base) {
+ printf("display: vram does not match boot_args.video.base\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+static uintptr_t display_map_fb(uintptr_t iova, u64 paddr, u64 size)
+{
+ if (iova == 0) {
+ u64 iova_disp0 = 0;
+ u64 iova_dcp = 0;
+
+ // start scanning for free iova space on vm-base
+ iova_dcp = dart_find_iova(dcp->dart_dcp, dart_vm_base(dcp->dart_dcp), size);
+ if (DART_IS_ERR(iova_dcp)) {
+ printf("display: failed to find IOVA for fb of %06zx bytes (dcp)\n", size);
+ return iova_dcp;
+ }
+
+ // try to map the fb to the same IOVA on disp0
+ iova_disp0 = dart_find_iova(dcp->dart_disp, iova_dcp, size);
+ if (DART_IS_ERR(iova_disp0)) {
+ printf("display: failed to find IOVA for fb of %06zx bytes (disp0)\n", size);
+ return iova_disp0;
+ }
+
+ // assume this results in the same IOVA, not sure if this is required but matches what iboot
+ // does on other models.
+ if (iova_disp0 != iova_dcp) {
+ printf("display: IOVA mismatch for fb between dcp (%08lx) and disp0 (%08lx)\n",
+ (u64)iova_dcp, (u64)iova_disp0);
+ return DART_PTR_ERR;
+ }
+
+ iova = iova_dcp;
+ }
+
+ int ret = dart_map(dcp->dart_disp, iova, (void *)paddr, size);
+ if (ret < 0) {
+ printf("display: failed to map fb to dart-disp0\n");
+ return DART_PTR_ERR;
+ }
+
+ ret = dart_map(dcp->dart_dcp, iova, (void *)paddr, size);
+ if (ret < 0) {
+ printf("display: failed to map fb to dart-dcp\n");
+ dart_unmap(dcp->dart_disp, iova, size);
+ return DART_PTR_ERR;
+ }
+
+ return iova;
+}
+
+int display_start_dcp(void)
+{
+ if (iboot)
+ return 0;
+
+ dcp = dcp_init("/arm-io/dcp", "/arm-io/dart-dcp", "/arm-io/dart-disp0");
+ if (!dcp) {
+ printf("display: failed to initialize DCP\n");
+ return -1;
+ }
+
+ // determine frame buffer PA and size from "/vram"
+ u64 pa, size;
+ if (display_get_vram(&pa, &size)) {
+ // use a safe fb_size
+ fb_size = cur_boot_args.video.stride * cur_boot_args.video.height *
+ ((cur_boot_args.video.depth + 7) / 8);
+ } else {
+ fb_size = size;
+ }
+
+ // Find the framebuffer DVA
+ fb_dva = dart_search(dcp->dart_disp, (void *)cur_boot_args.video.base);
+ // framebuffer is not mapped on the M1 Ultra Mac Studio
+ if (DART_IS_ERR(fb_dva))
+ fb_dva = display_map_fb(0, pa, size);
+ if (DART_IS_ERR(fb_dva)) {
+ printf("display: failed to find display DVA\n");
+ fb_dva = 0;
+ dcp_shutdown(dcp, false);
+ return -1;
+ }
+
+ iboot = dcp_ib_init(dcp);
+ if (!iboot) {
+ printf("display: failed to initialize DCP iBoot interface\n");
+ dcp_shutdown(dcp, false);
+ return -1;
+ }
+
+ return 0;
+}
+
+struct display_options {
+ bool retina;
+};
+
+int display_parse_mode(const char *config, dcp_timing_mode_t *mode, struct display_options *opts)
+{
+ memset(mode, 0, sizeof(*mode));
+
+ if (!config || !strcmp(config, "auto"))
+ return 0;
+
+ const char *s_w = config;
+ const char *s_h = strchr(config, 'x');
+ const char *s_fps = strchr(config, '@');
+
+ if (s_w && s_h) {
+ mode->width = atol(s_w);
+ mode->height = atol(s_h + 1);
+ mode->valid = mode->width && mode->height;
+ }
+
+ if (s_fps) {
+ mode->fps = atol(s_fps + 1) << 16;
+
+ const char *s_fps_frac = strchr(s_fps + 1, '.');
+ if (s_fps_frac) {
+ // Assumes two decimals...
+ mode->fps += (atol(s_fps_frac + 1) << 16) / 100;
+ }
+ }
+
+ const char *option = config;
+ while (option && opts) {
+ if (!strncmp(option + 1, "retina", 6))
+ opts->retina = true;
+ option = strchr(option + 1, ',');
+ }
+
+ printf("display: want mode: valid=%d %dx%d %d.%02d Hz\n", mode->valid, mode->width,
+ mode->height, mode->fps >> 16, ((mode->fps & 0xffff) * 100 + 0x7fff) >> 16);
+
+ return mode->valid;
+}
+
+static int display_swap(u64 iova, u32 stride, u32 width, u32 height)
+{
+ int ret;
+ int swap_id = ret = dcp_ib_swap_begin(iboot);
+ if (swap_id < 0) {
+ printf("display: failed to start swap\n");
+ return -1;
+ }
+
+ dcp_layer_t layer = {
+ .planes = {{
+ .addr = iova,
+ .stride = stride,
+ .addr_format = ADDR_PLANAR,
+ }},
+ .plane_cnt = 1,
+ .width = width,
+ .height = height,
+ .surface_fmt = FMT_w30r,
+ .colorspace = 2,
+ .eotf = EOTF_GAMMA_SDR,
+ .transform = XFRM_NONE,
+ };
+
+ dcp_rect_t rect = {width, height, 0, 0};
+
+ if ((ret = dcp_ib_swap_set_layer(iboot, 0, &layer, &rect, &rect)) < 0) {
+ printf("display: failed to set layer\n");
+ return -1;
+ }
+
+ if ((ret = dcp_ib_swap_end(iboot)) < 0) {
+ printf("display: failed to complete swap\n");
+ return -1;
+ }
+
+ return swap_id;
+}
+
+int display_configure(const char *config)
+{
+ dcp_timing_mode_t want;
+ struct display_options opts = {0};
+
+ display_parse_mode(config, &want, &opts);
+
+ u64 start_time = get_ticks();
+
+ int ret = display_start_dcp();
+ if (ret < 0)
+ return ret;
+
+ // Power on
+ if ((ret = dcp_ib_set_power(iboot, true)) < 0) {
+ printf("display: failed to set power\n");
+ return ret;
+ }
+
+ // Detect if display is connected
+ int timing_cnt, color_cnt;
+ int hpd = 0, retries = 0;
+
+ /* After boot DCP does not immediately report a connected display. Retry getting display
+ * information for 2 seconds.
+ */
+ while (retries++ < DISPLAY_STATUS_RETRIES) {
+ hpd = dcp_ib_get_hpd(iboot, &timing_cnt, &color_cnt);
+ if (hpd < 0)
+ ret = hpd;
+ else if (hpd && timing_cnt && color_cnt)
+ break;
+ if (retries < DISPLAY_STATUS_RETRIES)
+ mdelay(DISPLAY_STATUS_DELAY);
+ }
+ printf("display: waited %d ms for display status\n", (retries - 1) * DISPLAY_STATUS_DELAY);
+ if (ret < 0) {
+ printf("display: failed to get display status\n");
+ return 0;
+ }
+
+ printf("display: connected:%d timing_cnt:%d color_cnt:%d\n", hpd, timing_cnt, color_cnt);
+
+ if (!hpd || !timing_cnt || !color_cnt)
+ return 0;
+
+ // Find best modes
+ dcp_timing_mode_t *tmodes, tbest;
+ if ((ret = dcp_ib_get_timing_modes(iboot, &tmodes)) < 0) {
+ printf("display: failed to get timing modes\n");
+ return -1;
+ }
+ assert(ret == timing_cnt);
+ display_choose_timing_mode(tmodes, timing_cnt, &tbest, &want);
+
+ dcp_color_mode_t *cmodes, cbest;
+ if ((ret = dcp_ib_get_color_modes(iboot, &cmodes)) < 0) {
+ printf("display: failed to get color modes\n");
+ return -1;
+ }
+ assert(ret == color_cnt);
+ display_choose_color_mode(cmodes, color_cnt, &cbest);
+
+ // Set mode
+ if ((ret = dcp_ib_set_mode(iboot, &tbest, &cbest)) < 0) {
+ printf("display: failed to set mode\n");
+ return -1;
+ }
+
+ u64 fb_pa = cur_boot_args.video.base;
+ u64 tmp_dva = 0;
+
+ size_t size =
+ ALIGN_UP(tbest.width * tbest.height * ((cbest.bpp + 7) / 8) + 24 * SZ_16K, SZ_16K);
+
+ if (fb_size < size) {
+ printf("display: current framebuffer is too small for new mode\n");
+
+ /* rtkit uses 0x10000000 as DVA offset, FB starts in the first page */
+ if ((s64)size > 7 * SZ_32M) {
+ printf("display: not enough reserved L2 DVA space for fb size 0x%zx\n", size);
+ return -1;
+ }
+
+ cur_boot_args.mem_size -= size;
+ fb_pa = cur_boot_args.phys_base + cur_boot_args.mem_size;
+ /* add guard page between RAM and framebuffer */
+ // TODO: update mapping?
+ cur_boot_args.mem_size -= SZ_16K;
+
+ memset((void *)fb_pa, 0, size);
+
+ tmp_dva = iova_alloc(dcp->iovad_dcp, size);
+
+ tmp_dva = display_map_fb(tmp_dva, fb_pa, size);
+ if (DART_IS_ERR(tmp_dva)) {
+ printf("display: failed to map new fb\n");
+ return -1;
+ }
+
+ // Swap!
+ u32 stride = tbest.width * 4;
+ ret = display_swap(tmp_dva, stride, tbest.width, tbest.height);
+ if (ret < 0)
+ return ret;
+
+ /* wait for swap durations + 1ms */
+ u32 delay = (((1000 << 16) + tbest.fps - 1) / tbest.fps) + 1;
+ mdelay(delay);
+ dart_unmap(dcp->dart_disp, fb_dva, fb_size);
+ dart_unmap(dcp->dart_dcp, fb_dva, fb_size);
+
+ fb_dva = display_map_fb(fb_dva, fb_pa, size);
+ if (DART_IS_ERR(fb_dva)) {
+ printf("display: failed to map new fb\n");
+ fb_dva = 0;
+ return -1;
+ }
+
+ fb_size = size;
+ mmu_map_framebuffer(fb_pa, fb_size);
+
+ /* update ADT with the physical address of the new framebuffer */
+ u64 fb_reg[2] = {fb_pa, size};
+ int node = adt_path_offset(adt, "vram");
+ if (node >= 0) {
+ // TODO: adt_set_reg(adt, node, "vram", fb_pa, size);?
+ ret = adt_setprop(adt, node, "reg", &fb_reg, sizeof(fb_reg));
+ if (ret < 0)
+ printf("display: failed to update '/vram'\n");
+ }
+ node = adt_path_offset(adt, "/chosen/carveout-memory-map");
+ if (node >= 0) {
+ // TODO: adt_set_reg(adt, node, "vram", fb_pa, size);?
+ ret = adt_setprop(adt, node, "region-id-14", &fb_reg, sizeof(fb_reg));
+ if (ret < 0)
+ printf("display: failed to update '/chosen/carveout-memory-map/region-id-14'\n");
+ }
+ }
+
+ // Swap!
+ u32 stride = tbest.width * 4;
+ ret = display_swap(fb_dva, stride, tbest.width, tbest.height);
+ if (ret < 0)
+ return ret;
+
+ printf("display: swapped! (swap_id=%d)\n", ret);
+
+ if (fb_pa != cur_boot_args.video.base || cur_boot_args.video.stride != stride ||
+ cur_boot_args.video.width != tbest.width || cur_boot_args.video.height != tbest.height ||
+ cur_boot_args.video.depth != 30) {
+ cur_boot_args.video.base = fb_pa;
+ cur_boot_args.video.stride = stride;
+ cur_boot_args.video.width = tbest.width;
+ cur_boot_args.video.height = tbest.height;
+ cur_boot_args.video.depth = 30 | (opts.retina ? FB_DEPTH_FLAG_RETINA : 0);
+ fb_reinit();
+ }
+
+ /* Update for python / subsequent stages */
+ memcpy((void *)boot_args_addr, &cur_boot_args, sizeof(cur_boot_args));
+
+ if (tmp_dva) {
+ // unmap / free temporary dva
+ dart_unmap(dcp->dart_disp, tmp_dva, size);
+ dart_unmap(dcp->dart_dcp, tmp_dva, size);
+ iova_free(dcp->iovad_dcp, tmp_dva, size);
+ }
+
+ u64 msecs = ticks_to_msecs(get_ticks() - start_time);
+ printf("display: Modeset took %ld ms\n", msecs);
+
+ return 1;
+}
+
+int display_init(void)
+{
+ int node = adt_path_offset(adt, "/arm-io/disp0");
+
+ if (node < 0) {
+ printf("DISP0 node not found!\n");
+ return -1;
+ }
+
+ display_is_external = adt_getprop(adt, node, "external", NULL);
+ if (display_is_external)
+ printf("display: Display is external\n");
+ else
+ printf("display: Display is internal\n");
+
+ if (cur_boot_args.video.width == 640 && cur_boot_args.video.height == 1136) {
+ printf("display: Dummy framebuffer found, initializing display\n");
+ return display_configure(NULL);
+ } else if (display_is_external) {
+ printf("display: External display found, reconfiguring\n");
+ return display_configure(NULL);
+ } else {
+ printf("display: Display is already initialized (%ldx%ld)\n", cur_boot_args.video.width,
+ cur_boot_args.video.height);
+ return 0;
+ }
+}
+
+void display_shutdown(dcp_shutdown_mode mode)
+{
+ if (iboot) {
+ dcp_ib_shutdown(iboot);
+ switch (mode) {
+ case DCP_QUIESCED:
+ printf("display: Quiescing DCP (unconditional)\n");
+ dcp_shutdown(dcp, false);
+ break;
+ case DCP_SLEEP_IF_EXTERNAL:
+ if (!display_is_external)
+ printf("display: Quiescing DCP (internal)\n");
+ else
+ printf("display: Sleeping DCP (external)\n");
+ dcp_shutdown(dcp, display_is_external);
+ break;
+ case DCP_SLEEP:
+ printf("display: Sleeping DCP (unconditional)\n");
+ dcp_shutdown(dcp, true);
+ break;
+ }
+ iboot = NULL;
+ }
+}