Skip to content

omamobile-shell 0.2.120: recent apps start a failed window capture again, and keep each window's last picture - #658

Open
den-kloot wants to merge 3 commits into
derekross:mainfrom
den-kloot:omamobile-shell-switcher-cached-previews
Open

den-kloot wants to merge 3 commits into
derekross:mainfrom
den-kloot:omamobile-shell-switcher-cached-previews

Conversation

@den-kloot

@den-kloot den-kloot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What and why

On the owner's Pixel 9a, recent apps sometimes showed a card with no picture. In their screen recording at 09:33 that morning it was the Settings card, while WhatsApp and Home Assistant had theirs. In that same second, the shell's journal has three lines of "Ending recording due to screencopy failure".

That message is the whole story. When Hyprland fails a toplevel capture, Quickshell ends the recording for good: the context's stopped makes ScreencopyView drop it, the view's own stopped signal is never emitted (0.3.2 declares it, nothing emits it), and nothing starts the capture again. So the card stays empty until recent apps are opened again. Otherwise a card's first frame always comes at once: Quickshell asks for it with ignore_damage, and Hyprland copies such a frame immediately. So an empty card means a failed capture.

It happens on demand like this: with the on-screen keyboard up, open recent apps. The keyboard closes, the window grows (579 to 821 points here), and Hyprland fails the capture that was sized for the old window. That's how the owner usually opens recent apps (swipe … recents (keyboard) in the journal). Captures also fail now and then without a keyboard, sometimes all three cards' at once.

What this changes:

  • A card without a frame starts its capture again. A card that has no frame a second after recent apps open, or that loses its frame, drops its capture and starts a new one, three times at most while recent apps are open. It watches hasContent, since the view never says stopped.
  • A still per card. Each card keeps its window's last frame, at the card's size (grabToImage, a few hundred kilobytes each), and shows it until its capture has a frame. The stills live on the switcher, not on the cards, so a card rebuilt after another window closed has its picture at once, and a window that closes takes its still with it (SwitcherModel.js: stillOf, withStill, prunedStills).

The first commit also copied only the middle card live; the second takes that back out (measured below). The first commit's restart waited for stopped, so it never ran on a phone; the third commit fixes that.

Device and build

Checks (no phone needed)

  • tools/pr-check upstream/main passes
  • tests/all passes. On the phone one check fails, "display-msm should log not stamping on empty tree", and it fails the same way on unchanged main (c3cfd86) there; everything else passes.
  • tests/packages: omamobile-shell's test/all, 317 ok
  • Each changed package has its version bumped, and the new version is in the commit subject
  • Commits have area prefixes and bodies that say why; any AI help is a Co-Authored-By: or Assisted-by: trailer, never a Signed-off-by:

New tests:

  • test/switcher-model.js: the stills.
  • test/switcher-touch:
    • a card shows the still from its window's earlier frame while its capture has none, then the live frame once there is one;
    • a closed window loses its still;
    • a capture with no frame starts again three times, and no fourth; one with a frame is left alone, and one that loses its frame starts again.
  • The ScreencopyView stub gains hasContent and a colour, and drops its frame when it loses its source, as Quickshell's does. A failed capture is one that never gets a frame; like Quickshell's, the stub has no stopped.

On the phone

  • I ran this on my own phone: this branch's omamobile.switcher as the local plugin copy, on omamobile-shell 0.2.119 (omamobile-shell 0.2.119: a swipe up closes the shade card and the Wi-Fi and Bluetooth panels #553), whose switcher is the same as main's. Touches came from a uinput touchscreen, and recent apps were closed through the plugin between openings.
  • Every run below swapped the plugin copy and restarted the shell (omarchy-restart-shell). Without the restart the shell's local-plugin reload keeps the component it compiled first, so my earlier A/B runs in this PR compared a switcher with itself; I've withdrawn them.
  • Results:
    • Keyboard up, then recent apps:

      • with the agent's foot window alone, this branch: the first frame, the failure, and a second later the window at its new size;
      • with three windows, three failures on both. Main's cards stayed empty, and this branch's had their pictures at the 3 s screenshot.
    • Three windows, recent apps opened from home, screenshot after 3 s, twice each: main's first run had three failures and an empty card; this branch's runs and main's second had none and showed the window.

    • Load, two foot terminals redrawing a clock ten times a second, recent apps open, 12 s of CPU time, three runs each:

      Switcher Shell Hyprland Failures
      main 21.8, 27.4, 28.4 % 35.3, 47.2, 47.7 % 0, 0, 0
      this branch 27.7, 26.8, 27.3 % 47.9, 46.2, 47.1 % 3, 0, 0
      only the middle card live 30.3–31.0 % 39.9–42.0 %

      So with working captures this branch costs what main costs, and it got its cards back in the run with three failures. In an earlier series every run started with three failures. There, main's cards stayed dead and it read 2.5–10.7 % shell and 0.2–18.8 % Hyprland, which is the price of an empty card, not a saving. Live on the middle card only takes about 7 points off Hyprland and adds about 4 to the shell, and the neighbours would lose their live picture, so every card stays live.

  • What I did not test:
    • why Hyprland fails a capture without a keyboard (its log is off on the phone);
    • landscape.
  • Nothing in this change writes outside slot B and userdata, weakens the storage guard, or reads registers from userspace (AGENTS.md §1)
tools/phone-smoke report

phone-smoke: unknown

Kernel 7.3.0-rc6-omamobile-push1-gcbb1a7eb0a78, tools at unknown, 2026-11-11T09:00Z.

Check Result Detail
boot (required) ok kernel 7.3.0-rc6-omamobile-push1-gcbb1a7eb0a78, slot b, up 9392s
storage guard (required) ok 62 partitions; only userdata and slot B's boot images writable
root (required) ok ext4 on userdata, 82G free
kernel health (required) ok no oops or BUG; 0 WARN, 0 error lines in dmesg (0 known, 0 new)
systemd units not ok failed: omamobile-bt-addr@hci0.service
USB gadget (required) ok bound to 11210000.usb
display ok card1-DSI-1 connected, 1080x2424
GPU ok render node on panthor
session (required) ok Hyprland and the Omarchy shell running
lock ok PAM file present, the shell can lock (passwordPam true, locked false)
touch ok Synaptics TouchComm Touchscreen
buttons ok gpio-keys
Wi-Fi ok wlp1s0 on brcmfmac, link down
Bluetooth ok hci0, powered false
battery ok 76% Discharging, 31 °C
sound cards ok googleteguaocsn
sensors ok Accelerometer AmbientLight Proximity
cameras skip no libcamera tools
thermal ok hottest zone 44 °C

Packages: hyprland 0.57.0-3.1, libcamera 0.7.2-4.22, mesa 1:26.2.4-1.4, omacam 0.3.3-1, omamessages 0.2.2-1, omamobile-apps 0.2.2-1, omamobile-audio 0.5.18-1, omamobile-device-tegu 0.2.14-1, omamobile-firstboot 0.1.3-1, omamobile-gnss 0.2.9-1, omamobile-keyring 20261026-1, omamobile-mirror 0.1.1-1, omamobile-mmd 0.1.4-1, omamobile-modem 0.1.20-1, omamobile-modem-sit 0.3.34-1, omamobile-omarchy 0.1.41-1, omamobile-owl-widgets 0.1.2-1, omamobile-sensors 0.2.1-1, omamobile-settings 0.1.1-1, omamobile-shell 0.2.119-1, omamobile-voice 0.1.0-1, omaphone 0.2.3-1, omarchy 4.0.6-1, omatel-agent 0.2.4-2, quickshell 0.3.2.r7.g2b4501a-1

🤖 Generated with Claude Code

den-kloot and others added 2 commits November 11, 2026 09:55
… and copy live only the middle card

