gl_state: Remove program tracking
This commit is contained in:
parent
5ccb07933a
commit
1c4bf9cbfa
|
@ -99,14 +99,11 @@ void oglEnablei(GLenum cap, bool state, GLuint index) {
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
RasterizerOpenGL::RasterizerOpenGL(Core::System& system, Core::Frontend::EmuWindow& emu_window,
|
RasterizerOpenGL::RasterizerOpenGL(Core::System& system, Core::Frontend::EmuWindow& emu_window,
|
||||||
ScreenInfo& info)
|
ScreenInfo& info, GLShader::ProgramManager& program_manager)
|
||||||
: RasterizerAccelerated{system.Memory()}, texture_cache{system, *this, device},
|
: RasterizerAccelerated{system.Memory()}, texture_cache{system, *this, device},
|
||||||
shader_cache{*this, system, emu_window, device}, query_cache{system, *this}, system{system},
|
shader_cache{*this, system, emu_window, device}, query_cache{system, *this}, system{system},
|
||||||
screen_info{info}, buffer_cache{*this, system, device, STREAM_BUFFER_SIZE} {
|
screen_info{info}, program_manager{program_manager}, buffer_cache{*this, system, device,
|
||||||
shader_program_manager = std::make_unique<GLShader::ProgramManager>();
|
STREAM_BUFFER_SIZE} {
|
||||||
state.draw.shader_program = 0;
|
|
||||||
state.Apply();
|
|
||||||
|
|
||||||
CheckExtensions();
|
CheckExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,10 +225,10 @@ void RasterizerOpenGL::SetupShaders(GLenum primitive_mode) {
|
||||||
if (!gpu.regs.IsShaderConfigEnabled(index)) {
|
if (!gpu.regs.IsShaderConfigEnabled(index)) {
|
||||||
switch (program) {
|
switch (program) {
|
||||||
case Maxwell::ShaderProgram::Geometry:
|
case Maxwell::ShaderProgram::Geometry:
|
||||||
shader_program_manager->UseTrivialGeometryShader();
|
program_manager.UseGeometryShader(0);
|
||||||
break;
|
break;
|
||||||
case Maxwell::ShaderProgram::Fragment:
|
case Maxwell::ShaderProgram::Fragment:
|
||||||
shader_program_manager->UseTrivialFragmentShader();
|
program_manager.UseFragmentShader(0);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -262,13 +259,13 @@ void RasterizerOpenGL::SetupShaders(GLenum primitive_mode) {
|
||||||
switch (program) {
|
switch (program) {
|
||||||
case Maxwell::ShaderProgram::VertexA:
|
case Maxwell::ShaderProgram::VertexA:
|
||||||
case Maxwell::ShaderProgram::VertexB:
|
case Maxwell::ShaderProgram::VertexB:
|
||||||
shader_program_manager->UseProgrammableVertexShader(program_handle);
|
program_manager.UseVertexShader(program_handle);
|
||||||
break;
|
break;
|
||||||
case Maxwell::ShaderProgram::Geometry:
|
case Maxwell::ShaderProgram::Geometry:
|
||||||
shader_program_manager->UseProgrammableGeometryShader(program_handle);
|
program_manager.UseGeometryShader(program_handle);
|
||||||
break;
|
break;
|
||||||
case Maxwell::ShaderProgram::Fragment:
|
case Maxwell::ShaderProgram::Fragment:
|
||||||
shader_program_manager->UseProgrammableFragmentShader(program_handle);
|
program_manager.UseFragmentShader(program_handle);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
UNIMPLEMENTED_MSG("Unimplemented shader index={}, enable={}, offset=0x{:08X}", index,
|
UNIMPLEMENTED_MSG("Unimplemented shader index={}, enable={}, offset=0x{:08X}", index,
|
||||||
|
@ -550,7 +547,7 @@ void RasterizerOpenGL::Draw(bool is_indexed, bool is_instanced) {
|
||||||
bind_ubo_pushbuffer.Bind();
|
bind_ubo_pushbuffer.Bind();
|
||||||
bind_ssbo_pushbuffer.Bind();
|
bind_ssbo_pushbuffer.Bind();
|
||||||
|
|
||||||
shader_program_manager->ApplyTo(state);
|
program_manager.Update();
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
if (texture_cache.TextureBarrier()) {
|
if (texture_cache.TextureBarrier()) {
|
||||||
|
@ -613,8 +610,8 @@ void RasterizerOpenGL::DispatchCompute(GPUVAddr code_addr) {
|
||||||
const ProgramVariant variant(launch_desc.block_dim_x, launch_desc.block_dim_y,
|
const ProgramVariant variant(launch_desc.block_dim_x, launch_desc.block_dim_y,
|
||||||
launch_desc.block_dim_z, launch_desc.shared_alloc,
|
launch_desc.block_dim_z, launch_desc.shared_alloc,
|
||||||
launch_desc.local_pos_alloc);
|
launch_desc.local_pos_alloc);
|
||||||
state.draw.shader_program = kernel->GetHandle(variant);
|
glUseProgramStages(program_manager.GetHandle(), GL_COMPUTE_SHADER_BIT,
|
||||||
state.draw.program_pipeline = 0;
|
kernel->GetHandle(variant));
|
||||||
|
|
||||||
const std::size_t buffer_size =
|
const std::size_t buffer_size =
|
||||||
Tegra::Engines::KeplerCompute::NumConstBuffers *
|
Tegra::Engines::KeplerCompute::NumConstBuffers *
|
||||||
|
@ -632,9 +629,6 @@ void RasterizerOpenGL::DispatchCompute(GPUVAddr code_addr) {
|
||||||
bind_ubo_pushbuffer.Bind();
|
bind_ubo_pushbuffer.Bind();
|
||||||
bind_ssbo_pushbuffer.Bind();
|
bind_ssbo_pushbuffer.Bind();
|
||||||
|
|
||||||
state.ApplyShaderProgram();
|
|
||||||
state.ApplyProgramPipeline();
|
|
||||||
|
|
||||||
glDispatchCompute(launch_desc.grid_dim_x, launch_desc.grid_dim_y, launch_desc.grid_dim_z);
|
glDispatchCompute(launch_desc.grid_dim_x, launch_desc.grid_dim_y, launch_desc.grid_dim_z);
|
||||||
++num_queued_commands;
|
++num_queued_commands;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ struct DrawParameters;
|
||||||
class RasterizerOpenGL : public VideoCore::RasterizerAccelerated {
|
class RasterizerOpenGL : public VideoCore::RasterizerAccelerated {
|
||||||
public:
|
public:
|
||||||
explicit RasterizerOpenGL(Core::System& system, Core::Frontend::EmuWindow& emu_window,
|
explicit RasterizerOpenGL(Core::System& system, Core::Frontend::EmuWindow& emu_window,
|
||||||
ScreenInfo& info);
|
ScreenInfo& info, GLShader::ProgramManager& program_manager);
|
||||||
~RasterizerOpenGL() override;
|
~RasterizerOpenGL() override;
|
||||||
|
|
||||||
void Draw(bool is_indexed, bool is_instanced) override;
|
void Draw(bool is_indexed, bool is_instanced) override;
|
||||||
|
@ -218,8 +218,7 @@ private:
|
||||||
|
|
||||||
Core::System& system;
|
Core::System& system;
|
||||||
ScreenInfo& screen_info;
|
ScreenInfo& screen_info;
|
||||||
|
GLShader::ProgramManager& program_manager;
|
||||||
std::unique_ptr<GLShader::ProgramManager> shader_program_manager;
|
|
||||||
|
|
||||||
static constexpr std::size_t STREAM_BUFFER_SIZE = 128 * 1024 * 1024;
|
static constexpr std::size_t STREAM_BUFFER_SIZE = 128 * 1024 * 1024;
|
||||||
OGLBufferCache buffer_cache;
|
OGLBufferCache buffer_cache;
|
||||||
|
|
|
@ -123,7 +123,6 @@ void OGLProgram::Release() {
|
||||||
|
|
||||||
MICROPROFILE_SCOPE(OpenGL_ResourceDeletion);
|
MICROPROFILE_SCOPE(OpenGL_ResourceDeletion);
|
||||||
glDeleteProgram(handle);
|
glDeleteProgram(handle);
|
||||||
OpenGLState::GetCurState().ResetProgram(handle).Apply();
|
|
||||||
handle = 0;
|
handle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +140,6 @@ void OGLPipeline::Release() {
|
||||||
|
|
||||||
MICROPROFILE_SCOPE(OpenGL_ResourceDeletion);
|
MICROPROFILE_SCOPE(OpenGL_ResourceDeletion);
|
||||||
glDeleteProgramPipelines(1, &handle);
|
glDeleteProgramPipelines(1, &handle);
|
||||||
OpenGLState::GetCurState().ResetPipeline(handle).Apply();
|
|
||||||
handle = 0;
|
handle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,27 +10,21 @@ namespace OpenGL::GLShader {
|
||||||
|
|
||||||
using Tegra::Engines::Maxwell3D;
|
using Tegra::Engines::Maxwell3D;
|
||||||
|
|
||||||
ProgramManager::ProgramManager() {
|
ProgramManager::~ProgramManager() = default;
|
||||||
|
|
||||||
|
void ProgramManager::Create() {
|
||||||
pipeline.Create();
|
pipeline.Create();
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgramManager::~ProgramManager() = default;
|
void ProgramManager::Update() {
|
||||||
|
|
||||||
void ProgramManager::ApplyTo(OpenGLState& state) {
|
|
||||||
UpdatePipeline();
|
|
||||||
state.draw.shader_program = 0;
|
|
||||||
state.draw.program_pipeline = pipeline.handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProgramManager::UpdatePipeline() {
|
|
||||||
// Avoid updating the pipeline when values have no changed
|
// Avoid updating the pipeline when values have no changed
|
||||||
if (old_state == current_state) {
|
if (old_state == current_state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for AMD bug
|
// Workaround for AMD bug
|
||||||
constexpr GLenum all_used_stages{GL_VERTEX_SHADER_BIT | GL_GEOMETRY_SHADER_BIT |
|
static constexpr GLenum all_used_stages{GL_VERTEX_SHADER_BIT | GL_GEOMETRY_SHADER_BIT |
|
||||||
GL_FRAGMENT_SHADER_BIT};
|
GL_FRAGMENT_SHADER_BIT};
|
||||||
glUseProgramStages(pipeline.handle, all_used_stages, 0);
|
glUseProgramStages(pipeline.handle, all_used_stages, 0);
|
||||||
|
|
||||||
glUseProgramStages(pipeline.handle, GL_VERTEX_SHADER_BIT, current_state.vertex_shader);
|
glUseProgramStages(pipeline.handle, GL_VERTEX_SHADER_BIT, current_state.vertex_shader);
|
||||||
|
|
|
@ -29,25 +29,26 @@ static_assert(sizeof(MaxwellUniformData) < 16384,
|
||||||
|
|
||||||
class ProgramManager {
|
class ProgramManager {
|
||||||
public:
|
public:
|
||||||
explicit ProgramManager();
|
|
||||||
~ProgramManager();
|
~ProgramManager();
|
||||||
|
|
||||||
void ApplyTo(OpenGLState& state);
|
void Create();
|
||||||
|
|
||||||
void UseProgrammableVertexShader(GLuint program) {
|
void Update();
|
||||||
|
|
||||||
|
void UseVertexShader(GLuint program) {
|
||||||
current_state.vertex_shader = program;
|
current_state.vertex_shader = program;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UseProgrammableGeometryShader(GLuint program) {
|
void UseGeometryShader(GLuint program) {
|
||||||
current_state.geometry_shader = program;
|
current_state.geometry_shader = program;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UseProgrammableFragmentShader(GLuint program) {
|
void UseFragmentShader(GLuint program) {
|
||||||
current_state.fragment_shader = program;
|
current_state.fragment_shader = program;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UseTrivialGeometryShader() {
|
GLuint GetHandle() const {
|
||||||
current_state.geometry_shader = 0;
|
return pipeline.handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UseTrivialFragmentShader() {
|
void UseTrivialFragmentShader() {
|
||||||
|
@ -70,8 +71,6 @@ private:
|
||||||
GLuint geometry_shader{};
|
GLuint geometry_shader{};
|
||||||
};
|
};
|
||||||
|
|
||||||
void UpdatePipeline();
|
|
||||||
|
|
||||||
OGLPipeline pipeline;
|
OGLPipeline pipeline;
|
||||||
PipelineState current_state;
|
PipelineState current_state;
|
||||||
PipelineState old_state;
|
PipelineState old_state;
|
||||||
|
|
|
@ -85,36 +85,6 @@ void Enable(GLenum cap, GLuint index, bool& current_value, bool new_value) {
|
||||||
|
|
||||||
OpenGLState::OpenGLState() = default;
|
OpenGLState::OpenGLState() = default;
|
||||||
|
|
||||||
void OpenGLState::ApplyShaderProgram() {
|
void OpenGLState::Apply() {}
|
||||||
if (UpdateValue(cur_state.draw.shader_program, draw.shader_program)) {
|
|
||||||
glUseProgram(draw.shader_program);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLState::ApplyProgramPipeline() {
|
|
||||||
if (UpdateValue(cur_state.draw.program_pipeline, draw.program_pipeline)) {
|
|
||||||
glBindProgramPipeline(draw.program_pipeline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLState::Apply() {
|
|
||||||
MICROPROFILE_SCOPE(OpenGL_State);
|
|
||||||
ApplyShaderProgram();
|
|
||||||
ApplyProgramPipeline();
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenGLState& OpenGLState::ResetProgram(GLuint handle) {
|
|
||||||
if (draw.shader_program == handle) {
|
|
||||||
draw.shader_program = 0;
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenGLState& OpenGLState::ResetPipeline(GLuint handle) {
|
|
||||||
if (draw.program_pipeline == handle) {
|
|
||||||
draw.program_pipeline = 0;
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -13,11 +13,6 @@ namespace OpenGL {
|
||||||
|
|
||||||
class OpenGLState {
|
class OpenGLState {
|
||||||
public:
|
public:
|
||||||
struct {
|
|
||||||
GLuint shader_program = 0; // GL_CURRENT_PROGRAM
|
|
||||||
GLuint program_pipeline = 0; // GL_PROGRAM_PIPELINE_BINDING
|
|
||||||
} draw;
|
|
||||||
|
|
||||||
OpenGLState();
|
OpenGLState();
|
||||||
|
|
||||||
/// Get the currently active OpenGL state
|
/// Get the currently active OpenGL state
|
||||||
|
@ -28,13 +23,6 @@ public:
|
||||||
/// Apply this state as the current OpenGL state
|
/// Apply this state as the current OpenGL state
|
||||||
void Apply();
|
void Apply();
|
||||||
|
|
||||||
void ApplyShaderProgram();
|
|
||||||
void ApplyProgramPipeline();
|
|
||||||
|
|
||||||
/// Resets any references to the given resource
|
|
||||||
OpenGLState& ResetProgram(GLuint handle);
|
|
||||||
OpenGLState& ResetPipeline(GLuint handle);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static OpenGLState cur_state;
|
static OpenGLState cur_state;
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
#include "core/telemetry_session.h"
|
#include "core/telemetry_session.h"
|
||||||
#include "video_core/morton.h"
|
#include "video_core/morton.h"
|
||||||
#include "video_core/renderer_opengl/gl_rasterizer.h"
|
#include "video_core/renderer_opengl/gl_rasterizer.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_shader_manager.h"
|
||||||
#include "video_core/renderer_opengl/renderer_opengl.h"
|
#include "video_core/renderer_opengl/renderer_opengl.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
@ -158,9 +159,13 @@ public:
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr char vertex_shader[] = R"(
|
constexpr char VERTEX_SHADER[] = R"(
|
||||||
#version 430 core
|
#version 430 core
|
||||||
|
|
||||||
|
out gl_PerVertex {
|
||||||
|
vec4 gl_Position;
|
||||||
|
};
|
||||||
|
|
||||||
layout (location = 0) in vec2 vert_position;
|
layout (location = 0) in vec2 vert_position;
|
||||||
layout (location = 1) in vec2 vert_tex_coord;
|
layout (location = 1) in vec2 vert_tex_coord;
|
||||||
layout (location = 0) out vec2 frag_tex_coord;
|
layout (location = 0) out vec2 frag_tex_coord;
|
||||||
|
@ -181,7 +186,7 @@ void main() {
|
||||||
}
|
}
|
||||||
)";
|
)";
|
||||||
|
|
||||||
constexpr char fragment_shader[] = R"(
|
constexpr char FRAGMENT_SHADER[] = R"(
|
||||||
#version 430 core
|
#version 430 core
|
||||||
|
|
||||||
layout (location = 0) in vec2 frag_tex_coord;
|
layout (location = 0) in vec2 frag_tex_coord;
|
||||||
|
@ -426,10 +431,19 @@ void RendererOpenGL::InitOpenGLObjects() {
|
||||||
glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue,
|
glClearColor(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue,
|
||||||
0.0f);
|
0.0f);
|
||||||
|
|
||||||
// Link shaders and get variable locations
|
// Create shader programs
|
||||||
shader.CreateFromSource(vertex_shader, nullptr, fragment_shader);
|
OGLShader vertex_shader;
|
||||||
state.draw.shader_program = shader.handle;
|
vertex_shader.Create(VERTEX_SHADER, GL_VERTEX_SHADER);
|
||||||
state.Apply();
|
|
||||||
|
OGLShader fragment_shader;
|
||||||
|
fragment_shader.Create(FRAGMENT_SHADER, GL_FRAGMENT_SHADER);
|
||||||
|
|
||||||
|
vertex_program.Create(true, false, vertex_shader.handle);
|
||||||
|
fragment_program.Create(true, false, fragment_shader.handle);
|
||||||
|
|
||||||
|
// Create program pipeline
|
||||||
|
program_manager.Create();
|
||||||
|
glBindProgramPipeline(program_manager.GetHandle());
|
||||||
|
|
||||||
// Generate VBO handle for drawing
|
// Generate VBO handle for drawing
|
||||||
vertex_buffer.Create();
|
vertex_buffer.Create();
|
||||||
|
@ -468,7 +482,8 @@ void RendererOpenGL::CreateRasterizer() {
|
||||||
if (rasterizer) {
|
if (rasterizer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rasterizer = std::make_unique<RasterizerOpenGL>(system, emu_window, screen_info);
|
rasterizer =
|
||||||
|
std::make_unique<RasterizerOpenGL>(system, emu_window, screen_info, program_manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RendererOpenGL::ConfigureFramebufferTexture(TextureInfo& texture,
|
void RendererOpenGL::ConfigureFramebufferTexture(TextureInfo& texture,
|
||||||
|
@ -517,7 +532,8 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) {
|
||||||
// Set projection matrix
|
// Set projection matrix
|
||||||
const std::array ortho_matrix =
|
const std::array ortho_matrix =
|
||||||
MakeOrthographicMatrix(static_cast<float>(layout.width), static_cast<float>(layout.height));
|
MakeOrthographicMatrix(static_cast<float>(layout.width), static_cast<float>(layout.height));
|
||||||
glUniformMatrix3x2fv(ModelViewMatrixLocation, 1, GL_FALSE, ortho_matrix.data());
|
glProgramUniformMatrix3x2fv(vertex_program.handle, ModelViewMatrixLocation, 1, GL_FALSE,
|
||||||
|
std::data(ortho_matrix));
|
||||||
|
|
||||||
const auto& texcoords = screen_info.display_texcoords;
|
const auto& texcoords = screen_info.display_texcoords;
|
||||||
auto left = texcoords.left;
|
auto left = texcoords.left;
|
||||||
|
@ -562,6 +578,11 @@ void RendererOpenGL::DrawScreen(const Layout::FramebufferLayout& layout) {
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
// TODO: Signal state tracker about these changes
|
// TODO: Signal state tracker about these changes
|
||||||
|
program_manager.UseVertexShader(vertex_program.handle);
|
||||||
|
program_manager.UseGeometryShader(0);
|
||||||
|
program_manager.UseFragmentShader(fragment_program.handle);
|
||||||
|
program_manager.Update();
|
||||||
|
|
||||||
glEnable(GL_CULL_FACE);
|
glEnable(GL_CULL_FACE);
|
||||||
if (screen_info.display_srgb) {
|
if (screen_info.display_srgb) {
|
||||||
glEnable(GL_FRAMEBUFFER_SRGB);
|
glEnable(GL_FRAMEBUFFER_SRGB);
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include "common/math_util.h"
|
#include "common/math_util.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_shader_manager.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
@ -95,12 +96,16 @@ private:
|
||||||
|
|
||||||
// OpenGL object IDs
|
// OpenGL object IDs
|
||||||
OGLBuffer vertex_buffer;
|
OGLBuffer vertex_buffer;
|
||||||
OGLProgram shader;
|
OGLProgram vertex_program;
|
||||||
|
OGLProgram fragment_program;
|
||||||
OGLFramebuffer screenshot_framebuffer;
|
OGLFramebuffer screenshot_framebuffer;
|
||||||
|
|
||||||
/// Display information for Switch screen
|
/// Display information for Switch screen
|
||||||
ScreenInfo screen_info;
|
ScreenInfo screen_info;
|
||||||
|
|
||||||
|
/// Global dummy shader pipeline
|
||||||
|
GLShader::ProgramManager program_manager;
|
||||||
|
|
||||||
/// OpenGL framebuffer data
|
/// OpenGL framebuffer data
|
||||||
std::vector<u8> gl_framebuffer_data;
|
std::vector<u8> gl_framebuffer_data;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue