Praxis · M3 · tickets #359, #388 · PR #389

Box Leakage Explained

A pixel that averages what it covers sounds like the right answer to minification, and on most of the floor it is. This guide follows the case where it is not: a checker seen at a grazing angle, where each pixel's footprint spans many periods along the floor. It builds up the sampling theory needed to see why the exact average still shows moiré, shows that the GPU's sampler does the same thing, and reports what #388 measured for the filters that do better. Three live demos let you move the footprint and watch the residue.

Provenance. The code is main at 81b7e022. The defect and its exact reference are #359's (PR #389, docs/performance.md); the measurements are #388's, on an Apple M4 Pro, macOS 27.0.1, wgpu 30.0.1 on Metal, from a release build of 81b7e022 plus a measurement patch never merged. Where the guide brings in published work, it names the source.

Chapter 1 · tickets #359, #388

The arcs

Key idea. The arcs on minification's far floor are in a reference that averages the checker exactly over each pixel, so no step of the texture path makes them. Averaging over the pixel's footprint lets them through.

The Reference scene minification is a checker floor stretching to the horizon, with a checker period of 1/8 m. Captured at 2560×1440 at its Golden's frame, 59, its far band, rows 495 to 599, where the floor runs from about 60 m to 15 m out, shows arcs and diagonal stripes.

Ticket #359 asked where they come from. It built an exact reference: the checker averaged exactly over each pixel's footprint, the parallelogram uv ± dpdx(uv)/2 ± dpdy(uv)/2, over 3×3 subpixels, box-filtered in linear light. The arcs are there too, at the same places. The capture lies 1.68 8-bit steps RMSE from it in the band's sides. The sampler, the 16x anisotropy clamp, the cooker's box mips and their BC encoding each move the band by about a step or less, and none of them makes or removes the arcs.

So the question is not which part of the GPU gets the average wrong. It is why an exact average shows moiré at all.

Three crops of the far band, magnified twice. Top, the capture: grey with arcs and diagonal stripes. Middle, the exact box reference: the same arcs. Bottom, the exact Gaussian reference: an even grey with faint stripes only near the bottom.
The far band, left side, 2x. Top: the capture, with the sampler's 16x anisotropic filtering. Middle: #359's exact box reference. Bottom: the same exact subpixels resized with a Gaussian of σ 0.75 px instead of a box. The capture and the box reference share their arcs; the Gaussian removes them, and also greys out the stripes still resolved at the bottom of the band. Images from #388.
Chapter 2 · sampling theory

Sampling and Nyquist

Key idea. Samples one pixel apart can hold only patterns whose period is longer than two pixels. Finer detail does not vanish: it folds back and shows up as a coarser pattern that was never in the texture.

Sampling at spacing T can represent frequencies below the Nyquist frequency, 1/(2T). With one sample per pixel, T is one pixel, and the limit is half a cycle per pixel: a period of two pixels.

Energy above that limit folds back to a lower frequency. A sinusoid of f cycles per pixel, read at whole pixels, gives the same values as one of f − n cycles for any whole number n, so it shows up at its distance from the nearest whole number. A period of 0.9 px, 1.11 cycles per pixel, reads as 0.11 cycles per pixel: a slow wave with a period of about 9 pixels. That is aliasing. Mitchell and Netravali (1988) name the checkerboard as the typical source of conspicuous moiré from postaliasing, the aliasing a reconstruction filter adds when it lets high frequencies leak into the result.

On the far band, the floor is two different problems at once. Across the view, the checker's period stays about 4 to 14 pixels, well inside the limit, so the checks are resolved across. Along the floor, one pixel's footprint spans 14 to 0.9 periods, so the period along is between 1/14 and 1/0.9 of a pixel: everything there is past Nyquist. The checks are unresolved along.

The textbook cure is to remove the detail before sampling, with a prefilter. Averaging each pixel's footprint is exactly that, and the next chapter shows which filter it is.

Chapter 3 · the footprint's average

The pixel as a box

Key idea. Averaging a pixel's footprint is a box filter as wide as the footprint, followed by one sample per pixel. A box of width w keeps a sinusoid of period p's shape and scales its amplitude by sinc(w/p).

