Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:03 2001
From: Anze <aanzdev@gmail.com>
Date: Mon, 27 Jul 2026 18:00:00 +0200
Subject: [PATCH] usb: typec: mux: don't swallow EPROBE_DEFER in the dedup check

The 7.1 merge window added a "skip duplicates" filter to typec_mux_match()
and typec_switch_match(), and started passing the output array in as the
match data so it can be compared against. Both halves are broken for a
connector whose mux is not registered yet.

struct device is the FIRST member of struct typec_mux_dev and struct
typec_switch_dev, so container_of() on a NULL device yields NULL. When
class_find_device() finds nothing - the normal case while the mux driver
has not probed yet, e.g. an i2c retimer such as wcd939x-usbss - the dedup
loop compares that NULL against an array slot that is also NULL, matches,
and returns NULL. The caller reads that as "no such connection" instead
of the -EPROBE_DEFER it used to get, so the connector is created WITHOUT
its mux, permanently: the DP lanes and the AUX/SBU pair are never routed.

The arrays are also uninitialised stack (`struct typec_mux_dev
*mux_devs[TYPEC_MUX_MAX_DEVS];`), yet the dedup loop reads every slot
before fwnode_connection_find_matches() has filled them, so a valid mux
can be dropped by a chance comparison against stack garbage.

Bail out with -EPROBE_DEFER before the dedup loop when no device was
found, and zero-initialise both arrays.

Device-observed on an AYN Odin 3 (SM8750): dock DisplayPort output died
with the 7.0.11 -> 7.1.3 bump. The firmware enters DP alt mode correctly
(mux_ctrl=3, pin_assignment=4), raises HPD, and msm_dp powers up its
clocks and PHY - then the very first DPCD read times out
(drm_dp_dpcd_probe ... AUX -> (ret=-110)), link_ready stays false and the
connector reports disconnected, because nothing ever routed the lanes.

Signed-off-by: Anze <aanzdev@gmail.com>
---
--- a/drivers/usb/typec/mux.c 2026-07-27 17:07:50.988901877 +0200
+++ b/drivers/usb/typec/mux.c 2026-07-27 17:07:51.010296597 +0200
@@ -57,6 +57,8 @@
*/
dev = class_find_device(&typec_mux_class, NULL, fwnode,
switch_fwnode_match);
+ if (!dev)
+ return ERR_PTR(-EPROBE_DEFER);

/* Skip duplicates */
for (i = 0; i < TYPEC_MUX_MAX_DEVS; i++)
@@ -65,7 +67,7 @@
return NULL;
}

- return dev ? to_typec_switch_dev(dev) : ERR_PTR(-EPROBE_DEFER);
+ return to_typec_switch_dev(dev);
}

/**
@@ -79,7 +81,7 @@
*/
struct typec_switch *fwnode_typec_switch_get(struct fwnode_handle *fwnode)
{
- struct typec_switch_dev *sw_devs[TYPEC_MUX_MAX_DEVS];
+ struct typec_switch_dev *sw_devs[TYPEC_MUX_MAX_DEVS] = {};
struct typec_switch *sw;
int count;
int err;
@@ -292,6 +294,8 @@

dev = class_find_device(&typec_mux_class, NULL, fwnode,
mux_fwnode_match);
+ if (!dev)
+ return ERR_PTR(-EPROBE_DEFER);

/* Skip duplicates */
for (i = 0; i < TYPEC_MUX_MAX_DEVS; i++)
@@ -300,8 +304,7 @@
return NULL;
}

-
- return dev ? to_typec_mux_dev(dev) : ERR_PTR(-EPROBE_DEFER);
+ return to_typec_mux_dev(dev);
}

/**
@@ -315,7 +318,7 @@
*/
struct typec_mux *fwnode_typec_mux_get(struct fwnode_handle *fwnode)
{
- struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS];
+ struct typec_mux_dev *mux_devs[TYPEC_MUX_MAX_DEVS] = {};
struct typec_mux *mux;
int count;
int err;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX)

# External display support for the micro HDMI port.
#
# The micro HDMI port sits behind a Lontium LT8912B DSI-to-HDMI bridge
# with no DDC support at all (per Lontium's product brief), so the
# display's EDID can never be read and without help everything falls
# back to 1024x768. Arm a synthetic EDID on the connector with the two
# modes the bridge handles well:
#
# 1920x1080@60 (preferred)
# 1280x720@60
#
# The bridge caps at 1920x1080 / 150 MHz. 30 Hz modes were tested and
# come out broken, so they are not offered.
#
# Pick the mode with: set_setting external_display.mode auto|<mode>
#
# No audio on this port: the lt8912b driver has no audio support, so
# sound stays on the speakers. The USB-C DisplayPort path is unrelated
# and already handles EDID and audio natively.

# Mode list for a future settings menu, and the audio capability flag.
cat <<EOF >/storage/.config/profile.d/011-external_display
DEVICE_EXTERNAL_DISPLAY_MODES="1280x720@60 1920x1080@60"
DEVICE_EXTERNAL_DISPLAY_AUDIO="false"
EOF

# Arm the EDID before sway starts; the kernel picks it up on the next
# connector probe (i.e. when a display is plugged in).
EDID_B64="AP///////wAx2AAAAAAAAAUkAQOAEAl4AgAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAjqAGHE4LUBYLEUAAAAAAAAeAR0AclHQHiBuKFUAAAAAAAAeAAAAEAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAMQ="
for OVERRIDE in /sys/kernel/debug/dri/*/HDMI-A-1/edid_override; do
[ -e "${OVERRIDE}" ] || continue
echo "${EDID_B64}" | base64 -d >"${OVERRIDE}"
done
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
# Copyright (C) 2026 ROCKNIX (https://github.com/ROCKNIX)

# External display support for the micro HDMI port.
#
# The micro HDMI port sits behind a Lontium LT8912B DSI-to-HDMI bridge
# with no DDC support at all (per Lontium's product brief), so the
# display's EDID can never be read and without help everything falls
# back to 1024x768. Arm a synthetic EDID on the connector with the two
# modes the bridge handles well:
#
# 1920x1080@60 (preferred)
# 1280x720@60
#
# The bridge caps at 1920x1080 / 150 MHz. 30 Hz modes were tested and
# come out broken, so they are not offered.
#
# Pick the mode with: set_setting external_display.mode auto|<mode>
#
# No audio on this port: the lt8912b driver has no audio support, so
# sound stays on the speakers. The USB-C DisplayPort path is unrelated
# and already handles EDID and audio natively.

# Mode list for a future settings menu, and the audio capability flag.
cat <<EOF >/storage/.config/profile.d/011-external_display
DEVICE_EXTERNAL_DISPLAY_MODES="1280x720@60 1920x1080@60"
DEVICE_EXTERNAL_DISPLAY_AUDIO="false"
EOF

# Arm the EDID before sway starts; the kernel picks it up on the next
# connector probe (i.e. when a display is plugged in).
EDID_B64="AP///////wAx2AAAAAAAAAUkAQOAEAl4AgAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAjqAGHE4LUBYLEUAAAAAAAAeAR0AclHQHiBuKFUAAAAAAAAeAAAAEAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAMQ="
for OVERRIDE in /sys/kernel/debug/dri/*/HDMI-A-1/edid_override; do
[ -e "${OVERRIDE}" ] || continue
echo "${EDID_B64}" | base64 -d >"${OVERRIDE}"
done
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export OS_VERSION \
DEVICE_AUDIO_MIXER \
DEVICE_BATTERY_LED_STATUS \
DEVICE_DTB_SWITCH \
DEVICE_EXTERNAL_DISPLAY_AUDIO \
DEVICE_EXTERNAL_DISPLAY_MODES \
DEVICE_FAKE_JACKSENSE \
DEVICE_FUNC_KEYA_MODIFIER \
DEVICE_FUNC_KEYB_MODIFIER \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ display.saturation=50
dreamcast.integerscale=0
dreamcast.ratio=4/3
easyrpg.integerscale=0
external_display.mode=auto
famicom.integerscale=0
famicom.ratio=4/3
fbn.integerscale=0
Expand Down
139 changes: 139 additions & 0 deletions projects/ROCKNIX/packages/rocknix/sources/scripts/external-display
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (C) 2026-present ROCKNIX (https://github.com/ROCKNIX)

### Summary
# External display state, and blanking that is safe around one.
#
# Three callers need the same two answers and kept their own copies of
# both: rocknix-fake-suspend, output_monitor, and the platform power
# handlers for devices that have real suspend. Drifting apart on either
# one is a bug in itself, so they all come here now.
###

### Usage
# external-display connected exit 0 if an external display is attached
# external-display list connected external connectors, one per line
# external-display internals internal panel outputs, as the compositor names them
# external-display blank [backlight]
# external-display unblank
###

. /etc/profile

case $(get_setting system.loglevel) in
verbose) DEBUG=true ;;
*) DEBUG=false ;;
esac

