diff --git a/fsl/fsleyes/actions/__init__.py b/fsl/fsleyes/actions/__init__.py index 8d128acd88e40c084d5dcdf687ee13fe4b4c2e4e..f611bb0add0fb1ec9467fedd9345dc7d1e41f5ec 100644 --- a/fsl/fsleyes/actions/__init__.py +++ b/fsl/fsleyes/actions/__init__.py @@ -300,6 +300,10 @@ class ActionFactory(object): @otherCustomAction(arg1=8) def myAction3(self): # do things here + + + .. todo:: Merge/replace this class with the :class:`.memoize.Instanceify` + decorator. """ diff --git a/fsl/fsleyes/gl/glvolume.py b/fsl/fsleyes/gl/glvolume.py index 3eb8125ac25889f55ea633c6b813993eed9d02bb..cb9d6f989f4e6294c8380415c85bfbcfdfd03f48 100644 --- a/fsl/fsleyes/gl/glvolume.py +++ b/fsl/fsleyes/gl/glvolume.py @@ -273,11 +273,14 @@ class GLVolume(globject.GLImageObject): if self.ready(): if fslgl.glvolume_funcs.updateShaderState(self): self.notify() + + def cmapUpdate(*a): + self.refreshColourTextures() + self.notify() def colourUpdate(*a): self.refreshColourTextures() - if self.ready(): - shaderUpdate() + shaderUpdate() def imageRefresh(*a): async.wait([self.refreshImageTexture()], shaderUpdate) @@ -314,8 +317,8 @@ class GLVolume(globject.GLImageObject): opts .addListener('clippingRange', lName, shaderUpdate, weak=False) opts .addListener('clipImage', lName, clipUpdate, weak=False) opts .addListener('invertClipping', lName, shaderUpdate, weak=False) - opts .addListener('cmap', lName, colourUpdate, weak=False) - opts .addListener('negativeCmap', lName, colourUpdate, weak=False) + opts .addListener('cmap', lName, cmapUpdate, weak=False) + opts .addListener('negativeCmap', lName, cmapUpdate, weak=False) opts .addListener('useNegativeCmap', lName, colourUpdate, weak=False) opts .addListener('invert', lName, colourUpdate, weak=False) diff --git a/fsl/fsleyes/gl/shaders/arbp/program.py b/fsl/fsleyes/gl/shaders/arbp/program.py index c11c9e17166e12c76d595f803ef376095041b064..26445466247a42cc9fc127a838ce23fc3be76663 100644 --- a/fsl/fsleyes/gl/shaders/arbp/program.py +++ b/fsl/fsleyes/gl/shaders/arbp/program.py @@ -18,6 +18,7 @@ import OpenGL.raw.GL._types as gltypes import OpenGL.GL.ARB.fragment_program as arbfp import OpenGL.GL.ARB.vertex_program as arbvp +import fsl.utils.memoize as memoize import parse @@ -207,12 +208,17 @@ class ARBPShader(object): gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY) + @memoize.Instanceify(memoize.skipUnchanged) def setVertParam(self, name, value): """Sets the value of the specified vertex program parameter. .. note:: It is assumed that the value is either a sequence of length 4 (for vector parameters), or a ``numpy`` array of shape ``(n, 4)`` (for matrix parameters). + + .. note:: This method is decorated by the + :func:`.memoize.skipUnchanged` decorator, which returns + ``True`` if the value was changed, and ``False`` otherwise. """ pos = self.vertParamPositions[name] @@ -225,10 +231,15 @@ class ARBPShader(object): arbvp.GL_VERTEX_PROGRAM_ARB, pos + i, row[0], row[1], row[2], row[3]) - + + @memoize.Instanceify(memoize.skipUnchanged) def setFragParam(self, name, value): """Sets the value of the specified vertex program parameter. See :meth:`setVertParam` for infomration about possible values. + + .. note:: This method is decorated by the + :func:`.memoize.skipUnchanged` decorator, which returns + ``True`` if the value was changed, and ``False`` otherwise. """ pos = self.fragParamPositions[name] value = np.array(value, dtype=np.float32).reshape((-1, 4)) diff --git a/fsl/fsleyes/gl/shaders/glsl/program.py b/fsl/fsleyes/gl/shaders/glsl/program.py index c39c8a23d0902ce32b5a6c9cd652878c8d65b975..f17ee31fa4f1713913dcb38e2eb9bd3123a7923b 100644 --- a/fsl/fsleyes/gl/shaders/glsl/program.py +++ b/fsl/fsleyes/gl/shaders/glsl/program.py @@ -18,6 +18,8 @@ import OpenGL.GL.ARB.instanced_arrays as arbia import parse +import fsl.utils.memoize as memoize + log = logging.getLogger(__name__) @@ -149,12 +151,6 @@ class GLSLShader(object): self.vertUniforms, self.fragUniforms) - # We cache the most recent value for - # every uniform. When a call to set() - # is made, if the value is unchanged, - # we skip the GL call. - self.values = {n : None for n in self.positions.keys()} - # Buffers for vertex attributes self.buffers = {} @@ -239,31 +235,20 @@ class GLSLShader(object): gl.glDeleteBuffers(1, gltypes.GLuint(buf)) self.program = None - + + @memoize.Instanceify(memoize.skipUnchanged) def set(self, name, value): """Sets the value for the specified GLSL ``uniform`` variable. The ``GLSLShader`` keeps a copy of the value of every uniform, to avoid unnecessary GL calls. - :returns: ``True`` if the value was changed, ``False`` otherwise. + + .. note:: This method is decorated by the + :func:`.memoize.skipUnchanged` decorator, which returns + ``True`` if the value was changed, ``False`` otherwise. """ - oldVal = self.values[name] - - oldIsArray = isinstance(oldVal, np.ndarray) - newIsArray = isinstance(value, np.ndarray) - isarray = oldIsArray or newIsArray - - if oldIsArray and (not newIsArray): value = np.array(value) - if newIsArray and (not oldIsArray): oldVal = np.array(oldVal) - - if isarray: nochange = np.all(oldVal == value) - else: nochange = oldVal == value - - if nochange: - return False - vPos = self.positions[name] vType = self.types[ name] @@ -278,8 +263,6 @@ class GLSLShader(object): setfunc(vPos, value) - return True - def setAtt(self, name, value, divisor=None): """Sets the value for the specified GLSL ``attribute`` variable. diff --git a/fsl/utils/memoize.py b/fsl/utils/memoize.py index cf40efe67f514c82119bad87f0e2c33324bb17b4..bc4de759663eb16fcb5f7928d57579ae402b3e64 100644 --- a/fsl/utils/memoize.py +++ b/fsl/utils/memoize.py @@ -10,10 +10,14 @@ a function: .. autosummary:: :nosignatures: + Instanceify memoizeMD5 + skipUnchanged """ + import hashlib +import functools def memoizeMD5(func): @@ -46,3 +50,130 @@ def memoizeMD5(func): return result return wrapper + + +def skipUnchanged(func): + """This decorator is intended for use with *setter* functions - a function + which accepts a name and a value, and is intended to set some named + attribute to the given value. + + This decorator keeps a cache of name-value pairs. When the decorator is + called with a specific name and value, the cache is checked and, if the + given value is the same as the cached value, the decorated function is + *not* called. If the given value is different from the cached value (or + there is no value), the decorated function is called. + + .. note:: This decorator ignores the return value of the decorated + function. + + :returns: ``True`` if the underlying setter function was called, ``False`` + otherwise. + """ + + import numpy as np + + cache = {} + + def wrapper(name, value, *args, **kwargs): + + oldVal = cache.get(name, None) + + if oldVal is not None: + + oldIsArray = isinstance(oldVal, np.ndarray) + newIsArray = isinstance(value, np.ndarray) + isarray = oldIsArray or newIsArray + + if isarray: nochange = np.all(oldVal == value) + else: nochange = oldVal == value + + if nochange: + return False + + func(name, value, *args, **kwargs) + + cache[name] = value + + return True + + return wrapper + + +class Instanceify(object): + """This class is intended to be used to decorate other decorators, so they + can be applied to instance methods. For example, say we have the following + class:: + + + class Container(object): + + def __init__(self): + self.__items = {} + + @skipUnchanged + def set(self, name, value): + self.__items[name] = value + + + Given this definition, a single :func:`skipUnchanged` decorator will be + created and shared amongst all ``Container`` instances. This is not ideal, + as the value cache created by the :func:`skipUnchanged` decorator should + be associated with a single ``Container`` instance. + + + By redefining the ``Container`` class definition like so: + + + class Container(object): + + def __init__(self): + self.__items = {} + + @Instanceify(skipUnchanged) + def set(self, name, value): + self.__items[name] = value + + + a separate :func:`skipUnchanged` decorator is created for, and associated + with, every ``Container`` instance. + + + This is achieved because an ``Instanceify`` instance is a descriptor. When + first accessed as an instance attribute, an ``Instanceify`` instance will + create the real decorator function, and replace itself on the instance. + """ + + + def __init__(self, realDecorator): + """Create an ``Instanceify`` decorator. + + :arg realDecorator: A reference to the decorator that is to be + *instance-ified*. + """ + + self.__realDecorator = realDecorator + self.__func = None + + + def __call__(self, func): + """Called immediately after :meth:`__init__`, and passed the method + that is to be decorated. + """ + self.__func = func + return self + + + def __get__(self, instance, cls): + """When an ``Instanceify`` instance is accessed as an attribute of + another object, it will create the real (instance-ified) decorator, + and replace itself on the instance with the real decorator. + """ + + if instance is None: + return self.__func + + method = functools.partial(self.__func, instance) + decMethod = self.__realDecorator(method) + + setattr(instance, self.__func.__name__, decMethod) + return functools.update_wrapper(decMethod, self.__func)