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

New module 'async' which runs a task on a different thread. ImageTexture

uses this to prepare texture data, and GLObjects query ImageTextures to
make sure that they are ready - they are not drawn until textures are
ready. I have discovered that all my changes to prefilter were useless -
rgbvector/tensor vector texture is currently broken.
parent b935468c
No related branches found
No related tags found
No related merge requests found
......@@ -75,6 +75,13 @@ class GLLabel(globject.GLImageObject):
fslgl.gllabel_funcs.destroy(self)
globject.GLImageObject.destroy(self)
def ready(self):
"""Returns ``True`` if this ``GLLabel`` is ready to be drawn, ``False``
otherwise.
"""
return self.imageTexture is not None and self.imageTexture.ready()
def addListeners(self):
"""Called by :meth:`__init__`. Adds listeners to several properties of
......
......@@ -113,6 +113,7 @@ class GLObject(object):
getDisplayBounds
getDataResolution
ready
destroy
preDraw
draw
......@@ -183,6 +184,16 @@ class GLObject(object):
listener(self)
def ready(self):
"""This method must return ``True`` or ``False`` to indicate
whether this ``GLObject`` is ready to be drawn. The method should,
for example, make sure that all :class:`.ImageTexture` objects
are ready to be used.
"""
raise NotImplementedError('The ready method must be '
'implemented by GLObject subclasses')
def getDisplayBounds(self):
"""This method must calculate and return a bounding box, in the
display coordinate system, which contains the entire ``GLObject``.
......@@ -303,6 +314,11 @@ class GLSimpleObject(GLObject):
"""Create a ``GLSimpleObject``. """
GLObject.__init__(self)
def ready(self):
"""Overrides :meth:`GLObject.ready`. Returns ``True``. """
return True
def destroy( self):
"""Overrides :meth:`GLObject.destroy`. Does nothing. """
......
......@@ -204,6 +204,20 @@ class GLVector(globject.GLImageObject):
globject.GLImageObject.destroy(self)
def ready(self):
"""Returns ``True`` if this ``GLVector`` is ready to be drawn,
``False`` otherwise.
"""
return all((self.imageTexture is not None,
self.modulateTexture is not None,
self.clipTexture is not None,
self.colourTexture is not None,
self.imageTexture .ready(),
self.modulateTexture.ready(),
self.clipTexture .ready(),
self.colourTexture .ready()))
def addListeners(self):
"""Called by :meth:`__init__`. Adds listeners to properties of the
:class:`.Display` and :class:`.VectorOpts` instances, so that the GL
......
......@@ -212,6 +212,21 @@ class GLVolume(globject.GLImageObject):
globject.GLImageObject.destroy(self)
def ready(self):
"""Returns ``True`` if this ``GLVolume`` is ready to be drawn,
``False`` otherwise.
"""
if self.displayOpts.clipImage is None:
return (self.imageTexture is not None and
self.imageTexture.ready())
else:
return (self.imageTexture is not None and
self.clipTexture is not None and
self.imageTexture.ready() and
self.clipTexture .ready())
def addDisplayListeners(self):
"""Called by :meth:`__init__`.
......
......@@ -1151,6 +1151,11 @@ class SliceCanvas(props.HasProperties):
'for overlay {}!'.format(overlay))
continue
# The GLObject is not ready
# to be drawn yet.
if not globj.ready():
continue
# On-screen rendering - the globject is
# rendered directly to the screen canvas
if self.renderMode == 'onscreen':
......
......@@ -17,6 +17,7 @@ import OpenGL.GL as gl
import fsl.utils.transform as transform
import fsl.utils.status as status
import fsl.utils.async as async
import fsl.fsleyes.gl.routines as glroutines
import texture
......@@ -46,7 +47,16 @@ class ImageTexture(texture.Texture):
setResolution
setVolume
setNormalise
When an ``ImageTexture`` is created, and when its settings are changed, it
needs to prepare the image data to be passed to OpenGL - for large images,
this can be a time consuming process, so this is performed on a separate
thread (using the :mod:`.async` module). The :meth:`ready` method returns
``True`` or ``False`` to indicate whether the ``ImageTexture`` can be used
- you should not use the texture until :meth:`ready` returns ``True``.
"""
def __init__(self,
name,
......@@ -102,18 +112,26 @@ class ImageTexture(texture.Texture):
self.__name = '{}_{}'.format(type(self).__name__, id(self))
self.image = image
self.__nvals = nvals
# The dataMin/Max are updated
# in the imageDataChanged method
self.__dataMin = None
self.__dataMax = None
self.__interp = None
self.__resolution = None
self.__volume = None
self.__normalise = None
# The texture settings are updated in the set method.
# The prefilter is needed by the imageDataChanged
# method (which initialises dataMin/dataMax). All
# other attributes are initialised in the call
# to set() below.
self.__prefilter = prefilter
self.__prefilter = prefilter
self.__interp = None
self.__resolution = None
self.__volume = None
self.__normalise = None
# The __readay attribute is
# modified in the refresh method
self.__ready = False
self.__imageDataChanged(refresh=False)
......@@ -128,6 +146,13 @@ class ImageTexture(texture.Texture):
normalise=normalise)
def ready(self):
"""Returns ``True`` if this ``ImageTexture`` is ready to be used,
``False`` otherwise.
"""
return self.__ready
def destroy(self):
"""Must be called when this ``ImageTexture`` is no longer needed.
Deletes the texture handle, and removes the listener on the
......@@ -231,71 +256,89 @@ class ImageTexture(texture.Texture):
"""(Re-)generates the OpenGL texture used to store the image data.
"""
self.__determineTextureType()
data = self.__prepareTextureData()
# It is assumed that, for textures with more than one
# value per voxel (e.g. RGB textures), the data is
# arranged accordingly, i.e. with the voxel value
# dimension the fastest changing
if len(data.shape) == 4: self.textureShape = data.shape[1:]
else: self.textureShape = data.shape
log.debug('Refreshing 3D texture (id {}) for '
'{} (data shape: {})'.format(
self.getTextureHandle(),
self.getTextureName(),
self.textureShape))
# The image data is flattened, with fortran dimension
# ordering, so the data, as stored on the GPU, has its
# first dimension as the fastest changing.
data = data.ravel(order='F')
# Enable storage of tightly packed data of any size (i.e.
# our texture shape does not have to be divisible by 4).
gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
self.bindTexture()
# set interpolation routine
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_MAG_FILTER,
self.__interp)
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_MIN_FILTER,
self.__interp)
# Clamp texture borders to the edge
# values - it is the responsibility
# of the rendering logic to not draw
# anything outside of the image space
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_WRAP_S,
gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_WRAP_T,
gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_WRAP_R,
gl.GL_CLAMP_TO_EDGE)
# create the texture according to
# the format determined by the
# _determineTextureType method.
gl.glTexImage3D(gl.GL_TEXTURE_3D,
0,
self.texIntFmt,
self.textureShape[0],
self.textureShape[1],
self.textureShape[2],
0,
self.texFmt,
self.texDtype,
data)
self.unbindTexture()
self.__ready = False
# This can take a long time for large images, so we
# do it in a separate thread using the async module.
def genData():
self.__determineTextureType()
self.__data = self.__prepareTextureData()
# Once the genData function has finished,
# we'll configure the texture back on the
# main thread - OpenGL doesn't play nicely
# with multi-threading.
def configTexture():
data = self.__data
# It is assumed that, for textures with more than one
# value per voxel (e.g. RGB textures), the data is
# arranged accordingly, i.e. with the voxel value
# dimension the fastest changing
if len(data.shape) == 4: self.textureShape = data.shape[1:]
else: self.textureShape = data.shape
log.debug('Refreshing 3D texture (id {}) for '
'{} (data shape: {})'.format(
self.getTextureHandle(),
self.getTextureName(),
self.textureShape))
# The image data is flattened, with fortran dimension
# ordering, so the data, as stored on the GPU, has its
# first dimension as the fastest changing.
data = data.ravel(order='F')
# Enable storage of tightly packed data of any size (i.e.
# our texture shape does not have to be divisible by 4).
gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
self.bindTexture()
# set interpolation routine
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_MAG_FILTER,
self.__interp)
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_MIN_FILTER,
self.__interp)
# Clamp texture borders to the edge
# values - it is the responsibility
# of the rendering logic to not draw
# anything outside of the image space
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_WRAP_S,
gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_WRAP_T,
gl.GL_CLAMP_TO_EDGE)
gl.glTexParameteri(gl.GL_TEXTURE_3D,
gl.GL_TEXTURE_WRAP_R,
gl.GL_CLAMP_TO_EDGE)
# create the texture according to
# the format determined by the
# _determineTextureType method.
gl.glTexImage3D(gl.GL_TEXTURE_3D,
0,
self.texIntFmt,
self.textureShape[0],
self.textureShape[1],
self.textureShape[2],
0,
self.texFmt,
self.texDtype,
data)
self.unbindTexture()
self.__ready = True
async.run(
genData,
onFinish=configTexture,
name='{}.genData({})'.format(type(self).__name__, self.image))
def __imageDataChanged(self, *args, **kwargs):
......
#!/usr/bin/env python
#
# async.py - Run a function in a separate thread.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides a single function, :func:`run`, which simply
runs a function in a separate thread.
This doesn't seem like a worthy task to have a module 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.
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.
"""
import logging
import threading
log = logging.getLogger(__name__)
def run(task, onFinish=None, name=None):
"""Run the given ``task`` in a separate thread.
:arg task: The function to run. Must accept no arguments.
:arg onFinish: An optional function to schedule on the ``wx.MainLoop``
once the ``task`` has finished.
:arg name: An optional name to use for this task in log statements.
"""
if name is None: name = 'async task'
def wrapper():
log.debug('Running task "{}"...'.format(name))
task()
log.debug('Task "{}" finished'.format(name))
if onFinish is not None:
import wx
log.debug('Scheduling task "{}" finish handler '
'on wx.MainLoop'.format(name))
wx.CallAfter(onFinish)
thread = threading.Thread(target=wrapper)
thread.start()
return thread
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