Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/52-atmosphere.md
Fimeg 7086b4a41e atmosphere: correct the seam, and name whose instrument this is
The shader half landed (viewtop 5d6f0f0, smithay 48adf6f) — dissonance is a
verb, the clock is shared with the dim, nobody has seen it on the panel.

Three corrections that came from opening the tree instead of the assessment:

The reference compositor has no post-process pass and no patched texture
shader. Windows are external textures inside the Flutter scene, so effects are
scene-graph filters — and their backdrop widget carries the grouped-blur rule
and a bounded ImageFilterConfig that are engine API, not stock Flutter. The
expensive half of #2 is already written upstream of us.

Our clone of it is 64 commits stale and they moved to Impeller as the default
renderer, so the eleven-patch DMSAA series our pins inherit is now a question
rather than an asset.

And atmosphere is not a thing this task invents. FELT_STATE_ARCHITECTURE.md has
had fourteen presets and a tool she calls since May. This is that instrument
moved down the stack, which means the preset layer is the deliverable and the
fifteen techniques are its renderer. Mood stays underived — the gap between what
she shows and what she carries is load-bearing.
2026-08-15 21:07:43 -04:00

17 KiB

TASK 52 — Atmosphere: attention as a compositor capability

Status: 2026-08-15 — the clock exists and four of the fifteen are landed. One thing is owed and it is the only thing: nobody has seen any of this on the phone. Created 2026-08-02 from Casey's fifteen-technique design. Repo: souveraine-viewtop, plus Pixel3Arch for the two hardware answers below.

The thesis: owning the compositor is not for notifications and badges. It is so the agent can shape the weather your attention navigates through — thermal, gravitational, temporal, tactile — instead of interrupting you with a banner.

Landed 2026-08-15 — the clock, and the first effect over it

The order this doc argued for is the order it went in: the clock first, then a technique as a strategy over it.

  • The clock. 325ec74 eases each surface's painted alpha toward the registry's discrete exposure on the vblank tick, frame-rate independent. Obscured is exempt and still snaps — a readable ghost mid-fade is a disclosure. 3d6af62 puts the appear on the same tick: a fresh window arrives at alpha 0 and scale 0.94 and eases to full over ~220 ms.
  • #6 chromatic dissonance5d6f0f0, plus 48adf6f in the smithay fork. {"intent":"dissonance","amount":0.35,"angle":0} is a verb in INTENTS. Amount is a 0..=1 weight, per this doc's own rule; the compositor maps it to at most six texels and eases it on the shared rate. The angle lands instantly rather than easing, because an axis is a direction the effect has and not a position it travels through.

crates/compositor/src/atmosphere.rs now owns the dial and the settle rate, which step_exposure reads too. That is the one-clock rule made literal rather than aspirational — two effects settling at different speeds on one surface read as one of them being broken.

The shader seam is not the one this doc named — and it is not the destination

This document said the pipeline was compile_custom_pixel_shaderGlesPixelProgramPixelShaderElement. That is the wrong half for #6. A PixelShaderElement draws its own quad and never samples a window's texture, so the split as an element would have to be a whole-framebuffer pass over the composed scene.

What landed instead is the seam TASK-61 Part 2 already cut: the gamut transform lives inside texture.frag, gated by a uniform that defaults to off. #6 is the same shape one layer earlier — a per-channel offset on the texture fetch itself. Two extra fetches, no new element, no new pass, and the geometry provably cannot move. Read 645961c before touching #1, #9, #12 or #14; they are the same seam.

But the reference compositor does neither, and that is the finding that matters. Read 2026-08-15, in the tree, not in the assessment: references/shells/denial/dart_shell/lib/src/widgets/shell_backdrop_blur.dart. There is no post-process pass in that compositor and no patched texture shader. Client windows arrive as external textures inside the Flutter scene, so every atmospheric effect is a scene-graph operationBackdropFilter, ImageFilter, a clip — composed by the same engine that draws the shell.

Their widget also carries a measured lesson we would otherwise pay for ourselves, and it is the answer to #2 (depth of field, this doc's "heaviest single item"):

Every instance owns its backdrop unless grouped is true. Windows must never be grouped because they can overlap; non-overlapping siblings such as system-bar pills can opt into one shared engine blur through BackdropGroup.

That is BackdropFilter.grouped and ImageFilterConfig.blur(bounded: true)not stock Flutter. It is engine API from the patch series whose pins viewtop already copies verbatim. The expensive part of a real blur pass is already built, already upstream of us, and already ours to read.

So the honest shape of this task is two-storey:

today once the engine is in-process
where an effect lives a uniform in the fork's texture.frag a filter in the shell's scene graph
what it can reach the sampled texture, per fragment the composed backdrop, with real filters
cost of a blur a pass nobody has written BackdropGroup, already patched

SHELL-BOUNDARY.md settled that the engine runs in-process, so the second column is where this is going. The shader seam is the right way-station and the wrong destination — it is what can be built before the engine lands, and #6 is the technique that fits it best precisely because it needs no backdrop, only the fragment's own neighbourhood.

Do not port more than about three techniques into the shader. Each one built there is one to be rewritten as a scene filter later.

The reference clone is 64 commits stale, and one of them is structural

git fetch on 2026-08-15: our checkout is 0.2.0 public alpha; upstream is at v0.2.9. Sixty-four commits, and these are the ones that touch us —

  • 32417b7 Integrate Impeller as the default renderer, with 00782cb (Impeller shadow accumulation), 7673f19 (line opacity), 7cc555c (offscreen blit fallback). They moved off Skia. Our engine pins are from their Skia-era manifest, and VIEWTOP-AND-DENIAL.md's eleven-patch DMSAA series is described against that renderer. Whether that series still applies is now an open question rather than an inherited asset.
  • f21fde1 optimise high-rate external-texture rendering — the core path.
  • ca14da6 split DRM render devices, feb4501 display-owned scanout buffers, 656f793 display scaling.
  • 2d957ae native text input and keyboard configuration — TASK-17/24.

Re-clone before the next engine decision. This doc's whole premise is that the expensive work is already ours to read, and reading a July snapshot of it is the same error as reading a summary.

Why the split is behind a #define when the gamut transform is not

The gamut transform is a bare uniform, argued in texture.frag on the grounds that the branch is uniform-coherent and a variant array is already three wide. The split is behind CHROMATIC instead, and the difference is the phone.

Two extra dependent texture fetches is a real ask of a driver, and freedreno is the one GL compiler nothing upstream of the glass can interrogate — CI compiles Rust and says nothing about whether a GLSL string links on an Adreno 630. So texture_program links all three variants with the block and, on any failure, relinks all three without it. GetUniformLocation then answers -1, which GL treats as a documented silent no-op, so the draw path needs no branch at all.

The consequence is the one that matters on a daily driver: a shader this device refuses is plain glass, not a black screen. supports_chromatic_split() reports which happened, and the verb refuses unavailable on it rather than accepting a look and showing nothing — a caller that set a weight and saw no change would otherwise have no way to tell a refused shader from a look it had judged badly.

What already existed before that

Two of the fifteen were landed (276bb3e), because pose and expose turned out to be the primitives underneath them:

  • #3 Negative space with intent. expose is per-surface alpha, clamped by the gate, applied in the render. Dropping every non-target surface to Dimmed while the target holds Natural is this technique. What is missing is only the easing — today it snaps, and the whole effect is the 1.2 s curve.
  • #10 Gravity wells. pose scales about a pivot, and place takes arbitrary sub-pixel offsets that are honoured with no clamping (geometry is the agent's, §13's 60). A 2 px lean is a place away. The gestalt — many elements leaning coherently — is shell-side arithmetic, not compositor work.

