Five Decades to Real Time

1974 to 2016, in 0.3 ms

v2.3.0 release notes →

Scrutinizer simulates peripheral vision by degrading content away from your gaze. Until now, that meant MIP blur — fast and stable, but it averages away everything. v2.3.0 replaces it with a WebGPU compute pipeline that keeps what MIP blur destroys.

SOURCE MIP BLUR var(L) = 2.6 COMPUTE MONGREL var(L) = 4.4 MIP blur: Spatial detail → gone Local contrast → gone Mean luminance → preserved Compute mongrel: Spatial detail → gone (oriented noise) Local contrast → preserved This is the TTM signature.

The theoretical basis is the Texture Tiling Model (Freeman & Simoncelli 2011, Rosenholtz 2016): peripheral vision pools summary statistics — mean, variance, orientation energy — not blur. A simulation that preserves those statistics while destroying spatial layout produces mongrels (Rosenholtz et al. 2012): textures that look wrong up close, right in the periphery. MIP blur approximates acuity falloff models from the 1970s. This catches Scrutinizer up with the last decade.

WebGPU is less mature than WebGL — it can hard-crash the renderer, not just drop frames. Whether this could run in real time on integrated graphics was an open question. It can: ~900 lines, under 0.3 ms, with a safety harness that falls back to MIP blur if anything goes wrong.

The pipeline

Two WGSL compute passes run alongside the existing WebGL renderer on a half-resolution copy of each frame.

Source frame half-res RGBA PASS 1: STATS crowding-stats.wgsl 8×8 workgroups Oklab L: mean, σ Oklab a, b: tile mean 4-dir orientation energy CMF mip level 48 bytes/tile → storage buffer PASS 2: SYNTH crowding-synth.wgsl Read tile stats 4-grating oriented noise Scale by σL Tile-mean chrominance Oklab → sRGB pack alpha = (mip - 0.5) / 1.5 TEXTURE5 RGBA8 + alpha → fragment shader Two dispatches + async readback — <0.3ms on Apple M1 integrated GPU

The key idea: Pass 1 measures what’s in each tile (in Oklab, a perceptually uniform color space). Pass 2 synthesizes new pixels that match those measurements without preserving the original spatial layout. The result blends into the WebGL pipeline via eccentricity-dependent alpha. Implementation details in the v2.3.0 release notes.


Measuring the difference

Sample rectangular patches at each eccentricity from both rendering modes, convert to Oklab, and compare L-channel variance:

varianceRatio = var(mongrel_L) / var(mipBlur_L)

Above 1.0: the mongrel preserved more luminance contrast. Below 1.0: it smoothed more.

Results

We measured at three eccentricities using golden captures from a crowding test stimulus.

10° Foveal Transition Peripheral filtering three measured eccentricities
Three eccentricities sampled by the comparison script. 3° straddles the foveal boundary — dimmed in the table. 6° and 10° are true peripheral, where the two pipelines diverge.

Each row averages rectangular patches above and below the fixation point at a given eccentricity. The key column is L Ratio — luminance variance of the compute mongrel divided by MIP blur. Green means the mongrel preserved more local contrast (ratio > 1.0).

Ecc MIP Mongrel L Ratio C Ratio
3° * 2.64.41.69 0.93
4.14.11.04 0.89
10° 6.66.81.03 0.93

* 3° straddles the foveal boundary — transition zone, not true peripheral. 28px column, golden captures from v2.2. Full per-band data with above/below splits in the validation report. These measurements cover 3–10°; extending to wider eccentricities (>10°) is a planned follow-up.

The L ratios at 6° and 10° are both above 1.0 — modest (3–4%), but consistent. Both modes pool color similarly (C Ratio ≈ 0.9), which is expected: color is pooled aggressively in the periphery regardless of method. Luminance is where the two pipelines diverge.

Score card

#HypothesisResult
H5 Oklab L variance ratio > 1.0 at ≥6° PASS (avg 1.04)
H6 Chrominance variance ratio ≈ 1.0 at ≥6° (±0.25) PASS (avg 0.91)
3° L ratio ≈ 1.0 1.69 — transition zone, not passthrough

Finer pooling regions or a larger Gabor bank would likely increase the advantage. The pipeline and the measurement are in place — as of v2.3.0, this is the default rendering path.


Also in v2.3.0