HumbleUI / Skija

Java bindings for Skia
Apache License 2.0
498 stars 34 forks source link

[Question] Multiple windows with a shared DirectContext? #55

Closed AquilaAbriel closed 1 year ago

AquilaAbriel commented 1 year ago

I'm currently trying to figure out how to draw to multiple windows, but with no avail. All I could achieve so far is one window (the primary one) to render correctly, while the secondary window just displays a black screen. I tried replicating the LWJGL example of how to run multiple GL windows simultaneously, but that wasn't really helpful: https://github.com/LWJGL/lwjgl3/blob/master/modules/samples/src/test/java/org/lwjgl/demo/glfw/MultipleWindows.java Mainly because the LWJGL example relies on multiple context objects (one for each window), which I would like to avoid (if possible).

Some boilerplate code of my general order of operation so far:

Window constructor:

//Set default window hints
GLFW.glfwDefaultWindowHints();

//Set window as hidden
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);

//Set window resizable flag
if (resizable)
{
    GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE);
}
else
{
    GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_FALSE);
}

//Create GL window
boolean primaryWindow = false;
if (AEGameWindowManager.getPrimaryWindow() == null)
{
    //--> Create primary window and GL context
    windowHandle = GLFW.glfwCreateWindow(width, height, title, 0, 0);
    if (windowHandle == 0)
    {
        throw new RuntimeException("Error: Failed to create GL window");
    }
    primaryWindow = true;
}
else
{
    //--> Create secondary window, use shared context of primary window
    windowHandle = GLFW.glfwCreateWindow(width, height, title, 0, AEGameWindowManager.getPrimaryWindow().getWindowHandle());
    if (windowHandle == 0)
    {
        throw new RuntimeException("Error: Failed to create GL window");
    }
}

Is my assumption correct, that I have to specify the primary windows' handle as "shared" when I call glfwCreateWindow() on the secondary window creation? (If I want all my windows to use the same context as the primary window)

Window constructor continuation:

//Center window on primary screen
GLFWVidMode vidmode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
int xPos = Math.max(0, (vidmode.width() - width) / 2);
int yPos = Math.max(0, (vidmode.height() - height) / 2);
GLFW.glfwSetWindowPos(windowHandle, xPos, yPos);

//Update window dimensions
updateDimensions();

//Make window context current
GLFW.glfwMakeContextCurrent(windowHandle);

//Set v-sync state
setVSyncState(vsync);

//Unhide window
GLFW.glfwShowWindow(windowHandle);

//Initialize GL context
if (primaryWindow)
{
    initializeGLContext();
}

//Initialize GL window event callbacks
initializeGLCallbacks();

//Update SKIA resources
updateSkia();

Some additional window functions:

//-----------------------------------------------------------------------------------------------------//
//Initialize GL (I only have to do this once, after primary window creation?)
//-----------------------------------------------------------------------------------------------------//
private void initializeGLContext()
{
    //I have no idea what this does...
    if ("false".equals(System.getProperty("skija.staticLoad")))
    {
        Library.load();
    }
    boolean loaded = Library._loaded;

    //Create global gl context
    context = DirectContext.makeGL();
}
//-----------------------------------------------------------------------------------------------------//
//Update SKIA resources
//-----------------------------------------------------------------------------------------------------//
private void updateSkia()
{
    if (surface != null)
    {
        surface.close();
    }
    if (renderTarget != null)
    {
        renderTarget.close();
    }

    renderTarget = BackendRenderTarget.makeGL(
            frameBufferResolution.getX(),
            frameBufferResolution.getY(),
            /*msaaSamples*/0,
            /*stencil*/8,
            /*fbId*/0,
            FramebufferFormat.GR_GL_RGBA8);

    surface = Surface.makeFromBackendRenderTarget(
            context,
            renderTarget,
            SurfaceOrigin.BOTTOM_LEFT,
            SurfaceColorFormat.RGBA_8888,
            ColorSpace.getSRGB(),
            new SurfaceProps(PixelGeometry.RGB_H));
}

Do I have to assign an incremental framebuffer id for each backend render target? (I tried assigning a different id to the secondary window, but that didn't change anything.)

Window draw():

//-----------------------------------------------------------------------------------------------------//
//Draw this windows' content
//-----------------------------------------------------------------------------------------------------//
public void _draw()
{
    //Assign window to context?
    GLFW.glfwMakeContextCurrent(windowHandle);

    //Clear window surface
    //(The secondary window stays black, even if I use a different clear color...)
    surface.getCanvas().clear(0x00000000);

    //...Draw stuff...//

    //Swap front and back buffer
    GLFW.glfwSwapBuffers(windowHandle);
}

Main loop:

//Process window events
GLFW.glfwPollEvents();

AEGameWindow[] windows = AEGameWindowManager.getWindows();
for (AEGameWindow window : windows)
{
    //Draw window content
    window._draw();
}

//(Context is a static field)
AEGameWindow.getContext().flush();

It seems like I have to call DirectContext.flush() once each frame. But when exactly do I have to do this? After each window was drawn, or after all windows have been drawn? If I do this after each window was drawn, all windows stop working and only display a black screen...

My main question is: Are multiple windows even supported by Skija right now? And if yes, Is there an order of operation, I have to comply to? And does it matter if I have one context or multiple ones? The only time I need to access the windows' context is when I create a new Skija Surface. (I assume because the underlying rendertarget resource is held by the context) But if that is the case, why can I create a new Skija Image without referencing a context? (It's just a texture object I assume. It should be assigned to a context?)

dzaima commented 1 year ago

Multiple windows definitely do at least work with a separate DirectContext.makeGL() for each, as I have apps using that working (both with LWJGL or JWM for the window management). Can't comment on much else though, as I don't really know anything about the subject matter.

Of note is that JWM's included Skija wrapper (JWM and Skija being both under HumbleUI) also makes a new DirectContext for each surface, and ends up creating a new one any time it's resized.

AquilaAbriel commented 1 year ago

Thank you very much, for sharing your experience and for pointing me the JWM repository. It's good to know, that multiple windows do indeed work.

I have to say, I'm a bit surprised. When I read about OpenGL context objects I was under the impression, those were intended to be used very selectively and as long as the application is running.

But I will definitely give it a try and see what happens if I use multiple contexts.

AquilaAbriel commented 1 year ago

And it just works... great. As soon as I changed every window to having its own context, it just started working as intended. I have no Idea why, or if this solution might cause any long term issues but it works. Thank you very much and have good night. [Solved]

dzaima commented 1 year ago

I have to say, I'm a bit surprised. When I read about OpenGL context objects I was under the impression, those were intended to be used very selectively and as long as the application is running.

I imagine that's more a "don't create one (or, worse, dozens) per frame without good reason" than "don't make more than one ever".

this wiki page says

Each context can represent a separate viewable surface, like a window in an application.

so it looks like a separate one per surface/window is the intention.

AquilaAbriel commented 1 year ago

I'm sorry to have to reopen this issue, but it's still not working correctly. Having multiple Contexts just made the issue more complicated.

As long as I only draw primitive objects (squares, circles, lines etc.) everything works fine (on all windows). But as soon as I draw a path, or a texture, everything falls apart. (For some reason, drawing texts doesn't cause any issues...)

Scenario 1: Only one window -All draw calls work as intended.

Scenario 2: Two windows -Only drawing primitive objects works fine on both windows. -Drawing a texture to the primary window shows up black and the secondary window turns black. -Drawing a texture to the secondary window shows up correctly and the primary window continues working fine. -Drawing a path to the primary window doesn't show up and the secondary window turns black. -Drawing a path to the secondary window shows up correctly and the primary window truns black...

I can somewhat mitigate these issues by calling resetGLAll()() on the window's context, when I switch from one window to the other.

Scenario 3: Two windows + reset() context in between windows switching -Only drawing primitive objects works fine on both windows. -Drawing a texture to the primary window shows up black, but the secondary window contiues working fine! -Drawing a texture to the secondary window shows up correctly and the primary window continues working fine. (No change) -Drawing a path to the primary window doesn't show up, but the secondary window contiues working fine! -Drawing a path to the secondary window shows up correctly and the primary window continues working fine! -Drawing a path to both windows only shows up on the secondary window...

It's a mess honestly... And I have no idea, what is going wrong. (As faw as I can tell, GL is not reporting any errors) But since resetting the contexts does change some of the results, I suspect having multiple contexts is the root cause of the issue.

So I'm going back at my initial question: How do I create a shared context and use it across multiple windows?

dzaima commented 1 year ago

Could you post the code you got to with multiple contexts? (_draw mainly)

AquilaAbriel commented 1 year ago

Sure:

The draw method (part of the window class)

//-----------------------------------------------------------------------------------------------------//
//Draw this windows' content
//-----------------------------------------------------------------------------------------------------//
protected void _draw()
{
    if (isAlive())
    {
        //Assign window to context?
        GLFW.glfwMakeContextCurrent(windowHandle);

        //Clear window surface
        surface.getCanvas().clear(0x00000000);

        AESceneComposition composition = sceneManager.getComposition();
        if (composition != null)
        {
            AERenderTarget compositionTarget = composition.getCompositionTarget();
            if (compositionTarget != null && !compositionTarget.isDisposed())
            {
                //--> Draw scene composition target to window surface
                compositionTarget.drawTo(surface.getCanvas(), new AERectangleInt(0,0, compositionTarget.getWidth(), compositionTarget.getHeight()), new AERectangleInt(0,0, frameBufferResolution.getX(), frameBufferResolution.getY()));
            }
        }

        //Swap window frame buffers
        GLFW.glfwSwapBuffers(windowHandle);
    }
}

"composition target" ist just another surface I create as a offscreen render target. The acual draw mehtod of each window then only "overlays" the composition target onto the window surface.

Code from rendertarget:

//-----------------------------------------------------------------------------------------------------//
//Constructor
//-----------------------------------------------------------------------------------------------------//
public AERenderTarget(DirectContext context, int width, int height, int aaSamples)
{
    this.context = context;
    this.width = width;
    this.height = height;
    ImageInfo info = new ImageInfo(width, height, ColorType.RGBA_8888, ColorAlphaType.PREMUL);
    SurfaceProps props = new SurfaceProps(PixelGeometry.RGB_H);
    surface = Surface.makeRenderTarget(this.context, false, info, aaSamples, props);
}

I already checked to make sure, I always pass the "correct" conext object... But there might be an issue here.

Main game loop code:

while (AEGameWindowManager.getPrimaryWindow().isAlive() && alive)
{
    //Update game instance
    _update();

    AEGameWindow[] windows = AEGameWindowManager.getWindows();
    for (AEGameWindow window : windows)
    {
        if (window != AEGameWindowManager.getPrimaryWindow() && !window.isAlive())
        {
            //--> Terminate sub window if not alive anymore
            AEGameWindowManager.terminateWindow(window.getWindowName());
        }
        else if (window.isAlive())
        {
            //--> Update engine of alive windows
            window.getSceneManager()._input();
            window.getSceneManager()._update();
            window.getSceneManager()._draw();

            //Draw frame to window
            window._draw();

            //Flush context
            window.getContext().flush();

            window.getContext().resetGLAll();
        }
    }
}

The last context.resetGLAll() was just a hack to see if it helps. By the way, thank you very much for taking time to help me figure this out.

AquilaAbriel commented 1 year ago

Oh god I think I just figured it out... Of course I have to make the windows context current BEFORE I start making new texture objects and paths. glfwMakeContextCurrent() needs to be part of the game loop, not the window draw method.

while (AEGameWindowManager.getPrimaryWindow().isAlive() && alive)
{
    //Update game instance
    _update();

    AEGameWindow[] windows = AEGameWindowManager.getWindows();
    for (AEGameWindow window : windows)
    {
        if (window != AEGameWindowManager.getPrimaryWindow() && !window.isAlive())
        {
            //--> Terminate sub window if not alive anymore
            AEGameWindowManager.terminateWindow(window.getWindowName());
        }
        else if (window.isAlive())
        {
            //HERE! I need to make the context current before I start building context relevant object like textures!
            GLFW.glfwMakeContextCurrent(window.getWindowHandle());

            //--> Update engine of alive windows
            window.getSceneManager()._input();
            window.getSceneManager()._update();
            window.getSceneManager()._draw();

            //Draw frame to window
            window._draw();

            //Flush context
            window.getContext().flush();
        }
    }
}

I think it works now, sorry for bothering you again^^

AquilaAbriel commented 1 year ago

Ok, it's fixed (again... hopefully) It seems like Skija takes whatever context is current, when I make a new context relevant object like textures and paths.