Skip to content

AYN Odin 2: fix external display output: micro HDMI capped at 1024x768, USB-C output often undetected, audio broken across hotplug and sleep - #3080

Open
xiaodoudou wants to merge 3 commits into
ROCKNIX:nextfrom
xiaodoudou:odin2-external-display
Open

AYN Odin 2: fix external display output: micro HDMI capped at 1024x768, USB-C output often undetected, audio broken across hotplug and sleep#3080
xiaodoudou wants to merge 3 commits into
ROCKNIX:nextfrom
xiaodoudou:odin2-external-display

Conversation

@xiaodoudou

@xiaodoudou xiaodoudou commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What is the goal of this PR?

Make external displays work properly on the AYN Odin 2 / Odin 2 Mini, on both of their output paths: reliable detection, correct video modes with a way to choose them, and sane audio behaviour across hotplug and sleep.

While the work was driven and tested on the Odin 2, several fixes live in shared scripts and benefit any device with external display support: display detection when the compositor misses a hotplug event, internal panel rotation after unplug, volume handling across connect/disconnect, and the fake-suspend behaviour with an external display attached (including a timed shutdown that could power off a device while it was driving a TV).

These devices have two external display paths. The USB-C path uses native DisplayPort alt mode; this covers the Odin 2 Official Dock and equally any USB-C video adapter (tested, they behave the same): once the connector is detected, EDID negotiation and audio work natively, but the hotplug event itself is often missed on current builds, leaving the dock undetected (this PR fixes that, see below). The micro HDMI port however goes through a Lontium LT8912B DSI to HDMI bridge, which has no DDC support at all (per Lontium's product brief, confirmed by probing on hardware), so the sink's EDID can never be read on this port. The kernel therefore falls back to fail safe modes topping out at 1024x768, and the hardcoded 1920x1080@60 modeset in output_monitor fails silently.

What this PR does:

  1. Device quirk (AYN Odin 2, AYN Odin 2 Mini): injects a synthetic EDID into the HDMI connector at boot, advertising the modes the bridge can actually drive: 1920x1080@60 (preferred) and 1280x720@60. 1080p60 is the most this bridge can output (150 MHz pixel clock limit), so no higher modes are possible. 30 Hz modes were also tested on hardware, but the bridge produced a corrupted signal with every 30 Hz timing an EDID can encode, so only the two 60 Hz modes are present on this PR.
  2. The external_display.mode setting, applied live by output_monitor within about 10 seconds, no replug or reboot needed: auto (default, keeps today's 1080p60 behaviour) or a fixed WIDTHxHEIGHT@RATE. A fixed mode the sink cannot do falls back to auto.
  3. output_monitor robustness fixes found while testing on hardware:
    • DRM connector status is now the ground truth: the kernel can know a display is connected while sway missed the hotplug event, and the explicit re-enable brings it up. A/B tested on the Odin 2: on current builds the Odin 2 Official Dock (and any USB-C adapter alike) often stays undetected forever; with this change it is detected reliably.
    • Fixes the internal screen coming back sideways: when the device boots with an external display connected and that display is later unplugged, sway has never configured the internal panel's rotation, so the screen reappeared rotated 90 degrees. output_monitor now re-applies the correct rotation (read from the kernel's panel orientation property) whenever it re-enables the internal panel.
    • Audio switching is gated behind DEVICE_EXTERNAL_DISPLAY_AUDIO. The micro HDMI port carries video only, no audio: testing AYN's Android firmware on the same hardware shows the same behaviour, and reverse engineering with Claude Code (the mainline lt8912b driver has no audio support and the DSP topology has no backend for the bridge's audio input) reached the same conclusion. Gating the audio switching off also removes the profile probing churn (including a wireplumber restart retry loop) that could randomly reset sink volumes to 100%. The DP alt mode path keeps its native jack based audio switching, which works.
    • Volume is kept sane across connect/disconnect: the speaker amps are parked at their hardware volume register while an external display carries the audio (so the stream migration burst at unplug is inaudible), sinks fade back in to the user volume (audio.volume), and a lost default sink is re-picked explicitly (wireplumber loses it on some profile flips, leaving dead volume keys and silence). Audio used to blast at 100% during the handoff and stay wrong until a volume key press.
    • wpctl/pw-cli calls are wrapped in timeouts so a wedged audio stack cannot freeze display switching.
  4. 111-sway-init honours the configured mode for displays already connected at boot, and defaults added to the system.cfg template.
  5. Kernel: restore USB-C DisplayPort output on 7.1 (release 20260801): the 7.1 kernel bump broke USB-C video on SM8550 entirely: power delivery negotiates (the device charges, the typec partner registers) but DP-1 never leaves "disconnected" while a dock or adapter is plugged in, so nothing is ever routed to the display. This is the same typec mux dedup regression fixed for SM8750 in SM8750: restore dock DisplayPort output broken by the 7.1 bump #3068, whose commit message already notes it is not device specific. The 0517 patch from SM8750: restore dock DisplayPort output broken by the 7.1 bump #3068 is copied unchanged to the SM8550 patch set, and is hardware validated on an Odin 2 (see Testing).
  6. fake-suspend fixes, each verified on hardware and against the mainline msm DP code:
    • display_off powers off only internal panels. A DPMS off on an active DP output makes msm_dp_display_disable() report the audio jack as unplugged, and HPD plug events arriving while the display is off are silently discarded, leaving DP audio dead until a physical replug.
    • Sink muting removed entirely: wireplumber persists sink mute per route, so a sink that disappears while muted (external display audio during a sleep cycle) stays muted on disk forever. The frozen processes stop producing audio anyway.
    • check_hdmi_connected now also matches DP connectors. Before, a docked (USB-C DP) device would fake-suspend despite the existing ROCKNIX "do not suspend while on an external display" rule, and its timed shutdown would then power the device off after 15 minutes while it was driving a TV.

Usage (no UI yet)

This concerns the micro HDMI port: with no EDID available there, the mode has to be chosen from the fixed list below (the USB-C path negotiates with the display natively and needs no manual setting). Until a settings menu exists, the mode is set over SSH and picked up live:

set_setting external_display.mode 1280x720@60    # force 720p60
set_setting external_display.mode 1920x1080@60   # force 1080p60
set_setting external_display.mode auto           # default (see below)

auto reproduces what the current output_monitor code already hardcodes on connect (note: the wiki documents the older manual sway config method, not this): try 1920x1080@60 first, then 1920x1080 at any refresh rate, and if the display offers no 1080p at all, keep the display's own preferred mode. This PR does not change that default, it makes it explicit and selectable. On the micro HDMI port auto always lands on 1080p60, since the synthetic EDID advertises it as preferred. A fixed mode the display cannot do falls back to auto.

Behaviour on other devices

The device quirk (synthetic EDID, audio gate) installs only on the AYN Odin 2 / Odin 2 Mini; no other device gets it, SM8550 or otherwise. The changes to the shared scripts (output_monitor, fake-suspend, 111-sway-init) do apply to every device with external display support: they are written to preserve the existing defaults (mode auto keeps the current hardcoded behaviour, the amp parking only acts where the amp controls exist, the audio gate only acts where the quirk sets it), but they were hardware tested on the Odin 2 only. Reviewers may want a smoke test on one non Odin device with HDMI out.

Testing

  • How was this tested?

AYN Odin 2, ROCKNIX 20260701, real hardware over SSH, with a boot time rig replicating the shipped ordering (quirk arms the EDID before sway starts). Sink was a Magewell USB Capture Card reporting exact mode and refresh.

  • Test results:

