Repository: vispy/vispy

Issue:
Uniform arrays not supported in gloo
Here is a minimal example:

``` python
import numpy as np
from vispy import app
from vispy import gloo

vertex = """
uniform float u_arr[2];
void main() {
    gl_Position = vec4(0, 0, 0, 1);
}
"""

voronoi_frag = """
void main() {
    gl_FragColor = vec4(1., 1., 1., 1.0);
}
"""

class Canvas(app.Canvas):
    def __init__(self):
        app.Canvas.__init__(self, close_keys='escape')
        self.program = gloo.Program(vertex, voronoi_frag)
        self.program['u_arr'] = (1., 1.)

    def on_initialize(self, event):
        gloo.clear('white')

if __name__ == "__main__":
    c = Canvas()
    c.show()
    app.run()
```

ping @rougier @almarklein 



Discussion:
From a little bit of digging, it sounds like it's not uniformly supported on mobile devices:

https://www.opengl.org/discussion_boards/showthread.php/176674-Uniform-arrays-in-OpenGLES

So if we're shooting for ES 2.0 compatibility as much as possible, then it might violate that? (Although we are starting to support some things that aren't ES2.0 compatible like 3D textures...)

Having ES 2.0 compatibility means that the core functionality of vispy must work on ES 2.0 devices. It does _not_ mean that we should disallow more modern features. We should aim to expand support for desktop GL 3/4 as much as possible.

Makes sense. @lcampagn you don't expect this to work with the current version of variable parsing, right? BTW I also hit a problem at one point where if variables are defined on the same line as `uniform vec4 x, y`, then only `x` gets picked up by `Program`. Is it worth rolling this into the same issue?

Relevant code is here:
https://github.com/vispy/vispy/blob/master/vispy/gloo/shader.py#L197
The regex does appear to pick up the array syntax correctly.. I'm not sure what's supposed to happen past that point.

I've added a PR on Almar's PR for gloo (#307) that solved the problem but it did not make it to the final merge. Anyway, a clean parser is available at:

https://github.com/vispy/experimental/blob/master/nr_parser/parser.py

@rossant it looks like it might actually be coded to work how you did it in the Voronoi example, namely by accessing variables as `program['u_arr[0]'] = 0.0` or whatever. In this sense, uniform array access setting _is_ supported, the API just isn't ideal, right? You'd rather be able to do `program['u_arr'][0] = 0.0` or so?

@Eric89GXL indeed, but more importantly, I think we should also support the simple syntax `program['u_arr'] = myarr` by using [`glUniform*v`](https://www.khronos.org/opengles/sdk/docs/man/xhtml/glUniform.xml) functions.


Resolving patch:
diff --git a/vispy/gloo/glir.py b/vispy/gloo/glir.py
--- a/vispy/gloo/glir.py
+++ b/vispy/gloo/glir.py
@@ -430,6 +430,7 @@ class GlirProgram(GlirObject):
         'vec2': (2, gl.GL_FLOAT, np.float32),
         'vec3': (3, gl.GL_FLOAT, np.float32),
         'vec4': (4, gl.GL_FLOAT, np.float32),
+        'int': (1, gl.GL_INT, np.int32),
     }
     
     def create(self):
@@ -475,8 +476,8 @@ def set_shaders(self, vert, frag):
         vert_handle = gl.glCreateShader(gl.GL_VERTEX_SHADER)
         frag_handle = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
         # For both vertex and fragment shader: set source, compile, check
-        for code, handle, type in [(vert, vert_handle, 'vertex'), 
-                                   (frag, frag_handle, 'fragment')]:
+        for code, handle, type_ in [(vert, vert_handle, 'vertex'), 
+                                    (frag, frag_handle, 'fragment')]:
             gl.glShaderSource(handle, code)
             gl.glCompileShader(handle)
             status = gl.glGetShaderParameter(handle, gl.GL_COMPILE_STATUS)
@@ -484,7 +485,7 @@ def set_shaders(self, vert, frag):
                 errors = gl.glGetShaderInfoLog(handle)
                 errormsg = self._get_error(code, errors, 4)
                 raise RuntimeError("Shader compilation error in %s:\n%s" % 
-                                   (type + ' shader', errormsg))
+                                   (type_ + ' shader', errormsg))
         # Attach shaders
         gl.glAttachShader(self._handle, vert_handle)
         gl.glAttachShader(self._handle, frag_handle)
@@ -613,39 +614,48 @@ def set_texture(self, name, value):
                 unit = self._samplers[name][-1]  # Use existing unit            
             self._samplers[name] = tex._target, tex.handle, unit
             gl.glUniform1i(handle, unit)
