I E

Sunstone dev log 19: less overdraw

I managed to improve overdraw somewhat:

Before
After

This took my fragment shader invocation count from 18,360,824 to 10,568,652 (count pixel shader invocations in RenderDoc, save as CSV, then sum the column using Sqlite). However, it had no effect on my framerate. So the renderer wasn’t actually fragment-bound.

In a previous entry I mentioned that occlusion culling might be the solution to my fragment overdraw issues. While researching occlusion culling, I came across the idea of a depth prepass. Which got me thinking that something depth-related would be a better solution than occlusion culling. Even though it hasn’t fixed my performance issues, I think the overdraw solution is still worth having. These are my notes while working it all out.

Figuring out how to fix fragment overdraw

I’d prefer to use the early fragment test to skip occluded fragments. I don’t think that works for the current system, though. Even if I set up a depth prepass (so that early Z could be enabled for the colour pass), my depth buffer contains logarithmic values so I’d have to pass those as the z component of gl_Position and they’d be interpolated incorrectly.

If I can’t skip the fragment shader completely, then I have to rely on reducing the work it does. I could try re-implementing an early depth test in the fragment shader by sampling the depth texture (written from the depth pre-pass) and discarding when it fails the depth test. I don’t know what kind of speedup to expect from that, but it seems easy enough to build and find out. A brief web search suggests that texture sampling is relatively expensive, so maybe skipping 3 texture samples per fragment (base colour, metallic-roughness, ambient specular lookup) will be enough of a win?

12… hours… later…

Well, I tried to build it and it wasn’t as easy as I expected. The main thing I got hung up on was how to correctly sample the depth texture. It’s a multisampled texture when MSAA is on, so in addition to texture coordinates it takes a sample index. I don’t know how to map a sub-pixel fragment to the correct sample index.

I decided to replace explicit logarithmic depth with “reverse Z”. Reverse Z interpolates correctly for fragments, and the early fragment test works (even without a depth pre-pass) because gl_FragDepth is no longer written.

Why does reverse Z work?

This is a similar analysis to Sunstone dev log 16 - Depth buffer resolution analysis, but now the storage format is 32-bit floating point, which has variable resolution.

The layout for IEEE754 32-bit floats is:

Bits Content
31 sign
30-23 exponent
22-0 fraction

exponent = 0x0 is reserved for 0 and “subnormal” sumbers, and exponent = 0xFF is reserved for NaN/Infinity. That leaves 254 possible values for the exponent. exponent = 0x1 represents -126 and exponent = 0xFE represents 127.

Here’s an enumeration of some floating point values, starting at the smallest positive “normal” number:

Bits Value
0b0_00000001_00000000000000000000000 0b1.00000000000000000000000×2−126≈1.1754944×10−380b1.00000000000000000000000 \times 2^{-126} \approx 1.1754944 \times 10^{-38}
0b0_00000001_00000000000000000000001 0b1.00000000000000000000001×2−126≈1.1754945×10−380b1.00000000000000000000001 \times 2^{-126} \approx 1.1754945 \times 10^{-38}
0b0_00000001_00000000000000000000010 0b1.00000000000000000000010×2−126≈1.1754946×10−380b1.00000000000000000000010 \times 2^{-126} \approx 1.1754946 \times 10^{-38}
… …
0b0_00000001_11111111111111111111111 0b1.11111111111111111111111×2−126≈2.3509886×10−380b1.11111111111111111111111 \times 2^{-126} \approx 2.3509886 \times 10^{-38}
0b0_00000010_00000000000000000000000 0b1.00000000000000000000000×2−125≈2.3509887×10−380b1.00000000000000000000000 \times 2^{-125} \approx 2.3509887 \times 10^{-38}
… …

So the floating point numbers with exponent -126 have an even spacing of 0b0.00000000000000000000001×2−126=2−23×2−126≈1.4×10−450b0.00000000000000000000001 \times 2^{-126} = 2^{-23} \times 2^{-126} \approx 1.4 \times 10^{-45}. Floating point numbers with exponent -125 are spaced by 0b0.00000000000000000000001×2−125=2−23×2−125≈2.8×10−450b0.00000000000000000000001 \times 2^{-125} = 2^{-23} \times 2^{-125} \approx 2.8 \times 10^{-45}, which is precisely twice the spacing of the previous set. Then when the exponent increases by 1, the denoted number doubles and the spacing doubles.

This is essentially the relationship given by logarithmic depth (previous analysis), but the graph of Δz\Delta z for zz is a staircase instead of a straight line. For the first zz of each step, the ratio of Δz\Delta z to zz is 2−23≈1.2×10−72^{-23} \approx 1.2 \times 10^{-7}, so I’ll use that first value as a proxy for the rest of the zz values in the step and say that 32-bit floating point can discriminate a change in depth of around depth(z)×2−23\text{depth}(z) \times 2^{-23} at depth of depth(z)\text{depth}(z).

When I analysed logarithmic depth, I gave the equation

depth(z)=ff−n−1znff−n \text{depth}(z) = \frac{f}{f - n} - \frac{1}{z} \frac{nf}{f - n}

for fragment depth after perspective division. For reverse Z to work, the near and far planes have to be swapped, giving depth(n)=1\text{depth}(n) = 1 and depth(f)=0\text{depth}(f) = 0.

depth(z)=nn−f−1zfnn−f \text{depth}(z) = \frac{n}{n - f} - \frac{1}{z} \frac{fn}{n - f}

This gives

depth′(z)=1z2fnn−f \text{depth}'(z) = \frac{1}{z^2} \frac{fn}{n-f}

Again from last time, we had

Δz≈Δddepth′(z) \Delta z \approx \frac{\Delta d}{\text{depth}'(z)}

For this analysis it needs to be

Δz≈−Δddepth′(z) \Delta z \approx -\frac{\Delta d}{\text{depth}'(z)}

because depth′(z)\text{depth}'(z) is negative, due to depth\text{depth} being monotonically decreasing from 1 to 0.

If we take Δd≈2−23⋅depth(z)\Delta d \approx 2^{-23} \cdot \text{depth}(z):

Δz≈−depth(z)z2(n−f)223fn≈−(nn−f−1zfnn−f)z2(n−f)223fn≈−(z2nn−f−z21zfnn−f)(n−f)223fn≈−(z2nn−f−zfnn−f)(n−f)223fn≈−z2n−zfn223fn≈−z2−zf223f \begin{aligned} \Delta z & \approx -\frac{\text{depth}(z) z^2 (n-f)}{2^{23}fn} \\ & \approx -\frac{(\frac{n}{n - f} - \frac{1}{z} \frac{fn}{n - f}) z^2 (n-f)}{2^{23}fn} \\ & \approx -\frac{(z^2 \frac{n}{n - f} - z^2 \frac{1}{z} \frac{fn}{n - f}) (n-f)}{2^{23}fn} \\ & \approx -\frac{(z^2 \frac{n}{n - f} - z \frac{fn}{n - f}) (n-f)}{2^{23}fn} \\ & \approx -\frac{z^2 n - z fn}{2^{23}fn} \\ & \approx -\frac{z^2 - zf}{2^{23}f} \end{aligned}

then we get the following resolutions (f=1000f = 1000):

z Δz
0.001 1.1920917e-10
0.01 1.192081e-9
0.1 1.1919736e-8
1 1.1909008e-7
10 1.1801719e-6
100 1.0728836e-5
1000 N/A (resolution goes to 0 at ff)

The looks a lot like the linear relationship between zz and Δz\Delta z given by logarithmic depth. I was surprised when I first saw this, because the function for Δz\Delta z is still quadratic in zz. It works because it’s a negative quadratic function with a root at f=1000f = 1000:

The resolution gets worse, but not increasingly worse. And however bad it gets, it needs to come back down as we approach the root at ff.

What happens if we forget to swap the near and far planes? Using standard depth\text{depth} and depth′\text{depth}':

Δz≈depth(z)z2(f−n)223nf≈(ff−n−1znff−n)z2(f−n)223nf≈(z2ff−n−z21znff−n)(f−n)223nf≈(z2ff−n−znff−n)(f−n)223nf≈z2f−znf223nf≈z2−zn223n \begin{aligned} \Delta z & \approx \frac{\text{depth}(z) z^2 (f-n)}{2^{23}nf} \\ & \approx \frac{(\frac{f}{f - n} - \frac{1}{z} \frac{nf}{f - n}) z^2 (f-n)}{2^{23}nf} \\ & \approx \frac{(z^2 \frac{f}{f - n} - z^2 \frac{1}{z} \frac{nf}{f - n}) (f-n)}{2^{23}nf} \\ & \approx \frac{(z^2 \frac{f}{f - n} - z \frac{nf}{f - n}) (f-n)}{2^{23}nf} \\ & \approx \frac{z^2 f - z nf}{2^{23}nf} \\ & \approx \frac{z^2 - zn}{2^{23}n} \end{aligned}

We get a positive quadratic with a root at nn:

The resolution gets increasingly worse and there’s no upper bound on how bad it gets.

What I built

Next steps

References

← Sunstone dev log 18: overdraw Newest