linux/drivers/gpu/drm/display/drm_hdmi_state_helper.c

// SPDX-License-Identifier: MIT

#include <drm/drm_atomic.h>
#include <drm/drm_connector.h>
#include <drm/drm_edid.h>
#include <drm/drm_print.h>

#include <drm/display/drm_hdmi_helper.h>
#include <drm/display/drm_hdmi_state_helper.h>

/**
 * __drm_atomic_helper_connector_hdmi_reset() - Initializes all HDMI @drm_connector_state resources
 * @connector: DRM connector
 * @new_conn_state: connector state to reset
 *
 * Initializes all HDMI resources from a @drm_connector_state without
 * actually allocating it. This is useful for HDMI drivers, in
 * combination with __drm_atomic_helper_connector_reset() or
 * drm_atomic_helper_connector_reset().
 */
void __drm_atomic_helper_connector_hdmi_reset(struct drm_connector *connector,
					      struct drm_connector_state *new_conn_state)
{
	unsigned int max_bpc = connector->max_bpc;

	new_conn_state->max_bpc = max_bpc;
	new_conn_state->max_requested_bpc = max_bpc;
	new_conn_state->hdmi.broadcast_rgb = DRM_HDMI_BROADCAST_RGB_AUTO;
}
EXPORT_SYMBOL(__drm_atomic_helper_connector_hdmi_reset);

static const struct drm_display_mode *
connector_state_get_mode(const struct drm_connector_state *conn_state)
{
	struct drm_atomic_state *state;
	struct drm_crtc_state *crtc_state;
	struct drm_crtc *crtc;

	state = conn_state->state;
	if (!state)
		return NULL;

	crtc = conn_state->crtc;
	if (!crtc)
		return NULL;

	crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
	if (!crtc_state)
		return NULL;

	return &crtc_state->mode;
}

static bool hdmi_is_limited_range(const struct drm_connector *connector,
				  const struct drm_connector_state *conn_state)
{
	const struct drm_display_info *info = &connector->display_info;
	const struct drm_display_mode *mode =
		connector_state_get_mode(conn_state);

	/*
	 * The Broadcast RGB property only applies to RGB format, and
	 * i915 just assumes limited range for YCbCr output, so let's
	 * just do the same.
	 */
	if (conn_state->hdmi.output_format != HDMI_COLORSPACE_RGB)
		return true;

	if (conn_state->hdmi.broadcast_rgb == DRM_HDMI_BROADCAST_RGB_FULL)
		return false;

	if (conn_state->hdmi.broadcast_rgb == DRM_HDMI_BROADCAST_RGB_LIMITED)
		return true;

	if (!info->is_hdmi)
		return false;

	return drm_default_rgb_quant_range(mode) == HDMI_QUANTIZATION_RANGE_LIMITED;
}

