Skip to content

Commit

Permalink
Refactor canvas context to allow presenting as image (#586)
Browse files Browse the repository at this point in the history
* Refactor canvas context to allow presening as image

* some cleanup

* codegen

* Fix flicker

* cleaner

* fix error on exit

* looked into qt image draw performance a bit

* Fix/workaround for Qt on Wayland

* Fix glfw

* Give wx same treatment as qt

* Show warning when using offscreen rendering in qt and wx

* docs

* Update offscreen canvases. No more need for WgpuOfscreenCanvasBase

* Update notebook

* docs

* minor tweaks

* update tests

* Fix memtest

* remove debug text overlay

* Bit of docstrings

* explaine purpose of canvas context

* Rename surface_info -> present_info

* draw_to_screen -> present_method

* flake
  • Loading branch information
almarklein authored Sep 21, 2024
1 parent 8c077c4 commit 379599c
Show file tree
Hide file tree
Showing 19 changed files with 796 additions and 533 deletions.
1 change: 0 additions & 1 deletion docs/gui.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ The Canvas base classes
~WgpuCanvasInterface
~WgpuCanvasBase
~WgpuAutoGui
~WgpuOffscreenCanvasBase


For each supported GUI toolkit there is a module that implements a ``WgpuCanvas`` class,
Expand Down
6 changes: 3 additions & 3 deletions examples/triangle_glfw_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import glfw

from wgpu.backends.wgpu_native import GPUCanvasContext
from wgpu.gui.glfw import get_surface_info, get_physical_size
from wgpu.gui.glfw import get_glfw_present_info, get_physical_size
from wgpu.utils.device import get_default_device


Expand All @@ -29,9 +29,9 @@ class GlfwCanvas:
def __init__(self, window):
self._window = window

def get_surface_info(self):
def get_present_info(self):
"""get window and display id, includes some triage to deal with OS differences"""
return get_surface_info(self._window)
return get_glfw_present_info(self._window)

def get_physical_size(self):
"""get framebuffer size in integer pixels"""
Expand Down
8 changes: 4 additions & 4 deletions examples/triangle_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
app = QtWidgets.QApplication([])
canvas = WgpuCanvas(title="wgpu triangle in Qt subprocess")
print(json.dumps(canvas.get_surface_info()))
print(json.dumps(canvas.get_present_info()))
print(canvas.get_physical_size())
sys.stdout.flush()
Expand All @@ -42,15 +42,15 @@
class ProxyCanvas(WgpuCanvasBase):
def __init__(self):
super().__init__()
self._surface_info = json.loads(p.stdout.readline().decode())
self._present_info = json.loads(p.stdout.readline().decode())
self._psize = tuple(
int(x) for x in p.stdout.readline().decode().strip().strip("()").split(",")
)
print(self._psize)
time.sleep(0.2)

def get_surface_info(self):
return self._surface_info
def get_present_info(self):
return self._present_info

def get_physical_size(self):
return self._psize
Expand Down
129 changes: 109 additions & 20 deletions examples/wgpu-examples.ipynb

Large diffs are not rendered by default.

47 changes: 18 additions & 29 deletions tests/test_gui_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import numpy as np
import wgpu.gui # noqa
from testutils import run_tests, can_use_wgpu_lib, is_pypy
from pytest import mark
from pytest import mark, raises


class TheTestCanvas(wgpu.gui.WgpuCanvasBase):
Expand Down Expand Up @@ -37,10 +37,10 @@ def spam_method(self):
def test_base_canvas_context():
assert not issubclass(wgpu.gui.WgpuCanvasInterface, wgpu.GPUCanvasContext)
assert hasattr(wgpu.gui.WgpuCanvasInterface, "get_context")
# Provides good default already
canvas = wgpu.gui.WgpuCanvasInterface()
ctx = wgpu.GPUCanvasContext(canvas)
assert ctx.get_preferred_format(None) == "bgra8unorm-srgb"
# Cannot instantiate, because get_present_info is not implemented
with raises(NotImplementedError):
wgpu.GPUCanvasContext(canvas)


def test_canvas_logging(caplog):
Expand Down Expand Up @@ -80,12 +80,22 @@ def test_canvas_logging(caplog):
assert text.count("division by zero") == 4


class MyOffscreenCanvas(wgpu.gui.WgpuOffscreenCanvasBase):
class MyOffscreenCanvas(wgpu.gui.WgpuCanvasBase):
def __init__(self):
super().__init__()
self.textures = []
self.frame_count = 0
self.physical_size = 100, 100

def get_present_info(self):
return {
"method": "image",
"formats": ["rgba8unorm-srgb"],
}

def present_image(self, image, **kwargs):
self.frame_count += 1
self.array = np.frombuffer(image, np.uint8).reshape(image.shape)

def get_pixel_ratio(self):
return 1

Expand All @@ -99,26 +109,6 @@ def _request_draw(self):
# Note: this would normally schedule a call in a later event loop iteration
self._draw_frame_and_present()

def present(self, texture):
self.textures.append(texture)
device = texture._device
size = texture.size
bytes_per_pixel = 4
data = device.queue.read_texture(
{
"texture": texture,
"mip_level": 0,
"origin": (0, 0, 0),
},
{
"offset": 0,
"bytes_per_row": bytes_per_pixel * size[0],
"rows_per_image": size[1],
},
size,
)
self.array = np.frombuffer(data, np.uint8).reshape(size[1], size[0], 4)


@mark.skipif(not can_use_wgpu_lib, reason="Needs wgpu lib")
def test_run_bare_canvas():
Expand Down Expand Up @@ -181,7 +171,7 @@ def draw_frame():
render_pass.end()
device.queue.submit([command_encoder.finish()])

assert len(canvas.textures) == 0
assert canvas.frame_count == 0

# Draw 1
canvas.request_draw(draw_frame)
Expand Down Expand Up @@ -214,8 +204,7 @@ def draw_frame():
assert np.all(canvas.array[:, :, 1] == 255)

# We now have four unique texture objects
assert len(canvas.textures) == 4
assert len(set(canvas.textures)) == 4
assert canvas.frame_count == 4


def test_autogui_mixin():
Expand Down
2 changes: 1 addition & 1 deletion tests/test_gui_glfw.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def __init__(self):
self.window = glfw.create_window(300, 200, "canvas", None, None)
self._present_context = None

def get_surface_info(self):
def get_present_info(self):
if sys.platform.startswith("win"):
return {
"platform": "windows",
Expand Down
2 changes: 1 addition & 1 deletion tests_mem/test_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def make_draw_func_for_canvas(canvas):
so that we can really present something to a canvas being tested.
"""
ctx = canvas.get_context()
ctx.configure(device=DEVICE, format="bgra8unorm-srgb")
ctx.configure(device=DEVICE, format=None)

def draw():
ctx = canvas.get_context()
Expand Down
4 changes: 3 additions & 1 deletion tests_mem/test_gui_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ def test_release_canvas_context(n):
if app is None:
app = PySide6.QtWidgets.QApplication([""])

yield {}
yield {
"ignore": {"CommandBuffer"},
}

canvases = weakref.WeakSet()

Expand Down
Loading

0 comments on commit 379599c

Please sign in to comment.