Micro HDMI adapter (LT8912B path):

  • Hotplug with auto: 1920x1080 at a true 60 Hz on the sink (previously 1024x768)
  • Fixed modes 1280x720@60 and 1920x1080@60: applied live within 10s, verified on the sink OSD, and a fixed mode survives replug
  • 30 Hz modes: bridge outputs a broken signal for every EDID encodable 720p30 timing, hence not proposed on that PR due to failure on testing
  • Unplug: internal panel returns with correct orientation and layout (the rotation fix was verified after booting with HDMI connected, which previously came back rotated)
  • Audio: stays on speakers by design (see above); plug cycles no longer disturb volume

Odin 2 Official Dock and USB-C adapters (DisplayPort alt mode path):

  • On current builds the Official Dock frequently fails to come up: on plug the Odin's own screen blinks and rescales, but the display connected to the dock/adapter never gets a picture (the DP output is never properly enabled). Reproduced across reboots; A/B verified by installing and removing the change on the same hardware: with the PR it comes up reliably
  • Once detected: real 256 byte sink EDID negotiated, video clean, external_display.mode applies to this path too (auto / 720p60 / 1080p60 verified)
  • Audio switches to the display automatically via the DP jack and back to speakers on unplug, always at the user volume (previously it could blast at 100% or stay muted forever)
  • Sleep cycle while docked: on ROCKNIX 20260701 a single power button cycle reproducibly killed dock audio until reboot (DPMS off drops the DP audio jack, the mute is persisted against the dying sink, and the DSP route is torn down). With this PR the power button follows the existing ROCKNIX "no suspend while on an external display" rule on DP docks as well, and audio survives; undock/redock recovers video and audio fully

Release 20260801 (kernel 7.1.2) and the 0517 kernel patch: on stock 20260801 the dock is dead, PD negotiates but DP-1 stays "disconnected" for as long as it is plugged in. This branch was built for SM8550 and flashed on the Odin 2: with the dock connected the display comes up at boot, DP-1 reports connected, the sink's real 256 byte EDID is read and the output runs 1920x1080@60. Unplug, replug and the micro HDMI path were re-tested on that build and behave as described above, including the speakers staying on the user volume while the video-only micro HDMI output is connected. One note for anyone bisecting this: the boot line pmic-glink: Failed to create device link ... with supplier usb0-sbu-mux is present with and without the patch, so it is a fw_devlink artifact, not the signature of the fault; the reliable symptom is DP-1 never leaving "disconnected".

A third party USB-C hub (HDMI plus USB/ethernet) was also tested and behaves identically to the Official Dock. Not hardware tested: note that I do not have an AYN Odin 2 Mini (same bridge, same display path) to test, but it should work in theory. Other devices could maybe inherit from this PR if needed.

Additional Context

  • The synthetic EDID carries exact 60.000 Hz timings because wlr-randr matches refresh rates exactly in mHz.
  • Real EDID negotiation on the micro HDMI port is not possible on any OS: the LT8912B has no DDC support at all (Lontium product brief), so the synthetic EDID is the definitive solution for this port rather than a stopgap.
  • Micro HDMI audio is out of reach for good: the chip accepts I2S audio per its datasheet, but AYN's Android firmware is video only on this port as well (verified on the device), so the board most likely never wired the audio input.
  • Known issue (documented, fix attempted): for a few seconds around the audio profile flip on connect/disconnect, playback has dropouts while the streams migrate to the new sink. A null test on ROCKNIX 20260701 with factory-fresh wireplumber state and no extra scripts running shows the same dropouts, so it is base system behaviour (pipewire/wireplumber stream migration), not introduced here. Attempted and ruled out from userspace: volume fade curves, hardware amp ramping, pinning the combined "HiFi (DisplayPort, Speaker)" profile with auto-profile disabled (wireplumber then refuses to move the default sink, both via wpctl set-default and via a priority.session rule), and full wireplumber state resets. A real fix needs work in wireplumber/pipewire session handling and is left as a follow-up issue.
  • EmulationStation repaints its volume OSD once when the default sink switches; cosmetic, ES side.
  • Risk areas for review: output_monitor is shared by all devices with external connectors. The new behaviour is gated on quirk variables and on a single setting whose default preserves today's behaviour.