static bool
sink_supports_format_bpc(const struct drm_connector *connector,
			 const struct drm_display_info *info,
			 const struct drm_display_mode *mode,
			 unsigned int format, unsigned int bpc)
{
	struct drm_device *dev = connector->dev;
	u8 vic = drm_match_cea_mode(mode);

	/*
	 * CTA-861-F, section 5.4 - Color Coding & Quantization states
	 * that the bpc must be 8, 10, 12 or 16 except for the default
	 * 640x480 VIC1 where the value must be 8.
	 *
	 * The definition of default here is ambiguous but the spec
	 * refers to VIC1 being the default timing in several occasions
	 * so our understanding is that for the default timing (ie,
	 * VIC1), the bpc must be 8.
	 */
	if (vic == 1 && bpc != 8) {
		drm_dbg_kms(dev, "VIC1 requires a bpc of 8, got %u\n", bpc);
		return false;
	}

	if (!info->is_hdmi &&
	    (format != HDMI_COLORSPACE_RGB || bpc != 8)) {
		drm_dbg_kms(dev, "DVI Monitors require an RGB output at 8 bpc\n");
		return false;
	}

	if (!(connector->hdmi.supported_formats & BIT(format))) {
		drm_dbg_kms(dev, "%s format unsupported by the connector.\n",
			    drm_hdmi_connector_get_output_format_name(format));
		return false;
	}

	switch (format) {
	case HDMI_COLORSPACE_RGB:
		drm_dbg_kms(dev, "RGB Format, checking the constraints.\n");

		/*
		 * In some cases, like when the EDID readout fails, or
		 * is not an HDMI compliant EDID for some reason, the
		 * color_formats field will be blank and not report any
		 * format supported. In such a case, assume that RGB is
		 * supported so we can keep things going and light up
		 * the display.
		 */
		if (!(info->color_formats & DRM_COLOR_FORMAT_RGB444))
			drm_warn(dev, "HDMI Sink doesn't support RGB, something's wrong.\n");

		if (bpc == 10 && !(info->edid_hdmi_rgb444_dc_modes & DRM_EDID_HDMI_DC_30)) {
			drm_dbg_kms(dev, "10 BPC but sink doesn't support Deep Color 30.\n");
			return false;
		}

		if (bpc == 12 && !(info->edid_hdmi_rgb444_dc_modes & DRM_EDID_HDMI_DC_36)) {
			drm_dbg_kms(dev, "12 BPC but sink doesn't support Deep Color 36.\n");
			return false;
		}

		drm_dbg_kms(dev, "RGB format supported in that configuration.\n");

		return true;

	case HDMI_COLORSPACE_YUV420:
		/* TODO: YUV420 is unsupported at the moment. */
		drm_dbg_kms(dev, "YUV420 format isn't supported yet.\n");
		return false;

	case HDMI_COLORSPACE_YUV422:
		drm_dbg_kms(dev, "YUV422 format, checking the constraints.\n");

		if (!(info->color_formats & DRM_COLOR_FORMAT_YCBCR422)) {
			drm_dbg_kms(dev, "Sink doesn't support YUV422.\n");
			return false;
		}

		if (bpc > 12) {
			drm_dbg_kms(dev, "YUV422 only supports 12 bpc or lower.\n");
			return false;
		}

		/*
		 * HDMI Spec 1.3 - Section 6.5 Pixel Encodings and Color Depth
		 * states that Deep Color is not relevant for YUV422 so we
		 * don't need to check the Deep Color bits in the EDIDs here.
		 */

		drm_dbg_kms(dev, "YUV422 format supported in that configuration.\n");

		return true;

	case HDMI_COLORSPACE_YUV444:
		drm_dbg_kms(dev, "YUV444 format, checking the constraints.\n");

		if (!(info->color_formats & DRM_COLOR_FORMAT_YCBCR444)) {
			drm_dbg_kms(dev, "Sink doesn't support YUV444.\n");
			return false;
		}

		if (bpc == 10 && !(info->edid_hdmi_ycbcr444_dc_modes & DRM_EDID_HDMI_DC_30)) {
			drm_dbg_kms(dev, "10 BPC but sink doesn't support Deep Color 30.\n");
			return false;
		}

		if (bpc == 12 && !(info->edid_hdmi_ycbcr444_dc_modes & DRM_EDID_HDMI_DC_36)) {
			drm_dbg_kms(dev, "12 BPC but sink doesn't support Deep Color 36.\n");
			return false;
		}

		drm_dbg_kms(dev, "YUV444 format supported in that configuration.\n");

		return true;
	}

	drm_dbg_kms(dev, "Unsupported pixel format.\n");
	return false;
}

static enum drm_mode_status
hdmi_clock_valid(const struct drm_connector *connector,
		 const struct drm_display_mode *mode,
		 unsigned long long clock)
{
	const struct drm_connector_hdmi_funcs *funcs = connector->hdmi.funcs;
	const struct drm_display_info *info = &connector->display_info;

	if (info->max_tmds_clock && clock > info->max_tmds_clock * 1000)
		return MODE_CLOCK_HIGH;

	if (funcs && funcs->tmds_char_rate_valid) {
		enum drm_mode_status status;

		status = funcs->tmds_char_rate_valid(connector, mode, clock);
		if (status != MODE_OK)
			return status;
	}

	return MODE_OK;
}

static int
hdmi_compute_clock(const struct drm_connector *connector,
		   struct drm_connector_state *conn_state,
		   const struct drm_display_mode *mode,
		   unsigned int bpc, enum hdmi_colorspace fmt)
{
	enum drm_mode_status status;
	unsigned long long clock;

	clock = drm_hdmi_compute_mode_clock(mode, bpc, fmt);
	if (!clock)
		return -EINVAL;

	status = hdmi_clock_valid(connector, mode, clock);
	if (status != MODE_OK)
		return -EINVAL;

	conn_state->hdmi.tmds_char_rate = clock;

	return 0;
}