A pixel that averages its footprint weighs every point of it equally and ignores everything outside. Along the floor, that is a box filter of width w, the footprint's length, centred on the pixel's centre c. Its effect on one sinusoid takes one integral:

average of cos(2πx/p) over [c − w/2, c + w/2] = (p / (πw)) · sin(πw/p) · cos(2πc/p) = sinc(w/p) · cos(2πc/p) where sinc(x) = sin(πx) / (πx)

The box hands back the same sinusoid, read at the pixel's centre, scaled by sinc(w/p). That factor is the box's frequency response. A checker is a sum of such sinusoids, so each of them is scaled by its own factor.

For a footprint much shorter than a period, w/p near 0, sinc is near 1 and the detail passes: that is the near floor, where the checks should stay sharp. A filter that cut cleanly at Nyquist would pass everything with a period over two pixels and nothing below. The box does not do that, and the way it fails is the subject of this guide.

Chapter 4 · the box's response

Sidelobes: a residue that never settles

Key idea. sinc(w/p) is zero only when the footprint holds a whole number of periods. Between those it flips sign, and its envelope falls only as p/(πw), so a box over 14 periods still passes about 2% of the checker's contrast.

The whole periods inside the box average to zero. What is left is the part of a period that does not fit, divided by the whole width. The integral of a cosine over any stretch is at most p/π, the area of half a period's hump, so the residue is at most p/(πw) of the original: an envelope that falls as one over the width, not faster.

  • It is zero only when w is an exact multiple of p.
  • It changes sign between multiples: a footprint that gains or loses part of a period flips the residue from lighter to darker.
  • The first sidelobe peaks at w ≈ 1.43 p, at about 0.217 of the original, −13 dB.
  • At w = 14 p, the envelope is 1/(14π) ≈ 0.023: up to about 2% of the contrast, with either sign.

Pharr, Jakob and Humphreys call the box "just about the worst filter possible" in Physically Based Rendering, 4th edition, §8.8.3, because its frequency response lets high frequencies leak into the result. The demo below plots that response against the footprint's length, beside a Gaussian of σ = 0.75 w, the width chapter 7 uses.

box, sinc(w/p)Gaussian, σ = 0.75 wenvelope ±p/(πw)

Live demo. Top: one sinusoid of period p, the box of width w (shaded) and the Gaussian (dashed) that average it. Bottom: how much of its contrast each leaves, signed. Zoom on the tail to see the box's residue past 10 periods: small, never gone, and alternating. The Gaussian is below a thousandth of the contrast from w ≈ 0.8 p on.
Chapter 5 · one sample per pixel of the residue

From residue to arcs

Key idea. Neighbouring footprints tile the floor, so from one pixel to the next the residue's phase steps by w/p periods, which folds back to its distance from the nearest whole number. As w changes smoothly across the screen, that slow beat draws smooth bands: arcs.

A residue of 2% would be harmless if it were the same in every pixel: a slightly lighter or darker grey. It is not. Each pixel samples sinc(w/p) · cos(2πc/p) at its own centre c, and footprints that tile the floor put neighbouring centres w apart. Along a run of pixels with about the same w, pixel k reads:

residue(k) = sinc(w/p) · cos(2π k · w/p + phase)

That is a sinusoid of w/p cycles per pixel, sampled once per pixel, and chapter 2's folding applies: it shows up at w/p's distance from the nearest whole number. Where the footprint holds nearly a whole number of periods, the beat is slow and wide but faint, since sinc is near zero there. Near a whole number and a half, the beat is at Nyquist, alternating from pixel to pixel, and the residue is at a sidelobe's peak.

Across the screen, the footprint's length changes smoothly with distance and with the angle across the view, and so do the beat's size, sign and frequency. The result is smooth bands of lighter and darker grey that follow how the footprint's length changes over the floor. On minification they curve into the arcs of chapter 1.

