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
6 changes: 3 additions & 3 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
ARG FEX_PKG=ghcr.io/armada-os/armada-packages/fex@sha256:6301fb21fe1d540237b431e75c3369728d824e30b6cdc138faf44271b015785d
ARG MESA_PKG=ghcr.io/armada-os/armada-packages/mesa@sha256:d3faf6285a93c45e2edf16df963fb039ba31354a3edc2ff7c2618ba410ffee59
ARG MESA_PKG=ghcr.io/armada-os/armada-packages/mesa@sha256:7889b00b71ddeb294d3672c1c931663e03e5e35cab44041ce981765a1f449e16
ARG MESA_ANDROID_PKG=ghcr.io/armada-os/armada-packages/mesa-android@sha256:2ef4f1a325502f9ba695acda0ca995d996ed21bf4eb1e706d15351f73cd2b406
ARG MANGOHUD_PKG=ghcr.io/armada-os/armada-packages/mangohud@sha256:6ed92b44d267a8d2e1339968b59c2679cfd30e81494d4990dcc2c92e0be4fc10
ARG GAMESCOPE_PKG=ghcr.io/armada-os/armada-packages/gamescope@sha256:5c8896b2ef14b75e9e887bcea4a3ffd6d046bf9fe6e1a4133812880a76744a1d
ARG GAMESCOPE_SESSION_PKG=ghcr.io/armada-os/armada-packages/gamescope-session@sha256:d44de289a54eb6d7b2af9b0505fc7580106dbe62318edd4a9a3afd3383351fc8
ARG POWERDEVIL_PKG=ghcr.io/armada-os/armada-packages/powerdevil@sha256:f6d25143dca84f5f71076a3c992e06de87f7ae25fd046cfeb21999df989c4f8b
ARG KERNEL_PKG=ghcr.io/armada-os/armada-packages/kernel@sha256:c437bf717449d435df09c0ca0735add1d5a95e10e68821053740a4abf3c8f502
ARG KERNEL_PKG=ghcr.io/armada-os/armada-packages/kernel@sha256:3e1e4b24eedba6e9bf0c8b88f9e7aac36585e1afb5e95d0b1bd092b6ffb88719
ARG INPUTPLUMBER_PKG=ghcr.io/armada-os/armada-packages/inputplumber@sha256:1369b521b95af6b34b434ac930889faea6e1d18f0a4922a7e90bcb6837da1ad7
ARG EXTEST_PKG=ghcr.io/armada-os/armada-packages/extest@sha256:c68bd452dd8f9a20527862e87fd446045b86811dc222a2a1744ede8d8b858dfa
ARG NETWORKMANAGER_PKG=ghcr.io/armada-os/armada-packages/networkmanager@sha256:043eae7f6f236945bc66466337391384949f56ad19807f21fe2e9b6f5c488b5f
ARG JUPITER_HW_SUPPORT_PKG=ghcr.io/armada-os/armada-packages/jupiter-hw-support@sha256:9bb3b94ced508eccb11ae4ed98b00657c202bf78ad797bf6ece345d1ec19b552
ARG ARMADA_SPLASH_PKG=ghcr.io/armada-os/armada-packages/armada-splash@sha256:448d11f92d7687ef190dad1ba13d18cf7b4c428672027c595a0bdba0dbd3cea5
ARG ARMADA_SPLASH_PKG=ghcr.io/armada-os/armada-packages/armada-splash@sha256:d4a42ed2e876b5b5091e75bd70f5c86ecc701291f251e70ecd047858922be90a

FROM ${FEX_PKG} AS fex
FROM ${MESA_PKG} AS mesa
Expand Down
22 changes: 13 additions & 9 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,20 @@ build $target_image=image_name $tag=default_tag:
BUILD_ARGS+=("--build-arg" "ARMADA_VERSION=${ARMADA_VERSION}")

