Skip to content
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

Wayland support by forcing x11 #470

Merged
merged 10 commits into from
Mar 6, 2024
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
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
Loading