Skip to content
Snippets Groups Projects
Commit 02246487 authored by Paul McCarthy's avatar Paul McCarthy
Browse files

GL object generation is now performed in an wx.EVT_IDLE event handler,

via a new function async.idle (which abstracts away wx-iness). This
means that the status-bar is actually updated during startup.
parent b81ce5a4
No related branches found
No related tags found
No related merge requests found
......@@ -18,6 +18,7 @@ import props
import fsl.data.image as fslimage
import fsl.utils.status as status
import fsl.utils.async as async
import fsl.fsleyes.displaycontext as fsldisplay
import fsl.fsleyes.displaycontext.canvasopts as canvasopts
import fsl.fsleyes.gl.routines as glroutines
......@@ -719,13 +720,20 @@ class SliceCanvas(props.HasProperties):
self._refresh()
def __genGLObject(self, overlay, updateRenderTextures=True):
def __genGLObject(self, overlay, updateRenderTextures=True, refresh=True):
"""Creates a :class:`.GLObject` instance for the given ``overlay``,
destroying any existing instance.
If ``updateRenderTextures`` is ``True`` (the default), and the
:attr:`.renderMode` is ``offscreen`` or ``prerender``, any
render texture associated with the overlay is destroyed.
If ``refresh`` is ``True`` (the default), the :meth:`_refresh` method
is called after the ``GLObject`` has been created.
.. note:: If running in ``wx`` (i.e. via a :class:`.WXGLSliceCanvas`),
the :class:`.GLObject` instnace will be created on the
``wx.EVT_IDLE`` lopp (via the :mod:`.idle` module).
"""
display = self.displayCtx.getDisplay(overlay)
......@@ -748,58 +756,63 @@ class SliceCanvas(props.HasProperties):
if tex is not None:
glresources.delete(name)
# We need a GL context to create a new GL
# object. If we can't get it now, the
# _glObjects value for this overlay will
# stay as None, and the _draw method will
# manually call this method again later.
if not self._setGLContext():
return None
def create():
status.update('Creating GL representation '
'for {}...'.format(overlay.name))
# We need a GL context to create a new GL
# object. If we can't get it now, we simply
# reschedule this function to be run later
# on.
if not self._setGLContext():
async.idle(create)
return
globj = globject.createGLObject(overlay, display)
if globj is not None:
globj.setAxes(self.xax, self.yax)
globj.register(self.name, self._refresh)
self._glObjects[overlay] = globj
if updateRenderTextures:
self._updateRenderTextures()
display.addListener('overlayType',
self.name,
self.__overlayTypeChanged,
overwrite=True)
display.addListener('enabled',
self.name,
self._refresh,
overwrite=True)
# Listen for resolution changes on Image
# overlays - see __overlayResolutionChanged,
# and __resolutionLimitChanged
if isinstance(overlay, fslimage.Nifti1):
opts = display.getDisplayOpts()
opts.addListener('resolution',
self.name,
self.__overlayResolutionChanged,
overwrite=True)
return globj
status.update('Creating GL representation '
'for {}...'.format(overlay.name))
def _overlayListChanged(self, *a):
globj = globject.createGLObject(overlay, display)
if globj is not None:
globj.setAxes(self.xax, self.yax)
globj.register(self.name, self._refresh)
self._glObjects[overlay] = globj
if updateRenderTextures:
self._updateRenderTextures()
display.addListener('overlayType',
self.name,
self.__overlayTypeChanged,
overwrite=True)
display.addListener('enabled',
self.name,
self._refresh,
overwrite=True)
# Listen for resolution changes on Image
# overlays - see __overlayResolutionChanged,
# and __resolutionLimitChanged
if isinstance(overlay, fslimage.Nifti1):
opts = display.getDisplayOpts()
opts.addListener('resolution',
self.name,
self.__overlayResolutionChanged,
overwrite=True)
if refresh:
self._refresh()
async.idle(create)
def _overlayListChanged(self, *args, **kwargs):
"""This method is called every time an overlay is added or removed
to/from the overlay list.
For newly added overlays, it creates the appropriate :mod:`.GLObject`
type, which initialises the OpenGL data necessary to render the
overlay, and then triggers a refresh.
For newly added overlays, calls the :meth:`__genGLObject` method,
which initialises the OpenGL data necessary to render the
overlay.
"""
# Destroy any GL objects for overlays
......@@ -821,11 +834,20 @@ class SliceCanvas(props.HasProperties):
if overlay in self._glObjects:
continue
self.__genGLObject(overlay, updateRenderTextures=False)
self.__genGLObject(overlay,
updateRenderTextures=False,
refresh=False)
# All the GLObjects are created using
# async.idle, so we call refresh in the
# same way to make sure it gets called
# after all the GLObject creations.
def refresh():
self._updateRenderTextures()
self.__resolutionLimitChange()
self._refresh()
self._updateRenderTextures()
self.__resolutionLimitChange()
self._refresh()
async.idle(refresh)
def _overlayBoundsChanged(self, *a):
......@@ -1146,14 +1168,10 @@ class SliceCanvas(props.HasProperties):
continue
if globj is None:
globj = self.__genGLObject(overlay)
# We can't generate a GLObject for
# this overlay, for some reason
if globj is None:
log.warning('Cannot generate a GL representation '
'for overlay {}!'.format(overlay))
continue
# The GLObject has not been created
# yet - we assume here that the
# __genGLObject method is on the case
continue
# The GLObject is not ready
# to be drawn yet.
......
......@@ -252,8 +252,10 @@ class ImageTexture(texture.Texture, notifier.Notifier):
All other arguments are ignored.
"""
# The texture is already
# being refreshed - ignore
if not self.__ready:
raise RuntimeError('Texture already being refreshed')
return
refreshData = kwargs.get('refreshData', True)
refreshRange = kwargs.get('refreshRange', True)
......
......@@ -237,12 +237,16 @@ class RenderTextureStack(object):
:arg idx: Index of the ``RenderTexture``.
"""
zpos = self.__indexToZpos(idx)
xax = self.__xax
yax = self.__yax
globj = self.__globj
zpos = self.__indexToZpos(idx)
xax = self.__xax
yax = self.__yax
lo, hi = self.__globj.getDisplayBounds()
res = self.__globj.getDataResolution(xax, yax)
if not globj.ready():
return
lo, hi = globj.getDisplayBounds()
res = globj.getDataResolution(xax, yax)
if res is not None:
width = res[xax]
......@@ -268,9 +272,9 @@ class RenderTextureStack(object):
glroutines.show2D(xax, yax, width, height, lo, hi)
glroutines.clear((0, 0, 0, 0))
self.__globj.preDraw()
self.__globj.draw(zpos)
self.__globj.postDraw()
globj.preDraw()
globj.draw(zpos)
globj.postDraw()
tex.unbindAsRenderTarget()
gl.glViewport(*oldSize)
......
......@@ -4,20 +4,31 @@
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides a single function, :func:`run`, which simply
runs a function in a separate thread.
"""This module provides a couple of functions for running tasks
asynchronously - :func:`run` and :func:`idle`.
This doesn't seem like a worthy task to have a module of its own, but the
.. note:: The functions in this module are intended to be run from within a
``wx`` application. However, they will still work without ``wx``,
albeit with slightly modified behaviour.
The :func:`run` function simply runs a task in a separate thread. This
doesn't seem like a worthy task to have a function of its own, but the
:func:`run` function additionally provides the ability to schedule another
function to run on the ``wx.MainLoop`` when the original function has
completed.
completed. This therefore gives us a simple way to run a computationally
intensitve task off the main GUI thread (preventing the GUI from locking up),
and to perform some clean up/refresh afterwards.
This therefore gives us a simple way to run a computationally intensitve task
off the main GUI thread (preventing the GUI from locking up), and to perform
some clean up/refresh afterwards.
"""
The :func:`idle` function is a simple way to run a task on an ``wx``
``EVT_IDLE`` event handler. This effectively performs the same job as the
:func:`run` function, but is more suitable for short tasks which do not
warrant running in a separate thread.
"""
import Queue
import logging
import threading
......@@ -25,6 +36,19 @@ import threading
log = logging.getLogger(__name__)
def _haveWX():
"""Returns ``True`` if wqe are running within a ``wx`` application,
``False`` otherwise.
"""
try:
import wx
return wx.GetApp() is not None
except ImportError:
return False
def run(task, onFinish=None, name=None):
"""Run the given ``task`` in a separate thread.
......@@ -34,9 +58,13 @@ def run(task, onFinish=None, name=None):
once the ``task`` has finished.
:arg name: An optional name to use for this task in log statements.
.. note:: If a ``wx`` application is not running, the ``onFinish``
function is called directly from the task thread.
"""
if name is None: name = 'async task'
if name is None:
name = 'async task'
def wrapper():
......@@ -46,14 +74,80 @@ def run(task, onFinish=None, name=None):
log.debug('Task "{}" finished'.format(name))
if onFinish is not None:
import wx
log.debug('Scheduling task "{}" finish handler '
'on wx.MainLoop'.format(name))
if _haveWX():
import wx
log.debug('Scheduling task "{}" finish handler '
'on wx.MainLoop'.format(name))
wx.CallAfter(onFinish)
wx.CallAfter(onFinish)
else:
log.debug('Running task "{}" finish handler'.format(name))
onFinish()
thread = threading.Thread(target=wrapper)
thread.start()
return thread
_idleRegistered = False
"""Boolean flag indicating whether the :func:`wxIdleLoop` function has
been registered as a ``wx.EVT_IDLE`` event handler. Checked and set
in the :func:`idle` function.
"""
_idleQueue = Queue.Queue()
"""A ``Queue`` of functions which are to be run on the ``wx.EVT_IDLE``
loop.
"""
def _wxIdleLoop(ev):
"""Function which is called on ``wx.EVT_IDLE`` events. If there
is a function on the :attr:`_idleQueue`, it is popped and called.
"""
ev.Skip()
try: task = _idleQueue.get_nowait()
except Queue.Empty: return
task()
if _idleQueue.qsize() > 0:
ev.RequestMore()
def idle(task, name=None):
"""Run the given task on a ``wx.EVT_IDLE`` event.
:arg task: The task to run - a function which accepts no parameters.
:arg name: An optional name to use for this task in log statements.
"""
global _idleRegistered
global _idleTasks
if name is None:
name = 'idle task'
if _haveWX():
import wx
if not _idleRegistered:
wx.GetApp().Bind(wx.EVT_IDLE, _wxIdleLoop)
_idleRegistered = True
log.debug('Scheduling task "{}" '
'on wx idle loop'.format(name))
_idleQueue.put_nowait(task)
else:
log.debug('Running task "{}"'.format(name))
task()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment