xref: /aosp_15_r20/external/angle/src/libANGLE/renderer/gl/wgl/DXGISwapChainWindowSurfaceWGL.cpp (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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 // DXGISwapChainWindowSurfaceWGL.cpp: WGL implementation of egl::Surface for windows using a DXGI
8 // swapchain.
9 
10 #include "libANGLE/renderer/gl/wgl/DXGISwapChainWindowSurfaceWGL.h"
11 
12 #include "libANGLE/formatutils.h"
13 #include "libANGLE/renderer/gl/FramebufferGL.h"
14 #include "libANGLE/renderer/gl/RendererGL.h"
15 #include "libANGLE/renderer/gl/StateManagerGL.h"
16 #include "libANGLE/renderer/gl/TextureGL.h"
17 #include "libANGLE/renderer/gl/wgl/DisplayWGL.h"
18 #include "libANGLE/renderer/gl/wgl/FunctionsWGL.h"
19 
20 #include <EGL/eglext.h>
21 
22 namespace rx
23 {
24 
DXGISwapChainWindowSurfaceWGL(const egl::SurfaceState & state,StateManagerGL * stateManager,EGLNativeWindowType window,ID3D11Device * device,HANDLE deviceHandle,HDC deviceContext,const FunctionsGL * functionsGL,const FunctionsWGL * functionsWGL,EGLint orientation)25 DXGISwapChainWindowSurfaceWGL::DXGISwapChainWindowSurfaceWGL(const egl::SurfaceState &state,
26                                                              StateManagerGL *stateManager,
27                                                              EGLNativeWindowType window,
28                                                              ID3D11Device *device,
29                                                              HANDLE deviceHandle,
30                                                              HDC deviceContext,
31                                                              const FunctionsGL *functionsGL,
32                                                              const FunctionsWGL *functionsWGL,
33                                                              EGLint orientation)
34     : SurfaceWGL(state),
35       mWindow(window),
36       mStateManager(stateManager),
37       mFunctionsGL(functionsGL),
38       mFunctionsWGL(functionsWGL),
39       mDevice(device),
40       mDeviceHandle(deviceHandle),
41       mWGLDevice(deviceContext),
42       mSwapChainFormat(DXGI_FORMAT_UNKNOWN),
43       mSwapChainFlags(0),
44       mDepthBufferFormat(GL_NONE),
45       mFirstSwap(true),
46       mSwapChain(nullptr),
47       mSwapChain1(nullptr),
48       mFramebufferID(0),
49       mColorRenderbufferID(0),
50       mRenderbufferBufferHandle(nullptr),
51       mDepthRenderbufferID(0),
52       mTextureID(0),
53       mTextureHandle(nullptr),
54       mWidth(0),
55       mHeight(0),
56       mSwapInterval(1),
57       mOrientation(orientation)
58 {}
59 
~DXGISwapChainWindowSurfaceWGL()60 DXGISwapChainWindowSurfaceWGL::~DXGISwapChainWindowSurfaceWGL()
61 {
62     if (mRenderbufferBufferHandle != nullptr)
63     {
64         mFunctionsWGL->dxUnlockObjectsNV(mDeviceHandle, 1, &mRenderbufferBufferHandle);
65         mFunctionsWGL->dxUnregisterObjectNV(mDeviceHandle, mRenderbufferBufferHandle);
66     }
67 
68     if (mFramebufferID != 0)
69     {
70         mStateManager->deleteFramebuffer(mFramebufferID);
71         mFramebufferID = 0;
72     }
73 
74     if (mColorRenderbufferID != 0)
75     {
76         mStateManager->deleteRenderbuffer(mColorRenderbufferID);
77         mColorRenderbufferID = 0;
78     }
79 
80     if (mDepthRenderbufferID != 0)
81     {
82         mStateManager->deleteRenderbuffer(mDepthRenderbufferID);
83         mDepthRenderbufferID = 0;
84     }
85 
86     SafeRelease(mSwapChain);
87     SafeRelease(mSwapChain1);
88 }
89 
initialize(const egl::Display * display)90 egl::Error DXGISwapChainWindowSurfaceWGL::initialize(const egl::Display *display)
91 {
92     if (mOrientation != EGL_SURFACE_ORIENTATION_INVERT_Y_ANGLE)
93     {
94         // TODO(geofflang): Support the orientation extensions fully.  Currently only inverting Y is
95         // supported.  To support all orientations, an intermediate framebuffer will be needed with
96         // a blit before swap.
97         return egl::EglBadAttribute() << "DXGISwapChainWindowSurfaceWGL requires an orientation of "
98                                          "EGL_SURFACE_ORIENTATION_INVERT_Y_ANGLE.";
99     }
100 
101     RECT rect;
102     if (!GetClientRect(mWindow, &rect))
103     {
104         return egl::EglBadNativeWindow() << "Failed to query the window size.";
105     }
106     mWidth  = rect.right - rect.left;
107     mHeight = rect.bottom - rect.top;
108 
109     mSwapChainFormat   = DXGI_FORMAT_R8G8B8A8_UNORM;
110     mSwapChainFlags    = 0;
111     mDepthBufferFormat = GL_DEPTH24_STENCIL8;
112 
113     mFunctionsGL->genRenderbuffers(1, &mColorRenderbufferID);
114     mStateManager->bindRenderbuffer(GL_RENDERBUFFER, mColorRenderbufferID);
115 
116     mFunctionsGL->genRenderbuffers(1, &mDepthRenderbufferID);
117     mStateManager->bindRenderbuffer(GL_RENDERBUFFER, mDepthRenderbufferID);
118 
119     return createSwapChain();
120 }
121 
makeCurrent(const gl::Context * context)122 egl::Error DXGISwapChainWindowSurfaceWGL::makeCurrent(const gl::Context *context)
123 {
124     return egl::NoError();
125 }
126 
swap(const gl::Context * context)127 egl::Error DXGISwapChainWindowSurfaceWGL::swap(const gl::Context *context)
128 {
129     mFunctionsGL->flush();
130 
131     ANGLE_TRY(setObjectsLocked(false));
132 
133     HRESULT result = mSwapChain->Present(mSwapInterval, 0);
134     mFirstSwap     = false;
135 
136     ANGLE_TRY(setObjectsLocked(true));
137 
138     if (FAILED(result))
139     {
140         return egl::EglBadAlloc() << "Failed to present swap chain, " << gl::FmtHR(result);
141     }
142 
143     return checkForResize();
144 }
145 
postSubBuffer(const gl::Context * context,EGLint x,EGLint y,EGLint width,EGLint height)146 egl::Error DXGISwapChainWindowSurfaceWGL::postSubBuffer(const gl::Context *context,
147                                                         EGLint x,
148                                                         EGLint y,
149                                                         EGLint width,
150                                                         EGLint height)
151 {
152     ASSERT(width > 0 && height > 0);
153     ASSERT(mSwapChain1 != nullptr);
154 
155     mFunctionsGL->flush();
156 
157     ANGLE_TRY(setObjectsLocked(false));
158 
159     HRESULT result = S_OK;
160     if (mFirstSwap)
161     {
162         result     = mSwapChain1->Present(mSwapInterval, 0);
163         mFirstSwap = false;
164     }
165     else
166     {
167         RECT rect = {static_cast<LONG>(x), static_cast<LONG>(mHeight - y - height),
168                      static_cast<LONG>(x + width), static_cast<LONG>(mHeight - y)};
169         DXGI_PRESENT_PARAMETERS params = {1, &rect, nullptr, nullptr};
170         result                         = mSwapChain1->Present1(mSwapInterval, 0, &params);
171     }
172 
173     ANGLE_TRY(setObjectsLocked(true));
174 
175     if (FAILED(result))
176     {
177         return egl::EglBadAlloc() << "Failed to present swap chain, " << gl::FmtHR(result);
178     }
179 
180     return checkForResize();
181 }
182 
querySurfacePointerANGLE(EGLint attribute,void ** value)183 egl::Error DXGISwapChainWindowSurfaceWGL::querySurfacePointerANGLE(EGLint attribute, void **value)
184 {
185     UNREACHABLE();
186     return egl::NoError();
187 }
188 
bindTexImage(const gl::Context * context,gl::Texture * texture,EGLint buffer)189 egl::Error DXGISwapChainWindowSurfaceWGL::bindTexImage(const gl::Context *context,
190                                                        gl::Texture *texture,
191                                                        EGLint buffer)
192 {
193     ASSERT(mTextureHandle == nullptr);
194 
195     const TextureGL *textureGL = GetImplAs<TextureGL>(texture);
196     GLuint textureID           = textureGL->getTextureID();
197 
198     ID3D11Texture2D *colorBuffer = nullptr;
199     HRESULT result               = mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D),
200                                                          reinterpret_cast<void **>(&colorBuffer));
201     if (FAILED(result))
202     {
203         return egl::EglBadAlloc() << "Failed to query texture from swap chain, "
204                                   << gl::FmtHR(result);
205     }
206 
207     mTextureHandle = mFunctionsWGL->dxRegisterObjectNV(mDeviceHandle, colorBuffer, textureID,
208                                                        GL_TEXTURE_2D, WGL_ACCESS_READ_WRITE_NV);
209     SafeRelease(colorBuffer);
210     if (mTextureHandle == nullptr)
211     {
212         return egl::EglBadAlloc() << "Failed to register D3D object, "
213                                   << gl::FmtErr(HRESULT_CODE(GetLastError()));
214     }
215 
216     if (!mFunctionsWGL->dxLockObjectsNV(mDeviceHandle, 1, &mTextureHandle))
217     {
218         mFunctionsWGL->dxUnregisterObjectNV(mDeviceHandle, mTextureHandle);
219         mTextureHandle = nullptr;
220 
221         return egl::EglBadAlloc() << "Failed to lock D3D object, "
222                                   << gl::FmtErr(HRESULT_CODE(GetLastError()));
223     }
224 
225     mTextureID = textureID;
226 
227     return egl::NoError();
228 }
229 
releaseTexImage(const gl::Context * context,EGLint buffer)230 egl::Error DXGISwapChainWindowSurfaceWGL::releaseTexImage(const gl::Context *context, EGLint buffer)
231 {
232     ASSERT(mTextureHandle != nullptr);
233 
234     if (!mFunctionsWGL->dxUnlockObjectsNV(mDeviceHandle, 1, &mTextureHandle))
235     {
236         return egl::EglBadAlloc() << "Failed to unlock D3D object, "
237                                   << gl::FmtErr(HRESULT_CODE(GetLastError()));
238     }
239 
240     if (!mFunctionsWGL->dxUnregisterObjectNV(mDeviceHandle, mTextureHandle))
241     {
242         return egl::EglBadAlloc() << "Failed to unregister D3D object, "
243                                   << gl::FmtErr(HRESULT_CODE(GetLastError()));
244     }
245 
246     mTextureID     = 0;
247     mTextureHandle = nullptr;
248 
249     return egl::NoError();
250 }
251 
setSwapInterval(const egl::Display * display,EGLint interval)252 void DXGISwapChainWindowSurfaceWGL::setSwapInterval(const egl::Display *display, EGLint interval)
253 {
254     mSwapInterval = interval;
255 }
256 
getWidth() const257 EGLint DXGISwapChainWindowSurfaceWGL::getWidth() const
258 {
259     return static_cast<EGLint>(mWidth);
260 }
261 
getHeight() const262 EGLint DXGISwapChainWindowSurfaceWGL::getHeight() const
263 {
264     return static_cast<EGLint>(mHeight);
265 }
266 
isPostSubBufferSupported() const267 EGLint DXGISwapChainWindowSurfaceWGL::isPostSubBufferSupported() const
268 {
269     return mSwapChain1 != nullptr;
270 }
271 
getSwapBehavior() const272 EGLint DXGISwapChainWindowSurfaceWGL::getSwapBehavior() const
273 {
274     return EGL_BUFFER_DESTROYED;
275 }
276 
getDC() const277 HDC DXGISwapChainWindowSurfaceWGL::getDC() const
278 {
279     return mWGLDevice;
280 }
281 
attachToFramebuffer(const gl::Context * context,gl::Framebuffer * framebuffer)282 egl::Error DXGISwapChainWindowSurfaceWGL::attachToFramebuffer(const gl::Context *context,
283                                                               gl::Framebuffer *framebuffer)
284 {
285     FramebufferGL *framebufferGL = GetImplAs<FramebufferGL>(framebuffer);
286     ASSERT(framebufferGL->getFramebufferID() == 0);
287 
288     if (mFramebufferID == 0)
289     {
290         GLuint framebufferID = 0;
291         mFunctionsGL->genFramebuffers(1, &framebufferID);
292         mStateManager->bindFramebuffer(GL_FRAMEBUFFER, framebufferID);
293         mFunctionsGL->framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
294                                               mColorRenderbufferID);
295 
296         if (mDepthBufferFormat != GL_NONE)
297         {
298             const gl::InternalFormat &depthStencilFormatInfo =
299                 gl::GetSizedInternalFormatInfo(mDepthBufferFormat);
300             if (depthStencilFormatInfo.depthBits > 0)
301             {
302                 mFunctionsGL->framebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
303                                                       GL_RENDERBUFFER, mDepthRenderbufferID);
304             }
305             if (depthStencilFormatInfo.stencilBits > 0)
306             {
307                 mFunctionsGL->framebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT,
308                                                       GL_RENDERBUFFER, mDepthRenderbufferID);
309             }
310         }
311 
312         mFramebufferID = framebufferID;
313     }
314     framebufferGL->setFramebufferID(mFramebufferID);
315     return egl::NoError();
316 }
317 
detachFromFramebuffer(const gl::Context * context,gl::Framebuffer * framebuffer)318 egl::Error DXGISwapChainWindowSurfaceWGL::detachFromFramebuffer(const gl::Context *context,
319                                                                 gl::Framebuffer *framebuffer)
320 {
321     FramebufferGL *framebufferGL = GetImplAs<FramebufferGL>(framebuffer);
322     ASSERT(framebufferGL->getFramebufferID() == mFramebufferID);
323     framebufferGL->setFramebufferID(0);
324     return egl::NoError();
325 }
326 
setObjectsLocked(bool locked)327 egl::Error DXGISwapChainWindowSurfaceWGL::setObjectsLocked(bool locked)
328 {
329     if (mRenderbufferBufferHandle == nullptr)
330     {
331         ASSERT(mTextureHandle == nullptr);
332         return egl::NoError();
333     }
334 
335     HANDLE resources[] = {
336         mRenderbufferBufferHandle,
337         mTextureHandle,
338     };
339     GLint count = (mTextureHandle != nullptr) ? 2 : 1;
340 
341     if (locked)
342     {
343         if (!mFunctionsWGL->dxLockObjectsNV(mDeviceHandle, count, resources))
344         {
345             return egl::EglBadAlloc()
346                    << "Failed to lock object, " << gl::FmtErr(HRESULT_CODE(GetLastError()));
347         }
348     }
349     else
350     {
351         if (!mFunctionsWGL->dxUnlockObjectsNV(mDeviceHandle, count, resources))
352         {
353             return egl::EglBadAlloc()
354                    << "Failed to lock object, " << gl::FmtErr(HRESULT_CODE(GetLastError()));
355         }
356     }
357 
358     return egl::NoError();
359 }
360 
checkForResize()361 egl::Error DXGISwapChainWindowSurfaceWGL::checkForResize()
362 {
363     RECT rect;
364     if (!GetClientRect(mWindow, &rect))
365     {
366         return egl::EglBadNativeWindow() << "Failed to query the window size.";
367     }
368 
369     size_t newWidth  = rect.right - rect.left;
370     size_t newHeight = rect.bottom - rect.top;
371     if (newWidth != mWidth || newHeight != mHeight)
372     {
373         mWidth  = newWidth;
374         mHeight = newHeight;
375 
376         // TODO(geofflang): Handle resize by resizing the swap chain instead of re-creating it.
377         egl::Error error = createSwapChain();
378         if (error.isError())
379         {
380             return error;
381         }
382     }
383 
384     return egl::NoError();
385 }
386 
GetDXGIFactoryFromDevice(ID3D11Device * device)387 static IDXGIFactory *GetDXGIFactoryFromDevice(ID3D11Device *device)
388 {
389     IDXGIDevice *dxgiDevice = nullptr;
390     HRESULT result =
391         device->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void **>(&dxgiDevice));
392     if (FAILED(result))
393     {
394         return nullptr;
395     }
396 
397     IDXGIAdapter *dxgiAdapter = nullptr;
398     result = dxgiDevice->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void **>(&dxgiAdapter));
399     SafeRelease(dxgiDevice);
400     if (FAILED(result))
401     {
402         return nullptr;
403     }
404 
405     IDXGIFactory *dxgiFactory = nullptr;
406     result =
407         dxgiAdapter->GetParent(__uuidof(IDXGIFactory), reinterpret_cast<void **>(&dxgiFactory));
408     SafeRelease(dxgiAdapter);
409     if (FAILED(result))
410     {
411         return nullptr;
412     }
413 
414     return dxgiFactory;
415 }
416 
createSwapChain()417 egl::Error DXGISwapChainWindowSurfaceWGL::createSwapChain()
418 {
419     egl::Error error = setObjectsLocked(false);
420     if (error.isError())
421     {
422         return error;
423     }
424 
425     if (mRenderbufferBufferHandle)
426     {
427         mFunctionsWGL->dxUnregisterObjectNV(mDeviceHandle, mRenderbufferBufferHandle);
428         mRenderbufferBufferHandle = nullptr;
429     }
430 
431     // If this surface is bound to a texture, unregister it.
432     bool hadBoundSurface = (mTextureHandle != nullptr);
433     if (hadBoundSurface)
434     {
435         mFunctionsWGL->dxUnregisterObjectNV(mDeviceHandle, mTextureHandle);
436         mTextureHandle = nullptr;
437     }
438 
439     IDXGIFactory *dxgiFactory = GetDXGIFactoryFromDevice(mDevice);
440     if (dxgiFactory == nullptr)
441     {
442         return egl::EglBadNativeWindow() << "Failed to query the DXGIFactory.";
443     }
444 
445     IDXGIFactory2 *dxgiFactory2 = nullptr;
446     HRESULT result              = dxgiFactory->QueryInterface(__uuidof(IDXGIFactory2),
447                                                               reinterpret_cast<void **>(&dxgiFactory2));
448     if (SUCCEEDED(result))
449     {
450         ASSERT(dxgiFactory2 != nullptr);
451 
452         DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
453         swapChainDesc.BufferCount           = 1;
454         swapChainDesc.Format                = mSwapChainFormat;
455         swapChainDesc.Width                 = static_cast<UINT>(mWidth);
456         swapChainDesc.Height                = static_cast<UINT>(mHeight);
457         swapChainDesc.Format                = mSwapChainFormat;
458         swapChainDesc.Stereo                = FALSE;
459         swapChainDesc.SampleDesc.Count      = 1;
460         swapChainDesc.SampleDesc.Quality    = 0;
461         swapChainDesc.BufferUsage =
462             DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT | DXGI_USAGE_BACK_BUFFER;
463         swapChainDesc.BufferCount = 1;
464         swapChainDesc.Scaling     = DXGI_SCALING_STRETCH;
465         swapChainDesc.SwapEffect  = DXGI_SWAP_EFFECT_SEQUENTIAL;
466         swapChainDesc.AlphaMode   = DXGI_ALPHA_MODE_UNSPECIFIED;
467         swapChainDesc.Flags       = mSwapChainFlags;
468 
469         result = dxgiFactory2->CreateSwapChainForHwnd(mDevice, mWindow, &swapChainDesc, nullptr,
470                                                       nullptr, &mSwapChain1);
471         SafeRelease(dxgiFactory2);
472         SafeRelease(dxgiFactory);
473         if (FAILED(result))
474         {
475             return egl::EglBadAlloc()
476                    << "Failed to create swap chain for window, " << gl::FmtHR(result);
477         }
478 
479         mSwapChain = mSwapChain1;
480         mSwapChain->AddRef();
481     }
482     else
483     {
484         DXGI_SWAP_CHAIN_DESC swapChainDesc               = {};
485         swapChainDesc.BufferCount                        = 1;
486         swapChainDesc.BufferDesc.Format                  = mSwapChainFormat;
487         swapChainDesc.BufferDesc.Width                   = static_cast<UINT>(mWidth);
488         swapChainDesc.BufferDesc.Height                  = static_cast<UINT>(mHeight);
489         swapChainDesc.BufferDesc.Scaling                 = DXGI_MODE_SCALING_UNSPECIFIED;
490         swapChainDesc.BufferDesc.ScanlineOrdering        = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
491         swapChainDesc.BufferDesc.RefreshRate.Numerator   = 0;
492         swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
493         swapChainDesc.BufferUsage =
494             DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT | DXGI_USAGE_BACK_BUFFER;
495         swapChainDesc.Flags              = mSwapChainFlags;
496         swapChainDesc.OutputWindow       = mWindow;
497         swapChainDesc.SampleDesc.Count   = 1;
498         swapChainDesc.SampleDesc.Quality = 0;
499         swapChainDesc.Windowed           = TRUE;
500         swapChainDesc.SwapEffect         = DXGI_SWAP_EFFECT_DISCARD;
501 
502         result = dxgiFactory->CreateSwapChain(mDevice, &swapChainDesc, &mSwapChain);
503         SafeRelease(dxgiFactory);
504         if (FAILED(result))
505         {
506             return egl::EglBadAlloc()
507                    << "Failed to create swap chain for window, " << gl::FmtHR(result);
508         }
509     }
510 
511     ID3D11Texture2D *colorBuffer = nullptr;
512     result                       = mSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D),
513                                                          reinterpret_cast<void **>(&colorBuffer));
514     if (FAILED(result))
515     {
516         return egl::EglBadAlloc() << "Failed to query texture from swap chain, "
517                                   << gl::FmtHR(result);
518     }
519 
520     mStateManager->bindRenderbuffer(GL_RENDERBUFFER, mColorRenderbufferID);
521     mRenderbufferBufferHandle =
522         mFunctionsWGL->dxRegisterObjectNV(mDeviceHandle, colorBuffer, mColorRenderbufferID,
523                                           GL_RENDERBUFFER, WGL_ACCESS_READ_WRITE_NV);
524     SafeRelease(colorBuffer);
525     if (mRenderbufferBufferHandle == nullptr)
526     {
527         return egl::EglBadAlloc() << "Failed to register D3D object, "
528                                   << gl::FmtErr(HRESULT_CODE(GetLastError()));
529     }
530 
531     // Rebind the surface to the texture if needed.
532     if (hadBoundSurface)
533     {
534         mTextureHandle = mFunctionsWGL->dxRegisterObjectNV(mDeviceHandle, colorBuffer, mTextureID,
535                                                            GL_TEXTURE_2D, WGL_ACCESS_READ_WRITE_NV);
536         if (mTextureHandle == nullptr)
537         {
538             return egl::EglBadAlloc()
539                    << "Failed to register D3D object, " << gl::FmtErr(HRESULT_CODE(GetLastError()));
540         }
541     }
542 
543     error = setObjectsLocked(true);
544     if (error.isError())
545     {
546         return error;
547     }
548 
549     if (mDepthBufferFormat != GL_NONE)
550     {
551         ASSERT(mDepthRenderbufferID != 0);
552         mStateManager->bindRenderbuffer(GL_RENDERBUFFER, mDepthRenderbufferID);
553         mFunctionsGL->renderbufferStorage(GL_RENDERBUFFER, mDepthBufferFormat,
554                                           static_cast<GLsizei>(mWidth),
555                                           static_cast<GLsizei>(mHeight));
556     }
557 
558     mFirstSwap = true;
559 
560     return egl::NoError();
561 }
562 }  // namespace rx
563