2015-05-24 04:55:12 +00:00
|
|
|
// Copyright 2012 Dolphin Emulator Project
|
2021-07-05 01:22:19 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
2012-12-17 21:01:52 +00:00
|
|
|
|
2021-12-10 02:22:16 +00:00
|
|
|
#include "Common/GL/GLInterface/GLX.h"
|
|
|
|
|
2016-01-23 10:21:53 +00:00
|
|
|
#include <array>
|
|
|
|
#include <sstream>
|
2014-03-12 19:33:41 +00:00
|
|
|
|
2015-09-18 17:53:32 +00:00
|
|
|
#include "Common/Logging/Log.h"
|
2012-12-17 21:01:52 +00:00
|
|
|
|
2014-12-20 09:57:34 +00:00
|
|
|
#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
|
|
|
#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
|
|
|
|
|
|
|
|
typedef GLXContext (*PFNGLXCREATECONTEXTATTRIBSPROC)(Display*, GLXFBConfig, GLXContext, Bool,
|
|
|
|
const int*);
|
2022-02-18 18:56:42 +00:00
|
|
|
|
|
|
|
#ifndef GLX_EXT_swap_control
|
2018-03-26 12:09:22 +00:00
|
|
|
typedef void (*PFNGLXSWAPINTERVALEXTPROC)(Display*, GLXDrawable, int);
|
2022-02-18 18:56:42 +00:00
|
|
|
#endif
|
|
|
|
|
2018-03-26 12:09:22 +00:00
|
|
|
typedef int (*PFNGLXSWAPINTERVALMESAPROC)(unsigned int);
|
2014-12-20 09:57:34 +00:00
|
|
|
|
|
|
|
static PFNGLXCREATECONTEXTATTRIBSPROC glXCreateContextAttribs = nullptr;
|
2018-03-26 12:09:22 +00:00
|
|
|
static PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXTPtr = nullptr;
|
|
|
|
static PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESAPtr = nullptr;
|
2013-12-30 13:22:50 +00:00
|
|
|
|
2016-01-23 10:21:53 +00:00
|
|
|
static PFNGLXCREATEGLXPBUFFERSGIXPROC glXCreateGLXPbufferSGIX = nullptr;
|
|
|
|
static PFNGLXDESTROYGLXPBUFFERSGIXPROC glXDestroyGLXPbufferSGIX = nullptr;
|
|
|
|
|
2014-12-20 09:57:34 +00:00
|
|
|
static bool s_glxError;
|
|
|
|
static int ctxErrorHandler(Display* dpy, XErrorEvent* ev)
|
|
|
|
{
|
2015-01-07 20:48:59 +00:00
|
|
|
s_glxError = true;
|
|
|
|
return 0;
|
2014-12-20 09:57:34 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 13:03:33 +00:00
|
|
|
GLContextGLX::~GLContextGLX()
|
|
|
|
{
|
|
|
|
DestroyWindowSurface();
|
|
|
|
if (m_context)
|
|
|
|
{
|
|
|
|
if (glXGetCurrentContext() == m_context)
|
|
|
|
glXMakeCurrent(m_display, None, nullptr);
|
|
|
|
|
|
|
|
glXDestroyContext(m_display, m_context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-03 13:03:30 +00:00
|
|
|
bool GLContextGLX::IsHeadless() const
|
|
|
|
{
|
2018-10-03 13:03:33 +00:00
|
|
|
return !m_render_window;
|
2018-10-03 13:03:30 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 13:02:45 +00:00
|
|
|
void GLContextGLX::SwapInterval(int Interval)
|
2013-01-24 16:31:08 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
if (!m_drawable)
|
2018-03-26 12:09:22 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
// Try EXT_swap_control, then MESA_swap_control.
|
|
|
|
if (glXSwapIntervalEXTPtr)
|
2020-10-23 18:41:30 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
glXSwapIntervalEXTPtr(m_display, m_drawable, Interval);
|
2020-10-23 18:41:30 +00:00
|
|
|
}
|
2018-03-26 12:09:22 +00:00
|
|
|
else if (glXSwapIntervalMESAPtr)
|
2020-10-23 18:41:30 +00:00
|
|
|
{
|
2018-03-26 12:09:22 +00:00
|
|
|
glXSwapIntervalMESAPtr(static_cast<unsigned int>(Interval));
|
2020-10-23 18:41:30 +00:00
|
|
|
}
|
2013-01-24 16:31:08 +00:00
|
|
|
else
|
2020-10-23 18:41:30 +00:00
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"No support for SwapInterval (framerate clamped to monitor refresh rate).");
|
|
|
|
}
|
2013-01-24 16:31:08 +00:00
|
|
|
}
|
2018-03-26 12:09:22 +00:00
|
|
|
|
2018-10-03 13:02:45 +00:00
|
|
|
void* GLContextGLX::GetFuncAddress(const std::string& name)
|
2013-12-30 13:22:50 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
return reinterpret_cast<void*>(glXGetProcAddress(reinterpret_cast<const GLubyte*>(name.c_str())));
|
2013-12-30 13:22:50 +00:00
|
|
|
}
|
2013-01-24 16:31:08 +00:00
|
|
|
|
2018-10-03 13:02:45 +00:00
|
|
|
void GLContextGLX::Swap()
|
2012-12-17 21:01:52 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
glXSwapBuffers(m_display, m_drawable);
|
2012-12-17 21:01:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create rendering window.
|
|
|
|
// Call browser: Core.cpp:EmuThread() > main.cpp:Video_Initialize()
|
2019-04-10 14:40:19 +00:00
|
|
|
bool GLContextGLX::Initialize(const WindowSystemInfo& wsi, bool stereo, bool core)
|
2012-12-17 21:01:52 +00:00
|
|
|
{
|
2019-04-10 14:40:19 +00:00
|
|
|
m_display = static_cast<Display*>(wsi.display_connection);
|
2018-10-03 13:02:45 +00:00
|
|
|
int screen = DefaultScreen(m_display);
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2014-12-20 09:57:34 +00:00
|
|
|
// checking glx version
|
|
|
|
int glxMajorVersion, glxMinorVersion;
|
2018-10-03 13:02:45 +00:00
|
|
|
glXQueryVersion(m_display, &glxMajorVersion, &glxMinorVersion);
|
2014-12-20 09:57:34 +00:00
|
|
|
if (glxMajorVersion < 1 || (glxMajorVersion == 1 && glxMinorVersion < 4))
|
|
|
|
{
|
2020-10-23 18:41:30 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "glX-Version {}.{} detected, but need at least 1.4", glxMajorVersion,
|
|
|
|
glxMinorVersion);
|
2014-12-20 09:57:34 +00:00
|
|
|
return false;
|
|
|
|
}
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2014-12-20 09:57:34 +00:00
|
|
|
// loading core context creation function
|
|
|
|
glXCreateContextAttribs =
|
|
|
|
(PFNGLXCREATECONTEXTATTRIBSPROC)GetFuncAddress("glXCreateContextAttribsARB");
|
|
|
|
if (!glXCreateContextAttribs)
|
2012-12-17 21:01:52 +00:00
|
|
|
{
|
2020-10-23 18:41:30 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"glXCreateContextAttribsARB not found, do you support GLX_ARB_create_context?");
|
2014-12-20 09:57:34 +00:00
|
|
|
return false;
|
2012-12-17 21:01:52 +00:00
|
|
|
}
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2014-12-20 09:57:34 +00:00
|
|
|
// choosing framebuffer
|
|
|
|
int visual_attribs[] = {GLX_X_RENDERABLE,
|
|
|
|
True,
|
|
|
|
GLX_DRAWABLE_TYPE,
|
|
|
|
GLX_WINDOW_BIT,
|
|
|
|
GLX_X_VISUAL_TYPE,
|
|
|
|
GLX_TRUE_COLOR,
|
|
|
|
GLX_RED_SIZE,
|
|
|
|
8,
|
|
|
|
GLX_GREEN_SIZE,
|
|
|
|
8,
|
|
|
|
GLX_BLUE_SIZE,
|
|
|
|
8,
|
|
|
|
GLX_DEPTH_SIZE,
|
|
|
|
0,
|
|
|
|
GLX_STENCIL_SIZE,
|
|
|
|
0,
|
|
|
|
GLX_DOUBLEBUFFER,
|
|
|
|
True,
|
2017-06-26 10:32:09 +00:00
|
|
|
GLX_STEREO,
|
|
|
|
stereo ? True : False,
|
2014-12-20 09:57:34 +00:00
|
|
|
None};
|
|
|
|
int fbcount = 0;
|
2018-10-03 13:02:45 +00:00
|
|
|
GLXFBConfig* fbc = glXChooseFBConfig(m_display, screen, visual_attribs, &fbcount);
|
2014-12-20 09:57:34 +00:00
|
|
|
if (!fbc || !fbcount)
|
2014-08-30 21:01:19 +00:00
|
|
|
{
|
2020-10-23 18:41:30 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "Failed to retrieve a framebuffer config");
|
2014-12-20 09:57:34 +00:00
|
|
|
return false;
|
2014-08-30 21:01:19 +00:00
|
|
|
}
|
2018-10-03 13:02:45 +00:00
|
|
|
m_fbconfig = *fbc;
|
2014-12-20 09:57:34 +00:00
|
|
|
XFree(fbc);
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2015-09-06 11:58:18 +00:00
|
|
|
s_glxError = false;
|
|
|
|
XErrorHandler oldHandler = XSetErrorHandler(&ctxErrorHandler);
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2012-12-17 21:01:52 +00:00
|
|
|
// Create a GLX context.
|
2015-09-17 15:48:25 +00:00
|
|
|
if (core)
|
|
|
|
{
|
2018-10-14 13:32:50 +00:00
|
|
|
for (const auto& version : s_desktop_opengl_versions)
|
|
|
|
{
|
|
|
|
std::array<int, 9> context_attribs = {
|
|
|
|
{GLX_CONTEXT_MAJOR_VERSION_ARB, version.first, GLX_CONTEXT_MINOR_VERSION_ARB,
|
|
|
|
version.second, GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
|
|
|
|
GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, None}};
|
|
|
|
|
|
|
|
s_glxError = false;
|
2023-04-15 04:55:53 +00:00
|
|
|
m_context =
|
|
|
|
glXCreateContextAttribs(m_display, m_fbconfig, nullptr, True, &context_attribs[0]);
|
2018-10-14 13:32:50 +00:00
|
|
|
XSync(m_display, False);
|
|
|
|
if (!m_context || s_glxError)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Got a context.
|
2020-10-23 18:41:30 +00:00
|
|
|
INFO_LOG_FMT(VIDEO, "Created a GLX context with version {}.{}", version.first,
|
|
|
|
version.second);
|
2019-02-09 11:51:17 +00:00
|
|
|
m_attribs.insert(m_attribs.end(), context_attribs.begin(), context_attribs.end());
|
2018-10-14 13:32:50 +00:00
|
|
|
break;
|
|
|
|
}
|
2015-09-06 11:58:18 +00:00
|
|
|
}
|
2018-10-14 13:32:50 +00:00
|
|
|
|
|
|
|
// Failed to create any core contexts, try for anything.
|
2018-10-03 13:02:45 +00:00
|
|
|
if (!m_context || s_glxError)
|
2014-12-20 09:57:34 +00:00
|
|
|
{
|
2016-01-23 10:21:53 +00:00
|
|
|
std::array<int, 5> context_attribs_legacy = {
|
|
|
|
{GLX_CONTEXT_MAJOR_VERSION_ARB, 1, GLX_CONTEXT_MINOR_VERSION_ARB, 0, None}};
|
2014-12-20 09:57:34 +00:00
|
|
|
s_glxError = false;
|
2023-04-15 04:55:53 +00:00
|
|
|
m_context =
|
|
|
|
glXCreateContextAttribs(m_display, m_fbconfig, nullptr, True, &context_attribs_legacy[0]);
|
2018-10-03 13:02:45 +00:00
|
|
|
XSync(m_display, False);
|
2016-01-23 10:21:53 +00:00
|
|
|
m_attribs.clear();
|
|
|
|
m_attribs.insert(m_attribs.end(), context_attribs_legacy.begin(), context_attribs_legacy.end());
|
2015-09-06 11:58:18 +00:00
|
|
|
}
|
2018-10-03 13:02:45 +00:00
|
|
|
if (!m_context || s_glxError)
|
2015-09-06 11:58:18 +00:00
|
|
|
{
|
2020-10-23 18:41:30 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "Unable to create GL context.");
|
2017-07-31 12:53:28 +00:00
|
|
|
XSetErrorHandler(oldHandler);
|
2015-09-06 11:58:18 +00:00
|
|
|
return false;
|
2012-12-17 21:01:52 +00:00
|
|
|
}
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2018-03-26 12:09:22 +00:00
|
|
|
glXSwapIntervalEXTPtr = nullptr;
|
|
|
|
glXSwapIntervalMESAPtr = nullptr;
|
|
|
|
glXCreateGLXPbufferSGIX = nullptr;
|
|
|
|
glXDestroyGLXPbufferSGIX = nullptr;
|
|
|
|
m_supports_pbuffer = false;
|
|
|
|
|
2016-01-23 10:21:53 +00:00
|
|
|
std::string tmp;
|
2018-10-03 13:02:45 +00:00
|
|
|
std::istringstream buffer(glXQueryExtensionsString(m_display, screen));
|
2016-01-23 10:21:53 +00:00
|
|
|
while (buffer >> tmp)
|
|
|
|
{
|
|
|
|
if (tmp == "GLX_SGIX_pbuffer")
|
2018-03-26 12:09:22 +00:00
|
|
|
{
|
|
|
|
glXCreateGLXPbufferSGIX = reinterpret_cast<PFNGLXCREATEGLXPBUFFERSGIXPROC>(
|
|
|
|
GetFuncAddress("glXCreateGLXPbufferSGIX"));
|
|
|
|
glXDestroyGLXPbufferSGIX = reinterpret_cast<PFNGLXDESTROYGLXPBUFFERSGIXPROC>(
|
|
|
|
GetFuncAddress("glXDestroyGLXPbufferSGIX"));
|
|
|
|
m_supports_pbuffer = glXCreateGLXPbufferSGIX && glXDestroyGLXPbufferSGIX;
|
|
|
|
}
|
|
|
|
else if (tmp == "GLX_EXT_swap_control")
|
|
|
|
{
|
|
|
|
glXSwapIntervalEXTPtr =
|
|
|
|
reinterpret_cast<PFNGLXSWAPINTERVALEXTPROC>(GetFuncAddress("glXSwapIntervalEXT"));
|
|
|
|
}
|
|
|
|
else if (tmp == "GLX_MESA_swap_control")
|
|
|
|
{
|
|
|
|
glXSwapIntervalMESAPtr =
|
|
|
|
reinterpret_cast<PFNGLXSWAPINTERVALMESAPROC>(GetFuncAddress("glXSwapIntervalMESA"));
|
|
|
|
}
|
2016-01-23 10:21:53 +00:00
|
|
|
}
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2019-04-10 14:40:19 +00:00
|
|
|
if (!CreateWindowSurface(reinterpret_cast<Window>(wsi.render_surface)))
|
2014-08-21 19:39:03 +00:00
|
|
|
{
|
2020-10-23 18:41:30 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "Error: CreateWindowSurface failed\n");
|
2017-07-31 12:53:28 +00:00
|
|
|
XSetErrorHandler(oldHandler);
|
2014-08-21 19:39:03 +00:00
|
|
|
return false;
|
|
|
|
}
|
2017-07-31 12:53:28 +00:00
|
|
|
|
|
|
|
XSetErrorHandler(oldHandler);
|
2018-10-03 13:02:45 +00:00
|
|
|
m_opengl_mode = Mode::OpenGL;
|
2018-10-03 13:03:33 +00:00
|
|
|
return MakeCurrent();
|
2016-01-23 10:21:53 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 13:03:30 +00:00
|
|
|
std::unique_ptr<GLContext> GLContextGLX::CreateSharedContext()
|
2016-01-23 10:21:53 +00:00
|
|
|
{
|
|
|
|
s_glxError = false;
|
|
|
|
XErrorHandler oldHandler = XSetErrorHandler(&ctxErrorHandler);
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2018-10-03 13:03:30 +00:00
|
|
|
GLXContext new_glx_context =
|
|
|
|
glXCreateContextAttribs(m_display, m_fbconfig, m_context, True, &m_attribs[0]);
|
2018-10-03 13:02:45 +00:00
|
|
|
XSync(m_display, False);
|
2016-06-24 08:43:46 +00:00
|
|
|
|
2018-10-03 13:03:30 +00:00
|
|
|
if (!new_glx_context || s_glxError)
|
2016-01-23 10:21:53 +00:00
|
|
|
{
|
2020-10-23 18:41:30 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "Unable to create GL context.");
|
2017-07-31 12:53:28 +00:00
|
|
|
XSetErrorHandler(oldHandler);
|
2018-10-03 13:03:30 +00:00
|
|
|
return nullptr;
|
2016-01-23 10:21:53 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 13:03:30 +00:00
|
|
|
std::unique_ptr<GLContextGLX> new_context = std::make_unique<GLContextGLX>();
|
|
|
|
new_context->m_context = new_glx_context;
|
|
|
|
new_context->m_opengl_mode = m_opengl_mode;
|
|
|
|
new_context->m_supports_pbuffer = m_supports_pbuffer;
|
|
|
|
new_context->m_display = m_display;
|
|
|
|
new_context->m_fbconfig = m_fbconfig;
|
2018-10-03 13:03:33 +00:00
|
|
|
new_context->m_is_shared = true;
|
2018-10-03 13:03:30 +00:00
|
|
|
|
|
|
|
if (m_supports_pbuffer && !new_context->CreateWindowSurface(None))
|
2016-01-23 10:21:53 +00:00
|
|
|
{
|
2020-10-23 18:41:30 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "Error: CreateWindowSurface failed");
|
2017-07-31 12:53:28 +00:00
|
|
|
XSetErrorHandler(oldHandler);
|
2018-10-03 13:03:30 +00:00
|
|
|
return nullptr;
|
2016-01-23 10:21:53 +00:00
|
|
|
}
|
2017-07-31 12:53:28 +00:00
|
|
|
|
|
|
|
XSetErrorHandler(oldHandler);
|
2018-10-03 13:03:30 +00:00
|
|
|
return new_context;
|
2016-01-23 10:21:53 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 13:02:45 +00:00
|
|
|
bool GLContextGLX::CreateWindowSurface(Window window_handle)
|
2016-01-23 10:21:53 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
if (window_handle)
|
2016-01-23 10:21:53 +00:00
|
|
|
{
|
|
|
|
// Get an appropriate visual
|
2018-10-03 13:02:45 +00:00
|
|
|
XVisualInfo* vi = glXGetVisualFromFBConfig(m_display, m_fbconfig);
|
|
|
|
m_render_window = GLX11Window::Create(m_display, window_handle, vi);
|
|
|
|
if (!m_render_window)
|
2016-01-23 10:21:53 +00:00
|
|
|
return false;
|
|
|
|
|
2018-10-03 13:02:45 +00:00
|
|
|
m_backbuffer_width = m_render_window->GetWidth();
|
|
|
|
m_backbuffer_height = m_render_window->GetHeight();
|
|
|
|
m_drawable = static_cast<GLXDrawable>(m_render_window->GetWindow());
|
2016-01-23 10:21:53 +00:00
|
|
|
XFree(vi);
|
|
|
|
}
|
2017-07-31 12:53:28 +00:00
|
|
|
else if (m_supports_pbuffer)
|
2016-01-23 10:21:53 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
m_pbuffer = glXCreateGLXPbufferSGIX(m_display, m_fbconfig, 1, 1, nullptr);
|
2016-01-23 10:21:53 +00:00
|
|
|
if (!m_pbuffer)
|
|
|
|
return false;
|
2018-10-03 13:02:45 +00:00
|
|
|
|
|
|
|
m_drawable = static_cast<GLXDrawable>(m_pbuffer);
|
2016-01-23 10:21:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-10-03 13:02:45 +00:00
|
|
|
void GLContextGLX::DestroyWindowSurface()
|
2016-01-23 10:21:53 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
m_render_window.reset();
|
|
|
|
if (m_supports_pbuffer && m_pbuffer)
|
2016-01-23 10:21:53 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
glXDestroyGLXPbufferSGIX(m_display, m_pbuffer);
|
2016-01-23 10:21:53 +00:00
|
|
|
m_pbuffer = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-03 13:02:45 +00:00
|
|
|
bool GLContextGLX::MakeCurrent()
|
2012-12-17 21:01:52 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
return glXMakeCurrent(m_display, m_drawable, m_context);
|
2012-12-17 21:01:52 +00:00
|
|
|
}
|
2013-04-11 01:32:07 +00:00
|
|
|
|
2018-10-03 13:02:45 +00:00
|
|
|
bool GLContextGLX::ClearCurrent()
|
2013-04-11 01:32:07 +00:00
|
|
|
{
|
2018-10-03 13:02:45 +00:00
|
|
|
return glXMakeCurrent(m_display, None, nullptr);
|
2013-04-11 01:32:07 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 13:03:19 +00:00
|
|
|
void GLContextGLX::Update()
|
|
|
|
{
|
|
|
|
m_render_window->UpdateDimensions();
|
|
|
|
m_backbuffer_width = m_render_window->GetWidth();
|
|
|
|
m_backbuffer_height = m_render_window->GetHeight();
|
2012-12-17 21:01:52 +00:00
|
|
|
}
|