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

glCompressedTexImage2D error #19300

Closed
konstantinegorov opened this issue May 5, 2023 · 31 comments · Fixed by #22453
Closed

glCompressedTexImage2D error #19300

konstantinegorov opened this issue May 5, 2023 · 31 comments · Fixed by #22453

Comments

@konstantinegorov
Copy link

Hi!
I got the error "Uncaught TypeError TypeError: Failed to execute 'compressedTexImage2D' on 'WebGLRenderingContext': parameter 7 is not of type 'ArrayBufferView'", when I called glCompressedTexImage2D and passed null to the "data" parameter. How to avoid it? Is there any workaround?

@konstantinegorov
Copy link
Author

Is there any update?

@sbc100
Copy link
Collaborator

sbc100 commented Aug 17, 2023

Its hard to say what this might be without more details. Can you share a full backtrace? And the line of code at the call site? If possible a complete program would be even better.

There are several tests in the test directory can use glCompressedTexImage2D. It might be useful to take a look at test/browser/test_anisotropic.c, for example, to see if there is anything that you are doing differently.

@matusfedorko
Copy link

We started experiencing this problem too after upgrading from emscripten version 3.1.39 to version 3.1.64. Here is a minimal repro based on the anisotropic test:

#include "SDL/SDL.h"
#include "SDL/SDL_image.h"
#include "SDL/SDL_opengl.h"

#include <stdio.h>

int main(int argc, char *argv[])
{
    // Slightly different SDL initialization
    if ( SDL_Init(SDL_INIT_VIDEO) != 0 ) {
        printf("Unable to initialize SDL: %s\n", SDL_GetError());
        return 1;
    }

    SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );

    SDL_Surface *screen = SDL_SetVideoMode( 600, 600, 16, SDL_OPENGL );
    if ( !screen ) {
        printf("Unable to set video mode: %s\n", SDL_GetError());
        return 1;
    }

    // Clear the screen before drawing
    glClear( GL_COLOR_BUFFER_BIT );

    glViewport( 0, 0, 600, 600 );

    GLuint texture;
    glGenTextures( 1, &texture );
    glBindTexture( GL_TEXTURE_2D, texture );
    glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, 4, 4, 0, 8, NULL);

    SDL_GL_SwapBuffers();

#ifndef __EMSCRIPTEN__
    // Wait for 3 seconds to give us a chance to see the image
    SDL_Delay(2000);
#endif

    // Now we can delete the OpenGL texture and close down SDL
    glDeleteTextures( 1, &texture );

    SDL_Quit();

    return 0;
}

Compile like this:

emcc test_anisotropic.c -g -s ALLOW_MEMORY_GROWTH=1 -s INITIAL_MEMORY=2000MB -s MAXIMUM_MEMORY=4000MB -s USE_WEBGL2=1 -s MIN_WEBGL_VERSION=1 -s MAX_WEBGL_VERSION=2 -o main.html

The output is:

main.js:3810 Uncaught TypeError: Failed to execute 'compressedTexImage2D' on 'WebGL2RenderingContext': parameter 7 is not of type 'ArrayBufferView'.
    at _glCompressedTexImage2D (main.js:3810:9)
    at __main_argc_argv (test_anisotropic.c:31)
    at main.js:634:12
    at callMain (main.js:3973:15)
    at doRun (main.js:4011:23)
    at main.js:4020:7

The two necessary conditions to reproduce the issue are to use glCompressedTexImage2D with NULL pointer for the last argument and to compile with memory limit greater than 2GB.

This is how glCompressedTexImage2D implementation looks in 3.1.39:

  glCompressedTexImage2D: function(target, level, internalFormat, width, height, border, imageSize, data) {
#if MAX_WEBGL_VERSION >= 2
    if ({{{ isCurrentContextWebGL2() }}}) { // WebGL 2 provides new garbage-free entry points to call to WebGL. Use those always when possible.
      if (GLctx.currentPixelUnpackBufferBinding || !imageSize) {
        GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, imageSize, data);
      } else {
        GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, HEAPU8, data, imageSize);
      }
      return;
    }
