/* * copyright 2014-2020, björn ståhl * license: 3-clause bsd, see copying file in arcan source repository. * reference: http://arcan-fe.com */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "arcan_math.h" #include "arcan_general.h" #include "arcan_video.h" #include "arcan_audio.h" #include "arcan_videoint.h" #include "arcan_led.h" #include "arcan_shmif.h" #include "arcan_frameserver.h" #include "../agp/glfun.h" #include "arcan_event.h" #include "libbacklight.h" /* * Current details / notes: * * - The EGL Context management rules have mutated over the years, currently * we mess around with a shared 'device' context and then per display contexts * that we render to based on the buffer format properties desired for that * device. * * - The crutch to that is that when different format options are desired, say * a 565 one and a 10bit on EGL will choke since it does not fit the config on * the shared context. There is an extension to this, EGL_KHR_no_config_context * where we can simply stop and remove/refactor all the device_context/display * context settings. * * - But this is not the entire truth - enter multiGPU where we actually have * discrete and different contexts and possible different GL implementations * living in the same TLS. * * - For that case we need to track afinity to a agp_vstore and upload/synch * into each context based on that affinity. */ /* * mask out these types as they won't be useful, */ #define VIDEO_PLATFORM_IMPL #include "../../engine/arcan_conductor.h" #ifdef _DEBUG #define DEBUG 1 #else #define DEBUG 0 #endif /* * same debugging / tracing setup as in egl-dri.c */ #define debug_print(fmt, ...) \ do { if (DEBUG) arcan_warning("%lld:%s:%d:%s(): " fmt "\n",\ arcan_timemillis(), "egl-dri:", __LINE__, __func__,##__VA_ARGS__); } while (0) #ifndef verbose_print #define verbose_print #endif #include "egl.h" #define shmifext_buffer_plane agp_buffer_plane struct shmifext_color_buffer { union { unsigned int gl; } id; void* alloc_tags[4]; int type; }; #include "egl_gbm_helper.h" static bool lookup_drm_propval(int fd, uint32_t oid, uint32_t otype, const char* name, uint64_t* val, bool id); static const char* egl_errstr(); static void* lookup(void* tag, const char* sym, bool req) { dlerror(); void* res = dlsym(tag ? tag : RTLD_DEFAULT, sym); if (dlerror() != NULL && req){ arcan_fatal("agp lookup(%s) failed, missing req. symbol.\n", sym); } return res; } static void* lookup_call(void* tag, const char* sym, bool req) { PFNEGLGETPROCADDRESSPROC getproc = tag; void* res = getproc(sym); if (!res && req) arcan_fatal("agp lookup(%s) failed, missing req. symbol.\n", sym); return res; } static char* egl_envopts[] = { "[ for multiple devices, append _n to key (e.g. device_2=) ]", "", "display_device=/path/to/dev", "for multiple devices suffix with _n (n = 2,3..)", "draw_device=/path/to/dev", "set to display device unless provided", "device_buffer=method", "set buffer transfer method (gbm, streams)", "device_libs=lib1:lib2", "libs used for device", "device_connector=ind", "primary display connector index", "device_wait", "loop until an active connector is found", "device_nodpms", "set to disable power management controls", "device_direct_scanout", "enable direct rendertarget scanout", "display_context=1", "set outer shared headless context, per display contexts", NULL }; enum buffer_method { BUF_GBM, /* There is another option to running a 'display-less' EGL context and * that is to build the display around a pbuffer, but there seem to be * little utility to having that over this form of 'headless' */ BUF_HEADLESS, BUF_STREAM }; enum vsynch_method { VSYNCH_FLIP = 0, VSYNCH_CLOCK = 1, VSYNCH_IGNORE = 2 }; enum display_update_state { UPDATE_FLIP, /* swap between front and back bo */ UPDATE_DIRECT, UPDATE_FRONT, UPDATE_SKIP }; /* * Each open output device, can be shared between displays */ struct dev_node { int active; /*tristate, 0 = not used, 1 = active, 2 = displayless, 3 = inactive */ int draw_fd; int disp_fd; char* pathref; /* things we need to track to be able to forward devices to a client */ struct { int fd; uint8_t* metadata; size_t metadata_sz; } client_meta; int refc; /* dev_node to use instead of this when performing reset */ int gpu_index; bool have_altgpu; enum vsynch_method vsynch_method; /* card_id is some unique sequential identifier for this card * crtc is an allocation bitmap for output port<->display allocation * atomic is set if the driver kms side supports/needs atomic modesetting */ bool wait_connector; bool explicit_synch; int card_id; bool atomic; /* * method is the key driver for most paths in here, see the M_ enum values * above to indicate which of the elements here that are valid. */ enum buffer_method buftype; union { EGLDeviceEXT egldev; struct gbm_device* gbm; } buffer; /* Display is the display system connection, not to be confused with our normal * display, for that we have configs derived from the display which match the * visual of our underlying buffer method - these combined give the surface * within the context */ EGLint attrtbl[24]; EGLConfig config; EGLDisplay display; /* Each display has its own context in order to have different framebuffer out * configuration, then an outer headless context that all the other resources * are allocated with */ EGLContext context; int context_refc; const char* context_state; /* * to deal with multiple GPUs and multiple vendor libraries, these contexts are * managed per display and explicitly referenced / switched when we need to. */ char* egllib; char* agplib; struct egl_env eglenv; }; enum disp_state { DISP_UNUSED = 0, DISP_KNOWN = 1, DISP_MAPPED = 2, DISP_CLEANUP = 3, DISP_EXTSUSP = 4 }; /* * only the setup_cards_db() initialization path can handle more than one * device node, and it is incomplete still until we can maintain affinity * for all resources. */ #ifndef VIDEO_MAX_NODES #define VIDEO_MAX_NODES 4 #endif static struct dev_node nodes[VIDEO_MAX_NODES]; /* * we don't go with the normal GBM_ buffer names in order to have something * that maps between EGLStreams/... and covers the set of 'common' display * formats */ enum output_format { OUTPUT_DEFAULT = 0, /* RGB888 */ OUTPUT_DEEP = 1, OUTPUT_LOW = 2, OUTPUT_HDR = 3 }; /* * aggregation struct that represent one triple of display, card, bindings */ struct dispout { /* connect drm, gbm and EGLs idea of a device */ struct dev_node* device; unsigned long long last_update; int output_format; uint64_t frame_cookie; struct monitor_mode* mode_cache; size_t mode_cache_sz; /* the output buffers, actual fields use will vary with underlying * method, i.e. different for normal gbm, headless gbm and eglstreams */ struct { int in_flip, in_destroy, in_dumb_set; EGLConfig config; EGLContext context; EGLSurface esurf; EGLStreamKHR stream; EGLSyncKHR synch; struct gbm_bo* cur_bo, (* next_bo); uint32_t cur_fb; int format; struct gbm_surface* surface; /* If a vobj- has been set to be directly mapped and is of a compatible type * (e.g. shm or tui mapped) we use a single dumb buffer as our fb and draw * directly into it (the shmpage itself is our "back buffer") - the store * pointer will be our reference. With TUI, the rasterizer draws right into the * front buffer. This will currently cause tearing. The improvement there is a * synch interface so that we can have a worker thread that does the final * blit/upload from the text screen buffer. */ struct { bool enabled; size_t sz; struct agp_vstore agp; struct agp_vstore* ref; int fd; uint32_t fb; } dumb; } buffer; struct { bool reset_mode, primary; drmModeConnector* con; uint32_t con_id; drmModeModeInfo mode; int mode_set; drmModeCrtcPtr old_crtc; int crtc; int crtc_index; int plane_id; enum dpms_state dpms; char* edid_blob; size_t blob_sz; size_t gamma_size; uint16_t* orig_gamma; /* should track a small amount of possible overlay planes (one or two) and * allow the platform_map call to set them and their offsets individually */ } display; /* internal v-store and system mappings, rules for drawing final output */ arcan_vobj_id vid; bool force_compose; bool skip_blit; size_t dispw, disph, dispx, dispy; _Alignas(16) float projection[16]; _Alignas(16) float txcos[8]; enum blitting_hint hint; enum disp_state state; platform_display_id id; /* backlight is "a bit" quirky, we register a custom led controller that * is shared for all displays and processed while we're busy synching. * Subleds on this controller match the displayid of the display */ struct backlight* backlight; long backlight_brightness; }; static struct { struct dispout* last_display; size_t canvasw, canvash; long long destroy_pending; int ledid, ledind; uint8_t ledval[3]; int ledpair[2]; /* on rendertarget rebuild and so on the decay is set to a certain number * of frames (typically 3 - current, in-flight, draw-dst) so that the full * swapchain reflects the same contents and format */ size_t decay; long long last_card_scan; bool scan_pending; } egl_dri = { .ledind = 255 }; #ifndef CARD_RESCAN_DELAY_MS #define CARD_RESCAN_DELAY_MS 500 #endif #ifndef MAX_DISPLAYS #define MAX_DISPLAYS 16 #endif static struct dispout displays[MAX_DISPLAYS]; static struct dispout* allocate_display(struct dev_node* node) { uintptr_t tag; cfg_lookup_fun get_config = platform_config_lookup(&tag); for (size_t i = 0; i < MAX_DISPLAYS; i++){ if (displays[i].state == DISP_UNUSED){ displays[i].device = node; displays[i].display.primary = false; displays[i].id = i; node->refc++; displays[i].state = DISP_KNOWN; /* we currently force composition on all displays unless * explicitly turned on, as there seem to be some driver * issues with scanning out fbo color attachments */ displays[i].force_compose = !get_config( "video_device_direct_scanout", 0, NULL, tag); debug_print("(%zu) added, force composition? %d", i, (int) displays[i].force_compose); return &displays[i]; } } return NULL; } static struct dispout* get_display(size_t index) { if (index >= MAX_DISPLAYS) return NULL; else return &displays[index]; } static int adpms_to_dpms(enum dpms_state state) { switch (state){ case ADPMS_ON: return DRM_MODE_DPMS_ON; case ADPMS_STANDBY: return DRM_MODE_DPMS_STANDBY; case ADPMS_SUSPEND: return DRM_MODE_DPMS_SUSPEND; case ADPMS_OFF: return DRM_MODE_DPMS_OFF; default: return -1; } } static void set_device_context(struct dev_node* node) { verbose_print("context_state=device(%"PRIxPTR")", (uintptr_t)node->context); node->eglenv.make_current(node->display, EGL_NO_SURFACE, EGL_NO_SURFACE, node->context); node->context_state = "device"; } static void set_display_context(struct dispout* d) { if (d->buffer.context == EGL_NO_CONTEXT){ verbose_print("context_state=display(%"PRIxPTR")", (uintptr_t)d->device->context); d->device->eglenv.make_current( d->device->display, d->buffer.esurf, d->buffer.esurf, d->device->context); } else { verbose_print("context_state=display(uniq_%"PRIxPTR")", (uintptr_t)d->buffer.context); d->device->eglenv.make_current( d->device->display, d->buffer.esurf, d->buffer.esurf, d->buffer.context); } d->device->context_state = "display"; } /* * Same example as on khronos.org/registry/OpenGL/docs/rules.html */ static bool check_ext(const char* needle, const char* haystack) { const char* cpos = haystack; size_t len = strlen(needle); const char* eoe = haystack + strlen(haystack); while (cpos < eoe){ int n = strcspn(cpos, " "); if (len == n && strncmp(needle, cpos, n) == 0) return true; cpos += (n+1); } return false; } static void dpms_set(struct dispout* d, int level) { uintptr_t tag; cfg_lookup_fun get_config = platform_config_lookup(&tag); if (get_config("video_device_nodpms", 0, NULL, tag)){ return; } /* * FIXME: this needs to be deferred in the same way as disable / etc. */ drmModePropertyPtr prop; debug_print("dpms_set(%d) to %d", d->device->disp_fd, level); for (size_t i = 0; i < d->display.con->count_props; i++){ prop = drmModeGetProperty(d->device->disp_fd, d->display.con->props[i]); if (!prop) continue; if (strcmp(prop->name, "DPMS") == 0){ drmModeConnectorSetProperty(d->device->disp_fd, d->display.con->connector_id, prop->prop_id, level); i = d->display.con->count_props; } drmModeFreeProperty(prop); } } /* * free, dealloc, possibly re-index displays */ static void disable_display(struct dispout*, bool dealloc); /* * assumes that the video pipeline is in a state to safely * blit, will take the mapped objects and schedule buffer transfer */ static bool update_display(struct dispout*); static bool set_dumb_fb(struct dispout* d); static void close_devices(struct dev_node* node) { /* we might have a different device for drawing than for scanout */ int disp_fd = node->disp_fd; if (-1 != disp_fd){ /* the privsep- parent still has the device open in master */ if (node->pathref){ platform_device_release(node->pathref, 0); free(node->pathref); node->pathref = NULL; } close(disp_fd); node->disp_fd = -1; } /* another node might be used for drawing, assumed this does not * actually need master, if that turns out incorrect - duplicate the * pathref parts to drawref as well */ if (node->draw_fd != -1 && node->draw_fd != disp_fd){ close(node->draw_fd); node->draw_fd = -1; } /* render node */ if (node->client_meta.fd != -1){ close(node->client_meta.fd); node->client_meta.fd = -1; } } /* * Assumes that the individual displays allocated on the card have already * been properly disabled(disable_display(ptr, true)) */ static void release_card(size_t i) { if (!nodes[i].active) return; debug_print("release card (%d)", i); nodes[i].eglenv.make_current(nodes[i].display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (nodes[i].context != EGL_NO_CONTEXT){ nodes[i].eglenv.destroy_context(nodes[i].display, nodes[i].context); nodes[i].context = EGL_NO_CONTEXT; } switch (nodes[i].buftype){ case BUF_GBM: if (nodes[i].buffer.gbm){ debug_print("destroying device/gbm buffers"); gbm_device_destroy(nodes[i].buffer.gbm); nodes[i].buffer.gbm = NULL; } break; case BUF_HEADLESS: /* Should be destroyed with the EGL context */ break; case BUF_STREAM: /* Should be destroyed with the EGL context */ break; } close_devices(&nodes[i]); if (nodes[i].display != EGL_NO_DISPLAY){ debug_print("terminating card-egl display"); nodes[i].eglenv.terminate(nodes[i].display); nodes[i].display = EGL_NO_DISPLAY; } nodes[i].context_refc = 0; nodes[i].active = false; } /* the criterion for direct- mapping is a bit weird: * if the backing is entirely GPU based, then we need to juggle / queue buffers. * * The 'refcount' property is somewhat problematic as it means that the backing * store might be locked with scanout while we are also waiting to update it for * another consumer. */ static bool sane_direct_vobj(arcan_vobject* vobj, const char* domain) { debug_print( "direct=%s:sane_direct=%d:vobj=%d:no_txcos=%d:default_prg=%d:2d=%d", domain, (int)(vobj != NULL), (int)(vobj->vstore != NULL), (int)(vobj->txcos == NULL), (int)(!vobj->program || vobj->program == agp_default_shader(BASIC_2D)), (int)(vobj->vstore->txmapped == TXSTATE_TEX2D) ); return vobj && vobj->vstore && !vobj->txcos && (!vobj->program || vobj->program == agp_default_shader(BASIC_2D)) && vobj->vstore->txmapped == TXSTATE_TEX2D; } bool platform_video_map_buffer( struct agp_vstore* vs, struct agp_buffer_plane* planes, size_t n_planes) { if (!nodes[0].eglenv.create_image || !nodes[0].eglenv.image_target_texture2D) return false; struct dev_node* device = &nodes[0]; EGLDisplay dpy = device->display; struct egl_env* egl = &device->eglenv; EGLImage img = helper_dmabuf_eglimage(agp_env(), egl, dpy, planes, n_planes); if (!img){ debug_print("buffer import failed (%s)", egl_errstr()); return false; } /* might have an old eglImage around */ if (0 != vs->vinf.text.tag){ egl->destroy_image(dpy, (EGLImageKHR) vs->vinf.text.tag); } vs->w = planes[0].w; vs->h = planes[0].h; vs->bpp = sizeof(shmif_pixel); vs->txmapped = TXSTATE_TEX2D; agp_activate_vstore(vs); egl->image_target_texture2D(GL_TEXTURE_2D, img); agp_deactivate_vstore(vs); vs->vinf.text.tag = (uintptr_t) img; return true; } void setup_backlight_ledmap() { if (pipe(egl_dri.ledpair) == -1) return; egl_dri.ledid = arcan_led_register(egl_dri.ledpair[1], -1, "backlight", (struct led_capabilities){ .nleds = MAX_DISPLAYS, .variable_brightness = true, .rgb = false }); /* prepare the pipe-pair to be non-block and close-on-exit */ if (-1 == egl_dri.ledid){ close(egl_dri.ledpair[0]); close(egl_dri.ledpair[1]); egl_dri.ledpair[0] = egl_dri.ledpair[1] = -1; } else{ for (size_t i = 0; i < 2; i++){ int flags = fcntl(egl_dri.ledpair[i], F_GETFL); if (-1 != flags) fcntl(egl_dri.ledpair[i], F_SETFL, flags | O_NONBLOCK); flags = fcntl(egl_dri.ledpair[i], F_GETFD); if (-1 != flags) fcntl(egl_dri.ledpair[i], F_SETFD, flags | FD_CLOEXEC); } } } static char* last_err = "unknown"; static size_t err_sz = 0; #define SET_SEGV_MSG(X) last_err = (X); err_sz = sizeof(X); static void sigsegv_errmsg(int sign) { size_t nw __attribute__((unused)); nw = write(STDOUT_FILENO, last_err, err_sz); _exit(EXIT_FAILURE); } static const char* egl_errstr() { if (!egl_dri.last_display) return "No EGL display"; EGLint errc = egl_dri.last_display->device->eglenv.get_error(); switch(errc){ case EGL_SUCCESS: return "Success"; case EGL_NOT_INITIALIZED: return "Not initialize for the specific display connection"; case EGL_BAD_ACCESS: return "Cannot access the requested resource (wrong thread?)"; case EGL_BAD_ALLOC: return "Couldn't allocate resources for the requested operation"; case EGL_BAD_ATTRIBUTE: return "Unrecognized attribute or attribute value"; case EGL_BAD_CONTEXT: return "Context argument does not name a valid context"; case EGL_BAD_CONFIG: return "EGLConfig argument did not match a valid config"; case EGL_BAD_CURRENT_SURFACE: return "Current surface refers to an invalid destination"; case EGL_BAD_DISPLAY: return "The EGLDisplay argument does not match a valid display"; case EGL_BAD_SURFACE: return "EGLSurface argument does not name a valid surface"; case EGL_BAD_MATCH: return "Inconsistent arguments"; case EGL_BAD_PARAMETER: return "Invalid parameter passed to function"; case EGL_BAD_NATIVE_PIXMAP: return "NativePixmapType is invalid"; case EGL_BAD_NATIVE_WINDOW: return "Native Window Type does not refer to a valid window"; case EGL_CONTEXT_LOST: return "Power-management event has forced the context to drop"; default: return "Uknown Error"; } } static int setup_buffers_stream(struct dispout* d) { debug_print("EGLStream, building buffers for display"); if (!d->device->eglenv.create_stream || !d->device->eglenv.query_devices || !d->device->eglenv.query_device_string || !d->device->eglenv.get_platform_display || !d->device->eglenv.get_output_layers || !d->device->eglenv.create_stream || !d->device->eglenv.stream_consumer_output || !d->device->eglenv.create_stream_producer_surface){ debug_print("EGLStreams, buffers failed, missing functions"); return -1; } const char* extstr = d->device->eglenv.query_string(d->device->display, EGL_EXTENSIONS); const char* lastext; if (!check_ext(lastext = "EGL_EXT_output_base", extstr) || !check_ext(lastext = "EGL_EXT_output_drm", extstr) || !check_ext(lastext = "EGL_KHR_stream", extstr) || !check_ext(lastext = "EGL_EXT_stream_consumer_egloutput", extstr) || !check_ext(lastext = "EGL_KHR_stream_producer_eglsurface", extstr)){ debug_print("EGLstreams, couldn't find extension (%s)", lastext); return -1; } EGLAttrib layer_attrs[3] = {}; if (d->display.plane_id){ debug_print("(%d) match layer to drm plane (%d)", d->id, d->display.plane_id); layer_attrs[0] = EGL_DRM_PLANE_EXT; layer_attrs[1] = d->display.plane_id; layer_attrs[2] = EGL_NONE; } else { debug_print("(%d) match layer to crtc (%d)", d->id, d->display.crtc); layer_attrs[0] = EGL_DRM_CRTC_EXT; layer_attrs[1] = d->display.crtc; layer_attrs[2] = EGL_NONE; }; /* * 1. Match output layer to KMS plane */ EGLOutputLayerEXT layer; EGLint n_layers = 0; if (!d->device->eglenv.get_output_layers( d->device->display, layer_attrs, &layer, 1, &n_layers) || !n_layers){ debug_print("EGLstreams, couldn't get output layer for display"); return -1; } /* * 2. Create stream */ EGLint stream_attrs[] = { /* * EGL_STREAM_FIFO_LENGTH_KHR, 1, * EGL_CONSUMER_AUTO_ACQUIRE_EXT, EGL_TRUE, */ EGL_NONE }; d->buffer.stream = d->device->eglenv.create_stream(d->device->display, stream_attrs); if (d->buffer.stream == EGL_NO_STREAM_KHR){ debug_print("EGLstreams - couldn't create output stream"); return -1; } /* * 3. Map stream output */ if (!d->device->eglenv.stream_consumer_output( d->device->display, d->buffer.stream, layer)){ d->device->eglenv.destroy_stream(d->device->display, d->buffer.stream); d->buffer.stream = EGL_NO_STREAM_KHR; debug_print("EGLstreams - couldn't map output stream"); return -1; } /* 4. Get config and context, * two possible variants here - the default: * all displays uses the device context, the 'mixed HDR/SDR' setup is a * different display context that shares parts of the device context */ EGLint nc; if (!d->device->eglenv.choose_config( d->device->display, d->device->attrtbl, &d->buffer.config, 1, &nc)){ debug_print("couldn't chose a configuration (%s)", egl_errstr()); return -1; } const EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; uintptr_t tag; cfg_lookup_fun get_config = platform_config_lookup(&tag); size_t devind = 0; for (; devind < COUNT_OF(nodes); devind++) if (&nodes[devind] == d->device) break; bool shared_dev = !get_config("video_display_context", devind, NULL, tag); /* shared device context, no display context */ if (shared_dev){ debug_print("per-device context for display"); d->buffer.context = EGL_NO_CONTEXT; /* First display on the device re-creates the context, difference to the gbm * version here is that we still need to create the stream-bound surface even * if we have a context on the device. The reason for re-creating the context * is that the config the display needs is un-known at the time of first * creation */ if (!d->device->context_refc){ debug_print("first display on device, rebuild context to match"); d->device->eglenv.destroy_context(d->device->display, d->device->context); d->device->context = d->device->eglenv.create_context( d->device->display, d->buffer.config, EGL_NO_CONTEXT, context_attribs); } d->device->context_refc++; } else { verbose_print("creating NEW context for display with SHARED device context"); d->buffer.context = d->device->eglenv.create_context( d->device->display, d->buffer.config, d->device->context, context_attribs); if (d->buffer.context == NULL) { debug_print("couldn't create display context"); return false; } } /* * 5. Create stream-bound surface */ EGLint surface_attrs[] = { EGL_WIDTH, d->display.mode.hdisplay, EGL_HEIGHT, d->display.mode.vdisplay, EGL_NONE }; d->buffer.esurf = d->device->eglenv.create_stream_producer_surface( d->device->display, d->buffer.config, d->buffer.stream, surface_attrs); if (!d->buffer.esurf){ d->device->eglenv.destroy_stream(d->device->display, d->buffer.stream); d->buffer.stream = EGL_NO_STREAM_KHR; d->device->eglenv.destroy_context(d->device->display, d->buffer.context); d->buffer.context = EGL_NO_CONTEXT; debug_print("EGLstreams - couldn't create output surface"); return -1; } /* * 6. Activate context and buffers */ egl_dri.last_display = d; set_display_context(d); /* * 5. Set synchronization attributes on stream */ if (d->device->eglenv.stream_consumer_acquire_attrib){ EGLAttrib attr[] = { EGL_DRM_FLIP_EVENT_DATA_NV, (EGLAttrib) d, EGL_NONE, }; d->device->eglenv.stream_consumer_acquire_attrib( d->device->display, d->buffer.stream, attr); } /* * 8. make the drm node non-blocking (might be needed for * multiscreen, somewhat uncertain) */ int flags = fcntl(d->device->disp_fd, F_GETFL); if (-1 != flags) fcntl(d->device->disp_fd, F_SETFL, flags | O_NONBLOCK); /* * 8. we don't have a path for streaming the FBO directly, * (seems to be reasonably trivial though), so disable the * optimization for now */ d->force_compose = true; set_device_context(d->device); return 0; } static int setup_buffers_gbm(struct dispout* d) { SET_SEGV_MSG("libgbm(), creating scanout buffer" " failed catastrophically.\n") if (!d->device->eglenv.create_image){ map_eglext_functions(&d->device->eglenv, lookup_call, d->device->eglenv.get_proc_address); } /* preference order with -1 omitted. whatever is user-set on the display will * be added as preference, with the safe-bets at the end */ int gbm_formats[] = { -1, /* 565 */ -1, /* 10-bit, X */ -1, /* 10-bit, A */ -1, /* 64-bpp, XBGR16F */ -1, /* 64-bpp, ABGR16F */ GBM_FORMAT_XRGB8888, GBM_FORMAT_ARGB8888 }; const char* fmt_lbls[] = { "RGB565", "R10G10B10X", "R10G10B10A2", "F16X", "F16A", "xR8G8B8", "A8R8G8B8", }; /* * 10-bit output has very spotty driver support, so only allow it if it has * been explicitly set as interesting - big note is that the creation order * is somewhat fucked, the gbm output buffer defines the configuration of * the EGL node, note the other way around. */ if (d->output_format == OUTPUT_LOW){ gbm_formats[0] = GBM_FORMAT_RGB565; } if (d->output_format == OUTPUT_DEEP){ gbm_formats[1] = GBM_FORMAT_XRGB2101010; gbm_formats[2] = GBM_FORMAT_ARGB2101010; } else if (d->output_format == OUTPUT_HDR){ /* older distributions may still carry a header without this one so go the * preprocessor route for enabling */ #ifdef GBM_FORMAT_XBGR16161616F gbm_formats[3] = GBM_FORMAT_XBGR16161616F; gbm_formats[4] = GBM_FORMAT_ABGR16161616F; #endif } /* first get the set of configs from the display */ EGLint nc; d->device->eglenv.get_configs(d->device->display, NULL, 0, &nc); if (nc < 1){ debug_print("no configurations found for display, (%s)", egl_errstr()); return false; } EGLConfig configs[nc]; EGLint match = 0; /* filter them based on the desired attributes from the device itself */ d->device->eglenv.choose_config( d->device->display, d->device->attrtbl, configs, nc, &match); if (!match) return -1; /* then sweep the formats in desired order and look for a matching visual */ for (size_t i = 0; i < COUNT_OF(gbm_formats); i++){ if (gbm_formats[i] == -1) continue; bool got_config = false; for (size_t j = 0; j < nc && !got_config; j++){ EGLint id; if (!d->device->eglenv.get_config_attrib( d->device->display, configs[j], EGL_NATIVE_VISUAL_ID, &id)) continue; if (id == gbm_formats[i]){ d->buffer.config = configs[j]; got_config = true; } } if (!got_config){ debug_print("no matching gbm-format <-> visual " "<-> egl config for fmt: %d", (int)gbm_formats[i]); continue; } /* first time device setup will call this function in two stages, so there * might be a buffer already set when we get called the second time and it is * safe to actually bind the buffer to an EGL surface as the config should be * the right one */ if (!d->buffer.surface) d->buffer.surface = gbm_surface_create(d->device->buffer.gbm, d->display.mode.hdisplay, d->display.mode.vdisplay, gbm_formats[i], GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING); if (!d->buffer.surface) continue; if (!d->buffer.esurf) d->buffer.esurf = d->device->eglenv.create_window_surface( d->device->display, d->buffer.config,(uintptr_t)d->buffer.surface,NULL); /* we can accept buffer setup failure in this stage if we are being init:ed * and the EGL configuration doesn't exist */ if (d->buffer.esurf != EGL_NO_SURFACE){ d->buffer.format = gbm_formats[i]; debug_print("(gbm) picked output buffer format %s", fmt_lbls[i]); break; } gbm_surface_destroy(d->buffer.surface); d->buffer.surface = NULL; } if (!d->buffer.surface){ debug_print("couldn't find a gbm buffer format matching EGL display"); return -1; } /* finally, build the display- specific context with the new surface and * context - might not always use it due to direct-scanout vs. shaders etc. * but still needed */ EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; /* * Unfortunately there seem to be many strange driver issues with using a * headless shared context and doing the buffer swaps and scanout on the * others, we can solve that in two ways, one is simply force even the WORLDID * to be a FBO - which was already the default in the lwa backend. This would * require making the vint_world() RT double-buffered with a possible 'do I * have a non-default shader' extra blit stage and then the drm_add_fb2 call. * It's a scanout path that we likely need anyway. */ uintptr_t tag; cfg_lookup_fun get_config = platform_config_lookup(&tag); size_t devind = 0; for (; devind < COUNT_OF(nodes); devind++) if (&nodes[devind] == d->device) break; /* DEFAULT: per device context: let first display drive choice of config * and hope that other displays have the same preferred format */ bool shared_dev = !get_config("video_display_context", devind, NULL, tag); if (shared_dev && d->device->context_refc > 0){ d->buffer.context = EGL_NO_CONTEXT; egl_dri.last_display = d; set_device_context(d->device); d->device->context_refc++; return 0; } /* LEGACY: EGL_NO_CONFIG_KHR used to point to d->buffer.config but now * assumes that we can create contexts where any valid surface is also * valid as the context format */ EGLContext device = shared_dev ? NULL : d->device->context; EGLContext context = d->device->eglenv.create_context( d->device->display, EGL_NO_CONFIG_KHR, device, context_attribs ); if (!context){ debug_print("couldn't create egl context for display"); gbm_surface_destroy(d->buffer.surface); return -1; } set_device_context(d->device); /* per device context */ if (shared_dev){ d->buffer.context = EGL_NO_CONTEXT; d->device->context_refc++; d->device->eglenv.destroy_context(d->device->display, d->device->context); d->device->context = context; egl_dri.last_display = d; return 0; } /* per display (not EGLDisplay) context */ d->buffer.context = context; egl_dri.last_display = d; set_device_context(d->device); return 0; } static int setup_buffers(struct dispout* d) { switch (d->device->buftype){ case BUF_GBM: return setup_buffers_gbm(d); break; case BUF_HEADLESS: /* won't be needed, we only ever accept FBO management */ return 0; break; case BUF_STREAM: return setup_buffers_stream(d); break; } return 0; } size_t platform_video_displays(platform_display_id* dids, size_t* lim) { size_t rv = 0; for (size_t i = 0; i < MAX_DISPLAYS; i++){ if (displays[i].state == DISP_UNUSED) continue; if (dids && lim && *lim < rv) dids[rv] = i; rv++; } if (lim) *lim = MAX_DISPLAYS; return rv; } int platform_video_cardhandle(int cardn, int* buffer_method, size_t* metadata_sz, uint8_t** metadata) { if (cardn < 0 || cardn > COUNT_OF(nodes)) return -1; if (metadata_sz && metadata && nodes[cardn].eglenv.query_dmabuf_formats && nodes[cardn].eglenv.query_dmabuf_modifiers){ *metadata_sz = 0; *metadata = NULL; } else if (metadata_sz && metadata){ debug_print("no format/modifiers query support, sending simple card"); *metadata_sz = 0; *metadata = NULL; } if (buffer_method) *buffer_method = nodes[cardn].buftype; return nodes[cardn].client_meta.fd; } static bool realloc_buffers(struct dispout* d) { switch (d->device->buftype){ case BUF_GBM: gbm_surface_destroy(d->buffer.surface); d->buffer.surface = NULL; if (setup_buffers_gbm(d) != 0) return false; break; case BUF_HEADLESS: break; case BUF_STREAM: d->device->eglenv.destroy_stream(d->device->display, d->buffer.stream); d->buffer.stream = EGL_NO_STREAM_KHR; if (setup_buffers_stream(d) != 0) return false; break; } return true; } static float deadline_for_display(struct dispout* d) { /* [FIX-VRR: the actual target including 'slew' rate stepping should be * presented / calculated here based on the last synch, the * target and the stepping rate ] */ return 1000.0f / (float) (d->display.mode.vrefresh ? d->display.mode.vrefresh : 60.0); } bool platform_video_set_mode(platform_display_id disp, platform_mode_id mode, struct platform_mode_opts opts) { struct dispout* d = get_display(disp); if (!d || d->state != DISP_MAPPED || mode >= d->display.con->count_modes) if (memcmp(&d->display.mode, &d->display.con->modes[mode], sizeof(drmModeModeInfo)) == 0) return true; d->display.reset_mode = true; d->display.mode = d->display.con->modes[mode]; d->display.mode_set = mode; /* changes to the output format are reflected first in rebuild_buffers, if that * fails (e.g. the buffers do not fit the qualities of the display) it reverts * back to whatever OUTPUT_DEFAULT is set to rather than failing */ switch(opts.depth){ case VSTORE_HINT_LODEF: d->output_format = OUTPUT_LOW; break; case VSTORE_HINT_HIDEF: d->output_format = OUTPUT_DEEP; break; case VSTORE_HINT_F16: case VSTORE_HINT_F32: d->output_format = OUTPUT_HDR; break; default: d->output_format = OUTPUT_DEFAULT; } uint64_t pid; if (lookup_drm_propval(d->device->disp_fd, d->display.crtc, DRM_MODE_OBJECT_CRTC, "type", &pid, true)){ debug_print("setting_vrr: %f", opts.vrr); drmModeObjectSetProperty(d->device->disp_fd, d->display.crtc, DRM_MODE_OBJECT_CRTC, pid, fabs(opts.vrr) > EPSILON); } else debug_print("vrr_ignored:missing_vrr_property"); /* ATOMIC test goes here */ debug_print("(%d) schedule mode switch to %zu * %zu", (int) disp, d->display.mode.hdisplay, d->display.mode.vdisplay); build_orthographic_matrix(d->projection, 0, d->display.mode.hdisplay, d->display.mode.vdisplay, 0, 0, 1); d->dispw = d->display.mode.hdisplay; d->disph = d->display.mode.vdisplay; /* * reset scanout buffers to match new crtc mode if (d->buffer.cur_bo){ gbm_surface_release_buffer(d->buffer.surface, d->buffer.cur_bo); d->buffer.cur_bo = NULL; } if (d->buffer.next_bo){ gbm_surface_release_buffer(d->buffer.surface, d->buffer.next_bo); d->buffer.next_bo = NULL; } */ /* the BOs should die with the surface */ debug_print("modeset, destroy surface"); d->state = DISP_CLEANUP; d->device->eglenv.destroy_surface(d->device->display, d->buffer.esurf); d->buffer.esurf = EGL_NO_SURFACE; /* * drop current framebuffers if (d->buffer.cur_fb){ drmModeRmFB(d->device->fd, d->buffer.cur_fb); d->buffer.cur_fb = 0; } if(d->buffer.next_fb){ drmModeRmFB(d->device->fd, d->buffer.next_fb); d->buffer.next_fb = 0; } */ /* * setup / allocate a new set of buffers that match the new mode */ if (!realloc_buffers(d)) return false; d->state = DISP_MAPPED; return true; } bool platform_video_set_display_gamma(platform_display_id did, size_t n_ramps, uint16_t* r, uint16_t* g, uint16_t* b) { struct dispout* d = get_display(did); if (!d) return false; drmModeCrtc* inf = drmModeGetCrtc(d->device->disp_fd, d->display.crtc); if (!inf) return false; int rv = -1; if (inf->gamma_size > 0 && n_ramps == inf->gamma_size){ /* first time we get called, saved trhe original gamma for the display * so that we can restore it when the display gets deallocated */ if (!d->display.orig_gamma){ if (!platform_video_get_display_gamma(did, &d->display.gamma_size, &d->display.orig_gamma)){ drmModeFreeCrtc(inf); return false; } } rv = drmModeCrtcSetGamma(d->device->disp_fd, d->display.crtc, n_ramps, r, g, b); } drmModeFreeCrtc(inf); return rv == 0; } bool platform_video_get_display_gamma( platform_display_id did, size_t* n_ramps, uint16_t** outb) { struct dispout* d = get_display(did); if (!d || !n_ramps) return false; drmModeCrtc* inf = drmModeGetCrtc(d->device->disp_fd, d->display.crtc); if (!inf) return false; if (inf->gamma_size <= 0){ drmModeFreeCrtc(inf); return false; } *n_ramps = inf->gamma_size; uint16_t* ramps = malloc(*n_ramps * 3 * sizeof(uint16_t)); if (!ramps){ drmModeFreeCrtc(inf); return false; } bool rv = true; memset(ramps, '\0', *n_ramps * 3 * sizeof(uint16_t)); if (drmModeCrtcGetGamma(d->device->disp_fd, d->display.crtc, *n_ramps, &ramps[0], &ramps[*n_ramps], &ramps[2 * *n_ramps])){ free(ramps); rv = false; } *outb = ramps; drmModeFreeCrtc(inf); return rv; } static drmModePropertyPtr get_connector_property( struct dispout* d, const char* name, size_t* i) { for (; *i < d->display.con->count_props; *i++){ drmModePropertyPtr prop = drmModeGetProperty(d->device->disp_fd, d->display.con->props[*i]); if (!prop) continue; if (strcmp(prop->name, name) == 0) return prop; drmModeFreeProperty(prop); } return NULL; } static void fetch_edid(struct dispout* d) { drmModePropertyPtr prop; bool done = false; /* stick with the cached one */ if (d->display.edid_blob){ return; } for (size_t i = 0; i < d->display.con->count_props && !done; i++){ prop = drmModeGetProperty(d->device->disp_fd, d->display.con->props[i]); if (!prop) continue; if (!(prop->flags&DRM_MODE_PROP_BLOB) || strcmp(prop->name, "EDID") != 0){ drmModeFreeProperty(prop); continue; } drmModePropertyBlobPtr blob = drmModeGetPropertyBlob( d->device->disp_fd, d->display.con->prop_values[i]); if (!blob || (int)blob->length <= 0){ drmModeFreeProperty(prop); continue; } if ((d->display.edid_blob = malloc(blob->length))){ d->display.blob_sz = blob->length; memcpy(d->display.edid_blob, blob->data, blob->length); done = true; } drmModeFreePropertyBlob(blob); drmModeFreeProperty(prop); } } bool platform_video_display_edid( platform_display_id did, char** out, size_t* sz) { struct dispout* d = get_display(did); if (!d || d->state == DISP_UNUSED) return false; *out = NULL; *sz = 0; /* attempt to re-acquire the blob */ fetch_edid(d); /* allocate a new scratch copy of the cached blob */ if (d->display.edid_blob){ *sz = 0; *out = malloc(d->display.blob_sz); if (*out){ *sz = d->display.blob_sz; memcpy(*out, d->display.edid_blob, d->display.blob_sz); } return true; } return false; } /* * this platform does not currently support dynamic modes * (this should well be possible for old CRTs though).. */ bool platform_video_specify_mode( platform_display_id disp, struct monitor_mode mode) { return false; } static void drm_mode_tos(FILE* dst, unsigned val) { if ( (val & DRM_MODE_TYPE_BUILTIN) > 0){ fprintf(dst, "/builtin"); val &= ~DRM_MODE_TYPE_BUILTIN; } if ( (val & DRM_MODE_TYPE_CLOCK_C) > 0){ fprintf(dst, "/clock"); val &= ~DRM_MODE_TYPE_CLOCK_C; } if ( (val & DRM_MODE_TYPE_CRTC_C) > 0){ fprintf(dst, "/crtc"); val &= ~DRM_MODE_TYPE_CRTC_C; } if ( (val & DRM_MODE_TYPE_PREFERRED) > 0){ fprintf(dst, "/preferred"); val &= ~DRM_MODE_TYPE_PREFERRED; } if ( (val & DRM_MODE_TYPE_DEFAULT) > 0){ fprintf(dst, "/default"); val &= ~DRM_MODE_TYPE_DEFAULT; } if ( (val & DRM_MODE_TYPE_USERDEF) > 0){ fprintf(dst, "/userdef"); val &= ~DRM_MODE_TYPE_USERDEF; } if ( (val & DRM_MODE_TYPE_DRIVER) > 0){ fprintf(dst, "/driver"); val &= ~DRM_MODE_TYPE_DRIVER; } if ( val > 0 ) fprintf(dst, "/unknown(%d)", (int)val); } static void drm_mode_flag(FILE* dst, unsigned val) { if ( (val & DRM_MODE_FLAG_PHSYNC) > 0 ){ fprintf(dst, "/phsync"); val &= ~DRM_MODE_FLAG_PHSYNC; } if ( (val & DRM_MODE_FLAG_NHSYNC) > 0 ){ fprintf(dst, "/nhsync"); val &= ~DRM_MODE_FLAG_NHSYNC; } if ( (val & DRM_MODE_FLAG_PVSYNC) > 0 ){ fprintf(dst, "/pvsync"); val &= ~DRM_MODE_FLAG_PVSYNC; } if ( (val & DRM_MODE_FLAG_NVSYNC) > 0 ){ fprintf(dst, "/nvsync"); val &= ~DRM_MODE_FLAG_NVSYNC; } if ( (val & DRM_MODE_FLAG_INTERLACE) > 0 ){ fprintf(dst, "/interlace"); val &= ~DRM_MODE_FLAG_INTERLACE; } if ( (val & DRM_MODE_FLAG_DBLSCAN) > 0 ){ fprintf(dst, "/dblscan"); val &= ~DRM_MODE_FLAG_DBLSCAN; } if ( (val & DRM_MODE_FLAG_CSYNC) > 0 ){ fprintf(dst, "/csync"); val &= ~DRM_MODE_FLAG_CSYNC; } if ( (val & DRM_MODE_FLAG_PCSYNC) > 0 ){ fprintf(dst, "/pcsync"); val &= ~DRM_MODE_FLAG_PCSYNC; } if ( (val & DRM_MODE_FLAG_NCSYNC) > 0 ){ fprintf(dst, "/ncsync"); val &= ~DRM_MODE_FLAG_NCSYNC; } if ( (val & DRM_MODE_FLAG_HSKEW) > 0 ){ fprintf(dst, "/hskew"); val &= ~DRM_MODE_FLAG_HSKEW; } if ( (val & DRM_MODE_FLAG_BCAST) > 0 ){ fprintf(dst, "/bcast"); val &= ~DRM_MODE_FLAG_BCAST; } if ( (val & DRM_MODE_FLAG_PIXMUX) > 0 ){ fprintf(dst, "/pixmux"); val &= ~DRM_MODE_FLAG_PIXMUX; } if ( (val & DRM_MODE_FLAG_DBLCLK) > 0 ){ fprintf(dst, "/dblclk"); val &= ~DRM_MODE_FLAG_DBLCLK; } if ( (val & DRM_MODE_FLAG_CLKDIV2) > 0 ){ fprintf(dst, "/clkdiv2"); val &= ~DRM_MODE_FLAG_CLKDIV2; } if ( (val & DRM_MODE_FLAG_3D_MASK) > 0 ){ fprintf(dst, "/3dmask"); val &= ~DRM_MODE_FLAG_3D_MASK; } if ( (val & DRM_MODE_FLAG_3D_NONE) > 0 ){ fprintf(dst, "/3dnone"); val &= ~DRM_MODE_FLAG_3D_NONE; } if ( (val & DRM_MODE_FLAG_3D_FRAME_PACKING) > 0 ){ fprintf(dst, "/3dframep"); val &= ~DRM_MODE_FLAG_3D_FRAME_PACKING; } if ( (val & DRM_MODE_FLAG_3D_FIELD_ALTERNATIVE) > 0 ){ fprintf(dst, "/3dfield_alt"); val &= ~DRM_MODE_FLAG_3D_FIELD_ALTERNATIVE; } if ( (val & DRM_MODE_FLAG_3D_LINE_ALTERNATIVE) > 0 ){ fprintf(dst, "/3dline_alt"); val &= ~DRM_MODE_FLAG_3D_LINE_ALTERNATIVE; } if ( (val & DRM_MODE_FLAG_3D_SIDE_BY_SIDE_FULL) > 0 ){ fprintf(dst, "/3dsbs"); val &= ~DRM_MODE_FLAG_3D_SIDE_BY_SIDE_FULL; } if ( (val & DRM_MODE_FLAG_3D_L_DEPTH) > 0 ){ fprintf(dst, "/3dldepth"); val &= ~DRM_MODE_FLAG_3D_L_DEPTH; } if ( (val & DRM_MODE_FLAG_3D_L_DEPTH_GFX_GFX_DEPTH) > 0 ){ fprintf(dst, "/3dldepth_gfx2_depth"); val &= ~DRM_MODE_FLAG_3D_L_DEPTH_GFX_GFX_DEPTH; } if ( (val & DRM_MODE_FLAG_3D_TOP_AND_BOTTOM) > 0 ){ fprintf(dst, "/3dt&b"); val &= ~DRM_MODE_FLAG_3D_TOP_AND_BOTTOM; } if ( (val & DRM_MODE_FLAG_3D_SIDE_BY_SIDE_HALF) > 0 ){ fprintf(dst, "/3dsbs-h"); val &= ~DRM_MODE_FLAG_3D_SIDE_BY_SIDE_HALF; } if (val > 0){ fprintf(dst, "/unknown(%d)", (int) val); } } static void drm_mode_connector(FILE* fpek, int val) { switch(val){ case DRM_MODE_CONNECTOR_Unknown: fprintf(fpek, "unknown"); break; case DRM_MODE_CONNECTOR_VGA: fprintf(fpek, "vga"); break; case DRM_MODE_CONNECTOR_DVII: fprintf(fpek, "dvii"); break; case DRM_MODE_CONNECTOR_DVID: fprintf(fpek, "dvid"); break; case DRM_MODE_CONNECTOR_DVIA: fprintf(fpek, "dvia"); break; case DRM_MODE_CONNECTOR_Composite: fprintf(fpek, "composite"); break; case DRM_MODE_CONNECTOR_SVIDEO: fprintf(fpek, "s-video"); break; case DRM_MODE_CONNECTOR_Component: fprintf(fpek, "component"); break; case DRM_MODE_CONNECTOR_9PinDIN: fprintf(fpek, "9-pin din"); break; case DRM_MODE_CONNECTOR_DisplayPort: fprintf(fpek, "displayPort"); break; case DRM_MODE_CONNECTOR_HDMIA: fprintf(fpek, "hdmi-a"); break; case DRM_MODE_CONNECTOR_HDMIB: fprintf(fpek, "hdmi-b"); break; case DRM_MODE_CONNECTOR_TV: fprintf(fpek, "tv"); break; case DRM_MODE_CONNECTOR_eDP: fprintf(fpek, "eDP"); break; default: fprintf(fpek, "unknown"); } } /* should be passed on to font rendering */ static const char* subpixel_type(int val) { switch(val){ case DRM_MODE_SUBPIXEL_UNKNOWN: return "unknown"; case DRM_MODE_SUBPIXEL_HORIZONTAL_RGB: return "horiz- RGB"; case DRM_MODE_SUBPIXEL_HORIZONTAL_BGR: return "horiz- BGR"; case DRM_MODE_SUBPIXEL_VERTICAL_RGB: return "vert- RGB"; case DRM_MODE_SUBPIXEL_VERTICAL_BGR: return "vert- BGR"; default: return "unsupported"; } } static const char* connection_type(int conn) { switch(conn){ case DRM_MODE_CONNECTED: return "connected"; case DRM_MODE_DISCONNECTED: return "not connected"; case DRM_MODE_UNKNOWNCONNECTION: return "unknown"; default: return "undefined"; } } static void dump_connectors(FILE* dst, struct dev_node* node, bool shorth) { drmModeRes* res = drmModeGetResources(node->disp_fd); if (!res){ fprintf(dst, "DRM dump, couldn't acquire resource list\n"); return; } fprintf(dst, "DRM Dump: \n\tConnectors: %d\n", res->count_connectors); for (size_t i = 0; i < res->count_connectors; i++){ drmModeConnector* conn = drmModeGetConnector( node->disp_fd, res->connectors[i]); if (!conn) continue; fprintf(dst, "\t(%d), id:(%d), encoder:(%d), type: ", (int) i, conn->connector_id, conn->encoder_id); drm_mode_connector(dst, conn->connector_type); fprintf(dst, " phy(%d * %d), mode: %s, hinting: %s\n", (int)conn->mmWidth, (int)conn->mmHeight, connection_type(conn->connection), subpixel_type(conn->subpixel)); if (!shorth) for (size_t j = 0; j < conn->count_modes; j++){ fprintf(dst, "\t\t Mode (%d:%s): clock@%d, refresh@%d\n\t\tflags : ", (int)j, conn->modes[j].name, conn->modes[j].clock, conn->modes[j].vrefresh ) ; drm_mode_flag(dst, conn->modes[j].flags); fprintf(dst, " type : "); drm_mode_tos(dst, conn->modes[j].type); } fprintf(dst, "\n\n"); drmModeFreeConnector(conn); } drmModeFreeResources(res); } /* * first: successful setup_node[egl or gbm], then setup_node */ static bool setup_node(struct dev_node* node) { EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE, /* pad for robustness */ EGL_NONE, /* pad for robustness */ EGL_NONE, /* pad for strategy */ EGL_NONE, /* pad for strategy */ EGL_NONE, /* pad for PRIORITY */ EGL_NONE, /* pad for PRIORTIY */ EGL_NONE }; int ca_offset = 2; EGLint apiv; const char* ident = agp_ident(); EGLint attrtbl[24] = { EGL_RENDERABLE_TYPE, 0, EGL_RED_SIZE, 5, EGL_GREEN_SIZE, 6, EGL_BLUE_SIZE, 5, EGL_ALPHA_SIZE, 0, EGL_DEPTH_SIZE, 1, EGL_STENCIL_SIZE, 1, /* this only allows the context to return CONFIGs with floating point outputs, * the actual selection of such a config happens based on the GBM surface type * in setup_buffers */ EGL_COLOR_COMPONENT_TYPE_EXT, EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT, }; int attrofs = 14; switch (node->buftype){ case BUF_GBM: attrtbl[attrofs++] = EGL_SURFACE_TYPE; attrtbl[attrofs++] = EGL_WINDOW_BIT; break; case BUF_HEADLESS: break; case BUF_STREAM: attrtbl[attrofs++] = EGL_SURFACE_TYPE; attrtbl[attrofs++] = EGL_STREAM_BIT_KHR; break; } attrtbl[attrofs++] = EGL_NONE; /* right now, this platform won't support anything that isn't rendering using * xGL,VK/EGL which will be a problem for a software based AGP. When we get * one, we need a fourth scanout path (ffs) where the worldid rendertarget * writes into the scanout buffer immediately. It might be possibly for that * AGP to provide a 'faux' EGL implementation though. There also is the path * in here already for special calls to map_video_display via dumb buffers so * the best way forward is probably the fake EGL one. */ size_t i = 0; bool gles = true; if (strcmp(ident, "OPENGL21") == 0){ apiv = EGL_OPENGL_API; for (i = 0; attrtbl[i] != EGL_RENDERABLE_TYPE; i++); attrtbl[i+1] = EGL_OPENGL_BIT; gles = false; } else if (strcmp(ident, "GLES3") == 0 || strcmp(ident, "GLES2") == 0){ for (i = 0; attrtbl[i] != EGL_RENDERABLE_TYPE; i++); #ifndef EGL_OPENGL_ES2_BIT debug_print("EGL implementation do not support GLESv2, " "yet AGP platform requires it, use a different AGP platform."); return false; #endif #ifndef EGL_OPENGL_ES3_BIT #define EGL_OPENGL_ES3_BIT EGL_OPENGL_ES2_BIT #endif attrtbl[i+1] = EGL_OPENGL_ES3_BIT; apiv = EGL_OPENGL_ES_API; } else return false; SET_SEGV_MSG("EGL-dri(), getting the display failed\n"); if (!node->eglenv.initialize(node->display, NULL, NULL)){ debug_print("failed to initialize EGL"); return false; } /* * make sure the API we've selected match the AGP platform */ if (!node->eglenv.bind_api(apiv)){ debug_print("couldn't bind GL API"); return false; } /* * now copy the attributes that match our choice in API etc. so that the * correct buffers can be selected */ memcpy(node->attrtbl, attrtbl, sizeof(attrtbl)); EGLint match = 0; node->eglenv.choose_config(node->display, node->attrtbl, &node->config, 1, &match); node->context = node->eglenv.create_context( node->display, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, context_attribs); bool priority = false; const char* extstr = node->eglenv.query_string(node->display, EGL_EXTENSIONS); if (check_ext("EGL_IMG_context_priority", extstr)){ priority = true; context_attribs[ca_offset++] = EGL_CONTEXT_PRIORITY_LEVEL_IMG; context_attribs[ca_offset++] = EGL_CONTEXT_PRIORITY_HIGH_IMG; } /* Context creation can fail on an unavailable high priority level - then * try to downgrade and try again */ if (!node->context && priority){ context_attribs[--ca_offset] = EGL_NONE; context_attribs[--ca_offset] = EGL_NONE; node->context = node->eglenv.create_context( node->display, EGL_NO_CONFIG_KHR, node, context_attribs); } if (!node->context){ debug_print( "couldn't build an EGL context on the display, (%s)", egl_errstr()); return false; } set_device_context(node); return true; } /* * We have a circular dependency problem here "kind of": we need to know what * driver to pick in order to setup EGL for the node, but there is also an Egl * function to setup the EGLDevice from which we can get the node. For most * cases the driver would just resolve to a GLvnd implementation anyhow, but we * don't know and don't want the restriction when it comes to switching between * test drivers etc. */ static int setup_node_egl(int dst_ind, struct dev_node* node, int fd) { if (!node->eglenv.query_string){ debug_print("EGLStreams, couldn't get EGL extension string"); return -1; } const char* extstr = node->eglenv.query_string(EGL_NO_DISPLAY,EGL_EXTENSIONS); const char* lastext; if (!check_ext(lastext = "EGL_EXT_platform_base", extstr)){ debug_print("EGLStreams, missing extension (%s)", lastext); return -1; } if (!node->eglenv.query_devices){ debug_print("EGLStreams, couldn't find extensions/functions"); return -1; } EGLint numdev; if (!node->eglenv.query_devices(0, NULL, &numdev) || numdev < 1){ debug_print("EGLStreams, query failed or no devices found"); return -1; } EGLDeviceEXT devs[numdev]; if (!node->eglenv.query_devices(numdev, devs, &numdev)){ debug_print("EGLStreams, couldn't query device data"); return -1; } /* * sweep all devices that matches and expose the necessary extensions and pick * the first one (should possibly change that to a counter for multiple cards * again) or, if fd is provided, the one with stat data that match. */ bool found = false; for (size_t i = 0; i < numdev && !found; i++){ const char* ext = node->eglenv.query_device_string(devs[i], EGL_EXTENSIONS); if (!check_ext(lastext = "EGL_EXT_device_drm", ext)) continue; const char* fn = node->eglenv.query_device_string(devs[i], EGL_DRM_DEVICE_FILE_EXT); if (!fn) continue; int lfd = platform_device_open(fn, O_RDWR); /* no caller provided device, go with the one we found */ if (-1 == fd){ fd = lfd; found = true; node->buffer.egldev = devs[i]; } /* we we want to pair the incoming descriptor with the suggested one */ else { struct stat s1, s2; if (-1 == fstat(lfd, &s2) || -1 == fstat(fd, &s1) || s1.st_ino != s2.st_ino || s1.st_dev != s2.st_dev){ close(lfd); } else{ found = true; node->buffer.egldev = devs[i]; } } } /* * 1:1 for card-node:egldisplay might not be correct for the setup here * (with normal GBM, this doesn't really matter that much as we have * finer control over buffer scanout) */ if (found){ EGLint attribs[] = {EGL_DRM_MASTER_FD_EXT, fd, EGL_NONE}; node->disp_fd = node->draw_fd = fd; node->buftype = BUF_STREAM; node->atomic = drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) == 0 && drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1) == 0; node->display = node->eglenv.get_platform_display( EGL_PLATFORM_DEVICE_EXT, node->buffer.egldev, attribs); node->eglenv.swap_interval(node->display, 0); return 0; } return -1; } static void cleanup_node_gbm(struct dev_node* node) { close_devices(node); if (node->buffer.gbm) gbm_device_destroy(node->buffer.gbm); node->buffer.gbm = NULL; } static int setup_node_gbm(int devind, struct dev_node* node, int draw_fd, int disp_fd) { SET_SEGV_MSG("libdrm(), open device failed (check permissions) " " or use ARCAN_VIDEO_DEVICE environment.\n"); node->client_meta.fd = -1; node->client_meta.metadata = NULL; node->client_meta.metadata_sz = 0; node->disp_fd = disp_fd; node->draw_fd = draw_fd; SET_SEGV_MSG("libgbm(), create device failed catastrophically.\n"); node->buffer.gbm = gbm_create_device(node->draw_fd); if (!node->buffer.gbm){ debug_print("gbm, couldn't create gbm device on node"); cleanup_node_gbm(node); return -1; } node->buftype = BUF_GBM; if (node->eglenv.get_platform_display){ debug_print("gbm, using eglGetPlatformDisplayEXT"); node->display = node->eglenv.get_platform_display( EGL_PLATFORM_GBM_KHR, (void*)(node->buffer.gbm), NULL); } else{ debug_print("gbm, building display using native handle only"); node->display = node->eglenv.get_display((void*)(node->buffer.gbm)); } /* This is kept optional as not all drivers have it, and not all drivers * work well with it. The current state is opt-in at a certain cost, but * don't want the bug reports. */ uintptr_t tag; cfg_lookup_fun get_config = platform_config_lookup(&tag); char* devstr, (* cfgstr), (* altstr); node->atomic = get_config("video_device_atomic", devind, NULL, tag) && ( drmSetClientCap(node->disp_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) == 0 && drmSetClientCap(node->disp_fd, DRM_CLIENT_CAP_ATOMIC, 1) == 0 ); debug_print("gbm, node in atomic mode: %s", node->atomic ? "yes" : "no"); /* Set the render node environment variable here, this is primarily for legacy * clients that gets launched through arcan - the others should get the * descriptor from DEVICEHINT. It also won't work for multiple cards as the * last one would just overwrite */ char pbuf[24] = "/dev/dri/renderD128"; char* rdev = drmGetRenderDeviceNameFromFd(node->draw_fd); if (rdev){ debug_print("derived render-node: %s", rdev); node->client_meta.fd = open(rdev, O_RDWR | O_CLOEXEC); if (-1 != node->client_meta.fd) setenv("ARCAN_RENDER_NODE", rdev, 1); free(rdev); } /* If this fails for some reason (e.g. libdrm packaging on OpenBSD, then * try to fallback to a hardcoded default */ if (-1 == node->client_meta.fd){ setenv("ARCAN_RENDER_NODE", pbuf, 1); node->client_meta.fd = open(pbuf, O_RDWR | O_CLOEXEC); } return 0; } /* * foreach prop on object(id:type): * foreach modprob on prop: * found if name matches modprob -> true:set_val */ static bool lookup_drm_propval(int fd, uint32_t oid, uint32_t otype, const char* name, uint64_t* val, bool id) { drmModeObjectPropertiesPtr oprops = drmModeObjectGetProperties(fd, oid, otype); for (size_t i = 0; i < oprops->count_props; i++){ drmModePropertyPtr mprops = drmModeGetProperty(fd, oprops->props[i]); if (!mprops) continue; if (strcmp(name, mprops->name) == 0){ if (id){ *val = mprops->prop_id; } else *val = oprops->prop_values[i]; drmModeFreeObjectProperties(oprops); drmModeFreeProperty(mprops); return true; } drmModeFreeProperty(mprops); } drmModeFreeObjectProperties(oprops); return false; } /* * called once per updated display per frame, as part of the normal * draw / flip / ... cycle, bo is the returned gbm_surface_lock_front */ static int get_gbm_fb(struct dispout* d, enum display_update_state dstate, struct gbm_bo* bo, uint32_t* dst) { uint32_t new_fb; /* convert the currently mapped object */ if (dstate == UPDATE_DIRECT){ arcan_vobject* vobj = arcan_video_getobject(d->vid); struct rendertarget* newtgt = arcan_vint_findrt(vobj); if (!newtgt) return -1; /* though the rendertarget might not be ready for the first frame */ bool swap; struct agp_vstore* vs = agp_rendertarget_swap(newtgt->art, &swap); if (!swap){ verbose_print("(%d) no-swap on rtgt", d->id); return 0; } if (!vs->vinf.text.handle){ TRACE_MARK_ONESHOT( "egl-dri", "rendertarget-swap", TRACE_SYS_ERROR, 0, 0, "no allocator handle"); return -1; } struct shmifext_color_buffer* buf = (struct shmifext_color_buffer*) vs->vinf.text.handle; /* Now buf represents what we want, but it might still be pending - so create a fence * if we have fencing enabled and flush-out. */ if (d->device->explicit_synch){ d->buffer.synch = d->device->eglenv.create_synch(d->device->display, EGL_SYNC_NATIVE_FENCE_ANDROID, (EGLint[]){ EGL_SYNC_NATIVE_FENCE_FD_ANDROID, EGL_NO_NATIVE_FENCE_FD_ANDROID, EGL_NONE} ); } struct agp_fenv* env = agp_env(); env->flush(); bo = (struct gbm_bo*) buf->alloc_tags[0]; TRACE_MARK_ONESHOT("egl-dri", "rendertarget-swap", TRACE_SYS_DEFAULT, 0, 0, ""); } if (!bo){ TRACE_MARK_ONESHOT("egl-dri", "vobj-bo-fail", TRACE_SYS_DEFAULT, d->vid, 0, ""); return -1; } /* Three possible paths for getting the framebuffer id that can then be * scanned out: drmModeAddFB2WithModifiers, drmModeAddFB2 and drmModeAddFB * success rate depend on driver and overall config */ ssize_t n_planes = gbm_bo_get_plane_count(bo); if (n_planes < 0) n_planes = 1; uint32_t handles[n_planes]; uint32_t strides[n_planes]; uint32_t offsets[n_planes]; uint64_t modifiers[n_planes]; TRACE_MARK_ONESHOT("egl-dri", "bo-gbm-planes", TRACE_SYS_DEFAULT, n_planes, 0, ""); if (gbm_bo_get_handle_for_plane(bo, 0).s32 == -1){ handles[0] = gbm_bo_get_handle(bo).u32; strides[0] = gbm_bo_get_stride(bo); modifiers[0] = DRM_FORMAT_MOD_INVALID; TRACE_MARK_ONESHOT("egl-dri", "bo-handle", TRACE_SYS_ERROR, 0, 0, ""); } else { for (ssize_t i = 0; i < n_planes; i++){ strides[i] = gbm_bo_get_stride_for_plane(bo, i); handles[i] = gbm_bo_get_handle_for_plane(bo, i).u32; offsets[i] = gbm_bo_get_offset(bo, i); modifiers[i] = gbm_bo_get_modifier(bo); } } size_t bo_width = gbm_bo_get_width(bo); size_t bo_height = gbm_bo_get_height(bo); /* nop:ed for now, but the path for dealing with modifiers should be * considered as soon as we have the other setup for direct-scanout * of a client and metadata packing across the interface */ if (0){ if (drmModeAddFB2WithModifiers(d->device->disp_fd, bo_width, bo_height, gbm_bo_get_format(bo), handles, strides, offsets, modifiers, dst, 0)){ TRACE_MARK_ONESHOT("egl-dri", "drm-gbm-addfb2-mods", TRACE_SYS_ERROR, 0, 0, ""); return -1; } TRACE_MARK_ONESHOT("egl-dri", "drm-gbm-addfb2-mods", TRACE_SYS_DEFAULT, 0, 0, ""); } else if (drmModeAddFB2(d->device->disp_fd, bo_width, bo_height, gbm_bo_get_format(bo), handles, strides, offsets, dst, 0)){ if (drmModeAddFB(d->device->disp_fd, bo_width, bo_height, 24, 32, strides[0], handles[0], dst)){ TRACE_MARK_ONESHOT("egl-dri", "drm-gbm-addfb", TRACE_SYS_ERROR, 0, 0, ""); debug_print( "(%d) failed to add framebuffer (%s)", (int)d->id, strerror(errno)); return -1; } TRACE_MARK_ONESHOT("egl-dri", "drm-gbm-addfb", TRACE_SYS_DEFAULT, 0, 0, ""); } else { TRACE_MARK_ONESHOT("egl-dri", "drm-gbm-addfb2", TRACE_SYS_DEFAULT, 0, 0, ""); } return 1; } /* switch the display to work in 'dumb' mode with a single 'direct-out' buffer */ static bool set_dumb_fb(struct dispout* d) { struct drm_mode_create_dumb create = { .width = d->display.mode.hdisplay, .height = d->display.mode.vdisplay, .bpp = 32 }; assert(!d->buffer.dumb.enabled); int fd = d->device->disp_fd; if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create) < 0){ TRACE_MARK_ONESHOT("egl-dri", "create-dumb", TRACE_SYS_ERROR, 0, 0, ""); debug_print("(%d) create dumb-fb (%d*%d@%d bpp) failed", (int) d->id, create.width, create.height, create.bpp); return false; } struct agp_vstore* buf = &d->buffer.dumb.agp; buf->vinf.text.handle = create.handle; buf->vinf.text.stride = create.pitch; buf->vinf.text.s_raw = create.size; buf->w = d->display.mode.hdisplay; buf->h = d->display.mode.vdisplay; d->buffer.dumb.enabled = true; /* mark to switch on the next flip */ d->buffer.in_dumb_set = true; struct drm_mode_map_dumb mreq = { .handle = create.handle }; if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq) < 0){ TRACE_MARK_ONESHOT("egl-dri", "create-dumb-fbmap", TRACE_SYS_ERROR, 0, 0, ""); debug_print("(%d) couldn't map dumb-fb: %s", (int) d->id, strerror(errno)); return false; } /* note, do we get an offset here? */ d->buffer.dumb.agp.vinf.text.raw = mmap(0, create.size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mreq.offset); if (MAP_FAILED == d->buffer.dumb.agp.vinf.text.raw){ debug_print("(%d) couldn't mmap dumb-fb: %s", (int) d->id, strerror(errno)); TRACE_MARK_ONESHOT("egl-dri", "create-dumb-mmap", TRACE_SYS_ERROR, 0, 0, ""); return false; } TRACE_MARK_ONESHOT("egl-dri", "create-dumb", TRACE_SYS_DEFAULT, 0, 0, ""); memset(d->buffer.dumb.agp.vinf.text.raw, 0xaa, create.size); return true; } static void release_dumb_fb(struct dispout* d) { if (!d->buffer.dumb.enabled) return; drmModeRmFB(d->device->disp_fd, d->buffer.dumb.fd); struct drm_mode_destroy_dumb dreq = { .handle = d->buffer.dumb.fd }; drmIoctl(d->device->disp_fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dreq); close(d->buffer.dumb.fd); d->buffer.dumb.fd = -1; /* unref- the store, unlikely that we are the last consumer of this but some * edge (map -> vlayer deletes -> maps different, it can happen */ d->buffer.dumb.ref->refcount--; if (!d->buffer.dumb.ref->refcount){ if (d->buffer.dumb.ref->vinf.text.raw) arcan_mem_free(d->buffer.dumb.ref->vinf.text.raw); agp_drop_vstore(d->buffer.dumb.ref); } d->buffer.dumb.ref = NULL; munmap(d->buffer.dumb.agp.vinf.text.raw, d->buffer.dumb.sz); d->buffer.dumb.agp = (struct agp_vstore){}; d->buffer.dumb.enabled = false; /* if we have succeeded with switching to another framebuffer, restore * the old one by setting whatever was in that slot */ if (d->buffer.dumb.fb){ drmModeRmFB(d->device->disp_fd, d->buffer.dumb.fb); d->buffer.dumb.fb = 0; drmModeSetCrtc(d->device->disp_fd, d->display.crtc, d->buffer.cur_fb, 0, 0, &d->display.con_id, 1, &d->display.mode); } verbose_print("(%d) released dumb framebuffer", d->id); d->device->vsynch_method = VSYNCH_FLIP; } static bool resolve_add(int fd, drmModeAtomicReqPtr dst, uint32_t obj_id, drmModeObjectPropertiesPtr pptr, const char* name, uint32_t val) { for (size_t i = 0; i < pptr->count_props; i++){ drmModePropertyPtr prop = drmModeGetProperty(fd, pptr->props[i]); if (!prop) continue; if (strcmp(prop->name, name) == 0){ drmModeAtomicAddProperty(dst, obj_id, prop->prop_id, val); drmModeFreeProperty(prop); return true; } drmModeFreeProperty(prop); } return false; } static bool atomic_set_mode(struct dispout* d) { uint32_t mode; bool rv = false; int fd = d->device->disp_fd; if (0 != drmModeCreatePropertyBlob(fd, &d->display.mode, sizeof(drmModeModeInfo), &mode)){ debug_print("(%d) atomic-modeset, failed to create mode-prop"); return false; } drmModeAtomicReqPtr aptr = drmModeAtomicAlloc(); #define AADD(ID, LBL, VAL) if (!resolve_add(fd,aptr,(ID),pptr,(LBL),(VAL))){\ debug_print("(%d) atomic-modeset, failed to resolve prop %s", (int) d->id, (LBL));\ goto cleanup;\ } drmModeObjectPropertiesPtr pptr = drmModeObjectGetProperties(fd, d->display.crtc, DRM_MODE_OBJECT_CRTC); if (!pptr){ debug_print("(%d) atomic-modeset, failed to get crtc prop", (int) d->id); goto cleanup; } AADD(d->display.crtc, "MODE_ID", mode); AADD(d->display.crtc, "ACTIVE", 1); drmModeFreeObjectProperties(pptr); pptr = drmModeObjectGetProperties(fd, d->display.con->connector_id, DRM_MODE_OBJECT_CONNECTOR); if (!pptr){ debug_print("(%d) atomic-modeset, failed to get connector prop", (int)d->id); goto cleanup; } AADD(d->display.con->connector_id, "CRTC_ID", d->display.crtc); drmModeFreeObjectProperties(pptr); pptr = drmModeObjectGetProperties(fd, d->display.plane_id, DRM_MODE_OBJECT_PLANE); if (!pptr){ debug_print("(%d) atomic-modeset, failed to get plane props", (int)d->id); goto cleanup; } unsigned width = d->display.mode.hdisplay << 16; unsigned height = d->display.mode.vdisplay << 16; AADD(d->display.plane_id, "SRC_X", 0); AADD(d->display.plane_id, "SRC_Y", 0); AADD(d->display.plane_id, "SRC_W", width); AADD(d->display.plane_id, "SRC_H", height); AADD(d->display.plane_id, "CRTC_X", 0); AADD(d->display.plane_id, "CRTC_Y", 0); AADD(d->display.plane_id, "CRTC_W", width); AADD(d->display.plane_id, "CRTC_H", height); AADD(d->display.plane_id, "FB_ID", d->buffer.cur_fb); AADD(d->display.plane_id, "CRTC_ID", d->display.crtc); #undef AADD /* resolve sym:id for the properties on the objects we need: */ if (0 != drmModeAtomicCommit(fd,aptr, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL)){ goto cleanup; } else rv = true; cleanup: drmModeAtomicFree(aptr); drmModeDestroyPropertyBlob(fd, mode); return rv; } /* * foreach plane in plane-resources(dev): * if plane.crtc == display.crtc: * find type * if type is primary, set and true */ static bool find_plane(struct dispout* d) { drmModePlaneResPtr plane_res = drmModeGetPlaneResources(d->device->disp_fd); d->display.plane_id = 0; if (!plane_res){ debug_print("(%d) atomic-modeset, couldn't find plane on device",(int)d->id); return false; } for (size_t i = 0; i < plane_res->count_planes; i++){ drmModePlanePtr plane = drmModeGetPlane(d->device->disp_fd, plane_res->planes[i]); if (!plane){ debug_print("(%d) atomic-modeset, couldn't get plane (%zu)",(int)d->id,i); return false; } uint32_t crtcs = plane->possible_crtcs; drmModeFreePlane(plane); if (0 == (crtcs & (1 << d->display.crtc_index))) continue; uint64_t val; if (!lookup_drm_propval(d->device->disp_fd, plane_res->planes[i], DRM_MODE_OBJECT_PLANE, "type", &val, false)) continue; /* NOTE: There are additional constraints for PRIMARY planes that don't * apply to OVERLAY planes - we can't do scaling, plane size must cover * all of CRTC etc. If we use this wrong, check dmesg for something like * 'DRM: plane must cover entire CRTC' */ if (val == DRM_PLANE_TYPE_PRIMARY){ d->display.plane_id = plane_res->planes[i]; break; } } drmModeFreePlaneResources(plane_res); return d->display.plane_id != 0; } /* * sweep all displays, and see if the referenced CRTC id is in use. */ static struct dispout* crtc_used(struct dev_node* dev, int crtc) { for (size_t i = 0; i < MAX_DISPLAYS; i++){ if (displays[i].state == DISP_UNUSED) continue; if (displays[i].device != dev) continue; if (displays[i].display.crtc == crtc) return &displays[i]; } return NULL; } static int setup_kms(struct dispout* d, int conn_id, size_t w, size_t h) { SET_SEGV_MSG("egl-dri(), enumerating connectors on device failed.\n"); drmModeRes* res; retry: res = drmModeGetResources(d->device->disp_fd); if (!res){ debug_print("(%d) setup_kms, couldn't get resources (fd:%d)", (int)d->id, (int)d->device->disp_fd); return -1; } /* for the default case we don't have a connector, but for * newly detected displays we store / reserve */ if (!d->display.con) for (int i = 0; i < res->count_connectors; i++){ d->display.con = drmModeGetConnector(d->device->disp_fd, res->connectors[i]); if (d->display.con->connection == DRM_MODE_CONNECTED && (conn_id == -1 || conn_id == d->display.con->connector_id)) break; drmModeFreeConnector(d->display.con); d->display.con = NULL; } /* * No connector in place, set a retry- timer or give up. The other * option would be to switch over to display/headless mode */ if (!d->display.con){ drmModeFreeResources(res); /* only wait for the first display */ if (d == &displays[0] && d->device->wait_connector){ debug_print("(%d) setup-kms, no display - retry in 5s", (int)d->id); sleep(5); goto retry; } debug_print("(%d) setup-kms, no connected displays", (int)d->id); return -1; } d->display.con_id = d->display.con->connector_id; SET_SEGV_MSG("egl-dri(), enumerating connector/modes failed.\n"); /* * If dimensions are specified, find the closest match and on collision, * the one with the highest refresh rate. */ bool try_inherited_mode = true; int vrefresh = 0; /* * will just nop- out unless verbose defined */ for (ssize_t i = 0; i < d->display.con->count_modes; i++){ drmModeModeInfo* cm = &d->display.con->modes[i]; verbose_print("(%d) mode (%zu): %d*%d@%d Hz", d->id, i, cm->hdisplay, cm->vdisplay, cm->vrefresh); } /* * w and h comes from the old- style command-line to video_init calls * sets to 0 if the user didn't explicitly request anything else */ if (w != 0 && h != 0){ bool found = false; for (ssize_t i = 0; i < d->display.con->count_modes; i++){ drmModeModeInfo* cm = &d->display.con->modes[i]; /* * prefer exact match at highest vrefresh, otherwise we'll fall back to * whatever we inherit from the console or 'first best' */ if (cm->hdisplay == w && cm->vdisplay == h && cm->vrefresh > vrefresh){ d->display.mode = *cm; d->display.mode_set = i; d->dispw = cm->hdisplay; d->disph = cm->vdisplay; vrefresh = cm->vrefresh; try_inherited_mode = false; found = true; debug_print("(%d) hand-picked (-w, -h): " "%d*%d@%dHz", d->id, d->dispw, d->disph, vrefresh); } } if (!found){ /* but if not, drop the presets and return to auto-detect */ w = 0; h = 0; } } /* * If no dimensions are specified, grab the first one. (according to drm * documentation, that should be the most 'fitting') but also allow the * 'try_inherited_mode' using what is already on the connector. * * Note for ye who ventures in here, seems like some drivers still enjoy * returning ones that are actually 0*0, skip those. */ if (w == 0 && d->display.con->count_modes >= 1){ bool found = false; for (ssize_t i = 0; i < d->display.con->count_modes; i++){ drmModeModeInfo* cm = &d->display.con->modes[i]; if (!cm->hdisplay || !cm->vdisplay) continue; d->display.mode = *cm; d->display.mode_set = 0; d->dispw = cm->hdisplay; d->disph = cm->vdisplay; vrefresh = cm->vrefresh; found = true; debug_print("(%d) default connector mode: %d*%d@%dHz", d->id, d->dispw, d->disph, vrefresh); break; } /* everything is broken, just set a bad mode and let the rest of the error- * paths take care of the less-than-graceful exit */ if (!found){ d->display.mode = d->display.con->modes[0]; d->display.mode_set = 0; d->dispw = d->display.mode.hdisplay; d->disph = d->display.mode.vdisplay; debug_print("(%d) setup-kms, couldn't find any useful mode"); } debug_print("(%d) setup-kms, default-picked %zu*%zu", (int)d->id, (size_t)d->display.mode.hdisplay, (size_t)d->display.mode.vdisplay); } /* * Grab any EDID data now as we've had issues trying to query it on some * displays later while buffers etc. are queued(?). Some reports have hinted * that it's more dependent on race conditions on the kernel-driver side when * there are multiple EDID queries in flight which can happen as part of * on_hotplug(func) style event storms in independent software. */ fetch_edid(d); /* * foreach(encoder) * check_default_crtc -> not used ? allocate -> go * foreach possible_crtc -> first not used ? allocate -> go * * note that practically, the crtc search must also find a crtcs that supports * a certain 'plane configuration' that match the render configuration we want * to perform, which triggers on each setup where we have a change in vid to * display mappings (similar to defered modeset). */ SET_SEGV_MSG("libdrm(), setting matching encoder failed.\n"); bool crtc_found = false; /* mimic x11 modesetting driver use, sweep all encoders and pick the crtcs * that all of them support, and bias against inherited crtc on the first * encoder that aren't already mapped */ uint64_t mask = 0; mask = ~mask; uint64_t join = 0; for (int i = 0; i < res->count_encoders; i++){ drmModeEncoder* enc = drmModeGetEncoder(d->device->disp_fd, res->encoders[i]); if (!enc) continue; for (int j = 0; j < res->count_crtcs; j++){ if (!(enc->possible_crtcs & (1 << j))) mask &= enc->possible_crtcs; join |= enc->possible_crtcs; } drmModeFreeEncoder(enc); } if (!mask){ debug_print("libdrm(), no shared mask of crtcs, take full set"); mask = join; } /* now sweep the list of possible crtcs and pick the first one we don't have * already allocated to a display, uncertain if the crtc size was 32 or 64 * bit so might as well go for the higher */ for (uint64_t i = 0; i < 64 && i < res->count_crtcs; i++){ if (mask & ((uint64_t)1 << i)){ uint32_t crtc_val = res->crtcs[i]; struct dispout* crtc_disp = crtc_used(d->device, crtc_val); if (crtc_disp) continue; d->display.crtc = crtc_val; d->display.crtc_index = i; crtc_found = true; break; } } debug_print("(%d) picked crtc (%d) from encoder", (int)d->id, d->display.crtc); drmModeCrtc* crtc = drmModeGetCrtc(d->device->disp_fd, d->display.crtc); if (!crtc){ debug_print("couldn't retrieve chose crtc, giving up"); goto drop_disp; } /* sanity-check inherited mode (weird drivers + "non-graphical" defaults? */ if (crtc->mode_valid && try_inherited_mode && crtc->mode.hdisplay){ d->display.mode = crtc->mode; d->display.mode_set = 0; /* find the matching index */ for (size_t i = 0; i < d->display.con->count_modes; i++){ if (memcmp(&d->display.con->modes[i], &d->display.mode, sizeof(drmModeModeInfo)) == 0){ d->display.mode_set = i; break; } } d->dispw = d->display.mode.hdisplay; d->disph = d->display.mode.vdisplay; debug_print("(%d) trying tty- inherited mode ", (int)d->id); } if (!crtc_found){ debug_print("(%d) setup-kms, no working encoder/crtc", (int)d->id); goto drop_disp; return -1; } /* now we have a mode that is either hand-picked, inherited from the TTY or the * DRM default - alas in many cases this is not the one with the highest refresh * at that resolution, so sweep yet again and try and find one for that */ for (size_t i = 0; i < d->display.con->count_modes; i++){ drmModeModeInfo* cm = &d->display.con->modes[i]; if (cm->hdisplay == d->dispw && cm->vdisplay == d->disph && cm->vrefresh > vrefresh){ d->display.mode = *cm; d->display.mode_set = i; vrefresh = cm->vrefresh; debug_print( "(%d) higher refresh (%d) found at set resolution", vrefresh, (int)d->id); } } /* find a matching output-plane for atomic/streams */ if (d->device->atomic){ bool ok = true; if (!find_plane(d)){ debug_print("(%d) setup_kms, atomic-find_plane fail", (int)d->id); ok = false; } /* map a buffer to it so we get it to behave */ else if (!set_dumb_fb(d)){ debug_print("(%d) setup_kms, atomic dumb-fb fail", (int)d->id); ok = false; } /* and finally commit */ else if (!atomic_set_mode(d)){ debug_print("(%d) setup_kms, atomic modeset fail", (int)d->id); drmModeRmFB(d->device->disp_fd, d->buffer.cur_fb); d->buffer.cur_fb = 0; ok = false; } /* just disable atomic */ if (!ok){ debug_print("(%d) setup_kms, " "atomic modeset failed, fallback to legacy", d->id); d->device->atomic = false; drmModeFreeConnector(d->display.con); d->display.con = NULL; drmModeFreeResources(res); drmSetClientCap(d->device->disp_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 0); drmSetClientCap(d->device->disp_fd, DRM_CLIENT_CAP_ATOMIC, 0); return setup_kms(d, conn_id, w, h); } else if (!ok) goto drop_disp; } build_orthographic_matrix(d->projection, 0, d->display.mode.hdisplay, d->display.mode.vdisplay, 0, 0, 1); dpms_set(d, DRM_MODE_DPMS_ON); d->display.dpms = ADPMS_ON; drmModeFreeResources(res); return 0; drop_disp: drmModeFreeConnector(d->display.con); d->display.con = NULL; drmModeFreeResources(res); return -1; } /* this is the deprecated interface - import buffer has replaced it */ static bool map_handle_gbm(struct agp_vstore* dst, int64_t handle) { uint64_t invalid = DRM_FORMAT_MOD_INVALID; uint32_t hi = invalid >> 32; uint32_t lo = invalid & 0xffffffff; if (-1 == handle){ struct dispout* d = &displays[0]; d->device->eglenv.destroy_image( d->device->display, (EGLImage) dst->vinf.text.tag); dst->vinf.text.tag = 0; return true; } struct agp_buffer_plane plane = { .fd = handle, .gbm = { .mod_hi = DRM_FORMAT_MOD_INVALID >> 32, .mod_lo = DRM_FORMAT_MOD_INVALID & 0xffffffff, .offset = 0, .stride = dst->vinf.text.stride, .format = dst->vinf.text.format } }; return platform_video_map_buffer(dst, &plane, 1); } /* * There's a really ugly GLES- inheritance GOTCHA here that makes streams * a ******** pain to work with. 99.9% of all existing code works against * GL_TEXTURE_2D as the target for 2D buffers. Of course, there's a 'special' * GL_TEXTURE_EXTERNAL_OES target that also requires a different sampler in * the shader code. This leaves us with 3-ish options. * This is actually true for GBM- buffers as well, but so far MESA doesn't * really give a duck. * * 1. Mimic 'CopyTextureCHROMIUM' - explicit render-to-texture pass. * 2. The 'Cogl' approach - Make shader management exponentially worse by * tracking the dst- and when binding to a backend, compile/generate variants * where the sampler references are replaced with the 'right' target by * basically doing string- replacement on the program source. * 3. Move the complexity to the script level, breaking the opaqueness of * VIDs by forcing separate rules and gotcha's when the VID comes from an * external source. Possibly on the shader level by allowing additional * rule-slots. * +. Find some long-lost OpenGL function that relies on semi-defined behavior * to get rid of the _OES sampler type. There's probably something in * EGLImage. * +. Nasty hybrid: have a special external version of the 'default' and * when/if that one fails, fall back to 1/2. * * Granted, we need something similar for dma-buf when the source is one of * the many YUUVUUVUV formats. * * OpenGL: an arcane language with 400 different words for memcpy, where you * won't be sure of what src is, or dst, how much will actually be copied or * what happens to data in transit. */ bool map_handle_stream(struct agp_vstore* dst, int64_t handle) { /* * 1. eglCreateStreamFromFileDescriptorKHR(dpy, handle) * 2. glBindTexture (GL_TEXTURE_EXTERNAL_OES) * 3. eglStreamConsumerGLTextureExternalKHR(dpy, stream) * * to 'poll' the stream, we can go with eglStreamConsumerQueryStream and * check that for EGL_STREAM_STATE_NEW_FRAME_AVAILABLE_KHR. We need semantics * the the BUFFER_ calls in shmif and the corresponding place in engine/ * arcan_event.c to use a reserved identifier for matching against the * handle. * * then we have eglStreamConsumerAcquireKHR(dpy, stream) */ return false; } bool platform_video_map_handle( struct agp_vstore* dst, int64_t handle) { /* * MULTIGPU:FAIL * we need to follow the affinity for the specific [dst], and run the procedure * for each set bit in the field, if it is even possible to do with PRIME etc. * otherwise we need a mechanism to activate the DEVICEHINT event for the * provider to indicate that we need a portable handle. */ switch (nodes[0].buftype){ case BUF_GBM: return map_handle_gbm(dst, handle); break; case BUF_STREAM: return map_handle_stream(dst, handle); break; case BUF_HEADLESS: return false; break; } return false; } static void update_mode_cache(struct dispout* d) { debug_print("(%d) issuing mode scan", (int) d->id); drmModeConnector* conn = d->display.con; drmModeRes* res = drmModeGetResources(d->device->disp_fd); int count = conn->count_modes; if (!count) return; d->mode_cache_sz = count; d->mode_cache = arcan_alloc_mem( sizeof(struct monitor_mode) * d->mode_cache_sz, ARCAN_MEM_VSTRUCT, ARCAN_MEM_BZERO, ARCAN_MEMALIGN_NATURAL ); for (size_t i = 0; i < conn->count_modes; i++){ d->mode_cache[i].refresh = conn->modes[i].vrefresh; d->mode_cache[i].width = conn->modes[i].hdisplay; d->mode_cache[i].height = conn->modes[i].vdisplay; d->mode_cache[i].subpixel = subpixel_type(conn->subpixel); d->mode_cache[i].phy_width = conn->mmWidth; d->mode_cache[i].phy_height = conn->mmHeight; d->mode_cache[i].dynamic = false; d->mode_cache[i].id = i; d->mode_cache[i].depth = sizeof(av_pixel) * 8; } drmModeFreeResources(res); } struct monitor_mode* platform_video_query_modes( platform_display_id id, size_t* count) { bool free_conn = false; struct dispout* d = get_display(id); if (!d || d->state == DISP_UNUSED) return NULL; update_mode_cache(d); if (d->mode_cache){ *count = d->mode_cache_sz; return d->mode_cache; } *count = 0; return NULL; } static struct dispout* match_connector(int fd, drmModeConnector* con) { int j = 0; for (size_t i=0; i < MAX_DISPLAYS; i++){ struct dispout* d = &displays[i]; if (d->state == DISP_UNUSED) continue; if (d->device->disp_fd == fd && (d->display.con ? d->display.con->connector_id : d->display.con_id) == con->connector_id) return d; } return NULL; } /* * The cost for this function is rather unsavory, nouveau testing has shown * somewhere around ~110+ ms stalls for one re-scan */ static void query_card(struct dev_node* node) { debug_print("check resources on %i", node->disp_fd); drmModeRes* res = drmModeGetResources(node->disp_fd); if (!res){ debug_print("couldn't get resources for rescan on %i", node->disp_fd); return; } for (size_t i = 0; i < res->count_connectors; i++){ drmModeConnector* con = drmModeGetConnector(node->disp_fd, res->connectors[i]); struct dispout* d = match_connector(node->disp_fd, con); /* no display on connector */ if (con->connection != DRM_MODE_CONNECTED){ /* if there was one known, remove it and notify */ debug_print("(%zu) lost, disabled", (int)i); if (d){ debug_print("(%d) display lost, disabling", (int)d->id); disable_display(d, true); arcan_event ev = { .category = EVENT_VIDEO, .vid.kind = EVENT_VIDEO_DISPLAY_REMOVED, .vid.displayid = d->id, .vid.ledctrl = egl_dri.ledid, .vid.ledid = d->id }; arcan_event_enqueue(arcan_event_defaultctx(), &ev); arcan_conductor_release_display(d->device->card_id, d->id); } drmModeFreeConnector(con); continue; } /* do we already know about the connector? then do nothing */ if (d){ debug_print("(%d) already known", (int)d->id); drmModeFreeConnector(con); continue; } /* allocate display and mark as known but not mapped, give up * if we're out of display slots */ debug_print("unknown display detected"); d = allocate_display(&nodes[0]); if (!d){ debug_print("failed to allocate new display"); drmModeFreeConnector(con); continue; } /* save the ID for later so that we can match in match_connector */ d->display.con = con; d->display.con_id = con->connector_id; d->backlight = backlight_init( d->device->card_id, d->display.con->connector_type, d->display.con->connector_type_id ); debug_print( "(%d) assigned connector id (%d)",(int)d->id,(int)con->connector_id); if (d->backlight){ debug_print("(%d) display backlight assigned", (int)d->id); d->backlight_brightness = backlight_get_brightness(d->backlight); } arcan_event ev = { .category = EVENT_VIDEO, .vid.kind = EVENT_VIDEO_DISPLAY_ADDED, .vid.displayid = d->id, .vid.ledctrl = egl_dri.ledid, .vid.ledid = d->id, .vid.cardid = d->device->card_id }; arcan_conductor_register_display( d->device->card_id, d->id, SYNCH_STATIC, d->display.mode.vrefresh, d->vid); update_mode_cache(d); arcan_event_enqueue(arcan_event_defaultctx(), &ev); continue; /* don't want to free con */ } drmModeFreeResources(res); } void platform_video_query_displays() { debug_print("issuing display requery"); egl_dri.scan_pending = true; } static void disable_display(struct dispout* d, bool dealloc) { if (!d || d->state == DISP_UNUSED){ debug_print("disable_display called on unused display (%d)", (int)d->id); return; } debug_print("(%d) trying to disable", (int)d->id); if (d->buffer.in_flip){ debug_print("(%d) flip pending, deferring destruction", (int)d->id); d->buffer.in_destroy = true; egl_dri.destroy_pending |= 1 << d->id; return; } if (d->mode_cache){ arcan_mem_free(d->mode_cache); d->mode_cache = NULL; } d->device->refc--; if (d->buffer.in_destroy){ egl_dri.destroy_pending &= ~(1 << d->id); } d->buffer.in_destroy = false; if (d->display.edid_blob){ free(d->display.edid_blob); d->display.edid_blob = NULL; d->display.blob_sz = 0; } if (d->state == DISP_KNOWN){ d->device = NULL; d->state = DISP_UNUSED; return; } d->state = DISP_CLEANUP; set_display_context(d); debug_print("(%d) destroying EGL surface", (int)d->id); d->device->eglenv.destroy_surface(d->device->display, d->buffer.esurf); d->buffer.esurf = NULL; /* destroying the context has triggered driver bugs and hard to attribute UAFs * in the past, monitor this closely */ if (d->buffer.cur_fb){ debug_print("(%d) removing framebuffer", (int)d->id); drmModeRmFB(d->device->disp_fd, d->buffer.cur_fb); d->buffer.cur_fb = 0; } if (d->buffer.context != EGL_NO_CONTEXT){ debug_print("(%d) EGL - set device" "context, destroy display context", (int)d->id); set_device_context(d->device); d->device->eglenv.destroy_context(d->device->display, d->buffer.context); d->buffer.context = EGL_NO_CONTEXT; } if (d->device->buftype == BUF_GBM){ if (d->buffer.surface){ debug_print("destroy gbm surface"); if (d->buffer.cur_bo) gbm_surface_release_buffer(d->buffer.surface, d->buffer.cur_bo); gbm_surface_destroy(d->buffer.surface); } if (d->buffer.cur_bo) d->buffer.cur_bo = NULL; d->buffer.surface = NULL; } else debug_print("EGL- display"); /* restore the color LUTs, not 100% certain that this is the best approach here * since an external- launch then needs to figure out / manipulate them on its * own, losing color calibration and so on in the process */ if (d->display.orig_gamma){ debug_print("(%d) restoring device color LUTs"); drmModeCrtcSetGamma(d->device->disp_fd, d->display.crtc, d->display.gamma_size, d->display.orig_gamma, &d->display.orig_gamma[1*d->display.gamma_size], &d->display.orig_gamma[2*d->display.gamma_size] ); } /* in extended suspend, we have no idea which displays we are returning to so * the only real option is to fully deallocate even in EXTSUSP */ debug_print("(%d) release crtc id (%d)", (int)d->id,(int)d->display.crtc); if (d->display.old_crtc){ debug_print("(%d) old mode found, trying to reset", (int)d->id); if (d->device->atomic){ d->display.mode = d->display.old_crtc->mode; if (atomic_set_mode(d)){ debug_print("(%d) atomic-modeset failed on (%d)", (int)d->id, (int)d->display.con_id); } } else if (0 > drmModeSetCrtc( d->device->disp_fd, d->display.old_crtc->crtc_id, d->display.old_crtc->buffer_id, d->display.old_crtc->x, d->display.old_crtc->y, &d->display.con_id, 1, &d->display.old_crtc->mode )){ debug_print("Error setting old CRTC on %d", d->display.con_id); } } /* in the no-dealloc state we still want to remember which CRTCs etc were * set as those might have been changed as part of a modeset request */ if (!dealloc){ debug_print("(%d) switched state to EXTSUSP", (int)d->id); d->state = DISP_EXTSUSP; return; } /* gamma has already been restored above, but we need to free the resources */ debug_print("(%d) full deallocation requested", (int)d->id); if (d->display.orig_gamma){ free(d->display.orig_gamma); d->display.orig_gamma = NULL; } debug_print("(%d) freeing display connector", (int)d->id); drmModeFreeConnector(d->display.con); d->display.con = NULL; d->display.con_id = -1; d->display.mode_set = -1; drmModeFreeCrtc(d->display.old_crtc); d->display.old_crtc = NULL; /* d->device = NULL; */ d->state = DISP_UNUSED; if (d->backlight){ debug_print("(%d) resetting display backlight", (int)d->id); backlight_set_brightness(d->backlight, d->backlight_brightness); backlight_destroy(d->backlight); d->backlight = NULL; } } struct monitor_mode platform_video_dimensions() { struct monitor_mode res = { .width = egl_dri.canvasw, .height = egl_dri.canvash }; /* * this is done to work around how gl- agp handles scissoring as there's no * version of platform_video_dimension that worked for the display out */ if (egl_dri.last_display && egl_dri.last_display->display.mode_set != -1){ res.width = egl_dri.last_display->display.mode.hdisplay; res.height = egl_dri.last_display->display.mode.vdisplay; if (egl_dri.last_display->display.con){ res.phy_width = egl_dri.last_display->display.con->mmWidth; res.phy_height = egl_dri.last_display->display.con->mmHeight; } } /* * fake dimensions to provide an OK default PPCM (say 72) */ if (!res.phy_width) res.phy_width = (float)egl_dri.canvasw / (float)28.34645 * 10.0; if (!res.phy_height) res.phy_height = (float)egl_dri.canvash / (float)28.34645 * 10.0; return res; } void* platform_video_gfxsym(const char* sym) { return nodes[0].eglenv.get_proc_address(sym); } static void do_led(struct dispout* disp, uint8_t val) { if (disp && disp->backlight){ float lvl = (float) val / 255.0; float max_brightness = backlight_get_max_brightness(disp->backlight); backlight_set_brightness(disp->backlight, lvl * max_brightness); } } /* read-end of ledpair pipe is in nonblocking, so just run through * it and update the corresponding backlights */ static void flush_leds() { if (egl_dri.ledid < 0) return; uint8_t buf[2]; while (2 == read(egl_dri.ledpair[0], buf, 2)){ switch (tolower(buf[0])){ case 'A': egl_dri.ledind = 255; break; case 'a': egl_dri.ledind = buf[1]; break; case 'r': egl_dri.ledval[0] = buf[1]; break; case 'g': egl_dri.ledval[1] = buf[1]; break; case 'b': egl_dri.ledval[2] = buf[1]; break; case 'i': egl_dri.ledval[0] = egl_dri.ledval[1]=egl_dri.ledval[2]=buf[1]; case 'c': /* * don't expose an RGB capable backlight (are there such displays out there? * the other option would be to weight the gamma channel of the ramps but if * someone wants that behavior it is probably better to change all the ramps */ if (egl_dri.ledind != 255) do_led(get_display(egl_dri.ledind), egl_dri.ledval[0]); else for (size_t i = 0; i < MAX_DISPLAYS; i++) do_led(get_display(i), buf[1]); break; } } } static bool try_node(int draw_fd, int disp_fd, const char* pathref, int dst_ind, enum buffer_method method, int connid, int w, int h, bool ignore_display) { /* set default lookup function if none has been provided */ if (!nodes[dst_ind].eglenv.get_proc_address){ nodes[dst_ind].eglenv.get_proc_address = (PFNEGLGETPROCADDRESSPROC)eglGetProcAddress; } struct dev_node* node = &nodes[dst_ind]; node->active = true; map_egl_functions(&node->eglenv, lookup, NULL); map_eglext_functions( &node->eglenv, lookup_call, node->eglenv.get_proc_address); switch (method){ case BUF_GBM: if (0 != setup_node_gbm(dst_ind, node, draw_fd, disp_fd)){ node->eglenv.get_proc_address = NULL; debug_print("couldn't open (%d:%s) in GBM mode", draw_fd, pathref ? pathref : "(no path)"); release_card(dst_ind); return false; } break; case BUF_STREAM: if (0 != setup_node_egl(dst_ind, node, disp_fd)){ debug_print("couldn't open (%d:%s) in EGLStreams mode", draw_fd, pathref ? pathref : "(no path)"); release_card(dst_ind); return false; } break; case BUF_HEADLESS: break; } if (!setup_node(node)){ debug_print("setup/configure [%d](%d:%s)", dst_ind, draw_fd, pathref ? pathref : "(no path)"); release_card(dst_ind); return false; } /* used when we already have one */ if (ignore_display) return true; struct dispout* d = allocate_display(node); d->display.primary = dst_ind == 0; egl_dri.last_display = d; if (setup_kms(d, connid, w, h) != 0){ disable_display(d, true); debug_print("card found, but no working/connected display"); release_card(dst_ind); return false; } if (setup_buffers(d) == -1){ disable_display(d, true); release_card(dst_ind); return false; } d->backlight = backlight_init(d->device->card_id, d->display.con->connector_type, d->display.con->connector_type_id); if (d->backlight) d->backlight_brightness = backlight_get_brightness(d->backlight); return true; } /* * config/profile matching derived approach, for use when something more * specific and sophisticated is desired */ static bool try_card(size_t devind, int w, int h, size_t* dstind) { uintptr_t tag; cfg_lookup_fun get_config = platform_config_lookup(&tag); char* dispdevstr, (* cfgstr), (* altstr); int connind = -1; bool gbm = true; /* basic device, device_1, device_2 etc. search path */ if (!get_config("video_display_device", devind, &dispdevstr, tag)) return false; char* drawdevstr = NULL; get_config("video_draw_device", devind, &drawdevstr, tag); /* reference to another card_id, only one is active at any one moment * and card_1 should reference card_2 and vice versa. */ if (get_config("video_device_alternate", devind, &cfgstr, tag)){ /* sweep from devind down to 0 and see if there is a card with the * specified path, if so, open but don't activate this one */ } if (get_config("video_device_buffer", devind, &cfgstr, tag)){ if (strcmp(cfgstr, "streams") == 0){ gbm = false; debug_print("device_buffer forced to EGLstreams"); } else { debug_print("device_buffer fallback to GBM"); } free(cfgstr); } /* reload any possible library references */ if (nodes[devind].agplib){ free(nodes[devind].agplib); nodes[devind].agplib = NULL; } if (nodes[devind].egllib){ free(nodes[devind].egllib); nodes[devind].egllib = NULL; } get_config("video_device_egllib", devind, &nodes[devind].egllib, tag); get_config("video_device_agplib", devind, &nodes[devind].agplib, tag); /* hard- connector index set */ if (get_config("video_device_connector", devind, &cfgstr, tag)){ connind = strtol(cfgstr, NULL, 10) % INT_MAX; free(cfgstr); } nodes[devind].wait_connector = get_config("video_device_wait", devind, NULL, tag); int dispfd = platform_device_open(dispdevstr, O_RDWR | O_CLOEXEC); int drawfd = (drawdevstr ? platform_device_open(drawdevstr, O_RDWR | O_CLOEXEC) : dispfd); if (try_node(drawfd, dispfd, dispdevstr, *dstind, gbm ? BUF_GBM : BUF_STREAM, connind, w, h, false)){ debug_print("card at %d added", *dstind); nodes[*dstind].pathref = dispdevstr; nodes[*dstind].card_id = *dstind; *dstind++; return true; } /* don't need to close the disp/draw descriptors here, it is done in try_node */ else{ free(nodes[devind].egllib); free(nodes[devind].agplib); nodes[devind].egllib = nodes[devind].agplib = NULL; free(dispdevstr); free(drawdevstr); return false; } } static bool setup_cards_db(int w, int h) { size_t dstind = 0; for (size_t devind = 0; devind < COUNT_OF(nodes); devind++){ if (!try_card(devind, w, h, &dstind)) return dstind > 0; dstind++; } return dstind > 0; } /* * naive approach - for when there's no explicit configuration set. * This just globs a preset/os-specific path and takes the first * device that appears and can be opened. */ static bool setup_cards_basic(int w, int h) { #define DEVICE_PATH "/dev/dri/card%zu" /* sweep as there might be more GPUs but without any connected * display, indicating that it's not a valid target for autodetect. */ for (size_t i = 0; i < 4; i++){ char buf[sizeof(DEVICE_PATH)]; snprintf(buf, sizeof(buf), DEVICE_PATH, i); int fd = platform_device_open(buf, O_RDWR | O_CLOEXEC); debug_print("trying [basic/auto] setup on %s", buf); if (-1 != fd){ /* on Linux (basically only place that'd be relevant for now) we could * check proc/modules for nvidia, then go the sysfs route for the card node, * grab the vendor string and switch to streams that way - if so we could * dynamically load the GL libs etc. */ if (try_node(fd, fd, buf, 0, BUF_GBM, -1, w, h, false) || try_node(fd, fd, buf, 0, BUF_GBM, -1, w, h, false)){ nodes[0].pathref = strdup(buf); return true; } else{ debug_print("node setup failed"); close(fd); } } else debug_print("could not open %s - %s", buf, strerror(errno)); } /* in the no-dealloc state we still want to remember which CRTCs etc were * set as those might have been changed as part of a modeset request */ return false; } void platform_video_preinit() { } bool platform_video_init(uint16_t w, uint16_t h, uint8_t bpp, bool fs, bool frames, const char* title) { bool rv = false; struct sigaction old_sh; struct sigaction err_sh = { .sa_handler = sigsegv_errmsg }; if (getenv("ARCAN_VIDEO_DEBUGSTALL")){ volatile static bool spinwait = true; while (spinwait){} } /* * init after recovery etc. won't need seeding */ static bool seeded; if (!seeded){ for (size_t i = 0; i < VIDEO_MAX_NODES; i++){ nodes[i].disp_fd = -1; nodes[i].draw_fd = -1; } seeded = true; } /* * temporarily override segmentation fault handler here because it has happened * in libdrm for a number of "user-managable" settings (i.e. drm locked to X, * wrong permissions etc.) */ sigaction(SIGSEGV, &err_sh, &old_sh); if (setup_cards_db(w, h) || setup_cards_basic(w, h)){ struct dispout* d = egl_dri.last_display; set_display_context(d); egl_dri.canvasw = d->display.mode.hdisplay; egl_dri.canvash = d->display.mode.vdisplay; build_orthographic_matrix(d->projection, 0, egl_dri.canvasw, egl_dri.canvash, 0, 0, 1); memcpy(d->txcos, arcan_video_display.mirror_txcos, sizeof(float) * 8); d->vid = ARCAN_VIDEO_WORLDID; d->state = DISP_MAPPED; debug_print("(%d) mapped/default display at %zu*%zu", (int)d->id, (size_t)egl_dri.canvasw, (size_t)egl_dri.canvash); setup_backlight_ledmap(); /* * send a first 'added' event for display tracking as the * primary / connected display will not show up in the rescan */ arcan_event ev = { .category = EVENT_VIDEO, .vid.kind = EVENT_VIDEO_DISPLAY_ADDED, .vid.ledctrl = egl_dri.ledid, .vid.cardid = d->device->card_id }; arcan_event_enqueue(arcan_event_defaultctx(), &ev); platform_video_query_displays(); rv = true; } sigaction(SIGSEGV, &old_sh, NULL); return rv; } static bool in_external; void platform_video_reset(int id, int swap) { /* protect against some possible circular calls with VT switch invoked * while we are also trying to already go external */ if (in_external) return; arcan_video_prepare_external(true); /* at this stage, all the GPU resources should be non-volatile */ if (id != -1 || swap){ /* these case are more difficult as we also need to modify vstore affinity * masks (so we don't synch to multiple GPUs), and verify that the swapped * in GPU actually works */ } /* this is slightly incorrect as the agp_init function- environment, * ideally we'd also terminate the agp environment and rebuild on the * new / different card */ arcan_video_restore_external(true); in_external = false; } /* * for recovery, first emit a display added for the default 0 display, then * interate all already known displays and do the same. Lastly, do a rescan for * good measure */ void platform_video_recovery() { arcan_event ev = { .category = EVENT_VIDEO, .vid.kind = EVENT_VIDEO_DISPLAY_ADDED, .vid.ledctrl = egl_dri.ledid }; debug_print("video_recovery, injecting 'added' for mapped displays"); arcan_evctx* evctx = arcan_event_defaultctx(); arcan_event_enqueue(evctx, &ev); for (size_t i = 0; i < MAX_DISPLAYS; i++){ if (displays[i].state == DISP_MAPPED){ platform_video_map_display( ARCAN_VIDEO_WORLDID, displays[i].id, HINT_NONE); displays[i].vid = ARCAN_VIDEO_WORLDID; ev.vid.displayid = displays[i].id; ev.vid.cardid = displays[i].device->card_id; arcan_event_enqueue(evctx, &ev); } } /* * commented as it is quite expensive and shouldn't be necessary in short * resets, and the VT switching now has full card rebuild * platform_video_query_displays(); */ /* rebuild so that we guaranteed have a rendertarget */ arcan_video_display.no_stdout = false; arcan_video_resize_canvas(displays[0].dispw, displays[0].disph); } static void page_flip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void* data) { struct dispout* d = data; d->buffer.in_flip = 0; TRACE_MARK_ONESHOT("egl-dri", "flip-ack", TRACE_SYS_DEFAULT, d->id, frame, "flip"); verbose_print("(%d) flip(frame: %u, @ %u.%u)", (int) d->id, frame, sec, usec); switch(d->device->buftype){ case BUF_GBM:{ /* won't happen first frame or to-from dumb transition */ if (d->buffer.cur_bo) gbm_surface_release_buffer(d->buffer.surface, d->buffer.cur_bo); /* with in_dumb_set we have waited for flip to be released, then we will switch * to the 'dumb' flip handler in the future */ if (d->buffer.in_dumb_set){ debug_print("(%d) page-flip, switch to single-dumb buffer", d->id); struct agp_vstore* buf = &d->buffer.dumb.agp; /* create the framebuffer tied to the dumb buffer and set to the Crtc, * if it fails just continue as 'normal' without the dumb buffer */ if (0 == drmModeAddFB(d->device->disp_fd, buf->w, buf->h, 24, 32, buf->vinf.text.stride, buf->vinf.text.handle, &d->buffer.dumb.fb)){ d->buffer.cur_bo = NULL; d->device->vsynch_method = VSYNCH_IGNORE; drmModeSetCrtc(d->device->disp_fd, d->display.crtc, d->buffer.dumb.fb, 0, 0, &d->display.con_id, 1, &d->display.mode); debug_print("(%d) dumb-fb on crtc", d->id); } else { debug_print("(%d) couldn't add dumb-fb, revert", d->id); release_dumb_fb(d); d->buffer.cur_bo = d->buffer.next_bo; } d->buffer.in_dumb_set = false; } else d->buffer.cur_bo = d->buffer.next_bo; d->buffer.next_bo = NULL; verbose_print("(%d) gbm-bo, release %"PRIxPTR" with %"PRIxPTR, (int)d->id, (uintptr_t) d->buffer.cur_bo, (uintptr_t) d->buffer.next_bo); } break; case BUF_STREAM: break; case BUF_HEADLESS: break; } arcan_conductor_deadline(deadline_for_display(d)); } static bool dirty_displays() { struct dispout* d; int i = 0; while((d = get_display(i++))){ arcan_vobject* vobj = arcan_video_getobject(d->vid); if (!vobj) continue; struct rendertarget* tgt = arcan_vint_findrt(vobj); if (!tgt) continue; if (d->frame_cookie != tgt->frame_cookie){ verbose_print("(%d) frame-cookie (%zu) " "changed to (%zu)", d->id, d->frame_cookie, tgt->frame_cookie); return true; } } return false; } static bool get_pending(bool primary_only) { int i = 0; int pending = 0; struct dispout* d; while((d = get_display(i++))){ if (!primary_only || d->display.primary) pending |= d->buffer.in_flip; } return pending > 0; } /* * Real synchronization work is in this function. Go through all mapped * displays and wait for any pending events to finish, or the specified * timeout(ms) to elapse. * * Timeout is typically used for shutdown / cleanup operations where * normal background processing need to be ignored anyhow. */ static void flush_display_events(int timeout, bool yield) { struct dispout* d; verbose_print("flush display events, timeout: %d", timeout); unsigned long long start = arcan_timemillis(); int period = 4; if (timeout > 0){ period = timeout; } /* only flush, don't iterate */ else if (timeout == -1){ period = 0; } /* * NOTE: recent versions of DRM has added support to let us know which CRTC * actually provided a synch signal (through a .page_flip_handler2). When this * is more wide-spread, we should really switch to that kind of a system * because this is horrid. * * [SCANOUT-note] this is also where we have a .vblank_handler that would let * us re-raster / synch any directly mapped vobjs to the output. */ drmEventContext evctx = { .version = DRM_EVENT_CONTEXT_VERSION, .page_flip_handler = page_flip_handler }; do{ /* MULTICARD> for multiple cards, extend this pollset */ struct pollfd fds = { .fd = nodes[0].disp_fd, .events = POLLIN | POLLERR | POLLHUP }; int rv = poll(&fds, 1, period); if (rv == 1){ /* If we get HUP on a card we have open, it is basically as bad as a fatal * state, unless - we support hotplugging multi-GPUs, then that decision needs * to be re-evaluated as it is essentially a drop_card + drop all displays */ if (fds.revents & (POLLHUP | POLLERR)){ debug_print("(card-fd %d) broken/recovery missing", (int) nodes[0].disp_fd); arcan_fatal("GPU device lost / broken"); } else drmHandleEvent(nodes[0].disp_fd, &evctx); /* There is a special property here, 'PRIMARY'. The displays with this property * set are being used for synch, with the rest - we just accept the possibility * of tearing or ignore that display for another frame (extremes like 59.xx Hz * display main and a 30Hz secondary output) */ } else if (yield) { /* * With VFR changes, we should start passing the responsibility for dealing with * synch period and timeout here before proceeding with the next pass / cycle. */ int yv = arcan_conductor_yield(NULL, 0); if (-1 == yv) break; else period = yv; } } /* 3 possible timeouts: exit directly, wait indefinitely, wait for fixed period */ while (timeout != -1 && get_pending(true) && (!timeout || (timeout && arcan_timemillis() - start < timeout))); } static void flush_parent_commands() { /* Changed but not enough information to actually specify which card we need * to rescan in order for the changes to be detected. This needs cooperation * with the scripts anyhow as they need to rate-limit / invoke rescan. This * only takes care of an invalid or severed connection, moving device disc. * to a supervisory process would also require something in event.c */ int pv = platform_device_poll(NULL); switch(pv){ case -1: debug_print("parent connection severed"); arcan_event_enqueue(arcan_event_defaultctx(), &(struct arcan_event){ .category = EVENT_SYSTEM, .sys.kind = EVENT_SYSTEM_EXIT, .sys.errcode = EXIT_FAILURE }); break; case 5: debug_print("parent requested termination"); arcan_event_enqueue(arcan_event_defaultctx(), &(struct arcan_event){ .category = EVENT_SYSTEM, .sys.kind = EVENT_SYSTEM_EXIT, .sys.errcode = EXIT_FAILURE }); break; case 4: /* restore / rebuild out of context */ debug_print("received restore while not in suspend state"); break; case 3:{ /* suspend / release, if the parent connection is severed while in this * state, we'll leave it to restore external to shutdown */ debug_print("received tty switch request"); /* first release current resources, then ack the release on the tty */ arcan_video_prepare_external(false); platform_device_release("TTY", -1); int sock = platform_device_pollfd(); while (true){ poll(&(struct pollfd){ .fd = sock, .events = POLLIN | POLLERR | POLLHUP | POLLNVAL} , 1, -1); pv = platform_device_poll(NULL); if (pv == 4 || pv == -1){ debug_print("received restore request (%d)", pv); arcan_video_restore_external(false); break; } } } break; case 2: /* new display event */ arcan_event_enqueue(arcan_event_defaultctx(), &(struct arcan_event){ .category = EVENT_VIDEO, .vid.kind = EVENT_VIDEO_DISPLAY_CHANGED, }); break; default: break; } } /* * A lot more work / research is needed on this one to be able to handle all * weird edge-cases depending on the mapped displays and priorities (powersave? * tearfree? lowest possible latency in regards to other external clocks etc.) * - especially when mixing in future synch models that don't require VBlank */ void platform_video_synch( uint64_t tick_count, float fract, video_synchevent pre, video_synchevent post) { if (pre) pre(); /* * Destruction of hotplugged displays are deferred to this stage as it is hard * to know when it is actually safe to do in other places in the pipeline. * Wait until the queued transfers to a display have been finished before it is * put down. */ int i = 0; struct dispout* d; while (egl_dri.destroy_pending){ flush_display_events(30, true); int ind = __builtin_ffsll(egl_dri.destroy_pending) - 1; debug_print("synch, %d - destroy %d", ind); disable_display(&displays[ind], true); egl_dri.destroy_pending &= ~(1 << ind); } /* Some strategies might leave us with a display still in pending flip state * even though it has finished by now. If we don't flush those out, they will * skip updating one frame, so do a quick no-yield flush first */ if (get_pending(false)) flush_display_events(-1, false); /* * Rescanning displays is binned to this as well along with a rate limiting * timeout. This is to mitigate storms from KVMs plugging in multiple displays * or display events causing appl to reissue scan causing events causing new * scans. */ unsigned long long ts = arcan_timemillis(); if (egl_dri.scan_pending && ((ts < egl_dri.last_card_scan) || (ts - egl_dri.last_card_scan) > CARD_RESCAN_DELAY_MS)){ egl_dri.scan_pending = false; egl_dri.last_card_scan = arcan_timemillis(); for (size_t j = 0; j < COUNT_OF(nodes); j++){ debug_print("query_card: %zu", j); if (nodes[j].disp_fd != -1) query_card(&nodes[j]); } } /* the 'nd' here is much too coarse-grained, we need the affected objects and * rendertargets so that we can properly decide which ones to synch or not - * this is basically a left-over from old / naive design */ size_t nd; uint32_t cost_ms = arcan_vint_refresh(fract, &nd); /* * At this stage, the contents of all RTs have been synched, with nd == 0, * nothing has changed from what was draw last time. */ arcan_bench_register_cost( cost_ms ); bool clocked = false; bool updated = false; int method = 0; /* * If we have a real update, the display timing will request a deadline based * on whatever display that was updated so we can go with that */ if (nd > 0 || dirty_displays()){ while ( (d = get_display(i++)) ){ if (d->state == DISP_MAPPED && d->buffer.in_flip == 0){ updated |= update_display(d); clocked |= d->device->vsynch_method == VSYNCH_CLOCK; } } /* * Finally check for the callbacks, synchronize with the conductor and so on * the clocked is a failsafe for devices that don't support giving a vsynch * signal. */ if (get_pending(false) || updated) flush_display_events(clocked ? 16 : 0, true); } /* * If there are no updates, just 'fake' synch to the display with the lowest * refresh unless the yield function tells us to run in a processing- like * state (useful for displayless like processing). */ else { float refresh = 60.0; i = 0; while ((d = get_display(i++))){ if (d->state == DISP_MAPPED){ if (d->display.mode.vrefresh && d->display.mode.vrefresh > refresh) refresh = d->display.mode.vrefresh; } } /* * The other option would be to to set left as the deadline here, but that * makes the platform even worse when it comes to testing strategies etc. */ int left = 1000.0f / refresh; arcan_conductor_deadline(-1); arcan_conductor_fakesynch(left); } /* * The LEDs that are mapped as backlights via the internal pipe-led protocol * needs to be flushed separately, here is a decent time to get that out of the * way. [HDR-note] this might be insufficient for the HDR related backlight * control interfaces that seem to take a different path. */ flush_leds(); /* * Since we outsource device access to a possibly privileged layer, here is * the time to check for requests from the parent itself. */ flush_parent_commands(); if (post) post(); } bool platform_video_auth(int cardn, unsigned token) { int fd = platform_video_cardhandle(cardn, NULL, NULL, NULL); if (fd != -1){ bool auth_ok = drmAuthMagic(fd, token); debug_print("requested auth of (%u) on card (%d)", token, cardn); return auth_ok; } else return false; } void platform_video_shutdown() { int rc = 10; do{ for(size_t i = 0; i < MAX_DISPLAYS; i++){ unsigned long long start = arcan_timemillis(); disable_display(&displays[i], true); debug_print("shutdown (%zu) took %d ms", i, (int)(arcan_timemillis() - start)); } flush_display_events(30, false); } while (egl_dri.destroy_pending && rc-- > 0); for (size_t i = 0; i < sizeof(nodes)/sizeof(nodes[0]); i++) release_card(i); } const char* platform_video_capstr() { static char* buf; static size_t buf_sz; if (buf){ free(buf); buf = NULL; } FILE* stream = open_memstream(&buf, &buf_sz); if (!stream) return "platform/egl-dri capstr(), couldn't create memstream\n"; const char* vendor = (const char*) glGetString(GL_VENDOR); const char* render = (const char*) glGetString(GL_RENDERER); const char* version = (const char*) glGetString(GL_VERSION); const char* shading = (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION); const char* exts = (const char*) glGetString(GL_EXTENSIONS); const char* eglexts = ""; struct dispout* disp = get_display(0); if (disp){ eglexts = (const char*)disp->device->eglenv.query_string( disp->device->display, EGL_EXTENSIONS); dump_connectors(stream, disp->device, true); } fprintf(stream, "Video Platform (EGL-DRI)\n" "Vendor: %s\nRenderer: %s\nGL Version: %s\n" "GLSL Version: %s\n\n Extensions Supported: \n%s\n\n" "EGL Extensions supported: \n%s\n\n", vendor, render, version, shading, exts, eglexts); fclose(stream); return buf; } const char** platform_video_envopts() { return (const char**) egl_envopts; } enum dpms_state platform_video_dpms( platform_display_id disp, enum dpms_state state) { struct dispout* out = get_display(disp); if (!out || out->state <= DISP_KNOWN) return ADPMS_IGNORE; if (state == ADPMS_IGNORE) return out->display.dpms; if (state != out->display.dpms){ debug_print("dmps (%d) change to (%d)", (int)disp, state); dpms_set(out, adpms_to_dpms(state)); } out->display.dpms = state; return state; } size_t platform_video_decay() { size_t ret = egl_dri.decay; egl_dri.decay = 0; return ret; } static bool direct_scanout_alloc( struct agp_rendertarget* tgt, struct agp_vstore* vs, int action, void* tag) { struct dispout* display = tag; struct agp_fenv* env = agp_env(); if (action == RTGT_ALLOC_FREE){ debug_print("scanout_free:display=%d", (int) display->id); struct shmifext_color_buffer* buf = (struct shmifext_color_buffer*) vs->vinf.text.handle; /* slightly different to the one used in arcan/video.c as we keep the glid alive */ if (buf){ env->delete_textures(1, &vs->vinf.text.glid); display->device->eglenv.destroy_image( display->device->display, buf->alloc_tags[1]); gbm_bo_destroy(buf->alloc_tags[0]); buf->alloc_tags[0] = NULL; buf->alloc_tags[1] = NULL; free(buf); vs->vinf.text.glid = 0; vs->vinf.text.handle = 0; } else { agp_drop_vstore(vs); } } else if (action == RTGT_ALLOC_SETUP){ debug_print("scanout_alloc:display=%d:w=%zu:h=%zu", (int) display->id, (size_t) display->dispw, (size_t) display->disph); struct shmifext_color_buffer* buf = malloc(sizeof(struct shmifext_color_buffer)); *buf = (struct shmifext_color_buffer){ .id.gl = vs->vinf.text.glid }; if (!helper_alloc_color(env, &display->device->eglenv, display->device->buffer.gbm, display->device->display, buf, display->dispw, display->disph, display->buffer.format, 4, /* becomes USE_SCANOUT */ 0, NULL )){ debug_print("scanout_alloc:failed_fallback"); agp_empty_vstore(vs, vs->w, vs->h); free(buf); } else{ debug_print("scanout_alloc:ok:glid=%zu", (size_t) buf->id.gl); vs->vinf.text.glid = buf->id.gl; vs->vinf.text.handle = (uintptr_t) buf; } } return true; } void platform_video_invalidate_map( struct agp_vstore* vstore, struct agp_region region) { /* the same store could be mapped to multiple displays */ for (size_t i = 0; i < MAX_DISPLAYS; i++){ if (displays[i].state == DISP_UNUSED || &displays[i].buffer.dumb.agp != vstore || !displays[i].buffer.dumb.fb) continue; drmModeClip reg = { .x1 = region.x1, .y1 = region.y1, .x2 = region.x2, .y2 = region.y2 }; drmModeDirtyFB( displays[i].device->disp_fd, displays[i].buffer.dumb.fb, ®, 1); } } bool platform_video_map_display( arcan_vobj_id vid, platform_display_id id, enum blitting_hint hint) { struct display_layer_cfg cfg = { .opacity = 1.0, .hint = hint }; return platform_video_map_display_layer(vid, id, 0, cfg) >= 0; } ssize_t platform_video_map_display_layer(arcan_vobj_id id, platform_display_id disp, size_t layer, struct display_layer_cfg cfg) { enum blitting_hint hint = cfg.hint; struct dispout* d = get_display(disp); /* incomplete - should try and deal with cursor etc. */ if (layer) return -1; if (!d || d->state == DISP_UNUSED){ debug_print( "map_display(%d->%d) attempted on unused disp", (int)id, (int)disp); return -1; } /* we have a known but previously unmapped display, set it up */ if (d->state == DISP_KNOWN){ debug_print("map_display(%d->%d), known but unmapped", (int)id, (int)disp); if (setup_kms(d, d->display.con ? d->display.con->connector_id : -1, d->display.mode_set != -1 ? d->display.mode.hdisplay : 0, d->display.mode_set != -1 ? d->display.mode.vdisplay : 0) || setup_buffers(d) == -1){ debug_print("map_display(%d->%d) alloc/map failed", (int)id, (int)disp); return -1; } d->state = DISP_MAPPED; } arcan_vobject* vobj = arcan_video_getobject(id); if (!vobj){ debug_print("setting display(%d) to unmapped", (int) disp); d->display.dpms = ADPMS_OFF; d->vid = id; arcan_conductor_release_display(d->device->card_id, d->id); return 0; } /* remove any previous rendertarget scanout buffering */ if (d->vid){ arcan_vobject* old = arcan_video_getobject(id); struct rendertarget* tgt = NULL; if (old) tgt = arcan_vint_findrt(vobj); if (tgt) agp_rendertarget_dropswap(tgt->art); } /* the more recent rpack- based mapping format could/should get special * consideration here as we could then raster into a buffer directly for a * non-GL scanout path, avoiding some of the possible driver fuzz and latency * */ if (vobj->vstore->txmapped != TXSTATE_TEX2D){ debug_print("map_display(%d->%d) rejected, source not a valid texture", (int) id, (int) disp); return -1; } /* normal object may have origo in UL, WORLDID FBO in LL */ float txcos[8]; memcpy(txcos, vobj->txcos ? vobj->txcos : (vobj->vstore == arcan_vint_world() ? arcan_video_display.mirror_txcos : arcan_video_display.default_txcos), sizeof(float) * 8 ); debug_print("map_display(%d->%d) ok @%zu*%zu+%zu,%zu, hint: %d", (int) id, (int) disp, (size_t) d->dispw, (size_t) d->disph, (size_t) d->dispx, (size_t) d->dispy, (int) hint); d->frame_cookie = 0; d->display.primary = hint & HINT_FL_PRIMARY; memcpy(d->txcos, txcos, sizeof(float) * 8); /* this kind of hint management is implemented through texture coordinate * tricks, on a direct mapping, the capabilities of the output needs to be * checked as well in the same 'sane direct vobj' style to see if there are * layer flags that can be set to avoid a forced composition pass */ size_t iframes = 0; arcan_vint_applyhint(vobj, hint, txcos, d->txcos, &d->dispx, &d->dispy, &d->dispw, &d->disph, &iframes); arcan_video_display.ignore_dirty += iframes; /* turn on the display on mapping if it isn't already */ if (d->display.dpms == ADPMS_OFF){ dpms_set(d, DRM_MODE_DPMS_ON); d->display.dpms = ADPMS_ON; } if (hint & HINT_DIRECT) d->force_compose = false; /* need to remove this from the mapping hint so that it doesn't * hit HINT_NONE tests */ d->hint = hint & ~(HINT_FL_PRIMARY | HINT_DIRECT); d->vid = id; arcan_conductor_register_display( d->device->card_id, d->id, SYNCH_STATIC, d->display.mode.vrefresh, d->vid); /* we might have messed around with the projection, rebuild it to be sure */ struct rendertarget* newtgt = arcan_vint_findrt(vobj); release_dumb_fb(d); if (newtgt){ newtgt->inv_y = false; build_orthographic_matrix( newtgt->projection, 0, vobj->origw, 0, vobj->origh, 0, 1); if (!d->hint && !d->force_compose && sane_direct_vobj(vobj, "rtgt")){ /* before swapping, set an allocator for the rendertarget so that we can ensure * that we allocate from scanout capable memory - note that in that case the * contents is invalidated and a new render pass on the target is needed. This * is not that problematic with the normal render loop as the map call will come * in a 'good enough' order. */ bool swap; debug_print("(%d) setting up rtgt allocator for direct out"); agp_rendertarget_allocator(newtgt->art, direct_scanout_alloc, d); (void*) agp_rendertarget_swap(newtgt->art, &swap); } } /* ok: * - handle based external backend with bo_use_scanout and the right * modifiers, if the object is ok but the allocation isn't, wait for * a resize and then retry- mapping * * - shm based backing where we can just blit into a dumb buffer * * - tui based contents where we can raster into a dumb buffer */ else if (sane_direct_vobj(vobj, "simple_vid")){ TRACE_MARK_ONESHOT("egl-dri", "dumb-bo", TRACE_SYS_DEFAULT, d->id, 0, ""); debug_print("(%d) switching to dumb mode", d->id); if (set_dumb_fb(d)){ /* now swap in our 'real-object' - copy-blit as is for the first pass then * swap in our storage into the mapped source */ struct arcan_frameserver* fsrv = vobj->feed.state.ptr; struct agp_vstore* src = vobj->vstore; struct agp_vstore* dst = &d->buffer.dumb.agp; /* refcount and track so it doesn't disappear before we have released */ src->refcount++; d->buffer.dumb.ref = src; /* ensure local copying when there is a buffer transfer */ if (fsrv){ if (!(fsrv->desc.hints & SHMIF_RHINT_TPACK)) fsrv->flags.local_copy = true; src->dst_copy = dst; /* is there a pre-existing local store? */ if (src->vinf.text.raw) agp_vstore_copyreg(src, src->dst_copy, 0, 0, src->w, src->h); } /* MISSING: setting locked flag and aligning to vsync can save tearing, should * not be needed for tui as we can just reraster from the source but that part * is not finished */ } } /* reset the 'force composition' output path, this may cost a frame * being slightly delayed on map operations should the direct-scanout * fail, causing force_composition to be set */ uintptr_t tag; cfg_lookup_fun get_config = platform_config_lookup(&tag); d->force_compose = !(hint & HINT_DIRECT) && !get_config("video_device_direct_scanout", 0, NULL, tag); return 0; } static void drop_swapchain(struct dispout* d) { arcan_vobject* vobj = arcan_video_getobject(d->vid); if (!vobj) return; struct rendertarget* newtgt = arcan_vint_findrt(vobj); if (!newtgt) return; /* this will also reset any allocator set */ agp_rendertarget_dropswap(newtgt->art); arcan_video_display.ignore_dirty += 3; } static enum display_update_state draw_display(struct dispout* d) { bool swap_display = true; arcan_vobject* vobj = arcan_video_getobject(d->vid); agp_shader_id shid = agp_default_shader(BASIC_2D); if (!d->buffer.in_dumb_set && d->buffer.dumb.enabled){ return UPDATE_SKIP; } /* if a rendertarget is mapped to the display, check so that that rtgt itself * has had actual drawing operations done to it - as the same target can be * mapped with different scissor rects yielding no change and so on */ struct rendertarget* newtgt = arcan_vint_findrt(vobj); if (newtgt){ size_t nd = agp_rendertarget_dirty(newtgt->art, NULL); verbose_print("(%d) draw display, dirty regions: %zu", nd); if (nd || newtgt->frame_cookie != d->frame_cookie){ agp_rendertarget_dirty_reset(newtgt->art, NULL); } else{ verbose_print("(%d) no dirty, skip"); return UPDATE_SKIP; } newtgt->frame_cookie = d->frame_cookie; } /* * If the following conditions are valid, we can simply add the source vid * to the display directly, saving a full screen copy. */ if (d->hint == HINT_NONE && !d->force_compose && sane_direct_vobj(vobj, "rt_swap")){ swap_display = false; goto out; } /* * object invalid or mapped poorly, just reset to whatever the clear color is */ if (!vobj) { agp_rendertarget_clear(); goto out; } else{ if (vobj->program > 0) shid = vobj->program; agp_activate_vstore( d->vid == ARCAN_VIDEO_WORLDID ? arcan_vint_world() : vobj->vstore); } if (d->skip_blit){ verbose_print("(%d) skip draw, already composed", (int)d->id); d->skip_blit = false; } else { agp_shader_activate(shid); agp_shader_envv(PROJECTION_MATR, d->projection, sizeof(float)*16); agp_rendertarget_clear(); agp_blendstate(BLEND_NONE); agp_draw_vobj(0, 0, d->dispw, d->disph, d->txcos, NULL); verbose_print("(%d) draw, shader: %d, %zu*%zu", (int)d->id, (int)shid, (size_t)d->dispw, (size_t)d->disph); } /* * another rough corner case, if we have a store that is not world ID but * shared with different texture coordinates (to extend display), we need to * draw the cursor .. but if the texture coordinates indicate that we only draw * a subset, we need to check if the cursor is actually inside that area... * Seems more and more that accelerated cursors add to more state explosion * than they are worth .. */ if (vobj->vstore == arcan_vint_world()){ arcan_vint_drawcursor(false); } agp_deactivate_vstore(); out: if (swap_display){ verbose_print("(%d) pre-swap", (int)d->id); d->device->eglenv.swap_buffers(d->device->display, d->buffer.esurf); verbose_print("(%d) swapped", (int)d->id); return UPDATE_FLIP; } verbose_print("(%d) direct- path selected"); return UPDATE_DIRECT; } static bool update_display(struct dispout* d) { if (d->display.dpms != ADPMS_ON) return false; /* we want to know how multiple- displays drift against eachother */ d->last_update = arcan_timemillis(); /* * Make sure we target the right GL context * Notice that the context being set is that of the display buffer, * not the shared outer "headless" context. * * MULTIGPU> will also need to set the agp- current rendertarget */ set_display_context(d); egl_dri.last_display = d; /* activated- rendertarget covers scissor regions etc. so we want to reset */ agp_blendstate(BLEND_NONE); agp_activate_rendertarget(NULL); /* * Currently we only do binary damage / update tracking in that there are EGL * versions for saying 'this region is damaged, update that' to cut down on * fillrate/bw. This should likely be added to the agp_ layer as a simple dirty * list that the draw_vobj calls append to. Some is prepared for (see * agp_rendertarget_dirty), but more is needed in the drawing logic itself. */ enum display_update_state dstate = draw_display(d); uint32_t next_fb = 0; switch(d->device->buftype){ case BUF_STREAM:{ EGLAttrib attr[] = { EGL_DRM_FLIP_EVENT_DATA_NV, (EGLAttrib) d, EGL_NONE, }; if (d->device->vsynch_method == VSYNCH_FLIP){ if (!d->device->eglenv.stream_consumer_acquire_attrib( d->device->display, d->buffer.stream, attr)){ d->device->vsynch_method = VSYNCH_CLOCK; TRACE_MARK_ONESHOT("egl-dri", "eglstream-clock", TRACE_SYS_WARN, d->id, 0, "eglstream-vsynch fail"); debug_print("(%d) - no acq-attr, revert to clock", (int)d->id); } } /* dumb buffer, will never change */ next_fb = d->buffer.cur_fb; } break; case BUF_GBM:{ int rv = -1; /* We use rendertarget_swap for implementing front/back buffering in the * case of rendertarget scanout. */ if (dstate == UPDATE_DIRECT){ if ((rv = get_gbm_fb(d, dstate, NULL, &next_fb)) == -1){ TRACE_MARK_ONESHOT("egl-dri", "gbm-scanout", TRACE_SYS_WARN, d->id, 0, "gbm-direct scanout fail, compose"); debug_print("(%d) direct-scanout buffer " "conversion failed, falling back to composition", true); d->force_compose = true; dstate = draw_display(d); /* Free all textures/buffer objects, disable allocator for the rendertarget and * revert to normal 'gbm-flip + drawing to EGLSurface */ drop_swapchain(d); } else { TRACE_MARK_ONESHOT("egl-dri", "gbm-scanout", TRACE_SYS_WARN, d->id, 0, ""); } } if (dstate == UPDATE_FLIP){ if (!d->buffer.surface) goto out; d->buffer.next_bo = gbm_surface_lock_front_buffer(d->buffer.surface); if (!d->buffer.next_bo){ TRACE_MARK_ONESHOT("egl-dri", "gbm-buffer-lock-fail", TRACE_SYS_ERROR, d->id, 0, ""); verbose_print("(%d) update, failed to lock front buffer", (int)d->id); goto out; } if ((rv = get_gbm_fb(d, dstate, d->buffer.next_bo, &next_fb)) == -1){ TRACE_MARK_ONESHOT("egl-dri", "gbm-framebuffer-fail", TRACE_SYS_ERROR, d->id, 0, ""); debug_print("(%d) - couldn't get framebuffer handle", (int)d->id); gbm_surface_release_buffer(d->buffer.surface, d->buffer.next_bo); goto out; } if (rv == 0){ TRACE_MARK_ONESHOT("egl-dri", "gbm-buffer-release", TRACE_SYS_DEFAULT, d->id, 0, ""); gbm_surface_release_buffer(d->buffer.surface, d->buffer.next_bo); verbose_print("(%d) - no update for display", (int)d->id); goto out; } } } break; case BUF_HEADLESS: break; } bool new_crtc = false; /* mode-switching is defered to the first frame that is ready as things * might've happened in the engine between _init and draw */ if (d->display.reset_mode || !d->display.old_crtc){ /* save a copy of the old_crtc so we know what to restore on shutdown */ if (!d->display.old_crtc) d->display.old_crtc = drmModeGetCrtc(d->device->disp_fd, d->display.crtc); d->display.reset_mode = false; /* do any deferred ioctl- device actions to switch from text to graphics */ platform_device_open("TTYGRAPHICS", 0); new_crtc = true; } if (new_crtc){ debug_print("(%d) deferred modeset, switch now (%d*%d => %d*%d@%d)", (int) d->id, d->dispw, d->disph, d->display.mode.hdisplay, d->display.mode.vdisplay, d->display.mode.vrefresh ); if (d->device->atomic){ atomic_set_mode(d); } else { int rv = drmModeSetCrtc(d->device->disp_fd, d->display.crtc, next_fb, 0, 0, &d->display.con_id, 1, &d->display.mode); if (rv < 0){ debug_print("(%d) error (%d) setting Crtc for %d:%d(con:%d)", (int)d->id, errno, d->device->disp_fd, d->display.crtc, d->display.con_id); } egl_dri.decay = 4; } arcan_conductor_register_display( d->device->card_id, d->id, SYNCH_STATIC, d->display.mode.vrefresh, d->vid); } /* let DRM drive synch and wait for vsynch events on the file descriptor */ if (d->device->vsynch_method == VSYNCH_FLIP){ verbose_print("(%d) request flip (fd: %d, crtc: %"PRIxPTR", fb: %d)", (int)d->id, (uintptr_t) d->display.crtc, (int) next_fb); /* for Atomic, there is also a * DRM_MODE_ATOMIC_NONBLOCK (poll fd), DRM_MODE_ATOMIC_ALLOW_MODESET */ if (0 == drmModePageFlip(d->device->disp_fd, d->display.crtc, next_fb, DRM_MODE_PAGE_FLIP_EVENT, d)){ TRACE_MARK_ONESHOT("egl-dri", "vsynch-req", TRACE_SYS_DEFAULT, d->id, next_fb, "flip"); d->buffer.in_flip = 1; d->buffer.cur_fb = next_fb; verbose_print("(%d) in flip", (int)d->id); } else { debug_print("(%d) error scheduling vsynch-flip (%"PRIxPTR":%"PRIxPTR")", (int)d->id, (uintptr_t) d->buffer.cur_fb, (uintptr_t)next_fb); } } set_device_context(d->device); return true; out: set_device_context(d->device); return false; } void platform_video_prepare_external() { if (in_external) return; int rc = 10; debug_print("preparing external"); TRACE_MARK_ENTER("egl-dri", "external-handover", TRACE_SYS_DEFAULT, 0, 0, ""); do{ for(size_t i = 0; i < MAX_DISPLAYS; i++) disable_display(&displays[i], false); if (egl_dri.destroy_pending) flush_display_events(30, false); } while(egl_dri.destroy_pending && rc-- > 0); /* tell the privsep side that we no-longer need the GPU */ char* pathref = nodes[0].pathref; nodes[0].pathref = NULL; if (pathref){ platform_device_release(pathref, 0); close(nodes[0].disp_fd); } /* this will actually kill the pathref, restore needs it */ release_card(0); nodes[0].pathref = pathref; agp_dropenv(agp_env()); in_external = true; } void platform_video_restore_external() { debug_print("restoring external"); if (!in_external) return; TRACE_MARK_EXIT("egl-dri", "external-handover", TRACE_SYS_DEFAULT, 0, 0, ""); arcan_event_maskall(arcan_event_defaultctx()); /* this is a special place in malbolge, it is possible that the GPU has * disappeared when we restore, or that it has been replaced with a different * one - the setup for that is left hanging until the multi-GPU bits are in * place */ int lfd = -1; if (nodes[0].pathref){ /* our options in this case if not getting the GPU back are currently slim, * with a fallback software or remote AGP layer, the rest of the engine can be * left going, we have enough state to release clients to migrate somewhere */ lfd = platform_device_open(nodes[0].pathref, O_RDWR); if (-1 == lfd){ debug_print("couldn't re-acquire GPU after suspend"); goto give_up; } /* and re-associate draw with disp if that was the case before */ if (nodes[0].draw_fd != -1 && nodes[0].draw_fd == nodes[0].disp_fd){ nodes[0].draw_fd = lfd; } nodes[0].disp_fd = lfd; } /* rebuild the card itself now, if that fails, we are basically screwed, * go to crash recovery */ if (!try_node(lfd, lfd, nodes[0].pathref, 0,nodes[0].buftype, -1, -1, -1, true)){ debug_print("failed to rebuild display after external suspend"); goto give_up; } /* rebuild the mapped and known displays, extsusp is a marker that indicate * that the state of the engine is that the display is still alive, and should * be brought back to that state before we push 'removed' events */ for (size_t i = 0; i < MAX_DISPLAYS; i++){ if (displays[i].state == DISP_EXTSUSP){ /* refc? */ if (setup_kms(&displays[i], displays[i].display.con->connector_id, 0, 0) != 0){ debug_print( "(%d) restore external failed on kms setup", (int)displays[i].id); disable_display(&displays[i], true); } else if (setup_buffers(&displays[i]) == -1){ debug_print( "(%d) restore external failed on buffer alloc", (int)displays[i].id); disable_display(&displays[i], true); } debug_print("(%d) restore ok, flag reset", (int)displays[i].id); displays[i].state = DISP_MAPPED; displays[i].display.reset_mode = true; } } set_device_context(&nodes[0]); agp_init(); in_external = false; arcan_event_clearmask(arcan_event_defaultctx()); return; give_up: arcan_event_clearmask(arcan_event_defaultctx()); arcan_event_enqueue(arcan_event_defaultctx(), &(struct arcan_event){ .category = EVENT_SYSTEM, .sys.kind = EVENT_SYSTEM_EXIT, .sys.errcode = EXIT_FAILURE }); }