def test_cannot_use_unresolved_globals(): def compute_shader(index: ("input", "GlobalInvocationId", ivec3), ): color = stdlib + 1.0 # noqa with raises(pyshader.ShaderError) as info: pyshader.python2shader(compute_shader) assert "color = stdlib + 1.0" in str(info.value).lower()
def test_fail_unvalid_stlib_name(): def compute_shader(index: ("input", "GlobalInvocationId", ivec3), ): color = stdlib.foo # noqa with raises(pyshader.ShaderError) as info: pyshader.python2shader(compute_shader) assert "color = stdlib.foo" in str(info.value).lower()
def test_cannot_assign_same_slot(): def compute_shader( index: ("input", "GlobalInvocationId", ivec3), data1: ("buffer", 0, Array(i32)), data2: ("buffer", 0, Array(i32)), ): data2[index.x] = data1[index.x] with raises(pyshader.ShaderError) as err: pyshader.python2shader(compute_shader).to_spirv() assert "already taken" in str(err.value)
def test_andor1(): # Implicit conversion to truth values is not supported def compute_shader( index_xyz: ("input", "GlobalInvocationId", ivec3), data2: ("buffer", 1, Array(f32)), ): index = index_xyz.x if index < 5: val = f32(index - 3) and 99.0 else: val = f32(index - 6) and 99.0 data2[index] = val with pytest.raises(pyshader.ShaderError): pyshader.python2shader(compute_shader).to_spirv()
def test_cannot_call_non_funcs(): def compute_shader1( index: ("input", "GlobalInvocationId", ivec3), tex: ("texture", 0, "2d rg32i"), ): a = 1.0 a(1.0) def compute_shader2( index: ("input", "GlobalInvocationId", ivec3), tex: ("texture", 0, "2d rg32i"), ): a = 1.0 () # noqa with raises(pyshader.ShaderError): pyshader.python2shader(compute_shader1) with raises(pyshader.ShaderError): pyshader.python2shader(compute_shader2)
def test_no_duplicate_constants(): def vertex_shader(): positions = [vec2(0.0, 1.0), vec2(0.0, 1.0), vec2(0.0, 1.0)] # noqa m = pyshader.python2shader(vertex_shader) text = pyshader.dev.disassemble(m.to_spirv()) # One for 1.0, one for 0.0 assert text.count("OpConstant %float") == 2 # One for the vector, one for the array assert text.count("OpConstantComposite") == 2
def test_cannot_add_int_and_floats(): def compute_shader1(index: ("input", "GlobalInvocationId", ivec3), ): foo = 3.0 bar = foo + index.x # noqa x = pyshader.python2shader(compute_shader1) with raises(pyshader.ShaderError) as info: x.to_spirv() err = info.value.args[0] assert "source file" in err.lower() assert "test_py.py" in err.lower() assert "bar = foo + index.x" in err.lower()
def test_errror_reports_the_correct_name2(): # ... and sometimes that name is an array (a VariableAccessId internally) def compute_shader1(index: ("input", "GlobalInvocationId", ivec3), ): foo = [1, 2, 3] bar = foo # noqa spam = foo[0] + 1.0 # noqa def compute_shader2(index: ("input", "GlobalInvocationId", ivec3), ): foo = [1, 2, 3] bar = foo # noqa spam = bar[0] + 1.0 # noqa with raises(pyshader.ShaderError) as info1: pyshader.python2shader(compute_shader1).to_spirv() assert "spam = foo[0] + 1.0" in str(info1.value).lower() assert "variables: foo[0], 1.0" in str(info1.value).lower() with raises(pyshader.ShaderError) as info2: pyshader.python2shader(compute_shader2).to_spirv() assert "spam = bar[0] + 1.0" in str(info2.value).lower() assert "variables: bar[0], 1.0" in str(info2.value).lower()
def test_errror_reports_the_correct_name1(): # Sometimes, an object can be known by multiple names ... def compute_shader1(index: ("input", "GlobalInvocationId", ivec3), ): foo = 3.0 bar = foo # noqa spam = foo + 1 # noqa def compute_shader2(index: ("input", "GlobalInvocationId", ivec3), ): foo = 3.0 bar = foo # noqa spam = bar + 1 # noqa with raises(pyshader.ShaderError) as info1: pyshader.python2shader(compute_shader1).to_spirv() assert "spam = foo + 1" in str(info1.value).lower() assert "variables: foo, 1" in str(info1.value).lower() with raises(pyshader.ShaderError) as info2: pyshader.python2shader(compute_shader2).to_spirv() assert "spam = bar + 1" in str(info2.value).lower() assert "variables: bar, 1" in str(info2.value).lower()
def test_bytecode_output_src_opcodes(): def compute_shader(): a = 2 # noqa m = pyshader.python2shader(compute_shader) bc = m.to_bytecode() instructions = [x[0] for x in bc] assert instructions == [ "co_src_filename", "co_src_linenr", "co_entrypoint", "co_src_linenr", "co_load_constant", "co_store_name", "co_func_end", ]
def test_loop9(): # This is a very specific shader (volumeslice from pygfx) that produces # wrong results at some point, which was the notch needed to implement # variables using VarAccessId objects. See #56. def compute_shader( index_xyz: ("input", "GlobalInvocationId", ivec3), data2: ("buffer", 1, Array(f32)), ): ed2pl = [[0, 4], [0, 3], [0, 5], [0, 2], [1, 5], [1, 3], [1, 4], [1, 2]] intersect_flag = [0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0] i = 1 plane_index = ed2pl[i][0] vertices = [i, 0, 0, 0, 0, 0] i_start = i i_last = i max_iter = 6 for iter in range(1, max_iter): for i in range(12): if i != i_last and intersect_flag[i] == 1: if ed2pl[i][0] == plane_index: vertices[iter] = i plane_index = ed2pl[i][1] i_last = i break elif ed2pl[i][1] == plane_index: vertices[iter] = i plane_index = ed2pl[i][0] i_last = i break if i_last == i_start: max_iter = iter break index = index_xyz.x data2[index] = f32(vertices[index]) # On py36 this works, but generates different bytecode ... if sys.version_info > (3, 7): compute_shader = python2shader_and_validate(compute_shader) else: compute_shader = pyshader.python2shader(compute_shader) skip_if_no_wgpu() vertices = generate_list_of_floats_from_shader(6, compute_shader) print(vertices) assert vertices == [1, 3, 7, 5, 1, 0]
def test_spirv_output_opnames(): def compute_shader( index: ("input", "GlobalInvocationId", ivec3), data1: ("buffer", 0, Array(i32)), ): a = 2 b = a # noqa c = a + 1 # noqa m = pyshader.python2shader(compute_shader) text = pyshader.dev.disassemble(m.to_spirv()) # Check opname assert text.count("OpName") == 9 assert 'OpName %main "main"' in text assert 'OpName %index "index"' in text assert 'OpName %data1 "data1"' in text assert 'OpName %1 "1"' in text assert 'OpName %2 "2"' in text assert 'OpName %a "a"' in text assert 'OpName %b "b"' in text
def test_cannot_use_tuples_in_other_ways(): def compute_shader1(index: ("input", "GlobalInvocationId", ivec3), ): v = 3.0, 4.0 # noqa def compute_shader2(index: ("input", "GlobalInvocationId", ivec3), ): a = 3.0 b = 4.0 v = a, b # noqa def compute_shader3(index: ("input", "GlobalInvocationId", ivec3), ): v = vec2(3.0, 4.0) a, b = v with raises(pyshader.ShaderError): pyshader.python2shader(compute_shader1) with raises(pyshader.ShaderError): pyshader.python2shader(compute_shader2) with raises(pyshader.ShaderError): pyshader.python2shader(compute_shader3)
def mesh_renderer(wobject, render_info): """ Render function capable of rendering meshes. """ geometry = wobject.geometry material = wobject.material # noqa # Get stuff from material # ... # Get stuff from geometry fragment_shader = fragment_shader_simple # Use index buffer if present on the geometry index_buffer = getattr(geometry, "index", None) index_buffer = index_buffer if isinstance(index_buffer, BaseBuffer) else None # Collect vertex buffers vertex_buffers = [] vertex_buffers.append(geometry.positions) if getattr(geometry, "texcoords", None) is not None: vertex_buffers.append(geometry.texcoords) bindings0 = {0: (wgpu.BindingType.uniform_buffer, render_info.stdinfo)} bindings1 = {} bindings1[0] = wgpu.BindingType.uniform_buffer, material.uniform_buffer # Collect texture and sampler if material.map is not None: if isinstance(material.map, BaseTexture): raise TypeError( "material.map is a Texture, but must be a TextureView") elif not isinstance(material.map, TextureView): raise TypeError("material.map must be a TextureView") elif getattr(geometry, "texcoords", None) is None: raise ValueError( "material.map is present, but geometry has no texcoords") bindings1[1] = wgpu.BindingType.sampler, material.map bindings1[2] = wgpu.BindingType.sampled_texture, material.map if "rgba" in material.map.format: fragment_shader = fragment_shader_textured_rgba else: fragment_shader = fragment_shader_textured_gray # Use a version of the shader for float textures if necessary if "float" in material.map.format: if not hasattr(fragment_shader, "float_version"): func = fragment_shader.input tex_anno = func.__annotations__["t_tex"] func.__annotations__["t_tex"] = tex_anno[:2] + ("2d f32", ) fragment_shader.float_version = python2shader(func) func.__annotations__["t_tex"] = tex_anno fragment_shader = fragment_shader.float_version if index_buffer: n = len(index_buffer.data) else: n = len(vertex_buffers[0].data) # Put it together! return [{ "vertex_shader": vertex_shader, "fragment_shader": fragment_shader, "primitive_topology": wgpu.PrimitiveTopology.triangle_list, "indices": (range(n), range(1)), "index_buffer": index_buffer, "vertex_buffers": vertex_buffers, "bindings0": bindings0, "bindings1": bindings1, }]
def python2shader_and_validate(func): m = pyshader.python2shader(func) assert m.input is func validate_module(m, HASHES) return m
def python2shader_and_validate_nochecks(func): m = pyshader.python2shader(func) assert m.input is func validate_module(m, HASHES, check_bytecode=False, check_spirv=False) return m