Live demo. One row of 96 pixels on a sinusoid of period p, each pixel averaging its own footprint, the footprints tiling the row and growing by 30% from left to right, as they do along a floor that recedes. Top strip: what a box leaves in each pixel, as grey around the mean. Middle: the same values plotted, box in blue, Gaussian (σ = 0.75 w) in orange. Bottom strip: the Gaussian's. Nothing in the texture varies slowly, yet the box row shows slow waves. "Greys ×8" multiplies the residue before drawing, to make a 2% residue visible.
Chapter 6 · the sampler

The sampler is a box too

Key idea. Hardware anisotropic filtering averages N equal-weight probes along the footprint's major axis. Equal weights along a line make a box, with the same sidelobes.

The capture's 1.68-step distance from the exact box reference is no coincidence. The Vulkan specification's example of anisotropic filtering, which it leaves implementation-dependent, takes N isotropic probes along the footprint's major axis, each weighted equally:

N = ⌈major / minor⌉ the probes, up to the sampler's limit weight = 1 / N each probe's share LOD = log2(major / N) the mip they read

Each probe reads a mip about as wide as the footprint is across, and the probes sit along its length. Spread evenly with equal weights, they make a box as long as the major axis, smoothed a little by each probe's own width. Along the floor, that is chapter 3's box again. McCormack and colleagues' Feline paper (1999) says of equal-weight probes that they give "poor high-frequency rejection along the major axis".

Ticket #359 checked this on Praxis's hardware: the M4 Pro's sampler matched a software filter built from that rule to 0.23 steps on this floor. Anisotropy Explained covers the rule and why Praxis's colour textures allow 16 probes.

Chapter 7 · a different filter

A Gaussian instead

Key idea. A Gaussian of standard deviation σ keeps exp(−2π²σ²/p²) of a sinusoid of period p. That has no sidelobes and no sign flips and falls far faster than the box. Its price is blur: it also dims detail the box keeps.

Average the same sinusoid with Gaussian weights instead of equal ones and the integral gives:

Gaussian of σ: exp(−2π² σ² / p²) · cos(2πc/p) box of w: sinc(w/p) · cos(2πc/p)

The factor is always positive and only falls, so a pixel can never show a residue of the wrong sign, and once it is small it stays small. That removes both halves of chapter 5's mechanism.

The cost shows on detail that is resolved. At σ = 0.75 px, a sinusoid with a period of 4 px keeps exp(−2π² · 0.5625/16) ≈ 0.50 of its contrast, against the box's sinc(1/4) ≈ 0.90. The Gaussian halves checks that the box keeps nearly whole. That is the trade #388 measures: the far band's moiré against the near band's sharpness.

The demo renders a checker floor in software, the way #359's reference does: each of 4×4 subpixels averages the checker exactly over its own footprint, in linear light, and the pixel combines its subpixels with a box, or with a Gaussian over its neighbours' subpixels too. One sample at the pixel's centre is there for comparison.

Live demo. A checker of period 1/8 m seen from 1.2 m up, 480×240 pixels drawn at up to twice their size. Box shows faint arcs where a pixel spans more than a period along the floor; Gaussian removes them and greys the near checks; One sample shows the full aliasing a prefilter prevents. "Residue ×4" scales each pixel's distance from the checker's mean grey by 4 before drawing. The σ slider applies to Gaussian. The canvas's numbers differ from the scene's 2560×1440; the readout gives its rows.
Chapter 8 · the cheap knob

Why LOD bias barely helps

Key idea. A LOD bias reads a coarser mip for every probe, which widens each probe across the footprint. The N equal-weight probes still span the footprint's length as a box, so the sidelobes along it stay.

A LOD bias is the first knob most engines reach for against distant shimmer: add 1 to the mip level and every read comes from a mip with texels twice as wide. On a footprint seen face on, that blurs in every direction and does remove aliasing.

Here the aliasing is along the major axis, and chapter 6's rule fixes the probes' count and spacing from the footprint's length, not from the mip. A bias changes the width of each probe, which is the footprint's width, across. The box along stays as long as it was, with the same sinc. On this floor, bias +1 moves #388's moiré score from 5.31 to 5.00, and the image from 9.23 to 8.67 steps away from the exact Gaussian reference: the arcs stay, as the last crop of chapter 9's second figure shows.

Chapter 9 · #388's measurements

What #388 measured

