• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1//
2// Copyright 2015 The ANGLE Project Authors. All rights reserved.
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5//
6
7// DisplayCGL.mm: CGL implementation of egl::Display
8
9#import "libANGLE/renderer/gl/cgl/DisplayCGL.h"
10
11#import <Cocoa/Cocoa.h>
12#import <EGL/eglext.h>
13#import <dlfcn.h>
14
15#import "common/debug.h"
16#import "common/gl/cgl/FunctionsCGL.h"
17#import "common/system_utils.h"
18#import "gpu_info_util/SystemInfo_internal.h"
19#import "libANGLE/Display.h"
20#import "libANGLE/Error.h"
21#import "libANGLE/renderer/gl/RendererGL.h"
22#import "libANGLE/renderer/gl/cgl/ContextCGL.h"
23#import "libANGLE/renderer/gl/cgl/DeviceCGL.h"
24#import "libANGLE/renderer/gl/cgl/IOSurfaceSurfaceCGL.h"
25#import "libANGLE/renderer/gl/cgl/PbufferSurfaceCGL.h"
26#import "libANGLE/renderer/gl/cgl/WindowSurfaceCGL.h"
27#import "platform/PlatformMethods.h"
28
29namespace
30{
31
32const char *kDefaultOpenGLDylibName =
33    "/System/Library/Frameworks/OpenGL.framework/Libraries/libGL.dylib";
34const char *kFallbackOpenGLDylibName = "GL";
35
36}  // namespace
37
38namespace rx
39{
40
41namespace
42{
43
44// Global IOKit I/O registryID that can match a GPU across process boundaries.
45using IORegistryGPUID = uint64_t;
46
47// Code from WebKit to set an OpenGL context to use a particular GPU by ID.
48// https://trac.webkit.org/browser/webkit/trunk/Source/WebCore/platform/graphics/cocoa/GraphicsContextGLOpenGLCocoa.mm
49// Used with permission.
50static std::optional<GLint> GetVirtualScreenByRegistryID(CGLPixelFormatObj pixelFormatObj,
51                                                         IORegistryGPUID gpuID)
52{
53    if (@available(macOS 10.13, *))
54    {
55        // When a process does not have access to the WindowServer (as with Chromium's GPU process
56        // and WebKit's WebProcess), there is no way for OpenGL to tell which GPU is connected to a
57        // display. On 10.13+, find the virtual screen that corresponds to the preferred GPU by its
58        // registryID. CGLSetVirtualScreen can then be used to tell OpenGL which GPU it should be
59        // using.
60
61        GLint virtualScreenCount = 0;
62        CGLError error = CGLDescribePixelFormat(pixelFormatObj, 0, kCGLPFAVirtualScreenCount,
63                                                &virtualScreenCount);
64        if (error != kCGLNoError)
65        {
66            NOTREACHED();
67            return std::nullopt;
68        }
69
70        for (GLint virtualScreen = 0; virtualScreen < virtualScreenCount; ++virtualScreen)
71        {
72            GLint displayMask = 0;
73            error = CGLDescribePixelFormat(pixelFormatObj, virtualScreen, kCGLPFADisplayMask,
74                                           &displayMask);
75            if (error != kCGLNoError)
76            {
77                NOTREACHED();
78                return std::nullopt;
79            }
80
81            auto virtualScreenGPUID = angle::GetGpuIDFromOpenGLDisplayMask(displayMask);
82            if (virtualScreenGPUID == gpuID)
83            {
84                return virtualScreen;
85            }
86        }
87    }
88    return std::nullopt;
89}
90
91static std::optional<GLint> GetFirstAcceleratedVirtualScreen(CGLPixelFormatObj pixelFormatObj)
92{
93    if (@available(macOS 10.13, *))
94    {
95        GLint virtualScreenCount = 0;
96        CGLError error = CGLDescribePixelFormat(pixelFormatObj, 0, kCGLPFAVirtualScreenCount,
97                                                &virtualScreenCount);
98        if (error != kCGLNoError)
99        {
100            NOTREACHED();
101            return std::nullopt;
102        }
103        for (GLint virtualScreen = 0; virtualScreen < virtualScreenCount; ++virtualScreen)
104        {
105            GLint isAccelerated = 0;
106            error = CGLDescribePixelFormat(pixelFormatObj, virtualScreen, kCGLPFAAccelerated,
107                                           &isAccelerated);
108            if (error != kCGLNoError)
109            {
110                NOTREACHED();
111                return std::nullopt;
112            }
113            if (isAccelerated)
114            {
115                return virtualScreen;
116            }
117        }
118    }
119    return std::nullopt;
120}
121
122}  // anonymous namespace
123
124EnsureCGLContextIsCurrent::EnsureCGLContextIsCurrent(CGLContextObj context)
125    : mOldContext(CGLGetCurrentContext()), mResetContext(mOldContext != context)
126{
127    if (mResetContext)
128    {
129        CGLSetCurrentContext(context);
130    }
131}
132
133EnsureCGLContextIsCurrent::~EnsureCGLContextIsCurrent()
134{
135    if (mResetContext)
136    {
137        CGLSetCurrentContext(mOldContext);
138    }
139}
140
141class FunctionsGLCGL : public FunctionsGL
142{
143  public:
144    FunctionsGLCGL(void *dylibHandle) : mDylibHandle(dylibHandle) {}
145
146    ~FunctionsGLCGL() override { dlclose(mDylibHandle); }
147
148  private:
149    void *loadProcAddress(const std::string &function) const override
150    {
151        return dlsym(mDylibHandle, function.c_str());
152    }
153
154    void *mDylibHandle;
155};
156
157DisplayCGL::DisplayCGL(const egl::DisplayState &state)
158    : DisplayGL(state),
159      mEGLDisplay(nullptr),
160      mContext(nullptr),
161      mThreadsWithCurrentContext(),
162      mPixelFormat(nullptr),
163      mSupportsGPUSwitching(false),
164      mCurrentGPUID(0),
165      mDiscreteGPUPixelFormat(nullptr),
166      mDiscreteGPURefs(0),
167      mLastDiscreteGPUUnrefTime(0.0)
168{}
169
170DisplayCGL::~DisplayCGL() {}
171
172egl::Error DisplayCGL::initialize(egl::Display *display)
173{
174    mEGLDisplay = display;
175
176    angle::SystemInfo info;
177    // It's legal for GetSystemInfo to return false and thereby
178    // contain incomplete information.
179    (void)angle::GetSystemInfo(&info);
180
181    // This code implements the effect of the
182    // disableGPUSwitchingSupport workaround in FeaturesGL.
183    mSupportsGPUSwitching = info.isMacSwitchable && !info.hasNVIDIAGPU();
184
185    {
186        // TODO(cwallez) investigate which pixel format we want
187        std::vector<CGLPixelFormatAttribute> attribs;
188        attribs.push_back(kCGLPFAOpenGLProfile);
189        attribs.push_back(static_cast<CGLPixelFormatAttribute>(kCGLOGLPVersion_3_2_Core));
190        attribs.push_back(kCGLPFAAllowOfflineRenderers);
191        attribs.push_back(static_cast<CGLPixelFormatAttribute>(0));
192        GLint nVirtualScreens = 0;
193        CGLChoosePixelFormat(attribs.data(), &mPixelFormat, &nVirtualScreens);
194
195        if (mPixelFormat == nullptr)
196        {
197            return egl::EglNotInitialized() << "Could not create the context's pixel format.";
198        }
199    }
200
201    CGLCreateContext(mPixelFormat, nullptr, &mContext);
202    if (mContext == nullptr)
203    {
204        return egl::EglNotInitialized() << "Could not create the CGL context.";
205    }
206
207    if (mSupportsGPUSwitching)
208    {
209        auto gpuIndex = info.getPreferredGPUIndex();
210        if (gpuIndex)
211        {
212            auto gpuID         = info.gpus[*gpuIndex].systemDeviceId;
213            auto virtualScreen = GetVirtualScreenByRegistryID(mPixelFormat, gpuID);
214            if (virtualScreen)
215            {
216                CGLError error = CGLSetVirtualScreen(mContext, *virtualScreen);
217                ASSERT(error == kCGLNoError);
218                if (error == kCGLNoError)
219                {
220                    mCurrentGPUID = gpuID;
221                }
222            }
223        }
224        if (mCurrentGPUID == 0)
225        {
226            // Determine the currently active GPU on the system.
227            mCurrentGPUID = angle::GetGpuIDFromDisplayID(kCGDirectMainDisplay);
228        }
229    }
230
231    if (CGLSetCurrentContext(mContext) != kCGLNoError)
232    {
233        return egl::EglNotInitialized() << "Could not make the CGL context current.";
234    }
235    mThreadsWithCurrentContext.insert(angle::GetCurrentThreadUniqueId());
236
237    // There is no equivalent getProcAddress in CGL so we open the dylib directly
238    void *handle = dlopen(kDefaultOpenGLDylibName, RTLD_NOW);
239    if (!handle)
240    {
241        handle = dlopen(kFallbackOpenGLDylibName, RTLD_NOW);
242    }
243    if (!handle)
244    {
245        return egl::EglNotInitialized() << "Could not open the OpenGL Framework.";
246    }
247
248    std::unique_ptr<FunctionsGL> functionsGL(new FunctionsGLCGL(handle));
249    functionsGL->initialize(display->getAttributeMap());
250
251    mRenderer.reset(new RendererGL(std::move(functionsGL), display->getAttributeMap(), this));
252
253    const gl::Version &maxVersion = mRenderer->getMaxSupportedESVersion();
254    if (maxVersion < gl::Version(2, 0))
255    {
256        return egl::EglNotInitialized() << "OpenGL ES 2.0 is not supportable.";
257    }
258
259    auto &attributes = display->getAttributeMap();
260    mDeviceContextIsVolatile =
261        attributes.get(EGL_PLATFORM_ANGLE_DEVICE_CONTEXT_VOLATILE_CGL_ANGLE, GL_FALSE);
262
263    return DisplayGL::initialize(display);
264}
265
266void DisplayCGL::terminate()
267{
268    DisplayGL::terminate();
269
270    mRenderer.reset();
271    if (mPixelFormat != nullptr)
272    {
273        CGLDestroyPixelFormat(mPixelFormat);
274        mPixelFormat = nullptr;
275    }
276    if (mContext != nullptr)
277    {
278        CGLSetCurrentContext(nullptr);
279        CGLDestroyContext(mContext);
280        mContext = nullptr;
281        mThreadsWithCurrentContext.clear();
282    }
283    if (mDiscreteGPUPixelFormat != nullptr)
284    {
285        CGLDestroyPixelFormat(mDiscreteGPUPixelFormat);
286        mDiscreteGPUPixelFormat   = nullptr;
287        mLastDiscreteGPUUnrefTime = 0.0;
288    }
289}
290
291egl::Error DisplayCGL::prepareForCall()
292{
293    if (!mContext)
294    {
295        return egl::EglNotInitialized() << "Context not allocated.";
296    }
297    auto threadId = angle::GetCurrentThreadUniqueId();
298    if (mDeviceContextIsVolatile ||
299        mThreadsWithCurrentContext.find(threadId) == mThreadsWithCurrentContext.end())
300    {
301        if (CGLSetCurrentContext(mContext) != kCGLNoError)
302        {
303            return egl::EglBadAlloc() << "Could not make device CGL context current.";
304        }
305        mThreadsWithCurrentContext.insert(threadId);
306    }
307    return egl::NoError();
308}
309
310egl::Error DisplayCGL::releaseThread()
311{
312    ASSERT(mContext);
313    auto threadId = angle::GetCurrentThreadUniqueId();
314    if (mThreadsWithCurrentContext.find(threadId) != mThreadsWithCurrentContext.end())
315    {
316        if (CGLSetCurrentContext(nullptr) != kCGLNoError)
317        {
318            return egl::EglBadAlloc() << "Could not release device CGL context.";
319        }
320        mThreadsWithCurrentContext.erase(threadId);
321    }
322    return egl::NoError();
323}
324
325egl::Error DisplayCGL::makeCurrent(egl::Display *display,
326                                   egl::Surface *drawSurface,
327                                   egl::Surface *readSurface,
328                                   gl::Context *context)
329{
330    checkDiscreteGPUStatus();
331    return DisplayGL::makeCurrent(display, drawSurface, readSurface, context);
332}
333
334SurfaceImpl *DisplayCGL::createWindowSurface(const egl::SurfaceState &state,
335                                             EGLNativeWindowType window,
336                                             const egl::AttributeMap &attribs)
337{
338    return new WindowSurfaceCGL(state, mRenderer.get(), window, mContext);
339}
340
341SurfaceImpl *DisplayCGL::createPbufferSurface(const egl::SurfaceState &state,
342                                              const egl::AttributeMap &attribs)
343{
344    EGLint width  = static_cast<EGLint>(attribs.get(EGL_WIDTH, 0));
345    EGLint height = static_cast<EGLint>(attribs.get(EGL_HEIGHT, 0));
346    return new PbufferSurfaceCGL(state, mRenderer.get(), width, height);
347}
348
349SurfaceImpl *DisplayCGL::createPbufferFromClientBuffer(const egl::SurfaceState &state,
350                                                       EGLenum buftype,
351                                                       EGLClientBuffer clientBuffer,
352                                                       const egl::AttributeMap &attribs)
353{
354    ASSERT(buftype == EGL_IOSURFACE_ANGLE);
355
356    return new IOSurfaceSurfaceCGL(state, getRenderer(), mContext, clientBuffer, attribs);
357}
358
359SurfaceImpl *DisplayCGL::createPixmapSurface(const egl::SurfaceState &state,
360                                             NativePixmapType nativePixmap,
361                                             const egl::AttributeMap &attribs)
362{
363    UNIMPLEMENTED();
364    return nullptr;
365}
366
367ContextImpl *DisplayCGL::createContext(const gl::State &state,
368                                       gl::ErrorSet *errorSet,
369                                       const egl::Config *configuration,
370                                       const gl::Context *shareContext,
371                                       const egl::AttributeMap &attribs)
372{
373    bool usesDiscreteGPU = false;
374
375    if (attribs.get(EGL_POWER_PREFERENCE_ANGLE, EGL_LOW_POWER_ANGLE) == EGL_HIGH_POWER_ANGLE)
376    {
377        // Should have been rejected by validation if not supported.
378        ASSERT(mSupportsGPUSwitching);
379        usesDiscreteGPU = true;
380    }
381
382    return new ContextCGL(this, state, errorSet, mRenderer, usesDiscreteGPU);
383}
384
385DeviceImpl *DisplayCGL::createDevice()
386{
387    return new DeviceCGL();
388}
389
390egl::ConfigSet DisplayCGL::generateConfigs()
391{
392    // TODO(cwallez): generate more config permutations
393    egl::ConfigSet configs;
394
395    const gl::Version &maxVersion = getMaxSupportedESVersion();
396    ASSERT(maxVersion >= gl::Version(2, 0));
397    bool supportsES3 = maxVersion >= gl::Version(3, 0);
398
399    egl::Config config;
400
401    // Native stuff
402    config.nativeVisualID   = 0;
403    config.nativeVisualType = 0;
404    config.nativeRenderable = EGL_TRUE;
405
406    // Buffer sizes
407    config.redSize     = 8;
408    config.greenSize   = 8;
409    config.blueSize    = 8;
410    config.alphaSize   = 8;
411    config.depthSize   = 24;
412    config.stencilSize = 8;
413
414    config.colorBufferType = EGL_RGB_BUFFER;
415    config.luminanceSize   = 0;
416    config.alphaMaskSize   = 0;
417
418    config.bufferSize = config.redSize + config.greenSize + config.blueSize + config.alphaSize;
419
420    config.transparentType = EGL_NONE;
421
422    // Pbuffer
423    config.maxPBufferWidth  = 4096;
424    config.maxPBufferHeight = 4096;
425    config.maxPBufferPixels = 4096 * 4096;
426
427    // Caveat
428    config.configCaveat = EGL_NONE;
429
430    // Misc
431    config.sampleBuffers     = 0;
432    config.samples           = 0;
433    config.level             = 0;
434    config.bindToTextureRGB  = EGL_FALSE;
435    config.bindToTextureRGBA = EGL_FALSE;
436
437    config.bindToTextureTarget = EGL_TEXTURE_RECTANGLE_ANGLE;
438
439    config.surfaceType = EGL_WINDOW_BIT | EGL_PBUFFER_BIT;
440
441    config.minSwapInterval = 1;
442    config.maxSwapInterval = 1;
443
444    config.renderTargetFormat = GL_RGBA8;
445    config.depthStencilFormat = GL_DEPTH24_STENCIL8;
446
447    config.conformant     = EGL_OPENGL_ES2_BIT | (supportsES3 ? EGL_OPENGL_ES3_BIT_KHR : 0);
448    config.renderableType = config.conformant;
449
450    config.matchNativePixmap = EGL_NONE;
451
452    config.colorComponentType = EGL_COLOR_COMPONENT_TYPE_FIXED_EXT;
453
454    configs.add(config);
455    return configs;
456}
457
458bool DisplayCGL::testDeviceLost()
459{
460    // TODO(cwallez) investigate implementing this
461    return false;
462}
463
464egl::Error DisplayCGL::restoreLostDevice(const egl::Display *display)
465{
466    UNIMPLEMENTED();
467    return egl::EglBadDisplay();
468}
469
470bool DisplayCGL::isValidNativeWindow(EGLNativeWindowType window) const
471{
472    NSObject *layer = reinterpret_cast<NSObject *>(window);
473    return [layer isKindOfClass:[CALayer class]];
474}
475
476egl::Error DisplayCGL::validateClientBuffer(const egl::Config *configuration,
477                                            EGLenum buftype,
478                                            EGLClientBuffer clientBuffer,
479                                            const egl::AttributeMap &attribs) const
480{
481    ASSERT(buftype == EGL_IOSURFACE_ANGLE);
482
483    if (!IOSurfaceSurfaceCGL::validateAttributes(clientBuffer, attribs))
484    {
485        return egl::EglBadAttribute();
486    }
487
488    return egl::NoError();
489}
490
491CGLContextObj DisplayCGL::getCGLContext() const
492{
493    return mContext;
494}
495
496CGLPixelFormatObj DisplayCGL::getCGLPixelFormat() const
497{
498    return mPixelFormat;
499}
500
501void DisplayCGL::generateExtensions(egl::DisplayExtensions *outExtensions) const
502{
503    outExtensions->iosurfaceClientBuffer  = true;
504    outExtensions->surfacelessContext     = true;
505    outExtensions->waitUntilWorkScheduled = true;
506
507    // Contexts are virtualized so textures and semaphores can be shared globally
508    outExtensions->displayTextureShareGroup   = true;
509    outExtensions->displaySemaphoreShareGroup = true;
510
511    if (mSupportsGPUSwitching)
512    {
513        outExtensions->powerPreference = true;
514    }
515
516    DisplayGL::generateExtensions(outExtensions);
517}
518
519void DisplayCGL::generateCaps(egl::Caps *outCaps) const
520{
521    outCaps->textureNPOT = true;
522}
523
524egl::Error DisplayCGL::waitClient(const gl::Context *context)
525{
526    // TODO(cwallez) UNIMPLEMENTED()
527    return egl::NoError();
528}
529
530egl::Error DisplayCGL::waitNative(const gl::Context *context, EGLint engine)
531{
532    // TODO(cwallez) UNIMPLEMENTED()
533    return egl::NoError();
534}
535
536egl::Error DisplayCGL::waitUntilWorkScheduled()
537{
538    for (auto context : mState.contextMap)
539    {
540        context.second->flush();
541    }
542    return egl::NoError();
543}
544
545gl::Version DisplayCGL::getMaxSupportedESVersion() const
546{
547    return mRenderer->getMaxSupportedESVersion();
548}
549
550egl::Error DisplayCGL::makeCurrentSurfaceless(gl::Context *context)
551{
552    // We have nothing to do as mContext is always current, and that CGL is surfaceless by
553    // default.
554    return egl::NoError();
555}
556
557void DisplayCGL::initializeFrontendFeatures(angle::FrontendFeatures *features) const
558{
559    mRenderer->initializeFrontendFeatures(features);
560}
561
562void DisplayCGL::populateFeatureList(angle::FeatureList *features)
563{
564    mRenderer->getFeatures().populateFeatureList(features);
565}
566
567RendererGL *DisplayCGL::getRenderer() const
568{
569    return mRenderer.get();
570}
571
572egl::Error DisplayCGL::referenceDiscreteGPU()
573{
574    // Should have been rejected by validation if not supported.
575    ASSERT(mSupportsGPUSwitching);
576    // Create discrete pixel format if necessary.
577    if (mDiscreteGPUPixelFormat)
578    {
579        // Clear this out if necessary.
580        mLastDiscreteGPUUnrefTime = 0.0;
581    }
582    else
583    {
584        ASSERT(mLastDiscreteGPUUnrefTime == 0.0);
585        CGLPixelFormatAttribute discreteAttribs[] = {static_cast<CGLPixelFormatAttribute>(0)};
586        GLint numPixelFormats                     = 0;
587        if (CGLChoosePixelFormat(discreteAttribs, &mDiscreteGPUPixelFormat, &numPixelFormats) !=
588            kCGLNoError)
589        {
590            return egl::EglBadAlloc() << "Error choosing discrete pixel format.";
591        }
592    }
593    ++mDiscreteGPURefs;
594
595    return egl::NoError();
596}
597
598egl::Error DisplayCGL::unreferenceDiscreteGPU()
599{
600    // Should have been rejected by validation if not supported.
601    ASSERT(mSupportsGPUSwitching);
602    ASSERT(mDiscreteGPURefs > 0);
603    if (--mDiscreteGPURefs == 0)
604    {
605        auto *platform            = ANGLEPlatformCurrent();
606        mLastDiscreteGPUUnrefTime = platform->monotonicallyIncreasingTime(platform);
607    }
608
609    return egl::NoError();
610}
611
612void DisplayCGL::checkDiscreteGPUStatus()
613{
614    const double kDiscreteGPUTimeoutInSeconds = 10.0;
615
616    if (mLastDiscreteGPUUnrefTime != 0.0)
617    {
618        ASSERT(mSupportsGPUSwitching);
619        // A non-zero value implies that the timer is ticking on deleting the discrete GPU pixel
620        // format.
621        auto *platform = ANGLEPlatformCurrent();
622        ASSERT(platform);
623        double currentTime = platform->monotonicallyIncreasingTime(platform);
624        if (currentTime > mLastDiscreteGPUUnrefTime + kDiscreteGPUTimeoutInSeconds)
625        {
626            CGLDestroyPixelFormat(mDiscreteGPUPixelFormat);
627            mDiscreteGPUPixelFormat   = nullptr;
628            mLastDiscreteGPUUnrefTime = 0.0;
629        }
630    }
631}
632
633egl::Error DisplayCGL::handleGPUSwitch()
634{
635    if (mSupportsGPUSwitching)
636    {
637        uint64_t gpuID = angle::GetGpuIDFromDisplayID(kCGDirectMainDisplay);
638        if (gpuID != mCurrentGPUID)
639        {
640            auto virtualScreen = GetVirtualScreenByRegistryID(mPixelFormat, gpuID);
641            if (!virtualScreen)
642            {
643                virtualScreen = GetFirstAcceleratedVirtualScreen(mPixelFormat);
644            }
645            if (virtualScreen)
646            {
647                setContextToGPU(gpuID, *virtualScreen);
648            }
649        }
650    }
651
652    return egl::NoError();
653}
654
655egl::Error DisplayCGL::forceGPUSwitch(EGLint gpuIDHigh, EGLint gpuIDLow)
656{
657    if (mSupportsGPUSwitching)
658    {
659        uint64_t gpuID = static_cast<uint64_t>(static_cast<uint32_t>(gpuIDHigh)) << 32 |
660                         static_cast<uint32_t>(gpuIDLow);
661        if (gpuID != mCurrentGPUID)
662        {
663            auto virtualScreen = GetVirtualScreenByRegistryID(mPixelFormat, gpuID);
664            if (virtualScreen)
665            {
666                setContextToGPU(gpuID, *virtualScreen);
667            }
668        }
669    }
670    return egl::NoError();
671}
672
673void DisplayCGL::setContextToGPU(uint64_t gpuID, GLint virtualScreen)
674{
675    CGLError error = CGLSetVirtualScreen(mContext, virtualScreen);
676    ASSERT(error == kCGLNoError);
677    if (error == kCGLNoError)
678    {
679        // Performing the above operation seems to need a call to CGLSetCurrentContext to make
680        // the context work properly again. Failing to do this returns null strings for
681        // GL_VENDOR and GL_RENDERER.
682        CGLUpdateContext(mContext);
683        CGLSetCurrentContext(mContext);
684        onStateChange(angle::SubjectMessage::SubjectChanged);
685        mCurrentGPUID = gpuID;
686        mRenderer->handleGPUSwitch();
687    }
688}
689
690}  // namespace rx
691