Skip to content

Bring up Q25 input, Panfrost rendering and truthful battery status - #52

Draft
patrickjquinn wants to merge 132 commits into
mainfrom
fix/q25-software-welcome-and-scale
Draft

patrickjquinn wants to merge 132 commits into
mainfrom
fix/q25-software-welcome-and-scale

Conversation

@patrickjquinn

@patrickjquinn patrickjquinn commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

The Q25 device profile now applies 1.5× scaling and native evdev touch through greetd’s PAM session. Home seeding preserves migrated settings and keys. Guarded Panfrost startup checks the tested module/shim/KMS closure and rail relationship, then selects GPU rendering or the software fallback.

Battery state stays unknown when the kernel provides no measured capacity. Shell and Settings preserve charging independently, avoid false 0% warnings/shutdown, and clear invalidated UPower values. The status bar uses a battery glyph without a question-mark percentage placeholder. A separate recorded AUXADC patch fixes voltage scaling; it is not yet wired into the canonical kernel APK recipe.

Validation:

  • 7 session, 4 GPU-package and 4 real seeder regressions pass, plus the existing reachability/translation lints. CI now registers these tests and runs seeding with actual systemd-tmpfiles.
  • The full 783-package mutable root boots on Q25 with working keyboard/touch. User confirmed clean, smooth rendering.
  • New Shell, runner and Settings deployed; 12 runtime-file hashes and native-loader checks pass. Battery page visibly shows “Battery level unavailable” and “Charging”; corrected kernel reports 4.182–4.183 V.
  • A 16-swipe Settings run had steady compositor swap-notification P50 5.992–6.068 ms and P99 7.547–7.939 ms, without new GPU faults. These are not physical refresh or input-to-photon measurements.
  • One screenshot validation crash resolves to an invalid XDG role pointer in the secure-content check. Lifecycle repair and final device regression are in progress; this PR remains draft.

Bring-up limits: CPU0 offline, GUI affinity 4–7, temporary GPU PM-on/390 MHz policy, KMSRO with firmware SimpleDRM scanout, mutable carrier root. Native MediaTek display, dynamic GPU power, accurate battery percentage, modem operation and full phone qualification remain open. See devices/zinwa/q25/VALIDATION-20261030.md for measurements and limitations.

Stop the perpetual welcome glow opacity animation under Qt software rendering and reduced motion; retain accelerated animation. This is the QML source used by the deployed 639e396d efficiency candidate, without its diagnostic main.cpp instrumentation.

Declare Q25 scale policy as FORCE_DPI=240 in its device manifest and generated profile overlay, including the normal greetd session entry. Leave physical panel DPI unset. The target is reserved bring-up metadata, not a new image/flash pipeline; no CPU, kernel or GPU policy is installed.

Validation: seven device manifests valid; generated profile and overlay SHA512 checks pass; shell scripts and greetd TOML pass. Qt 6.11.2 qmllint exits 0 for baseline and candidate with identical 122 existing warnings and no new warnings. Earlier offscreen software steady CPU sample fell 20.67% to 1.83%; device observations were not a controlled same-scale A/B.
The shared udev rule hides touchscreens from libinput, but the Shell started its evdev touch reader only on EGLFS. LinuxFB therefore had no touchscreen consumer. Start the same Qt InputSupport reader on both backends, including colon-qualified platform specifications; preserve generic-plugin override behavior.

Validation: exact source condition passed 24 backend/generic-plugin combinations. main.cpp compiled and linked alone against the cached aarch64 musl Qt6.11.1 SDK and existing Rust objects. Remove the temporary Q25 LIBINPUT_IGNORE_DEVICE-clearing override when deploying to avoid duplicate libinput/evdev touch readers. Physical touch validation remains parent-owned.
Select a package-owned greetd config through its service drop-in and start both sessions through q25-shell-session. This sets LinuxFB/software/basic/fullscreen and DPI240 after PAM, then delegates to the normal launcher for D-Bus, paths and cgroups. Preserve explicit render-loop choices in the normal launcher while defaulting to threaded.

