protonscr

[BUG] SteamVR 2.12.9 Linux uncleared OpenGL error state after call to xrEndFrame()

steamvropen bug
ValveSoftware/SteamVR-for-Linux#799 · opened 2025-08-10 by cmbruns · updated 2025-10-18 · 1 comments · github
Ccmbruns 2025-08-10 github

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:

  1. Run SteamVR 2.12.9 on Linux.
  2. Run the attached script (see below) with the HMD worn, so the OpenXR session can enter the FOCUSED state.
  3. Notice how line 249 finds the uncleared OpenGL error state after the call to xr.end_frame()
  4. 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()
Ccmbruns 2025-10-18 github

Problem remains in STEAMVR 2.12.14

Nothing extracted yet.