def test_render_orange_dots(): """Render four orange dots and check that there are four orange square dots.""" @python2shader def vertex_shader( index: (RES_INPUT, "VertexId", i32), pos: (RES_OUTPUT, "Position", vec4), pointsize: (RES_OUTPUT, "PointSize", f32), ): positions = [ vec2(-0.5, -0.5), vec2(-0.5, +0.5), vec2(+0.5, -0.5), vec2(+0.5, +0.5), ] p = positions[index] pos = vec4(p, 0.0, 1.0) # noqa pointsize = 16.0 # noqa device = get_default_device() @python2shader def fragment_shader( out_color: (RES_OUTPUT, 0, vec4), ): out_color = vec4(1.0, 0.499, 0.0, 1.0) # noqa # Bindings and layout bind_group_layout = device.create_bind_group_layout(entries=[]) # zero bindings bind_group = device.create_bind_group(layout=bind_group_layout, entries=[]) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) # Render render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group top = wgpu.PrimitiveTopology.point_list # render_to_screen(*render_args, topology=top) a = render_to_texture(*render_args, size=(64, 64), topology=top) # Check that the background is all zero bg = a.copy() bg[8:24, 8:24, :] = 0 bg[8:24, 40:56, :] = 0 bg[40:56, 8:24, :] = 0 bg[40:56, 40:56, :] = 0 assert np.all(bg == 0) # Check the square for dot in ( a[8:24, 8:24, :], a[8:24, 40:56, :], a[40:56, 8:24, :], a[40:56, 40:56, :], ): assert np.all(dot[:, :, 0] == 255) # red assert np.all(dot[:, :, 1] == 127) # green assert np.all(dot[:, :, 2] == 0) # blue assert np.all(dot[:, :, 3] == 255) # alpha
def test_render_orange_square_indexed_indirect(): """Render an orange square, using an index buffer.""" device = get_default_device() @python2shader def fragment_shader( out_color: (RES_OUTPUT, 0, vec4), ): out_color = vec4(1.0, 0.499, 0.0, 1.0) # noqa # Bindings and layout bind_group_layout = device.create_bind_group_layout(entries=[]) # zero bindings bind_group = device.create_bind_group(layout=bind_group_layout, entries=[]) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) # Index buffer indices = (ctypes.c_int32 * 6)(0, 1, 2, 2, 1, 3) ibo = device.create_buffer_with_data( data=indices, usage=wgpu.BufferUsage.INDEX | wgpu.BufferUsage.MAP_WRITE, ) # Buffer with draw parameters for indirect draw call params = (ctypes.c_int32 * 5)(6, 1, 0, 0, 0) indirect_buffer = device.create_buffer_with_data( data=params, usage=wgpu.BufferUsage.INDIRECT, ) # Render render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group # render_to_screen(*render_args, topology=wgpu.PrimitiveTopology.triangle_list, ibo=ibo, indirect_buffer=indirect_buffer) a = render_to_texture( *render_args, size=(64, 64), topology=wgpu.PrimitiveTopology.triangle_list, ibo=ibo, indirect_buffer=indirect_buffer, ) # Check that the background is all zero bg = a.copy() bg[16:-16, 16:-16, :] = 0 assert np.all(bg == 0) # Check the square sq = a[16:-16, 16:-16, :] assert np.all(sq[:, :, 0] == 255) # red assert np.all(sq[:, :, 1] == 127) # green assert np.all(sq[:, :, 2] == 0) # blue assert np.all(sq[:, :, 3] == 255) # alpha
def test_render_orange_square_viewport(): """Render an orange square, in a sub-viewport of the rendered area.""" device = get_default_device() @python2shader def fragment_shader( out_color: (RES_OUTPUT, 0, vec4), ): out_color = vec4(1.0, 0.499, 0.0, 1.0) # noqa def cb(renderpass): renderpass.set_viewport(10, 20, 32, 32, 0, 100) # Bindings and layout bind_group_layout = device.create_bind_group_layout(entries=[]) # zero bindings bind_group = device.create_bind_group(layout=bind_group_layout, entries=[]) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) # Fiddled in a small test to covers the raising of an exception with raises(TypeError): device.create_bind_group( layout=bind_group_layout, entries=[{"resource": device}] ) # Render render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group # render_to_screen(*render_args, renderpass_callback=cb) a = render_to_texture(*render_args, size=(64, 64), renderpass_callback=cb) # Check that the background is all zero bg = a.copy() bg[20 + 8 : 52 - 8, 10 + 8 : 42 - 8, :] = 0 assert np.all(bg == 0) # Check the square sq = a[20 + 8 : 52 - 8, 10 + 8 : 42 - 8, :] assert np.all(sq[:, :, 0] == 255) # red assert np.all(sq[:, :, 1] == 127) # green assert np.all(sq[:, :, 2] == 0) # blue assert np.all(sq[:, :, 3] == 255) # alpha
def test_render_orange_square_color_attachment2(): """Render an orange square on a blue background, testing the LoadOp.load, though in this case the result is the same as the normal square test. """ device = get_default_device() @python2shader def fragment_shader( out_color: (RES_OUTPUT, 0, vec4), ): out_color = vec4(1.0, 0.499, 0.0, 1.0) # noqa # Bindings and layout bind_group_layout = device.create_bind_group_layout(entries=[]) # zero bindings bind_group = device.create_bind_group(layout=bind_group_layout, entries=[]) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) ca = { "resolve_target": None, "load_value": wgpu.LoadOp.load, # LoadOp.load or color "store_op": wgpu.StoreOp.store, } # Render render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group # render_to_screen(*render_args, color_attachment=ca) a = render_to_texture(*render_args, size=(64, 64), color_attachment=ca) # Check the background bg = a.copy() bg[16:-16, 16:-16, :] = 0 assert np.all(bg == 0) # Check the square sq = a[16:-16, 16:-16, :] assert np.all(sq[:, :, 0] == 255) # red assert np.all(sq[:, :, 1] == 127) # green assert np.all(sq[:, :, 2] == 0) # blue assert np.all(sq[:, :, 3] == 255) # alpha
def test_render_orange_square_scissor(): """Render an orange square, but scissor half the screen away.""" device = get_default_device() @python2shader def fragment_shader( out_color: (RES_OUTPUT, 0, vec4), ): out_color = vec4(1.0, 0.499, 0.0, 1.0) # noqa def cb(renderpass): renderpass.set_scissor_rect(0, 0, 32, 32) # Alse set blend color. Does not change outout, but covers the call. renderpass.set_blend_color((0, 0, 0, 1)) # Bindings and layout bind_group_layout = device.create_bind_group_layout(entries=[]) # zero bindings bind_group = device.create_bind_group(layout=bind_group_layout, entries=[]) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) # Render render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group # render_to_screen(*render_args, renderpass_callback=cb) a = render_to_texture(*render_args, size=(64, 64), renderpass_callback=cb) # Check that the background is all zero bg = a.copy() bg[16:32, 16:32, :] = 0 assert np.all(bg == 0) # Check the square sq = a[16:32, 16:32, :] assert np.all(sq[:, :, 0] == 255) # red assert np.all(sq[:, :, 1] == 127) # green assert np.all(sq[:, :, 2] == 0) # blue assert np.all(sq[:, :, 3] == 255) # alpha
def test_render_orange_square(): """Render an orange square and check that there is an orange square.""" device = get_default_device() # NOTE: the 0.499 instead of 0.5 is to make sure the resulting value is 127. # With 0.5 some drivers would produce 127 and others 128. @python2shader def fragment_shader( out_color: (RES_OUTPUT, 0, vec4), ): out_color = vec4(1.0, 0.499, 0.0, 1.0) # noqa # Bindings and layout bind_group_layout = device.create_bind_group_layout(entries=[]) # zero bindings bind_group = device.create_bind_group(layout=bind_group_layout, entries=[]) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) # Render render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group # render_to_screen(*render_args) a = render_to_texture(*render_args, size=(64, 64)) # Check that the background is all zero bg = a.copy() bg[16:-16, 16:-16, :] = 0 assert np.all(bg == 0) # Check the square sq = a[16:-16, 16:-16, :] assert np.all(sq[:, :, 0] == 255) # red assert np.all(sq[:, :, 1] == 127) # green assert np.all(sq[:, :, 2] == 0) # blue assert np.all(sq[:, :, 3] == 255) # alpha
def test_render_orange_square_depth(): """Render an orange square, but disable half of it using a depth test.""" device = get_default_device() @python2shader def vertex_shader2( index: (RES_INPUT, "VertexId", i32), pos: (RES_OUTPUT, "Position", vec4), ): positions = [ vec3(-0.5, -0.5, 0.0), vec3(-0.5, +0.5, 0.0), vec3(+0.5, -0.5, 0.2), vec3(+0.5, +0.5, 0.2), ] pos = vec4(positions[index], 1.0) # noqa @python2shader def fragment_shader( out_color: (RES_OUTPUT, 0, vec4), ): out_color = vec4(1.0, 0.499, 0.0, 1.0) # noqa def cb(renderpass): renderpass.set_stencil_reference(42) # Bindings and layout bind_group_layout = device.create_bind_group_layout(entries=[]) # zero bindings bind_group = device.create_bind_group(layout=bind_group_layout, entries=[]) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) # Create dept-stencil texture depth_stencil_texture = device.create_texture( size=(64, 64, 1), # when rendering to texture # size=(640, 480, 1), # when rendering to screen dimension=wgpu.TextureDimension.d2, format=wgpu.TextureFormat.depth24plus_stencil8, usage=wgpu.TextureUsage.OUTPUT_ATTACHMENT, ) depth_stencil_state = dict( format=wgpu.TextureFormat.depth24plus_stencil8, depth_write_enabled=True, depth_compare=wgpu.CompareFunction.less_equal, stencil_front={ "compare": wgpu.CompareFunction.equal, "fail_op": wgpu.StencilOperation.keep, "depth_fail_op": wgpu.StencilOperation.keep, "pass_op": wgpu.StencilOperation.keep, }, stencil_back={ "compare": wgpu.CompareFunction.equal, "fail_op": wgpu.StencilOperation.keep, "depth_fail_op": wgpu.StencilOperation.keep, "pass_op": wgpu.StencilOperation.keep, }, stencil_read_mask=0, stencil_write_mask=0, ) depth_stencil_attachment = dict( attachment=depth_stencil_texture.create_view(), depth_load_value=0.1, depth_store_op=wgpu.StoreOp.store, stencil_load_value=wgpu.LoadOp.load, stencil_store_op=wgpu.StoreOp.store, ) # Render render_args = device, vertex_shader2, fragment_shader, pipeline_layout, bind_group # render_to_screen(*render_args, renderpass_callback=cb, depth_stencil_state=depth_stencil_state, depth_stencil_attachment=depth_stencil_attachment) a = render_to_texture( *render_args, size=(64, 64), renderpass_callback=cb, depth_stencil_state=depth_stencil_state, depth_stencil_attachment=depth_stencil_attachment, ) # Check that the background is all zero bg = a.copy() bg[16:-16, 16:32, :] = 0 assert np.all(bg == 0) # Check the square sq = a[16:-16, 16:32, :] assert np.all(sq[:, :, 0] == 255) # red assert np.all(sq[:, :, 1] == 127) # green assert np.all(sq[:, :, 2] == 0) # blue assert np.all(sq[:, :, 3] == 255) # alpha
def test_render_orange_square_vbo(): """Render an orange square, using a VBO.""" device = get_default_device() @python2shader def vertex_shader( pos_in: (RES_INPUT, 0, vec2), pos: (RES_OUTPUT, "Position", vec4), ): pos = vec4(pos_in, 0.0, 1.0) # noqa @python2shader def fragment_shader( out_color: (RES_OUTPUT, 0, vec4), ): out_color = vec4(1.0, 0.499, 0.0, 1.0) # noqa # Bindings and layout bind_group_layout = device.create_bind_group_layout(entries=[]) # zero bindings bind_group = device.create_bind_group(layout=bind_group_layout, entries=[]) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) # Vertex buffer pos_data = (ctypes.c_float * 8)(-0.5, -0.5, -0.5, +0.5, +0.5, -0.5, +0.5, +0.5) vbo = device.create_buffer_with_data( data=pos_data, usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.MAP_WRITE, ) # Vertex buffer views vbo_view = { "array_stride": 4 * 2, "step_mode": "vertex", "attributes": [ { "format": wgpu.VertexFormat.float2, "offset": 0, "shader_location": 0, }, ], } # Render render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group # render_to_screen(*render_args, vbos=[vbo], vbo_views=[vbo_view]) a = render_to_texture(*render_args, size=(64, 64), vbos=[vbo], vbo_views=[vbo_view]) # Check that the background is all zero bg = a.copy() bg[16:-16, 16:-16, :] = 0 assert np.all(bg == 0) # Check the square sq = a[16:-16, 16:-16, :] assert np.all(sq[:, :, 0] == 255) # red assert np.all(sq[:, :, 1] == 127) # green assert np.all(sq[:, :, 2] == 0) # blue assert np.all(sq[:, :, 3] == 255) # alpha
def render_textured_square(fragment_shader, texture_format, texture_size, texture_data): """Render, and test the result. The resulting image must be a gradient on R and B, zeros on G and ones on A. """ nx, ny, nz = texture_size device = get_default_device() if can_use_vulkan_sdk: pyshader.dev.validate(vertex_shader) pyshader.dev.validate(fragment_shader) # Create texture texture = device.create_texture( size=(nx, ny, nz), dimension=wgpu.TextureDimension.d2, format=texture_format, usage=wgpu.TextureUsage.SAMPLED | wgpu.TextureUsage.COPY_DST, ) upload_to_texture(device, texture, texture_data, nx, ny, nz) # texture_view = texture.create_view() # or: texture_view = texture.create_view( format=texture_format, dimension=wgpu.TextureDimension.d2, ) # But not like these ... with raises(ValueError): texture_view = texture.create_view( dimension=wgpu.TextureDimension.d2, ) with raises(ValueError): texture_view = texture.create_view(mip_level_count=1, ) sampler = device.create_sampler(mag_filter="linear", min_filter="linear") # Determine texture component type from the format if texture_format.endswith(("norm", "float")): texture_component_type = wgpu.TextureComponentType.float elif "uint" in texture_format: texture_component_type = wgpu.TextureComponentType.uint else: texture_component_type = wgpu.TextureComponentType.sint # Bindings and layout bindings = [ { "binding": 0, "resource": texture_view }, { "binding": 1, "resource": sampler }, ] binding_layouts = [ { "binding": 0, "visibility": wgpu.ShaderStage.FRAGMENT, "type": wgpu.BindingType.sampled_texture, "view_dimension": wgpu.TextureViewDimension.d2, "texture_component_type": texture_component_type, }, { "binding": 1, "visibility": wgpu.ShaderStage.FRAGMENT, "type": wgpu.BindingType.sampler, }, ] bind_group_layout = device.create_bind_group_layout( entries=binding_layouts) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout]) bind_group = device.create_bind_group(layout=bind_group_layout, entries=bindings) # Render render_args = device, vertex_shader, fragment_shader, pipeline_layout, bind_group # render_to_screen(*render_args) a = render_to_texture(*render_args, size=(64, 64)) # print(a.max(), a[:,:,0].max()) # Check that the background is all zero bg = a.copy() bg[16:-16, 16:-16, :] = 0 assert np.all(bg == 0) # Check the square sq = a[16:-16, 16:-16, :] ref1 = [ [150, 150, 150, 150, 150, 150, 150, 150, 152, 155, 158, 161], [164, 167, 170, 173, 177, 180, 183, 186, 189, 192], [195, 198, 200, 200, 200, 200, 200, 200, 200, 200], ] ref2 = [ [150, 150, 150, 150, 150, 150, 150, 150, 147, 141, 134, 128], [122, 116, 109, 103, 97, 91, 84, 78, 72, 66], [59, 53, 50, 50, 50, 50, 50, 50, 50, 50], ] ref1, ref2 = sum(ref1, []), sum(ref2, []) assert np.allclose(sq[0, :, 0], ref1, atol=1) assert np.allclose(sq[:, 0, 0], ref2, atol=1) assert np.allclose(sq[0, :, 1], ref1, atol=1) assert np.allclose(sq[:, 0, 1], ref2, atol=1) assert np.all(sq[:, :, 2] == 0) # blue assert np.all(sq[:, :, 3] == 255) # alpha
def _compute_texture(compute_shader, texture_format, texture_dim, texture_size, data1): """ Apply a computation on a texture and validate the result. The shader should: * Add the x-coordinate to the red channel. * Add 1 to the green channel. * Multiply the blue channel by 2. * The alpha channel must remain equal. """ nx, ny, nz, nc = texture_size nbytes = ctypes.sizeof(data1) bpp = nbytes // (nx * ny * nz) # bytes per pixel if can_use_vulkan_sdk: pyshader.dev.validate(compute_shader) device = get_default_device() cshader = device.create_shader_module(code=compute_shader) # Create textures and views texture1 = device.create_texture( size=(nx, ny, nz), dimension=texture_dim, format=texture_format, usage=wgpu.TextureUsage.STORAGE | wgpu.TextureUsage.COPY_DST, ) texture2 = device.create_texture( size=(nx, ny, nz), dimension=texture_dim, format=texture_format, usage=wgpu.TextureUsage.STORAGE | wgpu.TextureUsage.COPY_SRC, ) texture_view1 = texture1.create_view() texture_view2 = texture2.create_view() # Determine texture component type from the format if texture_format.endswith(("norm", "float")): texture_component_type = wgpu.TextureComponentType.float elif "uint" in texture_format: texture_component_type = wgpu.TextureComponentType.uint else: texture_component_type = wgpu.TextureComponentType.sint # Create buffer that we need to upload the data buffer_usage = ( wgpu.BufferUsage.MAP_READ | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST ) buffer = device.create_buffer_with_data(data=data1, usage=buffer_usage) assert buffer.usage == buffer_usage # Define bindings # One can see here why we need 2 textures: one is readonly, one writeonly bindings = [ {"binding": 0, "resource": texture_view1}, {"binding": 1, "resource": texture_view2}, ] binding_layouts = [ { "binding": 0, "visibility": wgpu.ShaderStage.COMPUTE, "type": wgpu.BindingType.readonly_storage_texture, # <- "view_dimension": texture_dim, "storage_texture_format": texture_format, "texture_component_type": texture_component_type, }, { "binding": 1, "visibility": wgpu.ShaderStage.COMPUTE, "type": wgpu.BindingType.writeonly_storage_texture, # <- "view_dimension": texture_dim, "storage_texture_format": texture_format, "texture_component_type": texture_component_type, }, ] bind_group_layout = device.create_bind_group_layout(entries=binding_layouts) pipeline_layout = device.create_pipeline_layout( bind_group_layouts=[bind_group_layout] ) bind_group = device.create_bind_group(layout=bind_group_layout, entries=bindings) # Create a pipeline and run it compute_pipeline = device.create_compute_pipeline( layout=pipeline_layout, compute_stage={"module": cshader, "entry_point": "main"}, ) assert compute_pipeline.layout is pipeline_layout assert compute_pipeline.get_bind_group_layout(0) is bind_group_layout command_encoder = device.create_command_encoder() command_encoder.copy_buffer_to_texture( { "buffer": buffer, "offset": 0, "bytes_per_row": bpp * nx, "rows_per_image": ny, }, {"texture": texture1, "mip_level": 0, "origin": (0, 0, 0)}, (nx, ny, nz), ) compute_pass = command_encoder.begin_compute_pass() compute_pass.push_debug_group("foo") compute_pass.insert_debug_marker("setting pipeline") compute_pass.set_pipeline(compute_pipeline) compute_pass.insert_debug_marker("setting bind group") compute_pass.set_bind_group( 0, bind_group, [], 0, 999999 ) # last 2 elements not used compute_pass.insert_debug_marker("dispatch!") compute_pass.dispatch(nx, ny, nz) compute_pass.pop_debug_group() compute_pass.end_pass() command_encoder.copy_texture_to_buffer( {"texture": texture2, "mip_level": 0, "origin": (0, 0, 0)}, { "buffer": buffer, "offset": 0, "bytes_per_row": bpp * nx, "rows_per_image": ny, }, (nx, ny, nz), ) device.default_queue.submit([command_encoder.finish()]) # Read the current data of the output buffer data2 = data1.__class__.from_buffer(buffer.read_data()) # Numpy arrays are easier to work with a1 = np.ctypeslib.as_array(data1).reshape(nz, ny, nx, nc) a2 = np.ctypeslib.as_array(data2).reshape(nz, ny, nx, nc) # Validate! for x in range(nx): assert np.all(a2[:, :, x, 0] == a1[:, :, x, 0] + x) if nc >= 2: assert np.all(a2[:, :, :, 1] == a1[:, :, :, 1] + 1) if nc >= 3: assert np.all(a2[:, :, :, 2] == a1[:, :, :, 2] * 2) if nc >= 4: assert np.all(a2[:, :, :, 3] == a1[:, :, :, 3])