mirror of https://github.com/snes9xgit/snes9x.git
Slang shader support.
This commit is contained in:
parent
30c50f4fc4
commit
01f4fed8b5
|
@ -78,6 +78,32 @@ if opengl
|
|||
endif
|
||||
endif
|
||||
|
||||
slang = get_option('slang')
|
||||
if slang and opengl
|
||||
glslang_dep = c_compiler.find_library('glslang')
|
||||
spirv_dep = c_compiler.find_library('SPIRV')
|
||||
osdependent_dep = c_compiler.find_library('OSDependent')
|
||||
hlsl_dep = c_compiler.find_library('HLSL')
|
||||
ogl_compiler_dep = c_compiler.find_library('OGLCompiler')
|
||||
spv_remapper_dep = c_compiler.find_library('SPVRemapper')
|
||||
deps += [glslang_dep, spirv_dep, osdependent_dep, hlsl_dep, ogl_compiler_dep, spv_remapper_dep]
|
||||
|
||||
args += ['-DUSE_SLANG', '-DSPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS']
|
||||
srcs += ['../shaders/slang.cpp']
|
||||
srcs += ['../shaders/SPIRV-Cross/spirv_cfg.cpp',
|
||||
'../shaders/SPIRV-Cross/spirv_cfg.hpp',
|
||||
'../shaders/SPIRV-Cross/spirv_common.hpp',
|
||||
'../shaders/SPIRV-Cross/spirv_cross.cpp',
|
||||
'../shaders/SPIRV-Cross/spirv_cross.hpp',
|
||||
'../shaders/SPIRV-Cross/spirv_glsl.cpp',
|
||||
'../shaders/SPIRV-Cross/spirv_glsl.hpp',
|
||||
'../shaders/SPIRV-Cross/spirv_cross_parsed_ir.cpp',
|
||||
'../shaders/SPIRV-Cross/spirv_cross_parsed_ir.hpp',
|
||||
'../shaders/SPIRV-Cross/spirv_parser.cpp',
|
||||
'../shaders/SPIRV-Cross/spirv_parser.hpp',
|
||||
'../shaders/SPIRV-Cross/spirv.hpp']
|
||||
endif
|
||||
|
||||
wayland = get_option('wayland')
|
||||
if wayland and gtk_ver == 3
|
||||
wayland_dep = dependency('wayland-egl', required: false)
|
||||
|
@ -383,6 +409,7 @@ summary = [
|
|||
' GTK+ version: ' + gtk_ver.to_string(),
|
||||
' Wayland: ' + wayland.to_string(),
|
||||
' OpenGL: ' + opengl.to_string(),
|
||||
' slang shaders: ' + slang.to_string(),
|
||||
' XVideo: ' + xv.to_string(),
|
||||
' ALSA: ' + alsa.to_string(),
|
||||
' Open Sound System: ' + oss.to_string(),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
option('opengl', type: 'boolean', value: true, description: 'Build support for OpenGL')
|
||||
option('slang', type: 'boolean', value: true, description: 'Build support for slang-type shaders')
|
||||
option('xv', type: 'boolean', value: true, description: 'Build support for XV')
|
||||
option('portaudio', type: 'boolean', value: true, description: 'Build PortAudio sound driver')
|
||||
option('oss', type: 'boolean', value: true, description: 'Build OSS sound driver')
|
||||
|
|
|
@ -372,32 +372,38 @@ void S9xOpenGLDisplayDriver::update_texture_size (int width, int height)
|
|||
}
|
||||
}
|
||||
|
||||
bool S9xOpenGLDisplayDriver::load_shaders (const char *shader_file)
|
||||
bool S9xOpenGLDisplayDriver::load_shaders(const char *shader_file)
|
||||
{
|
||||
int length = strlen (shader_file);
|
||||
setlocale(LC_ALL, "C");
|
||||
std::string filename(shader_file);
|
||||
|
||||
setlocale (LC_ALL, "C");
|
||||
auto endswith = [&](std::string ext) -> bool {
|
||||
return filename.rfind(ext) == filename.length() - ext.length();
|
||||
};
|
||||
|
||||
if ((length > 6 && !strcasecmp(shader_file + length - 6, ".glslp")) ||
|
||||
(length > 5 && !strcasecmp(shader_file + length - 5, ".glsl")))
|
||||
if (endswith(".glslp") || endswith(".glsl")
|
||||
#ifdef USE_SLANG
|
||||
|| endswith(".slangp") || endswith(".slang")
|
||||
#endif
|
||||
)
|
||||
{
|
||||
glsl_shader = new GLSLShader;
|
||||
if (glsl_shader->load_shader ((char *) shader_file))
|
||||
if (glsl_shader->load_shader((char *)shader_file))
|
||||
{
|
||||
using_glsl_shaders = true;
|
||||
npot = true;
|
||||
|
||||
if (glsl_shader->param.size () > 0)
|
||||
window->enable_widget ("shader_parameters_item", true);
|
||||
if (glsl_shader->param.size() > 0)
|
||||
window->enable_widget("shader_parameters_item", true);
|
||||
|
||||
setlocale (LC_ALL, "");
|
||||
setlocale(LC_ALL, "");
|
||||
return true;
|
||||
}
|
||||
|
||||
delete glsl_shader;
|
||||
}
|
||||
|
||||
setlocale (LC_ALL, "");
|
||||
setlocale(LC_ALL, "");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -857,7 +857,7 @@ Snes9xPreferences::get_settings_from_dialog ()
|
|||
config->use_pbos != get_check ("use_pbos") ||
|
||||
config->pbo_format != pbo_format ||
|
||||
config->use_shaders != get_check ("use_shaders") ||
|
||||
get_check ("use_shaders"))
|
||||
(config->shader_filename.compare(get_entry_text("fragment_shader"))))
|
||||
{
|
||||
gfx_needs_restart = true;
|
||||
}
|
||||
|
|
121
shaders/glsl.cpp
121
shaders/glsl.cpp
|
@ -91,11 +91,27 @@ static const char *wrap_mode_enum_to_string(int val)
|
|||
bool GLSLShader::load_shader_preset_file(char *filename)
|
||||
{
|
||||
char key[256];
|
||||
int length = strlen(filename);
|
||||
bool singlepass = false;
|
||||
|
||||
if (strlen(filename) > 5 && !strcasecmp(&filename[strlen(filename) - 5], ".glsl"))
|
||||
if (length > 6 && (!strcasecmp(&filename[length - 5], ".glsl") ||
|
||||
!strcasecmp(&filename[length - 6], ".slang")))
|
||||
singlepass = true;
|
||||
|
||||
if (length > 7 && (!strcasecmp(&filename[length - 6], ".slang") ||
|
||||
!strcasecmp(&filename[length - 7], ".slangp")))
|
||||
{
|
||||
#ifdef USE_SLANG
|
||||
this->using_slang = true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (singlepass)
|
||||
{
|
||||
GLSLPass pass;
|
||||
this->pass.push_back (GLSLPass());
|
||||
this->pass.push_back(GLSLPass());
|
||||
|
||||
pass.scale_type_x = pass.scale_type_y = GLSL_VIEWPORT;
|
||||
pass.filter = GLSL_UNDEFINED;
|
||||
|
@ -106,7 +122,7 @@ bool GLSLShader::load_shader_preset_file(char *filename)
|
|||
pass.fp = 0;
|
||||
pass.scale_x = 1.0;
|
||||
pass.scale_y = 1.0;
|
||||
this->pass.push_back (pass);
|
||||
this->pass.push_back(pass);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -356,24 +372,50 @@ bool GLSLShader::load_shader(char *filename)
|
|||
|
||||
strip_parameter_pragmas(lines);
|
||||
|
||||
if (!compile_shader(lines,
|
||||
"#define VERTEX\n#define PARAMETER_UNIFORM\n",
|
||||
aliases.c_str(),
|
||||
GL_VERTEX_SHADER,
|
||||
&vertex_shader) || !vertex_shader)
|
||||
#ifdef USE_SLANG
|
||||
if (using_slang)
|
||||
{
|
||||
printf("Couldn't compile vertex shader in %s.\n", p->filename);
|
||||
return false;
|
||||
}
|
||||
slang_parse_pragmas(lines, i);
|
||||
|
||||
if (!compile_shader(lines,
|
||||
"#define FRAGMENT\n#define PARAMETER_UNIFORM\n",
|
||||
aliases.c_str(),
|
||||
GL_FRAGMENT_SHADER,
|
||||
&fragment_shader) || !fragment_shader)
|
||||
GLint retval;
|
||||
retval = slang_compile(lines, "vertex");
|
||||
if (retval < 0)
|
||||
{
|
||||
printf("Vertex shader in %s failed to compile.\n", p->filename);
|
||||
return false;
|
||||
}
|
||||
vertex_shader = retval;
|
||||
|
||||
retval = slang_compile(lines, "fragment");
|
||||
if (retval < 0)
|
||||
{
|
||||
printf ("Fragment shader in %s failed to compile.\n", p->filename);
|
||||
return false;
|
||||
}
|
||||
fragment_shader = retval;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
printf("Couldn't compile fragment shader in %s.\n", p->filename);
|
||||
return false;
|
||||
if (!compile_shader(lines,
|
||||
"#define VERTEX\n#define PARAMETER_UNIFORM\n",
|
||||
aliases.c_str(),
|
||||
GL_VERTEX_SHADER,
|
||||
&vertex_shader) || !vertex_shader)
|
||||
{
|
||||
printf("Couldn't compile vertex shader in %s.\n", p->filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!compile_shader(lines,
|
||||
"#define FRAGMENT\n#define PARAMETER_UNIFORM\n",
|
||||
aliases.c_str(),
|
||||
GL_FRAGMENT_SHADER,
|
||||
&fragment_shader) || !fragment_shader)
|
||||
{
|
||||
printf("Couldn't compile fragment shader in %s.\n", p->filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
p->program = glCreateProgram();
|
||||
|
@ -446,6 +488,8 @@ bool GLSLShader::load_shader(char *filename)
|
|||
hasAlpha ? GL_RGBA : GL_RGB,
|
||||
GL_UNSIGNED_BYTE,
|
||||
texData);
|
||||
l->width = width;
|
||||
l->height = height;
|
||||
free(texData);
|
||||
}
|
||||
else
|
||||
|
@ -469,6 +513,8 @@ bool GLSLShader::load_shader(char *filename)
|
|||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
stga.data);
|
||||
l->width = stga.width;
|
||||
l->height = stga.height;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -503,7 +549,12 @@ bool GLSLShader::load_shader(char *filename)
|
|||
glTexCoordPointer(2, GL_FLOAT, 0, tex_coords);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
|
||||
register_uniforms();
|
||||
#ifdef USE_SLANG
|
||||
if (using_slang)
|
||||
slang_introspect();
|
||||
else
|
||||
#endif
|
||||
register_uniforms();
|
||||
|
||||
prev_frame.resize(max_prev_frame);
|
||||
|
||||
|
@ -658,14 +709,24 @@ void GLSLShader::render(GLuint &orig,
|
|||
pass[i].wrap_mode);
|
||||
|
||||
glUseProgram(pass[i].program);
|
||||
set_shader_vars(i);
|
||||
#ifdef USE_SLANG
|
||||
if (using_slang)
|
||||
slang_set_shader_vars(i);
|
||||
else
|
||||
#endif
|
||||
set_shader_vars(i);
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
// reset vertex attribs set in set_shader_vars
|
||||
clear_shader_vars();
|
||||
#ifdef USE_SLANG
|
||||
if (using_slang)
|
||||
slang_clear_shader_vars();
|
||||
else
|
||||
#endif
|
||||
clear_shader_vars();
|
||||
|
||||
if (pass[i].srgb)
|
||||
{
|
||||
|
@ -685,12 +746,14 @@ void GLSLShader::render(GLuint &orig,
|
|||
orig = prev_frame.back().texture;
|
||||
prev_frame.pop_back();
|
||||
|
||||
GLSLPass newprevframe;
|
||||
newprevframe.width = width;
|
||||
newprevframe.height = height;
|
||||
newprevframe.texture = pass[0].texture;
|
||||
prev_frame.push_front(newprevframe);
|
||||
glBindTexture(GL_TEXTURE_2D, newprevframe.texture);
|
||||
GLSLPass *newprevframe = new GLSLPass;
|
||||
newprevframe->width = width;
|
||||
newprevframe->height = height;
|
||||
newprevframe->texture = pass[0].texture;
|
||||
|
||||
prev_frame.push_front(*newprevframe);
|
||||
glBindTexture(GL_TEXTURE_2D, newprevframe->texture);
|
||||
delete newprevframe;
|
||||
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internal_format);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, orig);
|
||||
|
@ -935,7 +998,7 @@ void GLSLShader::set_shader_vars(unsigned int p)
|
|||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
void GLSLShader::clear_shader_vars(void)
|
||||
void GLSLShader::clear_shader_vars()
|
||||
{
|
||||
for (unsigned int i = 0; i < vaos.size(); i++)
|
||||
glDisableVertexAttribArray(vaos[i]);
|
||||
|
@ -1026,7 +1089,7 @@ void GLSLShader::save(const char *filename)
|
|||
#undef outd
|
||||
|
||||
|
||||
void GLSLShader::destroy(void)
|
||||
void GLSLShader::destroy()
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glUseProgram(0);
|
||||
|
|
|
@ -69,6 +69,32 @@ typedef struct
|
|||
GLint Lut[9];
|
||||
} GLSLUniforms;
|
||||
|
||||
// Size must always follow texture type
|
||||
enum
|
||||
{
|
||||
SL_INVALID = 0,
|
||||
SL_PASSTEXTURE = 1,
|
||||
SL_PASSSIZE = 2,
|
||||
SL_PREVIOUSFRAMETEXTURE = 3,
|
||||
SL_PREVIOUSFRAMESIZE = 4,
|
||||
SL_LUTTEXTURE = 5,
|
||||
SL_LUTSIZE = 6,
|
||||
SL_MVP = 7,
|
||||
SL_FRAMECOUNT = 8,
|
||||
SL_PARAM = 9
|
||||
};
|
||||
|
||||
typedef struct
|
||||
{
|
||||
// Source
|
||||
int type;
|
||||
int num;
|
||||
|
||||
// Output
|
||||
GLint location; // -1 Indicates UBO
|
||||
GLint offset;
|
||||
} SlangUniform;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
char filename[PATH_MAX];
|
||||
|
@ -93,6 +119,12 @@ typedef struct
|
|||
GLuint filter;
|
||||
|
||||
GLSLUniforms unif;
|
||||
#ifdef USE_SLANG
|
||||
GLuint format;
|
||||
std::vector<SlangUniform> uniforms;
|
||||
std::vector<uint8_t> ubo_buffer;
|
||||
GLuint ubo;
|
||||
#endif
|
||||
} GLSLPass;
|
||||
|
||||
typedef struct
|
||||
|
@ -103,6 +135,8 @@ typedef struct
|
|||
GLuint texture;
|
||||
GLuint wrap_mode;
|
||||
bool mipmap;
|
||||
int width;
|
||||
int height;
|
||||
} GLSLLut;
|
||||
|
||||
typedef struct
|
||||
|
@ -124,13 +158,13 @@ typedef struct
|
|||
int viewport_y, int viewport_width, int viewport_height,
|
||||
GLSLViewportCallback vpcallback);
|
||||
void set_shader_vars(unsigned int pass);
|
||||
void clear_shader_vars(void);
|
||||
void clear_shader_vars();
|
||||
void strip_parameter_pragmas(std::vector<std::string> &lines);
|
||||
GLuint compile_shader(std::vector<std::string> &lines, const char *aliases,
|
||||
const char *defines, GLuint type, GLuint *out);
|
||||
void save(const char *filename);
|
||||
void destroy(void);
|
||||
void register_uniforms(void);
|
||||
void destroy();
|
||||
void register_uniforms();
|
||||
|
||||
ConfigFile conf;
|
||||
|
||||
|
@ -145,6 +179,17 @@ typedef struct
|
|||
GLuint vbo;
|
||||
GLuint prev_fbo;
|
||||
GLfloat *fa;
|
||||
|
||||
bool using_slang = false;
|
||||
#ifdef USE_SLANG
|
||||
std::string slang_get_stage(std::vector<std::string> &lines,
|
||||
std::string name);
|
||||
void slang_parse_pragmas(std::vector<std::string> &lines, int p);
|
||||
GLint slang_compile(std::vector<std::string> &lines, std::string stage);
|
||||
void slang_introspect();
|
||||
void slang_set_shader_vars(int p);
|
||||
void slang_clear_shader_vars();
|
||||
#endif
|
||||
} GLSLShader;
|
||||
|
||||
#endif
|
||||
|
|
|
@ -0,0 +1,699 @@
|
|||
#include "glsl.h"
|
||||
#include "shader_helpers.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <glslang/Public/ShaderLang.h>
|
||||
#include <SPIRV/GlslangToSpv.h>
|
||||
#include "SPIRV-Cross/spirv_cross.hpp"
|
||||
#include "SPIRV-Cross/spirv_glsl.hpp"
|
||||
|
||||
std::string GLSLShader::slang_get_stage(std::vector<std::string> &lines,
|
||||
std::string name)
|
||||
{
|
||||
std::ostringstream output;
|
||||
bool in_stage = true;
|
||||
|
||||
if (name.empty())
|
||||
return std::string("");
|
||||
|
||||
for (auto &line : lines)
|
||||
{
|
||||
if (line.find("#pragma stage") != std::string::npos)
|
||||
{
|
||||
if (line.find(std::string("#pragma stage ") + name) !=
|
||||
std::string::npos)
|
||||
in_stage = true;
|
||||
else
|
||||
in_stage = false;
|
||||
}
|
||||
else if (line.find("#pragma name") != std::string::npos ||
|
||||
line.find("#pragma format") != std::string::npos)
|
||||
{
|
||||
}
|
||||
else if (in_stage)
|
||||
{
|
||||
output << line << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return output.str();
|
||||
}
|
||||
|
||||
static GLuint string_to_format(char *format)
|
||||
{
|
||||
#define MATCH(s, f) \
|
||||
if (!strcmp(format, s)) \
|
||||
return f;
|
||||
MATCH("R8_UNORM", GL_R8);
|
||||
MATCH("R8_UINT", GL_R8UI);
|
||||
MATCH("R8_SINT", GL_R8I);
|
||||
MATCH("R8G8_UNORM", GL_RG8);
|
||||
MATCH("R8G8_UINT", GL_RG8UI);
|
||||
MATCH("R8G8_SINT", GL_RG8I);
|
||||
MATCH("R8G8B8A8_UNORM", GL_RGBA8);
|
||||
MATCH("R8G8B8A8_UINT", GL_RGBA8UI);
|
||||
MATCH("R8G8B8A8_SINT", GL_RGBA8I);
|
||||
MATCH("R8G8B8A8_SRGB", GL_SRGB8_ALPHA8);
|
||||
|
||||
MATCH("A2B10G10R10_UNORM_PACK32", GL_RGB10_A2);
|
||||
MATCH("A2B10G10R10_UINT_PACK32", GL_RGB10_A2UI);
|
||||
|
||||
MATCH("R16_UINT", GL_R16UI);
|
||||
MATCH("R16_SINT", GL_R16I);
|
||||
MATCH("R16_SFLOAT", GL_R16F);
|
||||
MATCH("R16G16_UINT", GL_RG16UI);
|
||||
MATCH("R16G16_SINT", GL_RG16I);
|
||||
MATCH("R16G16_SFLOAT", GL_RG16F);
|
||||
MATCH("R16G16B16A16_UINT", GL_RGBA16UI);
|
||||
MATCH("R16G16B16A16_SINT", GL_RGBA16I);
|
||||
MATCH("R16G16B16A16_SFLOAT", GL_RGBA16F);
|
||||
|
||||
MATCH("R32_UINT", GL_R32UI);
|
||||
MATCH("R32_SINT", GL_R32I);
|
||||
MATCH("R32_SFLOAT", GL_R32F);
|
||||
MATCH("R32G32_UINT", GL_RG32UI);
|
||||
MATCH("R32G32_SINT", GL_RG32I);
|
||||
MATCH("R32G32_SFLOAT", GL_RG32F);
|
||||
MATCH("R32G32B32A32_UINT", GL_RGBA32UI);
|
||||
MATCH("R32G32B32A32_SINT", GL_RGBA32I);
|
||||
MATCH("R32G32B32A32_SFLOAT", GL_RGBA32F);
|
||||
|
||||
return GL_RGBA;
|
||||
}
|
||||
|
||||
void GLSLShader::slang_parse_pragmas(std::vector<std::string> &lines, int p)
|
||||
{
|
||||
pass[p].format = GL_RGBA;
|
||||
|
||||
for (auto &line : lines)
|
||||
{
|
||||
if (line.find("#pragma name") != std::string::npos)
|
||||
{
|
||||
sscanf(line.c_str(), "#pragma name %255s", pass[p].alias);
|
||||
}
|
||||
else if (line.find("#pragma format") != std::string::npos)
|
||||
{
|
||||
char format[256];
|
||||
sscanf(line.c_str(), "#pragma format %255s", format);
|
||||
pass[p].format = string_to_format(format);
|
||||
if (pass[p].format == GL_RGBA16F || pass[p].format == GL_RGBA32F)
|
||||
pass[p].fp = TRUE;
|
||||
else if (pass[p].format == GL_SRGB8_ALPHA8)
|
||||
pass[p].srgb = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const TBuiltInResource DefaultTBuiltInResource = {
|
||||
/* .MaxLights = */ 32,
|
||||
/* .MaxClipPlanes = */ 6,
|
||||
/* .MaxTextureUnits = */ 32,
|
||||
/* .MaxTextureCoords = */ 32,
|
||||
/* .MaxVertexAttribs = */ 64,
|
||||
/* .MaxVertexUniformComponents = */ 4096,
|
||||
/* .MaxVaryingFloats = */ 64,
|
||||
/* .MaxVertexTextureImageUnits = */ 32,
|
||||
/* .MaxCombinedTextureImageUnits = */ 80,
|
||||
/* .MaxTextureImageUnits = */ 32,
|
||||
/* .MaxFragmentUniformComponents = */ 4096,
|
||||
/* .MaxDrawBuffers = */ 32,
|
||||
/* .MaxVertexUniformVectors = */ 128,
|
||||
/* .MaxVaryingVectors = */ 8,
|
||||
/* .MaxFragmentUniformVectors = */ 16,
|
||||
/* .MaxVertexOutputVectors = */ 16,
|
||||
/* .MaxFragmentInputVectors = */ 15,
|
||||
/* .MinProgramTexelOffset = */ -8,
|
||||
/* .MaxProgramTexelOffset = */ 7,
|
||||
/* .MaxClipDistances = */ 8,
|
||||
/* .MaxComputeWorkGroupCountX = */ 65535,
|
||||
/* .MaxComputeWorkGroupCountY = */ 65535,
|
||||
/* .MaxComputeWorkGroupCountZ = */ 65535,
|
||||
/* .MaxComputeWorkGroupSizeX = */ 1024,
|
||||
/* .MaxComputeWorkGroupSizeY = */ 1024,
|
||||
/* .MaxComputeWorkGroupSizeZ = */ 64,
|
||||
/* .MaxComputeUniformComponents = */ 1024,
|
||||
/* .MaxComputeTextureImageUnits = */ 16,
|
||||
/* .MaxComputeImageUniforms = */ 8,
|
||||
/* .MaxComputeAtomicCounters = */ 8,
|
||||
/* .MaxComputeAtomicCounterBuffers = */ 1,
|
||||
/* .MaxVaryingComponents = */ 60,
|
||||
/* .MaxVertexOutputComponents = */ 64,
|
||||
/* .MaxGeometryInputComponents = */ 64,
|
||||
/* .MaxGeometryOutputComponents = */ 128,
|
||||
/* .MaxFragmentInputComponents = */ 128,
|
||||
/* .MaxImageUnits = */ 8,
|
||||
/* .MaxCombinedImageUnitsAndFragmentOutputs = */ 8,
|
||||
/* .MaxCombinedShaderOutputResources = */ 8,
|
||||
/* .MaxImageSamples = */ 0,
|
||||
/* .MaxVertexImageUniforms = */ 0,
|
||||
/* .MaxTessControlImageUniforms = */ 0,
|
||||
/* .MaxTessEvaluationImageUniforms = */ 0,
|
||||
/* .MaxGeometryImageUniforms = */ 0,
|
||||
/* .MaxFragmentImageUniforms = */ 8,
|
||||
/* .MaxCombinedImageUniforms = */ 8,
|
||||
/* .MaxGeometryTextureImageUnits = */ 16,
|
||||
/* .MaxGeometryOutputVertices = */ 256,
|
||||
/* .MaxGeometryTotalOutputComponents = */ 1024,
|
||||
/* .MaxGeometryUniformComponents = */ 1024,
|
||||
/* .MaxGeometryVaryingComponents = */ 64,
|
||||
/* .MaxTessControlInputComponents = */ 128,
|
||||
/* .MaxTessControlOutputComponents = */ 128,
|
||||
/* .MaxTessControlTextureImageUnits = */ 16,
|
||||
/* .MaxTessControlUniformComponents = */ 1024,
|
||||
/* .MaxTessControlTotalOutputComponents = */ 4096,
|
||||
/* .MaxTessEvaluationInputComponents = */ 128,
|
||||
/* .MaxTessEvaluationOutputComponents = */ 128,
|
||||
/* .MaxTessEvaluationTextureImageUnits = */ 16,
|
||||
/* .MaxTessEvaluationUniformComponents = */ 1024,
|
||||
/* .MaxTessPatchComponents = */ 120,
|
||||
/* .MaxPatchVertices = */ 32,
|
||||
/* .MaxTessGenLevel = */ 64,
|
||||
/* .MaxViewports = */ 16,
|
||||
/* .MaxVertexAtomicCounters = */ 0,
|
||||
/* .MaxTessControlAtomicCounters = */ 0,
|
||||
/* .MaxTessEvaluationAtomicCounters = */ 0,
|
||||
/* .MaxGeometryAtomicCounters = */ 0,
|
||||
/* .MaxFragmentAtomicCounters = */ 8,
|
||||
/* .MaxCombinedAtomicCounters = */ 8,
|
||||
/* .MaxAtomicCounterBindings = */ 1,
|
||||
/* .MaxVertexAtomicCounterBuffers = */ 0,
|
||||
/* .MaxTessControlAtomicCounterBuffers = */ 0,
|
||||
/* .MaxTessEvaluationAtomicCounterBuffers = */ 0,
|
||||
/* .MaxGeometryAtomicCounterBuffers = */ 0,
|
||||
/* .MaxFragmentAtomicCounterBuffers = */ 1,
|
||||
/* .MaxCombinedAtomicCounterBuffers = */ 1,
|
||||
/* .MaxAtomicCounterBufferSize = */ 16384,
|
||||
/* .MaxTransformFeedbackBuffers = */ 4,
|
||||
/* .MaxTransformFeedbackInterleavedComponents = */ 64,
|
||||
/* .MaxCullDistances = */ 8,
|
||||
/* .MaxCombinedClipAndCullDistances = */ 8,
|
||||
/* .MaxSamples = */ 4,
|
||||
/* .maxMeshOutputVerticesNV = */ 256,
|
||||
/* .maxMeshOutputPrimitivesNV = */ 512,
|
||||
/* .maxMeshWorkGroupSizeX_NV = */ 32,
|
||||
/* .maxMeshWorkGroupSizeY_NV = */ 1,
|
||||
/* .maxMeshWorkGroupSizeZ_NV = */ 1,
|
||||
/* .maxTaskWorkGroupSizeX_NV = */ 32,
|
||||
/* .maxTaskWorkGroupSizeY_NV = */ 1,
|
||||
/* .maxTaskWorkGroupSizeZ_NV = */ 1,
|
||||
/* .maxMeshViewCountNV = */ 4,
|
||||
|
||||
/* .limits = */ {
|
||||
/* .nonInductiveForLoops = */ 1,
|
||||
/* .whileLoops = */ 1,
|
||||
/* .doWhileLoops = */ 1,
|
||||
/* .generalUniformIndexing = */ 1,
|
||||
/* .generalAttributeMatrixVectorIndexing = */ 1,
|
||||
/* .generalVaryingIndexing = */ 1,
|
||||
/* .generalSamplerIndexing = */ 1,
|
||||
/* .generalVariableIndexing = */ 1,
|
||||
/* .generalConstantMatrixVectorIndexing = */ 1,
|
||||
}};
|
||||
|
||||
GLint GLSLShader::slang_compile(std::vector<std::string> &lines,
|
||||
std::string stage)
|
||||
{
|
||||
static bool ProcessInitialized = false;
|
||||
|
||||
if (!ProcessInitialized)
|
||||
{
|
||||
ShInitialize();
|
||||
ProcessInitialized = true;
|
||||
}
|
||||
|
||||
std::string source = slang_get_stage(lines, stage);
|
||||
|
||||
EShLanguage language;
|
||||
if (!stage.compare("fragment"))
|
||||
language = EShLangFragment;
|
||||
else
|
||||
language = EShLangVertex;
|
||||
glslang::TShader shader(language);
|
||||
|
||||
const char *csource = source.c_str();
|
||||
shader.setStrings(&csource, 1);
|
||||
|
||||
EShMessages messages =
|
||||
(EShMessages)(EShMsgDefault | EShMsgVulkanRules | EShMsgSpvRules);
|
||||
|
||||
std::string debug;
|
||||
auto forbid_includer = glslang::TShader::ForbidIncluder();
|
||||
|
||||
if (!shader.preprocess(&DefaultTBuiltInResource, 100, ENoProfile, false,
|
||||
false, messages, &debug, forbid_includer))
|
||||
{
|
||||
puts(debug.c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!shader.parse(&DefaultTBuiltInResource, 100, false, messages))
|
||||
{
|
||||
puts(shader.getInfoLog());
|
||||
puts(shader.getInfoDebugLog());
|
||||
return -1;
|
||||
}
|
||||
|
||||
glslang::TProgram program;
|
||||
program.addShader(&shader);
|
||||
|
||||
if (!program.link(messages))
|
||||
{
|
||||
puts(shader.getInfoLog());
|
||||
puts(shader.getInfoDebugLog());
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> spirv;
|
||||
glslang::GlslangToSpv(*program.getIntermediate(language), spirv);
|
||||
|
||||
spirv_cross::CompilerGLSL glsl(std::move(spirv));
|
||||
|
||||
auto resources = glsl.get_shader_resources();
|
||||
if (resources.push_constant_buffers.size() > 1 ||
|
||||
resources.uniform_buffers.size() > 1)
|
||||
{
|
||||
puts("slang shader doesn't comply with spec:\n"
|
||||
" Too many UBOs or push constant buffers.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (language == EShLangFragment)
|
||||
{
|
||||
for (auto &rsrc : resources.stage_inputs)
|
||||
{
|
||||
// Some of the converted shaders have unmatched declarations for
|
||||
// this in fragment shader
|
||||
if (glsl.get_name(rsrc.id) == "FragCoord")
|
||||
{
|
||||
glsl.set_remapped_variable_state(rsrc.id, true);
|
||||
glsl.set_name(rsrc.id, "gl_FragCoord");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spirv_cross::CompilerGLSL::Options opts;
|
||||
opts.version = gl_version() * 10;
|
||||
opts.vulkan_semantics = false;
|
||||
glsl.set_common_options(opts);
|
||||
|
||||
std::string glsl_source = glsl.compile();
|
||||
|
||||
GLint status = 0;
|
||||
GLchar *cstring = (GLchar *)glsl_source.c_str();
|
||||
|
||||
GLint shaderid = glCreateShader(
|
||||
language == EShLangFragment ? GL_FRAGMENT_SHADER : GL_VERTEX_SHADER);
|
||||
glShaderSource(shaderid, 1, &cstring, NULL);
|
||||
glCompileShader(shaderid);
|
||||
|
||||
glGetShaderiv(shaderid, GL_COMPILE_STATUS, &status);
|
||||
|
||||
char info_log[1024];
|
||||
glGetShaderInfoLog(shaderid, 1024, NULL, info_log);
|
||||
if (*info_log)
|
||||
puts(info_log);
|
||||
|
||||
if (status == GL_FALSE)
|
||||
return -1;
|
||||
|
||||
return shaderid;
|
||||
}
|
||||
|
||||
static inline bool isalldigits(std::string str)
|
||||
{
|
||||
for (auto c : str)
|
||||
{
|
||||
if (!isdigit(c))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GLSLShader::slang_introspect()
|
||||
{
|
||||
max_prev_frame = 0;
|
||||
|
||||
for (int i = 1; i < (int)pass.size(); i++)
|
||||
{
|
||||
GLSLPass &p = pass[i];
|
||||
|
||||
int num_uniforms;
|
||||
glGetProgramiv(p.program, GL_ACTIVE_UNIFORMS, &num_uniforms);
|
||||
std::vector<GLuint> indices(num_uniforms);
|
||||
std::vector<GLint> block_indices(num_uniforms);
|
||||
std::vector<GLint> offsets(num_uniforms);
|
||||
std::vector<std::string> names;
|
||||
std::vector<GLint> locations(num_uniforms);
|
||||
|
||||
for (int j = 0; j < num_uniforms; j++)
|
||||
indices[j] = j;
|
||||
|
||||
glGetActiveUniformsiv(p.program, num_uniforms, indices.data(), GL_UNIFORM_BLOCK_INDEX, block_indices.data());
|
||||
glGetActiveUniformsiv(p.program, num_uniforms, indices.data(), GL_UNIFORM_OFFSET, offsets.data());
|
||||
|
||||
for (int j = 0; j < num_uniforms; j++)
|
||||
{
|
||||
char name[1024];
|
||||
glGetActiveUniformName(p.program, j, 1024, NULL, name);
|
||||
names.push_back(std::string(name));
|
||||
locations[j] = glGetUniformLocation(p.program, name);
|
||||
}
|
||||
|
||||
for (int j = 0; j < num_uniforms; j++)
|
||||
{
|
||||
SlangUniform u = { 0, 0, 0, 0 };
|
||||
std::string name;
|
||||
|
||||
if (locations[j] == -1)
|
||||
{
|
||||
u.location = -1;
|
||||
u.offset = offsets[j];
|
||||
}
|
||||
else
|
||||
u.location = locations[j];
|
||||
|
||||
// Strip off any container
|
||||
size_t dotloc = names[j].find('.');
|
||||
if (dotloc != std::string::npos)
|
||||
name = names[j].substr(dotloc + 1);
|
||||
else
|
||||
name = names[j];
|
||||
|
||||
auto indexedtexorsize = [&](std::string needle, int type) -> bool {
|
||||
if (name.find(needle) == 0)
|
||||
{
|
||||
std::string tmp = name.substr(needle.length());
|
||||
if (tmp.rfind("Size") == tmp.length() - 4)
|
||||
{
|
||||
u.type = type + 1;
|
||||
tmp = tmp.substr(0, tmp.length() - 4);
|
||||
}
|
||||
else
|
||||
u.type = type;
|
||||
|
||||
if (isalldigits(tmp))
|
||||
{
|
||||
u.num = std::stoi(tmp);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!name.compare("MVP"))
|
||||
{
|
||||
u.type = SL_MVP;
|
||||
}
|
||||
else if (!name.compare("Original"))
|
||||
{
|
||||
u.type = SL_PASSTEXTURE;
|
||||
u.num = 0;
|
||||
}
|
||||
else if (!name.compare("OriginalSize"))
|
||||
{
|
||||
u.type = SL_PASSSIZE;
|
||||
u.num = 0;
|
||||
}
|
||||
else if (!name.compare("Source"))
|
||||
{
|
||||
u.type = SL_PASSTEXTURE;
|
||||
u.num = i - 1;
|
||||
}
|
||||
else if (!name.compare("SourceSize"))
|
||||
{
|
||||
u.type = SL_PASSSIZE;
|
||||
u.num = i - 1;
|
||||
}
|
||||
else if (!name.compare("OutputSize"))
|
||||
{
|
||||
u.type = SL_PASSSIZE;
|
||||
u.num = i;
|
||||
}
|
||||
else if (!name.compare("FrameCount"))
|
||||
{
|
||||
u.type = SL_FRAMECOUNT;
|
||||
}
|
||||
else if (indexedtexorsize("OriginalHistory", SL_PREVIOUSFRAMETEXTURE))
|
||||
{
|
||||
// OriginalHistory0 is just this frame's pass 0
|
||||
if (u.num == 0)
|
||||
{
|
||||
if (u.type == SL_PREVIOUSFRAMETEXTURE)
|
||||
u.type = SL_PASSTEXTURE;
|
||||
if (u.type == SL_PREVIOUSFRAMESIZE)
|
||||
u.type = SL_PASSSIZE;
|
||||
}
|
||||
if (u.num > max_prev_frame)
|
||||
max_prev_frame = u.num;
|
||||
}
|
||||
else if (indexedtexorsize("PassOutput", SL_PASSTEXTURE))
|
||||
{
|
||||
// Their pass 0 is our pass 1
|
||||
u.num--;
|
||||
}
|
||||
else if (indexedtexorsize("User", SL_LUTTEXTURE))
|
||||
{
|
||||
}
|
||||
if (u.type != SL_INVALID)
|
||||
{
|
||||
p.uniforms.push_back(u);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool matched_lut = false;
|
||||
for (int k = 0; k < (int)lut.size(); k++)
|
||||
{
|
||||
std::string lutname(lut[k].id);
|
||||
|
||||
if (name.compare(lutname) == 0)
|
||||
{
|
||||
u.type = SL_LUTTEXTURE;
|
||||
u.num = k;
|
||||
matched_lut = true;
|
||||
}
|
||||
else if (name.compare(lutname + "Size") == 0)
|
||||
{
|
||||
u.type = SL_LUTSIZE;
|
||||
u.num = k;
|
||||
matched_lut = true;
|
||||
}
|
||||
}
|
||||
if (matched_lut)
|
||||
{
|
||||
p.uniforms.push_back(u);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool matched_alias = false;
|
||||
for (int k = 0; k < i; k++)
|
||||
{
|
||||
std::string alias(pass[k].alias);
|
||||
|
||||
if (name.compare(alias) == 0)
|
||||
{
|
||||
u.type = SL_PASSTEXTURE;
|
||||
u.num = k;
|
||||
matched_alias = true;
|
||||
}
|
||||
else if (name.compare(alias + "Size") == 0)
|
||||
{
|
||||
u.type = SL_PASSSIZE;
|
||||
u.num = k;
|
||||
matched_alias = true;
|
||||
}
|
||||
}
|
||||
if (matched_alias)
|
||||
{
|
||||
p.uniforms.push_back(u);
|
||||
continue;
|
||||
}
|
||||
|
||||
u.type = SL_INVALID;
|
||||
for (int k = 0; k < (int)param.size(); k++)
|
||||
{
|
||||
if (name.compare(param[k].id) == 0)
|
||||
{
|
||||
u.type = SL_PARAM;
|
||||
u.num = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (u.type != SL_INVALID)
|
||||
p.uniforms.push_back(u);
|
||||
}
|
||||
|
||||
bool uniform_block = false;
|
||||
for (auto &u : p.uniforms)
|
||||
if (u.location == -1)
|
||||
{
|
||||
uniform_block = true;
|
||||
break;
|
||||
}
|
||||
|
||||
GLint uniform_block_size = 0;
|
||||
if (uniform_block)
|
||||
{
|
||||
glGetActiveUniformBlockiv(p.program, 0, GL_UNIFORM_BLOCK_DATA_SIZE,
|
||||
&uniform_block_size);
|
||||
glGenBuffers(1, &p.ubo);
|
||||
p.ubo_buffer.resize(uniform_block_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const GLfloat coords[] = { 0.0f, 0.0f, 0.0f, 1.0f, // Vertex Positions
|
||||
1.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.0f, 1.0f, 0.0f, 1.0f,
|
||||
1.0f, 1.0f, 0.0f, 1.0f,
|
||||
0.0f, 0.0f, // Tex coords regular + 16
|
||||
1.0f, 0.0f,
|
||||
0.0f, 1.0f,
|
||||
1.0f, 1.0f,
|
||||
0.0f, 1.0f, // Tex coords inverted + 24
|
||||
1.0f, 1.0f,
|
||||
0.0f, 0.0f,
|
||||
1.0f, 0.0f };
|
||||
|
||||
static const GLfloat mvp_ortho[16] = { 2.0f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 2.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, -1.0f, 0.0f,
|
||||
-1.0f, -1.0f, 0.0f, 1.0f };
|
||||
|
||||
void GLSLShader::slang_clear_shader_vars()
|
||||
{
|
||||
glDisableVertexAttribArray(0);
|
||||
glDisableVertexAttribArray(1);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
}
|
||||
|
||||
void GLSLShader::slang_set_shader_vars(int p)
|
||||
{
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 32, coords, GL_STATIC_DRAW);
|
||||
|
||||
GLint attr = glGetAttribLocation(pass[p].program, "Position");
|
||||
if (attr > -1)
|
||||
{
|
||||
glEnableVertexAttribArray(attr);
|
||||
glVertexAttribPointer(attr, 4, GL_FLOAT, GL_FALSE, 0, (void *)(0));
|
||||
}
|
||||
|
||||
attr = glGetAttribLocation(pass[p].program, "TexCoord");
|
||||
if (attr > -1)
|
||||
{
|
||||
GLfloat *offset = 0;
|
||||
offset += 16;
|
||||
if (p == (int)pass.size() - 1)
|
||||
offset += 8;
|
||||
|
||||
glEnableVertexAttribArray(attr);
|
||||
glVertexAttribPointer(attr, 2, GL_FLOAT, GL_FALSE, 0, (void *)(offset));
|
||||
}
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
uint8_t *ubo = pass[p].ubo_buffer.data();
|
||||
GLint texunit = 0;
|
||||
|
||||
for (auto &u : pass[p].uniforms)
|
||||
{
|
||||
switch (u.type)
|
||||
{
|
||||
case SL_PREVIOUSFRAMETEXTURE:
|
||||
case SL_PASSTEXTURE:
|
||||
case SL_LUTTEXTURE:
|
||||
glActiveTexture(GL_TEXTURE0 + texunit);
|
||||
|
||||
if (u.type == SL_PASSTEXTURE)
|
||||
glBindTexture(GL_TEXTURE_2D, pass[u.num].texture);
|
||||
else if (u.type == SL_PREVIOUSFRAMETEXTURE)
|
||||
glBindTexture(GL_TEXTURE_2D, prev_frame[u.num - 1].texture);
|
||||
else if (u.type == SL_LUTTEXTURE)
|
||||
glBindTexture(GL_TEXTURE_2D, lut[u.num].texture);
|
||||
|
||||
if (u.location == -1)
|
||||
*((GLint *)(ubo + u.offset)) = texunit;
|
||||
else
|
||||
glUniform1i(u.location, texunit);
|
||||
texunit++;
|
||||
break;
|
||||
|
||||
case SL_PREVIOUSFRAMESIZE:
|
||||
case SL_PASSSIZE:
|
||||
case SL_LUTSIZE:
|
||||
{
|
||||
GLfloat size[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
if (u.type == SL_PASSSIZE)
|
||||
{
|
||||
size[0] = (GLfloat)pass[u.num].width;
|
||||
size[1] = (GLfloat)pass[u.num].height;
|
||||
size[2] = (GLfloat)1.0f / pass[u.num].width;
|
||||
size[3] = (GLfloat)1.0f / pass[u.num].height;
|
||||
}
|
||||
else if (u.type == SL_PREVIOUSFRAMESIZE)
|
||||
{
|
||||
size[0] = (GLfloat)prev_frame[u.num - 1].width;
|
||||
size[1] = (GLfloat)prev_frame[u.num - 1].height;
|
||||
size[2] = (GLfloat)1.0f / prev_frame[u.num - 1].width;
|
||||
size[3] = (GLfloat)1.0f / prev_frame[u.num - 1].height;
|
||||
}
|
||||
else if (u.type == SL_LUTSIZE)
|
||||
{
|
||||
size[0] = (GLfloat)lut[u.num].width;
|
||||
size[1] = (GLfloat)lut[u.num].height;
|
||||
size[2] = (GLfloat)1.0f / lut[u.num].width;
|
||||
size[3] = (GLfloat)1.0f / lut[u.num].height;
|
||||
}
|
||||
|
||||
if (u.location == -1)
|
||||
{
|
||||
GLfloat *data = (GLfloat *)(ubo + u.offset);
|
||||
data[0] = size[0];
|
||||
data[1] = size[1];
|
||||
data[2] = size[2];
|
||||
data[3] = size[3];
|
||||
}
|
||||
else
|
||||
glUniform4fv(u.location, 1, size);
|
||||
break;
|
||||
}
|
||||
|
||||
case SL_MVP:
|
||||
if (u.location == -1)
|
||||
{
|
||||
|
||||
GLfloat *data = (GLfloat *)(ubo + u.offset);
|
||||
for (int i = 0; i < 16; i++)
|
||||
data[i] = mvp_ortho[i];
|
||||
}
|
||||
else
|
||||
glUniformMatrix4fv(u.location, 1, GL_FALSE, mvp_ortho);
|
||||
break;
|
||||
|
||||
case SL_FRAMECOUNT:
|
||||
if (u.location == -1)
|
||||
*((GLuint *)(ubo + u.offset)) = frame_count;
|
||||
else
|
||||
glUniform1ui(u.location, frame_count);
|
||||
break;
|
||||
|
||||
case SL_PARAM:
|
||||
if (u.location == -1)
|
||||
*((GLfloat *)(ubo + u.offset)) = param[u.num].val;
|
||||
else
|
||||
glUniform1f(u.location, param[u.num].val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pass[p].ubo_buffer.size() > 0)
|
||||
{
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, pass[p].ubo);
|
||||
glBufferData(GL_UNIFORM_BUFFER, pass[p].ubo_buffer.size(), ubo,
|
||||
GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, 0, pass[p].ubo);
|
||||
glUniformBlockBinding(pass[p].program, 0, 0);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue