What a machine computes

A rounded viewport has two focal lengths

Compute the aspect ratio from a window's requested size and draw into a buffer of whole device pixels, and the picture has two focal lengths that differ by up to one part in three hundred. It looks like a defect that should grow worse on small screens and better on large ones. At the frame's edge it does neither, because the error is a bounded displacement of a fraction of a pixel on any window at all.

Worth reading first: A texture does not interpolate on the page · Recovering the camera from the picture it drew.

Halving a picture moves its centre ended by noticing a smaller thing beside the thing it measured. A picture of 1,000 by 562 reduced by three-quarters goes to 750 by 422, because the row count had to round, and a picture with one focal length has acquired two that disagree by about a tenth of a per cent. It is not a resize that causes this. Every window a renderer ever draws is in the same state, and the state is built into how windows and buffers are sized. On a screen at 1.25 device pixels a layout unit, a window 1,001 units wide has a buffer 1,251 pixels wide, which is 1.24975 of them to a unit, and a window 563 tall has one of 704 pixels, or 1.25044 to a unit.

A renderer then does a natural thing. It takes the window’s aspect ratio from the numbers it was given, 1001/5631001/563, builds a projection with that aspect, and draws into the buffer. The two axes now have slightly different scales from world to pixels. The pixel that is not square measured what a stretched camera costs, at a stretch of a third or a half. This one is much smaller, and the question is whether small is the end of it.

Two focal lengths from a whole-number buffer

Take a vertical field of view with half-angle tan⁡ϕ=t\tan\phi = t, a window of WW by HH layout units at a display scale dd, and a buffer of w=round(Wd)w = \mathrm{round}(Wd) by h=round(Hd)h = \mathrm{round}(Hd) pixels. A projection built with aspect a=W/Ha = W/H maps a camera-space direction X/ZX/Z to a horizontal position of w2(1+X/(Zat))\tfrac{w}{2}(1 + X/(Z a t)) and a vertical one to h2(1+Y/(Zt))\tfrac{h}{2}(1 + Y/(Zt)). So the two focal lengths in pixels are

fx=w2at,fy=h2t,fxfy=w/hW/H.f_x = \frac{w}{2at},\qquad f_y = \frac{h}{2t},\qquad \frac{f_x}{f_y} = \frac{w/h}{W/H}.

Their ratio is one when the rounding preserved the shape, and otherwise it is off. On whole-number display scales, rounding does nothing and the ratio is exactly one. On a fractional one, which covers most laptops and very many desktop monitors, it is almost never exactly one.

Two focal lengths from one window, at 1.25 device pixels a unitFor windows of many sizes and shapes on a display that scales by 1.25, the difference between the horizontal and vertical focal lengths the renderer ends up using when it computes the aspect ratio from the window and draws into a buffer rounded to whole device pixels. The worst here is 3.32 parts in a thousand, on the smallest windows. It falls as one over the width, and it is never zero for long: the sign changes with how the two sizes round.-2021e+32e+33e+3the buffer's width (device pixels)how unequal the focals are (parts in 1000)1494 windows at 1.25×worst 3.32 in 1000
Fig. 1 The difference between the two focal lengths, over many windows at 1.25 device pixels a unit. It is largest for small windows and it changes sign unpredictably. The worst here is about one part in three hundred.

The scatter has the shape a reader would expect. The difference falls as one over the size, since half a pixel of rounding is a smaller share of a larger buffer. At a width of 320 the two focal lengths can differ by 3.3 parts in a thousand, and across a modern 2,000-pixel window by a few parts in ten thousand. It has no preferred sign: a window can be stretched either way, and resizing a window by one layout unit can flip it.

That scatter makes the small windows look like the problem: a phone-sized canvas with a third of a per cent of anisotropy. A reader who stops here concludes that the error is small on large screens and worth a check on small ones. That turns out to be the wrong picture of it, and the correct one is both simpler and more reassuring.

The error is a displacement, and it is bounded

An anisotropy matters to a picture only through where it moves things. Fix the vertical scale. A mark at the frame’s left or right edge then stands away from where equal focal lengths would have put it by

Δ=(fxfy−1)w2\Delta = \left(\frac{f_x}{f_y} - 1\right)\frac{w}{2}

pixels. Now write the rounding explicitly: w=Wd+δww = Wd + \delta_w and h=Hd+δhh = Hd + \delta_h, each δ\delta no larger than a half. Then to first order the ratio minus one is δw/w−δh/h\delta_w/w - \delta_h/h, so

Δ≈δw2−δh2⋅wh,∣Δ∣≤14(1+wh).\Delta \approx \frac{\delta_w}{2} - \frac{\delta_h}{2}\cdot\frac{w}{h},\qquad |\Delta| \le \frac{1}{4}\left(1 + \frac{w}{h}\right).

The width has cancelled. A relative error falling as one over the size and a distance to the frame’s edge growing with it have exactly offset each other. The displacement at the edge depends only on the window’s shape, and for 16:9 it cannot exceed 14(1+16/9)=0.694\tfrac14(1 + 16/9) = 0.694 px, whether the window is a small embed or a whole 4K monitor.

At the frame's edge the rounding never grows past a boundThe same difference in focal lengths, expressed as how far a mark at the frame's edge stands from where equal focals would place it. For 16:9 windows at 1.25× the largest here is 0.569 px, against a bound of 0.694 px that the rounding cannot exceed at any size: half a pixel of rounding each way, in the width and in the height, the height's scaled up by the aspect. The relative error falls with size and the distance to the edge grows with size, and the two cancel.-0.50000.5001e+32e+33e+3the buffer's width (device pixels)displacement at the frame's edge (px)16:9 windows at 1.25×bound ±0.694 px
Fig. 2 The same difference, as a displacement at the frame’s edge, for 16:9 windows at 1.25 device pixels a unit. Unlike the previous figure, the cloud does not narrow with size. The lines are the bound, which comes from the width and height each rounding by at most a half.

The sweep confirms it over 15,408 windows, from 320 to 2,560 layout units, six shapes and eight display scales. No window breaks the bound. The largest displacement anywhere is 0.708 px, on a 21:9 window for which the bound is 0.833, and most sit a long way inside it.

So the mental model should be turned over. The picture’s edge is out by less than one pixel, at any size, and the small window has no special problem. It only looks as if it does when the error is written as a proportion. In the same way, a pixel is not a point keeps its half pixel of convention at any resolution: it is a smaller share of a larger picture, and it is still half a pixel.

How large the displacement usually isOver 15,408 windows, of sizes 320 to 2,560 units, six shapes and eight display scales, the share falling in each band of displacement at the frame's edge: 0 to 0.02 px 54.3%, 0.02 to 0.05 px 2.4%, 0.05 to 0.1 px 5.0%, 0.1 to 0.2 px 12.7%, 0.2 to 0.4 px 17.5%, 0.4 to 0.8 px 8.1%. A window on a whole-number scale rounds nothing, and those windows make up the first band's largest part.0 to 0.02 px54.3%0.02 to 0.05 px2.4%0.05 to 0.1 px5.0%0.1 to 0.2 px12.7%0.2 to 0.4 px17.5%0.4 to 0.8 px8.1%displacement at the frame's edgemostly below a tenth
Fig. 3 The share of all the windows swept that fall in each band of displacement at the edge. A little over half round nothing or next to nothing, about a quarter sit between a fifth and four-fifths of a pixel, and the remainder lie between.

Of the windows swept, 54% displace the edge by under 0.02 px, a figure dominated by the whole-number display scales, and 8% by more than 0.4 px. Across the fractional scales the typical window is a good fraction of a pixel out and does not improve as it grows.

The bound depends on the shape, and on which axis is held

The width disappeared from the bound, and the shape did not. A square window has a bound of half a pixel at its edge. A 16:9 window has 0.69, a 21:9 window 0.83, and a window with the 32:9 proportions of a very wide monitor reaches 1.14. Those are small numbers, but they do show which arrangement is worst: the wide window, whose height’s rounding is multiplied up by how much longer its width is.

A portrait window goes the other way, as long as the vertical is held and the displacement is taken along the width: a 9:16 window has a bound of about 0.39. That is less impressive than it sounds, because the same difference is still there, and at the top and bottom of the frame relative to a horizontal axis held fixed it would come to more. Holding the vertical is a convention, and the bound moves with the convention. The whole of the displacement is the difference between two scales across a frame, and a renderer can place the zero of that difference anywhere it likes, at the cost of more error at the other edge.

So a fairer statement of the limit is about both rounding errors, each up to a half, each scaled by the share of the frame it acts over. Nothing in the geometry of the projection can make it larger. It is one of those rare results where a defect is worst-cased by simple counting and a sweep of fifteen thousand windows cannot find anything that exceeds the count.

A window being dragged

The rounding also explains a small thing that a careful viewer may have seen and not been able to name.

Resizing a window by one unit at a timeA window 562 units tall dragged wider one unit at a time at 1.25×. 1.25·W lands exactly on a whole device pixel every fourth step, and the displacement at the edge runs through a cycle with that period, between -0.575 and -0.196 px. 1.25·562 = 702.5, which rounds to 703, and that fixed rounding sets the middle of the cycle.-0.600-0.500-0.400-0.300-0.2001e+31.005e+31.01e+31.015e+3the window's width (layout units)displacement at the edge (px)height fixed at 562 units, 1.25×the cycle has a period
Fig. 4 A window whose height is fixed, dragged wider one layout unit at a time at 1.25×. The displacement at the edge runs through a regular cycle, because the width lands exactly on a whole device pixel every fourth unit. The height’s rounding, held fixed, sets the level of the whole cycle.

As the window is dragged wider, the displacement cycles with a period of four layout units at this scale, varying by a good third of a pixel. A static scene being rendered in a resizing window therefore has edge detail shifting back and forth by that amount as it grows, in a repeating pattern unrelated to anything in the scene. It would be masked by the far larger reflow of the content itself, but that is what makes the whole mistake so easy to live with: there has never been a moment when it was the most visible thing on the screen.

Cumulative effects are a different matter. This displacement is a scale across the frame, and so it has no effect at the frame’s centre and its full effect at the edge. Viewed from a tile’s coordinates, it has a part of the picture off by a little over a half pixel in a place where a neighbouring tile may not be. That is where the bookkeeping in a tile is an off-centre frustum becomes exact and not merely tidy. A renderer that sets each tile’s window by rounding its share of the frame and computes each tile’s aspect from its own rounded size is not rendering one picture. Seams between tiles at one of these scales can then show the displacement, which the unbroken frame would hide.

No picture can see it

How large must a displacement be before anything in a single picture could tell? Reading a pixel’s aspect ratio from one picture requires an instrument, and the instrument available is a box with a square face, which reads a pixel’s width from vanishing points, as a square closes the line of cameras develops. So try that instrument, with a window of 1,000 by 562 rounded as badly as it can be.

No picture of a box can see itThe scatter of a pixel's width read back from one picture of a box with a square on one face, with half a pixel of reading error, and the largest difference a 1000-by-562 window's rounding can produce. Even ninety-six long edges read the width only to 3.7 parts in a thousand, against 1.39 for the rounding, so a camera recovery that finds a 2-to-1 anamorphic at once could not tell this window from a square one.6 edges of 160 px13.09‰12 edges of 200 px9.94‰24 edges of 240 px6.64‰48 edges of 300 px4.48‰96 edges of 320 px3.66‰the rounding itself1.39‰parts in a thousandbelow any reading
Fig. 5 The scatter of a pixel’s width read back from one picture of a box with a square on one face, against the largest difference this window’s rounding could produce. It takes ninety-six long edges a direction before the reading approaches the same size as the effect, and it is still over twice as large.

Six edges of 160 px read to half a pixel give a scatter of about 13 parts in a thousand. Ninety-six long edges give 3.7. The window’s rounding is at most 1.39. So this particular fault is below the sharpest test a picture can supply, by more than a factor of two even with a target far better than any real scene. The conclusion is limited to reading it through the picture’s geometry. A calibrated test, one that renders a known circle with hundreds of pixels on its rim and reads it back as a whole, could detect it easily. It is simply that nothing about a normal picture ever has.

There is a useful negative conclusion there. A recovered camera cannot be used to diagnose this. A camera recovered from a rendered image will report one focal length if the method assumes one and a pair if it allows two, and in the second case the reported difference will be noise. Recovering the camera gives no warning, which is correct: the camera it reports reproduces the picture to better than any reading.

Centring has the same arithmetic

Rounding reaches a viewport in one other way. A picture of a fixed shape centred in a larger buffer leaves bars at the sides or above and below, and each bar must be a whole number of pixels.

A centred picture in an odd space is half a pixel outA picture of a stated shape centred in a buffer. Whenever the leftover space is an odd number of pixels the exact centre falls between two whole positions, and the viewport must round it, moving the picture half a pixel off the frame's middle: 1920×1080 at 1.78 leaves 0.00 px, 1920×1200 at 1.78 leaves 0.00 px, 1921×1080 at 1.78 leaves 0.50 px, 1920×1081 at 1.78 leaves 0.50 px, 1366×768 at 1.78 leaves 0.00 px, 1707×960 at 2.39 leaves 0.00 px. It is the same size as the half pixel a sampling convention can produce, and it moves the whole picture rather than stretching it.1920×1080 at 1.780.00 px1920×1200 at 1.780.00 px1921×1080 at 1.780.50 px1920×1081 at 1.780.50 px1366×768 at 1.780.00 px1707×960 at 2.390.00 pxletterbox or pillarboxparity decides
Fig. 6 A picture of a stated shape centred in a buffer, with the distance its exact centre stands from a whole position. An even amount of leftover space divides into two whole bars, and an odd amount does not, so the viewport has to fall half a pixel to one side.

At 1,920 by 1,080, a 16:9 picture fills the buffer, and all is well. At 1,921 by 1,080 one column is left over, half of it on each side, and half a column is not available. The viewport goes half a pixel off centre, and the picture’s principal point is half a pixel from the buffer’s middle. It is exactly the half pixel of the sampling convention, and it arrives through a route nobody thinks of as sampling. Whether it matters depends on who is reading. A frame stored with its bars and read back with the assumption that the picture is centred gets a principal point half a pixel out. A frame read by its viewport does not, since the viewport has already been reported honestly.

The cure is one line, and that line is about units

All of the anisotropy goes away by taking the aspect from the buffer rather than the window:

a=wh.a = \frac{w}{h}.

The two focal lengths are then equal, exactly, on every window, by construction: fx=(w/2)/(at)=h/(2t)=fyf_x = (w/2)/(a t) = h/(2t) = f_y. The field of view moves by a tiny amount, since the vertical half-angle is unchanged and the horizontal one is computed from the whole-number shape instead of the layout shape, and that is correct, since the buffer’s shape is what is on the screen. Over the same windows the ratio comes out at one, to the arithmetic floor.

So why does anybody do otherwise? Because a layout system hands a canvas its size in layout units, and hands it that number first. The aspect is computed where the size is known, and the buffer is sized a line or two later, by another function, perhaps through a device-scale helper and a different rounding rule. There is often no point in the program at which both numbers are in hand, and every test passes whichever is used, because nothing that reads a picture can detect the difference, as shown above. So the fix is very cheap and nothing demands it.

The analogy with the earlier resize is close. There, a multiplication looked correct because it composed with itself. Here, an aspect looks correct because it is what the window claims to be. In both, the true number is available in the pipeline, and the mistake comes from asking the wrong component.

Why it might matter anyway

A displacement of 0.7 px at the frame’s edge is below the threshold of almost anything viewed on its own. Three cases are worth naming where it is not.

Stereo. Two buffers rounded independently, for two eyes, carry their own displacements. If they differ, the difference is a horizontal disparity that varies across the frame, and a horizontal disparity of half a pixel at the edge of a head-mounted display’s panel is a stated depth at that position. In practice, two eyes usually have identical buffers, so the rounding is identical and cancels. The case to worry about is the one where each eye’s buffer has been given its own shape or scale.

Rendered training data. A pipeline that renders images for a model to learn camera poses from will often report the exact intrinsics it asked for. If the aspect came from the window, the intrinsics are off by exactly this much and the labels have a window-dependent bias. It sits at fractions of a pixel, comparable to the targets such models are scored on.

Reprojection and history. A temporal renderer, reprojecting last frame’s pixels to this frame’s positions, can keep picking up a fraction of a pixel when the window has changed, which makes a resize leave a brief smear at the frame’s edges and nowhere else. That is a small price, and one that does not recur once the size settles.

A second scale on the way to the screen is a different fault

It treats a display’s scale as one number per window. A browser can report a fractional scale that was itself rounded, or apply a further scale to the buffer on its way to the screen, and that second scale is a resample with all the consequences of the halved picture and none of the bound here. A mismatched canvas in a web page, whose buffer does not match the on-screen size, is a different and much larger fault of that kind.

The sweep’s windows also come in six shapes and a fixed step of seven layout units. A window between those steps is not tested directly, but the bound does not depend on the steps, since it comes from the rounding limits themselves.

And the displacement is taken at the frame’s edge in the horizontal direction with the vertical held fixed, which is one choice of what to hold still. A renderer that treats both axes alike, or holds the horizontal field, splits the same difference differently. Every such choice moves the error between the two edges without altering its size by more than the factor of the shape.

Still open: what a real display’s own scale does on top

Every number here is a rounding between a layout’s coordinates and a buffer. On a fractional display, a compositor then often scales that buffer to the panel’s real pixels, typically drawing at a whole-number scale and reducing, and that is a resample with a real filter. If it uses the true map, the earlier essay’s arithmetic applies to the composition. If it uses another, there is yet another centre in play.

The measurement that settles what that adds composes three steps — a layout’s rounding into a buffer, a compositor’s scale from buffer to panel, and a readback from panel to a screenshot — and tracks a mark at the frame’s edge through all three, at the common scales. It asks whether the composition can exceed the bound set here or whether the bound survives it, and whether a screenshot of a rendered scene, taken at the panel’s size, still shows the camera that the renderer thought it was using.

An error that falls with the size when written as a proportion, and stays still when written as a distance, is an error about pixels. It should be stated in pixels.

What links here

Computed from the collection, not written here: the essays that point at this one.

Shares its objects with

Essays that name at least two of the same things, and that neither author linked.

Named objects

A flat tag is an object no other essay names yet.

AnisotropyAspect ratioCamera calibrationCamera matrixFocal lengthPrincipal pointQuantisationSampling grid