Repository: psychopy/psychopy

Issue:
[Bug]: 'fast' operations (contrast, opacity, etc.) are slow in recent versions
### PsychoPy Version

2023.2.3

### What OS are your PsychoPy running on?

Windows 11, Linux-based systems

### Bug Description

Setting an attribute like contrast on a GratingStim seems to be very slow in the current version. Those operations take about 100 milliseconds per frame on a simple example script (below) on my machine - greatly exceeding any time budget for not dropping frames.

### Expected Behaviour

I had expected that it would be a fast operation (as per [the docs](https://www.psychopy.org/general/rendering.html#fast-and-slow-functions)). If I go back to version 2020.2.10 and run the same script, the operations then take the expected around 0.3 milliseconds per frame. If I go to version 2021.1.0, then it is back to 100 milliseconds per frame.

### Steps to Reproduce

For example, the script below just shows a GratingStim with a random noise texture. On each frame, the contrast is set (to a constant, just to trigger the setting consequences). A clock prints the elapsed time for setting the contrast and drawing the stimulus.

```python
import psychopy.visual
import psychopy.core

import numpy as np

tex_size = 2048

tex = np.random.rand(tex_size, tex_size) * 2 - 1

with psychopy.visual.Window(
    size=(600, 600), units="pix", fullscr=False
) as win:

    stim = psychopy.visual.GratingStim(
        win=win,
        size=(tex_size, tex_size),
        tex=tex,
    )

    timer = psychopy.core.Clock()
    op_timer = psychopy.core.Clock()

    while timer.getTime() < 5:

        op_timer.reset()
        stim.contrast = 1
        stim.draw()
        print(f"{op_timer.getTime() * 1000:.04f} ms")

        win.flip()
```

### Additional context

Having a look at the source code, it seems like setting these attributes now require a rebuild of the texture (and the associated time cost). That seems like unintended behaviour, given it makes setting the attributes in a temporal context not feasible.


Discussion:
It's true that setting contrast should not require a rebuild of the texture and if that's happening we should make sure it's fixed. I'll take a look at why. Thanks for the headsup

Resolving patch:
diff --git a/psychopy/visual/grating.py b/psychopy/visual/grating.py
--- a/psychopy/visual/grating.py
+++ b/psychopy/visual/grating.py
@@ -208,6 +208,7 @@ def __init__(self,
         GL.glGenTextures(1, ctypes.byref(self._maskID))
         self.__dict__['texRes'] = texRes  # must be power of 2
         self.interpolate = interpolate
+        self._needTextureUpdate = True
 
         # NB Pedestal isn't currently being used during rendering - this is a
         # place-holder
@@ -318,46 +319,6 @@ def phase(self, value):
         self.__dict__['phase'] = value
         self._needUpdate = True
 
-    # overload ColorMixin methods so that they refresh the image after being called
-    @property
-    def foreColor(self):
-        # Call setter of parent mixin
-        return ColorMixin.foreColor.fget(self)
-
-    @foreColor.setter
-    def foreColor(self, value):
-        # Call setter of parent mixin
-        ColorMixin.foreColor.fset(self, value)
-        # Reset texture
-        self._needTextureUpdate = True
-        self._needUpdate = True
-
-    @property
-    def contrast(self):
-        # Call setter of parent mixin
-        return ColorMixin.contrast.fget(self)
-
-    @contrast.setter
-    def contrast(self, value):
-        # Call setter of parent mixin
-        ColorMixin.contrast.fset(self, value)
-        # Reset texture
-        self._needTextureUpdate = True
-        self._needUpdate = True
-
-    @property
-    def opacity(self):
-        # Call setter of parent mixin
-        return BaseVisualStim.opacity.fget(self)
-
-    @opacity.setter
-    def opacity(self, value):
-        # Call setter of parent mixin
-        BaseVisualStim.opacity.fset(self, value)
-        # Reset texture
-        self._needTextureUpdate = True
-        self._needUpdate = True
-
     @attributeSetter
     def tex(self, value):
         """Texture to used on the stimulus as a grating (aka carrier).
diff --git a/psychopy/visual/image.py b/psychopy/visual/image.py
--- a/psychopy/visual/image.py
+++ b/psychopy/visual/image.py
@@ -235,46 +235,6 @@ def draw(self, win=None):
         # return the view to previous state
         GL.glPopMatrix()
 
-    # overload ColorMixin methods so that they refresh the image after being called
-    @property
-    def foreColor(self):
-        # Call setter of parent mixin
-        return ColorMixin.foreColor.fget(self)
-
-    @foreColor.setter
-    def foreColor(self, value):
-        # Call setter of parent mixin
-        ColorMixin.foreColor.fset(self, value)
-        # Reset the image and mask-
-        self.setImage(self._imName, log=False)
-        self.texRes = self.__dict__['texRes']  # rebuilds the mask
-
-    @property
-    def contrast(self):
-        # Call setter of parent mixin
-        return ColorMixin.contrast.fget(self)
-
-    @contrast.setter
-    def contrast(self, value):
-        # Call setter of parent mixin
-        ColorMixin.contrast.fset(self, value)
-        # Reset the image and mask-
-        self.setImage(self._imName, log=False)
-        self.texRes = self.__dict__['texRes']  # rebuilds the mask
-
-    @property
-    def opacity(self):
-        # Call setter of parent mixin
-        return BaseVisualStim.opacity.fget(self)
-
-    @opacity.setter
-    def opacity(self, value):
-        # Call setter of parent mixin
-        BaseVisualStim.opacity.fset(self, value)
-        # Reset the image and mask-
-        self.setImage(self._imName, log=False)
-        self.texRes = self.__dict__['texRes']  # rebuilds the mask
-
     def _movieFrameToTexture(self, movieSrc):
         """Convert a movie frame to a texture and use it.
 


Test patch: