/** * libgamma — Display server abstraction layer for gamma ramp adjustments * Copyright © 2014 Mattias Andrée (maandree@member.fsf.org) * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this library. If not, see . */ #ifndef HAVE_GAMMA_METHOD_LINUX_DRM # error Compiling gamma-linux-drm.c without HAVE_GAMMA_METHOD_LINUX_DRM #endif #define _GNU_SOURCE #include "gamma-linux-drm.h" #include "libgamma-error.h" #include "edid.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifndef O_CLOEXEC # define O_CLOEXEC 02000000 #endif #ifndef NGROUPS_MAX # define NGROUPS_MAX 65536 #endif #ifndef PATH_MAX # define PATH_MAX 4096 #endif /** * Graphics card data for the Direct Rendering Manager adjustment method */ typedef struct libgamma_drm_card_data { /** * File descriptor for the connection to the graphics card */ int fd; /** * The graphics card's mode resources */ drmModeRes* res; /** * Resources for open connectors */ drmModeConnector** connectors; /** * Resources for open encoders */ drmModeEncoder** encoders; } libgamma_drm_card_data_t; /** * Return the capabilities of the adjustment method * * @param this The data structure to fill with the method's capabilities */ void libgamma_linux_drm_method_capabilities(libgamma_method_capabilities_t* restrict this) { this->crtc_information = CRTC_INFO_EDID | CRTC_INFO_WIDTH_MM | CRTC_INFO_HEIGHT_MM | CRTC_INFO_WIDTH_MM_EDID | CRTC_INFO_HEIGHT_MM_EDID | CRTC_INFO_GAMMA_SIZE | CRTC_INFO_GAMMA_DEPTH | CRTC_INFO_SUBPIXEL_ORDER | CRTC_INFO_ACTIVE | CRTC_INFO_CONNECTOR_NAME | CRTC_INFO_CONNECTOR_TYPE | CRTC_INFO_GAMMA; this->default_site_known = 1; this->multiple_sites = 0; this->multiple_partitions = 1; this->multiple_crtcs = 1; this->partitions_are_graphics_cards = 1; this->site_restore = 0; this->partition_restore = 0; this->crtc_restore = 0; this->identical_gamma_sizes = 1; this->fixed_gamma_size = 0; this->fixed_gamma_depth = 1; this->real = 1; this->fake = 0; } /** * Initialise an allocated site state * * @param this The site state to initialise * @param site The site identifier, unless it is `NULL` it must a * `free`:able. One the state is destroyed the library * will attempt to free it. There you should not free * it yourself, and it must not be a string constant * or allocate on the stack. Note however that it will * not be free:d if this function fails. * @return Zero on success, otherwise (negative) the value of an * error identifier provided by this library */ int libgamma_linux_drm_site_initialise(libgamma_site_state_t* restrict this, char* restrict site) { char pathname[PATH_MAX]; struct stat _attr; if (site != NULL) return LIBGAMMA_NO_SUCH_SITE; /* Count the number of available graphics cards by * stat:ing there existence in an API filesystem. */ this->partitions_available = 0; for (;;) { snprintf(pathname, sizeof(pathname) / sizeof(char), DRM_DEV_NAME, DRM_DIR_NAME, (int)(this->partitions_available)); if (stat(pathname, &_attr)) break; if (this->partitions_available++ > INT_MAX) return LIBGAMMA_IMPOSSIBLE_AMOUNT; } return 0; } /** * Release all resources held by a site state * * @param this The site state */ void libgamma_linux_drm_site_destroy(libgamma_site_state_t* restrict this) { (void) this; } /** * Restore the gamma ramps all CRTCS with a site to the system settings * * @param this The site state * @return Zero on success, otherwise (negative) the value of an * error identifier provided by this library */ int libgamma_linux_drm_site_restore(libgamma_site_state_t* restrict this) { (void) this; return errno = ENOTSUP, LIBGAMMA_ERRNO_SET; } /** * Initialise an allocated partition state * * @param this The partition state to initialise * @param site The site state for the site that the partition belongs to * @param partition The the index of the partition within the site * @return Zero on success, otherwise (negative) the value of an * error identifier provided by this library */ int libgamma_linux_drm_partition_initialise(libgamma_partition_state_t* restrict this, libgamma_site_state_t* restrict site, size_t partition) { int rc = 0; libgamma_drm_card_data_t* restrict data; char pathname[PATH_MAX]; (void) site; /* Check for partition index overflow. */ if (partition > INT_MAX) return LIBGAMMA_NO_SUCH_PARTITION; /* Allocate and initialise graphics card data. */ this->data = NULL; data = malloc(sizeof(libgamma_drm_card_data_t)); if (data == NULL) return LIBGAMMA_ERRNO_SET; data->fd = -1; data->res = NULL; /* Get the pathname for the graphics card. */ snprintf(pathname, sizeof(pathname) / sizeof(char), DRM_DEV_NAME, DRM_DIR_NAME, (int)partition); /* Acquire access to the graphics card. */ data->fd = open(pathname, O_RDWR | O_CLOEXEC); if (data->fd < 0) { if ((errno == ENXIO) || (errno == ENODEV)) rc = LIBGAMMA_NO_SUCH_PARTITION; else if (errno == EACCES) { struct stat attr; int r; r = stat(pathname, &attr); rc = LIBGAMMA_NO_SUCH_PARTITION; #define __test(R, W) ((attr.st_mode & (R | W)) == (R | W)) if (r) rc = errno == EACCES ? LIBGAMMA_NO_SUCH_PARTITION : LIBGAMMA_ERRNO_SET; else if ((attr.st_uid == geteuid() && __test(S_IRUSR, S_IWUSR)) || (attr.st_gid == getegid() && __test(S_IRGRP, S_IWGRP)) || __test(S_IROTH, S_IWOTH)) rc = LIBGAMMA_DEVICE_ACCESS_FAILED; else if (attr.st_gid == 0 /* root group */ || __test(S_IRGRP, S_IWGRP)) rc = LIBGAMMA_DEVICE_RESTRICTED; else { gid_t supplemental_groups[NGROUPS_MAX]; struct group* group; int i, n; n = getgroups(NGROUPS_MAX, supplemental_groups); if (n < 0) { rc = LIBGAMMA_ERRNO_SET; goto fail_data; } for (i = 0; i < n; i++) if (supplemental_groups[i] == attr.st_gid) break; if (i != n) { rc = LIBGAMMA_DEVICE_ACCESS_FAILED; goto fail_data; } rc = LIBGAMMA_DEVICE_REQUIRE_GROUP; errno = 0; group = getgrgid(attr.st_gid); /* TODO: Not thread-safe. */ libgamma_group_gid = attr.st_gid; libgamma_group_name = group != NULL ? group->gr_name : NULL; } #undef __test } else rc = LIBGAMMA_ERRNO_SET; goto fail_data; } /* Acquire mode resources. */ data->res = drmModeGetResources(data->fd); if (data->res == NULL) { rc = LIBGAMMA_ACQUIRING_MODE_RESOURCES_FAILED; goto fail_fd; } /* Get the number of CRTC:s that are available in the partition. */ if (data->res->count_crtcs < 0) { rc = LIBGAMMA_NEGATIVE_CRTC_COUNT; goto fail_res; } this->crtcs_available = (size_t)(data->res->count_crtcs); return 0; fail_res: drmModeFreeResources(data->res); fail_fd: close(data->fd); fail_data: free(data); return rc; } /** * Release all connectors and encoders * * @param this The graphics card data */ static void release_connectors_and_encoders(libgamma_drm_card_data_t* restrict this) { size_t i, n; if (this->encoders != NULL) for (i = 0, n = (size_t)(this->res->count_connectors); i < n; i++) if (this->encoders[i] != NULL) drmModeFreeEncoder(this->encoders[i]); free(this->encoders); this->encoders = NULL; if (this->connectors != NULL) for (i = 0, n = (size_t)(this->res->count_connectors); i < n; i++) if (this->connectors[i] != NULL) drmModeFreeConnector(this->connectors[i]); free(this->connectors); this->connectors = NULL; } /** * Release all resources held by a partition state * * @param this The partition state */ void libgamma_linux_drm_partition_destroy(libgamma_partition_state_t* restrict this) { libgamma_drm_card_data_t* restrict data = this->data; release_connectors_and_encoders(data); if (data->res != NULL) drmModeFreeResources(data->res); if (data->fd >= 0) close(data->fd); free(data); } /** * Restore the gamma ramps all CRTCS with a partition to the system settings * * @param this The partition state * @return Zero on success, otherwise (negative) the value of an * error identifier provided by this library */ int libgamma_linux_drm_partition_restore(libgamma_partition_state_t* restrict this) { (void) this; return errno = ENOTSUP, LIBGAMMA_ERRNO_SET; } /** * Initialise an allocated CRTC state * * @param this The CRTC state to initialise * @param partition The partition state for the partition that the CRTC belongs to * @param crtc The the index of the CRTC within the site * @return Zero on success, otherwise (negative) the value of an * error identifier provided by this library */ int libgamma_linux_drm_crtc_initialise(libgamma_crtc_state_t* restrict this, libgamma_partition_state_t* restrict partition, size_t crtc) { libgamma_drm_card_data_t* restrict card; uint32_t crtc_id; if (crtc >= partition->crtcs_available) return LIBGAMMA_NO_SUCH_CRTC; card = partition->data; crtc_id = card->res->crtcs[crtc]; this->data = (void*)(size_t)crtc_id; return 0; } /** * Release all resources held by a CRTC state * * @param this The CRTC state */ void libgamma_linux_drm_crtc_destroy(libgamma_crtc_state_t* restrict this) { (void) this; } /** * Restore the gamma ramps for a CRTC to the system settings for that CRTC * * @param this The CRTC state * @return Zero on success, otherwise (negative) the value of an * error identifier provided by this library */ int libgamma_linux_drm_crtc_restore(libgamma_crtc_state_t* restrict this) { (void) this; return errno = ENOTSUP, LIBGAMMA_ERRNO_SET; } /** * Find the connector that a CRTC belongs to * * @param this The CRTC state * @param error Output of the error value to store of error report fields for data that requires the connector '* @return The CRTC's conncetor, `NULL` on error */ static drmModeConnector* find_connector(libgamma_crtc_state_t* restrict this, int* restrict error) { uint32_t crtc_id = (uint32_t)(size_t)(this->data); libgamma_drm_card_data_t* restrict card = this->partition->data; size_t i, n = (size_t)(card->res->count_connectors); /* Open connectors and encoders if not already opened. */ if (card->connectors == NULL) { /* We use calloc so all non-loaded elements are `NULL` after an error. */ if ((card->connectors = calloc(n, sizeof(drmModeConnector*))) == NULL) goto fail; if ((card->encoders = calloc(n, sizeof(drmModeEncoder*))) == NULL) goto fail; for (i = 0; i < n; i++) if (((card->connectors[i] = drmModeGetConnector(card->fd, card->res->connectors[i])) == NULL) || ((card->encoders[i] = drmModeGetEncoder(card->fd, card->connectors[i]->encoder_id)) == NULL)) goto fail; } /* Find connector. */ *error = 0; for (i = 0; i < n; i++) if (card->encoders[i]->crtc_id == crtc_id) return card->connectors[i]; *error = LIBGAMMA_CONNECTOR_UNKNOWN; return NULL; fail: *error = errno; release_connectors_and_encoders(card); return NULL; } /** * Get the size of the gamma ramps for a CRTC * * @param out Instance of a data structure to fill with the information about the CRTC * @param crtc The state of the CRTC whose information should be read * @return The value stored in `out->gamma_size_error` */ static int get_gamma_ramp_size(libgamma_crtc_information_t* restrict out, const libgamma_crtc_state_t* restrict crtc) { libgamma_drm_card_data_t* restrict card = crtc->partition->data; uint32_t crtc_id = card->res->crtcs[crtc->crtc]; drmModeCrtc* restrict crtc_info; errno = 0; crtc_info = drmModeGetCrtc(card->fd, crtc_id); out->gamma_size_error = crtc_info == NULL ? errno : 0; if (out->gamma_size_error == 0) { out->red_gamma_size = (size_t)(crtc_info->gamma_size); out->green_gamma_size = (size_t)(crtc_info->gamma_size); out->blue_gamma_size = (size_t)(crtc_info->gamma_size); out->gamma_size_error = crtc_info->gamma_size < 2 ? LIBGAMMA_SINGLETON_GAMMA_RAMP : 0; drmModeFreeCrtc(crtc_info); } return out->gamma_size_error; } /** * Read information from the CRTC's conncetor * * @param crtc The state of the CRTC whose information should be read * @param out Instance of a data structure to fill with the information about the CRTC * @param connector The CRTC's connector * @param fields OR:ed identifiers for the information about the CRTC that should be read * @return Non-zero if at least on error occured */ static int read_connector_data(libgamma_crtc_state_t* restrict crtc, libgamma_crtc_information_t* restrict out, const drmModeConnector* restrict connector, int32_t fields) { const char* restrict connector_name_base = NULL; /* Get some information that does not require too much work. */ if ((fields & (CRTC_INFO_WIDTH_MM | CRTC_INFO_HEIGHT_MM | CRTC_INFO_CONNECTOR_TYPE | CRTC_INFO_ACTIVE | CRTC_INFO_SUBPIXEL_ORDER))) { /* Get whether or not a monitor is plugged in. */ out->active = connector->connection == DRM_MODE_CONNECTED; out->active_error = connector->connection == DRM_MODE_UNKNOWNCONNECTION ? LIBGAMMA_STATE_UNKNOWN : 0; if (out->active == 0) { if ((fields & (CRTC_INFO_WIDTH_MM | CRTC_INFO_HEIGHT_MM | CRTC_INFO_SUBPIXEL_ORDER))) out->width_mm_error = out->height_mm_error = out->subpixel_order_error = LIBGAMMA_NOT_CONNECTED; goto not_connected; } /* Get viewport dimension. */ out->width_mm = connector->mmWidth; out->height_mm = connector->mmHeight; /* Get subpixel order. */ #define __select(value) \ case DRM_MODE_SUBPIXEL_##value: out->subpixel_order = LIBGAMMA_SUBPIXEL_ORDER_##value; break switch (connector->subpixel) { __select (UNKNOWN); __select (HORIZONTAL_RGB); __select (HORIZONTAL_BGR); __select (VERTICAL_RGB); __select (VERTICAL_BGR); __select (NONE); default: out->subpixel_order_error = LIBGAMMA_SUBPIXEL_ORDER_NOT_RECOGNISED; break; } #undef __select not_connected: /* Get connector type. */ #define __select(type, name) \ case DRM_MODE_CONNECTOR_##type: out->connector_type = LIBGAMMA_CONNECTOR_TYPE_##type, connector_name_base = name; break switch (connector->connector_type) { #ifndef DRM_MODE_CONNECTOR_VIRTUAL # define DRM_MODE_CONNECTOR_VIRTUAL 15 #endif #ifndef DRM_MODE_CONNECTOR_DSI # define DRM_MODE_CONNECTOR_DSI 16 #endif __select (Unknown, "Unknown" ); __select (VGA, "VGA" ); __select (DVII, "DVI-I" ); __select (DVID, "DVI-D" ); __select (DVIA, "DVI-A" ); __select (Composite, "Composite"); __select (SVIDEO, "SVIDEO" ); __select (LVDS, "LVDS" ); __select (Component, "Component"); __select (9PinDIN, "DIN" ); __select (DisplayPort, "DP" ); __select (HDMIA, "HDMI-A" ); __select (HDMIB, "HDMI-B" ); __select (TV, "TV" ); __select (eDP, "eDP" ); __select (VIRTUAL, "VIRTUAL" ); __select (DSI, "DSI" ); default: out->connector_type_error = LIBGAMMA_CONNECTOR_TYPE_NOT_RECOGNISED; out->connector_name_error = LIBGAMMA_CONNECTOR_TYPE_NOT_RECOGNISED; break; } #undef __select } /* Get the connector's name. */ if ((fields & CRTC_INFO_CONNECTOR_NAME) && (out->connector_name_error == 0)) { libgamma_drm_card_data_t* restrict card = crtc->partition->data; uint32_t type = connector->connector_type; size_t i, n = (size_t)(card->res->count_connectors), c = 0; out->connector_name = malloc((strlen(connector_name_base) + 12) * sizeof(char)); if (out->connector_name == NULL) return (out->connector_name_error = errno); for (i = 0; (i < n) && (card->connectors[i] != connector); i++) if (card->connectors[i]->connector_type == type) c++; sprintf(out->connector_name, "%s-" PRIu32, connector_name_base, (uint32_t)(c + 1)); } /* Did something go wrong? */ return out->subpixel_order_error | out->active_error | out->connector_name_error; } /** * Get the extended display identification data for a monitor * * @param crtc The CRTC state * @param out Instance of a data structure to fill with the information about the CRTC * @param connector The CRTC's connector * @reutnr Non-zero on error */ static int get_edid(libgamma_crtc_state_t* restrict crtc, libgamma_crtc_information_t* restrict out, drmModeConnector* connector) { libgamma_drm_card_data_t* restrict card = crtc->partition->data; int prop_n = connector->count_props; int prop_i; drmModePropertyRes* restrict prop; drmModePropertyBlobRes* restrict blob; for (prop_i = 0; prop_i < prop_n; prop_i++) { prop = drmModeGetProperty(card->fd, connector->props[prop_i]); if (prop == NULL) continue; if (!strcmp("EDID", prop->name)) { blob = drmModeGetPropertyBlob(card->fd, (uint32_t)(connector->prop_values[prop_i])); if (blob == NULL) { drmModeFreeProperty(prop); return out->edid_error = LIBGAMMA_PROPERTY_VALUE_QUERY_FAILED; } if (blob->data != NULL) { out->edid_length = blob->length; out->edid = malloc(out->edid_length * sizeof(unsigned char)); if (out->edid == NULL) out->edid_error = errno; else memcpy(out->edid, blob->data, (size_t)(out->edid_length) * sizeof(char)); drmModeFreePropertyBlob(blob); return out->edid == NULL; } drmModeFreePropertyBlob(blob); } drmModeFreeProperty(prop); } return out->edid_error = LIBGAMMA_EDID_NOT_FOUND; } /** * Read information about a CRTC * * @param this Instance of a data structure to fill with the information about the CRTC * @param crtc The state of the CRTC whose information should be read * @param fields OR:ed identifiers for the information about the CRTC that should be read * @return Zero on success, -1 on error. On error refer to the error reports in `this`. */ int libgamma_linux_drm_get_crtc_information(libgamma_crtc_information_t* restrict this, libgamma_crtc_state_t* restrict crtc, int32_t fields) { #define _E(FIELD) ((fields & FIELD) ? LIBGAMMA_CRTC_INFO_NOT_SUPPORTED : 0) int e = 0; drmModeConnector* restrict connector; int require_connector; int free_edid; int error; /* Wipe all error indicators. */ memset(this, 0, sizeof(libgamma_crtc_information_t)); /* We need to free the EDID after us if it is not explicitly requested. */ free_edid = (fields & CRTC_INFO_EDID) == 0; /* Figure out what fields we need to get the data for to get the data for other fields. */ if ((fields & (CRTC_INFO_WIDTH_MM_EDID | CRTC_INFO_HEIGHT_MM_EDID | CRTC_INFO_GAMMA))) fields |= CRTC_INFO_EDID; if ((fields & CRTC_INFO_CONNECTOR_NAME)) fields |= CRTC_INFO_CONNECTOR_TYPE; if (CRTC_INFO_EDID | CRTC_INFO_WIDTH_MM | CRTC_INFO_HEIGHT_MM | CRTC_INFO_SUBPIXEL_ORDER) fields |= CRTC_INFO_ACTIVE; /* Figure out whether we require the connector to get all information we want. */ require_connector = fields & (CRTC_INFO_ACTIVE | CRTC_INFO_CONNECTOR_TYPE); if (require_connector == 0) goto cont; if ((connector = find_connector(crtc, &error)) == NULL) { e |= this->width_mm_error = this->height_mm_error = this->connector_type = this->subpixel_order_error = this->active_error = this->connector_name_error = this->edid_error = this->gamma_error = this->width_mm_edid_error = this->height_mm_edid_error = error; goto cont; } e |= read_connector_data(crtc, this, connector, fields); if ((fields & CRTC_INFO_EDID) == 0) goto cont; if (this->active_error || (this->active == 0)) { e |= this->edid_error = this->gamma_error = this->width_mm_edid_error = this->height_mm_edid_error = LIBGAMMA_NOT_CONNECTED; goto cont; } e |= get_edid(crtc, this, connector); if (this->edid == NULL) { this->gamma_error = this->width_mm_edid_error = this->height_mm_edid_error = this->edid_error; goto cont; } if ((fields & (CRTC_INFO_WIDTH_MM_EDID | CRTC_INFO_HEIGHT_MM_EDID | CRTC_INFO_GAMMA))) e |= libgamma_parse_edid(this, fields); cont: e |= (fields & CRTC_INFO_GAMMA_SIZE) ? get_gamma_ramp_size(this, crtc) : 0; this->gamma_depth = 16; e |= this->gamma_support_error = _E(CRTC_INFO_GAMMA_SUPPORT); /* Free the EDID after us. */ if (free_edid) { free(this->edid); this->edid = NULL; } return e ? -1 : 0; #undef _E } /** * Get current the gamma ramps for a CRTC, 16-bit gamma-depth version * * @param this The CRTC state * @param ramps The gamma ramps to fill with the current values * @return Zero on success, otherwise (negative) the value of an * error identifier provided by this library */ int libgamma_linux_drm_crtc_get_gamma_ramps(libgamma_crtc_state_t* restrict this, libgamma_gamma_ramps_t* restrict ramps) { libgamma_drm_card_data_t* restrict card = this->partition->data; int r; #ifdef DEBUG if ((ramps->red_size != ramps->green_size) || (ramps->red_size != ramps->blue_size)) return LIBGAMMA_MIXED_GAMMA_RAMP_SIZE; #endif r = drmModeCrtcGetGamma(card->fd, (uint32_t)(size_t)(this->data), (uint32_t)(ramps->red_size), ramps->red, ramps->green, ramps->blue); return r ? LIBGAMMA_GAMMA_RAMP_READ_FAILED : 0; } /** * Set the gamma ramps for a CRTC, 16-bit gamma-depth version * * @param this The CRTC state * @param ramps The gamma ramps to apply * @return Zero on success, otherwise (negative) the value of an * error identifier provided by this library */ int libgamma_linux_drm_crtc_set_gamma_ramps(libgamma_crtc_state_t* restrict this, libgamma_gamma_ramps_t ramps) { libgamma_drm_card_data_t* restrict card = this->partition->data; int r; #ifdef DEBUG if ((ramps.red_size != ramps.green_size) || (ramps.red_size != ramps.blue_size)) return LIBGAMMA_MIXED_GAMMA_RAMP_SIZE; #endif r = drmModeCrtcSetGamma(card->fd, (uint32_t)(size_t)(this->data), (uint32_t)(ramps.red_size), ramps.red, ramps.green, ramps.blue); if (r) switch (errno) { case EACCES: case EAGAIN: case EIO: /* Permission denied errors must be ignored, because we do not * have permission to do this while a display server is active. * We are also checking for some other error codes just in case. */ case EBUSY: case EINPROGRESS: /* It is hard to find documentation for DRM (in fact all of this is * just based on the functions names and some testing,) perhaps we * could get this if we are updating to fast. */ break; case EBADF: case ENODEV: case ENXIO: /* XXX: I have not actually tested removing my graphics card or, * monitor but I imagine either of these is what would happen. */ return LIBGAMMA_GRAPHICS_CARD_REMOVED; default: return LIBGAMMA_ERRNO_SET; } return 0; }