From 468d76e876c03021a87f3be0628e84002447bb9a Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 20:14:49 +1100 Subject: [PATCH 01/10] drm/apple: select compatible SDR color modes for external H17P Choose advertised full-range 8/10-bit SDR RGB modes for the experimental external H17P profile. Prefer sRGB or BT.709 over default RGB, then use firmware scores. Reject timings that only offer incompatible color modes instead of selecting BT.2020 for native SDR scanout. Leave internal and legacy policy unchanged. The working J700 selected BT.2020 RGB while displaying an SDR desktop. Production-parser tests cover 65 valid, malformed, legacy and high-refresh cases. Hardware color appearance still requires qualification. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + drivers/gpu/drm/apple/iomfb_template.c | 4 +- drivers/gpu/drm/apple/parser.c | 44 ++++- drivers/gpu/drm/apple/parser.h | 2 +- tools/diagnostics/test-apple-external-sdr.py | 188 +++++++++++++++++++ 5 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 tools/diagnostics/test-apple-external-sdr.py diff --git a/MAINTAINERS b/MAINTAINERS index 9e9fbe808e1bc..90a72c4fd6b16 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2021,6 +2021,7 @@ F: tools/diagnostics/apple-j700-dp-test.c F: tools/diagnostics/test-apple-afk-admission.py F: tools/diagnostics/test-apple-atc-unwind.py F: tools/diagnostics/test-apple-dcp-readiness.py +F: tools/diagnostics/test-apple-external-sdr.py F: tools/diagnostics/test-apple-j700-crossbar.py F: tools/diagnostics/test-apple-j700-d134.py F: tools/diagnostics/test-apple-j700-dp.py diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 575b99606e1ac..31764f1bc3497 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -985,7 +985,9 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, dcp->width_mm, dcp->height_mm, dcp->notch_height, dcp->fixed_connector_type == - DRM_MODE_CONNECTOR_eDP); + DRM_MODE_CONNECTOR_eDP, + dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL); if (IS_ERR(dcp->modes)) { dev_warn(dcp->dev, "failed to parse modes\n"); diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 16027c1923369..73e7e1cf315c6 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -390,10 +390,32 @@ static int fill_color_mode(struct dcp_color_mode *color, return 0; } +/* External SDR scanout must not select a wide-gamut mode by score alone. */ +static unsigned int external_sdr_rank(const struct color_mode *color) +{ + if (color->id < 0 || color->id > U32_MAX || + color->eotf != DCP_EOTF_SDR_GAMMA || + color->pixel_encoding != DCP_COLOR_FORMAT_RGB || + color->dynamic_range != DCP_COLOR_YCBCR_RANGE_FULL || + (color->depth != 8 && color->depth != 10)) + return 0; + + switch (color->colorimetry) { + case DCP_COLORIMETRY_SRGB: + case DCP_COLORIMETRY_BT709: + return 2; + case DCP_COLORIMETRY_RGB: + return 1; + default: + return 0; + } +} + static int parse_color_modes(struct dcp_parse_ctx *handle, - struct dcp_display_mode *out) + struct dcp_display_mode *out, bool external_sdr) { struct iterator outer_it; + unsigned int best_sdr_rank = 0; int ret = 0; out->sdr_444.score = -1; out->sdr_rgb.score = -1; @@ -461,6 +483,18 @@ static int parse_color_modes(struct dcp_parse_ctx *handle, cmode.eotf, cmode.dynamic_range, cmode.pixel_encoding); + if (external_sdr) { + unsigned int rank = external_sdr_rank(&cmode); + + if (!rank || rank < best_sdr_rank) + continue; + if (rank > best_sdr_rank) + out->sdr_rgb.score = -1; + fill_color_mode(&out->sdr_rgb, &cmode); + best_sdr_rank = rank; + continue; + } + if (cmode.eotf == DCP_EOTF_SDR_GAMMA) { if (cmode.pixel_encoding == DCP_COLOR_FORMAT_RGB && cmode.depth <= 10) @@ -492,7 +526,7 @@ static u64 calculate_clock(struct dimension *horiz, struct dimension *vert) static int parse_mode(struct dcp_parse_ctx *handle, struct dcp_display_mode *out, s64 *score, int width_mm, - int height_mm, unsigned notch_height, bool internal) + int height_mm, unsigned notch_height, bool internal, bool external_sdr) { int ret = 0; struct iterator it; @@ -532,7 +566,7 @@ static int parse_mode(struct dcp_parse_ctx *handle, else if (!strcmp(key, "MaximumVariableRefreshRate")) ret = parse_int_bound(it.handle, &max_vrr, 0, U32_MAX); else if (!strcmp(key, "ColorModes")) - ret = parse_color_modes(it.handle, out); + ret = parse_color_modes(it.handle, out, external_sdr); else if (!strcmp(key, "ID")) ret = parse_int_bound(it.handle, &id, 0, U32_MAX); else if (!strcmp(key, "IsVirtual")) @@ -638,7 +672,7 @@ static int parse_mode(struct dcp_parse_ctx *handle, struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, unsigned int *count, int width_mm, int height_mm, unsigned notch_height, - bool internal) + bool internal, bool external_sdr) { struct iterator it; int ret; @@ -663,7 +697,7 @@ struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, mode = &modes[*count]; ret = parse_mode(it.handle, mode, &score, width_mm, height_mm, - notch_height, internal); + notch_height, internal, external_sdr); /* Errors for a single mode are recoverable -- just skip it. */ if (ret) { diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index bfb0a06c8760a..1f3b8782b7bfc 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -105,7 +105,7 @@ int parse(const void *blob, size_t size, struct dcp_parse_ctx *ctx); struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, unsigned int *count, int width_mm, int height_mm, unsigned notch_height, - bool internal); + bool internal, bool external_sdr); int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, int *height_mm); int parse_epic_service_init(struct dcp_parse_ctx *handle, const char **name, diff --git a/tools/diagnostics/test-apple-external-sdr.py b/tools/diagnostics/test-apple-external-sdr.py new file mode 100644 index 0000000000000..f72c26e38108c --- /dev/null +++ b/tools/diagnostics/test-apple-external-sdr.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Parse real-format TimingElements blobs with the production SDR selection code.""" +import hashlib +import os +from pathlib import Path +import re +import shlex +import struct +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +base = root / 'drivers/gpu/drm/apple' +source = (base / 'parser.c').read_text() +header = (base / 'parser.h').read_text() +body = source[source.index('#define DCP_PARSE_HEADER'):source.index('int parse_display_attributes(')] +header = header[header.index('struct apple_dcp;'):header.index('int parse(')] +# Pin the integration scope: the template passes its explicit external profile, +# independent of the existing internal-panel boolean. +template = (base / 'iomfb_template.c').read_text() +call = re.search(r'enumerate_modes\(&ctx,.*?\);', template, re.S).group() +assert re.search(r'dcp->hw\.iomfb_method_profile\s*==\s*DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL\)', call) +print('Production mode parser SHA256:', hashlib.sha256((header + body).encode()).hexdigest()) + + +def wire(value, out): + out += b'\0' * (-len(out) % 4) + if isinstance(value, bool): + out += struct.pack(' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int64_t s64; +#define __packed __attribute__((packed)) +#define IS_ENABLED(x) 0 +#define U32_MAX UINT32_MAX +#define U16_MAX UINT16_MAX +#define S64_MAX INT64_MAX +#define S64_MIN INT64_MIN +#define round_up(n, a) (((n) + (a) - 1) & ~((a) - 1)) +#define ERR_PTR(n) ((void *)(intptr_t)(n)) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define IS_ERR(p) ((uintptr_t)(p) > (uintptr_t)-4096) +#define IS_ERR_OR_NULL(p) (!(p) || IS_ERR(p)) +#define GFP_KERNEL 0 +#define kmalloc(n, f) malloc(n) +#define kcalloc(n, s, f) calloc(n,s) +#define kfree(p) free(p) +#define mul_u32_u32(a, b) ((u64)(u32)(a) * (u32)(b)) +#define DIV_ROUND_CLOSEST_ULL(n, d) (((n) + (d) / 2) / (d)) +#define trace_iomfb_color_mode(...) ((void)0) +#define trace_iomfb_parse_mode_fail(...) ((void)0) +#define trace_iomfb_parse_mode_success(...) ((void)0) +#define trace_iomfb_timing_mode(...) ((void)0) +#define DRM_MODE_TYPE_DRIVER 1 +#define DRM_MODE_TYPE_PREFERRED 2 +struct drm_display_mode { + unsigned int type; + int clock, vdisplay, vsync_start, vsync_end, vtotal; + int hdisplay, hsync_start, hsync_end, htotal, width_mm, height_mm; +}; +static void drm_mode_set_name(struct drm_display_mode *mode) { (void)mode; } +''' +data = '\n'.join('static const unsigned char blob_%u[] = {%s};' % + (i, ','.join(map(str, case[1]))) for i, case in enumerate(cases)) +suffix = r''' +int main(void) +{ +''' +for i, (name, blob, external, count, identity, depth, clock) in enumerate(cases): + suffix += f''' + {{ + struct dcp_parse_ctx ctx; + unsigned int count = 99; + assert(parse(blob_{i}, sizeof(blob_{i}), &ctx) == 0); + struct dcp_display_mode *modes = enumerate_modes(&ctx, &count, 600, 340, 0, + {str(name.startswith('internal')).lower()}, {str(external).lower()}); + if (IS_ERR(modes) || count != {count}) {{ + fprintf(stderr, "case {i}: {name} count=%u\\n", count); + abort(); + }} +''' + if count: + suffix += f''' + assert(modes[0].color_mode_id == {identity}); + assert(modes[0].sdr_rgb.depth == {depth}); + assert(modes[0].mode.clock == {clock}); + assert(modes[0].mode.type & DRM_MODE_TYPE_PREFERRED); +''' + suffix += ' free(modes);\n }\n' +suffix += f' puts("PASS: {len(cases)} production binary-parser SDR, malformed-input, legacy and high-refresh cases");\n return 0;\n}}\n' +with tempfile.TemporaryDirectory(prefix='apple-external-sdr-') as directory: + directory = Path(directory) + unit = directory / 'test.c' + unit.write_text(prefix + header + body + data + suffix) + command = shlex.split(os.environ.get('CC', 'clang')) + command += ['-std=gnu11', '-Wall', '-Wextra', '-Werror', '-Wno-sign-compare', '-O1', '-g', + '-fsanitize=address,undefined', '-fno-omit-frame-pointer'] + subprocess.run(command + [str(unit), '-o', str(directory / 'test')], check=True) + subprocess.run([str(directory / 'test')], check=True) From 014aed99ea14aa654fa4d0b9472e577702e55ed5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 20:18:06 +1100 Subject: [PATCH 02/10] drm/apple: retrieve external H17P EDID through bounded AFKV2 inline calls Use the external DPAV endpoint's inline framing with independent request length and response capacity. Validate actual service and EDID lengths before reading them, retain timeout ownership, and queue a connector reprobe when the service appears late. Preserve the legacy padded OOB path and EDID reader, and keep internal H17P excluded. Host tests exercise the production sender, service envelope, EDID reader, ring admission and late notification with malformed replies and timeout cases. Hardware identity and color qualification remain pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 168 ++++++++++++++---- drivers/gpu/drm/apple/afk.h | 7 + drivers/gpu/drm/apple/afkv2-codec.h | 52 ++++++ drivers/gpu/drm/apple/epic/dpavserv-edid.h | 48 +++++ drivers/gpu/drm/apple/epic/dpavservep.c | 74 ++++++-- drivers/gpu/drm/apple/iomfb.c | 7 +- .../drivers/gpu/apple-afkv2/Makefile | 2 +- .../selftests/drivers/gpu/apple-afkv2/README | 16 ++ .../drivers/gpu/apple-afkv2/extract-edid.py | 79 ++++++++ .../drivers/gpu/apple-afkv2/extract-notify.py | 50 ++++++ .../drivers/gpu/apple-afkv2/extract-ring.py | 50 ++++++ .../drivers/gpu/apple-afkv2/extract-send.py | 118 ++++++++++++ .../gpu/apple-afkv2/extract-service.py | 86 +++++++++ .../apple-afkv2/host-include/linux/errno.h | 5 + .../drivers/gpu/apple-afkv2/inline-edid.c | 113 ++++++++++++ .../gpu/apple-afkv2/run-afkv2-tests.sh | 42 +++++ 16 files changed, 869 insertions(+), 48 deletions(-) create mode 100644 drivers/gpu/drm/apple/epic/dpavserv-edid.h create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/extract-notify.py create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/extract-ring.py create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/extract-service.py create mode 100644 tools/testing/selftests/drivers/gpu/apple-afkv2/inline-edid.c diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 3fecf8d931db5..2d7fe02b99ab1 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -853,15 +853,23 @@ static void afk_v2_receive(struct apple_dcp_afkep *ep, u32 channel, u32 type, spin_lock_irqsave(&service->lock, flags); ret = -EPROTO; if (slot < MAX_PENDING_CMDS && test_bit(slot, service->cmd_map) && - !service->cmds[slot].done && response.id == service->cmds[slot].tag) - ret = afkv2_check_response(&frame, &response, service->channel, - service->cmds[slot].type, service->cmds[slot].tag, - service->cmds[slot].rxbuf_dma, - service->cmds[slot].rxlen, - service->cmds[slot].txbuf_dma, - service->cmds[slot].txlen); - if (!ret) - service->cmds[slot].reply_length = response.value ? 0 : response.rx_size; + !service->cmds[slot].done && response.id == service->cmds[slot].tag) { + struct epic_cmd_info *cmd = &service->cmds[slot]; + + if (ep->inline_v2) + ret = afkv2_check_inline_response(&frame, &response, service->channel, + cmd->type, cmd->tag, cmd->rxlen); + else + ret = afkv2_check_response(&frame, &response, service->channel, + cmd->type, cmd->tag, cmd->rxbuf_dma, + cmd->rxlen, cmd->txbuf_dma, cmd->txlen); + if (!ret) { + cmd->reply_length = response.value ? 0 : + (ep->inline_v2 ? response.inline_size : response.rx_size); + if (ep->inline_v2 && !response.value && cmd->reply_length) + memcpy(cmd->rxbuf, response.inline_data, cmd->reply_length); + } + } spin_unlock_irqrestore(&service->lock, flags); if (!ret) { legacy.retcode = cpu_to_le32(response.value); @@ -1219,20 +1227,29 @@ static int afk_send_epic_token(struct apple_dcp_afkep *ep, u32 channel, u16 tag, return -EOPNOTSUPP; v2_flags = 1; } else if (ecat == EPIC_CAT_COMMAND) { - const struct epic_cmd *cmd = payload; - - if (stype != EPIC_SUBTYPE_STD_SERVICE || !cmd || - payload_len != sizeof(*cmd)) - return -EINVAL; - ret = afkv2_encode_oob_command(v2_command, sizeof(v2_command), tag, - le64_to_cpu(cmd->rxbuf), - le32_to_cpu(cmd->rxlen), - le64_to_cpu(cmd->txbuf), - le32_to_cpu(cmd->txlen)); - if (ret) - return ret; - payload = v2_command; - payload_len = sizeof(v2_command); + if (ep->inline_v2) { + struct afkv2_command command; + + if (stype != EPIC_SUBTYPE_STD_SERVICE || + afkv2_decode_command(payload, payload_len, &command) || + command.oob || command.id != tag) + return -EINVAL; + } else { + const struct epic_cmd *cmd = payload; + + if (stype != EPIC_SUBTYPE_STD_SERVICE || !cmd || + payload_len != sizeof(*cmd)) + return -EINVAL; + ret = afkv2_encode_oob_command(v2_command, sizeof(v2_command), tag, + le64_to_cpu(cmd->rxbuf), + le32_to_cpu(cmd->rxlen), + le64_to_cpu(cmd->txbuf), + le32_to_cpu(cmd->txlen)); + if (ret) + return ret; + payload = v2_command; + payload_len = sizeof(v2_command); + } v2_category = AFKV2_COMMAND; } else if (ecat == EPIC_CAT_REPLY) { v2_category = AFKV2_RESPONSE; @@ -1451,12 +1468,28 @@ int afk_send_command(struct apple_epic_service *service, u8 type, MSEC_PER_SEC); } +static int afk_send_command_timeout_len(struct apple_epic_service *service, u8 type, + const void *payload, size_t payload_len, + void *output, size_t output_len, u32 *retcode, + unsigned int timeout_ms, size_t *received); + int afk_send_command_timeout(struct apple_epic_service *service, u8 type, const void *payload, size_t payload_len, void *output, size_t output_len, u32 *retcode, unsigned int timeout_ms) +{ + return afk_send_command_timeout_len(service, type, payload, payload_len, + output, output_len, retcode, timeout_ms, NULL); +} + +static int afk_send_command_timeout_len(struct apple_epic_service *service, u8 type, + const void *payload, size_t payload_len, + void *output, size_t output_len, u32 *retcode, + unsigned int timeout_ms, size_t *received) { struct epic_cmd cmd; + void *inline_command = NULL; + size_t copy_len = output_len; void *rxbuf, *txbuf; dma_addr_t rxbuf_dma, txbuf_dma; unsigned long flags; @@ -1465,6 +1498,8 @@ int afk_send_command_timeout(struct apple_epic_service *service, u8 type, struct apple_dcp_afkep *ep; DECLARE_COMPLETION_ONSTACK(completion); + if (received) + *received = 0; service = afk_service_get(service); if (!service) return -ENODEV; @@ -1494,6 +1529,17 @@ int afk_send_command_timeout(struct apple_epic_service *service, u8 type, } memcpy(txbuf, payload, payload_len); + if (ep->inline_v2) { + if (payload_len > AFKV2_MAX_PAYLOAD - AFKV2_COMMAND_SIZE) { + ret = -E2BIG; + goto err_free_txbuf; + } + inline_command = kmalloc(AFKV2_COMMAND_SIZE + payload_len, GFP_KERNEL); + if (!inline_command) { + ret = -ENOMEM; + goto err_free_txbuf; + } + } memset(&cmd, 0, sizeof(cmd)); cmd.retcode = cpu_to_le32(0); @@ -1531,9 +1577,21 @@ int afk_send_command_timeout(struct apple_epic_service *service, u8 type, service->cmds[idx].completion = &completion; init_completion(&completion); - ret = afk_send_epic(service->ep, service->channel, tag, - EPIC_TYPE_COMMAND, EPIC_CAT_COMMAND, type, &cmd, - sizeof(cmd)); + if (ep->inline_v2) { + ret = afkv2_encode_inline_command(inline_command, + AFKV2_COMMAND_SIZE + payload_len, + tag, payload, payload_len, output_len); + if (!ret) + ret = afk_send_epic(ep, service->channel, tag, EPIC_TYPE_COMMAND, + EPIC_CAT_COMMAND, type, inline_command, + AFKV2_COMMAND_SIZE + payload_len); + } else { + ret = afk_send_epic(service->ep, service->channel, tag, + EPIC_TYPE_COMMAND, EPIC_CAT_COMMAND, type, &cmd, + sizeof(cmd)); + } + kfree(inline_command); + inline_command = NULL; spin_unlock_irqrestore(&service->lock, flags); if (ret) goto err_free_cmd; @@ -1565,13 +1623,17 @@ int afk_send_command_timeout(struct apple_epic_service *service, u8 type, ret = 0; if (retcode) *retcode = service->cmds[idx].retcode; + if (ep->inline_v2 && received) + copy_len = service->cmds[idx].reply_length; if (ep->wire_v2 && !service->cmds[idx].retcode && - service->cmds[idx].reply_length < output_len) + service->cmds[idx].reply_length < copy_len) ret = -EMSGSIZE; else if (output && output_len && (!ep->wire_v2 || !service->cmds[idx].retcode)) { if (ep->wire_v2) dma_rmb(); - memcpy(output, rxbuf, output_len); + memcpy(output, rxbuf, copy_len); + if (received) + *received = copy_len; } err_free_cmd: @@ -1580,6 +1642,8 @@ int afk_send_command_timeout(struct apple_epic_service *service, u8 type, bitmap_release_region(service->cmd_map, idx, 0); err_unlock: spin_unlock_irqrestore(&service->lock, flags); +err_free_txbuf: + kfree(inline_command); dma_free_coherent(ep->dcp->dev, payload_len, txbuf, txbuf_dma); err_free_rxbuf: dma_free_coherent(ep->dcp->dev, output_len, rxbuf, rxbuf_dma); @@ -1597,12 +1661,40 @@ int afk_service_call(struct apple_epic_service *service, u16 group, u32 command, output_pad, MSEC_PER_SEC); } +static int afk_service_call_timeout_len(struct apple_epic_service *service, u16 group, + u32 command, const void *data, size_t data_len, + size_t data_pad, void *output, size_t output_len, + size_t output_pad, unsigned int timeout_ms, + size_t *received); + +int afk_service_call_len(struct apple_epic_service *service, u16 group, + u32 command, const void *data, size_t data_len, + size_t data_pad, void *output, size_t output_len, + size_t output_pad, size_t *received) +{ + return afk_service_call_timeout_len(service, group, command, data, data_len, + data_pad, output, output_len, output_pad, + MSEC_PER_SEC, received); +} + int afk_service_call_timeout(struct apple_epic_service *service, u16 group, u32 command, const void *data, size_t data_len, size_t data_pad, void *output, size_t output_len, size_t output_pad, unsigned int timeout_ms) +{ + return afk_service_call_timeout_len(service, group, command, data, data_len, + data_pad, output, output_len, output_pad, + timeout_ms, NULL); +} + +static int afk_service_call_timeout_len(struct apple_epic_service *service, u16 group, + u32 command, const void *data, size_t data_len, + size_t data_pad, void *output, size_t output_len, + size_t output_pad, unsigned int timeout_ms, + size_t *received) { struct epic_service_call *call; + size_t wire_received = 0, result_size; void *bfr; size_t bfr_len = max(data_len + data_pad, output_len + output_pad) + sizeof(*call); @@ -1610,6 +1702,8 @@ int afk_service_call_timeout(struct apple_epic_service *service, u16 group, u32 retcode; u32 retlen; + if (received) + *received = 0; if (!service) return -ENODEV; @@ -1622,14 +1716,17 @@ int afk_service_call_timeout(struct apple_epic_service *service, u16 group, memset(call, 0, sizeof(*call)); call->group = cpu_to_le16(group); call->command = cpu_to_le32(command); - call->data_len = cpu_to_le32(data_len + data_pad); + call->data_len = cpu_to_le32(service->ep->inline_v2 ? data_len : + data_len + data_pad); call->magic = cpu_to_le32(EPIC_SERVICE_CALL_MAGIC); memcpy(bfr + sizeof(*call), data, data_len); - ret = afk_send_command_timeout(service, EPIC_SUBTYPE_STD_SERVICE, bfr, - bfr_len, bfr, bfr_len, &retcode, - timeout_ms); + /* Inline carries only the request; its response capacity is independent. */ + ret = afk_send_command_timeout_len(service, EPIC_SUBTYPE_STD_SERVICE, bfr, + service->ep->inline_v2 ? sizeof(*call) + data_len : + bfr_len, bfr, bfr_len, &retcode, timeout_ms, + received ? &wire_received : NULL); if (ret) { dev_warn(service->ep->dcp->dev, "AFK[ep:%02x]: %s (chan:%u) service call group %u cmd %u failed to complete: %d\n", @@ -1646,6 +1743,13 @@ int afk_service_call_timeout(struct apple_epic_service *service, u16 group, ret = -EINVAL; goto out; } + if (received) { + ret = afkv2_service_reply_size(bfr, wire_received, group, command, + output_len, &result_size); + if (ret) + goto out; + *received = result_size; + } if (le32_to_cpu(call->magic) != EPIC_SERVICE_CALL_MAGIC || le16_to_cpu(call->group) != group || le32_to_cpu(call->command) != command) { diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index b8c889da5c356..45fe9db64d30f 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -329,6 +329,8 @@ struct apple_dcp_afkep { u32 endpoint; /* Set before afk_start; protocol selection is per endpoint. */ bool wire_v2; + /* External H17P DPAV only; other endpoints retain owned-DMA commands. */ + bool inline_v2; struct workqueue_struct *wq; struct completion started; @@ -382,4 +384,9 @@ int afk_service_call_timeout(struct apple_epic_service *service, u16 group, u32 command, const void *data, size_t data_len, size_t data_pad, void *output, size_t output_len, size_t output_pad, unsigned int timeout_ms); +int afk_service_call_len(struct apple_epic_service *service, u16 group, + u32 command, const void *data, size_t data_len, + size_t data_pad, void *output, size_t output_len, + size_t output_pad, size_t *received); + #endif diff --git a/drivers/gpu/drm/apple/afkv2-codec.h b/drivers/gpu/drm/apple/afkv2-codec.h index 8990182275354..9ef52b2e70ea2 100644 --- a/drivers/gpu/drm/apple/afkv2-codec.h +++ b/drivers/gpu/drm/apple/afkv2-codec.h @@ -179,6 +179,26 @@ static inline int afkv2_encode_oob_command(void *payload, size_t capacity, return 0; } +/* Inline requests carry a response capacity separately from their body. */ +static inline int afkv2_encode_inline_command(void *payload, size_t capacity, + u8 id, const void *body, + size_t size, u32 response_capacity) +{ + u8 *p = payload; + + if (!payload || (size && !body) || size > AFKV2_MAX_PAYLOAD - AFKV2_COMMAND_SIZE || + response_capacity > AFKV2_MAX_PAYLOAD) + return -EINVAL; + if (capacity < AFKV2_COMMAND_SIZE || size > capacity - AFKV2_COMMAND_SIZE) + return -ENOSPC; + if (size) + memmove(p + AFKV2_COMMAND_SIZE, body, size); + memset(p, 0, AFKV2_COMMAND_SIZE); + p[1] = id; + afkv2_put32(p + 4, response_capacity); + return 0; +} + static inline int afkv2_decode_command(const void *payload, size_t size, struct afkv2_command *out) { @@ -250,6 +270,38 @@ static inline int afkv2_check_response(const struct afkv2_frame *frame, return 0; } +/* Kept separate so DPAV inline replies cannot loosen owned-DMA checks. */ +static inline int afkv2_check_inline_response(const struct afkv2_frame *frame, + const struct afkv2_command *response, + u16 interface, u8 type, u8 id, + size_t capacity) +{ + if (!frame || !response || frame->category != AFKV2_RESPONSE || + frame->interface != interface || frame->type != type || response->id != id || + frame->token || frame->transport_flags || frame->interface_flags || + frame->reserved || response->oob || response->inline_size > capacity) + return -EPROTO; + return 0; +} + +/* The standard service envelope precedes the actual, possibly short reply. */ +static inline int afkv2_service_reply_size(const void *body, size_t received, + u16 group, u32 command, size_t capacity, + size_t *size) +{ + const u8 *p = body; + u32 length; + + if (!body || !size || received < 64 || afkv2_get32(p + 12) != 0x69706378 || + afkv2_get16(p + 2) != group || afkv2_get32(p + 4) != command) + return -EPROTO; + length = afkv2_get32(p + 8); + if (length > received - 64 || length > capacity) + return -EMSGSIZE; + *size = length; + return 0; +} + /* Reject a peer command without touching its advertised DMA buffers. */ static inline int afkv2_encode_error_reply(void *record, size_t capacity, u8 sequence, const struct afkv2_frame *request, diff --git a/drivers/gpu/drm/apple/epic/dpavserv-edid.h b/drivers/gpu/drm/apple/epic/dpavserv-edid.h new file mode 100644 index 0000000000000..31baaaea7070d --- /dev/null +++ b/drivers/gpu/drm/apple/epic/dpavserv-edid.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DPAVSERV_EDID_H +#define APPLE_DPAVSERV_EDID_H + +#include "../afkv2-codec.h" + +#define DPAV_EDID_HEADER_SIZE 48U +#define EDID_LEADING_DATA_SIZE 8U +#define EDID_BLOCK_SIZE 128U +#define EDID_MAX_SIZE 32768U +#define EDID_BUF_SIZE (EDID_LEADING_DATA_SIZE + EDID_MAX_SIZE) + +/* Validate only bytes actually returned by firmware, never zero-fill padding. */ +static inline int dpavserv_edid_size(const void *response, size_t received, + size_t *edid_size) +{ + static const u8 header[] = { 0, 255, 255, 255, 255, 255, 255, 0 }; + const u8 *p = response; + u64 used; + size_t length, offset, i; + + if (!response || !edid_size || received < DPAV_EDID_HEADER_SIZE) + return -EIO; + if (afkv2_get64(p) != EDID_BUF_SIZE) + return -EIO; + used = afkv2_get64(p + 32); + if (used < EDID_LEADING_DATA_SIZE + EDID_BLOCK_SIZE || used > EDID_BUF_SIZE || + used > received - DPAV_EDID_HEADER_SIZE) + return -EIO; + length = used - EDID_LEADING_DATA_SIZE; + p += DPAV_EDID_HEADER_SIZE + EDID_LEADING_DATA_SIZE; + if (length % EDID_BLOCK_SIZE || memcmp(p, header, sizeof(header)) || + ((size_t)p[126] + 1) * EDID_BLOCK_SIZE > length) + return -EIO; + /* Permit extra whole blocks for HF-EEODB, as drm_edid supports it. */ + for (offset = 0; offset < length; offset += EDID_BLOCK_SIZE) { + u8 sum = 0; + + for (i = 0; i < EDID_BLOCK_SIZE; i++) + sum += p[offset + i]; + if (sum) + return -EIO; + } + *edid_size = length; + return 0; +} + +#endif diff --git a/drivers/gpu/drm/apple/epic/dpavservep.c b/drivers/gpu/drm/apple/epic/dpavservep.c index 4ddce69860379..1b4019cc159f9 100644 --- a/drivers/gpu/drm/apple/epic/dpavservep.c +++ b/drivers/gpu/drm/apple/epic/dpavservep.c @@ -2,6 +2,9 @@ /* Copyright The Asahi Linux Contributors */ #include "dpavservep.h" +#include "dpavserv-edid.h" + +#define EDID_EXT_BLOCK_COUNT_OFFSET 0x7E #include @@ -12,8 +15,23 @@ #include "../afk.h" #include "../dcp.h" #include "../dcp-internal.h" +#include "../connector.h" #include "../trace.h" +static void dcpavserv_notify_edid(struct apple_epic_service *service) +{ + struct apple_dcp *dcp = service->ep->dcp; + struct apple_connector *connector = READ_ONCE(dcp->connector); + + if (!service->ep->inline_v2 || READ_ONCE(dcp->quiescing) || !connector || + !READ_ONCE(dcp->nr_modes) || !READ_ONCE(connector->connected) || + READ_ONCE(connector->base.registration_state) != DRM_CONNECTOR_REGISTERED || + READ_ONCE(connector->dcp) != to_platform_device(dcp->dev)) + return; + /* get_modes may already have run before this service was announced. */ + schedule_work(&connector->hotplug_wq); +} + static void dcpavserv_init(struct apple_epic_service *service, const char *name, const char *class, s64 unit) { @@ -35,6 +53,11 @@ static void dcpavserv_init(struct apple_epic_service *service, const char *name, service->cookie = &dcp->dcpavserv; complete(&dcp->dcpavserv.enable_completion); spin_unlock_irqrestore(&dcp->dcpavserv.lock, flags); + if (service->ep->inline_v2) { + dev_info(dcp->dev, "DCPAVSERV: inline EDID service ready on channel %u\n", + service->channel); + dcpavserv_notify_edid(service); + } } } @@ -100,12 +123,6 @@ struct dpavserv_copy_edid_cmd { u8 _pad2[8]; } __packed; -#define EDID_LEADING_DATA_SIZE 8 -#define EDID_BLOCK_SIZE 128 -#define EDID_EXT_BLOCK_COUNT_OFFSET 0x7E -#define EDID_MAX_SIZE SZ_32K -#define EDID_BUF_SIZE (EDID_LEADING_DATA_SIZE + EDID_MAX_SIZE) - struct dpavserv_copy_edid_resp { __le64 max_size; u8 _pad1[24]; @@ -182,7 +199,7 @@ static int dcpdpserv_report(struct apple_epic_service *service, return parse_report(service, type, data, data_size); } -static const struct drm_edid *dcpavserv_read_edid(struct apple_epic_service *service) +static const struct drm_edid *dcpavserv_read_edid_legacy(struct apple_epic_service *service) { struct dpavserv_copy_edid_cmd cmd; struct dpavserv_copy_edid_resp *resp __free(kfree) = NULL; @@ -229,6 +246,32 @@ static const struct drm_edid *dcpavserv_read_edid(struct apple_epic_service *ser data_size - EDID_LEADING_DATA_SIZE); } +static const struct drm_edid *dcpavserv_read_edid_inline(struct apple_epic_service *service) +{ + struct dpavserv_copy_edid_cmd cmd; + struct dpavserv_copy_edid_resp *resp __free(kfree) = NULL; + size_t resp_size = sizeof(*resp) + EDID_BUF_SIZE; + size_t received, edid_size; + int ret; + + memset(&cmd, 0, sizeof(cmd)); + cmd.max_size = cpu_to_le64(EDID_BUF_SIZE); + resp = kzalloc(resp_size, GFP_KERNEL); + if (!resp) + return ERR_PTR(-ENOMEM); + + ret = afk_service_call_len(service, 1, 7, &cmd, sizeof(cmd), EDID_BUF_SIZE, + resp, resp_size, 0, &received); + if (ret < 0) + return ERR_PTR(ret); + + ret = dpavserv_edid_size(resp, received, &edid_size); + if (ret) + return ERR_PTR(ret); + + return drm_edid_alloc(resp->data + EDID_LEADING_DATA_SIZE, edid_size); +} + const struct drm_edid *dcpavserv_copy_edid(struct apple_dcp *dcp) { struct apple_epic_service *service; @@ -236,12 +279,13 @@ const struct drm_edid *dcpavserv_copy_edid(struct apple_dcp *dcp) unsigned long flags; spin_lock_irqsave(&dcp->dcpavserv.lock, flags); - service = afk_service_get(dcp->dcpavserv.service); + service = dcp->dcpavserv.enabled ? afk_service_get(dcp->dcpavserv.service) : NULL; spin_unlock_irqrestore(&dcp->dcpavserv.lock, flags); if (!service) return ERR_PTR(-ENODEV); - edid = dcpavserv_read_edid(service); + edid = service->ep->inline_v2 ? dcpavserv_read_edid_inline(service) : + dcpavserv_read_edid_legacy(service); spin_lock_irqsave(&dcp->dcpavserv.lock, flags); if (dcp->dcpavserv.service != service) { if (!IS_ERR(edid)) @@ -281,6 +325,10 @@ int dpavservep_init(struct apple_dcp *dcp) ep = afk_init(dcp, DPAVSERV_ENDPOINT, dpavservep_ops); if (IS_ERR(ep)) return PTR_ERR(ep); + /* Only the external H17P DPAV endpoint uses inline AFKV2 service calls. */ + ep->wire_v2 = dcp->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; + ep->inline_v2 = ep->wire_v2; dcp->dcpavservep = ep; dcp->dcpavservep->match_epic_name = true; @@ -295,8 +343,10 @@ int dpavservep_init(struct apple_dcp *dcp) ret = wait_for_completion_timeout(&dcp->dcpavserv.enable_completion, msecs_to_jiffies(1000)); - if (ret >= 0) - return 0; + if (!ret) { + dev_warn(dcp->dev, "DCPAVSERV: timed out waiting for EDID service announcement\n"); + return -ETIMEDOUT; + } - return ret; + return 0; } diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 786eb53383329..50b240511442b 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1017,9 +1017,10 @@ int dcp_get_modes(struct drm_connector *connector) } drm_connector_set_vrr_capable_property(connector, vrr_capable); - /* H17P sends no EPIC commands; see afk_send_epic(). */ - if (dcp->nr_modes && dcp->fw_compat != DCP_FIRMWARE_H17P && - !apple_connector->drm_edid) { + /* Internal H17P still has no supported EDID command transport. */ + if (dcp->nr_modes && !apple_connector->drm_edid && + (dcp->fw_compat != DCP_FIRMWARE_H17P || + (dcp->dcpavservep && dcp->dcpavservep->inline_v2))) { const struct drm_edid *edid; edid = dcpavserv_copy_edid(dcp); if (IS_ERR_OR_NULL(edid)) { diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/Makefile b/tools/testing/selftests/drivers/gpu/apple-afkv2/Makefile index 65780f3d5003f..df1b033d9cb32 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/Makefile +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/Makefile @@ -4,6 +4,6 @@ export HOSTCC export AFKV2_SOURCE_ROOT := $(abspath ../../../../../..) TEST_PROGS := run-afkv2-tests.sh -TEST_FILES := codec.c owned-prefix.c owned-suffix.c extract-owned.py host-include +TEST_FILES := codec.c owned-prefix.c owned-suffix.c extract-owned.py inline-edid.c extract-service.py extract-send.py extract-edid.py extract-ring.py extract-notify.py host-include include ../../../lib.mk diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/README b/tools/testing/selftests/drivers/gpu/apple-afkv2/README index 42eb152d716a2..064cdcf018595 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/README +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/README @@ -22,3 +22,19 @@ DMA coherency, firmware, a graphics device or any hardware display behavior. All build products are temporary. No device node is opened and no GPU, firmware or target machine is contacted. The extractor fails if production function boundaries change, requiring review rather than silently testing a stale copy. + +The inline EDID tests cover response context, status, declared/actual lengths, +EDID header/checksum/block limits, and continued rejection of inline responses +by the OOB matcher. The actual service-call body is extracted for short reply, +error status and timeout checks; its wire sender is mocked. + +The real sender is also extracted to test a 128-byte mocked command payload +with a 32 KiB response capacity, OOB behavior, send failures and timeout +retention. The two production EDID readers are tested independently to +prove legacy checksum tolerance remains unchanged. + +The complete encoded EDID request is 160 bytes including the queue entry, +consumes 192 bytes at a 64-byte stride, and needs a 256-byte empty ring. The +ring-admission block is extracted from the real transmitter to check this. +The late-publication notifier is extracted to test asynchronous hotplug +queueing and connector-registration, quiesce, routing and legacy guards. diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py new file mode 100644 index 0000000000000..0ab0f5eda8cac --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-edid.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Verify legacy EDID tolerance and stricter external-inline parsing separately.""" +import pathlib +import re +import sys + +source = pathlib.Path(sys.argv[1]).read_text() + +def function(name): + match = re.search(r"static const struct drm_edid \*" + name + r"\([^;]*?\)\n\{", source) + if not match: + raise RuntimeError("production reader missing: " + name) + end, depth = match.end(), 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[match.start():end] + +assert "service->ep->inline_v2 ? dcpavserv_read_edid_inline(service)" in source +prefix = r''' +#undef NDEBUG +#include +#include +#include +#include +#include "epic/dpavserv-edid.h" +#define GFP_KERNEL 0 +#define kzalloc(n,f) calloc(1,n) +#define cpu_to_le64(x) (x) +#define le64_to_cpu(x) (x) +#define EDID_EXT_BLOCK_COUNT_OFFSET 0x7e +#define ERR_PTR(x) ((void*)(intptr_t)(x)) +#define IS_ERR(p) ((uintptr_t)(p)>(uintptr_t)-4096) +static void cleanup_free(void *p) { free(*(void**)p); } +#define __free(x) __attribute__((cleanup(cleanup_free))) +struct apple_epic_service { int unused; }; +struct drm_edid { size_t size; }; +struct dpavserv_copy_edid_cmd { u64 max_size;u8 pad1[24];u64 used_size;u8 pad2[8]; }; +struct dpavserv_copy_edid_resp { u64 max_size;u8 pad1[24];u64 used_size;u8 pad2[8];u8 data[]; }; +static u8 fixture[48+8+256]; +static bool truncated; +static const struct drm_edid *drm_edid_alloc(const void *p,size_t n) +{ struct drm_edid *e=malloc(sizeof(*e));(void)p;e->size=n;return e; } +static int afk_service_call(struct apple_epic_service *s,u16 group,u32 command, + const void *data,size_t data_len,size_t data_pad,void *output,size_t output_len,size_t output_pad) +{ + const struct dpavserv_copy_edid_cmd *cmd=data; + (void)s;(void)data_pad;(void)output_pad; + assert(group==1 && command==7 && data_len==48 && cmd->max_size==EDID_BUF_SIZE); + assert(output_len>=sizeof(fixture));memcpy(output,fixture,sizeof(fixture));return 0; +} +static int afk_service_call_len(struct apple_epic_service *s,u16 group,u32 command, + const void *data,size_t data_len,size_t data_pad,void *output,size_t output_len, + size_t output_pad,size_t *received) +{ int ret=afk_service_call(s,group,command,data,data_len,data_pad,output,output_len,output_pad); + *received=sizeof(fixture)-(truncated?1:0);return ret; } +static void fix_checksum(u8 *p) +{ unsigned int i;u8 sum=0;p[127]=0;for(i=0;i<127;i++)sum+=p[i];p[127]=-sum; } +''' +suffix = r''' +int main(void) +{ + struct apple_epic_service service={0};const struct drm_edid *e; + u8 *base=fixture+56,*extension=base+128; + afkv2_put64(fixture,EDID_BUF_SIZE);afkv2_put64(fixture+32,8+256); + memset(base+1,255,6);base[126]=1;fix_checksum(base); + extension[0]=2; /* deliberately invalid extension checksum */ + e=dcpavserv_read_edid_legacy(&service);assert(!IS_ERR(e) && e->size==256);free((void*)e); + e=dcpavserv_read_edid_inline(&service);assert(IS_ERR(e)); + fix_checksum(extension); + e=dcpavserv_read_edid_inline(&service);assert(!IS_ERR(e) && e->size==256);free((void*)e); + truncated=true; + e=dcpavserv_read_edid_inline(&service);assert(IS_ERR(e)); + e=dcpavserv_read_edid_legacy(&service);assert(!IS_ERR(e));free((void*)e); + puts("PASS: actual legacy EDID checksum tolerance preserved; inline checksum/actual-length validation enforced"); +} +''' +pathlib.Path(sys.argv[2]).write_text(prefix + function("dcpavserv_read_edid_legacy") + function("dcpavserv_read_edid_inline") + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-notify.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-notify.py new file mode 100644 index 0000000000000..26579f8399cb1 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-notify.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise actual late-EDID publication notification guards with mock work.""" +import pathlib +import re +import sys +source = pathlib.Path(sys.argv[1]).read_text() +match = re.search(r"static void dcpavserv_notify_edid\([^;]*?\)\n\{", source) +if not match: + raise RuntimeError("EDID notification helper missing") +end, depth = match.end(), 1 +while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 +prefix = r''' +#undef NDEBUG +#include +#include +#include +#include +#define READ_ONCE(x) (x) +#define DRM_CONNECTOR_REGISTERED 1 +struct base { int registration_state; }; +struct apple_connector { bool connected;struct base base;void *dcp;int hotplug_wq; }; +struct apple_dcp { struct apple_connector *connector;bool quiescing;unsigned int nr_modes;void *dev; }; +struct ep { struct apple_dcp *dcp;bool inline_v2; }; +struct apple_epic_service { struct ep *ep; }; +static unsigned int queued; +static void *to_platform_device(void *p) { return p; } +static void schedule_work(int *w) { (*w)++;queued++; } +''' +suffix = r''' +int main(void) +{ + int device,other;struct apple_dcp dcp={.dev=&device}; + struct ep ep={.dcp=&dcp,.inline_v2=true};struct apple_epic_service service={.ep=&ep}; + struct apple_connector connector={.dcp=&device}; + dcpavserv_notify_edid(&service);assert(queued==0); + dcp.connector=&connector;dcpavserv_notify_edid(&service);assert(queued==0); + dcp.nr_modes=1;connector.connected=true;dcpavserv_notify_edid(&service);assert(queued==0); + connector.base.registration_state=DRM_CONNECTOR_REGISTERED; + dcpavserv_notify_edid(&service);assert(queued==1 && connector.hotplug_wq==1); + dcp.quiescing=true;dcpavserv_notify_edid(&service);assert(queued==1); + dcp.quiescing=false;connector.dcp=&other;dcpavserv_notify_edid(&service);assert(queued==1); + connector.dcp=&device;ep.inline_v2=false;dcpavserv_notify_edid(&service);assert(queued==1); + ep.inline_v2=true;connector.connected=false;dcpavserv_notify_edid(&service);assert(queued==1); + puts("PASS: actual late-EDID notification queues ready connector only; legacy/quiesce/unrouted/unregistered guarded"); +} +''' +pathlib.Path(sys.argv[2]).write_text(prefix + source[match.start():end] + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-ring.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-ring.py new file mode 100644 index 0000000000000..65d6fdeca7a54 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-ring.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Check full AFKV2 frame size against the production ring-admission block.""" +import pathlib +import sys + +source = pathlib.Path(sys.argv[1]).read_text() +start = source.index("\ttotal_epic_size = sizeof(*ehdr) +", source.index("static int afk_send_epic_token", source.index("int afk_receive_message"))) +end = source.index("\n\thdr = hdr2 = NULL;", start) +block = source[start:end] +prefix = r''' +#undef NDEBUG +#include +#include +#include "afkv2-codec.h" +#define IS_ALIGNED(x,a) (!((x)&((a)-1))) +#define ALIGN(x,a) (((x)+(a)-1)&~((a)-1)) +struct epic_hdr { u8 data[16]; }; +struct epic_sub_hdr { u8 data[24]; }; +struct epic_sub_hdr_h17p { u8 data[8]; }; +struct afk_qe { u8 data[16]; }; +struct ring { bool ready; u32 stride,bufsz; }; +struct ep { bool wire_v2; struct ring txbfr; }; +static int check_ring(struct ep *ep,u32 rptr,u32 wptr,size_t payload_len,size_t *total) +{ + struct epic_hdr *ehdr;struct epic_sub_hdr *eshdr;struct afk_qe *hdr; + bool compact=false;size_t total_epic_size,total_size;int ret=0; +''' +suffix = r''' + *total=total_size; +out: + return ret; +} +int main(void) +{ + u8 request[112]={0},command[120],frame[144];size_t written,total; + struct ep ep={.wire_v2=true,.txbfr={.ready=true,.stride=64,.bufsz=256}}; + assert(!afkv2_encode_inline_command(command,sizeof(command),1,request,sizeof(request),32888)); + assert(!afkv2_encode_frame(frame,sizeof(frame),0,1,0,0xc0,AFKV2_COMMAND,0,command,sizeof(command),&written)); + assert(written==144 && written+sizeof(struct afk_qe)==160); + assert(!check_ring(&ep,0,0,sizeof(command),&total)); + assert(total==160 && ALIGN(total,64)==192); + ep.txbfr.bufsz=192;assert(check_ring(&ep,0,0,sizeof(command),&total)==-ENOSPC); + ep.txbfr.bufsz=128;assert(check_ring(&ep,0,0,sizeof(command),&total)==-EPROTO); + ep.txbfr.bufsz=256;assert(check_ring(&ep,192,0,sizeof(command),&total)==-ENOSPC); + assert(check_ring(&ep,0,1,sizeof(command),&total)==-EPROTO); + puts("PASS: full request 160 bytes / 192-byte aligned ring consumption; 256-byte empty ring accepted, smaller capacity rejected"); +} +''' +pathlib.Path(sys.argv[2]).write_text(prefix + block + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py new file mode 100644 index 0000000000000..92ca8857cc104 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-send.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile the real command sender against synthetic ring and DMA operations.""" +import pathlib +import re +import sys + +source = pathlib.Path(sys.argv[1]).read_text() +match = re.search(r"static int afk_send_command_timeout_len\([^;]*?\)\n\{", source) +if not match: + raise RuntimeError("production command sender not found") +start, end, depth = match.start(), match.end(), 1 +while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 +prefix = r''' +#undef NDEBUG +#include +#include +#include +#include +#include "afkv2-codec.h" +#define SZ_1M 1048576U +#define GFP_KERNEL 0 +#define MAX_PENDING_CMDS 16 +#define cpu_to_le32(x) (x) +#define cpu_to_le64(x) (x) +#define kmalloc(n,f) malloc(n) +#define kfree(p) free(p) +#define READ_ONCE(x) (x) +#define spin_lock_irqsave(l,f) ((void)(l),(f)=0) +#define spin_unlock_irqrestore(l,f) ((void)(l),(void)(f)) +#define dma_rmb() do {} while(0) +#define dev_warn(...) do {} while(0) +#define EPIC_TYPE_COMMAND 3 +#define EPIC_CAT_COMMAND 0x30 +#define DECLARE_COMPLETION_ONSTACK(n) int n=0 +#define init_completion(p) (*(p)=0) +#define msecs_to_jiffies(x) (x) +#define wait_for_completion_timeout(p,t) ((void)(t),*(p)) +typedef u64 dma_addr_t; +struct epic_cmd { u32 retcode; u64 rxbuf,txbuf; u32 rxlen,txlen; u8 rxcookie,txcookie; }; +struct cmd_info { + u16 tag; u8 type; size_t reply_length; void *rxbuf,*txbuf; + dma_addr_t rxbuf_dma,txbuf_dma; size_t rxlen,txlen; u32 retcode; + bool done,free_on_ack; int *completion; +}; +struct dcp { void *dev; bool quiescing; }; +struct apple_dcp_afkep { struct dcp *dcp; bool wire_v2,inline_v2; int endpoint; }; +struct ops { const char *name; }; +struct apple_epic_service { + struct apple_dcp_afkep *ep; struct ops *ops; unsigned int channel; unsigned long cmd_map[1]; + int lock; bool enabled,torndown; u8 cmd_tag; struct cmd_info cmds[16]; +}; +static struct apple_epic_service *active; +static unsigned int frees, sends; +static int wire_fail, timeout, reply_status; +static struct apple_epic_service *afk_service_get(struct apple_epic_service *s) { return s->enabled?s:NULL; } +static void afk_service_put(struct apple_epic_service *s) { (void)s; } +static void *dma_alloc_coherent(void *dev,size_t n,dma_addr_t *address,int flags) +{ void *p=calloc(1,n);(void)dev;(void)flags;*address=(uintptr_t)p;return p; } +static void dma_free_coherent(void *dev,size_t n,void *p,dma_addr_t a) +{ (void)dev;(void)n;assert((uintptr_t)p==a);free(p);frees++; } +static int bitmap_find_free_region(unsigned long *map,unsigned int n,int order) +{ (void)n;(void)order;assert(!*map);*map=1;return 0; } +static void bitmap_release_region(unsigned long *map,int i,int order) +{ (void)i;(void)order;*map=0; } +static int afk_send_epic(struct apple_dcp_afkep *ep,u32 channel,u16 tag,int type,int category,u8 subtype,const void *payload,size_t length) +{ + struct cmd_info *c=&active->cmds[0];struct afkv2_command parsed; + assert(channel==4 && type==3 && category==0x30 && subtype==0xc0); + sends++; + /* The payload-envelope mock bounds the command separately from its response. */ + assert(length<=128); + if(ep->inline_v2) { + assert(length==120); + assert(!afkv2_decode_command(payload,length,&parsed)); + assert(!parsed.oob && parsed.id==tag && parsed.value==32888 && parsed.inline_size==112); + assert(parsed.inline_data[0]==0x5a); + } else { + const struct epic_cmd *legacy=payload; + assert(length==sizeof(*legacy) && legacy->txlen==112 && legacy->rxlen==32888); + } + if(wire_fail)return -ENOSPC; + if(timeout)return 0; + c->reply_length=ep->inline_v2?4:32888;c->retcode=reply_status; + memset(c->rxbuf,0x77,c->reply_length);c->done=true;*c->completion=1; + return 0; +} +''' +suffix = r''' +int main(void) +{ + struct dcp dcp={0};struct apple_dcp_afkep ep={.dcp=&dcp,.wire_v2=true,.inline_v2=true}; + struct ops ops={.name="test"};struct apple_epic_service s={.ep=&ep,.ops=&ops,.channel=4,.enabled=true}; + u8 request[112]={0x5a},*output=calloc(1,32888);size_t received;u32 status=99; + active=&s; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(received==4 && status==0 && output[0]==0x77 && frees==2 && sends==1 && !s.cmd_map[0]); + /* Existing strict callers still reject a short inline result. */ + assert(afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,NULL)==-EMSGSIZE); + reply_status=5;received=99; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(status==5 && received==0);reply_status=0; + wire_fail=1; + assert(afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)==-ENOSPC && !s.cmd_map[0]); + wire_fail=0;ep.inline_v2=false; + assert(!afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)); + assert(received==32888 && status==0); + ep.inline_v2=true;timeout=1; + assert(afk_send_command_timeout_len(&s,0xc0,request,112,output,32888,&status,1000,&received)==-ETIMEDOUT); + assert(s.cmd_map[0] && s.cmds[0].free_on_ack && !s.cmds[0].completion); + /* Mock the late-reply owner's release, avoiding leaks in this host test. */ + free(s.cmds[0].rxbuf);free(s.cmds[0].txbuf);free(output); + puts("PASS: extracted production sender, 128-byte command envelope / 32888-byte reply, strict OOB and timeout ownership"); +} +''' +pathlib.Path(sys.argv[2]).write_text(prefix + source[start:end] + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-service.py b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-service.py new file mode 100644 index 0000000000000..68e3f5e6ba980 --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/extract-service.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise the production service-call body with a mocked wire reply.""" +import pathlib +import re +import sys + +source = pathlib.Path(sys.argv[1]).read_text() +match = re.search(r"static int afk_service_call_timeout_len\([^;]*?\)\n\{", source) +if not match: + raise RuntimeError("production service-call definition not found") +start = match.start() +end = match.end() +depth = 1 +while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 +prefix = r''' +#undef NDEBUG +#include +#include +#include +#include "afkv2-codec.h" +#define GFP_KERNEL 0 +#define kzalloc(n,f) calloc(1,n) +#define kfree(p) free(p) +#define max(a,b) ((a)>(b)?(a):(b)) +#define cpu_to_le16(x) (x) +#define cpu_to_le32(x) (x) +#define le16_to_cpu(x) (x) +#define le32_to_cpu(x) (x) +#define EPIC_SERVICE_CALL_MAGIC 0x69706378 +#define EPIC_SUBTYPE_STD_SERVICE 0xc0 +#define dev_warn(...) do {} while(0) +#define dev_dbg(...) do {} while(0) +struct epic_service_call { + u8 pad0[2]; u16 group; u32 command, data_len, magic; u8 pad1[48]; +}; +struct dcp { void *dev; }; +struct ops { const char *name; }; +struct ep { struct dcp *dcp; int endpoint; bool inline_v2; }; +struct apple_epic_service { struct ep *ep; struct ops *ops; u32 channel; }; +static int error_status, malformed, short_header, too_long, wire_error; +static int afk_send_command_timeout_len(struct apple_epic_service *s, u8 type, + const void *payload, size_t payload_len, void *output, size_t output_len, + u32 *retcode, unsigned int timeout_ms, size_t *received) +{ + struct epic_service_call *call=output; + (void)payload; + assert(payload_len==(s->ep->inline_v2?68:output_len)); + assert(call->data_len==(s->ep->inline_v2?4:4100)); + assert(type==0xc0 && timeout_ms==1000 && output_len>=68); + if(wire_error) return -ETIMEDOUT; + call->group=1;call->command=7;call->magic=EPIC_SERVICE_CALL_MAGIC; + call->data_len=too_long?5:4; + ((u8*)output)[64]=0xde;((u8*)output)[65]=0xad; + ((u8*)output)[66]=0xbe;((u8*)output)[67]=0xef; + if(malformed)call->command=8; + *retcode=error_status; + if(received)*received=short_header?63:68; + return 0; +} +''' +suffix = r''' +int main(void) +{ + struct ep ep={.inline_v2=true};struct apple_epic_service service={.ep=&ep}; + u8 input[4]={0}, output[128]={0}; size_t received=999; + assert(!afk_service_call_timeout_len(&service,1,7,input,4,4096,output,128,0,1000,&received)); + assert(received==4 && output[0]==0xde && output[3]==0xef && output[4]==0); + error_status=5;received=999; + assert(afk_service_call_timeout_len(&service,1,7,input,4,4096,output,128,0,1000,&received)==-EINVAL && received==0); + error_status=0;malformed=1; + assert(afk_service_call_timeout_len(&service,1,7,input,4,4096,output,128,0,1000,&received)); + malformed=0;short_header=1; + assert(afk_service_call_timeout_len(&service,1,7,input,4,4096,output,128,0,1000,&received)); + short_header=0;too_long=1; + assert(afk_service_call_timeout_len(&service,1,7,input,4,4096,output,128,0,1000,&received)); + too_long=0;wire_error=1; + assert(afk_service_call_timeout_len(&service,1,7,input,4,4096,output,128,0,1000,&received)==-ETIMEDOUT); + wire_error=0; ep.inline_v2=false; + assert(!afk_service_call_timeout_len(&service,1,7,input,4,4096,output,128,0,1000,NULL)); + puts("PASS: extracted production service-call actual request / legacy padding / short reply / status / context / length tests"); +} +''' +pathlib.Path(sys.argv[2]).write_text(prefix + source[start:end] + suffix) diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h index a5c3a3f44a591..23e11801f1e76 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/host-include/linux/errno.h @@ -3,3 +3,8 @@ #define ENOSPC 28 #define E2BIG 7 #define EPROTO 71 +#define EIO 5 +#define EMSGSIZE 90 +#define ENODEV 19 +#define ETIMEDOUT 110 +#define ENOMEM 12 diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/inline-edid.c b/tools/testing/selftests/drivers/gpu/apple-afkv2/inline-edid.c new file mode 100644 index 0000000000000..7ce16fd2b6a8b --- /dev/null +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/inline-edid.c @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#undef NDEBUG +#include +#include +#include "epic/dpavserv-edid.h" + +static unsigned int assertions; +#define CHECK(x) do { assertions++; assert(x); } while (0) + +static void checksum(u8 *block) +{ + u8 sum = 0; + unsigned int i; + + block[127] = 0; + for (i = 0; i < 127; i++) + sum += block[i]; + block[127] = -sum; +} + +int main(void) +{ + u8 command[192], packet[216], body[136] = {0}; + u8 edid[DPAV_EDID_HEADER_SIZE + 8 + 3 * 128] = {0}; + struct afkv2_frame frame; + struct afkv2_command reply; + size_t written, length = 0; + u8 *base = edid + DPAV_EDID_HEADER_SIZE + 8; + + CHECK(!afkv2_encode_inline_command(command, sizeof(command), 0x91, + body, sizeof(body), 400)); + CHECK(command[0] == 0 && command[1] == 0x91); + CHECK(afkv2_get32(command + 4) == 400); + CHECK(!afkv2_encode_frame(packet, sizeof(packet), 5, 4, 0, 0xc0, + AFKV2_RESPONSE, 0, command, 8 + sizeof(body), &written)); + /* Response uses status in the request-capacity field. */ + afkv2_put32(packet + AFKV2_HEADER_SIZE + 4, 0); + CHECK(!afkv2_decode_frame(packet, written, false, &frame)); + CHECK(!afkv2_decode_command(frame.payload, frame.payload_size, &reply)); + CHECK(!afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 135)); + CHECK(afkv2_check_inline_response(&frame, &reply, 5, 0xc0, 0x91, 136)); + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x92, 136)); + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc1, 0x91, 136)); + frame.category = AFKV2_COMMAND; + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); + frame.category = AFKV2_RESPONSE; + frame.token = 1; + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); + frame.token = 0; + frame.reserved = 1; + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); + frame.reserved = 0; + reply.oob = true; + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); + reply.oob = false; + /* The original OOB matcher must continue rejecting inline replies. */ + CHECK(afkv2_check_response(&frame, &reply, 4, 0xc0, 0x91, 0, 136, 0, 0)); + reply.value = 7; + CHECK(!afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); + CHECK(reply.value != 0); /* Error status is preserved for the caller. */ + CHECK(afkv2_encode_inline_command(command, 7, 1, body, 1, 1)); + CHECK(afkv2_encode_inline_command(command, sizeof(command), 1, NULL, 1, 1)); + CHECK(afkv2_encode_inline_command(command, sizeof(command), 1, body, 1, + AFKV2_MAX_PAYLOAD + 1)); + + afkv2_put16(body + 2, 1); + afkv2_put32(body + 4, 7); + afkv2_put32(body + 8, 72); + afkv2_put32(body + 12, 0x69706378); + CHECK(!afkv2_service_reply_size(body, 136, 1, 7, 72, &length) && length == 72); + CHECK(afkv2_service_reply_size(body, 135, 1, 7, 72, &length)); + CHECK(afkv2_service_reply_size(body, 136, 1, 7, 71, &length)); + CHECK(afkv2_service_reply_size(body, 63, 1, 7, 72, &length)); + CHECK(afkv2_service_reply_size(body, 136, 2, 7, 72, &length)); + CHECK(afkv2_service_reply_size(body, 136, 1, 8, 72, &length)); + body[12] ^= 1; + CHECK(afkv2_service_reply_size(body, 136, 1, 7, 72, &length)); + + afkv2_put64(edid, EDID_BUF_SIZE); + afkv2_put64(edid + 32, 8 + 128); + memset(base + 1, 255, 6); + base[18] = 1; + base[19] = 4; + checksum(base); + CHECK(!dpavserv_edid_size(edid, 48 + 8 + 128, &length) && length == 128); + CHECK(dpavserv_edid_size(edid, 47, &length)); + CHECK(dpavserv_edid_size(edid, 48 + 8 + 127, &length)); + base[0] = 1; + CHECK(dpavserv_edid_size(edid, sizeof(edid), &length)); + base[0] = 0; + base[24] ^= 1; + CHECK(dpavserv_edid_size(edid, sizeof(edid), &length)); + base[24] ^= 1; + base[126] = 1; + checksum(base); + CHECK(dpavserv_edid_size(edid, sizeof(edid), &length)); + afkv2_put64(edid + 32, 8 + 256); + CHECK(!dpavserv_edid_size(edid, 48 + 8 + 256, &length) && length == 256); + /* HF-EEODB can supply more complete blocks than base[126] declares. */ + base[126] = 0; + checksum(base); + CHECK(!dpavserv_edid_size(edid, sizeof(edid), &length) && length == 256); + afkv2_put64(edid + 32, 8 + 255); + CHECK(dpavserv_edid_size(edid, sizeof(edid), &length)); + afkv2_put64(edid + 32, EDID_BUF_SIZE + 1); + CHECK(dpavserv_edid_size(edid, sizeof(edid), &length)); + afkv2_put64(edid + 32, 8 + 128); + afkv2_put64(edid, EDID_BUF_SIZE - 1); + CHECK(dpavserv_edid_size(edid, sizeof(edid), &length)); + printf("PASS: inline AFKV2 and EDID validation (%u checks)\n", assertions); + return 0; +} diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh b/tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh index 4e13f6d05d986..4348eebc78246 100755 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/run-afkv2-tests.sh @@ -38,3 +38,45 @@ $afkv2_cc -std=gnu11 -Wall -Wextra -Werror ${CFLAGS:-} $afkv2_sanitizers \ -I"$afkv2_dir/host-include" -I"$afkv2_root/drivers/gpu/drm/apple" \ "$afkv2_tmp/owned.c" -o "$afkv2_tmp/owned" "$afkv2_tmp/owned" + +# Inline DPAV transport and actual-length EDID response validation. +# shellcheck disable=SC2086 +$afkv2_cc -std=c11 -Wall -Wextra -Werror -pedantic ${CFLAGS:-} $afkv2_sanitizers \ + -I"$afkv2_dir/host-include" -I"$afkv2_root/drivers/gpu/drm/apple" \ + "$afkv2_dir/inline-edid.c" -o "$afkv2_tmp/inline-edid" +"$afkv2_tmp/inline-edid" +"$afkv2_python" "$afkv2_dir/extract-service.py" \ + "$afkv2_root/drivers/gpu/drm/apple/afk.c" "$afkv2_tmp/service.c" +# shellcheck disable=SC2086 +$afkv2_cc -std=gnu11 -Wall -Wextra -Werror ${CFLAGS:-} $afkv2_sanitizers \ + -I"$afkv2_dir/host-include" -I"$afkv2_root/drivers/gpu/drm/apple" \ + "$afkv2_tmp/service.c" -o "$afkv2_tmp/service" +"$afkv2_tmp/service" + +"$afkv2_python" "$afkv2_dir/extract-send.py" \ + "$afkv2_root/drivers/gpu/drm/apple/afk.c" "$afkv2_tmp/send.c" +# shellcheck disable=SC2086 +$afkv2_cc -std=gnu11 -Wall -Wextra -Werror ${CFLAGS:-} $afkv2_sanitizers \ + -I"$afkv2_dir/host-include" -I"$afkv2_root/drivers/gpu/drm/apple" \ + "$afkv2_tmp/send.c" -o "$afkv2_tmp/send" +"$afkv2_tmp/send" + +"$afkv2_python" "$afkv2_dir/extract-edid.py" \ + "$afkv2_root/drivers/gpu/drm/apple/epic/dpavservep.c" "$afkv2_tmp/edid.c" +# shellcheck disable=SC2086 +$afkv2_cc -std=gnu11 -Wall -Wextra -Werror ${CFLAGS:-} $afkv2_sanitizers \ + -I"$afkv2_dir/host-include" -I"$afkv2_root/drivers/gpu/drm/apple" \ + "$afkv2_tmp/edid.c" -o "$afkv2_tmp/edid" +"$afkv2_tmp/edid" + +"$afkv2_python" "$afkv2_dir/extract-ring.py" \ + "$afkv2_root/drivers/gpu/drm/apple/afk.c" "$afkv2_tmp/ring.c" +"$afkv2_python" "$afkv2_dir/extract-notify.py" \ + "$afkv2_root/drivers/gpu/drm/apple/epic/dpavservep.c" "$afkv2_tmp/notify.c" +for afkv2_test in ring notify; do + # shellcheck disable=SC2086 + $afkv2_cc -std=gnu11 -Wall -Wextra -Werror ${CFLAGS:-} $afkv2_sanitizers \ + -I"$afkv2_dir/host-include" -I"$afkv2_root/drivers/gpu/drm/apple" \ + "$afkv2_tmp/$afkv2_test.c" -o "$afkv2_tmp/$afkv2_test" + "$afkv2_tmp/$afkv2_test" +done From e9ee1ffbcb4ca2f3f103329804ebe76b6de741fc Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 20:28:37 +1100 Subject: [PATCH 03/10] drm/apple: add bounded tracepoints for external output qualification Expose AFK headers before protocol decoding, submitted surface metadata, CTM values, and A423/A428 reply values through disabled-by-default tracepoints. Capture at most 32 received header bytes and do not dereference firmware-advertised addresses. Preserve all firmware requests and callback results. Host tests exercise the actual trace assignments and callback continuation with zero/short/null and unaligned inputs. These observers support investigating the unqualified DPAV announcement and output-color behavior without changing protocol policy. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + drivers/gpu/drm/apple/afk.c | 2 + drivers/gpu/drm/apple/iomfb_template.c | 19 ++- drivers/gpu/drm/apple/trace.h | 86 ++++++++++++++ tools/diagnostics/test-apple-display-trace.py | 108 ++++++++++++++++++ 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 tools/diagnostics/test-apple-display-trace.py diff --git a/MAINTAINERS b/MAINTAINERS index 90a72c4fd6b16..62910290250a9 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2021,6 +2021,7 @@ F: tools/diagnostics/apple-j700-dp-test.c F: tools/diagnostics/test-apple-afk-admission.py F: tools/diagnostics/test-apple-atc-unwind.py F: tools/diagnostics/test-apple-dcp-readiness.py +F: tools/diagnostics/test-apple-display-trace.py F: tools/diagnostics/test-apple-external-sdr.py F: tools/diagnostics/test-apple-j700-crossbar.py F: tools/diagnostics/test-apple-j700-d134.py diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 2d7fe02b99ab1..aab67b94afc64 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -894,6 +894,8 @@ static void afk_recv_handle(struct apple_dcp_afkep *ep, u32 channel, u32 type, u8 *payload; size_t payload_size; + trace_afk_recv_header(ep, channel, type, data_size, data); + if (ep->wire_v2) { afk_v2_receive(ep, channel, type, data, data_size); return; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 31764f1bc3497..248ee966e33a6 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -68,8 +69,16 @@ static void dcp_swap_submit(struct apple_dcp *dcp, bool oob, dcp_callback_t cb, void *cookie) { const void *data = request; + unsigned int i; if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { + for (i = 0; i < SWAP_SURFACES; i++) { + const struct dcp_surface *surface = &request->surf[i].base; + + if (!request->surf_null[i]) + trace_iomfb_surface(dcp, i, surface->format, surface->xfer_func, + surface->colorspace, surface->is_premultiplied); + } iomfb_serialize_present_h17p(&dcp->present_h17p, request); if (!iomfb_apply_backlight_h17p(dcp, request, &dcp->present_h17p)) { if (cb) @@ -2093,11 +2102,18 @@ void iomfb_present_backlight_h17p(struct apple_dcp *dcp) } #endif +static void dcp_post_swap_a428(struct apple_dcp *dcp, void *out, void *cookie) +{ + trace_iomfb_control_result(dcp, 428, !!out, out ? get_unaligned_le32(out) : 0); + do_swap(dcp, out, cookie); +} + static void dcp_pre_swap_a428(struct apple_dcp *dcp, void *out, void *cookie) { u32 identity = 0x10000; - dcp_pipe_set_428(dcp, false, &identity, do_swap, cookie); + trace_iomfb_control_result(dcp, 423, !!out, out ? get_unaligned_le32(out) : 0); + dcp_pipe_set_428(dcp, false, &identity, dcp_post_swap_a428, cookie); } #endif @@ -2375,6 +2391,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru mat.matrix[0] = mat.matrix[4] = mat.matrix[8] = 1LLU << 32; } + trace_iomfb_ctm(dcp, mat.location, mat.matrix, mat.matrix_null); #if DCP_FW_VER >= DCP_FW_VERSION(26, 0, 0) iomfb_set_matrix(dcp, false, &mat, dcp_pre_swap_a428, NULL); #else diff --git a/drivers/gpu/drm/apple/trace.h b/drivers/gpu/drm/apple/trace.h index d3c5106e5d66d..085f28ac44fae 100644 --- a/drivers/gpu/drm/apple/trace.h +++ b/drivers/gpu/drm/apple/trace.h @@ -205,6 +205,92 @@ TRACE_EVENT( print_epic_category(__entry->category), __entry->subtype, __entry->tag)); +/* Raw prefix before any protocol-specific decode; no peer address is followed. */ +TRACE_EVENT(afk_recv_header, + TP_PROTO(struct apple_dcp_afkep *ep, u32 channel, u32 type, + size_t data_size, const void *data), + TP_ARGS(ep, channel, type, data_size, data), + TP_STRUCT__entry(__string(devname, dev_name(ep->dcp->dev)) + __field(u8, endpoint) + __field(u32, channel) + __field(u32, type) + __field(size_t, data_size) + __field(u8, captured) + __array(u8, header, 32)), + TP_fast_assign(__assign_str(devname); + __entry->endpoint = ep->endpoint; + __entry->channel = channel; + __entry->type = type; + __entry->data_size = data_size; + __entry->captured = data ? min_t(size_t, data_size, 32) : 0; + memset(__entry->header, 0, sizeof(__entry->header)); + if (__entry->captured) + memcpy(__entry->header, data, __entry->captured); + ), + TP_printk("%s: endpoint 0x%x channel 0x%x type 0x%x size %zu header[%u]=%s", + __get_str(devname), __entry->endpoint, __entry->channel, + __entry->type, __entry->data_size, __entry->captured, + __print_hex(__entry->header, __entry->captured)) +); + +TRACE_EVENT(iomfb_ctm, + TP_PROTO(struct apple_dcp *dcp, u32 location, const void *matrix, u8 matrix_null), + TP_ARGS(dcp, location, matrix, matrix_null), + TP_STRUCT__entry(__string(devname, dev_name(dcp->dev)) + __field(u32, location) + __field(u8, matrix_null) + __array(u64, matrix, 9)), + TP_fast_assign(__assign_str(devname); + __entry->location = location; + __entry->matrix_null = matrix_null; + if (matrix) + memcpy(__entry->matrix, matrix, sizeof(__entry->matrix)); + else + memset(__entry->matrix, 0, sizeof(__entry->matrix)); + ), + TP_printk("%s: location %u null %u matrix=%s", + __get_str(devname), __entry->location, __entry->matrix_null, + __print_array(__entry->matrix, 9, sizeof(u64))) +); + +TRACE_EVENT(iomfb_control_result, + TP_PROTO(struct apple_dcp *dcp, u32 method, bool valid, u32 result), + TP_ARGS(dcp, method, valid, result), + TP_STRUCT__entry(__string(devname, dev_name(dcp->dev)) + __field(u32, method) + __field(bool, valid) + __field(u32, result)), + TP_fast_assign(__assign_str(devname); + __entry->method = method; + __entry->valid = valid; + __entry->result = result; + ), + TP_printk("%s: A%u reply_valid %u result 0x%08x", + __get_str(devname), __entry->method, __entry->valid, __entry->result) +); + +TRACE_EVENT(iomfb_surface, + TP_PROTO(struct apple_dcp *dcp, u32 index, u32 format, u8 xfer_func, + u8 colorspace, u8 premultiplied), + TP_ARGS(dcp, index, format, xfer_func, colorspace, premultiplied), + TP_STRUCT__entry(__string(devname, dev_name(dcp->dev)) + __field(u32, index) + __field(u32, format) + __field(u8, xfer_func) + __field(u8, colorspace) + __field(u8, premultiplied)), + TP_fast_assign(__assign_str(devname); + __entry->index = index; + __entry->format = format; + __entry->xfer_func = xfer_func; + __entry->colorspace = colorspace; + __entry->premultiplied = premultiplied; + ), + TP_printk("%s: surface %u format 0x%08x xfer %u colorspace %u source_premul %u", + __get_str(devname), __entry->index, __entry->format, + __entry->xfer_func, __entry->colorspace, __entry->premultiplied) +); + TRACE_EVENT(iomfb_callback, TP_PROTO(struct apple_dcp *dcp, int tag, const char *name), TP_ARGS(dcp, tag, name), diff --git a/tools/diagnostics/test-apple-display-trace.py b/tools/diagnostics/test-apple-display-trace.py new file mode 100644 index 0000000000000..c4b5d041d6401 --- /dev/null +++ b/tools/diagnostics/test-apple-display-trace.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Host checks for bounded trace assignments and unchanged color callback flow.""" +import argparse +from pathlib import Path +import re +import subprocess +import tempfile + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--source-root', type=Path, default=Path(__file__).resolve().parents[2]) +args = parser.parse_args() +root = args.source_root / 'drivers/gpu/drm/apple' +trace = (root / 'trace.h').read_text() +template = (root / 'iomfb_template.c').read_text() + +def trace_assignment(name): + start = trace.index('TRACE_EVENT(' + name + ',') + start = trace.index('TP_fast_assign(', start) + len('TP_fast_assign(') + end, depth = start, 1 + while depth: + depth += (trace[end] == '(') - (trace[end] == ')') + end += 1 + return trace[start:end - 1] + +def function(name): + match = re.search(r'static void ' + name + r'\([^;]*?\)\n\{', template) + if not match: + raise RuntimeError('missing callback: ' + name) + end, depth = match.end(), 1 + while depth: + depth += (template[end] == '{') - (template[end] == '}') + end += 1 + return template[match.start():end] + +prefix = r''' +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64; +#define __assign_str(x) ((void)0) +#define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) +struct apple_dcp { int unused; }; +struct apple_dcp_afkep { u8 endpoint; }; +struct header_entry { u8 endpoint;u32 channel,type;size_t data_size;u8 captured,header[32]; }; +struct ctm_entry { u32 location;u8 matrix_null;u64 matrix[9]; }; +static unsigned int calls, swapped;static u32 methods[2],results[2];static bool valids[2]; +static void *swap_cookie,*reply_data;static u8 ack[5];static bool null_ack; +static u32 get_unaligned_le32(const void *p) { const u8 *b=p;return b[0]|(u32)b[1]<<8|(u32)b[2]<<16|(u32)b[3]<<24; } +static void trace_iomfb_control_result(struct apple_dcp *d,u32 m,bool valid,u32 result) +{ (void)d;assert(calls<2);methods[calls]=m;valids[calls]=valid;results[calls++]=result; } +static void do_swap(struct apple_dcp *d,void *out,void *cookie) +{ (void)d;swapped++;swap_cookie=cookie;reply_data=out; } +static void dcp_pipe_set_428(struct apple_dcp *d,bool oob,const u32 *identity, + void (*cb)(struct apple_dcp*,void*,void*),void *cookie) +{ assert(!oob && *identity==0x10000);cb(d,null_ack?NULL:ack+1,cookie); } +''' +code = prefix +code += 'static void capture(struct header_entry *__entry,struct apple_dcp_afkep *ep,u32 channel,u32 type,size_t data_size,const void *data) {\n' + trace_assignment('afk_recv_header') + '\n}\n' +code += 'static void capture_ctm(struct ctm_entry *__entry,u32 location,const void *matrix,u8 matrix_null) {\n' + trace_assignment('iomfb_ctm') + '\n}\n' +code += function('dcp_post_swap_a428') + '\n' + function('dcp_pre_swap_a428') +code += r''' +int main(void) +{ + struct apple_dcp_afkep ep={.endpoint=0x28};struct header_entry e;size_t n,i; + for(n=0;n<=64;n++) { + u8 *p=malloc(n?n:1);for(i=0;i Date: Mon, 12 Oct 2026 20:39:37 +1100 Subject: [PATCH 04/10] drm/apple: refine inline reply validation and trace silent DPAV rings Treat the transport/interface/reserved header fields as opaque, matching captured firmware responses and the owned-DMA validator, while retaining identity, type, tag, category and payload-bound checks. Add an optional read-only ring snapshot at the DPAV announcement timeout; it does not consume data or send another command. Host tests preserve response mismatch rejection, short-reply checks and timeout ownership, and verify the snapshot's enable, readiness, stop and pointer guards. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/afk.c | 31 +++++++++++++++ drivers/gpu/drm/apple/afk.h | 1 + drivers/gpu/drm/apple/afkv2-codec.h | 7 ++-- drivers/gpu/drm/apple/epic/dpavservep.c | 1 + drivers/gpu/drm/apple/trace.h | 34 +++++++++++++++++ tools/diagnostics/test-apple-display-trace.py | 38 +++++++++++++++++-- .../drivers/gpu/apple-afkv2/inline-edid.c | 12 +++++- 7 files changed, 116 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index aab67b94afc64..e1c87840a6c89 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -158,6 +158,37 @@ int afk_start(struct apple_dcp_afkep *ep) return 0; } +/* Read only host-owned ring headers; never consume an entry or notify firmware. */ +void afk_trace_ring_snapshot(struct apple_dcp_afkep *ep) +{ + unsigned long flags; + u32 rx_rptr = 0, rx_wptr = 0, tx_rptr = 0, tx_wptr = 0; + bool rx_ready, tx_ready; + + if (!ep || !ep->inline_v2 || !trace_afk_ring_snapshot_enabled()) + return; + spin_lock_irqsave(&ep->lock, flags); + if (ep->stopping) { + spin_unlock_irqrestore(&ep->lock, flags); + return; + } + rx_ready = READ_ONCE(ep->rxbfr.ready) && ep->rxbfr.rptr && ep->rxbfr.wptr; + tx_ready = READ_ONCE(ep->txbfr.ready) && ep->txbfr.rptr && ep->txbfr.wptr; + dma_rmb(); + if (rx_ready) { + rx_rptr = le32_to_cpu(READ_ONCE(*ep->rxbfr.rptr)); + rx_wptr = le32_to_cpu(READ_ONCE(*ep->rxbfr.wptr)); + } + if (tx_ready) { + tx_rptr = le32_to_cpu(READ_ONCE(*ep->txbfr.rptr)); + tx_wptr = le32_to_cpu(READ_ONCE(*ep->txbfr.wptr)); + } + trace_afk_ring_snapshot(ep, rx_ready, rx_rptr, rx_wptr, + READ_ONCE(ep->rxbfr.bufsz), tx_ready, tx_rptr, + tx_wptr, READ_ONCE(ep->txbfr.bufsz)); + spin_unlock_irqrestore(&ep->lock, flags); +} + static void afk_getbuf(struct apple_dcp_afkep *ep, u64 message) { u32 size = FIELD_GET(GETBUF_SIZE, message) << BLOCK_SHIFT; diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 45fe9db64d30f..36b07a556f6d4 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -364,6 +364,7 @@ struct apple_dcp_afkep *afk_init(struct apple_dcp *dcp, u32 endpoint, int afk_v2_copy_owned(struct apple_epic_service *service, u64 dva, void *buffer, size_t size, bool to_peer); int afk_start(struct apple_dcp_afkep *ep); +void afk_trace_ring_snapshot(struct apple_dcp_afkep *ep); void afk_shutdown(struct apple_dcp_afkep *ep); void afk_quiesce(struct apple_dcp_afkep *ep); int afk_receive_message(struct apple_dcp_afkep *ep, u64 message); diff --git a/drivers/gpu/drm/apple/afkv2-codec.h b/drivers/gpu/drm/apple/afkv2-codec.h index 9ef52b2e70ea2..a06a1efd5fc7e 100644 --- a/drivers/gpu/drm/apple/afkv2-codec.h +++ b/drivers/gpu/drm/apple/afkv2-codec.h @@ -270,7 +270,9 @@ static inline int afkv2_check_response(const struct afkv2_frame *frame, return 0; } -/* Kept separate so DPAV inline replies cannot loosen owned-DMA checks. */ +/* Kept separate so DPAV inline replies cannot loosen owned-DMA checks. + * Transport/interface flags and the reserved word are opaque firmware data. + */ static inline int afkv2_check_inline_response(const struct afkv2_frame *frame, const struct afkv2_command *response, u16 interface, u8 type, u8 id, @@ -278,8 +280,7 @@ static inline int afkv2_check_inline_response(const struct afkv2_frame *frame, { if (!frame || !response || frame->category != AFKV2_RESPONSE || frame->interface != interface || frame->type != type || response->id != id || - frame->token || frame->transport_flags || frame->interface_flags || - frame->reserved || response->oob || response->inline_size > capacity) + frame->token || response->oob || response->inline_size > capacity) return -EPROTO; return 0; } diff --git a/drivers/gpu/drm/apple/epic/dpavservep.c b/drivers/gpu/drm/apple/epic/dpavservep.c index 1b4019cc159f9..64e836ef48811 100644 --- a/drivers/gpu/drm/apple/epic/dpavservep.c +++ b/drivers/gpu/drm/apple/epic/dpavservep.c @@ -344,6 +344,7 @@ int dpavservep_init(struct apple_dcp *dcp) ret = wait_for_completion_timeout(&dcp->dcpavserv.enable_completion, msecs_to_jiffies(1000)); if (!ret) { + afk_trace_ring_snapshot(ep); dev_warn(dcp->dev, "DCPAVSERV: timed out waiting for EDID service announcement\n"); return -ETIMEDOUT; } diff --git a/drivers/gpu/drm/apple/trace.h b/drivers/gpu/drm/apple/trace.h index 085f28ac44fae..d19609f116f2c 100644 --- a/drivers/gpu/drm/apple/trace.h +++ b/drivers/gpu/drm/apple/trace.h @@ -233,6 +233,40 @@ TRACE_EVENT(afk_recv_header, __print_hex(__entry->header, __entry->captured)) ); +TRACE_EVENT(afk_ring_snapshot, + TP_PROTO(struct apple_dcp_afkep *ep, bool rx_ready, u32 rx_rptr, + u32 rx_wptr, u32 rx_size, bool tx_ready, u32 tx_rptr, + u32 tx_wptr, u32 tx_size), + TP_ARGS(ep, rx_ready, rx_rptr, rx_wptr, rx_size, + tx_ready, tx_rptr, tx_wptr, tx_size), + TP_STRUCT__entry(__string(devname, dev_name(ep->dcp->dev)) + __field(u8, endpoint) + __field(bool, rx_ready) + __field(u32, rx_rptr) + __field(u32, rx_wptr) + __field(u32, rx_size) + __field(bool, tx_ready) + __field(u32, tx_rptr) + __field(u32, tx_wptr) + __field(u32, tx_size)), + TP_fast_assign(__assign_str(devname); + __entry->endpoint = ep->endpoint; + __entry->rx_ready = rx_ready; + __entry->rx_rptr = rx_rptr; + __entry->rx_wptr = rx_wptr; + __entry->rx_size = rx_size; + __entry->tx_ready = tx_ready; + __entry->tx_rptr = tx_rptr; + __entry->tx_wptr = tx_wptr; + __entry->tx_size = tx_size; + ), + TP_printk("%s: endpoint 0x%x rx_ready %u rx_rptr %#x rx_wptr %#x rx_size %#x tx_ready %u tx_rptr %#x tx_wptr %#x tx_size %#x", + __get_str(devname), __entry->endpoint, __entry->rx_ready, + __entry->rx_rptr, __entry->rx_wptr, __entry->rx_size, + __entry->tx_ready, __entry->tx_rptr, __entry->tx_wptr, + __entry->tx_size) +); + TRACE_EVENT(iomfb_ctm, TP_PROTO(struct apple_dcp *dcp, u32 location, const void *matrix, u8 matrix_null), TP_ARGS(dcp, location, matrix, matrix_null), diff --git a/tools/diagnostics/test-apple-display-trace.py b/tools/diagnostics/test-apple-display-trace.py index c4b5d041d6401..f0c32db9e769f 100644 --- a/tools/diagnostics/test-apple-display-trace.py +++ b/tools/diagnostics/test-apple-display-trace.py @@ -13,6 +13,7 @@ root = args.source_root / 'drivers/gpu/drm/apple' trace = (root / 'trace.h').read_text() template = (root / 'iomfb_template.c').read_text() +afk = (root / 'afk.c').read_text() def trace_assignment(name): start = trace.index('TRACE_EVENT(' + name + ',') @@ -44,7 +45,19 @@ def function(name): #define __assign_str(x) ((void)0) #define min_t(t,a,b) ((t)(a)<(t)(b)?(t)(a):(t)(b)) struct apple_dcp { int unused; }; -struct apple_dcp_afkep { u8 endpoint; }; +struct ring { bool ready; u32 *rptr,*wptr,bufsz; }; +struct apple_dcp_afkep { u8 endpoint; bool inline_v2,stopping; int lock; struct ring rxbfr,txbfr; }; +#define READ_ONCE(x) (x) +#define le32_to_cpu(x) (x) +#define spin_lock_irqsave(l,f) ((void)(l),(f)=0) +#define spin_unlock_irqrestore(l,f) ((void)(l),(void)(f)) +#define dma_rmb() do {} while(0) +static bool enabled; +static unsigned int snapshots; +static u32 rx_seen,rx_write_seen,tx_seen,tx_write_seen; +static bool trace_afk_ring_snapshot_enabled(void) { return enabled; } +static void trace_afk_ring_snapshot(struct apple_dcp_afkep *ep,bool rr,u32 rp,u32 wp,u32 rs,bool tr,u32 tp,u32 tw,u32 ts) +{ (void)ep;(void)rr;(void)rs;(void)tr;(void)ts;snapshots++;rx_seen=rp;rx_write_seen=wp;tx_seen=tp;tx_write_seen=tw; } struct header_entry { u8 endpoint;u32 channel,type;size_t data_size;u8 captured,header[32]; }; struct ctm_entry { u32 location;u8 matrix_null;u64 matrix[9]; }; static unsigned int calls, swapped;static u32 methods[2],results[2];static bool valids[2]; @@ -58,7 +71,13 @@ def function(name): void (*cb)(struct apple_dcp*,void*,void*),void *cookie) { assert(!oob && *identity==0x10000);cb(d,null_ack?NULL:ack+1,cookie); } ''' -code = prefix +match = re.search(r"void afk_trace_ring_snapshot\([^;]*?\)\n\{", afk) +assert match +end, depth = match.end(), 1 +while depth: + depth += (afk[end] == '{') - (afk[end] == '}') + end += 1 +code = prefix + afk[match.start():end] + '\n' code += 'static void capture(struct header_entry *__entry,struct apple_dcp_afkep *ep,u32 channel,u32 type,size_t data_size,const void *data) {\n' + trace_assignment('afk_recv_header') + '\n}\n' code += 'static void capture_ctm(struct ctm_entry *__entry,u32 location,const void *matrix,u8 matrix_null) {\n' + trace_assignment('iomfb_ctm') + '\n}\n' code += function('dcp_post_swap_a428') + '\n' + function('dcp_pre_swap_a428') @@ -95,7 +114,20 @@ def function(name): assert(calls==2 && !valids[0] && !valids[1] && !results[0] && !results[1]); assert(swapped==2 && swap_cookie==&cookie && reply_data==NULL); } - puts("PASS: actual trace assignments bounded/null-safe; CTM nine coefficients; A423/A428 callback flow preserved"); + { + u32 r=128,w=256,tr=64,tw=192;ep.inline_v2=true; + afk_trace_ring_snapshot(&ep);assert(!snapshots); + enabled=true;ep.rxbfr.rptr=(u32*)1;ep.txbfr.rptr=(u32*)1; + afk_trace_ring_snapshot(&ep);assert(snapshots==1 && !rx_seen && !tx_seen); + ep.rxbfr=(struct ring){.ready=true,.rptr=&r,.wptr=&w,.bufsz=8192}; + ep.txbfr=(struct ring){.ready=true,.rptr=&tr,.wptr=&tw,.bufsz=8192}; + afk_trace_ring_snapshot(&ep);assert(snapshots==2 && rx_seen==128 && rx_write_seen==256 && tx_seen==64 && tx_write_seen==192); + assert(r==128 && w==256 && tr==64 && tw==192); + ep.stopping=true;afk_trace_ring_snapshot(&ep);assert(snapshots==2); + ep.stopping=false;ep.inline_v2=false;afk_trace_ring_snapshot(&ep);assert(snapshots==2); + afk_trace_ring_snapshot(NULL);assert(snapshots==2); + } + puts("PASS: bounded trace/callback flow and read-only ring snapshot enable/ready/stop guards"); } ''' with tempfile.TemporaryDirectory(prefix='apple-display-trace-') as tmp: diff --git a/tools/testing/selftests/drivers/gpu/apple-afkv2/inline-edid.c b/tools/testing/selftests/drivers/gpu/apple-afkv2/inline-edid.c index 7ce16fd2b6a8b..b711adf804794 100644 --- a/tools/testing/selftests/drivers/gpu/apple-afkv2/inline-edid.c +++ b/tools/testing/selftests/drivers/gpu/apple-afkv2/inline-edid.c @@ -48,8 +48,16 @@ int main(void) frame.token = 1; CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); frame.token = 0; - frame.reserved = 1; - CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); + /* R30 responses carry these opaque nonzero header fields. */ + frame.transport_flags = 0x18; + frame.interface_flags = 0x0400; + frame.reserved = 0x3607ebe0; + CHECK(!afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); + /* Context and bounds remain mandatory even with opaque header values. */ + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x92, 136)); + CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 135)); + frame.transport_flags = 0; + frame.interface_flags = 0; frame.reserved = 0; reply.oob = true; CHECK(afkv2_check_inline_response(&frame, &reply, 4, 0xc0, 0x91, 136)); From cb49a7e2b4ba09ac47e6b780692f33aeac241421 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 20:42:08 +1100 Subject: [PATCH 05/10] drm/apple: honor external timing color exclusions before SDR selection Exclude firmware-listed unsafe and DSC-required color IDs before choosing an external H17P SDR candidate. Read restrictions independently of property order and reject malformed or duplicate lists instead of silently treating them as empty. Preserve internal and legacy parsing. The captured dock route permits 60 Hz at 10-bit and 120 Hz at 8-bit; its 144/180 Hz timings have no eligible nonvirtual SDR candidate after exclusions. This is metadata admission, not a new physical-link qualification or a global refresh cap. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/parser.c | 104 ++++++++++++++++++- tools/diagnostics/test-apple-external-sdr.py | 61 +++++++++++ 2 files changed, 162 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 73e7e1cf315c6..691bc91e3542b 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -411,8 +411,92 @@ static unsigned int external_sdr_rank(const struct color_mode *color) } } +/* Firmware lists color element IDs which this timing cannot safely use. */ +#define DCP_TIMING_COLOR_ID_LIMIT 64 + +struct timing_color_ids { + u32 ids[DCP_TIMING_COLOR_ID_LIMIT]; + unsigned int count; +}; + +struct timing_color_restrictions { + struct timing_color_ids unsafe; + struct timing_color_ids dsc_required; +}; + +static bool timing_color_contains(const struct timing_color_ids *ids, u32 id) +{ + unsigned int i; + + for (i = 0; i < ids->count; i++) + if (ids->ids[i] == id) + return true; + return false; +} + +static int parse_timing_color_ids(struct dcp_parse_ctx *handle, + struct timing_color_ids *ids) +{ + struct iterator it; + int ret; + + ret = iterator_begin(handle, &it, false); + if (ret) + return ret; + if (it.len > DCP_TIMING_COLOR_ID_LIMIT) + return -E2BIG; + for (; it.idx < it.len; it.idx++) { + s64 id; + + ret = parse_int_bound(handle, &id, 0, U32_MAX); + if (ret) + return ret; + if (timing_color_contains(ids, id)) + return -EINVAL; + ids->ids[ids->count++] = id; + } + return 0; +} + +static int parse_timing_color_restrictions(struct dcp_parse_ctx *handle, + struct timing_color_restrictions *limits) +{ + struct iterator it; + bool unsafe_seen = false, dsc_seen = false, colors_seen = false; + int ret; + + ret = iterator_begin(handle, &it, true); + if (ret) + return ret; + for (; it.idx < it.len; it.idx++) { + char *key = parse_string(handle); + + if (IS_ERR(key)) + return PTR_ERR(key); + if (!strcmp(key, "UnsafeColorElementIDs")) { + ret = unsafe_seen ? -EINVAL : + parse_timing_color_ids(handle, &limits->unsafe); + unsafe_seen = true; + } else if (!strcmp(key, "DSCRequiredColorElementIDs")) { + ret = dsc_seen ? -EINVAL : + parse_timing_color_ids(handle, &limits->dsc_required); + dsc_seen = true; + } else if (!strcmp(key, "ColorModes")) { + ret = colors_seen ? -EINVAL : skip(handle); + colors_seen = true; + } else { + ret = skip(handle); + } + kfree(key); + if (ret) + return ret; + } + return 0; +} + static int parse_color_modes(struct dcp_parse_ctx *handle, - struct dcp_display_mode *out, bool external_sdr) + struct dcp_display_mode *out, bool external_sdr, + const struct timing_color_restrictions *limits) { struct iterator outer_it; unsigned int best_sdr_rank = 0; @@ -484,8 +568,13 @@ static int parse_color_modes(struct dcp_parse_ctx *handle, cmode.pixel_encoding); if (external_sdr) { - unsigned int rank = external_sdr_rank(&cmode); + unsigned int rank; + if (cmode.id <= U32_MAX && + (timing_color_contains(&limits->unsafe, cmode.id) || + timing_color_contains(&limits->dsc_required, cmode.id))) + continue; + rank = external_sdr_rank(&cmode); if (!rank || rank < best_sdr_rank) continue; if (rank > best_sdr_rank) @@ -531,6 +620,7 @@ static int parse_mode(struct dcp_parse_ctx *handle, int ret = 0; struct iterator it; struct dimension horiz = { .active = -1 }, vert = { .active = -1 }; + struct timing_color_restrictions limits = {}; s64 min_vrr = 0, max_vrr = 0; u64 clock; s64 id = -1; @@ -546,6 +636,14 @@ static int parse_mode(struct dcp_parse_ctx *handle, }; *score = -1; + if (external_sdr) { + struct dcp_parse_ctx scan = *handle; + + /* ColorModes can precede either list: collect restrictions first. */ + ret = parse_timing_color_restrictions(&scan, &limits); + if (ret) + return ret; + } ret = iterator_begin(handle, &it, true); if (ret) return ret; @@ -566,7 +664,7 @@ static int parse_mode(struct dcp_parse_ctx *handle, else if (!strcmp(key, "MaximumVariableRefreshRate")) ret = parse_int_bound(it.handle, &max_vrr, 0, U32_MAX); else if (!strcmp(key, "ColorModes")) - ret = parse_color_modes(it.handle, out, external_sdr); + ret = parse_color_modes(it.handle, out, external_sdr, &limits); else if (!strcmp(key, "ID")) ret = parse_int_bound(it.handle, &id, 0, U32_MAX); else if (!strcmp(key, "IsVirtual")) diff --git a/tools/diagnostics/test-apple-external-sdr.py b/tools/diagnostics/test-apple-external-sdr.py index f72c26e38108c..1ece679366267 100644 --- a/tools/diagnostics/test-apple-external-sdr.py +++ b/tools/diagnostics/test-apple-external-sdr.py @@ -24,6 +24,10 @@ print('Production mode parser SHA256:', hashlib.sha256((header + body).encode()).hexdigest()) +class Pairs(list): + """A wire dictionary which preserves duplicate keys for rejection tests.""" + + def wire(value, out): out += b'\0' * (-len(out) % 4) if isinstance(value, bool): @@ -35,6 +39,11 @@ def wire(value, out): out += struct.pack(' #include From 9303ecf2c6ab901cdf9697b24d21632807c1a7c3 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 20:49:18 +1100 Subject: [PATCH 06/10] drm/apple: derive bounded external monitor metadata for initial attachment Expose the firmware-reported identity, primaries and preferred timing while raw DPAV EDID is unavailable on the experimental H17P external path. Keep this synthetic base block out of the raw cache, require complete metadata from the same initial connection, and invalidate it on route or cable loss. Do not synthesize extra timings, audio, HDR, VRR or link capabilities. Serialize EDID property and cache changes with connector probing. Defer commit-context disconnect cleanup through a persistent invalidation flag, so rapid reconnection cannot lose it or invert DRM modeset lock ordering. Include bounded-parser, independent EDID decoding and concurrent lifetime tests. Hardware qualification remains pending. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 2 + drivers/gpu/drm/apple/Makefile | 2 +- drivers/gpu/drm/apple/connector.h | 3 + drivers/gpu/drm/apple/dcp-display-metadata.c | 204 +++++++++ drivers/gpu/drm/apple/dcp-display-metadata.h | 24 ++ drivers/gpu/drm/apple/dcp-internal.h | 23 + drivers/gpu/drm/apple/dcp-metadata-epoch.h | 48 +++ drivers/gpu/drm/apple/dcp.c | 183 +++++++- drivers/gpu/drm/apple/iomfb.c | 66 ++- drivers/gpu/drm/apple/iomfb_template.c | 20 +- drivers/gpu/drm/apple/parser.c | 274 +++++++++++- drivers/gpu/drm/apple/parser.h | 6 + .../display-attributes-fixture.json | 23 + .../test-apple-display-metadata-lifetime.py | 395 ++++++++++++++++++ .../test-apple-display-metadata.py | 363 ++++++++++++++++ tools/diagnostics/test-apple-external-sdr.py | 6 +- 16 files changed, 1625 insertions(+), 17 deletions(-) create mode 100644 drivers/gpu/drm/apple/dcp-display-metadata.c create mode 100644 drivers/gpu/drm/apple/dcp-display-metadata.h create mode 100644 drivers/gpu/drm/apple/dcp-metadata-epoch.h create mode 100644 tools/diagnostics/display-attributes-fixture.json create mode 100644 tools/diagnostics/test-apple-display-metadata-lifetime.py create mode 100644 tools/diagnostics/test-apple-display-metadata.py diff --git a/MAINTAINERS b/MAINTAINERS index 62910290250a9..cff04513aaad7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2018,9 +2018,11 @@ F: rust/kernel/soc/apple/aop.rs F: sound/soc/apple/t8140_aop_audio.rs F: tools/diagnostics/apple-dptx-training-test.c F: tools/diagnostics/apple-j700-dp-test.c +F: tools/diagnostics/display-attributes-fixture.json F: tools/diagnostics/test-apple-afk-admission.py F: tools/diagnostics/test-apple-atc-unwind.py F: tools/diagnostics/test-apple-dcp-readiness.py +F: tools/diagnostics/test-apple-display-metadata*.py F: tools/diagnostics/test-apple-display-trace.py F: tools/diagnostics/test-apple-external-sdr.py F: tools/diagnostics/test-apple-j700-crossbar.py diff --git a/drivers/gpu/drm/apple/Makefile b/drivers/gpu/drm/apple/Makefile index 4980b2863225b..601689ee3b172 100644 --- a/drivers/gpu/drm/apple/Makefile +++ b/drivers/gpu/drm/apple/Makefile @@ -6,7 +6,7 @@ appledrm-y := apple_drv.o appledrm-y += afk.o dcp.o dcp_backlight.o dptxep.o iomfb.o parser.o systemep.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += audio.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += av.o -appledrm-y += connector.o +appledrm-y += connector.o dcp-display-metadata.o appledrm-y += ibootep.o appledrm-y += iomfb_v12_3.o appledrm-y += iomfb_v13_3.o diff --git a/drivers/gpu/drm/apple/connector.h b/drivers/gpu/drm/apple/connector.h index 4cb58502c1ef8..6f73e975ebdc1 100644 --- a/drivers/gpu/drm/apple/connector.h +++ b/drivers/gpu/drm/apple/connector.h @@ -43,6 +43,9 @@ struct apple_connector { u32 candidate_crtcs; const struct drm_edid *drm_edid; + bool drm_edid_firmware_derived; + bool edid_invalidation_pending; + u64 drm_edid_generation; /* Workqueue for sending hotplug events to the associated device */ struct work_struct hotplug_wq; diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.c b/drivers/gpu/drm/apple/dcp-display-metadata.c new file mode 100644 index 0000000000000..cc4c69736d098 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-display-metadata.c @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +#include +#include +#include +#include +#include + +#include "dcp-display-metadata.h" + +static int dcp_metadata_timing(struct detailed_timing *timing, + const struct drm_display_mode *mode, + const struct dcp_display_metadata *metadata) +{ + struct detailed_pixel_timing *pixel = &timing->data.pixel_data; + u32 hblank, vblank, hoff, voff, hsync, vsync; + u32 width = metadata->width_cm * 10, height = metadata->height_cm * 10; + u32 hflags = mode->flags & (DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NHSYNC); + u32 vflags = mode->flags & (DRM_MODE_FLAG_PVSYNC | DRM_MODE_FLAG_NVSYNC); + + if (mode->clock <= 0 || mode->clock > 655350 || mode->clock % 10 || + mode->hdisplay <= 0 || mode->vdisplay <= 0 || + mode->hdisplay > 4095 || mode->vdisplay > 4095 || + mode->htotal <= mode->hdisplay || mode->vtotal <= mode->vdisplay || + mode->hsync_start < mode->hdisplay || mode->hsync_end <= mode->hsync_start || + mode->hsync_end > mode->htotal || mode->vsync_start < mode->vdisplay || + mode->vsync_end <= mode->vsync_start || mode->vsync_end > mode->vtotal || + (hflags != DRM_MODE_FLAG_PHSYNC && hflags != DRM_MODE_FLAG_NHSYNC) || + (vflags != DRM_MODE_FLAG_PVSYNC && vflags != DRM_MODE_FLAG_NVSYNC) || + (mode->flags & ~(DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NHSYNC | + DRM_MODE_FLAG_PVSYNC | DRM_MODE_FLAG_NVSYNC))) + return -EINVAL; + hblank = mode->htotal - mode->hdisplay; + vblank = mode->vtotal - mode->vdisplay; + hoff = mode->hsync_start - mode->hdisplay; + voff = mode->vsync_start - mode->vdisplay; + hsync = mode->hsync_end - mode->hsync_start; + vsync = mode->vsync_end - mode->vsync_start; + if (hblank > 4095 || vblank > 4095 || hoff > 1023 || hsync > 1023 || + voff > 63 || vsync > 63 || width > 4095 || height > 4095) + return -EINVAL; + + timing->pixel_clock = cpu_to_le16(mode->clock / 10); + pixel->hactive_lo = mode->hdisplay; + pixel->hblank_lo = hblank; + pixel->hactive_hblank_hi = (mode->hdisplay >> 8) << 4 | hblank >> 8; + pixel->vactive_lo = mode->vdisplay; + pixel->vblank_lo = vblank; + pixel->vactive_vblank_hi = (mode->vdisplay >> 8) << 4 | vblank >> 8; + pixel->hsync_offset_lo = hoff; + pixel->hsync_pulse_width_lo = hsync; + pixel->vsync_offset_pulse_width_lo = (voff & 15) << 4 | (vsync & 15); + pixel->hsync_vsync_offset_pulse_width_hi = (hoff >> 8) << 6 | (hsync >> 8) << 4 | + (voff >> 4) << 2 | vsync >> 4; + pixel->width_mm_lo = width; + pixel->height_mm_lo = height; + pixel->width_height_mm_hi = (width >> 8) << 4 | height >> 8; + pixel->misc = DRM_EDID_PT_SEPARATE_SYNC; + if (hflags == DRM_MODE_FLAG_PHSYNC) + pixel->misc |= DRM_EDID_PT_HSYNC_POSITIVE; + if (vflags == DRM_MODE_FLAG_PVSYNC) + pixel->misc |= DRM_EDID_PT_VSYNC_POSITIVE; + return 0; +} + +static int dcp_metadata_text(struct detailed_timing *timing, u8 type, const char *text) +{ + size_t length = strnlen(text, 14); + u8 *out = timing->data.other_data.data.str.str; + + if (!length || length > 13) + return -EINVAL; + for (size_t i = 0; i < length; i++) + if (text[i] < 0x20 || text[i] > 0x7e) + return -EINVAL; + timing->data.other_data.type = type; + memset(out, ' ', 13); + memcpy(out, text, length); + if (length < 13) + out[length] = '\n'; + return 0; +} + +static s64 dcp_metadata_side(const u32 *a, const u32 *b, const u32 *point) +{ + return ((s64)b[0] - a[0]) * ((s64)point[1] - a[1]) - + ((s64)b[1] - a[1]) * ((s64)point[0] - a[0]); +} + +/* Build only a base block: the authoritative firmware mode list stays separate. */ +static int dcp_metadata_block(struct edid *edid, const struct dcp_display_metadata *metadata, + const struct drm_display_mode *mode) +{ + u32 coordinate[8], manufacturer = 0, gamma; + u8 checksum = 0; + int ret; + + if (!metadata->width_cm || metadata->width_cm > 255 || + !metadata->height_cm || metadata->height_cm > 255 || + metadata->product > U16_MAX || metadata->week > 53 || + metadata->year < 1990 || metadata->year > 2245 || + metadata->gamma < 65536 || metadata->gamma > (354U << 16) / 100 || + strnlen(metadata->manufacturer, 4) != 3) + return -EINVAL; + for (unsigned int i = 0; i < 3; i++) { + if (metadata->manufacturer[i] < 'A' || metadata->manufacturer[i] > 'Z') + return -EINVAL; + manufacturer = manufacturer << 5 | (metadata->manufacturer[i] - '@'); + } + for (unsigned int i = 0; i < 8; i++) { + u32 value = i < 6 ? metadata->chromaticity[i / 2][i % 2] : metadata->white[i - 6]; + + if (!value || value >= 65536) + return -EINVAL; + coordinate[i] = DIV_ROUND_CLOSEST(value, 64); + if (!coordinate[i] || coordinate[i] > 1023) + return -EINVAL; + if ((i & 1) && coordinate[i - 1] + coordinate[i] > 1024) + return -EINVAL; + } + /* Degenerate primaries or a white point outside their triangle are not usable. */ + { + s64 area = dcp_metadata_side(coordinate, coordinate + 2, coordinate + 4); + + if (!area) + return -EINVAL; + for (unsigned int i = 0; i < 3; i++) { + s64 side = dcp_metadata_side(coordinate + 2 * i, + coordinate + 2 * ((i + 1) % 3), coordinate + 6); + + if ((area > 0 && side < 0) || (area < 0 && side > 0)) + return -EINVAL; + } + } + gamma = DIV_ROUND_CLOSEST(metadata->gamma * 100, 65536) - 100; + if (gamma > 254) + return -EINVAL; + + memset(edid, 0, sizeof(*edid)); + memcpy(edid->header, "\x00\xff\xff\xff\xff\xff\xff\x00", 8); + edid->product_id.manufacturer_name = cpu_to_be16(manufacturer); + edid->product_id.product_code = cpu_to_le16(metadata->product); + edid->product_id.serial_number = cpu_to_le32(metadata->serial); + edid->product_id.week_of_manufacture = metadata->week; + edid->product_id.year_of_manufacture = metadata->year - 1990; + edid->version = 1; + edid->revision = 4; + /* No claim about connector interface, bit depth, HDR, audio or VRR. */ + edid->input = DRM_EDID_INPUT_DIGITAL; + edid->width_cm = metadata->width_cm; + edid->height_cm = metadata->height_cm; + edid->gamma = gamma; + edid->features = DRM_EDID_FEATURE_PREFERRED_TIMING | DRM_EDID_FEATURE_RGB; + edid->red_green_lo = (coordinate[0] & 3) << 6 | (coordinate[1] & 3) << 4 | + (coordinate[2] & 3) << 2 | (coordinate[3] & 3); + edid->blue_white_lo = (coordinate[4] & 3) << 6 | (coordinate[5] & 3) << 4 | + (coordinate[6] & 3) << 2 | (coordinate[7] & 3); + edid->red_x = coordinate[0] >> 2; + edid->red_y = coordinate[1] >> 2; + edid->green_x = coordinate[2] >> 2; + edid->green_y = coordinate[3] >> 2; + edid->blue_x = coordinate[4] >> 2; + edid->blue_y = coordinate[5] >> 2; + edid->white_x = coordinate[6] >> 2; + edid->white_y = coordinate[7] >> 2; + memset(edid->standard_timings, 1, sizeof(edid->standard_timings)); + ret = dcp_metadata_timing(&edid->detailed_timings[0], mode, metadata); + if (!ret) + ret = dcp_metadata_text(&edid->detailed_timings[1], EDID_DETAIL_MONITOR_NAME, + metadata->name); + if (!ret) + ret = dcp_metadata_text(&edid->detailed_timings[2], EDID_DETAIL_MONITOR_STRING, + "DCP metadata"); + if (!ret) + ret = dcp_metadata_text(&edid->detailed_timings[3], + metadata->serial_text[0] ? EDID_DETAIL_MONITOR_SERIAL : + EDID_DETAIL_MONITOR_STRING, + metadata->serial_text[0] ? + metadata->serial_text : "FW derived"); + if (ret) + return ret; + for (unsigned int i = 0; i < sizeof(*edid) - 1; i++) + checksum += ((const u8 *)edid)[i]; + edid->checksum = -checksum; + return 0; +} + +const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *metadata, + const struct drm_display_mode *mode) +{ + struct edid block; + const struct drm_edid *edid; + int ret = dcp_metadata_block(&block, metadata, mode); + + if (ret) + return ERR_PTR(ret); + edid = drm_edid_alloc(&block, sizeof(block)); + if (!edid) + return ERR_PTR(-ENOMEM); + if (!drm_edid_valid(edid)) { + drm_edid_free(edid); + return ERR_PTR(-EINVAL); + } + return edid; +} diff --git a/drivers/gpu/drm/apple/dcp-display-metadata.h b/drivers/gpu/drm/apple/dcp-display-metadata.h new file mode 100644 index 0000000000000..537994420ebd6 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-display-metadata.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCP_DISPLAY_METADATA_H +#define APPLE_DCP_DISPLAY_METADATA_H + +#include + +/* Values reported by DisplayAttributes, not a copy of the sink's EDID. */ +struct dcp_display_metadata { + u32 width_cm, height_cm; + u32 chromaticity[3][2]; + u32 white[2]; + u32 gamma; + u32 product, serial, week, year; + char manufacturer[4]; + char name[14]; + char serial_text[14]; +}; + +struct drm_edid; +struct drm_display_mode; +const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *metadata, + const struct drm_display_mode *mode); + +#endif diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 48f4112151dd3..cd265c6ced986 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -18,6 +18,8 @@ #include "dptxep.h" #include "dcp_backlight.h" +#include "dcp-display-metadata.h" +#include "dcp-metadata-epoch.h" #include "iomfb.h" #include "iomfb_h17p.h" #include "iomfb-state.h" @@ -103,6 +105,9 @@ enum { struct dcp_chunks { size_t length; void *data; + size_t received; + bool contiguous; + struct dcp_metadata_ticket metadata_ticket; }; #define DCP_MAX_MAPPINGS (128) /* should be enough */ @@ -264,6 +269,15 @@ struct apple_dcp { /* Active chunked transfer. There can only be one at a time. */ struct dcp_chunks chunks; + struct { + /* Protects connection tickets and cached metadata snapshots. */ + spinlock_t lock; + struct dcp_metadata_epoch epoch; + struct dcp_display_metadata attributes; + struct drm_display_mode preferred; + struct dcp_metadata_ticket attributes_ticket, timing_ticket; + bool attributes_valid, timing_valid, hpd_seen; + } metadata; /* Queued swap. Owned by the DCP to avoid per-swap memory allocation */ union { @@ -452,6 +466,15 @@ struct apple_dcp { } iomfb; }; +void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, bool connected); +void dcp_firmware_metadata_hotplug(struct apple_dcp *dcp, bool connected); +void dcp_firmware_metadata_begin(struct apple_dcp *dcp); +void dcp_firmware_metadata_attributes(struct apple_dcp *dcp, + const struct dcp_display_metadata *attributes); +void dcp_firmware_metadata_timing(struct apple_dcp *dcp); +bool dcp_firmware_metadata_current(struct apple_dcp *dcp, u64 generation); +const struct drm_edid *dcp_firmware_edid(struct apple_dcp *dcp, u64 *generation); + void iomfb_scanout_complete_h17p(struct apple_dcp *dcp); void iomfb_present_backlight_h17p(struct apple_dcp *dcp); bool iomfb_present_brightness_only_h17p(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/dcp-metadata-epoch.h b/drivers/gpu/drm/apple/dcp-metadata-epoch.h new file mode 100644 index 0000000000000..c2ab0c22a74e0 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-metadata-epoch.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCP_METADATA_EPOCH_H +#define APPLE_DCP_METADATA_EPOCH_H + +#include + +/* Caller serializes these fields with the metadata snapshot lock. */ +struct dcp_metadata_epoch { + u64 generation; + bool connected; + bool associated_once; + bool invalidated; +}; + +struct dcp_metadata_ticket { + u64 generation; + bool associated; +}; + +static inline void dcp_metadata_epoch_connection(struct dcp_metadata_epoch *epoch, + u64 generation, bool connected) +{ + if (epoch->associated_once && + (!connected || generation != epoch->generation)) + epoch->invalidated = true; + if (connected) + epoch->associated_once = true; + epoch->generation = generation; + epoch->connected = connected; +} + +static inline struct dcp_metadata_ticket +dcp_metadata_begin(const struct dcp_metadata_epoch *epoch) +{ + return (struct dcp_metadata_ticket) { + .generation = epoch->generation, + .associated = epoch->connected && !epoch->invalidated, + }; +} + +static inline bool dcp_metadata_matches(const struct dcp_metadata_epoch *epoch, + struct dcp_metadata_ticket ticket) +{ + return ticket.associated && epoch->connected && !epoch->invalidated && + ticket.generation == epoch->generation; +} + +#endif diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 4f2b4923135af..6a6ad92c0a891 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -344,6 +344,8 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, route->port->secondary_connector : route->port->connector; int ret; + dcp_firmware_metadata_connection(dcp, READ_ONCE(dcp->typec_generation), false); + /* * The fixed output's HPD handler leaves disconnects to DCP, so the port * can still be marked connected to a display that is gone. Release it @@ -439,6 +441,8 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) bool was_tunnel = route->tunnel; int ret = 0; + dcp_firmware_metadata_connection(dcp, READ_ONCE(dcp->typec_generation), false); + /* * Under tb_lock so a DCP link (re)configuration can neither select the * crossbar nor restart the tunnel pixel clock behind our back. @@ -1798,6 +1802,168 @@ void dcp_drm_crtc_page_flip(struct apple_dcp *dcp, ktime_t now) spin_unlock_irqrestore(&crtc->base.dev->event_lock, flags); } +static bool dcp_firmware_metadata_enabled(struct apple_dcp *dcp) +{ + return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; +} + +void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, bool connected) +{ + unsigned long flags; + + if (!dcp_firmware_metadata_enabled(dcp)) + return; + spin_lock_irqsave(&dcp->metadata.lock, flags); + if (generation != dcp->metadata.epoch.generation || !connected) { + dcp->metadata.attributes_valid = false; + dcp->metadata.timing_valid = false; + dcp->metadata.hpd_seen = false; + } + dcp_metadata_epoch_connection(&dcp->metadata.epoch, generation, connected); + spin_unlock_irqrestore(&dcp->metadata.lock, flags); +} + +void dcp_firmware_metadata_hotplug(struct apple_dcp *dcp, bool connected) +{ + unsigned long flags; + + if (!dcp_firmware_metadata_enabled(dcp)) + return; + spin_lock_irqsave(&dcp->metadata.lock, flags); + if (connected) { + dcp->metadata.hpd_seen = true; + } else if (dcp->metadata.hpd_seen) { + dcp_metadata_epoch_connection(&dcp->metadata.epoch, + dcp->metadata.epoch.generation, false); + dcp->metadata.attributes_valid = false; + dcp->metadata.timing_valid = false; + } + spin_unlock_irqrestore(&dcp->metadata.lock, flags); +} + +void dcp_firmware_metadata_begin(struct apple_dcp *dcp) +{ + unsigned long flags; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + dcp->chunks.metadata_ticket = dcp_metadata_begin(&dcp->metadata.epoch); + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + dcp->chunks.received = 0; + dcp->chunks.contiguous = true; +} + +static void dcp_firmware_metadata_notify(struct apple_dcp *dcp) +{ + unsigned long flags; + bool ready; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + ready = dcp->metadata.attributes_valid && dcp->metadata.timing_valid && + dcp_metadata_matches(&dcp->metadata.epoch, dcp->metadata.attributes_ticket) && + dcp_metadata_matches(&dcp->metadata.epoch, dcp->metadata.timing_ticket); + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (ready && dcp->connector && READ_ONCE(dcp->connector->connected)) + schedule_work(&dcp->connector->hotplug_wq); +} + +static bool dcp_firmware_metadata_complete(struct apple_dcp *dcp) +{ + return dcp->chunks.contiguous && dcp->chunks.received == dcp->chunks.length && + dcp_metadata_matches(&dcp->metadata.epoch, dcp->chunks.metadata_ticket); +} + +void dcp_firmware_metadata_attributes(struct apple_dcp *dcp, + const struct dcp_display_metadata *attributes) +{ + unsigned long flags; + + if (!dcp_firmware_metadata_enabled(dcp)) + return; + spin_lock_irqsave(&dcp->metadata.lock, flags); + if (dcp_metadata_matches(&dcp->metadata.epoch, dcp->chunks.metadata_ticket)) { + dcp->metadata.attributes_valid = attributes && dcp_firmware_metadata_complete(dcp); + if (dcp->metadata.attributes_valid) { + dcp->metadata.attributes = *attributes; + dcp->metadata.attributes_ticket = dcp->chunks.metadata_ticket; + } + } + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + dcp_firmware_metadata_notify(dcp); +} + +void dcp_firmware_metadata_timing(struct apple_dcp *dcp) +{ + unsigned long flags; + unsigned int i; + + if (!dcp_firmware_metadata_enabled(dcp)) + return; + spin_lock_irqsave(&dcp->metadata.lock, flags); + if (!dcp_metadata_matches(&dcp->metadata.epoch, dcp->chunks.metadata_ticket)) + goto unlock; + dcp->metadata.timing_valid = false; + if (!dcp_firmware_metadata_complete(dcp)) + goto unlock; + for (i = 0; i < dcp->nr_modes; i++) { + if (!(dcp->modes[i].mode.type & DRM_MODE_TYPE_PREFERRED) || + !dcp->modes[i].metadata_timing_valid) + continue; + dcp->metadata.preferred = dcp->modes[i].mode; + dcp->metadata.timing_ticket = dcp->chunks.metadata_ticket; + dcp->metadata.timing_valid = true; + break; + } +unlock: + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + dcp_firmware_metadata_notify(dcp); +} + +bool dcp_firmware_metadata_current(struct apple_dcp *dcp, u64 generation) +{ + unsigned long flags; + bool valid; + + spin_lock_irqsave(&dcp->metadata.lock, flags); + valid = !dcp->metadata.epoch.invalidated && dcp->metadata.epoch.connected && + generation == dcp->metadata.epoch.generation; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + return valid; +} + +const struct drm_edid *dcp_firmware_edid(struct apple_dcp *dcp, u64 *generation) +{ + struct dcp_display_metadata attributes; + struct drm_display_mode preferred; + const struct drm_edid *edid; + unsigned long flags; + bool valid; + + if (!dcp_firmware_metadata_enabled(dcp)) + return ERR_PTR(-ENODEV); + spin_lock_irqsave(&dcp->metadata.lock, flags); + valid = dcp->metadata.attributes_valid && dcp->metadata.timing_valid && + dcp_metadata_matches(&dcp->metadata.epoch, dcp->metadata.attributes_ticket) && + dcp_metadata_matches(&dcp->metadata.epoch, dcp->metadata.timing_ticket); + if (valid) { + attributes = dcp->metadata.attributes; + preferred = dcp->metadata.preferred; + *generation = dcp->metadata.epoch.generation; + } + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (!valid) + return ERR_PTR(-ENODATA); + edid = dcp_metadata_edid(&attributes, &preferred); + spin_lock_irqsave(&dcp->metadata.lock, flags); + valid = !dcp->metadata.epoch.invalidated && dcp->metadata.epoch.connected && + *generation == dcp->metadata.epoch.generation; + spin_unlock_irqrestore(&dcp->metadata.lock, flags); + if (!valid && !IS_ERR(edid)) { + drm_edid_free(edid); + return ERR_PTR(-ESTALE); + } + return edid; +} + void dcp_set_dimensions(struct apple_dcp *dcp) { int i; @@ -2387,12 +2553,13 @@ static void dcp_typec_reconnect_work(struct work_struct *work) static void disconnected_hpd_event(struct apple_connector *con) { - if (con && con->connected) { - con->connected = 0; - drm_edid_free(con->drm_edid); - con->drm_edid = NULL; - drm_kms_helper_connector_hotplug_event(&con->base); - } + if (!con) + return; + + /* May run with DRM modeset locks held; defer property/cache cleanup. */ + WRITE_ONCE(con->connected, false); + WRITE_ONCE(con->edid_invalidation_pending, true); + schedule_work(&con->hotplug_wq); } static int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port) @@ -2419,6 +2586,7 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) guard(mutex)(&dcp->hpd_mutex); dcp->typec_generation++; WRITE_ONCE(dcp->typec_cable_connected, true); + dcp_firmware_metadata_connection(dcp, dcp->typec_generation, true); dcp->typec_reconnect_tries = 0; dcp->placeholder_retried = false; cancel_delayed_work(&dcp->typec_reconnect_wq); @@ -2440,6 +2608,7 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) scoped_guard(mutex, &dcp->hpd_mutex) { WRITE_ONCE(dcp->typec_cable_connected, false); dcp->typec_generation++; + dcp_firmware_metadata_connection(dcp, dcp->typec_generation, false); } WRITE_ONCE(dcp->typec_crtc_off, false); reinit_completion(&dcp->typec_iomfb_hpd_ready); @@ -3364,6 +3533,7 @@ static void dcp_disable_typec_work(struct apple_dcp *dcp, bool release_cable) if (release_cable) WRITE_ONCE(dcp->typec_cable_connected, false); dcp->typec_generation++; + dcp_firmware_metadata_connection(dcp, dcp->typec_generation, false); } /* Block new enqueues as well as draining users of the AFK endpoints. */ disable_delayed_work_sync(&dcp->typec_reconnect_wq); @@ -3751,6 +3921,7 @@ static int dcp_platform_probe(struct platform_device *pdev) mutex_init(&dcp->hpd_mutex); mutex_init(&dcp->tb_lock); spin_lock_init(&dcp->mode_state.lock); + spin_lock_init(&dcp->metadata.lock); spin_lock_init(&dcp->dcpavserv.lock); dcp->hw = *(struct apple_dcp_hw_data *)of_device_get_match_data(dev); if (dcp->hw.iomfb_method_profile == diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 50b240511442b..c10821fe2a538 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -736,6 +736,23 @@ void dcp_retrain_oob(struct apple_connector *connector) schedule_work(&connector->hotplug_wq); } +static void dcp_clear_connector_edid(struct apple_connector *connector) +{ + lockdep_assert_held(&connector->base.dev->mode_config.mutex); + drm_edid_connector_update(&connector->base, NULL); + drm_edid_free(connector->drm_edid); + connector->drm_edid = NULL; + connector->drm_edid_firmware_derived = false; +} + +static void dcp_consume_edid_invalidation(struct apple_connector *connector) +{ + lockdep_assert_held(&connector->base.dev->mode_config.mutex); + /* Preserve a disconnect even if the port reconnected before probing. */ + if (xchg(&connector->edid_invalidation_pending, false)) + dcp_clear_connector_edid(connector); +} + void dcp_hotplug(struct work_struct *work) { struct apple_connector *connector; @@ -744,9 +761,17 @@ void dcp_hotplug(struct work_struct *work) int ret; connector = container_of(work, struct apple_connector, hotplug_wq); + scoped_guard(mutex, &connector->base.dev->mode_config.mutex) + dcp_consume_edid_invalidation(connector); pdev = READ_ONCE(connector->dcp); if (!pdev) { /* a Type-C port unrouted after this was queued */ + scoped_guard(mutex, &connector->base.dev->mode_config.mutex) { + if (!READ_ONCE(connector->dcp) && connector->drm_edid_firmware_derived) { + drm_edid_connector_update(&connector->base, NULL); + connector->drm_edid_firmware_derived = false; + } + } drm_kms_helper_connector_hotplug_event(&connector->base); return; } @@ -754,9 +779,9 @@ void dcp_hotplug(struct work_struct *work) dev_info(dcp->dev, "%s() connected:%d valid_mode:%d nr_modes:%u\n", __func__, connector->connected, READ_ONCE(dcp->mode_state.valid), dcp->nr_modes); - if (!connector->connected) { - drm_edid_free(connector->drm_edid); - connector->drm_edid = NULL; + scoped_guard(mutex, &connector->base.dev->mode_config.mutex) { + if (!READ_ONCE(connector->connected)) + dcp_clear_connector_edid(connector); } /* @@ -990,9 +1015,16 @@ int dcp_get_modes(struct drm_connector *connector) bool vrr_capable = false; int i; + dcp_consume_edid_invalidation(apple_connector); + /* A Type-C port has no pipeline while the fabric is moving it. */ - if (!pdev) + if (!pdev) { + if (apple_connector->drm_edid_firmware_derived) { + drm_edid_connector_update(connector, NULL); + apple_connector->drm_edid_firmware_derived = false; + } return 0; + } dcp = platform_get_drvdata(pdev); for (i = 0; i < dcp->nr_modes; ++i) { @@ -1038,8 +1070,34 @@ int dcp_get_modes(struct drm_connector *connector) } } if (dcp->nr_modes && apple_connector->drm_edid) { + apple_connector->drm_edid_firmware_derived = false; drm_edid_connector_update(connector, apple_connector->drm_edid); dcp_retry_placeholder_edid(dcp, apple_connector->drm_edid); + } else { + u64 generation = 0; + const struct drm_edid *derived = dcp->nr_modes ? + dcp_firmware_edid(dcp, &generation) : ERR_PTR(-ENODATA); + + if (!IS_ERR(derived)) { + bool log = !apple_connector->drm_edid_firmware_derived || + apple_connector->drm_edid_generation != generation; + int update; + + apple_connector->drm_edid_firmware_derived = true; + apple_connector->drm_edid_generation = generation; + update = drm_edid_connector_update(connector, derived); + drm_edid_free(derived); + if (update || !dcp_firmware_metadata_current(dcp, generation)) { + drm_edid_connector_update(connector, NULL); + apple_connector->drm_edid_firmware_derived = false; + } else if (log) { + dev_info(dcp->dev, "using firmware-derived EDID metadata for connection %llu\n", + generation); + } + } else if (apple_connector->drm_edid_firmware_derived) { + drm_edid_connector_update(connector, NULL); + apple_connector->drm_edid_firmware_derived = false; + } } /* diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 248ee966e33a6..03438d24a7557 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -941,6 +941,7 @@ static u8 dcpep_cb_prop_start(struct apple_dcp *dcp, u32 *length) #endif dcp->chunks.length = *length; + dcp_firmware_metadata_begin(dcp); dcp->chunks.data = kzalloc(*length, GFP_KERNEL); if (!dcp->chunks.data) { @@ -959,11 +960,17 @@ static u8 dcpep_cb_prop_chunk(struct apple_dcp *dcp, return false; } - if (req->offset + req->length > dcp->chunks.length) { + if (req->offset > dcp->chunks.length || req->length > sizeof(req->data) || + req->length > dcp->chunks.length - req->offset) { + dcp->chunks.contiguous = false; dev_warn(dcp->dev, "ignoring overflowing chunk\n"); return false; } + if (req->offset != dcp->chunks.received) + dcp->chunks.contiguous = false; + if (dcp->chunks.contiguous) + dcp->chunks.received += req->length; memcpy(dcp->chunks.data + req->offset, req->data, req->length); return true; } @@ -1002,11 +1009,15 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, dev_warn(dcp->dev, "failed to parse modes\n"); dcp->modes = NULL; dcp->nr_modes = 0; + dcp_firmware_metadata_timing(dcp); return false; } if (dcp->nr_modes == 0) dev_warn(dcp->dev, "TimingElements without valid modes!\n"); + dcp_firmware_metadata_timing(dcp); } else if (!strcmp(req->key, "DisplayAttributes")) { + struct dcp_parse_ctx metadata_ctx = ctx; + ret = parse_display_attributes(&ctx, &dcp->width_mm, &dcp->height_mm); @@ -1016,6 +1027,12 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, } dcp_set_dimensions(dcp); + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) { + struct dcp_display_metadata metadata; + + ret = parse_display_metadata(&metadata_ctx, &metadata); + dcp_firmware_metadata_attributes(dcp, ret ? NULL : &metadata); + } } return true; @@ -1871,6 +1888,7 @@ static void dcpep_cb_hotplug(struct apple_dcp *dcp, u64 *connected) if (dcp_is_typec_output(dcp) && *connected && dcp->nr_modes) complete_all(&dcp->typec_iomfb_hpd_ready); + dcp_firmware_metadata_hotplug(dcp, !!(*connected)); action = dcp_mode_hotplug(&dcp->mode_state, !!(*connected), connector ? &connector->connected : NULL); /* diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 691bc91e3542b..b723fba76d456 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -299,7 +299,7 @@ int parse(const void *blob, size_t size, struct dcp_parse_ctx *ctx) return 0; } -static int parse_dimension(struct dcp_parse_ctx *handle, struct dimension *dim) +static int parse_dimension(struct dcp_parse_ctx *handle, struct dimension *dim, bool polarity) { struct iterator it; int ret = 0; @@ -310,6 +310,8 @@ static int parse_dimension(struct dcp_parse_ctx *handle, struct dimension *dim) .front_porch = -1, .sync_width = -1, .precise_sync_rate = -1, + .sync_polarity = -1, + .pixel_repetition = -1, }; ret = iterator_begin(handle, &it, true); @@ -331,6 +333,18 @@ static int parse_dimension(struct dcp_parse_ctx *handle, struct dimension *dim) ret = parse_int_bound(it.handle, &dim->sync_width, 0, U16_MAX); else if (!strcmp(key, "PreciseSyncRate")) ret = parse_int_bound(it.handle, &dim->precise_sync_rate, 1, U32_MAX); + else if (polarity && !strcmp(key, "SyncPolarity")) { + if (dim->sync_polarity >= 0) + ret = -EINVAL; + else + ret = parse_int_bound(it.handle, &dim->sync_polarity, 0, 1); + } else if (polarity && !strcmp(key, "PixelRepetition")) { + if (dim->pixel_repetition >= 0) + ret = -EINVAL; + else + ret = parse_int_bound(it.handle, &dim->pixel_repetition, + 0, U32_MAX); + } else ret = skip(it.handle); @@ -626,6 +640,8 @@ static int parse_mode(struct dcp_parse_ctx *handle, s64 id = -1; s64 best_color_mode = -1; bool is_virtual = false; + bool preferred = false, interlaced = false; + bool have_preferred = false, have_interlaced = false; struct drm_display_mode *mode = &out->mode; *out = (struct dcp_display_mode) { @@ -656,9 +672,9 @@ static int parse_mode(struct dcp_parse_ctx *handle, else if (is_virtual) ret = skip(it.handle); else if (!strcmp(key, "HorizontalAttributes")) - ret = parse_dimension(it.handle, &horiz); + ret = parse_dimension(it.handle, &horiz, external_sdr); else if (!strcmp(key, "VerticalAttributes")) - ret = parse_dimension(it.handle, &vert); + ret = parse_dimension(it.handle, &vert, external_sdr); else if (!strcmp(key, "MinimumVariableRefreshRate")) ret = parse_int_bound(it.handle, &min_vrr, 0, U32_MAX); else if (!strcmp(key, "MaximumVariableRefreshRate")) @@ -669,7 +685,19 @@ static int parse_mode(struct dcp_parse_ctx *handle, ret = parse_int_bound(it.handle, &id, 0, U32_MAX); else if (!strcmp(key, "IsVirtual")) ret = parse_bool(it.handle, &is_virtual); - else if (!strcmp(key, "Score")) + else if (external_sdr && !strcmp(key, "IsPreferred")) { + if (have_preferred) + ret = -EINVAL; + else + ret = parse_bool(it.handle, &preferred); + have_preferred = true; + } else if (external_sdr && !strcmp(key, "IsInterlaced")) { + if (have_interlaced) + ret = -EINVAL; + else + ret = parse_bool(it.handle, &interlaced); + have_interlaced = true; + } else if (!strcmp(key, "Score")) ret = parse_int_bound(it.handle, score, 0, S64_MAX); else ret = skip(it.handle); @@ -755,6 +783,12 @@ static int parse_mode(struct dcp_parse_ctx *handle, .height_mm = height_mm, }; + if (external_sdr && horiz.sync_polarity >= 0 && vert.sync_polarity >= 0) + mode->flags = (horiz.sync_polarity ? DRM_MODE_FLAG_PHSYNC : DRM_MODE_FLAG_NHSYNC) | + (vert.sync_polarity ? DRM_MODE_FLAG_PVSYNC : DRM_MODE_FLAG_NVSYNC); + out->metadata_timing_valid = external_sdr && have_preferred && preferred && + have_interlaced && !interlaced && horiz.pixel_repetition == 0 && + vert.pixel_repetition == 0 && horiz.sync_polarity >= 0 && vert.sync_polarity >= 0; drm_mode_set_name(mode); out->timing_mode_id = id; @@ -861,6 +895,238 @@ int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, return 0; } +/* Metadata strings must fit an EDID text descriptor without hidden suffixes. */ +static int parse_metadata_string(struct dcp_parse_ctx *handle, char *out, size_t size) +{ + const struct dcp_parse_tag *tag = parse_tag_of_type(handle, DCP_TYPE_STRING); + const u8 *value; + + if (IS_ERR(tag)) + return PTR_ERR(tag); + if (!tag->size || tag->size >= size) + return -EINVAL; + value = parse_bytes(handle, tag->size); + if (IS_ERR(value)) + return PTR_ERR(value); + for (unsigned int i = 0; i < tag->size; i++) + if (value[i] < 0x20 || value[i] > 0x7e) + return -EINVAL; + memcpy(out, value, tag->size); + out[tag->size] = 0; + return 0; +} + +static int parse_metadata_number(struct dcp_parse_ctx *handle, u32 *out, u32 min, u32 max) +{ + s64 value; + int ret = parse_int_bound(handle, &value, min, max); + + if (!ret) + *out = value; + return ret; +} + +static int parse_metadata_point(struct dcp_parse_ctx *handle, u32 point[2], u32 *gamma) +{ + struct iterator it; + unsigned int seen = 0; + int ret = iterator_begin(handle, &it, true); + + if (ret) + return ret; + for (; it.idx < it.len; it.idx++) { + char key[64]; + + ret = parse_metadata_string(handle, key, sizeof(key)); + if (ret) + return ret; + if (!strcmp(key, "X")) { + if (seen & 1) + return -EINVAL; + seen |= 1; + ret = parse_metadata_number(handle, &point[0], 1, 65535); + } else if (!strcmp(key, "Y")) { + if (seen & 2) + return -EINVAL; + seen |= 2; + ret = parse_metadata_number(handle, &point[1], 1, 65535); + } else if (gamma && !strcmp(key, "Gamma")) { + if (seen & 4) + return -EINVAL; + seen |= 4; + ret = parse_metadata_number(handle, gamma, 65536, (354U << 16) / 100); + } else { + ret = skip(handle); + } + if (ret) + return ret; + } + if (seen != (gamma ? 7 : 3) || point[0] + point[1] > 65536) + return -EINVAL; + return 0; +} + +static int parse_metadata_chromaticity(struct dcp_parse_ctx *handle, + struct dcp_display_metadata *out) +{ + static const char * const names[] = { "Red", "Green", "Blue" }; + struct iterator it; + unsigned int seen = 0; + int ret = iterator_begin(handle, &it, true); + + if (ret) + return ret; + for (; it.idx < it.len; it.idx++) { + char key[64]; + unsigned int color; + + ret = parse_metadata_string(handle, key, sizeof(key)); + if (ret) + return ret; + for (color = 0; color < ARRAY_SIZE(names); color++) + if (!strcmp(key, names[color])) + break; + if (color < ARRAY_SIZE(names)) { + if (seen & 1U << color) + return -EINVAL; + seen |= 1U << color; + ret = parse_metadata_point(handle, out->chromaticity[color], NULL); + } else { + ret = skip(handle); + } + if (ret) + return ret; + } + return seen == 7 ? 0 : -EINVAL; +} + +static int parse_metadata_product(struct dcp_parse_ctx *handle, struct dcp_display_metadata *out) +{ + struct iterator it; + unsigned int seen = 0; + u32 legacy = 0; + int ret = iterator_begin(handle, &it, true); + + if (ret) + return ret; + for (; it.idx < it.len; it.idx++) { + char key[64]; + + ret = parse_metadata_string(handle, key, sizeof(key)); + if (ret) + return ret; + if (!strcmp(key, "ManufacturerID")) { + if (seen & 1) + return -EINVAL; + seen |= 1; + ret = parse_metadata_string(handle, out->manufacturer, + sizeof(out->manufacturer)); + } else if (!strcmp(key, "ProductName")) { + if (seen & 2) + return -EINVAL; + seen |= 2; + ret = parse_metadata_string(handle, out->name, sizeof(out->name)); + } else if (!strcmp(key, "ProductID")) { + if (seen & 4) + return -EINVAL; + seen |= 4; + ret = parse_metadata_number(handle, &out->product, 0, U16_MAX); + } else if (!strcmp(key, "WeekOfManufacture")) { + if (seen & 8) + return -EINVAL; + seen |= 8; + ret = parse_metadata_number(handle, &out->week, 0, 53); + } else if (!strcmp(key, "YearOfManufacture")) { + if (seen & 16) + return -EINVAL; + seen |= 16; + ret = parse_metadata_number(handle, &out->year, 1990, 2245); + } else if (!strcmp(key, "SerialNumber")) { + if (seen & 64) + return -EINVAL; + seen |= 64; + ret = parse_metadata_number(handle, &out->serial, 0, U32_MAX); + } else if (!strcmp(key, "AlphanumericSerialNumber")) { + if (seen & 128) + return -EINVAL; + seen |= 128; + ret = parse_metadata_string(handle, out->serial_text, + sizeof(out->serial_text)); + } else if (!strcmp(key, "LegacyManufacturerID")) { + if (seen & 32) + return -EINVAL; + seen |= 32; + ret = parse_metadata_number(handle, &legacy, 0, 0x7fff); + } else { + ret = skip(handle); + } + if (ret) + return ret; + } + if ((seen & 31) != 31 || strlen(out->manufacturer) != 3) + return -EINVAL; + for (unsigned int i = 0; i < 3; i++) + if (out->manufacturer[i] < 'A' || out->manufacturer[i] > 'Z') + return -EINVAL; + if ((seen & 32) && legacy != ((out->manufacturer[0] - '@') << 10 | + (out->manufacturer[1] - '@') << 5 | + (out->manufacturer[2] - '@'))) + return -EINVAL; + return 0; +} + +int parse_display_metadata(struct dcp_parse_ctx *handle, struct dcp_display_metadata *out) +{ + struct dcp_display_metadata parsed = {}; + struct iterator it; + unsigned int seen = 0; + int ret = iterator_begin(handle, &it, true); + + if (ret) + return ret; + for (; it.idx < it.len; it.idx++) { + char key[64]; + + ret = parse_metadata_string(handle, key, sizeof(key)); + if (ret) + return ret; + if (!strcmp(key, "MaxHorizontalImageSize")) { + if (seen & 1) + return -EINVAL; + seen |= 1; + ret = parse_metadata_number(handle, &parsed.width_cm, 1, 255); + } else if (!strcmp(key, "MaxVerticalImageSize")) { + if (seen & 2) + return -EINVAL; + seen |= 2; + ret = parse_metadata_number(handle, &parsed.height_cm, 1, 255); + } else if (!strcmp(key, "Chromaticity")) { + if (seen & 4) + return -EINVAL; + seen |= 4; + ret = parse_metadata_chromaticity(handle, &parsed); + } else if (!strcmp(key, "DefaultWhitePoint")) { + if (seen & 8) + return -EINVAL; + seen |= 8; + ret = parse_metadata_point(handle, parsed.white, &parsed.gamma); + } else if (!strcmp(key, "ProductAttributes")) { + if (seen & 16) + return -EINVAL; + seen |= 16; + ret = parse_metadata_product(handle, &parsed); + } else { + ret = skip(handle); + } + if (ret) + return ret; + } + if (seen != 31) + return -EINVAL; + *out = parsed; + return 0; +} + int parse_epic_service_init(struct dcp_parse_ctx *handle, const char **name, const char **class, s64 *unit, bool h17p_keys) { diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 1f3b8782b7bfc..50e0580f98080 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -7,6 +7,8 @@ /* For mode parsing */ #include +#include "dcp-display-metadata.h" + struct apple_dcp; struct dcp_parse_ctx { @@ -94,11 +96,14 @@ struct dcp_display_mode { struct dcp_color_mode sdr; struct dcp_color_mode best; bool vrr; + bool metadata_timing_valid; }; struct dimension { s64 total, front_porch, sync_width, active; s64 precise_sync_rate; + s64 sync_polarity; + s64 pixel_repetition; }; int parse(const void *blob, size_t size, struct dcp_parse_ctx *ctx); @@ -106,6 +111,7 @@ struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, unsigned int *count, int width_mm, int height_mm, unsigned notch_height, bool internal, bool external_sdr); +int parse_display_metadata(struct dcp_parse_ctx *handle, struct dcp_display_metadata *out); int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, int *height_mm); int parse_epic_service_init(struct dcp_parse_ctx *handle, const char **name, diff --git a/tools/diagnostics/display-attributes-fixture.json b/tools/diagnostics/display-attributes-fixture.json new file mode 100644 index 0000000000000..9324ae496d516 --- /dev/null +++ b/tools/diagnostics/display-attributes-fixture.json @@ -0,0 +1,23 @@ +{ + "MaxHorizontalImageSize": 60, + "MaxVerticalImageSize": 33, + "Chromaticity": { + "Red": {"X": 44608, "Y": 20288}, + "Green": {"X": 16960, "Y": 40448}, + "Blue": {"X": 9344, "Y": 5184} + }, + "DefaultWhitePoint": {"X": 20544, "Y": 21568, "Gamma": 144179}, + "WhitePoints": [{"X": 20544, "Y": 21568, "Gamma": 144179}], + "DefaultColorSpaceIsSRGB": true, + "SupportsPQEOTF": true, + "ProductAttributes": { + "LegacyManufacturerID": 1138, + "ProductID": 2895, + "SerialNumber": 305419896, + "WeekOfManufacture": 7, + "YearOfManufacture": 2013, + "AlphanumericSerialNumber": "ANON000000001", + "ProductName": "QG270 S3", + "ManufacturerID": "ACR" + } +} diff --git a/tools/diagnostics/test-apple-display-metadata-lifetime.py b/tools/diagnostics/test-apple-display-metadata-lifetime.py new file mode 100644 index 0000000000000..bc64678c26d5d --- /dev/null +++ b/tools/diagnostics/test-apple-display-metadata-lifetime.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise actual metadata epoch/cache and connector raw-EDID precedence paths.""" +from pathlib import Path +import os +import shlex +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +base = root / 'drivers/gpu/drm/apple' +dcp = (base / 'dcp.c').read_text() +iomfb = (base / 'iomfb.c').read_text() + + +def function(text, signature): + start = text.index(signature) + opening = text.index('{', start) + while ';' in text[start:opening]: + start = text.index(signature, start + 1) + opening = text.index('{', start) + end, depth = opening + 1, 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + return text[start:end] + + +metadata = function((base / 'dcp-display-metadata.h').read_text(), 'struct dcp_display_metadata {') + ';' +epoch = (base / 'dcp-metadata-epoch.h').read_text().replace('#include ', '') +cache = dcp[dcp.index('static bool dcp_firmware_metadata_enabled('):dcp.index('void dcp_set_dimensions(')] +getter = function(iomfb, 'int dcp_get_modes(') +cleanup = function(iomfb, 'static void dcp_clear_connector_edid(') + '\n' + function(iomfb, 'static void dcp_consume_edid_invalidation(') +hotplug = function(iomfb, 'void dcp_hotplug(') +disconnect = function(dcp, 'static void disconnected_hpd_event(') +chunk_callbacks = function((base / 'iomfb_template.c').read_text(), 'static u8 dcpep_cb_prop_start(') + '\n' + function((base / 'iomfb_template.c').read_text(), 'static u8 dcpep_cb_prop_chunk(') +# Check the actual ingress/egress hooks as well as executing the shared helpers. +assert dcp.count('dcp_firmware_metadata_connection(dcp, dcp->typec_generation, true)') == 1 +assert dcp.count('dcp_firmware_metadata_connection(dcp, dcp->typec_generation, false)') == 2 +assert dcp.count('dcp_firmware_metadata_connection(dcp, READ_ONCE(dcp->typec_generation), false)') == 2 +template = (base / 'iomfb_template.c').read_text() +assert 'dcp_firmware_metadata_begin(dcp);' in template +assert 'dcp_firmware_metadata_hotplug(dcp, !!(*connected));' in template +assert 'dcp_firmware_metadata_attributes(dcp, ret ? NULL : &metadata);' in template +assert 'dcp->chunks.received += req->length;' in template +prefix = r''' +#include +#include +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +#define READ_ONCE(x) (x) +#define WRITE_ONCE(x,v) ((x)=(v)) +#define xchg(p,v) __atomic_exchange_n(p,v,__ATOMIC_SEQ_CST) +#define lockdep_assert_held(p) assert(held_mutex==(p)) +#define ERR_PTR(n) ((void *)(intptr_t)(n)) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define IS_ERR(p) ((uintptr_t)(p) > (uintptr_t)-4096) +#define IS_ERR_OR_NULL(p) (!(p) || IS_ERR(p)) +#define DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL 2 +#define DCP_FIRMWARE_H17P 2 +#define DRM_MODE_TYPE_PREFERRED 2 +#define DCP_FW_VER 26 +#define DCP_FW_VERSION(a,b,c) (a) +#define SZ_1M (1024*1024) +#define GFP_KERNEL 0 +#define kzalloc(n,f) calloc(1,n) +#define dev_warn(...) ((void)0) +#define dev_err(...) ((void)0) +#define dev_info(...) ((void)0) +#define dev_dbg(...) ((void)0) +#define container_of(p,t,m) ((t *)((char *)(p) - offsetof(t,m))) +typedef struct { bool held; } spinlock_t; +#define spin_lock_irqsave(p, flags) do { assert(!(p)->held); (p)->held=true; (flags)=0; } while (0) +#define spin_unlock_irqrestore(p, flags) do { (void)(flags); assert((p)->held); (p)->held=false; } while (0) +struct drm_display_mode { unsigned int type; int clock; }; +struct dcp_display_mode { struct drm_display_mode mode; bool vrr, metadata_timing_valid; u32 min_vrr, max_vrr; }; +struct work_struct { unsigned int scheduled; }; +struct mutex { pthread_mutex_t native; }; +static _Thread_local struct mutex *held_mutex; +static _Thread_local bool modeset_held; +static void mutex_lock(struct mutex *m) { assert(!modeset_held); assert(held_mutex != m); assert(!pthread_mutex_lock(&m->native)); held_mutex=m; } +static void mutex_unlock(struct mutex *m) { assert(held_mutex==m); held_mutex=NULL; assert(!pthread_mutex_unlock(&m->native)); } +static struct mutex *acquire(struct mutex *m) { mutex_lock(m); return m; } +static void release(struct mutex **m) { if (*m) mutex_unlock(*m); } +static struct mutex *once(struct mutex *m) { mutex_unlock(m); return NULL; } +#define scoped_guard(kind,value) for(struct mutex *g __attribute__((cleanup(release)))=acquire(value); g; g=once(g)) +struct test_blob { unsigned int identity, references, magic; }; +struct drm_device { void *dev; struct { struct mutex mutex; } mode_config; }; +struct drm_connector { struct drm_device *dev; void *state; struct test_blob *edid_blob_ptr; struct { struct { unsigned int min_vfreq, max_vfreq; } monitor_range; } display_info; }; +struct dcp_set_dcpav_prop_chunk_req { u8 data[4096]; u32 offset, length; }; +struct apple_dcp; +struct platform_device { struct apple_dcp *data; }; +struct drm_edid { unsigned int identity; }; +struct apple_connector { + struct drm_connector base; + struct platform_device *dcp; + const struct drm_edid *drm_edid; + bool drm_edid_firmware_derived; + bool edid_invalidation_pending; + u64 drm_edid_generation; + bool connected; + struct work_struct hotplug_wq; +}; +#define to_apple_connector(p) container_of(p, struct apple_connector, base) +#define platform_get_drvdata(p) ((p)->data) +''' +state = r''' +struct apple_dcp { + void *dev; + struct { int iomfb_method_profile; } hw; + struct apple_connector *connector; + struct { void *data; size_t length, received; bool contiguous; struct dcp_metadata_ticket metadata_ticket; } chunks; + struct { + spinlock_t lock; + struct dcp_metadata_epoch epoch; + struct dcp_display_metadata attributes; + struct drm_display_mode preferred; + struct dcp_metadata_ticket attributes_ticket, timing_ticket; + bool attributes_valid, timing_valid, hpd_seen; + } metadata; + struct dcp_display_mode *modes; + unsigned int nr_modes; + int fw_compat; + struct { bool valid; } mode_state; + struct { bool inline_v2; } *dcpavservep; +}; +static struct apple_dcp *current_for_update; +static unsigned int allocations, raw_reads, mode_adds, clears, property_identity, blob_allocations, notifications, retrains; +static bool invalidate_during_update; +static bool update_error; +static bool raw_available; +static const struct drm_edid *allocate(unsigned int identity) +{ + struct drm_edid *edid = malloc(sizeof(*edid)); assert(edid); + edid->identity = identity; allocations++; return edid; +} +static void drm_edid_free(const struct drm_edid *edid) +{ if (edid) { assert(allocations); allocations--; free((void *)edid); } } +static const struct drm_edid *dcp_metadata_edid(const struct dcp_display_metadata *m, + const struct drm_display_mode *mode) +{ assert(mode->clock); return allocate(m->serial); } +static void schedule_work(struct work_struct *w) { w->scheduled++; } +static struct drm_display_mode *drm_mode_duplicate(struct drm_device *d, const struct drm_display_mode *m) +{ (void)d; struct drm_display_mode *copy=malloc(sizeof(*copy)); *copy=*m; return copy; } +static void drm_mode_probed_add(struct drm_connector *c, struct drm_display_mode *m) +{ (void)c; mode_adds++; free(m); } +static void drm_connector_set_vrr_capable_property(struct drm_connector *c, bool v) +{ (void)c; (void)v; } +static bool dcp_has_panel(struct apple_dcp *p) { (void)p; return false; } +static const struct drm_edid *dcpavserv_copy_edid(struct apple_dcp *p) +{ (void)p; raw_reads++; return raw_available ? allocate(0xfeed) : ERR_PTR(-ENODEV); } +static int drm_edid_connector_update(struct drm_connector *c, const struct drm_edid *e) +{ + assert(held_mutex==&c->dev->mode_config.mutex); + if (e && update_error) return -ENOMEM; + struct test_blob *old=c->edid_blob_ptr; + struct test_blob *next=NULL; + if (e) { + next=malloc(sizeof(*next)); assert(next); + *next=(struct test_blob){ .identity=e->identity, .references=1, .magic=0xcafe }; + blob_allocations++; + } else clears++; + if (old) { assert(old->magic==0xcafe && old->references==1); old->magic=0; free(old); blob_allocations--; } + c->edid_blob_ptr=next; + property_identity=e ? e->identity : 0; + if (invalidate_during_update && e && e->identity!=0xfeed) current_for_update->metadata.epoch.invalidated=true; + return 0; +} +static bool dcp_is_usb4_output(struct apple_dcp *p) { (void)p; return false; } +static bool apple_dp_tunnel_t602x(void) { return false; } +#define DRM_MODE_LINK_STATUS_BAD 1 +static void drm_connector_set_link_status_property(struct drm_connector *c, int status) { (void)c; (void)status; } +static int dcp_retrain_active_crtc(struct apple_connector *c) +{ + assert(held_mutex!=&c->base.dev->mode_config.mutex); + retrains++; + return 0; +} +static void drm_kms_helper_connector_hotplug_event(struct drm_connector *c) +{ + assert(held_mutex!=&c->dev->mode_config.mutex); + mutex_lock(&c->dev->mode_config.mutex); + if (c->edid_blob_ptr) assert(c->edid_blob_ptr->magic==0xcafe); + notifications++; + mutex_unlock(&c->dev->mode_config.mutex); +} +static void dcp_retry_placeholder_edid(struct apple_dcp *p, const struct drm_edid *e) +{ (void)p; assert(e->identity==0xfeed); } +''' +suffix = r''' +static struct apple_dcp p; +static struct apple_connector c; +static struct platform_device platform; +static struct drm_device drm={ .mode_config.mutex.native=PTHREAD_MUTEX_INITIALIZER }; +static struct dcp_display_mode modes[1] = {{ .mode={ .type=DRM_MODE_TYPE_PREFERRED, .clock=148500 }, .metadata_timing_valid=true }}; +static struct dcp_display_metadata attrs = { .serial=1234 }; +static int probe(struct drm_connector *connector) +{ + mutex_lock(&connector->dev->mode_config.mutex); + int ret=dcp_get_modes(connector); + mutex_unlock(&connector->dev->mode_config.mutex); + return ret; +} +static void initial(void) +{ + mutex_lock(&drm.mode_config.mutex); + drm_edid_free(c.drm_edid); + c.base.dev=&drm; + drm_edid_connector_update(&c.base,NULL); + mutex_unlock(&drm.mode_config.mutex); + free(p.chunks.data); + assert(!allocations); + memset(&p,0,sizeof(p)); memset(&c,0,sizeof(c)); + p.hw.iomfb_method_profile=DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; + p.fw_compat=DCP_FIRMWARE_H17P; + static typeof(*p.dcpavservep) av={true}; p.dcpavservep=&av; + p.connector=&c; p.modes=modes; p.nr_modes=1; + platform.data=&p; c.dcp=&platform; c.base.dev=&drm; c.connected=true; + p.mode_state.valid=true; + raw_reads=mode_adds=clears=property_identity=0; raw_available=false; + current_for_update=&p; invalidate_during_update=false; update_error=false; modes[0].metadata_timing_valid=true; +} +static void begin(bool complete) +{ + dcp_firmware_metadata_begin(&p); + p.chunks.length=100; p.chunks.received=complete ? 100 : 99; +} +static bool eligible(void) +{ + u64 generation; + const struct drm_edid *edid=dcp_firmware_edid(&p,&generation); + bool valid=!IS_ERR(edid); + if (valid) { assert(generation==1 && edid->identity==attrs.serial); drm_edid_free(edid); } + return valid; +} +static void publish(void) +{ + begin(true); dcp_firmware_metadata_attributes(&p,&attrs); + begin(true); dcp_firmware_metadata_timing(&p); +} +static void *probe_thread(void *unused) +{ + (void)unused; + for(unsigned int i=0;i<1000;i++) { + mutex_lock(&drm.mode_config.mutex); + c.connected=true; raw_available=!!(i&1); + assert(dcp_get_modes(&c.base)==1); + mutex_unlock(&drm.mode_config.mutex); + } + return NULL; +} +static void *hotplug_thread(void *unused) +{ + (void)unused; + for(unsigned int i=0;i<1000;i++) { + mutex_lock(&drm.mode_config.mutex); c.connected=false; mutex_unlock(&drm.mode_config.mutex); + dcp_hotplug(&c.hotplug_wq); + } + return NULL; +} +static void *reader_thread(void *unused) +{ + (void)unused; + for(unsigned int i=0;i<2000;i++) { + mutex_lock(&drm.mode_config.mutex); + struct test_blob *b=c.base.edid_blob_ptr; + if(b) { + assert(b->magic==0xcafe && b->references==1); + b->references++; + assert(b->identity==1234 || b->identity==0xfeed); + b->references--; + } + mutex_unlock(&drm.mode_config.mutex); + } + return NULL; +} +int main(void) +{ + initial(); begin(true); /* A pre-association blob cannot become current later. */ + dcp_firmware_metadata_connection(&p,1,true); + dcp_firmware_metadata_attributes(&p,&attrs); + begin(true); dcp_firmware_metadata_timing(&p); + assert(!eligible()); + begin(true); dcp_firmware_metadata_attributes(&p,&attrs); + assert(eligible() && c.hotplug_wq.scheduled); + /* Raw failure admits fallback, but fallback never blocks a later raw read. */ + assert(probe(&c.base)==1 && c.drm_edid_firmware_derived && raw_reads==1); + assert(!c.drm_edid && property_identity==1234 && !allocations && mode_adds==1); + raw_available=true; + assert(probe(&c.base)==1 && !c.drm_edid_firmware_derived && raw_reads==2); + assert(c.drm_edid->identity==0xfeed && mode_adds==2); + assert(probe(&c.base)==1 && raw_reads==2); /* A raw cache remains authoritative. */ + + initial(); dcp_firmware_metadata_connection(&p,1,true); + begin(false); dcp_firmware_metadata_attributes(&p,&attrs); + begin(true); dcp_firmware_metadata_timing(&p); assert(!eligible()); + begin(true); p.chunks.contiguous=false; dcp_firmware_metadata_attributes(&p,&attrs); + assert(!eligible()); + begin(true); dcp_firmware_metadata_attributes(&p,&attrs); assert(eligible()); + assert(probe(&c.base)==1 && c.drm_edid_firmware_derived); + dcp_firmware_metadata_connection(&p,2,false); + dcp_firmware_metadata_connection(&p,3,true); + publish(); assert(!eligible()); /* First connection only, even with later complete records. */ + assert(probe(&c.base)==1 && !c.drm_edid && !c.drm_edid_firmware_derived && clears && !allocations); + + initial(); dcp_firmware_metadata_connection(&p,1,true); begin(true); + dcp_firmware_metadata_connection(&p,2,true); /* Change while a transfer is in flight. */ + dcp_firmware_metadata_attributes(&p,&attrs); assert(!eligible()); + initial(); dcp_firmware_metadata_connection(&p,1,true); + dcp_firmware_metadata_hotplug(&p,false); /* Initial no-HPD state is not a second attachment. */ + publish(); assert(eligible()); + dcp_firmware_metadata_hotplug(&p,true); + dcp_firmware_metadata_hotplug(&p,false); assert(!eligible()); + initial(); p.hw.iomfb_method_profile=0; + dcp_firmware_metadata_connection(&p,1,true); publish(); assert(!eligible()); + assert(probe(&c.base)==1 && !c.drm_edid); + initial(); dcp_firmware_metadata_connection(&p,1,true); + modes[0].metadata_timing_valid=false; publish(); assert(!eligible()); + initial(); dcp_firmware_metadata_connection(&p,1,true); publish(); + invalidate_during_update=true; + assert(probe(&c.base)==1 && !c.drm_edid_firmware_derived && !property_identity && !allocations); + initial(); dcp_firmware_metadata_connection(&p,1,true); publish(); update_error=true; + assert(probe(&c.base)==1 && !c.drm_edid_firmware_derived && !allocations); + initial(); dcp_firmware_metadata_connection(&p,1,true); + u32 length=64; + struct dcp_set_dcpav_prop_chunk_req chunk={ .length=32 }; + assert(dcpep_cb_prop_start(&p,&length)); + assert(dcpep_cb_prop_chunk(&p,&chunk)); + assert(p.chunks.received==32 && p.chunks.contiguous); + dcp_firmware_metadata_attributes(&p,&attrs); assert(!p.metadata.attributes_valid); + chunk.offset=32; assert(dcpep_cb_prop_chunk(&p,&chunk)); + dcp_firmware_metadata_attributes(&p,&attrs); assert(p.metadata.attributes_valid); + initial(); dcp_firmware_metadata_connection(&p,1,true); + assert(dcpep_cb_prop_start(&p,&length)); + assert(dcpep_cb_prop_chunk(&p,&chunk)); /* Out-of-order first chunk. */ + chunk.offset=0; assert(dcpep_cb_prop_chunk(&p,&chunk)); + assert(!p.chunks.contiguous); + dcp_firmware_metadata_attributes(&p,&attrs); assert(!p.metadata.attributes_valid); + chunk.offset=UINT32_MAX; chunk.length=8; + assert(!dcpep_cb_prop_chunk(&p,&chunk)); + initial(); dcp_firmware_metadata_connection(&p,1,true); publish(); + assert(probe(&c.base)==1); + c.dcp=NULL; + dcp_hotplug(&c.hotplug_wq); + assert(!c.drm_edid_firmware_derived && !c.base.edid_blob_ptr); + c.dcp=&platform; c.connected=true; c.base.state=&p; p.mode_state.valid=false; + dcp_hotplug(&c.hotplug_wq); assert(retrains); + p.mode_state.valid=true; + assert(probe(&c.base)==1); + modeset_held=true; disconnected_hpd_event(&c); modeset_held=false; + assert(!c.connected && c.edid_invalidation_pending && c.base.edid_blob_ptr); + dcp_hotplug(&c.hotplug_wq); + assert(!c.edid_invalidation_pending && !c.drm_edid_firmware_derived && !c.base.edid_blob_ptr); + /* A fast reconnect cannot retain a raw EDID from the previous attachment. */ + c.connected=true; raw_available=true; + assert(probe(&c.base)==1 && c.drm_edid); + unsigned int previous_reads=raw_reads; + modeset_held=true; disconnected_hpd_event(&c); modeset_held=false; + c.connected=true; + assert(probe(&c.base)==1 && raw_reads==previous_reads+1); + assert(!c.edid_invalidation_pending); + /* Worker-first ordering also consumes the invalidation after reconnect. */ + modeset_held=true; disconnected_hpd_event(&c); modeset_held=false; + c.connected=true; + dcp_hotplug(&c.hotplug_wq); + assert(!c.drm_edid && !c.edid_invalidation_pending); + assert(probe(&c.base)==1 && c.drm_edid); + initial(); dcp_firmware_metadata_connection(&p,1,true); publish(); + pthread_t a,b,reader; + assert(!pthread_create(&a,NULL,probe_thread,NULL)); + assert(!pthread_create(&b,NULL,hotplug_thread,NULL)); + assert(!pthread_create(&reader,NULL,reader_thread,NULL)); + assert(!pthread_join(a,NULL) && !pthread_join(b,NULL) && !pthread_join(reader,NULL)); + initial(); assert(!blob_allocations); + puts("PASS: mutex-serialized EDID/blob get_modes, hotplug, sysfs reader and same-epoch publication, first-connection scope, raw precedence, invalidation, no added modes"); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix='dcp-metadata-lifetime-') as tmp: + tmp = Path(tmp) + (tmp / 'test.c').write_text(prefix + metadata + epoch + state + cache + chunk_callbacks + cleanup + getter + hotplug + disconnect + suffix) + command = shlex.split(os.environ.get('CC','clang')) + command += ['-std=gnu11','-Wall','-Wextra','-Werror','-Wno-sign-compare','-O1','-g', + '-fsanitize=address,undefined','-fno-omit-frame-pointer','-pthread'] + subprocess.run(command + [str(tmp / 'test.c'),'-o',str(tmp / 'test')],check=True) + subprocess.run([str(tmp / 'test')],check=True) diff --git a/tools/diagnostics/test-apple-display-metadata.py b/tools/diagnostics/test-apple-display-metadata.py new file mode 100644 index 0000000000000..e0508bbf431d8 --- /dev/null +++ b/tools/diagnostics/test-apple-display-metadata.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise the actual metadata parser/builder and independently decode base EDID.""" +import argparse +import copy +import json +import os +from pathlib import Path +import shlex +import struct +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +base = root / 'drivers/gpu/drm/apple' +args = argparse.ArgumentParser(description=__doc__) +args.add_argument('--attributes-json', type=Path, + default=Path(__file__).with_name('display-attributes-fixture.json')) +args.add_argument('--timings-bin', type=Path, help='Optional captured TimingElements binary (not embedded in public fixtures)') +args = args.parse_args() +fixture = json.loads(args.attributes_json.read_text()) + + +def block(text, marker): + start = text.index(marker) + opening = text.index('{', start) + end, depth = opening + 1, 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + return text[start:end] + + +class ObjectPairs(list): + pass + + +def wire(value, out): + out += b'\0' * (-len(out) % 4) + if isinstance(value, bool): + out += struct.pack('', '') +timing_fixture = [{ + "HorizontalAttributes": {"Total": 2200, "FrontPorch": 88, "SyncWidth": 44, + "Active": 1920, "PreciseSyncRate": 4423680, "SyncPolarity": 1, "PixelRepetition": 0}, + "VerticalAttributes": {"Total": 1125, "FrontPorch": 4, "SyncWidth": 5, + "Active": 1080, "PreciseSyncRate": 3932160, "SyncPolarity": 1, "PixelRepetition": 0}, + "ColorModes": [{"ID": 85, "Score": 1, "IsVirtual": False, "PixelEncoding": 0, + "Colorimetry": 10, "Depth": 10, "DynamicRange": 0, "EOTF": 0}], + "ID": 62, "Score": 16224, "IsPreferred": True, "IsInterlaced": False, "IsVirtual": False, +}] +timing_raw = bytearray(struct.pack(' +#include +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int64_t s64; +typedef u16 __le16; +typedef u16 __be16; +typedef u32 __le32; +#define __packed __attribute__((packed)) +#undef static_assert +#define static_assert(c) _Static_assert(c, #c) +#define IS_ENABLED(x) 0 +#define U32_MAX UINT32_MAX +#define U16_MAX UINT16_MAX +#define S64_MAX INT64_MAX +#define S64_MIN INT64_MIN +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define round_up(n, a) (((n) + (a) - 1) & ~((a) - 1)) +#define ERR_PTR(n) ((void *)(intptr_t)(n)) +#define PTR_ERR(p) ((int)(intptr_t)(p)) +#define IS_ERR(p) ((uintptr_t)(p) > (uintptr_t)-4096) +#define IS_ERR_OR_NULL(p) (!(p) || IS_ERR(p)) +#define GFP_KERNEL 0 +#define kmalloc(n, f) malloc(n) +#define kcalloc(n, s, f) calloc(n,s) +#define kfree(p) free(p) +#define mul_u32_u32(a, b) ((u64)(u32)(a) * (u32)(b)) +#define DIV_ROUND_CLOSEST_ULL(n, d) (((n) + (d) / 2) / (d)) +#define DIV_ROUND_CLOSEST(n, d) (((n) + (d) / 2) / (d)) +#define trace_iomfb_color_mode(...) ((void)0) +#define trace_iomfb_parse_mode_fail(...) ((void)0) +#define trace_iomfb_parse_mode_success(...) ((void)0) +#define trace_iomfb_timing_mode(...) ((void)0) +#define DRM_MODE_TYPE_DRIVER 1 +#define DRM_MODE_TYPE_PREFERRED 2 +#define DRM_MODE_FLAG_PHSYNC 1 +#define DRM_MODE_FLAG_NHSYNC 2 +#define DRM_MODE_FLAG_PVSYNC 4 +#define DRM_MODE_FLAG_NVSYNC 8 +#define cpu_to_le16(v) (v) +#define cpu_to_le32(v) (v) +#define cpu_to_be16(v) __builtin_bswap16(v) +struct drm_display_mode { + unsigned int type, flags; + int clock, vdisplay, vsync_start, vsync_end, vtotal; + int hdisplay, hsync_start, hsync_end, htotal, width_mm, height_mm; +}; +static void drm_mode_set_name(struct drm_display_mode *mode) { (void)mode; } +''' +stubs = r''' +struct drm_edid { struct edid raw; }; +static const struct drm_edid *drm_edid_alloc(const void *data, size_t size) +{ + assert(size == 128); + struct drm_edid *result = malloc(sizeof(*result)); + memcpy(&result->raw, data, size); + return result; +} +static void drm_edid_free(const struct drm_edid *edid) { free((void *)edid); } +static bool drm_edid_valid(const struct drm_edid *edid) +{ + const u8 *raw = (const u8 *)&edid->raw; + unsigned int sum = 0; + for (unsigned int i = 0; i < 128; i++) sum += raw[i]; + return !(sum & 255) && !raw[126] && !memcmp(raw, "\0\xff\xff\xff\xff\xff\xff\0", 8); +} +''' +suffix = r''' +static void epochs(void) +{ + struct dcp_metadata_epoch e = {0}; + struct dcp_metadata_ticket before = dcp_metadata_begin(&e); + assert(!dcp_metadata_matches(&e, before)); + dcp_metadata_epoch_connection(&e, 1, true); + struct dcp_metadata_ticket current = dcp_metadata_begin(&e); + assert(dcp_metadata_matches(&e, current)); + assert(!dcp_metadata_matches(&e, before)); /* Never retag pre-association metadata. */ + dcp_metadata_epoch_connection(&e, 1, true); + assert(dcp_metadata_matches(&e, current)); + dcp_metadata_epoch_connection(&e, 2, false); + assert(!dcp_metadata_matches(&e, current)); + dcp_metadata_epoch_connection(&e, 3, true); + assert(!dcp_metadata_matches(&e, dcp_metadata_begin(&e))); /* Later attachment ineligible. */ + e = (struct dcp_metadata_epoch){0}; + dcp_metadata_epoch_connection(&e, 9, true); + current = dcp_metadata_begin(&e); + dcp_metadata_epoch_connection(&e, 10, true); /* A route change also invalidates. */ + assert(!dcp_metadata_matches(&e, current)); +} +int main(int argc, char **argv) +{ + assert(argc == 4 || argc == 5); + epochs(); + timing_proof(); + FILE *f = fopen(argv[1], "rb"); + assert(f && !fseek(f, 0, SEEK_END)); + long length = ftell(f); + assert(length > 0 && !fseek(f, 0, SEEK_SET)); + void *data = malloc(length); + assert(fread(data, length, 1, f) == 1); + fclose(f); + struct dcp_parse_ctx ctx; + struct dcp_display_metadata metadata, unchanged; + memset(&metadata, 0xa5, sizeof(metadata)); unchanged = metadata; + assert(!parse(data, length, &ctx)); + int ret = parse_display_metadata(&ctx, &metadata); + if (atoi(argv[2])) { + assert(ret == -EINVAL && !memcmp(&metadata, &unchanged, sizeof(metadata))); + free(data); return 0; + } + assert(!ret); + struct dcp_parse_ctx timing_ctx; + unsigned int count = 0; + assert(!parse(timing_blob, sizeof(timing_blob), &timing_ctx)); + struct dcp_display_mode *timings = enumerate_modes(&timing_ctx, &count, + metadata.width_cm * 10, metadata.height_cm * 10, 0, false, true); + assert(!IS_ERR(timings) && count == 1 && timings[0].metadata_timing_valid); + struct drm_display_mode mode = timings[0].mode; + assert(mode.flags == (DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC)); + assert(mode.clock == 148500 && mode.hdisplay == 1920 && mode.vdisplay == 1080); + free(timings); + const struct drm_edid *edid = dcp_metadata_edid(&metadata, &mode); + assert(!IS_ERR(edid)); + f = fopen(argv[3], "wb"); assert(f); + assert(fwrite(&edid->raw, 128, 1, f) == 1); fclose(f); + drm_edid_free(edid); + mode.flags = 0; + assert(IS_ERR(dcp_metadata_edid(&metadata, &mode))); + mode.flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC; + mode.clock++; + assert(IS_ERR(dcp_metadata_edid(&metadata, &mode))); + free(data); + if (argc == 5) { + f=fopen(argv[4],"rb"); assert(f && !fseek(f,0,SEEK_END)); + length=ftell(f); assert(length>0 && !fseek(f,0,SEEK_SET)); + data=malloc(length); assert(fread(data,length,1,f)==1); fclose(f); + assert(!parse(data,length,&timing_ctx)); count=0; + timings=enumerate_modes(&timing_ctx,&count,600,330,0,false,true); + assert(!IS_ERR(timings)); bool found60=false, found120=false; + for(unsigned int i=0;iH', 1138) + assert struct.unpack_from('> 4) << 8) == 1920 + assert raw[57] + ((raw[58] & 15) << 8) == 280 + assert raw[59] + ((raw[61] >> 4) << 8) == 1080 + assert raw[60] + ((raw[61] & 15) << 8) == 45 + assert raw[62] == 88 and raw[63] == 44 and raw[64] == 0x45 + assert raw[71] == 0x1e + chroma = [raw[27] * 4 + ((raw[25] >> 6) & 3), + raw[28] * 4 + ((raw[25] >> 4) & 3), + raw[29] * 4 + ((raw[25] >> 2) & 3), raw[30] * 4 + (raw[25] & 3), + raw[31] * 4 + ((raw[26] >> 6) & 3), + raw[32] * 4 + ((raw[26] >> 4) & 3), + raw[33] * 4 + ((raw[26] >> 2) & 3), raw[34] * 4 + (raw[26] & 3)] + assert chroma == [697,317,265,632,146,81,321,337] + assert raw[72:77] == bytes.fromhex('000000fc00') + assert b'QG270 S3' in raw and b'DCP metadata' in raw + result = subprocess.run(['edid-decode', '--check', str(tmp / 'edid')], capture_output=True, text=True) + if result.returncode: + print(result.stdout) + print(result.stderr) + assert result.returncode == 0 + print(f'PASS: {len(cases)} actual schema cases, epoch transitions, exact base block and independent edid-decode') diff --git a/tools/diagnostics/test-apple-external-sdr.py b/tools/diagnostics/test-apple-external-sdr.py index 1ece679366267..695a6059da419 100644 --- a/tools/diagnostics/test-apple-external-sdr.py +++ b/tools/diagnostics/test-apple-external-sdr.py @@ -201,10 +201,14 @@ def restricted(rate=60, unsafe=None, dsc=None, colors=None, before=False): #define trace_iomfb_parse_mode_fail(...) ((void)0) #define trace_iomfb_parse_mode_success(...) ((void)0) #define trace_iomfb_timing_mode(...) ((void)0) +#define DRM_MODE_FLAG_PHSYNC 1 +#define DRM_MODE_FLAG_NHSYNC 2 +#define DRM_MODE_FLAG_PVSYNC 4 +#define DRM_MODE_FLAG_NVSYNC 8 #define DRM_MODE_TYPE_DRIVER 1 #define DRM_MODE_TYPE_PREFERRED 2 struct drm_display_mode { - unsigned int type; + unsigned int type, flags; int clock, vdisplay, vsync_start, vsync_end, vtotal; int hdisplay, hsync_start, hsync_end, htotal, width_mm, height_mm; }; From 070123c2917ecc01a397df0162feca2295df7d8d Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 20:54:54 +1100 Subject: [PATCH 07/10] drm/apple: trace external metadata admission state Record bounded connection tickets and validity flags while holding the metadata lock. Trace initial association, property receipt and probing to distinguish an ineligible connection from incomplete or rejected metadata without logging monitor contents or weakening admission. The tracepoint is disabled by default. Exercise the observation path in the existing metadata lifetime test. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp.c | 25 ++++++++++++++++- drivers/gpu/drm/apple/trace.h | 28 +++++++++++++++++++ .../test-apple-display-metadata-lifetime.py | 8 +++++- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 6a6ad92c0a891..3c3be773c9db7 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1807,6 +1807,23 @@ static bool dcp_firmware_metadata_enabled(struct apple_dcp *dcp) return dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL; } +/* Called under metadata.lock; no monitor contents or pointers are exposed. */ +static void dcp_trace_metadata(struct apple_dcp *dcp, const char *phase, bool valid) +{ + u32 flags = (dcp->metadata.epoch.connected ? BIT(0) : 0) | + (dcp->metadata.epoch.associated_once ? BIT(1) : 0) | + (dcp->metadata.epoch.invalidated ? BIT(2) : 0) | + (dcp->metadata.attributes_valid ? BIT(3) : 0) | + (dcp->metadata.timing_valid ? BIT(4) : 0) | + (dcp->chunks.metadata_ticket.associated ? BIT(5) : 0) | + (dcp->chunks.contiguous ? BIT(6) : 0) | (valid ? BIT(7) : 0); + + if (dcp_firmware_metadata_enabled(dcp)) + trace_iomfb_metadata(dcp, phase, dcp->metadata.epoch.generation, + dcp->chunks.metadata_ticket.generation, flags, + dcp->chunks.received, dcp->chunks.length); +} + void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, bool connected) { unsigned long flags; @@ -1820,6 +1837,7 @@ void dcp_firmware_metadata_connection(struct apple_dcp *dcp, u64 generation, boo dcp->metadata.hpd_seen = false; } dcp_metadata_epoch_connection(&dcp->metadata.epoch, generation, connected); + dcp_trace_metadata(dcp, "connection", connected); spin_unlock_irqrestore(&dcp->metadata.lock, flags); } @@ -1838,6 +1856,7 @@ void dcp_firmware_metadata_hotplug(struct apple_dcp *dcp, bool connected) dcp->metadata.attributes_valid = false; dcp->metadata.timing_valid = false; } + dcp_trace_metadata(dcp, "hotplug", connected); spin_unlock_irqrestore(&dcp->metadata.lock, flags); } @@ -1847,9 +1866,10 @@ void dcp_firmware_metadata_begin(struct apple_dcp *dcp) spin_lock_irqsave(&dcp->metadata.lock, flags); dcp->chunks.metadata_ticket = dcp_metadata_begin(&dcp->metadata.epoch); - spin_unlock_irqrestore(&dcp->metadata.lock, flags); dcp->chunks.received = 0; dcp->chunks.contiguous = true; + dcp_trace_metadata(dcp, "begin", dcp->chunks.metadata_ticket.associated); + spin_unlock_irqrestore(&dcp->metadata.lock, flags); } static void dcp_firmware_metadata_notify(struct apple_dcp *dcp) @@ -1887,6 +1907,7 @@ void dcp_firmware_metadata_attributes(struct apple_dcp *dcp, dcp->metadata.attributes_ticket = dcp->chunks.metadata_ticket; } } + dcp_trace_metadata(dcp, "attributes", !!attributes); spin_unlock_irqrestore(&dcp->metadata.lock, flags); dcp_firmware_metadata_notify(dcp); } @@ -1914,6 +1935,7 @@ void dcp_firmware_metadata_timing(struct apple_dcp *dcp) break; } unlock: + dcp_trace_metadata(dcp, "timing", dcp->metadata.timing_valid); spin_unlock_irqrestore(&dcp->metadata.lock, flags); dcp_firmware_metadata_notify(dcp); } @@ -1944,6 +1966,7 @@ const struct drm_edid *dcp_firmware_edid(struct apple_dcp *dcp, u64 *generation) valid = dcp->metadata.attributes_valid && dcp->metadata.timing_valid && dcp_metadata_matches(&dcp->metadata.epoch, dcp->metadata.attributes_ticket) && dcp_metadata_matches(&dcp->metadata.epoch, dcp->metadata.timing_ticket); + dcp_trace_metadata(dcp, "probe", valid); if (valid) { attributes = dcp->metadata.attributes; preferred = dcp->metadata.preferred; diff --git a/drivers/gpu/drm/apple/trace.h b/drivers/gpu/drm/apple/trace.h index d19609f116f2c..0af0e0d0d654a 100644 --- a/drivers/gpu/drm/apple/trace.h +++ b/drivers/gpu/drm/apple/trace.h @@ -76,6 +76,34 @@ { DPTX_APCALL_DEVICE_BUSY_TIMEOUT, "device_busy_timeout" }, \ { DPTX_APCALL_DEVICE_NOT_STARTED, "device_not_started" }) +TRACE_EVENT(iomfb_metadata, + TP_PROTO(struct apple_dcp *dcp, const char *phase, u64 generation, + u64 ticket_generation, u32 flags, u32 received, u32 length), + TP_ARGS(dcp, phase, generation, ticket_generation, flags, received, length), + TP_STRUCT__entry( + __string(devname, dev_name(dcp->dev)) + __string(phase, phase) + __field(u64, generation) + __field(u64, ticket_generation) + __field(u32, flags) + __field(u32, received) + __field(u32, length) + ), + TP_fast_assign( + __assign_str(devname); + __assign_str(phase); + __entry->generation = generation; + __entry->ticket_generation = ticket_generation; + __entry->flags = flags; + __entry->received = received; + __entry->length = length; + ), + TP_printk("%s: %s generation=%llu ticket=%llu flags=%#x received=%u length=%u", + __get_str(devname), __get_str(phase), __entry->generation, + __entry->ticket_generation, __entry->flags, + __entry->received, __entry->length) +); + TRACE_EVENT(dcp_recv_msg, TP_PROTO(struct apple_dcp *dcp, u8 endpoint, u64 message), TP_ARGS(dcp, endpoint, message), diff --git a/tools/diagnostics/test-apple-display-metadata-lifetime.py b/tools/diagnostics/test-apple-display-metadata-lifetime.py index bc64678c26d5d..d85daa509ef8c 100644 --- a/tools/diagnostics/test-apple-display-metadata-lifetime.py +++ b/tools/diagnostics/test-apple-display-metadata-lifetime.py @@ -57,6 +57,12 @@ def function(text, signature): typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; +#define BIT(n) (1u << (n)) +static unsigned int trace_observations; +#define trace_iomfb_metadata(d,p,g,t,f,r,l) do { \ + (void)(d); (void)(p); (void)(g); (void)(t); (void)(f); (void)(r); (void)(l); \ + trace_observations++; \ +} while (0) #define READ_ONCE(x) (x) #define WRITE_ONCE(x,v) ((x)=(v)) #define xchg(p,v) __atomic_exchange_n(p,v,__ATOMIC_SEQ_CST) @@ -380,7 +386,7 @@ def function(text, signature): assert(!pthread_create(&b,NULL,hotplug_thread,NULL)); assert(!pthread_create(&reader,NULL,reader_thread,NULL)); assert(!pthread_join(a,NULL) && !pthread_join(b,NULL) && !pthread_join(reader,NULL)); - initial(); assert(!blob_allocations); + initial(); assert(!blob_allocations && trace_observations); puts("PASS: mutex-serialized EDID/blob get_modes, hotplug, sysfs reader and same-epoch publication, first-connection scope, raw precedence, invalidation, no added modes"); return 0; } From b44fc82491ed70a81f1d514492c76d4df73f6df2 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 20:59:53 +1100 Subject: [PATCH 08/10] drm/apple: validate delivered property extent before metadata admission H17P advertises one extra byte after the word-aligned serialized property, but does not send that byte. Requiring received == advertised discarded complete same-connection TimingElements and DisplayAttributes. Validate the complete serialized object against bytes actually received. Accept exact lengths or the observed one-byte framing convention only; reject gaps, excess trailing content and truncated values. Perform the bounded structural walk outside the metadata spinlock, then retain the existing connection-ticket checks. No absent byte is used as payload. Exercise captured property extents, truncation mutations and a missing final string byte alongside metadata lifetime and decoder tests. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-internal.h | 1 + drivers/gpu/drm/apple/dcp.c | 6 ++-- drivers/gpu/drm/apple/iomfb_template.c | 6 ++++ drivers/gpu/drm/apple/parser.c | 16 ++++++++++ drivers/gpu/drm/apple/parser.h | 1 + .../test-apple-display-metadata-lifetime.py | 5 +++- .../test-apple-display-metadata.py | 30 +++++++++++++++++++ 7 files changed, 62 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index cd265c6ced986..0f108bc2a1880 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -107,6 +107,7 @@ struct dcp_chunks { void *data; size_t received; bool contiguous; + bool payload_complete; struct dcp_metadata_ticket metadata_ticket; }; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 3c3be773c9db7..0bffc92c19959 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1816,7 +1816,8 @@ static void dcp_trace_metadata(struct apple_dcp *dcp, const char *phase, bool va (dcp->metadata.attributes_valid ? BIT(3) : 0) | (dcp->metadata.timing_valid ? BIT(4) : 0) | (dcp->chunks.metadata_ticket.associated ? BIT(5) : 0) | - (dcp->chunks.contiguous ? BIT(6) : 0) | (valid ? BIT(7) : 0); + (dcp->chunks.contiguous ? BIT(6) : 0) | (valid ? BIT(7) : 0) | + (dcp->chunks.payload_complete ? BIT(8) : 0); if (dcp_firmware_metadata_enabled(dcp)) trace_iomfb_metadata(dcp, phase, dcp->metadata.epoch.generation, @@ -1868,6 +1869,7 @@ void dcp_firmware_metadata_begin(struct apple_dcp *dcp) dcp->chunks.metadata_ticket = dcp_metadata_begin(&dcp->metadata.epoch); dcp->chunks.received = 0; dcp->chunks.contiguous = true; + dcp->chunks.payload_complete = false; dcp_trace_metadata(dcp, "begin", dcp->chunks.metadata_ticket.associated); spin_unlock_irqrestore(&dcp->metadata.lock, flags); } @@ -1888,7 +1890,7 @@ static void dcp_firmware_metadata_notify(struct apple_dcp *dcp) static bool dcp_firmware_metadata_complete(struct apple_dcp *dcp) { - return dcp->chunks.contiguous && dcp->chunks.received == dcp->chunks.length && + return dcp->chunks.contiguous && dcp->chunks.payload_complete && dcp_metadata_matches(&dcp->metadata.epoch, dcp->chunks.metadata_ticket); } diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 03438d24a7557..cfbea578769fa 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -986,6 +986,12 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, return false; } + /* Validate serialized extent outside the metadata spinlock. */ + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL) + dcp->chunks.payload_complete = dcp->chunks.contiguous && + dcp_property_complete(dcp->chunks.data, dcp->chunks.received, + dcp->chunks.length); + /* used just as opaque pointer for tracing */ ctx.dcp = dcp; diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index b723fba76d456..8c2a1069417b6 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -299,6 +299,22 @@ int parse(const void *blob, size_t size, struct dcp_parse_ctx *ctx) return 0; } +bool dcp_property_complete(const void *blob, size_t received, size_t advertised) +{ + struct dcp_parse_ctx ctx; + + if (!blob || !received || received > U32_MAX - 3 || + received > advertised || advertised - received > 1) + return false; + /* H17P may count one unsent byte after the aligned serialized object. */ + if (advertised != received && (received & 3)) + return false; + /* Bound decoding to bytes actually delivered, never allocation padding. */ + if (parse(blob, received, &ctx) || skip(&ctx)) + return false; + return ctx.pos == received || round_up(ctx.pos, 4) == received; +} + static int parse_dimension(struct dcp_parse_ctx *handle, struct dimension *dim, bool polarity) { struct iterator it; diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 50e0580f98080..0392b09751ac9 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -107,6 +107,7 @@ struct dimension { }; int parse(const void *blob, size_t size, struct dcp_parse_ctx *ctx); +bool dcp_property_complete(const void *blob, size_t received, size_t advertised); struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, unsigned int *count, int width_mm, int height_mm, unsigned notch_height, diff --git a/tools/diagnostics/test-apple-display-metadata-lifetime.py b/tools/diagnostics/test-apple-display-metadata-lifetime.py index d85daa509ef8c..407c8a2da0229 100644 --- a/tools/diagnostics/test-apple-display-metadata-lifetime.py +++ b/tools/diagnostics/test-apple-display-metadata-lifetime.py @@ -43,6 +43,7 @@ def function(text, signature): assert 'dcp_firmware_metadata_hotplug(dcp, !!(*connected));' in template assert 'dcp_firmware_metadata_attributes(dcp, ret ? NULL : &metadata);' in template assert 'dcp->chunks.received += req->length;' in template +assert 'dcp_property_complete(dcp->chunks.data, dcp->chunks.received,' in template prefix = r''' #include #include @@ -124,7 +125,7 @@ def function(text, signature): void *dev; struct { int iomfb_method_profile; } hw; struct apple_connector *connector; - struct { void *data; size_t length, received; bool contiguous; struct dcp_metadata_ticket metadata_ticket; } chunks; + struct { void *data; size_t length, received; bool contiguous, payload_complete; struct dcp_metadata_ticket metadata_ticket; } chunks; struct { spinlock_t lock; struct dcp_metadata_epoch epoch; @@ -239,6 +240,7 @@ def function(text, signature): { dcp_firmware_metadata_begin(&p); p.chunks.length=100; p.chunks.received=complete ? 100 : 99; + p.chunks.payload_complete=complete; } static bool eligible(void) { @@ -344,6 +346,7 @@ def function(text, signature): assert(p.chunks.received==32 && p.chunks.contiguous); dcp_firmware_metadata_attributes(&p,&attrs); assert(!p.metadata.attributes_valid); chunk.offset=32; assert(dcpep_cb_prop_chunk(&p,&chunk)); + p.chunks.payload_complete=true; /* Structural validator tested separately. */ dcp_firmware_metadata_attributes(&p,&attrs); assert(p.metadata.attributes_valid); initial(); dcp_firmware_metadata_connection(&p,1,true); assert(dcpep_cb_prop_start(&p,&length)); diff --git a/tools/diagnostics/test-apple-display-metadata.py b/tools/diagnostics/test-apple-display-metadata.py index e0508bbf431d8..8a5e34e6c006f 100644 --- a/tools/diagnostics/test-apple-display-metadata.py +++ b/tools/diagnostics/test-apple-display-metadata.py @@ -17,6 +17,8 @@ args.add_argument('--attributes-json', type=Path, default=Path(__file__).with_name('display-attributes-fixture.json')) args.add_argument('--timings-bin', type=Path, help='Optional captured TimingElements binary (not embedded in public fixtures)') +args.add_argument('--property-bin', type=Path, action='append', default=[], + help='Optional captured property to validate against delivered extent') args = args.parse_args() fixture = json.loads(args.attributes_json.read_text()) @@ -219,7 +221,23 @@ def wire(value, out): } int main(int argc, char **argv) { + if (argc == 3 && !strcmp(argv[1], "--property")) { + FILE *f=fopen(argv[2],"rb"); assert(f && !fseek(f,0,SEEK_END)); + long n=ftell(f); assert(n>20 && !fseek(f,0,SEEK_SET)); + unsigned char *b=malloc(n); assert(fread(b,n,1,f)==1); fclose(f); + assert(dcp_property_complete(b,n-1,n)); + assert(dcp_property_complete(b,n-1,n-1)); + for(unsigned int cut=1;cut<=16;cut++) + assert(!dcp_property_complete(b,n-1-cut,n-cut)); + assert(!dcp_property_complete(b,n-1,n+1)); + free(b); return 0; + } assert(argc == 4 || argc == 5); + /* A missing final string byte must not be supplied by zero allocation. */ + unsigned char incomplete[]={0xd3,0,0,0,1,0,0,0x82,1,0,0,0x89,0x42}; + assert(dcp_property_complete(incomplete,sizeof(incomplete),sizeof(incomplete))); + assert(!dcp_property_complete(incomplete,12,13)); + assert(!dcp_property_complete(NULL,0,0)); epochs(); timing_proof(); FILE *f = fopen(argv[1], "rb"); @@ -239,6 +257,16 @@ def wire(value, out): free(data); return 0; } assert(!ret); + assert(dcp_property_complete(data,length,length)); + size_t aligned=round_up((size_t)length,4); + unsigned char *padded=malloc(aligned+4); + memset(padded,0xa5,aligned+4); memcpy(padded,data,length); + assert(dcp_property_complete(padded,aligned,aligned+1)); + assert(!dcp_property_complete(data,length-1,length)); + assert(!dcp_property_complete(padded,aligned,aligned+2)); + assert(!dcp_property_complete(padded,aligned+4,aligned+5)); + assert(!dcp_property_complete(padded,aligned-1,aligned+1)); + free(padded); struct dcp_parse_ctx timing_ctx; unsigned int count = 0; assert(!parse(timing_blob, sizeof(timing_blob), &timing_ctx)); @@ -331,6 +359,8 @@ def wire(value, out): raw = bytearray(struct.pack(' Date: Mon, 12 Oct 2026 22:39:09 +1100 Subject: [PATCH 09/10] diagnostics: verify output placement and separate physical visibility The frame test measures GTK/compositor feedback, which cannot prove the monitor is showing the selected input or new scanout. Record the actual GDK surface output and state the measurement scope explicitly. Add a bounded manual grayscale comparison pattern to measure the output midtone response without changing monitor settings or kernel controls. Retain the existing moving pattern and feedback fields. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- .../diagnostics/wayland-output-frame-test.py | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/tools/diagnostics/wayland-output-frame-test.py b/tools/diagnostics/wayland-output-frame-test.py index e9f0893ead65d..8b0dc64b28ea5 100644 --- a/tools/diagnostics/wayland-output-frame-test.py +++ b/tools/diagnostics/wayland-output-frame-test.py @@ -13,7 +13,8 @@ parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--connector", default="DP-1") -parser.add_argument("--seconds", type=int, default=8, choices=range(3, 31)) +parser.add_argument("--seconds", type=int, default=8, choices=range(3, 61)) +parser.add_argument("--pattern", choices=("moving", "gamma"), default="moving") args = parser.parse_args() Gtk.init() display = Gdk.Display.get_default() @@ -30,12 +31,44 @@ area = Gtk.DrawingArea() window.set_child(area) started = time.monotonic() -stats = {"connector": args.connector, "ticks": 0, "draws": 0} +stats = {"connector": args.connector, "pattern": args.pattern, "ticks": 0, "draws": 0, + "actual_connector": None, "physical_visibility": "requires user observation"} feedback = set() complete_frames = set() def draw(_area, context, width, height): + surface = window.get_surface() + monitor = display.get_monitor_at_surface(surface) if surface else None + if monitor: + stats["actual_connector"] = monitor.get_connector() + if args.pattern == "gamma": + context.set_source_rgb(0.08, 0.08, 0.08) + context.paint() + context.set_source_rgb(1, 1, 1) + context.select_font_face("Sans") + context.set_font_size(24) + context.move_to(24, 48) + context.show_text("Match a solid grey to the checkerboard brightness; view from a distance") + y, h = int(height * 0.25), int(height * 0.5) + w = int(width * 0.15) + left = int(width * 0.04) + for row in range(0, h, 2): + for col in range(0, w, 2): + value = ((row // 2) + (col // 2)) % 2 + context.set_source_rgb(value, value, value) + context.rectangle(left + col, y + row, 2, 2) + context.fill() + for index, value in enumerate((0.50, 0.67, 0.735, 0.80)): + x = int(width * (0.24 + index * 0.19)) + context.set_source_rgb(value, value, value) + context.rectangle(x, y, w, h) + context.fill() + context.set_source_rgb(1, 1, 1) + context.move_to(x, y + h + 40) + context.show_text(f"{value * 100:g}%") + stats["draws"] += 1 + return phase = (time.monotonic() - started) % 2 / 2 context.set_source_rgb(0.06, 0.10, 0.17) context.paint() @@ -55,7 +88,8 @@ def tick(widget, clock): presented = previous.get_presentation_time() if presented: feedback.add(presented) - widget.queue_draw() + if args.pattern == "moving": + widget.queue_draw() return GLib.SOURCE_CONTINUE @@ -76,8 +110,10 @@ def finish(): stats["presentation_feedback_frames"] = len(feedback) stats["complete_timings"] = len(complete_frames) stats["callback_pass"] = stats["ticks"] >= args.seconds * 15 -stats["draw_pass"] = stats["draws"] >= args.seconds * 15 +stats["draw_pass"] = stats["draws"] >= (args.seconds * 15 if args.pattern == "moving" else 1) stats["presentation_pass"] = len(feedback) >= args.seconds * 10 +stats["target_pass"] = stats["actual_connector"] == args.connector +stats["verification_scope"] = "compositor feedback and output placement; physical scanout is not measured" print(json.dumps(stats, sort_keys=True)) -raise SystemExit(0 if all(stats[k] for k in - ("callback_pass", "draw_pass", "presentation_pass")) else 1) +checks = ("callback_pass", "draw_pass", "presentation_pass", "target_pass") if args.pattern == "moving" else ("draw_pass", "target_pass") +raise SystemExit(0 if all(stats[k] for k in checks) else 1) From 91997e1c42f72d7d65c9c753213f52d9725af6f5 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Mon, 12 Oct 2026 22:44:34 +1100 Subject: [PATCH 10/10] drm/apple: refresh diagnostic output placement after mapping Track placement in frame ticks, since the first static draw can precede fullscreen mapping. Retry drawing while placement differs from the requested output. Generated-by: LLM Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/diagnostics/wayland-output-frame-test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/diagnostics/wayland-output-frame-test.py b/tools/diagnostics/wayland-output-frame-test.py index 8b0dc64b28ea5..b31f5f34051b0 100644 --- a/tools/diagnostics/wayland-output-frame-test.py +++ b/tools/diagnostics/wayland-output-frame-test.py @@ -88,7 +88,11 @@ def tick(widget, clock): presented = previous.get_presentation_time() if presented: feedback.add(presented) - if args.pattern == "moving": + surface = window.get_surface() + monitor = display.get_monitor_at_surface(surface) if surface else None + if monitor: + stats["actual_connector"] = monitor.get_connector() + if args.pattern == "moving" or stats["actual_connector"] != args.connector: widget.queue_draw() return GLib.SOURCE_CONTINUE