Skip to content
Porthole
Search Porthole
Ctrl
K
Close
GitHub
Select color theme
Dark
Light
System
Open navigation menu
Home
Downloads
Device images
Browser installer
Signed APK packages
Release evidence
Release pipelines
Devices
Device directory
Google Pixel 7 Pro
Google Pixel 2 XL
About Porthole
Project status
Get started
Working guide
Setting up a new host
Running pmbootstrap safely
Configuration
Performance
Architecture
Contributing
Building and reviewing releases
Nura naming and upstream compatibility
Developer reference
Commands
Tools
Profile keys
For agents
AGENTS.md
The rules, and what they cost
Knowledge base
Browse by topic
How to read this
Laws
A check that fires on a healthy tree gets muted
A null from a path that never executed is not a refutation
An empty reading means unknown, never changed
Decide which of your numbers is the control before you run the arm
Exit codes are an API — distinguish retryable from not
The instrument is guilty until proven innocent
Never judge a boot by the screen
Poll, never sleep
Read the vendor implementation before inventing a mechanism
The shipped configuration is not the running configuration — read the value back
The lock says who has the device, never what the device is doing
Traps
A 4:4:4 test clip makes working hardware decode look broken
A vendor's reference board is not the SoC, and tools will accept it silently
Every browser arm runs at 75-78 C with the big cores capped to 1.0-1.5 GHz, and a phone on a PC's USB port discharges under that load until it browns out
A build survives the porthole run that started it, and takes the buildroot lock to the grave
A count-based poll loop silently caps how long an experiment may take
A freshly built kernel cannot RAM-boot against the modules already on the device
A freshness indicator that measures a different object than the one you compared turns "nobody looked" into confidence
pmbootstrap zap inside the workspace tears down porthole's own bind mounts, and the build then refuses about a version
A hard hang writes nothing to disk — capture on the host, before you trigger
A journal grep counts the grep that is asking
A browser launched from ssh, systemd-run --scope, or set-environment does not get environment.d -- and on taimen that means Skia-GPU
A --lax workspace never installs sccache, so a QEMU-only Rust build dies in prepare
A long sudo credential cache is unlimited root for every process you run
A module parameter that does not exist is silently ignored
A module reload re-registers the card and leaves capture broken — audio needs a reboot
With the phosh app grid open, a launched app goes straight to recents -- and every measurement of it is void
A noarch or all dependency makes pmbootstrap ask for gcc-<native>, and every fresh workspace dies on its first packaging rung
A package mirror needs a host-arch index, even for a phone port
A partial chroot umount in the workspace leaves /dev/urandom, /dev/zero and /dev/tty as empty root-only files -- and a 5.5 h build dies on the last step
A RAM-booted kernel cannot survive a long sleep — suspend work needs a flashed slot
A staged package name and a touched log invent a build that is not running
A shipped default is indistinguishable from a real answer, and safety checks complete themselves on it
A sideloaded device apk can eat the radio stack
A stale inherited env outbuilds the profile
A stall catcher on the browser must pick the WebKitWebProcess with the most threads, and thread names are truncated from the front
A systemd drop-in cannot remove an ordering dependency
A green suite hides tests that pass whether or not the defect is present
A tree-built module carries BTF the running kernel rejects, and modprobe blames a symlink loop
A UCM device switch cycles the whole verb, so a DisableSequence runs mid-use
Every Nth boot lands in the bootloader" is a retry countdown, not a glitch
Correct downloaded bytes fail when checksums are out of source order
An arm behind the phosh lockscreen measures a still page, and it looks exactly like the change under test breaking WebKit
An injected touch does not wake a blanked screen, and every gesture arm then measures a still image
An instrument must report 'I could not run' differently from 'I ran and saw nothing'
On, unlocked and showing your app are three different things -- an injected gesture drives whatever is actually on top, and every frame statistic then describes that
androidboot.bootreason says watchdog on every boot of taimen, including clean ones -- it is not a reset-reason oracle
The pmOS wiki is behind Anubis, but its MediaWiki API is not -- fetch wikitext, not HTML
Package build succeeds but the tool reports its APK missing
apk info -W does not resolve /lib -> /usr/lib, and the right path differs for modules and firmware
apk-tools 3 trusts only /etc/apk/keys, and the keys packages install to /usr/share/apk/keys -- so a fresh rootfs can install nothing at all
A new APR service inherits a protection domain the board deletes
Base a kernel aport on pristine kernel.org or a pinned tag — never an untagged vendor fork
build mod from the tree against a kernel that ships from the aport is refused by MODVERSIONS -- and on a no-reload module the refusal lands at the next boot, with the shipped module already gone
busybox `reboot bootloader` silently discards the word "bootloader
Python floor test fills disk after an image build
systemd-analyze critical-chain shows the longest path, not the floor
No CCACHE_ export reaches a cross compile, because crossdirect execs with a literal environment
dmesg can be empty about boot while the journal still has everything
The dtbo must match the kernel, and the bootloader reads it from the active slot
Packaged DTBO has a correct payload but a different digest
Every manifest commit predating pkg fork says unknown
On Pixel 7 and later, `fastboot boot` ignores the ramdisk — there is no RAM-boot safety net
Fixing one stale read leaves the other reads in the same function stale
FROZEN (kernel alive, userspace gone) is a distinct state and the watchdog will not save you
Fork audit selects hundreds of unrelated packages
git apply silently skips 'diff --git' patches and exits 0, so a source tree ends up half-patched
Image build finishes export then cannot find verifier
A device stopped in the initramfs looks exactly like a frozen one, and is nothing like it
apk add <firmware-pkg> (and apk fix) can FLASH the boot partition
MemoryHigh= on an app scope arms systemd-oomd against that app
porthole build mod pushes an unstripped tree module into a stripped packaged set -- on venus-core that bootloops taimen
A monitor vif on ath10k can be added alongside managed but receives nothing
Never flash a kernel built from the source tree when the device ships from an aport series
cgroup I/O control is inert unless the kernel config enables it
No log channel survives s2idle here: netconsole is gagged and its netdev is down
A fresh install has no passwordless sudo, and that silently disables every tool
olddefconfig silently drops symbols whose dependencies are unmet
One browser arm cannot resolve anything under ~10% here: the same build gave 29% and 73% frames-over-budget
Parallel artifact extraction corrupts APK payloads
pmbootstrap parses an APKBUILD line by line and never runs the shell
A privileged container cannot see new loop partition nodes
After any boot test, prove which kernel answered
pstore has never produced a record on 7.2.2, even though ramoops registers and enables a console
Pushing one module while its sibling stays old is worse than pushing neither
Reading the qfprom corrected region through nvmem hard resets the phone
Retrying an upload must rebuild stale indexes
A device-side script run on the host produces plausible, entirely wrong output
Rust wrapper missing with prebuilt dependencies
Settings subpage is compiled but not navigable
Host keys change on essentially every boot, which constrains both correctness and speed
Every `source envkernel.sh` stacks another /mnt/linux bind mount
A _p<timestamp> dev snapshot outranks a release, so apk installs a kernel from days ago
A drop-in that adds a StateDirectory= reorders $STATE_DIRECTORY, and the daemon writes to whichever sorts first
A second environment.d file setting the same variable deletes the first one's value
The boot header hash does not cover the cmdline — which makes it the cheapest liveness test
The bootloader reboot can drop the phone off USB entirely
A battery that will not charge is usually the debug cable, not the driver
The default kernel tree can be parked on an old branch, and then every tool reports the wrong kernel version perfectly truthfully
The device keeps its USB default route after the host drops the link, and then has no internet at all
The DPU vsync counter is phoc's output rate, not the app's -- a browser scrolling at 30 fps and presenting video at 15 fps both read "60 fps, 0 jank
Writing msm's reset debugfs faults the GPU but no client loses its context, so it cannot test GPU-reset recovery
drm_panel_add_follower() succeeding does not mean the callbacks will ever run
The SLPI wake-gesture subscription is spent on arrival: subscribe AFTER the handover, never at boot
The taimen-v7.2 tree was ten venus patches behind its own aport series, and a venus_core built from it wedges the SoC
The USB gadget is a DHCP server, and NetworkManager lets it take the host's default route and DNS
A fresh boot.img mtime says nothing about which kernel is inside it
The tree defconfig and the aport config can differ at the same commit, and modprobe pays for it
Two concurrent pmbootstrap builds share one buildroot and silently destroy each other
Unmasking suspend before an automated wait can loop a device out of reach
A uprobe set after the process started never fires
The USB gadget hands the host a new MAC on every boot
lsusb can label a running pmOS USB gadget as "fastboot
Wait long enough before calling a boot failed
An out-of-range watchdog timeout turns the watchdog OFF, it does not clamp
WAYLAND_DEBUG=1 grows a multi-hundred-MB tmpfs log inside the process you are measuring
Findings
A board file that declines to mention a DT property does not unset it, and a comment is not a revert
A (created) MVM carries call audio: what matters is whether the modem had a call, not the joined flag
An envkernel _p snapshot blocks every install, and nothing reports it until a build refuses twelve minutes in
A killed build is resurrected by the shared log it died writing
The NFC interrupt storm was a devicetree pull-up, not a driver bug
Wake-on-WLAN works -- a magic packet wakes taimen from s2idle, and the association survives suspend
A phone can reach the bootloader and never enumerate, and porthole read that as never reaching it
Parsing a shebang catches less than running the tool, and pmbootstrap --version does not need a config
The display-wake reset: a runtime power collapse too short to discharge GX leaves the a5xx CP alive, and hw_init reprograms CP_RB_BASE underneath it
The overnight resume hang is a touch on a wake-gesture-armed suspended screen, not a suspend regression
A TrustZone command can return status 0 and do nothing -- the tell is how long it took
A venus firmware assert wedges the video GDSC, and the driver's recovery then retries every 10 ms forever
Three of the four a540 GPMU limiter "gaps" are vendor parity; only the throttle bit and the stale power level are real
GPU rasterisation is visibly wrong on a540 because the a5xx GMEM store never resolves multisample buffers
ACP gives a UCM verb with two outputs on one PCM two profiles, not two ports -- so callaudiod's speaker button cannot work
Android's INTERACTION boost is the remaining perf delta -- the scaling infrastructure already has vendor parity
TEST-SSID does accept this client -- intermittently, with the stock frame
TEST-SSID refuses this client, and our association request is not the reason
With the display/decode stack clean, the remaining jank is app-shaped -- GJS GC in Maps, main-thread layout in WebKit 2.48, init CPU in browser launches
Call audio is silent because mainline has no voice service — not UCM, not the sound server, not the modem
Chromium segfaults on every launch under phoc because a phone with no physical keyboard is sent no wl_keyboard.keymap
Clipping WebKit's compositing to the damaged rectangles is worth ~8% and 3 C on taimen, not a step change
Crossdirect hands every link step to qemu on purpose, not just compile
Deep sleep is blocked by a 5.3k/s tick inside the frozen window, not by a missing RPM voter
Disabling the CPU retention idle state, as the vendor does, is worth about 0.02 mW and does not deepen idle
EAS never starts on msm8998: the CPU OPP tables carry CPR corners, not power
envkernel activation costs 0.8 s, not 14 s -- the 14 s is a one-off apk add
Every envkernel kernel build compiles from scratch, because envkernel disables ccache on purpose
Epiphany on YouTube is a memory ceiling, and the GPU buffers are charged to its cgroup
Epiphany's frame on m.youtube.com is ~20 ms of TextureMapper CPU walking 315 layers plus a ~10 ms GPU/FrameDone tail, serialized -- the kernel sees two submits per frame, so a5xx batch overhead is not the limit
Epiphany's window updates at 12-30 fps on YouTube because WebKit's compositor thread spends 35-50 ms per frame on a 237-layer page -- not the video, not GTK, not the GPU clock, not damage tracking
Every command exiting 1 with empty output is a full disk, not a broken harness
The Type-C Rp advertisement is ignored, so a 3 A source charges at 500 mA -- and AICL, not the driver, is what bounds the request
The FPC trustlet commits an enrolment only with a Gatekeeper/keymaster-signed auth token, which postmarketOS cannot produce
A from-scratch taimen install brings venus, GStreamer and the radios up with no hand-edits
fuse2fs cannot stand in for the loop device, because the loop device is exposing a partition table
The gold OSM -110 is the ACD auto-transfer poll, and four tempting causes are dead
GStreamer's V4L2 pool copies every venus frame when the sink offers no video meta: 12 fps at 4K, 82 fps otherwise
Hardware decode does work in Epiphany -- the residual ceiling is WebKit's ~7 processes per tab, not the decoder and not GEM runaway
Holding VDD_MX does not stop the display-wake crash -- neither enabled nor at TURBO
The 188-patch series rebases onto v7.2 with 16 small conflicts
PORTHOLE_LAX_BUILD=1 saves no measurable time, and the zap it skips is not the wall clock
Mainline's a540_gpmu.fw2 accepts five voltage-table levels; a sixth stops the GPMU booting at all
Mainline a5xx_hw_init programs the A540 at parity with kgsl's a5xx_start; the one extra mainline write is VPC ALLFLATOPTDIS
The SoC can never reach VDD-min because mainline msm8998 has no RPM sleep-set handshake, not because something is voting against it
NetworkManager tears down wlan0 on PrepareForSleep, so wake-on-WLAN only works on suspend paths logind never hears about
There is no hardware video decode on taimen -- venus is described in DT but not built
ath10k turns off mac80211's idle connection polling, then disables the firmware keepalive too
Opening a PCM is not starting it: the codec only moves data at TRIGGER_START
The panel corruption and degraded phosh were one GPU reset's wreckage -- not display tearing, and not the rd_ptr patch
A GPU reset aborts phosh, not phoc -- the session restarts and looks like a reboot
pm_test rejects processors and core when the sleep state is s2idle
pmbootstrap shutdown unmounts porthole's own container binds, not just pmbootstrap's chroot mounts
QMI_DATA_LEN fields must be u32 on the host since 7.2, or every request is -EINVAL
No RAM survives a reset on taimen, so pstore/ramoops and ram_console are all dead ends
request_firmware() from a file's ->open() shares that open()'s symlink budget, and a split firmware runs out
The slow wake is two drivers, not the PM core, and one of them is blocked by a 60-byte firmware file that declares no features
A bindgen Rust aport builds in minutes with cross-native2, once pmbootstrap sets cargo up for it
Rewriting porthole, pmbootstrap or the msm8998 drivers in Rust does not address the class of error this port actually hits
taimen invents a new WLAN MAC every boot -- and it is not rmtfs, not caldata, and not a rate mismatch
taimen's rear camera module is three I2C parts -- IMX362 (0x1a), LC898214XD focus (0x72) and an LC898123F40 OIS controller (0x3e) that also holds the module calibration
taimen's skin thermistor needs no driver: bd_therm2 is VADC 0x51 and mainline already scales it
There may be no 30 fps lock at all -- the panel runs at 60 and ph-framprobe reports ~31 whether the screen is on or OFF
The a540 GPU faults under Skia-GPU are Skia blur/downsample passes stalling SP/TPL1 -- not binning, not fp16, and a different class from the compositor's one VSC fault
The phosh top-right strip is the FIRST GMEM tile, restored with the previous process's MSAA registers -- fd5 tile init never programs them
The aport series ships; linux/ is a topic-branch workshop, and diffing it against a checked-out branch means nothing
porthole build auto spends 14.7 s making a _p apk its router never opens, and leaves it behind
The browser stutter is a blocked WebKit main thread, not the display stack
The unpainted band during a fast fling is a 200 ms record() on the WebKit main thread, and no prepaint knob bridges it
The 244 mW of always-on idle was the camera sensors alone; venus and Easel are inside the noise
Epiphany's frame period is CPU paint PLUS the GPU tail, serialized -- frameDone to next frame start is 0.1 ms
Epiphany's frame period is pinned at 2 refreshes because the CPU paint alone exceeds one -- every sub-quantum optimisation measures as neutral
The Epiphany memory bound is not too tight -- the phone is genuinely full, and the swap is zram
The missing 4-way handshake is ath10k's rx_confused latch: one split A-MSDU disables data RX for the life of the firmware
The monitor vif was never deaf -- the radiotap parser was, and the phone is LOUD
The mainline msm8998 thermal zone is a cliff -- one passive trip, no limit, 2.36 GHz to 500 MHz in about 7 s
The reserved VHT channel-width pair was the refusal -- clamping it associates 8/8
The two unattributed halves of the s2idle cycle are one PM notifier each -- Easel's PCIe revival and the firmware cache
The browser scroll stall is the page's own JavaScript -- not the engine, not the tile-record path, and not page settling
The whole session is back to 30 fps on 7.2 -- the commit pipelining IS present, and the missing CTL_START patch is NOT why (msm8998 has no such interrupt)
The "SIGKILL wedges venus until reboot" was VP9 bandwidth starvation misread -- venus survives SIGKILL on both codecs
The vendor's skin thermal ladder drops the die 22 C under sustained load, and its own first two rungs stop it reaching the rest
The skin thermal zone storms the PMIC ADC at ~1600 interrupts a second, awake and idle, in the shipped config
The SLPI refuses to arm a wake gesture unless tlmm 75 is already high, and it gives up after 500 ms
The SLPI needs to OWN the touch interrupt, not just the i2c mux, before it will report a wake gesture
The top-right corruption is freedreno's GMEM tile path, not a GPU fault -- the boundary is the a5xx bin column at x=1024
The vendor runs four thermal layers on msm8998; mainline runs one, and two of the other three are hardware we switch off
The msm8998 venus wedge was wrapper clock auto-gating, and one write closes it
In-call volume: the vocproc refuses VSS_IVOLUME_CMD_SET_STEP without a registered volume calibration table
The display-wake crash dies inside a5xx_hw_init() -- it IS a GPU register access, and the instrument that said otherwise could not see this window
The display-wake crash needs GPU runtime suspend AND devfreq polling -- and it is not a GPU register access
The display-wake crash is not in any of msm's devfreq callbacks -- but it is specific to the GPU's devfreq
The browser's whole WebKit environment lived in ~/.config and a rootfs reflash deleted it, which reads as four separate new bugs
The WebKitWebProcess SIGSEGV is Epiphany asking for a FULL_DOCUMENT snapshot of a 237522 px page: Skia refuses a raster surface over 2 GB and WebKit dereferences the null
The workspace caches kernel compiles now: 18% dearer the first time, 2.5x faster every repeat
The workspace edit-build loop is 6-8 s, and ccache is still hit zero times
244 mW of taimen's screen-off idle is three blocks nothing is using
Finger drags on GTK4 WebKit ride the scrolling thread, and r61's two patches cut the Wikipedia drag from 289 layouts per 10 s to 10
Two thirds of successful associations never complete the 4-way handshake, and ath10k says nothing
The blank/unblank corruption is BELOW the compositor -- grim captures a clean frame while the glass shows garbage
VA-API cannot be put on top of venus: it is a STATEFUL V4L2 decoder and VA-API's model is stateless, which is why only GStreamer-based browsers get hardware decode here
Hardware video decode works on taimen -- three more root causes, and no power collapse
Enabling venus on msm8998 kills the SoC instantly, and the missing bus vote is not why
msm8998 TZ refuses venus resume with -EINVAL, mainline swallows it, and the whole block stays dark
wpa_supplicant's vht_capa cannot change Supported Channel Width Set or Extended NSS BW -- mac80211 drops it
The waylandsink 5 fps cap was two upstream policies colliding -- wlroots hides LINEAR from v3 clients, GStreamer refuses INVALID
waylandsink delivers 5 fps where glimagesink and gtk4paintablesink do 60 -- and WebKit's compositor is the browser's ceiling, not venus
Epiphany recomposites the whole viewport every frame: WebKit collects damage but UseDamagingInformationForCompositing defaults off, and UnifyDamagedRegions makes a playing video's damage the whole viewport
WebKitGTK's frame loop allows one frame in flight, released from GTK's snapshot(); on a5xx every render pass is a separate kernel submit -- so the compositor's cost is passes, not pixels
Five things a rootless container cannot do that pmbootstrap assumes, and what each one costs
Every rung pays ~14s to activate envkernel, and that dwarfs the compile
WiFi dies while every layer still reports connected -- and it is not the CPU, the GPU, or board-2.bin
wlroots does DRM output layers only through libliftoff
wlroots drops every client texture on a renderer swap and never re-imports one, so static layer-surfaces stay blank after a GPU reset
YouTube's judder on taimen is 2160p60 being served, plus venus clocked for 30 fps because vdec had no VIDIOC_G_PARM -- not the compositor, not buffer counts
The multi-second YouTube freezes are a CSS blur() painted in software on the web process main thread, because the GTK port never composites a layer for its filter
Playbooks
Moving a device between states without losing an hour
Playbook: first boot
Playbook: getting at vendor firmware
Playbook: storage, USB and ssh
Playbook: writing a device tree
Playbook: display
Playbook: suspend and deep sleep
Playbook: wifi, bluetooth and modem
Playbook: NFC
Playbook: the daily-driver push
Playbook: getting the work upstream
Workflow
How an agent should work on a bring-up
Commit conventions for a public port
Sharing one physical device between several workers
Evidence discipline
The handoff document
Devices
Google Pixel 2 XL (taimen, MSM8998) — the reference port
Memory
What is worth remembering across sessions on a bring-up
GitHub
Select color theme
Dark
Light
System
Netconsole
All topics
Note
Type
Scope
Confidence
The USB gadget hands the host a new MAC on every boot
trap
generic
proven