#endif
    GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, data ? {{{ makeHEAPView('U8', 'data', 'data+imageSize') }}} : null);
  },

And this is how it looks in 3.1.64:

  glCompressedTexImage2D: (target, level, internalFormat, width, height, border, imageSize, data) => {
#if MAX_WEBGL_VERSION >= 2
    if ({{{ isCurrentContextWebGL2() }}}) {
      if (GLctx.currentPixelUnpackBufferBinding || !imageSize) {
        GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, imageSize, data);
        return;
      }
#if WEBGL_USE_GARBAGE_FREE_APIS
      GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, HEAPU8, data, imageSize);
      return;
#endif
    }
#endif
    GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, data ? {{{ makeHEAPView('U8', 'data', 'data+imageSize') }}} : null);
  },

The different part is WEBGL_USE_GARBAGE_FREE_APIS which is false with higher memory limit. I don't know enough about emscripten to fix this properly, but hopefully now the issue will get addressed.

@kripken
Copy link
Member

kripken commented Aug 16, 2024

Thanks for the testcase @matusfedorko , I bisected this to "Avoid garbage-free WebGL APIs when memory size is over 2gb." (#21445)

@sbc100 PTAL

@sbc100
Copy link
Collaborator

sbc100 commented Aug 19, 2024

What is the glCompressedTexImage2D supposed to be doing here: glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, 4, 4, 0, 8, NULL);?

i.e. what does it mean to set the pixels argument to NULL?

It seems like before #21445 we would end up calling GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, HEAPU8, data, imageSize); which I think would read imageSize bytes from address zero (data is zero/null) of the main memoy.. which seem wrong.

I'm not sure how this is supposed to work I guess. @juj might know? Also @kainino0x ?

@sbc100
Copy link
Collaborator

sbc100 commented Aug 19, 2024

According the MDN docs it should be possible to call compressedTexImage2D with only 6 arguments. But according the errors I'm seeing in chrome it is requiring the a 7th argument.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 19, 2024

Actually the 6 argument version fails here with webgl1 too: TypeError: Failed to execute 'compressedTexImage2D' on 'WebGLRenderingContext': 7 arguments required, but only 6 present.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 19, 2024

So this code also fails without memory growth on WebGL1: TypeError: Failed to execute 'compressedTexImage2D' on 'WebGLRenderingContext': parameter 7 is not of type 'ArrayBufferView'.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 22, 2024

Doing a little research it sounds like passing null for pixels is generally don't to reserve space for following calls to glCompressedTexSubImage2D? Is that why you are passing null in this case?

From the docs it looks like maybe glTexImage2D supports this null but glCompressedTexSubImage2D does not?

See: https://community.khronos.org/t/glcompressedteximage2d-and-null-data/41505
And: https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml
And: https://registry.khronos.org/OpenGL-Refpages/gl4/html/glCompressedTexImage2D.xhtml

Node that glTexImage2D mentions "data may be a null pointer. In this case, texture memory is allocated to accommodate a texture of width width and height height." but glCompressedTexImage2D docs do no say this.

ping @kainino0x @juj

@matusfedorko
Copy link

Yes, we pass NULL to allocate space for the texture. I tried that suggestion with glTexImage2D, but it does not work. In desktop GL (NVidia) the output is all black and in Chrome I get some error (I don't remember what it was exactly, but if you want you can try to modify the repro sample). I also noticed that reference docs do not explicitly mention NULL, but docs are not specification and it seems only logical to me that glCompressedTexImage2D/glCompressedTexSubImage2D would behave analogously to glTexImage2D/glTexSubImage2D.

@matusfedorko
Copy link

Actually. in the OpenGL 4.6 specification (https://registry.khronos.org/OpenGL/specs/gl/glspec46.core.pdf) on page 230 I found this:

If the data argument of CompressedTexImage1D, CompressedTexImage2D, or CompressedTexImage3D is NULL, and the pixel unpack buffer object is zero, a texture image with unspecified image contents is created, just as when a NULL pointer is passed to TexImage1D, TexImage2D, or TexImage3D.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 22, 2024

Sadly i looks like I can't omit the last argument or set it to null using WebGL2's compressedTexImage2D. See #19300 (comment).

I wonder if this is bug since https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/compressedTexImage2D mentions a six argument version in WebGL1 and says that that WebGL API adds overloads.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 22, 2024

Perhaps we can map compressedTexImage2D to texImage2D internally in the case the pixels is NULL. According the webgl spec pixels cannot be null for compressedTexImage2D, only for texImage2D, although that might be an oversight in the spec?

https://registry.khronos.org/webgl/specs/latest/1.0/#COMPRESSEDTEXIMAGE2D

texImage2D in the same doc says "If pixels is null, a buffer of sufficient size initialized to 0 is passed." and indeed has a signature where the final argument is optional unlike compressedTexImage2D.

@kenrussell maybe you can help here?

@matusfedorko
Copy link

And how about the first overload compressedTexImage2D(target, level, internalformat, width, height, border) ? I don't see any way to pass a pointer to data to it, so I assume it must be intended for the texture allocation use case. Otherwise it would be useless. Alternatively the GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, HEAPU8, data, imageSize); version seems to work well as a workaround.

@matusfedorko
Copy link

Well, so I did some tests. That overload did not work (maybe because I have WebGL 2 context and not 1), but GLctx.compressedTexImage2D(target, level, internalFormat, width, height, border, HEAPU8, data, imageSize); works fine. glTexImage2D generates WebGL: INVALID_OPERATION: texImage2D: compressed texture formats are not accepted

@juj
Copy link
Collaborator

juj commented Aug 23, 2024

Posted a note over to Khronos GitHub tracker.

Indeed it looks like Emscripten code has all this time been uploading garbage bytes starting from memory address 0. I don't remember if that was intentional, or if it was accidental. However, I recommend to restore that original behavior, and add a comment in the code to point to KhronosGroup/WebGL#3686 to explain why that is done.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 23, 2024

What about the idea of using textImage2D instead of compressedTexImage2D when data in null? @matusfedorko you say that didn't work but I don't see how/why it could fail? Am I missing something?

@sbc100
Copy link
Collaborator

sbc100 commented Aug 23, 2024

@matusfedorko does you sample code above actually draw anything to screen? I'm like to use it as a test case for landing this fix, but it would be good if it actually used a compressed subimage and drew something I think.

@juj
Copy link
Collaborator

juj commented Aug 23, 2024

What about the idea of using textImage2D instead of compressedTexImage2D when data in null

Unfortunately calling texImage2D will establish the GPU side backing store of the texture to contain an uncompressed texture, and not a compressed texture. Later calling compressedTexSubImage2D to attempt to upload compressed texture data to it would cause an error since the types wouldn't match.

Or later calling compressedTexImage2D on the texture would just redo/discard the GPU side memory allocation that the earlier texImage2D call had made.

@matusfedorko
Copy link

matusfedorko commented Aug 23, 2024

What about the idea of using textImage2D instead of compressedTexImage2D when data in null? @matusfedorko you say that didn't work but I don't see how/why it could fail? Am I missing something?

It generates an error (I tested in Chrome). Precisely WebGL: INVALID_OPERATION: texImage2D: compressed texture formats are not accepted (and it also fails in desktop GL). And also rendering is broken (black pixels where the texture was supposed to be visible).

@matusfedorko
Copy link

@matusfedorko does you sample code above actually draw anything to screen? I'm like to use it as a test case for landing this fix, but it would be good if it actually used a compressed subimage and drew something I think.

No, but feel free to adjust it so that it does. I did not have time to make anything fancy, but a barebones sample that demonstrates the issue clearly.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 23, 2024

What about the idea of using textImage2D instead of compressedTexImage2D when data in null

Unfortunately calling texImage2D will establish the GPU side backing store of the texture to contain an uncompressed texture, and not a compressed texture. Later calling compressedTexSubImage2D to attempt to upload compressed texture data to it would cause an error since the types wouldn't match.

Or later calling compressedTexImage2D on the texture would just redo/discard the GPU side memory allocation that the earlier texImage2D call had made.

I see. How about passing new ArrayBuffer(size) ? Would that work? I guess it would create garbage.. but maybe this API is not called too often? Maybe OK, at least until the upstream issue is fixed?

Or we could us a static empty buffer than we realloc each time? That would have a constant size overhead.

@kenrussell
Copy link
Collaborator

From the beginning of the WebGL spec, compressedTexImage2D was expected to receive valid compressed texture data.

It was only during the introduction of WebGL 2.0, when applications could control textures' internalformat, that the possibility of allocating an "empty" compressed texture was introduced into the API. At that point it became valid to allocate a compressed texture using texStorage2D (or 3D) and fill its levels with compressedTexSubImage2D (or 3D).

Therefore, yes, if the application passes NULL to glCompressedTexImage2D and is using WebGL 2.0 under the hood, it would be valid to call texStorage2D with the correct format and internalformat to allocate it. If it's using WebGL 1.0 then I would recommend synthesizing an OpenGL error. An alternative is to allocate a large zeroed buffer (the necessary size of this buffer is defined in the various WebGL compressed texture specs like that for S3TC), but I would strongly recommend against this. Such allocations are not cheap and are also not guaranteed to be promptly garbage collected.

@matusfedorko there is no overload compressedTexImage2D(target, level, internalformat, width, height, border). The overloads can be found here:

https://registry.khronos.org/webgl/specs/latest/1.0/

https://registry.khronos.org/webgl/specs/latest/2.0/

and are:

For WebGL 1.0:

undefined compressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, [AllowShared] ArrayBufferView data);

From WebGL 2.0, taking the compressed texture data from a (GPU-side) pixel buffer object:

undefined compressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, GLintptr offset);