Replace the earlier base-config symlink takeover. Keep the reviewed Q25 CPU4-7 policy and process-scoped noafbc workaround from the source handoff. GPU module packaging remains a separate delivery: retain device-mediatek-g99/greetd dependencies rather than depend on its unmerged q25-gpu-render package.

Validation: four empty-environment/config/launcher tests pass; seven device manifests valid; overlay and launcher checksums and shell syntax pass. No binary build, frozen image changes or target calls.
Append pipeline patch0052 so tmpfiles copies defaults only into missing entries and the first-run seeder preserves existing configuration even when its marker is absent. Keep empty files, dangling config symlinks and authorized keys through repeated boot.

Validation: reproduce failures with the original generated seeder, apply patch0052 with fuzz0, then pass all four tests against the actual generated shell/tmpfiles payload. Patch numbering check passes with only documented absent0011. No image rebuild or deployment.
@github-actions

Copy link
Copy Markdown

Formatting changes required
This PR does not follow the formatting rules.
Please run:

clang-format -i <files>
qmlformat -i <files> (must be from Qt 6)
cargo fmt --manifest-path crates/Cargo.toml --all

Commit the updated files and push again.

@patrickjquinn patrickjquinn changed the title Bring up Q25 touch, scaling and guarded Panfrost rendering Bring up Q25 input, Panfrost rendering and truthful battery status Oct 30, 2026
…n recovery

Deckard d5c3178..83e46e5: the slimmed Android 16 runtime; idle apps'
anonymous memory paged out instead of the container's file cache being
reclaimed (an Android cold start after idle went from 10-11 s back to
~0.75 s on the HackberryPi); and a container daemon that finishes an
interrupted teardown instead of refusing every later start.
Alpine edge moved to clippy 1.99, whose pedantic assert_is_empty rejects
assert!(x.is_empty()): CI stopped at reef-backend and would have stopped
at pilotd and keel-actions-schema next. Each check now binds the value
once and prints it when it fails. battery_measurement's tests take the
workspace's usual unwrap allowance. The whole workspace is clean under
clippy 1.99 with -D warnings; pilotd, reef-backend and
keel-actions-schema tests pass.
Doze left a keyboard's light on all night (the Q25's sidecar exposes it
as q25::kbd_backlight). Turning the screen off now saves the level of
every LED named *::kbd_backlight and writes 0; waking restores exactly
that level, so a light the user had at 0 stays at 0, and a repeated off
does not overwrite the saved level. It scans on each off, since the
LED's module may load after the shell starts.

On the Q25 via the power key: 255 -> 0 in Doze -> 255 on wake; a user
level of 0 stayed 0; five cycles at 128 restored 128 each time.
The Q25's call, menu, back and end keys did nothing. Call answers a
ringing call or opens Phone; end declines or hangs up, else goes home;
menu (KEY_APPSELECT, which Qt has no key for, so it is matched by scan
code) toggles Active Frames and the app grid, or goes home from an app;
back is back. The incoming-call overlay's answer and decline are now
functions the keys share with its buttons.

Verified on the Q25: each key from home and from Phone, with the shell
staying up; locked, the keys do nothing.
a412694 cleared a toplevel's cached roles on destruction; popups kept
theirs. QML reads the surface's xdgSurface property, so a popup whose
role died before its wl_surface left a dangling pointer behind. The
unread xdgPopup property is dropped rather than tracked.

Ran on the Q25: unlock, Phone open/close and the grid toggle with the
shell staying up.
@patrickjquinn
patrickjquinn force-pushed the fix/q25-software-welcome-and-scale branch from 9961083 to 189c7bb Compare October 30, 2026 21:33
The keyboard light driver was loaded by hand, so it was gone after a
reboot. The device package now loads it after the keyboard (softdep)
with its control options, and udev's existing LED rule lets the shell
write it.

systemd-backlight saved the light's level at shutdown, and Doze had
usually put it out, so it came back at 0 and the shell restored that 0
on every wake. That unit is masked for this light; the keyboard's own
255 is the boot level.

