__init__.py 49.2 KB
Newer Older
Paul McCarthy's avatar
Paul McCarthy committed
1
2
#!/usr/bin/env python
#
3
# __init__.py - OpenGL data and rendering for FSLeyes.
Paul McCarthy's avatar
Paul McCarthy committed
4
5
6
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
7
8
9
10
"""This package contains the OpenGL data and rendering stuff for *FSLeyes*.
On-screen and off-screen rendering is supported, and two OpenGL versions (1.4
and 2.1) are supported.  The contents of this package can be broadly
categorised into the following:
Paul McCarthy's avatar
Paul McCarthy committed
11

12
 - *Canvases*: A canvas is a thing that can be drawn on.
Paul McCarthy's avatar
Paul McCarthy committed
13

14
 - *Objects*:  An object is a thing which can be drawn on a canvas.
Paul McCarthy's avatar
Paul McCarthy committed
15
16


17
18
19
-----------
Quick start
-----------
Paul McCarthy's avatar
Paul McCarthy committed
20

21
::
Paul McCarthy's avatar
Paul McCarthy committed
22

23
    import fsleyes.gl                 as fslgl
24
    import fsleyes.gl.wxglslicecanvas as slicecanvas
Paul McCarthy's avatar
Paul McCarthy committed
25

26
27
28
    # This function will be called when
    # the GL context is ready to be used.
    def ready():
Paul McCarthy's avatar
Paul McCarthy committed
29

30
31
32
33
34
        # The fsleyes.gl package needs to do
        # some initialisation that can only
        # be performed once a GL context has
        # been created.
        fslgl.bootstrap()
Paul McCarthy's avatar
Paul McCarthy committed
35

36
37
38
39
40
41
42
43
44
45
        # Once a GL context has been created,
        # you can do stuff! The SliceCanvas
        # will take care of creating and
        # managing GLObjects for each overlay
        # in the overlay list.
        canvas = slicecanvas.WXGLSliceCanvas(parent, overlayList, displayCtx)

    # Create a GL context, and tell it to
    # call our function when it is ready.
    fslgl.getGLContext(ready=ready)
Paul McCarthy's avatar
Paul McCarthy committed
46

47
48
49
50
51
52
53
    # ...

    # When you're finished, call the shutdown
    # function to clear the context (only
    # necessary for on-screen rendering)
    fslgl.shutdown()

Paul McCarthy's avatar
Paul McCarthy committed
54

55
56
57
58
59
60
61
62
63
64
65
--------
Canvases
--------


A *canvas* is the destination for an OpenGL scene render. The following
canvases are defined in the ``gl`` package:

.. autosummary::
   :nosignatures:

66
67
   ~fsleyes.gl.slicecanvas.SliceCanvas
   ~fsleyes.gl.lightboxcanvas.LightBoxCanvas
68
   ~fsleyes.gl.scene3dcanvas.Scene3DCanvas
69
   ~fsleyes.gl.colourbarcanvas.ColourBarCanvas
70
71
72
73
74
75
76
77


These classes are not intended to be used directly. This is because the ``gl``
package has been written to support two primary use-cases:

  - *On-screen* display of a scene using a :class:`wx.glcanvas.GLCanvas`
    canvas.

78
  - *Off-screen* rendering of a scene to a file.
79
80
81
82
83
84
85
86
87
88
89


Because of this, the canvas classes listed above are not dependent upon the
OpenGL environment in which they are used (i.e. on-screen or off-screen).
Instead, two base classes are provided for each of the use-cases:


.. autosummary::
   :nosignatures:

   WXGLCanvasTarget
90
   OffScreenCanvasTarget
91
92
93
94
95
96
97
98


And the following sub-classes are defined, providing use-case specific
implementations for each of the available canvases:

.. autosummary::
   :nosignatures:

99
100
   ~fsleyes.gl.wxglslicecanvas.WXGLSliceCanvas
   ~fsleyes.gl.wxgllightboxcanvas.WXGLLightBoxCanvas
101
   ~fsleyes.gl.wxglscene3dcanvas.WXGLScene3DCanvas
102
   ~fsleyes.gl.wxglcolourbarcanvas.WXGLColourBarCanvas
103
104
   ~fsleyes.gl.offscreenslicecanvas.OffScreenSliceCanvas
   ~fsleyes.gl.offscreenlightboxcanvas.OffScreenLightBoxCanvas
105
   ~fsleyes.gl.offscreenscene3dcanvas.OffScreenScene3DCanvas
106
   ~fsleyes.gl.offscreencolourbarcanvas.OffScreenColourBarCanvas
107
108
109
110
111
112
113
114
115
116
117
118
119
120


The classes listed above are the ones which are intended to be instantiated
and used by application code.


--------------
``gl`` objects
--------------


With the exception of the :class:`.ColourBarCanvas`, everything that is drawn
on a canvas derives from the :class:`.GLObject` base class. A ``GLObject``
manages the underlying data structures, GL resources (e.g. shaders and
121
122
123
124
textures), and rendering routines required to draw an object, in 2D or 3D, to a
canvas.  The following ``GLObject`` sub-classes correspond to each of the
possible types (the :attr:`.Display.overlayType` property) that an overlay can
be displayed as:
125
126
127

.. autosummary::

128
   ~fsleyes.gl.glvolume.GLVolume
129
   ~fsleyes.gl.glrgbvolume.GLRGBVolume
130
   ~fsleyes.gl.glcomplex.GLComplex
131
132
133
134
   ~fsleyes.gl.glmask.GLMask
   ~fsleyes.gl.gllabel.GLLabel
   ~fsleyes.gl.gllinevector.GLLineVector
   ~fsleyes.gl.glrgbvector.GLRGBVector
135
   ~fsleyes.gl.glmesh.GLMesh
Paul McCarthy's avatar
Paul McCarthy committed
136
   ~fsleyes.gl.glmip.GLMIP
137
   ~fsleyes.gl.gltensor.GLTensor
138
   ~fsleyes.gl.glsh.GLSH
139
   ~fsleyes.gl.gltractogram.GLTractogram
140

141
142

These objects are created and destroyed automatically by the canvas classes
143
instances, so application code does not need to worry about them too much.
Paul McCarthy's avatar
Paul McCarthy committed
144
145


146
147
148
149
150
===========
Annotations
===========


151
152
153
154
155
Canvases can be *annotated* in a few ways, by use of the :class:`.Annotations`
class. An ``Annotations`` object allows lines, rectangles, and other simple
shapes to be rendered on top of the ``GLObject`` renderings which represent
the overlays in the :class:`.OverlayList`.  The ``Annotations`` object for a
canvas instance can be accessed through its ``getAnnotations`` method.
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170


---------------------------------
OpenGL versions and bootstrapping
---------------------------------


*FSLeyes* needs to be able to run in restricted environments, such as within a
VNC session, and over SSH. In such environments the available OpenGL version
could be quite old, so the ``gl`` package has been written to support an
environment as old as OpenGL 1.4.


The available OpenGL API version can only be determined once an OpenGL context
has been created, and a display is available for rendering. The package-level
171
:func:`getGLContext` function allows a context to be created.
172
173
174
175
176
177
178
179
180
181
182


The data structures and rendering logic for some ``GLObject`` classes differs
depending on the OpenGL version that is available. Therefore, the code for
these ``GLObject`` classes may be duplicated, with one version for OpenGL 1.4,
and another version for OpenGL 2.1.  The ``GLObject`` code which targets a
specific OpenGL version lives within either the :mod:`.gl14` or :mod:`.gl21`
sub-packages.


Because of this, the package-level :func:`bootstrap` function must be called
183
before any ``GLObject`` instances are created, but *after* a GL context has
Paul McCarthy's avatar
Paul McCarthy committed
184
been created.
Paul McCarthy's avatar
Paul McCarthy committed
185
186
"""

