iOS/M1 does not generate consistent depth from multiple passes

We are using first pass depth. I know it's not recommended, but we have one and need it. Deferred renders uses this, and we do too.

We've tried setting [invariant] on the position, and now are resorting to slope and depth biasing the second pass. We even set -fpreserve-invariance on the compiler. This whole construct is confusing. "invariant" was added in MSL 2.1, but requires iOS 13 to set that compiler flag, and then other code states that flag must be set for iOS 14 and macOS11 SDK use (minSDK? buildSDK?). We also tried disabling -fno-fast-math to no avail.

But why is a simple v = v * m calculation different once polys hit the near plane or the viewport edges. The polys then seem to per-tile z-fight. Some tiles have stripes of z, and some are just completely missing. These are the same tris going through two shaders that do the same vertex calc.

That shouldn't be happening, unless the tiles are computing gradients per tile incorrectly from the one pass to the next. On long clipped tris, it looks like a hardware/driver bug computing consistent depths across the same triangles. This was tested on older (iPhone 6) and newer iOS devices and M1 MBP.

Hello Alecazam, could you clarify where it states that invariant requires iOS 13? Checking the Metal Shading Language specification I see it's minimum iOS 14. A To reiterate the documentation,

Compilers prior to IOS 14.0 and macOS 11.0, the calculation is likely (although not guaranteed) to be invariant. This calculation is now guaranteed to be invariant when passing -fpreserve-invariance option or setting the preserveInvariance on the MTLCompilerOptions from the Metal API for runtime compilation. Note that [[invariant]] is ignored if the options are not passed. This position invariance is essential for techniques such as shadow volumes or a z-prepass.

As for the depth inconsistencies, when you say that the calculation is different, I assume you mean the depths produced from your depth/visibility pre-pass and the g-buffer pass? [[invariant]] with the -fpreserve-invariance flag should work in that case.

I'm wondering this if this is just a matter of some precision being lost when storing the depth, so you could try the most extreme precision MTLPixelFormatDepth32Float, and if that works try MTLPixelFormatDepth24[Unorm|Float]_Stencil8.

If that doesn't work, please file a bug report using the Feedback Assistant, provide a reproducing Xcode project & your device configuration, then post the FB number here so we can take a look at it.

Here's the developer doc built from the sources, that mentions iOS 13.

https://developer.apple.com/documentation/metal/mtlcompileoptions/3564462-preserveinvariance?language=objc

And here's the flag itself.

/*!  @property preserveInvariance  @abstract If YES, set the compiler to compile shaders to preserve invariance. The default is false.  */ @property (readwrite, nonatomic) BOOL preserveInvariance API_AVAILABLE(macos(11.0), macCatalyst(14.0), ios(13.0)); @end

We ran this on new and old hardware, but our app targets minimum iOS 12 since our minspec is a 5S, and we can't move higher since Apple dropped iOS updates for it in 12.

We will also move to reverseZ and infiniteZ, but that's a much bigger code change. This should also improve depth precision, but here it's using the same matrix for both passes, and the depth precision shouldn't matter.

iOS/M1 does not generate consistent depth from multiple passes
 
 
Q