KhronosGroup/WebGL

Add test of manual mipmap chain generation in WebGL 2.0

Opened this issue · 12 comments

After the updates from https://anglebug.com/4690 , ANGLE guarantees support for allocating a texture in WebGL 2.0 via TexStorage2D and manually generating mipmaps by iteratively rendering to level N+1 while sampling at level N. (Specifically, that this is not considered a rendering feedback loop.)

A test that this works has been added to ANGLE's test suite, but it should be ported to JavaScript and added to the WebGL 2.0 conformance test suite to guarantee that this functionality works across browsers.

Is there something unique to TexStorage2D here? It seems like manually creating mip levels with texImage2D and then trying to render from one mip to another should also work without being considered a feedback loop.

Not really. It's only known that texStorage2D works for this purpose. If the new test verifies that textures allocated with texImage2D work too, that's great. But if that part of the test fails, it will require more adjustments to ANGLE's validation.

I thought this

gl.texStorage2D(gl.TEXTURE_2D, 2, gl.RGBA8, 2, 2);

and this

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texImage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

are equivalent except for the fact that first one is immutable.

I have my own test here

https://jsgist.org/?src=137493160103a6bc85bff8d47eb50045

It's definitely failing using texImage2D but the error seems wrong, even if it's not supposed to work

GL_INVALID_FRAMEBUFFER_OPERATION: Framebuffer is incomplete: Attachment level is not in the [base level, max level] range

Oh, I had a wrong setting, they both work. Arguably neither should have worked before though and the error message still seems wrong.

For details, here's the current code

Instead of

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LOD, 0);
  ...
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LOD, 100);

I had

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, 0);
  ...
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, 100);

It's not clear to me which one should work or if both should work but, with it using MAX_LEVEL, texStorage2D worked at texImage2D did not which is arguably a bug. They should either both work or both fail

//import 'https://greggman.github.io/webgl-lint/webgl-lint.js';
import * as twgl from 'https://twgljs.org/dist/5.x/twgl-full.module.js';

function main(useTexStorage) {
  log(useTexStorage ? 'texStorage2D' : 'texImage2D');
  const gl = document.createElement('canvas').getContext('webgl2');
  if (!gl) {
    return alert("need WebGL2");
  }
  document.body.appendChild(gl.canvas);

  const vs = `#version 300 es
  void main() {
    gl_PointSize = 300.0;
    gl_Position = vec4(0, 0, 0, 1);
  }
  `;

  const tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  // make a 2x2 texture with 2 mip levels
  if (useTexStorage) {
    gl.texStorage2D(gl.TEXTURE_2D, 2, gl.RGBA8, 2, 2);
  } else {
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
    gl.texImage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
  }

  // fill mip level 0 with red
  gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 2, 2, gl.RGBA, gl.UNSIGNED_BYTE,
    new Uint8Array([
      255, 0, 0, 255,
      255, 0, 0, 255,
      255, 0, 0, 255,
      255, 0, 0, 255,
    ]));
  // fill mip level 1 with yellow
  gl.texSubImage2D(gl.TEXTURE_2D, 1, 0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE,
    new Uint8Array([
      255, 255, 0, 255,
    ]));

  // bind mip level 1 to a framebuffer
  const fb = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 1);
  console.log(twgl.glEnumToString(gl, gl.checkFramebufferStatus(gl.FRAMEBUFFER)));
  checkError(gl);

  // render mip level 0 to mip level 1, swapping red and blue 
  const fs = `#version 300 es
  precision mediump float;

  uniform highp sampler2D tex;
  out vec4 fragColor;

  void main() {
    fragColor = texture(tex, vec2(0)).bgra;
  } 
  `;

  const program = twgl.createProgram(gl, [vs, fs]);

  gl.viewport(0, 0, 1, 1);

  // set to use only first mip level (so no feedback loop).
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LOD, 0);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  // draw a single point
  gl.useProgram(program);
  gl.drawArrays(gl.POINTS, 0, 1);
  checkError(gl);

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LOD, 100);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST);

  // render the texture showing both mips.
  const fs2 = `#version 300 es
  precision mediump float;
  uniform sampler2D tex;
  out vec4 outColor;
  void main() {
    outColor = texture(tex, gl_PointCoord.xy, mod(floor(gl_FragCoord.x / 16.0), 2.0) * 1000.0);
  }
  `;
  const prg2 = twgl.createProgram(gl, [vs, fs2]);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  gl.useProgram(prg2);
  gl.drawArrays(gl.POINTS, 0, 1);
  checkError(gl);
}
main(true);
main(false);

function checkError(gl) {
  const err = gl.getError();
  if (err) {
    console.error(twgl.glEnumToString(gl, err));
  }
}


function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = args.join(' ');
  document.body.appendChild(elem);
}

Just for my own curiosity I checked in OpenGL on mac. Either MAX_LEVEL and MAX_LOD work there (of course GL doesn't check for feedback IIRC).

https://github.com/greggman/macos-opengl-experiments/blob/main/macos-opengl-mip-to-mip-2d/examples/example_apple_opengl2/main.mm

Immutable and non-immutable textures are treated differently regarding framebuffer attachment completeness (OpenGL ES 3.0, Section 4.4.4.1):

The framebuffer attachment point attachment is said to be framebuffer attachment complete if the value of FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE for attachment is NONE (i.e., no image is attached), or if all of the following conditions are true:

  • ...
  • If the value of FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is TEXTURE and the value of FRAMEBUFFER_ATTACHMENT_OBJECT_NAME does not name an immutable-format texture, then the value of FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL must be in the range [levelbase,q], where levelbase is the value of TEXTURE_BASE_LEVEL and q is the effective maximum texture level defined in the Mipmapping discussion of section 3.8.10.4.
  • If the value of FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is TEXTURE and the value of FRAMEBUFFER_ATTACHMENT_OBJECT_NAME does not name an immutable-format texture and the value of FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL is not levelbase, then the texture must be mipmap complete, and if FRAMEBUFFER_ATTACHMENT_OBJECT_NAME names a cubemap texture, the texture must also be cube complete.

I'm confused.

The text about is only about non-immutable formats, where as texStorage2D is about immuatable-formats

AFAICT, the point of the text above is effectively about the fact that non-immutable formats can have non-homogenious levels. Each level can be a different internal format and can have any random size. Vs immutable-formats where this is never true. With immutable formats all levels are already guaranteed to be the same format and all levels are guaranteed to have the correct size for their level.

All it's basically saying is that the range of mips being used has to follow the normal rules (they must all be the same format and all the correct size for a mip-chain within the currently defined range. TEXTURE_BASE_LEVEL and TEXTURE_MAX_LEVEL defines the current range).

So, validation is the same after that. Effectively, check that the range of mips from TEXTURE_BASE_LEVEL to TEXTURE_MAX_LEVEL follows the same rules as an immutable format texture. Otherwise, everything else is the same.

Am I missing something?

It is valid to create an immutable texture and then use a level outside of the base-max range as a framebuffer attachment. It is not valid to do the same with non-immutable textures.

Got it, thanks!

I believe #3221 was my attempt to exhaustively test this kind of thing, but it would be great to add anything I missed!