Crop and framing — what shipped, and what it cost

Built 2026-08-25 → 26 in one long session. A framing control on the Mac chooses which part of the phone’s sensor becomes both the viewfinder image and the saved still. Shipped and verified on an iPhone 16 Pro; 546 tests; branch phase0-phase1-link-and-shutter at 4c0d828.

Related: aposemati · aposemati-apple-api-measurements · aposemati-transport-findings · aposemati-build-process-lessons · aposemati-phase2-live-preview · ⭐ aposemati-loopback-blind-spot


The reversal of “framing is metadata”

The original design specified framing as a transform stored beside an untouched 48 MP original, with Photo Booth’s non-destructive model as prior art.

What shipped is destructive. The crop is applied to the saved still, and the uncropped original is never written to disk at all — a failed crop loses the shot rather than saving the wrong image. The owner asked for this explicitly (“what I see on the preview should be what the still contains”; “the still should never be written to disk before editing the crop”), and reaffirmed it when the contradiction with the vault was found the next morning.

What the destructive choice costs, measured

A 2× crop takes 48.8 MP → 12.2 MP, and there is no original to go back to. The non-destructive design would have kept both and cost nothing but a rectangle in metadata — and would have made the HDR gain-map work below unnecessary, since an untouched file keeps its gain map for free.

The contradiction was found by reading the vault, and only after the feature was built. Nothing in the repo contradicted itself; the disagreement was between the repo and this vault. That is an argument for checking here first on a project with a decade of notes.


What was built

  • Lens picker — wide / ultra-wide / telephoto / front, each labelled with the stills it delivers. Switching swaps the session input in place, so interruption observers, the preview stream and the stall watchdog all survive it.
  • Framing control — a schematic (outer rectangle = frame, inner block = crop, dragged to pan), a zoom slider with stops at 1×/2×/3×/4×, and a live readout of the resulting still resolution.
  • One normalized region drives the phone’s preview crop and the Mac’s still crop.
  • Countdown — 3‑2‑1 before the shutter, off/3/5/10 like Photo Booth. Second press cancels.

Zoom is what costs megapixels, not the feature. At zoom 1 the phone’s bytes go to disk untouched — no decode, no re-encode, full bit depth, gain map intact, ~0 ms. Only a zoomed still is re-encoded (~300 ms of Mac work).


The preview ran at 17 fps and nobody had noticed

The lag people could feel after the earlier queue-depth fix was the capture frame rate, not anything in our code. Every drop counter read zero throughout — the pacer never skipped, AVFoundation never discarded a late frame, the relay never left one unread.

AVCaptureSession’s .photo preset silently selected a format whose video side runs 17 fps, delivering a 1608×1206 buffer derived from the full-sensor readout. A format offering 4032×3024 video at 30 fps and 8064×6048 stills was sitting in device.formats the entire time, unused.

Now 30–31 fps, verified on hardware.

It was found by printing the format table, not by reasoning about it

Two mechanisms were argued for first and both were wrong: low light (bright light moved it 1–2 fps) and zero-shutter-lag (disabling it changed nothing). See Naming a mechanism before measuring it.

Full API detail: Format selection.


Two defects found on hardware — one now closed

Neither was found by 546 tests, eight task reviews or a whole-branch review. Both appeared within ten minutes of a phone being plugged in.

✅ The viewfinder freezes permanently — ROOT-CAUSED AND FIXED 2026-08-26

Focus away from the Mac app and it stops updating — and does not resume on refocus or on a camera switch.

Excluded by measurement: the frame stream hanging (disproved by mutation test), the main actor blocking (sample shows it parked in mach_msg_trap), macOS suspending the app (alive and idle), the link being down (the phone reports 30 fps throughout).

The leading hypothesis was right, and narrower than stated. NetworkPreviewChannel never installed a stateUpdateHandler, so its only two routes to shutDown() were a receive error (in code only a reader reaches) and a send error (which QUIC datagrams never report over the radio). The phone only sends, so on the phone the channel had no path to shutDown() at all. Fixed with a [weak self] stateUpdateHandler in the shared class plus the retry loop the Mac already had — full story, mutation evidence and the reason the harness could never have caught it in aposemati-loopback-blind-spot.

Why 546 tests missed it

Over awdl0 a datagram send into a dead peer succeeds; on loopback the same send fails with POSIXErrorCode 57. The harness takes the error path the field never takes, so a test written for this defect passes against unmodified code — aposemati-loopback-blind-spot.

Do not fix this by watching for silence — and note the citation was being misread

The stall watchdog deliberately ignores silence because silence is peer-controlled, and keying on it hands an attacker a lever. See The one hard residualnot HANDOVER.md’s “spec §4.3”, which does not exist. The rule binds the receiver-side reassembler stall watchdog and says nothing about connection state. A stateUpdateHandler is a local transport fact, not silence, which is why the fix above is compliant.

Predates this work.

One shutter press in seven produced no file

May be the crop rule working as designed. Unknowable retrospectively — see below. Still open as of 2026-08-26; the freeze fix does not touch it.

The enabling gap: the Mac has no instrumentation — CLOSED 2026-08-26

The phone got a readout during this work (delivered buffer, crop, output, rung, bytes, frame rate, drop counters) and it is what made the format and orientation findings possible. The Mac writes nothing to the unified log, and print goes to stdout — nowhere, when launched with open.

Instrumenting the Mac was the cheapest high-value work remaining, and it blocked both diagnoses. It landed in 6607614, and it paid immediately — the freeze above was diagnosed from a four-line Mac log showing a clean 32-second open/finish/retry cycle, which is unreadable without it.