diff --git a/web/README.md b/web/README.md index 68da901..1af825b 100644 --- a/web/README.md +++ b/web/README.md @@ -95,27 +95,34 @@ seeking is the one thing a real player can do that the mockup could not. ## The Raspberry Pi profile -`?pi=1` loads the same app with the two per-frame costs cut down. It exists for -musicdolphin - a Pi 4 driving a 1920x1080 kiosk screen, where the app was spending -about 70% of a core with nothing happening. +`?pi=1` loads the same app with the ambient canvas painting a quarter of the pixels. It +exists for musicdolphin - a Pi 4 driving a 1920x1080 kiosk screen - where Firefox +rasterizes 2D canvas in the content process, and the canvas repaints the whole screen +every frame whether or not anything is playing. -What it changes, all of it in `src/lib/lowPower.ts`: +Everything in `src/lib/lowPower.ts` was measured on the device. Sum of the Firefox +process tree, idle on the browse screen, 15 s average: -| | Full | `?pi=1` | -|---|---|---| -| Ambient canvas backing store | 1:1 with the screen | half scale, capped at 1280 px | -| Ambient canvas frame rate | display refresh | 30 fps | -| Bubbles alive at once | unlimited | 60 | -| Progress-bar re-renders | every animation frame | 10 per second | -| `backdrop-filter` glass blur | on | off - plain translucent instead | -| Decorative CSS animation loops | on | off | +| | idle CPU | +|---|---| +| full app | 53.5% | +| + ambient canvas at half resolution | **25.0%** | +| + 30 fps cap on top of that | 25.0% | +| + decorative CSS animation loops off | 24.6% | +| + `backdrop-filter` glass blur off | **118.1%** | -The principle is *make the expensive thing cheaper before switching it off*. The canvas -is the app's whole look, so it stays and paints a quarter of the pixels half as often - -on a soft gradient behind content that is close to invisible. Only the effects with no -cheap version go: a real backdrop blur, and the decorative loops that carry no -information. The aquarium pets stay on even here; they are the reward the typing game -pays out, and a handful of `transform` writes is not what is slow. +Two things to take from that table. The canvas resolution is the whole win, and +**turning off the backdrop blur makes it five times worse** - the blur is what promotes +each glass panel to its own compositing layer, so a canvas frame underneath repaints +only the canvas. Without it the canvas and the album grid above it share one layer and +every frame repaints all of it. The most expensive-looking CSS in the app is what keeps +the rest of it cheap; `SHOW_GLASS_BLUR` stays on everywhere, and so do the decorative +loops, which measured as noise. + +So the profile is one real change plus three caps that only bite while something is +playing, and are therefore not in the table: 30 fps on the canvas, 60 bubbles alive at +once, and ten progress-bar re-renders a second instead of sixty (`usePlaybackClock` +pushes a React `setState` per animation frame, into both `PlayView` and `PlayerBar`). The flag is read once at module load from the URL and nowhere else - no persistence, no auto-detection. That is what makes "is this the profile or the hardware?" answerable by diff --git a/web/src/lib/lowPower.ts b/web/src/lib/lowPower.ts index 506fa6d..207035a 100644 --- a/web/src/lib/lowPower.ts +++ b/web/src/lib/lowPower.ts @@ -2,21 +2,36 @@ * * The device this exists for is musicdolphin: a Pi 4 driving a 1920x1080 monitor * through a Firefox kiosk window. The GPU is fine there (`V3D 4.2`, accelerated), but - * Firefox on Linux composites in the parent process, and the two things this app does - * every frame - repaint a full-screen canvas, and blur a live backdrop copy behind - * every card - were between them eating about 70% of a core with nothing happening on - * screen. Everything below is aimed at those two costs and nothing else. + * Firefox on Linux rasterizes 2D canvas in the content process, and the ambient canvas + * repaints the full screen every frame. Idle - nothing playing, nothing moving but the + * gradient - that was half a core in the content process alone. * - * The rule the profile follows: *make the expensive things cheaper before switching - * any of them off*. The ambient canvas stays - it is the app's whole look - it just - * paints a quarter of the pixels half as often, which on a soft gradient behind - * content is close to invisible. Only the effects with no cheap version (a real - * backdrop blur, the decorative animation loops) actually go. + * Everything here was measured on the device rather than reasoned about, and the + * reasoning lost. Sum of the Firefox process tree, idle on the browse screen, 15s + * average: + * + * full app 53.5% + * + ambient canvas at half resolution 25.0% <- the whole win + * + 30fps cap on top of that 25.0% (no idle change) + * + decorative CSS animation loops switched off 24.6% (no change; left on) + * + backdrop-filter glass blur switched off 118.1% <- 5x WORSE + * + * That last row is why this module is so much smaller than it started out. On this + * device `backdrop-filter` is load-bearing *for performance*: it promotes each glass + * panel to its own compositing layer, so a canvas frame underneath repaints only the + * canvas. Take the blur away and the canvas and everything stacked above it collapse + * into one layer, and every single frame repaints the whole page - all of the album + * grid included. The most expensive-looking CSS in the app is the thing keeping the + * rest of it cheap. Do not "optimize" it away without re-running the numbers above. + * + * So the profile is one real change - paint a quarter of the pixels - plus three caps + * that only bite while something is playing (bubbles spawn, curves are sampled per + * frame, the progress bar re-renders) and that are therefore not in the table above. * * Read once at module load, from the URL and nowhere else: no persistence, no * auto-detection. The kiosk points Firefox at `http://localhost:8080/?pi=1` (see the * `pi_kiosk_url` host var in the ansible repo) and a laptop opening the same page gets - * the full-fat version, which is what makes "is this the Pi profile or the hardware?" + * the full-fat version, which is what makes "is this the profile or the hardware?" * answerable by editing the address bar. */ @@ -33,17 +48,18 @@ function readLowPowerFlag(): boolean { export interface AmbienceQuality { /** Multiplies the canvas backing store relative to its on-screen size, before * `devicePixelRatio`. At 0.5 the loop fills a quarter of the pixels and the browser - * scales the result up; on a blurred-by-nature gradient with soft translucent - * bubbles over it, that upscale is hard to see and the fill cost is the whole - * point - `fillRect` over the gradient is proportional to pixels and happens every - * single frame. */ + * scales the result back up. This is the one change that moved the number: 53.5% -> + * 25.0% idle. On a soft gradient with translucent circles drifting over it, the + * upscale is not something you can point at on the screen. */ resolutionScale: number; /** Hard ceiling on either backing-store axis. Also a guard against a runaway * measurement hitting the browser's canvas allocation limit. */ maxBackingStorePx: number; /** Frames per second the loop will paint. 0 means "whatever the display offers". - * The gradient eases over ~2s and the bubbles drift; neither has anything to say - * at 60fps that it can't say at 30. */ + * Worth nothing at idle once the resolution is halved - see the table above - and + * kept only for the playing case, where each frame also samples the mood curve and + * moves every bubble. The gradient eases over ~2s; nothing here has anything to say + * at 60fps it cannot say at 30. */ maxFps: number; /** Ceiling on bubbles alive at once. 0 means unlimited. Spawn rate is driven by the * music, so a loud track on a big screen can otherwise pile up more circles per @@ -59,10 +75,12 @@ export const AMBIENCE_QUALITY: AmbienceQuality = LOW_POWER * render. 0 means "every animation frame", which is what it has always done. * * `usePlaybackClock` exists to stop the progress bar stepping twice a second, and it - * does that with a `setState` per animation frame - so `PlayView` and `PlayerBar` - * each re-render sixty times a second for the whole length of a track. A progress bar - * a thousand-odd pixels wide advances one pixel every few *hundred* milliseconds on a + * does that with a `setState` per animation frame - so `PlayView` and `PlayerBar` each + * re-render sixty times a second for the whole length of a track. A progress bar a + * thousand-odd pixels wide advances one pixel every few *hundred* milliseconds on a * three-minute track, and the time label beside it only has one-second resolution, so * ten updates a second is already more than either can show. + * + * Only bites while something is playing, so it is not in the idle table above. */ export const PLAYBACK_CLOCK_FPS = LOW_POWER ? 10 : 0; diff --git a/web/src/lib/theme.ts b/web/src/lib/theme.ts index b528c06..f8ba247 100644 --- a/web/src/lib/theme.ts +++ b/web/src/lib/theme.ts @@ -11,7 +11,6 @@ import type { CSSProperties } from "react"; -import { LOW_POWER } from "./lowPower"; import type { Group } from "./search"; import type { UiState } from "./keyboard"; @@ -35,13 +34,14 @@ export const ROW_SPACING = 25; * container, not per card, so cost stays flat regardless of grid size. */ export const ANIMATE_VIEW_TRANSITIONS = true; -/** Frost the glass panels/cards/rows with a real backdrop blur. `backdrop-filter` is - * one of the most GPU-expensive CSS effects in the app - a live backdrop copy+blur - * per element, repeated for every album card in the unvirtualized grid - and unlike - * the canvas there is no cheaper version of it to fall back to, so `?pi=1` switches - * it off outright: a plain translucent background, no blur. This is the one visible - * cost of the Pi profile. */ -export const SHOW_GLASS_BLUR = !LOW_POWER; +/** Frost the glass panels/cards/rows with a real backdrop blur. + * + * The obvious thing to cut on weak hardware, and on the Pi that is exactly wrong: it + * measured five times *worse* with the blur off (118% of a core against 25%). The blur + * is what promotes each panel to its own compositing layer; without it the animated + * canvas and the whole album grid above it share one layer and every canvas frame + * repaints all of it. See the table in lib/lowPower.ts before touching this. */ +export const SHOW_GLASS_BLUR = true; /** Render the play view's animated canvas background (gradient + bubbles, both * redrawn every frame at 60fps). Off falls back to `.stage`'s own static CSS @@ -53,10 +53,10 @@ export const SHOW_GLASS_BLUR = !LOW_POWER; export const SHOW_AMBIENCE = true; /** Run the purely decorative CSS animation loops: the room page's bubble field and - * the dolphin mascot's bob/swim. Individually cheap (opacity/transform only), but - * a couple of dozen elements animating forever still means a repaint every frame on - * a machine compositing in software, and they carry no information. */ -export const SHOW_DECORATIVE_ANIMATIONS = !LOW_POWER; + * the dolphin mascot's bob/swim. Left on even under `?pi=1`: switching them off on the + * Pi moved the idle figure by 0.4 percentage points, which is noise, and they are the + * kind of detail this app is for. */ +export const SHOW_DECORATIVE_ANIMATIONS = true; // ------------------------------------------------------------------ colors -- diff --git a/web/src/lib/tippen/theme.ts b/web/src/lib/tippen/theme.ts index 1941b22..2b3e9af 100644 --- a/web/src/lib/tippen/theme.ts +++ b/web/src/lib/tippen/theme.ts @@ -4,24 +4,21 @@ * through components. The hue lives in styles/app.css because CSS is where it is used; * it is repeated here only for the canvas, which cannot read a custom property. */ -import { LOW_POWER } from "../lowPower"; - /** The turquoise lagoon. Music is 210, Hörbücher 55, "Mein Zimmer" 300. */ export const HUE = 175; -/** Frost the glass panels with a real backdrop blur. Expensive on weak GPUs, and the - * on-screen keyboard frosts every single key - off under `?pi=1`, like the music app's - * matching toggle. */ -export const SHOW_GLASS_BLUR = !LOW_POWER; +/** Frost the glass panels with a real backdrop blur. Looks like the first thing to cut + * on a weak GPU; on the Pi it measured five times worse with it off, because the blur is + * what gives each panel its own compositing layer. Same finding as the music app's + * matching toggle - see the table in ../lowPower.ts. */ +export const SHOW_GLASS_BLUR = true; -/** The decorative rising bubbles behind everything. A dozen elements animating forever, - * carrying no information, so `?pi=1` drops them. */ -export const SHOW_BUBBLES = !LOW_POWER; +/** The decorative rising bubbles behind everything. A dozen elements on a CSS transform + * loop; measured as noise on the Pi, so they stay. */ +export const SHOW_BUBBLES = true; /** The earned pets swimming behind every screen. The reward that is always in view - off - * only to rule it out when chasing a performance problem. Kept on even under `?pi=1`: a - * handful of images moved by writing `transform`, and taking away what she earned to save - * a few frames is the wrong trade. */ + * only to rule it out when chasing a performance problem. */ export const SHOW_AQUARIUM_CREATURES = true; /** Fade screens in on entry. */