diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 796ab306e..f763d65b9 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -108,8 +108,8 @@ jobs: uses: ./.github/actions/install_unix_deps continue-on-error: false with: - # for artifact fetching - dependencies: "jq wget" + # for artifact fetching, graphics libs + dependencies: "jq wget libgl1 libegl1" dependent_exes: "jq wget" - name: Set environment variables diff --git a/cuda_bindings/tests/test_graphics_apis.py b/cuda_bindings/tests/test_graphics_apis.py index ae2f074d5..d010796e8 100644 --- a/cuda_bindings/tests/test_graphics_apis.py +++ b/cuda_bindings/tests/test_graphics_apis.py @@ -1,29 +1,104 @@ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +import contextlib +import ctypes +import ctypes.util +import os +import sys + import pytest from cuda.bindings import runtime as cudart -def test_graphics_api_smoketest(): - # Due to lazy importing in pyglet, pytest.importorskip doesn't work +@contextlib.contextmanager +def _gl_context(): + """ + Yield a (tex_id, tex_target) with a current GL context. + Tries: + 1) Windows: hidden WGL window (no EGL) + 2) Linux with DISPLAY/wayland: hidden window + 3) Linux headless: EGL headless if available + Skips if none work. + """ + pyglet = pytest.importorskip("pyglet") + + # Prefer non-headless when a display is available; it's more portable and avoids EGL. + if sys.platform.startswith("linux") and not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")): + if ctypes.util.find_library("EGL") is None: + pytest.skip("No DISPLAY and no EGL runtime available for headless context.") + pyglet.options["headless"] = True + + # Create a minimal offscreen/hidden context + win = None try: - import pyglet - - tex = pyglet.image.Texture.create(512, 512) - except (ImportError, AttributeError): - pytest.skip("pyglet not available or could not create GL context") - # return to make linters happy - return - - err, gfx_resource = cudart.cudaGraphicsGLRegisterImage( - tex.id, tex.target, cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsWriteDiscard - ) - error_name = cudart.cudaGetErrorName(err)[1].decode() - if error_name == "cudaSuccess": - assert int(gfx_resource) != 0 - else: - assert error_name in ("cudaErrorInvalidValue", "cudaErrorUnknown") + if not pyglet.options.get("headless"): + # Hidden window path (WGL on Windows, GLX/WLS on Linux) + from pyglet import gl + + config = gl.Config(double_buffer=False) + win = pyglet.window.Window(visible=False, config=config) + win.switch_to() + else: + # Headless EGL path; pyglet will arrange a pbuffer-like headless context + from pyglet.gl import headless # noqa: F401 (import side-effect creates context) + + # Make a tiny texture so we have a real GL object to register + from pyglet.gl import gl as _gl + + tex_id = _gl.GLuint(0) + _gl.glGenTextures(1, ctypes.byref(tex_id)) + target = _gl.GL_TEXTURE_2D + _gl.glBindTexture(target, tex_id.value) + _gl.glTexParameteri(target, _gl.GL_TEXTURE_MIN_FILTER, _gl.GL_NEAREST) + _gl.glTexParameteri(target, _gl.GL_TEXTURE_MAG_FILTER, _gl.GL_NEAREST) + width, height = 16, 16 + _gl.glTexImage2D(target, 0, _gl.GL_RGBA8, width, height, 0, _gl.GL_RGBA, _gl.GL_UNSIGNED_BYTE, None) + + yield int(tex_id.value), int(target) + + except Exception as e: + # Convert any pyglet/GL creation failure into a clean skip + pytest.skip(f"Could not create GL context/texture: {type(e).__name__}: {e}") + finally: + # Best-effort cleanup + try: + from pyglet.gl import gl as _gl + + if tex_id.value: + _gl.glDeleteTextures(1, ctypes.byref(tex_id)) + except Exception: # noqa: S110 + pass + try: + if win is not None: + win.close() + except Exception: # noqa: S110 + pass + + +@pytest.mark.parametrize( + "flags", + [ + cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsNone, + cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsWriteDiscard, + ], +) +def test_cuda_gl_register_image_smoketest(flags): + with _gl_context() as (tex_id, tex_target): + # Register + err, resource = cudart.cudaGraphicsGLRegisterImage(tex_id, tex_target, flags) + name = cudart.cudaGetErrorName(err)[1].decode() + + # Map error expectations by environment: + # - success: we actually exercised the API + # - operating-system: typical when the driver/runtime refuses interop (e.g., no GPU/driver in CI container) + acceptable = {"cudaSuccess", "cudaErrorOperatingSystem"} + + assert name in acceptable, f"cudaGraphicsGLRegisterImage returned {name}" + if name == "cudaSuccess": + assert int(resource) != 0 + # Unregister to be tidy + cudart.cudaGraphicsUnregisterResource(resource) def test_cuda_register_image_invalid():