emscripten-core / emscripten

Emscripten: An LLVM-to-WebAssembly Compiler
Other
25.7k stars 3.3k forks source link

Black screen using SDL2 OpenGL ES 3, problem with strings #15400

Open AntonStrickland opened 2 years ago

AntonStrickland commented 2 years ago

I have already posted about this issue in the Discord and in the mailing list to no avail so far. It's frustrating because my project takes 5-10 minutes to compile and so only being able to debug this via trial and error it is a considerable waste of time for me to try to deal with this on my own. As a result, I've already spent days (if not weeks) on what should be a simple issue that someone who has experience with web assembly should be able to resolve relatively quickly. That said, no one in those other places has managed to figure it out so I'm posting here for additional help.

I have been stuck on a very simple issue for a couple weeks and need someone who knows what they are doing. I'm trying to use SDL2 and WebGL2 (OpenGL ES 3) for a C++ game, and sometimes it works, but sometimes it doesn't, depending on seemingly arbitrary code. Please help.

compile command: em++ -D EMSCRIPTEN main.cpp -o game.html -s NO_EXIT_RUNTIME=1 -s OFFSCREEN_FRAMEBUFFER=1 -s USE_WEBGL2=1 -s FULL_ES3=1 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"] -s EXPORTED_RUNTIME_METHODS="['cwrap', 'ccall']" -s USE_SDL=2 -s USE_SDL_MIXER=2 -s USE_SDL_TTF=2 -s ALLOW_MEMORY_GROWTH=1 --std=c++17

run command:

start chrome http://localhost:8000/game.html
python -m http.server

main.cpp:

#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include <string>
#include <iostream>

#ifdef EMSCRIPTEN
    #include <emscripten.h>
    #include <emscripten/html5.h>
    #include <GLES3/gl3.h>
#else
    #include <GL/glew.h>
#endif

SDL_Window* window;
SDL_GLContext mainContext;

void Log(const std::string& message)
{
    // If you DON'T comment out this line, the screen will NOT go black,
    // but you will receive this error:
    // Could not create EGL context (context attributes are not supported)
    // SDL_GL_SetSwapInterval failed. No OpenGL context has been made current
    //std::cout << message << std::endl;
}

void InitOpenGL(const int screenWidth, const int screenHeight, SDL_Window* window, SDL_GLContext& mainContext)
{
    SDL_Init( SDL_INIT_VIDEO );
    TTF_Init();  

    std::cout << "Init OpenGL" << std::endl;

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);

    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

    window = SDL_CreateWindow("...", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, screenWidth, screenHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);

    // If you comment out the below line, the screen will go black
    Log("" + std::string(SDL_GetError())  + std::string(SDL_GetError()));

    EmscriptenWebGLContextAttributes attrs;
    attrs.antialias = true;
    attrs.majorVersion = 3;
    attrs.minorVersion = 2;
    attrs.alpha = true;
    EMSCRIPTEN_WEBGL_CONTEXT_HANDLE webgl_context = emscripten_webgl_create_context("#canvas", &attrs);
    emscripten_webgl_make_context_current(webgl_context);
    mainContext = SDL_GL_CreateContext(window);
    Log("" + std::string(SDL_GetError()));

    std::cout << "GL_VERSION: " << glGetString(GL_VERSION) << std::endl;    

    if (SDL_GL_SetSwapInterval(1) != 0)
    {
        Log("ERROR: SDL_GL_SetSwapInterval failed. " + std::string(SDL_GetError()));
    }   

    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    glDepthMask(false);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glViewport(0, 0, screenWidth, screenHeight);
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);

    SDL_GL_SwapWindow(window);  

    std::cout << "End of OpenGL init" << std::endl;
}

