diff --git a/cuda_bindings/tests/test_graphics_apis.py b/cuda_bindings/tests/test_graphics_apis.py index 6dd2eeff0d8..bd87fc4d2e6 100644 --- a/cuda_bindings/tests/test_graphics_apis.py +++ b/cuda_bindings/tests/test_graphics_apis.py @@ -9,7 +9,11 @@ import pyglet import pytest -from cuda_python_test_helpers.graphics import is_gl_context_unavailable, open_gl_window +from cuda_python_test_helpers.graphics import ( + gl_context_not_on_nvidia_gpu_reason, + is_gl_context_unavailable, + open_gl_window, +) from cuda.bindings import runtime as cudart @@ -63,6 +67,8 @@ def _gl_context(): tex_id = None try: + if reason := gl_context_not_on_nvidia_gpu_reason(): + pytest.skip(reason) tex_id, target = _allocate_gl_texture(win) yield int(tex_id.value), int(target) finally: diff --git a/cuda_core/tests/test_graphics.py b/cuda_core/tests/test_graphics.py index 0f1bd621f7a..b578b1f3d0d 100644 --- a/cuda_core/tests/test_graphics.py +++ b/cuda_core/tests/test_graphics.py @@ -13,6 +13,7 @@ import pyglet import pytest from cuda_python_test_helpers.graphics import ( + gl_context_not_on_nvidia_gpu_reason, is_gl_context_unavailable, open_gl_window, select_headless_egl_device_for_cuda, @@ -130,6 +131,8 @@ def _gl_context_and_buffer(nbytes=1024): buf_id = None try: + if reason := gl_context_not_on_nvidia_gpu_reason(): + pytest.skip(reason) buf_id = _allocate_gl_buffer(win, nbytes) yield int(buf_id.value), nbytes finally: @@ -158,6 +161,8 @@ def _gl_context_and_texture(width=16, height=16): tex_id = None try: + if reason := gl_context_not_on_nvidia_gpu_reason(): + pytest.skip(reason) tex_id, target = _allocate_gl_texture(win, width, height) yield int(tex_id.value), int(target) finally: diff --git a/cuda_core/tests/test_helpers.py b/cuda_core/tests/test_helpers.py index b35c9958d13..35873da5231 100644 --- a/cuda_core/tests/test_helpers.py +++ b/cuda_core/tests/test_helpers.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 +import ctypes import sys import time import types @@ -360,7 +361,11 @@ def test_oom_diagnostics_probe_basics_is_live_and_cheap(init_cuda): # --------------------------------------------------------------------------- import pytest -from cuda_python_test_helpers.graphics import is_gl_context_unavailable, open_gl_window +from cuda_python_test_helpers.graphics import ( + gl_context_not_on_nvidia_gpu_reason, + is_gl_context_unavailable, + open_gl_window, +) @pytest.mark.thread_unsafe(reason="patches the process-wide pyglet module") @@ -437,3 +442,28 @@ def test_is_gl_context_unavailable_accepts_genuine(exc): ) def test_is_gl_context_unavailable_rejects_unrelated(exc): assert is_gl_context_unavailable(exc) is False + + +def _patch_gl_vendor(monkeypatch, vendor): + # Only GL_VENDOR is answered; any other query (e.g. GL_RENDERER) returns NULL. + gl = types.SimpleNamespace( + GL_VENDOR=1, + GL_RENDERER=2, + glGetString=lambda name: ctypes.c_char_p(vendor if name == 1 else None), + ) + monkeypatch.setitem(sys.modules, "pyglet.gl", types.SimpleNamespace(gl=gl)) + + +@pytest.mark.thread_unsafe(reason="patches the process-wide pyglet.gl module") +def test_gl_context_not_on_nvidia_gpu_reason_accepts_nvidia(monkeypatch): + _patch_gl_vendor(monkeypatch, b"NVIDIA Corporation") + assert gl_context_not_on_nvidia_gpu_reason() is None + + +@pytest.mark.thread_unsafe(reason="patches the process-wide pyglet.gl module") +@pytest.mark.parametrize("vendor", [b"AMD", b"Mesa", None]) +def test_gl_context_not_on_nvidia_gpu_reason_rejects_other_vendors(monkeypatch, vendor): + _patch_gl_vendor(monkeypatch, vendor) + reason = gl_context_not_on_nvidia_gpu_reason() + assert reason is not None + assert "not on an NVIDIA GPU" in reason diff --git a/cuda_python_test_helpers/cuda_python_test_helpers/graphics.py b/cuda_python_test_helpers/cuda_python_test_helpers/graphics.py index 8f1d824393e..a3da80dbe35 100644 --- a/cuda_python_test_helpers/cuda_python_test_helpers/graphics.py +++ b/cuda_python_test_helpers/cuda_python_test_helpers/graphics.py @@ -110,6 +110,31 @@ def open_gl_window(): return None +def gl_context_not_on_nvidia_gpu_reason() -> str | None: + """Return a skip reason if the current GL context is not on an NVIDIA GPU, else None. + + CUDA-GL interop requires the GL context to be rendered by an NVIDIA GPU. + When the context is created on a GPU from another vendor (e.g. on + multi-vendor GPU systems), `cuGraphicsGLRegister*` fails with + `CUDA_ERROR_UNKNOWN`, which is indistinguishable from a real failure at + the call site. Caller must have a current GL context. + """ + from pyglet.gl import gl + + def _get_string(name): + value = gl.glGetString(name) + return ctypes.cast(value, ctypes.c_char_p).value.decode(errors="replace") if value else "" + + vendor = _get_string(gl.GL_VENDOR) + if "NVIDIA" in vendor: + return None + renderer = _get_string(gl.GL_RENDERER) + return ( + f"GL context is not on an NVIDIA GPU (GL_VENDOR={vendor!r}, GL_RENDERER={renderer!r}); " + "CUDA-GL interop requires one." + ) + + _GL_CONTEXT_UNAVAILABLE_EXC_NAMES = frozenset( { "NoSuchDisplayException",