Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions drivers/gpu/drm/apple/iomfb_template.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
#include "trace.h"
#include "version_utils.h"

#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER
static bool experimental_h17p_1080p180_rgb8;
module_param(experimental_h17p_1080p180_rgb8, bool, 0444);
MODULE_PARM_DESC(experimental_h17p_1080p180_rgb8,
"Diagnose advertised external 1080p180 SDR RGB8 timings marked unsafe");
#endif

/* Register defines used in bandwidth setup structure */
#define REG_DOORBELL_BIT(idx) (2 + (idx))

Expand Down Expand Up @@ -1003,6 +1010,11 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp,
}

if (!strcmp(req->key, "TimingElements")) {
#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER
ctx.diagnostic_1080p180_rgb8 = experimental_h17p_1080p180_rgb8 &&
dcp->hw.iomfb_method_profile ==
DCP_IOMFB_METHODS_H17P_EXTERNAL_EXPERIMENTAL;
#endif
dcp->modes = enumerate_modes(&ctx, &dcp->nr_modes,
dcp->width_mm, dcp->height_mm,
dcp->notch_height,
Expand Down
70 changes: 58 additions & 12 deletions drivers/gpu/drm/apple/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -526,10 +526,11 @@ static int parse_timing_color_restrictions(struct dcp_parse_ctx *handle,

static int parse_color_modes(struct dcp_parse_ctx *handle,
struct dcp_display_mode *out, bool external_sdr,
const struct timing_color_restrictions *limits)
const struct timing_color_restrictions *limits,
struct dcp_color_mode *unsafe_rgb8)
{
struct iterator outer_it;
unsigned int best_sdr_rank = 0;
unsigned int best_sdr_rank = 0, best_unsafe_rank = 0;
int ret = 0;
out->sdr_444.score = -1;
out->sdr_rgb.score = -1;
Expand Down Expand Up @@ -601,10 +602,24 @@ static int parse_color_modes(struct dcp_parse_ctx *handle,
unsigned int rank;

if (cmode.id <= U32_MAX &&
(timing_color_contains(&limits->unsafe, cmode.id) ||
timing_color_contains(&limits->dsc_required, cmode.id)))
timing_color_contains(&limits->dsc_required, cmode.id))
continue;
rank = external_sdr_rank(&cmode);
if (cmode.id <= U32_MAX &&
timing_color_contains(&limits->unsafe, cmode.id)) {
/*
* Collect only a timing-local diagnostic candidate. Its
* timing and opt-in scope are checked after the full parse.
*/
if (unsafe_rgb8 && cmode.depth == 8 && rank &&
rank >= best_unsafe_rank) {
if (rank > best_unsafe_rank)
unsafe_rgb8->score = -1;
fill_color_mode(unsafe_rgb8, &cmode);
best_unsafe_rank = rank;
}
continue;
}
if (!rank || rank < best_sdr_rank)
continue;
if (rank > best_sdr_rank)
Expand Down Expand Up @@ -651,6 +666,9 @@ static int parse_mode(struct dcp_parse_ctx *handle,
struct iterator it;
struct dimension horiz = { .active = -1 }, vert = { .active = -1 };
struct timing_color_restrictions limits = {};
struct dcp_color_mode unsafe_rgb8 = { .score = -1 };
bool have_horiz = false, have_vert = false, have_virtual = false;
bool duplicate_scope = false;
s64 min_vrr = 0, max_vrr = 0;
u64 clock;
s64 id = -1;
Expand Down Expand Up @@ -687,21 +705,29 @@ static int parse_mode(struct dcp_parse_ctx *handle,
ret = PTR_ERR(key);
else if (is_virtual)
ret = skip(it.handle);
else if (!strcmp(key, "HorizontalAttributes"))
else if (!strcmp(key, "HorizontalAttributes")) {
duplicate_scope |= have_horiz;
have_horiz = true;
ret = parse_dimension(it.handle, &horiz, external_sdr);
else if (!strcmp(key, "VerticalAttributes"))
} else if (!strcmp(key, "VerticalAttributes")) {
duplicate_scope |= have_vert;
have_vert = true;
ret = parse_dimension(it.handle, &vert, external_sdr);
else if (!strcmp(key, "MinimumVariableRefreshRate"))
} else if (!strcmp(key, "MinimumVariableRefreshRate"))
ret = parse_int_bound(it.handle, &min_vrr, 0, U32_MAX);
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, &limits);
ret = parse_color_modes(it.handle, out, external_sdr, &limits,
handle->diagnostic_1080p180_rgb8 ?
&unsafe_rgb8 : NULL);
else if (!strcmp(key, "ID"))
ret = parse_int_bound(it.handle, &id, 0, U32_MAX);
else if (!strcmp(key, "IsVirtual"))
else if (!strcmp(key, "IsVirtual")) {
duplicate_scope |= have_virtual;
have_virtual = true;
ret = parse_bool(it.handle, &is_virtual);
else if (external_sdr && !strcmp(key, "IsPreferred")) {
} else if (external_sdr && !strcmp(key, "IsPreferred")) {
if (have_preferred)
ret = -EINVAL;
else
Expand All @@ -726,6 +752,25 @@ static int parse_mode(struct dcp_parse_ctx *handle,
return ret;
}
}
/*
* Never replace a normal candidate or create a timing/color pairing.
* This diagnostic only exposes an otherwise rejected firmware timing;
* it neither repairs link timing nor establishes that "unsafe" is safe.
* The observed 2080x1106 total and 11796583/65536 Hz are deliberate:
* do not widen this to arbitrary nominal-180 Hz blanking or rates.
*/
if (external_sdr && !internal && handle->diagnostic_1080p180_rgb8 &&
out->sdr_rgb.score < 0 && unsafe_rgb8.score >= 0 &&
!duplicate_scope && have_horiz && have_vert && have_virtual && !is_virtual &&
have_interlaced && !interlaced && !notch_height &&
horiz.active == 1920 && vert.active == 1080 &&
horiz.total == 2080 && vert.total == 1106 &&
vert.precise_sync_rate == 11796583 &&
horiz.pixel_repetition == 0 && vert.pixel_repetition == 0 &&
horiz.sync_polarity >= 0 && vert.sync_polarity >= 0) {
out->sdr_rgb = unsafe_rgb8;
out->unsafe_rgb8_diagnostic = true;
}
if (out->sdr_rgb.score >= 0)
best_color_mode = out->sdr_rgb.id;
else if (out->sdr_444.score >= 0)
Expand Down Expand Up @@ -802,7 +847,8 @@ static int parse_mode(struct dcp_parse_ctx *handle,
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 &&
out->metadata_timing_valid = external_sdr && !out->unsafe_rgb8_diagnostic &&
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);
Expand Down Expand Up @@ -862,7 +908,7 @@ struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle,
/* Process a successful mode */
(*count)++;

if (score > best_score) {
if (!mode->unsafe_rgb8_diagnostic && score > best_score) {
best_score = score;
best_mode = mode;
}
Expand Down
3 changes: 3 additions & 0 deletions drivers/gpu/drm/apple/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct dcp_parse_ctx {
struct apple_dcp *dcp;
const void *blob;
u32 pos, len;
/* Set only by the opt-in external H17P TimingElements caller. */
bool diagnostic_1080p180_rgb8;
};

enum dcp_color_eotf {
Expand Down Expand Up @@ -97,6 +99,7 @@ struct dcp_display_mode {
struct dcp_color_mode best;
bool vrr;
bool metadata_timing_valid;
bool unsafe_rgb8_diagnostic;
};

struct dimension {
Expand Down
37 changes: 37 additions & 0 deletions tools/diagnostics/h17p-1080p180-rgb8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# H17P external 1080p180 RGB8 diagnostic

This is a default-off source diagnostic, not a production mode-policy change.
The boot/load-time `experimental_h17p_1080p180_rgb8` appledrm parameter only
reaches the experimental external H17P TimingElements parser. Normal parsing,
other firmware profiles, internal panels and existing valid selections remain
unchanged. The parser resets the opt-in for every new property blob.

Only an otherwise rejected firmware timing can be exposed: explicit nonvirtual,
progressive 1920x1080 with totals 2080x1106 and the captured fixed-point
rate 11796583/65536 Hz (approximately 180.00157 Hz), with no pixel repetition
and known sync polarities. Its own advertised ColorModes must contain a scored nonvirtual
8-bit full-range SDR RGB candidate in sRGB, BT.709 or Default RGB. The exception
applies only to UnsafeColorElementIDs; DSCRequiredColorElementIDs, malformed
restriction lists, parser bounds and normal link/modeset checks still refuse.
No global color ID is combined with a different timing. The timing fields and
firmware IDs are kept as received, without changing link registers or blanking.

An added diagnostic timing is never marked preferred and cannot supply the
firmware-derived EDID preferred timing. This does not stop a compositor from
restoring an explicitly saved mode, so use a separately reviewed controlled
session and record the actual selection. Safe modes are not converted to RGB8.

Before enabling on hardware, capture the current monitor/dock/physical route's
TimingElements and confirm the local candidate, DSC list, full range, depth,
clock and timing. A capture from a different dock/port is not evidence for the
current route. The existing short-vblank firmware warning is not bypassed or
reinterpreted; a successful advertisement alone proves no transport safety.
Physical output, sustained presentation, errors and return to the baseline mode
require separate qualification. Do not infer production safety or relax other
guards from this diagnostic.

`test-apple-external-sdr.py` exercises the production parser with the option off
and on, malformed/DSC/range/depth/virtual/scope negatives, reversed dictionary
order, timing-local candidates, and non-preferred diagnostic mode handling.
Fixtures are independently generated protocol dictionaries with no private
monitor payloads or identity fields.
100 changes: 95 additions & 5 deletions tools/diagnostics/test-apple-external-sdr.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ def timing(colors, rate=60, identity=203, score=10):

cases = []

def case(name, timings, external=True, count=1, identity=9, depth=8, clock=148500):
def case(name, timings, external=True, count=1, identity=9, depth=8, clock=148500,
diagnostic=False, diagnostic_selected=False, preferred_index=0):
out = bytearray(struct.pack('<I', 0xd3))
wire(timings, out)
cases.append((name, bytes(out), external, count, identity, depth, clock))
cases.append((name, bytes(out), external, count, identity, depth, clock,
diagnostic, diagnostic_selected, preferred_index))


wide = color(84, 1000, Colorimetry=9, Depth=10)
Expand Down Expand Up @@ -167,6 +169,89 @@ def restricted(rate=60, unsafe=None, dsc=None, colors=None, before=False):
case('duplicate ColorModes fail closed', [duplicate_colors], count=0)
case('bad restriction skips only its timing', [restricted(180, [80, 85], []), restricted(60, [], [])], identity=85, depth=10)

# Diagnostic-only retry: no private monitor identity or captured blob is embedded.
# The binary dictionaries still go through the complete production parser.
def trial_timing(colors=None, rate=180):
item = restricted(rate, [80, 85], [], colors)
item['IsInterlaced'] = False
item['HorizontalAttributes']['Total'] = 2080
item['VerticalAttributes'].update(Total=1106, PreciseSyncRate=11796583)
for axis in ('HorizontalAttributes', 'VerticalAttributes'):
item[axis].update(PixelRepetition=0, SyncPolarity=1)
return item


def trial_case(name, item=None, **kwargs):
defaults = dict(identity=80, clock=414090, diagnostic=True,
diagnostic_selected=True, preferred_index=-1)
defaults.update(kwargs)
case('diagnostic ' + name, [item if item is not None else trial_timing()], **defaults)

trial_case('explicit enable exposes only RGB8')
trial_case('default remains refused', diagnostic=False, count=0)
trial_case('legacy keeps existing 10bit selection', external=False, identity=85,
depth=10, diagnostic_selected=False, preferred_index=0)
case('internal diagnostic flag cannot admit unsafe timing', [trial_timing()],
diagnostic=True, count=0)
item = trial_timing(); item['UnsafeColorElementIDs'] = [80]
trial_case('existing safe candidate is not replaced', item, identity=85, depth=10,
diagnostic_selected=False, preferred_index=0)
item = trial_timing(); item['DSCRequiredColorElementIDs'] = [80]
trial_case('DSC remains forbidden', item, count=0)
# No timing-local 8-bit entry: a global/sibling timing cannot provide one.
trial_case('10bit-only timing remains refused', trial_timing([color85]), count=0)
for label, changes in [
('limited range', dict(DynamicRange=1)), ('HDR', dict(EOTF=2)),
('YUV', dict(PixelEncoding=2)), ('wide gamut', dict(Colorimetry=9)),
('12bit', dict(Depth=12)), ('virtual color', dict(IsVirtual=True)),
('negative score', dict(Score=-1)), ('invalid ID', dict(ID=1 << 32)),
]:
trial_case(label, trial_timing([color(80, 242999, **changes)]), count=0)
for axis, key, value in [
('HorizontalAttributes', 'Active', 1919),
('VerticalAttributes', 'Active', 1079),
('HorizontalAttributes', 'Total', 100),
('VerticalAttributes', 'PreciseSyncRate', 11796582),
('VerticalAttributes', 'PreciseSyncRate', 180 << 16),
('HorizontalAttributes', 'Total', 2200),
('VerticalAttributes', 'Total', 1125),
('VerticalAttributes', 'PreciseSyncRate', 181 << 16),
('HorizontalAttributes', 'PixelRepetition', 1),
('VerticalAttributes', 'PixelRepetition', 1),
('HorizontalAttributes', 'SyncPolarity', 2),
]:
item = trial_timing(); item[axis][key] = value
trial_case(axis + key + str(value), item, count=0)
for key in ('IsVirtual', 'IsInterlaced'):
item = trial_timing(); item.pop(key)
trial_case('missing ' + key, item, count=0)
item = trial_timing(); item[key] = True
trial_case('true ' + key, item, count=0)
for axis in ('HorizontalAttributes', 'VerticalAttributes'):
for key in ('PixelRepetition', 'SyncPolarity'):
item = trial_timing(); item[axis].pop(key)
trial_case('missing ' + axis + key, item, count=0)
for key in ('HorizontalAttributes', 'VerticalAttributes', 'IsVirtual'):
item = trial_timing()
duplicate = Pairs(list(item.items()) + [(key, item[key])])
trial_case('duplicate scope ' + key, duplicate, count=0)
for key in ('UnsafeColorElementIDs', 'DSCRequiredColorElementIDs'):
item = trial_timing(); item[key] = [80, 80]
trial_case('duplicate restriction ' + key, item, count=0)
# All key orders must preserve the same selection and non-preferred status.
item = trial_timing()
trial_case('reversed dictionary order', dict(reversed(list(item.items()))))
item = trial_timing(); item['IsPreferred'] = True
trial_case('firmware preferred does not promote trial', item)
# A high firmware score must not make the trial the desktop default.
item = trial_timing(); item['Score'] = 999999
case('diagnostic trial cannot displace normal preferred mode', [item, restricted(60, [], [])],
count=2, identity=80, depth=8, clock=414090, diagnostic=True,
diagnostic_selected=True, preferred_index=1)
# No shared candidate pool: successful sibling timing does not rescue an unsafe-only 10bit mode.
case('diagnostic candidates remain timing-local', [trial_timing([color85]), restricted(60, [], [])],
identity=85, depth=10, diagnostic=True)

prefix = r'''
#include <assert.h>
#include <stdbool.h>
Expand Down Expand Up @@ -220,12 +305,14 @@ def restricted(rate=60, unsafe=None, dsc=None, colors=None, before=False):
int main(void)
{
'''
for i, (name, blob, external, count, identity, depth, clock) in enumerate(cases):
for i, (name, blob, external, count, identity, depth, clock, diagnostic, diagnostic_selected, preferred_index) in enumerate(cases):
suffix += f'''
{{
struct dcp_parse_ctx ctx;
struct dcp_parse_ctx ctx = {{ .diagnostic_1080p180_rgb8 = true }};
unsigned int count = 99;
assert(parse(blob_{i}, sizeof(blob_{i}), &ctx) == 0);
assert(!ctx.diagnostic_1080p180_rgb8);
ctx.diagnostic_1080p180_rgb8 = {str(diagnostic).lower()};
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}) {{
Expand All @@ -238,7 +325,10 @@ def restricted(rate=60, unsafe=None, dsc=None, colors=None, before=False):
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);
assert(modes[0].unsafe_rgb8_diagnostic == {str(diagnostic_selected).lower()});
assert(!modes[0].unsafe_rgb8_diagnostic || !modes[0].metadata_timing_valid);
for (unsigned int mode = 0; mode < count; mode++)
assert(!!(modes[mode].mode.type & DRM_MODE_TYPE_PREFERRED) == ((int)mode == {preferred_index}));
'''
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'
Expand Down