and WebGL 2.0's "garbage-free" entry point which selects the region from the passed ArrayBufferView:

undefined compressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, [AllowShared] ArrayBufferView srcData, optional unsigned long long srcOffset = 0, optional GLuint srcLengthOverride = 0);

@sbc100
Copy link
Collaborator

sbc100 commented Aug 23, 2024

At that point it became valid to allocate a compressed texture using texStorage2D (or 3D) and fill its levels with compressedTexSubImage2D (or 3D).

This is the part that doesn't seem to be working according to @matusfedorko. Hopefully I can create a test/sample to verify this.

@matusfedorko
Copy link

matusfedorko commented Aug 23, 2024

At that point it became valid to allocate a compressed texture using texStorage2D (or 3D) and fill its levels with compressedTexSubImage2D (or 3D).

This is the part that doesn't seem to be working according to @matusfedorko. Hopefully I can create a test/sample to verify this.

Oh wait, but texStorage2D is different than texImage2D. I tried the latter one. I think texStorage2D could work.

@matusfedorko there is no overload compressedTexImage2D(target, level, internalformat, width, height, border). The overloads can be found here:

Thanks for the info. I went off of the MDN docs in my comment, which show that overload in the first place.

@juj
Copy link
Collaborator

juj commented Aug 23, 2024

This is the part that doesn't seem to be working according to @matusfedorko

Note that texImage2D and texStorage2D are different functionalities of OpenGL/WebGL.

We can not call texStorage2D as a special-cased path inside compressedTexImage2D. This is because there are many functional differences (see OpenGL ES 3.0 specification 3.8.4 Immutable-Format Texture Images) that would result.

How about passing new ArrayBuffer(size) ? Would that work?

That would work, but would be pretty disappointing. If someone has their "GPU memory is compromised, but CPU memory is intact" op-sec hat on, then we could certainly do that.

