gamescope/src/drm.cpp

3253 lines
94 KiB
C++

// DRM output stuff
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <poll.h>
extern "C" {
#include <wlr/types/wlr_buffer.h>
}
#include "drm.hpp"
#include "main.hpp"
#include "modegen.hpp"
#include "vblankmanager.hpp"
#include "wlserver.hpp"
#include "log.hpp"
#include "gpuvis_trace_utils.h"
#include "steamcompmgr.hpp"
#include <algorithm>
#include <thread>
#include <unordered_set>
extern "C" {
#include "libdisplay-info/info.h"
#include "libdisplay-info/edid.h"
#include "libdisplay-info/cta.h"
}
#include "gamescope-control-protocol.h"
struct drm_t g_DRM = {};
uint32_t g_nDRMFormat = DRM_FORMAT_INVALID;
uint32_t g_nDRMFormatOverlay = DRM_FORMAT_INVALID; // for partial composition, we may have more limited formats than base planes + alpha.
bool g_bRotated = false;
bool g_bUseLayers = true;
bool g_bDebugLayers = false;
const char *g_sOutputName = nullptr;
#ifndef DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP
#define DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP 0x15
#endif
struct drm_color_ctm2 {
/*
* Conversion matrix in S31.32 sign-magnitude
* (not two's complement!) format.
*/
__u64 matrix[12];
};
bool g_bSupportsAsyncFlips = false;
enum drm_mode_generation g_drmModeGeneration = DRM_MODE_GENERATE_CVT;
enum g_panel_orientation g_drmModeOrientation = PANEL_ORIENTATION_AUTO;
std::atomic<uint64_t> g_drmEffectiveOrientation[DRM_SCREEN_TYPE_COUNT]{ {DRM_MODE_ROTATE_0}, {DRM_MODE_ROTATE_0} };
bool g_bForceDisableColorMgmt = false;
static LogScope drm_log("drm");
static LogScope drm_verbose_log("drm", LOG_SILENT);
static std::map< std::string, std::string > pnps = {};
drm_screen_type drm_get_connector_type(drmModeConnector *connector);
static void drm_unset_mode( struct drm_t *drm );
static void drm_unset_connector( struct drm_t *drm );
static uint32_t steam_deck_display_rates[] =
{
40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
70
};
static uint32_t galileo_display_rates[] =
{
40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
70, 71, 72, 73, 74, 75, 76, 77, 78, 79,
80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
100,101,102,103,104,105,106,107,108,109,
110,111,112,113,114,115,116,117,118,119,
120,
};
static uint32_t get_conn_display_info_flags(struct drm_t *drm, struct connector *connector)
{
if (!connector)
return 0;
uint32_t flags = 0;
if ( drm_get_connector_type(connector->connector) == DRM_SCREEN_TYPE_INTERNAL )
flags |= GAMESCOPE_CONTROL_DISPLAY_FLAG_INTERNAL_DISPLAY;
if ( connector->vrr_capable )
flags |= GAMESCOPE_CONTROL_DISPLAY_FLAG_SUPPORTS_VRR;
if ( connector->metadata.supportsST2084 )
flags |= GAMESCOPE_CONTROL_DISPLAY_FLAG_SUPPORTS_HDR;
return flags;
}
void drm_send_gamescope_control(wl_resource *control, struct drm_t *drm)
{
// assumes wlserver_lock HELD!
if ( !drm->connector )
return;
auto& conn = drm->connector;
uint32_t flags = get_conn_display_info_flags( drm, drm->connector );
struct wl_array display_rates;
wl_array_init(&display_rates);
if ( conn->valid_display_rates.size() )
{
size_t size = conn->valid_display_rates.size() * sizeof(uint32_t);
uint32_t *ptr = (uint32_t *)wl_array_add( &display_rates, size );
memcpy( ptr, conn->valid_display_rates.data(), size );
}
gamescope_control_send_active_display_info( control, drm->connector->name, drm->connector->make, drm->connector->model, flags, &display_rates );
wl_array_release(&display_rates);
}
static void update_connector_display_info_wl(struct drm_t *drm)
{
wlserver_lock();
for ( const auto &control : wlserver.gamescope_controls )
{
drm_send_gamescope_control(control, drm);
}
wlserver_unlock();
}
inline uint64_t drm_calc_s31_32(float val)
{
// S31.32 sign-magnitude
float integral = 0.0f;
float fractional = modf( fabsf( val ), &integral );
union
{
struct
{
uint64_t fractional : 32;
uint64_t integral : 31;
uint64_t sign_part : 1;
} s31_32_bits;
uint64_t s31_32;
} color;
color.s31_32_bits.sign_part = val < 0 ? 1 : 0;
color.s31_32_bits.integral = uint64_t( integral );
color.s31_32_bits.fractional = uint64_t( fractional * float( 1ull << 32 ) );
return color.s31_32;
}
static struct fb& get_fb( struct drm_t& drm, uint32_t id )
{
std::lock_guard<std::mutex> m( drm.fb_map_mutex );
return drm.fb_map[ id ];
}
static struct crtc *find_crtc_for_connector(struct drm_t *drm, const struct connector *connector) {
for (size_t i = 0; i < drm->crtcs.size(); i++) {
uint32_t crtc_mask = 1 << i;
if (connector->possible_crtcs & crtc_mask)
return &drm->crtcs[i];
}
return nullptr;
}
static bool get_plane_formats(struct drm_t *drm, struct plane *plane, struct wlr_drm_format_set *formats) {
for (uint32_t k = 0; k < plane->plane->count_formats; k++) {
uint32_t fmt = plane->plane->formats[k];
wlr_drm_format_set_add(formats, fmt, DRM_FORMAT_MOD_INVALID);
}
if (plane->props.count("IN_FORMATS") > 0) {
uint64_t blob_id = plane->initial_prop_values["IN_FORMATS"];
drmModePropertyBlobRes *blob = drmModeGetPropertyBlob(drm->fd, blob_id);
if (!blob) {
drm_log.errorf_errno("drmModeGetPropertyBlob(IN_FORMATS) failed");
return false;
}
struct drm_format_modifier_blob *data =
(struct drm_format_modifier_blob *)blob->data;
uint32_t *fmts = (uint32_t *)((char *)data + data->formats_offset);
struct drm_format_modifier *mods = (struct drm_format_modifier *)
((char *)data + data->modifiers_offset);
for (uint32_t i = 0; i < data->count_modifiers; ++i) {
for (int j = 0; j < 64; ++j) {
if (mods[i].formats & ((uint64_t)1 << j)) {
wlr_drm_format_set_add(formats,
fmts[j + mods[i].offset], mods[i].modifier);
}
}
}
drmModeFreePropertyBlob(blob);
}
return true;
}
static const char *get_enum_name(const drmModePropertyRes *prop, uint64_t value)
{
for (int i = 0; i < prop->count_enums; i++) {
if (prop->enums[i].value == value)
return prop->enums[i].name;
}
return nullptr;
}
static uint32_t pick_plane_format( const struct wlr_drm_format_set *formats, uint32_t Xformat, uint32_t Aformat )
{
uint32_t result = DRM_FORMAT_INVALID;
for ( size_t i = 0; i < formats->len; i++ ) {
uint32_t fmt = formats->formats[i].format;
if ( fmt == Xformat ) {
// Prefer formats without alpha channel for main plane
result = fmt;
} else if ( result == DRM_FORMAT_INVALID && fmt == Aformat ) {
result = fmt;
}
}
return result;
}
/* Pick a primary plane that can be connected to the chosen CRTC. */
static struct plane *find_primary_plane(struct drm_t *drm)
{
struct plane *primary = nullptr;
for (size_t i = 0; i < drm->planes.size(); i++) {
struct plane *plane = &drm->planes[i];
if (!(plane->plane->possible_crtcs & (1 << drm->crtc_index)))
continue;
uint64_t plane_type = drm->planes[i].initial_prop_values["type"];
if (plane_type == DRM_PLANE_TYPE_PRIMARY) {
primary = plane;
break;
}
}
if (primary == nullptr)
return nullptr;
return primary;
}
static void drm_unlock_fb_internal( struct drm_t *drm, struct fb *fb );
std::atomic<uint64_t> g_nCompletedPageFlipCount = { 0u };
static void page_flip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, unsigned int crtc_id, void *data)
{
uint64_t flipcount = (uint64_t)data;
g_nCompletedPageFlipCount = flipcount;
if ( g_DRM.crtc->id != crtc_id )
return;
// This is the last vblank time
uint64_t vblanktime = sec * 1'000'000'000lu + usec * 1'000lu;
vblank_mark_possible_vblank(vblanktime);
// TODO: get the fbids_queued instance from data if we ever have more than one in flight
drm_verbose_log.debugf("page_flip_handler %" PRIu64, flipcount);
gpuvis_trace_printf("page_flip_handler %" PRIu64, flipcount);
for ( uint32_t i = 0; i < g_DRM.fbids_on_screen.size(); i++ )
{
uint32_t previous_fbid = g_DRM.fbids_on_screen[ i ];
assert( previous_fbid != 0 );
struct fb &previous_fb = get_fb( g_DRM, previous_fbid );
if ( --previous_fb.n_refs == 0 )
{
// we flipped away from this previous fbid, now safe to delete
std::lock_guard<std::mutex> lock( g_DRM.free_queue_lock );
for ( uint32_t i = 0; i < g_DRM.fbid_unlock_queue.size(); i++ )
{
if ( g_DRM.fbid_unlock_queue[ i ] == previous_fbid )
{
drm_verbose_log.debugf("deferred unlock %u", previous_fbid);
drm_unlock_fb_internal( &g_DRM, &get_fb( g_DRM, previous_fbid ) );
g_DRM.fbid_unlock_queue.erase( g_DRM.fbid_unlock_queue.begin() + i );
break;
}
}
for ( uint32_t i = 0; i < g_DRM.fbid_free_queue.size(); i++ )
{
if ( g_DRM.fbid_free_queue[ i ] == previous_fbid )
{
drm_verbose_log.debugf( "deferred free %u", previous_fbid );
drm_drop_fbid( &g_DRM, previous_fbid );
g_DRM.fbid_free_queue.erase( g_DRM.fbid_free_queue.begin() + i );
break;
}
}
}
}
g_DRM.fbids_on_screen = g_DRM.fbids_queued;
g_DRM.fbids_queued.clear();
g_DRM.flip_lock.unlock();
}
void flip_handler_thread_run(void)
{
pthread_setname_np( pthread_self(), "gamescope-kms" );
struct pollfd pollfd = {
.fd = g_DRM.fd,
.events = POLLIN,
};
while ( true )
{
int ret = poll( &pollfd, 1, -1 );
if ( ret < 0 ) {
drm_log.errorf_errno( "polling for DRM events failed" );
break;
}
drmEventContext evctx = {
.version = 3,
.page_flip_handler2 = page_flip_handler,
};
drmHandleEvent(g_DRM.fd, &evctx);
}
}
static const drmModePropertyRes *get_prop(struct drm_t *drm, uint32_t prop_id)
{
if (drm->props.count(prop_id) > 0) {
return drm->props[prop_id];
}
drmModePropertyRes *prop = drmModeGetProperty(drm->fd, prop_id);
if (!prop) {
drm_log.errorf_errno("drmModeGetProperty failed");
return nullptr;
}
drm->props[prop_id] = prop;
return prop;
}
static bool get_object_properties(struct drm_t *drm, uint32_t obj_id, uint32_t obj_type, std::map<std::string, const drmModePropertyRes *> &map, std::map<std::string, uint64_t> &values)
{
drmModeObjectProperties *props = drmModeObjectGetProperties(drm->fd, obj_id, obj_type);
if (!props) {
drm_log.errorf_errno("drmModeObjectGetProperties failed");
return false;
}
map = {};
values = {};
for (uint32_t i = 0; i < props->count_props; i++) {
const drmModePropertyRes *prop = get_prop(drm, props->props[i]);
if (!prop) {
return false;
}
map[prop->name] = prop;
values[prop->name] = props->prop_values[i];
}
drmModeFreeObjectProperties(props);
return true;
}
static bool compare_modes( drmModeModeInfo mode1, drmModeModeInfo mode2 )
{
bool goodRefresh1 = mode1.vrefresh >= 60;
bool goodRefresh2 = mode2.vrefresh >= 60;
if (goodRefresh1 != goodRefresh2)
return goodRefresh1;
bool preferred1 = mode1.type & DRM_MODE_TYPE_PREFERRED;
bool preferred2 = mode2.type & DRM_MODE_TYPE_PREFERRED;
if (preferred1 != preferred2)
return preferred1;
int area1 = mode1.hdisplay * mode1.vdisplay;
int area2 = mode2.hdisplay * mode2.vdisplay;
if (area1 != area2)
return area1 > area2;
return mode1.vrefresh > mode2.vrefresh;
}
static void
drm_hdr_parse_edid(drm_t *drm, struct connector *connector, const struct di_edid *edid)
{
struct connector_metadata_t *metadata = &connector->metadata;
const struct di_edid_chromaticity_coords* chroma = di_edid_get_chromaticity_coords(edid);
const struct di_cta_hdr_static_metadata_block* hdr_static_metadata = NULL;
const struct di_cta_colorimetry_block* colorimetry = NULL;
const struct di_edid_cta* cta = NULL;
const struct di_edid_ext* const* exts = di_edid_get_extensions(edid);
for (; *exts != NULL; exts++) {
if ((cta = di_edid_ext_get_cta(*exts)))
break;
}
if (cta) {
const struct di_cta_data_block* const* blocks = di_edid_cta_get_data_blocks(cta);
for (; *blocks != NULL; blocks++) {
if (!hdr_static_metadata && (hdr_static_metadata = di_cta_data_block_get_hdr_static_metadata(*blocks)))
continue;
if (!colorimetry && (colorimetry = di_cta_data_block_get_colorimetry(*blocks)))
continue;
}
}
struct hdr_metadata_infoframe *infoframe = &metadata->defaultHdrMetadata.hdmi_metadata_type1;
if (chroma) {
infoframe->display_primaries[0].x = color_xy_to_u16(chroma->red_x);
infoframe->display_primaries[0].y = color_xy_to_u16(chroma->red_y);
infoframe->display_primaries[1].x = color_xy_to_u16(chroma->green_x);
infoframe->display_primaries[1].y = color_xy_to_u16(chroma->green_y);
infoframe->display_primaries[2].x = color_xy_to_u16(chroma->blue_x);
infoframe->display_primaries[2].y = color_xy_to_u16(chroma->blue_y);
infoframe->white_point.x = color_xy_to_u16(chroma->white_x);
infoframe->white_point.y = color_xy_to_u16(chroma->white_y);
}
/* Some sane defaults for SDR for displays with missing data... */
infoframe->max_display_mastering_luminance = nits_to_u16(1000.0f);
infoframe->min_display_mastering_luminance = nits_to_u16_dark(0.0f);
infoframe->max_cll = nits_to_u16(400.0f);
infoframe->max_fall = nits_to_u16(400.0f);
if (hdr_static_metadata) {
if (hdr_static_metadata->desired_content_max_luminance)
infoframe->max_display_mastering_luminance = nits_to_u16(hdr_static_metadata->desired_content_max_luminance);
if (hdr_static_metadata->desired_content_min_luminance)
infoframe->min_display_mastering_luminance = nits_to_u16_dark(hdr_static_metadata->desired_content_min_luminance);
/* To be filled in by the app based on the scene, default to desired_content_max_luminance.
*
* Using display's max_fall for the default metadata max_cll to avoid displays
* overcompensating with tonemapping for SDR content.
*/
float default_max_fall = hdr_static_metadata->desired_content_max_frame_avg_luminance
? hdr_static_metadata->desired_content_max_frame_avg_luminance
: hdr_static_metadata->desired_content_max_luminance;
if (default_max_fall) {
infoframe->max_cll = nits_to_u16(default_max_fall);
infoframe->max_fall = nits_to_u16(default_max_fall);
}
}
metadata->supportsST2084 =
chroma &&
colorimetry && colorimetry->bt2020_rgb &&
hdr_static_metadata && hdr_static_metadata->eotfs && hdr_static_metadata->eotfs->pq;
if (metadata->supportsST2084) {
metadata->defaultHdrMetadata.metadata_type = 0;
infoframe->metadata_type = 0;
infoframe->eotf = HDMI_EOTF_ST2084;
metadata->hdr10_metadata_blob = drm_create_hdr_metadata_blob(drm, &metadata->defaultHdrMetadata);
if (metadata->hdr10_metadata_blob == nullptr) {
fprintf(stderr, "Failed to create blob for HDR_OUTPUT_METADATA. Falling back to null blob.\n");
}
}
const char *coloroverride = getenv( "GAMESCOPE_INTERNAL_COLORIMETRY_OVERRIDE" );
if (coloroverride && drm_get_connector_type(connector->connector) == DRM_SCREEN_TYPE_INTERNAL)
{
if (sscanf( coloroverride, "%f %f %f %f %f %f %f %f",
&metadata->colorimetry.primaries.r.x, &metadata->colorimetry.primaries.r.y,
&metadata->colorimetry.primaries.g.x, &metadata->colorimetry.primaries.g.y,
&metadata->colorimetry.primaries.b.x, &metadata->colorimetry.primaries.b.y,
&metadata->colorimetry.white.x, &metadata->colorimetry.white.y ) == 8 )
{
drm_log.infof("[colorimetry]: GAMESCOPE_INTERNAL_COLORIMETRY_OVERRIDE detected");
}
else
{
drm_log.errorf("[colorimetry]: GAMESCOPE_INTERNAL_COLORIMETRY_OVERRIDE specified, but could not parse \"rx ry gx gy bx by wx wy\"");
}
}
else if (connector->is_steam_deck_display && !connector->is_galileo_display)
{
drm_log.infof("[colorimetry]: Steam Deck (internal display) detected.");
// Hardcode Steam Deck display info to support
// BIOSes with missing info for this in EDID.
drm_log.infof("[colorimetry]: using default steamdeck colorimetry");
metadata->colorimetry = displaycolorimetry_steamdeck_measured;
metadata->eotf = EOTF_Gamma22;
}
else if (chroma && chroma->red_x != 0.0f)
{
drm_log.infof("[colorimetry]: EDID with colorimetry detected. Using it");
metadata->colorimetry.primaries = { { chroma->red_x, chroma->red_y }, { chroma->green_x, chroma->green_y }, { chroma->blue_x, chroma->blue_y } };
metadata->colorimetry.white = { chroma->white_x, chroma->white_y };
metadata->eotf = infoframe->eotf == HDMI_EOTF_ST2084 ? EOTF_PQ : EOTF_Gamma22;
}
else
{
// No valid chroma data in the EDID, fill it in ourselves.
if (infoframe->eotf == HDMI_EOTF_ST2084)
{
drm_log.infof("[colorimetry]: EDID does not define colorimetry. Assuming rec2020 based on HDMI_EOTF_ST2084 support");
// Fallback to 2020 primaries for HDR
metadata->colorimetry = displaycolorimetry_2020;
metadata->eotf = EOTF_PQ;
}
else
{
// Fallback to 709 primaries for SDR
drm_log.infof("[colorimetry]: EDID does not define colorimetry. Assuming rec709 / gamma 2.2");
metadata->colorimetry = displaycolorimetry_709;
metadata->eotf = EOTF_Gamma22;
}
}
drm_log.infof("[colorimetry]: r %f %f", metadata->colorimetry.primaries.r.x, metadata->colorimetry.primaries.r.y);
drm_log.infof("[colorimetry]: g %f %f", metadata->colorimetry.primaries.g.x, metadata->colorimetry.primaries.g.y);
drm_log.infof("[colorimetry]: b %f %f", metadata->colorimetry.primaries.b.x, metadata->colorimetry.primaries.b.y);
drm_log.infof("[colorimetry]: w %f %f", metadata->colorimetry.white.x, metadata->colorimetry.white.y);
}
static constexpr uint32_t EDID_MAX_BLOCK_COUNT = 256;
static constexpr uint32_t EDID_BLOCK_SIZE = 128;
static constexpr uint32_t EDID_MAX_STANDARD_TIMING_COUNT = 8;
static constexpr uint32_t EDID_BYTE_DESCRIPTOR_COUNT = 4;
static constexpr uint32_t EDID_BYTE_DESCRIPTOR_SIZE = 18;
static constexpr uint32_t EDID_MAX_DESCRIPTOR_STANDARD_TIMING_COUNT = 6;
static constexpr uint32_t EDID_MAX_DESCRIPTOR_COLOR_POINT_COUNT = 2;
static constexpr uint32_t EDID_MAX_DESCRIPTOR_ESTABLISHED_TIMING_III_COUNT = 44;
static constexpr uint32_t EDID_MAX_DESCRIPTOR_CVT_TIMING_CODES_COUNT = 4;
static inline uint8_t get_bit_range(uint8_t val, size_t high, size_t low)
{
size_t n;
uint8_t bitmask;
assert(high <= 7 && high >= low);
n = high - low + 1;
bitmask = (uint8_t) ((1 << n) - 1);
return (uint8_t) (val >> low) & bitmask;
}
static inline void set_bit_range(uint8_t *val, size_t high, size_t low, uint8_t bits)
{
size_t n;
uint8_t bitmask;
assert(high <= 7 && high >= low);
n = high - low + 1;
bitmask = (uint8_t) ((1 << n) - 1);
assert((bits & ~bitmask) == 0);
*val |= (uint8_t)(bits << low);
}
static inline void patch_edid_checksum(uint8_t* block)
{
uint8_t sum = 0;
for (uint32_t i = 0; i < EDID_BLOCK_SIZE - 1; i++)
sum += block[i];
uint8_t checksum = uint32_t(256) - uint32_t(sum);
block[127] = checksum;
}
static bool validate_block_checksum(const uint8_t* data)
{
uint8_t sum = 0;
size_t i;
for (i = 0; i < EDID_BLOCK_SIZE; i++) {
sum += data[i];
}
return sum == 0;
}
const char *drm_get_patched_edid_path()
{
return getenv("GAMESCOPE_PATCHED_EDID_FILE");
}
static uint8_t encode_max_luminance(float nits)
{
if (nits == 0.0f)
return 0;
return ceilf((logf(nits / 50.0f) / logf(2.0f)) * 32.0f);
}
static void create_patched_edid( const uint8_t *orig_data, size_t orig_size, drm_t *drm, struct connector *conn )
{
// A zero length indicates that the edid parsing failed.
if (orig_size == 0) {
return;
}
std::vector<uint8_t> edid(orig_data, orig_data + orig_size);
if ( g_bRotated )
{
// Patch width, height.
drm_log.infof("[patched edid] Patching dims %ux%u -> %ux%u", edid[0x15], edid[0x16], edid[0x16], edid[0x15]);
std::swap(edid[0x15], edid[0x16]);
for (uint32_t i = 0; i < EDID_BYTE_DESCRIPTOR_COUNT; i++)
{
uint8_t *byte_desc_data = &edid[0x36 + i * EDID_BYTE_DESCRIPTOR_SIZE];
if (byte_desc_data[0] || byte_desc_data[1])
{
uint32_t horiz = (get_bit_range(byte_desc_data[4], 7, 4) << 8) | byte_desc_data[2];
uint32_t vert = (get_bit_range(byte_desc_data[7], 7, 4) << 8) | byte_desc_data[5];
drm_log.infof("[patched edid] Patching res %ux%u -> %ux%u", horiz, vert, vert, horiz);
std::swap(byte_desc_data[4], byte_desc_data[7]);
std::swap(byte_desc_data[2], byte_desc_data[5]);
break;
}
}
patch_edid_checksum(&edid[0]);
}
// If we are debugging HDR support lazily on a regular Deck,
// just hotpatch the edid for the game so we get values we want as if we had
// an external display attached.
// (Allows for debugging undocked fallback without undocking/redocking)
if ( g_bForceHDRSupportDebug && !conn->metadata.supportsST2084 )
{
// TODO: Allow for override of min luminance
float flMaxPeakLuminance = g_ColorMgmt.pending.hdrTonemapDisplayMetadata.BIsValid() ?
g_ColorMgmt.pending.hdrTonemapDisplayMetadata.flWhitePointNits :
g_ColorMgmt.pending.flInternalDisplayBrightness;
drm_log.infof("[edid] Patching HDR static metadata. max peak luminance/max frame avg luminance = %f nits", flMaxPeakLuminance );
const uint8_t new_hdr_static_metadata_block[]
{
(1 << HDMI_EOTF_SDR) | (1 << HDMI_EOTF_TRADITIONAL_HDR) | (1 << HDMI_EOTF_ST2084), /* supported eotfs */
1, /* type 1 */
encode_max_luminance(flMaxPeakLuminance), /* desired content max peak luminance */
encode_max_luminance(flMaxPeakLuminance * 0.8f), /* desired content max frame avg luminance */
0, /* desired content min luminance -- 0 is technically "undefined" */
};
int ext_count = int(edid.size() / EDID_BLOCK_SIZE) - 1;
assert(ext_count == edid[0x7E]);
bool has_cta_block = false;
bool has_hdr_metadata_block = false;
for (int i = 0; i < ext_count; i++)
{
uint8_t *ext_data = &edid[EDID_BLOCK_SIZE + i * EDID_BLOCK_SIZE];
uint8_t tag = ext_data[0];
if (tag == DI_EDID_EXT_CEA)
{
has_cta_block = true;
uint8_t dtd_start = ext_data[2];
uint8_t flags = ext_data[3];
if (dtd_start == 0)
{
drm_log.infof("[josh edid] Hmmmm.... dtd start is 0. Interesting... Not going further! :-(");
continue;
}
if (flags != 0)
{
drm_log.infof("[josh edid] Hmmmm.... non-zero CTA flags. Interesting... Not going further! :-(");
continue;
}
const int CTA_HEADER_SIZE = 4;
int j = CTA_HEADER_SIZE;
while (j < dtd_start)
{
uint8_t data_block_header = ext_data[j];
uint8_t data_block_tag = get_bit_range(data_block_header, 7, 5);
uint8_t data_block_size = get_bit_range(data_block_header, 4, 0);
if (j + 1 + data_block_size > dtd_start)
{
drm_log.infof("[josh edid] Hmmmm.... CTA malformatted. Interesting... Not going further! :-(");
break;
}
uint8_t *data_block = &ext_data[j + 1];
if (data_block_tag == 7) // extended
{
uint8_t extended_tag = data_block[0];
uint8_t *extended_block = &data_block[1];
uint8_t extended_block_size = data_block_size - 1;
if (extended_tag == 6) // hdr static
{
if (extended_block_size >= sizeof(new_hdr_static_metadata_block))
{
drm_log.infof("[josh edid] Patching existing HDR Metadata with our own!");
memcpy(extended_block, new_hdr_static_metadata_block, sizeof(new_hdr_static_metadata_block));
has_hdr_metadata_block = true;
}
}
}
j += 1 + data_block_size; // account for header size.
}
if (!has_hdr_metadata_block)
{
const int hdr_metadata_block_size_plus_headers = sizeof(new_hdr_static_metadata_block) + 2; // +1 for header, +1 for extended header -> +2
drm_log.infof("[josh edid] No HDR metadata block to patch... Trying to insert one.");
// Assert that the end of the data blocks == dtd_start
if (dtd_start != j)
{
drm_log.infof("[josh edid] dtd_start != end of blocks. Giving up patching. I'm too scared to attempt it.");
}
// Move back the dtd to make way for our block at the end.
uint8_t *dtd = &ext_data[dtd_start];
memmove(dtd + hdr_metadata_block_size_plus_headers, dtd, hdr_metadata_block_size_plus_headers);
dtd_start += hdr_metadata_block_size_plus_headers;
// Data block is where the dtd was.
uint8_t *data_block = dtd;
// header
data_block[0] = 0;
set_bit_range(&data_block[0], 7, 5, 7); // extended tag
set_bit_range(&data_block[0], 4, 0, sizeof(new_hdr_static_metadata_block) + 1); // size (+1 for extended header, does not include normal header)
// extended header
data_block[1] = 6; // hdr metadata extended tag
memcpy(&data_block[2], new_hdr_static_metadata_block, sizeof(new_hdr_static_metadata_block));
}
patch_edid_checksum(ext_data);
bool sum_valid = validate_block_checksum(ext_data);
drm_log.infof("[josh edid] CTA Checksum valid? %s", sum_valid ? "Y" : "N");
}
}
if (!has_cta_block)
{
drm_log.infof("[josh edid] Couldn't patch for HDR metadata as we had no CTA block! Womp womp =c");
}
}
bool sum_valid = validate_block_checksum(&edid[0]);
drm_log.infof("[josh edid] BASE Checksum valid? %s", sum_valid ? "Y" : "N");
// Write it out then flip it over atomically.
const char *filename = drm_get_patched_edid_path();
if (!filename)
{
drm_log.errorf("[josh edid] Couldn't write patched edid. No Path.");
return;
}
char filename_tmp[PATH_MAX];
snprintf(filename_tmp, sizeof(filename_tmp), "%s.tmp", filename);
FILE *file = fopen(filename_tmp, "wb");
if (!file)
{
drm_log.errorf("[josh edid] Couldn't open file: %s", filename_tmp);
return;
}
fwrite(edid.data(), 1, edid.size(), file);
fflush(file);
fclose(file);
rename(filename_tmp, filename);
drm_log.infof("[josh edid] Wrote new edid to: %s", filename);
}
void drm_update_patched_edid( drm_t *drm )
{
if (!drm || !drm->connector)
return;
create_patched_edid(drm->connector->edid_data.data(), drm->connector->edid_data.size(), drm, drm->connector);
}
#define GALILEO_SDC_PID 0x3003
#define GALILEO_BOE_PID 0x3004
static void parse_edid( drm_t *drm, struct connector *conn)
{
memset(conn->make_pnp, 0, sizeof(conn->make_pnp));
free(conn->make);
conn->make = NULL;
free(conn->model);
conn->model = NULL;
if (conn->props.count("EDID") == 0) {
return;
}
uint64_t blob_id = conn->initial_prop_values["EDID"];
if (blob_id == 0) {
return;
}
drmModePropertyBlobRes *blob = drmModeGetPropertyBlob(drm->fd, blob_id);
if (!blob) {
drm_log.errorf_errno("drmModeGetPropertyBlob(EDID) failed");
return;
}
struct di_info *info = di_info_parse_edid(blob->data, blob->length);
if (!info) {
drmModeFreePropertyBlob(blob);
drm_log.errorf("Failed to parse edid");
return;
}
conn->edid_data = std::vector<uint8_t>((const uint8_t*)blob->data, ((const uint8_t*)(blob->data)) + blob->length);
drmModeFreePropertyBlob(blob);
const struct di_edid *edid = di_info_get_edid(info);
const struct di_edid_vendor_product *vendor_product = di_edid_get_vendor_product(edid);
char pnp_id[] = {
vendor_product->manufacturer[0],
vendor_product->manufacturer[1],
vendor_product->manufacturer[2],
'\0',
};
memcpy(conn->make_pnp, pnp_id, sizeof(pnp_id));
if (pnps.count(pnp_id) > 0) {
conn->make = strdup(pnps[pnp_id].c_str());
}
else {
// Some vendors like AOC, don't have a PNP id listed,
// but their name is literally just "AOC", so just
// use the PNP name directly.
conn->make = strdup(pnp_id);
}
const struct di_edid_display_descriptor *const *descriptors = di_edid_get_display_descriptors(edid);
for (size_t i = 0; descriptors[i] != NULL; i++) {
const struct di_edid_display_descriptor *desc = descriptors[i];
if (di_edid_display_descriptor_get_tag(desc) == DI_EDID_DISPLAY_DESCRIPTOR_PRODUCT_NAME) {
conn->model = strdup(di_edid_display_descriptor_get_string(desc));
}
}
drm_log.infof("Connector make %s model %s", conn->make_pnp, conn->model );
conn->is_steam_deck_display =
(strcmp(conn->make_pnp, "WLC") == 0 && strcmp(conn->model, "ANX7530 U") == 0) ||
(strcmp(conn->make_pnp, "ANX") == 0 && strcmp(conn->model, "ANX7530 U") == 0) ||
(strcmp(conn->make_pnp, "VLV") == 0 && strcmp(conn->model, "ANX7530 U") == 0) ||
(strcmp(conn->make_pnp, "VLV") == 0 && strcmp(conn->model, "Jupiter") == 0);
if ((vendor_product->product == GALILEO_SDC_PID) || (vendor_product->product == GALILEO_BOE_PID)) {
conn->is_galileo_display = vendor_product->product;
conn->valid_display_rates = std::span(galileo_display_rates);
} else {
conn->is_galileo_display = 0;
if ( conn->is_steam_deck_display )
conn->valid_display_rates = std::span(steam_deck_display_rates);
}
drm_hdr_parse_edid(drm, conn, edid);
di_info_destroy(info);
}
static bool refresh_state( drm_t *drm )
{
drmModeRes *resources = drmModeGetResources(drm->fd);
if (resources == nullptr) {
drm_log.errorf_errno("drmModeGetResources failed");
return false;
}
// Add connectors which appeared
for (int i = 0; i < resources->count_connectors; i++) {
uint32_t conn_id = resources->connectors[i];
if (drm->connectors.count(conn_id) == 0) {
struct connector conn = { .id = conn_id };
drm->connectors[conn_id] = conn;
}
}
// Remove connectors which disappeared
auto it = drm->connectors.begin();
while (it != drm->connectors.end()) {
struct connector *conn = &it->second;
bool found = false;
for (int j = 0; j < resources->count_connectors; j++) {
if (resources->connectors[j] == conn->id) {
found = true;
break;
}
}
if (!found) {
drm_log.debugf("connector '%s' disappeared", conn->name);
if (drm->connector == conn) {
drm_log.infof("current connector '%s' disappeared", conn->name);
drm->connector = nullptr;
}
free(conn->name);
conn->name = nullptr;
conn->metadata.hdr10_metadata_blob = nullptr;
drmModeFreeConnector(conn->connector);
it = drm->connectors.erase(it);
} else {
it++;
}
}
drmModeFreeResources(resources);
// Re-probe connectors props and status
for (auto &kv : drm->connectors) {
struct connector *conn = &kv.second;
if (conn->connector != nullptr) {
conn->metadata.hdr10_metadata_blob = nullptr;
drmModeFreeConnector(conn->connector);
}
conn->connector = drmModeGetConnector(drm->fd, conn->id);
if (conn->connector == nullptr) {
drm_log.errorf_errno("drmModeGetConnector failed");
return false;
}
if (!get_object_properties(drm, conn->id, DRM_MODE_OBJECT_CONNECTOR, conn->props, conn->initial_prop_values)) {
return false;
}
/* sort modes by preference: preferred flag, then highest area, then
* highest refresh rate */
std::stable_sort(conn->connector->modes, conn->connector->modes + conn->connector->count_modes, compare_modes);
parse_edid(drm, conn);
if ( conn->name != nullptr )
continue;
const char *type_str = drmModeGetConnectorTypeName(conn->connector->connector_type);
if (!type_str)
type_str = "Unknown";
char name[128] = {};
snprintf(name, sizeof(name), "%s-%d", type_str, conn->connector->connector_type_id);
conn->name = strdup(name);
conn->possible_crtcs = drmModeConnectorGetPossibleCrtcs(drm->fd, conn->connector);
if (!conn->possible_crtcs)
drm_log.errorf_errno("drmModeConnectorGetPossibleCrtcs failed");
conn->has_colorspace = conn->props.contains( "Colorspace" );
conn->has_hdr_output_metadata = conn->props.contains( "HDR_OUTPUT_METADATA" );
conn->has_content_type = conn->props.contains( "content type" );
conn->current.crtc_id = conn->initial_prop_values["CRTC_ID"];
if (conn->has_colorspace)
conn->current.colorspace = conn->initial_prop_values["Colorspace"];
if (conn->has_hdr_output_metadata)
conn->current.hdr_output_metadata = std::make_shared<wlserver_hdr_metadata>(nullptr, conn->initial_prop_values["HDR_OUTPUT_METADATA"], false);
if (conn->has_content_type)
conn->current.content_type = conn->initial_prop_values["content type"];
conn->target_refresh = 0;
conn->vrr_capable = !!conn->initial_prop_values["vrr_capable"];
drm_log.debugf("found new connector '%s'", conn->name);
}
for (size_t i = 0; i < drm->crtcs.size(); i++) {
struct crtc *crtc = &drm->crtcs[i];
if (!get_object_properties(drm, crtc->id, DRM_MODE_OBJECT_CRTC, crtc->props, crtc->initial_prop_values)) {
return false;
}
crtc->has_gamma_lut = crtc->props.contains( "GAMMA_LUT" );
if (!crtc->has_gamma_lut)
drm_log.infof("CRTC %" PRIu32 " has no gamma LUT support", crtc->id);
crtc->has_degamma_lut = crtc->props.contains( "DEGAMMA_LUT" );
if (!crtc->has_degamma_lut)
drm_log.infof("CRTC %" PRIu32 " has no degamma LUT support", crtc->id);
crtc->has_ctm = crtc->props.contains( "CTM" );
if (!crtc->has_ctm)
drm_log.infof("CRTC %" PRIu32 " has no CTM support", crtc->id);
crtc->has_vrr_enabled = crtc->props.contains( "VRR_ENABLED" );
if (!crtc->has_vrr_enabled)
drm_log.infof("CRTC %" PRIu32 " has no VRR_ENABLED support", crtc->id);
crtc->has_valve1_regamma_tf = crtc->props.contains( "VALVE1_CRTC_REGAMMA_TF" );
if (!crtc->has_valve1_regamma_tf)
drm_log.infof("CRTC %" PRIu32 " has no VALVE1_CRTC_REGAMMA_TF support", crtc->id);
crtc->current.active = crtc->initial_prop_values["ACTIVE"];
if (crtc->has_vrr_enabled)
drm->current.vrr_enabled = crtc->initial_prop_values["VRR_ENABLED"];
if (crtc->has_valve1_regamma_tf)
drm->current.output_tf = (drm_valve1_transfer_function) crtc->initial_prop_values["VALVE1_CRTC_REGAMMA_TF"];
}
for (size_t i = 0; i < drm->planes.size(); i++) {
struct plane *plane = &drm->planes[i];
if (!get_object_properties(drm, plane->id, DRM_MODE_OBJECT_PLANE, plane->props, plane->initial_prop_values)) {
return false;
}
plane->has_color_mgmt = plane->props.contains( "VALVE1_PLANE_BLEND_TF" );
}
return true;
}
static bool get_resources(struct drm_t *drm)
{
drmModeRes *resources = drmModeGetResources(drm->fd);
if (resources == nullptr) {
drm_log.errorf_errno("drmModeGetResources failed");
return false;
}
for (int i = 0; i < resources->count_crtcs; i++) {
struct crtc crtc = { .id = resources->crtcs[i] };
crtc.crtc = drmModeGetCrtc(drm->fd, crtc.id);
if (crtc.crtc == nullptr) {
drm_log.errorf_errno("drmModeGetCrtc failed");
return false;
}
drm->crtcs.push_back(crtc);
}
drmModeFreeResources(resources);
drmModePlaneRes *plane_resources = drmModeGetPlaneResources(drm->fd);
if (!plane_resources) {
drm_log.errorf_errno("drmModeGetPlaneResources failed");
return false;
}
for (uint32_t i = 0; i < plane_resources->count_planes; i++) {
struct plane plane = { .id = plane_resources->planes[i] };
plane.plane = drmModeGetPlane(drm->fd, plane.id);
if (plane.plane == nullptr) {
drm_log.errorf_errno("drmModeGetPlane failed");
return false;
}
drm->planes.push_back(plane);
}
drmModeFreePlaneResources(plane_resources);
if (!refresh_state(drm))
return false;
for (size_t i = 0; i < drm->crtcs.size(); i++) {
struct crtc *crtc = &drm->crtcs[i];
crtc->pending = crtc->current;
}
return true;
}
struct mode_blocklist_entry
{
uint32_t width, height, refresh;
};
// Filter out reporting some modes that are required for
// certain certifications, but are completely useless,
// and probably don't fit the display pixel size.
static mode_blocklist_entry g_badModes[] =
{
{ 4096, 2160, 0 },
};
static const drmModeModeInfo *find_mode( const drmModeConnector *connector, int hdisplay, int vdisplay, uint32_t vrefresh )
{
for (int i = 0; i < connector->count_modes; i++) {
const drmModeModeInfo *mode = &connector->modes[i];
bool bad = false;
for (const auto& badMode : g_badModes) {
bad |= (badMode.width == 0 || mode->hdisplay == badMode.width)
&& (badMode.height == 0 || mode->vdisplay == badMode.height)
&& (badMode.refresh == 0 || mode->vrefresh == badMode.refresh);
}
if (bad)
continue;
if (hdisplay != 0 && hdisplay != mode->hdisplay)
continue;
if (vdisplay != 0 && vdisplay != mode->vdisplay)
continue;
if (vrefresh != 0 && vrefresh != mode->vrefresh)
continue;
return mode;
}
return NULL;
}
static std::unordered_map<std::string, int> parse_connector_priorities(const char *str)
{
std::unordered_map<std::string, int> priorities{};
if (!str) {
return priorities;
}
int i = 0;
char *buf = strdup(str);
char *name = strtok(buf, ",");
while (name) {
priorities[name] = i;
i++;
name = strtok(nullptr, ",");
}
free(buf);
return priorities;
}
static int get_connector_priority(struct drm_t *drm, const char *name)
{
if (drm->connector_priorities.count(name) > 0) {
return drm->connector_priorities[name];
}
if (drm->connector_priorities.count("*") > 0) {
return drm->connector_priorities["*"];
}
return drm->connector_priorities.size();
}
static bool get_saved_mode(const char *description, saved_mode &mode_info)
{
const char *mode_file = getenv("GAMESCOPE_MODE_SAVE_FILE");
if (!mode_file || !*mode_file)
return false;
FILE *file = fopen(mode_file, "r");
if (!file)
return false;
char line[256];
while (fgets(line, sizeof(line), file))
{
char saved_description[256];
bool valid = sscanf(line, "%255[^:]:%dx%d@%d", saved_description, &mode_info.width, &mode_info.height, &mode_info.refresh) == 4;
if (valid && !strcmp(saved_description, description))
{
fclose(file);
return true;
}
}
fclose(file);
return false;
}
static bool setup_best_connector(struct drm_t *drm, bool force, bool initial)
{
if (drm->connector && drm->connector->connector->connection != DRM_MODE_CONNECTED) {
drm_log.infof("current connector '%s' disconnected", drm->connector->name);
drm->connector = nullptr;
}
struct connector *best = nullptr;
int best_priority = INT_MAX;
for (auto &kv : drm->connectors) {
struct connector *conn = &kv.second;
if (conn->connector->connection != DRM_MODE_CONNECTED)
continue;
if (drm->force_internal && drm_get_connector_type(conn->connector) == DRM_SCREEN_TYPE_EXTERNAL)
continue;
int priority = get_connector_priority(drm, conn->name);
if (priority < best_priority) {
best = conn;
best_priority = priority;
}
}
if (!force) {
if ((!best && drm->connector) || (best && best == drm->connector)) {
// Let's keep our current connector
return true;
}
}
if (best == nullptr) {
drm_log.infof("cannot find any connected connector!");
drm_unset_connector(drm);
drm_unset_mode(drm);
const struct wlserver_output_info wlserver_output_info = {
.description = "Virtual screen",
};
wlserver_lock();
wlserver_set_output_info(&wlserver_output_info);
wlserver_unlock();
return true;
}
if (!drm_set_connector(drm, best)) {
return false;
}
char description[256];
if (drm_get_connector_type(best->connector) == DRM_SCREEN_TYPE_INTERNAL) {
snprintf(description, sizeof(description), "Internal screen");
} else if (best->make && best->model) {
snprintf(description, sizeof(description), "%s %s", best->make, best->model);
} else if (best->model) {
snprintf(description, sizeof(description), "%s", best->model);
} else {
snprintf(description, sizeof(description), "External screen");
}
const drmModeModeInfo *mode = nullptr;
if ( drm->preferred_width != 0 || drm->preferred_height != 0 || drm->preferred_refresh != 0 )
{
mode = find_mode(best->connector, drm->preferred_width, drm->preferred_height, drm->preferred_refresh);
}
if (!mode && drm_get_connector_type(best->connector) == DRM_SCREEN_TYPE_EXTERNAL) {
saved_mode mode_info;
if (get_saved_mode(description, mode_info))
mode = find_mode(best->connector, mode_info.width, mode_info.height, mode_info.refresh);
}
if (!mode) {
mode = find_mode(best->connector, 0, 0, 0);
}
if (!mode) {
drm_log.errorf("could not find mode!");
return false;
}
best->target_refresh = mode->vrefresh;
if (!drm_set_mode(drm, mode)) {
return false;
}
const struct wlserver_output_info wlserver_output_info = {
.description = description,
.phys_width = (int) best->connector->mmWidth,
.phys_height = (int) best->connector->mmHeight,
};
wlserver_lock();
wlserver_set_output_info(&wlserver_output_info);
wlserver_unlock();
if (!initial)
create_patched_edid(best->edid_data.data(), best->edid_data.size(), drm, best);
update_connector_display_info_wl( drm );
return true;
}
void load_pnps(void)
{
const char *filename = HWDATA_PNP_IDS;
FILE *f = fopen(filename, "r");
if (!f) {
drm_log.infof("failed to open PNP IDs file at '%s'", filename);
return;
}
char *line = NULL;
size_t line_size = 0;
while (getline(&line, &line_size, f) >= 0) {
char *nl = strchr(line, '\n');
if (nl) {
*nl = '\0';
}
char *sep = strchr(line, '\t');
if (!sep) {
continue;
}
*sep = '\0';
std::string id(line);
std::string name(sep + 1);
pnps[id] = name;
}
free(line);
fclose(f);
}
static bool env_to_bool(const char *env)
{
if (!env || !*env)
return false;
return !!atoi(env);
}
bool init_drm(struct drm_t *drm, int width, int height, int refresh, bool wants_adaptive_sync)
{
load_pnps();
drm->wants_vrr_enabled = wants_adaptive_sync;
drm->preferred_width = width;
drm->preferred_height = height;
drm->preferred_refresh = refresh;
drm->device_name = nullptr;
dev_t dev_id = 0;
if (vulkan_primary_dev_id(&dev_id)) {
drmDevice *drm_dev = nullptr;
if (drmGetDeviceFromDevId(dev_id, 0, &drm_dev) != 0) {
drm_log.errorf("Failed to find DRM device with device ID %" PRIu64, (uint64_t)dev_id);
return false;
}
assert(drm_dev->available_nodes & (1 << DRM_NODE_PRIMARY));
drm->device_name = strdup(drm_dev->nodes[DRM_NODE_PRIMARY]);
drm_log.infof("opening DRM node '%s'", drm->device_name);
}
else
{
drm_log.infof("warning: picking an arbitrary DRM device");
}
drm->fd = wlsession_open_kms( drm->device_name );
if ( drm->fd < 0 )
{
drm_log.errorf("Could not open KMS device");
return false;
}
if (drmSetClientCap(drm->fd, DRM_CLIENT_CAP_ATOMIC, 1) != 0) {
drm_log.errorf("drmSetClientCap(ATOMIC) failed");
return false;
}
if (drmGetCap(drm->fd, DRM_CAP_CURSOR_WIDTH, &drm->cursor_width) != 0) {
drm->cursor_width = 64;
}
if (drmGetCap(drm->fd, DRM_CAP_CURSOR_HEIGHT, &drm->cursor_height) != 0) {
drm->cursor_height = 64;
}
uint64_t cap;
if (drmGetCap(drm->fd, DRM_CAP_ADDFB2_MODIFIERS, &cap) == 0 && cap != 0) {
drm->allow_modifiers = true;
}
g_bSupportsAsyncFlips = drmGetCap(drm->fd, DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP, &cap) == 0 && cap != 0;
if (!g_bSupportsAsyncFlips)
drm_log.errorf("Immediate flips are not supported by the KMS driver");
static bool async_disabled = env_to_bool(getenv("GAMESCOPE_DISABLE_ASYNC_FLIPS"));
if ( async_disabled )
{
g_bSupportsAsyncFlips = false;
drm_log.errorf("Immediate flips disabled from environment");
}
if (!get_resources(drm)) {
return false;
}
drm->lo_device = liftoff_device_create( drm->fd );
if ( drm->lo_device == nullptr )
return false;
if ( liftoff_device_register_all_planes( drm->lo_device ) < 0 )
return false;
drm_log.infof("Connectors:");
for (const auto &kv : drm->connectors) {
const struct connector *conn = &kv.second;
const char *status_str = "disconnected";
if ( conn->connector->connection == DRM_MODE_CONNECTED )
status_str = "connected";
drm_log.infof(" %s (%s)", conn->name, status_str);
}
drm->connector_priorities = parse_connector_priorities( g_sOutputName );
if (!setup_best_connector(drm, true, true)) {
return false;
}
// Fetch formats which can be scanned out
for (size_t i = 0; i < drm->planes.size(); i++) {
struct plane *plane = &drm->planes[i];
if (!get_plane_formats(drm, plane, &drm->formats))
return false;
}
// TODO: intersect primary planes formats instead
struct plane *primary_plane = drm->primary;
if (primary_plane == nullptr) {
primary_plane = find_primary_plane(drm);
}
if (primary_plane == nullptr) {
drm_log.errorf("Failed to find a primary plane");
return false;
}
if (!get_plane_formats(drm, primary_plane, &drm->primary_formats)) {
return false;
}
// Pick a 10-bit format at first for our composition buffer, for a couple of reasons:
//
// 1. Many game engines automatically render to 10-bit formats such as UE4 which means
// that when we have to composite, we can keep the same HW dithering that we would get if
// we just scanned them out directly.
//
// 2. When compositing HDR content as a fallback when we undock, it avoids introducing
// a bunch of horrible banding when going to G2.2 curve.
// It ensures that we can dither that.
g_nDRMFormat = pick_plane_format(&drm->primary_formats, DRM_FORMAT_XRGB2101010, DRM_FORMAT_ARGB2101010);
if ( g_nDRMFormat == DRM_FORMAT_INVALID ) {
g_nDRMFormat = pick_plane_format(&drm->primary_formats, DRM_FORMAT_XBGR2101010, DRM_FORMAT_ABGR2101010);
if ( g_nDRMFormat == DRM_FORMAT_INVALID ) {
g_nDRMFormat = pick_plane_format(&drm->primary_formats, DRM_FORMAT_XRGB8888, DRM_FORMAT_ARGB8888);
if ( g_nDRMFormat == DRM_FORMAT_INVALID ) {
drm_log.errorf("Primary plane doesn't support any formats >= 8888");
return false;
}
}
}
// ARGB8888 is the Xformat and AFormat here in this function as we want transparent overlay
g_nDRMFormatOverlay = pick_plane_format(&drm->primary_formats, DRM_FORMAT_ARGB2101010, DRM_FORMAT_ARGB2101010);
if ( g_nDRMFormatOverlay == DRM_FORMAT_INVALID ) {
g_nDRMFormatOverlay = pick_plane_format(&drm->primary_formats, DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR2101010);
if ( g_nDRMFormatOverlay == DRM_FORMAT_INVALID ) {
g_nDRMFormatOverlay = pick_plane_format(&drm->primary_formats, DRM_FORMAT_ARGB8888, DRM_FORMAT_ARGB8888);
if ( g_nDRMFormatOverlay == DRM_FORMAT_INVALID ) {
drm_log.errorf("Overlay plane doesn't support any formats >= 8888");
return false;
}
}
}
drm->kms_in_fence_fd = -1;
std::thread flip_handler_thread( flip_handler_thread_run );
flip_handler_thread.detach();
if (g_bUseLayers) {
liftoff_log_set_priority(g_bDebugLayers ? LIFTOFF_DEBUG : LIFTOFF_ERROR);
}
hdr_output_metadata sdr_metadata;
memset(&sdr_metadata, 0, sizeof(sdr_metadata));
drm->sdr_static_metadata = drm_create_hdr_metadata_blob(drm, &sdr_metadata);
drm->flipcount = 0;
drm->needs_modeset = true;
return true;
}
static int add_property(drmModeAtomicReq *req, uint32_t obj_id, std::map<std::string, const drmModePropertyRes *> &props, const char *name, uint64_t value)
{
if ( props.count( name ) == 0 )
{
drm_log.errorf("no property %s on object %u", name, obj_id);
return -ENOENT;
}
const drmModePropertyRes *prop = props[ name ];
int ret = drmModeAtomicAddProperty(req, obj_id, prop->prop_id, value);
if ( ret < 0 )
{
drm_log.errorf_errno( "drmModeAtomicAddProperty failed" );
}
return ret;
}
static int add_connector_property(drmModeAtomicReq *req, struct connector *conn, const char *name, uint64_t value)
{
return add_property(req, conn->id, conn->props, name, value);
}
static int add_crtc_property(drmModeAtomicReq *req, struct crtc *crtc, const char *name, uint64_t value)
{
return add_property(req, crtc->id, crtc->props, name, value);
}
static int add_plane_property(drmModeAtomicReq *req, struct plane *plane, const char *name, uint64_t value)
{
return add_property(req, plane->id, plane->props, name, value);
}
static std::shared_ptr<wlserver_hdr_metadata> get_default_hdr_metadata(struct drm_t *drm, struct connector *connector)
{
if ( !connector->has_hdr_output_metadata )
return nullptr;
if ( !connector->metadata.supportsST2084 )
return nullptr;
return drm->sdr_static_metadata;
}
void finish_drm(struct drm_t *drm)
{
// Disable all connectors, CRTCs and planes. This is necessary to leave a
// clean KMS state behind. Some other KMS clients might not support all of
// the properties we use, e.g. "rotation" and Xorg don't play well
// together.
drmModeAtomicReq *req = drmModeAtomicAlloc();
for ( auto &kv : drm->connectors ) {
struct connector *conn = &kv.second;
add_connector_property(req, conn, "CRTC_ID", 0);
if (conn->has_colorspace)
add_connector_property(req, conn, "Colorspace", 0);
// HACK HACK: Setting to 0 doesn't disable HDR properly.
// Set an SDR metadata blob.
if (conn->has_hdr_output_metadata)
{
auto metadata = get_default_hdr_metadata( drm, conn );
add_connector_property(req, conn, "HDR_OUTPUT_METADATA", metadata ? metadata->blob : 0);
}
if (conn->has_content_type)
add_connector_property(req, conn, "content type", 0);
}
for ( size_t i = 0; i < drm->crtcs.size(); i++ ) {
add_crtc_property(req, &drm->crtcs[i], "MODE_ID", 0);
if ( drm->crtcs[i].has_gamma_lut )
add_crtc_property(req, &drm->crtcs[i], "GAMMA_LUT", 0);
if ( drm->crtcs[i].has_degamma_lut )
add_crtc_property(req, &drm->crtcs[i], "DEGAMMA_LUT", 0);
if ( drm->crtcs[i].has_ctm )
add_crtc_property(req, &drm->crtcs[i], "CTM", 0);
if ( drm->crtcs[i].has_vrr_enabled )
add_crtc_property(req, &drm->crtcs[i], "VRR_ENABLED", 0);
if ( drm->crtcs[i].has_valve1_regamma_tf )
add_crtc_property(req, &drm->crtcs[i], "VALVE1_CRTC_REGAMMA_TF", 0);
add_crtc_property(req, &drm->crtcs[i], "ACTIVE", 0);
}
for ( size_t i = 0; i < drm->planes.size(); i++ ) {
struct plane *plane = &drm->planes[i];
add_plane_property(req, plane, "FB_ID", 0);
add_plane_property(req, plane, "CRTC_ID", 0);
add_plane_property(req, plane, "SRC_X", 0);
add_plane_property(req, plane, "SRC_Y", 0);
add_plane_property(req, plane, "SRC_W", 0);
add_plane_property(req, plane, "SRC_H", 0);
add_plane_property(req, plane, "CRTC_X", 0);
add_plane_property(req, plane, "CRTC_Y", 0);
add_plane_property(req, plane, "CRTC_W", 0);
add_plane_property(req, plane, "CRTC_H", 0);
if (plane->props.count("rotation") > 0)
add_plane_property(req, plane, "rotation", DRM_MODE_ROTATE_0);
if (plane->props.count("alpha") > 0)
add_plane_property(req, plane, "alpha", 0xFFFF);
if (plane->props.count("VALVE1_PLANE_DEGAMMA_TF") > 0)
add_plane_property(req, plane, "VALVE1_PLANE_DEGAMMA_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
if (plane->props.count("VALVE1_PLANE_HDR_MULT") > 0)
add_plane_property(req, plane, "VALVE1_PLANE_HDR_MULT", 0x100000000ULL);
if (plane->props.count("VALVE1_PLANE_SHAPER_TF") > 0)
add_plane_property(req, plane, "VALVE1_PLANE_SHAPER_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
if (plane->props.count("VALVE1_PLANE_SHAPER_LUT") > 0)
add_plane_property(req, plane, "VALVE1_PLANE_SHAPER_LUT", 0 );
if (plane->props.count("VALVE1_PLANE_LUT3D") > 0)
add_plane_property(req, plane, "VALVE1_PLANE_LUT3D", 0 );
if (plane->props.count("VALVE1_PLANE_BLEND_TF") > 0)
add_plane_property(req, plane, "VALVE1_PLANE_BLEND_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
if (plane->props.count("VALVE1_PLANE_BLEND_LUT") > 0)
add_plane_property(req, plane, "VALVE1_PLANE_BLEND_LUT", 0 );
if (plane->props.count("VALVE1_PLANE_CTM") > 0)
add_plane_property(req, plane, "VALVE1_PLANE_CTM", 0 );
}
// We can't do a non-blocking commit here or else risk EBUSY in case the
// previous page-flip is still in flight.
uint32_t flags = DRM_MODE_ATOMIC_ALLOW_MODESET;
int ret = drmModeAtomicCommit( drm->fd, req, flags, nullptr );
if ( ret != 0 ) {
drm_log.errorf_errno( "finish_drm: drmModeAtomicCommit failed" );
}
drmModeAtomicFree(req);
free(drm->device_name);
// We can't close the DRM FD here, it might still be in use by the
// page-flip handler thread.
}
int drm_commit(struct drm_t *drm, const struct FrameInfo_t *frameInfo )
{
int ret;
assert( drm->req != nullptr );
// if (drm->kms_in_fence_fd != -1) {
// add_plane_property(req, plane_id, "IN_FENCE_FD", drm->kms_in_fence_fd);
// }
// drm->kms_out_fence_fd = -1;
// add_crtc_property(req, drm->crtc_id, "OUT_FENCE_PTR",
// (uint64_t)(unsigned long)&drm->kms_out_fence_fd);
assert( drm->fbids_queued.size() == 0 );
bool isPageFlip = drm->flags & DRM_MODE_PAGE_FLIP_EVENT;
if ( isPageFlip ) {
drm->flip_lock.lock();
// Do it before the commit, as otherwise the pageflip handler could
// potentially beat us to the refcount checks.
for ( uint32_t i = 0; i < drm->fbids_in_req.size(); i++ )
{
struct fb &fb = get_fb( g_DRM, drm->fbids_in_req[ i ] );
assert( fb.held_refs );
fb.n_refs++;
}
drm->fbids_queued = drm->fbids_in_req;
}
g_DRM.flipcount++;
drm_verbose_log.debugf("flip commit %" PRIu64, (uint64_t)g_DRM.flipcount);
gpuvis_trace_printf( "flip commit %" PRIu64, (uint64_t)g_DRM.flipcount );
ret = drmModeAtomicCommit(drm->fd, drm->req, drm->flags, (void*)(uint64_t)g_DRM.flipcount );
if ( ret != 0 )
{
drm_log.errorf_errno( "flip error" );
if ( ret != -EBUSY && ret != -EACCES )
{
drm_log.errorf( "fatal flip error, aborting" );
if ( isPageFlip )
drm->flip_lock.unlock();
abort();
}
drm->pending = drm->current;
for ( size_t i = 0; i < drm->crtcs.size(); i++ )
{
drm->crtcs[i].pending = drm->crtcs[i].current;
}
for (auto &kv : drm->connectors) {
struct connector *conn = &kv.second;
conn->pending = conn->current;
}
// Undo refcount if the commit didn't actually work
for ( uint32_t i = 0; i < drm->fbids_in_req.size(); i++ )
{
get_fb( g_DRM, drm->fbids_in_req[ i ] ).n_refs--;
}
drm->fbids_queued.clear();
g_DRM.flipcount--;
if ( isPageFlip )
drm->flip_lock.unlock();
goto out;
} else {
drm->fbids_in_req.clear();
drm->current = drm->pending;
for ( size_t i = 0; i < drm->crtcs.size(); i++ )
{
if ( drm->pending.mode_id != drm->current.mode_id )
drmModeDestroyPropertyBlob(drm->fd, drm->current.mode_id);
for ( uint32_t i = 0; i < EOTF_Count; i++ )
{
if ( drm->pending.lut3d_id[i] != drm->current.lut3d_id[i] )
drmModeDestroyPropertyBlob(drm->fd, drm->current.lut3d_id[i]);
if ( drm->pending.shaperlut_id[i] != drm->current.shaperlut_id[i] )
drmModeDestroyPropertyBlob(drm->fd, drm->current.shaperlut_id[i]);
}
drm->crtcs[i].current = drm->crtcs[i].pending;
}
for (auto &kv : drm->connectors) {
struct connector *conn = &kv.second;
conn->current = conn->pending;
}
}
// Update the draw time
// Ideally this would be updated by something right before the page flip
// is queued and would end up being the new page flip, rather than here.
// However, the page flip handler is called when the page flip occurs,
// not when it is successfully queued.
g_uVblankDrawTimeNS = get_time_in_nanos() - g_SteamCompMgrVBlankTime.pipe_write_time;
if ( isPageFlip ) {
// Wait for flip handler to unlock
drm->flip_lock.lock();
drm->flip_lock.unlock();
}
// if (drm->kms_in_fence_fd != -1) {
// close(drm->kms_in_fence_fd);
// drm->kms_in_fence_fd = -1;
// }
//
// drm->kms_in_fence_fd = drm->kms_out_fence_fd;
out:
drmModeAtomicFree( drm->req );
drm->req = nullptr;
return ret;
}
uint32_t drm_fbid_from_dmabuf( struct drm_t *drm, struct wlr_buffer *buf, struct wlr_dmabuf_attributes *dma_buf )
{
uint32_t fb_id = 0;
if ( !wlr_drm_format_set_has( &drm->formats, dma_buf->format, dma_buf->modifier ) )
{
drm_verbose_log.errorf( "Cannot import FB to DRM: format 0x%" PRIX32 " and modifier 0x%" PRIX64 " not supported for scan-out", dma_buf->format, dma_buf->modifier );
return 0;
}
uint32_t handles[4] = {0};
uint64_t modifiers[4] = {0};
for ( int i = 0; i < dma_buf->n_planes; i++ ) {
if ( drmPrimeFDToHandle( drm->fd, dma_buf->fd[i], &handles[i] ) != 0 )
{
drm_log.errorf_errno("drmPrimeFDToHandle failed");
goto out;
}
/* KMS requires all planes to have the same modifier */
modifiers[i] = dma_buf->modifier;
}
if ( dma_buf->modifier != DRM_FORMAT_MOD_INVALID )
{
if ( !drm->allow_modifiers )
{
drm_log.errorf("Cannot import DMA-BUF: has a modifier (0x%" PRIX64 "), but KMS doesn't support them", dma_buf->modifier);
goto out;
}
if ( drmModeAddFB2WithModifiers( drm->fd, dma_buf->width, dma_buf->height, dma_buf->format, handles, dma_buf->stride, dma_buf->offset, modifiers, &fb_id, DRM_MODE_FB_MODIFIERS ) != 0 )
{
drm_log.errorf_errno("drmModeAddFB2WithModifiers failed");
goto out;
}
}
else
{
if ( drmModeAddFB2( drm->fd, dma_buf->width, dma_buf->height, dma_buf->format, handles, dma_buf->stride, dma_buf->offset, &fb_id, 0 ) != 0 )
{
drm_log.errorf_errno("drmModeAddFB2 failed");
goto out;
}
}
drm_verbose_log.debugf("make fbid %u", fb_id);
/* Nested scope so fb doesn't end up in the out: label */
{
struct fb &fb = get_fb( *drm, fb_id );
assert( fb.held_refs == 0 );
fb.id = fb_id;
fb.buf = buf;
if (!buf)
fb.held_refs++;
fb.n_refs = 0;
}
out:
for ( int i = 0; i < dma_buf->n_planes; i++ ) {
if ( handles[i] == 0 )
continue;
// GEM handles aren't ref'counted by the kernel. Two DMA-BUFs may
// return the same GEM handle, we need to be careful not to
// double-close them.
bool already_closed = false;
for ( int j = 0; j < i; j++ ) {
if ( handles[i] == handles[j] )
already_closed = true;
}
if ( already_closed )
continue;
struct drm_gem_close args = { .handle = handles[i] };
if ( drmIoctl( drm->fd, DRM_IOCTL_GEM_CLOSE, &args ) != 0 ) {
drm_log.errorf_errno( "drmIoctl(GEM_CLOSE) failed" );
}
}
return fb_id;
}
void drm_drop_fbid( struct drm_t *drm, uint32_t fbid )
{
struct fb &fb = get_fb( *drm, fbid );
assert( fb.held_refs == 0 ||
fb.buf == nullptr );
fb.held_refs = 0;
if ( fb.n_refs != 0 )
{
std::lock_guard<std::mutex> lock( drm->free_queue_lock );
drm->fbid_free_queue.push_back( fbid );
return;
}
if (drmModeRmFB( drm->fd, fbid ) != 0 )
{
drm_log.errorf_errno( "drmModeRmFB failed" );
}
}
static void drm_unlock_fb_internal( struct drm_t *drm, struct fb *fb )
{
assert( fb->held_refs == 0 );
assert( fb->n_refs == 0 );
if ( fb->buf != nullptr )
{
wlserver_lock();
wlr_buffer_unlock( fb->buf );
wlserver_unlock();
}
}
void drm_lock_fbid( struct drm_t *drm, uint32_t fbid )
{
struct fb &fb = get_fb( *drm, fbid );
assert( fb.n_refs == 0 );
if ( fb.held_refs++ == 0 )
{
if ( fb.buf != nullptr )
{
wlserver_lock();
wlr_buffer_lock( fb.buf );
wlserver_unlock();
}
}
}
void drm_unlock_fbid( struct drm_t *drm, uint32_t fbid )
{
struct fb &fb = get_fb( *drm, fbid );
assert( fb.held_refs > 0 );
if ( --fb.held_refs != 0 )
return;
if ( fb.n_refs != 0 )
{
std::lock_guard<std::mutex> lock( drm->free_queue_lock );
drm->fbid_unlock_queue.push_back( fbid );
return;
}
/* FB isn't being used in any page-flip, free it immediately */
drm_verbose_log.debugf("free fbid %u", fbid);
drm_unlock_fb_internal( drm, &fb );
}
static uint64_t determine_drm_orientation(struct drm_t *drm, struct connector *conn, const drmModeModeInfo *mode)
{
drm_screen_type screenType = drm_get_connector_type(conn->connector);
if (conn && conn->props.count("panel orientation") > 0)
{
const char *orientation = get_enum_name(conn->props["panel orientation"], conn->initial_prop_values["panel orientation"]);
if (strcmp(orientation, "Normal") == 0)
{
return DRM_MODE_ROTATE_0;
}
else if (strcmp(orientation, "Left Side Up") == 0)
{
return DRM_MODE_ROTATE_90;
}
else if (strcmp(orientation, "Upside Down") == 0)
{
return DRM_MODE_ROTATE_180;
}
else if (strcmp(orientation, "Right Side Up") == 0)
{
return DRM_MODE_ROTATE_270;
}
}
else
{
if (screenType == DRM_SCREEN_TYPE_INTERNAL && mode)
{
// Auto-detect portait mode for internal displays
return mode->hdisplay < mode->vdisplay ? DRM_MODE_ROTATE_270 : DRM_MODE_ROTATE_0;
}
else
{
return DRM_MODE_ROTATE_0;
}
}
return DRM_MODE_ROTATE_0;
}
/* Handle the orientation of the display */
static void update_drm_effective_orientation(struct drm_t *drm, struct connector *conn, const drmModeModeInfo *mode)
{
drm_screen_type screenType = drm_get_connector_type(conn->connector);
if (screenType == DRM_SCREEN_TYPE_INTERNAL)
{
switch ( g_drmModeOrientation )
{
case PANEL_ORIENTATION_0:
g_drmEffectiveOrientation[screenType] = DRM_MODE_ROTATE_0;
break;
case PANEL_ORIENTATION_90:
g_drmEffectiveOrientation[screenType] = DRM_MODE_ROTATE_90;
break;
case PANEL_ORIENTATION_180:
g_drmEffectiveOrientation[screenType] = DRM_MODE_ROTATE_180;
break;
case PANEL_ORIENTATION_270:
g_drmEffectiveOrientation[screenType] = DRM_MODE_ROTATE_270;
break;
case PANEL_ORIENTATION_AUTO:
g_drmEffectiveOrientation[screenType] = determine_drm_orientation(drm, conn, mode);
break;
}
}
else
{
g_drmEffectiveOrientation[screenType] = determine_drm_orientation(drm, conn, mode);
}
}
static void update_drm_effective_orientations(struct drm_t *drm, struct connector *conn, const drmModeModeInfo *mode)
{
drm_screen_type screenType = drm_get_connector_type(conn->connector);
if (screenType == DRM_SCREEN_TYPE_INTERNAL)
{
update_drm_effective_orientation(drm, conn, mode);
return;
}
else if (screenType == DRM_SCREEN_TYPE_EXTERNAL)
{
update_drm_effective_orientation(drm, conn, mode);
struct connector *internal_conn = nullptr;
for ( auto &kv : drm->connectors ) {
struct connector *kv_con = &kv.second;
if (kv_con->connector)
{
drm_screen_type kv_screentype = drm_get_connector_type(kv_con->connector);
if (kv_screentype == DRM_SCREEN_TYPE_INTERNAL)
{
internal_conn = kv_con;
break;
}
}
}
if (internal_conn)
{
const drmModeModeInfo *default_internal_mode = find_mode(internal_conn->connector, 0, 0, 0);
update_drm_effective_orientation(drm, internal_conn, default_internal_mode);
}
}
}
/* Prepares an atomic commit without using libliftoff */
static int
drm_prepare_basic( struct drm_t *drm, const struct FrameInfo_t *frameInfo )
{
// Discard cases where our non-liftoff path is known to fail
drm_screen_type screenType = drm_get_screen_type(drm);
// It only supports one layer
if ( frameInfo->layerCount > 1 )
{
drm_verbose_log.errorf("drm_prepare_basic: cannot handle %d layers", frameInfo->layerCount);
return -EINVAL;
}
if ( frameInfo->layers[ 0 ].fbid == 0 )
{
drm_verbose_log.errorf("drm_prepare_basic: layer has no FB");
return -EINVAL;
}
drmModeAtomicReq *req = drm->req;
uint32_t fb_id = frameInfo->layers[ 0 ].fbid;
drm->fbids_in_req.push_back( fb_id );
add_plane_property(req, drm->primary, "rotation", g_drmEffectiveOrientation[screenType] );
add_plane_property(req, drm->primary, "FB_ID", fb_id);
add_plane_property(req, drm->primary, "CRTC_ID", drm->crtc->id);
add_plane_property(req, drm->primary, "SRC_X", 0);
add_plane_property(req, drm->primary, "SRC_Y", 0);
const uint16_t srcWidth = frameInfo->layers[ 0 ].tex->width();
const uint16_t srcHeight = frameInfo->layers[ 0 ].tex->height();
add_plane_property(req, drm->primary, "SRC_W", srcWidth << 16);
add_plane_property(req, drm->primary, "SRC_H", srcHeight << 16);
gpuvis_trace_printf ( "legacy flip fb_id %u src %ix%i", fb_id,
srcWidth, srcHeight );
int64_t crtcX = frameInfo->layers[ 0 ].offset.x * -1;
int64_t crtcY = frameInfo->layers[ 0 ].offset.y * -1;
int64_t crtcW = srcWidth / frameInfo->layers[ 0 ].scale.x;
int64_t crtcH = srcHeight / frameInfo->layers[ 0 ].scale.y;
if ( g_bRotated )
{
int64_t imageH = frameInfo->layers[ 0 ].tex->contentHeight() / frameInfo->layers[ 0 ].scale.y;
int64_t tmp = crtcX;
crtcX = g_nOutputHeight - imageH - crtcY;
crtcY = tmp;
tmp = crtcW;
crtcW = crtcH;
crtcH = tmp;
}
add_plane_property(req, drm->primary, "CRTC_X", crtcX);
add_plane_property(req, drm->primary, "CRTC_Y", crtcY);
add_plane_property(req, drm->primary, "CRTC_W", crtcW);
add_plane_property(req, drm->primary, "CRTC_H", crtcH);
gpuvis_trace_printf ( "crtc %li,%li %lix%li", crtcX, crtcY, crtcW, crtcH );
// TODO: disable all planes except drm->primary
unsigned test_flags = (drm->flags & DRM_MODE_ATOMIC_ALLOW_MODESET) | DRM_MODE_ATOMIC_TEST_ONLY;
int ret = drmModeAtomicCommit( drm->fd, drm->req, test_flags, NULL );
if ( ret != 0 && ret != -EINVAL && ret != -ERANGE ) {
drm_log.errorf_errno( "drmModeAtomicCommit failed" );
}
return ret;
}
// Only used for NV12 buffers
static drm_color_encoding drm_get_color_encoding(EStreamColorspace colorspace)
{
switch (colorspace)
{
default:
case k_EStreamColorspace_Unknown:
return DRM_COLOR_YCBCR_BT709;
case k_EStreamColorspace_BT601:
return DRM_COLOR_YCBCR_BT601;
case k_EStreamColorspace_BT601_Full:
return DRM_COLOR_YCBCR_BT601;
case k_EStreamColorspace_BT709:
return DRM_COLOR_YCBCR_BT709;
case k_EStreamColorspace_BT709_Full:
return DRM_COLOR_YCBCR_BT709;
}
}
static drm_color_range drm_get_color_range(EStreamColorspace colorspace)
{
switch (colorspace)
{
default:
case k_EStreamColorspace_Unknown:
return DRM_COLOR_YCBCR_FULL_RANGE;
case k_EStreamColorspace_BT601:
return DRM_COLOR_YCBCR_LIMITED_RANGE;
case k_EStreamColorspace_BT601_Full:
return DRM_COLOR_YCBCR_FULL_RANGE;
case k_EStreamColorspace_BT709:
return DRM_COLOR_YCBCR_LIMITED_RANGE;
case k_EStreamColorspace_BT709_Full:
return DRM_COLOR_YCBCR_FULL_RANGE;
}
}
template <typename T>
void hash_combine(size_t& s, const T& v)
{
std::hash<T> h;
s^= h(v) + 0x9e3779b9 + (s<< 6) + (s>> 2);
}
struct LiftoffStateCacheEntry
{
LiftoffStateCacheEntry()
{
memset(this, 0, sizeof(LiftoffStateCacheEntry));
}
int nLayerCount;
struct LiftoffLayerState_t
{
bool ycbcr;
uint32_t zpos;
uint32_t srcW, srcH;
uint32_t crtcX, crtcY, crtcW, crtcH;
drm_color_encoding colorEncoding;
drm_color_range colorRange;
GamescopeAppTextureColorspace colorspace;
} layerState[ k_nMaxLayers ];
bool operator == (const LiftoffStateCacheEntry& entry) const
{
return !memcmp(this, &entry, sizeof(LiftoffStateCacheEntry));
}
};
struct LiftoffStateCacheEntryKasher
{
size_t operator()(const LiftoffStateCacheEntry& k) const
{
size_t hash = 0;
hash_combine(hash, k.nLayerCount);
for ( int i = 0; i < k.nLayerCount; i++ )
{
hash_combine(hash, k.layerState[i].ycbcr);
hash_combine(hash, k.layerState[i].zpos);
hash_combine(hash, k.layerState[i].srcW);
hash_combine(hash, k.layerState[i].srcH);
hash_combine(hash, k.layerState[i].crtcX);
hash_combine(hash, k.layerState[i].crtcY);
hash_combine(hash, k.layerState[i].crtcW);
hash_combine(hash, k.layerState[i].crtcH);
hash_combine(hash, k.layerState[i].colorEncoding);
hash_combine(hash, k.layerState[i].colorRange);
hash_combine(hash, k.layerState[i].colorspace);
}
return hash;
}
};
std::unordered_set<LiftoffStateCacheEntry, LiftoffStateCacheEntryKasher> g_LiftoffStateCache;
static inline drm_valve1_transfer_function colorspace_to_plane_degamma_tf(GamescopeAppTextureColorspace colorspace)
{
switch ( colorspace )
{
default: // Linear in this sense is SRGB. Linear = sRGB image view doing automatic sRGB -> Linear which doesn't happen on DRM side.
case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB:
return DRM_VALVE1_TRANSFER_FUNCTION_SRGB;
case GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU:
case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB:
// Use LINEAR TF for scRGB float format as 80 nit = 1.0 in scRGB, which matches
// what PQ TF decodes to/encodes from.
// AMD internal format is FP16, and generally expected for 1.0 -> 80 nit.
// which just so happens to match scRGB.
return DRM_VALVE1_TRANSFER_FUNCTION_LINEAR;
case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ:
return DRM_VALVE1_TRANSFER_FUNCTION_PQ;
}
}
static inline drm_valve1_transfer_function colorspace_to_plane_shaper_tf(GamescopeAppTextureColorspace colorspace)
{
switch ( colorspace )
{
default:
case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB:
return DRM_VALVE1_TRANSFER_FUNCTION_SRGB;
case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB: // scRGB Linear -> PQ for shaper + 3D LUT
case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ:
return DRM_VALVE1_TRANSFER_FUNCTION_PQ;
case GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU:
return DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT;
}
}
static inline uint32_t ColorSpaceToEOTFIndex( GamescopeAppTextureColorspace colorspace )
{
switch ( colorspace )
{
default:
case GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR: // Not actually linear, just Linear vs sRGB image views in Vulkan. Still viewed as sRGB on the DRM side.
case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB:
// SDR sRGB content treated as native Gamma 22 curve. No need to do sRGB -> 2.2 or whatever.
return EOTF_Gamma22;
case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB:
// Okay, so this is WEIRD right? OKAY Let me explain it to you.
// The plan for scRGB content is to go from scRGB -> PQ in a SHAPER_TF
// before indexing into the shaper. (input from colorspace_to_plane_regamma_tf!)
return EOTF_PQ;
case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ:
return EOTF_PQ;
}
}
LiftoffStateCacheEntry FrameInfoToLiftoffStateCacheEntry( struct drm_t *drm, const FrameInfo_t *frameInfo )
{
LiftoffStateCacheEntry entry{};
entry.nLayerCount = frameInfo->layerCount;
for ( int i = 0; i < entry.nLayerCount; i++ )
{
const uint16_t srcWidth = frameInfo->layers[ i ].tex->width();
const uint16_t srcHeight = frameInfo->layers[ i ].tex->height();
int32_t crtcX = -frameInfo->layers[ i ].offset.x;
int32_t crtcY = -frameInfo->layers[ i ].offset.y;
uint64_t crtcW = srcWidth / frameInfo->layers[ i ].scale.x;
uint64_t crtcH = srcHeight / frameInfo->layers[ i ].scale.y;
if (g_bRotated)
{
int64_t imageH = frameInfo->layers[ i ].tex->contentHeight() / frameInfo->layers[ i ].scale.y;
const int32_t x = crtcX;
const uint64_t w = crtcW;
crtcX = g_nOutputHeight - imageH - crtcY;
crtcY = x;
crtcW = crtcH;
crtcH = w;
}
entry.layerState[i].zpos = frameInfo->layers[ i ].zpos;
entry.layerState[i].srcW = srcWidth << 16;
entry.layerState[i].srcH = srcHeight << 16;
entry.layerState[i].crtcX = crtcX;
entry.layerState[i].crtcY = crtcY;
entry.layerState[i].crtcW = crtcW;
entry.layerState[i].crtcH = crtcH;
entry.layerState[i].ycbcr = frameInfo->layers[i].isYcbcr();
if ( entry.layerState[i].ycbcr )
{
entry.layerState[i].colorEncoding = drm_get_color_encoding( g_ForcedNV12ColorSpace );
entry.layerState[i].colorRange = drm_get_color_range( g_ForcedNV12ColorSpace );
entry.layerState[i].colorspace = GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB;
}
else
{
entry.layerState[i].colorspace = frameInfo->layers[ i ].colorspace;
}
}
return entry;
}
static bool is_liftoff_caching_enabled()
{
static bool disabled = env_to_bool(getenv("GAMESCOPE_LIFTOFF_CACHE_DISABLE"));
return !disabled;
}
bool g_bDisableShaperAnd3DLUT = false;
bool g_bDisableDegamma = false;
bool g_bDisableRegamma = false;
bool g_bDisableBlendTF = false;
bool g_bSinglePlaneOptimizations = true;
static int
drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, bool needs_modeset )
{
drm_screen_type screenType = drm_get_screen_type(drm);
auto entry = FrameInfoToLiftoffStateCacheEntry( drm, frameInfo );
// If we are modesetting, reset the state cache, we might
// move to another CRTC or whatever which might have differing caps.
// (same with different modes)
if (needs_modeset)
g_LiftoffStateCache.clear();
if (is_liftoff_caching_enabled())
{
if (g_LiftoffStateCache.count(entry) != 0)
return -EINVAL;
}
bool bSinglePlane = frameInfo->layerCount < 2 && g_bSinglePlaneOptimizations;
for ( int i = 0; i < k_nMaxLayers; i++ )
{
if ( i < frameInfo->layerCount )
{
if ( frameInfo->layers[ i ].fbid == 0 )
{
drm_verbose_log.errorf("drm_prepare_liftoff: layer %d has no FB", i );
return -EINVAL;
}
liftoff_layer_set_property( drm->lo_layers[ i ], "FB_ID", frameInfo->layers[ i ].fbid);
drm->fbids_in_req.push_back( frameInfo->layers[ i ].fbid );
liftoff_layer_set_property( drm->lo_layers[ i ], "zpos", entry.layerState[i].zpos );
liftoff_layer_set_property( drm->lo_layers[ i ], "alpha", frameInfo->layers[ i ].opacity * 0xffff);
liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_X", 0);
liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_Y", 0);
liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_W", entry.layerState[i].srcW );
liftoff_layer_set_property( drm->lo_layers[ i ], "SRC_H", entry.layerState[i].srcH );
liftoff_layer_set_property( drm->lo_layers[ i ], "rotation", g_drmEffectiveOrientation[screenType] );
liftoff_layer_set_property( drm->lo_layers[ i ], "CRTC_X", entry.layerState[i].crtcX);
liftoff_layer_set_property( drm->lo_layers[ i ], "CRTC_Y", entry.layerState[i].crtcY);
liftoff_layer_set_property( drm->lo_layers[ i ], "CRTC_W", entry.layerState[i].crtcW);
liftoff_layer_set_property( drm->lo_layers[ i ], "CRTC_H", entry.layerState[i].crtcH);
if ( entry.layerState[i].ycbcr )
{
liftoff_layer_set_property( drm->lo_layers[ i ], "COLOR_ENCODING", entry.layerState[i].colorEncoding );
liftoff_layer_set_property( drm->lo_layers[ i ], "COLOR_RANGE", entry.layerState[i].colorRange );
if ( drm_supports_color_mgmt( drm ) )
{
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_DEGAMMA_TF", DRM_VALVE1_TRANSFER_FUNCTION_BT709 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_LUT", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_LUT3D", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_BLEND_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
}
}
else if ( frameInfo->layers[i].applyColorMgmt )
{
liftoff_layer_unset_property( drm->lo_layers[ i ], "COLOR_ENCODING" );
liftoff_layer_unset_property( drm->lo_layers[ i ], "COLOR_RANGE" );
if ( drm_supports_color_mgmt( drm ) )
{
drm_valve1_transfer_function degamma_tf = colorspace_to_plane_degamma_tf( entry.layerState[i].colorspace );
drm_valve1_transfer_function shaper_tf = colorspace_to_plane_shaper_tf( entry.layerState[i].colorspace );
if (!g_bDisableDegamma)
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_DEGAMMA_TF", degamma_tf );
else
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_DEGAMMA_TF", 0 );
if ( !g_bDisableShaperAnd3DLUT )
{
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_LUT", drm->pending.shaperlut_id[ ColorSpaceToEOTFIndex( entry.layerState[i].colorspace ) ] );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_TF", shaper_tf );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_LUT3D", drm->pending.lut3d_id[ ColorSpaceToEOTFIndex( entry.layerState[i].colorspace ) ] );
// Josh: See shaders/colorimetry.h colorspace_blend_tf if you have questions as to why we start doing sRGB for BLEND_TF despite potentially working in Gamma 2.2 space prior.
}
else
{
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_LUT", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_TF", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_LUT3D", 0 );
}
}
}
else
{
if ( drm_supports_color_mgmt( drm ) )
{
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_DEGAMMA_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_LUT", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_TF", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_LUT3D", 0 );
}
}
if ( drm_supports_color_mgmt( drm ) )
{
if (!g_bDisableBlendTF && !bSinglePlane)
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_BLEND_TF", drm->pending.output_tf );
else
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_BLEND_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
if (frameInfo->layers[i].ctm != nullptr)
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_CTM", frameInfo->layers[i].ctm->blob );
else
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_CTM", 0 );
}
}
else
{
liftoff_layer_set_property( drm->lo_layers[ i ], "FB_ID", 0 );
liftoff_layer_unset_property( drm->lo_layers[ i ], "COLOR_ENCODING" );
liftoff_layer_unset_property( drm->lo_layers[ i ], "COLOR_RANGE" );
if ( drm_supports_color_mgmt( drm ) )
{
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_DEGAMMA_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_LUT", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_SHAPER_TF", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_LUT3D", 0 );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_BLEND_TF", DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT );
liftoff_layer_set_property( drm->lo_layers[ i ], "VALVE1_PLANE_CTM", 0 );
}
}
}
int ret = liftoff_output_apply( drm->lo_output, drm->req, drm->flags );
if ( ret == 0 )
{
// We don't support partial composition yet
if ( liftoff_output_needs_composition( drm->lo_output ) )
ret = -EINVAL;
}
// If we aren't modesetting and we got -EINVAL, that means that we
// probably can't do this layout, so add it to our state cache so we don't
// try it again.
if (!needs_modeset)
{
if (ret == -EINVAL)
g_LiftoffStateCache.insert(entry);
}
if ( ret == 0 )
drm_verbose_log.debugf( "can drm present %i layers", frameInfo->layerCount );
else
drm_verbose_log.debugf( "can NOT drm present %i layers", frameInfo->layerCount );
return ret;
}
bool g_bForceAsyncFlips = false;
/* Prepares an atomic commit for the provided scene-graph. Returns 0 on success,
* negative errno on failure or if the scene-graph can't be presented directly. */
int drm_prepare( struct drm_t *drm, bool async, const struct FrameInfo_t *frameInfo )
{
drm->pending.screen_type = drm_get_screen_type(drm);
drm_update_vrr_state(drm);
drm_update_color_mgmt(drm);
drm->fbids_in_req.clear();
bool needs_modeset = drm->needs_modeset.exchange(false);
assert( drm->req == nullptr );
drm->req = drmModeAtomicAlloc();
bool bConnectorSupportsHDR = drm->connector->metadata.supportsST2084;
bool bConnectorHDR = g_bOutputHDREnabled && bConnectorSupportsHDR;
if (drm->connector != nullptr) {
if (drm->connector->has_colorspace) {
drm->connector->pending.colorspace = ( bConnectorHDR ) ? DRM_MODE_COLORIMETRY_BT2020_RGB : DRM_MODE_COLORIMETRY_DEFAULT;
}
if (drm->connector->has_content_type) {
drm->connector->pending.content_type = DRM_MODE_CONTENT_TYPE_GAME;
}
if ( bConnectorHDR )
{
if (drm->connector->has_hdr_output_metadata) {
auto hdr_output_metadata = get_default_hdr_metadata( drm, drm->connector );
if ( drm->connector->metadata.hdr10_metadata_blob )
hdr_output_metadata = drm->connector->metadata.hdr10_metadata_blob;
auto feedback = steamcompmgr_get_base_layer_swapchain_feedback();
if (feedback && feedback->hdr_metadata_blob)
hdr_output_metadata = feedback->hdr_metadata_blob;
drm->connector->pending.hdr_output_metadata = hdr_output_metadata;
}
}
else
{
if (drm->connector->has_hdr_output_metadata && bConnectorSupportsHDR)
{
drm->connector->pending.hdr_output_metadata = drm->sdr_static_metadata;
}
else
{
drm->connector->pending.hdr_output_metadata = nullptr;
}
}
}
bool bSinglePlane = frameInfo->layerCount < 2 && g_bSinglePlaneOptimizations;
if ( drm_supports_color_mgmt( &g_DRM ) && frameInfo->applyOutputColorMgmt )
{
if ( !g_bDisableRegamma && !bSinglePlane )
{
drm->pending.output_tf = g_bOutputHDREnabled
? DRM_VALVE1_TRANSFER_FUNCTION_PQ
: DRM_VALVE1_TRANSFER_FUNCTION_SRGB;
}
else
{
drm->pending.output_tf = DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT;
}
}
else
{
drm->pending.output_tf = DRM_VALVE1_TRANSFER_FUNCTION_DEFAULT;
}
uint32_t flags = DRM_MODE_ATOMIC_NONBLOCK;
// We do internal refcounting with these events
if ( drm->crtc != nullptr )
flags |= DRM_MODE_PAGE_FLIP_EVENT;
if ( async || g_bForceAsyncFlips )
flags |= DRM_MODE_PAGE_FLIP_ASYNC;
if ( needs_modeset ) {
flags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
// Disable all connectors and CRTCs
for ( auto &kv : drm->connectors ) {
struct connector *conn = &kv.second;
if ( conn->current.crtc_id == 0 )
continue;
conn->pending.crtc_id = 0;
int ret = add_connector_property( drm->req, conn, "CRTC_ID", 0 );
if (ret < 0)
return ret;
if (conn->has_colorspace) {
ret = add_connector_property( drm->req, conn, "Colorspace", 0 );
if (ret < 0)
return ret;
}
if (conn->has_hdr_output_metadata) {
ret = add_connector_property( drm->req, conn, "HDR_OUTPUT_METADATA", 0 );
if (ret < 0)
return ret;
}
if (conn->has_content_type) {
ret = add_connector_property( drm->req, conn, "content type", 0 );
if (ret < 0)
return ret;
}
}
for ( size_t i = 0; i < drm->crtcs.size(); i++ ) {
struct crtc *crtc = &drm->crtcs[i];
// We can't disable a CRTC if it's already disabled, or else the
// kernel will error out with "requesting event but off".
if (crtc->current.active == 0)
continue;
int ret = add_crtc_property(drm->req, crtc, "MODE_ID", 0);
if (ret < 0)
return ret;
if (crtc->has_gamma_lut)
{
int ret = add_crtc_property(drm->req, crtc, "GAMMA_LUT", 0);
if (ret < 0)
return ret;
}
if (crtc->has_degamma_lut)
{
int ret = add_crtc_property(drm->req, crtc, "DEGAMMA_LUT", 0);
if (ret < 0)
return ret;
}
if (crtc->has_ctm)
{
int ret = add_crtc_property(drm->req, crtc, "CTM", 0);
if (ret < 0)
return ret;
}
if (crtc->has_vrr_enabled)
{
int ret = add_crtc_property(drm->req, crtc, "VRR_ENABLED", 0);
if (ret < 0)
return ret;
}
if (crtc->has_valve1_regamma_tf)
{
int ret = add_crtc_property(drm->req, crtc, "VALVE1_CRTC_REGAMMA_TF", 0);
if (ret < 0)
return ret;
}
ret = add_crtc_property(drm->req, crtc, "ACTIVE", 0);
if (ret < 0)
return ret;
crtc->pending.active = 0;
}
// Then enable the one we've picked
int ret = 0;
if (drm->connector != nullptr) {
// Always set our CRTC_ID for the modeset, especially
// as we zero-ed it above.
drm->connector->pending.crtc_id = drm->crtc->id;
ret = add_connector_property(drm->req, drm->connector, "CRTC_ID", drm->crtc->id);
if (ret < 0)
return ret;
if (drm->connector->has_colorspace) {
ret = add_connector_property(drm->req, drm->connector, "Colorspace", drm->connector->pending.colorspace);
if (ret < 0)
return ret;
}
if (drm->connector->has_hdr_output_metadata) {
uint32_t value = drm->connector->pending.hdr_output_metadata ? drm->connector->pending.hdr_output_metadata->blob : 0;
ret = add_connector_property(drm->req, drm->connector, "HDR_OUTPUT_METADATA", value);
if (ret < 0)
return ret;
}
if (drm->connector->has_content_type) {
ret = add_connector_property(drm->req, drm->connector, "content type", drm->connector->pending.content_type);
if (ret < 0)
return ret;
}
ret = add_crtc_property(drm->req, drm->crtc, "MODE_ID", drm->pending.mode_id);
if (ret < 0)
return ret;
if (drm->crtc->has_vrr_enabled)
{
ret = add_crtc_property(drm->req, drm->crtc, "VRR_ENABLED", drm->pending.vrr_enabled);
if (ret < 0)
return ret;
}
if (drm->crtc->has_valve1_regamma_tf)
{
ret = add_crtc_property(drm->req, drm->crtc, "VALVE1_CRTC_REGAMMA_TF", drm->pending.output_tf);
if (ret < 0)
return ret;
}
ret = add_crtc_property(drm->req, drm->crtc, "ACTIVE", 1);
if (ret < 0)
return ret;
drm->crtc->pending.active = 1;
}
}
else
{
if (drm->connector != nullptr) {
if (drm->connector->has_colorspace && drm->connector->pending.colorspace != drm->connector->current.colorspace) {
int ret = add_connector_property(drm->req, drm->connector, "Colorspace", drm->connector->pending.colorspace);
if (ret < 0)
return ret;
}
if (drm->connector->has_hdr_output_metadata && drm->connector->pending.hdr_output_metadata != drm->connector->current.hdr_output_metadata) {
uint32_t value = drm->connector->pending.hdr_output_metadata ? drm->connector->pending.hdr_output_metadata->blob : 0;
int ret = add_connector_property(drm->req, drm->connector, "HDR_OUTPUT_METADATA", value);
if (ret < 0)
return ret;
}
if (drm->connector->has_content_type && drm->connector->pending.content_type != drm->connector->current.content_type) {
int ret = add_connector_property(drm->req, drm->connector, "content type", drm->connector->pending.content_type);
if (ret < 0)
return ret;
}
}
if (drm->crtc != nullptr) {
if ( drm->crtc->has_vrr_enabled && drm->pending.vrr_enabled != drm->current.vrr_enabled )
{
int ret = add_crtc_property(drm->req, drm->crtc, "VRR_ENABLED", drm->pending.vrr_enabled );
if (ret < 0)
return ret;
}
if ( drm->crtc->has_valve1_regamma_tf && drm->pending.output_tf != drm->current.output_tf )
{
int ret = add_crtc_property(drm->req, drm->crtc, "VALVE1_CRTC_REGAMMA_TF", drm->pending.output_tf );
if (ret < 0)
return ret;
}
}
}
drm->flags = flags;
int ret;
if ( drm->crtc == nullptr ) {
ret = 0;
} else if ( g_bUseLayers == true ) {
ret = drm_prepare_liftoff( drm, frameInfo, needs_modeset );
} else {
ret = drm_prepare_basic( drm, frameInfo );
}
if ( ret != 0 ) {
drmModeAtomicFree( drm->req );
drm->req = nullptr;
drm->fbids_in_req.clear();
if ( needs_modeset )
drm->needs_modeset = true;
}
return ret;
}
void drm_rollback( struct drm_t *drm )
{
drm->pending = drm->current;
for ( size_t i = 0; i < drm->crtcs.size(); i++ )
{
drm->crtcs[i].pending = drm->crtcs[i].current;
}
for (auto &kv : drm->connectors) {
struct connector *conn = &kv.second;
conn->pending = conn->current;
}
}
bool drm_poll_state( struct drm_t *drm )
{
int out_of_date = drm->out_of_date.exchange(false);
if ( !out_of_date )
return false;
refresh_state( drm );
setup_best_connector(drm, out_of_date >= 2, false);
return true;
}
static bool drm_set_crtc( struct drm_t *drm, struct crtc *crtc )
{
drm->crtc = crtc;
drm->needs_modeset = true;
for (size_t i = 0; i < drm->crtcs.size(); i++) {
if (drm->crtcs[i].id == drm->crtc->id) {
drm->crtc_index = i;
break;
}
}
drm->primary = find_primary_plane( drm );
if ( drm->primary == nullptr ) {
drm_log.errorf("could not find a suitable primary plane");
return false;
}
struct liftoff_output *lo_output = liftoff_output_create( drm->lo_device, crtc->id );
if ( lo_output == nullptr )
return false;
for ( int i = 0; i < k_nMaxLayers; i++ )
{
liftoff_layer_destroy( drm->lo_layers[ i ] );
drm->lo_layers[ i ] = liftoff_layer_create( lo_output );
if ( drm->lo_layers[ i ] == nullptr )
return false;
}
liftoff_output_destroy( drm->lo_output );
drm->lo_output = lo_output;
return true;
}
bool drm_set_connector( struct drm_t *drm, struct connector *conn )
{
drm_log.infof("selecting connector %s", conn->name);
struct crtc *crtc = find_crtc_for_connector(drm, conn);
if (crtc == nullptr) {
drm_log.errorf("no CRTC found!");
return false;
}
if (!drm_set_crtc(drm, crtc)) {
return false;
}
drm->connector = conn;
drm->needs_modeset = true;
return true;
}
static void drm_unset_connector( struct drm_t *drm )
{
drm->crtc = nullptr;
drm->primary = nullptr;
for ( int i = 0; i < k_nMaxLayers; i++ )
{
liftoff_layer_destroy( drm->lo_layers[ i ] );
drm->lo_layers[ i ] = nullptr;
}
liftoff_output_destroy(drm->lo_output);
drm->lo_output = nullptr;
drm->connector = nullptr;
drm->needs_modeset = true;
}
void drm_set_vrr_enabled(struct drm_t *drm, bool enabled)
{
drm->wants_vrr_enabled = enabled;
}
bool drm_get_vrr_in_use(struct drm_t *drm)
{
return drm->current.vrr_enabled;
}
drm_screen_type drm_get_connector_type(drmModeConnector *connector)
{
if (connector->connector_type == DRM_MODE_CONNECTOR_eDP ||
connector->connector_type == DRM_MODE_CONNECTOR_LVDS ||
connector->connector_type == DRM_MODE_CONNECTOR_DSI)
return DRM_SCREEN_TYPE_INTERNAL;
return DRM_SCREEN_TYPE_EXTERNAL;
}
drm_screen_type drm_get_screen_type(struct drm_t *drm)
{
if (!drm->connector || !drm->connector->connector)
return DRM_SCREEN_TYPE_INTERNAL;
return drm_get_connector_type(drm->connector->connector);
}
bool drm_update_color_mgmt(struct drm_t *drm)
{
if ( !drm_supports_color_mgmt( drm ) )
return true;
if ( g_ColorMgmt.serial == drm->current.color_mgmt_serial )
return true;
drm->pending.color_mgmt_serial = g_ColorMgmt.serial;
for ( uint32_t i = 0; i < EOTF_Count; i++ )
{
drm->pending.shaperlut_id[ i ] = 0;
drm->pending.lut3d_id[ i ] = 0;
}
for ( uint32_t i = 0; i < EOTF_Count; i++ )
{
if ( !g_ColorMgmtLuts[i].HasLuts() )
continue;
uint32_t shaper_blob_id = 0;
if (drmModeCreatePropertyBlob(drm->fd, g_ColorMgmtLuts[i].lut1d, sizeof(g_ColorMgmtLuts[i].lut1d), &shaper_blob_id) != 0) {
drm_log.errorf_errno("Unable to create SHAPERLUT property blob");
return false;
}
drm->pending.shaperlut_id[ i ] = shaper_blob_id;
uint32_t lut3d_blob_id = 0;
if (drmModeCreatePropertyBlob(drm->fd, g_ColorMgmtLuts[i].lut3d, sizeof(g_ColorMgmtLuts[i].lut3d), &lut3d_blob_id) != 0) {
drm_log.errorf_errno("Unable to create LUT3D property blob");
return false;
}
drm->pending.lut3d_id[ i ] = lut3d_blob_id;
}
return true;
}
bool drm_update_vrr_state(struct drm_t *drm)
{
drm->pending.vrr_enabled = false;
if ( drm->connector && drm->crtc && drm->crtc->has_vrr_enabled )
{
if ( drm->wants_vrr_enabled && drm->connector->vrr_capable )
drm->pending.vrr_enabled = true;
}
if (drm->pending.vrr_enabled != drm->current.vrr_enabled)
drm->needs_modeset = true;
return true;
}
static void drm_unset_mode( struct drm_t *drm )
{
drm->pending.mode_id = 0;
drm->needs_modeset = true;
g_nOutputWidth = drm->preferred_width;
g_nOutputHeight = drm->preferred_height;
if (g_nOutputHeight == 0)
g_nOutputHeight = 720;
if (g_nOutputWidth == 0)
g_nOutputWidth = g_nOutputHeight * 16 / 9;
g_nOutputRefresh = drm->preferred_refresh;
if (g_nOutputRefresh == 0)
g_nOutputRefresh = 60;
g_drmEffectiveOrientation[DRM_SCREEN_TYPE_INTERNAL] = DRM_MODE_ROTATE_0;
g_drmEffectiveOrientation[DRM_SCREEN_TYPE_EXTERNAL] = DRM_MODE_ROTATE_0;
g_bRotated = false;
}
bool drm_set_mode( struct drm_t *drm, const drmModeModeInfo *mode )
{
if (!drm->connector || !drm->connector->connector)
return false;
uint32_t mode_id = 0;
if (drmModeCreatePropertyBlob(drm->fd, mode, sizeof(*mode), &mode_id) != 0)
return false;
drm_screen_type screenType = drm_get_screen_type(drm);
drm_log.infof("selecting mode %dx%d@%uHz", mode->hdisplay, mode->vdisplay, mode->vrefresh);
drm->pending.mode_id = mode_id;
drm->needs_modeset = true;
g_nOutputRefresh = mode->vrefresh;
update_drm_effective_orientations(drm, drm->connector, mode);
switch ( g_drmEffectiveOrientation[screenType] )
{
case DRM_MODE_ROTATE_0:
case DRM_MODE_ROTATE_180:
g_bRotated = false;
g_nOutputWidth = mode->hdisplay;
g_nOutputHeight = mode->vdisplay;
break;
case DRM_MODE_ROTATE_90:
case DRM_MODE_ROTATE_270:
g_bRotated = true;
g_nOutputWidth = mode->vdisplay;
g_nOutputHeight = mode->hdisplay;
break;
}
return true;
}
bool drm_set_refresh( struct drm_t *drm, int refresh )
{
int width = g_nOutputWidth;
int height = g_nOutputHeight;
if ( g_bRotated ) {
int tmp = width;
width = height;
height = tmp;
}
if (!drm->connector || !drm->connector->connector)
return false;
drmModeConnector *connector = drm->connector->connector;
const drmModeModeInfo *existing_mode = find_mode(connector, width, height, refresh);
drmModeModeInfo mode = {0};
if ( existing_mode )
{
mode = *existing_mode;
}
else
{
/* TODO: check refresh is within the EDID limits */
switch ( g_drmModeGeneration )
{
case DRM_MODE_GENERATE_CVT:
generate_cvt_mode( &mode, width, height, refresh, true, false );
break;
case DRM_MODE_GENERATE_FIXED:
{
const drmModeModeInfo *preferred_mode = find_mode(connector, 0, 0, 0);
generate_fixed_mode( &mode, preferred_mode, refresh, drm->connector->is_steam_deck_display, drm->connector->is_galileo_display );
break;
}
}
}
mode.type = DRM_MODE_TYPE_USERDEF;
return drm_set_mode(drm, &mode);
}
bool drm_set_resolution( struct drm_t *drm, int width, int height )
{
if (!drm->connector || !drm->connector->connector)
return false;
drmModeConnector *connector = drm->connector->connector;
const drmModeModeInfo *mode = find_mode(connector, width, height, 0);
if ( !mode )
{
return false;
}
return drm_set_mode(drm, mode);
}
int drm_get_default_refresh(struct drm_t *drm)
{
if ( drm->preferred_refresh )
return drm->preferred_refresh;
if ( drm->connector && drm->connector->target_refresh )
return drm->connector->target_refresh;
if ( drm->connector && drm->connector->connector )
{
drmModeConnector *connector = drm->connector->connector;
const drmModeModeInfo *mode = find_mode( connector, g_nOutputWidth, g_nOutputHeight, 0);
if ( mode )
return mode->vrefresh;
}
return 60;
}
bool drm_get_vrr_capable(struct drm_t *drm)
{
if ( drm->connector )
return drm->connector->vrr_capable;
return false;
}
bool drm_supports_st2084(struct drm_t *drm)
{
if ( drm->connector )
return drm->connector->metadata.supportsST2084;
return false;
}
void drm_set_hdr_state(struct drm_t *drm, bool enabled) {
if (drm->enable_hdr != enabled) {
drm->needs_modeset = true;
drm->enable_hdr = enabled;
}
}
const char *drm_get_connector_name(struct drm_t *drm)
{
if ( !drm->connector )
return nullptr;
return drm->connector->name;
}
const char *drm_get_device_name(struct drm_t *drm)
{
return drm->device_name;
}
std::pair<uint32_t, uint32_t> drm_get_connector_identifier(struct drm_t *drm)
{
if ( !drm->connector )
return { 0u, 0u };
return std::make_pair(drm->connector->connector->connector_type, drm->connector->connector->connector_type_id);
}
std::shared_ptr<wlserver_hdr_metadata> drm_create_hdr_metadata_blob(struct drm_t *drm, hdr_output_metadata *metadata)
{
uint32_t blob = 0;
if (!BIsNested())
{
int ret = drmModeCreatePropertyBlob(drm->fd, metadata, sizeof(*metadata), &blob);
if (ret != 0) {
drm_log.errorf("Failed to create blob for HDR_OUTPUT_METADATA. (%s) Falling back to null blob.", strerror(-ret));
blob = 0;
}
if (!blob)
return nullptr;
}
return std::make_shared<wlserver_hdr_metadata>(metadata, blob);
}
void drm_destroy_blob(struct drm_t *drm, uint32_t blob)
{
drmModeDestroyPropertyBlob(drm->fd, blob);
}
std::shared_ptr<wlserver_ctm> drm_create_ctm(struct drm_t *drm, glm::mat3x4 ctm)
{
uint32_t blob = 0;
if (!BIsNested())
{
drm_color_ctm2 ctm2;
for (uint32_t i = 0; i < 12; i++)
{
float *data = (float*)&ctm;
ctm2.matrix[i] = drm_calc_s31_32(data[i]);
}
int ret = drmModeCreatePropertyBlob(drm->fd, &ctm2, sizeof(ctm2), &blob);
if (ret != 0) {
drm_log.errorf("Failed to create blob for CTM. (%s) Falling back to null blob.", strerror(-ret));
blob = 0;
}
if (!blob)
return nullptr;
}
return std::make_shared<wlserver_ctm>(ctm, blob);
}
bool drm_supports_color_mgmt(struct drm_t *drm)
{
if (g_bForceDisableColorMgmt)
return false;
if (!drm->primary)
return false;
return drm->primary->has_color_mgmt;
}
void drm_get_native_colorimetry( struct drm_t *drm,
displaycolorimetry_t *displayColorimetry, EOTF *displayEOTF,
displaycolorimetry_t *outputEncodingColorimetry, EOTF *outputEncodingEOTF )
{
if ( !drm || !drm->connector )
{
*displayColorimetry = displaycolorimetry_709;
*displayEOTF = EOTF_Gamma22;
*outputEncodingColorimetry = displaycolorimetry_709;
*outputEncodingEOTF = EOTF_Gamma22;
}
*displayColorimetry = drm->connector->metadata.colorimetry;
*displayEOTF = drm->connector->metadata.eotf;
// For HDR output, expected content colorspace != native colorspace.
if (drm->connector->metadata.supportsST2084 && g_bOutputHDREnabled)
{
*outputEncodingColorimetry = displaycolorimetry_2020;
*outputEncodingEOTF = EOTF_PQ;
}
else
{
*outputEncodingColorimetry = drm->connector->metadata.colorimetry;
*outputEncodingEOTF = drm->connector->metadata.eotf;
}
if (!g_bOutputHDREnabled)
{
*displayEOTF = EOTF_Gamma22;
*outputEncodingEOTF = EOTF_Gamma22;
}
}
std::span<uint32_t> drm_get_valid_refresh_rates( struct drm_t *drm )
{
if (drm && drm->connector)
return drm->connector->valid_display_rates;
return std::span<uint32_t>{};
}