A recent-apps card copied its window live through Hyprland's toplevel
capture, and showed nothing until the window gave a frame. Every app sits
on a screen of its own, and Hyprland hands a frame over only when the
window draws: a still page (the Settings app's About list) gave none, so
its card stayed empty until something on the page changed. A capture
Hyprland fails ends for good ("Ending recording due to screencopy
failure", eight times in one morning on the 9a), and the card stays empty
until recent apps are opened again. And every card was rebuilt, empty,
when another window closed.

- Each card keeps its window's last frame as a still, at the card's size
  (grabToImage, so a few hundred kilobytes each), and shows it until its
  capture has a frame. The stills live on the switcher, not on the cards,
  so a rebuilt card has its picture at once; a window that closes takes
  its still with it (SwitcherModel.js: stillOf, withStill, prunedStills).
- Only the card in the middle copies live; the others copy one frame
  each. Before, every open window was captured live at the screen's size
  while recent apps were open.
- A capture that stops is started again a second later, three times at
  most while recent apps are open.

Tests: test/switcher-model.js (the stills), and in test/switcher-touch the
middle card live and its neighbours not, the still shown while the capture
has no frame and gone with its window, and the retries. The ScreencopyView
stub gains hasContent, a stopped signal and a colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first commit copied only the middle card live and its neighbours
one frame each, to save work. Measured on the 9a with five windows
(recent apps open, swiped through every card, 8 s of CPU time and the
dma-buf total from debugfs): the shell's CPU was 31.8 % before and 32.9 %
after, and the dma-bufs 239 MB before and 235 MB after. A neighbour that
copies one frame still holds its buffer, and an idle window is copied
only when it draws, so nothing measurable was saved, and the neighbours
lost their live picture. Back to every card live.

What stays: a stopped capture starts again (Quickshell ends a recording
for good when Hyprland fails one, "Ending recording due to screencopy
failure", three times in the second of the owner's empty card), and each
card keeps its window's last frame as a still for when its capture has
none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@den-kloot den-kloot changed the title omamobile-shell 0.2.120: recent apps keep each window's last picture, and copy live only the middle card omamobile-shell 0.2.120: recent apps start a failed window capture again, and keep each window's last picture Nov 11, 2026
@den-kloot

Copy link
Copy Markdown
Contributor Author

One more measurement on keeping only the middle card live, this time with windows that keep drawing. The setup:

  • 5 windows, two of them foot terminals redrawing a clock ten times a second, so they are the two cards a centred card shows beside it;
  • the screen kept on, recent apps open for 12 s per round, 4 rounds per variant.
shell CPU (median) Hyprland CPU (median) dma-buf total
every card live (this branch's head) 27.5 % 47.0 % 290 MB
only the middle card live (the first commit) 27.9 % 48.2 % 290-306 MB

Every card live costs no more than the middle card alone, even with neighbours that redraw, so the head keeps every card live. An earlier round I discarded had the screen off: with the panel dark, Hyprland renders nothing, and no capture runs at all.

…ard has no frame

The restart in the first commit waited for ScreencopyView's stopped
signal, but Quickshell 0.3.2 declares that signal and never emits it:
when Hyprland fails a capture, the view only drops its context. So on a
phone the restart never ran; test/switcher-touch passed only because its
stub emitted the signal.

Now a card that has no frame a second after recent apps open, or that
loses its frame, drops its capture and starts a new one, three times at
most while recent apps are open. The stub no longer has the signal, and
the test leaves the capture without a frame, as Hyprland's failure does.

Reproduced on the 9a: with the on-screen keyboard up, opening recent apps
closes the keyboard, the window grows from 579 to 821 points, and Hyprland
fails the capture that was sized for the old window. Before: the card
stayed empty. After: the first frame, the failure, and a second later the
window at its new size.

The measurement in the previous commit is void: the shell's local-plugin
reload keeps the component it compiled first, so both runs ran the same
switcher. Measured again with a shell restart before each run (two foot
terminals redrawing ten times a second, recent apps open, 12 s, three
rounds): every card live, shell 26.6-27.3 % and Hyprland 46.4-49.2 %;
only the middle card live, shell 30.3-31.0 % and Hyprland 39.9-42.0 %.
Hyprland saves about 7 points, the shell spends about 4 more, and the
neighbours would lose their live picture, so every card stays live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@den-kloot

Copy link
Copy Markdown
Contributor Author

A correction, and a fix in db2d0d1.

My earlier measurements here were void. I swapped the plugin copy and relied on the shell's local-plugin reload, but that reload keeps the component it compiled first, so both sides of each A/B ran the same switcher. That includes my last comment ("only the middle card live saves nothing") and the main-against-branch load in the first version of the description. I've redone everything with omarchy-restart-shell before each run and updated the description.

The restart never ran on a phone. Quickshell 0.3.2's ScreencopyView declares stopped but never emits it. When Hyprland fails a capture, the view just drops its context. The test stub emitted it, so the tests passed. db2d0d1 watches hasContent instead: a card with no frame a second after recent apps open, or that loses its frame, starts its capture again, three times at most.

It reproduces now: with the on-screen keyboard up, open recent apps. The window grows as the keyboard closes, and Hyprland fails the capture. With three windows that was three failures on both main and this branch. Main's cards stayed empty, and this branch's had their pictures within 3 s.

Load with valid runs (two terminals redrawing ten times a second, 12 s, three runs each):

Switcher Shell Hyprland
main, no failures 21.8–28.4 % 35.3–47.7 %
this branch 26.8–27.7 % 46.2–47.9 %
only the middle card live 30.3–31.0 % 39.9–42.0 %

With working captures, this branch costs what main costs. When main looks cheaper, that's because its failed captures stay dead. Middle-card-only takes about 7 points off Hyprland and adds about 4 to the shell, so I kept every card live.

@derekross

Copy link
Copy Markdown
Owner

Thank you for this, and especially for the two corrections you made on your own: finding that the local-plugin reload kept the first compiled component, so your A/B runs compared a switcher with itself, and finding that the restart never ran on a phone because the test stub emitted a signal the real view doesn't. I read Quickshell at the commit the phones run (2b4501a), and you're right on both counts: view.hpp declares stopped and nothing in view.cpp emits it, and when the context stops, the view deletes it and clears hasContent while captureSource stays set, so nothing ever starts a capture again. Setting the source to null and back is what createContext() needs, so your restart does the right thing.

I ran test/switcher-touch on your head and broke five of the new lines one at a time (the retry limit, the resting flip, the still's visibility, the prune when a window closes, the reset when recent apps open). Each failed a check with a clear name. Good tests.

One change before I merge:

The stills' size. keepStill() grabs at the card's size times Screen.devicePixelRatio. A grab result is an uncompressed image at 4 bytes a pixel, and the card is 72 % of the screen's width and about two thirds of its height, so by my arithmetic one still is about 4.9 MB on the 9a (778x1568) and about 8.5 MB on the 7 Pro (1037x2040), not a few hundred kilobytes. The switcher keeps one per open window until the window closes, so eight apps would pin 40 to 70 MB in the shell for a picture that shows for a second at most. Could you grab at the card's logical size (without the pixel-ratio factor), or smaller, and note in the comment what one still costs? If you can measure the shell's memory with several windows before and after, even better; my numbers are computed, not measured.

One question:

Did you see the still itself on the phone? Your results describe the restart, and they're convincing. But I couldn't find what the card showed in the second between the failure and the new frame, which is what the stills are for. The offscreen test grabs the stub's rectangle, so it can't tell us whether grabToImage gets a picture out of the real capture view. A screenshot about half a second after opening recent apps with the keyboard up would answer it.

Small things, as you see fit: the still is taken at a capture's first frame, so "last picture" really means "from the last time recent apps were open", and the README line could say that. And the "screencopy failure" with no keyboard involved deserves its own issue with the journal line, so we can chase the cause separately.

After that I'll try it on my 7 Pro (recent apps with the keyboard up, and with several apps from the home screen) and merge. It goes in after #553, and I'll set the version number at merge.

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.

2 participants