Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test of manual mipmap chain generation in WebGL 2.0 #3614

Open
kenrussell opened this issue Jan 26, 2024 · 12 comments
Open

Add test of manual mipmap chain generation in WebGL 2.0 #3614

kenrussell opened this issue Jan 26, 2024 · 12 comments

Comments

@kenrussell
Copy link
Member

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.

@greggman
Copy link
Contributor

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.

@kenrussell
Copy link
Member Author

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.

@greggman
Copy link
Contributor

greggman commented Jan 26, 2024

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.

@greggman
Copy link
Contributor

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

@greggman
Copy link
Contributor

greggman commented Jan 27, 2024

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

@greggman
Copy link
Contributor

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);
}

@greggman
Copy link
Contributor

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

@lexaknyazev
Copy link
Member

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.

@greggman
Copy link
Contributor

greggman commented Jan 29, 2024

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?

@lexaknyazev
Copy link
Member

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.

@greggman
Copy link
Contributor

Got it, thanks!

@kdashg
Copy link
Contributor

kdashg commented Jan 30, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants