Skip to content

Improve get_current_texture for window resizing #705

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

Merged
merged 8 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions wgpu/_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,13 +448,12 @@ def _create_texture_bitmap(self):
# Note that the label 'present' is used by read_texture() to determine
# that it can use a shared copy buffer.
device = self._config["device"]
self._texture = device.create_texture(
return device.create_texture(
label="present",
size=(width, height, 1),
format=self._config["format"],
usage=self._config["usage"] | flags.TextureUsage.COPY_SRC,
)
return self._texture

def _create_texture_screen(self):
raise NotImplementedError()
Expand Down
164 changes: 107 additions & 57 deletions wgpu/backends/wgpu_native/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,8 @@ class GPUCanvasContext(classes.GPUCanvasContext):
# the more cryptic Rust panics.

_surface_id = ffi.NULL
_wgpu_config = None
_skip_present_screen = False

def __init__(self, canvas, present_methods):
super().__init__(canvas, present_methods)
Expand All @@ -685,6 +687,9 @@ def __init__(self, canvas, present_methods):
else: # method == "bitmap"
self._surface_id = ffi.NULL

# A stat for get_current_texture
self._number_of_successive_unsuccesful_textures = 0

def _get_capabilities_screen(self, adapter):
adapter_id = adapter._internal
surface_id = self._surface_id
Expand Down Expand Up @@ -815,6 +820,7 @@ def _configure_screen(
c_present_mode = getattr(lib, f"WGPUPresentMode_{present_mode.capitalize()}")

# Prepare config object
width, height = self._get_canvas().get_physical_size()

# H: nextInChain: WGPUChainedStruct *, device: WGPUDevice, format: WGPUTextureFormat, usage: WGPUTextureUsage/int, width: int, height: int, viewFormatCount: int, viewFormats: WGPUTextureFormat *, alphaMode: WGPUCompositeAlphaMode, presentMode: WGPUPresentMode
self._wgpu_config = new_struct_p(
Expand All @@ -826,52 +832,70 @@ def _configure_screen(
viewFormatCount=len(view_formats),
viewFormats=c_view_formats,
alphaMode=c_alpha_mode,
width=0,
height=0,
presentMode=c_present_mode,
width=width, # overriden elsewhere in this class
height=height, # overriden elsewhere in this class
)

def _configure_screen_real(self, width, height):
# Configure now (if possible)
self._configure_screen_real()

def _configure_screen_real(self):
# If a texture is still active, better release it first
self._drop_texture()
# Set the size
self._wgpu_config.width = width
self._wgpu_config.height = height
if width <= 0 or height <= 0:
raise RuntimeError(
"Cannot configure canvas that has no pixels ({width}x{height})."
)
# Configure, and store the config if we did not error out
if self._surface_id:
if (
self._surface_id
and self._wgpu_config.width > 0
and self._wgpu_config.height > 0
):
# H: void f(WGPUSurface surface, WGPUSurfaceConfiguration const * config)
libf.wgpuSurfaceConfigure(self._surface_id, self._wgpu_config)
else:
# Set size to zero, to trigger auto-configure later
self._wgpu_config.width = 0

def _unconfigure_screen(self):
if self._surface_id:
# H: void f(WGPUSurface surface)
libf.wgpuSurfaceUnconfigure(self._surface_id)
self._wgpu_config = None

def _create_texture_screen(self):
surface_id = self._surface_id
# Check
if self._surface_id is None:
raise RuntimeError("Looks like the CanvasContext is already destroyed.")
if self._wgpu_config is None:
raise RuntimeError(
"Cannot get surface texture because the CanvasContext has not yet been configured."
)

# Reconfigure when the canvas has resized.
# On some systems (Windows+Qt) this is not necessary, because
# the texture status would be Outdated below, resulting in a
# reconfigure. But on others (e.g. glfwf) the texture size does
# not have to match the window size, apparently. The downside
# for doing this check on the former systems, is that errors
# get logged, which would not be there if we did not
# pre-emptively reconfigure. These log entries are harmless but
# annoying, and I currently don't know how to prevent them
# elegantly. See issue #352
# When the window size has changed, we need to reconfigure. If we wouldn't:
#
# * On some systems (seen on MacOS with glfw and Qt) the texture status that we get below
# will happily report 'SuccessOptimal', even when the the window has resized, and the
# texture will simply be stretched to fit the window. I believe this can be considered a bug.
# * On other systems (seen on Windows and Linux) the texture status would report 'SuccessSuboptimal',
# and the texture will either be stretched (Windows) or blitted to the window leaving either
# part of the texture invisible, or making part of the window black/transparent (Linux).
# * On some systems the texture status is 'Outdated' even if we do set the size. We deal with
# that by providing a dummy texture, and warn when this happens too often in succession.

# Get size info
old_size = (self._wgpu_config.width, self._wgpu_config.height)
new_size = tuple(self._get_canvas().get_physical_size())
if old_size != new_size:
self._configure_screen_real(*new_size)

# Try to obtain a texture.
# `If it fails, depending on status, we reconfigure and try again.

if new_size[0] <= 0 or new_size[1] <= 0:
# It's the responsibility of the drawing /scheduling logic to prevent this case.
raise RuntimeError("Cannot get texture for a canvas with zero pixels.")

# Re-configure when the size has changed.
if new_size != old_size:
self._wgpu_config.width = new_size[0]
self._wgpu_config.height = new_size[1]
self._configure_screen_real()

# Prepare for obtaining a texture.
status_str_map = enum_int2str["SurfaceGetCurrentTextureStatus"]
# H: nextInChain: WGPUChainedStructOut *, texture: WGPUTexture, status: WGPUSurfaceGetCurrentTextureStatus
surface_texture = new_struct_p(
"WGPUSurfaceTexture *",
Expand All @@ -880,41 +904,65 @@ def _create_texture_screen(self):
# not used: status
)

for attempt in [1, 2]:
# H: void f(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture)
libf.wgpuSurfaceGetCurrentTexture(surface_id, surface_texture)
status_int = surface_texture.status
status_str = enum_int2str["SurfaceGetCurrentTextureStatus"].get(
status_int, "Unknown"
)
texture_id = surface_texture.texture
if status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
break # Yay! Everything is good and we can render this frame.
elif status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal:
# Still OK - the surface can present the frame, but in a suboptimal way. The surface may need reconfiguration.
logger.warning("The surface texture is suboptimal.")
break
# Try to obtain texture
# H: void f(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture)
libf.wgpuSurfaceGetCurrentTexture(self._surface_id, surface_texture)
status_int = surface_texture.status
status_str = status_str_map.get(status_int, "Unknown")
texture_id = surface_texture.texture

if status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
# Yay! Everything is good and we can render this frame.
self._number_of_successive_unsuccesful_textures = 0
elif status_int in [
lib.WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal,
lib.WGPUSurfaceGetCurrentTextureStatus_Timeout,
lib.WGPUSurfaceGetCurrentTextureStatus_Outdated,
lib.WGPUSurfaceGetCurrentTextureStatus_Lost,
]:
if texture_id:
# H: void f(WGPUTexture texture)
libf.wgpuTextureRelease(texture_id)
if attempt == 1 and status_int in [
texture_id = 0
# Try to re-configure, if we can
self._configure_screen_real()
# H: void f(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture)
libf.wgpuSurfaceGetCurrentTexture(self._surface_id, surface_texture)
status_int = surface_texture.status
status_str = status_str_map.get(status_int, "Unknown")
texture_id = surface_texture.texture

# If still not optimal, we need to make some decisions ...
if status_int != lib.WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
# It's ok if we miss a sporadic frame during resizing, but warn if it becomes too much.
self._number_of_successive_unsuccesful_textures += 1
if self._number_of_successive_unsuccesful_textures > 5:
n = self._number_of_successive_unsuccesful_textures
self._number_of_successive_unsuccesful_textures = 0
logger.warning(
f"No succesful surface texture obtained for {n} frames: {status_str!r}"
)
# Decide what to do
if status_int == lib.WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal:
# Can still use the texture
pass
elif status_int in [
lib.WGPUSurfaceGetCurrentTextureStatus_Timeout,
lib.WGPUSurfaceGetCurrentTextureStatus_Outdated,
lib.WGPUSurfaceGetCurrentTextureStatus_Lost,
]:
# Configure and try again.
# On Window+Qt this happens e.g. when the window has resized
# (status==Outdated), but also when moving the window from one
# monitor to another with different scale-factor.
logger.info(
f"Re-configuring canvas context, because {status_str} ({status_int})."
)
self._configure_screen_real(*new_size)
# Use a dummy texture that we cannot present
if texture_id:
# H: void f(WGPUTexture texture)
libf.wgpuTextureRelease(texture_id)
texture_id = 0
self._skip_present_screen = True
return self._create_texture_bitmap()
else:
# WGPUSurfaceGetCurrentTextureStatus_OutOfMemory
# WGPUSurfaceGetCurrentTextureStatus_DeviceLost
# WGPUSurfaceGetCurrentTextureStatus_Error
# Or if this is the second attempt.
# This is something we cannot recover from.
raise RuntimeError(
f"Cannot get surface texture: {status_str} ({status_int})."
)
Expand Down Expand Up @@ -959,15 +1007,17 @@ def _create_texture_screen(self):
"format": format,
"usage": usage,
}

device = self._config["device"]
return GPUTexture(label, texture_id, device, tex_info)

def _present_screen(self):
# H: WGPUStatus f(WGPUSurface surface)
status = libf.wgpuSurfacePresent(self._surface_id)
if status != lib.WGPUStatus_Success:
raise RuntimeError("Error calling wgpuSurfacePresent")
if self._skip_present_screen:
self._skip_present_screen = False
else:
# H: WGPUStatus f(WGPUSurface surface)
status = libf.wgpuSurfacePresent(self._surface_id)
if status != lib.WGPUStatus_Success:
logger.warning("wgpuSurfacePresent failed")

def _release(self):
self._drop_texture()
Expand Down
2 changes: 1 addition & 1 deletion 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 CanvasToneMappingMode missing in wgpu.h
* Wrote 255 enum mappings and 47 struct-field mappings to wgpu_native/_mappings.py
* Validated 149 C function calls
* Validated 151 C function calls
* Not using 69 C functions
* Validated 95 C structs