-    
-    def set_uniform(self, name, type, value):
+
+    def set_uniform(self, name, type_, value):
         """ Set a uniform value. Value is assumed to have been checked.
         """
         if not self._linked:
             raise RuntimeError('Cannot set uniform when program has no code')
         # Get handle for the uniform, first try cache
         handle = self._handles.get(name, -1)
+        count = 1
         if handle < 0:
             if name in self._known_invalid:
                 return
             handle = gl.glGetUniformLocation(self._handle, name)
             self._unset_variables.discard(name)  # Mark as set
+            # if we set a uniform_array, mark all as set
+            if not type_.startswith('mat'):
+                count = value.nbytes // (4 * self.ATYPEINFO[type_][0])
+            if count > 1:
+                for ii in range(count):
+                    if '%s[%s]' % (name, ii) in self._unset_variables:
+                        self._unset_variables.discard('%s[%s]' % (name, ii))
+
             self._handles[name] = handle  # Store in cache
             if handle < 0:
                 self._known_invalid.add(name)
                 logger.warning('Variable %s is not an active uniform' % name)
                 return
         # Look up function to call
-        funcname = self.UTYPEMAP[type]
+        funcname = self.UTYPEMAP[type_]
         func = getattr(gl, funcname)
         # Program needs to be active in order to set uniforms
         self.activate()
         # Triage depending on type 
-        if type.startswith('mat'):
+        if type_.startswith('mat'):
             # Value is matrix, these gl funcs have alternative signature
             transpose = False  # OpenGL ES 2.0 does not support transpose
             func(handle, 1, transpose, value)
         else:
             # Regular uniform
-            func(handle, 1, value)
+            func(handle, count, value)
     
-    def set_attribute(self, name, type, value):
+    def set_attribute(self, name, type_, value):
         """ Set an attribute value. Value is assumed to have been checked.
         """
         if not self._linked:
@@ -669,14 +679,14 @@ def set_attribute(self, name, type, value):
         # Triage depending on VBO or tuple data
         if value[0] == 0:
             # Look up function call
-            funcname = self.ATYPEMAP[type]
+            funcname = self.ATYPEMAP[type_]
             func = getattr(gl, funcname)
             # Set data
             self._attributes[name] = 0, handle, func, value[1:]
         else:
             # Get meta data
             vbo_id, stride, offset = value
-            size, gtype, dtype = self.ATYPEINFO[type]
+            size, gtype, dtype = self.ATYPEINFO[type_]
             # Get associated VBO
             vbo = self._parser.get_object(vbo_id)
             if vbo is None:
diff --git a/vispy/gloo/program.py b/vispy/gloo/program.py
--- a/vispy/gloo/program.py
+++ b/vispy/gloo/program.py
@@ -126,7 +126,7 @@ def __init__(self, vert=None, frag=None, count=0):
         self._buffer = None  # Set to None in draw()
         if self._count > 0:
             dtype = []
-            for kind, type_, name in self._code_variables.values():
+            for kind, type_, name, size in self._code_variables.values():
                 if kind == 'attribute':
                     dt, numel = self._gtypes[type_]
                     dtype.append((name, dt, numel))
@@ -171,11 +171,12 @@ def variables(self):
         -------
         variables : list
             Each variable is represented as a tuple (kind, type, name),
-            where `kind` is 'attribute', 'uniform', 'varying' or 'const'.
+            where `kind` is 'attribute', 'uniform', 'uniform_array',
+            'varying' or 'const'.
         """
         # Note that internally the variables are stored as a dict
         # that maps names -> tuples, for easy looking up by name.
-        return list(self._code_variables.values())
+        return [x[:3] for x in self._code_variables.values()]
    
     def _parse_variables_from_code(self):
         """ Parse uniforms, attributes and varyings from the source code.
@@ -201,17 +202,17 @@ def _parse_variables_from_code(self):
             regex = re.compile(var_regexp.replace('VARIABLE', kind),
                                flags=re.MULTILINE)
             for m in re.finditer(regex, code):
-                size = -1
                 gtype = m.group('type')
-                if m.group('size'):
-                    size = int(m.group('size'))
+                size = int(m.group('size')) if m.group('size') else -1
+                this_kind = kind
                 if size >= 1:
+                    # uniform arrays get added both as individuals and full
                     for i in range(size):
                         name = '%s[%d]' % (m.group('name'), i)
-                        self._code_variables[name] = kind, gtype, name 
-                else:
-                    name = m.group('name')
-                    self._code_variables[name] = kind, gtype, name
+                        self._code_variables[name] = kind, gtype, name, -1
+                    this_kind = 'uniform_array'
+                name = m.group('name')
+                self._code_variables[name] = this_kind, gtype, name, size
 
         # Now that our code variables are up-to date, we can process
         # the variables that were set but yet unknown.
@@ -280,7 +281,7 @@ def __setitem__(self, name, data):
             return
         
         if name in self._code_variables:
-            kind, type_, name = self._code_variables[name]
+            kind, type_, name, size = self._code_variables[name]
             
             if kind == 'uniform':
                 if type_.startswith('sampler'):
@@ -314,6 +315,20 @@ def __setitem__(self, name, data):
                     # Store and send GLIR command
                     self._user_variables[name] = data
                     self._glir.command('UNIFORM', self._id, name, type_, data)
+
+            elif kind == 'uniform_array':
+                # Normal uniform; convert to np array and check size
+                dtype, numel = self._gtypes[type_]
+                data = np.atleast_2d(data).astype(dtype)
+                need_shape = (size, numel)
+                if data.shape != need_shape:
+                    raise ValueError('Uniform array %r needs shape %s not %s'
+                                     % (name, need_shape, data.shape))
+                data = data.ravel()
+                # Store and send GLIR command
+                self._user_variables[name] = data
+                self._glir.command('UNIFORM', self._id, name, type_, data)
+            
             elif kind == 'attribute':
                 # Is this a constant value per vertex
                 is_constant = False


Test patch:
diff --git a/vispy/gloo/tests/test_program.py b/vispy/gloo/tests/test_program.py
--- a/vispy/gloo/tests/test_program.py
+++ b/vispy/gloo/tests/test_program.py
@@ -88,8 +88,14 @@ def test_uniform(self):
         
         # Text array unoforms
         program = Program("uniform float A[10];", "foo")
-        assert ('uniform', 'float', 'A[0]') in program.variables
-        assert len(program.variables) == 10
+        assert ('uniform_array', 'float', 'A') in program.variables
+        assert len(program.variables) == 11  # array plus elements
+        self.assertRaises(ValueError, program.__setitem__, 'A',
+                          np.ones((9, 1)))
+        program['A'] = np.ones((10, 1))
+        program['A[0]'] = 0
+        assert 'A[0]' in program._user_variables
+        assert 'A[0]' not in program._pending_variables
         
         # Init program
         program = Program("uniform float A;", 
diff --git a/vispy/gloo/tests/test_use_gloo.py b/vispy/gloo/tests/test_use_gloo.py
--- a/vispy/gloo/tests/test_use_gloo.py
+++ b/vispy/gloo/tests/test_use_gloo.py
@@ -114,4 +114,60 @@ def test_use_texture3D():
             assert_allclose(out, expected, atol=1./255.)
 
 
+@requires_application()
+def test_use_uniforms():
+    """Test using uniform arrays"""
+    VERT_SHADER = """
+    attribute vec2 a_pos;
+    varying vec2 v_pos;
+
+    void main (void)
+    {
+        v_pos = a_pos;
+        gl_Position = vec4(a_pos, 0., 1.);
+    }
+    """
+
+    FRAG_SHADER = """
+    varying vec2 v_pos;
+    uniform vec3 u_color[2];
+
+    void main()
+    {
+        gl_FragColor = vec4((u_color[0] + u_color[1]) / 2., 1.);
+    }
+    """
+    shape = (300, 300)
+    with Canvas(size=shape) as c:
+        c.context.glir.set_verbose(True)
+        assert_equal(c.size, shape[::-1])
+        shape = (3, 3)
+        set_viewport((0, 0) + shape)
+        program = Program(VERT_SHADER, FRAG_SHADER)
+        program['a_pos'] = [[-1., -1.], [1., -1.], [-1., 1.], [1., 1.]]
+        program['u_color'] = np.ones((2, 3))
+        c.context.clear('k')
+        program.draw('triangle_strip')
+        out = _screenshot()
+        assert_allclose(out[:, :, 0] / 255., np.ones(shape), atol=1. / 255.)
+
+        # now set one element
+        program['u_color[1]'] = np.zeros(3, np.float32)
+        c.context.clear('k')
+        program.draw('triangle_strip')
+        out = _screenshot()
+        assert_allclose(out[:, :, 0] / 255., 127.5 / 255. * np.ones(shape),
+                        atol=1. / 255.)
+
+        # and the other
+        assert_raises(ValueError, program.__setitem__, 'u_color',
+                      np.zeros(3, np.float32))
+        program['u_color'] = np.zeros((2, 3), np.float32)
+        program['u_color[0]'] = np.ones(3, np.float32)
+        c.context.clear((0.33,) * 3)
+        program.draw('triangle_strip')
+        out = _screenshot()
+        assert_allclose(out[:, :, 0] / 255., 127.5 / 255. * np.ones(shape),
+                        atol=1. / 255.)
+
 run_tests_if_main()