AI Usage

Did you use AI tools to help write this code? PARTIALLY

Parts of the implementation were review/refactor with Claude Code. Root PR, debug analysis, design decisions, hardware testing and validation were done on a real Odin 2 by a human (Me).

@xiaodoudou
xiaodoudou force-pushed the odin2-external-display branch 3 times, most recently from 2734859 to 450b766 Compare August 2, 2026 03:00
@xiaodoudou

xiaodoudou commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Heads up for reviewers: testing on the new release 20260801 shows the 7.1 kernel bump has broken the USB-C DisplayPort path on SM8550 entirely, which affects the dock half of this PR.

Symptom on an AYN Odin 2 on 20260801 (kernel 7.1.2): power delivery negotiates (device charges, typec partner registers) but DP-1 never leaves "disconnected", and the boot log shows the connector being built without its SBU mux:

qcom_pmic_glink pmic-glink: Failed to create device link (0x180) with supplier usb0-sbu-mux for /pmic-glink/connector@0

The same dock works on the same hardware on release 20260701 (kernel 7.0.11). This is the typec_mux_match() dedup regression from the 7.1 merge window, already identified and fixed for SM8750 in #3068, whose commit message notes it is not device specific.

@xiaodoudou
xiaodoudou marked this pull request as draft August 2, 2026 03:08
@xiaodoudou
xiaodoudou force-pushed the odin2-external-display branch from 450b766 to b0840b8 Compare August 2, 2026 13:57
@xiaodoudou

Copy link
Copy Markdown
Contributor Author

Update: the 0517 kernel patch is now hardware validated on the Odin 2. I built this branch for SM8550 and flashed the image on the device: with the dock connected the display comes up at boot, DP-1 reports connected, the sink's 256 byte EDID is read, and the output runs 1920x1080@60. Unplug, replug and the micro HDMI path were re-tested on the same build. On stock 20260801 the same dock never leaves "disconnected".

Correction to my earlier comment: the boot line pmic-glink: Failed to create device link (0x180) with supplier usb0-sbu-mux appears on the patched, working build as well, so it is a fw_devlink cycle artifact rather than the signature of the regression. The reliable symptom is DP-1 staying "disconnected" with a dock plugged in.

Rebased on next and marking this ready for review.

PS: the PR description itself has been amended as well, so reviewers reading it now get the corrected wording rather than this comment's version: item 5 no longer cites that boot line as the symptom, and the Testing section carries the hardware results above.

@xiaodoudou
xiaodoudou marked this pull request as ready for review August 2, 2026 14:03
@xiaodoudou
xiaodoudou marked this pull request as draft August 3, 2026 07:42
@xiaodoudou

Copy link
Copy Markdown
Contributor Author

Let's put this PR in Draft until #2954 is getting merged.

@xiaodoudou

Copy link
Copy Markdown
Contributor Author

I keep this PR as draft waiting any progress regarding #3126

@spycat88
spycat88 force-pushed the next branch 2 times, most recently from c93f6bd to 135d112 Compare August 13, 2026 00:00
@xiaodoudou
xiaodoudou force-pushed the odin2-external-display branch from b0840b8 to 67e4eee Compare August 14, 2026 08:17
@xiaodoudou
xiaodoudou marked this pull request as ready for review August 14, 2026 08:17
@xiaodoudou
xiaodoudou force-pushed the odin2-external-display branch from 67e4eee to 3af5b96 Compare August 14, 2026 08:19
xiaodoudou and others added 2 commits August 14, 2026 16:22
check_hdmi_connected in rocknix-fake-suspend, _get_present_externals
and _get_all_internals in output_monitor, and the internal panel
blanking all answered the same two questions from their own copies:
is an external display attached, and which outputs are internal
panels. Three copies of each invites them to drift apart, and
drifting changes behaviour silently.

Move both into /usr/bin/external-display and have the callers ask:

  external-display connected|list|internals|blank [backlight]|unblank

fake-suspend loses its connector glob, its compositor ladder and its
backlight helpers, 66 lines net. output_monitor loses two functions.

Two behaviour changes fall out. blank falls back to the backlight
when the DPMS path powered nothing off, where before an internal
panel missing from wlr-randr left the screen on. And the connector
match now accepts a two digit index, so HDMI-A-10 counts.

blank touches internal panels only while unblank restores every
output. That asymmetry is deliberate: powering off an external
output drops the DP audio jack, while powering one on is free and
recovers an output that something else left dark.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found on an AYN Odin 2 on kernel 7.1.2. Unplug an external display
and audio is gone until a reboot. Every userspace indicator looks
healthy while it happens, which is what made it hard to see: the
Speaker sink is marked default, both sinks sit at the user volume,
and the amp registers read 0.

The DSP is where it goes wrong. DP0 Jack stays on after the cable
is pulled, so the route stays on DISPLAY_PORT_RX_0 with
RX_CODEC_DMA_RX_0 and PRIMARY_MI2S_RX switched off, and playback
lands on a backend with nothing on the end of it. Userspace does
not notice because nothing in userspace is wrong.

_switch_audio was gated off entirely here because the micro HDMI
port carries no audio. That gate also suppressed the switch back,
which is the only thing that rebuilds the route. It was invisible
until now because on 7.0 the jack reported the unplug and
wireplumber moved audio back by itself. Gate the outbound direction
only: docking still needs no help, coming back does.

Re-selecting the profile re-initialises the codec and resets SPK_L
and SPK_R PCM Playback Volume to their silent 1023 default, and
that can land after the single write that unparks them. Assert the
unpark until it sticks, otherwise the speakers stay mute with
everything else looking fine.

Pin auto to 60 Hz while here. Falling back to 1920x1080 at any
refresh let a dock's EDID put the output at 100 or 144 Hz. auto now
tries 1080p60, then 720p60, then leaves the sink on its preferred
mode.

Tested on an AYN Odin 2, dock and micro HDMI, connect and
disconnect: audio returns to the speakers every time with no
reboot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xiaodoudou
xiaodoudou force-pushed the odin2-external-display branch from 3af5b96 to d8f0ef3 Compare August 14, 2026 08:22
@xiaodoudou

Copy link
Copy Markdown
Contributor Author

Rebased onto current next, and out of draft. This isn't waiting on #3126 anymore.

Here is why it was waiting. Once real suspend works, the power key stops going through rocknix-fake-suspend, and the rule that stops the device going to sleep while it is driving a TV goes with it. So this looked like it needed the new suspend code to land first.

It doesn't. Two scripts were each answering the same two questions with their own code: is an external display plugged in, and which outputs are the built-in panels. Both answers now come from one helper:

external-display connected|list|internals|blank [backlight]|unblank

rocknix-fake-suspend and output_monitor both call it, and it is 66 lines shorter than the code it replaces. Whatever handles the power key once real suspend lands calls the same helper, so the rule survives the change without anyone rewriting it.

Doing that refactor turned up two bugs, both fixed here. Audio stayed dead after unplugging a display until a reboot: switching back to the speakers was being skipped along with switching out to the display, and switching back is the only thing that rebuilds the route. And auto was taking whatever refresh rate a dock advertised, up to 144 Hz, so it is 60 Hz only now. Both mechanisms are in the commit messages. The second one makes the Usage section above out of date and I will fix it.

All retested end to end on an AYN Odin 2 over fake suspend: dock and micro HDMI, connect and disconnect, power key docked and undocked. Audio comes back on the speakers every time with no reboot.

If suspend land as merged, will do a new PR to follow this new helper for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant