Merge pull request #4613 from BreadFish64/gles5
video_core: add GLES support
This commit is contained in:
commit
f409342ab5
|
@ -1,5 +1,5 @@
|
||||||
These files were generated by the [glad](https://github.com/Dav1dde/glad) OpenGL loader generator and have been checked in as-is. You can re-generate them using glad with the following command:
|
These files were generated by the [glad](https://github.com/Dav1dde/glad) OpenGL loader generator and have been checked in as-is. You can re-generate them using glad with the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
python -m glad --profile core --out-path glad/ --api gl=3.3 --generator=c
|
python -m glad --profile core --out-path glad/ --api "gl=3.3,gles2=3.2" --generator=c
|
||||||
```
|
```
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
#define __khrplatform_h_
|
#define __khrplatform_h_
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Copyright (c) 2008-2009 The Khronos Group Inc.
|
** Copyright (c) 2008-2018 The Khronos Group Inc.
|
||||||
**
|
**
|
||||||
** Permission is hereby granted, free of charge, to any person obtaining a
|
** Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
** copy of this software and/or associated documentation files (the
|
** copy of this software and/or associated documentation files (the
|
||||||
|
@ -26,18 +26,16 @@
|
||||||
|
|
||||||
/* Khronos platform-specific types and definitions.
|
/* Khronos platform-specific types and definitions.
|
||||||
*
|
*
|
||||||
* $Revision: 32517 $ on $Date: 2016-03-11 02:41:19 -0800 (Fri, 11 Mar 2016) $
|
* The master copy of khrplatform.h is maintained in the Khronos EGL
|
||||||
|
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
|
||||||
|
* The last semantic modification to khrplatform.h was at commit ID:
|
||||||
|
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
|
||||||
*
|
*
|
||||||
* Adopters may modify this file to suit their platform. Adopters are
|
* Adopters may modify this file to suit their platform. Adopters are
|
||||||
* encouraged to submit platform specific modifications to the Khronos
|
* encouraged to submit platform specific modifications to the Khronos
|
||||||
* group so that they can be included in future versions of this file.
|
* group so that they can be included in future versions of this file.
|
||||||
* Please submit changes by sending them to the public Khronos Bugzilla
|
* Please submit changes by filing pull requests or issues on
|
||||||
* (http://khronos.org/bugzilla) by filing a bug against product
|
* the EGL Registry repository linked above.
|
||||||
* "Khronos (general)" component "Registry".
|
|
||||||
*
|
|
||||||
* A predefined template which fills in some of the bug fields can be
|
|
||||||
* reached using http://tinyurl.com/khrplatform-h-bugreport, but you
|
|
||||||
* must create a Bugzilla login first.
|
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* See the Implementer's Guidelines for information about where this file
|
* See the Implementer's Guidelines for information about where this file
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -105,6 +105,7 @@ void Config::ReadValues() {
|
||||||
Settings::values.use_cpu_jit = sdl2_config->GetBoolean("Core", "use_cpu_jit", true);
|
Settings::values.use_cpu_jit = sdl2_config->GetBoolean("Core", "use_cpu_jit", true);
|
||||||
|
|
||||||
// Renderer
|
// Renderer
|
||||||
|
Settings::values.use_gles = sdl2_config->GetBoolean("Renderer", "use_gles", false);
|
||||||
Settings::values.use_hw_renderer = sdl2_config->GetBoolean("Renderer", "use_hw_renderer", true);
|
Settings::values.use_hw_renderer = sdl2_config->GetBoolean("Renderer", "use_hw_renderer", true);
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
// Hardware shader is broken on macos thanks to poor drivers.
|
// Hardware shader is broken on macos thanks to poor drivers.
|
||||||
|
|
|
@ -92,6 +92,10 @@ udp_pad_index=
|
||||||
use_cpu_jit =
|
use_cpu_jit =
|
||||||
|
|
||||||
[Renderer]
|
[Renderer]
|
||||||
|
# Whether to render using GLES or OpenGL
|
||||||
|
# 0 (default): OpenGL, 1: GLES
|
||||||
|
use_gles =
|
||||||
|
|
||||||
# Whether to use software or hardware rendering.
|
# Whether to use software or hardware rendering.
|
||||||
# 0: Software, 1 (default): Hardware
|
# 0: Software, 1 (default): Hardware
|
||||||
use_hw_renderer =
|
use_hw_renderer =
|
||||||
|
|
|
@ -122,8 +122,13 @@ EmuWindow_SDL2::EmuWindow_SDL2(bool fullscreen) {
|
||||||
SDL_SetMainReady();
|
SDL_SetMainReady();
|
||||||
|
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||||
|
if (Settings::values.use_gles) {
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||||
|
} else {
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||||
|
}
|
||||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||||
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
|
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
|
||||||
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
|
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
|
||||||
|
@ -155,7 +160,9 @@ EmuWindow_SDL2::EmuWindow_SDL2(bool fullscreen) {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gladLoadGLLoader(static_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
auto gl_load_func = Settings::values.use_gles ? gladLoadGLES2Loader : gladLoadGLLoader;
|
||||||
|
|
||||||
|
if (!gl_load_func(static_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
||||||
LOG_CRITICAL(Frontend, "Failed to initialize GL functions: {}", SDL_GetError());
|
LOG_CRITICAL(Frontend, "Failed to initialize GL functions: {}", SDL_GetError());
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@ void LogSetting(const std::string& name, const T& value) {
|
||||||
void LogSettings() {
|
void LogSettings() {
|
||||||
LOG_INFO(Config, "Citra Configuration:");
|
LOG_INFO(Config, "Citra Configuration:");
|
||||||
LogSetting("Core_UseCpuJit", Settings::values.use_cpu_jit);
|
LogSetting("Core_UseCpuJit", Settings::values.use_cpu_jit);
|
||||||
|
LogSetting("Renderer_UseGLES", Settings::values.use_gles);
|
||||||
LogSetting("Renderer_UseHwRenderer", Settings::values.use_hw_renderer);
|
LogSetting("Renderer_UseHwRenderer", Settings::values.use_hw_renderer);
|
||||||
LogSetting("Renderer_UseHwShader", Settings::values.use_hw_shader);
|
LogSetting("Renderer_UseHwShader", Settings::values.use_hw_shader);
|
||||||
LogSetting("Renderer_ShadersAccurateGs", Settings::values.shaders_accurate_gs);
|
LogSetting("Renderer_ShadersAccurateGs", Settings::values.shaders_accurate_gs);
|
||||||
|
|
|
@ -130,6 +130,7 @@ struct Values {
|
||||||
u64 init_time;
|
u64 init_time;
|
||||||
|
|
||||||
// Renderer
|
// Renderer
|
||||||
|
bool use_gles;
|
||||||
bool use_hw_renderer;
|
bool use_hw_renderer;
|
||||||
bool use_hw_shader;
|
bool use_hw_shader;
|
||||||
bool shaders_accurate_gs;
|
bool shaders_accurate_gs;
|
||||||
|
|
|
@ -41,6 +41,8 @@ add_library(video_core STATIC
|
||||||
renderer_opengl/gl_state.h
|
renderer_opengl/gl_state.h
|
||||||
renderer_opengl/gl_stream_buffer.cpp
|
renderer_opengl/gl_stream_buffer.cpp
|
||||||
renderer_opengl/gl_stream_buffer.h
|
renderer_opengl/gl_stream_buffer.h
|
||||||
|
renderer_opengl/gl_vars.cpp
|
||||||
|
renderer_opengl/gl_vars.h
|
||||||
renderer_opengl/pica_to_gl.h
|
renderer_opengl/pica_to_gl.h
|
||||||
renderer_opengl/renderer_opengl.cpp
|
renderer_opengl/renderer_opengl.cpp
|
||||||
renderer_opengl/renderer_opengl.h
|
renderer_opengl/renderer_opengl.h
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
|
#include "video_core/renderer_opengl/gl_rasterizer_cache.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
#include "video_core/utils.h"
|
#include "video_core/utils.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
|
@ -50,6 +51,17 @@ static constexpr std::array<FormatTuple, 5> fb_format_tuples = {{
|
||||||
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4}, // RGBA4
|
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4}, // RGBA4
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
// Same as above, with minor changes for OpenGL ES. Replaced
|
||||||
|
// GL_UNSIGNED_INT_8_8_8_8 with GL_UNSIGNED_BYTE and
|
||||||
|
// GL_BGR with GL_RGB
|
||||||
|
static constexpr std::array<FormatTuple, 5> fb_format_tuples_oes = {{
|
||||||
|
{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE}, // RGBA8
|
||||||
|
{GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE}, // RGB8
|
||||||
|
{GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1}, // RGB5A1
|
||||||
|
{GL_RGB565, GL_RGB, GL_UNSIGNED_SHORT_5_6_5}, // RGB565
|
||||||
|
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4}, // RGBA4
|
||||||
|
}};
|
||||||
|
|
||||||
static constexpr std::array<FormatTuple, 4> depth_format_tuples = {{
|
static constexpr std::array<FormatTuple, 4> depth_format_tuples = {{
|
||||||
{GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT}, // D16
|
{GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT}, // D16
|
||||||
{},
|
{},
|
||||||
|
@ -63,6 +75,9 @@ static const FormatTuple& GetFormatTuple(PixelFormat pixel_format) {
|
||||||
const SurfaceType type = SurfaceParams::GetFormatType(pixel_format);
|
const SurfaceType type = SurfaceParams::GetFormatType(pixel_format);
|
||||||
if (type == SurfaceType::Color) {
|
if (type == SurfaceType::Color) {
|
||||||
ASSERT(static_cast<std::size_t>(pixel_format) < fb_format_tuples.size());
|
ASSERT(static_cast<std::size_t>(pixel_format) < fb_format_tuples.size());
|
||||||
|
if (GLES) {
|
||||||
|
return fb_format_tuples_oes[static_cast<unsigned int>(pixel_format)];
|
||||||
|
}
|
||||||
return fb_format_tuples[static_cast<unsigned int>(pixel_format)];
|
return fb_format_tuples[static_cast<unsigned int>(pixel_format)];
|
||||||
} else if (type == SurfaceType::Depth || type == SurfaceType::DepthStencil) {
|
} else if (type == SurfaceType::Depth || type == SurfaceType::DepthStencil) {
|
||||||
std::size_t tuple_idx = static_cast<std::size_t>(pixel_format) - 14;
|
std::size_t tuple_idx = static_cast<std::size_t>(pixel_format) - 14;
|
||||||
|
@ -72,6 +87,77 @@ static const FormatTuple& GetFormatTuple(PixelFormat pixel_format) {
|
||||||
return tex_tuple;
|
return tex_tuple;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenGL ES does not support glGetTexImage. Obtain the pixels by attaching the
|
||||||
|
* texture to a framebuffer.
|
||||||
|
* Originally from https://github.com/apitrace/apitrace/blob/master/retrace/glstate_images.cpp
|
||||||
|
*/
|
||||||
|
static inline void GetTexImageOES(GLenum target, GLint level, GLenum format, GLenum type,
|
||||||
|
GLint height, GLint width, GLint depth, GLubyte* pixels) {
|
||||||
|
|
||||||
|
memset(pixels, 0x80, height * width * 4);
|
||||||
|
|
||||||
|
GLenum texture_binding = GL_NONE;
|
||||||
|
switch (target) {
|
||||||
|
case GL_TEXTURE_2D:
|
||||||
|
texture_binding = GL_TEXTURE_BINDING_2D;
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_CUBE_MAP_POSITIVE_X:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
|
||||||
|
texture_binding = GL_TEXTURE_BINDING_CUBE_MAP;
|
||||||
|
break;
|
||||||
|
case GL_TEXTURE_3D_OES:
|
||||||
|
texture_binding = GL_TEXTURE_BINDING_3D_OES;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint texture = 0;
|
||||||
|
glGetIntegerv(texture_binding, &texture);
|
||||||
|
if (!texture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLint prev_fbo = 0;
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
|
||||||
|
switch (target) {
|
||||||
|
case GL_TEXTURE_2D:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_POSITIVE_X:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
|
||||||
|
case GL_TEXTURE_CUBE_MAP_NEGATIVE_Z: {
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level);
|
||||||
|
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
LOG_DEBUG(Render_OpenGL, "Framebuffer is incomplete, status: {:X}", status);
|
||||||
|
}
|
||||||
|
glReadPixels(0, 0, width, height, format, type, pixels);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GL_TEXTURE_3D_OES:
|
||||||
|
for (int i = 0; i < depth; i++) {
|
||||||
|
glFramebufferTexture3D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture,
|
||||||
|
level, i);
|
||||||
|
glReadPixels(0, 0, width, height, format, type, pixels + 4 * i * width * height);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo);
|
||||||
|
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
}
|
||||||
|
|
||||||
template <typename Map, typename Interval>
|
template <typename Map, typename Interval>
|
||||||
constexpr auto RangeFromInterval(Map& map, const Interval& interval) {
|
constexpr auto RangeFromInterval(Map& map, const Interval& interval) {
|
||||||
return boost::make_iterator_range(map.equal_range(interval));
|
return boost::make_iterator_range(map.equal_range(interval));
|
||||||
|
@ -841,7 +927,12 @@ void CachedSurface::DownloadGLTexture(const MathUtil::Rectangle<u32>& rect, GLui
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
if (GLES) {
|
||||||
|
GetTexImageOES(GL_TEXTURE_2D, 0, tuple.format, tuple.type, height, width, 0,
|
||||||
|
&gl_buffer[buffer_offset]);
|
||||||
|
} else {
|
||||||
glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, &gl_buffer[buffer_offset]);
|
glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, &gl_buffer[buffer_offset]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state.ResetTexture(texture.handle);
|
state.ResetTexture(texture.handle);
|
||||||
state.draw.read_framebuffer = read_fb_handle;
|
state.draw.read_framebuffer = read_fb_handle;
|
||||||
|
@ -982,16 +1073,15 @@ RasterizerCacheOpenGL::RasterizerCacheOpenGL() {
|
||||||
d24s8_abgr_buffer.Create();
|
d24s8_abgr_buffer.Create();
|
||||||
d24s8_abgr_buffer_size = 0;
|
d24s8_abgr_buffer_size = 0;
|
||||||
|
|
||||||
const char* vs_source = R"(
|
std::string vs_source = R"(
|
||||||
#version 330 core
|
|
||||||
const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
const vec2 vertices[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
const char* fs_source = R"(
|
|
||||||
#version 330 core
|
|
||||||
|
|
||||||
|
std::string fs_source = GLES ? fragment_shader_precision_OES : "";
|
||||||
|
fs_source += R"(
|
||||||
uniform samplerBuffer tbo;
|
uniform samplerBuffer tbo;
|
||||||
uniform vec2 tbo_size;
|
uniform vec2 tbo_size;
|
||||||
uniform vec4 viewport;
|
uniform vec4 viewport;
|
||||||
|
@ -1004,7 +1094,7 @@ void main() {
|
||||||
color = texelFetch(tbo, tbo_offset).rabg;
|
color = texelFetch(tbo, tbo_offset).rabg;
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
d24s8_abgr_shader.Create(vs_source, fs_source);
|
d24s8_abgr_shader.Create(vs_source.c_str(), fs_source.c_str());
|
||||||
|
|
||||||
OpenGLState state = OpenGLState::GetCurState();
|
OpenGLState state = OpenGLState::GetCurState();
|
||||||
GLuint old_program = state.draw.shader_program;
|
GLuint old_program = state.draw.shader_program;
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
#include "video_core/renderer_opengl/gl_shader_decompiler.h"
|
#include "video_core/renderer_opengl/gl_shader_decompiler.h"
|
||||||
#include "video_core/renderer_opengl/gl_shader_gen.h"
|
#include "video_core/renderer_opengl/gl_shader_gen.h"
|
||||||
#include "video_core/renderer_opengl/gl_shader_util.h"
|
#include "video_core/renderer_opengl/gl_shader_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
using Pica::FramebufferRegs;
|
using Pica::FramebufferRegs;
|
||||||
|
@ -1250,7 +1251,6 @@ std::string GenerateFragmentShader(const PicaFSConfig& config, bool separable_sh
|
||||||
const auto& state = config.state;
|
const auto& state = config.state;
|
||||||
|
|
||||||
std::string out = R"(
|
std::string out = R"(
|
||||||
#version 330 core
|
|
||||||
#extension GL_ARB_shader_image_load_store : enable
|
#extension GL_ARB_shader_image_load_store : enable
|
||||||
#extension GL_ARB_shader_image_size : enable
|
#extension GL_ARB_shader_image_size : enable
|
||||||
#define ALLOW_SHADOW (defined(GL_ARB_shader_image_load_store) && defined(GL_ARB_shader_image_size))
|
#define ALLOW_SHADOW (defined(GL_ARB_shader_image_load_store) && defined(GL_ARB_shader_image_size))
|
||||||
|
@ -1260,10 +1260,16 @@ std::string GenerateFragmentShader(const PicaFSConfig& config, bool separable_sh
|
||||||
out += "#extension GL_ARB_separate_shader_objects : enable\n";
|
out += "#extension GL_ARB_separate_shader_objects : enable\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GLES) {
|
||||||
|
out += fragment_shader_precision_OES;
|
||||||
|
}
|
||||||
|
|
||||||
out += GetVertexInterfaceDeclaration(false, separable_shader);
|
out += GetVertexInterfaceDeclaration(false, separable_shader);
|
||||||
|
|
||||||
out += R"(
|
out += R"(
|
||||||
|
#ifndef CITRA_GLES
|
||||||
in vec4 gl_FragCoord;
|
in vec4 gl_FragCoord;
|
||||||
|
#endif // CITRA_GLES
|
||||||
|
|
||||||
out vec4 color;
|
out vec4 color;
|
||||||
|
|
||||||
|
@ -1300,13 +1306,13 @@ float LookupLightingLUT(int lut_index, int index, float delta) {
|
||||||
|
|
||||||
float LookupLightingLUTUnsigned(int lut_index, float pos) {
|
float LookupLightingLUTUnsigned(int lut_index, float pos) {
|
||||||
int index = clamp(int(pos * 256.0), 0, 255);
|
int index = clamp(int(pos * 256.0), 0, 255);
|
||||||
float delta = pos * 256.0 - index;
|
float delta = pos * 256.0 - float(index);
|
||||||
return LookupLightingLUT(lut_index, index, delta);
|
return LookupLightingLUT(lut_index, index, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
float LookupLightingLUTSigned(int lut_index, float pos) {
|
float LookupLightingLUTSigned(int lut_index, float pos) {
|
||||||
int index = clamp(int(pos * 128.0), -128, 127);
|
int index = clamp(int(pos * 128.0), -128, 127);
|
||||||
float delta = pos * 128.0 - index;
|
float delta = pos * 128.0 - float(index);
|
||||||
if (index < 0) index += 256;
|
if (index < 0) index += 256;
|
||||||
return LookupLightingLUT(lut_index, index, delta);
|
return LookupLightingLUT(lut_index, index, delta);
|
||||||
}
|
}
|
||||||
|
@ -1492,10 +1498,10 @@ vec4 secondary_fragment_color = vec4(0.0);
|
||||||
// Negate the condition if we have to keep only the pixels outside the scissor box
|
// Negate the condition if we have to keep only the pixels outside the scissor box
|
||||||
if (state.scissor_test_mode == RasterizerRegs::ScissorMode::Include)
|
if (state.scissor_test_mode == RasterizerRegs::ScissorMode::Include)
|
||||||
out += "!";
|
out += "!";
|
||||||
out += "(gl_FragCoord.x >= scissor_x1 && "
|
out += "(gl_FragCoord.x >= float(scissor_x1) && "
|
||||||
"gl_FragCoord.y >= scissor_y1 && "
|
"gl_FragCoord.y >= float(scissor_y1) && "
|
||||||
"gl_FragCoord.x < scissor_x2 && "
|
"gl_FragCoord.x < float(scissor_x2) && "
|
||||||
"gl_FragCoord.y < scissor_y2)) discard;\n";
|
"gl_FragCoord.y < float(scissor_y2))) discard;\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// After perspective divide, OpenGL transform z_over_w from [-1, 1] to [near, far]. Here we use
|
// After perspective divide, OpenGL transform z_over_w from [-1, 1] to [near, far]. Here we use
|
||||||
|
@ -1527,7 +1533,7 @@ vec4 secondary_fragment_color = vec4(0.0);
|
||||||
if (state.fog_mode == TexturingRegs::FogMode::Fog) {
|
if (state.fog_mode == TexturingRegs::FogMode::Fog) {
|
||||||
// Get index into fog LUT
|
// Get index into fog LUT
|
||||||
if (state.fog_flip) {
|
if (state.fog_flip) {
|
||||||
out += "float fog_index = (1.0 - depth) * 128.0;\n";
|
out += "float fog_index = (1.0 - float(depth)) * 128.0;\n";
|
||||||
} else {
|
} else {
|
||||||
out += "float fog_index = depth * 128.0;\n";
|
out += "float fog_index = depth * 128.0;\n";
|
||||||
}
|
}
|
||||||
|
@ -1589,7 +1595,7 @@ do {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string GenerateTrivialVertexShader(bool separable_shader) {
|
std::string GenerateTrivialVertexShader(bool separable_shader) {
|
||||||
std::string out = "#version 330 core\n";
|
std::string out = "";
|
||||||
if (separable_shader) {
|
if (separable_shader) {
|
||||||
out += "#extension GL_ARB_separate_shader_objects : enable\n";
|
out += "#extension GL_ARB_separate_shader_objects : enable\n";
|
||||||
}
|
}
|
||||||
|
@ -1624,8 +1630,10 @@ void main() {
|
||||||
normquat = vert_normquat;
|
normquat = vert_normquat;
|
||||||
view = vert_view;
|
view = vert_view;
|
||||||
gl_Position = vert_position;
|
gl_Position = vert_position;
|
||||||
|
#if !defined(CITRA_GLES) || defined(GL_EXT_clip_cull_distance)
|
||||||
gl_ClipDistance[0] = -vert_position.z; // fixed PICA clipping plane z <= 0
|
gl_ClipDistance[0] = -vert_position.z; // fixed PICA clipping plane z <= 0
|
||||||
gl_ClipDistance[1] = dot(clip_coef, vert_position);
|
gl_ClipDistance[1] = dot(clip_coef, vert_position);
|
||||||
|
#endif // !defined(CITRA_GLES) || defined(GL_EXT_clip_cull_distance)
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
@ -1634,7 +1642,7 @@ void main() {
|
||||||
|
|
||||||
std::optional<std::string> GenerateVertexShader(const Pica::Shader::ShaderSetup& setup,
|
std::optional<std::string> GenerateVertexShader(const Pica::Shader::ShaderSetup& setup,
|
||||||
const PicaVSConfig& config, bool separable_shader) {
|
const PicaVSConfig& config, bool separable_shader) {
|
||||||
std::string out = "#version 330 core\n";
|
std::string out = "";
|
||||||
if (separable_shader) {
|
if (separable_shader) {
|
||||||
out += "#extension GL_ARB_separate_shader_objects : enable\n";
|
out += "#extension GL_ARB_separate_shader_objects : enable\n";
|
||||||
}
|
}
|
||||||
|
@ -1742,9 +1750,12 @@ struct Vertex {
|
||||||
semantic(VSOutputAttributes::POSITION_Y) + ", " +
|
semantic(VSOutputAttributes::POSITION_Y) + ", " +
|
||||||
semantic(VSOutputAttributes::POSITION_Z) + ", " +
|
semantic(VSOutputAttributes::POSITION_Z) + ", " +
|
||||||
semantic(VSOutputAttributes::POSITION_W) + ");\n";
|
semantic(VSOutputAttributes::POSITION_W) + ");\n";
|
||||||
|
semantic(VSOutputAttributes::POSITION_W) + ");\n";
|
||||||
out += " gl_Position = vtx_pos;\n";
|
out += " gl_Position = vtx_pos;\n";
|
||||||
|
out += "#if !defined(CITRA_GLES) || defined(GL_EXT_clip_cull_distance)\n";
|
||||||
out += " gl_ClipDistance[0] = -vtx_pos.z;\n"; // fixed PICA clipping plane z <= 0
|
out += " gl_ClipDistance[0] = -vtx_pos.z;\n"; // fixed PICA clipping plane z <= 0
|
||||||
out += " gl_ClipDistance[1] = dot(clip_coef, vtx_pos);\n\n";
|
out += " gl_ClipDistance[1] = dot(clip_coef, vtx_pos);\n";
|
||||||
|
out += "#endif // !defined(CITRA_GLES) || defined(GL_EXT_clip_cull_distance)\n\n";
|
||||||
|
|
||||||
out += " vec4 vtx_quat = GetVertexQuaternion(vtx);\n";
|
out += " vec4 vtx_quat = GetVertexQuaternion(vtx);\n";
|
||||||
out += " normquat = mix(vtx_quat, -vtx_quat, bvec4(quats_opposite));\n\n";
|
out += " normquat = mix(vtx_quat, -vtx_quat, bvec4(quats_opposite));\n\n";
|
||||||
|
@ -1787,7 +1798,7 @@ void EmitPrim(Vertex vtx0, Vertex vtx1, Vertex vtx2) {
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string GenerateFixedGeometryShader(const PicaFixedGSConfig& config, bool separable_shader) {
|
std::string GenerateFixedGeometryShader(const PicaFixedGSConfig& config, bool separable_shader) {
|
||||||
std::string out = "#version 330 core\n";
|
std::string out = "";
|
||||||
if (separable_shader) {
|
if (separable_shader) {
|
||||||
out += "#extension GL_ARB_separate_shader_objects : enable\n\n";
|
out += "#extension GL_ARB_separate_shader_objects : enable\n\n";
|
||||||
}
|
}
|
||||||
|
@ -1822,7 +1833,7 @@ void main() {
|
||||||
std::optional<std::string> GenerateGeometryShader(const Pica::Shader::ShaderSetup& setup,
|
std::optional<std::string> GenerateGeometryShader(const Pica::Shader::ShaderSetup& setup,
|
||||||
const PicaGSConfig& config,
|
const PicaGSConfig& config,
|
||||||
bool separable_shader) {
|
bool separable_shader) {
|
||||||
std::string out = "#version 330 core\n";
|
std::string out = "";
|
||||||
if (separable_shader) {
|
if (separable_shader) {
|
||||||
out += "#extension GL_ARB_separate_shader_objects : enable\n";
|
out += "#extension GL_ARB_separate_shader_objects : enable\n";
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,33 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <glad/glad.h>
|
#include <glad/glad.h>
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "video_core/renderer_opengl/gl_shader_util.h"
|
#include "video_core/renderer_opengl/gl_shader_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
GLuint LoadShader(const char* source, GLenum type) {
|
GLuint LoadShader(const char* source, GLenum type) {
|
||||||
|
const std::string version = GLES ? R"(
|
||||||
|
#version 310 es
|
||||||
|
|
||||||
|
#define CITRA_GLES
|
||||||
|
|
||||||
|
#if defined(GL_ANDROID_extension_pack_es31a)
|
||||||
|
#extension GL_ANDROID_extension_pack_es31a : enable
|
||||||
|
#endif // defined(GL_ANDROID_extension_pack_es31a)
|
||||||
|
|
||||||
|
#if defined(GL_EXT_clip_cull_distance)
|
||||||
|
#extension GL_EXT_clip_cull_distance : enable
|
||||||
|
#endif // defined(GL_EXT_clip_cull_distance)
|
||||||
|
)"
|
||||||
|
: "#version 330\n";
|
||||||
|
|
||||||
const char* debug_type;
|
const char* debug_type;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case GL_VERTEX_SHADER:
|
case GL_VERTEX_SHADER:
|
||||||
|
@ -26,8 +44,9 @@ GLuint LoadShader(const char* source, GLenum type) {
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::array<const char*, 2> src_arr{version.data(), source};
|
||||||
GLuint shader_id = glCreateShader(type);
|
GLuint shader_id = glCreateShader(type);
|
||||||
glShaderSource(shader_id, 1, &source, nullptr);
|
glShaderSource(shader_id, static_cast<GLsizei>(src_arr.size()), src_arr.data(), nullptr);
|
||||||
LOG_DEBUG(Render_OpenGL, "Compiling {} shader...", debug_type);
|
LOG_DEBUG(Render_OpenGL, "Compiling {} shader...", debug_type);
|
||||||
glCompileShader(shader_id);
|
glCompileShader(shader_id);
|
||||||
|
|
||||||
|
@ -44,7 +63,7 @@ GLuint LoadShader(const char* source, GLenum type) {
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR(Render_OpenGL, "Error compiling {} shader:\n{}", debug_type,
|
LOG_ERROR(Render_OpenGL, "Error compiling {} shader:\n{}", debug_type,
|
||||||
&shader_error[0]);
|
&shader_error[0]);
|
||||||
LOG_ERROR(Render_OpenGL, "Shader source code:\n{}", source);
|
LOG_ERROR(Render_OpenGL, "Shader source code:\n{}{}", src_arr[0], src_arr[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shader_id;
|
return shader_id;
|
||||||
|
|
|
@ -9,6 +9,17 @@
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
|
// High precision may or may not supported in GLES3. If it isn't, use medium precision instead.
|
||||||
|
static constexpr char fragment_shader_precision_OES[] = R"(
|
||||||
|
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
||||||
|
precision highp float;
|
||||||
|
precision highp samplerBuffer;
|
||||||
|
#else
|
||||||
|
precision mediump float;
|
||||||
|
precision mediump samplerBuffer;
|
||||||
|
#endif // GL_FRAGMENT_PRECISION_HIGH
|
||||||
|
)";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function to create and compile an OpenGL GLSL shader
|
* Utility function to create and compile an OpenGL GLSL shader
|
||||||
* @param source String of the GLSL shader program
|
* @param source String of the GLSL shader program
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "common/common_funcs.h"
|
#include "common/common_funcs.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
|
@ -193,9 +194,14 @@ void OpenGLState::Apply() const {
|
||||||
glBlendEquationSeparate(blend.rgb_equation, blend.a_equation);
|
glBlendEquationSeparate(blend.rgb_equation, blend.a_equation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GLES3 does not support glLogicOp
|
||||||
|
if (!GLES) {
|
||||||
if (logic_op != cur_state.logic_op) {
|
if (logic_op != cur_state.logic_op) {
|
||||||
glLogicOp(logic_op);
|
glLogicOp(logic_op);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
LOG_TRACE(Render_OpenGL, "glLogicOps are unimplemented...");
|
||||||
|
}
|
||||||
|
|
||||||
// Textures
|
// Textures
|
||||||
for (unsigned i = 0; i < ARRAY_SIZE(texture_units); ++i) {
|
for (unsigned i = 0; i < ARRAY_SIZE(texture_units); ++i) {
|
||||||
|
@ -319,7 +325,8 @@ void OpenGLState::Apply() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clip distance
|
// Clip distance
|
||||||
for (std::size_t i = 0; i < clip_distance.size(); ++i) {
|
if (!GLES || GLAD_GL_EXT_clip_cull_distance) {
|
||||||
|
for (size_t i = 0; i < clip_distance.size(); ++i) {
|
||||||
if (clip_distance[i] != cur_state.clip_distance[i]) {
|
if (clip_distance[i] != cur_state.clip_distance[i]) {
|
||||||
if (clip_distance[i]) {
|
if (clip_distance[i]) {
|
||||||
glEnable(GL_CLIP_DISTANCE0 + static_cast<GLenum>(i));
|
glEnable(GL_CLIP_DISTANCE0 + static_cast<GLenum>(i));
|
||||||
|
@ -328,6 +335,7 @@ void OpenGLState::Apply() const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cur_state = *this;
|
cur_state = *this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2019 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
bool GLES;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2019 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
extern bool GLES;
|
||||||
|
}
|
|
@ -21,14 +21,13 @@
|
||||||
#include "core/tracer/recorder.h"
|
#include "core/tracer/recorder.h"
|
||||||
#include "video_core/debug_utils/debug_utils.h"
|
#include "video_core/debug_utils/debug_utils.h"
|
||||||
#include "video_core/rasterizer_interface.h"
|
#include "video_core/rasterizer_interface.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
#include "video_core/renderer_opengl/renderer_opengl.h"
|
#include "video_core/renderer_opengl/renderer_opengl.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
static const char vertex_shader[] = R"(
|
static const char vertex_shader[] = R"(
|
||||||
#version 150 core
|
|
||||||
|
|
||||||
in vec2 vert_position;
|
in vec2 vert_position;
|
||||||
in vec2 vert_tex_coord;
|
in vec2 vert_tex_coord;
|
||||||
out vec2 frag_tex_coord;
|
out vec2 frag_tex_coord;
|
||||||
|
@ -50,8 +49,6 @@ void main() {
|
||||||
)";
|
)";
|
||||||
|
|
||||||
static const char fragment_shader[] = R"(
|
static const char fragment_shader[] = R"(
|
||||||
#version 150 core
|
|
||||||
|
|
||||||
in vec2 frag_tex_coord;
|
in vec2 frag_tex_coord;
|
||||||
out vec4 color;
|
out vec4 color;
|
||||||
|
|
||||||
|
@ -279,7 +276,13 @@ void RendererOpenGL::InitOpenGLObjects() {
|
||||||
0.0f);
|
0.0f);
|
||||||
|
|
||||||
// Link shaders and get variable locations
|
// Link shaders and get variable locations
|
||||||
|
if (GLES) {
|
||||||
|
std::string frag_source(fragment_shader_precision_OES);
|
||||||
|
frag_source += fragment_shader;
|
||||||
|
shader.Create(vertex_shader, frag_source.data());
|
||||||
|
} else {
|
||||||
shader.Create(vertex_shader, fragment_shader);
|
shader.Create(vertex_shader, fragment_shader);
|
||||||
|
}
|
||||||
state.draw.shader_program = shader.handle;
|
state.draw.shader_program = shader.handle;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
uniform_modelview_matrix = glGetUniformLocation(shader.handle, "modelview_matrix");
|
uniform_modelview_matrix = glGetUniformLocation(shader.handle, "modelview_matrix");
|
||||||
|
@ -344,7 +347,7 @@ void RendererOpenGL::ConfigureFramebufferTexture(TextureInfo& texture,
|
||||||
case GPU::Regs::PixelFormat::RGBA8:
|
case GPU::Regs::PixelFormat::RGBA8:
|
||||||
internal_format = GL_RGBA;
|
internal_format = GL_RGBA;
|
||||||
texture.gl_format = GL_RGBA;
|
texture.gl_format = GL_RGBA;
|
||||||
texture.gl_type = GL_UNSIGNED_INT_8_8_8_8;
|
texture.gl_type = GLES ? GL_UNSIGNED_BYTE : GL_UNSIGNED_INT_8_8_8_8;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GPU::Regs::PixelFormat::RGB8:
|
case GPU::Regs::PixelFormat::RGB8:
|
||||||
|
@ -353,7 +356,9 @@ void RendererOpenGL::ConfigureFramebufferTexture(TextureInfo& texture,
|
||||||
// mostly everywhere) for words or half-words.
|
// mostly everywhere) for words or half-words.
|
||||||
// TODO: check how those behave on big-endian processors.
|
// TODO: check how those behave on big-endian processors.
|
||||||
internal_format = GL_RGB;
|
internal_format = GL_RGB;
|
||||||
texture.gl_format = GL_BGR;
|
|
||||||
|
// GLES Dosen't support BGR , Use RGB instead
|
||||||
|
texture.gl_format = GLES ? GL_RGB : GL_BGR;
|
||||||
texture.gl_type = GL_UNSIGNED_BYTE;
|
texture.gl_type = GL_UNSIGNED_BYTE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -555,7 +560,7 @@ Core::System::ResultStatus RendererOpenGL::Init() {
|
||||||
return Core::System::ResultStatus::ErrorVideoCore_ErrorGenericDrivers;
|
return Core::System::ResultStatus::ErrorVideoCore_ErrorGenericDrivers;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GLAD_GL_VERSION_3_3) {
|
if (!(GLAD_GL_VERSION_3_3 || GLAD_GL_ES_VERSION_3_1)) {
|
||||||
return Core::System::ResultStatus::ErrorVideoCore_ErrorBelowGL33;
|
return Core::System::ResultStatus::ErrorVideoCore_ErrorBelowGL33;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "video_core/pica.h"
|
#include "video_core/pica.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
#include "video_core/renderer_opengl/renderer_opengl.h"
|
#include "video_core/renderer_opengl/renderer_opengl.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
|
@ -36,6 +37,8 @@ Core::System::ResultStatus Init(EmuWindow& emu_window, Memory::MemorySystem& mem
|
||||||
g_memory = &memory;
|
g_memory = &memory;
|
||||||
Pica::Init();
|
Pica::Init();
|
||||||
|
|
||||||
|
OpenGL::GLES = Settings::values.use_gles;
|
||||||
|
|
||||||
g_renderer = std::make_unique<OpenGL::RendererOpenGL>(emu_window);
|
g_renderer = std::make_unique<OpenGL::RendererOpenGL>(emu_window);
|
||||||
Core::System::ResultStatus result = g_renderer->Init();
|
Core::System::ResultStatus result = g_renderer->Init();
|
||||||
|
|
||||||
|
|
Reference in New Issue