Verified on the Q25 across two reboots, the second from Doze: both
modules load, the light comes up at 255, goes to 0 in Doze and back to
255 on wake. The .ko itself is still installed by hand, like the
keyboard driver; neither is in a kernel package yet.
The network actor probed NetworkManager's devices once, at start. A Wi-Fi
adapter that came later (a firmware load after boot, a USB dongle, a
NetworkManager restart) was never seen, so Settings said "no adapter"
while NetworkManager had one. It now re-probes, and rescans, whenever
NetworkManager's Devices change, and drops a device path that is gone.

Verified on the Q25: with the shell running, Wi-Fi brought up late and
NetworkManager restarted, Settings lists the nearby networks.
The first pipeline image for the Q25 only replaces the root file on the
carrier, so the risk is in what the new root lacks compared with the
hand-staged one the phone runs today. This compares the apk package set,
systemd enablement and the /etc and /usr/libexec file lists (type, mode,
owner, sha256 or link target) and fails on any difference that no
--explain rule accounts for.

The live side is captured read-only over ssh with python3 -I -B so no
bytecode lands in the phone's root. The image side is read straight from
the ext4 file: debugfs rdump ran at about 2 MB/s on the build drive and
loses ownership, and rootless fuse2fs refuses to mount, so a small
extent-only reader walks the image instead. Its test builds an image
with mkfs.ext4 -d and checks it against the source tree.
per-phone-state.toml lists what any new root will differ in on a
lived-in phone (identity, clock, connections, TuneD state, runtime
bytecode, sshd enablement). hand-staged-20261030.toml records what was
copied and enabled by hand on top of q25-duranium-20261030-eff639e,
grouped as Q25-IMAGE-PLAN §1.4 (a) packaged, (b) unpackaged and (d)
junk.

With both files the frozen eff639e image diffs against the live phone
with no unexplained difference (97 explained: 15 unit, 82 file, 0
package). A pipeline image is checked with per-phone-state.toml alone,
so every hand-staged item has to be in its packages.
The first image keeps the proven kernel, carrier initramfs and DTB, so
the new boot entry is the current default entry with only
q25.rootfs_file changed. The script moves the image onto the carrier
under a new name (never overwriting one), writes the entry, backs up
loader.conf as loader.conf.before-NAME and points default at the new
entry; the old entry stays as the fallback. rollback restores
loader.conf and removes the entries but leaves the image to inspect.

--counted builds the §3.4 layout instead: marathon-q25-b-NAME+3.conf,
a marathon-q25-a-fallback.conf copy of the current entry, and a glob
default. It stays opt-in until T2/T3 prove boot counting and a bless
unit exists, since without one a good entry falls back after three
boots. sync is scoped to the touched filesystem with sync -f.
The Q25 target now has scripts/build-zinwa-q25-image.sh. It builds the
root alone and leaves the phone's ESP, kernel and initramfs as they are,
so build.bootloader is none, not uki.
The package still shipped the first hx32062se, whose proximity
thresholds were the stock 3800/3300. On the Q25 the sensor reads 0 with
nothing near and only 500-1400 with a finger over it, so it never
reported near. Its ALS reading also included the chip's ~273k dark
offset, so lux was meaningless.

This is the v7 driver the phone has been running: near defaults to 600
(a near_level parameter, then the DT proximity-near-level, override
it), the chip's far threshold sits at 65% of near, and the ALS value is
offset-corrected from efuse 0x95 with the stock gain-1 lux scale.

The shipped binary is the phone's module (sha256 7b4e9ac6) without
debug info. Rebuilt from this source with build.sh's flags against the
7.0.0-q25-cfi-test tree it matches under kocmp.py.
hx32062se takes its near level from the DT before its own default of
600, and both the Q25's live DTB and the board DTS in the kernel series
still say proximity-near-level = <3800>, the stock value. The sensor
never reads above about 1400, so with that level it never reports near.

The phone has been running with a hand-placed
/etc/modprobe.d/q25-hx.conf setting near_level=600; ship the same
option in the device package so a fresh root keeps it. It can go once
the DTB carries 600.
The phone runs a newer wlan_drv_gen4m_6789 (sha256 1390ea66) than the
package shipped, so a fresh root would quietly lose the two knobs the
display-interference A/B test uses:

