Skip to content

Commit 5aa7a92

Browse files
fyellinalmarklein
andauthored
Implemention for multi-draw features (#583)
* Initial multi-draw stuff * First pass for multi_draw * Fix text bug. Need help with documenation * Try to fix doc text. * Update docs/backends.rst per almarklein request. Co-authored-by: Almar Klein <[email protected]> * More test info. Fix leak * Fix some mismerging * Cleanup and comment for tests. --------- Co-authored-by: Almar Klein <[email protected]>
1 parent 6516d37 commit 5aa7a92

File tree

5 files changed

+400
-3
lines changed

5 files changed

+400
-3
lines changed

docs/backends.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,36 @@ bytes you wish to change.
159159
:param data_offset: The starting offset in the data at which to begin copying.
160160

161161

162+
There are two functions that allow you to perform multiple draw calls at once.
163+
Both require that you enable the feature "multi-draw-indirect".
164+
165+
Typically, these calls do not reduce work or increase parallelism on the GPU. Rather
166+
they reduce driver overhead on the CPU.
167+
168+
.. py:function:: wgpu.backends.wgpu_native.multi_draw_indirect(render_pass_encoder, buffer, *, offset=0, count):
169+
170+
Equivalent to::
171+
for i in range(count):
172+
render_pass_encoder.draw_indirect(buffer, offset + i * 16)
173+
174+
:param render_pass_encoder: The current render pass encoder.
175+
:param buffer: The indirect buffer containing the arguments.
176+
:param offset: The byte offset in the indirect buffer containing the first argument.
177+
:param count: The number of draw operations to perform.
178+
179+
.. py:function:: wgpu.backends.wgpu_native.multi_draw_indexed_indirect(render_pass_encoder, buffer, *, offset=0, count):
180+
181+
Equivalent to::
182+
for i in range(count):
183+
render_pass_encoder.draw_indexed_indirect(buffer, offset + i * 2-)
184+
185+
186+
:param render_pass_encoder: The current render pass encoder.
187+
:param buffer: The indirect buffer containing the arguments.
188+
:param offset: The byte offset in the indirect buffer containing the first argument.
189+
:param count: The number of draw operations to perform.
190+
191+
162192
The js_webgpu backend
163193
---------------------
164194

tests/test_wgpu_vertex_instance.py

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
import itertools
2+
3+
import numpy as np
4+
import pytest
5+
import wgpu.utils
6+
from tests.testutils import can_use_wgpu_lib, run_tests
7+
from wgpu import TextureFormat
8+
from wgpu.backends.wgpu_native.extras import (
9+
multi_draw_indexed_indirect,
10+
multi_draw_indirect,
11+
)
12+
13+
MAX_INFO = 100
14+
15+
if not can_use_wgpu_lib:
16+
pytest.skip("Skipping tests that need the wgpu lib", allow_module_level=True)
17+
18+
19+
"""
20+
The fundamental informartion about any of the many draw commands is the
21+
<vertex_instance, instance_index> pair that is passed to the vertex shader. By using
22+
point-list topology, each call to the vertex shader turns into a single call to the
23+
fragment shader, where the pair is recorded.
24+
25+
(To modify a buffer in the vertex shader requires the feature vertex-writable-storage)
26+
27+
We call various combinations of draw functions and verify that they generate precisely
28+
the pairs (those possibly in a different order) that we expect.
29+
"""
30+
SHADER_SOURCE = (
31+
f"""
32+
const MAX_INFO: u32 = {MAX_INFO}u;
33+
"""
34+
"""
35+
@group(0) @binding(0) var<storage, read_write> data: array<vec2u>;
36+
@group(0) @binding(1) var<storage, read_write> counter: atomic<u32>;
37+
38+
struct VertexOutput {
39+
@builtin(position) position: vec4f,
40+
@location(0) info: vec2u
41+
}
42+
43+
const POSITION: vec4f = vec4f(0, 0, 0, 1);
44+
45+
@vertex
46+
fn vertexMain(
47+
@builtin(vertex_index) vertexIndex: u32,
48+
@builtin(instance_index) instanceIndex: u32
49+
) -> VertexOutput {
50+
let info = vec2u(vertexIndex, instanceIndex);
51+
return VertexOutput(POSITION, info);
52+
}
53+
54+
@fragment
55+
fn fragmentMain(@location(0) info: vec2u) -> @location(0) vec4f {
56+
let index = atomicAdd(&counter, 1u);
57+
data[index % MAX_INFO] = info;
58+
return vec4f();
59+
}
60+
"""
61+
)
62+
63+
BIND_GROUP_ENTRIES = [
64+
{"binding": 0, "visibility": "FRAGMENT", "buffer": {"type": "storage"}},
65+
{"binding": 1, "visibility": "FRAGMENT", "buffer": {"type": "storage"}},
66+
]
67+
68+
69+
class Runner:
70+
REQUIRED_FEATURES = ["multi-draw-indirect", "indirect-first-instance"]
71+
72+
@classmethod
73+
def is_usable(cls):
74+
adapter = wgpu.gpu.request_adapter(power_preference="high-performance")
75+
return set(cls.REQUIRED_FEATURES) <= adapter.features
76+
77+
def __init__(self):
78+
adapter = wgpu.gpu.request_adapter(power_preference="high-performance")
79+
self.device = adapter.request_device(required_features=self.REQUIRED_FEATURES)
80+
self.output_texture = self.device.create_texture(
81+
# Actual size is immaterial. Could just be 1x1
82+
size=[128, 128],
83+
format=TextureFormat.rgba8unorm,
84+
usage="RENDER_ATTACHMENT|COPY_SRC",
85+
)
86+
shader = self.device.create_shader_module(code=SHADER_SOURCE)
87+
bind_group_layout = self.device.create_bind_group_layout(
88+
entries=BIND_GROUP_ENTRIES
89+
)
90+
render_pipeline_layout = self.device.create_pipeline_layout(
91+
bind_group_layouts=[bind_group_layout]
92+
)
93+
self.pipeline = self.device.create_render_pipeline(
94+
layout=render_pipeline_layout,
95+
vertex={
96+
"module": shader,
97+
"entry_point": "vertexMain",
98+
},
99+
fragment={
100+
"module": shader,
101+
"entry_point": "fragmentMain",
102+
"targets": [{"format": self.output_texture.format}],
103+
},
104+
primitive={
105+
"topology": "point-list",
106+
},
107+
)
108+
109+
self.data_buffer = self.device.create_buffer(
110+
size=MAX_INFO * 2 * 4, usage="STORAGE|COPY_SRC"
111+
)
112+
self.counter_buffer = self.device.create_buffer(
113+
size=4, usage="STORAGE|COPY_SRC|COPY_DST"
114+
)
115+
self.bind_group = self.device.create_bind_group(
116+
layout=self.pipeline.get_bind_group_layout(0),
117+
entries=[
118+
{"binding": 0, "resource": {"buffer": self.data_buffer}},
119+
{"binding": 1, "resource": {"buffer": self.counter_buffer}},
120+
],
121+
)
122+
self.render_pass_descriptor = {
123+
"color_attachments": [
124+
{
125+
"clear_value": (0, 0, 0, 0), # only first value matters
126+
"load_op": "clear",
127+
"store_op": "store",
128+
"view": self.output_texture.create_view(),
129+
}
130+
],
131+
}
132+
# Args are [vertex_count, instant_count, first_vertex, first_instance]
133+
self.draw_args1 = [2, 3, 100, 10]
134+
self.draw_args2 = [1, 1, 30, 50]
135+
expected_draw_args1 = set(itertools.product((100, 101), (10, 11, 12)))
136+
expected_draw_args2 = {(30, 50)}
137+
self.expected_result_draw = expected_draw_args1 | expected_draw_args2
138+
139+
# Args are [vertex_count, instance_count, index_buffer_offset, vertex_offset, first_instance]
140+
self.draw_indexed_args1 = [4, 2, 1, 100, 1000]
141+
self.draw_indexed_args2 = [1, 1, 7, 200, 2000]
142+
self.expected_result_draw_indexed = set(
143+
itertools.product((103, 105, 107, 111), (1000, 1001))
144+
)
145+
self.expected_result_draw_indexed.add((219, 2000))
146+
147+
indices = (2, 3, 5, 7, 11, 13, 17, 19)
148+
self.draw_indexed_args1 = (4, 2, 1, 100, 1000)
149+
self.draw_indexed_args2 = (1, 1, 7, 200, 2000)
150+
expected_draw_indexed_args1 = set(
151+
itertools.product((103, 105, 107, 111), (1000, 1001))
152+
)
153+
expected_draw_indexed_args2 = {(219, 2000)}
154+
self.expected_result_draw_indexed = (
155+
expected_draw_indexed_args1 | expected_draw_indexed_args2
156+
)
157+
158+
# We're going to want to try calling these draw functions from a buffer, and it
159+
# would be nice to test that these buffers have an offset
160+
self.draw_data_buffer = self.device.create_buffer_with_data(
161+
data=np.uint32([0, 0, *self.draw_args1, *self.draw_args2]), usage="INDIRECT"
162+
)
163+
self.draw_data_buffer_indexed = self.device.create_buffer_with_data(
164+
data=np.uint32([0, 0, *self.draw_indexed_args1, *self.draw_indexed_args2]),
165+
usage="INDIRECT",
166+
)
167+
168+
# And let's not forget our index buffer.
169+
self.index_buffer = self.device.create_buffer_with_data(
170+
data=(np.uint32(indices)), usage="INDEX"
171+
)
172+
173+
def create_render_bundle_encoder(self, draw_function):
174+
render_bundle_encoder = self.device.create_render_bundle_encoder(
175+
color_formats=[self.output_texture.format]
176+
)
177+
render_bundle_encoder.set_pipeline(self.pipeline)
178+
render_bundle_encoder.set_bind_group(0, self.bind_group)
179+
render_bundle_encoder.set_index_buffer(self.index_buffer, "uint32")
180+
181+
draw_function(render_bundle_encoder)
182+
return render_bundle_encoder.finish()
183+
184+
def run_draw_test(self, expected_result, draw_function):
185+
encoder = self.device.create_command_encoder()
186+
encoder.clear_buffer(self.counter_buffer)
187+
this_pass = encoder.begin_render_pass(**self.render_pass_descriptor)
188+
this_pass.set_pipeline(self.pipeline)
189+
this_pass.set_bind_group(0, self.bind_group)
190+
this_pass.set_index_buffer(self.index_buffer, "uint32")
191+
draw_function(this_pass)
192+
this_pass.end()
193+
self.device.queue.submit([encoder.finish()])
194+
count = self.device.queue.read_buffer(self.counter_buffer).cast("i")[0]
195+
if count > MAX_INFO:
196+
pytest.fail("Too many data points written to output buffer")
197+
# Get the result as a series of tuples
198+
info_view = self.device.queue.read_buffer(self.data_buffer, size=count * 2 * 4)
199+
info = np.frombuffer(info_view, dtype=np.uint32).reshape(-1, 2)
200+
info = [tuple(info[i]) for i in range(len(info))]
201+
info_set = set(info)
202+
assert len(info) == len(info_set)
203+
assert info_set == expected_result
204+
205+
206+
if not Runner.is_usable():
207+
pytest.skip("Runner don't have all required features", allow_module_level=True)
208+
209+
210+
@pytest.fixture(scope="module")
211+
def runner():
212+
return Runner()
213+
214+
215+
def test_draw(runner):
216+
def draw(encoder):
217+
encoder.draw(*runner.draw_args1)
218+
encoder.draw(*runner.draw_args2)
219+
220+
runner.run_draw_test(runner.expected_result_draw, draw)
221+
222+
223+
def test_draw_indirect(runner):
224+
def draw(encoder):
225+
encoder.draw_indirect(runner.draw_data_buffer, 8)
226+
encoder.draw_indirect(runner.draw_data_buffer, 8 + 16)
227+
228+
runner.run_draw_test(runner.expected_result_draw, draw)
229+
230+
231+
def test_draw_mixed(runner):
232+
def draw(encoder):
233+
encoder.draw(*runner.draw_args1)
234+
encoder.draw_indirect(runner.draw_data_buffer, 8 + 16)
235+
236+
runner.run_draw_test(runner.expected_result_draw, draw)
237+
238+
239+
def test_multi_draw_indirect(runner):
240+
def draw(encoder):
241+
multi_draw_indirect(encoder, runner.draw_data_buffer, offset=8, count=2)
242+
243+
runner.run_draw_test(runner.expected_result_draw, draw)
244+
245+
246+
def test_draw_via_encoder(runner):
247+
def draw(encoder):
248+
encoder.draw(*runner.draw_args1)
249+
encoder.draw_indirect(runner.draw_data_buffer, 8 + 16)
250+
251+
render_bundle_encoder = runner.create_render_bundle_encoder(draw)
252+
for _ in range(2):
253+
# We run this test twice to verify that encoders are reusable.
254+
runner.run_draw_test(
255+
runner.expected_result_draw,
256+
lambda encoder: encoder.execute_bundles([render_bundle_encoder]),
257+
)
258+
259+
260+
def test_draw_via_multiple_encoders(runner):
261+
# Make sure that execute_bundles() works with multiple encoders.
262+
def draw1(encoder):
263+
encoder.draw(*runner.draw_args1)
264+
265+
def draw2(encoder):
266+
encoder.draw_indirect(runner.draw_data_buffer, 8 + 16)
267+
268+
render_bundle_encoder1 = runner.create_render_bundle_encoder(draw1)
269+
render_bundle_encoder2 = runner.create_render_bundle_encoder(draw2)
270+
271+
runner.run_draw_test(
272+
runner.expected_result_draw,
273+
lambda encoder: encoder.execute_bundles(
274+
[render_bundle_encoder1, render_bundle_encoder2]
275+
),
276+
)
277+
278+
279+
def test_draw_indexed(runner):
280+
def draw(encoder):
281+
encoder.draw_indexed(*runner.draw_indexed_args1)
282+
encoder.draw_indexed(*runner.draw_indexed_args2)
283+
284+
runner.run_draw_test(runner.expected_result_draw_indexed, draw)
285+
286+
287+
def test_draw_indexed_indirect(runner):
288+
def draw(encoder):
289+
encoder.draw_indexed_indirect(runner.draw_data_buffer_indexed, 8)
290+
encoder.draw_indexed_indirect(runner.draw_data_buffer_indexed, 8 + 20)
291+
292+
runner.run_draw_test(runner.expected_result_draw_indexed, draw)
293+
294+
295+
def test_draw_indexed_mixed(runner):
296+
def draw(encoder):
297+
encoder.draw_indexed_indirect(runner.draw_data_buffer_indexed, 8)
298+
encoder.draw_indexed(*runner.draw_indexed_args2)
299+
300+
runner.run_draw_test(runner.expected_result_draw_indexed, draw)
301+
302+
303+
def test_multi_draw_indexed_indirect(runner):
304+
def draw(encoder):
305+
multi_draw_indexed_indirect(
306+
encoder, runner.draw_data_buffer_indexed, offset=8, count=2
307+
)
308+
309+
runner.run_draw_test(runner.expected_result_draw_indexed, draw)
310+
311+
312+
def test_draw_indexed_via_encoder(runner):
313+
def draw(encoder):
314+
encoder.draw_indexed_indirect(runner.draw_data_buffer_indexed, 8)
315+
encoder.draw_indexed(*runner.draw_indexed_args2)
316+
317+
render_bundle_encoder = runner.create_render_bundle_encoder(draw)
318+
for _ in range(2):
319+
runner.run_draw_test(
320+
runner.expected_result_draw_indexed,
321+
lambda encoder: encoder.execute_bundles([render_bundle_encoder]),
322+
)
323+
324+
325+
if __name__ == "__main__":
326+
run_tests(globals())

wgpu/backends/wgpu_native/_api.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,6 +3019,24 @@ def _set_push_constants(self, visibility, offset, size_in_bytes, data, data_offs
30193019
self._internal, int(visibility), offset, size, c_data + data_offset
30203020
)
30213021

3022+
def _multi_draw_indirect(self, buffer, offset, count):
3023+
# H: void f(WGPURenderPassEncoder encoder, WGPUBuffer buffer, uint64_t offset, uint32_t count)
3024+
libf.wgpuRenderPassEncoderMultiDrawIndirect(
3025+
self._internal, buffer._internal, int(offset), int(count)
3026+
)
3027+
3028+
def _multi_draw_indexed_indirect(self, buffer, offset, count):
3029+
# H: void f(WGPURenderPassEncoder encoder, WGPUBuffer buffer, uint64_t offset, uint32_t count)
3030+
libf.wgpuRenderPassEncoderMultiDrawIndexedIndirect(
3031+
self._internal, buffer._internal, int(offset), int(count)
3032+
)
3033+
3034+
def _release(self):
3035+
if self._internal is not None and libf is not None:
3036+
self._internal, internal = None, self._internal
3037+
# H: void f(WGPURenderPassEncoder renderPassEncoder)
3038+
libf.wgpuRenderPassEncoderRelease(internal)
3039+
30223040

30233041
class GPURenderBundleEncoder(
30243042
classes.GPURenderBundleEncoder,

0 commit comments

Comments
 (0)