pauldmccarthy / fsleyes

This is a mirror. Feel free to use the issue tracker. PRs welcome.
https://git.fmrib.ox.ac.uk/fsl/fsleyes/fsleyes/
Other
22 stars 11 forks source link

Empty GLContext set by wxPython on windows prevents startup. #107

Open shangjiaxuan opened 1 year ago

shangjiaxuan commented 1 year ago

In file wxglslicecanvas.py the line

wxgl.GLCanvas          .__init__(self, parent, **attrs)

Sets the current wgl context to None (from OpenGL.WGL.GetCurrentContext()) on windows, and subsequent startup code will fail at any GL function (glGenTextures in current code) with INVALID_OPERATION.

A workaround would be calling self._setGLContext() immediately. But since while loading initial screen, the self.IsShownOnScreen() is always false, the workaround would require to commenting the following lines in __init__.py in fsleyes.gl's _setGLContext:

        if not (fwidgets.isalive(self) and self.IsShownOnScreen()):
            return False

With these edits, fsleyes starts the window on windows.

shangjiaxuan commented 1 year ago

Line that changes current context to NULL on windows: https://github.com/pauldmccarthy/fsleyes/blob/9a01ff62c6f1e6ad746092dc5045baefc6760e12/fsleyes/gl/wxglslicecanvas.py#L44

Line that throws invalid operation error: https://github.com/pauldmccarthy/fsleyes/blob/9a01ff62c6f1e6ad746092dc5045baefc6760e12/fsleyes/gl/textures/texture.py#L86

shangjiaxuan commented 1 year ago

self._setGLContext() needs to be after the context is retrieved in CanvasTarget contruction, thus:

        wxgl.GLCanvas          .__init__(self, parent, **attrs)
        fslgl.WXGLCanvasTarget .__init__(self)
        self._setGLContext()
shangjiaxuan commented 1 year ago

It seems reasonable to add the line to the end of WXGLCanvasTarget.__init__ to enforce this on all wxCanvas types:

self._setGLContext()
pauldmccarthy commented 1 year ago

Hi @shangjiaxuan, can you share a full stack trace of this error occurring?

shangjiaxuan commented 1 year ago

@pauldmccarthy of course. But I doubt the delayed error handling will give anything usefull. python ./Lib/site-packages/fsleyes/main.py gives:

 WARNING              logs.py   69: __call__        - Failure on glGenTextures: Traceback (most recent call last):
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\latebind.py", line 43, in __call__
    return self._finalCall( *args, **named )
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: 'NoneType' object is not callable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\logs.py", line 67, in __call__
    return function( *args, **named )
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\error.py", line 230, in glCheckError
    raise self._errorClass(
OpenGL.error.GLError: GLError(
        err = 1282,
        description = b'invalid operation',
        baseOperation = glGenTextures,
        cArguments = (1, array([0], dtype=uint32))
)

 WARNING             frame.py 1458: __restoreState  - Previous layout could not be restored - falling back to default layout.
 WARNING              logs.py   69: __call__        - Failure on glGenTextures: Traceback (most recent call last):
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\logs.py", line 67, in __call__
    return function( *args, **named )
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\error.py", line 230, in glCheckError
    raise self._errorClass(
OpenGL.error.GLError: GLError(
        err = 1282,
        description = b'invalid operation',
        baseOperation = glGenTextures,
        cArguments = (1, array([0], dtype=uint32))
)

 WARNING          __init__.py  731: create          - GLContext callback function raised GLError: GLError(
        err = 1282,
        description = b'invalid operation',
        baseOperation = glGenTextures,
        pyArgs = (
                1,
                <object object at 0x0000013528666EB0>,
        ),
        cArgs = (1, array([0], dtype=uint32)),
        cArguments = (1, array([0], dtype=uint32))
)
Traceback (most recent call last):
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\gl\__init__.py", line 728, in create
    ready()
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\main.py", line 583, in realCallback
    callback()
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\main.py", line 370, in buildGui
    frame = makeFrame(namespace[0],
            ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\main.py", line 795, in makeFrame
    frame = fsleyesframe.FSLeyesFrame(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\frame.py", line 311, in __init__
    self.__restoreState(restore)
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\frame.py", line 1464, in __restoreState
    layouts.loadLayout(self, 'default')
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\layouts.py", line 104, in loadLayout
    applyLayout(frame, name, layout, **kwargs)
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\layouts.py", line 143, in applyLayout
    frame.addViewPanel(vp, defaultLayout=False)
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\frame.py", line 511, in addViewPanel
    panel = panelCls(self.__mainPanel, self.__overlayList, childDC, self)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\views\orthopanel.py", line 219, in __init__
    self.__labelMgr = ortholabels.OrthoLabels(
                      ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\gl\ortholabels.py", line 79, in __init__
    cannots[side] = annot.text('', 0, 0, hold=True)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\gl\annotations.py", line 228, in text
    obj   = TextAnnotation(self, *args, **kwargs)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\gl\annotations.py", line 1374, in __init__
    self.__text      = gltext.Text()
                       ^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\gl\text.py", line 117, in __init__
    self.__texture   = textures.Texture2D(
                       ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\gl\textures\texture2d.py", line 192, in __init__
    texture.Texture.__init__(self, name, 2, nvals, **kwargs)
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\gl\textures\texture.py", line 556, in __init__
    TextureBase         .__init__(self, name, ndims, nvals)
  File "C:\Users\shang\fsleyes\Lib\site-packages\fsleyes\gl\textures\texture.py", line 86, in __init__
    self.__texture     = int(gl.glGenTextures(1))
                             ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\latebind.py", line 43, in __call__
    return self._finalCall( *args, **named )
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\wrapper.py", line 678, in wrapperCall
    raise err
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\wrapper.py", line 671, in wrapperCall
    result = wrappedOperation( *cArguments )
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\logs.py", line 67, in __call__
    return function( *args, **named )
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\shang\fsleyes\Lib\site-packages\OpenGL\error.py", line 230, in glCheckError
    raise self._errorClass(
OpenGL.error.GLError: GLError(
        err = 1282,
        description = b'invalid operation',
        baseOperation = glGenTextures,
        pyArgs = (
                1,
                <object object at 0x0000013528666EB0>,
        ),
        cArgs = (1, array([0], dtype=uint32)),
        cArguments = (1, array([0], dtype=uint32))
)

Then the floating load window freezes at Creating FSLeyes interface...

I suggest adding log.error("{}".format(OpenGL.WGL.GetCurrentContext())) and log.error("{}".format(OpenGL.GLX.GetCurrentContext())) (linux x11, EGL for wayland? and CGL for mac?) before and after this line (and anywhere to see if a context is current) to see my point: https://github.com/pauldmccarthy/fsleyes/blob/9a01ff62c6f1e6ad746092dc5045baefc6760e12/fsleyes/gl/wxglslicecanvas.py#L44

shangjiaxuan commented 1 year ago

Typically I would assume GLCanvas to be HDC (Surface in egl/gles terms, or a Swapchain in modern graphics (dxgi directx (d3d10+) and vulkan) terms, Drawable in x11 terms). This maps to the Target concept in fsleyes. The OpenGL context is HGLRC or ID3D11Device+ID3D11DeviceContext or VkDevice+VkQueue equavalent, and is separate from window itself on most platforms.

Since wxGLCanvas inherits from wxWindow, the equavalent will be HWND+HDC on windows OpenGL. The event system is coupled into image output. This reflects MacOS CGL limitation of tying a context to a window. However, there are more in discrepancy than that:

For event system (Window): Windows window can only process events on the creating thread, X11 window can do those on any thread, while Cocoa window can only process events from main thread GCD.

For image output (SwapChain) relationship with event system (Window): On windows and x11 this seems to seperable, on MacOS this seems to be done in Cocoa Views, which I don't quite understand.

For image output (SwapChain) relationship with context (Device): WGL and GLX can use the same context for all image output, CGL context is always bound to one specific image output.

These discrepancies seems to have made its way to upper wx functions?

pauldmccarthy commented 1 year ago

Hi @shangjiaxuan, apologies for the delay (busy week). Can I also ask how you have installed FSLeyes?

I think you are probably correct in that the behaviour differs across platforms. I'm not sure if this should be considered a bug in wxPython/wxWidgets, as OpenGL context creation/management is an inherently platform-specific process. And I certainly have no objection to adding a work-around to the FSLeyes codebase, although I don't have access to a Windows machine, and my naive attempts to install FSLeyes on an Amazon EC2 were unsuccessful due to the absence of a modern GL driver.

The process that FSLeyes uses to initialise the GL context is as follows:

  1. A "dummy" wx.Frame and wx.glcanvas.GLCanvas are created - these are used solely for the purposes of creating a GL context
  2. A wx.glcanavs.GLContext is created, using the dummy GLCanvas from step 1.
  3. The dummy GLCanvas is set as the rendering target (this happens here)
  4. The dummy wx.Frame is hidden, but kept alive, for the duration of execution.

On my personal machine (Ubuntu 22.04, EGL), OpenGL.EGL.eglGetCurrentContext.address returns NULL before setting the rendering target in step 3, and then non-NULL immediately afterwards. Would you be able to perform the same check on your own system?

def getctx():
    import OpenGL.EGL as egl
    try:
        return str(egl.eglGetCurrentContext().address)
    except ValueError:
        return 'NULL'
...
print(getctx())  # prints NULL
self.__context.SetCurrent(self.__canvas) # line 967, linked above in step 3
print(getctx())  # prints non-NULL
...
shangjiaxuan commented 1 year ago

I installed fsleyes using pip install fsleyes. On windows new wxPython installation does not resolve two dependencies and need to be manually installed: attrdict3 requests.

The context creation as mentioned was successful (first call to getContext succeeds and sets the context). It's when later after overlay info are loaded, when the initializing a specific overlay (orthopanel in this case for new setup), that the error happens. Context creation is called only once.

I added lots of logging in the code and saw that context creation was successful. It's after creating the "view subwindow" that the context gets "reset" to NULL. This NULL persists until the error is thrown. This is the reason I referenced the line

https://github.com/pauldmccarthy/fsleyes/blob/9a01ff62c6f1e6ad746092dc5045baefc6760e12/fsleyes/gl/wxglslicecanvas.py#L44

instead of the first context creation. It's instantiating a specific subview, and context gets reset to NULL in the process.

sercharpak commented 1 year ago

Did you manage to solve this @shangjiaxuan ? If so could you share in a step by step process what changes were needed?

shangjiaxuan commented 1 year ago

@sercharpak This is more of a workaround. You need to apply the following two changes;

  1. Restore the gl context: https://github.com/pauldmccarthy/fsleyes/issues/107#issuecomment-1484443649
  2. Force the restore to happen when in loading screen: https://github.com/pauldmccarthy/fsleyes/issues/107#issue-1640417627