Skip to content

Commit

Permalink
Wayland support by forcing x11 (#470)
Browse files Browse the repository at this point in the history
* Wayland support wip

* Make glfw and qt work by forcing x11

* tweaks

* Fix test

* Update docs

* Move some stuff to new gui utils module

* force x11 on Wayland for wx too

* Fix comment

* Forcing an empty commit.

---------

Co-authored-by: Korijn van Golen <[email protected]>
  • Loading branch information
almarklein and Korijn authored Mar 6, 2024
1 parent cd3165f commit eced145
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 189 deletions.
7 changes: 3 additions & 4 deletions docs/start.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,9 @@ On MacOS you need at least 10.13 (High Sierra) to have Metal/Vulkan support.
Linux
+++++

On Linux, it's advisable to install the proprietary drivers of your GPU
(if you have a dedicated GPU). You may need to ``apt install
mesa-vulkan-drivers``. Wayland support is currently broken (we could use
a hand to fix this).
On Linux, it's advisable to install the proprietary drivers of your GPU (if you
have a dedicated GPU). You may need to ``apt install mesa-vulkan-drivers``. On
Wayland, wgpu-py requires XWayland (available by default on most distributions).

Binary wheels for Linux are only available for **manylinux_2_24**.
This means that the installation requires ``pip >= 20.3``, and you need
Expand Down
11 changes: 6 additions & 5 deletions examples/triangle_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""

import sys
import json
import time
import subprocess

Expand All @@ -23,14 +24,14 @@

code = """
import sys
import json
from PySide6 import QtWidgets # Use either PySide6 or PyQt6
from wgpu.gui.qt import WgpuCanvas
app = QtWidgets.QApplication([])
canvas = WgpuCanvas(title="wgpu triangle in Qt subprocess")
print(canvas.get_window_id())
#print(canvas.get_display_id())
print(json.dumps(canvas.get_surface_info()))
print(canvas.get_physical_size())
sys.stdout.flush()
Expand All @@ -41,15 +42,15 @@
class ProxyCanvas(WgpuCanvasBase):
def __init__(self):
super().__init__()
self._window_id = int(p.stdout.readline().decode())
self._surface_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_window_id(self):
return self._window_id
def get_surface_info(self):
return self._surface_info

def get_physical_size(self):
return self._psize
Expand Down
2 changes: 1 addition & 1 deletion tests/test_gui_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def handler(event):


def test_weakbind():
weakbind = wgpu.gui.base.weakbind
weakbind = wgpu.gui._gui_utils.weakbind

xx = []

Expand Down
32 changes: 21 additions & 11 deletions tests/test_gui_glfw.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
like the canvas context and surface texture.
"""

import os
import sys
import time
import weakref
Expand Down Expand Up @@ -172,22 +171,33 @@ def __init__(self):
self.window = glfw.create_window(300, 200, "canvas", None, None)
self._present_context = None

def get_window_id(self):
def get_surface_info(self):
if sys.platform.startswith("win"):
return int(glfw.get_win32_window(self.window))
return {
"platform": "windows",
"window": int(glfw.get_win32_window(self.window)),
}
elif sys.platform.startswith("darwin"):
return int(glfw.get_cocoa_window(self.window))
return {
"platform": "cocoa",
"window": int(glfw.get_cocoa_window(self.window)),
}
elif sys.platform.startswith("linux"):
is_wayland = "wayland" in os.getenv("XDG_SESSION_TYPE", "").lower()
is_wayland = hasattr(glfw, "get_wayland_display")
if is_wayland:
return int(glfw.get_wayland_window(self.window))
return {
"platform": "wayland",
"window": int(glfw.get_wayland_window(self.window)),
"display": int(glfw.get_wayland_display()),
}
else:
return int(glfw.get_x11_window(self.window))
return {
"platform": "x11",
"window": int(glfw.get_x11_window(self.window)),
"display": int(glfw.get_x11_display()),
}
else:
raise RuntimeError(f"Cannot get GLFW window id on {sys.platform}.")

def get_display_id(self):
return wgpu.WgpuCanvasInterface.get_display_id(self)
raise RuntimeError(f"Cannot get GLFW surafce info on {sys.platform}.")