void Render() 
{
    static bool showMessage = false;
    if (!showMessage)
    {
        std::cout << "...Rendering..." << std::endl;
        std::cout << "GL_VERSION: " << glGetString(GL_VERSION) << std::endl;        
        showMessage = true;
    }

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glClearColor(1.0f, 0.1f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram(0);
    SDL_GL_SwapWindow(window);
}

void MainLoop()
{
    Render();
}

void BeforeMainLoop()
{
    std::cout << "Attempting to set and enter the main loop..." << std::endl;
    emscripten_set_main_loop((em_callback_func)MainLoop, 0, 0);
}

int main() 
{
    InitOpenGL(1280, 720, window, mainContext);
    BeforeMainLoop();

    return 1;
}

Basically, I have a simple program (just over 100 lines of code) that changes the background color of the screen by calling glClear(). Probably the most basic OpenGL program one can write. When the color is pink, that means it's working, and when it's black, it's not working. In the above link there is main.cpp which works correctly. I have been doing trial and error for this past week commenting out random lines of code to isolate the problem, but it seems like changing arbitrary lines of code stops it from working. I've noticed that it happens when calling some SDL functions, but even just commenting out printing to the console is enough to break it. See main.cpp for details.

I don't know if this is just an issue with me not understanding how this works or if this is an actual bug in emscripten or the SDL port. it's frustrating because when it works, it works all the way (I can use shaders and textures to have a working app), but if I change just a tiny little thing (even just commenting out an unrelated line of code), suddenly nothing works at all. I have no idea where to even start debugging this.

So I know this is getting long, but I'm very confused. Since technically my code works, but it's one step away from breaking, and I have no idea what that step is or why. My ultimate goal is to copy and paste this code into a larger project, but every time I do so, it results in a black screen with no indication of what I did wrong.

I would imagine that there has to be a way to do this since people are obviously making WebGL games in C++, but since there are no tutorials online for how to do that it becomes impossible to even set up a simple "hello world" script. I purchased a book on how to make games in web assembly but they use the SDL renderer and not OpenGL rendering. Other OpenGL web assembly tutorials just use OpenGL without SDL. Surely there must be a way to use them together as I have done for my desktop app? Is this an issue with my code or build commands, or is it a problem with emscripten or the SDL port? My code is pretty minimal so I'm leaning towards the latter, but unfortunately if that's the case then that means I can only wait around for someone to fix it, which isn't ideal. I would rather it be something wrong on my end so I can just fix it myself and move on. I would really like to know that my game can run on the web using emscripten, and it would be disappointing if it couldn't.

Recently I've discovered more causes for the black screen and wonder if maybe the issue is related to strings in some way. If I add a class that takes a string as a parameter in the constructor, and then ONLY if I instantiate it, like so:

class Test 
{
public:
    Test(std::string test) {} ;
};

int main() 
{
    Test test("test");
    InitOpenGL(1280, 720, window, mainContext);
    BeforeMainLoop();

    return 1;
}

Then that also results in a black screen. Instantiating a vector of strings also results in a black screen, like so: std::vector<std::string> languages = { "english", "japanese" };

And my original problem with the Log function also happens to involve strings on some level. If I change the constructor from a string to a const char then it works. But changing all instances of strings to const char is not ideal (my program needs strings), and I actually tried that with the Log function and that did not help, so I'm still looking for a solution. So hopefully this information is helpful for someone to figure out what is wrong and how to fix it.

AntonStrickland commented 2 years ago

It looks like I have found a solution thanks to someone in the Discord server. Here is what I changed:

  EmscriptenWebGLContextAttributes attrs;
    attrs.antialias = true;
    attrs.majorVersion = 3;
    attrs.minorVersion = 2;
    attrs.alpha = true;
    attrs.powerPreference = EM_WEBGL_POWER_PREFERENCE_DEFAULT;

    // The following lines must be done in exact order, or it will break!
    emscripten_webgl_init_context_attributes(&attrs); // you MUST init the attributes before creating the context
    attrs.majorVersion = 3; // you MUST set the version AFTER the above line
    EMSCRIPTEN_WEBGL_CONTEXT_HANDLE webgl_context = emscripten_webgl_create_context("#canvas", &attrs);
    emscripten_webgl_make_context_current(webgl_context);
    mainContext = SDL_GL_CreateContext(window);

When you modify my main.cpp with this code then it actually shows the game on the screen and all of the above errors I mentioned disappear. So the solution is to call emscripten_webgl_init_context_attributes between creating the attributes and before creating the context. However, if you do that then it sets up the webgl context using OpenGL ES 2.0 (WebGL 1.0) which is not what I want (because I would need to change all my shader code). So that's why you need to set the attributes AGAIN after calling it (to set majorVersion=3) which results in OpenGL ES 3.0 / WebGL 2.0.

Of course I have no idea WHY any of this needs to be done. I hope this can either be fixed or documented somewhere with an explanation as to why it breaks and why this fixes it. It should not have been this difficult or taken this long to find a workaround that involves two lines of code. I think someone should still look deeper into this "fix" because you're just writing arbitrary lines of code to solve an arbitrary issue, but at least I can continue working on my game without waiting for someone to fix it.

As for my game, I managed to copy and paste this code into my larger game project and it worked like a charm. However, I get a different (unrelated) error now which is that the browser crashes because it runs out of memory... but at least I can see the game now (for a split second before it crashes). The preloaded data file is ~80 MB which isn't huge by video game standards, but it might be too big for the browser to download. I did set ALLOW_MEMORY_GROWTH=1 but that didn't seem to prevent the issue. I don't think that's an issue with emscripten though. I'm just glad I got past this one.

juj commented 2 years ago

The emscripten_webgl_create_context() API is used to create GL contexts using WebGL version identifiers, not by using GLES version identifiers. Therefore one should pass either majorVersion = 1 and minorVersion = 0 to create a WebGL 1 context, or majorVersion = 2 and minorVersion = 0 to create a WebGL 2 context (and build with -s MAX_WEBGL_VERSION=2 to enable targeting WebGL 2 at compile time).

Also, this sample is creating two different GL contexts, one by calling emscripten_webgl_create_context() and another by calling SDL_GL_CreateContext(). You should not create a context twice, but just create with one or the other API. If you are using SDL API in the application, then you should use SDL to create the GL Context.

If you are not interested in using SDL, you can find a small emscripten_webgl_create_context() based render test at https://github.com/juj/webgl_render_test .

juj commented 2 years ago

the solution is to call emscripten_webgl_init_context_attributes between creating the attributes and before creating the context

In C/C++, if you create a struct on the stack, its contents will be uninitialized memory until you assign values to them. The preferred way to initialize the values is to call emscripten_webgl_init_context_attributes(&attrs) to initialize the attributes to their defaults. Another way to zero-initialize all fields is to use the = {}; syntax, and do

  EmscriptenWebGLContextAttributes attrs = {};
  ...

That being said, the application is mixing the use of two different GL context creation APIs. If you are using SDL, you can just drop all the emscripten_webgl_... stuff from it.

For example, the test https://github.com/emscripten-core/emscripten/blob/main/tests/webgl_create_context2.cpp shows how to create a context using Emscripten WebGL API, whereas the test https://github.com/emscripten-core/emscripten/blob/main/tests/gl_renderers.c shows how to create a GL context using the SDL API (though that particular sample uses old desktop GL calls, so beyond the context initialization, is not a particularly good modern best practices)