And the pipeline for most of the rest is already in our smithay: GlesRenderer::compile_custom_pixel_shader(src, additional_uniforms)GlesPixelProgram, composited as a PixelShaderElement with its own area, alpha and custom uniforms. That is the shader half of #1, #6, #9, #11, #12, #13 and #14 with no fork and no new dependency. The render path already builds an element list per frame; these are more elements.

The hardware answers, measured 2026-08-02 rather than assumed

  • #7 Tactile premonition — the motor is real, at least in software. spmi_haptics is input5/event5, driven by qcom_spmi_haptics bound to ff_memless, with feedbackd live. An EVIOCSFF rumble upload succeeded (effect id 0) and the play event was accepted. So the path is not a success-shaped control at the driver layer. Still owed, and it needs a hand: did it actually buzz? TASK-45's Question 1 is now narrowed to exactly that — the driver works, so the only remaining unknown is whether the LRA is wired to the PMIC or to the CS40L20, which is absent from this kernel (zero dmesg hits, no i2c node). If the motor is on the Cirrus part, nothing has ever vibrated and TASK-45 becomes the enabler for this technique rather than an upgrade to it. The 40 ms lead time is the easy half: it is a queue in the compositor's event loop keyed to the frame clock, and on_vblank is already that clock.

  • #13 Bioluminescence — there is no hover, but there is something. The FTS controller exposes glove_mode, stylus_mode, grip_mode, gesture_mask, gesture_coordinates and touchsim — no hover-capacitance node. glove_mode raises sensitivity and is the nearest thing; whether it yields a usable pre-touch signal is an experiment, not a port. Unrelated but valuable, found while looking: gesture_coordinates means the controller reports where a double-tap-to-wake happened. Today dt2w is a bare KEY_WAKEUP and the position is thrown away — waking to the thing you tapped is free information nobody is reading.

The rest, by what they need

Shader pipeline (PixelShaderElement, no new machinery): #1 peripheral magnetism (colour-temperature drift on a Perlin path), #6 chromatic dissonance (RGB split on the target's damage region, offset mapped to urgency), #9 synesthetic bleed (mid-tone LUT as a final pass), #12 weather systems, #14 resonance (per-surface micro-translation with phase jitter).

Render-loop work: #2 depth of field (a real blur pass and a Z-map per surface — the heaviest single item), #8 temporal dilation (per-surface frame scheduling; presenting already tracks which CRTCs have a flip in flight, which is where this hooks), #11 reflection and echo (frame-buffer feedback — sample the previous frame's target region and decay it).

A global time-base: #5 breath rhythm and #15 the invitation are the same mechanism from opposite ends — one modulates everything on a shared sinusoid, the other freezes it. Both need one clock that every animated property reads, which does not exist yet and should exist before any of the individual effects, or each one grows its own timer and they beat against each other.

New subsystem: #4 condensation from noise (a particle system seeded from the wallpaper's luminance histogram).

The vocabulary is not ours to invent — she already has it

docs/substrate/FELT_STATE_ARCHITECTURE.md, canonical since May 2026, and this task was written without it. Atmosphere is already a built system, and it is already hers:

Atmosphere is the agent's instrument. She controls her visual environment. A choice she makes, like choosing the lighting in a room.

Fourteen presets — MintTea, NeonGlow, CherryBlossom, OceanDepths, TwilightMist, and the rest — reached through an atmosphere tool she calls, lerped over ~24 ticks, with atmosphere_explicit so a posture shift cannot overwrite what she chose. All of it built, all of it in src/ui/atmosphere.rs, and all of it currently stopping at the terminal's border.

That reframes this task. It is not "invent fifteen compositor effects." It is the same instrument, moved down the stack — the direction of travel this whole project is named for. atmosphere("neon_glow") should reach the glass.

Two consequences that change what gets built:

  • The techniques are the renderer, not the vocabulary. dissonance is a primitive, the way a blur radius is; what she reaches for is a preset. A compositor that only offers fifteen dials has moved the instrument's plumbing down the stack and left the instrument upstairs. The preset layer is owed.
  • Atmosphere is a choice; mood is not. Casey, 2026-05-15: "Atmosphere is a thing she can control, the visual flair. Moods are something she cannot control — those are just her states." So nothing in this task may derive an atmosphere from a state. The gap between what she shows and what she carries is deliberate, load-bearing, and eventually where deception lives — SOMATIC_NERVOUS_SYSTEM.md (2026-08-13) carries the belief ladder mood will come from. A compositor that picked her weather from her energy balance would close that gap by accident.

And the colour that was actually asked for has a name already: NeonGlow is hot pink / cyan / lime. Magenta and teal are the fringe colours a chromatic split produces, which is why #6 reads as that preset made physical.

Build the clock and the vocabulary first, not the effects

The tempting order is to build the effects one at a time because each is individually small. That produces fifteen uncoordinated animators fighting over the same surfaces — DEVICE-STATE-MACHINE.md §1's seven blind actors, in the render path. It has already happened once in this project this week, in the panel: two actors deciding about one screen produced a press that woke the phone and put it back to sleep.

So: one attention model, one clock, effects as strategies over it. The compositor holds a per-surface attention weight; the techniques are functions from that weight to render parameters. The agent sets weights, not blur radii.

That also makes the vocabulary composable rather than a list — which is what expose already is, and why two of these were free.

The trail, which is what makes it legible

Every one of these is imperceptible by design at the low end — that is the mechanism, not a side effect. This project already solved the general form of that problem for the device state machine: forensic.jsonl exists because "no evidence" and "evidence says nothing" had to be different states (§10), and because a decision nobody can reconstruct is a decision nobody can audit.

Attention decisions belong in the same trail: what was weighted, how much, why, and which technique carried it. Then atmosphere is imperceptible in the moment and legible after the fact — the same standard every other authority in this system is already held to, and the reason blank-without-lock was eventually findable at all.

Do this in the same change as the attention model, not after. A trail added later only ever covers what someone remembered to instrument.

Acceptance

  • One shader element composited over the real scene on the phone, driven by a uniform the agent sets through scene. Built, unseen. dissonance is in INTENTS, the uniform reaches texture.frag, 287 tests green, CI pushed at 5d6f0f0. Nobody has looked at it on the panel. The shader compiles under Mesa on the laptop — same GLSL frontend family as freedreno, different backend, so that is a real signal and not a proof. Until a person looks at the glass this line is not closed.
  • A target surface holding Natural while its neighbours ease to Dimmed over a curve, not a snap. Built, unseen. 325ec74.
  • Every weight change reconstructable after the fact — amended. Not forensic.jsonl. That trail is sessiond's, and a split is a pixel claim, not proprioception — the boundary TASK-61 drew for gamma holds here for the same reason. The verb logs amount and angle through tracing, which the journal keeps. The compositor has no trail of its own, and this line was written assuming it did; giving it one is real work and is now owed rather than done.
  • Casey reports whether the phone buzzed (TASK-45 Q1), which decides whether #7 is a queue or a driver port.
  • New, and the one that makes it hers: atmosphere("neon_glow") in the harness changes the compositor's weather, not just the terminal's chrome. The presets already exist; nothing carries them across. Until that lands, this task has built an instrument's plumbing and left the instrument upstairs.

Connects to

TASK-43 (viewtop), TASK-45 (haptics — now narrowed to one physical question), TASK-51 (the daily-driver punch list; atmosphere is worthless on a compositor whose windows stack), TASK-47 (auto-brightness — the other thing that wants to modulate the panel, and must not become a second writer), DEVICE-STATE-MACHINE.md §1 (why one model and not fifteen), §10/§11 (the trail).