Skip to content

Commit 8a454a1

Browse files
authored
Context.get_current_texture() can now raise DrawCancelled (#820)
* Context.get_current_texture() can now raise DrawCancelled. * clean * codegen and lint * add docs * ruff/codegen * doc * extra note
1 parent 8173849 commit 8a454a1

8 files changed

Lines changed: 62 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ Possible sections in each release:
2929
* `wgpu.backends.wgpu_native.extras.create_pipeline_layout(..., push_constant_layouts)` -> `device.create_pipeline_layout(..., immediate_size)`
3030
* `wgpu.backends.wgpu_native.extras.set_push_constants()` -> `encoder.set_immediates()` (and different signature)
3131
* in wgsl `var<push_constant>` -> `var<immediate>`
32-
32+
* ``context.get_current_texture()`` now raises ``wgpu.DrawCancelled`` when no texture could be obtained, e.g. when the window is occluded.
33+
Downstream code should capture that error and skip the frame, optionally invoking some sort of sleep to save energy.
34+
See https://github.com/pygfx/wgpu-py/pull/820 for context.
3335

3436

3537
## [v0.31.1] - 23-06-2026

codegen/apipatcher.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,16 @@ def patch_base_api(code):
3838
idl = get_idl_parser()
3939

4040
# Write __all__
41-
extra_public_classes = ["GPUPromise"]
41+
extra_public_classes = ["GPUPromise", "DrawCancelled"]
4242
all_public_classes = [*idl.classes.keys(), *extra_public_classes]
43+
all_public_classes.sort()
44+
all_public_classes.remove("GPU")
45+
all_public_classes.insert(0, "GPU") # ruff RUF022 wants it at the top
4346
part1, found_all, part2 = code.partition("\n__all__ =")
4447
if found_all:
4548
part2 = part2.split("]", 1)[-1]
4649
line = "\n__all__ = ["
47-
line += ", ".join(f'"{name}"' for name in sorted(all_public_classes))
50+
line += ", ".join(f'"{name}"' for name in all_public_classes)
4851
line += "]"
4952
code = part1 + line + part2
5053

docs/wgpu.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Async code
5656

5757
Some methods and properties in the WebGPU API are asynchronous. In wgpu-py, these methods
5858
are always suffixed with ``_async``. These method also have a synchronous variant, which
59-
come in two flafours:
59+
come in two flavours:
6060

6161
* If the method has the plain method name (no suffix), the synchronous method is
6262
available in WebGPU as well. There's no problem to use this variant.
@@ -180,6 +180,9 @@ Error handling
180180
Errors in wgpu-native are raised as Python errors where possible. Uncaught errors
181181
and warnings are logged using the ``wgpu`` logger.
182182

183+
A special :class:`DrawCancelled` exception is raised by ``context.get_current_texture()`` when no
184+
texture could be obtained (e.g. when the window is occluded) and the frame must be skipped.
185+
183186
There are specific exceptions that can be raised:
184187
* :class:`GPUError` is the generic (base) error class.
185188
* :class:`GPUValidationError` is for wgpu validation errors. Shader errors also fall into this category.
@@ -221,6 +224,7 @@ List of GPU classes
221224
:template: wgpu_class_layout.rst
222225

223226
~GPU
227+
~DrawCancelled
224228
~GPUAdapterInfo
225229
~GPUAdapter
226230
~GPUBindGroup

examples/gui_direct.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ def main():
5151
context.set_physical_size(*glfw.get_framebuffer_size(window))
5252

5353
# draw a frame
54-
draw_frame()
54+
try:
55+
draw_frame()
56+
except wgpu.DrawCancelled: # window occluded
57+
time.sleep(0.1)
58+
continue
5559
# present the frame to the screen
5660
context.present()
5761
# stats

examples/gui_direct2.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ def run(self):
8989
print(err)
9090

9191
# draw a frame
92-
draw_frame()
92+
try:
93+
draw_frame()
94+
except wgpu.DrawCancelled: # window occluded
95+
time.sleep(0.1)
96+
continue
9397
# present the frame to the screen
9498
context.present()
9599
# stats

wgpu/_classes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
__all__ = [
2525
"GPU",
26+
"DrawCancelled",
2627
"GPUAdapter",
2728
"GPUAdapterInfo",
2829
"GPUBindGroup",
@@ -2449,6 +2450,13 @@ def reason(self) -> enums.DeviceLostReasonEnum:
24492450
return self._reason
24502451

24512452

2453+
@apidiff.add("Added because Python deals with native windows")
2454+
class DrawCancelled(Exception): # noqa: N818
2455+
"""An excepion raised to cancel a draw when the surface texture could not be obtained,
2456+
e.g. because the window is hidden or occluded.
2457+
"""
2458+
2459+
24522460
class GPUError(Exception):
24532461
"""A generic GPU error."""
24542462

wgpu/backends/wgpu_native/_api.py

Lines changed: 28 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,6 @@ class GPUCanvasContext(classes.GPUCanvasContext):
706706

707707
_surface_id = ffi.NULL
708708
_wgpu_config = None
709-
_skip_present_screen = False
710709

711710
def __init__(self, present_info: dict):
712711
super().__init__(present_info)
@@ -930,8 +929,6 @@ def _create_texture_screen(self):
930929
# Clear buffer, so we only have to perform these checks when set_physical_size has been called.
931930
self._new_physical_size = None
932931

933-
# Prepare for obtaining a texture.
934-
status_str_map = enum_int2str["SurfaceGetCurrentTextureStatus"]
935932
# H: nextInChain: WGPUChainedStruct *, texture: WGPUTexture, status: WGPUSurfaceGetCurrentTextureStatus
936933
surface_texture = new_struct_p(
937934
"WGPUSurfaceTexture *",
@@ -944,64 +941,61 @@ def _create_texture_screen(self):
944941
# H: void f(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture)
945942
libf.wgpuSurfaceGetCurrentTexture(self._surface_id, surface_texture)
946943
status_int = surface_texture.status
947-
status_str = status_str_map.get(status_int, "Unknown")
948944
texture_id = surface_texture.texture
949945

950946
if status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
951947
# Yay! Everything is good and we can render this frame.
952948
self._number_of_successive_unsuccesful_textures = 0
949+
elif status_int == lib.WGPUSurfaceGetCurrentTextureStatus_Occluded:
950+
# Nothing is wrong, but this frame must be skipped
951+
self._number_of_successive_unsuccesful_textures = 0
952+
raise classes.DrawCancelled("Occluded")
953953
elif status_int in [
954954
lib.WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal,
955-
lib.WGPUSurfaceGetCurrentTextureStatus_Timeout,
956955
lib.WGPUSurfaceGetCurrentTextureStatus_Outdated,
957956
lib.WGPUSurfaceGetCurrentTextureStatus_Lost,
958957
]:
958+
# We can try to re-configure in some cases
959959
if texture_id:
960960
# H: void f(WGPUTexture texture)
961961
libf.wgpuTextureRelease(texture_id)
962962
texture_id = 0
963-
# Try to re-configure, if we can
964963
self._configure_screen_real()
965964
# H: void f(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture)
966965
libf.wgpuSurfaceGetCurrentTexture(self._surface_id, surface_texture)
967966
status_int = surface_texture.status
968-
status_str = status_str_map.get(status_int, "Unknown")
969967
texture_id = surface_texture.texture
970968

971969
# If still not optimal, we need to make some decisions ...
972970
if status_int != lib.WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
973971
# It's ok if we miss a sporadic frame during resizing, but warn if it becomes too much.
972+
status_str_map = enum_int2str["SurfaceGetCurrentTextureStatus"]
973+
status_str = status_str_map.get(status_int, "Unknown")
974974
self._number_of_successive_unsuccesful_textures += 1
975-
if self._number_of_successive_unsuccesful_textures > 5:
976-
n = self._number_of_successive_unsuccesful_textures
977-
self._number_of_successive_unsuccesful_textures = 0
975+
n = self._number_of_successive_unsuccesful_textures
976+
if (n % 100 == 0) or n in (5, 12, 25, 50):
978977
logger.warning(
979978
f"No succesful surface texture obtained for {n} frames: {status_str!r}"
980979
)
980+
981981
# Decide what to do
982982
if status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal:
983983
# Can still use the texture
984984
pass
985-
elif status_int in [
986-
lib.WGPUSurfaceGetCurrentTextureStatus_Timeout,
987-
lib.WGPUSurfaceGetCurrentTextureStatus_Outdated,
988-
lib.WGPUSurfaceGetCurrentTextureStatus_Lost,
989-
]:
990-
# Use a dummy texture that we cannot present
991-
if texture_id:
992-
# H: void f(WGPUTexture texture)
993-
libf.wgpuTextureRelease(texture_id)
994-
texture_id = 0
995-
self._skip_present_screen = True
996-
return self._create_plain_texture()
997985
else:
986+
# lib.WGPUSurfaceGetCurrentTextureStatus_Timeout,
987+
# lib.WGPUSurfaceGetCurrentTextureStatus_Outdated,
988+
# lib.WGPUSurfaceGetCurrentTextureStatus_Lost,
998989
# WGPUSurfaceGetCurrentTextureStatus_OutOfMemory
999990
# WGPUSurfaceGetCurrentTextureStatus_DeviceLost
1000991
# WGPUSurfaceGetCurrentTextureStatus_Error
1001-
# This is something we cannot recover from.
1002-
raise RuntimeError(
1003-
f"Cannot get surface texture: {status_str} ({status_int})."
1004-
)
992+
993+
# Skip the frame
994+
if texture_id:
995+
# H: void f(WGPUTexture texture)
996+
libf.wgpuTextureRelease(texture_id)
997+
texture_id = 0
998+
raise classes.DrawCancelled(f"{status_str} ({status_int})")
1005999

10061000
# I don't expect this to happen, but let's check just in case.
10071001
if not texture_id:
@@ -1046,29 +1040,11 @@ def _create_texture_screen(self):
10461040
device = self._config["device"]
10471041
return GPUTexture(label, texture_id, device, tex_info)
10481042

1049-
def _create_plain_texture(self):
1050-
# To have a dummy texture in case we have a size mismatch and must drop frames
1051-
width, height = self._physical_size
1052-
width, height = max(width, 1), max(height, 1)
1053-
1054-
# Note that the label 'present' is used by read_texture() to determine
1055-
# that it can use a shared copy buffer.
1056-
device = self._config["device"]
1057-
return device.create_texture(
1058-
label="present",
1059-
size=(width, height, 1),
1060-
format=self._config["format"],
1061-
usage=self._config["usage"] | flags.TextureUsage.COPY_SRC,
1062-
)
1063-
10641043
def _present_screen(self):
1065-
if self._skip_present_screen:
1066-
self._skip_present_screen = False
1067-
else:
1068-
# H: WGPUStatus f(WGPUSurface surface)
1069-
status = libf.wgpuSurfacePresent(self._surface_id)
1070-
if status != lib.WGPUStatus_Success:
1071-
logger.warning("wgpuSurfacePresent failed")
1044+
# H: WGPUStatus f(WGPUSurface surface)
1045+
status = libf.wgpuSurfacePresent(self._surface_id)
1046+
if status != lib.WGPUStatus_Success:
1047+
logger.warning("wgpuSurfacePresent failed")
10721048

10731049
def _release(self):
10741050
self._drop_texture()
@@ -4213,6 +4189,10 @@ class GPUInternalError(classes.GPUInternalError, GPUError):
42134189
pass
42144190

42154191

4192+
class DrawCancelled(classes.DrawCancelled):
4193+
pass
4194+
4195+
42164196
# %%
42174197

42184198

wgpu/resources/codegen_report.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
* Diffs for GPUTextureView: add size, add texture
2121
* Diffs for GPUBindingCommandsMixin: change set_bind_group
2222
* Diffs for GPUQueue: add read_buffer, add read_texture, hide copy_external_image_to_texture
23-
* Validated 38 classes, 121 methods, 52 properties
23+
* Diffs for DrawCancelled: add DrawCancelled
24+
* Validated 39 classes, 121 methods, 52 properties
2425
### Patching API for backends/wgpu_native/_api.py
25-
* Validated 38 classes, 116 methods, 0 properties
26+
* Validated 39 classes, 115 methods, 0 properties
2627
## Validating backends/wgpu_native/_api.py
2728
* Enum PipelineErrorReason missing in webgpu.h
2829
* Enum AutoLayoutMode missing in webgpu.h

0 commit comments

Comments
 (0)