187

Paul McCarthy's avatar
Paul McCarthy committed
188
import os
189
import sys
190
import logging
191
import platform
Paul McCarthy's avatar
Paul McCarthy committed
192

193
import fsl.utils.idle                     as idle
194
import fsl.version                        as fslversion
195
from   fsl.utils.platform import platform as fslplatform
Paul McCarthy's avatar
Paul McCarthy committed
196
import fsleyes_widgets                    as fwidgets
197
import fsleyes.gl.highdpi                 as highdpi
198

Paul McCarthy's avatar
Paul McCarthy committed
199
200
201
202

log = logging.getLogger(__name__)


203
import OpenGL  # noqa
Paul McCarthy's avatar
Paul McCarthy committed
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221


# Make PyOpenGL throw an error, instead of implicitly
# converting, if we pass incorrect types to OpenGL functions.
OpenGL.ERROR_ON_COPY = True


# These flags should be set to True
# for development, False for production
OpenGL.ERROR_CHECKING = True
OpenGL.ERROR_LOGGING  = True


# If FULL_LOGGING is enabled,
# every GL call will be logged.
# OpenGL.FULL_LOGGING   = True


222
223
224
225
226
227
228
229
GL_VERSION = None
"""Set in :func:`bootstrap`. String containing the available "major.minor"
OpenGL version.
"""


GL_COMPATIBILITY = None
"""Set in :func:`bootstrap`. String containing the target "major.minor"
230
OpenGL compatibility version ("1.4", "2.1", or "3.3").
231
232
233
234
235
236
237
238
239
240
"""


GL_RENDERER = None
"""Set in :func:`bootstrap`. Contains a description of the OpenGL renderer in
 use.
"""


def _selectPyOpenGLPlatform():
241
242
243
244
245
246
247
248
249
250
    """Pyopengl sometimes doesn't select a suitable platform, so in some
    circumstances we need to force things (but not if ``PYOPENGL_PLATFORM``
    is already set in the environment).
    """
    if 'PYOPENGL_PLATFORM' in os.environ:
        return

    # If no display, osmesa on all platforms
    if not fwidgets.canHaveGui():
        os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
        return

    # We only need special handling on linux
    if not fslplatform.os.lower() == 'linux':
        return

    wxver  = fwidgets.wxVersion()
    wxplat = fwidgets.wxPlatform()

    # Don't know what version of wxpython
    # we have - don't do anything
    if wxver is None:
        return

    # GTK3 versions of wxpython>=4.1.1 use EGL
    # for GL initialisation. All older wxpython
    # versions, and all GTK2 versions, use GLX.
    #
    # PyOpenGL>=3.1.6 will use EGL if it detects
    # that it is running under Wayland.  But this
    # breaks things if we have a GTK2 wxpython.
    if wxplat == fwidgets.WX_GTK2:
        os.environ['PYOPENGL_PLATFORM'] = 'x11'
    elif fslversion.compareVersions(wxver, '4.1.1') >= 0:
        os.environ['PYOPENGL_PLATFORM'] = 'egl'
276
277


278
279
280
281
282
283
284
285
286
287
288
289
_selectPyOpenGLPlatform()


def glIsSoftwareRenderer():
    """Returns ``True`` if the OpenGL renderer appears to be software based,
    ``False`` otherwise, or ``None`` :func:`bootstrap` has not been called yet.

    .. note:: This check is based on heuristics, ans is not guaranteed to
              be correct.
    """
    if GL_RENDERER is None:
        return None
290

291
292
293
294
295
    # There doesn't seem to be any quantitative
    # method for determining whether we are using
    # software-based rendering, so a hack is
    # necessary.
    renderer = GL_RENDERER.lower()
296
297
298
299
300
301
302
303
304

    # "software" / "chromium" -> software renderer
    # But SVGA3D/llvmpipe are super fast, so if
    # we're using either of them, pretend that
    # we're on hardware
    sw     = any(('software' in renderer, 'chromium' in renderer))
    fastsw = any(('llvmpipe' in renderer, 'svga3d'   in renderer))

    return sw and (not fastsw)
305
306


307
def hasExtension(ext, glver=None):
308
    """Wrapper around ``OpenGL.extensions.hasExtension``. Short-circuits
309
310
311
312
    and always returns ``True`` if the OpenGL version in use is >= 3.3.

    If ``glver`` is not provided, it is set to the value of
    ``GL_COMPATIBILITY``.
313
    """
314
315
316
    if glver is None:
        glver = float(GL_COMPATIBILITY)
    if glver >= 3.3:
317
318
319
320
321
        return True
    import OpenGL.extensions as glexts
    return glexts.hasExtension(ext)


322
def bootstrap(glVersion=None):
Paul McCarthy's avatar
Paul McCarthy committed
323
324
325
326
327
328
329
    """Imports modules appropriate to the specified OpenGL version.

    The available OpenGL API version can only be queried once an OpenGL
    context is created, and a canvas is available to draw on. This makes
    things a bit complicated, because it means that we are only able to
    choose how to draw things when we actually need to draw them.

330

Paul McCarthy's avatar
Paul McCarthy committed
331
332
333
334
    This function should be called after an OpenGL context has been created,
    and a canvas is available for drawing, but before any attempt to draw
    anything.  It will figure out which version-dependent package needs to be
    loaded, and will attach all of the modules contained in said package to
335
    the :mod:`~fsleyes.gl` package.  The version-independent modules may
Paul McCarthy's avatar
Paul McCarthy committed
336
337
    then simply access these version-dependent modules through this module.

338
339

    After the :func:`boostrap` function has been called, the following
340
    package-level attributes will be available on the ``gl`` package:
341
342
343


    ====================== ====================================================
344
    ``GL_COMPATIBILITY``   A string containing the target OpenGL version, in
345
346
                           the format ``major.minor``, e.g. ``2.1``.

347
348
    ``GL_VERSION``         A string containing the available OpenGL version.

349
350
    ``GL_RENDERER``        A string containing the name of the OpenGL renderer.

351
352
353
    ``glvolume_funcs``     The version-specific module containing functions for
                           rendering :class:`.GLVolume` instances.

354
355
356
    ``glrgbvolume_funcs``  The version-specific module containing functions for
                           rendering :class:`.GLRGBVolume` instances.

357
358
359
360
361
362
    ``glrgbvector_funcs``  The version-specific module containing functions for
                           rendering :class:`.GLRGBVector` instances.

    ``gllinevector_funcs`` The version-specific module containing functions for
                           rendering :class:`.GLLineVector` instances.

363
364
    ``glmesh_funcs``       The version-specific module containing functions for
                           rendering :class:`.GLMesh` instances.
Paul McCarthy's avatar
Paul McCarthy committed
365

366
367
368
    ``glmask_funcs``       The version-specific module containing functions for
                           rendering :class:`.GLMask` instances.

369
    ``gllabel_funcs``      The version-specific module containing functions for
370
                           rendering :class:`.GLLabel` instances.
Paul McCarthy's avatar
Paul McCarthy committed
371

372
    ``gltensor_funcs``     The version-specific module containing functions for
373
                           rendering :class:`.GLTensor` instances.
Paul McCarthy's avatar
Paul McCarthy committed
374

375
    ``glsh_funcs``         The version-specific module containing functions for
Paul McCarthy's avatar
Paul McCarthy committed
376
                           rendering :class:`.GLSH` instances.
377
378
379

    ``glmip_funcs``        The version-specific module containing functions for
                           rendering :class:`.GLMIP` instances.
380
381
    ``gltractogram_funcs`` The version-specific module containing functions for
                           rendering :class:`.GLTractogram` instances.
382
    ====================== ====================================================
383
384


385
386
387
    :arg glVersion: A tuple containing the desired (major, minor) OpenGL API
                    version to use. If ``None``, the best possible API
                    version will be used.
Paul McCarthy's avatar
Paul McCarthy committed
388
389
    """