static bool
hdmi_try_format_bpc(const struct drm_connector *connector,
		    struct drm_connector_state *conn_state,
		    const struct drm_display_mode *mode,
		    unsigned int bpc, enum hdmi_colorspace fmt)
{
	const struct drm_display_info *info = &connector->display_info;
	struct drm_device *dev = connector->dev;
	int ret;

	drm_dbg_kms(dev, "Trying %s output format\n",
		    drm_hdmi_connector_get_output_format_name(fmt));

	if (!sink_supports_format_bpc(connector, info, mode, fmt, bpc)) {
		drm_dbg_kms(dev, "%s output format not supported with %u bpc\n",
			    drm_hdmi_connector_get_output_format_name(fmt),
			    bpc);
		return false;
	}

	ret = hdmi_compute_clock(connector, conn_state, mode, bpc, fmt);
	if (ret) {
		drm_dbg_kms(dev, "Couldn't compute clock for %s output format and %u bpc\n",
			    drm_hdmi_connector_get_output_format_name(fmt),
			    bpc);
		return false;
	}

	drm_dbg_kms(dev, "%s output format supported with %u (TMDS char rate: %llu Hz)\n",
		    drm_hdmi_connector_get_output_format_name(fmt),
		    bpc, conn_state->hdmi.tmds_char_rate);

	return true;
}

static int
hdmi_compute_format(const struct drm_connector *connector,
		    struct drm_connector_state *conn_state,
		    const struct drm_display_mode *mode,
		    unsigned int bpc)
{
	struct drm_device *dev = connector->dev;

	/*
	 * TODO: Add support for YCbCr420 output for HDMI 2.0 capable
	 * devices, for modes that only support YCbCr420.
	 */
	if (hdmi_try_format_bpc(connector, conn_state, mode, bpc, HDMI_COLORSPACE_RGB)) {
		conn_state->hdmi.output_format = HDMI_COLORSPACE_RGB;
		return 0;
	}

	drm_dbg_kms(dev, "Failed. No Format Supported for that bpc count.\n");

	return -EINVAL;
}

static int
hdmi_compute_config(const struct drm_connector *connector,
		    struct drm_connector_state *conn_state,
		    const struct drm_display_mode *mode)
{
	struct drm_device *dev = connector->dev;
	unsigned int max_bpc = clamp_t(unsigned int,
				       conn_state->max_bpc,
				       8, connector->max_bpc);
	unsigned int bpc;
	int ret;

	for (bpc = max_bpc; bpc >= 8; bpc -= 2) {
		drm_dbg_kms(dev, "Trying with a %d bpc output\n", bpc);

		ret = hdmi_compute_format(connector, conn_state, mode, bpc);
		if (ret)
			continue;

		conn_state->hdmi.output_bpc = bpc;

		drm_dbg_kms(dev,
			    "Mode %ux%u @ %uHz: Found configuration: bpc: %u, fmt: %s, clock: %llu\n",
			    mode->hdisplay, mode->vdisplay, drm_mode_vrefresh(mode),
			    conn_state->hdmi.output_bpc,
			    drm_hdmi_connector_get_output_format_name(conn_state->hdmi.output_format),
			    conn_state->hdmi.tmds_char_rate);

		return 0;
	}

	return -EINVAL;
}

static int hdmi_generate_avi_infoframe(const struct drm_connector *connector,
				       struct drm_connector_state *conn_state)
{
	const struct drm_display_mode *mode =
		connector_state_get_mode(conn_state);
	struct drm_connector_hdmi_infoframe *infoframe =
		&conn_state->hdmi.infoframes.avi;
	struct hdmi_avi_infoframe *frame =
		&infoframe->data.avi;
	bool is_limited_range = conn_state->hdmi.is_limited_range;
	enum hdmi_quantization_range rgb_quant_range =
		is_limited_range ? HDMI_QUANTIZATION_RANGE_LIMITED : HDMI_QUANTIZATION_RANGE_FULL;
	int ret;

