2020-06-29 16:46:57 +00:00
|
|
|
#include "opengl_host_display.h"
|
2020-10-21 15:25:33 +00:00
|
|
|
#include "common/align.h"
|
2020-01-10 03:31:12 +00:00
|
|
|
#include "common/assert.h"
|
|
|
|
#include "common/log.h"
|
2021-02-04 16:16:45 +00:00
|
|
|
#include "common/string_util.h"
|
2021-02-13 14:52:34 +00:00
|
|
|
#include "common_host_interface.h"
|
2020-09-01 02:46:58 +00:00
|
|
|
#include "imgui.h"
|
|
|
|
#include "imgui_impl_opengl3.h"
|
2020-09-12 15:19:57 +00:00
|
|
|
#include "postprocessing_shadergen.h"
|
2021-01-30 05:19:44 +00:00
|
|
|
#include <array>
|
|
|
|
#include <tuple>
|
2021-01-07 15:11:19 +00:00
|
|
|
Log_SetChannel(OpenGLHostDisplay);
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
namespace FrontendCommon {
|
|
|
|
|
|
|
|
class OpenGLHostDisplayTexture : public HostDisplayTexture
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
|
|
|
public:
|
2021-01-23 08:05:33 +00:00
|
|
|
OpenGLHostDisplayTexture(GL::Texture texture, HostDisplayPixelFormat format)
|
|
|
|
: m_texture(std::move(texture)), m_format(format)
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2021-01-23 08:05:33 +00:00
|
|
|
}
|
|
|
|
~OpenGLHostDisplayTexture() override = default;
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2021-01-23 08:05:33 +00:00
|
|
|
void* GetHandle() const override { return reinterpret_cast<void*>(static_cast<uintptr_t>(m_texture.GetGLId())); }
|
|
|
|
u32 GetWidth() const override { return m_texture.GetWidth(); }
|
|
|
|
u32 GetHeight() const override { return m_texture.GetHeight(); }
|
|
|
|
u32 GetLayers() const override { return 1; }
|
|
|
|
u32 GetLevels() const override { return 1; }
|
|
|
|
u32 GetSamples() const override { return m_texture.GetSamples(); }
|
|
|
|
HostDisplayPixelFormat GetFormat() const override { return m_format; }
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2021-01-23 08:05:33 +00:00
|
|
|
GLuint GetGLID() const { return m_texture.GetGLId(); }
|
2019-12-31 06:17:17 +00:00
|
|
|
|
|
|
|
private:
|
2021-01-23 08:05:33 +00:00
|
|
|
GL::Texture m_texture;
|
|
|
|
HostDisplayPixelFormat m_format;
|
2019-12-31 06:17:17 +00:00
|
|
|
};
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
OpenGLHostDisplay::OpenGLHostDisplay() = default;
|
2020-04-22 11:13:51 +00:00
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
OpenGLHostDisplay::~OpenGLHostDisplay()
|
|
|
|
{
|
|
|
|
AssertMsg(!m_gl_context, "Context should have been destroyed by now");
|
|
|
|
}
|
2020-04-22 11:13:51 +00:00
|
|
|
|
|
|
|
HostDisplay::RenderAPI OpenGLHostDisplay::GetRenderAPI() const
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
return m_gl_context->IsGLES() ? RenderAPI::OpenGLES : RenderAPI::OpenGL;
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
2020-04-22 11:13:51 +00:00
|
|
|
void* OpenGLHostDisplay::GetRenderDevice() const
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-04-22 11:13:51 +00:00
|
|
|
void* OpenGLHostDisplay::GetRenderContext() const
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-01-02 07:45:25 +00:00
|
|
|
return m_gl_context.get();
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
2020-12-17 15:59:32 +00:00
|
|
|
static constexpr std::array<std::tuple<GLenum, GLenum, GLenum>, static_cast<u32>(HostDisplayPixelFormat::Count)>
|
|
|
|
s_display_pixel_format_mapping = {{
|
|
|
|
{}, // Unknown
|
2021-01-30 05:19:44 +00:00
|
|
|
{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE}, // RGBA8
|
|
|
|
{GL_RGBA8, GL_BGRA, GL_UNSIGNED_BYTE}, // BGRA8
|
|
|
|
{GL_RGB565, GL_RGB, GL_UNSIGNED_SHORT_5_6_5}, // RGB565
|
2020-12-17 15:59:32 +00:00
|
|
|
{GL_RGB5_A1, GL_BGRA, GL_UNSIGNED_SHORT_1_5_5_5_REV} // RGBA5551
|
|
|
|
}};
|
|
|
|
|
2021-01-23 08:05:33 +00:00
|
|
|
std::unique_ptr<HostDisplayTexture> OpenGLHostDisplay::CreateTexture(u32 width, u32 height, u32 layers, u32 levels,
|
|
|
|
u32 samples, HostDisplayPixelFormat format,
|
|
|
|
const void* data, u32 data_stride,
|
|
|
|
bool dynamic /* = false */)
|
|
|
|
{
|
|
|
|
if (layers != 1 || levels != 1)
|
|
|
|
return {};
|
|
|
|
|
|
|
|
const auto [gl_internal_format, gl_format, gl_type] = s_display_pixel_format_mapping[static_cast<u32>(format)];
|
|
|
|
|
|
|
|
// TODO: Set pack width
|
|
|
|
Assert(!data || data_stride == (width * sizeof(u32)));
|
|
|
|
|
|
|
|
GL::Texture tex;
|
|
|
|
if (!tex.Create(width, height, samples, gl_internal_format, gl_format, gl_type, data, data_stride))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
return std::make_unique<OpenGLHostDisplayTexture>(std::move(tex), format);
|
|
|
|
}
|
|
|
|
|
2020-04-22 11:13:51 +00:00
|
|
|
void OpenGLHostDisplay::UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height,
|
|
|
|
const void* texture_data, u32 texture_data_stride)
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
OpenGLHostDisplayTexture* tex = static_cast<OpenGLHostDisplayTexture*>(texture);
|
2020-12-17 15:59:32 +00:00
|
|
|
const auto [gl_internal_format, gl_format, gl_type] =
|
|
|
|
s_display_pixel_format_mapping[static_cast<u32>(texture->GetFormat())];
|
|
|
|
GLint alignment;
|
|
|
|
if (texture_data_stride & 1)
|
|
|
|
alignment = 1;
|
|
|
|
else if (texture_data_stride & 2)
|
|
|
|
alignment = 2;
|
|
|
|
else
|
|
|
|
alignment = 4;
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-04-11 08:54:09 +00:00
|
|
|
GLint old_texture_binding = 0, old_alignment = 0, old_row_length = 0;
|
2019-12-31 06:17:17 +00:00
|
|
|
glGetIntegerv(GL_TEXTURE_BINDING_2D, &old_texture_binding);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, tex->GetGLID());
|
2021-01-25 15:34:13 +00:00
|
|
|
|
|
|
|
glGetIntegerv(GL_UNPACK_ALIGNMENT, &old_alignment);
|
2020-12-17 15:59:32 +00:00
|
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
|
2021-01-25 15:34:13 +00:00
|
|
|
|
|
|
|
if (!m_use_gles2_draw_path)
|
|
|
|
{
|
|
|
|
glGetIntegerv(GL_UNPACK_ROW_LENGTH, &old_row_length);
|
|
|
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, texture_data_stride / GetDisplayPixelFormatSize(texture->GetFormat()));
|
|
|
|
}
|
2020-04-11 08:54:09 +00:00
|
|
|
|
2020-12-17 15:59:32 +00:00
|
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, gl_format, gl_type, texture_data);
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2021-01-25 15:34:13 +00:00
|
|
|
if (!m_use_gles2_draw_path)
|
|
|
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, old_row_length);
|
|
|
|
|
2020-04-11 08:54:09 +00:00
|
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, old_alignment);
|
2019-12-31 06:17:17 +00:00
|
|
|
glBindTexture(GL_TEXTURE_2D, old_texture_binding);
|
|
|
|
}
|
|
|
|
|
2020-12-17 15:59:32 +00:00
|
|
|
bool OpenGLHostDisplay::DownloadTexture(const void* texture_handle, HostDisplayPixelFormat texture_format, u32 x, u32 y,
|
|
|
|
u32 width, u32 height, void* out_data, u32 out_data_stride)
|
2020-03-15 14:03:49 +00:00
|
|
|
{
|
2020-12-17 15:59:32 +00:00
|
|
|
GLint alignment;
|
|
|
|
if (out_data_stride & 1)
|
|
|
|
alignment = 1;
|
|
|
|
else if (out_data_stride & 2)
|
|
|
|
alignment = 2;
|
|
|
|
else
|
|
|
|
alignment = 4;
|
|
|
|
|
2020-03-15 14:03:49 +00:00
|
|
|
GLint old_alignment = 0, old_row_length = 0;
|
|
|
|
glGetIntegerv(GL_PACK_ALIGNMENT, &old_alignment);
|
2020-12-17 15:59:32 +00:00
|
|
|
glPixelStorei(GL_PACK_ALIGNMENT, alignment);
|
2021-01-25 15:34:13 +00:00
|
|
|
if (!m_use_gles2_draw_path)
|
|
|
|
{
|
|
|
|
glGetIntegerv(GL_PACK_ROW_LENGTH, &old_row_length);
|
|
|
|
glPixelStorei(GL_PACK_ROW_LENGTH, out_data_stride / GetDisplayPixelFormatSize(texture_format));
|
|
|
|
}
|
2020-03-15 14:03:49 +00:00
|
|
|
|
|
|
|
const GLuint texture = static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture_handle));
|
2020-12-17 15:59:32 +00:00
|
|
|
const auto [gl_internal_format, gl_format, gl_type] =
|
|
|
|
s_display_pixel_format_mapping[static_cast<u32>(texture_format)];
|
|
|
|
|
|
|
|
GL::Texture::GetTextureSubImage(texture, 0, x, y, 0, width, height, 1, gl_format, gl_type, height * out_data_stride,
|
|
|
|
out_data);
|
2020-03-15 14:03:49 +00:00
|
|
|
|
|
|
|
glPixelStorei(GL_PACK_ALIGNMENT, old_alignment);
|
2021-01-25 15:34:13 +00:00
|
|
|
if (!m_use_gles2_draw_path)
|
|
|
|
glPixelStorei(GL_PACK_ROW_LENGTH, old_row_length);
|
2020-03-15 14:03:49 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-02-04 16:28:03 +00:00
|
|
|
void OpenGLHostDisplay::BindDisplayPixelsTexture()
|
|
|
|
{
|
|
|
|
if (m_display_pixels_texture_id == 0)
|
|
|
|
{
|
|
|
|
glGenTextures(1, &m_display_pixels_texture_id);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, m_display_pixels_texture_id);
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, m_display_linear_filtering ? GL_LINEAR : GL_NEAREST);
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, m_display_linear_filtering ? GL_LINEAR : GL_NEAREST);
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
|
|
|
|
m_display_texture_is_linear_filtered = m_display_linear_filtering;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
glBindTexture(GL_TEXTURE_2D, m_display_pixels_texture_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLHostDisplay::UpdateDisplayPixelsTextureFilter()
|
|
|
|
{
|
|
|
|
if (m_display_linear_filtering == m_display_texture_is_linear_filtered)
|
|
|
|
return;
|
|
|
|
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, m_display_linear_filtering ? GL_LINEAR : GL_NEAREST);
|
|
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, m_display_linear_filtering ? GL_LINEAR : GL_NEAREST);
|
|
|
|
m_display_texture_is_linear_filtered = m_display_linear_filtering;
|
|
|
|
}
|
|
|
|
|
2020-10-21 15:25:33 +00:00
|
|
|
bool OpenGLHostDisplay::SupportsDisplayPixelFormat(HostDisplayPixelFormat format) const
|
|
|
|
{
|
|
|
|
return (std::get<0>(s_display_pixel_format_mapping[static_cast<u32>(format)]) != static_cast<GLenum>(0));
|
|
|
|
}
|
|
|
|
|
|
|
|
bool OpenGLHostDisplay::BeginSetDisplayPixels(HostDisplayPixelFormat format, u32 width, u32 height, void** out_buffer,
|
|
|
|
u32* out_pitch)
|
|
|
|
{
|
|
|
|
const u32 pixel_size = GetDisplayPixelFormatSize(format);
|
|
|
|
const u32 stride = Common::AlignUpPow2(width * pixel_size, 4);
|
|
|
|
const u32 size_required = stride * height * pixel_size;
|
2021-02-06 05:02:28 +00:00
|
|
|
|
2021-02-27 10:53:00 +00:00
|
|
|
if (m_use_pbo_for_pixels)
|
2020-10-21 15:25:33 +00:00
|
|
|
{
|
2021-02-06 05:02:28 +00:00
|
|
|
const u32 buffer_size = Common::AlignUpPow2(size_required * 2, 4 * 1024 * 1024);
|
|
|
|
if (!m_display_pixels_texture_pbo || m_display_pixels_texture_pbo->GetSize() < buffer_size)
|
|
|
|
{
|
|
|
|
m_display_pixels_texture_pbo.reset();
|
|
|
|
m_display_pixels_texture_pbo = GL::StreamBuffer::Create(GL_PIXEL_UNPACK_BUFFER, buffer_size);
|
|
|
|
if (!m_display_pixels_texture_pbo)
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto map = m_display_pixels_texture_pbo->Map(GetDisplayPixelFormatSize(format), size_required);
|
|
|
|
m_display_texture_format = format;
|
|
|
|
m_display_pixels_texture_pbo_map_offset = map.buffer_offset;
|
|
|
|
m_display_pixels_texture_pbo_map_size = size_required;
|
|
|
|
*out_buffer = map.pointer;
|
|
|
|
*out_pitch = stride;
|
2020-10-21 15:25:33 +00:00
|
|
|
}
|
2021-02-06 05:02:28 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
if (m_gles_pixels_repack_buffer.size() < size_required)
|
|
|
|
m_gles_pixels_repack_buffer.resize(size_required);
|
2020-10-21 15:25:33 +00:00
|
|
|
|
2021-02-06 05:02:28 +00:00
|
|
|
*out_buffer = m_gles_pixels_repack_buffer.data();
|
|
|
|
*out_pitch = stride;
|
|
|
|
}
|
2020-10-21 15:25:33 +00:00
|
|
|
|
2021-02-04 16:28:03 +00:00
|
|
|
BindDisplayPixelsTexture();
|
2020-10-21 15:25:33 +00:00
|
|
|
SetDisplayTexture(reinterpret_cast<void*>(static_cast<uintptr_t>(m_display_pixels_texture_id)), format, width, height,
|
|
|
|
0, 0, width, height);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLHostDisplay::EndSetDisplayPixels()
|
|
|
|
{
|
|
|
|
const u32 width = static_cast<u32>(m_display_texture_view_width);
|
|
|
|
const u32 height = static_cast<u32>(m_display_texture_view_height);
|
|
|
|
|
|
|
|
const auto [gl_internal_format, gl_format, gl_type] =
|
|
|
|
s_display_pixel_format_mapping[static_cast<u32>(m_display_texture_format)];
|
|
|
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, m_display_pixels_texture_id);
|
2021-02-27 10:53:00 +00:00
|
|
|
if (m_use_pbo_for_pixels)
|
2021-02-06 05:02:28 +00:00
|
|
|
{
|
|
|
|
m_display_pixels_texture_pbo->Unmap(m_display_pixels_texture_pbo_map_size);
|
|
|
|
m_display_pixels_texture_pbo->Bind();
|
|
|
|
glTexImage2D(GL_TEXTURE_2D, 0, gl_internal_format, width, height, 0, gl_format, gl_type,
|
|
|
|
reinterpret_cast<void*>(static_cast<uintptr_t>(m_display_pixels_texture_pbo_map_offset)));
|
|
|
|
m_display_pixels_texture_pbo->Unbind();
|
2020-10-21 15:25:33 +00:00
|
|
|
|
2021-02-06 05:02:28 +00:00
|
|
|
m_display_pixels_texture_pbo_map_offset = 0;
|
|
|
|
m_display_pixels_texture_pbo_map_size = 0;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// glTexImage2D should be quicker on Mali...
|
|
|
|
glTexImage2D(GL_TEXTURE_2D, 0, gl_internal_format, width, height, 0, gl_format, gl_type,
|
|
|
|
m_gles_pixels_repack_buffer.data());
|
|
|
|
}
|
|
|
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
2020-10-21 15:25:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool OpenGLHostDisplay::SetDisplayPixels(HostDisplayPixelFormat format, u32 width, u32 height, const void* buffer,
|
|
|
|
u32 pitch)
|
|
|
|
{
|
2021-02-04 16:28:03 +00:00
|
|
|
BindDisplayPixelsTexture();
|
2020-10-21 15:25:33 +00:00
|
|
|
|
|
|
|
const auto [gl_internal_format, gl_format, gl_type] = s_display_pixel_format_mapping[static_cast<u32>(format)];
|
2021-02-04 16:16:45 +00:00
|
|
|
const u32 pixel_size = GetDisplayPixelFormatSize(format);
|
|
|
|
const bool is_packed_tightly = (pitch == (pixel_size * width));
|
|
|
|
|
|
|
|
// If we have GLES3, we can set row_length.
|
|
|
|
if (!m_use_gles2_draw_path || is_packed_tightly)
|
|
|
|
{
|
|
|
|
if (!is_packed_tightly)
|
|
|
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, pitch / pixel_size);
|
|
|
|
|
|
|
|
glTexImage2D(GL_TEXTURE_2D, 0, gl_internal_format, width, height, 0, gl_format, gl_type, buffer);
|
2020-10-21 15:25:33 +00:00
|
|
|
|
2021-02-04 16:16:45 +00:00
|
|
|
if (!is_packed_tightly)
|
|
|
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Otherwise, we need to repack the image.
|
|
|
|
const u32 packed_pitch = width * pixel_size;
|
|
|
|
const u32 repack_size = packed_pitch * height;
|
2021-02-06 05:02:28 +00:00
|
|
|
if (m_gles_pixels_repack_buffer.size() < repack_size)
|
|
|
|
m_gles_pixels_repack_buffer.resize(repack_size);
|
|
|
|
StringUtil::StrideMemCpy(m_gles_pixels_repack_buffer.data(), packed_pitch, buffer, pitch, packed_pitch, height);
|
2021-02-04 16:16:45 +00:00
|
|
|
glTexImage2D(GL_TEXTURE_2D, 0, gl_internal_format, width, height, 0, gl_format, gl_type,
|
2021-02-06 05:02:28 +00:00
|
|
|
m_gles_pixels_repack_buffer.data());
|
2021-02-04 16:16:45 +00:00
|
|
|
}
|
2020-10-21 15:25:33 +00:00
|
|
|
|
|
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
|
|
|
|
SetDisplayTexture(reinterpret_cast<void*>(static_cast<uintptr_t>(m_display_pixels_texture_id)), format, width, height,
|
|
|
|
0, 0, width, height);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-04-22 11:13:51 +00:00
|
|
|
void OpenGLHostDisplay::SetVSync(bool enabled)
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-10-27 01:58:46 +00:00
|
|
|
if (m_gl_context->GetWindowInfo().type == WindowInfo::Type::Surfaceless)
|
|
|
|
return;
|
|
|
|
|
2019-12-31 06:17:17 +00:00
|
|
|
// Window framebuffer has to be bound to call SetSwapInterval.
|
|
|
|
GLint current_fbo = 0;
|
|
|
|
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, ¤t_fbo);
|
|
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
2020-05-07 12:49:04 +00:00
|
|
|
m_gl_context->SetSwapInterval(enabled ? 1 : 0);
|
2019-12-31 06:17:17 +00:00
|
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, current_fbo);
|
|
|
|
}
|
|
|
|
|
2020-04-22 11:13:51 +00:00
|
|
|
const char* OpenGLHostDisplay::GetGLSLVersionString() const
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
if (GetRenderAPI() == RenderAPI::OpenGLES)
|
2020-02-15 12:11:51 +00:00
|
|
|
{
|
|
|
|
if (GLAD_GL_ES_VERSION_3_0)
|
|
|
|
return "#version 300 es";
|
|
|
|
else
|
|
|
|
return "#version 100";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (GLAD_GL_VERSION_3_3)
|
|
|
|
return "#version 330";
|
|
|
|
else
|
|
|
|
return "#version 130";
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
2020-04-22 11:13:51 +00:00
|
|
|
std::string OpenGLHostDisplay::GetGLSLVersionHeader() const
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
|
|
|
std::string header = GetGLSLVersionString();
|
|
|
|
header += "\n\n";
|
2020-06-29 16:46:57 +00:00
|
|
|
if (GetRenderAPI() == RenderAPI::OpenGLES)
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
|
|
|
header += "precision highp float;\n";
|
|
|
|
header += "precision highp int;\n\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
return header;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void APIENTRY GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,
|
|
|
|
const GLchar* message, const void* userParam)
|
|
|
|
{
|
|
|
|
switch (severity)
|
|
|
|
{
|
|
|
|
case GL_DEBUG_SEVERITY_HIGH_KHR:
|
2021-02-05 00:54:51 +00:00
|
|
|
Log_ErrorPrint(message);
|
2019-12-31 06:17:17 +00:00
|
|
|
break;
|
|
|
|
case GL_DEBUG_SEVERITY_MEDIUM_KHR:
|
|
|
|
Log_WarningPrint(message);
|
|
|
|
break;
|
|
|
|
case GL_DEBUG_SEVERITY_LOW_KHR:
|
2021-02-05 00:54:51 +00:00
|
|
|
Log_InfoPrint(message);
|
2019-12-31 06:17:17 +00:00
|
|
|
break;
|
|
|
|
case GL_DEBUG_SEVERITY_NOTIFICATION:
|
|
|
|
// Log_DebugPrint(message);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
bool OpenGLHostDisplay::HasRenderDevice() const
|
2020-02-15 15:14:28 +00:00
|
|
|
{
|
|
|
|
return static_cast<bool>(m_gl_context);
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
bool OpenGLHostDisplay::HasRenderSurface() const
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
return m_window_info.type != WindowInfo::Type::Surfaceless;
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-12-26 13:22:24 +00:00
|
|
|
bool OpenGLHostDisplay::CreateRenderDevice(const WindowInfo& wi, std::string_view adapter_name, bool debug_device,
|
|
|
|
bool threaded_presentation)
|
2020-06-29 16:46:57 +00:00
|
|
|
{
|
|
|
|
m_gl_context = GL::Context::Create(wi);
|
2020-05-07 12:49:04 +00:00
|
|
|
if (!m_gl_context)
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-05-07 12:49:04 +00:00
|
|
|
Log_ErrorPrintf("Failed to create any GL context");
|
2021-05-18 13:20:20 +00:00
|
|
|
m_gl_context.reset();
|
2019-12-31 06:17:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-04-02 14:55:09 +00:00
|
|
|
m_window_info = m_gl_context->GetWindowInfo();
|
2020-05-07 12:49:04 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-12-26 13:22:24 +00:00
|
|
|
bool OpenGLHostDisplay::InitializeRenderDevice(std::string_view shader_cache_directory, bool debug_device,
|
|
|
|
bool threaded_presentation)
|
2020-05-07 12:49:04 +00:00
|
|
|
{
|
2021-02-27 10:53:00 +00:00
|
|
|
m_use_gles2_draw_path = (GetRenderAPI() == RenderAPI::OpenGLES && !GLAD_GL_ES_VERSION_3_0);
|
2021-01-25 15:34:13 +00:00
|
|
|
if (!m_use_gles2_draw_path)
|
|
|
|
glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, reinterpret_cast<GLint*>(&m_uniform_buffer_alignment));
|
2020-09-12 15:19:57 +00:00
|
|
|
|
2021-02-06 10:19:08 +00:00
|
|
|
// Doubt GLES2 drivers will support PBOs efficiently.
|
2021-02-07 12:05:46 +00:00
|
|
|
m_use_pbo_for_pixels = !m_use_gles2_draw_path;
|
2021-02-27 10:53:00 +00:00
|
|
|
if (GetRenderAPI() == RenderAPI::OpenGLES)
|
2021-02-06 10:19:08 +00:00
|
|
|
{
|
|
|
|
// Adreno seems to corrupt textures through PBOs...
|
|
|
|
const char* gl_vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
|
2021-06-21 12:16:30 +00:00
|
|
|
if (std::strstr(gl_vendor, "Qualcomm") || std::strstr(gl_vendor, "Broadcom"))
|
2021-02-06 10:19:08 +00:00
|
|
|
m_use_pbo_for_pixels = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Log_VerbosePrintf("Using GLES2 draw path: %s", m_use_gles2_draw_path ? "yes" : "no");
|
|
|
|
Log_VerbosePrintf("Using PBO for streaming: %s", m_use_pbo_for_pixels ? "yes" : "no");
|
|
|
|
|
2020-01-19 04:53:49 +00:00
|
|
|
if (debug_device && GLAD_GL_KHR_debug)
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
if (GetRenderAPI() == RenderAPI::OpenGLES)
|
|
|
|
glDebugMessageCallbackKHR(GLDebugCallback, nullptr);
|
|
|
|
else
|
|
|
|
glDebugMessageCallback(GLDebugCallback, nullptr);
|
|
|
|
|
2019-12-31 06:17:17 +00:00
|
|
|
glEnable(GL_DEBUG_OUTPUT);
|
2020-06-29 16:46:57 +00:00
|
|
|
// glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
if (!CreateResources())
|
|
|
|
return false;
|
|
|
|
|
2020-10-31 04:16:22 +00:00
|
|
|
// Start with vsync on.
|
|
|
|
SetVSync(true);
|
|
|
|
|
2019-12-31 06:17:17 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
bool OpenGLHostDisplay::MakeRenderContextCurrent()
|
2020-04-22 11:13:51 +00:00
|
|
|
{
|
2020-05-07 12:49:04 +00:00
|
|
|
if (!m_gl_context->MakeCurrent())
|
2020-04-22 11:13:51 +00:00
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to make GL context current");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
bool OpenGLHostDisplay::DoneRenderContextCurrent()
|
2020-04-22 11:13:51 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
return m_gl_context->DoneCurrent();
|
2020-04-22 11:13:51 +00:00
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
void OpenGLHostDisplay::DestroyRenderDevice()
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
if (!m_gl_context)
|
|
|
|
return;
|
|
|
|
|
|
|
|
DestroyResources();
|
|
|
|
|
2020-05-07 12:49:04 +00:00
|
|
|
m_gl_context->DoneCurrent();
|
2020-01-02 07:45:25 +00:00
|
|
|
m_gl_context.reset();
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
bool OpenGLHostDisplay::ChangeRenderWindow(const WindowInfo& new_wi)
|
2020-04-22 11:13:51 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
Assert(m_gl_context);
|
|
|
|
|
|
|
|
if (!m_gl_context->ChangeSurface(new_wi))
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to change surface");
|
|
|
|
return false;
|
|
|
|
}
|
2020-05-07 12:49:04 +00:00
|
|
|
|
2021-04-02 14:55:09 +00:00
|
|
|
m_window_info = m_gl_context->GetWindowInfo();
|
2020-04-22 11:13:51 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
void OpenGLHostDisplay::ResizeRenderWindow(s32 new_window_width, s32 new_window_height)
|
|
|
|
{
|
|
|
|
if (!m_gl_context)
|
|
|
|
return;
|
2020-04-22 11:13:51 +00:00
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
m_gl_context->ResizeSurface(static_cast<u32>(new_window_width), static_cast<u32>(new_window_height));
|
2021-04-02 14:55:09 +00:00
|
|
|
m_window_info = m_gl_context->GetWindowInfo();
|
2020-06-29 16:46:57 +00:00
|
|
|
}
|
|
|
|
|
2020-11-02 09:52:01 +00:00
|
|
|
bool OpenGLHostDisplay::SupportsFullscreen() const
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-11-01 14:38:54 +00:00
|
|
|
bool OpenGLHostDisplay::IsFullscreen()
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool OpenGLHostDisplay::SetFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-02-13 14:33:35 +00:00
|
|
|
HostDisplay::AdapterAndModeList OpenGLHostDisplay::GetAdapterAndModeList()
|
|
|
|
{
|
2021-02-13 14:52:34 +00:00
|
|
|
AdapterAndModeList aml;
|
|
|
|
|
|
|
|
if (m_gl_context)
|
|
|
|
{
|
|
|
|
for (const GL::Context::FullscreenModeInfo& fmi : m_gl_context->EnumerateFullscreenModes())
|
|
|
|
{
|
|
|
|
aml.fullscreen_modes.push_back(
|
|
|
|
CommonHostInterface::GetFullscreenModeString(fmi.width, fmi.height, fmi.refresh_rate));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return aml;
|
2021-02-13 14:33:35 +00:00
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
void OpenGLHostDisplay::DestroyRenderSurface()
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
if (!m_gl_context)
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_window_info = {};
|
|
|
|
if (!m_gl_context->ChangeSurface(m_window_info))
|
|
|
|
Log_ErrorPrintf("Failed to switch to surfaceless");
|
|
|
|
}
|
|
|
|
|
|
|
|
bool OpenGLHostDisplay::CreateImGuiContext()
|
|
|
|
{
|
2021-01-30 05:19:44 +00:00
|
|
|
return ImGui_ImplOpenGL3_Init(GetGLSLVersionString());
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
void OpenGLHostDisplay::DestroyImGuiContext()
|
2020-01-02 07:45:25 +00:00
|
|
|
{
|
|
|
|
ImGui_ImplOpenGL3_Shutdown();
|
|
|
|
}
|
|
|
|
|
2021-01-30 04:39:56 +00:00
|
|
|
bool OpenGLHostDisplay::UpdateImGuiFontTexture()
|
|
|
|
{
|
|
|
|
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
|
|
|
return ImGui_ImplOpenGL3_CreateFontsTexture();
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
bool OpenGLHostDisplay::CreateResources()
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-11-21 08:08:39 +00:00
|
|
|
if (!m_use_gles2_draw_path)
|
|
|
|
{
|
|
|
|
static constexpr char fullscreen_quad_vertex_shader[] = R"(
|
2019-12-31 06:17:17 +00:00
|
|
|
uniform vec4 u_src_rect;
|
|
|
|
out vec2 v_tex0;
|
|
|
|
|
|
|
|
void main()
|
|
|
|
{
|
|
|
|
vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
|
|
v_tex0 = u_src_rect.xy + pos * u_src_rect.zw;
|
|
|
|
gl_Position = vec4(pos * vec2(2.0f, -2.0f) + vec2(-1.0f, 1.0f), 0.0f, 1.0f);
|
|
|
|
}
|
|
|
|
)";
|
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
static constexpr char display_fragment_shader[] = R"(
|
2019-12-31 06:17:17 +00:00
|
|
|
uniform sampler2D samp0;
|
|
|
|
|
|
|
|
in vec2 v_tex0;
|
|
|
|
out vec4 o_col0;
|
|
|
|
|
|
|
|
void main()
|
|
|
|
{
|
2020-05-02 16:59:00 +00:00
|
|
|
o_col0 = vec4(texture(samp0, v_tex0).rgb, 1.0);
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
2020-06-30 15:56:21 +00:00
|
|
|
)";
|
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
static constexpr char cursor_fragment_shader[] = R"(
|
2020-06-30 15:56:21 +00:00
|
|
|
uniform sampler2D samp0;
|
|
|
|
|
|
|
|
in vec2 v_tex0;
|
|
|
|
out vec4 o_col0;
|
|
|
|
|
|
|
|
void main()
|
|
|
|
{
|
|
|
|
o_col0 = texture(samp0, v_tex0);
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
)";
|
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
if (!m_display_program.Compile(GetGLSLVersionHeader() + fullscreen_quad_vertex_shader, {},
|
|
|
|
GetGLSLVersionHeader() + display_fragment_shader) ||
|
|
|
|
!m_cursor_program.Compile(GetGLSLVersionHeader() + fullscreen_quad_vertex_shader, {},
|
|
|
|
GetGLSLVersionHeader() + cursor_fragment_shader))
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to compile display shaders");
|
|
|
|
return false;
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
if (GetRenderAPI() != RenderAPI::OpenGLES)
|
|
|
|
{
|
|
|
|
m_display_program.BindFragData(0, "o_col0");
|
|
|
|
m_cursor_program.BindFragData(0, "o_col0");
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
if (!m_display_program.Link() || !m_cursor_program.Link())
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to link display programs");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_display_program.Bind();
|
|
|
|
m_display_program.RegisterUniform("u_src_rect");
|
|
|
|
m_display_program.RegisterUniform("samp0");
|
|
|
|
m_display_program.Uniform1i(1, 0);
|
|
|
|
m_cursor_program.Bind();
|
|
|
|
m_cursor_program.RegisterUniform("u_src_rect");
|
|
|
|
m_cursor_program.RegisterUniform("samp0");
|
|
|
|
m_cursor_program.Uniform1i(1, 0);
|
|
|
|
|
|
|
|
glGenVertexArrays(1, &m_display_vao);
|
|
|
|
|
|
|
|
// samplers
|
|
|
|
glGenSamplers(1, &m_display_nearest_sampler);
|
|
|
|
glSamplerParameteri(m_display_nearest_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
|
|
glSamplerParameteri(m_display_nearest_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
|
|
glGenSamplers(1, &m_display_linear_sampler);
|
|
|
|
glSamplerParameteri(m_display_linear_sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
|
|
glSamplerParameteri(m_display_linear_sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
2020-11-21 08:08:39 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
static constexpr char fullscreen_quad_vertex_shader[] = R"(
|
|
|
|
#version 100
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
attribute highp vec2 a_pos;
|
|
|
|
attribute highp vec2 a_tex0;
|
|
|
|
varying highp vec2 v_tex0;
|
|
|
|
|
|
|
|
void main()
|
|
|
|
{
|
|
|
|
gl_Position = vec4(a_pos, 0.0, 1.0);
|
|
|
|
v_tex0 = a_tex0;
|
|
|
|
}
|
|
|
|
)";
|
|
|
|
|
|
|
|
static constexpr char display_fragment_shader[] = R"(
|
|
|
|
#version 100
|
|
|
|
|
|
|
|
uniform highp sampler2D samp0;
|
|
|
|
|
|
|
|
varying highp vec2 v_tex0;
|
|
|
|
|
|
|
|
void main()
|
|
|
|
{
|
|
|
|
gl_FragColor = vec4(texture2D(samp0, v_tex0).rgb, 1.0);
|
|
|
|
}
|
|
|
|
)";
|
|
|
|
|
|
|
|
static constexpr char cursor_fragment_shader[] = R"(
|
|
|
|
#version 100
|
|
|
|
|
|
|
|
uniform highp sampler2D samp0;
|
|
|
|
|
|
|
|
varying highp vec2 v_tex0;
|
|
|
|
|
|
|
|
void main()
|
|
|
|
{
|
|
|
|
gl_FragColor = texture2D(samp0, v_tex0);
|
|
|
|
}
|
|
|
|
)";
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
if (!m_display_program.Compile(fullscreen_quad_vertex_shader, {}, display_fragment_shader) ||
|
|
|
|
!m_cursor_program.Compile(fullscreen_quad_vertex_shader, {}, cursor_fragment_shader))
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to compile display shaders");
|
|
|
|
return false;
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
m_display_program.BindAttribute(0, "a_pos");
|
|
|
|
m_display_program.BindAttribute(1, "a_tex0");
|
|
|
|
m_cursor_program.BindAttribute(0, "a_pos");
|
|
|
|
m_cursor_program.BindAttribute(1, "a_tex0");
|
|
|
|
|
|
|
|
if (!m_display_program.Link() || !m_cursor_program.Link())
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to link display programs");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_display_program.Bind();
|
|
|
|
m_display_program.RegisterUniform("samp0");
|
|
|
|
m_display_program.Uniform1i(0, 0);
|
|
|
|
m_cursor_program.Bind();
|
|
|
|
m_cursor_program.RegisterUniform("samp0");
|
|
|
|
m_cursor_program.Uniform1i(0, 0);
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
void OpenGLHostDisplay::DestroyResources()
|
2020-01-02 07:45:25 +00:00
|
|
|
{
|
2020-09-12 15:19:57 +00:00
|
|
|
m_post_processing_chain.ClearStages();
|
|
|
|
m_post_processing_input_texture.Destroy();
|
|
|
|
m_post_processing_ubo.reset();
|
|
|
|
m_post_processing_stages.clear();
|
|
|
|
|
2020-10-21 15:25:33 +00:00
|
|
|
if (m_display_pixels_texture_id != 0)
|
|
|
|
{
|
|
|
|
glDeleteTextures(1, &m_display_pixels_texture_id);
|
|
|
|
m_display_pixels_texture_id = 0;
|
|
|
|
}
|
|
|
|
|
2020-01-02 07:45:25 +00:00
|
|
|
if (m_display_vao != 0)
|
2020-11-21 08:08:39 +00:00
|
|
|
{
|
2020-01-02 07:45:25 +00:00
|
|
|
glDeleteVertexArrays(1, &m_display_vao);
|
2020-11-21 08:08:39 +00:00
|
|
|
m_display_vao = 0;
|
|
|
|
}
|
2020-01-02 07:45:25 +00:00
|
|
|
if (m_display_linear_sampler != 0)
|
2020-11-21 08:08:39 +00:00
|
|
|
{
|
2020-01-02 07:45:25 +00:00
|
|
|
glDeleteSamplers(1, &m_display_linear_sampler);
|
2020-11-21 08:08:39 +00:00
|
|
|
m_display_linear_sampler = 0;
|
|
|
|
}
|
2020-01-02 07:45:25 +00:00
|
|
|
if (m_display_nearest_sampler != 0)
|
2020-11-21 08:08:39 +00:00
|
|
|
{
|
2020-01-02 07:45:25 +00:00
|
|
|
glDeleteSamplers(1, &m_display_nearest_sampler);
|
2020-11-21 08:08:39 +00:00
|
|
|
m_display_nearest_sampler = 0;
|
|
|
|
}
|
2020-01-02 07:45:25 +00:00
|
|
|
|
2020-06-30 15:56:21 +00:00
|
|
|
m_cursor_program.Destroy();
|
2020-01-02 07:45:25 +00:00
|
|
|
m_display_program.Destroy();
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
bool OpenGLHostDisplay::Render()
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-11-03 05:58:40 +00:00
|
|
|
if (ShouldSkipDisplayingFrame())
|
|
|
|
{
|
|
|
|
if (ImGui::GetCurrentContext())
|
|
|
|
ImGui::Render();
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-04-27 16:36:39 +00:00
|
|
|
glDisable(GL_SCISSOR_TEST);
|
2019-12-31 06:17:17 +00:00
|
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
2020-05-02 16:59:00 +00:00
|
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
2019-12-31 06:17:17 +00:00
|
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
RenderDisplay();
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
if (ImGui::GetCurrentContext())
|
|
|
|
RenderImGui();
|
|
|
|
|
|
|
|
RenderSoftwareCursor();
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-05-07 12:49:04 +00:00
|
|
|
m_gl_context->SwapBuffers();
|
2020-06-29 16:46:57 +00:00
|
|
|
return true;
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2021-03-05 16:11:17 +00:00
|
|
|
bool OpenGLHostDisplay::RenderScreenshot(u32 width, u32 height, std::vector<u32>* out_pixels, u32* out_stride,
|
|
|
|
HostDisplayPixelFormat* out_format)
|
|
|
|
{
|
|
|
|
GL::Texture texture;
|
|
|
|
if (!texture.Create(width, height, 1, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, nullptr) || !texture.CreateFramebuffer())
|
|
|
|
return false;
|
|
|
|
|
2021-04-27 16:36:39 +00:00
|
|
|
glDisable(GL_SCISSOR_TEST);
|
2021-03-05 16:11:17 +00:00
|
|
|
texture.BindFramebuffer(GL_FRAMEBUFFER);
|
|
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
|
|
|
|
if (HasDisplayTexture())
|
|
|
|
{
|
|
|
|
const auto [left, top, draw_width, draw_height] = CalculateDrawRect(width, height, 0);
|
|
|
|
|
|
|
|
if (!m_post_processing_chain.IsEmpty())
|
|
|
|
{
|
|
|
|
ApplyPostProcessingChain(texture.GetGLFramebufferID(), left, height - top - draw_height, draw_width, draw_height,
|
|
|
|
m_display_texture_handle, m_display_texture_width, m_display_texture_height,
|
|
|
|
m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_width,
|
|
|
|
m_display_texture_view_height, width, height);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
RenderDisplay(left, height - top - draw_height, draw_width, draw_height, m_display_texture_handle,
|
|
|
|
m_display_texture_width, m_display_texture_height, m_display_texture_view_x,
|
|
|
|
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height,
|
|
|
|
m_display_linear_filtering);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
out_pixels->resize(width * height);
|
|
|
|
*out_stride = sizeof(u32) * width;
|
|
|
|
*out_format = HostDisplayPixelFormat::RGBA8;
|
|
|
|
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, out_pixels->data());
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
void OpenGLHostDisplay::RenderImGui()
|
|
|
|
{
|
|
|
|
ImGui::Render();
|
|
|
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
2019-12-31 06:17:17 +00:00
|
|
|
GL::Program::ResetLastProgram();
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
void OpenGLHostDisplay::RenderDisplay()
|
2019-12-31 06:17:17 +00:00
|
|
|
{
|
2020-06-29 16:46:57 +00:00
|
|
|
if (!HasDisplayTexture())
|
2019-12-31 06:17:17 +00:00
|
|
|
return;
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
const auto [left, top, width, height] = CalculateDrawRect(GetWindowWidth(), GetWindowHeight(), m_display_top_margin);
|
2020-09-12 15:19:57 +00:00
|
|
|
|
|
|
|
if (!m_post_processing_chain.IsEmpty())
|
|
|
|
{
|
2020-09-15 15:12:49 +00:00
|
|
|
ApplyPostProcessingChain(0, left, GetWindowHeight() - top - height, width, height, m_display_texture_handle,
|
|
|
|
m_display_texture_width, m_display_texture_height, m_display_texture_view_x,
|
2021-03-05 16:11:17 +00:00
|
|
|
m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height,
|
|
|
|
GetWindowWidth(), GetWindowHeight());
|
2020-09-12 15:19:57 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-30 14:38:06 +00:00
|
|
|
RenderDisplay(left, GetWindowHeight() - top - height, width, height, m_display_texture_handle,
|
|
|
|
m_display_texture_width, m_display_texture_height, m_display_texture_view_x, m_display_texture_view_y,
|
|
|
|
m_display_texture_view_width, m_display_texture_view_height, m_display_linear_filtering);
|
2020-06-29 16:46:57 +00:00
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
static void DrawFullscreenQuadES2(s32 tex_view_x, s32 tex_view_y, s32 tex_view_width, s32 tex_view_height,
|
|
|
|
s32 tex_width, s32 tex_height)
|
|
|
|
{
|
|
|
|
const float tex_left = static_cast<float>(tex_view_x) / static_cast<float>(tex_width);
|
|
|
|
const float tex_right = tex_left + static_cast<float>(tex_view_width) / static_cast<float>(tex_width);
|
|
|
|
const float tex_top = static_cast<float>(tex_view_y) / static_cast<float>(tex_height);
|
|
|
|
const float tex_bottom = tex_top + static_cast<float>(tex_view_height) / static_cast<float>(tex_height);
|
|
|
|
const std::array<std::array<float, 4>, 4> vertices = {{
|
|
|
|
{{-1.0f, -1.0f, tex_left, tex_bottom}}, // bottom-left
|
|
|
|
{{1.0f, -1.0f, tex_right, tex_bottom}}, // bottom-right
|
|
|
|
{{-1.0f, 1.0f, tex_left, tex_top}}, // top-left
|
|
|
|
{{1.0f, 1.0f, tex_right, tex_top}}, // top-right
|
|
|
|
}};
|
|
|
|
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
|
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(vertices[0]), &vertices[0][0]);
|
|
|
|
glEnableVertexAttribArray(0);
|
|
|
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertices[0]), &vertices[0][2]);
|
|
|
|
glEnableVertexAttribArray(1);
|
|
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
|
|
glDisableVertexAttribArray(1);
|
|
|
|
glDisableVertexAttribArray(0);
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
void OpenGLHostDisplay::RenderDisplay(s32 left, s32 bottom, s32 width, s32 height, void* texture_handle,
|
|
|
|
u32 texture_width, s32 texture_height, s32 texture_view_x, s32 texture_view_y,
|
|
|
|
s32 texture_view_width, s32 texture_view_height, bool linear_filter)
|
|
|
|
{
|
2020-09-15 15:12:49 +00:00
|
|
|
glViewport(left, bottom, width, height);
|
2019-12-31 06:17:17 +00:00
|
|
|
glDisable(GL_BLEND);
|
|
|
|
glDisable(GL_CULL_FACE);
|
|
|
|
glDisable(GL_DEPTH_TEST);
|
|
|
|
glDepthMask(GL_FALSE);
|
2020-06-29 16:46:57 +00:00
|
|
|
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture_handle)));
|
2020-11-21 08:08:39 +00:00
|
|
|
m_display_program.Bind();
|
|
|
|
|
|
|
|
if (!m_use_gles2_draw_path)
|
|
|
|
{
|
2021-01-03 15:32:14 +00:00
|
|
|
const float position_adjust = m_display_linear_filtering ? 0.5f : 0.0f;
|
|
|
|
const float size_adjust = m_display_linear_filtering ? 1.0f : 0.0f;
|
|
|
|
const float flip_adjust = (texture_view_height < 0) ? -1.0f : 1.0f;
|
|
|
|
m_display_program.Uniform4f(
|
|
|
|
0, (static_cast<float>(texture_view_x) + position_adjust) / static_cast<float>(texture_width),
|
|
|
|
(static_cast<float>(texture_view_y) + (position_adjust * flip_adjust)) / static_cast<float>(texture_height),
|
|
|
|
(static_cast<float>(texture_view_width) - size_adjust) / static_cast<float>(texture_width),
|
|
|
|
(static_cast<float>(texture_view_height) - (size_adjust * flip_adjust)) / static_cast<float>(texture_height));
|
2020-11-21 08:08:39 +00:00
|
|
|
glBindSampler(0, linear_filter ? m_display_linear_sampler : m_display_nearest_sampler);
|
|
|
|
glBindVertexArray(m_display_vao);
|
|
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
|
|
glBindSampler(0, 0);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-02-04 16:28:03 +00:00
|
|
|
if (static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture_handle)) == m_display_pixels_texture_id)
|
|
|
|
UpdateDisplayPixelsTextureFilter();
|
|
|
|
|
2020-11-21 08:08:39 +00:00
|
|
|
DrawFullscreenQuadES2(m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_width,
|
|
|
|
m_display_texture_view_height, m_display_texture_width, m_display_texture_height);
|
|
|
|
}
|
2020-06-29 16:46:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLHostDisplay::RenderSoftwareCursor()
|
|
|
|
{
|
|
|
|
if (!HasSoftwareCursor())
|
|
|
|
return;
|
|
|
|
|
|
|
|
const auto [left, top, width, height] = CalculateSoftwareCursorDrawRect();
|
2020-06-30 14:43:17 +00:00
|
|
|
RenderSoftwareCursor(left, GetWindowHeight() - top - height, width, height, m_cursor_texture.get());
|
2020-06-29 16:46:57 +00:00
|
|
|
}
|
|
|
|
|
2020-06-30 14:43:17 +00:00
|
|
|
void OpenGLHostDisplay::RenderSoftwareCursor(s32 left, s32 bottom, s32 width, s32 height,
|
2020-06-29 16:46:57 +00:00
|
|
|
HostDisplayTexture* texture_handle)
|
|
|
|
{
|
2020-06-30 14:43:17 +00:00
|
|
|
glViewport(left, bottom, width, height);
|
2020-06-29 16:46:57 +00:00
|
|
|
glEnable(GL_BLEND);
|
|
|
|
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
|
|
|
|
glBlendEquationSeparate(GL_FUNC_ADD, GL_FUNC_ADD);
|
|
|
|
glDisable(GL_CULL_FACE);
|
|
|
|
glDisable(GL_DEPTH_TEST);
|
|
|
|
glDepthMask(GL_FALSE);
|
2020-06-30 15:56:21 +00:00
|
|
|
m_cursor_program.Bind();
|
2020-06-29 16:46:57 +00:00
|
|
|
glBindTexture(GL_TEXTURE_2D, static_cast<OpenGLHostDisplayTexture*>(texture_handle)->GetGLID());
|
2020-11-21 08:08:39 +00:00
|
|
|
|
|
|
|
if (!m_use_gles2_draw_path)
|
|
|
|
{
|
|
|
|
m_cursor_program.Uniform4f(0, 0.0f, 0.0f, 1.0f, 1.0f);
|
|
|
|
glBindSampler(0, m_display_linear_sampler);
|
|
|
|
glBindVertexArray(m_display_vao);
|
|
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
|
|
glBindSampler(0, 0);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
const s32 tex_width = static_cast<s32>(static_cast<OpenGLHostDisplayTexture*>(texture_handle)->GetWidth());
|
|
|
|
const s32 tex_height = static_cast<s32>(static_cast<OpenGLHostDisplayTexture*>(texture_handle)->GetHeight());
|
|
|
|
DrawFullscreenQuadES2(0, 0, tex_width, tex_height, tex_width, tex_height);
|
|
|
|
}
|
2019-12-31 06:17:17 +00:00
|
|
|
}
|
2020-06-29 16:46:57 +00:00
|
|
|
|
2020-09-12 15:19:57 +00:00
|
|
|
bool OpenGLHostDisplay::SetPostProcessingChain(const std::string_view& config)
|
|
|
|
{
|
|
|
|
if (config.empty())
|
|
|
|
{
|
|
|
|
m_post_processing_input_texture.Destroy();
|
|
|
|
m_post_processing_stages.clear();
|
|
|
|
m_post_processing_chain.ClearStages();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!m_post_processing_chain.CreateFromString(config))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
m_post_processing_stages.clear();
|
|
|
|
|
|
|
|
FrontendCommon::PostProcessingShaderGen shadergen(HostDisplay::RenderAPI::OpenGL, false);
|
|
|
|
|
|
|
|
for (u32 i = 0; i < m_post_processing_chain.GetStageCount(); i++)
|
|
|
|
{
|
|
|
|
const PostProcessingShader& shader = m_post_processing_chain.GetShaderStage(i);
|
|
|
|
const std::string vs = shadergen.GeneratePostProcessingVertexShader(shader);
|
|
|
|
const std::string ps = shadergen.GeneratePostProcessingFragmentShader(shader);
|
|
|
|
|
|
|
|
PostProcessingStage stage;
|
|
|
|
stage.uniforms_size = shader.GetUniformsSize();
|
|
|
|
if (!stage.program.Compile(vs, {}, ps))
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("Failed to compile post-processing program, disabling.");
|
|
|
|
m_post_processing_stages.clear();
|
|
|
|
m_post_processing_chain.ClearStages();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!shadergen.UseGLSLBindingLayout())
|
|
|
|
{
|
|
|
|
stage.program.BindUniformBlock("UBOBlock", 1);
|
|
|
|
stage.program.Bind();
|
|
|
|
stage.program.Uniform1i("samp0", 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!stage.program.Link())
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("Failed to link post-processing program, disabling.");
|
|
|
|
m_post_processing_stages.clear();
|
|
|
|
m_post_processing_chain.ClearStages();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_post_processing_stages.push_back(std::move(stage));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!m_post_processing_ubo)
|
|
|
|
{
|
|
|
|
m_post_processing_ubo = GL::StreamBuffer::Create(GL_UNIFORM_BUFFER, 1 * 1024 * 1024);
|
|
|
|
if (!m_post_processing_ubo)
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("Failed to allocate uniform buffer for postprocessing");
|
|
|
|
m_post_processing_stages.clear();
|
|
|
|
m_post_processing_chain.ClearStages();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_post_processing_ubo->Unbind();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool OpenGLHostDisplay::CheckPostProcessingRenderTargets(u32 target_width, u32 target_height)
|
|
|
|
{
|
|
|
|
DebugAssert(!m_post_processing_stages.empty());
|
|
|
|
|
|
|
|
if (m_post_processing_input_texture.GetWidth() != target_width ||
|
|
|
|
m_post_processing_input_texture.GetHeight() != target_height)
|
|
|
|
{
|
2020-10-30 14:38:06 +00:00
|
|
|
if (!m_post_processing_input_texture.Create(target_width, target_height, 1, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE) ||
|
2020-09-12 15:19:57 +00:00
|
|
|
!m_post_processing_input_texture.CreateFramebuffer())
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const u32 target_count = (static_cast<u32>(m_post_processing_stages.size()) - 1);
|
|
|
|
for (u32 i = 0; i < target_count; i++)
|
|
|
|
{
|
|
|
|
PostProcessingStage& pps = m_post_processing_stages[i];
|
|
|
|
if (pps.output_texture.GetWidth() != target_width || pps.output_texture.GetHeight() != target_height)
|
|
|
|
{
|
2020-10-30 14:38:06 +00:00
|
|
|
if (!pps.output_texture.Create(target_width, target_height, 1, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE) ||
|
2020-09-12 15:19:57 +00:00
|
|
|
!pps.output_texture.CreateFramebuffer())
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void OpenGLHostDisplay::ApplyPostProcessingChain(GLuint final_target, s32 final_left, s32 final_top, s32 final_width,
|
|
|
|
s32 final_height, void* texture_handle, u32 texture_width,
|
|
|
|
s32 texture_height, s32 texture_view_x, s32 texture_view_y,
|
2021-03-05 16:11:17 +00:00
|
|
|
s32 texture_view_width, s32 texture_view_height, u32 target_width,
|
|
|
|
u32 target_height)
|
2020-09-12 15:19:57 +00:00
|
|
|
{
|
2021-03-05 16:11:17 +00:00
|
|
|
if (!CheckPostProcessingRenderTargets(target_width, target_height))
|
2020-09-12 15:19:57 +00:00
|
|
|
{
|
2021-03-05 16:11:17 +00:00
|
|
|
RenderDisplay(final_left, target_height - final_top - final_height, final_width, final_height, texture_handle,
|
2020-09-15 15:12:49 +00:00
|
|
|
texture_width, texture_height, texture_view_x, texture_view_y, texture_view_width,
|
|
|
|
texture_view_height, m_display_linear_filtering);
|
2020-09-12 15:19:57 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// downsample/upsample - use same viewport for remainder
|
|
|
|
m_post_processing_input_texture.BindFramebuffer(GL_DRAW_FRAMEBUFFER);
|
|
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
2021-03-05 16:11:17 +00:00
|
|
|
RenderDisplay(final_left, target_height - final_top - final_height, final_width, final_height, texture_handle,
|
2020-09-15 15:12:49 +00:00
|
|
|
texture_width, texture_height, texture_view_x, texture_view_y, texture_view_width, texture_view_height,
|
|
|
|
m_display_linear_filtering);
|
2020-09-12 15:19:57 +00:00
|
|
|
|
|
|
|
texture_handle = reinterpret_cast<void*>(static_cast<uintptr_t>(m_post_processing_input_texture.GetGLId()));
|
|
|
|
texture_width = m_post_processing_input_texture.GetWidth();
|
|
|
|
texture_height = m_post_processing_input_texture.GetHeight();
|
|
|
|
texture_view_x = final_left;
|
|
|
|
texture_view_y = final_top;
|
|
|
|
texture_view_width = final_width;
|
|
|
|
texture_view_height = final_height;
|
|
|
|
|
|
|
|
m_post_processing_ubo->Bind();
|
|
|
|
|
|
|
|
const u32 final_stage = static_cast<u32>(m_post_processing_stages.size()) - 1u;
|
|
|
|
for (u32 i = 0; i < static_cast<u32>(m_post_processing_stages.size()); i++)
|
|
|
|
{
|
|
|
|
PostProcessingStage& pps = m_post_processing_stages[i];
|
|
|
|
if (i == final_stage)
|
|
|
|
{
|
|
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, final_target);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
pps.output_texture.BindFramebuffer(GL_DRAW_FRAMEBUFFER);
|
|
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
}
|
|
|
|
|
|
|
|
pps.program.Bind();
|
|
|
|
glBindSampler(0, m_display_linear_sampler);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture_handle)));
|
|
|
|
glBindSampler(0, m_display_nearest_sampler);
|
|
|
|
|
|
|
|
const auto map_result = m_post_processing_ubo->Map(m_uniform_buffer_alignment, pps.uniforms_size);
|
|
|
|
m_post_processing_chain.GetShaderStage(i).FillUniformBuffer(
|
|
|
|
map_result.pointer, texture_width, texture_height, texture_view_x, texture_view_y, texture_view_width,
|
|
|
|
texture_view_height, GetWindowWidth(), GetWindowHeight(), 0.0f);
|
|
|
|
m_post_processing_ubo->Unmap(pps.uniforms_size);
|
|
|
|
glBindBufferRange(GL_UNIFORM_BUFFER, 1, m_post_processing_ubo->GetGLBufferId(), map_result.buffer_offset,
|
|
|
|
pps.uniforms_size);
|
|
|
|
|
|
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
|
|
|
|
|
|
if (i != final_stage)
|
|
|
|
texture_handle = reinterpret_cast<void*>(static_cast<uintptr_t>(pps.output_texture.GetGLId()));
|
|
|
|
}
|
|
|
|
|
|
|
|
glBindSampler(0, 0);
|
|
|
|
m_post_processing_ubo->Unbind();
|
|
|
|
}
|
|
|
|
|
2020-06-29 16:46:57 +00:00
|
|
|
} // namespace FrontendCommon
|