@kenrussell
Copy link
Collaborator

How about passing new ArrayBuffer(size) ? Would that work?

That would work, but would be pretty disappointing.

Agreed; as I mentioned above, such allocations are not cheap and are also not guaranteed to be promptly garbage collected. It would be better to initialize the compressed texture with whatever random data is at address 0 in the Wasm heap, as Jukka points out the code used to work before.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 23, 2024

How about passing new ArrayBuffer(size) ? Would that work?

That would work, but would be pretty disappointing.

Agreed; as I mentioned above, such allocations are not cheap and are also not guaranteed to be promptly garbage collected. It would be better to initialize the compressed texture with whatever random data is at address 0 in the Wasm heap, as Jukka points out the code used to work before.

How about reusing a global allocation:

if (!pixels) {
  pixels = globalTmpBuf = realloc(globalTmpBuf, size);
}

That would limit us to single allocation that only happens rarely, and avoid reading from address zero.

Regardless is seems like a bug in the spec that the final argument it not optional right?

@kenrussell
Copy link
Collaborator

Textures in 3D applications tend to be very large. This global allocation will be wasted space that is only present in order to give these compressed textures zeroed-out data. The data in the Wasm heap is controlled entirely by the application; there is no chance that any data uploaded to the texture will cause a leak of information that the application shouldn't be able to see.

The guarantees in OpenGL ES's C API are much weaker than those provided by WebGL, where nearly all undefined behavior was resolved and turned into well-defined behavior. Applications compiled with Emscripten are only relying on the C API's semantics.

For this reason I once again strongly support @juj 's suggestion to just upload data from the 0 address in the Wasm heap if the app calls glCompressedTexImage2D with a NULL pointer. This behavior is 100% compatible with the C API's semantics.

@sbc100
Copy link
Collaborator

sbc100 commented Aug 24, 2024

Textures in 3D applications tend to be very large. This global allocation will be wasted space that is only present in order to give these compressed textures zeroed-out data. The data in the Wasm heap is controlled entirely by the application; there is no chance that any data uploaded to the texture will cause a leak of information that the application shouldn't be able to see.

The guarantees in OpenGL ES's C API are much weaker than those provided by WebGL, where nearly all undefined behavior was resolved and turned into well-defined behavior. Applications compiled with Emscripten are only relying on the C API's semantics.

For this reason I once again strongly support @juj 's suggestion to just upload data from the 0 address in the Wasm heap if the app calls glCompressedTexImage2D with a NULL pointer. This behavior is 100% compatible with the C API's semantics.

OK lets go with that previous/odd behaviour.

Is it compatible though? I thought passing NULL as the last argument guaranteed that the new space was zero-initialized. The data at address zero in the wasm memory is likely not going to be zeros. We put C static data at address 1024, and we write cookies to the first 8 bytes of memory.

@kenrussell
Copy link
Collaborator

Is it compatible though? I thought passing NULL as the last argument guaranteed that the new space was zero-initialized. The data at address zero in the wasm memory is likely not going to be zeros. We put C static data at address 1024, and we write cookies to the first 8 bytes of memory.

The GLES 3.0 spec says (per Jukka's comment above) that if NULL is passed as the data argument, the image's contents are unspecified. This behavior is compatible.

sbc100 added a commit to sbc100/emscripten that referenced this issue Aug 26, 2024
I believe this has always been an issue with emscripten's WebGL1 code
which only recently became more prevalent with emscripten-core#21445.

Fixes: emscripten-core#19300
sbc100 added a commit to sbc100/emscripten that referenced this issue Aug 27, 2024
I believe this has always been an issue with emscripten's WebGL1 code
which only recently became more prevalent with emscripten-core#21445.

Fixes: emscripten-core#19300
sbc100 added a commit that referenced this issue Aug 27, 2024
I believe this has always been an issue with emscripten's WebGL1 code
which only recently became more prevalent with #21445.

Fixes: #19300
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

Successfully merging a pull request may close this issue.

6 participants