	ret = drm_hdmi_avi_infoframe_from_display_mode(frame, connector, mode);
	if (ret)
		return ret;

	frame->colorspace = conn_state->hdmi.output_format;

	/*
	 * FIXME: drm_hdmi_avi_infoframe_quant_range() doesn't handle
	 * YUV formats at all at the moment, so if we ever support YUV
	 * formats this needs to be revised.
	 */
	drm_hdmi_avi_infoframe_quant_range(frame, connector, mode, rgb_quant_range);
	drm_hdmi_avi_infoframe_colorimetry(frame, conn_state);
	drm_hdmi_avi_infoframe_bars(frame, conn_state);

	infoframe->set = true;

	return 0;
}

static int hdmi_generate_spd_infoframe(const struct drm_connector *connector,
				       struct drm_connector_state *conn_state)
{
	struct drm_connector_hdmi_infoframe *infoframe =
		&conn_state->hdmi.infoframes.spd;
	struct hdmi_spd_infoframe *frame =
		&infoframe->data.spd;
	int ret;

	ret = hdmi_spd_infoframe_init(frame,
				      connector->hdmi.vendor,
				      connector->hdmi.product);
	if (ret)
		return ret;

	frame->sdi = HDMI_SPD_SDI_PC;

	infoframe->set = true;

	return 0;
}

static int hdmi_generate_hdr_infoframe(const struct drm_connector *connector,
				       struct drm_connector_state *conn_state)
{
	struct drm_connector_hdmi_infoframe *infoframe =
		&conn_state->hdmi.infoframes.hdr_drm;
	struct hdmi_drm_infoframe *frame =
		&infoframe->data.drm;
	int ret;

	if (connector->max_bpc < 10)
		return 0;

	if (!conn_state->hdr_output_metadata)
		return 0;

	ret = drm_hdmi_infoframe_set_hdr_metadata(frame, conn_state);
	if (ret)
		return ret;

	infoframe->set = true;

	return 0;
}

static int hdmi_generate_hdmi_vendor_infoframe(const struct drm_connector *connector,
					       struct drm_connector_state *conn_state)
{
	const struct drm_display_info *info = &connector->display_info;
	const struct drm_display_mode *mode =
		connector_state_get_mode(conn_state);
	struct drm_connector_hdmi_infoframe *infoframe =
		&conn_state->hdmi.infoframes.hdmi;
	struct hdmi_vendor_infoframe *frame =
		&infoframe->data.vendor.hdmi;
	int ret;

	if (!info->has_hdmi_infoframe)
		return 0;

	ret = drm_hdmi_vendor_infoframe_from_display_mode(frame, connector, mode);
	if (ret)
		return ret;

	infoframe->set = true;

	return 0;
}

static int
hdmi_generate_infoframes(const struct drm_connector *connector,
			 struct drm_connector_state *conn_state)
{
	const struct drm_display_info *info = &connector->display_info;
	int ret;

	if (!info->is_hdmi)
		return 0;

	ret = hdmi_generate_avi_infoframe(connector, conn_state);
	if (ret)
		return ret;

	ret = hdmi_generate_spd_infoframe(connector, conn_state);
	if (ret)
		return ret;

	/*
	 * Audio Infoframes will be generated by ALSA, and updated by
	 * drm_atomic_helper_connector_hdmi_update_audio_infoframe().
	 */

	ret = hdmi_generate_hdr_infoframe(connector, conn_state);
	if (ret)
		return ret;

	ret = hdmi_generate_hdmi_vendor_infoframe(connector, conn_state);
	if (ret)
		return ret;

	return 0;
}

/**
 * drm_atomic_helper_connector_hdmi_check() - Helper to check HDMI connector atomic state
 * @connector: DRM Connector
 * @state: the DRM State object
 *
 * Provides a default connector state check handler for HDMI connectors.
 * Checks that a desired connector update is valid, and updates various
 * fields of derived state.
 *
 * RETURNS:
 * Zero on success, or an errno code otherwise.
 */
int drm_atomic_helper_connector_hdmi_check(struct drm_connector *connector,
					   struct drm_atomic_state *state)
{
	struct drm_connector_state *old_conn_state =
		drm_atomic_get_old_connector_state(state, connector);
	struct drm_connector_state *new_conn_state =
		drm_atomic_get_new_connector_state(state, connector);
	const struct drm_display_mode *mode =
		connector_state_get_mode(new_conn_state);
	int ret;