- q25_online_scan (runtime, default 1): 0 makes mtk_cfg80211_scan
  return -EBUSY while connected, which stops bgscan and UI scans from
  dwelling off-channel;
- q25_pdma_bt_size (load time, default 3, the stock value): caps the
  WFDMA burst size asicPdmaConfig programs.

Defaults keep stock behaviour. 0002 gains the four hunks; the shipped
binary is the phone's. Rebuilt from the donor plus this 0002 against
the 7.0.0-q25-cfi-test tree it matches under kocmp.py, and
wmt_chrdev_wifi from the same build still matches the shipped one.
abuild -d skips build-base as well as the dependencies, so a NO_DEPS
build of an aport that compiles (modemmanager-mtk-ccci) stopped at
meson's "Unknown compiler(s): cc".
abuild -d leaves libmm-glib uninstalled, so tracing the package's
shared-library dependencies failed with "libmm-glib.so.0: path not
found". modemmanager-mtk-ccci depends only on Alpine packages, so it
builds with them; the q25-* aports still build without depends, since
those are each other.
abuild's srcdir holds symlinks to the downloaded sources, and zstd
skips a symlinked input file ("is a symbolic link, ignoring"), which
-q hid: package() failed with no message. Reading the image on stdin
follows the link.
…ips it

build-device-marathon-apk.sh generated the profile only when
device-profile.conf stood alone on a line of the APKBUILD. The Q25's
source= starts with it, so nothing was generated and abuild checksum
failed. Match the file name as a word instead; the same device aports
still get a profile and enchilada, which ships none, still gets none.
The overlay shipped a whole wpa_supplicant.service because the frozen
image's wpa_supplicant had no unit. The pipeline image installs
wpa_supplicant-systemd, which owns that file, so apk refused the
overlay. Depend on wpa_supplicant-systemd and keep only the Q25
difference, the control socket and syslog flags, in a drop-in.
Pipeline patch 0054: build-image.py forwards MARATHON_DEV_SSH_KEY to
mkosi with --environment. PassEnvironment alone dropped it, so images
built since 0050 had no authorized key and sshd disabled; the first
Q25 pipeline root came out that way.
Android's netd in Deckard's container programs legacy iptables, and
on the Q25 none of those tables exist: ip6tables-restore fails with
"unable to initialize table 'filter'", system_server restarts every
two minutes and Android never finishes booting.

The netfilter set already builds every legacy table and the matches
the kernel config enables; ship ip_tables, ip6_tables, the filter, nat,
mangle and raw tables for both families, ipt_REJECT and ip6t_REJECT
with nf_reject_ipv4/6, and xt_mark, xt_conntrack, xt_comment,
xt_multiport, xt_tcpmss, xt_TCPMSS, xt_addrtype and xt_REDIRECT.

The kernel config leaves connmark, state, u32, owner, bpf, pkttype,
policy, length, limit, socket, NFLOG, nfnetlink_log, CT, IDLETIMER,
quota, hashlimit and iprange unset, so those cannot be built.

depmod -e against the kernel's System.map resolves every symbol, and
the vermagic matches the modules already shipped.
netd in the container cannot load modules itself, so the tables are
loaded at boot, as on the HackberryPi.
The slider's floor was bias level 4 of 20 (4.4 V). Level 1 (4.1 V),
stock's lowest, shows no ghosting on the panel and is visibly dimmer,
so the floor is now 0.05. The OCP2131 bias is the only brightness
control: nothing answers at 0x11, so there is no WLED driver.
The motor hangs off the MT6366 vibr LDO, as stock's regulator-vibrator
node says, but the board DT has no haptic node and the kernel leaves
regulator-haptic unbuilt. q25-haptic creates the regulator-haptic
device with a 2.8 V supply aliased to vibr by name; the mainline driver,
built unmodified, turns it into an FF_RUMBLE input device, which the
shell's haptics already drive. Loaded at boot from modules-load.d.

Verified on the phone: a 300 ms enable of vibr buzzes the motor, and
the module pair registers input regulator-haptic with FF_RUMBLE.
With the legacy tables loaded, netd's iptables-restore still fails on
connmark, CONNMARK and bpf, so its rule set never applies. The kernel
config leaves these and the rest of netd's matches and targets unset.

