diff --git a/tests/test_wgpu_occlusion_query.py b/tests/test_wgpu_occlusion_query.py new file mode 100644 index 00000000..aa85135f --- /dev/null +++ b/tests/test_wgpu_occlusion_query.py @@ -0,0 +1,178 @@ +""" +Test occlusion queries. +""" + +import numpy as np +import sys + +import wgpu +from pytest import skip +from testutils import run_tests, get_default_device +from testutils import can_use_wgpu_lib, is_ci +from wgpu import flags + +if not can_use_wgpu_lib: + skip("Skipping tests that need the wgpu lib", allow_module_level=True) +elif is_ci and sys.platform == "win32": + skip("These tests fail on dx12 for some reason", allow_module_level=True) + + +default_shader_source = """ + +// Draws a square with side 0.1 centered at the indicated location. +// If reverse, we take the vertices clockwise rather than counterclockwise so that +// we can test culling. + +struct Uniform { + center: vec3f, + reverse: u32, // Actually a bool +} + +@group(0) @binding(0) var uniform : Uniform; + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4 { + var positions = array( + vec2f(-0.05, -0.05), + vec2f( 0.05, -0.05), + vec2f(-0.05, 0.05), + vec2f( 0.05, 0.05), + ); + var p = positions[vertex_index]; + if bool(uniform.reverse) { + // Swapping x and y will cause the coordinates to be cw instead of ccw + p = vec2f(p.y, p.x); + } + return vec4f(p, 0.0, 1.0) + vec4f(uniform.center, 0); +} +""" + + +def test_render_occluding_squares(): + device = get_default_device() + + # Bindings and layout + bind_group_entries = [ + {"binding": 0, "visibility": flags.ShaderStage.VERTEX, "buffer": {}} + ] + bind_group_layout = device.create_bind_group_layout(entries=bind_group_entries) + pipeline_layout = device.create_pipeline_layout( + bind_group_layouts=[bind_group_layout] + ) + + depth_texture = device.create_texture( + size=[1024, 1024], + usage=wgpu.TextureUsage.RENDER_ATTACHMENT, + format="depth32float", + ) + + shader = device.create_shader_module(code=default_shader_source) + render_pipeline = device.create_render_pipeline( + layout=pipeline_layout, + vertex={ + "module": shader, + "entry_point": "vs_main", + }, + primitive={ + "topology": wgpu.PrimitiveTopology.triangle_strip, + "cull_mode": wgpu.CullMode.back, + }, + depth_stencil={ + "depth_write_enabled": True, + "depth_compare": "less", + "format": "depth32float", + }, + ) + + bind_groups = [] + expected_result = [] + + # Each test draws a square of size 0.1 centered at + # + # with the z corrdinate being "z" + # "Result" indicates whether drawing this square generates any non-occluded points. + def draw_square(result, x_offset=0.0, y_offset=0.0, z=0.5, reverse=False): + # See WGSL above for order. Add padding. + data = np.float32((x_offset, y_offset, z, 0)) + data.view(dtype=np.uint32)[3] = reverse + buffer = device.create_buffer_with_data( + data=data, usage=flags.BufferUsage.UNIFORM + ) + binding = device.create_bind_group( + layout=render_pipeline.get_bind_group_layout(0), + entries=[{"binding": 0, "resource": {"buffer": buffer}}], + ) + bind_groups.append(binding) + expected_result.append(result) + + # These tests have to be run in the order shown, as some of the squares occlude + # later squares. + draw_square(True) + # Draw the same small square again. But because of clipping, nothing is drawn. + draw_square(False) + # Same small square again, but bring it forward a little bit + draw_square(True, z=0.4) + # Same small square, but bring it so far forward it's outside the cip area. + draw_square(False, z=-2) + + # small square in the corner of the clipping area, partially in, partially out + draw_square(True, x_offset=0.95, y_offset=0.95) + # small square completely outside the clipping area. + draw_square(False, x_offset=2, y_offset=2) + + # Draw a square that should be visible, but it is culled because it is a rear- + # facing rectangle. And to keep us honest, redraw the example again, but have it + # face forward. + draw_square(False, x_offset=0.1, y_offset=0.1, reverse=True) + draw_square(True, x_offset=0.1, y_offset=0.1) + + occlusion_query_set = device.create_query_set( + type="occlusion", count=len(bind_groups) + ) + occlusion_buffer = device.create_buffer( + size=len(bind_groups) * np.uint64().itemsize, + usage=wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.QUERY_RESOLVE, + ) + + command_encoder = device.create_command_encoder() + + depth_stencil_attachment = { + "view": depth_texture.create_view(), + "depth_clear_value": 1.0, + "depth_load_op": "clear", + "depth_store_op": "store", + "stencil_clear_value": 1.0, + "stencil_load_op": "clear", + "stencil_store_op": "store", + } + + render_pass = command_encoder.begin_render_pass( + color_attachments=[], + depth_stencil_attachment=depth_stencil_attachment, + occlusion_query_set=occlusion_query_set, + ) + + render_pass.set_pipeline(render_pipeline) + # Draw each of the squares in the order given + for index, binding in enumerate(bind_groups): + render_pass.set_bind_group(0, binding) + render_pass.begin_occlusion_query(index) + render_pass.draw(4) + render_pass.end_occlusion_query() + render_pass.end() + # Get the result of the occlusion test + command_encoder.resolve_query_set( + occlusion_query_set, 0, len(bind_groups), occlusion_buffer, 0 + ) + device.queue.submit([command_encoder.finish()]) + + memory_view = device.queue.read_buffer(occlusion_buffer) + array = np.frombuffer(memory_view, dtype=np.uint64) + # https://www.w3.org/TR/webgpu/#occlusion + # Any non-zero value indicates that at least one sample passed. + actual_result = [bool(x) for x in array] + assert actual_result == expected_result + + +if __name__ == "__main__": + run_tests(globals()) diff --git a/wgpu/backends/wgpu_native/_api.py b/wgpu/backends/wgpu_native/_api.py index 428a938e..3bc4e653 100644 --- a/wgpu/backends/wgpu_native/_api.py +++ b/wgpu/backends/wgpu_native/_api.py @@ -2404,6 +2404,11 @@ def begin_render_pass( ), ) + c_occlusion_query_set = ffi.NULL + if occlusion_query_set is not None: + c_occlusion_query_set = occlusion_query_set._internal + objects_to_keep_alive[c_occlusion_query_set] = occlusion_query_set + # H: nextInChain: WGPUChainedStruct *, label: char *, colorAttachmentCount: int, colorAttachments: WGPURenderPassColorAttachment *, depthStencilAttachment: WGPURenderPassDepthStencilAttachment *, occlusionQuerySet: WGPUQuerySet, timestampWrites: WGPURenderPassTimestampWrites * struct = new_struct_p( "WGPURenderPassDescriptor *", @@ -2412,7 +2417,7 @@ def begin_render_pass( colorAttachmentCount=len(c_color_attachments_list), depthStencilAttachment=c_depth_stencil_attachment, timestampWrites=c_timestamp_writes_struct, - # not used: occlusionQuerySet + occlusionQuerySet=c_occlusion_query_set, # not used: nextInChain ) @@ -2769,10 +2774,12 @@ def execute_bundles(self, bundles): raise NotImplementedError() def begin_occlusion_query(self, query_index): - raise NotImplementedError() + # H: void f(WGPURenderPassEncoder renderPassEncoder, uint32_t queryIndex) + libf.wgpuRenderPassEncoderBeginOcclusionQuery(self._internal, int(query_index)) def end_occlusion_query(self): - raise NotImplementedError() + # H: void f(WGPURenderPassEncoder renderPassEncoder) + libf.wgpuRenderPassEncoderEndOcclusionQuery(self._internal) def _release(self): if self._internal is not None and libf is not None: diff --git a/wgpu/resources/codegen_report.md b/wgpu/resources/codegen_report.md index 84b65735..02c52ce4 100644 --- a/wgpu/resources/codegen_report.md +++ b/wgpu/resources/codegen_report.md @@ -29,6 +29,6 @@ * Enum CanvasAlphaMode missing in wgpu.h * Enum field DeviceLostReason.unknown missing in wgpu.h * Wrote 235 enum mappings and 47 struct-field mappings to wgpu_native/_mappings.py -* Validated 113 C function calls -* Not using 91 C functions +* Validated 115 C function calls +* Not using 89 C functions * Validated 75 C structs