Skip to content

Commit 72997c6

Browse files
almarkleinVipitis
andauthored
Add preferred features for preconfigure_default_device (#817)
* Add preferred features for preconfigure_default_device * use intersect * Apply suggestion from @Vipitis Co-authored-by: Jan <Vipitis@users.noreply.github.com> * Apply suggestion from @almarklein * tweak docs and error message --------- Co-authored-by: Jan <Vipitis@users.noreply.github.com>
1 parent 414c53b commit 72997c6

2 files changed

Lines changed: 88 additions & 6 deletions

File tree

tests/test_util_default_device.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,70 @@ def test_default_device_configure_required_features(caplog):
222222
helper.preconfigure_default_device("test", required_features={"shader-f16"})
223223

224224

225+
def test_default_device_configure_preferred_features(caplog):
226+
227+
# This is normal
228+
229+
helper = DefaultDeviceHelper()
230+
helper.preconfigure_default_device("test", required_features={"float32-filterable"})
231+
device = helper.get_default_device()
232+
assert device.features == {"float32-filterable"}
233+
234+
# This does not work; not a standard feature
235+
236+
helper = DefaultDeviceHelper()
237+
with pytest.raises(ValueError):
238+
helper.preconfigure_default_device(
239+
"test", required_features={"texture-format16bit-norm"}
240+
)
241+
242+
# preferred features to the rescue
243+
244+
helper = DefaultDeviceHelper()
245+
helper.preconfigure_default_device(
246+
"test", preferred_features={"texture-format16bit-norm"}
247+
)
248+
device = helper.get_default_device()
249+
assert device.features == {"texture-format16bit-norm"}
250+
251+
# Dropping also works
252+
253+
helper = DefaultDeviceHelper()
254+
helper.preconfigure_default_device(
255+
"test", preferred_features={"texture-format16bit-norm"}
256+
)
257+
helper.preconfigure_default_device(
258+
"test", preferred_features={"!texture-format16bit-norm"}
259+
)
260+
device = helper.get_default_device()
261+
assert device.features == set()
262+
263+
# Another variant
264+
265+
helper = DefaultDeviceHelper()
266+
helper.preconfigure_default_device(
267+
"test",
268+
preferred_features={
269+
"float32-filterable",
270+
"texture-format16bit-norm",
271+
"not-actuallt-a-feature",
272+
},
273+
)
274+
device = helper.get_default_device()
275+
assert device.features == {"float32-filterable", "texture-format16bit-norm"}
276+
277+
# A pattern for pygfx
278+
helper = DefaultDeviceHelper()
279+
helper.preconfigure_default_device(
280+
"test", preferred_features={"texture-formats-tier1", "texture-format16bit-norm"}
281+
)
282+
device = helper.get_default_device()
283+
# At least one should be active
284+
assert device.features & {"texture-formats-tier1", "texture-format16bit-norm"}
285+
# Its currently this one, but this will likely change, see https://github.com/gfx-rs/wgpu/issues/8122
286+
assert device.features == {"texture-format16bit-norm"}
287+
288+
225289
def test_default_device_configure_required_limits(caplog):
226290
helper = DefaultDeviceHelper()
227291

wgpu/utils/device.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def preconfigure_default_device(
4444
adapter: GPUAdapter | None = None,
4545
# Device arguments
4646
label: str | None = None,
47+
preferred_features: set[str] | None = None,
4748
required_features: set[enums.FeatureNameEnum] | None = None,
4849
required_limits: dict[str, int | None] | None = None,
4950
# default_queue: structs.QueueDescriptorStruct | None = None,
@@ -58,7 +59,7 @@ def preconfigure_default_device(
5859
use wgpu can each require the features they need. For required features
5960
the union of set features is used. For required limits the minimum of
6061
each set limit is used. For the other arguments, the last set value is
61-
used, and a warning is logged when a value is overriden.
62+
used, and a warning is logged when a value is overridden.
6263
6364
Arguments:
6465
caller_info (str): A very brief description of the code that calls
@@ -74,9 +75,12 @@ def preconfigure_default_device(
7475
Setting the adapter overrules all other adapter settings
7576
(feature_level, power_preference, force_fallback_adapter, canvas).
7677
label (str): A human-readable label for the device.
77-
required_features (list of str): the features (extensions) that you need.
78-
Features can also be discarded by prefixing them with '!'. This is not recommended
79-
unless for testing and very specific use-cases.
78+
preferred_features (set of str): the features (extensions) that you want but do not strictly need.
79+
Check ``device.features`` for its success. Backend-specific / native features are allowed too.
80+
Preferred features can also be discarded by prefixing them with '!'.
81+
required_features (set of str): the features (extensions) that you need.
82+
Required features can also be discarded by prefixing them with '!'. This is not recommended
83+
except for testing and very specific use-cases. Only official features (from ``wgpu.FeatureName``) are allowed.
8084
required_limits (dict): the various limits that you want to apply.
8185
Limits can also be discarded by setting their value to None.
8286
"""
@@ -98,6 +102,8 @@ def preconfigure_default_device(
98102

99103
if isinstance(required_features, (tuple, list)):
100104
required_features = set(required_features)
105+
if isinstance(preferred_features, (tuple, list)):
106+
preferred_features = set(preferred_features)
101107

102108
ak, dk = self._adapter_kwargs, self._device_kwargs
103109

@@ -108,6 +114,7 @@ def preconfigure_default_device(
108114
(ak, "canvas", canvas, None, None),
109115
(ak, "adapter", adapter, GPUAdapter, None),
110116
(dk, "label", label, str, None),
117+
(dk, "preferred_features", preferred_features, set, None),
111118
(dk, "required_features", required_features, set, enums.FeatureName),
112119
(dk, "required_limits", required_limits, dict, None),
113120
]:
@@ -127,8 +134,11 @@ def preconfigure_default_device(
127134
for value in values:
128135
value = value.lstrip("!")
129136
if value not in arg_values:
137+
tip = ""
138+
if arg_name == "required_features":
139+
tip = f" If {value!r} is a native feature, use it in preferred_features instead."
130140
raise ValueError(
131-
f"preconfigure_default_device ({caller_info}): {what} must be a one of {set(arg_values)}, but got {value!r}."
141+
f"preconfigure_default_device ({caller_info}): {what} must be a one of {arg_values}, but got {value!r}.{tip}"
132142
)
133143
if isinstance(arg_value, set):
134144
cur_value = arg_dict.setdefault(arg_name, set())
@@ -195,12 +205,20 @@ def get_default_device(self) -> GPUDevice:
195205
The default device can be configured at import-time using ``preconfigure_default_device()``.
196206
"""
197207
if self._the_device is None:
208+
# Get adapter
198209
adapter: GPUAdapter = self._adapter_kwargs.pop("adapter", None)
199210
if adapter is None:
200211
adapter = wgpu.gpu.request_adapter_sync(**self._adapter_kwargs)
201-
self._the_device = adapter.request_device_sync(**self._device_kwargs)
212+
# Handle preferred features
213+
kwargs = self._device_kwargs.copy()
214+
required_features = kwargs.get("required_features", set())
215+
extra_features = kwargs.pop("preferred_features", set()) & adapter.features
216+
kwargs["required_features"] = required_features | extra_features
217+
# Create device
218+
self._the_device = adapter.request_device_sync(**kwargs)
202219
self._adapter_kwargs.clear()
203220
self._device_kwargs.clear()
221+
204222
return self._the_device
205223

206224

0 commit comments

Comments
 (0)