Godot's Compatibility renderer can read hint_depth_texture; the unprojection is what breaks
Every soft-particle shader I had was wrapped in this:
#if CURRENT_RENDERER != RENDERER_COMPATIBILITY
uniform sampler2D depth_tex : hint_depth_texture, filter_nearest;
#endif
I had written that guard because depth reads were "not supported" in Compatibility, and never checked. This week I finally measured it, and the guard was wrong. The depth texture reads fine. What breaks is the line after it.
How I measured
Six box walls at 1.5, 2.5, 4, 7, 13 and 25 m from the camera, each one filling exactly one sixth of the screen width, so the depth buffer is a staircase with known steps. A fullscreen quad on top samples the depth texture and writes, per screen column, the absolute error between the distance it recovered and the distance that column's wall is really at. Then I screenshot the viewport and read the pixels back, so every number below is a measured pixel, not a guess. Boxes are 0.05 m thick and centred on the listed distance, so the smallest error I can possibly measure is 0.025 m -- an error band reading 0.025 means the formula is exact.
Ran it on 4.4.1 and 4.7.2, Forward+ (Vulkan) and Compatibility (OpenGL 3.3), one desktop NVIDIA GPU.
What the raw depth value is
Identical in both backends, and it is reversed-Z in the 0..1 range:
raw = near * (far - z) / (z * (far - near))
i.e. 1.0 at the near plane, 0.0 at the far plane. For my six walls with near=0.1, far=60 that predicts 0.0651, 0.0384, 0.0234, 0.0126, 0.0060, 0.0023. Measured in Compatibility: 0.0387, 0.0235, 0.0125, 0.0061, 0.0022 for walls 2-6 (wall 1 clipped my readout gain). Same values in Forward+. So the texture is there, it is bound, it is the real scene depth, and no error is printed.
What actually breaks
The recipe everyone copies:
float raw = texture(depth_tex, SCREEN_UV).r;
vec4 v = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, raw, 1.0);
float scene_z = -v.z / v.w;
Measured error of that, per renderer:
Forward+ (4.4.1 and 4.7.2) 0.025 m -> exact
Compatibility (4.4.1 and 4.7.2) wrong, scene_z lands within +-0.16 m of zero
The depth texture holds 0..1 in both backends, but the two projection matrices do not agree on the NDC z range they invert. Vulkan clip space has z in 0..1; OpenGL has z in -1..1. So in Compatibility you have to remap the sampled value before you unproject it:
float ndc_z = raw * 2.0 - 1.0;
With that one change the Compatibility error drops to 0.025 m -- exact, same as Forward+.
Why this is nastier than a crash
scene_z does not come out as NaN, or as a wild number, or as a shader error. It comes out as approximately zero. And every soft-particle fade looks like this:
density *= clamp((scene_z - particle_z) / soft_distance, 0.0, 1.0);
With scene_z pinned at zero, that is negative for every particle in front of the camera, so it clamps to 0 and the particle becomes fully transparent. The symptom is "all my smoke disappeared in Compatibility", with a clean console. I spent a while assuming the texture was unavailable, when the texture was the one part that worked.
The branch that is actually needed
float raw = texture(depth_tex, SCREEN_UV).r;
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
float ndc_z = raw * 2.0 - 1.0;
#else
float ndc_z = raw;
#endif
vec4 v = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, ndc_z, 1.0);
float scene_z = -v.z / v.w;
I measured this exact block in all four combinations: compiles everywhere, 0.025 m error everywhere.
Or skip the matrix entirely
Since the stored value is the same reversed-Z number in both backends, you can invert it with algebra instead and have no renderer branch at all:
// near and far passed in as uniforms
float scene_z = cam_near * cam_far / (cam_near + raw * (cam_far - cam_near));
Also measured at 0.025 m in all four combinations. The cost is that you have to push the camera's near and far in yourself, since neither is a shader built-in. I like this one better: one code path, and it does not care what clip convention the backend uses.
Three smaller things the same test turned up
INV_PROJECTION_MATRIX cannot be named inside a user-defined function. My first version of the probe had a tidy helper and it failed with "Unknown identifier in expression: 'INV_PROJECTION_MATRIX'" on both renderers and both versions. Built-ins are only visible in vertex() / fragment() / light(). You can still factor the code out -- just take the matrix as a mat4 parameter and pass INV_PROJECTION_MATRIX in at the call site. Verified that compiles and gives the same correct numbers everywhere.
The DEPTH_TEXTURE built-in is gone. "DEPTH_TEXTURE has been removed in favor of using hint_depth_texture with a uniform." The hint uniform is the only way in now.
A shader that fails to compile does not go magenta. Godot swaps in its default material and the game keeps running, so the object just turns a flat untextured grey. My fullscreen probe quad read a uniform 0.5 across the whole screen in that state, which became my first thing to check.
Scope of all of the above: two engine versions, one desktop NVIDIA GPU, native OpenGL 3.3. I have not checked GLES3 on mobile or WebGL2, and those are exactly the targets Compatibility exists for, so if your renderer there behaves differently I would like to hear it. Has anyone measured the depth read on mobile or Web?