### Connector status is the kernel's, so it is right even when the
### compositor missed the hotplug event. DP covers the USB-C alt mode docks
### and any adapter behind them; the leading dash is what keeps eDP panels
### from matching as external.
_external_status_files() {
local STATUS
for STATUS in /sys/class/drm/card*-HDMI-A-[0-9]*/status \
/sys/class/drm/card*-DP-[0-9]*/status; do
[ -f "${STATUS}" ] && echo "${STATUS}"
done
}

_connector_name() {
local NAME="${1%/status}"
NAME="${NAME##*/}"
echo "${NAME#card*-}"
}

### Which compositor is both configured and running.
_compositor_is() {
case "${UI_SERVICE}" in
*"${1}"*) ;;
*) return 1 ;;
esac
pgrep "${1}" >/dev/null 2>&1
}

_backlight() {
local BL
for BL in /sys/class/backlight/*/bl_power; do
[ -w "${BL}" ] && echo "${1}" >"${BL}"
done
}

list() {
local STATUS
for STATUS in $(_external_status_files); do
[ "$(cat "${STATUS}")" = "connected" ] && _connector_name "${STATUS}"
done
return 0
}

connected() {
local STATUS
for STATUS in $(_external_status_files); do
[ "$(cat "${STATUS}")" = "connected" ] && return 0
done
return 1
}

internals() {
wlr-randr 2>/dev/null | awk '/^(DSI|eDP|LVDS)-/ { print $1 }'
}

### Internal panels only. A DPMS off on an active DP output makes
### msm_dp_display_disable() report the audio jack as unplugged, and the HPD
### plug events that would undo it are discarded while the output is off, so
### dock audio stays dead until a physical replug.
blank() {
local OUT COUNT=0

if [ "${1}" = "backlight" ]; then
${DEBUG} && log $0 "Blank: backlight"
_backlight 4
return 0
fi

if _compositor_is sway; then
for OUT in $(internals); do
swaymsg "output ${OUT} power off" >/dev/null 2>&1 && COUNT=$((COUNT + 1))
done
elif _compositor_is weston; then
weston-dpms -m off >/dev/null 2>&1 && COUNT=1
fi

### No compositor, or it knew of no internal panel to blank. Without this
### the screen would simply stay on.
if [ "${COUNT}" -eq 0 ]; then
${DEBUG} && log $0 "Blank: no panel powered off via DPMS, using backlight"
_backlight 4
else
${DEBUG} && log $0 "Blank: ${COUNT} panel(s) powered off"
fi
}

### Both layers, unconditionally: whichever one blanked the panel, the other
### must not be left holding it dark. Every output rather than the internals
### alone, so an external that something else powered down comes back as
### well; powering on a display that is already on does nothing.
unblank() {
${DEBUG} && log $0 "Unblank"
_backlight 0

if _compositor_is sway; then
swaymsg "output * power on" >/dev/null 2>&1
elif _compositor_is weston; then
weston-dpms -m on >/dev/null 2>&1
fi
}

case "${1}" in
connected) connected ;;
list) list ;;
internals) internals ;;
blank) blank "${2}" ;;
unblank) unblank ;;
*)
echo "usage: ${0##*/} connected|list|internals|blank [backlight]|unblank" >&2
exit 2
;;
esac
Loading