# Allow local armada-packages images to override pinned package images.
mapfile -t PKG_VARS < <(sed -n 's/^ARG \([A-Z0-9_]*_PKG\)=.*/\1/p' Containerfile)
declare -A KNOWN_PKG_VARS=()
for var in "${PKG_VARS[@]}"; do
KNOWN_PKG_VARS["${var}"]=1
done
PKG_VARS=()
while IFS= read -r var; do
PKG_VARS+=("${var}")
done < <(sed -n 's/^ARG \([A-Z0-9_]*_PKG\)=.*/\1/p' Containerfile)
for p in ${ARMADA_LOCAL_PKGS:-}; do
var="$(echo "$p" | tr '[:lower:]-' '[:upper:]_')_PKG"
if [[ -z "${KNOWN_PKG_VARS[$var]:-}" ]]; then
known=0
for known_var in "${PKG_VARS[@]}"; do
if [[ "${known_var}" == "${var}" ]]; then
known=1
break
fi
done
if [[ "${known}" != 1 ]]; then
echo "unknown package in ARMADA_LOCAL_PKGS: ${p}" >&2
exit 1
fi
Expand All @@ -110,14 +116,12 @@ build $target_image=image_name $tag=default_tag:
PULL_POLICY="newer"
[[ "${LOCAL_PKG}" == 1 ]] && PULL_POLICY="missing"

SECRET_ARGS=()
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
SECRET_ARGS+=("--secret" "id=GITHUB_TOKEN,env=GITHUB_TOKEN")
BUILD_ARGS+=("--secret" "id=GITHUB_TOKEN,env=GITHUB_TOKEN")
fi

podman build \
"${BUILD_ARGS[@]}" \
"${SECRET_ARGS[@]}" \
--platform linux/arm64 \
--pull="${PULL_POLICY}" \
--tag "${target_image}:${tag}" \
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,16 @@ desktop. The **Bazaar** app store and the **Armada Installer**

### Power button and sleep

Pressing the power button does a "fake suspend" (inspired by ROCKNIX) rather than
real S3 sleep: it blanks the screen and freezes the session, and the same press
wakes it. Because the device does not truly sleep, idle battery drain is higher
than it would be with real suspend.
The Retroid Pocket 6 uses SM8550 low-power s2idle, which reaches the SoC's
cluster sleep state while retaining the firmware wake contract. The AYN Odin 2
family and AYN Thor use deep suspend-to-RAM.
Other devices use a "fake suspend" inspired by ROCKNIX: it blanks the screen and
freezes the session, and the same press wakes it. Fake suspend does not truly
sleep, so its idle battery drain is higher.

The Retroid Pocket 6 charger-wake design and its relationship to Qualcomm's
Android implementation are documented in
[`docs/deep-sleep-charger-wake.md`](docs/deep-sleep-charger-wake.md).

## Updating

Expand Down
2 changes: 2 additions & 0 deletions build_files/10-base-packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ dnf5 -y install --setopt=install_weak_deps=False \
sudo \
rsync \
curl \
git \
jq \
htop \
lsof \
scx-scheds \
unzip \
fuse \
evtest \
dbus-x11 \
xdg-user-dirs \
Expand Down
3 changes: 2 additions & 1 deletion build_files/20-install-kernel.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash
set -euxo pipefail

KVER="7.0.11"
KVER="7.1.5"
TARBALL="/packages/kernel/armada-kernel-${KVER}.tar.zst"

# bootc expects exactly one kernel under /usr/lib/modules.
Expand All @@ -19,6 +19,7 @@ depmod -a "${KVER}" -b /
# at its runtime path.
mkdir -p /usr/lib/firmware
cp -a /ctx/system_files/usr/lib/firmware/. /usr/lib/firmware/
bash /ctx/build_files/ensure-rp6-firmware.sh

echo "armada kernel ${KVER} installed at /usr/lib/modules/${KVER}/"
ls -la "/usr/lib/modules/${KVER}/" | head -10
3 changes: 3 additions & 0 deletions build_files/55-generate-initramfs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dracut \
--kver "${KVER}" \
--add ostree \
--add armada-splash \
--add armada-ostree-fallback \
"${IMG}" "${KVER}"

# dracut drops modules silently: fail the build rather than ship without.
Expand All @@ -29,6 +30,8 @@ for required in \
usr/libexec/armada/armada-splash-launcher \
usr/libexec/armada/device-env \
usr/share/armada/splash/splash.asp \
usr/libexec/armada/armada-ostree-fallback \
usr/lib/systemd/system/ostree-prepare-root.service.d/armada-fallback.conf \
usr/lib/ostree/ostree-prepare-root; do
if ! awk -v p="${required}" '$NF == p { found=1 } END { exit !found }' <<<"${contents}"; then
echo "ERROR: ${required} missing from initramfs"
Expand Down
30 changes: 30 additions & 0 deletions build_files/ensure-rp6-firmware.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail

FIRMWARE_ROOT="${FIRMWARE_ROOT:-/usr/lib/firmware}"
ODIN2_DIR="${FIRMWARE_ROOT}/qcom/sm8550/ayn/odin2"
RP6_PARENT="${FIRMWARE_ROOT}/qcom/sm8550/retroidpocket"
RP6_DIR="${RP6_PARENT}/rp6"

# The RP6 currently uses the same ADSP and speaker-amplifier firmware as the
# Odin 2. Keep the device-specific kernel path so native RP6 firmware can
# replace this compatibility link later without another DTB change.
if [[ ! -e "${RP6_DIR}" && ! -L "${RP6_DIR}" ]]; then
mkdir -p "${RP6_PARENT}"
ln -s ../ayn/odin2 "${RP6_DIR}"
fi

for required in \
adsp.mbn \
adsp_dtb.mbn \
adspr.jsn \
adsps.jsn \
adspua.jsn \
aw883xx_acf.bin \
battmgr.jsn; do
if [[ ! -r "${RP6_DIR}/${required}" ]]; then
echo "ERROR: RP6 firmware missing: ${RP6_DIR}/${required}" >&2
echo " compatibility source: ${ODIN2_DIR}/${required}" >&2
exit 1
fi
done
151 changes: 151 additions & 0 deletions docs/deep-sleep-charger-wake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Deep-sleep charger wake on Qualcomm handhelds

## Why the wake source is broad

SM8550-class devices do not deliver USB-C charger attachment to Linux through
the DWC3 gadget PHY while the PHY is powered down. Type-C and charging state are
owned by Qualcomm's `charger_pd` firmware on the ADSP and reach Linux through
the `PMIC_RTR_ADSP_APPS` GLINK service.

This is also how published Android kernels for the same SoC are structured:

- The OnePlus SM8550 Kalama device tree places the battery charger and UCSI
clients under PMIC GLINK on the ADSP:
<https://github.com/OnePlusOSS/android_kernel_modules_and_devicetree_oneplus_sm8550/blob/0241347436d48df011e2a6f6fe2d8740c26a379c/kernel_platform/qcom/proprietary/devicetree/qcom/kalama.dtsi>
- Qualcomm's downstream IPCC driver enables the shared parent IRQ as a wake
source:
<https://github.com/OnePlusOSS/android_kernel_oneplus_sm8550/blob/c462ef8ffab7a58e035ee04705b16cdfced494b1/drivers/mailbox/qcom-ipcc.c#L240-L247>
- Its GLINK transport calls `pm_system_wakeup()` on the first GLINK packet
received during suspend, before the packet's client channel is decoded:
<https://github.com/OnePlusOSS/android_kernel_oneplus_sm8550/blob/c462ef8ffab7a58e035ee04705b16cdfced494b1/drivers/rpmsg/qcom_glink_native.c#L1482-L1491>
- The battery client subsequently publishes the decoded power-supply change and
takes a short wake lock so userspace can process it:
<https://github.com/OnePlusOSS/android_kernel_oneplus_sm8550/blob/c462ef8ffab7a58e035ee04705b16cdfced494b1/drivers/power/supply/qti_battery_charger.c#L836-L885>

Motorola's published Qualcomm kernel uses the same unconditional IPCC parent
wake, and another SM8550 Android tree contains the same first-packet GLINK wake
logic:

- <https://github.com/MotorolaMobilityLLC/kernel-msm/blob/0450c5076a8ec9415a10888c8c183ef6ff5f91b8/drivers/mailbox/qcom-ipcc.c>
- <https://github.com/samsung-sm8550/android_kernel_samsung_sm8550/blob/cdb041119cfd53132ecd2ae1eb91a680473f5fc9/drivers/rpmsg/qcom_glink_native.c>

The interrupt controller therefore cannot select charger packets while the AP
is asleep. It only knows that the ADSP has queued GLINK data. Packet semantics
become available after the AP has started resuming.

ROCKNIX PR #2954 identified a concrete source of background traffic on the RP6:
the ADSP charger firmware sends an unsolicited `BATTMGR_NOTIFICATION` (opcode
`0x7`) shortly after suspend entry. Its IPCC patch removes `IRQF_NO_SUSPEND` so
that notification is deferred instead of being handled against suspended
devices. Armada carries that fix. Charger-attach support then deliberately
marks the selected ADSP GLINK edge wake-capable, so a separate wake policy is
still required to distinguish a real offline-to-online transition from those
unsolicited transport notifications:

<https://github.com/ROCKNIX/distribution/pull/2954>

## Why Android does not expose every transport wake

Android combines this broad kernel wake source with a continuously running
autosuspend policy. AOSP's SystemSuspend service repeatedly uses
`/sys/power/wakeup_count` and `/sys/power/state` whenever no wake lock requires
the system to remain awake:

<https://source.android.com/docs/core/power/systemsuspend>

When external power actually changes, Android's PowerManagerService can turn
that background resume into an intentional, visible wake:

<https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android13-release/services/core/java/com/android/server/power/PowerManagerService.java>

Armada uses systemd's one-shot suspend transaction instead. Without an
additional policy, every GLINK interrupt ends that transaction and thaws the
graphical session, including unrelated ADSP traffic.

## Armada implementation

The kernel side deliberately follows the Qualcomm architecture:

1. Propagate wake configuration from the selected ADSP GLINK child IRQ through
the shared IPCC parent.
2. Mark only the Retroid Pocket 6 ADSP GLINK edge as a wake source.
3. Defer GLINK callbacks until orderly device resume instead of using
`IRQF_NO_SUSPEND`.

The userspace side fills Android's wake-policy role while systemd-sleep still
has `user.slice` frozen:

1. Snapshot external-power state and the physical power-key interrupt count
before entering suspend. Keep the external-power snapshot updated across
hidden background resumes, so unplug followed by replug is recognized as a
new offline-to-online transition within the same suspend transaction.
2. Read the standard kernel wake IRQ record from `/sys/power/pm_wakeup_irq`,
enabled by `CONFIG_PM_SLEEP_DEBUG`, and resolve it through `/proc/interrupts`
in the system-sleep hook.
3. Accept an increased physical power-key count or a real external-power
transition from offline to online.
4. For a positively identified GLINK/IPCC transport wake with no charger
transition, use the atomic `wakeup_count` protocol and return directly to
`mem` sleep.
5. If another notification races the `wakeup_count`/`power/state` handshake and
the kernel rejects the resuspend with `-EBUSY`, re-check the power key and
charger state and retry the already-classified background transaction. The
retry count is bounded so a persistent kernel failure still resumes safely.
6. Treat missing or unrecognized data as a normal resume. Classification must
fail open so diagnostics or future kernel changes cannot trap the device in
suspend.

This does not recursively invoke systemd, manipulate Gamescope, or run display
commands during resume. Pre/post system-sleep hooks run inside the existing
systemd-sleep process-freeze boundary.

### Why the standard wake IRQ record is sufficient

Hardware traces on the Retroid Pocket 6 show that the GIC IPCC summary IRQ 13
returns the SoC from firmware deep sleep. Linux then dispatches the nested
GLINK child IRQ 230 after noirq device resume. The generic IRQ wakeup code
records IRQ 13, which is enough for policy: both `ipcc_0` and `glink-smem` are
transport wakes whose packet meaning is only available later in resume.

`/sys/power/pm_wakeup_irq` is compiled only when `CONFIG_PM_DEBUG=y`, which
derives `CONFIG_PM_SLEEP_DEBUG=y`. Armada uses these options for the standard
wake-reason attribute; verbose `pm_debug_messages` and `pm_print_times` remain
off. An early test kernel omitted the options, so the attribute did not exist
and the deliberately fail-open policy classified every transport wake as
`unknown`. Enabling the interface is sufficient; no custom IPCC wake-reason
patch is required.

The attribute has been documented in Linux's sysfs ABI since April 2015, with
the explicit purpose of reporting the first armed IRQ seen during the most
recent suspend/resume cycle:

<https://github.com/torvalds/linux/blob/master/Documentation/ABI/testing/sysfs-power>

It remains in the `ABI/testing` category rather than `ABI/stable`, so the
userspace policy checks for its presence and fails open to a normal resume if
it is unavailable. Armada does not enable `PM_ADVANCED_DEBUG`, `PM_TEST_SUSPEND`,
or the PM watchdog.

The relevant upstream implementations are:

- generic IRQ wake accounting:
<https://github.com/torvalds/linux/blob/master/kernel/irq/pm.c>
- wake IRQ storage and access:
<https://github.com/torvalds/linux/blob/master/drivers/base/power/wakeup.c>
- the sysfs attribute and its Kconfig guard:
<https://github.com/torvalds/linux/blob/master/kernel/power/main.c>

The userspace policy also compares the physical power-key interrupt counter
because power-button resume can have unrelated GLINK traffic queued by the time
the IPCC handler runs.

## Rejected alternatives

- **DWC3 device-mode PHY wake IRQs:** the IRQs could be armed, but physical
charger attachment did not assert them while the PHY was powered down.
- **Cable-orientation GPIO:** the exposed GPIO changes with plug orientation,
not attachment, and is therefore not a usable VBUS signal.
- **Recursive suspend plus compositor blanking:** this exposed part of the
graphical resume path, produced display flashes, and broke ordinary resume.
- **Charger-only filtering in the interrupt handler:** the GLINK packet owner
and opcode are not known until after the shared IPCC wake has resumed the AP.
2 changes: 1 addition & 1 deletion post_process/finalize-armada-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
set -euxo pipefail

RAW_IMAGE="${1:-output/raw/disk.raw}"
ROCKNIX_ABL_VERSION="${ROCKNIX_ABL_VERSION:-v1.1.6}"
ROCKNIX_ABL_VERSION="${ROCKNIX_ABL_VERSION:-v1.1.7}"
OUT="${OUT:-output/armada-$(TZ='America/New_York' date +%Y%m%d).img.gz}"
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)

Expand Down
11 changes: 1 addition & 10 deletions post_process/make-bootimg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,7 @@ for _name in ${SUPPORTED_DTBS}; do
done
CONTENT_ID=$(sudo bash -c "source '${ARMADA_LIB}/bootimg-args'; armada_bootimg_content_id '${KPATH}' '${IPATH}' ${_DTB_ARGS}")
STAMP_ID=$(armada_bootimg_id "${LINUX_LINE}" "${INITRD_LINE}" "${OPTIONS_LINE}" "${DTB_LIST}" "${ARMADA_LIB}/bootimg-args" "${CONTENT_ID}")
CMDLINE="${OPTIONS_LINE}"

# Fit the 512-byte cmdline: drop serial console, ostree= first, keep splash kargs.
_drop=" console=ttyS0 "
_ostree=""; _rest=""
for _t in ${CMDLINE}; do
case "${_drop}" in *" ${_t} "*) continue ;; esac
case "${_t}" in ostree=*) _ostree="${_t}" ;; *) _rest="${_rest} ${_t}" ;; esac
done
CMDLINE="${_ostree}${_rest}"
CMDLINE=$(armada_bootimg_cmdline "${OPTIONS_LINE}") || { echo "ERROR: no ostree= karg in ${BLS}"; exit 1; }

if [[ "${#CMDLINE}" -gt "${ARMADA_CMDLINE_MAX}" ]]; then
echo "ERROR: cmdline is ${#CMDLINE}B, over the ${ARMADA_CMDLINE_MAX}B boot-header limit"; exit 1
Expand Down
Loading