Skip to content

Commit bd54418

Browse files
authored
Improve get_current_texture for window resizing (#705)
* Improve get_current_texture for window resizing * dont present when using dummy tex * warn on successive fails * small fix * correct * Wrap up * codegen
1 parent 2465dc8 commit bd54418

File tree

3 files changed

+109
-60
lines changed

3 files changed

+109
-60
lines changed

wgpu/_classes.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,13 +448,12 @@ def _create_texture_bitmap(self):
448448
# Note that the label 'present' is used by read_texture() to determine
449449
# that it can use a shared copy buffer.
450450
device = self._config["device"]
451-
self._texture = device.create_texture(
451+
return device.create_texture(
452452
label="present",
453453
size=(width, height, 1),
454454
format=self._config["format"],
455455
usage=self._config["usage"] | flags.TextureUsage.COPY_SRC,
456456
)
457-
return self._texture
458457

459458
def _create_texture_screen(self):
460459
raise NotImplementedError()

wgpu/backends/wgpu_native/_api.py

Lines changed: 107 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,8 @@ class GPUCanvasContext(classes.GPUCanvasContext):
674674
# the more cryptic Rust panics.
675675

676676
_surface_id = ffi.NULL
677+
_wgpu_config = None
678+
_skip_present_screen = False
677679

678680
def __init__(self, canvas, present_methods):
679681
super().__init__(canvas, present_methods)
@@ -685,6 +687,9 @@ def __init__(self, canvas, present_methods):
685687
else: # method == "bitmap"
686688
self._surface_id = ffi.NULL
687689

690+
# A stat for get_current_texture
691+
self._number_of_successive_unsuccesful_textures = 0
692+
688693
def _get_capabilities_screen(self, adapter):
689694
adapter_id = adapter._internal
690695
surface_id = self._surface_id
@@ -815,6 +820,7 @@ def _configure_screen(
815820
c_present_mode = getattr(lib, f"WGPUPresentMode_{present_mode.capitalize()}")
816821

817822
# Prepare config object
823+
width, height = self._get_canvas().get_physical_size()
818824

819825
# H: nextInChain: WGPUChainedStruct *, device: WGPUDevice, format: WGPUTextureFormat, usage: WGPUTextureUsage/int, width: int, height: int, viewFormatCount: int, viewFormats: WGPUTextureFormat *, alphaMode: WGPUCompositeAlphaMode, presentMode: WGPUPresentMode
820826
self._wgpu_config = new_struct_p(
@@ -826,52 +832,70 @@ def _configure_screen(
826832
viewFormatCount=len(view_formats),
827833
viewFormats=c_view_formats,
828834
alphaMode=c_alpha_mode,
829-
width=0,
830-
height=0,
831835
presentMode=c_present_mode,
836+
width=width, # overriden elsewhere in this class
837+
height=height, # overriden elsewhere in this class
832838
)
833839

834-
def _configure_screen_real(self, width, height):
840+
# Configure now (if possible)
841+
self._configure_screen_real()
842+
843+
def _configure_screen_real(self):
835844
# If a texture is still active, better release it first
836845
self._drop_texture()
837-
# Set the size
838-
self._wgpu_config.width = width
839-
self._wgpu_config.height = height
840-
if width <= 0 or height <= 0:
841-
raise RuntimeError(
842-
"Cannot configure canvas that has no pixels ({width}x{height})."
843-
)
844846
# Configure, and store the config if we did not error out
845-
if self._surface_id:
847+
if (
848+
self._surface_id
849+
and self._wgpu_config.width > 0
850+
and self._wgpu_config.height > 0
851+
):
846852
# H: void f(WGPUSurface surface, WGPUSurfaceConfiguration const * config)
847853
libf.wgpuSurfaceConfigure(self._surface_id, self._wgpu_config)
854+
else:
855+
# Set size to zero, to trigger auto-configure later
856+
self._wgpu_config.width = 0
848857

849858
def _unconfigure_screen(self):
850859
if self._surface_id:
851860
# H: void f(WGPUSurface surface)
852861
libf.wgpuSurfaceUnconfigure(self._surface_id)
862+
self._wgpu_config = None
853863

854864
def _create_texture_screen(self):
855-
surface_id = self._surface_id
865+
# Check
866+
if self._surface_id is None:
867+
raise RuntimeError("Looks like the CanvasContext is already destroyed.")
868+
if self._wgpu_config is None:
869+
raise RuntimeError(
870+
"Cannot get surface texture because the CanvasContext has not yet been configured."
871+
)
856872

857-
# Reconfigure when the canvas has resized.
858-
# On some systems (Windows+Qt) this is not necessary, because
859-
# the texture status would be Outdated below, resulting in a
860-
# reconfigure. But on others (e.g. glfwf) the texture size does
861-
# not have to match the window size, apparently. The downside
862-
# for doing this check on the former systems, is that errors
863-
# get logged, which would not be there if we did not
864-
# pre-emptively reconfigure. These log entries are harmless but
865-
# annoying, and I currently don't know how to prevent them
866-
# elegantly. See issue #352
873+
# When the window size has changed, we need to reconfigure. If we wouldn't:
874+
#
875+
# * On some systems (seen on MacOS with glfw and Qt) the texture status that we get below
876+
# will happily report 'SuccessOptimal', even when the the window has resized, and the
877+
# texture will simply be stretched to fit the window. I believe this can be considered a bug.
878+
# * On other systems (seen on Windows and Linux) the texture status would report 'SuccessSuboptimal',
879+
# and the texture will either be stretched (Windows) or blitted to the window leaving either
880+
# part of the texture invisible, or making part of the window black/transparent (Linux).
881+
# * On some systems the texture status is 'Outdated' even if we do set the size. We deal with
882+
# that by providing a dummy texture, and warn when this happens too often in succession.
883+
884+
# Get size info
867885
old_size = (self._wgpu_config.width, self._wgpu_config.height)
868886
new_size = tuple(self._get_canvas().get_physical_size())
869-
if old_size != new_size:
870-
self._configure_screen_real(*new_size)
871-
872-
# Try to obtain a texture.
873-
# `If it fails, depending on status, we reconfigure and try again.
874-
887+
if new_size[0] <= 0 or new_size[1] <= 0:
888+
# It's the responsibility of the drawing /scheduling logic to prevent this case.
889+
raise RuntimeError("Cannot get texture for a canvas with zero pixels.")
890+
891+
# Re-configure when the size has changed.
892+
if new_size != old_size:
893+
self._wgpu_config.width = new_size[0]
894+
self._wgpu_config.height = new_size[1]
895+
self._configure_screen_real()
896+
897+
# Prepare for obtaining a texture.
898+
status_str_map = enum_int2str["SurfaceGetCurrentTextureStatus"]
875899
# H: nextInChain: WGPUChainedStructOut *, texture: WGPUTexture, status: WGPUSurfaceGetCurrentTextureStatus
876900
surface_texture = new_struct_p(
877901
"WGPUSurfaceTexture *",
@@ -880,41 +904,65 @@ def _create_texture_screen(self):
880904
# not used: status
881905
)
882906

883-
for attempt in [1, 2]:
884-
# H: void f(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture)
885-
libf.wgpuSurfaceGetCurrentTexture(surface_id, surface_texture)
886-
status_int = surface_texture.status
887-
status_str = enum_int2str["SurfaceGetCurrentTextureStatus"].get(
888-
status_int, "Unknown"
889-
)
890-
texture_id = surface_texture.texture
891-
if status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
892-
break # Yay! Everything is good and we can render this frame.
893-
elif status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal:
894-
# Still OK - the surface can present the frame, but in a suboptimal way. The surface may need reconfiguration.
895-
logger.warning("The surface texture is suboptimal.")
896-
break
907+
# Try to obtain texture
908+
# H: void f(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture)
909+
libf.wgpuSurfaceGetCurrentTexture(self._surface_id, surface_texture)
910+
status_int = surface_texture.status
911+
status_str = status_str_map.get(status_int, "Unknown")
912+
texture_id = surface_texture.texture
913+
914+
if status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
915+
# Yay! Everything is good and we can render this frame.
916+
self._number_of_successive_unsuccesful_textures = 0
917+
elif status_int in [
918+
lib.WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal,
919+
lib.WGPUSurfaceGetCurrentTextureStatus_Timeout,
920+
lib.WGPUSurfaceGetCurrentTextureStatus_Outdated,
921+
lib.WGPUSurfaceGetCurrentTextureStatus_Lost,
922+
]:
897923
if texture_id:
898924
# H: void f(WGPUTexture texture)
899925
libf.wgpuTextureRelease(texture_id)
900-
if attempt == 1 and status_int in [
926+
texture_id = 0
927+
# Try to re-configure, if we can
928+
self._configure_screen_real()
929+
# H: void f(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture)
930+
libf.wgpuSurfaceGetCurrentTexture(self._surface_id, surface_texture)
931+
status_int = surface_texture.status
932+
status_str = status_str_map.get(status_int, "Unknown")
933+
texture_id = surface_texture.texture
934+
935+
# If still not optimal, we need to make some decisions ...
936+
if status_int != lib.WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
937+
# It's ok if we miss a sporadic frame during resizing, but warn if it becomes too much.
938+
self._number_of_successive_unsuccesful_textures += 1
939+
if self._number_of_successive_unsuccesful_textures > 5:
940+
n = self._number_of_successive_unsuccesful_textures
941+
self._number_of_successive_unsuccesful_textures = 0
942+
logger.warning(
943+
f"No succesful surface texture obtained for {n} frames: {status_str!r}"
944+
)
945+
# Decide what to do
946+
if status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal:
947+
# Can still use the texture
948+
pass
949+
elif status_int in [
901950
lib.WGPUSurfaceGetCurrentTextureStatus_Timeout,
902951
lib.WGPUSurfaceGetCurrentTextureStatus_Outdated,
903952
lib.WGPUSurfaceGetCurrentTextureStatus_Lost,
904953
]:
905-
# Configure and try again.
906-
# On Window+Qt this happens e.g. when the window has resized
907-
# (status==Outdated), but also when moving the window from one
908-
# monitor to another with different scale-factor.
909-
logger.info(
910-
f"Re-configuring canvas context, because {status_str} ({status_int})."
911-
)
912-
self._configure_screen_real(*new_size)
954+
# Use a dummy texture that we cannot present
955+
if texture_id:
956+
# H: void f(WGPUTexture texture)
957+
libf.wgpuTextureRelease(texture_id)
958+
texture_id = 0
959+
self._skip_present_screen = True
960+
return self._create_texture_bitmap()
913961
else:
914962
# WGPUSurfaceGetCurrentTextureStatus_OutOfMemory
915963
# WGPUSurfaceGetCurrentTextureStatus_DeviceLost
916964
# WGPUSurfaceGetCurrentTextureStatus_Error
917-
# Or if this is the second attempt.
965+
# This is something we cannot recover from.
918966
raise RuntimeError(
919967
f"Cannot get surface texture: {status_str} ({status_int})."
920968
)
@@ -959,15 +1007,17 @@ def _create_texture_screen(self):
9591007
"format": format,
9601008
"usage": usage,
9611009
}
962-
9631010
device = self._config["device"]
9641011
return GPUTexture(label, texture_id, device, tex_info)
9651012

9661013
def _present_screen(self):
967-
# H: WGPUStatus f(WGPUSurface surface)
968-
status = libf.wgpuSurfacePresent(self._surface_id)
969-
if status != lib.WGPUStatus_Success:
970-
raise RuntimeError("Error calling wgpuSurfacePresent")
1014+
if self._skip_present_screen:
1015+
self._skip_present_screen = False
1016+
else:
1017+
# H: WGPUStatus f(WGPUSurface surface)
1018+
status = libf.wgpuSurfacePresent(self._surface_id)
1019+
if status != lib.WGPUStatus_Success:
1020+
logger.warning("wgpuSurfacePresent failed")
9711021

9721022
def _release(self):
9731023
self._drop_texture()

wgpu/resources/codegen_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
* Enum CanvasAlphaMode missing in wgpu.h
3030
* Enum CanvasToneMappingMode missing in wgpu.h
3131
* Wrote 255 enum mappings and 47 struct-field mappings to wgpu_native/_mappings.py
32-
* Validated 149 C function calls
32+
* Validated 151 C function calls
3333
* Not using 69 C functions
3434
* Validated 95 C structs

0 commit comments

Comments
 (0)