	new_conn_state->hdmi.is_limited_range = hdmi_is_limited_range(connector, new_conn_state);

	ret = hdmi_compute_config(connector, new_conn_state, mode);
	if (ret)
		return ret;

	ret = hdmi_generate_infoframes(connector, new_conn_state);
	if (ret)
		return ret;

	if (old_conn_state->hdmi.broadcast_rgb != new_conn_state->hdmi.broadcast_rgb ||
	    old_conn_state->hdmi.output_bpc != new_conn_state->hdmi.output_bpc ||
	    old_conn_state->hdmi.output_format != new_conn_state->hdmi.output_format) {
		struct drm_crtc *crtc = new_conn_state->crtc;
		struct drm_crtc_state *crtc_state;

		crtc_state = drm_atomic_get_crtc_state(state, crtc);
		if (IS_ERR(crtc_state))
			return PTR_ERR(crtc_state);

		crtc_state->mode_changed = true;
	}

	return 0;
}
EXPORT_SYMBOL(drm_atomic_helper_connector_hdmi_check);

static int clear_device_infoframe(struct drm_connector *connector,
				  enum hdmi_infoframe_type type)
{
	const struct drm_connector_hdmi_funcs *funcs = connector->hdmi.funcs;
	struct drm_device *dev = connector->dev;
	int ret;

	drm_dbg_kms(dev, "Clearing infoframe type 0x%x\n", type);

	if (!funcs || !funcs->clear_infoframe) {
		drm_dbg_kms(dev, "Function not implemented, bailing.\n");
		return 0;
	}

	ret = funcs->clear_infoframe(connector, type);
	if (ret) {
		drm_dbg_kms(dev, "Call failed: %d\n", ret);
		return ret;
	}

	return 0;
}

static int clear_infoframe(struct drm_connector *connector,
			   struct drm_connector_hdmi_infoframe *old_frame)
{
	int ret;

	ret = clear_device_infoframe(connector, old_frame->data.any.type);
	if (ret)
		return ret;

	return 0;
}

static int write_device_infoframe(struct drm_connector *connector,
				  union hdmi_infoframe *frame)
{
	const struct drm_connector_hdmi_funcs *funcs = connector->hdmi.funcs;
	struct drm_device *dev = connector->dev;
	u8 buffer[HDMI_INFOFRAME_SIZE(MAX)];
	int ret;
	int len;

	drm_dbg_kms(dev, "Writing infoframe type %x\n", frame->any.type);

	if (!funcs || !funcs->write_infoframe) {
		drm_dbg_kms(dev, "Function not implemented, bailing.\n");
		return -EINVAL;
	}

	len = hdmi_infoframe_pack(frame, buffer, sizeof(buffer));
	if (len < 0)
		return len;

	ret = funcs->write_infoframe(connector, frame->any.type, buffer, len);
	if (ret) {
		drm_dbg_kms(dev, "Call failed: %d\n", ret);
		return ret;
	}

	return 0;
}

static int write_infoframe(struct drm_connector *connector,
			   struct drm_connector_hdmi_infoframe *new_frame)
{
	int ret;

	ret = write_device_infoframe(connector, &new_frame->data);
	if (ret)
		return ret;

	return 0;
}

static int write_or_clear_infoframe(struct drm_connector *connector,
				    struct drm_connector_hdmi_infoframe *old_frame,
				    struct drm_connector_hdmi_infoframe *new_frame)
{
	if (new_frame->set)
		return write_infoframe(connector, new_frame);

	if (old_frame->set && !new_frame->set)
		return clear_infoframe(connector, old_frame);

	return 0;
}

/**
 * drm_atomic_helper_connector_hdmi_update_infoframes - Update the Infoframes
 * @connector: A pointer to the HDMI connector
 * @state: The HDMI connector state to generate the infoframe from
 *
 * This function is meant for HDMI connector drivers to write their
 * infoframes. It will typically be used in a
 * @drm_connector_helper_funcs.atomic_enable implementation.
 *
 * Returns:
 * Zero on success, error code on failure.
 */
int drm_atomic_helper_connector_hdmi_update_infoframes(struct drm_connector *connector,
						       struct drm_atomic_state *state)
{
	struct drm_connector_state *old_conn_state =
		drm_atomic_get_old_connector_state(state, connector);
	struct drm_connector_state *new_conn_state =
		drm_atomic_get_new_connector_state(state, connector);
	struct drm_display_info *info = &connector->display_info;
	int ret;

	if (!info->is_hdmi)
		return 0;

	mutex_lock(&connector->hdmi.infoframes.lock);

	ret = write_or_clear_infoframe(connector,
				       &old_conn_state->hdmi.infoframes.avi,
				       &new_conn_state->hdmi.infoframes.avi);
	if (ret)
		goto out;

	if (connector->hdmi.infoframes.audio.set) {
		ret = write_infoframe(connector,
				      &connector->hdmi.infoframes.audio);
		if (ret)
			goto out;
	}

	ret = write_or_clear_infoframe(connector,
				       &old_conn_state->hdmi.infoframes.hdr_drm,
				       &new_conn_state->hdmi.infoframes.hdr_drm);
	if (ret)
		goto out;

	ret = write_or_clear_infoframe(connector,
				       &old_conn_state->hdmi.infoframes.spd,
				       &new_conn_state->hdmi.infoframes.spd);
	if (ret)
		goto out;

	if (info->has_hdmi_infoframe) {
		ret = write_or_clear_infoframe(connector,
					       &old_conn_state->hdmi.infoframes.hdmi,
					       &new_conn_state->hdmi.infoframes.hdmi);
		if (ret)
			goto out;
	}

out:
	mutex_unlock(&connector->hdmi.infoframes.lock);
	return ret;
}
EXPORT_SYMBOL(drm_atomic_helper_connector_hdmi_update_infoframes);

/**
 * drm_atomic_helper_connector_hdmi_update_audio_infoframe - Update the Audio Infoframe
 * @connector: A pointer to the HDMI connector
 * @frame: A pointer to the audio infoframe to write
 *
 * This function is meant for HDMI connector drivers to update their
 * audio infoframe. It will typically be used in one of the ALSA hooks
 * (most likely prepare).
 *
 * Returns:
 * Zero on success, error code on failure.
 */
int
drm_atomic_helper_connector_hdmi_update_audio_infoframe(struct drm_connector *connector,
							struct hdmi_audio_infoframe *frame)
{
	struct drm_connector_hdmi_infoframe *infoframe =
		&connector->hdmi.infoframes.audio;
	struct drm_display_info *info = &connector->display_info;
	int ret;

	if (!info->is_hdmi)
		return 0;

	mutex_lock(&connector->hdmi.infoframes.lock);

	memcpy(&infoframe->data, frame, sizeof(infoframe->data));
	infoframe->set = true;

	ret = write_infoframe(connector, infoframe);

	mutex_unlock(&connector->hdmi.infoframes.lock);

	return ret;
}
EXPORT_SYMBOL(drm_atomic_helper_connector_hdmi_update_audio_infoframe);

/**
 * drm_atomic_helper_connector_hdmi_clear_audio_infoframe - Stop sending the Audio Infoframe
 * @connector: A pointer to the HDMI connector
 *
 * This function is meant for HDMI connector drivers to stop sending their
 * audio infoframe. It will typically be used in one of the ALSA hooks
 * (most likely shutdown).
 *
 * Returns:
 * Zero on success, error code on failure.
 */
int
drm_atomic_helper_connector_hdmi_clear_audio_infoframe(struct drm_connector *connector)
{
	struct drm_connector_hdmi_infoframe *infoframe =
		&connector->hdmi.infoframes.audio;
	struct drm_display_info *info = &connector->display_info;
	int ret;

	if (!info->is_hdmi)
		return 0;

	mutex_lock(&connector->hdmi.infoframes.lock);

	infoframe->set = false;

	ret = clear_infoframe(connector, infoframe);

	memset(&infoframe->data, 0, sizeof(infoframe->data));

	mutex_unlock(&connector->hdmi.infoframes.lock);

	return ret;
}
EXPORT_SYMBOL(drm_atomic_helper_connector_hdmi_clear_audio_infoframe);