def get_physical_size(self):
psize = glfw.get_framebuffer_size(self.window)
Expand Down
3 changes: 1 addition & 2 deletions wgpu/backends/wgpu_native/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,7 @@ def request_adapter(
# able to create a surface texture for it (from this adapter).
surface_id = ffi.NULL
if canvas is not None:
window_id = canvas.get_window_id()
if window_id: # e.g. could be an off-screen canvas
if canvas.get_surface_info(): # e.g. could be an off-screen canvas
surface_id = canvas.get_context()._get_surface_id()

# ----- Select backend
Expand Down
46 changes: 27 additions & 19 deletions wgpu/backends/wgpu_native/_helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Utilities used in the wgpu-native backend.
"""

import os
import sys
import ctypes

Expand Down Expand Up @@ -96,12 +95,18 @@ def get_surface_id_from_canvas(canvas):
"""Get an id representing the surface to render to. The way to
obtain this id differs per platform and GUI toolkit.
"""
win_id = canvas.get_window_id()

# Use cached
surface_id = getattr(canvas, "_wgpu_surface_id", None)
if surface_id:
return surface_id

surface_info = canvas.get_surface_info()

if sys.platform.startswith("win"): # no-cover
struct = ffi.new("WGPUSurfaceDescriptorFromWindowsHWND *")
struct.hinstance = ffi.NULL
struct.hwnd = ffi.cast("void *", int(win_id))
struct.hwnd = ffi.cast("void *", int(surface_info["window"]))
struct.chain.sType = lib.WGPUSType_SurfaceDescriptorFromWindowsHWND

elif sys.platform.startswith("darwin"): # no-cover
Expand All @@ -115,7 +120,7 @@ def get_surface_id_from_canvas(canvas):
# [ns_window.contentView setLayer:metal_layer];
# surface = wgpu_create_surface_from_metal_layer(metal_layer);
# }
window = ctypes.c_void_p(win_id)
window = ctypes.c_void_p(surface_info["window"])

cw = ObjCInstance(window)
try:
Expand Down Expand Up @@ -156,26 +161,25 @@ def get_surface_id_from_canvas(canvas):
struct.chain.sType = lib.WGPUSType_SurfaceDescriptorFromMetalLayer

elif sys.platform.startswith("linux"): # no-cover
display_id = canvas.get_display_id()
is_wayland = "wayland" in os.getenv("XDG_SESSION_TYPE", "").lower()
is_xcb = False
if is_wayland:
# todo: wayland seems to be broken right now
platform = surface_info.get("platform", "x11")
if platform == "x11":
struct = ffi.new("WGPUSurfaceDescriptorFromXlibWindow *")
struct.display = ffi.cast("void *", surface_info["display"])
struct.window = int(surface_info["window"])
struct.chain.sType = lib.WGPUSType_SurfaceDescriptorFromXlibWindow
elif platform == "wayland":
struct = ffi.new("WGPUSurfaceDescriptorFromWaylandSurface *")
struct.display = ffi.cast("void *", display_id)
struct.surface = ffi.cast("void *", win_id)
struct.display = ffi.cast("void *", surface_info["display"])
struct.surface = ffi.cast("void *", surface_info["window"])
struct.chain.sType = lib.WGPUSType_SurfaceDescriptorFromWaylandSurface
elif is_xcb:
elif platform == "xcb":
# todo: xcb untested
struct = ffi.new("WGPUSurfaceDescriptorFromXcbWindow *")
struct.connection = ffi.NULL # ?? ffi.cast("void *", display_id)
struct.window = int(win_id)
struct.connection = ffi.cast("void *", surface_info["connection"]) # ??
struct.window = int(surface_info["window"])
struct.chain.sType = lib.WGPUSType_SurfaceDescriptorFromXlibWindow
else:
struct = ffi.new("WGPUSurfaceDescriptorFromXlibWindow *")
struct.display = ffi.cast("void *", display_id)
struct.window = int(win_id)
struct.chain.sType = lib.WGPUSType_SurfaceDescriptorFromXlibWindow
raise RuntimeError("Unexpected Linux surface platform '{platform}'.")

else: # no-cover
raise RuntimeError("Cannot get surface id: unsupported platform.")
Expand All @@ -184,7 +188,11 @@ def get_surface_id_from_canvas(canvas):
surface_descriptor.label = ffi.NULL
surface_descriptor.nextInChain = ffi.cast("WGPUChainedStruct *", struct)

return lib.wgpuInstanceCreateSurface(get_wgpu_instance(), surface_descriptor)
surface_id = lib.wgpuInstanceCreateSurface(get_wgpu_instance(), surface_descriptor)

# Cache and return
canvas._wgpu_surface_id = surface_id
return surface_id


# The functions below are copied from codegen/utils.py
Expand Down
1 change: 1 addition & 0 deletions wgpu/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Code to provide a canvas to render to.
"""

from . import _gui_utils # noqa: F401
from .base import WgpuCanvasInterface, WgpuCanvasBase, WgpuAutoGui # noqa: F401
from .offscreen import WgpuOffscreenCanvasBase # noqa: F401

Expand Down
108 changes: 108 additions & 0 deletions wgpu/gui/_gui_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
""" Private gui utilities.
"""

import os
import sys
import weakref
import logging
import ctypes.util
from contextlib import contextmanager

from .._coreutils import error_message_hash


logger = logging.getLogger("wgpu")


err_hashes = {}


@contextmanager
def log_exception(kind):
"""Context manager to log any exceptions, but only log a one-liner
for subsequent occurances of the same error to avoid spamming by
repeating errors in e.g. a draw function or event callback.
"""
try:
yield
except Exception as err:
# Store exc info for postmortem debugging
exc_info = list(sys.exc_info())
exc_info[2] = exc_info[2].tb_next # skip *this* function
sys.last_type, sys.last_value, sys.last_traceback = exc_info
# Show traceback, or a one-line summary
msg = str(err)
msgh = error_message_hash(msg)
if msgh not in err_hashes:
# Provide the exception, so the default logger prints a stacktrace.
# IDE's can get the exception from the root logger for PM debugging.
err_hashes[msgh] = 1
logger.error(kind, exc_info=err)
else:
# We've seen this message before, return a one-liner instead.
err_hashes[msgh] = count = err_hashes[msgh] + 1
msg = kind + ": " + msg.split("\n")[0].strip()
msg = msg if len(msg) <= 70 else msg[:69] + "…"
logger.error(msg + f" ({count})")


def weakbind(method):
"""Replace a bound method with a callable object that stores the `self` using a weakref."""
ref = weakref.ref(method.__self__)
class_func = method.__func__
del method

def proxy(*args, **kwargs):
self = ref()
if self is not None:
return class_func(self, *args, **kwargs)

proxy.__name__ = class_func.__name__
return proxy


SYSTEM_IS_WAYLAND = "wayland" in os.getenv("XDG_SESSION_TYPE", "").lower()

if sys.platform.startswith("linux") and SYSTEM_IS_WAYLAND:
# Force glfw to use X11. Note that this does not work if glfw is already imported.
if "glfw" not in sys.modules:
os.environ["PYGLFW_LIBRARY_VARIANT"] = "x11"
# Force Qt to use X11. Qt is more flexible - it ok if e.g. PySide6 is already imported.
os.environ["QT_QPA_PLATFORM"] = "xcb"
# Force wx to use X11, probably.
os.environ["GDK_BACKEND"] = "x11"


_x11_display = None


def get_alt_x11_display():
"""Get (the pointer to) a process-global x11 display instance."""
# Ideally we'd get the real display object used by the GUI toolkit.
# But this is not always possible. In that case, using an alt display
# object can be used.
global _x11_display
assert sys.platform.startswith("linux")
if _x11_display is None:
x11 = ctypes.CDLL(ctypes.util.find_library("X11"))
x11.XOpenDisplay.restype = ctypes.c_void_p
_x11_display = x11.XOpenDisplay(None)
return _x11_display


_wayland_display = None


def get_alt_wayland_display():
"""Get (the pointer to) a process-global Wayland display instance."""
# Ideally we'd get the real display object used by the GUI toolkit.
# This creates a global object, similar to what we do for X11.
# Unfortunately, this segfaults, so it looks like the real display object
# is needed? Leaving this here for reference.
global _wayland_display
assert sys.platform.startswith("linux")
if _wayland_display is None:
wl = ctypes.CDLL(ctypes.util.find_library("wayland-client"))
wl.wl_display_connect.restype = ctypes.c_void_p
_wayland_display = wl.wl_display_connect(None)
return _wayland_display
Loading

0 comments on commit eced145

Please sign in to comment.