390
391
392
393
394
    import OpenGL.GL             as gl
    import fsleyes.gl.gl14       as gl14
    import fsleyes.gl.gl21       as gl21
    import fsleyes.gl.gl33       as gl33
    import fsleyes.gl.extensions as glexts
Paul McCarthy's avatar
Paul McCarthy committed
395
396
397
398

    thismod = sys.modules[__name__]

    if hasattr(thismod, '_bootstrapped'):
399
        return
Paul McCarthy's avatar
Paul McCarthy committed
400
401

    if glVersion is None:
402
403
        glver = gl.glGetString(gl.GL_VERSION).decode('latin1').split()[0]
        major, minor = [int(v) for v in glver.split('.')][:2]
Paul McCarthy's avatar
Paul McCarthy committed
404
405
406
    else:
        major, minor = glVersion

407
408
409
    # glVersion contains the actual GL version
    # verstr contains the target compatibility
    # GL version
410
    glVersion = major + minor / 10.0
411
    glpkg     = None
412
413
414
    if glVersion >= 3.3:
        verstr = '3.3'
        glpkg  = gl33
415
    elif glVersion >= 2.1:
Paul McCarthy's avatar
Paul McCarthy committed
416
417
        verstr = '2.1'
        glpkg  = gl21
418
    elif glVersion >= 1.4:
Paul McCarthy's avatar
Paul McCarthy committed
419
420
        verstr = '1.4'
        glpkg  = gl14
421
    else: raise RuntimeError('OpenGL 1.4 or newer is required '
Paul McCarthy's avatar
Paul McCarthy committed
422
                             '(detected version: {:0.1f})'.format(glVersion))
Paul McCarthy's avatar
Paul McCarthy committed
423
424
425
426

    # The gl21 implementation depends on a
    # few extensions - if they're not present,
    # fall back to the gl14 implementation
427
    if verstr == '2.1':
Paul McCarthy's avatar
Paul McCarthy committed
428
429

        # List any GL21 extensions here
430
431
        exts = ['GL_EXT_framebuffer_object',
                'GL_ARB_instanced_arrays',
432
                'GL_ARB_draw_instanced']
Paul McCarthy's avatar
Paul McCarthy committed
433

434
        if not all(hasExtension(e, 2.1) for e in exts):
435
436
437
            log.warning('One of these OpenGL extensions is '
                        'not available: [{}]. Falling back '
                        'to an older OpenGL implementation.'
Paul McCarthy's avatar
Paul McCarthy committed
438
                        .format(', '.join(exts)))
Paul McCarthy's avatar
Paul McCarthy committed
439
            verstr = '1.4'
440
            glpkg  = gl14
Paul McCarthy's avatar
Paul McCarthy committed
441
442
443
444

    # If using GL14, and the ARB_vertex_program
    # and ARB_fragment_program extensions are
    # not present, we're screwed.
445
    if verstr == '1.4':
Paul McCarthy's avatar
Paul McCarthy committed
446

Paul McCarthy's avatar
Paul McCarthy committed
447
448
        exts = ['GL_EXT_framebuffer_object',
                'GL_ARB_vertex_program',
449
450
                'GL_ARB_fragment_program',
                'GL_ARB_texture_non_power_of_two']
Paul McCarthy's avatar
Paul McCarthy committed
451

452
        if not all(hasExtension(e, 1.4) for e in exts):
Paul McCarthy's avatar
Paul McCarthy committed
453
454
455
456
457
            raise RuntimeError('One of these OpenGL extensions is '
                               'not available: [{}]. This software '
                               'cannot run on the available graphics '
                               'hardware.'.format(', '.join(exts)))

458
459
        # Tensor/SH/MIP/tractogram overlays
        # are not available in GL14
460
        import fsleyes.displaycontext as dc
461
462
        dc.ALL_OVERLAY_TYPES            .remove('tensor')
        dc.ALL_OVERLAY_TYPES            .remove('sh')
Paul McCarthy's avatar
Paul McCarthy committed
463
        dc.ALL_OVERLAY_TYPES            .remove('mip')
464
        dc.ALL_OVERLAY_TYPES            .remove('tractogram')
465
466
        dc.OVERLAY_TYPES['DTIFitTensor'].remove('tensor')
        dc.OVERLAY_TYPES['Image']       .remove('sh')
Paul McCarthy's avatar
Paul McCarthy committed
467
        dc.OVERLAY_TYPES['Image']       .remove('tensor')