Key idea. Weighting the probes along the major axis by a Gaussian came within 1.11 steps of the exact Gaussian reference, for about 0.28 ms on minification. Stretching the hardware footprint or blurring the finished image cut the moiré too, but strayed farther from that reference, and making a wide resolve see inside the pixel costs about 10 ms on town.

Ticket #388 tried each kind of change on the M4 Pro and scored the far band's outer 800 pixels on each side, in 8-bit steps, three ways:

  • Moiré: the standard deviation of the band's grey minus its own Gaussian blur of σ 6 px, so it measures structure finer than that blur.
  • RMSE against the exact box reference, #359's.
  • RMSE against the exact Gaussian reference: the same 3× subpixels resized with a Gaussian of σ 0.75 px (magick -filter gaussian -define filter:sigma=0.75).
ImageMoiréRMSE vs exact boxRMSE vs exact Gaussian
Capture (hardware 16x anisotropic)5.311.689.23
Exact, box (reference)5.2509.05
Exact, Gaussian σ 0.75 px1.189.050
Gaussian-weighted probes along the major axis, σ 0.75 px (software, in the lit pass)1.279.131.11
Same, σ 0.5 px2.966.094.13
Hardware footprint doubled (textureSampleGrad, 2x derivatives)2.0510.253.00
LOD bias +15.002.148.67
Gaussian blur σ 0.75 px of the 1x capture (what a wide MSAA resolve does when shading runs once per pixel)1.408.371.90

The highlighted row is the filter of chapter 7 applied where the texture is read. In the lit pass, a software filter takes probes along the major axis and weights them by a Gaussian instead of equally, the approach Feline and, on GPU hardware, Mavridis and Papaioannou (2011) take. At σ 0.75 px it lands 1.11 steps from the exact Gaussian reference. At σ 0.5 px it keeps more of the box's residue and sits between the two references.

Doubling the derivatives makes the hardware box twice as long, which halves its envelope, and twice as wide: the moiré falls to 2.05, but the image lies farther from the box reference than any other row, 10.25, and 3.00 from the Gaussian one, against the Gaussian probes' 1.11, because it blurs across as well as along.

Four crops of the far band, magnified twice. Gaussian-weighted probes at sigma 0.75: even grey, stripes only near the bottom. At sigma 0.5: faint ripples. Doubled footprint: arcs weaker but present, with a dotted row near the top. LOD bias +1: arcs as strong as the capture's.
Texture filters, far band, left side, 2x. From top: Gaussian-weighted probes at σ 0.75 px, at σ 0.5 px, the hardware footprint doubled, and LOD bias +1. Images from #388.

Filtering after shading

The last row asks whether the MSAA resolve could do the Gaussian's work instead. Praxis's lit pass shades once per pixel, so the four samples of a pixel inside a surface hold the same texture colour (MSAA Fragment Merging Explained). A resolve wider than the pixel can then only blend pixels that are already sampled, which is a blur of the 1x image. That cuts the moiré score to 1.40, since the metric measures fine structure, and lands 1.90 steps from the Gaussian reference. For a wide resolve to see texture detail inside a pixel, shading has to run per sample first.

Three crops of the far band, magnified twice. Wide blur of the 1x capture: the arcs softened but visible. Per-sample shading with a box resolve: arcs and stripes. Exact Gaussian: even grey.
Resolves, far band, left side, 2x. From top: a Gaussian blur of σ 0.75 px of the 1x capture, what a wide resolve does with per-pixel shading; per-sample shading with the usual box resolve, which scores 3.58 for moiré, 5.60 from the exact box and 5.46 from the exact Gaussian, since each sample still filters a whole pixel's footprint; and the exact Gaussian reference. Images from #388.

What they cost

Costs come from interleaved bench medians on the M4 Pro, with the GPU under other load, so they are indicative. The software Gaussian probes at σ 0.75 add about 0.28 ms to minification's gate (rays off) and about 0.6 to 0.7 ms on town. Per-sample shading, which a wide resolve needs before it can see texture detail inside the pixel, adds about 1.0 ms on minification and 10 to 12 ms to town's gate.

Chapter 10 · the owner's decision

Options, no choice made