None of their sources needs kernel-side support that the config lacks,
so the net set builds them out of tree with their options forced to m:
xt_connmark, xt_bpf, xt_owner, xt_u32, xt_state, xt_pkttype, xt_limit,
xt_quota, xt_hashlimit, xt_iprange, xt_length, xt_IDLETIMER, xt_NFLOG
with nfnetlink_log, and xt_CT. nfnetlink_log reads the bridge port
through br_private.h, since the config has BRIDGE_NETFILTER.

xt_socket is left out: nf_socket_ipv4/6 call udp4_lib_lookup and
udp6_lib_lookup, which the kernel only builds with NF_SOCKET or
NF_TPROXY set.

The modules load through iptables' own autoload once ip_tables is in.
The vermagic matches the shipped modules, and depmod -e against the
kernel's System.map resolves every symbol.
netd routes its blackhole network through dummy0 and logs "cannot find
interface dummy0" without it. The kernel config has DUMMY=m but the
kernel APK does not ship it, so build it from drivers/net/dummy.c with
the net set.

It is not loaded at boot: netd only looks dummy0 up, and loading the
module creates dummy0 in the host's namespace, not the container's.
Creating it with "ip link add dummy0 type dummy" in the container's
namespace autoloads the module.
Android never ran on the Q25: polkit refused the phone user's start
of deckard-container, and without persist.deckard.multi_windows each
app ran inside one Android display that Marathon never mapped, so
launches timed out with no surface.

Ship the HackberryPi's polkit rule and a deckard.cfg seed: multi-window,
idle freeze off, density 240, and Marathon's app area as Android's
display, 720x633 px at scale 1.5, so 480x422. Verified on the phone:
Android boots in 20 s and microG opens as a Marathon window.
The host read the shell's ambience with a block_on on the GUI thread over
the process's shared session connection, and the read never returned
within its second: every app connected to a host with no ambience and
took it later as a change. Read it in follow_ambience once the change
stream is subscribed, so a change during the read is not lost.

Traced on the Q25: the host now holds the ambience 250 ms after start.
Every app faded in twice: once by the shell's reveal, then out and back
in about 400 ms later, its first frames in Keel's default colours. Keel
fetches the host's properties asynchronously; the ambience arrived after
the window mapped, counted as a change, and Keel ran its 400/600/400 ms
ambience fade.

Before mapping, fetch the ambience from the host over a short peer
connection and pass it to Keel's ShellClient while the window is hidden,
where Keel skips the fade. The later reply then matches and does nothing.
Verified on the Q25 by sampling the scanout during launches: one fade,
in the ambience's colours from the first frame.
The Quick Settings shade and the Hub peek showed a frozen, blurred copy of
the wallpaper whatever was behind them. They now frost the layers behind:
the open app's surface, or the wallpaper and the home screen.

AppBackdropBlur captures its layers at a quarter of its size, blurs them
there into a layer and scales that up filtered, so the blur runs on
180x180 on the Q25 instead of full size with mipmaps. A live capture
redraws only when a layer changes and only while visible. The lock
screen's frozen backdrop uses the same path. blurMax is now in the small
texture's pixels.

Checked on the Q25 over the calculator and over home.
Deckard 51e20b5: a fresh install has the images but no instance, and
nothing ran Init, so the first start failed ("deckard doesn't exist")
and Android never booted. Start now initializes from the installed
images when the instance is missing. Found on the Zinwa Q25.
Lint_TestReachability failed: nothing ran test_q25_root_diff.py or
test_q25_root_install.py. Register both; the diff test builds its ext4
fixture with mkfs.ext4, so CI installs e2fsprogs. Both pass in alpine:edge
as an ordinary user.
deckard-images 23.2.20261101: the 20261028 images with netd rebuilt from
Marathon-Deckard image/lineage-23.2 f8500aa, which logs and carries on when
XfrmController cannot open its netlink socket instead of exiting. Without
it Android never finished booting on the Zinwa Q25. The vendor image is
unchanged.

Tested on the Q25 with the released artifact: boot_completed in 20 s,
network inside Android, apps open as Marathon windows.
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