Describe the bug
Steam VR version 2.12.9 on Linux leaves an uncleared OpenGL error state after calling xrEndFrame(...).
To Reproduce
Steps to reproduce the behavior:
Run SteamVR 2.12.9 on Linux.
Run the attached script (see below) with the HMD worn, so the OpenXR session can enter the FOCUSED state.
Notice how line 249 finds the uncleared OpenGL error state after the call to xr.end_frame()
This is a problem, because it makes subsequent OpenGL calls seem to error. Please clean up after yourself.
Expected behavior
If OpenGL has a no-error state before the call to xr.end_frame(), it should continue to have a no-error OpenGL error state afterwards. I believe this worked correctly as recently as July.
System Information (please complete the following information):
Please use the latest Steam beta client and SteamVR beta for your bug reports!
Steam client version (build number or date): Steam Version: 1754677506
SteamVR version: 2.12.9
Distribution (e.g. Ubuntu): Ubuntu 24 LTS
Steam runtime diagnostics: [generate via Help -> Steam Runtime Diagnostics in the Steam client]
Steam and SteamVR logs: [generate by running this command in a terminal tar -zcvf ~/Desktop/steam-logs.tar.gz ~/.steam/steam/logs]
Minidumps: [run the following command: tar -zcvf ~/Desktop/steam-minidumps.tar.gz /tmp/dumps]
Screenshots
If applicable, add screenshots to help explain your problem.
Additional context
Output from script below. The two ERROR lines arise during the call to xrEndFrame(), presumably within the guts of SteamVR, and appear because this script hooks up OpenGL debugging. The final WARNING line comes from the repro script itself.
/home/cmbruns/.steam/debian-installation/steamapps/common/SteamVR/bin/vrwebhelper/linux64/vrwebhelper.sh: line 17: STEAMVR_VRENV: unbound variable
Session state is IDLE
Session state is READY
Session state is SYNCHRONIZED
Session state is VISIBLE
Session state is FOCUSED
ERROR:steam_vr_linux_repro:OpenGL Message: GL_INVALID_VALUE error generated. Not a semaphore.
ERROR:steam_vr_linux_repro:OpenGL Message: GL_INVALID_VALUE error generated. Not a semaphore.
WARNING:steam_vr_linux_repro:Ignoring OpenGL error 1281 after xr.end_frame(...)
Repro script. See especially line 248 if error != GL.GL_NO_ERROR:...
"""
Repro script for problem with SteamVR version 2.12.9
Tested on Ubuntu Linux with OpenGL and nvidia GPU
I recall this not being a problem in July 2025, presumably with an
earlier version of SteamVR.
August 9, 2025
"""
from ctypes import POINTER, c_void_p, byref, cast, pointer
import logging
import glfw
from OpenGL import GL
from OpenGL import GLX
import xr
from xr import SessionCreateInfo
logging.basicConfig()
logger = logging.getLogger("steam_vr_linux_repro")
logger.setLevel(logging.DEBUG)
def gl_debug_message_callback(_source, _msg_type, _msg_id, severity, length, raw, _user):
"""Redirect OpenGL debug messages"""
log_level = {
GL.GL_DEBUG_SEVERITY_HIGH: logging.ERROR,
GL.GL_DEBUG_SEVERITY_MEDIUM: logging.WARNING,
GL.GL_DEBUG_SEVERITY_LOW: logging.INFO,
GL.GL_DEBUG_SEVERITY_NOTIFICATION: logging.DEBUG,
}[severity]
logger.log(log_level, f"OpenGL Message: {raw[0:length].decode()}")
gl_debug_message_proc = GL.GLDEBUGPROC(gl_debug_message_callback)
# Create an off-screen context using glfw
if not glfw.init():
raise RuntimeError("Failed to initialize GLFW")
# hidden, single‐buffered context
glfw.window_hint(glfw.VISIBLE, glfw.FALSE)
glfw.window_hint(glfw.DOUBLEBUFFER, glfw.FALSE)
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 4)
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 1)
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
glfw.window_hint(glfw.OPENGL_DEBUG_CONTEXT, True)
# tiny 1×1 window just to get a context
window = glfw.create_window(1, 1, "", None, None)
glfw.make_context_current(window)
glfw.swap_interval(0)
GL.glDebugMessageCallback(gl_debug_message_proc, None)
# Create OpenXR instance and system
instance = xr.create_instance(xr.InstanceCreateInfo(
enabled_extension_names=[xr.KHR_OPENGL_ENABLE_EXTENSION_NAME],
))
system_id = xr.get_system(instance, xr.SystemGetInfo(
form_factor=xr.FormFactor.HEAD_MOUNTED_DISPLAY,
))
# Set up OpenXR OpenGL stuff
pxrGetOpenGLGraphicsRequirementsKHR = cast(
xr.get_instance_proc_addr(
instance=instance,
name="xrGetOpenGLGraphicsRequirementsKHR",
),
xr.PFN_xrGetOpenGLGraphicsRequirementsKHR
)
graphics_requirements = xr.GraphicsRequirementsOpenGLKHR()
result = pxrGetOpenGLGraphicsRequirementsKHR(
instance,
system_id,
byref(graphics_requirements)
)
result = xr.check_result(xr.Result(result))
assert not result.is_exception()
graphics_binding = xr.GraphicsBindingOpenGLXlibKHR(
x_display=GLX.glXGetCurrentDisplay(),
glx_drawable=GLX.glXGetCurrentDrawable(),
glx_context=GLX.glXGetCurrentContext(),
)
graphics_binding_pointer = cast(pointer(graphics_binding), c_void_p)
# Create OpenXR session
session = xr.create_session(instance, create_info=SessionCreateInfo(
system_id=system_id,
next=graphics_binding_pointer,
))
# Create OpenXR swapchains
config_views = xr.enumerate_view_configuration_views(
instance=instance,
system_id=system_id,
view_configuration_type=xr.ViewConfigurationType.PRIMARY_STEREO,
)
fbo = GL.glGenFramebuffers(1)
swapchain_formats = xr.enumerate_swapchain_formats(session)
color_swapchain_format = GL.GL_SRGB8
assert color_swapchain_format in swapchain_formats
swapchains = []
swapchain_image_buffers = []
swapchain_sizes = []
swapchain_image_ptr_buffers = []
for vp in config_views:
swapchain_create_info = xr.SwapchainCreateInfo(
array_size=1,
format=color_swapchain_format,
width=vp.recommended_image_rect_width,
height=vp.recommended_image_rect_height,
mip_count=1,
face_count=1,
sample_count=vp.recommended_swapchain_sample_count,
usage_flags=xr.SwapchainUsageFlags.SAMPLED_BIT | xr.SwapchainUsageFlags.COLOR_ATTACHMENT_BIT,
)
swapchain_sizes.append((swapchain_create_info.width, swapchain_create_info.height))
swapchain = xr.create_swapchain(
session=session,
create_info=swapchain_create_info,
)
swapchains.append(swapchain)
swapchain_image_buffer = xr.enumerate_swapchain_images(
swapchain=swapchain,
element_type=xr.SwapchainImageOpenGLKHR,
)
swapchain_image_buffers.append(swapchain_image_buffer)
capacity = len(swapchain_image_buffer)
swapchain_image_ptr_buffer = (POINTER(xr.SwapchainImageBaseHeader) * capacity)()
for ix in range(capacity):
swapchain_image_ptr_buffer[ix] = cast(
byref(swapchain_image_buffer[ix]),
POINTER(xr.SwapchainImageBaseHeader))
swapchain_image_ptr_buffers.append(swapchain_image_ptr_buffer)
space = xr.create_reference_space(
session=session,
create_info=xr.ReferenceSpaceCreateInfo(),
)
session_state = xr.SessionState.IDLE
session_is_running = False
# Loop over frames (30 is plenty to hit the problem):
for _ in range(30):
# Poll xr events
while True:
try:
event_buffer = xr.poll_event(instance)
event_type = xr.StructureType(event_buffer.type)
if event_type == xr.StructureType.EVENT_DATA_SESSION_STATE_CHANGED:
event = cast(
byref(event_buffer),
POINTER(xr.EventDataSessionStateChanged)).contents
session_state = xr.SessionState(event.state)
print(f"Session state is {session_state.name}")
if session_state == xr.SessionState.READY:
xr.begin_session(
session=session,
begin_info=xr.SessionBeginInfo(
xr.ViewConfigurationType.PRIMARY_STEREO,
),
)
session_is_running = True
except xr.EventUnavailable:
break
if session_is_running:
if session_state in (
xr.SessionState.READY,
xr.SessionState.SYNCHRONIZED,
xr.SessionState.VISIBLE,
xr.SessionState.FOCUSED,
):
frame_state = xr.wait_frame(session)
xr.begin_frame(session)
render_layers = []
if frame_state.should_render:
layer = xr.CompositionLayerProjection(space=space)
view_state, views = xr.locate_views(
session=session,
view_locate_info=xr.ViewLocateInfo(
view_configuration_type=xr.ViewConfigurationType.PRIMARY_STEREO,
display_time=frame_state.predicted_display_time,
space=space,
)
)
num_views = len(views)
projection_layer_views = tuple(xr.CompositionLayerProjectionView() for _ in range(num_views))
vsf = view_state.view_state_flags
if (vsf & xr.VIEW_STATE_POSITION_VALID_BIT == 0
or vsf & xr.VIEW_STATE_ORIENTATION_VALID_BIT == 0):
continue # There are no valid tracking poses for the views.
for view_index, view in enumerate(views):
view_swapchain = swapchains[view_index]
swapchain_image_index = xr.acquire_swapchain_image(
swapchain=view_swapchain,
acquire_info=xr.SwapchainImageAcquireInfo(),
)
xr.wait_swapchain_image(
swapchain=view_swapchain,
wait_info=xr.SwapchainImageWaitInfo(timeout=xr.INFINITE_DURATION),
)
layer_view = projection_layer_views[view_index]
assert layer_view.type == xr.StructureType.COMPOSITION_LAYER_PROJECTION_VIEW
layer_view.pose = view.pose
layer_view.fov = view.fov
layer_view.sub_image.swapchain = view_swapchain
layer_view.sub_image.image_rect.offset[:] = [0, 0]
width, height = swapchain_sizes[view_index]
layer_view.sub_image.image_rect.extent[:] = [
width, height, ]
swapchain_image_ptr = swapchain_image_ptr_buffers[view_index][swapchain_image_index]
swapchain_image = cast(swapchain_image_ptr, POINTER(xr.SwapchainImageOpenGLKHR)).contents
assert layer_view.sub_image.image_array_index == 0 # texture arrays not supported.
color_texture = swapchain_image.image
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, fbo)
GL.glViewport(layer_view.sub_image.image_rect.offset.x,
layer_view.sub_image.image_rect.offset.y,
layer_view.sub_image.image_rect.extent.width,
layer_view.sub_image.image_rect.extent.height)
GL.glBindTexture(GL.GL_TEXTURE_2D, color_texture)
GL.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, GL.GL_TEXTURE_2D,
color_texture, 0)
# Render scene (uniform pink, for simplicity)
GL.glClearColor(1, 0.7, 0.7, 1) # pink
GL.glClear(GL.GL_COLOR_BUFFER_BIT)
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0)
xr.release_swapchain_image(
swapchain=view_swapchain,
release_info=xr.SwapchainImageReleaseInfo()
)
layer.views = projection_layer_views
render_layers.append(byref(layer))
# Finish Frame
assert GL.glGetError() == GL.GL_NO_ERROR # See? No problem up to here
xr.end_frame(
session,
frame_end_info=xr.FrameEndInfo(
display_time=frame_state.predicted_display_time,
environment_blend_mode=xr.EnvironmentBlendMode.OPAQUE,
layers=render_layers,
)
)
# This is a workaround to avoid later false OpenGL errors
# Clear opengl error vomited from Linux SteamVR internals
error = GL.glGetError()
if error != GL.GL_NO_ERROR:
logger.warning(f"Ignoring OpenGL error {error} after xr.end_frame(...)")
# Clean up
xr.destroy_space(space)
for s in swapchains:
xr.destroy_swapchain(s)
GL.glDeleteFramebuffers(1, [fbo,])
xr.destroy_session(session)
xr.destroy_instance(instance)
glfw.terminate()
Describe the bug
Steam VR version 2.12.9 on Linux leaves an uncleared OpenGL error state after calling
xrEndFrame(...).To Reproduce
Steps to reproduce the behavior:
Expected behavior
If OpenGL has a no-error state before the call to xr.end_frame(), it should continue to have a no-error OpenGL error state afterwards. I believe this worked correctly as recently as July.
System Information (please complete the following information):
Please use the latest Steam beta client and SteamVR beta for your bug reports!
tar -zcvf ~/Desktop/steam-logs.tar.gz ~/.steam/steam/logs]tar -zcvf ~/Desktop/steam-minidumps.tar.gz /tmp/dumps]Screenshots
If applicable, add screenshots to help explain your problem.
Additional context
Output from script below. The two
ERRORlines arise during the call to xrEndFrame(), presumably within the guts of SteamVR, and appear because this script hooks up OpenGL debugging. The finalWARNINGline comes from the repro script itself.Repro script. See especially line 248
if error != GL.GL_NO_ERROR:...