Implement EGL_ANGLE_context_virtualization on GLX

Implement the EGL_ANGLE_context_virtualization extension that,
despite its name, actually allows the de-virtualization of EGL
contexts.

Implement ContextGLX and RendererGLX classes to hold the
native glx context per-renderer, rather than per-DisplayGLX
as before. Where ContextGL and RendererGL were created, create
these new subclasses instead.

Remove the glx::Context and glx::Pbuffer from DisplayGLX in
favour of the ones in RendererGLX instead.

Factor a new DisplayGLX::createRenderer() out of
DisplayGLX::initialize(). This creates a glx::Context and
glx::Pbuffer for the new Renderer. Implement
DisplayGLX::mVirtualizationGroups to store a map from virtualization
group to Renderer.

Add an EGLContext test that exercises eglMakeCurrent and
eglGetCurrentContext on multiple threads.

Add a parameter to EGLWindow to request EGL_PBUFFER_BIT.
Some tests were silently relying on the backend choosing a config
that supports pbuffers, but GLX does not. Modify the tests to
explicitly request support

Bug: angleproject:40096700, chromium:491346041
Change-Id: I71b309c44f48e956e67794daf2ba067bd9fbfbad
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7688012
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Stephen White <senorblanco@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
diff --git a/src/libANGLE/renderer/gl/gl_backend.gni b/src/libANGLE/renderer/gl/gl_backend.gni
index c4801b5..1d89bf7 100644
--- a/src/libANGLE/renderer/gl/gl_backend.gni
+++ b/src/libANGLE/renderer/gl/gl_backend.gni
@@ -95,6 +95,8 @@
 
 if (angle_use_x11) {
   gl_backend_sources += [
+    "glx/ContextGLX.cpp",
+    "glx/ContextGLX.h",
     "glx/DisplayGLX.cpp",
     "glx/DisplayGLX.h",
     "glx/DisplayGLX_api.h",
@@ -104,6 +106,8 @@
     "glx/PbufferSurfaceGLX.h",
     "glx/PixmapSurfaceGLX.cpp",
     "glx/PixmapSurfaceGLX.h",
+    "glx/RendererGLX.cpp",
+    "glx/RendererGLX.h",
     "glx/SurfaceGLX.h",
     "glx/WindowSurfaceGLX.cpp",
     "glx/WindowSurfaceGLX.h",
diff --git a/src/libANGLE/renderer/gl/glx/ContextGLX.cpp b/src/libANGLE/renderer/gl/glx/ContextGLX.cpp
new file mode 100644
index 0000000..25773a7
--- /dev/null
+++ b/src/libANGLE/renderer/gl/glx/ContextGLX.cpp
@@ -0,0 +1,32 @@
+//
+// Copyright 2026 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+
+#include "libANGLE/renderer/gl/glx/ContextGLX.h"
+#include "libANGLE/renderer/gl/glx/RendererGLX.h"
+
+namespace rx
+{
+
+ContextGLX::ContextGLX(const gl::State &state,
+                       gl::ErrorSet *errorSet,
+                       const std::shared_ptr<RendererGLX> &renderer,
+                       RobustnessVideoMemoryPurgeStatus robustnessVideoMemoryPurgeStatus)
+    : ContextGL(state, errorSet, renderer, robustnessVideoMemoryPurgeStatus), mRendererGLX(renderer)
+{}
+
+ContextGLX::~ContextGLX() {}
+
+glx::Context ContextGLX::getContext() const
+{
+    return mRendererGLX->getContext();
+}
+
+glx::Pbuffer ContextGLX::getPbuffer() const
+{
+    return mRendererGLX->getPbuffer();
+}
+
+}  // namespace rx
diff --git a/src/libANGLE/renderer/gl/glx/ContextGLX.h b/src/libANGLE/renderer/gl/glx/ContextGLX.h
new file mode 100644
index 0000000..45ab942
--- /dev/null
+++ b/src/libANGLE/renderer/gl/glx/ContextGLX.h
@@ -0,0 +1,38 @@
+//
+// Copyright 2026 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+
+// ContextGLX.h: Context class for GLX on Linux.
+
+#ifndef LIBANGLE_RENDERER_GL_GLX_CONTEXTGLX_H_
+#define LIBANGLE_RENDERER_GL_GLX_CONTEXTGLX_H_
+
+#include "libANGLE/renderer/gl/ContextGL.h"
+#include "libANGLE/renderer/gl/glx/FunctionsGLX.h"
+
+namespace rx
+{
+
+class RendererGLX;
+struct ExternalContextState;
+
+class ContextGLX : public ContextGL
+{
+  public:
+    ContextGLX(const gl::State &state,
+               gl::ErrorSet *errorSet,
+               const std::shared_ptr<RendererGLX> &renderer,
+               RobustnessVideoMemoryPurgeStatus robustnessVideoMemoryPurgeStatus);
+    ~ContextGLX() override;
+
+    glx::Context getContext() const;
+    glx::Pbuffer getPbuffer() const;
+
+  private:
+    std::shared_ptr<RendererGLX> mRendererGLX;
+};
+}  // namespace rx
+
+#endif  // LIBANGLE_RENDERER_GL_GLX_RENDERERGLX_H_
diff --git a/src/libANGLE/renderer/gl/glx/DisplayGLX.cpp b/src/libANGLE/renderer/gl/glx/DisplayGLX.cpp
index 0846ba9..6338ca3 100644
--- a/src/libANGLE/renderer/gl/glx/DisplayGLX.cpp
+++ b/src/libANGLE/renderer/gl/glx/DisplayGLX.cpp
@@ -20,7 +20,6 @@
 #include "libANGLE/Context.h"
 #include "libANGLE/Display.h"
 #include "libANGLE/Surface.h"
-#include "libANGLE/renderer/gl/ContextGL.h"
 #include "libANGLE/renderer/gl/RendererGL.h"
 #include "libANGLE/renderer/gl/renderergl_utils.h"
 
@@ -28,6 +27,7 @@
 
 #include <EGL/eglext.h>
 
+#include "libANGLE/renderer/gl/glx/ContextGLX.h"
 #include "libANGLE/renderer/gl/glx/DisplayGLX_api.h"
 #include "libANGLE/renderer/gl/glx/PbufferSurfaceGLX.h"
 #include "libANGLE/renderer/gl/glx/PixmapSurfaceGLX.h"
@@ -78,9 +78,7 @@
     : DisplayGL(state),
       mRequestedVisual(-1),
       mContextConfig(nullptr),
-      mContext(nullptr),
       mCurrentNativeContexts(),
-      mInitPbuffer(0),
       mUsesNewXDisplay(false),
       mIsMesa(false),
       mHasMultisample(false),
@@ -93,7 +91,6 @@
       mMinSwapInterval(0),
       mMaxSwapInterval(0),
       mCurrentSwapInterval(-1),
-      mCurrentDrawable(0),
       mXDisplay(nullptr),
       mEGLDisplay(nullptr)
 {}
@@ -229,10 +226,27 @@
         XFree(candidates);
     }
 
-    const auto &eglAttributes = display->getAttributeMap();
+    ANGLE_TRY(createRenderer(0, true, &mRenderer));
+    ASSERT(mRenderer);
+    const gl::Version &maxVersion = mRenderer->getMaxSupportedESVersion();
+    if (maxVersion < gl::Version(2, 0))
+    {
+        return egl::Error(EGL_NOT_INITIALIZED, "OpenGL ES 2.0 is not supportable.");
+    }
+
+    return DisplayGL::initialize(display);
+}
+
+egl::Error DisplayGLX::createRenderer(glx::Context shareContext,
+                                      bool makeNewContextCurrent,
+                                      std::shared_ptr<RendererGLX> *outRenderer)
+{
+    glx::Context context = 0;
+
+    const auto &eglAttributes = mEGLDisplay->getAttributeMap();
     if (mHasARBCreateContext)
     {
-        egl::Error error = initializeContext(mContextConfig, eglAttributes, &mContext);
+        egl::Error error = initializeContext(shareContext, mContextConfig, eglAttributes, &context);
         if (error.isError())
         {
             return error;
@@ -262,17 +276,15 @@
         }
         ASSERT(numVisuals == 1);
 
-        mContext = mGLX.createContext(&visuals[0], nullptr, true);
+        context = mGLX.createContext(&visuals[0], shareContext, true);
         XFree(visuals);
 
-        if (!mContext)
+        if (!context)
         {
             return egl::Error(EGL_NOT_INITIALIZED, "Could not create GL context.");
         }
     }
-    ASSERT(mContext);
-
-    mCurrentNativeContexts[angle::GetCurrentThreadUniqueId()] = mContext;
+    ASSERT(context);
 
     // FunctionsGL and DisplayGL need to make a few GL calls, for example to
     // query the version of the context so we need to make the context current.
@@ -287,13 +299,13 @@
     int initPbufferAttribs[] = {
         GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, None,
     };
-    mInitPbuffer = mGLX.createPbuffer(mContextConfig, initPbufferAttribs);
-    if (!mInitPbuffer)
+    glx::Pbuffer pbuffer = mGLX.createPbuffer(mContextConfig, initPbufferAttribs);
+    if (!pbuffer)
     {
         return egl::Error(EGL_NOT_INITIALIZED, "Could not create the initialization pbuffer.");
     }
 
-    if (!mGLX.makeCurrent(mInitPbuffer, mContext))
+    if (!mGLX.makeCurrent(pbuffer, context))
     {
         return egl::Error(EGL_NOT_INITIALIZED,
                           "Could not make the initialization pbuffer current.");
@@ -326,38 +338,37 @@
 
     syncXCommands(false);
 
-    mRenderer.reset(new RendererGL(std::move(functionsGL), eglAttributes, this));
-    const gl::Version &maxVersion = mRenderer->getMaxSupportedESVersion();
-    if (maxVersion < gl::Version(2, 0))
+    outRenderer->reset(
+        new RendererGLX(std::move(functionsGL), eglAttributes, mGLX, this, context, pbuffer));
+
+    NativeContext &currentContext = mCurrentNativeContexts[angle::GetCurrentThreadUniqueId()];
+
+    if (makeNewContextCurrent)
     {
-        return egl::Error(EGL_NOT_INITIALIZED, "OpenGL ES 2.0 is not supportable.");
+        currentContext = {context, pbuffer};
+    }
+    else
+    {
+        // Reset the current context back to the previous state
+        if (mGLX.makeCurrent(currentContext.drawable, currentContext.context) == EGL_FALSE)
+        {
+            return egl::Error(EGL_CONTEXT_LOST, "Failed to make the previous context current");
+        }
     }
 
-    return DisplayGL::initialize(display);
+    return egl::NoError();
 }
 
 void DisplayGLX::terminate()
 {
     DisplayGL::terminate();
 
-    if (mInitPbuffer)
-    {
-        mGLX.destroyPbuffer(mInitPbuffer);
-        mInitPbuffer = 0;
-    }
-
     mCurrentNativeContexts.clear();
 
-    if (mContext)
-    {
-        mGLX.destroyContext(mContext);
-        mContext = nullptr;
-    }
+    mRenderer.reset();
 
     mGLX.terminate();
 
-    mRenderer.reset();
-
     if (mUsesNewXDisplay)
     {
         XCloseDisplay(mXDisplay);
@@ -369,25 +380,29 @@
                                    egl::Surface *readSurface,
                                    gl::Context *context)
 {
-    glx::Drawable newDrawable =
-        (drawSurface ? GetImplAs<SurfaceGLX>(drawSurface)->getDrawable() : mInitPbuffer);
-    glx::Context newContext = mContext;
-    // If the thread calling makeCurrent does not have the correct context current (either mContext
-    // or 0), we need to set it current.
-    if (!context)
+    NativeContext &currentContext = mCurrentNativeContexts[angle::GetCurrentThreadUniqueId()];
+    glx::Context newContext       = 0;
+    glx::Drawable newDrawable     = 0;
+    if (context)
     {
-        newDrawable = 0;
-        newContext  = 0;
+        ContextGLX *newContextGLX = GetImplAs<ContextGLX>(context);
+        newContext                = newContextGLX->getContext();
+        if (drawSurface)
+        {
+            newDrawable = GetImplAs<SurfaceGLX>(drawSurface)->getDrawable();
+        }
+        else
+        {
+            newDrawable = newContextGLX->getPbuffer();
+        }
     }
-    if (newDrawable != mCurrentDrawable ||
-        newContext != mCurrentNativeContexts[angle::GetCurrentThreadUniqueId()])
+    if (newDrawable != currentContext.drawable || newContext != currentContext.context)
     {
         if (mGLX.makeCurrent(newDrawable, newContext) != True)
         {
             return egl::Error(EGL_CONTEXT_LOST, "Failed to make the GLX context current");
         }
-        mCurrentNativeContexts[angle::GetCurrentThreadUniqueId()] = newContext;
-        mCurrentDrawable                                          = newDrawable;
+        currentContext = {newContext, newDrawable};
     }
 
     return DisplayGL::makeCurrent(display, drawSurface, readSurface, context);
@@ -464,10 +479,44 @@
 {
     RobustnessVideoMemoryPurgeStatus robustnessVideoMemoryPurgeStatus =
         GetRobustnessVideoMemoryPurge(attribs);
-    return new ContextGL(state, errorSet, mRenderer, robustnessVideoMemoryPurgeStatus);
+    EGLAttrib virtualizationGroup =
+        attribs.get(EGL_CONTEXT_VIRTUALIZATION_GROUP_ANGLE, EGL_DONT_CARE);
+    bool globalTextureShareGroup =
+        attribs.get(EGL_DISPLAY_TEXTURE_SHARE_GROUP_ANGLE, EGL_FALSE) == EGL_TRUE;
+
+    std::shared_ptr<RendererGLX> renderer = mRenderer;
+
+    if (virtualizationGroup != EGL_DONT_CARE)
+    {
+        renderer = mVirtualizationGroups[virtualizationGroup].lock();
+        if (!renderer)
+        {
+            glx::Context nativeShareContext = 0;
+            if (globalTextureShareGroup)
+            {
+                nativeShareContext = mRenderer->getContext();
+            }
+            else if (shareContext)
+            {
+                ContextGLX *shareContextGLX = GetImplAs<ContextGLX>(shareContext);
+                nativeShareContext          = shareContextGLX->getContext();
+            }
+            // Create a new renderer for this context.
+            egl::Error error = createRenderer(nativeShareContext, false, &renderer);
+            if (error.isError())
+            {
+                ERR() << "Failed to create a shared renderer: " << error.getMessage();
+                return nullptr;
+            }
+
+            mVirtualizationGroups[virtualizationGroup] = renderer;
+        }
+    }
+    return new ContextGLX(state, errorSet, renderer, robustnessVideoMemoryPurgeStatus);
 }
 
-egl::Error DisplayGLX::initializeContext(glx::FBConfig config,
+egl::Error DisplayGLX::initializeContext(glx::Context shareContext,
+                                         glx::FBConfig config,
                                          const egl::AttributeMap &eglAttributes,
                                          glx::Context *context)
 {
@@ -502,7 +551,7 @@
         {
             profileMask |= GLX_CONTEXT_CORE_PROFILE_BIT_ARB;
         }
-        return createContextAttribs(config, requestedVersion, profileMask, context);
+        return createContextAttribs(shareContext, config, requestedVersion, profileMask, context);
     }
 
     // The only way to get a core profile context of the highest version using
@@ -529,7 +578,8 @@
             profileFlag |= GLX_CONTEXT_ES2_PROFILE_BIT_EXT;
         }
 
-        egl::Error error = createContextAttribs(config, info.version, profileFlag, context);
+        egl::Error error =
+            createContextAttribs(shareContext, config, info.version, profileFlag, context);
         if (!error.isError())
         {
             return error;
@@ -881,6 +931,8 @@
 
     outExtensions->robustnessVideoMemoryPurgeNV = mHasNVRobustnessVideoMemoryPurge;
 
+    outExtensions->contextVirtualizationANGLE = true;
+
     DisplayGL::generateExtensions(outExtensions);
 }
 
@@ -903,7 +955,8 @@
     return result;
 }
 
-egl::Error DisplayGLX::createContextAttribs(glx::FBConfig,
+egl::Error DisplayGLX::createContextAttribs(glx::Context shareContext,
+                                            glx::FBConfig,
                                             const Optional<gl::Version> &version,
                                             int profileMask,
                                             glx::Context *context) const
@@ -947,7 +1000,7 @@
     // (the error handler is NOT per-display).
     XSync(mXDisplay, False);
     auto oldErrorHandler = XSetErrorHandler(IgnoreX11Errors);
-    *context = mGLX.createContextAttribsARB(mContextConfig, nullptr, True, attribs.data());
+    *context = mGLX.createContextAttribsARB(mContextConfig, shareContext, True, attribs.data());
     XSetErrorHandler(oldErrorHandler);
 
     if (!*context)
diff --git a/src/libANGLE/renderer/gl/glx/DisplayGLX.h b/src/libANGLE/renderer/gl/glx/DisplayGLX.h
index 6e38676..ad457a7 100644
--- a/src/libANGLE/renderer/gl/glx/DisplayGLX.h
+++ b/src/libANGLE/renderer/gl/glx/DisplayGLX.h
@@ -14,9 +14,9 @@
 
 #include "common/Optional.h"
 #include "libANGLE/renderer/gl/DisplayGL.h"
-#include "libANGLE/renderer/gl/RendererGL.h"
 
 #include "libANGLE/renderer/gl/glx/FunctionsGLX.h"
+#include "libANGLE/renderer/gl/glx/RendererGLX.h"
 
 namespace rx
 {
@@ -95,12 +95,16 @@
 
     void populateFeatureList(angle::FeatureList *features) override;
 
+    egl::Error createRenderer(glx::Context shareContext,
+                              bool makeNewContextCurrent,
+                              std::shared_ptr<RendererGLX> *outRenderer);
     RendererGL *getRenderer() const override;
 
     angle::NativeWindowSystem getWindowSystem() const override;
 
   private:
-    egl::Error initializeContext(glx::FBConfig config,
+    egl::Error initializeContext(glx::Context shareContext,
+                                 glx::FBConfig config,
                                  const egl::AttributeMap &eglAttributes,
                                  glx::Context *context);
 
@@ -110,22 +114,24 @@
     egl::Error makeCurrentSurfaceless(gl::Context *context) override;
 
     int getGLXFBConfigAttrib(glx::FBConfig config, int attrib) const;
-    egl::Error createContextAttribs(glx::FBConfig,
+    egl::Error createContextAttribs(glx::Context shareContext,
+                                    glx::FBConfig,
                                     const Optional<gl::Version> &version,
                                     int profileMask,
                                     glx::Context *context) const;
 
-    std::shared_ptr<RendererGL> mRenderer;
+    std::shared_ptr<RendererGLX> mRenderer;
 
     std::map<int, glx::FBConfig> configIdToGLXConfig;
 
     EGLint mRequestedVisual;
     glx::FBConfig mContextConfig;
-    glx::Context mContext;
-    angle::HashMap<uint64_t, glx::Context> mCurrentNativeContexts;
-
-    // A pbuffer the context is current on during ANGLE initialization
-    glx::Pbuffer mInitPbuffer;
+    struct NativeContext
+    {
+        glx::Context context;
+        glx::Drawable drawable;
+    };
+    angle::HashMap<uint64_t, NativeContext> mCurrentNativeContexts;
 
     bool mUsesNewXDisplay;
     bool mIsMesa;
@@ -148,7 +154,7 @@
     int mMaxSwapInterval;
     int mCurrentSwapInterval;
 
-    glx::Drawable mCurrentDrawable;
+    std::map<EGLAttrib, std::weak_ptr<RendererGLX>> mVirtualizationGroups;
 
     FunctionsGLX mGLX;
     Display *mXDisplay;
diff --git a/src/libANGLE/renderer/gl/glx/RendererGLX.cpp b/src/libANGLE/renderer/gl/glx/RendererGLX.cpp
new file mode 100644
index 0000000..df39faf
--- /dev/null
+++ b/src/libANGLE/renderer/gl/glx/RendererGLX.cpp
@@ -0,0 +1,49 @@
+//
+// Copyright 2026 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+
+#include "libANGLE/renderer/gl/glx/RendererGLX.h"
+
+#include "libANGLE/renderer/gl/glx/DisplayGLX.h"
+
+namespace rx
+{
+
+RendererGLX::RendererGLX(std::unique_ptr<FunctionsGL> functionsGL,
+                         const egl::AttributeMap &attribMap,
+                         const FunctionsGLX &glx,
+                         DisplayGLX *display,
+                         glx::Context context,
+                         glx::Pbuffer pbuffer)
+    : RendererGL(std::move(functionsGL), attribMap, display),
+      mGLX(glx),
+      mContext(context),
+      mPbuffer(pbuffer)
+{}
+
+RendererGLX::~RendererGLX()
+{
+    if (mPbuffer)
+    {
+        mGLX.destroyPbuffer(mPbuffer);
+    }
+
+    if (mContext)
+    {
+        mGLX.destroyContext(mContext);
+    }
+}
+
+glx::Context RendererGLX::getContext() const
+{
+    return mContext;
+}
+
+glx::Pbuffer RendererGLX::getPbuffer() const
+{
+    return mPbuffer;
+}
+
+}  // namespace rx
diff --git a/src/libANGLE/renderer/gl/glx/RendererGLX.h b/src/libANGLE/renderer/gl/glx/RendererGLX.h
new file mode 100644
index 0000000..1841182
--- /dev/null
+++ b/src/libANGLE/renderer/gl/glx/RendererGLX.h
@@ -0,0 +1,43 @@
+//
+// Copyright 2026 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+
+// RendererGLX.h: Renderer class for GL on Linux.  Owns a GLX context object.
+
+#ifndef LIBANGLE_RENDERER_GL_GLX_RENDERERGLX_H_
+#define LIBANGLE_RENDERER_GL_GLX_RENDERERGLX_H_
+
+#include "libANGLE/renderer/gl/RendererGL.h"
+
+#include "libANGLE/renderer/gl/glx/FunctionsGLX.h"
+
+#undef Success
+
+namespace rx
+{
+class DisplayGLX;
+
+class RendererGLX : public RendererGL
+{
+  public:
+    RendererGLX(std::unique_ptr<FunctionsGL> functionsGL,
+                const egl::AttributeMap &attribMap,
+                const FunctionsGLX &glx,
+                DisplayGLX *display,
+                glx::Context context,
+                glx::Pbuffer pbuffer);
+    ~RendererGLX() override;
+
+    glx::Context getContext() const;
+    glx::Pbuffer getPbuffer() const;
+
+  private:
+    const FunctionsGLX &mGLX;
+    glx::Context mContext;
+    glx::Pbuffer mPbuffer;
+};
+}  // namespace rx
+
+#endif  // LIBANGLE_RENDERER_GL_GLX_RENDERERGLX_H_
diff --git a/src/tests/angle_end2end_tests.gni b/src/tests/angle_end2end_tests.gni
index bce0a90..f3a6155 100644
--- a/src/tests/angle_end2end_tests.gni
+++ b/src/tests/angle_end2end_tests.gni
@@ -14,6 +14,7 @@
   "egl_tests/EGLContextCompatibilityTest.cpp",
   "egl_tests/EGLContextPassthroughShadersTest.cpp",
   "egl_tests/EGLContextSharingTest.cpp",
+  "egl_tests/EGLContextStateTest.cpp",
   "egl_tests/EGLCreateContextAttribsTest.cpp",
   "egl_tests/EGLDebugTest.cpp",
   "egl_tests/EGLDisplaySelectionTest.cpp",
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index aa6be5b..90cee03 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -1592,6 +1592,26 @@
 42266575 WIN GLES : RGBTextureBufferTestES31.*/* = SKIP
 42266575 LINUX NVIDIA OpenGL : RGBTextureBufferTestES31.*/* = SKIP
 
+// Virtualized contexts on multiple contexts not supported on Win (wgl)
+491346041 WIN OpenGL : EGLContextStateTest.MultipleThreads/* = SKIP
+491346041 WIN GLES : EGLContextStateTest.MultipleThreads/* = SKIP
+
+// Failing on Desktop GL Linux when contexts are devirtualized
+40096700 LINUX OPENGL : MultithreadingTestES3.SharedSrgbTextureMultipleContexts/* = SKIP
+40096700 LINUX OPENGL : MultithreadingTestES3.SimultaneousUploadAndDraw/* = SKIP
+40096700 LINUX OPENGL : MultithreadingTestES3.SimultaneousBufferBind/* = SKIP
+40096700 LINUX OPENGL : MultithreadingTestES3.SimultaneousBufferBindAndGen/* = SKIP
+40096700 LINUX OPENGL : MultithreadingTestES3.UniformBlockBinding/* = SKIP
+
+// Test does not yet support devirtualized contexts.
+40096700 LINUX OPENGL : EGLContextSharingTest.DeleteReaderOfSharedTexture/* = SKIP
+
+// Failing on Intel Linux
+40096700 LINUX OPENGL INTEL : MultithreadingTestES3.CreateNewContextAfterTextureUploadOnMainThread/* = SKIP
+40096700 LINUX OPENGL INTEL : MultithreadingTestES3.MultithreadFenceDraw/* = SKIP
+40096700 LINUX OPENGL INTEL : MultithreadingTestES3.MultithreadFenceTexImage/* = SKIP
+40096700 LINUX OPENGL INTEL : MultithreadingTestES3.RenderThenSampleInNewContextWithDifferentPriority/* = SKIP
+
 42266091 WIN INTEL : *EmulateCopyTexImage2DFromRenderbuffers* = SKIP
 
 42266092 WIN SWIFTSHADER : VulkanPerformanceCounterTest.EndXfbAfterRenderPassClosed/* = SKIP
diff --git a/src/tests/egl_tests/EGLBlobCacheTest.cpp b/src/tests/egl_tests/EGLBlobCacheTest.cpp
index 1a77b5d..6ba4541 100644
--- a/src/tests/egl_tests/EGLBlobCacheTest.cpp
+++ b/src/tests/egl_tests/EGLBlobCacheTest.cpp
@@ -153,6 +153,7 @@
     {
         // Force disply caching off. Blob cache functions require it.
         forceNewDisplay();
+        setPbuffer(true);
     }
 
     void testSetUp() override
diff --git a/src/tests/egl_tests/EGLContextSharingTest.cpp b/src/tests/egl_tests/EGLContextSharingTest.cpp
index 0db1932..3f1ee22 100644
--- a/src/tests/egl_tests/EGLContextSharingTest.cpp
+++ b/src/tests/egl_tests/EGLContextSharingTest.cpp
@@ -60,6 +60,29 @@
         getEGLWindow()->makeCurrent();
     }
 
+    bool chooseConfig(EGLDisplay dpy, EGLConfig *config) const
+    {
+        bool result  = false;
+        EGLint count = 0;
+        EGLint clientVersion =
+            GetParam().majorVersion == 3 ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_ES2_BIT;
+        EGLint attribs[] = {EGL_RED_SIZE,
+                            8,
+                            EGL_GREEN_SIZE,
+                            8,
+                            EGL_BLUE_SIZE,
+                            8,
+                            EGL_RENDERABLE_TYPE,
+                            clientVersion,
+                            EGL_SURFACE_TYPE,
+                            EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
+                            EGL_NONE};
+
+        result = eglChooseConfig(dpy, attribs, config, 1, &count);
+        EXPECT_EGL_TRUE(result && (count > 0));
+        return result;
+    }
+
     EGLContext mContexts[2] = {EGL_NO_CONTEXT, EGL_NO_CONTEXT};
     GLuint mTexture;
 };
@@ -105,30 +128,6 @@
         ASSERT_EGL_SUCCESS() << "Error during test TearDown";
     }
 
-    bool chooseConfig(EGLConfig *config) const
-    {
-        bool result          = false;
-        EGLint count         = 0;
-        EGLint clientVersion = mMajorVersion == 3 ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_ES2_BIT;
-        EGLint attribs[]     = {EGL_RED_SIZE,
-                                8,
-                                EGL_GREEN_SIZE,
-                                8,
-                                EGL_BLUE_SIZE,
-                                8,
-                                EGL_ALPHA_SIZE,
-                                8,
-                                EGL_RENDERABLE_TYPE,
-                                clientVersion,
-                                EGL_SURFACE_TYPE,
-                                EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
-                                EGL_NONE};
-
-        result = eglChooseConfig(mDisplay, attribs, config, 1, &count);
-        EXPECT_EGL_TRUE(result && (count > 0));
-        return result;
-    }
-
     bool createContext(EGLConfig config,
                        EGLContext *context,
                        EGLContext share_context = EGL_NO_CONTEXT)
@@ -944,7 +943,7 @@
         EGLContext ctx;
         EGLSurface srf;
         EGLConfig config = EGL_NO_CONFIG_KHR;
-        EXPECT_TRUE(chooseConfig(&config));
+        EXPECT_TRUE(chooseConfig(mDisplay, &config));
         EXPECT_TRUE(createContext(config, &ctx));
 
         EXPECT_TRUE(createPbufferSurface(mDisplay, config, 1280, 720, &srf));
@@ -972,7 +971,7 @@
         EGLContext ctx;
         EGLSurface srf;
         EGLConfig config = EGL_NO_CONFIG_KHR;
-        EXPECT_TRUE(chooseConfig(&config));
+        EXPECT_TRUE(chooseConfig(mDisplay, &config));
         EXPECT_TRUE(createContext(config, &ctx));
 
         EXPECT_TRUE(createPbufferSurface(mDisplay, config, 1280, 720, &srf));
@@ -1019,7 +1018,7 @@
     EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr));
 
     EGLConfig config = EGL_NO_CONFIG_KHR;
-    EXPECT_TRUE(chooseConfig(&config));
+    EXPECT_TRUE(chooseConfig(mDisplay, &config));
 
     mOsWindow->initialize("EGLContextSharingTestNoFixture", kWidth, kHeight);
     EXPECT_TRUE(createWindowSurface(config, mOsWindow->getNativeWindow(), &mSurface));
@@ -1072,7 +1071,7 @@
         EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY);
         EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr));
         config = EGL_NO_CONFIG_KHR;
-        EXPECT_TRUE(chooseConfig(&config));
+        EXPECT_TRUE(chooseConfig(mDisplay, &config));
         EXPECT_TRUE(createContext(config, &mContexts[1]));
 
         // Thread1's terminate call will make mSurface an invalid handle, recreate a new surface
@@ -1137,7 +1136,7 @@
     EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr));
 
     EGLConfig config = EGL_NO_CONFIG_KHR;
-    EXPECT_TRUE(chooseConfig(&config));
+    EXPECT_TRUE(chooseConfig(mDisplay, &config));
 
     mOsWindow->initialize("EGLContextSharingTestNoFixture", kWidth, kHeight);
     EXPECT_TRUE(createWindowSurface(config, mOsWindow->getNativeWindow(), &mSurface));
@@ -1190,7 +1189,7 @@
         EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY);
         EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr));
         config = EGL_NO_CONFIG_KHR;
-        EXPECT_TRUE(chooseConfig(&config));
+        EXPECT_TRUE(chooseConfig(mDisplay, &config));
         EXPECT_TRUE(createContext(config, &mContexts[1]));
 
         // Thread1's terminate call will make mSurface an invalid handle, recreate a new surface
@@ -1273,7 +1272,7 @@
     EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr));
 
     EGLConfig config = EGL_NO_CONFIG_KHR;
-    EXPECT_TRUE(chooseConfig(&config));
+    EXPECT_TRUE(chooseConfig(mDisplay, &config));
 
     mOsWindow->initialize("EGLContextSharingTestNoFixture", kWidth, kHeight);
     EXPECT_TRUE(createWindowSurface(config, mOsWindow->getNativeWindow(), &mSurface));
@@ -1316,7 +1315,7 @@
     EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr));
 
     EGLConfig config = EGL_NO_CONFIG_KHR;
-    EXPECT_TRUE(chooseConfig(&config));
+    EXPECT_TRUE(chooseConfig(mDisplay, &config));
 
     mOsWindow->initialize("EGLContextSharingTestNoFixture", kWidth, kHeight);
     EXPECT_TRUE(createWindowSurface(config, mOsWindow->getNativeWindow(), &mSurface));
@@ -1586,7 +1585,7 @@
     EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr));
 
     EGLConfig config = EGL_NO_CONFIG_KHR;
-    EXPECT_TRUE(chooseConfig(&config));
+    EXPECT_TRUE(chooseConfig(mDisplay, &config));
 
     // Create a context and immediately destroy it.  Note that no window surface should be created
     // for this test.  Regression test for platforms that expose multiple queue families in Vulkan,
@@ -1618,19 +1617,6 @@
         ASSERT_EGL_SUCCESS() << "Error during EGLPriorityContextSharingTestNoFixture TearDown";
     }
 
-    bool chooseConfig(EGLConfig *config) const
-    {
-        bool result          = false;
-        EGLint count         = 0;
-        EGLint clientVersion = mMajorVersion == 3 ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_ES2_BIT;
-        EGLint attribs[]     = {EGL_RENDERABLE_TYPE, clientVersion, EGL_SURFACE_TYPE,
-                                EGL_WINDOW_BIT | EGL_PBUFFER_BIT, EGL_NONE};
-
-        result = eglChooseConfig(mDisplay, attribs, config, 1, &count);
-        EXPECT_EGL_TRUE(result && (count > 0));
-        return result;
-    }
-
     EGLDisplay mDisplay  = EGL_NO_DISPLAY;
     const EGLint kWidth  = 64;
     const EGLint kHeight = 64;
@@ -1643,7 +1629,7 @@
     ANGLE_SKIP_TEST_IF(!IsEGLDisplayExtensionEnabled(mDisplay, "EGL_IMG_context_priority"));
 
     EGLConfig config = EGL_NO_CONFIG_KHR;
-    EXPECT_TRUE(chooseConfig(&config));
+    EXPECT_TRUE(chooseConfig(mDisplay, &config));
 
     // Initialize contexts
     constexpr size_t kContextCount = 2;
diff --git a/src/tests/egl_tests/EGLContextStateTest.cpp b/src/tests/egl_tests/EGLContextStateTest.cpp
new file mode 100644
index 0000000..0b0c3c7
--- /dev/null
+++ b/src/tests/egl_tests/EGLContextStateTest.cpp
@@ -0,0 +1,110 @@
+//
+// Copyright 2026 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// EGLContextStateTest.cpp:
+//   Tests relating to Context state.
+
+#include <gtest/gtest.h>
+
+#include "common/tls.h"
+#include "test_utils/ANGLETest.h"
+#include "test_utils/MultiThreadSteps.h"
+#include "test_utils/angle_test_configs.h"
+#include "test_utils/gl_raii.h"
+#include "util/EGLWindow.h"
+#include "util/OSWindow.h"
+#include "util/test_utils.h"
+
+using namespace angle;
+
+namespace
+{
+
+class EGLContextStateTest : public ANGLETest<>
+{
+  public:
+    void testSetUp() override
+    {
+        EGLAttrib dispattrs[] = {EGL_PLATFORM_ANGLE_TYPE_ANGLE, GetParam().getRenderer(), EGL_NONE};
+
+        mDisplay = eglGetPlatformDisplay(GetEglPlatform(),
+                                         reinterpret_cast<void *>(EGL_DEFAULT_DISPLAY), dispattrs);
+        EXPECT_TRUE(mDisplay != EGL_NO_DISPLAY);
+        EXPECT_EGL_TRUE(eglInitialize(mDisplay, nullptr, nullptr));
+
+        int nConfigs = 0;
+        ASSERT_EGL_TRUE(eglGetConfigs(mDisplay, nullptr, 0, &nConfigs));
+        ASSERT_GE(nConfigs, 1);
+
+        int nReturnedConfigs = 0;
+        std::vector<EGLConfig> configs(nConfigs);
+        ASSERT_EGL_TRUE(eglGetConfigs(mDisplay, configs.data(), nConfigs, &nReturnedConfigs));
+        ASSERT_EQ(nConfigs, nReturnedConfigs);
+        mConfig = configs[0];
+    }
+
+    EGLContext createContext(EGLConfig config)
+    {
+        EGLint attribs[] = {EGL_CONTEXT_MAJOR_VERSION, 2, EGL_CONTEXT_VIRTUALIZATION_GROUP_ANGLE,
+                            mVirtualizationGroup++, EGL_NONE};
+        if (!IsEGLDisplayExtensionEnabled(mDisplay, "EGL_ANGLE_context_virtualization"))
+        {
+            attribs[2] = EGL_NONE;
+        }
+
+        EGLContext context = eglCreateContext(mDisplay, config, nullptr, attribs);
+        EXPECT_NE(context, EGL_NO_CONTEXT);
+        return context;
+    }
+
+    void testTearDown() override {}
+
+    EGLDisplay mDisplay;
+    EGLConfig mConfig;
+    std::atomic<EGLint> mVirtualizationGroup;
+};
+
+// Test eglMakeCurrent() and eglGetCurrentContext() on multiple threads.
+TEST_P(EGLContextStateTest, MultipleThreads)
+{
+    EGLContext contexts[2] = {EGL_NO_CONTEXT, EGL_NO_CONTEXT};
+
+    contexts[0] = createContext(mConfig);
+    contexts[1] = createContext(mConfig);
+
+    {
+        EXPECT_EQ(eglGetCurrentContext(), EGL_NO_CONTEXT);
+        EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, contexts[0]));
+        EXPECT_EQ(eglGetCurrentContext(), contexts[0]);
+        EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, contexts[1]));
+        EXPECT_EQ(eglGetCurrentContext(), contexts[1]);
+    }
+
+    std::thread thread([this, contexts]() {
+        auto prevContext = eglGetCurrentContext();
+        EXPECT_EQ(prevContext, EGL_NO_CONTEXT);
+        EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, contexts[0]));
+        EXPECT_EQ(eglGetCurrentContext(), contexts[0]);
+        EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, prevContext));
+        EXPECT_EQ(eglGetCurrentContext(), prevContext);
+    });
+    thread.join();
+    EXPECT_EGL_TRUE(eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT));
+}
+
+ANGLE_INSTANTIATE_TEST(EGLContextStateTest,
+                       WithNoFixture(ES2_D3D9()),
+                       WithNoFixture(ES2_D3D11()),
+                       WithNoFixture(ES3_D3D11()),
+                       WithNoFixture(ES2_METAL()),
+                       WithNoFixture(ES3_METAL()),
+                       WithNoFixture(ES2_OPENGL()),
+                       WithNoFixture(ES3_OPENGL()),
+                       WithNoFixture(ES2_OPENGLES()),
+                       WithNoFixture(ES3_OPENGLES()),
+                       WithNoFixture(ES2_VULKAN()),
+                       WithNoFixture(ES3_VULKAN()));
+
+}  // namespace
diff --git a/src/tests/egl_tests/EGLMultiContextTest.cpp b/src/tests/egl_tests/EGLMultiContextTest.cpp
index de69b58..053d5f7 100644
--- a/src/tests/egl_tests/EGLMultiContextTest.cpp
+++ b/src/tests/egl_tests/EGLMultiContextTest.cpp
@@ -38,7 +38,10 @@
 class EGLMultiContextTest : public ANGLETest<>
 {
   public:
-    EGLMultiContextTest() : mContexts{EGL_NO_CONTEXT, EGL_NO_CONTEXT}, mTexture(0) {}
+    EGLMultiContextTest() : mContexts{EGL_NO_CONTEXT, EGL_NO_CONTEXT}, mTexture(0)
+    {
+        setPbuffer(true);
+    }
 
     void testTearDown() override
     {
@@ -119,6 +122,12 @@
         Flush,
         Finish,
     };
+
+    bool hasFenceSyncExtension() const
+    {
+        return IsEGLDisplayExtensionEnabled(getEGLWindow()->getDisplay(), "EGL_KHR_fence_sync");
+    }
+
     void testFenceWithOpenRenderPass(FenceTest test, FlushMethod flushMethod);
 
     EGLContext mContexts[2];
@@ -459,6 +468,7 @@
 void EGLMultiContextTest::testFenceWithOpenRenderPass(FenceTest test, FlushMethod flushMethod)
 {
     ANGLE_SKIP_TEST_IF(!platformSupportsMultithreading());
+    ANGLE_SKIP_TEST_IF(!hasFenceSyncExtension());
 
     constexpr uint32_t kWidth  = 100;
     constexpr uint32_t kHeight = 200;
diff --git a/src/tests/gl_tests/ImageTest.cpp b/src/tests/gl_tests/ImageTest.cpp
index 411bb51..357d150 100644
--- a/src/tests/gl_tests/ImageTest.cpp
+++ b/src/tests/gl_tests/ImageTest.cpp
@@ -157,6 +157,7 @@
         setConfigBlueBits(8);
         setConfigAlphaBits(8);
         setConfigDepthBits(24);
+        setPbuffer(true);
     }
 
     const char *getVS() const
diff --git a/src/tests/gl_tests/MultithreadingTest.cpp b/src/tests/gl_tests/MultithreadingTest.cpp
index 7d28662..11fd062 100644
--- a/src/tests/gl_tests/MultithreadingTest.cpp
+++ b/src/tests/gl_tests/MultithreadingTest.cpp
@@ -37,6 +37,7 @@
         setConfigGreenBits(8);
         setConfigBlueBits(8);
         setConfigAlphaBits(8);
+        setPbuffer(true);
     }
 
     bool hasFenceSyncExtension() const
diff --git a/src/tests/test_utils/ANGLETest.cpp b/src/tests/test_utils/ANGLETest.cpp
index 8394357..63476f2 100644
--- a/src/tests/test_utils/ANGLETest.cpp
+++ b/src/tests/test_utils/ANGLETest.cpp
@@ -1676,6 +1676,11 @@
     mFixture->configParams.robustResourceInit = enabled;
 }
 
+void ANGLETestBase::setPbuffer(bool enabled)
+{
+    mFixture->configParams.pbuffer = enabled;
+}
+
 void ANGLETestBase::setMutableRenderBuffer(bool enabled)
 {
     mFixture->configParams.mutableRenderBuffer = enabled;
diff --git a/src/tests/test_utils/ANGLETest.h b/src/tests/test_utils/ANGLETest.h
index 1910cc1..fd49f8f 100644
--- a/src/tests/test_utils/ANGLETest.h
+++ b/src/tests/test_utils/ANGLETest.h
@@ -527,6 +527,7 @@
     void setBindGeneratesResource(bool bindGeneratesResource);
     void setClientArraysEnabled(bool enabled);
     void setRobustResourceInit(bool enabled);
+    void setPbuffer(bool enabled);
     void setMutableRenderBuffer(bool enabled);
     void setContextProgramCacheEnabled(bool enabled);
     void setContextResetStrategy(EGLenum resetStrategy);
diff --git a/util/EGLWindow.cpp b/util/EGLWindow.cpp
index 3a2fb54..fd71983 100644
--- a/util/EGLWindow.cpp
+++ b/util/EGLWindow.cpp
@@ -53,6 +53,8 @@
       clientArraysEnabled(true),
       // The default value of EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT is EGL_FALSE.
       robustAccess(false),
+      // Tests that require EGL_PBUFFER_BIT should request it explicitly.
+      pbuffer(false),
       // EGL_RENDER_BUFFER requires EGL 1.4+ or extension support.
       mutableRenderBuffer(false),
       samples(-1),
@@ -339,7 +341,8 @@
 
     std::vector<EGLint> configAttributes = {
         EGL_SURFACE_TYPE,
-        EGL_WINDOW_BIT | (params.mutableRenderBuffer ? EGL_MUTABLE_RENDER_BUFFER_BIT_KHR : 0),
+        EGL_WINDOW_BIT | (params.pbuffer ? EGL_PBUFFER_BIT : 0) |
+            (params.mutableRenderBuffer ? EGL_MUTABLE_RENDER_BUFFER_BIT_KHR : 0),
         EGL_RED_SIZE,
         (mConfigParams.redBits >= 0) ? mConfigParams.redBits : EGL_DONT_CARE,
         EGL_GREEN_SIZE,
diff --git a/util/EGLWindow.h b/util/EGLWindow.h
index 99256fe..6cd687f 100644
--- a/util/EGLWindow.h
+++ b/util/EGLWindow.h
@@ -55,6 +55,7 @@
     bool bindGeneratesResource;
     bool clientArraysEnabled;
     bool robustAccess;
+    bool pbuffer;
     bool mutableRenderBuffer;
     EGLint samples;
     bool contextProgramCacheEnabled;