/* $NetBSD: linux_hdmi.c,v 1.10 2022/07/10 13:56:44 riastradh Exp $ */ /*- * Copyright (c) 2014 The NetBSD Foundation, Inc. * All rights reserved. * * This code is derived from software contributed to The NetBSD Foundation * by Taylor R. Campbell. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include __KERNEL_RCSID(0, "$NetBSD: linux_hdmi.c,v 1.10 2022/07/10 13:56:44 riastradh Exp $"); #include #include #include #include #include #include /* Infoframe headers */ static void hdmi_infoframe_header_init(struct hdmi_infoframe_header *header, enum hdmi_infoframe_type type, uint8_t vers, uint8_t length) { header->type = type; header->version = vers; header->length = length; } static int hdmi_infoframe_header_check(const struct hdmi_infoframe_header *header, enum hdmi_infoframe_type type, uint8_t vers, uint8_t length) { if (header->type != type || header->version != vers || header->length != length) return -EINVAL; return 0; } static ssize_t hdmi_infoframe_header_pack(const struct hdmi_infoframe_header *header, uint8_t length, void *buf, size_t size) { uint8_t *const p = buf; KASSERT(length >= HDMI_INFOFRAME_HEADER_SIZE); if (size < length) return -ENOSPC; p[0] = header->type; p[1] = header->version; p[2] = (length - HDMI_INFOFRAME_HEADER_SIZE); p[3] = 0; /* checksum */ return HDMI_INFOFRAME_HEADER_SIZE; } static uint8_t hdmi_infoframe_checksum(const void *buf, size_t length) { const uint8_t *p = buf; uint8_t checksum = 0; while (length--) checksum += *p++; return 256 - checksum; } static int hdmi_infoframe_header_unpack(struct hdmi_infoframe_header *header, const void *buf, size_t size) { const uint8_t *const p = buf; if (size < HDMI_INFOFRAME_HEADER_SIZE) return -EINVAL; if (p[2] > size - HDMI_INFOFRAME_HEADER_SIZE) return -EINVAL; if (hdmi_infoframe_checksum(buf, p[2] + HDMI_INFOFRAME_HEADER_SIZE)) return -EINVAL; hdmi_infoframe_header_init(header, p[0], p[1], p[2]); return 0; } static void hdmi_infoframe_set_checksum(void *buf, size_t length) { uint8_t *p = buf; p[3] = hdmi_infoframe_checksum(buf, length); } /* Audio infoframes */ int hdmi_audio_infoframe_init(struct hdmi_audio_infoframe *frame) { static const struct hdmi_audio_infoframe zero_frame; *frame = zero_frame; hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_AUDIO, 1, HDMI_AUDIO_INFOFRAME_SIZE); return 0; } ssize_t hdmi_audio_infoframe_pack(const struct hdmi_audio_infoframe *frame, void *buf, size_t size) { const size_t length = HDMI_INFOFRAME_HEADER_SIZE + HDMI_AUDIO_INFOFRAME_SIZE; uint8_t channels = 0; uint8_t *p = buf; int ret; KASSERT(frame->header.length == HDMI_AUDIO_INFOFRAME_SIZE); ret = hdmi_infoframe_header_pack(&frame->header, length, p, size); if (ret < 0) return ret; KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE); p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; if (frame->channels >= 2) channels = frame->channels - 1; p[0] = __SHIFTIN(frame->coding_type, __BITS(7,4)); p[0] |= __SHIFTIN(channels, __BITS(2,0)); p[1] = __SHIFTIN(frame->sample_frequency, __BITS(4,2)); p[1] |= __SHIFTIN(frame->sample_size, __BITS(1,0)); p[2] = __SHIFTIN(frame->coding_type_ext, __BITS(5,0)); p[3] = __SHIFTIN(frame->level_shift_value, __BITS(6,3)); p[4] = __SHIFTIN(frame->downmix_inhibit? 1 : 0, __BIT(7)); /* PB6 to PB10 are reserved */ p[5] = 0; p[6] = 0; p[7] = 0; p[8] = 0; p[9] = 0; CTASSERT(HDMI_AUDIO_INFOFRAME_SIZE == 10); hdmi_infoframe_set_checksum(buf, length); return length; } static int hdmi_audio_infoframe_unpack(struct hdmi_audio_infoframe *frame, const void *buf, size_t size) { const uint8_t *p = buf; int ret; ret = hdmi_infoframe_header_unpack(&frame->header, p, size); if (ret) return ret; if (frame->header.length != HDMI_AUDIO_INFOFRAME_SIZE) return -EINVAL; p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; frame->coding_type = __SHIFTOUT(p[0], __BITS(7,4)); frame->channels = __SHIFTOUT(p[0], __BITS(2,0)); frame->sample_frequency = __SHIFTOUT(p[1], __BITS(4,2)); frame->sample_size = __SHIFTOUT(p[1], __BITS(1,0)); frame->coding_type_ext = __SHIFTOUT(p[2], __BITS(5,0)); frame->level_shift_value = __SHIFTOUT(p[3], __BITS(6,3)); frame->downmix_inhibit = __SHIFTOUT(p[4], __BIT(7)); return 0; } /* AVI infoframes */ int hdmi_avi_infoframe_init(struct hdmi_avi_infoframe *frame) { static const struct hdmi_avi_infoframe zero_frame; *frame = zero_frame; hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_AVI, 2, HDMI_AVI_INFOFRAME_SIZE); return 0; } int hdmi_avi_infoframe_check(const struct hdmi_avi_infoframe *frame) { int ret; ret = hdmi_infoframe_header_check(&frame->header, HDMI_INFOFRAME_TYPE_AVI, 2, HDMI_AVI_INFOFRAME_SIZE); if (ret) return ret; return 0; } ssize_t hdmi_avi_infoframe_pack(const struct hdmi_avi_infoframe *frame, void *buf, size_t size) { const size_t length = HDMI_INFOFRAME_HEADER_SIZE + HDMI_AVI_INFOFRAME_SIZE; uint8_t *p = buf; int ret; KASSERT(frame->header.length == HDMI_AVI_INFOFRAME_SIZE); ret = hdmi_infoframe_header_pack(&frame->header, length, p, size); if (ret < 0) return ret; KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE); p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; p[0] = __SHIFTIN(frame->colorspace, __BITS(6,5)); p[0] |= __SHIFTIN(frame->active_aspect & 0xf? 1 : 0, __BIT(4)); p[0] |= __SHIFTIN(frame->top_bar || frame->bottom_bar, __BIT(3)); p[0] |= __SHIFTIN(frame->left_bar || frame->right_bar, __BIT(2)); p[0] |= __SHIFTIN(frame->scan_mode, __BITS(1,0)); p[1] = __SHIFTIN(frame->colorimetry, __BITS(7,6)); p[1] |= __SHIFTIN(frame->picture_aspect, __BITS(5,4)); p[1] |= __SHIFTIN(frame->active_aspect, __BITS(3,0)); p[2] = __SHIFTIN(frame->itc? 1 : 0, __BIT(7)); p[2] |= __SHIFTIN(frame->extended_colorimetry, __BITS(6,4)); p[2] |= __SHIFTIN(frame->quantization_range, __BITS(3,2)); p[2] |= __SHIFTIN(frame->nups, __BITS(1,0)); p[3] = frame->video_code; p[4] = __SHIFTIN(frame->ycc_quantization_range, __BITS(7,6)); p[4] |= __SHIFTIN(frame->content_type, __BITS(5,4)); p[4] |= __SHIFTIN(frame->pixel_repeat, __BITS(3,0)); le16enc(&p[5], frame->top_bar); le16enc(&p[7], frame->bottom_bar); le16enc(&p[9], frame->left_bar); le16enc(&p[11], frame->right_bar); CTASSERT(HDMI_AVI_INFOFRAME_SIZE == 13); hdmi_infoframe_set_checksum(buf, length); return length; } static int hdmi_avi_infoframe_unpack(struct hdmi_avi_infoframe *frame, const void *buf, size_t size) { const uint8_t *p = buf; int ret; ret = hdmi_infoframe_header_unpack(&frame->header, p, size); if (ret) return ret; if (frame->header.length != HDMI_AVI_INFOFRAME_SIZE) return -EINVAL; p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; frame->colorspace = __SHIFTOUT(p[0], __BITS(6,5)); frame->scan_mode = __SHIFTOUT(p[0], __BITS(1,0)); frame->colorimetry = __SHIFTOUT(p[1], __BITS(7,6)); frame->picture_aspect = __SHIFTOUT(p[1], __BITS(5,4)); if (p[0] & __BIT(4)) frame->active_aspect = __SHIFTOUT(p[1], __BITS(3,0)); frame->itc = __SHIFTOUT(p[2], __BIT(7)); frame->extended_colorimetry = __SHIFTOUT(p[2], __BITS(6,4)); frame->quantization_range = __SHIFTOUT(p[2], __BITS(3,2)); frame->nups = __SHIFTOUT(p[2], __BITS(1,0)); frame->video_code = p[3]; frame->ycc_quantization_range = __SHIFTOUT(p[4], __BITS(7,6)); frame->content_type = __SHIFTOUT(p[4], __BITS(5,4)); frame->pixel_repeat = __SHIFTOUT(p[4], __BITS(3,0)); if (p[0] & __BIT(3)) { frame->top_bar = le16dec(&p[5]); frame->bottom_bar = le16dec(&p[7]); } if (p[0] & __BIT(2)) { frame->left_bar = le16dec(&p[9]); frame->right_bar = le16dec(&p[11]); } return 0; } /* DRM infoframes */ int hdmi_drm_infoframe_init(struct hdmi_drm_infoframe *frame) { static const struct hdmi_drm_infoframe zero_frame; *frame = zero_frame; hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_DRM, 1, HDMI_DRM_INFOFRAME_SIZE); return 0; } int hdmi_drm_infoframe_check(const struct hdmi_drm_infoframe *frame) { int ret; ret = hdmi_infoframe_header_check(&frame->header, HDMI_INFOFRAME_TYPE_DRM, 1, HDMI_DRM_INFOFRAME_SIZE); if (ret) return ret; return 0; } __strong_alias(linux_hdmi_drm_infoframe_pack_only,linux_hdmi_drm_infoframe_pack) /* XXX */ ssize_t hdmi_drm_infoframe_pack(const struct hdmi_drm_infoframe *frame, void *buf, size_t size) { const size_t length = HDMI_INFOFRAME_HEADER_SIZE + HDMI_DRM_INFOFRAME_SIZE; uint8_t *p = buf; unsigned i; int ret; KASSERT(frame->header.length == HDMI_DRM_INFOFRAME_SIZE); ret = hdmi_infoframe_header_pack(&frame->header, length, p, size); if (ret < 0) return ret; KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE); p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; p[0] = frame->eotf; p[1] = frame->metadata_type; for (i = 0; i < __arraycount(frame->display_primaries); i++) { le16enc(&p[2 + 4*i], frame->display_primaries[i].x); le16enc(&p[2 + 4*i + 2], frame->display_primaries[i].y); } le16enc(&p[14], frame->white_point.x); le16enc(&p[16], frame->white_point.y); le16enc(&p[18], frame->min_display_mastering_luminance); le16enc(&p[20], frame->max_display_mastering_luminance); le16enc(&p[22], frame->max_cll); le16enc(&p[24], frame->max_fall); CTASSERT(HDMI_DRM_INFOFRAME_SIZE == 26); hdmi_infoframe_set_checksum(buf, length); return length; } static int hdmi_drm_infoframe_unpack(struct hdmi_drm_infoframe *frame, const void *buf, size_t size) { const uint8_t *p = buf; unsigned i; int ret; ret = hdmi_infoframe_header_unpack(&frame->header, p, size); if (ret) return ret; if (frame->header.length != HDMI_DRM_INFOFRAME_SIZE) return -EINVAL; p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; frame->eotf = p[0]; frame->metadata_type = p[1]; for (i = 0; i < __arraycount(frame->display_primaries); i++) { frame->display_primaries[i].x = le16dec(&p[2 + 4*i]); frame->display_primaries[i].y = le16dec(&p[2 + 4*i + 2]); } frame->white_point.x = le16dec(&p[14]); frame->white_point.y = le16dec(&p[16]); frame->min_display_mastering_luminance = le16dec(&p[18]); frame->max_display_mastering_luminance = le16dec(&p[20]); frame->max_cll = le16dec(&p[22]); frame->max_fall = le16dec(&p[24]); return 0; } /* SPD infoframes */ int hdmi_spd_infoframe_init(struct hdmi_spd_infoframe *frame, const char *vendor, const char *product) { static const struct hdmi_spd_infoframe zero_frame; *frame = zero_frame; hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_SPD, 1, HDMI_SPD_INFOFRAME_SIZE); strncpy(frame->vendor, vendor, sizeof(frame->vendor)); strncpy(frame->product, product, sizeof(frame->product)); return 0; } int hdmi_spd_infoframe_check(const struct hdmi_spd_infoframe *frame) { int ret; ret = hdmi_infoframe_header_check(&frame->header, HDMI_INFOFRAME_TYPE_SPD, 1, HDMI_SPD_INFOFRAME_SIZE); if (ret) return ret; return 0; } ssize_t hdmi_spd_infoframe_pack(const struct hdmi_spd_infoframe *frame, void *buf, size_t size) { const size_t length = HDMI_INFOFRAME_HEADER_SIZE + HDMI_SPD_INFOFRAME_SIZE; uint8_t *p = buf; int ret; KASSERT(frame->header.length == HDMI_SPD_INFOFRAME_SIZE); ret = hdmi_infoframe_header_pack(&frame->header, length, p, size); if (ret < 0) return ret; KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE); p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; memcpy(&p[0], frame->vendor, 8); memcpy(&p[8], frame->product, 16); p[24] = frame->sdi; CTASSERT(HDMI_SPD_INFOFRAME_SIZE == 25); hdmi_infoframe_set_checksum(buf, length); return length; } static int hdmi_spd_infoframe_unpack(struct hdmi_spd_infoframe *frame, const void *buf, size_t size) { const uint8_t *p = buf; int ret; ret = hdmi_infoframe_header_unpack(&frame->header, p, size); if (ret) return ret; if (frame->header.length != HDMI_SPD_INFOFRAME_SIZE) return -EINVAL; p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; memcpy(frame->vendor, &p[0], 8); memcpy(frame->product, &p[8], 16); frame->sdi = p[24]; return 0; } /* Vendor infoframes */ int hdmi_vendor_infoframe_init(struct hdmi_vendor_infoframe *frame) { static const struct hdmi_vendor_infoframe zero_frame; *frame = zero_frame; hdmi_infoframe_header_init(&frame->header, HDMI_INFOFRAME_TYPE_VENDOR, 1, 0 /* depends on s3d_struct */); frame->oui = HDMI_IEEE_OUI; frame->s3d_struct = HDMI_3D_STRUCTURE_INVALID; return 0; } static size_t hdmi_vendor_infoframe_length(const struct hdmi_vendor_infoframe *frame) { if (frame->vic) { return 5; } else if (frame->s3d_struct != HDMI_3D_STRUCTURE_INVALID) { if (frame->s3d_struct < HDMI_3D_STRUCTURE_SIDE_BY_SIDE_HALF) return 5; else return 6; } else { return 4; } } int hdmi_vendor_infoframe_check(const struct hdmi_vendor_infoframe *frame) { if (frame->header.type != HDMI_INFOFRAME_TYPE_VENDOR || frame->header.version != 1) return -EINVAL; /* frame->header.length not used when packing */ /* At most one may be supplied. */ if (frame->vic != 0 && frame->s3d_struct != HDMI_3D_STRUCTURE_INVALID) return -EINVAL; return 0; } ssize_t hdmi_vendor_infoframe_pack(const struct hdmi_vendor_infoframe *frame, void *buf, size_t size) { uint8_t *p = buf; size_t length; int ret; /* At most one may be supplied. */ if (frame->vic != 0 && frame->s3d_struct != HDMI_3D_STRUCTURE_INVALID) return -EINVAL; length = HDMI_INFOFRAME_HEADER_SIZE; length += hdmi_vendor_infoframe_length(frame); ret = hdmi_infoframe_header_pack(&frame->header, length, p, size); if (ret < 0) return ret; KASSERT(ret == HDMI_INFOFRAME_HEADER_SIZE); p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; p[0] = 0x03; p[1] = 0x0c; p[2] = 0x00; if (frame->vic) { p[3] = __SHIFTIN(0x1, __BITS(6,5)); p[4] = frame->vic; } else if (frame->s3d_struct != HDMI_3D_STRUCTURE_INVALID) { p[3] = __SHIFTIN(0x2, __BITS(6,5)); p[4] = __SHIFTIN(frame->s3d_struct, __BITS(7,4)); if (frame->s3d_struct >= HDMI_3D_STRUCTURE_SIDE_BY_SIDE_HALF) p[5] = __SHIFTIN(frame->s3d_ext_data, __BITS(7,4)); } else { p[3] = __SHIFTIN(0x0, __BITS(6,5)); } hdmi_infoframe_set_checksum(buf, length); return length; } static int hdmi_vendor_infoframe_unpack(struct hdmi_vendor_infoframe *frame, const void *buf, size_t size) { const uint8_t *p = buf; int ret; ret = hdmi_infoframe_header_unpack(&frame->header, p, size); if (ret) return ret; if (frame->header.length < 4) return -EINVAL; p += HDMI_INFOFRAME_HEADER_SIZE; size -= HDMI_INFOFRAME_HEADER_SIZE; if (p[0] != 0x03 || p[1] != 0x0c || p[2] != 0x00) return -EINVAL; switch (__SHIFTOUT(p[3], __BITS(6,5))) { case 0x0: if (frame->header.length != 4) return -EINVAL; break; case 0x1: if (frame->header.length != 5) return -EINVAL; frame->vic = p[4]; break; case 0x2: if (frame->header.length < 5) return -EINVAL; frame->s3d_struct = __SHIFTOUT(p[4], __BITS(7,4)); if (frame->s3d_struct < HDMI_3D_STRUCTURE_SIDE_BY_SIDE_HALF) { if (frame->header.length != 5) return -EINVAL; } else { if (frame->header.length != 6) return -EINVAL; frame->s3d_ext_data = __SHIFTOUT(p[5], __BITS(7,4)); } break; default: return -EINVAL; } return 0; } /* union infoframe */ __strong_alias(linux_hdmi_infoframe_pack_only,linux_hdmi_infoframe_pack) /* XXX */ ssize_t hdmi_infoframe_pack(const union hdmi_infoframe *frame, void *buf, size_t size) { switch (frame->any.type) { case HDMI_INFOFRAME_TYPE_VENDOR: return hdmi_vendor_infoframe_pack(&frame->vendor.hdmi, buf, size); case HDMI_INFOFRAME_TYPE_AVI: return hdmi_avi_infoframe_pack(&frame->avi, buf, size); case HDMI_INFOFRAME_TYPE_SPD: return hdmi_spd_infoframe_pack(&frame->spd, buf, size); case HDMI_INFOFRAME_TYPE_AUDIO: return hdmi_audio_infoframe_pack(&frame->audio, buf, size); case HDMI_INFOFRAME_TYPE_DRM: return hdmi_drm_infoframe_pack(&frame->drm, buf, size); default: return -EINVAL; } } int hdmi_infoframe_unpack(union hdmi_infoframe *frame, const void *buf, size_t size) { int ret; memset(frame, 0, sizeof(*frame)); ret = hdmi_infoframe_header_unpack(&frame->any, buf, size); if (ret) return ret; switch (frame->any.type) { case HDMI_INFOFRAME_TYPE_VENDOR: return hdmi_vendor_infoframe_unpack(&frame->vendor.hdmi, buf, size); case HDMI_INFOFRAME_TYPE_AVI: return hdmi_avi_infoframe_unpack(&frame->avi, buf, size); case HDMI_INFOFRAME_TYPE_SPD: return hdmi_spd_infoframe_unpack(&frame->spd, buf, size); case HDMI_INFOFRAME_TYPE_AUDIO: return hdmi_audio_infoframe_unpack(&frame->audio, buf, size); case HDMI_INFOFRAME_TYPE_DRM: return hdmi_drm_infoframe_unpack(&frame->drm, buf, size); default: return -EINVAL; } } void hdmi_infoframe_log(const char *level, struct device *device, const union hdmi_infoframe *frame) { hexdump(printf, device_xname(device), frame, sizeof(*frame)); }