Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement occlusion query #544

Merged
merged 3 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions tests/test_wgpu_occlusion_query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""
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":
fyellin marked this conversation as resolved.
Show resolved Hide resolved
skip("These tests fail on dx12 for some reason", allow_module_level=True)


default_shader_source = """

struct Uniform {
offset: vec3f,
scale: vec2f,
}

@group(0) @binding(0) var<uniform> uniform : Uniform;

@vertex
fn vs_main(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4<f32> {
var positions = array<vec2f, 4>(
vec2f(-0.5, -0.5),
vec2f( 0.5, -0.5),
vec2f(-0.5, 0.5),
vec2f( 0.5, 0.5),
);
let p = positions[vertex_index];
return vec4f(p * uniform.scale, 0.0, 1.0) + vec4f(uniform.offset, 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 to the screen centered at
# <x_offset, y_offset, z_offset + .5>
# and where the sides of the square are length "side". "Result" indicates
# what we expect the result of the occlusion test to be
def add_test(result, x_offset=0.0, y_offset=0.0, z=0.5, reverse=False):
# See WGSL above for order. Add padding.
x_side = y_side = 0.1
if reverse:
y_side = -y_side
data = np.float32((x_offset, y_offset, z, 0, x_side, y_side, 0, 0))
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.
# small square in the center of the clipping area
add_test(True)
# small square in the corner of the clipping area, partially in, partially out
add_test(True, x_offset=0.95, y_offset=0.95)
# small square completely outside the clipping area.
add_test(False, x_offset=2, y_offset=2)
# Same as the first depth, but the previous big square completely occludes it
add_test(False)
# Same small square again, but bring it forward a little bit
add_test(True, z=0.4)
# Same small square, but so far forward that it's outside the clipping box
add_test(False, z=-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.
add_test(False, x_offset=0.1, y_offset=0.1, reverse=True)
add_test(True, x_offset=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 = [x != 0 for x in array]
assert actual_result == expected_result


if __name__ == "__main__":
run_tests(globals())
13 changes: 10 additions & 3 deletions wgpu/backends/wgpu_native/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *",
Expand All @@ -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
)

Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions wgpu/resources/codegen_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading