Praxis · M3 · ticket #184 · PR #257

Sixteen probes for colour, four for data

On minification's floor, 16x anisotropic filtering cost the lit pass 214 µs of its 557 µs on the M4 Pro at 1440p. Ticket #184 measured what lower settings save and what they change in the image, and the owner chose one: the colour Channels keep 16x and the data Channels drop to 4x. The checker stays as sharp, the pass saves 126 µs, and a sparkle in the light's reflection goes away. This guide covers the rule that sets how many probes a pixel takes, where in the frame the cost and the change land, why fewer probes on the bumps removed the sparkle, and how a supersampled reference judged the images, and where it could not.

Provenance. #184, part 1 of the owner's fourth visual check in milestone 3 (M3), is closed. PR #257 recorded the measured candidates in docs/performance.md ("Anisotropy candidates") on 2026-10-22, and the owner chose candidate C that day. The PR's later commits made the change: two samplers in the Material bind group, minification's and normal-tangent's Goldens regenerated on both backends, and the M4 Pro Baselines re-recorded. It merged the same day as f4c7442, and the code, ADR and glossary wording quoted here still match main at 69e02dc (2026-10-24), and at fd2ee9b (2026-10-26) but for the doc comment on trilinear_sampler, from which #295 dropped "(#144)". Measured numbers come from the PR and were checked against the captures made for it. The PR publishes the candidates' 640 × 360 captures but, at 1440p, only halved difference images and zooms, so this guide's own 1440p counts cannot be rechecked from public files. Numbers this guide computes are marked as its model. Since the merge, the Froxel lights (#261 to #263, Froxel Lights Explained) and the shadow mask the lit pass reads (#277, Shadow Rays Explained) have grown minification's lit pass, with these samplers unchanged: its M4 Pro Baseline is 488 µs at fd2ee9b, against 426.9 µs at the merge, so this guide's costs are as #184 measured them on 064d2ba.

Read first

Distant Textures Explained (#153) introduces texels and pixels, mips and trilinear filtering, the footprint of a pixel on a floor seen at a grazing angle, anisotropic filtering, normal maps and Toksvig, through the minification scene. In one paragraph: far down a floor, one pixel covers many texels. Mips are copies of a texture averaged down by halves, and trilinear filtering reads the two whose texels best match the pixel. At a grazing angle a pixel's footprint is long and thin, so anisotropic filtering reads a mip that matches its width and takes several probes along its length. A normal map's mips average its tilts towards flat, so the cooker widens roughness by how much the tilts disagreed (Toksvig). Minification is a 100 m glossy checker floor with a bump every 8 texels, seen from 1.2 m up, under one point light whose reflection lies about 16 m out.

Chapter 1 · the specifications' rule

How many probes a pixel takes

A pixel's footprint on the floor has two lengths in texels, across and along. Their ratio is the footprint's anisotropy. The sampler takes about as many probes as the ratio, up to its maximum, from the mip whose texels match the footprint's width, and averages them. Past the maximum it reads a coarser mip, so that its probes still cover the length, and the texture blurs across.

The extension EXT_texture_filter_anisotropic wrote the rule down in 1999 as one possible approach, and the Vulkan specification keeps its steps. They start from how fast the texture coordinates change from one pixel to the next along the screen's x and y, in texels per pixel:

Px = |∂(u,v)/∂x| Py = |∂(u,v)/∂y| Pmax = max(Px, Py) Pmin = min(Px, Py) N = min(⌈Pmax / Pmin⌉, maxAniso) the probes λ = log2(Pmax / N) the mip level they read probe i sits i/(N+1) − ½ of a pixel along the longer axis, i = 1 … N; the result is their mean

Each probe is an ordinary trilinear read at λ, so it blends the texels of mips ⌊λ⌋ and ⌊λ⌋ + 1. While the ratio stays under the maximum, Pmax / N comes out a little under Pmin: the mip matches the footprint's width, and the probes tile its length. Past the maximum, the mip follows the length divided by the maximum, coarser than the width needs. Vulkan writes the level with the unrounded ratio, λ = log2(Pmax / η) with η = min(Pmax / Pmin, maxAniso), less than one level from the extension's.

Both specifications leave the scheme to the implementation. It may round N up, to powers of two for instance, and approximate Px and Py. So the demos here model the specifications' rule, not the M4 Pro's hardware. In wgpu the maximum is anisotropy_clamp on the sampler descriptor, which "must be at least 1" and needs every filter mode linear above 1. The Capability floor requires the ANISOTROPIC_FILTERING downlevel flag, without which wgpu silently clamps every sampler's anisotropy to 1 (ADR 0021).

On minification's floor

For a pixel straight ahead, this guide's arithmetic gives the footprint in closed form. A pixel at depth z along the view axis spans z / f metres across and z² / (f h) metres along, where h is the camera's height and f the focal length in pixels: 1,738 at 1440p for the 45° vertical field of view (src/camera/mod.rs), 435 at 640 × 360. So the ratio is z / h, the depth over the camera's height. The floor holds 256 texels a metre, since its 256-texel Textures repeat every metre (src/scenes/mod.rs).

16 m out at 1440p, a pixel spans 2.4 texels across and 31 along, a ratio of 13.3: 14 probes at 16x, from mips 1 and 2. At 640 × 360, the Golden's size, the same pixel spans 9.4 by 126 texels, the figures Distant Textures Explained gave, and its probes read mips 3 and 4. The model's geometry matches the captures: it puts the floor's far edge, 96 m out, at row 481.4 of 1440 and 120.4 of 360, and the captures' first floor rows are 481 and 120.

Texture:
Maximum:
Frame:
the pixel's footprinteach probe's mip texel, 2λ texels a side
Screen row, straight ahead
Footprint, across × along
Ratio, along ÷ across
Probes N
Mip level λ = log2(along ÷ N)

Live demo, a model. One pixel straight ahead on minification's floor, drawn in texel space with the length running left to right: its footprint, and the probes the specifications' rule gives it, each drawn as the mip texel it reads. The camera looks at the floor 8 m ahead from the chosen height, with the 45° field of view, and the floor holds 256 texels a metre. The checker's checks are 16 texels and the bumps repeat every 8, as src/scenes/mod.rs makes them; where they repeat in under 4 screen pixels the drawing shows their average. Choosing a texture sets its sampler's maximum to the owner's pick, 16x for colour and 4x for data. The roughness readout uses minification's cooked mips (chapter 4). This is the rule as the specifications write it, not a measurement of the M4 Pro.
Distance aheadRow of 1440Across × along, texelsRatio16x: N, λ4x: N, λ
5 m8710.75 × 3.24.35, −0.634, −0.31
8 m7201.19 × 8.06.77, 0.204, 1.01
12 m6341.77 × 17.810.011, 0.694, 2.15
16 m5912.36 × 31.413.314, 1.174, 2.97
30 m5294.40 × 10924.916, 2.774, 4.77
96 m48114.0 × 1,11079.316, 6.124, 8.12
The footprint along the floor at 1440p, by this guide's model, for a camera 1.2 m up. A negative λ means each probe spans less than a texel of the footprint's length, so the probes read mip 0, magnified. Up to a ratio of 4 both settings take the same probes; past it, 4x reads a mip log2(N ÷ 4) levels coarser than 16x does, N being 16x's probe count, so at most two levels once 16x reaches its own cap at a ratio of 16.
Chapter 2 · measured on the M4 Pro

What 16x costs

At 1440p, 16x on every Channel costs minification's lit pass 214 µs over 1x, 38% of the pass's 557.2 µs. The lit pass does all the sampling: measured against 064d2ba, every setting moves the GPU frame total by what it moves the lit pass, within 2.1 µs. Lower settings cost less, but not in step with the probes they save: over 1x, 2x costs nothing, 4x 26 µs and 8x 104 µs.

The A/B ran release builds of 064d2ba, main at the time, and of 064d2ba with a measurement patch, not committed, that gives each Material Channel its own sampler and reads each one's anisotropy from an environment variable. praxis bench ran minification at 2560 × 1440 with 4x MSAA, 248 warm-up and 1,800 measured frames, in three rounds, each running every variant once in rotated order, with the load average about 13 from other builds and a browser running on the machine. At 16x the patch times as one sampler does, and its capture matches the old Golden pixel for pixel.

Measure:
Show:

Live chart, measured. minification's lit pass, or the whole frame's GPU time, for each anisotropy setting: the bar is the median of the three rounds' medians and the ticks are the rounds. Settings list the anisotropy of albedo, normal, metallic-roughness, occlusion and emissive. Point at, tap or focus a row for its rounds and its saving. The GPU frame totals are given because the tonemapping pass's timestamps overlap the lit pass on Apple's tile-based GPU. From docs/performance.md, "Anisotropy candidates".
  • Three Textures pay. The floor samples three Textures, in the albedo, normal and metallic-roughness Channels; occlusion and emissive read Layer 0's 1×1 Textures, whose anisotropy costs nothing: those two at 1x leave the pass at 557.5 µs.
  • Channels overlap. Albedo alone at 16x costs 42 µs over 1x everywhere, yet dropping albedo alone to 1x from 16x everywhere saves 84 µs. Costs measured one Channel at a time do not add up.
  • MSAA does not multiply it. The measurements at the end of milestone 2 (M2's exit) found 222 µs of a 478 µs main pass, before the pass had MSAA. The lit pass reads no sample index, so it shades once per pixel, and 4x MSAA leaves its texture reads as they were (this guide's reading of main.wgsl).
  • Surfaces that face the camera do not pay. In those measurements, clamping the anisotropy to 1 took the helmet from 357 to 356 µs and left normal-tangent at 202 µs. They face the camera, so most of their footprints are close to square.
  • Against 064d2ba. 8x everywhere saves 110 µs; colour 16x with data 8x saves 74, with data 4x 126 and with data 2x 158. Data 4x's 126 µs is 1.1% of ADR 0013's 12 ms GPU Budget.

Probe counts do not explain the cost. By the specifications' rule, the floor fills 66.5% of the 1440p frame, and its average pixel takes 2.00 probes at 2x, 3.59 at 4x, 5.00 at 8x and 6.25 at 16x (this guide's model). Yet over 1x, 2x costs nothing measurable, and 16x costs twice what 8x does. The specifications leave the scheme to the implementation, so on the M4 Pro the cost of a probe is something to measure, not to model.

Chapter 3 · measured in the captures

Where the image changes

Every difference sits in one band below the horizon, where a pixel's footprint is longer, in texels, than the lower anisotropy times its width. Nearer the camera, both settings take the same probes from the same mips. For the candidates that lower only the data Channels, the band also ends further out: beyond about 23 m, 16x itself reads mips in which the bumps have averaged away (chapter 4).

The 1440p frame of minification, dimmed, with a red band across the floor from a little below the horizon to about a third of the way down the floor, split in the middle by the highlight's white core.
Colour 16x, data 4x against 16x everywhere, 1440p captures, halved: the candidate dimmed to a third, with every pixel that differs by more than one 8-bit step painted red. The highlight's core is not red because it is white in both. From PR #257's images (colour-16x-data-4x-difference-1440p.png), JPEG-encoded for this page.

By this guide's count of the 1440p captures made for PR #257, which the PR publishes only as halved difference images and zooms, colour 16x with data 4x differs from 16x in rows 552 to 870: 567,416 pixels, 15.4% of the frame. The model puts row 552 at 22.8 m straight ahead and row 870 at 5.0 m, where the ratio is 4.3. 8x everywhere differs in rows 492 to 749, through the checker too, and its differences of more than 7 steps all lie above row 680; the model puts a ratio of 8 straight ahead at row 679. Of the candidates put to the owner, 8x everywhere moves 10.9% of the 1440p frame (400,069 pixels), colour 16x with data 4x 15.4% (567,416) and with data 2x 25.0% (920,160), all of it in that band.

The Golden's size hides some of it

The Golden is 640 × 360. There each pixel's footprint is four times wider and longer in texels, so its probes read mips two levels coarser, and most of the band reads mips where the bumps have already averaged away. Colour 16x with data 8x changes 221 pixels of the Golden, 0.1%, but 273,950 at 1440p, 7.4%, whose pixels sample finer mips that still hold the bumps. Against the Golden of 064d2ba:

Candidate at 640 × 360Pixels changedBeyond 1 stepLargest step
8x everywhere18,8927,22910
Colour 16x, data 8x22101
Colour 16x, data 4x18,1194,09929
Colour 16x, data 2x74,41437,702102
Captures at the Golden's size against the Golden of 064d2ba, 230,400 pixels in all. From docs/performance.md; this guide recounted all four from the captures made for the PR. In the first demo, switch the frame to 640 × 360: at every distance the probes' mip rises by two levels.
Chapter 4 · the cooker, the sampler and the shader

Why fewer probes removed the sparkle

The cooker widens each mip's roughness by the spread of the normals inside one of that mip's texels. The sampler picks the mip by the footprint's width, so the widening covers the width. Along the length, the probes average the normals, and the shader rebuilds a unit normal from that average, so the spread along the length never reaches roughness. Reading the data Channels at 4x makes their probes read coarser mips, whose texels hold whole bumps.

What each mip holds

Toksvig's widening runs per mip in the cooker (src/cook/texture.rs; ADR 0018). For each texel of a mip, it takes the unit normals the texel covers, their mean m and their spread 1 − |m|², and widens the roughness so that α′² = α² + 2σ², where α, the highlight's width in the GGX model, is the roughness squared, and σ² = (1 − |m|) / |m| after Toksvig, as Distant Textures Explained introduced. This guide ran the cooker's algorithm on minification's normal and roughness Textures, in f64 before BC5 encoding. It gives three kinds of mip:

MipEach texel coversNormalsRoughness
01 texeltilted up to 23.1°0.200, the floor's gloss
12 × 2 texelseach tilted 16.5°0.417
2 and coarser4 × 4 texels and moreflat0.585, all of the bumps' spread
minification's cooked normal and roughness mips, from this guide's run of the cooker's algorithm: the bumps' 8-bit normals decoded and renormalised, each mip a 2 × 2 box mean renormalised, the spread carried as the cooker carries it, and roughness widened from 51/255 by widened_roughness.

Mip 2 is flat because of where the bumps sit. A bump repeats every 8 texels, so a mip 2 texel covers half a bump each way, and the slopes at the four texel centres across half a bump cancel: cos(π/8) + cos(3π/8) + cos(5π/8) + cos(7π/8) = 0. From mip 2 on, every texel holds a flat normal and the bumps' whole spread as roughness.

Where each sampler reaches mip 2

So what decides the shading is the mip the data probes read. By the model at 1440p, 16x reads only mip 2 and coarser from 22.9 m out, a boundary at row 551.6 of the screen; 8x from 16.1 m (row 589.9), 4x from 11.4 m (row 644.0), 2x from 8.0 m (row 720.5) and 1x from 5.6 m (row 828.7). Two measurements match it to within a row: every data-Channel candidate's differences from 16x begin at row 552, and data 2x and data 1x render identical pixels above row 720 (this guide's count of the captures made for PR #257).

Data Channels at:
Frame:
16x, as before #184the data Channels' settingthe floor's gloss and the bumps' whole spreadmeasured: rows that differ from 16x

Live chart, a model checked against measurements. The roughness the data probes read straight ahead, from 2 m to the floor's far edge at 96 m on a logarithmic scale, for a camera 1.2 m up: the trilinear blend of the cooked mips above, at the level the specifications' rule gives. Where the two lines part, the settings shade differently. The washed band, at 1440p only, is the measured span of rows in which the chosen setting's capture differs from 16x, converted to distance by the model; it begins where 16x reaches mip 2 for every setting. Drag across the chart or use the slider to read a distance.

What the shader does with the probes

Between about 5 and 23 m, 16x's probes read mip 0 or mip 1, blended with the next: tilted normals and roughness under 0.585. At the light's mirror point, 16.4 m out by this guide's arithmetic, it takes 14 probes at λ 1.23, mostly from mip 1, and reads roughness 0.456; data at 4x takes 4 probes at λ 3.04 and reads 0.585 on flat normals. The 14 probes average their tilted normals along the length, and tangent_space_normal rebuilds z = √(1 − x² − y²) from the averaged x and y (src/render/shaders/shading.wgsl), so the normal comes back to unit length. The spread the averaging removed is lost: it is in neither the normal nor the roughness. Toksvig's 2004 brief names the result: "Most algorithms simply renormalize, but this paper explores how the shortening can be used as a measure of normal variation to eliminate the common problem of strobing/sparkling noise due to aliasing of specular highlights."

That is what the captures show. By eye, 16x and 8x render the highlight as a broad white column with single-pixel sparkle along its flanks and lower end, specular aliasing; the data Channels at 4x or 2x render a smoother, narrower V without the sparkle. Under ADR 0008 nothing the image depends on reads screen-space history, the pixels of earlier frames, so a temporal mode could only ever be an option, and nothing can be counted on to average that sparkle away. No candidate shows the light's mirror image, which the reference does: Toksvig widens one lobe and keeps no sharp peak.

The trade

Lowering the data Channels costs some detail. Beyond a footprint 4 texels long per texel wide, the bumps shade flatter than at 16x. Data at 2x saves 32 µs more and smooths further, but flattens the distant bumps' shading enough to move the checker 19% further from the reference than 16x everywhere does. The colour Channels stay at 16x because the checker is albedo, sharp detail that a coarser mip blurs across: 8x everywhere moves the checker 15% further from the reference, and 4x 81% (chapter 5).

Chapter 5 · the reference and its limit

Judging the images against a reference

To score each candidate, the work on #184 rendered the frame with nine samples per pixel and measured how far each one-sample capture lies from it. The score is fair on the checker, a fixed pattern. In the highlight, the reference resolves glints that no one-sample render can place, and the score rewards blur there, so the highlight was judged by eye.

How the reference was made

The reference is minification at 16x, captured at 7680 × 4320, three times 1440p each way, and box-filtered to 2560 × 1440 in linear light, so each pixel averages nine. The command:

magick main-7680x4320.png -alpha off -colorspace RGB -filter box \
  -resize 2560x1440! -colorspace sRGB -depth 8 reference-2560x1440.png

It averages the displayed colours, after the tonemapping pass, decoded to linear light. It is Praxis's own 16x render with more samples, so it measures how each candidate filters against more samples of the same shading, not against the physics of a real floor.

The score is the RMSE in 8-bit steps over the band of rows 484 to 759, the floor from about 86 m to 7 m out by the model, and within it over the highlight's columns 1000 to 1559 and the checker either side, columns 0 to 899 and 1660 to 2559:

CandidateBandHighlightChecker
16x everywhere16.0933.892.98
8x16.7235.053.44
4x16.8934.505.40
2x16.0428.989.54
1x19.3929.9215.15
Colour 16x, data 8x16.6335.022.98
Colour 16x, data 4x16.3434.323.06
Colour 16x, data 2x13.6128.093.55
Colour 16x, data 1x13.4227.653.59
RMSE against the reference, in 8-bit steps, from docs/performance.md; this guide recomputed every value from the 1440p captures and the reference made for the PR, which are not published in full. The checker figure combines its two sides.

The checker, a fair test

The checker is fixed in texture space, so the reference and every candidate show it in the same place, and its error measures filtering. 16x everywhere is closest, at 2.98 steps, as is colour 16x with data 8x. 8x is 15% further off, and 4x 81%. Keeping the albedo at 16x keeps the checker within 3% of 16x with data at 4x, and within 19% with data at 2x, through the bumps' shading.

The highlight, where RMSE favours blur

In the highlight the reference holds glints: it samples each pixel nine times, so it catches the bumps' reflections where they are. A one-sample render's glints land wherever its single sample happens to fall. RMSE squares every difference, so a glint in the wrong place costs twice, once where it should be and once where it is, while a blur that never commits costs less. That is why the smoothest settings score best there: data at 1x 27.65 steps, data at 2x 28.09 and 2x everywhere 28.98, against 33.89 for 16x. The highlight was judged by eye instead.

Live demo, a toy. Twenty-four pixels of 8-bit grey: the reference holds one glint of 255 on a floor of 80; the sharp render holds the same glint, placed where the slider puts it; the blurred render spreads the glint's excess evenly over five pixels. RMSE is computed as the table's is, the square root of the mean squared difference.
Region:
Candidate:
Band RMSE, steps
Highlight RMSE
Checker RMSE
Live demo, measured images. The reference left of the divider and a candidate right of it, both 500 × 200 crops of the 1440p frame starting at row 484: the highlight from column 1030, the checker from column 250. Each crop is drawn at up to twice its size with square pixels where the page is wide enough, as the PR's zooms were. The "data" candidates keep the colour Channels at 16x. Drag across the image or use the slider; the RMSE figures are over the table's full regions, with 16x's in brackets. Crops of the 1440p captures made for PR #257, JPEG-encoded for this page.
Chapter 6 · PR #257

The decision and the change

On 2026-10-22 the owner chose candidate C: the colour Channels, albedo and emissive, at 16x, and the data Channels, normal, metallic-roughness and occlusion, at 4x. It was judged on minification then, rather than with town's first Goldens (#206) as the milestone had paired them, so those Goldens were made under the settled sampler when PR #273 added town.

CandidateLit pass µsAgainst 16xChecker errorHighlight, by eye
A. 16x everywhere557.22.98 stepsbroad white column, sparkle on its flanks
B. 8x everywhere446.9−110 µs3.44 (+15%)as A, softer flanks
C. Colour 16x, data 4x430.8−126 µs3.06 (+3%)narrower V, no sparkle
D. Colour 16x, data 2x399.1−158 µs3.55 (+19%)smoothest V
The four candidates PR #257 put to the owner, in a table its description has since dropped; the measurements are in docs/performance.md. C dominates B: it is cheaper, closer to the reference on the checker, and by eye its highlight loses the sparkle. The PR recommended both C and judging it now, on minification.

Two samplers

The Material bind group now carries one sampler per kind of Channel, as the glossary says: "Albedo and emissive are the colour Channels, encoded sRGB; normal, metallic-roughness and occlusion are the data Channels, encoded linear. Each kind has its own sampler." (CONTEXT.md, Channel). In src/assets/material.rs:

const COLOUR_ANISOTROPY: u16 = 16;
const DATA_ANISOTROPY: u16 = 4;

let colour_sampler = trilinear_sampler(device, "Colour Channel Sampler", COLOUR_ANISOTROPY);
let data_sampler = trilinear_sampler(device, "Data Channel Sampler", DATA_ANISOTROPY);

/// A trilinear, repeating sampler (#144) at `anisotropy`, which the
/// Capability floor's anisotropic filtering keeps wgpu from clamping to 1
/// (ADR 0021).
fn trilinear_sampler(device: &wgpu::Device, label: &str, anisotropy: u16) -> wgpu::Sampler {
    device.create_sampler(&wgpu::SamplerDescriptor {
        label: Some(label),
        address_mode_u: wgpu::AddressMode::Repeat,
        address_mode_v: wgpu::AddressMode::Repeat,
        mag_filter: wgpu::FilterMode::Linear,
        min_filter: wgpu::FilterMode::Linear,
        mipmap_filter: wgpu::MipmapFilterMode::Linear,
        anisotropy_clamp: anisotropy,
        ..Default::default()
    })
}

The lit pass's fragment shader picks the sampler by Channel (src/render/shaders/main.wgsl):

textureSample(albedo_textures[material.layers.x], colour_sampler, in.uv),
textureSample(metallic_roughness_textures[material.layers.z], data_sampler, in.uv).rg,
textureSample(occlusion_textures[material.layers.w], data_sampler, in.uv).r,
textureSample(emissive_textures[material.extra.x], colour_sampler, in.uv).rgb
…
let normal_sample = textureSample(normal_textures[material.layers.y], data_sampler, in.uv).rg;

The bind group's definition declares the two as colour_sampler and data_sampler (src/render/bind_groups.rs), in place of the one pbr_sampler. ADR 0017 now records the 4x as "a choice of cost and image the samplers make, not a cap the streaming design imposes", and ADR 0021 the two settings.

The Metal Goldens moved with it. minification's new Golden is candidate C's capture, pixel for pixel, and differs from the old one in 18,119 pixels (this guide's count). normal-tangent moves 5 pixels by one step at its quads' grazing edges. Of the other scenes with Goldens, none samples a data Channel Texture: uv-grid, orrery, texture-wall, pbr-sweep and far-coplanar capture identically at 1440p, and the shadows scene, which #259 added just before the merge, has no Textures and kept its Goldens. The helmet, which has no Golden, samples all three data Channels through SciFiHelmet's normal, metallic-roughness and occlusion Textures: at 1280 × 720 its capture changed in 702 pixels, 50 of them by more than one step and none by more than 6, all on the helmet's grazing silhouette (PR #257's description). The merge regenerated minification's and normal-tangent's software-Vulkan Goldens too, and minification's Metal Golden on main is still candidate C's capture.

Chapter 7

What stays open

The decision settles the two samplers every Material uses, judged on minification. It does not make the cooked roughness see a footprint's length.

  • town's first Goldens (#206) were made under these samplers when PR #273 added town (c8f7071).
  • The length's spread. Past a ratio of 4, each data probe's mip texel spans a quarter of the footprint's length, so most of the spread along it reaches roughness. Up to a ratio of 4, the spread along the length beyond the width still does not. The lit pass computes no screen-space derivatives of its own: its shaders call no dpdx, dpdy or fwidth. The visibility survey (#45) noted that MSAA anti-aliases coverage, not shading, and that a forward pass has the quad derivatives that geometric specular anti-aliasing needs.
  • Reading, not plans. LEAN mapping stores the bumps' normal distribution "in a linearly-filterable form compatible with standard MIP and anisotropic filtering hardware", so averaging probes would average the spread as well. Kaplanyan, Hill, Patney and Lefohn filter the normal distribution over a region estimated from effects "such as pixel footprint".
Reference

Reading list

Grouped by source, each with what it is good for here. Repo links that #184 changed are pinned to its merge, f4c7442; the others to 064d2ba, main when #184 was measured; they are unchanged at the merge. Each outside citation was checked against its publisher, registry or the book's own contents.

This ticket and its PR

In the repo

Filtering normals

Books

  • Akenine-Möller, Haines, Hoffman, Pesce, Iwanicki and Hillaire, Real-Time Rendering, 4th edition, CRC Press, 2018: section 6.2, Image Texturing, minification at pp. 182–189 and anisotropic filtering at pp. 187–188 the footprint and the rule
  • The same book, section 9.13, Blending and Filtering Materials, pp. 365–374: Toksvig at p. 369, LEAN and CLEAN mapping at p. 370 filtering normals and their distributions; pages from the book's table of contents and index
Reference

Glossary

Texel
One pixel of a texture.
Mip
A pre-averaged copy of a texture, half the size of the one before it.
Footprint
The patch of a surface, in texels, that one screen pixel covers.
Anisotropy
A footprint's length over its width; straight ahead on minification's floor, the depth over the camera's height.
Probe
One trilinear read along a footprint; anisotropic filtering averages N of them.
Maximum anisotropy
The most probes a sampler takes: wgpu's anisotropy_clamp, 16 for colour and 4 for data.
Mip level λ
log2 of the texels each probe spans; a trilinear read blends mips ⌊λ⌋ and ⌊λ⌋ + 1.
Trilinear filtering
Reading the two nearest mips and blending them.
Channel
One texture role of a Material: albedo, normal, metallic-roughness, occlusion or emissive.
Colour Channels
Albedo and emissive, encoded sRGB, sampled at 16x.
Data Channels
Normal, metallic-roughness and occlusion, encoded linear, sampled at 4x.
Layer
One numbered texture within a Channel; Layer 0 is a neutral default.
Toksvig
Widening roughness by the spread of the normals under a footprint, read from their average's length; Praxis does it per mip, in the cooker.
Specular aliasing
Highlights that sparkle from pixel to pixel, from detail finer than a pixel.
Supersampled reference
The frame rendered with nine samples per pixel and averaged in linear light.
RMSE
The square root of the mean squared difference, here in 8-bit steps.
Lit pass
The pass that shades each pixel and samples every Material Texture.
GPU frame total
The frame's GPU time from its first pass's start to its last pass's end; since #266, plus the dynamic TLAS's build; since #291, from the passes' earliest start to their latest end.
Golden
A committed image that a Reference scene must reproduce within tolerance.
Baseline
The recorded measurements of a Reference scene on one adapter.
Reference scene
A Scene with a fixed camera path and injected time, the same frames every run.
Budget
A numeric limit on one metric of a Reference scene that a change must not regress.
Interleaved rounds
Every variant run once a round, in rotated order, so load affects all alike.
Tile-based GPU
A GPU that shades the screen in small tiles held on chip, as Apple's do.
Built from ticket #184 and PR #257, merged as f4c7442: docs/performance.md, the captures and the reference made for the PR, ADRs 0008, 0013, 0017, 0018 and 0021, and the code they name, as checked on 2026-10-22 and against main at 69e02dc on 2026-10-24. Where the guide computes for itself (the footprint model, the cooked mips, its counts of the captures), it says so. The images are crops of the 1440p captures made for the PR and one of the PR's difference images, JPEG-encoded for this page.