Some years ago @menzel started this thread:

Which describes a problem with OpenGL and Direct3D where you are trying to implement a bilinear filter yourself. As I just hit this exact problem and found Menzel's post very useful I thought I'd add my solution which avoids the unpredictability of the strange offset.

We're trying to pull in 4 texels and then sum them with weights. If one uses gather to load the samples, then one can't exactly predict which samples will be loaded as the HW is doing integer maths and the shader is doing float maths.

So the trick is to force the Gather to pull in the texels you want. This is the modified version of the filter code Menzel put in his thread.

Code :
vec4 textureBilinear( in sampler2D tex, in vec2 coord, in float useOffset )
    // Get texture size in pixels:
    vec2 colorTextureSize = vec2(textureSize(tex, 0));
    // Convert UV coordinates to pixel coordinates and get pixel index of top left pixel (assuming UVs are relative to top left corner of texture)
    vec2 pixCoord = coord * colorTextureSize - 0.5f;    // First pixel goes from -0.5 to +0.4999 (0.0 is center) last pixel goes from (size - 1.5) to (size - 0.5000001)
    vec2 originPixCoord = floor(pixCoord);              // Pixel index coordinates of bottom left pixel of set of 4 we will be blending
    // For Gather we want UV coordinates of bottom right corner of top left pixel
    vec2 gatherUV = ((originPixCoord + 1.0f) / colorTextureSize;
    // Gather from all surounding texels:
    vec4 red   = textureGather(tex, gatherUV, 0);
    vec4 green = textureGather(tex, gatherUV, 1);
    vec4 blue  = textureGather(tex, gatherUV, 2);
    vec4 alpha = textureGather(tex, gatherUV, 3);
    // Swizzle the gathered components to create four colours
    vec4 c00 = vec4(red.w, green.w, blue.w, alpha.w);
    vec4 c01 = vec4(red.x, green.x, blue.x, alpha.x);
    vec4 c11 = vec4(red.y, green.y, blue.y, alpha.y);
    vec4 c10 = vec4(red.z, green.z, blue.z, alpha.z);
    // Filter weight is fract(coord * colorTextureSize - 0.5f) = (coord * colorTextureSize - 0.5f) - floor(coord * colorTextureSize - 0.5f)
    vec2 filterWeight = pixCoord - originPixCoord;
    // Bi-linear mixing:
    vec4 temp0 = mix(c01, c11, filterWeight.x);
    vec4 temp1 = mix(c00, c10, filterWeight.x);
    return mix(temp1, temp0, filterWeight.y);

It's worth noting that we can avoid the Swizzle of the gathered components by using Sample, in which case we need to sample half a pixel different because we want the center of the specific texel. We should have point sampling enabled, but if bilinear is enabled it will still work.

Code :
    // For Sample we want UV coordinates of center of top left pixel
    vec2 sampleUV = ((originPixCoord + 0.5f) / colorTextureSize;
    // Sample from all surounding texels
    vec4 c00 = texture(tex, sampleUV);
    vec4 c01 = textureOffset(tex, sampleUV, vec2(0,1));
    vec4 c11 = textureOffset(tex, sampleUV, vec2(1,1));
    vec4 c10 = textureOffset(tex, sampleUV, vec2(1,0));