Paul McCarthy's avatar
Paul McCarthy committed
468
        dc.OVERLAY_TYPES['Image']       .remove('mip')
Paul McCarthy's avatar
Paul McCarthy committed
469

470
    renderer = gl.glGetString(gl.GL_RENDERER).decode('latin1')
Paul McCarthy's avatar
Paul McCarthy committed
471
472
473
    log.debug('Using OpenGL {} implementation with renderer {}'.format(
        verstr, renderer))

474
475
476
477
478
479
480
481
482
483
484
485
486
    # Import GL version-specific sub-modules
    globjects = ['glvolume', 'glrgbvolume', 'glrgbvector', 'gllinevector',
                 'glmask', 'glmesh', 'gllabel', 'gltensor', 'glsh', 'glmip',
                 'gltractogram']

    for globj in globjects:
        modname = '{}_funcs'.format(globj)
        setattr(thismod, modname, getattr(glpkg, modname, None))

    thismod.GL_VERSION       = str(glVersion)
    thismod.GL_COMPATIBILITY = verstr
    thismod.GL_RENDERER      = renderer
    thismod._bootstrapped    = True
Paul McCarthy's avatar
Paul McCarthy committed
487

488
489
490
    # Initialise extensions module.
    glexts.initialise()

491
492
    # If we're using a software based renderer,
    # reduce the default performance settings
493
    if glIsSoftwareRenderer():
494
495
496
497
498

        log.debug('Software-based rendering detected - '
                  'lowering default performance settings.')

        import fsleyes.displaycontext as dc
499
        dc.SceneOpts.highDpi = False
500
501


502
def getGLContext(**kwargs):
503
504
    """Create and return a GL context object for on- or off-screen OpenGL
    rendering.
Paul McCarthy's avatar
Paul McCarthy committed
505
506

    If a context object has already been created, it is returned.
Paul McCarthy's avatar
Paul McCarthy committed
507
    Otherwise, one is created and returned.
Paul McCarthy's avatar
Paul McCarthy committed
508

509
    See the :class:`GLContext` class for details on the arguments.
510
511
512
513

    .. warning:: Use the ``ready`` argument to
                 :meth:`GLContext.__init__`, and don't call :func:`bootstrap`
                 until it has been called!
Paul McCarthy's avatar
Paul McCarthy committed
514
515
516
517
518
    """

    thismod = sys.modules[__name__]

    # A context has already been created
519
    if hasattr(thismod, '_glContext'):
520
521
522
523
524
525
526

        # If a callback was provided,
        # make sure it gets called.
        callback = kwargs.pop('ready', None)
        if callback is not None:
            idle.idle(callback)

527
        return thismod._glContext
Paul McCarthy's avatar
Paul McCarthy committed
528

529
    thismod._glContext = GLContext(**kwargs)
Paul McCarthy's avatar
Paul McCarthy committed
530

531
    return thismod._glContext
532

Paul McCarthy's avatar
Paul McCarthy committed
533

534
535
536
537
538
539
540
541
542
543
544
545
546
547
def shutdown():
    """Must be called when the GL rendering context is no longer needed.
    Destroys the context object, and resources associated with it.

    Does not need to be called for off-screen rendering.
    """

    thismod = sys.modules[__name__]
    context = getattr(thismod, '_glContext', None)
    if context is not None:
        context.destroy()
        delattr(thismod, '_glContext')


548
class GLContext:
549
550
551
    """The ``GLContext`` class manages the creation of, and access to, an
    OpenGL context. This class abstracts away the differences between
    creation of on-screen and off-screen rendering contexts.
552
553
554
555
    It contains two methods:

      - :meth:`setTarget`, which may be used to set a
        :class:`.WXGLCanvasTarget` or an :class:`OffScreenCanvasTarget` as the
Paul McCarthy's avatar
Paul McCarthy committed
556
        GL rendering target.
557
558
      - :meth:`destroy`, which must be called when the context is no longer
        needed.
559
560
561
562


    On-screen rendering is performed via the ``wx.GLCanvas.GLContext``
    context, whereas off-screen rendering is performed  via
563
    ``OpenGL.raw.osmesa.mesa`` (OSMesa is assumed to be available).
564
565
566
567
568
569
570
571
572
573


    If it is possible to do so, a ``wx.glcanvas.GLContext`` will be created,
    even if an off-screen context has been requested. This is because
    using the native graphics card is nearly always preferable to using
    OSMesa.


    *Creating an on-screen GL context*

Paul McCarthy's avatar
Paul McCarthy committed
574

575
    A ``wx.glcanvas.GLContext`` may only be created once a
576
577
578
    ``wx.glcanvas.GLCanvas`` has been created, and is visible on screen.  The
    ``GLContext`` class therefore creates a dummy ``wx.Frame`` and
    ``GLCanvas``, and displays it, before creating the ``wx`` GL context.
579
580


581
582
583
584
585
586
    A reference to this dummy ``wx.Frame`` is retained, because destroying it
    can result in ``GLXBadCurrentWindow`` errors when running on
    macOS+XQuartz. The frame is destroyed on calls to the ``GLContext.destroy``
    method.


587
    Because ``wx`` contexts may be used even when an off-screen rendering
588
589
590
591
592
593
    context has been requested, the ``GLContext`` class has the ability to
    create and run a temporary ``wx.App``, on which the canvas and context
    creation process is executed. This horrible ability is necessary, because
    a ``wx.GLContext`` must be created on a ``wx`` application loop. We cannot
    otherwise guarantee that the ``wx.GLCanvas`` will be visible before the
    ``wx.GLContext`` is created.
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609


    The above issue has the effect that the real underlying ``wx.GLContext``
    may only be created after the ``GLContext.__init__`` method has returned.
    Therefore, you must use the ``ready`` callback function if you are
    creating a ``wx`` GL context - this function will be called when the
    ``GLContext`` is ready to be used.


    You can get away without using the ``ready`` callback in the following
    situations:

      - When you are 100% sure that you will be using OSMesa.

      - When there is not (and never will be) a ``wx.MainLoop`` running, and
        you pass in ``createApp=True``.
610
    """
Paul McCarthy's avatar
Paul McCarthy committed
611

612
613
614
    def __init__(self,
                 offscreen=False,
                 other=None,
615
                 target=None,
616
                 requestVersion=None,
617
                 createApp=False,
618
619
                 ready=None,
                 raiseErrors=False):
620
621
        """Create a ``GLContext``.

622
        :arg offscreen:      On-screen or off-screen context?
Paul McCarthy's avatar
Paul McCarthy committed
623

624
625
        :arg other:          Another ``GLContext`` instance with which GL state
                             should be shared.
626

627
628
629
        :arg target:         If ``other`` is not ``None``, this must be a
                             reference to a ``WXGLCanvasTarget``, the rendering
                             target for the new context.
Paul McCarthy's avatar
Paul McCarthy committed
630

631
632
633
634
        :arg requestVersion: A tuple containing the desired (major, minor)
                             OpenGL API version to use. If ``None``, the best
                             possible API version will be used.a floating point
                             number.
635

636
637
638
        :arg createApp:      If ``True``, and if possible, this ``GLContext``
                             will create and run a ``wx.App`` so that it can
                             create a ``wx.glcanvas.GLContext``.
639

640
641
642
643
644
645
        :arg ready:          Function which will be called when the context has
                             been created and is ready to use.

        :are raiseErrors:    Defaults to ``False``. If ``True``, and if the
                             ``ready`` function raises an error, that error is
                             not caught.
646
        """
647

648
649
650
651
652
653
        def defaultReady():
            pass

        if ready is None:
            ready = defaultReady

654
655
656
657
658
        self.__offscreen = offscreen
        self.__ownApp    = False
        self.__context   = None
        self.__canvas    = None
        self.__parent    = None
659
        self.__buffer    = None
660
        self.__app       = None
661

662
663
664
        if requestVersion is not None:
            requestVersion = tuple(requestVersion)

665
        osmesa     = os.environ.get('PYOPENGL_PLATFORM', None) == 'osmesa'
666
667
        canHaveGui = fwidgets.canHaveGui()
        haveGui    = fwidgets.haveGui()
668
669

        # On-screen contexts *must* be
670
671
        # created via a wx event loop
        if (not offscreen) and not (haveGui or createApp):
672
673
674
            raise ValueError('On-screen GL contexts must be '
                             'created on the wx.MainLoop')

675
        # For off-screen, only use OSMesa
676
        # if we have no cnoice, or if
677
        # dictated by PYOPENGL_PLATFORM.
678
679
        # Otherewise we use wx if possible.
        if offscreen and (osmesa or (not canHaveGui)):
680
681
            self.__createOSMesaContext()
            ready()
682
            return
683

684
        self.__ownApp = (not haveGui) and createApp
685

686
687
688
689
        # A context already exists - we don't
        # need to create a GL canvas to create
        # another one.
        if other is not None:
690
691
692
            self.__createWXGLContext(other=other.__context,
                                     target=target,
                                     requestVersion=requestVersion)
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
            ready()
            return

        # Create a wx.App if we've been
        # given permission to do so
        # (via the createApp argument)
        if self.__ownApp:
            log.debug('Creating temporary wx.App')

            import fsleyes.main as fm
            self.__app = fm.FSLeyesApp()

        # Create a parent for the GL
        # canvas, and the canvas itself
        self.__createWXGLParent()
        self.__createWXGLCanvas()

        # This function creates the context
        # and does some clean-up afterwards.
        # It gets scheduled on the wx idle
        # loop.
        def create():

Paul McCarthy's avatar
Paul McCarthy committed
716
717
            app = self.__app

718
            self.__createWXGLContext(requestVersion=requestVersion)
719
720
721

            # If we've created and started
            # our own loop, kill it
722
            if self.__ownApp:
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
                log.debug('Exiting temporary wx.MainLoop')
                app.ExitMainLoop()

            if ready is not None:

                try:
                    ready()

                except Exception as e:
                    log.warning('GLContext callback function raised '
                                '{}: {}'.format(type(e).__name__,
                                                str(e)),
                                                exc_info=True)
                    if raiseErrors:
                        raise e

739
740
741
742
743
744
            # We keep the parent around, because
            # destroying it can cause GLXBadCurrentWindow
            # errors when running on macOS+XQuartz. It is
            # destroyed when the GLContext.destroy() method
            # is called.
            self.__parent.Hide()
745

746
747
748
749
750
751
752
753
754
755
756
757
758
        # If we've created our own wx.App, run its
        # main loop - we need to run the loop
        # in order to display the GL canvas and
        # context. But we can kill the loop as soon
        # as this is done (in the create function
        # above).  If an existing wx.App is running,
        # we just schedule the context creation
        # routine on it.
        idle.idle(create, alwaysQueue=True)

        if self.__ownApp:
            log.debug('Starting temporary wx.MainLoop')
            self.__app.MainLoop()
759

760

761
762
763
764
765
    def destroy(self):
        """Called by the module-level :func:`shutdown` function. If this is an
        on-screen context, the dummy canvas and frame that were created at
        initialisation are destroyed.
        """
766

767
768
769
770
771
772
        # We need to destroy the OSMesa context,
        # otherwise it will stay in memory
        if self.__buffer is not None:
            import OpenGL.raw.osmesa.mesa as osmesa
            osmesa.OSMesaDestroyContext(self.__context)

773
774
775
776
777
        # Clear refs to wx frame/canvas
        # before destroying the wx.App,
        # as problems can otherwise occur
        app            = self.__app
        self.__app     = None
778
        self.__context = None
779
        self.__buffer  = None
780
781
        self.__parent  = None
        self.__canvas  = None
782
783
784

        if app is not None:
            app.Destroy()
785
786
787


    def setTarget(self, target=None):
788
789
        """Set the given ``WXGLCanvasTarget`` or ``OffScreenCanvasTarget`` as
        the target for GL rendering with this context.
790
791
792
793

        If ``target`` is None, and this is an on-screen rendering context,
        the dummy ``wx.glcanvas.GLCanvas`` that was used to create this
        ``GLContext`` is set as the rendering target.
794
        """
795
796
        # not necessary for offscreen rendering
        if self.__offscreen:
797
            return True
798

799
800
        # destroy() has been called
        if self.__context is None:
801
            return False
802

803
        if target is None and self.__canvas is not None:
804
            return self.__context.SetCurrent(self.__canvas)
805
806
807
808

        else:
            import wx.glcanvas as wxgl
            if isinstance(target, wxgl.GLCanvas):
809
                return self.__context.SetCurrent(target)
810

Paul McCarthy's avatar
Paul McCarthy committed
811

812
813
814
    def __createWXGLParent(self):
        """Create a dummy ``wx.Frame`` to be used as the parent for the
        dummy ``wx.glcanvas.GLCanvas``.
815
        """
816

817
        import wx
818
819
820
821
822
823
824
825
826
827
828
829
830

        # Override ShouldPreventAppExit, meaning
        # that the wx.App.MainLoop will exit even
        # if a DummyFrame still exists. The wx
        # equivalent of marking a thread as a
        # daemon.
        class DummyFrame(wx.Frame):
            def ShouldPreventAppExit(self):
                return False

        log.debug('Creating dummy wx.Frame for GL context creation')

        self.__parent = DummyFrame(None, style=0)
831
832
        self.__parent.SetSize((0, 0))
        self.__parent.Show(True)
Paul McCarthy's avatar
Paul McCarthy committed
833

834

835
836
837
838
839
    def __createWXGLCanvas(self):
        """Create a dummy ``wx.glcanvas.GLCanvas`` instance which is to
        be used to create a context. Assigns the canvas to an attributed
        called ``__canvas``.
        """
840

841
        import wx.glcanvas as wxgl
842

843
844
        log.debug('Creating temporary wx.GLCanvas')

845
846
847
848
849
850
851
852
853
        # There's something wrong with wxPython's
        # GLCanvas (on OSX at least) - the pixel
        # format attributes have to be set on the
        # *first* GLCanvas that is created -
        # setting them on subsequent canvases will
        # have no effect. But if you set them on
        # the first canvas, all canvases that are
        # subsequently created will inherit the
        # same properties.
854
        attrs = WXGLCanvasTarget.displayAttributes()
855

856
857
858
859
        # GLCanvas initialisation with an attribute
        # list fails when running in a nomachine-like
        # remote desktop session. No idea why.
        try:
860
            self.__canvas = wxgl.GLCanvas(self.__parent, **attrs)
861
            self.__canvas.SetSize((0, 0))
862
863
864
865
866

        # Creating without attribute list works ok
        # though. This does mean that we don't have
        # control over depth/stencil buffer sizes,
        # under these remote desktop environments.
867
        except Exception:
868
            WXGLCanvasTarget.displayAttributes.disabled = True
869
            self.__canvas = wxgl.GLCanvas(self.__parent)
870
            self.__canvas.SetSize((0, 0))
871

872
        self.__canvas.Show(True)
873
874


875
876
877
878
    def __createWXGLContext(self,
                            other=None,
                            target=None,
                            requestVersion=None):
879
880
881
        """Creates a ``wx.glcanvas.GLContext`` object, assigning it to
        an attribute called ``__context``. Assumes that a
        ``wx.glcanvas.GLCanvas`` has already been created.
882

883
884
885
886
887
888
        :arg other:          Another `wx.glcanvas.GLContext`` instance with
                             which the new context should share GL state.

        :arg target:         If ``other`` is not ``None``, this must be a
                             ``wx.glcanvas.GLCanvas``, the rendering target
                             for the new context.
889

890
891
892
893
        :arg requestVersion: A tuple containing the desired (major, minor)
                             OpenGL API version to use. If ``None``, the best
                             possible API version will be used.a floating point
                             number.
894
895

        .. warning:: This method *must* be called via the ``wx.MainLoop``.
896
        """
897

898
        import                wx
899
        import wx.glcanvas as wxgl
900

901
        # Creating a GL context is a pain for various reasons.
902
        #
903
        #  1. wxPython only supports requesting core/
904
905
        #     compatiblity profiles from 4.1.1 onwards.
        #  2. In 4.1.1, the GLContext interface changed
906
907
908
909
910
911
912
913
914
915
916
917
        #     so that we have to create and pass a
        #     GLContextAttrs object
        #  3. Step 2 works on macOS, but on linux only
        #     seems to work with GTK3/wayland/EGL builds
        #     of wxpython.
        #  4. In order to find out whether we can use
        #     OpenGL 3.3, we have to create a GLContext,
        #     and check whether it isOK().
        #
        # For older/non-GTK3 builds, we can't request a
        # particular profile, so we create a GLContext
        # with no arguments, and just hope.
918
919
        candidates = []
        wxver      = fwidgets.wxVersion()
920
        wxplat     = fwidgets.wxPlatform()
921

922
923
        if wxplat in (fwidgets.WX_GTK3, fwidgets.WX_MAC_COCOA) and \
           wxver is not None                                   and \
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
           fslversion.compareVersions(wxver, '4.1.1') >= 0:
            # Request 3.3 core profile unless caller
            # has requested an older version.
            if requestVersion is None or requestVersion >= (3, 3):
                coreAttrs = wxgl.GLContextAttrs()
                coreAttrs.OGLVersion(3, 3)
                coreAttrs.CoreProfile()
                coreAttrs.EndList()
                candidates.append({'ctxAttrs' : coreAttrs})
            # Request compat profile if we
            # can't request a core profile.
            compatAttrs = wxgl.GLContextAttrs()
            compatAttrs.CompatibilityProfile()
            compatAttrs.EndList()
            candidates.append({'ctxAttrs' : compatAttrs})

        # Fall back for older wxPython where we
        # can't use a GLContextAttrs object. In
        # this case, we get what we're given.
        candidates.append({})
944

945
946
        log.debug('Creating wx.GLContext')

947
        for  candidate in candidates:
948
949
950
951
            if other is not None:
                ctx = wxgl.GLContext(target, other=other, **candidate)
            else:
                ctx = wxgl.GLContext(self.__canvas, **candidate)
952

953
954
            if not hasattr(ctx, 'IsOK'):
                break
955
956
            if ctx.IsOK():
                break
957
        else:
958
959
960
            raise RuntimeError('Cannot create GL context')

        self.__context = ctx
961

962
963
964
965
966
        # We can't set the context target
        # until the dummy canvas is
        # physically shown on the screen.
        while not self.__canvas.IsShownOnScreen():
            wx.GetApp().Yield()
967

968
        self.__context.SetCurrent(self.__canvas)
969

970

971
972
973
974
    def __createOSMesaContext(self):
        """Creates an OSMesa context, assigning it to an attribute called
        ``__context``.
        """
Paul McCarthy's avatar
Paul McCarthy committed
975

976
977
978
        import OpenGL.GL              as gl
        import OpenGL.raw.osmesa.mesa as osmesa
        import OpenGL.arrays          as glarrays
Paul McCarthy's avatar
Paul McCarthy committed
979

980
981
        log.debug('Creating gl.OSMesaContext')

982
983
984
        # We have to create a dummy buffer
        # for the off-screen context.
        buffer  = glarrays.GLubyteArray.zeros((640, 480, 4))
985
        context = osmesa.OSMesaCreateContextExt(
Paul McCarthy's avatar
Paul McCarthy committed
986
            gl.GL_RGBA, 8, 4, 0, None)
987
988
        osmesa.OSMesaMakeCurrent(context,
                                 buffer,
Paul McCarthy's avatar
Paul McCarthy committed
989
990
                                 gl.GL_UNSIGNED_BYTE,
                                 640,
991
                                 480)
Paul McCarthy's avatar
Paul McCarthy committed
992

993
994
        self.__buffer  = buffer
        self.__context = context
Paul McCarthy's avatar
Paul McCarthy committed
995
996


997
class OffScreenCanvasTarget:
998
    """Base class for canvas objects which support off-screen rendering. """
Paul McCarthy's avatar
Paul McCarthy committed
999

1000
    def __init__(self, width, height):
For faster browsing, not all history is shown. View entire blame