Key idea. Which filter a pixel applies is a technique decision, not a defect to fix, so it goes to the owner. #388's research note sets out the options; ADR 0008 has already ruled out any that need temporal accumulation.

Everything above says what each kind of filter buys and costs; none of it picks one. The research note docs/research/minification-filtering.md, written for #388, ends in five options with their sources, measurements and costs:

  • A. Keep the box: hardware anisotropic filtering as today, arcs included.
  • B. Gaussian-weighted probes along the major axis at σ 0.75 px, in software in the lit pass, for the colour textures.
  • C. The same at σ 0.5 px, keeping more detail and more residue.
  • D. A hardware footprint widened 1.5 to 2 times through textureSampleGrad, cheaper and blurrier for more residue.
  • E. Per-sample shading with a wide custom resolve, at per-sample shading's cost.

One family is out before the choice. ADR 0008 keeps every frame stable on its own, with no temporal accumulation, so stochastic texture filtering, which relies on DLSS-style temporal integration to average its noise away, does not apply.

Review

Check yourself

One pixel's footprint spans exactly 3 periods of the checker along the floor; its neighbour's spans 3.5. How much of the contrast does each pass, and why does the difference matter?

sinc(3) = 0: whole periods average to nothing, so the first pixel shows the mean grey. sinc(3.5) = sin(3.5π)/(3.5π) = −1/(3.5π) ≈ −0.091: the second passes about 9% of the contrast with its sign flipped. Neighbours that differ in the amount and sign of their residue, as the footprint grows smoothly, are what one sample per pixel turns into bands.

Why does LOD bias +1 leave the arcs, when it makes every texture read blurrier?

A bias picks a coarser mip for each probe, so each probe is wider: that is the footprint's width, across. The probes' count and spacing come from the footprint's length, and they still weigh the major axis equally, as a box of the same length with the same sinc. #388 measured the moiré going from 5.31 to 5.00 only.

A Gaussian of σ 0.75 px removes the arcs. What does it cost a sinusoid with a period of 4 px, and why can't Praxis get the result from a wider MSAA resolve for free?

It keeps exp(−2π² · 0.5625/16) ≈ 0.50 of the contrast, against the box's sinc(1/4) ≈ 0.90: resolved detail blurs. Praxis shades once per pixel, so the samples inside a pixel hold the same texture colour, and a wider resolve can only blur pixels already sampled (1.90 steps from the Gaussian reference, 8.37 from the box). To see texture detail inside the pixel, shading must run per sample, about 1.0 ms more on minification and 10 to 12 ms on town's gate.

Reference

Reading list

Grouped by source, each with what it is good for here.

These tickets

In the repo

Earlier volumes

Papers and references

Reference

Glossary

Reference scene and Golden are CONTEXT.md's definitions; the rest are this guide's own or the series'.

Nyquist frequency
Half the sampling rate. One sample per pixel holds periods longer than two pixels; finer detail folds back.
Aliasing, moiré
Detail past Nyquist showing up as a coarser false pattern; moiré is its banded form.
Prefilter
A filter applied before sampling, to remove what the samples cannot hold.
Footprint
The patch of a surface one pixel covers; at a grazing angle, long along the view and narrow across it.
Box filter
An equal-weight average over a window; it scales a sinusoid of period p by sinc(w/p).
Sidelobe
A hump of a filter's response past its main lobe, passing frequencies it should block, here with alternating sign.
Residue
What a filter leaves of detail it should remove; for a box, up to p/(πw) of the contrast.
Gaussian filter
A bell-weighted average; it scales a sinusoid by exp(−2π²σ²/p²), with no sidelobes.
Probe
One trilinear read along a pixel's footprint; anisotropic filtering averages N of them.
LOD bias
A constant added to the mip level the sampler computes.
Reference scene
A Scene paired with a fixed camera path and injected time, so that every run produces the same frames.
Golden
A committed image that a Reference scene must reproduce within tolerance.
Built from tickets #359 and #388, PR #389, main at 81b7e022, CONTEXT.md and the sources in the reading list. Every measurement is #359's or #388's own, on one Apple M4 Pro. The three demos are this page's illustrative models.