Native Swift/Metal SSB redraw while dragging aberration controls, 2026-10-24#
Historical: describes the code as of 2026-10-24; the
complexWave(complex_wave) estimator is retired. See the current fit methods.
Question#
Can the native MetalSSBJointModel.reconstruct re-render a fitted SSB phase at
30 frames per second (≤ 33 ms per frame, median and p90) while a user drags an
aberration control, after a fifth-order fit, at 3× output sampling, with every
BF pixel, all 14 aberration terms, sample tilt and depth spread, float32 and
complex64, and the default phase_of_mean estimator?
Workload and boundary#
Apple M5 Max, 40-core GPU, 128 GB unified memory; the benchmark checks the Metal device name before timing. No thermal or performance warning was recorded. No other GPU job ran during the timings.
Geometry of a 512 × 512 × 192 × 192 acquisition at 300 kV, 30 mrad and a 0.373 Å scan step: 8,804 BF pixels (radius 52.95 px). The 256 × 256 rows use the same detector and step. Counts are deterministic synthetic values; the work depends on the geometry, grid and fitted terms, not on count values.
Low order: C10, C12, φ12 with tilt (1.2, −0.8) mrad and 15 nm depth spread. Fifth order: the same plus all 12 higher-order terms nonzero (C21 … C56).
One observation = one
reconstructcall with a changed C10, synchronized: correction, mirror, origin, inverse FFT and phase into a host-visible buffer. Engine preparation and the first 3 calls are excluded; 20 warm observations. App display, colormap and FFT panel are excluded.Runner:
swift run -c release metal-ssb-redraw-benchmark --scans 256,512(native/swift/Benchmarks/MetalSSBRedrawBenchmark). Before = the same runner built againstf5599f73; after =518c6761. Both ran in one quiet session.
Results#
Per-frame time, median / p90, ms.
Scan |
Order |
Output |
Before |
After |
Speedup |
|---|---|---|---|---|---|
256² |
low |
1× |
13.5 / 14.0 |
5.8 / 5.9 |
2.3× |
256² |
low |
2× |
25.3 / 25.7 |
8.7 / 8.8 |
2.9× |
256² |
low |
3× |
24.3 / 24.8 |
8.9 / 9.1 |
2.7× |
256² |
low |
4× |
24.3 / 24.9 |
9.4 / 9.5 |
2.6× |
256² |
fifth |
1× |
29.1 / 29.3 |
6.4 / 7.1 |
4.6× |
256² |
fifth |
2× |
62.8 / 63.7 |
9.9 / 10.1 |
6.4× |
256² |
fifth |
3× |
67.9 / 69.3 |
9.9 / 10.0 |
6.9× |
256² |
fifth |
4× |
68.1 / 68.9 |
10.3 / 10.4 |
6.6× |
512² |
low |
1× |
29.9 / 30.6 |
20.5 / 21.2 |
1.5× |
512² |
low |
2× |
65.3 / 67.0 |
30.5 / 31.6 |
2.1× |
512² |
low |
3× |
60.4 / 64.3 |
31.2 / 31.5 |
1.9× |
512² |
low |
4× |
60.6 / 61.6 |
32.2 / 33.0 |
1.9× |
512² |
fifth |
1× |
101.5 / 102.9 |
21.8 / 22.7 |
4.7× |
512² |
fifth |
2× |
203.0 / 212.3 |
34.6 / 35.1 |
5.9× |
512² |
fifth |
3× |
214.9 / 223.8 |
33.7 / 33.9 |
6.4× |
512² |
fifth |
4× |
224.6 / 227.2 |
35.7 / 36.3 |
6.3× |
The table’s “after” column ran directly after the ~10-minute “before” sweep. The target case run alone, twice: 28.8 ms median / 29.8–30.0 ms p90 (34.7 FPS). 300 consecutive frames: 29.2 ms median / 32.2 ms p90. The 30 FPS target at 512² fifth order 3× is therefore met for a drag, with about 1 ms p90 margin; after many minutes of continuous load it sits at the 33 ms line. 2× and 4× at 512² fifth order are 1–3 ms above it.
60 FPS at 512² 1× is not reachable on this machine with every BF pixel in complex64: the 9.3 GB Fourier cache G(q, k) must be read each frame, and measured read bandwidth is 450–510 GB/s (≥ 19 ms). Fifth-order 1× reaches 45.9 FPS. At 256², every case exceeds 97 FPS.
What changed (ssb_redraw.metal)#
Anti-Hermitian half: Γ(−q, k) = −conj Γ(q, k) also for odd orders, because the minus sideband uses χ(k − q); only source rows 0 … N/2 are evaluated and mirrored, with the output Nyquist column evaluated directly.
One thread per source frequency visits its output aliases inside each 32-pixel batch, so aliases reread that batch of G from cache. Per-alias threads streamed G from DRAM once per alias (~17 GB per frame at 3×).
χ as a row-collapsed polynomial: per BF pixel and output row, χ is a degree ≤ 6 polynomial in the output column; the threadgroup computes the coefficients once, and each pair costs one Horner evaluation. Bivariate coefficients are built on the host in double precision from the packed float32 terms.
Conservative per-SIMD-group compaction: each group keeps only the BF pixels whose minus or plus sideband can open in its column interval (94% lane use), split into minus-only, plus-only and both. With one open sideband, conj Γ / |Γ| = ±exp(iΔχ): aperture and depth magnitudes cancel. Only the sinc sign is used, with a guarded fallback to the general formula near the 1e-8 normalization floor.
fast::sincos, which is bit-identical toprecise::sincosfor |x| ≤ 200 rad on this GPU, branch-free aperture weights, and padded float4 rows.
8× keeps the earlier per-term kernel.
Parity#
Against the per-term kernel (
SSBRedrawReconstructionTests): max wrapped phase error 5.2e-8 rad, RMS 1.5e-8 at 128²; 1.4e-7 / 5.6e-8 at 256². This covers 1–4×, cached and streamed spectra, low and fifth order, with and without depth. The stated bound is 1e-5 / 1e-6.Against an independent double-precision fifth-order definition: ≤ 1.1e-7 rad, bound 2e-4.
Full-size benchmark phases, after vs before: ≤ 3.9e-6 rad at 512², ≤ 8.5e-7 at 256², correlation ≥ 0.99999972. These synthetic phases have small contrast (std 1e-3 rad).
The new
complexWaveestimator agrees with Python MPSphase_estimator="complex_wave": 2.5e-8 rad in phase, 3.1e-7 of the mean amplitude, on a committed synthetic fixture.
Rejected candidates#
These variants were measured and not kept:
Accumulators for all F² aliases in registers: correct but slower (53–57 ms).
G staged in threadgroup memory: 52–55 ms. Threadgroup memory shares the on-chip pool with registers and cache on this GPU.
Several columns per thread, two pixels per iteration, software prefetch, rint-based sign: neutral or slower.
A bug found during development: a barrier inside non-uniform control flow produced a nondeterministic race; the shipped kernel’s barriers are uniform.
Limits#
The app’s on-screen render time and displayed FPS were not measured: control of the app was declined for the headed check, so the headed check remains open. Kernel time is not displayed frame rate.
Synthetic counts at the real geometry; the real acquisition’s BF count can differ slightly (8,807 vs 8,804).
One Apple GPU; other Apple GPUs are not qualified.
Private evidence: experiment 20261024-metal-ssb-interactive (raw JSON, logs,
phase dumps). Kernel SHA-256 (ssb_redraw.metal):
b20d7914811d0fb2941d59907ba6e15607829f0988b08498f262d9942a3a5e34.