Skip to content

Commit 02588a6

Browse files
authored
Make test_cuda_gl_register_image_smoketest() more robust. (#1210)
* Make test_cuda_gl_register_image_smoketest() more robust. * Add libegl1 to dependencies in test-wheel-linux.yml * Also add libgl1 to dependencies in test-wheel-linux.yml
1 parent 3dcdd6d commit 02588a6

File tree

2 files changed

+95
-20
lines changed

2 files changed

+95
-20
lines changed

.github/workflows/test-wheel-linux.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ jobs:
108108
uses: ./.github/actions/install_unix_deps
109109
continue-on-error: false
110110
with:
111-
# for artifact fetching
112-
dependencies: "jq wget"
111+
# for artifact fetching, graphics libs
112+
dependencies: "jq wget libgl1 libegl1"
113113
dependent_exes: "jq wget"
114114

115115
- name: Set environment variables

cuda_bindings/tests/test_graphics_apis.py

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,104 @@
11
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE
33

4+
import contextlib
5+
import ctypes
6+
import ctypes.util
7+
import os
8+
import sys
9+
410
import pytest
511
from cuda.bindings import runtime as cudart
612

713

8-
def test_graphics_api_smoketest():
9-
# Due to lazy importing in pyglet, pytest.importorskip doesn't work
14+
@contextlib.contextmanager
15+
def _gl_context():
16+
"""
17+
Yield a (tex_id, tex_target) with a current GL context.
18+
Tries:
19+
1) Windows: hidden WGL window (no EGL)
20+
2) Linux with DISPLAY/wayland: hidden window
21+
3) Linux headless: EGL headless if available
22+
Skips if none work.
23+
"""
24+
pyglet = pytest.importorskip("pyglet")
25+
26+
# Prefer non-headless when a display is available; it's more portable and avoids EGL.
27+
if sys.platform.startswith("linux") and not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
28+
if ctypes.util.find_library("EGL") is None:
29+
pytest.skip("No DISPLAY and no EGL runtime available for headless context.")
30+
pyglet.options["headless"] = True
31+
32+
# Create a minimal offscreen/hidden context
33+
win = None
1034
try:
11-
import pyglet
12-
13-
tex = pyglet.image.Texture.create(512, 512)
14-
except (ImportError, AttributeError):
15-
pytest.skip("pyglet not available or could not create GL context")
16-
# return to make linters happy
17-
return
18-
19-
err, gfx_resource = cudart.cudaGraphicsGLRegisterImage(
20-
tex.id, tex.target, cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsWriteDiscard
21-
)
22-
error_name = cudart.cudaGetErrorName(err)[1].decode()
23-
if error_name == "cudaSuccess":
24-
assert int(gfx_resource) != 0
25-
else:
26-
assert error_name in ("cudaErrorInvalidValue", "cudaErrorUnknown")
35+
if not pyglet.options.get("headless"):
36+
# Hidden window path (WGL on Windows, GLX/WLS on Linux)
37+
from pyglet import gl
38+
39+
config = gl.Config(double_buffer=False)
40+
win = pyglet.window.Window(visible=False, config=config)
41+
win.switch_to()
42+
else:
43+
# Headless EGL path; pyglet will arrange a pbuffer-like headless context
44+
from pyglet.gl import headless # noqa: F401 (import side-effect creates context)
45+
46+
# Make a tiny texture so we have a real GL object to register
47+
from pyglet.gl import gl as _gl
48+
49+
tex_id = _gl.GLuint(0)
50+
_gl.glGenTextures(1, ctypes.byref(tex_id))
51+
target = _gl.GL_TEXTURE_2D
52+
_gl.glBindTexture(target, tex_id.value)
53+
_gl.glTexParameteri(target, _gl.GL_TEXTURE_MIN_FILTER, _gl.GL_NEAREST)
54+
_gl.glTexParameteri(target, _gl.GL_TEXTURE_MAG_FILTER, _gl.GL_NEAREST)
55+
width, height = 16, 16
56+
_gl.glTexImage2D(target, 0, _gl.GL_RGBA8, width, height, 0, _gl.GL_RGBA, _gl.GL_UNSIGNED_BYTE, None)
57+
58+
yield int(tex_id.value), int(target)
59+
60+
except Exception as e:
61+
# Convert any pyglet/GL creation failure into a clean skip
62+
pytest.skip(f"Could not create GL context/texture: {type(e).__name__}: {e}")
63+
finally:
64+
# Best-effort cleanup
65+
try:
66+
from pyglet.gl import gl as _gl
67+
68+
if tex_id.value:
69+
_gl.glDeleteTextures(1, ctypes.byref(tex_id))
70+
except Exception: # noqa: S110
71+
pass
72+
try:
73+
if win is not None:
74+
win.close()
75+
except Exception: # noqa: S110
76+
pass
77+
78+
79+
@pytest.mark.parametrize(
80+
"flags",
81+
[
82+
cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsNone,
83+
cudart.cudaGraphicsRegisterFlags.cudaGraphicsRegisterFlagsWriteDiscard,
84+
],
85+
)
86+
def test_cuda_gl_register_image_smoketest(flags):
87+
with _gl_context() as (tex_id, tex_target):
88+
# Register
89+
err, resource = cudart.cudaGraphicsGLRegisterImage(tex_id, tex_target, flags)
90+
name = cudart.cudaGetErrorName(err)[1].decode()
91+
92+
# Map error expectations by environment:
93+
# - success: we actually exercised the API
94+
# - operating-system: typical when the driver/runtime refuses interop (e.g., no GPU/driver in CI container)
95+
acceptable = {"cudaSuccess", "cudaErrorOperatingSystem"}
96+
97+
assert name in acceptable, f"cudaGraphicsGLRegisterImage returned {name}"
98+
if name == "cudaSuccess":
99+
assert int(resource) != 0
100+
# Unregister to be tidy
101+
cudart.cudaGraphicsUnregisterResource(resource)
27102

28103

29104
def test_cuda_register_image_invalid():

0 commit comments

Comments
 (0)