libretro / RetroArch

Cross-platform, sophisticated frontend for the libretro API. Licensed GPLv3.
http://www.libretro.com
GNU General Public License v3.0
9.77k stars 1.77k forks source link

textureLod() and textureGrad() do not perform as expected across all graphics drivers. #16567

Open fishcu opened 1 month ago

fishcu commented 1 month ago

Description

When using textureLod or textureGrad() texture sampling with custom screen-space derivatives, the result appears to use nearest-neighbor (NN) mip-map sampling. This results in harsh transitions between mip-map levels.

Here's a screenshot showing the actual behavior for textureGrad(). This screenshot uses the vulkan graphics driver. image

This was tested on different graphics drivers with Slang shaders. The behavior is as follows:

Here's a screenshot that uses the d3d12 driver: image

Expected behavior

I would expect that textureGrad() uses proper tri-linear interpolation for smooth anti-aliasing / anisotropy behavior.

Here's a simulation of the expected behavior using two texture taps and manual interpolation in the shader code: image

Actual behavior

See description.

Steps to reproduce the bug

Here's a minimal fragment shader that shows the issue with textureLod():

void main() {
    vec3 res =
        textureLod(Source, vTexCoord, vTexCoord.x * 8.0).rgb;
    FragColor = vec4(res, 1.0);
}

Here's a minimal fragment shader that shows the issue with textureGrad():

void main() {
    vec3 res =
        textureGrad(Source, vTexCoord,
                    vec2(log2(1.0 - vTexCoord.x) / 32.0, 0.0), dFdy(vTexCoord))
            .rgb;
    FragColor = vec4(res, 1.0);
}

Make sure to use these minimal examples with a preset that computes mip-map levels properly. Currently, this necessitates a primary "stock" pass to produce the mip-mapping in the first place. Example:

shaders = 2

shader0 = stock.slang
filter_linear0 = false
scale_type0 = source

shader1 = tri_linear_bug.slang
filter_linear1 = true
scale_type1 = viewport
mipmap_input1 = true

The code for the manual interpolation using 2 taps is as follows:

void main() {
    float lod = vTexCoord.x * 8.0;
    vec3 res_1 = textureLod(Source, vTexCoord, floor(lod)).rgb;
    vec3 res_2 = textureLod(Source, vTexCoord, floor(lod) + 1.0).rgb;
    FragColor = vec4(mix(res_1, res_2, fract(lod)), 1.0);
}

Bisect Results

Did not do a bisect.

Version/Commit

RA version 1.18.0 Commit hash 72a10eda9a

Environment information

kokoko3k commented 1 month ago

I can at least confirm that lods interpolation is only working with external texture sampling.

fishcu commented 1 month ago

I have found that even simple texture() calls will attempt to use mip-maps if present.

However, they also show the nearest neighbor mip-map level behavior that I have demonstrated above.

It would be better if shader passes as textures behaved the same way as sampling from other "external" textures.

fishcu commented 1 month ago

Here's one more example with the accompanying code. A strong deformation is appended to a scanline shader to produce strong aliasing during minimization sampling.

Here's the code for all three images:

// 1. Bilinear interpolation (worst)
// FragColor = vec4(textureLod(Source, uv, 0).rgb, 1.0);
// 2. Bilinear with nearest neighbor mip-mapping (less aliasing, but visible seams)
// FragColor = vec4(texture(Source, uv).rgb, 1.0);
// 3. Anisotropic trilinear filtering (best)
const vec2 d_uv_dx = dFdx(uv) * param.SourceSize.xy;
const vec2 d_uv_dy = dFdy(uv) * param.SourceSize.xy;
const float lambda_base =
    max(0.0, 0.5 * log2(max(dot(d_uv_dx, d_uv_dx), dot(d_uv_dy, d_uv_dy))));
float lambda_i;
const float lambda_f = modf(lambda_base, lambda_i);
const vec3 rgb = mix(textureLod(Source, uv, lambda_i).rgb,
                        textureLod(Source, uv, lambda_i + 1.0).rgb, lambda_f);
FragColor = vec4(rgb, 1.0);

Here are the three images in order: image image image