Merge pull request #1425 from ReinUsesLisp/geometry-shaders
gl_shader_decompiler: Implement geometry shaders
This commit is contained in:
commit
ee1b204749
|
@ -314,6 +314,15 @@ enum class TextureMiscMode : u64 {
|
||||||
PTP,
|
PTP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class IsberdMode : u64 {
|
||||||
|
None = 0,
|
||||||
|
Patch = 1,
|
||||||
|
Prim = 2,
|
||||||
|
Attr = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class IsberdShift : u64 { None = 0, U16 = 1, B32 = 2 };
|
||||||
|
|
||||||
enum class IpaInterpMode : u64 {
|
enum class IpaInterpMode : u64 {
|
||||||
Linear = 0,
|
Linear = 0,
|
||||||
Perspective = 1,
|
Perspective = 1,
|
||||||
|
@ -340,6 +349,87 @@ struct IpaMode {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class SystemVariable : u64 {
|
||||||
|
LaneId = 0x00,
|
||||||
|
VirtCfg = 0x02,
|
||||||
|
VirtId = 0x03,
|
||||||
|
Pm0 = 0x04,
|
||||||
|
Pm1 = 0x05,
|
||||||
|
Pm2 = 0x06,
|
||||||
|
Pm3 = 0x07,
|
||||||
|
Pm4 = 0x08,
|
||||||
|
Pm5 = 0x09,
|
||||||
|
Pm6 = 0x0a,
|
||||||
|
Pm7 = 0x0b,
|
||||||
|
OrderingTicket = 0x0f,
|
||||||
|
PrimType = 0x10,
|
||||||
|
InvocationId = 0x11,
|
||||||
|
Ydirection = 0x12,
|
||||||
|
ThreadKill = 0x13,
|
||||||
|
ShaderType = 0x14,
|
||||||
|
DirectBeWriteAddressLow = 0x15,
|
||||||
|
DirectBeWriteAddressHigh = 0x16,
|
||||||
|
DirectBeWriteEnabled = 0x17,
|
||||||
|
MachineId0 = 0x18,
|
||||||
|
MachineId1 = 0x19,
|
||||||
|
MachineId2 = 0x1a,
|
||||||
|
MachineId3 = 0x1b,
|
||||||
|
Affinity = 0x1c,
|
||||||
|
InvocationInfo = 0x1d,
|
||||||
|
WscaleFactorXY = 0x1e,
|
||||||
|
WscaleFactorZ = 0x1f,
|
||||||
|
Tid = 0x20,
|
||||||
|
TidX = 0x21,
|
||||||
|
TidY = 0x22,
|
||||||
|
TidZ = 0x23,
|
||||||
|
CtaParam = 0x24,
|
||||||
|
CtaIdX = 0x25,
|
||||||
|
CtaIdY = 0x26,
|
||||||
|
CtaIdZ = 0x27,
|
||||||
|
NtId = 0x28,
|
||||||
|
CirQueueIncrMinusOne = 0x29,
|
||||||
|
Nlatc = 0x2a,
|
||||||
|
SmSpaVersion = 0x2c,
|
||||||
|
MultiPassShaderInfo = 0x2d,
|
||||||
|
LwinHi = 0x2e,
|
||||||
|
SwinHi = 0x2f,
|
||||||
|
SwinLo = 0x30,
|
||||||
|
SwinSz = 0x31,
|
||||||
|
SmemSz = 0x32,
|
||||||
|
SmemBanks = 0x33,
|
||||||
|
LwinLo = 0x34,
|
||||||
|
LwinSz = 0x35,
|
||||||
|
LmemLosz = 0x36,
|
||||||
|
LmemHioff = 0x37,
|
||||||
|
EqMask = 0x38,
|
||||||
|
LtMask = 0x39,
|
||||||
|
LeMask = 0x3a,
|
||||||
|
GtMask = 0x3b,
|
||||||
|
GeMask = 0x3c,
|
||||||
|
RegAlloc = 0x3d,
|
||||||
|
CtxAddr = 0x3e, // .fmask = F_SM50
|
||||||
|
BarrierAlloc = 0x3e, // .fmask = F_SM60
|
||||||
|
GlobalErrorStatus = 0x40,
|
||||||
|
WarpErrorStatus = 0x42,
|
||||||
|
WarpErrorStatusClear = 0x43,
|
||||||
|
PmHi0 = 0x48,
|
||||||
|
PmHi1 = 0x49,
|
||||||
|
PmHi2 = 0x4a,
|
||||||
|
PmHi3 = 0x4b,
|
||||||
|
PmHi4 = 0x4c,
|
||||||
|
PmHi5 = 0x4d,
|
||||||
|
PmHi6 = 0x4e,
|
||||||
|
PmHi7 = 0x4f,
|
||||||
|
ClockLo = 0x50,
|
||||||
|
ClockHi = 0x51,
|
||||||
|
GlobalTimerLo = 0x52,
|
||||||
|
GlobalTimerHi = 0x53,
|
||||||
|
HwTaskId = 0x60,
|
||||||
|
CircularQueueEntryIndex = 0x61,
|
||||||
|
CircularQueueEntryAddressLow = 0x62,
|
||||||
|
CircularQueueEntryAddressHigh = 0x63,
|
||||||
|
};
|
||||||
|
|
||||||
union Instruction {
|
union Instruction {
|
||||||
Instruction& operator=(const Instruction& instr) {
|
Instruction& operator=(const Instruction& instr) {
|
||||||
value = instr.value;
|
value = instr.value;
|
||||||
|
@ -914,6 +1004,18 @@ union Instruction {
|
||||||
}
|
}
|
||||||
} bra;
|
} bra;
|
||||||
|
|
||||||
|
union {
|
||||||
|
BitField<39, 1, u64> emit; // EmitVertex
|
||||||
|
BitField<40, 1, u64> cut; // EndPrimitive
|
||||||
|
} out;
|
||||||
|
|
||||||
|
union {
|
||||||
|
BitField<31, 1, u64> skew;
|
||||||
|
BitField<32, 1, u64> o;
|
||||||
|
BitField<33, 2, IsberdMode> mode;
|
||||||
|
BitField<47, 2, IsberdShift> shift;
|
||||||
|
} isberd;
|
||||||
|
|
||||||
union {
|
union {
|
||||||
BitField<20, 16, u64> imm20_16;
|
BitField<20, 16, u64> imm20_16;
|
||||||
BitField<36, 1, u64> product_shift_left;
|
BitField<36, 1, u64> product_shift_left;
|
||||||
|
@ -936,6 +1038,10 @@ union Instruction {
|
||||||
BitField<36, 5, u64> index;
|
BitField<36, 5, u64> index;
|
||||||
} cbuf36;
|
} cbuf36;
|
||||||
|
|
||||||
|
// Unsure about the size of this one.
|
||||||
|
// It's always used with a gpr0, so any size should be fine.
|
||||||
|
BitField<20, 8, SystemVariable> sys20;
|
||||||
|
|
||||||
BitField<47, 1, u64> generates_cc;
|
BitField<47, 1, u64> generates_cc;
|
||||||
BitField<61, 1, u64> is_b_imm;
|
BitField<61, 1, u64> is_b_imm;
|
||||||
BitField<60, 1, u64> is_b_gpr;
|
BitField<60, 1, u64> is_b_gpr;
|
||||||
|
@ -975,6 +1081,8 @@ public:
|
||||||
TMML, // Texture Mip Map Level
|
TMML, // Texture Mip Map Level
|
||||||
EXIT,
|
EXIT,
|
||||||
IPA,
|
IPA,
|
||||||
|
OUT_R, // Emit vertex/primitive
|
||||||
|
ISBERD,
|
||||||
FFMA_IMM, // Fused Multiply and Add
|
FFMA_IMM, // Fused Multiply and Add
|
||||||
FFMA_CR,
|
FFMA_CR,
|
||||||
FFMA_RC,
|
FFMA_RC,
|
||||||
|
@ -1034,6 +1142,7 @@ public:
|
||||||
MOV_C,
|
MOV_C,
|
||||||
MOV_R,
|
MOV_R,
|
||||||
MOV_IMM,
|
MOV_IMM,
|
||||||
|
MOV_SYS,
|
||||||
MOV32_IMM,
|
MOV32_IMM,
|
||||||
SHL_C,
|
SHL_C,
|
||||||
SHL_R,
|
SHL_R,
|
||||||
|
@ -1209,6 +1318,8 @@ private:
|
||||||
INST("1101111101011---", Id::TMML, Type::Memory, "TMML"),
|
INST("1101111101011---", Id::TMML, Type::Memory, "TMML"),
|
||||||
INST("111000110000----", Id::EXIT, Type::Trivial, "EXIT"),
|
INST("111000110000----", Id::EXIT, Type::Trivial, "EXIT"),
|
||||||
INST("11100000--------", Id::IPA, Type::Trivial, "IPA"),
|
INST("11100000--------", Id::IPA, Type::Trivial, "IPA"),
|
||||||
|
INST("1111101111100---", Id::OUT_R, Type::Trivial, "OUT_R"),
|
||||||
|
INST("1110111111010---", Id::ISBERD, Type::Trivial, "ISBERD"),
|
||||||
INST("0011001-1-------", Id::FFMA_IMM, Type::Ffma, "FFMA_IMM"),
|
INST("0011001-1-------", Id::FFMA_IMM, Type::Ffma, "FFMA_IMM"),
|
||||||
INST("010010011-------", Id::FFMA_CR, Type::Ffma, "FFMA_CR"),
|
INST("010010011-------", Id::FFMA_CR, Type::Ffma, "FFMA_CR"),
|
||||||
INST("010100011-------", Id::FFMA_RC, Type::Ffma, "FFMA_RC"),
|
INST("010100011-------", Id::FFMA_RC, Type::Ffma, "FFMA_RC"),
|
||||||
|
@ -1255,6 +1366,7 @@ private:
|
||||||
INST("0100110010011---", Id::MOV_C, Type::Arithmetic, "MOV_C"),
|
INST("0100110010011---", Id::MOV_C, Type::Arithmetic, "MOV_C"),
|
||||||
INST("0101110010011---", Id::MOV_R, Type::Arithmetic, "MOV_R"),
|
INST("0101110010011---", Id::MOV_R, Type::Arithmetic, "MOV_R"),
|
||||||
INST("0011100-10011---", Id::MOV_IMM, Type::Arithmetic, "MOV_IMM"),
|
INST("0011100-10011---", Id::MOV_IMM, Type::Arithmetic, "MOV_IMM"),
|
||||||
|
INST("1111000011001---", Id::MOV_SYS, Type::Trivial, "MOV_SYS"),
|
||||||
INST("000000010000----", Id::MOV32_IMM, Type::ArithmeticImmediate, "MOV32_IMM"),
|
INST("000000010000----", Id::MOV32_IMM, Type::ArithmeticImmediate, "MOV32_IMM"),
|
||||||
INST("0100110001100---", Id::FMNMX_C, Type::Arithmetic, "FMNMX_C"),
|
INST("0100110001100---", Id::FMNMX_C, Type::Arithmetic, "FMNMX_C"),
|
||||||
INST("0101110001100---", Id::FMNMX_R, Type::Arithmetic, "FMNMX_R"),
|
INST("0101110001100---", Id::FMNMX_R, Type::Arithmetic, "FMNMX_R"),
|
||||||
|
|
|
@ -255,7 +255,7 @@ DrawParameters RasterizerOpenGL::SetupDraw() {
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RasterizerOpenGL::SetupShaders() {
|
void RasterizerOpenGL::SetupShaders(GLenum primitive_mode) {
|
||||||
MICROPROFILE_SCOPE(OpenGL_Shader);
|
MICROPROFILE_SCOPE(OpenGL_Shader);
|
||||||
const auto& gpu = Core::System::GetInstance().GPU().Maxwell3D();
|
const auto& gpu = Core::System::GetInstance().GPU().Maxwell3D();
|
||||||
|
|
||||||
|
@ -270,6 +270,11 @@ void RasterizerOpenGL::SetupShaders() {
|
||||||
|
|
||||||
// Skip stages that are not enabled
|
// Skip stages that are not enabled
|
||||||
if (!gpu.regs.IsShaderConfigEnabled(index)) {
|
if (!gpu.regs.IsShaderConfigEnabled(index)) {
|
||||||
|
switch (program) {
|
||||||
|
case Maxwell::ShaderProgram::Geometry:
|
||||||
|
shader_program_manager->UseTrivialGeometryShader();
|
||||||
|
break;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,11 +293,18 @@ void RasterizerOpenGL::SetupShaders() {
|
||||||
switch (program) {
|
switch (program) {
|
||||||
case Maxwell::ShaderProgram::VertexA:
|
case Maxwell::ShaderProgram::VertexA:
|
||||||
case Maxwell::ShaderProgram::VertexB: {
|
case Maxwell::ShaderProgram::VertexB: {
|
||||||
shader_program_manager->UseProgrammableVertexShader(shader->GetProgramHandle());
|
shader_program_manager->UseProgrammableVertexShader(
|
||||||
|
shader->GetProgramHandle(primitive_mode));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Maxwell::ShaderProgram::Geometry: {
|
||||||
|
shader_program_manager->UseProgrammableGeometryShader(
|
||||||
|
shader->GetProgramHandle(primitive_mode));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Maxwell::ShaderProgram::Fragment: {
|
case Maxwell::ShaderProgram::Fragment: {
|
||||||
shader_program_manager->UseProgrammableFragmentShader(shader->GetProgramHandle());
|
shader_program_manager->UseProgrammableFragmentShader(
|
||||||
|
shader->GetProgramHandle(primitive_mode));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -302,12 +314,13 @@ void RasterizerOpenGL::SetupShaders() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the const buffers for this shader stage.
|
// Configure the const buffers for this shader stage.
|
||||||
current_constbuffer_bindpoint = SetupConstBuffers(static_cast<Maxwell::ShaderStage>(stage),
|
current_constbuffer_bindpoint =
|
||||||
shader, current_constbuffer_bindpoint);
|
SetupConstBuffers(static_cast<Maxwell::ShaderStage>(stage), shader, primitive_mode,
|
||||||
|
current_constbuffer_bindpoint);
|
||||||
|
|
||||||
// Configure the textures for this shader stage.
|
// Configure the textures for this shader stage.
|
||||||
current_texture_bindpoint = SetupTextures(static_cast<Maxwell::ShaderStage>(stage), shader,
|
current_texture_bindpoint = SetupTextures(static_cast<Maxwell::ShaderStage>(stage), shader,
|
||||||
current_texture_bindpoint);
|
primitive_mode, current_texture_bindpoint);
|
||||||
|
|
||||||
// When VertexA is enabled, we have dual vertex shaders
|
// When VertexA is enabled, we have dual vertex shaders
|
||||||
if (program == Maxwell::ShaderProgram::VertexA) {
|
if (program == Maxwell::ShaderProgram::VertexA) {
|
||||||
|
@ -317,8 +330,6 @@ void RasterizerOpenGL::SetupShaders() {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
shader_program_manager->UseTrivialGeometryShader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t RasterizerOpenGL::CalculateVertexArraysSize() const {
|
std::size_t RasterizerOpenGL::CalculateVertexArraysSize() const {
|
||||||
|
@ -580,7 +591,7 @@ void RasterizerOpenGL::DrawArrays() {
|
||||||
|
|
||||||
SetupVertexArrays();
|
SetupVertexArrays();
|
||||||
DrawParameters params = SetupDraw();
|
DrawParameters params = SetupDraw();
|
||||||
SetupShaders();
|
SetupShaders(params.primitive_mode);
|
||||||
|
|
||||||
buffer_cache.Unmap();
|
buffer_cache.Unmap();
|
||||||
|
|
||||||
|
@ -719,7 +730,7 @@ void RasterizerOpenGL::SamplerInfo::SyncWithConfig(const Tegra::Texture::TSCEntr
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 RasterizerOpenGL::SetupConstBuffers(Maxwell::ShaderStage stage, Shader& shader,
|
u32 RasterizerOpenGL::SetupConstBuffers(Maxwell::ShaderStage stage, Shader& shader,
|
||||||
u32 current_bindpoint) {
|
GLenum primitive_mode, u32 current_bindpoint) {
|
||||||
MICROPROFILE_SCOPE(OpenGL_UBO);
|
MICROPROFILE_SCOPE(OpenGL_UBO);
|
||||||
const auto& gpu = Core::System::GetInstance().GPU();
|
const auto& gpu = Core::System::GetInstance().GPU();
|
||||||
const auto& maxwell3d = gpu.Maxwell3D();
|
const auto& maxwell3d = gpu.Maxwell3D();
|
||||||
|
@ -771,7 +782,7 @@ u32 RasterizerOpenGL::SetupConstBuffers(Maxwell::ShaderStage stage, Shader& shad
|
||||||
buffer.address, size, static_cast<std::size_t>(uniform_buffer_alignment));
|
buffer.address, size, static_cast<std::size_t>(uniform_buffer_alignment));
|
||||||
|
|
||||||
// Now configure the bindpoint of the buffer inside the shader
|
// Now configure the bindpoint of the buffer inside the shader
|
||||||
glUniformBlockBinding(shader->GetProgramHandle(),
|
glUniformBlockBinding(shader->GetProgramHandle(primitive_mode),
|
||||||
shader->GetProgramResourceIndex(used_buffer),
|
shader->GetProgramResourceIndex(used_buffer),
|
||||||
current_bindpoint + bindpoint);
|
current_bindpoint + bindpoint);
|
||||||
|
|
||||||
|
@ -787,7 +798,8 @@ u32 RasterizerOpenGL::SetupConstBuffers(Maxwell::ShaderStage stage, Shader& shad
|
||||||
return current_bindpoint + static_cast<u32>(entries.size());
|
return current_bindpoint + static_cast<u32>(entries.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 RasterizerOpenGL::SetupTextures(Maxwell::ShaderStage stage, Shader& shader, u32 current_unit) {
|
u32 RasterizerOpenGL::SetupTextures(Maxwell::ShaderStage stage, Shader& shader,
|
||||||
|
GLenum primitive_mode, u32 current_unit) {
|
||||||
MICROPROFILE_SCOPE(OpenGL_Texture);
|
MICROPROFILE_SCOPE(OpenGL_Texture);
|
||||||
const auto& gpu = Core::System::GetInstance().GPU();
|
const auto& gpu = Core::System::GetInstance().GPU();
|
||||||
const auto& maxwell3d = gpu.Maxwell3D();
|
const auto& maxwell3d = gpu.Maxwell3D();
|
||||||
|
@ -802,8 +814,8 @@ u32 RasterizerOpenGL::SetupTextures(Maxwell::ShaderStage stage, Shader& shader,
|
||||||
|
|
||||||
// Bind the uniform to the sampler.
|
// Bind the uniform to the sampler.
|
||||||
|
|
||||||
glProgramUniform1i(shader->GetProgramHandle(), shader->GetUniformLocation(entry),
|
glProgramUniform1i(shader->GetProgramHandle(primitive_mode),
|
||||||
current_bindpoint);
|
shader->GetUniformLocation(entry), current_bindpoint);
|
||||||
|
|
||||||
const auto texture = maxwell3d.GetStageTexture(entry.GetStage(), entry.GetOffset());
|
const auto texture = maxwell3d.GetStageTexture(entry.GetStage(), entry.GetOffset());
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ private:
|
||||||
* @returns The next available bindpoint for use in the next shader stage.
|
* @returns The next available bindpoint for use in the next shader stage.
|
||||||
*/
|
*/
|
||||||
u32 SetupConstBuffers(Tegra::Engines::Maxwell3D::Regs::ShaderStage stage, Shader& shader,
|
u32 SetupConstBuffers(Tegra::Engines::Maxwell3D::Regs::ShaderStage stage, Shader& shader,
|
||||||
u32 current_bindpoint);
|
GLenum primitive_mode, u32 current_bindpoint);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Configures the current textures to use for the draw command.
|
* Configures the current textures to use for the draw command.
|
||||||
|
@ -130,7 +130,7 @@ private:
|
||||||
* @returns The next available bindpoint for use in the next shader stage.
|
* @returns The next available bindpoint for use in the next shader stage.
|
||||||
*/
|
*/
|
||||||
u32 SetupTextures(Tegra::Engines::Maxwell3D::Regs::ShaderStage stage, Shader& shader,
|
u32 SetupTextures(Tegra::Engines::Maxwell3D::Regs::ShaderStage stage, Shader& shader,
|
||||||
u32 current_unit);
|
GLenum primitive_mode, u32 current_unit);
|
||||||
|
|
||||||
/// Syncs the viewport to match the guest state
|
/// Syncs the viewport to match the guest state
|
||||||
void SyncViewport();
|
void SyncViewport();
|
||||||
|
@ -207,7 +207,7 @@ private:
|
||||||
|
|
||||||
DrawParameters SetupDraw();
|
DrawParameters SetupDraw();
|
||||||
|
|
||||||
void SetupShaders();
|
void SetupShaders(GLenum primitive_mode);
|
||||||
|
|
||||||
enum class AccelDraw { Disabled, Arrays, Indexed };
|
enum class AccelDraw { Disabled, Arrays, Indexed };
|
||||||
AccelDraw accelerate_draw = AccelDraw::Disabled;
|
AccelDraw accelerate_draw = AccelDraw::Disabled;
|
||||||
|
|
|
@ -68,6 +68,10 @@ CachedShader::CachedShader(VAddr addr, Maxwell::ShaderProgram program_type)
|
||||||
program_result = GLShader::GenerateVertexShader(setup);
|
program_result = GLShader::GenerateVertexShader(setup);
|
||||||
gl_type = GL_VERTEX_SHADER;
|
gl_type = GL_VERTEX_SHADER;
|
||||||
break;
|
break;
|
||||||
|
case Maxwell::ShaderProgram::Geometry:
|
||||||
|
program_result = GLShader::GenerateGeometryShader(setup);
|
||||||
|
gl_type = GL_GEOMETRY_SHADER;
|
||||||
|
break;
|
||||||
case Maxwell::ShaderProgram::Fragment:
|
case Maxwell::ShaderProgram::Fragment:
|
||||||
program_result = GLShader::GenerateFragmentShader(setup);
|
program_result = GLShader::GenerateFragmentShader(setup);
|
||||||
gl_type = GL_FRAGMENT_SHADER;
|
gl_type = GL_FRAGMENT_SHADER;
|
||||||
|
@ -80,11 +84,16 @@ CachedShader::CachedShader(VAddr addr, Maxwell::ShaderProgram program_type)
|
||||||
|
|
||||||
entries = program_result.second;
|
entries = program_result.second;
|
||||||
|
|
||||||
|
if (program_type != Maxwell::ShaderProgram::Geometry) {
|
||||||
OGLShader shader;
|
OGLShader shader;
|
||||||
shader.Create(program_result.first.c_str(), gl_type);
|
shader.Create(program_result.first.c_str(), gl_type);
|
||||||
program.Create(true, shader.handle);
|
program.Create(true, shader.handle);
|
||||||
SetShaderUniformBlockBindings(program.handle);
|
SetShaderUniformBlockBindings(program.handle);
|
||||||
VideoCore::LabelGLObject(GL_PROGRAM, program.handle, addr);
|
VideoCore::LabelGLObject(GL_PROGRAM, program.handle, addr);
|
||||||
|
} else {
|
||||||
|
// Store shader's code to lazily build it on draw
|
||||||
|
geometry_programs.code = program_result.first;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint CachedShader::GetProgramResourceIndex(const GLShader::ConstBufferEntry& buffer) {
|
GLuint CachedShader::GetProgramResourceIndex(const GLShader::ConstBufferEntry& buffer) {
|
||||||
|
@ -110,6 +119,21 @@ GLint CachedShader::GetUniformLocation(const GLShader::SamplerEntry& sampler) {
|
||||||
return search->second;
|
return search->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GLuint CachedShader::LazyGeometryProgram(OGLProgram& target_program,
|
||||||
|
const std::string& glsl_topology,
|
||||||
|
const std::string& debug_name) {
|
||||||
|
if (target_program.handle != 0) {
|
||||||
|
return target_program.handle;
|
||||||
|
}
|
||||||
|
const std::string source{geometry_programs.code + "layout (" + glsl_topology + ") in;\n"};
|
||||||
|
OGLShader shader;
|
||||||
|
shader.Create(source.c_str(), GL_GEOMETRY_SHADER);
|
||||||
|
target_program.Create(true, shader.handle);
|
||||||
|
SetShaderUniformBlockBindings(target_program.handle);
|
||||||
|
VideoCore::LabelGLObject(GL_PROGRAM, target_program.handle, addr, debug_name);
|
||||||
|
return target_program.handle;
|
||||||
|
};
|
||||||
|
|
||||||
Shader ShaderCacheOpenGL::GetStageProgram(Maxwell::ShaderProgram program) {
|
Shader ShaderCacheOpenGL::GetStageProgram(Maxwell::ShaderProgram program) {
|
||||||
const VAddr program_addr{GetShaderAddress(program)};
|
const VAddr program_addr{GetShaderAddress(program)};
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "common/assert.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "video_core/rasterizer_cache.h"
|
#include "video_core/rasterizer_cache.h"
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
@ -38,9 +39,32 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the GL program handle for the shader
|
/// Gets the GL program handle for the shader
|
||||||
GLuint GetProgramHandle() const {
|
GLuint GetProgramHandle(GLenum primitive_mode) {
|
||||||
|
if (program_type != Maxwell::ShaderProgram::Geometry) {
|
||||||
return program.handle;
|
return program.handle;
|
||||||
}
|
}
|
||||||
|
switch (primitive_mode) {
|
||||||
|
case GL_POINTS:
|
||||||
|
return LazyGeometryProgram(geometry_programs.points, "points", "ShaderPoints");
|
||||||
|
case GL_LINES:
|
||||||
|
case GL_LINE_STRIP:
|
||||||
|
return LazyGeometryProgram(geometry_programs.lines, "lines", "ShaderLines");
|
||||||
|
case GL_LINES_ADJACENCY:
|
||||||
|
case GL_LINE_STRIP_ADJACENCY:
|
||||||
|
return LazyGeometryProgram(geometry_programs.lines_adjacency, "lines_adjacency",
|
||||||
|
"ShaderLinesAdjacency");
|
||||||
|
case GL_TRIANGLES:
|
||||||
|
case GL_TRIANGLE_STRIP:
|
||||||
|
case GL_TRIANGLE_FAN:
|
||||||
|
return LazyGeometryProgram(geometry_programs.triangles, "triangles", "ShaderTriangles");
|
||||||
|
case GL_TRIANGLES_ADJACENCY:
|
||||||
|
case GL_TRIANGLE_STRIP_ADJACENCY:
|
||||||
|
return LazyGeometryProgram(geometry_programs.triangles_adjacency, "triangles_adjacency",
|
||||||
|
"ShaderLines");
|
||||||
|
default:
|
||||||
|
UNREACHABLE_MSG("Unknown primitive mode.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the GL program resource location for the specified resource, caching as needed
|
/// Gets the GL program resource location for the specified resource, caching as needed
|
||||||
GLuint GetProgramResourceIndex(const GLShader::ConstBufferEntry& buffer);
|
GLuint GetProgramResourceIndex(const GLShader::ConstBufferEntry& buffer);
|
||||||
|
@ -49,12 +73,30 @@ public:
|
||||||
GLint GetUniformLocation(const GLShader::SamplerEntry& sampler);
|
GLint GetUniformLocation(const GLShader::SamplerEntry& sampler);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/// Generates a geometry shader or returns one that already exists.
|
||||||
|
GLuint LazyGeometryProgram(OGLProgram& target_program, const std::string& glsl_topology,
|
||||||
|
const std::string& debug_name);
|
||||||
|
|
||||||
VAddr addr;
|
VAddr addr;
|
||||||
Maxwell::ShaderProgram program_type;
|
Maxwell::ShaderProgram program_type;
|
||||||
GLShader::ShaderSetup setup;
|
GLShader::ShaderSetup setup;
|
||||||
GLShader::ShaderEntries entries;
|
GLShader::ShaderEntries entries;
|
||||||
|
|
||||||
|
// Non-geometry program.
|
||||||
OGLProgram program;
|
OGLProgram program;
|
||||||
|
|
||||||
|
// Geometry programs. These are needed because GLSL needs an input topology but it's not
|
||||||
|
// declared by the hardware. Workaround this issue by generating a different shader per input
|
||||||
|
// topology class.
|
||||||
|
struct {
|
||||||
|
std::string code;
|
||||||
|
OGLProgram points;
|
||||||
|
OGLProgram lines;
|
||||||
|
OGLProgram lines_adjacency;
|
||||||
|
OGLProgram triangles;
|
||||||
|
OGLProgram triangles_adjacency;
|
||||||
|
} geometry_programs;
|
||||||
|
|
||||||
std::map<u32, GLuint> resource_cache;
|
std::map<u32, GLuint> resource_cache;
|
||||||
std::map<u32, GLint> uniform_cache;
|
std::map<u32, GLint> uniform_cache;
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <boost/optional.hpp>
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
|
@ -29,11 +30,32 @@ using Tegra::Shader::SubOp;
|
||||||
constexpr u32 PROGRAM_END = MAX_PROGRAM_CODE_LENGTH;
|
constexpr u32 PROGRAM_END = MAX_PROGRAM_CODE_LENGTH;
|
||||||
constexpr u32 PROGRAM_HEADER_SIZE = sizeof(Tegra::Shader::Header);
|
constexpr u32 PROGRAM_HEADER_SIZE = sizeof(Tegra::Shader::Header);
|
||||||
|
|
||||||
|
enum : u32 { POSITION_VARYING_LOCATION = 0, GENERIC_VARYING_START_LOCATION = 1 };
|
||||||
|
|
||||||
|
constexpr u32 MAX_GEOMETRY_BUFFERS = 6;
|
||||||
|
constexpr u32 MAX_ATTRIBUTES = 0x100; // Size in vec4s, this value is untested
|
||||||
|
|
||||||
class DecompileFail : public std::runtime_error {
|
class DecompileFail : public std::runtime_error {
|
||||||
public:
|
public:
|
||||||
using std::runtime_error::runtime_error;
|
using std::runtime_error::runtime_error;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Translate topology
|
||||||
|
static std::string GetTopologyName(Tegra::Shader::OutputTopology topology) {
|
||||||
|
switch (topology) {
|
||||||
|
case Tegra::Shader::OutputTopology::PointList:
|
||||||
|
return "points";
|
||||||
|
case Tegra::Shader::OutputTopology::LineStrip:
|
||||||
|
return "line_strip";
|
||||||
|
case Tegra::Shader::OutputTopology::TriangleStrip:
|
||||||
|
return "triangle_strip";
|
||||||
|
default:
|
||||||
|
LOG_CRITICAL(Render_OpenGL, "Unknown output topology {}", static_cast<u32>(topology));
|
||||||
|
UNREACHABLE();
|
||||||
|
return "points";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Describes the behaviour of code path of a given entry point and a return point.
|
/// Describes the behaviour of code path of a given entry point and a return point.
|
||||||
enum class ExitMethod {
|
enum class ExitMethod {
|
||||||
Undetermined, ///< Internal value. Only occur when analyzing JMP loop.
|
Undetermined, ///< Internal value. Only occur when analyzing JMP loop.
|
||||||
|
@ -253,8 +275,9 @@ enum class InternalFlag : u64 {
|
||||||
class GLSLRegisterManager {
|
class GLSLRegisterManager {
|
||||||
public:
|
public:
|
||||||
GLSLRegisterManager(ShaderWriter& shader, ShaderWriter& declarations,
|
GLSLRegisterManager(ShaderWriter& shader, ShaderWriter& declarations,
|
||||||
const Maxwell3D::Regs::ShaderStage& stage, const std::string& suffix)
|
const Maxwell3D::Regs::ShaderStage& stage, const std::string& suffix,
|
||||||
: shader{shader}, declarations{declarations}, stage{stage}, suffix{suffix} {
|
const Tegra::Shader::Header& header)
|
||||||
|
: shader{shader}, declarations{declarations}, stage{stage}, suffix{suffix}, header{header} {
|
||||||
BuildRegisterList();
|
BuildRegisterList();
|
||||||
BuildInputList();
|
BuildInputList();
|
||||||
}
|
}
|
||||||
|
@ -358,11 +381,13 @@ public:
|
||||||
* @param reg The destination register to use.
|
* @param reg The destination register to use.
|
||||||
* @param elem The element to use for the operation.
|
* @param elem The element to use for the operation.
|
||||||
* @param attribute The input attribute to use as the source value.
|
* @param attribute The input attribute to use as the source value.
|
||||||
|
* @param vertex The register that decides which vertex to read from (used in GS).
|
||||||
*/
|
*/
|
||||||
void SetRegisterToInputAttibute(const Register& reg, u64 elem, Attribute::Index attribute,
|
void SetRegisterToInputAttibute(const Register& reg, u64 elem, Attribute::Index attribute,
|
||||||
const Tegra::Shader::IpaMode& input_mode) {
|
const Tegra::Shader::IpaMode& input_mode,
|
||||||
|
boost::optional<Register> vertex = {}) {
|
||||||
const std::string dest = GetRegisterAsFloat(reg);
|
const std::string dest = GetRegisterAsFloat(reg);
|
||||||
const std::string src = GetInputAttribute(attribute, input_mode) + GetSwizzle(elem);
|
const std::string src = GetInputAttribute(attribute, input_mode, vertex) + GetSwizzle(elem);
|
||||||
shader.AddLine(dest + " = " + src + ';');
|
shader.AddLine(dest + " = " + src + ';');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,18 +416,31 @@ public:
|
||||||
* are stored as floats, so this may require conversion.
|
* are stored as floats, so this may require conversion.
|
||||||
* @param attribute The destination output attribute.
|
* @param attribute The destination output attribute.
|
||||||
* @param elem The element to use for the operation.
|
* @param elem The element to use for the operation.
|
||||||
* @param reg The register to use as the source value.
|
* @param val_reg The register to use as the source value.
|
||||||
|
* @param buf_reg The register that tells which buffer to write to (used in geometry shaders).
|
||||||
*/
|
*/
|
||||||
void SetOutputAttributeToRegister(Attribute::Index attribute, u64 elem, const Register& reg) {
|
void SetOutputAttributeToRegister(Attribute::Index attribute, u64 elem, const Register& val_reg,
|
||||||
|
const Register& buf_reg) {
|
||||||
const std::string dest = GetOutputAttribute(attribute);
|
const std::string dest = GetOutputAttribute(attribute);
|
||||||
const std::string src = GetRegisterAsFloat(reg);
|
const std::string src = GetRegisterAsFloat(val_reg);
|
||||||
|
|
||||||
if (!dest.empty()) {
|
if (!dest.empty()) {
|
||||||
// Can happen with unknown/unimplemented output attributes, in which case we ignore the
|
// Can happen with unknown/unimplemented output attributes, in which case we ignore the
|
||||||
// instruction for now.
|
// instruction for now.
|
||||||
|
if (stage == Maxwell3D::Regs::ShaderStage::Geometry) {
|
||||||
|
// TODO(Rodrigo): nouveau sets some attributes after setting emitting a geometry
|
||||||
|
// shader. These instructions use a dirty register as buffer index. To avoid some
|
||||||
|
// drivers from complaining for the out of boundary writes, guard them.
|
||||||
|
const std::string buf_index{"min(" + GetRegisterAsInteger(buf_reg) + ", " +
|
||||||
|
std::to_string(MAX_GEOMETRY_BUFFERS - 1) + ')'};
|
||||||
|
shader.AddLine("amem[" + buf_index + "][" +
|
||||||
|
std::to_string(static_cast<u32>(attribute)) + ']' +
|
||||||
|
GetSwizzle(elem) + " = " + src + ';');
|
||||||
|
} else {
|
||||||
shader.AddLine(dest + GetSwizzle(elem) + " = " + src + ';');
|
shader.AddLine(dest + GetSwizzle(elem) + " = " + src + ';');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates code representing a uniform (C buffer) register, interpreted as the input type.
|
/// Generates code representing a uniform (C buffer) register, interpreted as the input type.
|
||||||
std::string GetUniform(u64 index, u64 offset, GLSLRegister::Type type,
|
std::string GetUniform(u64 index, u64 offset, GLSLRegister::Type type,
|
||||||
|
@ -441,58 +479,18 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add declarations for registers
|
/// Add declarations.
|
||||||
void GenerateDeclarations(const std::string& suffix) {
|
void GenerateDeclarations(const std::string& suffix) {
|
||||||
for (const auto& reg : regs) {
|
GenerateRegisters(suffix);
|
||||||
declarations.AddLine(GLSLRegister::GetTypeString() + ' ' + reg.GetPrefixString() +
|
GenerateInternalFlags();
|
||||||
std::to_string(reg.GetIndex()) + '_' + suffix + " = 0;");
|
GenerateInputAttrs();
|
||||||
}
|
GenerateOutputAttrs();
|
||||||
declarations.AddNewLine();
|
GenerateConstBuffers();
|
||||||
|
GenerateSamplers();
|
||||||
for (u32 ii = 0; ii < static_cast<u64>(InternalFlag::Amount); ii++) {
|
GenerateGeometry();
|
||||||
const InternalFlag code = static_cast<InternalFlag>(ii);
|
|
||||||
declarations.AddLine("bool " + GetInternalFlag(code) + " = false;");
|
|
||||||
}
|
|
||||||
declarations.AddNewLine();
|
|
||||||
|
|
||||||
for (const auto element : declr_input_attribute) {
|
|
||||||
// TODO(bunnei): Use proper number of elements for these
|
|
||||||
u32 idx =
|
|
||||||
static_cast<u32>(element.first) - static_cast<u32>(Attribute::Index::Attribute_0);
|
|
||||||
declarations.AddLine("layout(location = " + std::to_string(idx) + ")" +
|
|
||||||
GetInputFlags(element.first) + "in vec4 " +
|
|
||||||
GetInputAttribute(element.first, element.second) + ';');
|
|
||||||
}
|
|
||||||
declarations.AddNewLine();
|
|
||||||
|
|
||||||
for (const auto& index : declr_output_attribute) {
|
|
||||||
// TODO(bunnei): Use proper number of elements for these
|
|
||||||
declarations.AddLine("layout(location = " +
|
|
||||||
std::to_string(static_cast<u32>(index) -
|
|
||||||
static_cast<u32>(Attribute::Index::Attribute_0)) +
|
|
||||||
") out vec4 " + GetOutputAttribute(index) + ';');
|
|
||||||
}
|
|
||||||
declarations.AddNewLine();
|
|
||||||
|
|
||||||
for (const auto& entry : GetConstBuffersDeclarations()) {
|
|
||||||
declarations.AddLine("layout(std140) uniform " + entry.GetName());
|
|
||||||
declarations.AddLine('{');
|
|
||||||
declarations.AddLine(" vec4 c" + std::to_string(entry.GetIndex()) +
|
|
||||||
"[MAX_CONSTBUFFER_ELEMENTS];");
|
|
||||||
declarations.AddLine("};");
|
|
||||||
declarations.AddNewLine();
|
|
||||||
}
|
|
||||||
declarations.AddNewLine();
|
|
||||||
|
|
||||||
const auto& samplers = GetSamplers();
|
|
||||||
for (const auto& sampler : samplers) {
|
|
||||||
declarations.AddLine("uniform " + sampler.GetTypeString() + ' ' + sampler.GetName() +
|
|
||||||
';');
|
|
||||||
}
|
|
||||||
declarations.AddNewLine();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a list of constant buffer declarations
|
/// Returns a list of constant buffer declarations.
|
||||||
std::vector<ConstBufferEntry> GetConstBuffersDeclarations() const {
|
std::vector<ConstBufferEntry> GetConstBuffersDeclarations() const {
|
||||||
std::vector<ConstBufferEntry> result;
|
std::vector<ConstBufferEntry> result;
|
||||||
std::copy_if(declr_const_buffers.begin(), declr_const_buffers.end(),
|
std::copy_if(declr_const_buffers.begin(), declr_const_buffers.end(),
|
||||||
|
@ -500,7 +498,7 @@ public:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a list of samplers used in the shader
|
/// Returns a list of samplers used in the shader.
|
||||||
const std::vector<SamplerEntry>& GetSamplers() const {
|
const std::vector<SamplerEntry>& GetSamplers() const {
|
||||||
return used_samplers;
|
return used_samplers;
|
||||||
}
|
}
|
||||||
|
@ -509,7 +507,7 @@ public:
|
||||||
/// necessary.
|
/// necessary.
|
||||||
std::string AccessSampler(const Sampler& sampler, Tegra::Shader::TextureType type,
|
std::string AccessSampler(const Sampler& sampler, Tegra::Shader::TextureType type,
|
||||||
bool is_array, bool is_shadow) {
|
bool is_array, bool is_shadow) {
|
||||||
const std::size_t offset = static_cast<std::size_t>(sampler.index.Value());
|
const auto offset = static_cast<std::size_t>(sampler.index.Value());
|
||||||
|
|
||||||
// If this sampler has already been used, return the existing mapping.
|
// If this sampler has already been used, return the existing mapping.
|
||||||
const auto itr =
|
const auto itr =
|
||||||
|
@ -530,6 +528,129 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/// Generates declarations for registers.
|
||||||
|
void GenerateRegisters(const std::string& suffix) {
|
||||||
|
for (const auto& reg : regs) {
|
||||||
|
declarations.AddLine(GLSLRegister::GetTypeString() + ' ' + reg.GetPrefixString() +
|
||||||
|
std::to_string(reg.GetIndex()) + '_' + suffix + " = 0;");
|
||||||
|
}
|
||||||
|
declarations.AddNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates declarations for internal flags.
|
||||||
|
void GenerateInternalFlags() {
|
||||||
|
for (u32 ii = 0; ii < static_cast<u64>(InternalFlag::Amount); ii++) {
|
||||||
|
const InternalFlag code = static_cast<InternalFlag>(ii);
|
||||||
|
declarations.AddLine("bool " + GetInternalFlag(code) + " = false;");
|
||||||
|
}
|
||||||
|
declarations.AddNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates declarations for input attributes.
|
||||||
|
void GenerateInputAttrs() {
|
||||||
|
if (stage != Maxwell3D::Regs::ShaderStage::Vertex) {
|
||||||
|
const std::string attr =
|
||||||
|
stage == Maxwell3D::Regs::ShaderStage::Geometry ? "gs_position[]" : "position";
|
||||||
|
declarations.AddLine("layout (location = " + std::to_string(POSITION_VARYING_LOCATION) +
|
||||||
|
") in vec4 " + attr + ';');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto element : declr_input_attribute) {
|
||||||
|
// TODO(bunnei): Use proper number of elements for these
|
||||||
|
u32 idx =
|
||||||
|
static_cast<u32>(element.first) - static_cast<u32>(Attribute::Index::Attribute_0);
|
||||||
|
if (stage != Maxwell3D::Regs::ShaderStage::Vertex) {
|
||||||
|
// If inputs are varyings, add an offset
|
||||||
|
idx += GENERIC_VARYING_START_LOCATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string attr{GetInputAttribute(element.first, element.second)};
|
||||||
|
if (stage == Maxwell3D::Regs::ShaderStage::Geometry) {
|
||||||
|
attr = "gs_" + attr + "[]";
|
||||||
|
}
|
||||||
|
declarations.AddLine("layout (location = " + std::to_string(idx) + ") " +
|
||||||
|
GetInputFlags(element.first) + "in vec4 " + attr + ';');
|
||||||
|
}
|
||||||
|
|
||||||
|
declarations.AddNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates declarations for output attributes.
|
||||||
|
void GenerateOutputAttrs() {
|
||||||
|
if (stage != Maxwell3D::Regs::ShaderStage::Fragment) {
|
||||||
|
declarations.AddLine("layout (location = " + std::to_string(POSITION_VARYING_LOCATION) +
|
||||||
|
") out vec4 position;");
|
||||||
|
}
|
||||||
|
for (const auto& index : declr_output_attribute) {
|
||||||
|
// TODO(bunnei): Use proper number of elements for these
|
||||||
|
const u32 idx = static_cast<u32>(index) -
|
||||||
|
static_cast<u32>(Attribute::Index::Attribute_0) +
|
||||||
|
GENERIC_VARYING_START_LOCATION;
|
||||||
|
declarations.AddLine("layout (location = " + std::to_string(idx) + ") out vec4 " +
|
||||||
|
GetOutputAttribute(index) + ';');
|
||||||
|
}
|
||||||
|
declarations.AddNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates declarations for constant buffers.
|
||||||
|
void GenerateConstBuffers() {
|
||||||
|
for (const auto& entry : GetConstBuffersDeclarations()) {
|
||||||
|
declarations.AddLine("layout (std140) uniform " + entry.GetName());
|
||||||
|
declarations.AddLine('{');
|
||||||
|
declarations.AddLine(" vec4 c" + std::to_string(entry.GetIndex()) +
|
||||||
|
"[MAX_CONSTBUFFER_ELEMENTS];");
|
||||||
|
declarations.AddLine("};");
|
||||||
|
declarations.AddNewLine();
|
||||||
|
}
|
||||||
|
declarations.AddNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates declarations for samplers.
|
||||||
|
void GenerateSamplers() {
|
||||||
|
const auto& samplers = GetSamplers();
|
||||||
|
for (const auto& sampler : samplers) {
|
||||||
|
declarations.AddLine("uniform " + sampler.GetTypeString() + ' ' + sampler.GetName() +
|
||||||
|
';');
|
||||||
|
}
|
||||||
|
declarations.AddNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates declarations used for geometry shaders.
|
||||||
|
void GenerateGeometry() {
|
||||||
|
if (stage != Maxwell3D::Regs::ShaderStage::Geometry)
|
||||||
|
return;
|
||||||
|
|
||||||
|
declarations.AddLine(
|
||||||
|
"layout (" + GetTopologyName(header.common3.output_topology) +
|
||||||
|
", max_vertices = " + std::to_string(header.common4.max_output_vertices) + ") out;");
|
||||||
|
declarations.AddNewLine();
|
||||||
|
|
||||||
|
declarations.AddLine("vec4 amem[" + std::to_string(MAX_GEOMETRY_BUFFERS) + "][" +
|
||||||
|
std::to_string(MAX_ATTRIBUTES) + "];");
|
||||||
|
declarations.AddNewLine();
|
||||||
|
|
||||||
|
constexpr char buffer[] = "amem[output_buffer]";
|
||||||
|
declarations.AddLine("void emit_vertex(uint output_buffer) {");
|
||||||
|
++declarations.scope;
|
||||||
|
for (const auto element : declr_output_attribute) {
|
||||||
|
declarations.AddLine(GetOutputAttribute(element) + " = " + buffer + '[' +
|
||||||
|
std::to_string(static_cast<u32>(element)) + "];");
|
||||||
|
}
|
||||||
|
|
||||||
|
declarations.AddLine("position = " + std::string(buffer) + '[' +
|
||||||
|
std::to_string(static_cast<u32>(Attribute::Index::Position)) + "];");
|
||||||
|
|
||||||
|
// If a geometry shader is attached, it will always flip (it's the last stage before
|
||||||
|
// fragment). For more info about flipping, refer to gl_shader_gen.cpp.
|
||||||
|
declarations.AddLine("position.xy *= viewport_flip.xy;");
|
||||||
|
declarations.AddLine("gl_Position = position;");
|
||||||
|
declarations.AddLine("position.w = 1.0;");
|
||||||
|
declarations.AddLine("EmitVertex();");
|
||||||
|
--declarations.scope;
|
||||||
|
declarations.AddLine('}');
|
||||||
|
declarations.AddNewLine();
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates code representing a temporary (GPR) register.
|
/// Generates code representing a temporary (GPR) register.
|
||||||
std::string GetRegister(const Register& reg, unsigned elem) {
|
std::string GetRegister(const Register& reg, unsigned elem) {
|
||||||
if (reg == Register::ZeroIndex) {
|
if (reg == Register::ZeroIndex) {
|
||||||
|
@ -586,11 +707,19 @@ private:
|
||||||
|
|
||||||
/// Generates code representing an input attribute register.
|
/// Generates code representing an input attribute register.
|
||||||
std::string GetInputAttribute(Attribute::Index attribute,
|
std::string GetInputAttribute(Attribute::Index attribute,
|
||||||
const Tegra::Shader::IpaMode& input_mode) {
|
const Tegra::Shader::IpaMode& input_mode,
|
||||||
|
boost::optional<Register> vertex = {}) {
|
||||||
|
auto GeometryPass = [&](const std::string& name) {
|
||||||
|
if (stage == Maxwell3D::Regs::ShaderStage::Geometry && vertex) {
|
||||||
|
return "gs_" + name + '[' + GetRegisterAsInteger(vertex.value(), 0, false) + ']';
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
switch (attribute) {
|
switch (attribute) {
|
||||||
case Attribute::Index::Position:
|
case Attribute::Index::Position:
|
||||||
if (stage != Maxwell3D::Regs::ShaderStage::Fragment) {
|
if (stage != Maxwell3D::Regs::ShaderStage::Fragment) {
|
||||||
return "position";
|
return GeometryPass("position");
|
||||||
} else {
|
} else {
|
||||||
return "vec4(gl_FragCoord.x, gl_FragCoord.y, gl_FragCoord.z, 1.0)";
|
return "vec4(gl_FragCoord.x, gl_FragCoord.y, gl_FragCoord.z, 1.0)";
|
||||||
}
|
}
|
||||||
|
@ -619,7 +748,7 @@ private:
|
||||||
UNREACHABLE();
|
UNREACHABLE();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "input_attribute_" + std::to_string(index);
|
return GeometryPass("input_attribute_" + std::to_string(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_CRITICAL(HW_GPU, "Unhandled input attribute: {}", static_cast<u32>(attribute));
|
LOG_CRITICAL(HW_GPU, "Unhandled input attribute: {}", static_cast<u32>(attribute));
|
||||||
|
@ -672,7 +801,7 @@ private:
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates code representing an output attribute register.
|
/// Generates code representing the declaration name of an output attribute register.
|
||||||
std::string GetOutputAttribute(Attribute::Index attribute) {
|
std::string GetOutputAttribute(Attribute::Index attribute) {
|
||||||
switch (attribute) {
|
switch (attribute) {
|
||||||
case Attribute::Index::Position:
|
case Attribute::Index::Position:
|
||||||
|
@ -708,6 +837,7 @@ private:
|
||||||
std::vector<SamplerEntry> used_samplers;
|
std::vector<SamplerEntry> used_samplers;
|
||||||
const Maxwell3D::Regs::ShaderStage& stage;
|
const Maxwell3D::Regs::ShaderStage& stage;
|
||||||
const std::string& suffix;
|
const std::string& suffix;
|
||||||
|
const Tegra::Shader::Header& header;
|
||||||
};
|
};
|
||||||
|
|
||||||
class GLSLGenerator {
|
class GLSLGenerator {
|
||||||
|
@ -1103,8 +1233,8 @@ private:
|
||||||
return offset + 1;
|
return offset + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
shader.AddLine("// " + std::to_string(offset) + ": " + opcode->GetName() + " (" +
|
shader.AddLine(
|
||||||
std::to_string(instr.value) + ')');
|
fmt::format("// {}: {} (0x{:016x})", offset, opcode->GetName(), instr.value));
|
||||||
|
|
||||||
using Tegra::Shader::Pred;
|
using Tegra::Shader::Pred;
|
||||||
ASSERT_MSG(instr.pred.full_pred != Pred::NeverExecute,
|
ASSERT_MSG(instr.pred.full_pred != Pred::NeverExecute,
|
||||||
|
@ -1826,7 +1956,7 @@ private:
|
||||||
const auto LoadNextElement = [&](u32 reg_offset) {
|
const auto LoadNextElement = [&](u32 reg_offset) {
|
||||||
regs.SetRegisterToInputAttibute(instr.gpr0.Value() + reg_offset, next_element,
|
regs.SetRegisterToInputAttibute(instr.gpr0.Value() + reg_offset, next_element,
|
||||||
static_cast<Attribute::Index>(next_index),
|
static_cast<Attribute::Index>(next_index),
|
||||||
input_mode);
|
input_mode, instr.gpr39.Value());
|
||||||
|
|
||||||
// Load the next attribute element into the following register. If the element
|
// Load the next attribute element into the following register. If the element
|
||||||
// to load goes beyond the vec4 size, load the first element of the next
|
// to load goes beyond the vec4 size, load the first element of the next
|
||||||
|
@ -1890,8 +2020,8 @@ private:
|
||||||
|
|
||||||
const auto StoreNextElement = [&](u32 reg_offset) {
|
const auto StoreNextElement = [&](u32 reg_offset) {
|
||||||
regs.SetOutputAttributeToRegister(static_cast<Attribute::Index>(next_index),
|
regs.SetOutputAttributeToRegister(static_cast<Attribute::Index>(next_index),
|
||||||
next_element,
|
next_element, instr.gpr0.Value() + reg_offset,
|
||||||
instr.gpr0.Value() + reg_offset);
|
instr.gpr39.Value());
|
||||||
|
|
||||||
// Load the next attribute element into the following register. If the element
|
// Load the next attribute element into the following register. If the element
|
||||||
// to load goes beyond the vec4 size, load the first element of the next
|
// to load goes beyond the vec4 size, load the first element of the next
|
||||||
|
@ -2734,6 +2864,52 @@ private:
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OpCode::Id::OUT_R: {
|
||||||
|
ASSERT(instr.gpr20.Value() == Register::ZeroIndex);
|
||||||
|
ASSERT_MSG(stage == Maxwell3D::Regs::ShaderStage::Geometry,
|
||||||
|
"OUT is expected to be used in a geometry shader.");
|
||||||
|
|
||||||
|
if (instr.out.emit) {
|
||||||
|
// gpr0 is used to store the next address. Hardware returns a pointer but
|
||||||
|
// we just return the next index with a cyclic cap.
|
||||||
|
const std::string current{regs.GetRegisterAsInteger(instr.gpr8, 0, false)};
|
||||||
|
const std::string next = "((" + current + " + 1" + ") % " +
|
||||||
|
std::to_string(MAX_GEOMETRY_BUFFERS) + ')';
|
||||||
|
shader.AddLine("emit_vertex(" + current + ");");
|
||||||
|
regs.SetRegisterToInteger(instr.gpr0, false, 0, next, 1, 1);
|
||||||
|
}
|
||||||
|
if (instr.out.cut) {
|
||||||
|
shader.AddLine("EndPrimitive();");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OpCode::Id::MOV_SYS: {
|
||||||
|
switch (instr.sys20) {
|
||||||
|
case Tegra::Shader::SystemVariable::InvocationInfo: {
|
||||||
|
LOG_WARNING(HW_GPU, "MOV_SYS instruction with InvocationInfo is incomplete");
|
||||||
|
regs.SetRegisterToInteger(instr.gpr0, false, 0, "0u", 1, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
LOG_CRITICAL(HW_GPU, "Unhandled system move: {}",
|
||||||
|
static_cast<u32>(instr.sys20.Value()));
|
||||||
|
UNREACHABLE();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OpCode::Id::ISBERD: {
|
||||||
|
ASSERT(instr.isberd.o == 0);
|
||||||
|
ASSERT(instr.isberd.skew == 0);
|
||||||
|
ASSERT(instr.isberd.shift == Tegra::Shader::IsberdShift::None);
|
||||||
|
ASSERT(instr.isberd.mode == Tegra::Shader::IsberdMode::None);
|
||||||
|
ASSERT_MSG(stage == Maxwell3D::Regs::ShaderStage::Geometry,
|
||||||
|
"ISBERD is expected to be used in a geometry shader.");
|
||||||
|
LOG_WARNING(HW_GPU, "ISBERD instruction is incomplete");
|
||||||
|
regs.SetRegisterToFloat(instr.gpr0, 0, regs.GetRegisterAsFloat(instr.gpr8), 1, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case OpCode::Id::BRA: {
|
case OpCode::Id::BRA: {
|
||||||
ASSERT_MSG(instr.bra.constant_buffer == 0,
|
ASSERT_MSG(instr.bra.constant_buffer == 0,
|
||||||
"BRA with constant buffers are not implemented");
|
"BRA with constant buffers are not implemented");
|
||||||
|
@ -2907,7 +3083,7 @@ private:
|
||||||
|
|
||||||
ShaderWriter shader;
|
ShaderWriter shader;
|
||||||
ShaderWriter declarations;
|
ShaderWriter declarations;
|
||||||
GLSLRegisterManager regs{shader, declarations, stage, suffix};
|
GLSLRegisterManager regs{shader, declarations, stage, suffix, header};
|
||||||
|
|
||||||
// Declarations
|
// Declarations
|
||||||
std::set<std::string> declr_predicates;
|
std::set<std::string> declr_predicates;
|
||||||
|
|
|
@ -17,7 +17,18 @@ ProgramResult GenerateVertexShader(const ShaderSetup& setup) {
|
||||||
std::string out = "#version 430 core\n";
|
std::string out = "#version 430 core\n";
|
||||||
out += "#extension GL_ARB_separate_shader_objects : enable\n\n";
|
out += "#extension GL_ARB_separate_shader_objects : enable\n\n";
|
||||||
out += Decompiler::GetCommonDeclarations();
|
out += Decompiler::GetCommonDeclarations();
|
||||||
out += "bool exec_vertex();\n";
|
|
||||||
|
out += R"(
|
||||||
|
out gl_PerVertex {
|
||||||
|
vec4 gl_Position;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(std140) uniform vs_config {
|
||||||
|
vec4 viewport_flip;
|
||||||
|
uvec4 instance_id;
|
||||||
|
uvec4 flip_stage;
|
||||||
|
};
|
||||||
|
)";
|
||||||
|
|
||||||
if (setup.IsDualProgram()) {
|
if (setup.IsDualProgram()) {
|
||||||
out += "bool exec_vertex_b();\n";
|
out += "bool exec_vertex_b();\n";
|
||||||
|
@ -28,19 +39,18 @@ ProgramResult GenerateVertexShader(const ShaderSetup& setup) {
|
||||||
Maxwell3D::Regs::ShaderStage::Vertex, "vertex")
|
Maxwell3D::Regs::ShaderStage::Vertex, "vertex")
|
||||||
.get_value_or({});
|
.get_value_or({});
|
||||||
|
|
||||||
|
out += program.first;
|
||||||
|
|
||||||
|
if (setup.IsDualProgram()) {
|
||||||
|
ProgramResult program_b =
|
||||||
|
Decompiler::DecompileProgram(setup.program.code_b, PROGRAM_OFFSET,
|
||||||
|
Maxwell3D::Regs::ShaderStage::Vertex, "vertex_b")
|
||||||
|
.get_value_or({});
|
||||||
|
out += program_b.first;
|
||||||
|
}
|
||||||
|
|
||||||
out += R"(
|
out += R"(
|
||||||
|
|
||||||
out gl_PerVertex {
|
|
||||||
vec4 gl_Position;
|
|
||||||
};
|
|
||||||
|
|
||||||
out vec4 position;
|
|
||||||
|
|
||||||
layout (std140) uniform vs_config {
|
|
||||||
vec4 viewport_flip;
|
|
||||||
uvec4 instance_id;
|
|
||||||
};
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
position = vec4(0.0, 0.0, 0.0, 0.0);
|
position = vec4(0.0, 0.0, 0.0, 0.0);
|
||||||
exec_vertex();
|
exec_vertex();
|
||||||
|
@ -52,27 +62,52 @@ void main() {
|
||||||
|
|
||||||
out += R"(
|
out += R"(
|
||||||
|
|
||||||
|
// Check if the flip stage is VertexB
|
||||||
|
if (flip_stage[0] == 1) {
|
||||||
// Viewport can be flipped, which is unsupported by glViewport
|
// Viewport can be flipped, which is unsupported by glViewport
|
||||||
position.xy *= viewport_flip.xy;
|
position.xy *= viewport_flip.xy;
|
||||||
|
}
|
||||||
gl_Position = position;
|
gl_Position = position;
|
||||||
|
|
||||||
// TODO(bunnei): This is likely a hack, position.w should be interpolated as 1.0
|
// TODO(bunnei): This is likely a hack, position.w should be interpolated as 1.0
|
||||||
// For now, this is here to bring order in lieu of proper emulation
|
// For now, this is here to bring order in lieu of proper emulation
|
||||||
|
if (flip_stage[0] == 1) {
|
||||||
position.w = 1.0;
|
position.w = 1.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
)";
|
)";
|
||||||
|
|
||||||
out += program.first;
|
return {out, program.second};
|
||||||
|
}
|
||||||
|
|
||||||
if (setup.IsDualProgram()) {
|
ProgramResult GenerateGeometryShader(const ShaderSetup& setup) {
|
||||||
ProgramResult program_b =
|
std::string out = "#version 430 core\n";
|
||||||
Decompiler::DecompileProgram(setup.program.code_b, PROGRAM_OFFSET,
|
out += "#extension GL_ARB_separate_shader_objects : enable\n\n";
|
||||||
Maxwell3D::Regs::ShaderStage::Vertex, "vertex_b")
|
out += Decompiler::GetCommonDeclarations();
|
||||||
|
out += "bool exec_geometry();\n";
|
||||||
|
|
||||||
|
ProgramResult program =
|
||||||
|
Decompiler::DecompileProgram(setup.program.code, PROGRAM_OFFSET,
|
||||||
|
Maxwell3D::Regs::ShaderStage::Geometry, "geometry")
|
||||||
.get_value_or({});
|
.get_value_or({});
|
||||||
out += program_b.first;
|
out += R"(
|
||||||
}
|
out gl_PerVertex {
|
||||||
|
vec4 gl_Position;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout (std140) uniform gs_config {
|
||||||
|
vec4 viewport_flip;
|
||||||
|
uvec4 instance_id;
|
||||||
|
uvec4 flip_stage;
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
exec_geometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
)";
|
||||||
|
out += program.first;
|
||||||
return {out, program.second};
|
return {out, program.second};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +122,6 @@ ProgramResult GenerateFragmentShader(const ShaderSetup& setup) {
|
||||||
Maxwell3D::Regs::ShaderStage::Fragment, "fragment")
|
Maxwell3D::Regs::ShaderStage::Fragment, "fragment")
|
||||||
.get_value_or({});
|
.get_value_or({});
|
||||||
out += R"(
|
out += R"(
|
||||||
in vec4 position;
|
|
||||||
layout(location = 0) out vec4 FragColor0;
|
layout(location = 0) out vec4 FragColor0;
|
||||||
layout(location = 1) out vec4 FragColor1;
|
layout(location = 1) out vec4 FragColor1;
|
||||||
layout(location = 2) out vec4 FragColor2;
|
layout(location = 2) out vec4 FragColor2;
|
||||||
|
@ -100,6 +134,7 @@ layout(location = 7) out vec4 FragColor7;
|
||||||
layout (std140) uniform fs_config {
|
layout (std140) uniform fs_config {
|
||||||
vec4 viewport_flip;
|
vec4 viewport_flip;
|
||||||
uvec4 instance_id;
|
uvec4 instance_id;
|
||||||
|
uvec4 flip_stage;
|
||||||
};
|
};
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
@ -110,5 +145,4 @@ void main() {
|
||||||
out += program.first;
|
out += program.first;
|
||||||
return {out, program.second};
|
return {out, program.second};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace OpenGL::GLShader
|
} // namespace OpenGL::GLShader
|
|
@ -195,6 +195,12 @@ private:
|
||||||
*/
|
*/
|
||||||
ProgramResult GenerateVertexShader(const ShaderSetup& setup);
|
ProgramResult GenerateVertexShader(const ShaderSetup& setup);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the GLSL geometry shader program source code for the given GS program
|
||||||
|
* @returns String of the shader source code
|
||||||
|
*/
|
||||||
|
ProgramResult GenerateGeometryShader(const ShaderSetup& setup);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the GLSL fragment shader program source code for the given FS program
|
* Generates the GLSL fragment shader program source code for the given FS program
|
||||||
* @returns String of the shader source code
|
* @returns String of the shader source code
|
||||||
|
|
|
@ -18,6 +18,14 @@ void MaxwellUniformData::SetFromRegs(const Maxwell3D::State::ShaderStageInfo& sh
|
||||||
|
|
||||||
// We only assign the instance to the first component of the vector, the rest is just padding.
|
// We only assign the instance to the first component of the vector, the rest is just padding.
|
||||||
instance_id[0] = state.current_instance;
|
instance_id[0] = state.current_instance;
|
||||||
|
|
||||||
|
// Assign in which stage the position has to be flipped
|
||||||
|
// (the last stage before the fragment shader).
|
||||||
|
if (gpu.regs.shader_config[static_cast<u32>(Maxwell3D::Regs::ShaderProgram::Geometry)].enable) {
|
||||||
|
flip_stage[0] = static_cast<u32>(Maxwell3D::Regs::ShaderProgram::Geometry);
|
||||||
|
} else {
|
||||||
|
flip_stage[0] = static_cast<u32>(Maxwell3D::Regs::ShaderProgram::VertexB);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace OpenGL::GLShader
|
} // namespace OpenGL::GLShader
|
||||||
|
|
|
@ -21,8 +21,9 @@ struct MaxwellUniformData {
|
||||||
void SetFromRegs(const Maxwell3D::State::ShaderStageInfo& shader_stage);
|
void SetFromRegs(const Maxwell3D::State::ShaderStageInfo& shader_stage);
|
||||||
alignas(16) GLvec4 viewport_flip;
|
alignas(16) GLvec4 viewport_flip;
|
||||||
alignas(16) GLuvec4 instance_id;
|
alignas(16) GLuvec4 instance_id;
|
||||||
|
alignas(16) GLuvec4 flip_stage;
|
||||||
};
|
};
|
||||||
static_assert(sizeof(MaxwellUniformData) == 32, "MaxwellUniformData structure size is incorrect");
|
static_assert(sizeof(MaxwellUniformData) == 48, "MaxwellUniformData structure size is incorrect");
|
||||||
static_assert(sizeof(MaxwellUniformData) < 16384,
|
static_assert(sizeof(MaxwellUniformData) < 16384,
|
||||||
"MaxwellUniformData structure must be less than 16kb as per the OpenGL spec");
|
"MaxwellUniformData structure must be less than 16kb as per the OpenGL spec");
|
||||||
|
|
||||||
|
@ -36,6 +37,10 @@ public:
|
||||||
vs = program;
|
vs = program;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UseProgrammableGeometryShader(GLuint program) {
|
||||||
|
gs = program;
|
||||||
|
}
|
||||||
|
|
||||||
void UseProgrammableFragmentShader(GLuint program) {
|
void UseProgrammableFragmentShader(GLuint program) {
|
||||||
fs = program;
|
fs = program;
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,17 +169,21 @@ static void LabelGLObject(GLenum identifier, GLuint handle, VAddr addr,
|
||||||
const std::string nice_addr = fmt::format("0x{:016x}", addr);
|
const std::string nice_addr = fmt::format("0x{:016x}", addr);
|
||||||
std::string object_label;
|
std::string object_label;
|
||||||
|
|
||||||
|
if (extra_info.empty()) {
|
||||||
switch (identifier) {
|
switch (identifier) {
|
||||||
case GL_TEXTURE:
|
case GL_TEXTURE:
|
||||||
object_label = extra_info + "@" + nice_addr;
|
object_label = "Texture@" + nice_addr;
|
||||||
break;
|
break;
|
||||||
case GL_PROGRAM:
|
case GL_PROGRAM:
|
||||||
object_label = "ShaderProgram@" + nice_addr;
|
object_label = "Shader@" + nice_addr;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
object_label = fmt::format("Object(0x{:x})@{}", identifier, nice_addr);
|
object_label = fmt::format("Object(0x{:x})@{}", identifier, nice_addr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
object_label = extra_info + '@' + nice_addr;
|
||||||
|
}
|
||||||
glObjectLabel(identifier, handle, -1, static_cast<const GLchar*>(object_label.c_str()));
|
glObjectLabel(identifier, handle, -1, static_cast<const GLchar*>(object_label.c_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in New Issue