WebGL shader code written in GLSL is based on the OpenGL-ES standard, see GLSL 1.2 spec pdf.
GLSL 1.2 has no support for multidimensional arrays (array of arrays), and loops require a constant expression for iteration. These limitations make it very hard to write generic shader programs.
PythonJS shader translation provides a workaround, and supports one-level-deep array of arrays and iteration over them. The input array data and sizes can change at runtime because the shader is fully recompiled each call to its wrapper function. Attributes from the current scope in JavaScript can also be inlined into the shader. Read the syntax documentation here.
array of array example
class myclass:
def __init__(self, s):
self.s = s
def my_method(self):
return self.s
def run(self, w, h):
self.array = [ [x*y*0.5 for y in range(h)] for x in range(w) ]
@returns( array=64 )
@gpu.main
def gpufunc():
float* A = self.array
float b = self.my_method()
for subarray in A:
for j in range( len(self.array[0]) ):
b += subarray[j]
return b
return gpufunc()
GLSL output
The inner function gpufunc becomes main below.
Inside gpufunc above, the assignment to A as a float pointer float* A = self.array
triggers the wrapper code to unroll A into A_𝑛 and inline the values,
'𝑛' is the length of the array.
void main() {
float A_0[4];
A_0[0]=0.0;A_0[1]=0.0;A_0[2]=0.0;A_0[3]=0.0;float A_1[4];A_1[0]=0.0;A_1[1]=0.5;A_1[2]=1.0;A_1[3]=1.5;float A_2[4];A_2[0]=0.0;A_2[1]=1.0;A_2[2]=2.0;A_2[3]=3.0;float A_3[4];A_3[0]=0.0;A_3[1]=1.5;A_3[2]=3.0;A_3[3]=4.5;float A_4[4];A_4[0]=0.0;A_4[1]=2.0;A_4[2]=4.0;A_4[3]=6.0;float A_5[4];A_5[0]=0.0;A_5[1]=2.5;A_5[2]=5.0;A_5[3]=7.5;float A_6[4];A_6[0]=0.0;A_6[1]=3.0;A_6[2]=6.0;A_6[3]=9.0;float A_7[4];A_7[0]=0.0;A_7[1]=3.5;A_7[2]=7.0;A_7[3]=10.5;
...
the wrapper inlines the runtime value of float b = self.my_method().
Iteration over the list for subarray in A: is translated into a for loop
that copies the data from A_𝑛 into the iterator target subarray.
float b;
b = 0.1;
for (int _iter=0; _iter < 4; _iter++) {
float subarray[4];
if (_iter==0) { for (int _J=0; _J<4; _J++) {subarray[_J] = A_0[_J];} }
if (_iter==1) { for (int _J=0; _J<4; _J++) {subarray[_J] = A_1[_J];} }
if (_iter==2) { for (int _J=0; _J<4; _J++) {subarray[_J] = A_2[_J];} }
if (_iter==3) { for (int _J=0; _J<4; _J++) {subarray[_J] = A_3[_J];} }
for (int j=0; j < 4; j++) {
b += subarray[j];
}
}
out_float = b;
}
array of structs example
To iterate over an array of structs, wrap the struct* with iter in a for loop.
The struct below contains a float number 'num' and array of floats 'arr'.
The nested loop iterates over the indices of the structs array 'arr'.
class myclass:
def new_struct(self, g):
return {
'num' : g,
'arr' : [0.1 for s in range(6)]
}
def run(self, w):
self.array = [ self.new_struct( x ) for x in range(w) ]
@returns( array=64 )
@gpu.main
def gpufunc():
struct* A = self.array
float b = 0.0
for s in iter(A):
b += s.num
for i in range(len(s.arr)):
b += s.arr[i]
return b
return gpufunc()
GLSL output
The assignment struct* A = self.array triggers the wrapper code to generate a struct typedef
that is inserted into the shader header. self.array is inlined at runtime as 𝙎𝙩𝙧𝙪𝙘𝙩𝙉𝙖𝙢𝙚 A_𝑛
Before the struct is constructed the array attribute arr is assigned to the variable _arrA_𝑛.
The for loop switches the iterator target s based on the loop index 𝑛.
void main( ) {
float b;
b=0.0;
float _arrA_0[6];_arrA_0[0]=0.1;_arrA_0[1]=0.1;_arrA_0[2]=0.1;_arrA_0[3]=0.1;_arrA_0[4]=0.1;_arrA_0[5]=0.1;
𝙎𝙩𝙧𝙪𝙘𝙩𝙉𝙖𝙢𝙚 A_0 = 𝙎𝙩𝙧𝙪𝙘𝙩𝙉𝙖𝙢𝙚(0.0,_arrA_0);
...
𝙎𝙩𝙧𝙪𝙘𝙩𝙉𝙖𝙢𝙚 A_6 = 𝙎𝙩𝙧𝙪𝙘𝙩𝙉𝙖𝙢𝙚(6.0,_arrA_6);
float _arrA_7[6];_arrA_7[0]=0.1;_arrA_7[1]=0.1;_arrA_7[2]=0.1;_arrA_7[3]=0.1;_arrA_7[4]=0.1;_arrA_7[5]=0.1;
𝙎𝙩𝙧𝙪𝙘𝙩𝙉𝙖𝙢𝙚 A_7 = 𝙎𝙩𝙧𝙪𝙘𝙩𝙉𝙖𝙢𝙚(7.0,_arrA_7);
for (int _iter=0; _iter < 8; _iter++) {
𝙎𝙩𝙧𝙪𝙘𝙩𝙉𝙖𝙢𝙚 s;
if (_iter==0) { s=A_0;}
if (_iter==1) { s=A_1;}
if (_iter==2) { s=A_2;}
if (_iter==3) { s=A_3;}
if (_iter==4) { s=A_4;}
if (_iter==5) { s=A_5;}
if (_iter==6) { s=A_6;}
if (_iter==7) { s=A_7;}
b += s.num;
for (int i=0; i < 6; i++) {
b += s.arr[i];
}
}
out_float = b;
}