From 2b4568df6a9ce0bd8c132f473f82c5fc5d2e5fbd Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 12 Sep 2020 14:52:18 +1000 Subject: [PATCH 1/9] ShaderGen: Split HW shadergen from base --- src/core/CMakeLists.txt | 2 + src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 2 + src/core/gpu_hw_shadergen.cpp | 486 +-------------------------------- src/core/gpu_hw_shadergen.h | 34 +-- src/core/shadergen.cpp | 488 ++++++++++++++++++++++++++++++++++ src/core/shadergen.h | 44 +++ 7 files changed, 544 insertions(+), 514 deletions(-) create mode 100644 src/core/shadergen.cpp create mode 100644 src/core/shadergen.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5994347cd..2151d26b3 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -71,6 +71,8 @@ add_library(core save_state_version.h settings.cpp settings.h + shadergen.cpp + shadergen.h sio.cpp sio.h spu.cpp diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index e1fe31cd2..706bb946a 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -85,6 +85,7 @@ + @@ -134,6 +135,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index e928a6f1c..ae512e65e 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -47,6 +47,7 @@ + @@ -97,5 +98,6 @@ + \ No newline at end of file diff --git a/src/core/gpu_hw_shadergen.cpp b/src/core/gpu_hw_shadergen.cpp index 460478a13..d0050c8d7 100644 --- a/src/core/gpu_hw_shadergen.cpp +++ b/src/core/gpu_hw_shadergen.cpp @@ -5,174 +5,15 @@ #include Log_SetChannel(GPU_HW_ShaderGen); -GPU_HW_ShaderGen::GPU_HW_ShaderGen(HostDisplay::RenderAPI render_api, u32 resolution_scale, bool true_color, - bool scaled_dithering, GPUTextureFilter texture_filtering, bool uv_limits, - bool supports_dual_source_blend) - : m_render_api(render_api), m_resolution_scale(resolution_scale), m_true_color(true_color), - m_scaled_dithering(scaled_dithering), m_texture_filter(texture_filtering), m_uv_limits(uv_limits), - m_glsl(render_api != HostDisplay::RenderAPI::D3D11), m_supports_dual_source_blend(supports_dual_source_blend), - m_use_glsl_interface_blocks(false) +GPU_HW_ShaderGen::GPU_HW_ShaderGen(HostDisplay::RenderAPI render_api, u32 resolution_scale, bool true_color, bool scaled_dithering, GPUTextureFilter texture_filtering, bool uv_limits, bool supports_dual_source_blend) : + ShaderGen(render_api, supports_dual_source_blend), + m_resolution_scale(resolution_scale), m_true_color(true_color), + m_scaled_dithering(scaled_dithering), m_texture_filter(texture_filtering), m_uv_limits(uv_limits) { - if (m_glsl) - { - if (m_render_api == HostDisplay::RenderAPI::OpenGL || m_render_api == HostDisplay::RenderAPI::OpenGLES) - SetGLSLVersionString(); - - m_use_glsl_interface_blocks = (IsVulkan() || GLAD_GL_ES_VERSION_3_2 || GLAD_GL_VERSION_3_2); - m_use_glsl_binding_layout = (IsVulkan() || UseGLSLBindingLayout()); - } } GPU_HW_ShaderGen::~GPU_HW_ShaderGen() = default; -bool GPU_HW_ShaderGen::UseGLSLBindingLayout() -{ - return (GLAD_GL_ES_VERSION_3_1 || GLAD_GL_VERSION_4_2 || - (GLAD_GL_ARB_explicit_attrib_location && GLAD_GL_ARB_explicit_uniform_location && - GLAD_GL_ARB_shading_language_420pack)); -} - -static void DefineMacro(std::stringstream& ss, const char* name, bool enabled) -{ - ss << "#define " << name << " " << BoolToUInt32(enabled) << "\n"; -} - -void GPU_HW_ShaderGen::SetGLSLVersionString() -{ - const char* glsl_version = reinterpret_cast(glGetString(GL_SHADING_LANGUAGE_VERSION)); - const bool glsl_es = (m_render_api == HostDisplay::RenderAPI::OpenGLES); - Assert(glsl_version != nullptr); - - // Skip any strings in front of the version code. - const char* glsl_version_start = glsl_version; - while (*glsl_version_start != '\0' && (*glsl_version_start < '0' || *glsl_version_start > '9')) - glsl_version_start++; - - int major_version = 0, minor_version = 0; - if (std::sscanf(glsl_version_start, "%d.%d", &major_version, &minor_version) == 2) - { - // Cap at GLSL 4.3, we're not using anything newer for now. - if (!glsl_es && (major_version > 4 || (major_version == 4 && minor_version > 30))) - { - major_version = 4; - minor_version = 30; - } - else if (glsl_es && (major_version > 3 || (major_version == 3 && minor_version > 20))) - { - major_version = 3; - minor_version = 20; - } - } - else - { - Log_ErrorPrintf("Invalid GLSL version string: '%s' ('%s')", glsl_version, glsl_version_start); - if (glsl_es) - { - major_version = 3; - minor_version = 0; - } - m_glsl_version_string = glsl_es ? "300" : "130"; - } - - char buf[128]; - std::snprintf(buf, sizeof(buf), "#version %d%02d%s", major_version, minor_version, - (glsl_es && major_version >= 3) ? " es" : ""); - m_glsl_version_string = buf; -} - -void GPU_HW_ShaderGen::WriteHeader(std::stringstream& ss) -{ - if (m_render_api == HostDisplay::RenderAPI::OpenGL || m_render_api == HostDisplay::RenderAPI::OpenGLES) - ss << m_glsl_version_string << "\n\n"; - else if (m_render_api == HostDisplay::RenderAPI::Vulkan) - ss << "#version 450 core\n\n"; - - // Extension enabling for OpenGL. - if (m_render_api == HostDisplay::RenderAPI::OpenGLES) - { - // Enable EXT_blend_func_extended for dual-source blend on OpenGL ES. - if (GLAD_GL_EXT_blend_func_extended) - ss << "#extension GL_EXT_blend_func_extended : require\n"; - } - else if (m_render_api == HostDisplay::RenderAPI::OpenGL) - { - // Need extensions for binding layout if GL<4.3. - if (m_use_glsl_binding_layout && !GLAD_GL_VERSION_4_3) - { - ss << "#extension GL_ARB_explicit_attrib_location : require\n"; - ss << "#extension GL_ARB_explicit_uniform_location : require\n"; - ss << "#extension GL_ARB_shading_language_420pack : require\n"; - } - - if (!GLAD_GL_VERSION_3_2) - ss << "#extension GL_ARB_uniform_buffer_object : require\n"; - - // Enable SSBOs if it's not required by the version. - if (!GLAD_GL_VERSION_4_3 && !GLAD_GL_ES_VERSION_3_1 && GLAD_GL_ARB_shader_storage_buffer_object) - ss << "#extension GL_ARB_shader_storage_buffer_object : require\n"; - } - - DefineMacro(ss, "API_OPENGL", m_render_api == HostDisplay::RenderAPI::OpenGL); - DefineMacro(ss, "API_OPENGL_ES", m_render_api == HostDisplay::RenderAPI::OpenGLES); - DefineMacro(ss, "API_D3D11", m_render_api == HostDisplay::RenderAPI::D3D11); - DefineMacro(ss, "API_VULKAN", m_render_api == HostDisplay::RenderAPI::Vulkan); - - if (m_render_api == HostDisplay::RenderAPI::OpenGLES) - { - ss << "precision highp float;\n"; - ss << "precision highp int;\n"; - ss << "precision highp sampler2D;\n"; - - if (GLAD_GL_ES_VERSION_3_2) - ss << "precision highp usamplerBuffer;\n"; - - ss << "\n"; - } - - if (m_glsl) - { - ss << "#define GLSL 1\n"; - ss << "#define float2 vec2\n"; - ss << "#define float3 vec3\n"; - ss << "#define float4 vec4\n"; - ss << "#define int2 ivec2\n"; - ss << "#define int3 ivec3\n"; - ss << "#define int4 ivec4\n"; - ss << "#define uint2 uvec2\n"; - ss << "#define uint3 uvec3\n"; - ss << "#define uint4 uvec4\n"; - ss << "#define nointerpolation flat\n"; - ss << "#define frac fract\n"; - ss << "#define lerp mix\n"; - - ss << "#define CONSTANT const\n"; - ss << "#define VECTOR_EQ(a, b) ((a) == (b))\n"; - ss << "#define VECTOR_NEQ(a, b) ((a) != (b))\n"; - ss << "#define VECTOR_COMP_EQ(a, b) equal((a), (b))\n"; - ss << "#define VECTOR_COMP_NEQ(a, b) notEqual((a), (b))\n"; - ss << "#define SAMPLE_TEXTURE(name, coords) texture(name, coords)\n"; - ss << "#define LOAD_TEXTURE(name, coords, mip) texelFetch(name, coords, mip)\n"; - ss << "#define LOAD_TEXTURE_OFFSET(name, coords, mip, offset) texelFetchOffset(name, coords, mip, offset)\n"; - ss << "#define LOAD_TEXTURE_BUFFER(name, index) texelFetch(name, index)\n"; - } - else - { - ss << "#define HLSL 1\n"; - ss << "#define roundEven round\n"; - ss << "#define CONSTANT static const\n"; - ss << "#define VECTOR_EQ(a, b) (all((a) == (b)))\n"; - ss << "#define VECTOR_NEQ(a, b) (any((a) != (b)))\n"; - ss << "#define VECTOR_COMP_EQ(a, b) ((a) == (b))\n"; - ss << "#define VECTOR_COMP_NEQ(a, b) ((a) != (b))\n"; - ss << "#define SAMPLE_TEXTURE(name, coords) name.Sample(name##_ss, coords)\n"; - ss << "#define LOAD_TEXTURE(name, coords, mip) name.Load(int3(coords, mip))\n"; - ss << "#define LOAD_TEXTURE_OFFSET(name, coords, mip, offset) name.Load(int3(coords, mip), offset)\n"; - ss << "#define LOAD_TEXTURE_BUFFER(name, index) name.Load(index)\n"; - } - - ss << "\n"; -} - void GPU_HW_ShaderGen::WriteCommonFunctions(std::stringstream& ss) { ss << "CONSTANT uint RESOLUTION_SCALE = " << m_resolution_scale << "u;\n"; @@ -224,272 +65,6 @@ float4 RGBA5551ToRGBA8(uint v) )"; } -void GPU_HW_ShaderGen::DeclareUniformBuffer(std::stringstream& ss, const std::initializer_list& members, - bool push_constant_on_vulkan) -{ - if (IsVulkan()) - { - if (push_constant_on_vulkan) - ss << "layout(push_constant) uniform PushConstants\n"; - else - ss << "layout(std140, set = 0, binding = 0) uniform UBOBlock\n"; - } - else if (m_glsl) - { - if (m_use_glsl_binding_layout) - ss << "layout(std140, binding = 1) uniform UBOBlock\n"; - else - ss << "layout(std140) uniform UBOBlock\n"; - } - else - { - ss << "cbuffer UBOBlock : register(b0)\n"; - } - - ss << "{\n"; - for (const char* member : members) - ss << member << ";\n"; - ss << "};\n\n"; -} - -void GPU_HW_ShaderGen::DeclareTexture(std::stringstream& ss, const char* name, u32 index) -{ - if (m_glsl) - { - if (IsVulkan()) - ss << "layout(set = 0, binding = " << (index + 1u) << ") "; - else if (m_use_glsl_binding_layout) - ss << "layout(binding = " << index << ") "; - - ss << "uniform sampler2D " << name << ";\n"; - } - else - { - ss << "Texture2D " << name << " : register(t" << index << ");\n"; - ss << "SamplerState " << name << "_ss : register(s" << index << ");\n"; - } -} - -void GPU_HW_ShaderGen::DeclareTextureBuffer(std::stringstream& ss, const char* name, u32 index, bool is_int, - bool is_unsigned) -{ - if (m_glsl) - { - if (IsVulkan()) - ss << "layout(set = 0, binding = " << index << ") "; - else if (m_use_glsl_binding_layout) - ss << "layout(binding = " << index << ") "; - - ss << "uniform " << (is_int ? (is_unsigned ? "u" : "i") : "") << "samplerBuffer " << name << ";\n"; - } - else - { - ss << "Buffer<" << (is_int ? (is_unsigned ? "uint4" : "int4") : "float4") << "> " << name << " : register(t" - << index << ");\n"; - } -} - -void GPU_HW_ShaderGen::DeclareVertexEntryPoint( - std::stringstream& ss, const std::initializer_list& attributes, u32 num_color_outputs, - u32 num_texcoord_outputs, const std::initializer_list>& additional_outputs, - bool declare_vertex_id, const char* output_block_suffix) -{ - if (m_glsl) - { - if (m_use_glsl_binding_layout) - { - u32 attribute_counter = 0; - for (const char* attribute : attributes) - { - ss << "layout(location = " << attribute_counter << ") in " << attribute << ";\n"; - attribute_counter++; - } - } - else - { - for (const char* attribute : attributes) - ss << "in " << attribute << ";\n"; - } - - if (m_use_glsl_interface_blocks) - { - if (IsVulkan()) - ss << "layout(location = 0) "; - - ss << "out VertexData" << output_block_suffix << " {\n"; - for (u32 i = 0; i < num_color_outputs; i++) - ss << " float4 v_col" << i << ";\n"; - - for (u32 i = 0; i < num_texcoord_outputs; i++) - ss << " float2 v_tex" << i << ";\n"; - - for (const auto [qualifiers, name] : additional_outputs) - ss << " " << qualifiers << " " << name << ";\n"; - ss << "};\n"; - } - else - { - for (u32 i = 0; i < num_color_outputs; i++) - ss << "out float4 v_col" << i << ";\n"; - - for (u32 i = 0; i < num_texcoord_outputs; i++) - ss << "out float2 v_tex" << i << ";\n"; - - for (const auto [qualifiers, name] : additional_outputs) - ss << qualifiers << " out " << name << ";\n"; - } - - ss << "#define v_pos gl_Position\n\n"; - if (declare_vertex_id) - { - if (IsVulkan()) - ss << "#define v_id uint(gl_VertexIndex)\n"; - else - ss << "#define v_id uint(gl_VertexID)\n"; - } - - ss << "\n"; - ss << "void main()\n"; - } - else - { - ss << "void main(\n"; - - if (declare_vertex_id) - ss << " in uint v_id : SV_VertexID,\n"; - - u32 attribute_counter = 0; - for (const char* attribute : attributes) - { - ss << " in " << attribute << " : ATTR" << attribute_counter << ",\n"; - attribute_counter++; - } - - for (u32 i = 0; i < num_color_outputs; i++) - ss << " out float4 v_col" << i << " : COLOR" << i << ",\n"; - - for (u32 i = 0; i < num_texcoord_outputs; i++) - ss << " out float2 v_tex" << i << " : TEXCOORD" << i << ",\n"; - - u32 additional_counter = num_texcoord_outputs; - for (const auto [qualifiers, name] : additional_outputs) - { - ss << " " << qualifiers << " out " << name << " : TEXCOORD" << additional_counter << ",\n"; - additional_counter++; - } - - ss << " out float4 v_pos : SV_Position)\n"; - } -} - -void GPU_HW_ShaderGen::DeclareFragmentEntryPoint( - std::stringstream& ss, u32 num_color_inputs, u32 num_texcoord_inputs, - const std::initializer_list>& additional_inputs, - bool declare_fragcoord /* = false */, u32 num_color_outputs /* = 1 */, bool depth_output /* = false */) -{ - if (m_glsl) - { - if (m_use_glsl_interface_blocks) - { - if (IsVulkan()) - ss << "layout(location = 0) "; - - ss << "in VertexData {\n"; - for (u32 i = 0; i < num_color_inputs; i++) - ss << " float4 v_col" << i << ";\n"; - - for (u32 i = 0; i < num_texcoord_inputs; i++) - ss << " float2 v_tex" << i << ";\n"; - - for (const auto [qualifiers, name] : additional_inputs) - ss << " " << qualifiers << " " << name << ";\n"; - ss << "};\n"; - } - else - { - for (u32 i = 0; i < num_color_inputs; i++) - ss << "in float4 v_col" << i << ";\n"; - - for (u32 i = 0; i < num_texcoord_inputs; i++) - ss << "in float2 v_tex" << i << ";\n"; - - for (const auto [qualifiers, name] : additional_inputs) - ss << qualifiers << " in " << name << ";\n"; - } - - if (declare_fragcoord) - ss << "#define v_pos gl_FragCoord\n"; - - if (depth_output) - ss << "#define o_depth gl_FragDepth\n"; - - if (m_use_glsl_binding_layout) - { - if (m_supports_dual_source_blend) - { - for (u32 i = 0; i < num_color_outputs; i++) - ss << "layout(location = 0, index = " << i << ") out float4 o_col" << i << ";\n"; - } - else - { - Assert(num_color_outputs <= 1); - for (u32 i = 0; i < num_color_outputs; i++) - ss << "layout(location = 0" << i << ") out float4 o_col" << i << ";\n"; - } - } - else - { - for (u32 i = 0; i < num_color_outputs; i++) - ss << "out float4 o_col" << i << ";\n"; - } - - ss << "\n"; - - ss << "void main()\n"; - } - else - { - { - ss << "void main(\n"; - - for (u32 i = 0; i < num_color_inputs; i++) - ss << " in float4 v_col" << i << " : COLOR" << i << ",\n"; - - for (u32 i = 0; i < num_texcoord_inputs; i++) - ss << " in float2 v_tex" << i << " : TEXCOORD" << i << ",\n"; - - u32 additional_counter = num_texcoord_inputs; - for (const auto [qualifiers, name] : additional_inputs) - { - ss << " " << qualifiers << " in " << name << " : TEXCOORD" << additional_counter << ",\n"; - additional_counter++; - } - - if (declare_fragcoord) - ss << " in float4 v_pos : SV_Position,\n"; - - if (depth_output) - { - ss << " out float o_depth : SV_Depth"; - if (num_color_outputs > 0) - ss << ",\n"; - else - ss << ")\n"; - } - - for (u32 i = 0; i < num_color_outputs; i++) - { - ss << " out float4 o_col" << i << " : SV_Target" << i; - - if (i == (num_color_outputs - 1)) - ss << ")\n"; - else - ss << ",\n"; - } - } - } -} - void GPU_HW_ShaderGen::WriteBatchUniformBuffer(std::stringstream& ss) { DeclareUniformBuffer(ss, @@ -1371,41 +946,6 @@ float4 SampleFromVRAM(uint4 texpage, float2 coords) return ss.str(); } -std::string GPU_HW_ShaderGen::GenerateScreenQuadVertexShader() -{ - std::stringstream ss; - WriteHeader(ss); - DeclareVertexEntryPoint(ss, {}, 0, 1, {}, true); - ss << R"( -{ - v_tex0 = float2(float((v_id << 1) & 2u), float(v_id & 2u)); - v_pos = float4(v_tex0 * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f), 0.0f, 1.0f); - #if API_OPENGL || API_OPENGL_ES || API_VULKAN - v_pos.y = -v_pos.y; - #endif -} -)"; - - return ss.str(); -} - -std::string GPU_HW_ShaderGen::GenerateFillFragmentShader() -{ - std::stringstream ss; - WriteHeader(ss); - DeclareUniformBuffer(ss, {"float4 u_fill_color"}, true); - DeclareFragmentEntryPoint(ss, 0, 1, {}, false, 1, true); - - ss << R"( -{ - o_col0 = u_fill_color; - o_depth = u_fill_color.a; -} -)"; - - return ss.str(); -} - std::string GPU_HW_ShaderGen::GenerateInterlacedFillFragmentShader() { std::stringstream ss; @@ -1427,24 +967,6 @@ std::string GPU_HW_ShaderGen::GenerateInterlacedFillFragmentShader() return ss.str(); } -std::string GPU_HW_ShaderGen::GenerateCopyFragmentShader() -{ - std::stringstream ss; - WriteHeader(ss); - DeclareUniformBuffer(ss, {"float4 u_src_rect"}, true); - DeclareTexture(ss, "samp0", 0); - DeclareFragmentEntryPoint(ss, 0, 1, {}, false, 1); - - ss << R"( -{ - float2 coords = u_src_rect.xy + v_tex0 * u_src_rect.zw; - o_col0 = SAMPLE_TEXTURE(samp0, coords); -} -)"; - - return ss.str(); -} - std::string GPU_HW_ShaderGen::GenerateDisplayFragmentShader(bool depth_24bit, GPU_HW::InterlacedRenderMode interlace_mode) { diff --git a/src/core/gpu_hw_shadergen.h b/src/core/gpu_hw_shadergen.h index c2c399404..a881d4b41 100644 --- a/src/core/gpu_hw_shadergen.h +++ b/src/core/gpu_hw_shadergen.h @@ -1,25 +1,18 @@ #pragma once #include "gpu_hw.h" -#include "host_display.h" -#include -#include +#include "shadergen.h" -class GPU_HW_ShaderGen +class GPU_HW_ShaderGen : public ShaderGen { public: GPU_HW_ShaderGen(HostDisplay::RenderAPI render_api, u32 resolution_scale, bool true_color, bool scaled_dithering, GPUTextureFilter texture_filtering, bool uv_limits, bool supports_dual_source_blend); ~GPU_HW_ShaderGen(); - static bool UseGLSLBindingLayout(); - std::string GenerateBatchVertexShader(bool textured); std::string GenerateBatchFragmentShader(GPU_HW::BatchRenderMode transparency, GPU::TextureMode texture_mode, bool dithering, bool interlacing); - std::string GenerateScreenQuadVertexShader(); - std::string GenerateFillFragmentShader(); std::string GenerateInterlacedFillFragmentShader(); - std::string GenerateCopyFragmentShader(); std::string GenerateDisplayFragmentShader(bool depth_24bit, GPU_HW::InterlacedRenderMode interlace_mode); std::string GenerateVRAMReadFragmentShader(); std::string GenerateVRAMWriteFragmentShader(bool use_ssbo); @@ -27,36 +20,13 @@ public: std::string GenerateVRAMUpdateDepthFragmentShader(); private: - ALWAYS_INLINE bool IsVulkan() const { return (m_render_api == HostDisplay::RenderAPI::Vulkan); } - - void SetGLSLVersionString(); - void WriteHeader(std::stringstream& ss); - void DeclareUniformBuffer(std::stringstream& ss, const std::initializer_list& members, - bool push_constant_on_vulkan); - void DeclareTexture(std::stringstream& ss, const char* name, u32 index); - void DeclareTextureBuffer(std::stringstream& ss, const char* name, u32 index, bool is_int, bool is_unsigned); - void DeclareVertexEntryPoint(std::stringstream& ss, const std::initializer_list& attributes, - u32 num_color_outputs, u32 num_texcoord_outputs, - const std::initializer_list>& additional_outputs, - bool declare_vertex_id = false, const char* output_block_suffix = ""); - void DeclareFragmentEntryPoint(std::stringstream& ss, u32 num_color_inputs, u32 num_texcoord_inputs, - const std::initializer_list>& additional_inputs, - bool declare_fragcoord = false, u32 num_color_outputs = 1, bool depth_output = false); - void WriteCommonFunctions(std::stringstream& ss); void WriteBatchUniformBuffer(std::stringstream& ss); void WriteBatchTextureFilter(std::stringstream& ss, GPUTextureFilter texture_filter); - HostDisplay::RenderAPI m_render_api; u32 m_resolution_scale; bool m_true_color; bool m_scaled_dithering; GPUTextureFilter m_texture_filter; bool m_uv_limits; - bool m_glsl; - bool m_supports_dual_source_blend; - bool m_use_glsl_interface_blocks; - bool m_use_glsl_binding_layout; - - std::string m_glsl_version_string; }; diff --git a/src/core/shadergen.cpp b/src/core/shadergen.cpp new file mode 100644 index 000000000..bb4d74439 --- /dev/null +++ b/src/core/shadergen.cpp @@ -0,0 +1,488 @@ +#include "shadergen.h" +#include "common/assert.h" +#include "common/log.h" +#include +#include +Log_SetChannel(ShaderGen); + +ShaderGen::ShaderGen(HostDisplay::RenderAPI render_api, bool supports_dual_source_blend) + : m_render_api(render_api), m_glsl(render_api != HostDisplay::RenderAPI::D3D11), + m_supports_dual_source_blend(supports_dual_source_blend), m_use_glsl_interface_blocks(false) +{ + if (m_glsl) + { + if (m_render_api == HostDisplay::RenderAPI::OpenGL || m_render_api == HostDisplay::RenderAPI::OpenGLES) + SetGLSLVersionString(); + + m_use_glsl_interface_blocks = (IsVulkan() || GLAD_GL_ES_VERSION_3_2 || GLAD_GL_VERSION_3_2); + m_use_glsl_binding_layout = (IsVulkan() || UseGLSLBindingLayout()); + } +} + +ShaderGen::~ShaderGen() = default; + +bool ShaderGen::UseGLSLBindingLayout() +{ + return (GLAD_GL_ES_VERSION_3_1 || GLAD_GL_VERSION_4_2 || + (GLAD_GL_ARB_explicit_attrib_location && GLAD_GL_ARB_explicit_uniform_location && + GLAD_GL_ARB_shading_language_420pack)); +} + +void ShaderGen::DefineMacro(std::stringstream& ss, const char* name, bool enabled) +{ + ss << "#define " << name << " " << BoolToUInt32(enabled) << "\n"; +} + +void ShaderGen::SetGLSLVersionString() +{ + const char* glsl_version = reinterpret_cast(glGetString(GL_SHADING_LANGUAGE_VERSION)); + const bool glsl_es = (m_render_api == HostDisplay::RenderAPI::OpenGLES); + Assert(glsl_version != nullptr); + + // Skip any strings in front of the version code. + const char* glsl_version_start = glsl_version; + while (*glsl_version_start != '\0' && (*glsl_version_start < '0' || *glsl_version_start > '9')) + glsl_version_start++; + + int major_version = 0, minor_version = 0; + if (std::sscanf(glsl_version_start, "%d.%d", &major_version, &minor_version) == 2) + { + // Cap at GLSL 4.3, we're not using anything newer for now. + if (!glsl_es && (major_version > 4 || (major_version == 4 && minor_version > 30))) + { + major_version = 4; + minor_version = 30; + } + else if (glsl_es && (major_version > 3 || (major_version == 3 && minor_version > 20))) + { + major_version = 3; + minor_version = 20; + } + } + else + { + Log_ErrorPrintf("Invalid GLSL version string: '%s' ('%s')", glsl_version, glsl_version_start); + if (glsl_es) + { + major_version = 3; + minor_version = 0; + } + m_glsl_version_string = glsl_es ? "300" : "130"; + } + + char buf[128]; + std::snprintf(buf, sizeof(buf), "#version %d%02d%s", major_version, minor_version, + (glsl_es && major_version >= 3) ? " es" : ""); + m_glsl_version_string = buf; +} + +void ShaderGen::WriteHeader(std::stringstream& ss) +{ + if (m_render_api == HostDisplay::RenderAPI::OpenGL || m_render_api == HostDisplay::RenderAPI::OpenGLES) + ss << m_glsl_version_string << "\n\n"; + else if (m_render_api == HostDisplay::RenderAPI::Vulkan) + ss << "#version 450 core\n\n"; + + // Extension enabling for OpenGL. + if (m_render_api == HostDisplay::RenderAPI::OpenGLES) + { + // Enable EXT_blend_func_extended for dual-source blend on OpenGL ES. + if (GLAD_GL_EXT_blend_func_extended) + ss << "#extension GL_EXT_blend_func_extended : require\n"; + } + else if (m_render_api == HostDisplay::RenderAPI::OpenGL) + { + // Need extensions for binding layout if GL<4.3. + if (m_use_glsl_binding_layout && !GLAD_GL_VERSION_4_3) + { + ss << "#extension GL_ARB_explicit_attrib_location : require\n"; + ss << "#extension GL_ARB_explicit_uniform_location : require\n"; + ss << "#extension GL_ARB_shading_language_420pack : require\n"; + } + + if (!GLAD_GL_VERSION_3_2) + ss << "#extension GL_ARB_uniform_buffer_object : require\n"; + + // Enable SSBOs if it's not required by the version. + if (!GLAD_GL_VERSION_4_3 && !GLAD_GL_ES_VERSION_3_1 && GLAD_GL_ARB_shader_storage_buffer_object) + ss << "#extension GL_ARB_shader_storage_buffer_object : require\n"; + } + + DefineMacro(ss, "API_OPENGL", m_render_api == HostDisplay::RenderAPI::OpenGL); + DefineMacro(ss, "API_OPENGL_ES", m_render_api == HostDisplay::RenderAPI::OpenGLES); + DefineMacro(ss, "API_D3D11", m_render_api == HostDisplay::RenderAPI::D3D11); + DefineMacro(ss, "API_VULKAN", m_render_api == HostDisplay::RenderAPI::Vulkan); + + if (m_render_api == HostDisplay::RenderAPI::OpenGLES) + { + ss << "precision highp float;\n"; + ss << "precision highp int;\n"; + ss << "precision highp sampler2D;\n"; + + if (GLAD_GL_ES_VERSION_3_2) + ss << "precision highp usamplerBuffer;\n"; + + ss << "\n"; + } + + if (m_glsl) + { + ss << "#define GLSL 1\n"; + ss << "#define float2 vec2\n"; + ss << "#define float3 vec3\n"; + ss << "#define float4 vec4\n"; + ss << "#define int2 ivec2\n"; + ss << "#define int3 ivec3\n"; + ss << "#define int4 ivec4\n"; + ss << "#define uint2 uvec2\n"; + ss << "#define uint3 uvec3\n"; + ss << "#define uint4 uvec4\n"; + ss << "#define nointerpolation flat\n"; + ss << "#define frac fract\n"; + ss << "#define lerp mix\n"; + + ss << "#define CONSTANT const\n"; + ss << "#define VECTOR_EQ(a, b) ((a) == (b))\n"; + ss << "#define VECTOR_NEQ(a, b) ((a) != (b))\n"; + ss << "#define VECTOR_COMP_EQ(a, b) equal((a), (b))\n"; + ss << "#define VECTOR_COMP_NEQ(a, b) notEqual((a), (b))\n"; + ss << "#define SAMPLE_TEXTURE(name, coords) texture(name, coords)\n"; + ss << "#define LOAD_TEXTURE(name, coords, mip) texelFetch(name, coords, mip)\n"; + ss << "#define LOAD_TEXTURE_OFFSET(name, coords, mip, offset) texelFetchOffset(name, coords, mip, offset)\n"; + ss << "#define LOAD_TEXTURE_BUFFER(name, index) texelFetch(name, index)\n"; + } + else + { + ss << "#define HLSL 1\n"; + ss << "#define roundEven round\n"; + ss << "#define CONSTANT static const\n"; + ss << "#define VECTOR_EQ(a, b) (all((a) == (b)))\n"; + ss << "#define VECTOR_NEQ(a, b) (any((a) != (b)))\n"; + ss << "#define VECTOR_COMP_EQ(a, b) ((a) == (b))\n"; + ss << "#define VECTOR_COMP_NEQ(a, b) ((a) != (b))\n"; + ss << "#define SAMPLE_TEXTURE(name, coords) name.Sample(name##_ss, coords)\n"; + ss << "#define LOAD_TEXTURE(name, coords, mip) name.Load(int3(coords, mip))\n"; + ss << "#define LOAD_TEXTURE_OFFSET(name, coords, mip, offset) name.Load(int3(coords, mip), offset)\n"; + ss << "#define LOAD_TEXTURE_BUFFER(name, index) name.Load(index)\n"; + } + + ss << "\n"; +} + +void ShaderGen::DeclareUniformBuffer(std::stringstream& ss, const std::initializer_list& members, + bool push_constant_on_vulkan) +{ + if (IsVulkan()) + { + if (push_constant_on_vulkan) + ss << "layout(push_constant) uniform PushConstants\n"; + else + ss << "layout(std140, set = 0, binding = 0) uniform UBOBlock\n"; + } + else if (m_glsl) + { + if (m_use_glsl_binding_layout) + ss << "layout(std140, binding = 1) uniform UBOBlock\n"; + else + ss << "layout(std140) uniform UBOBlock\n"; + } + else + { + ss << "cbuffer UBOBlock : register(b0)\n"; + } + + ss << "{\n"; + for (const char* member : members) + ss << member << ";\n"; + ss << "};\n\n"; +} + +void ShaderGen::DeclareTexture(std::stringstream& ss, const char* name, u32 index) +{ + if (m_glsl) + { + if (IsVulkan()) + ss << "layout(set = 0, binding = " << (index + 1u) << ") "; + else if (m_use_glsl_binding_layout) + ss << "layout(binding = " << index << ") "; + + ss << "uniform sampler2D " << name << ";\n"; + } + else + { + ss << "Texture2D " << name << " : register(t" << index << ");\n"; + ss << "SamplerState " << name << "_ss : register(s" << index << ");\n"; + } +} + +void ShaderGen::DeclareTextureBuffer(std::stringstream& ss, const char* name, u32 index, bool is_int, bool is_unsigned) +{ + if (m_glsl) + { + if (IsVulkan()) + ss << "layout(set = 0, binding = " << index << ") "; + else if (m_use_glsl_binding_layout) + ss << "layout(binding = " << index << ") "; + + ss << "uniform " << (is_int ? (is_unsigned ? "u" : "i") : "") << "samplerBuffer " << name << ";\n"; + } + else + { + ss << "Buffer<" << (is_int ? (is_unsigned ? "uint4" : "int4") : "float4") << "> " << name << " : register(t" + << index << ");\n"; + } +} + +void ShaderGen::DeclareVertexEntryPoint( + std::stringstream& ss, const std::initializer_list& attributes, u32 num_color_outputs, + u32 num_texcoord_outputs, const std::initializer_list>& additional_outputs, + bool declare_vertex_id, const char* output_block_suffix) +{ + if (m_glsl) + { + if (m_use_glsl_binding_layout) + { + u32 attribute_counter = 0; + for (const char* attribute : attributes) + { + ss << "layout(location = " << attribute_counter << ") in " << attribute << ";\n"; + attribute_counter++; + } + } + else + { + for (const char* attribute : attributes) + ss << "in " << attribute << ";\n"; + } + + if (m_use_glsl_interface_blocks) + { + if (IsVulkan()) + ss << "layout(location = 0) "; + + ss << "out VertexData" << output_block_suffix << " {\n"; + for (u32 i = 0; i < num_color_outputs; i++) + ss << " float4 v_col" << i << ";\n"; + + for (u32 i = 0; i < num_texcoord_outputs; i++) + ss << " float2 v_tex" << i << ";\n"; + + for (const auto [qualifiers, name] : additional_outputs) + ss << " " << qualifiers << " " << name << ";\n"; + ss << "};\n"; + } + else + { + for (u32 i = 0; i < num_color_outputs; i++) + ss << "out float4 v_col" << i << ";\n"; + + for (u32 i = 0; i < num_texcoord_outputs; i++) + ss << "out float2 v_tex" << i << ";\n"; + + for (const auto [qualifiers, name] : additional_outputs) + ss << qualifiers << " out " << name << ";\n"; + } + + ss << "#define v_pos gl_Position\n\n"; + if (declare_vertex_id) + { + if (IsVulkan()) + ss << "#define v_id uint(gl_VertexIndex)\n"; + else + ss << "#define v_id uint(gl_VertexID)\n"; + } + + ss << "\n"; + ss << "void main()\n"; + } + else + { + ss << "void main(\n"; + + if (declare_vertex_id) + ss << " in uint v_id : SV_VertexID,\n"; + + u32 attribute_counter = 0; + for (const char* attribute : attributes) + { + ss << " in " << attribute << " : ATTR" << attribute_counter << ",\n"; + attribute_counter++; + } + + for (u32 i = 0; i < num_color_outputs; i++) + ss << " out float4 v_col" << i << " : COLOR" << i << ",\n"; + + for (u32 i = 0; i < num_texcoord_outputs; i++) + ss << " out float2 v_tex" << i << " : TEXCOORD" << i << ",\n"; + + u32 additional_counter = num_texcoord_outputs; + for (const auto [qualifiers, name] : additional_outputs) + { + ss << " " << qualifiers << " out " << name << " : TEXCOORD" << additional_counter << ",\n"; + additional_counter++; + } + + ss << " out float4 v_pos : SV_Position)\n"; + } +} + +void ShaderGen::DeclareFragmentEntryPoint( + std::stringstream& ss, u32 num_color_inputs, u32 num_texcoord_inputs, + const std::initializer_list>& additional_inputs, + bool declare_fragcoord /* = false */, u32 num_color_outputs /* = 1 */, bool depth_output /* = false */) +{ + if (m_glsl) + { + if (m_use_glsl_interface_blocks) + { + if (IsVulkan()) + ss << "layout(location = 0) "; + + ss << "in VertexData {\n"; + for (u32 i = 0; i < num_color_inputs; i++) + ss << " float4 v_col" << i << ";\n"; + + for (u32 i = 0; i < num_texcoord_inputs; i++) + ss << " float2 v_tex" << i << ";\n"; + + for (const auto [qualifiers, name] : additional_inputs) + ss << " " << qualifiers << " " << name << ";\n"; + ss << "};\n"; + } + else + { + for (u32 i = 0; i < num_color_inputs; i++) + ss << "in float4 v_col" << i << ";\n"; + + for (u32 i = 0; i < num_texcoord_inputs; i++) + ss << "in float2 v_tex" << i << ";\n"; + + for (const auto [qualifiers, name] : additional_inputs) + ss << qualifiers << " in " << name << ";\n"; + } + + if (declare_fragcoord) + ss << "#define v_pos gl_FragCoord\n"; + + if (depth_output) + ss << "#define o_depth gl_FragDepth\n"; + + if (m_use_glsl_binding_layout) + { + if (m_supports_dual_source_blend) + { + for (u32 i = 0; i < num_color_outputs; i++) + ss << "layout(location = 0, index = " << i << ") out float4 o_col" << i << ";\n"; + } + else + { + Assert(num_color_outputs <= 1); + for (u32 i = 0; i < num_color_outputs; i++) + ss << "layout(location = 0" << i << ") out float4 o_col" << i << ";\n"; + } + } + else + { + for (u32 i = 0; i < num_color_outputs; i++) + ss << "out float4 o_col" << i << ";\n"; + } + + ss << "\n"; + + ss << "void main()\n"; + } + else + { + { + ss << "void main(\n"; + + for (u32 i = 0; i < num_color_inputs; i++) + ss << " in float4 v_col" << i << " : COLOR" << i << ",\n"; + + for (u32 i = 0; i < num_texcoord_inputs; i++) + ss << " in float2 v_tex" << i << " : TEXCOORD" << i << ",\n"; + + u32 additional_counter = num_texcoord_inputs; + for (const auto [qualifiers, name] : additional_inputs) + { + ss << " " << qualifiers << " in " << name << " : TEXCOORD" << additional_counter << ",\n"; + additional_counter++; + } + + if (declare_fragcoord) + ss << " in float4 v_pos : SV_Position,\n"; + + if (depth_output) + { + ss << " out float o_depth : SV_Depth"; + if (num_color_outputs > 0) + ss << ",\n"; + else + ss << ")\n"; + } + + for (u32 i = 0; i < num_color_outputs; i++) + { + ss << " out float4 o_col" << i << " : SV_Target" << i; + + if (i == (num_color_outputs - 1)) + ss << ")\n"; + else + ss << ",\n"; + } + } + } +} + +std::string ShaderGen::GenerateScreenQuadVertexShader() +{ + std::stringstream ss; + WriteHeader(ss); + DeclareVertexEntryPoint(ss, {}, 0, 1, {}, true); + ss << R"( +{ + v_tex0 = float2(float((v_id << 1) & 2u), float(v_id & 2u)); + v_pos = float4(v_tex0 * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f), 0.0f, 1.0f); + #if API_OPENGL || API_OPENGL_ES || API_VULKAN + v_pos.y = -v_pos.y; + #endif +} +)"; + + return ss.str(); +} + +std::string ShaderGen::GenerateFillFragmentShader() +{ + std::stringstream ss; + WriteHeader(ss); + DeclareUniformBuffer(ss, {"float4 u_fill_color"}, true); + DeclareFragmentEntryPoint(ss, 0, 1, {}, false, 1, true); + + ss << R"( +{ + o_col0 = u_fill_color; + o_depth = u_fill_color.a; +} +)"; + + return ss.str(); +} + +std::string ShaderGen::GenerateCopyFragmentShader() +{ + std::stringstream ss; + WriteHeader(ss); + DeclareUniformBuffer(ss, {"float4 u_src_rect"}, true); + DeclareTexture(ss, "samp0", 0); + DeclareFragmentEntryPoint(ss, 0, 1, {}, false, 1); + + ss << R"( +{ + float2 coords = u_src_rect.xy + v_tex0 * u_src_rect.zw; + o_col0 = SAMPLE_TEXTURE(samp0, coords); +} +)"; + + return ss.str(); +} diff --git a/src/core/shadergen.h b/src/core/shadergen.h new file mode 100644 index 000000000..f44742635 --- /dev/null +++ b/src/core/shadergen.h @@ -0,0 +1,44 @@ +#pragma once +#include "gpu_hw.h" +#include "host_display.h" +#include +#include + +class ShaderGen +{ +public: + ShaderGen(HostDisplay::RenderAPI render_api, bool supports_dual_source_blend); + ~ShaderGen(); + + static bool UseGLSLBindingLayout(); + + std::string GenerateScreenQuadVertexShader(); + std::string GenerateFillFragmentShader(); + std::string GenerateCopyFragmentShader(); + +protected: + ALWAYS_INLINE bool IsVulkan() const { return (m_render_api == HostDisplay::RenderAPI::Vulkan); } + + void SetGLSLVersionString(); + void DefineMacro(std::stringstream& ss, const char* name, bool enabled); + void WriteHeader(std::stringstream& ss); + void DeclareUniformBuffer(std::stringstream& ss, const std::initializer_list& members, + bool push_constant_on_vulkan); + void DeclareTexture(std::stringstream& ss, const char* name, u32 index); + void DeclareTextureBuffer(std::stringstream& ss, const char* name, u32 index, bool is_int, bool is_unsigned); + void DeclareVertexEntryPoint(std::stringstream& ss, const std::initializer_list& attributes, + u32 num_color_outputs, u32 num_texcoord_outputs, + const std::initializer_list>& additional_outputs, + bool declare_vertex_id = false, const char* output_block_suffix = ""); + void DeclareFragmentEntryPoint(std::stringstream& ss, u32 num_color_inputs, u32 num_texcoord_inputs, + const std::initializer_list>& additional_inputs, + bool declare_fragcoord = false, u32 num_color_outputs = 1, bool depth_output = false); + + HostDisplay::RenderAPI m_render_api; + bool m_glsl; + bool m_supports_dual_source_blend; + bool m_use_glsl_interface_blocks; + bool m_use_glsl_binding_layout; + + std::string m_glsl_version_string; +}; From 84b0522abca72a497a65bd864c7f735312df030d Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 13 Sep 2020 00:04:57 +1000 Subject: [PATCH 2/9] Common/Rectangle: Make it work with float types --- src/common/rectangle.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/common/rectangle.h b/src/common/rectangle.h index bb2fa4dff..c7369e57e 100644 --- a/src/common/rectangle.h +++ b/src/common/rectangle.h @@ -10,11 +10,8 @@ namespace Common { template struct Rectangle { - enum : T - { - InvalidMinCoord = std::numeric_limits::max(), - InvalidMaxCoord = std::numeric_limits::min() - }; + static constexpr T InvalidMinCoord = std::numeric_limits::max(); + static constexpr T InvalidMaxCoord = std::numeric_limits::min(); /// Default constructor - initializes to an invalid coordinate range suitable for including points. constexpr Rectangle() : left(InvalidMinCoord), top(InvalidMinCoord), right(InvalidMaxCoord), bottom(InvalidMaxCoord) From aa383d0bdab9ed2ad9f6587785c4017e521363b9 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 13 Sep 2020 00:05:17 +1000 Subject: [PATCH 3/9] Common/String: Support constructing from string_view --- src/common/string.cpp | 5 +++++ src/common/string.h | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/common/string.cpp b/src/common/string.cpp index 6b57854af..ecde9ee40 100644 --- a/src/common/string.cpp +++ b/src/common/string.cpp @@ -166,6 +166,11 @@ String::String(String&& moveString) Assign(moveString); } +String::String(const std::string_view& sv) +{ + AppendString(sv.data(), static_cast(sv.size())); +} + String::~String() { StringDataRelease(m_pStringData); diff --git a/src/common/string.h b/src/common/string.h index a0c1c7b9d..90d3c5966 100644 --- a/src/common/string.h +++ b/src/common/string.h @@ -54,6 +54,9 @@ public: // Construct a string from a data object, does not increment the reference count on the string data, use carefully. explicit String(StringData* pStringData) : m_pStringData(pStringData) {} + // Creates string from string_view. + String(const std::string_view& sv); + // Destructor. Child classes may not have any destructors, as this is not virtual. ~String(); @@ -307,6 +310,12 @@ public: Assign(copyString.GetCharArray()); } + StackString(const std::string_view& sv) : String(&m_sStringData) + { + InitStackStringData(); + AppendString(sv.data(), static_cast(sv.size())); + } + // Override the fromstring method static StackString FromFormat(const char* FormatString, ...) { From f63192c6b203bfdfe124cdb7732caffebdb3957b Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 13 Sep 2020 00:05:38 +1000 Subject: [PATCH 4/9] Vulkan/SwapChain: Fix incorrect return from GetTextureFormat() --- src/common/vulkan/swap_chain.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/vulkan/swap_chain.h b/src/common/vulkan/swap_chain.h index e811b3a5f..eb2ab335c 100644 --- a/src/common/vulkan/swap_chain.h +++ b/src/common/vulkan/swap_chain.h @@ -31,7 +31,7 @@ public: ALWAYS_INLINE VkSurfaceKHR GetSurface() const { return m_surface; } ALWAYS_INLINE VkSurfaceFormatKHR GetSurfaceFormat() const { return m_surface_format; } - ALWAYS_INLINE VkFormat GetTextureFormat() const { return m_texture_format; } + ALWAYS_INLINE VkFormat GetTextureFormat() const { return m_surface_format.format; } ALWAYS_INLINE bool IsVSyncEnabled() const { return m_vsync_enabled; } ALWAYS_INLINE VkSwapchainKHR GetSwapChain() const { return m_swap_chain; } ALWAYS_INLINE u32 GetWidth() const { return m_width; } @@ -85,7 +85,6 @@ private: VkSurfaceKHR m_surface = VK_NULL_HANDLE; VkSurfaceFormatKHR m_surface_format = {}; VkPresentModeKHR m_present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR; - VkFormat m_texture_format = VK_FORMAT_UNDEFINED; VkRenderPass m_load_render_pass = VK_NULL_HANDLE; VkRenderPass m_clear_render_pass = VK_NULL_HANDLE; From 666d7fbb991557c49e4ca3b9b582e261e8299c4d Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 13 Sep 2020 00:06:01 +1000 Subject: [PATCH 5/9] Vulkan/Context: Add pipeline destruction deferring --- src/common/vulkan/context.cpp | 6 ++++++ src/common/vulkan/context.h | 1 + 2 files changed, 7 insertions(+) diff --git a/src/common/vulkan/context.cpp b/src/common/vulkan/context.cpp index 9db2f5512..90959dd31 100644 --- a/src/common/vulkan/context.cpp +++ b/src/common/vulkan/context.cpp @@ -997,6 +997,12 @@ void Context::DeferImageViewDestruction(VkImageView object) resources.cleanup_resources.push_back([this, object]() { vkDestroyImageView(m_device, object, nullptr); }); } +void Context::DeferPipelineDestruction(VkPipeline pipeline) +{ + FrameResources& resources = m_frame_resources[m_current_frame]; + resources.cleanup_resources.push_back([this, pipeline]() { vkDestroyPipeline(m_device, pipeline, nullptr); }); +} + static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objectType, uint64_t object, size_t location, int32_t messageCode, diff --git a/src/common/vulkan/context.h b/src/common/vulkan/context.h index 2d4780511..6fec69044 100644 --- a/src/common/vulkan/context.h +++ b/src/common/vulkan/context.h @@ -163,6 +163,7 @@ public: void DeferFramebufferDestruction(VkFramebuffer object); void DeferImageDestruction(VkImage object); void DeferImageViewDestruction(VkImageView object); + void DeferPipelineDestruction(VkPipeline pipeline); // Wait for a fence to be completed. // Also invokes callbacks for completion. From d09a802a175fd504a64fcce6658dd0e3a2b0b429 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 13 Sep 2020 00:30:54 +1000 Subject: [PATCH 6/9] Vulkan/Context: Use VK_LAYER_KHRONOS_validation --- src/common/vulkan/context.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/vulkan/context.cpp b/src/common/vulkan/context.cpp index 90959dd31..f827574b9 100644 --- a/src/common/vulkan/context.cpp +++ b/src/common/vulkan/context.cpp @@ -91,7 +91,7 @@ bool Context::CheckValidationLayerAvailablility() return strcmp(it.extensionName, VK_EXT_DEBUG_REPORT_EXTENSION_NAME) == 0; }) != extension_list.end() && std::find_if(layer_list.begin(), layer_list.end(), [](const auto& it) { - return strcmp(it.layerName, "VK_LAYER_LUNARG_standard_validation") == 0; + return strcmp(it.layerName, "VK_LAYER_KHRONOS_validation") == 0; }) != layer_list.end()); } @@ -123,7 +123,7 @@ VkInstance Context::CreateVulkanInstance(bool enable_surface, bool enable_debug_ // Enable debug layer on debug builds if (enable_validation_layer) { - static const char* layer_names[] = {"VK_LAYER_LUNARG_standard_validation"}; + static const char* layer_names[] = {"VK_LAYER_KHRONOS_validation"}; instance_create_info.enabledLayerCount = 1; instance_create_info.ppEnabledLayerNames = layer_names; } From 58047783393524c01601f6b301c58e6f5ab8542b Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 13 Sep 2020 01:19:05 +1000 Subject: [PATCH 7/9] D3D11/Texture: Fix dimensions not getting reset on destroy --- src/common/d3d11/texture.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/d3d11/texture.cpp b/src/common/d3d11/texture.cpp index 1662c822c..1b03d49a6 100644 --- a/src/common/d3d11/texture.cpp +++ b/src/common/d3d11/texture.cpp @@ -121,6 +121,8 @@ void Texture::Destroy() m_rtv.Reset(); m_srv.Reset(); m_texture.Reset(); + m_width = 0; + m_height = 0; } } // namespace D3D11 \ No newline at end of file From 2819715260179ccd2cfe493b1666ab4ab26ba484 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 13 Sep 2020 01:19:57 +1000 Subject: [PATCH 8/9] FrontendCommon: Add a post processing implementation --- src/core/gpu_hw_opengl.cpp | 2 + src/core/host_display.h | 2 + src/core/host_interface.cpp | 1 + src/core/settings.cpp | 5 + src/core/settings.h | 1 + src/core/shadergen.cpp | 9 +- src/core/shadergen.h | 1 + .../libretro_host_display.cpp | 5 + .../libretro_host_display.h | 3 +- src/frontend-common/CMakeLists.txt | 6 + src/frontend-common/common_host_interface.cpp | 11 + src/frontend-common/d3d11_host_display.cpp | 183 +++++++- src/frontend-common/d3d11_host_display.h | 26 ++ src/frontend-common/frontend-common.vcxproj | 6 + .../frontend-common.vcxproj.filters | 6 + src/frontend-common/opengl_host_display.cpp | 204 ++++++++- src/frontend-common/opengl_host_display.h | 29 ++ src/frontend-common/postprocessing_chain.cpp | 203 +++++++++ src/frontend-common/postprocessing_chain.h | 36 ++ src/frontend-common/postprocessing_shader.cpp | 412 ++++++++++++++++++ src/frontend-common/postprocessing_shader.h | 106 +++++ .../postprocessing_shadergen.cpp | 184 ++++++++ .../postprocessing_shadergen.h | 21 + src/frontend-common/vulkan_host_display.cpp | 348 ++++++++++++++- src/frontend-common/vulkan_host_display.h | 40 ++ 25 files changed, 1828 insertions(+), 22 deletions(-) create mode 100644 src/frontend-common/postprocessing_chain.cpp create mode 100644 src/frontend-common/postprocessing_chain.h create mode 100644 src/frontend-common/postprocessing_shader.cpp create mode 100644 src/frontend-common/postprocessing_shader.h create mode 100644 src/frontend-common/postprocessing_shadergen.cpp create mode 100644 src/frontend-common/postprocessing_shadergen.h diff --git a/src/core/gpu_hw_opengl.cpp b/src/core/gpu_hw_opengl.cpp index cedc9144c..2284815f7 100644 --- a/src/core/gpu_hw_opengl.cpp +++ b/src/core/gpu_hw_opengl.cpp @@ -99,6 +99,7 @@ void GPU_HW_OpenGL::ResetGraphicsAPIState() if (m_resolution_scale > 1 && !m_supports_geometry_shaders) glLineWidth(1.0f); glBindVertexArray(0); + m_uniform_stream_buffer->Unbind(); } void GPU_HW_OpenGL::RestoreGraphicsAPIState() @@ -114,6 +115,7 @@ void GPU_HW_OpenGL::RestoreGraphicsAPIState() if (m_resolution_scale > 1 && !m_supports_geometry_shaders) glLineWidth(static_cast(m_resolution_scale)); glBindVertexArray(m_vao_id); + m_uniform_stream_buffer->Bind(); SetScissorFromDrawingArea(); m_batch_ubo_dirty = true; diff --git a/src/core/host_display.h b/src/core/host_display.h index 58fc3d7ee..3bd7f414b 100644 --- a/src/core/host_display.h +++ b/src/core/host_display.h @@ -69,6 +69,8 @@ public: virtual bool CreateResources() = 0; virtual void DestroyResources() = 0; + virtual bool SetPostProcessingChain(const std::string_view& config) = 0; + /// Call when the window size changes externally to recreate any resources. virtual void ResizeRenderWindow(s32 new_window_width, s32 new_window_height) = 0; diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 03cbc51aa..7d378cf92 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -396,6 +396,7 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si) si.SetBoolValue("Display", "ShowResolution", false); si.SetBoolValue("Display", "Fullscreen", false); si.SetBoolValue("Display", "VSync", true); + si.SetStringValue("Display", "PostProcessChain", ""); si.SetBoolValue("CDROM", "ReadThread", true); si.SetBoolValue("CDROM", "RegionCheck", true); diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 4c354287a..deb631772 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -133,6 +133,7 @@ void Settings::Load(SettingsInterface& si) display_show_speed = si.GetBoolValue("Display", "ShowSpeed", false); display_show_resolution = si.GetBoolValue("Display", "ShowResolution", false); video_sync_enabled = si.GetBoolValue("Display", "VSync", true); + display_post_process_chain = si.GetStringValue("Display", "PostProcessChain", ""); cdrom_read_thread = si.GetBoolValue("CDROM", "ReadThread", true); cdrom_region_check = si.GetBoolValue("CDROM", "RegionCheck", true); @@ -242,6 +243,10 @@ void Settings::Save(SettingsInterface& si) const si.SetBoolValue("Display", "ShowSpeed", display_show_speed); si.SetBoolValue("Display", "ShowResolution", display_show_speed); si.SetBoolValue("Display", "VSync", video_sync_enabled); + if (display_post_process_chain.empty()) + si.DeleteValue("Display", "PostProcessChain"); + else + si.SetStringValue("Display", "PostProcessChain", display_post_process_chain.c_str()); si.SetBoolValue("CDROM", "ReadThread", cdrom_read_thread); si.SetBoolValue("CDROM", "RegionCheck", cdrom_region_check); diff --git a/src/core/settings.h b/src/core/settings.h index aa5f63f9b..a59900304 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -84,6 +84,7 @@ struct Settings GPURenderer gpu_renderer = GPURenderer::Software; std::string gpu_adapter; + std::string display_post_process_chain; u32 gpu_resolution_scale = 1; bool gpu_use_debug_device = false; bool gpu_true_color = true; diff --git a/src/core/shadergen.cpp b/src/core/shadergen.cpp index bb4d74439..4dc9e58ef 100644 --- a/src/core/shadergen.cpp +++ b/src/core/shadergen.cpp @@ -169,8 +169,7 @@ void ShaderGen::WriteHeader(std::stringstream& ss) ss << "\n"; } -void ShaderGen::DeclareUniformBuffer(std::stringstream& ss, const std::initializer_list& members, - bool push_constant_on_vulkan) +void ShaderGen::WriteUniformBufferDeclaration(std::stringstream& ss, bool push_constant_on_vulkan) { if (IsVulkan()) { @@ -190,6 +189,12 @@ void ShaderGen::DeclareUniformBuffer(std::stringstream& ss, const std::initializ { ss << "cbuffer UBOBlock : register(b0)\n"; } +} + +void ShaderGen::DeclareUniformBuffer(std::stringstream& ss, const std::initializer_list& members, + bool push_constant_on_vulkan) +{ + WriteUniformBufferDeclaration(ss, push_constant_on_vulkan); ss << "{\n"; for (const char* member : members) diff --git a/src/core/shadergen.h b/src/core/shadergen.h index f44742635..65d6bdc40 100644 --- a/src/core/shadergen.h +++ b/src/core/shadergen.h @@ -22,6 +22,7 @@ protected: void SetGLSLVersionString(); void DefineMacro(std::stringstream& ss, const char* name, bool enabled); void WriteHeader(std::stringstream& ss); + void WriteUniformBufferDeclaration(std::stringstream& ss, bool push_constant_on_vulkan); void DeclareUniformBuffer(std::stringstream& ss, const std::initializer_list& members, bool push_constant_on_vulkan); void DeclareTexture(std::stringstream& ss, const char* name, u32 index); diff --git a/src/duckstation-libretro/libretro_host_display.cpp b/src/duckstation-libretro/libretro_host_display.cpp index 0bdabe75f..88ca8e746 100644 --- a/src/duckstation-libretro/libretro_host_display.cpp +++ b/src/duckstation-libretro/libretro_host_display.cpp @@ -156,6 +156,11 @@ void LibretroHostDisplay::ResizeRenderWindow(s32 new_window_width, s32 new_windo m_window_info.surface_height = new_window_height; } +bool LibretroHostDisplay::SetPostProcessingChain(const std::string_view& config) +{ + return false; +} + std::unique_ptr LibretroHostDisplay::CreateTexture(u32 width, u32 height, const void* data, u32 data_stride, bool dynamic) { diff --git a/src/duckstation-libretro/libretro_host_display.h b/src/duckstation-libretro/libretro_host_display.h index 03917a4ef..af49a7dda 100644 --- a/src/duckstation-libretro/libretro_host_display.h +++ b/src/duckstation-libretro/libretro_host_display.h @@ -1,6 +1,5 @@ #pragma once #include "core/host_display.h" -#include class LibretroHostDisplay final : public HostDisplay { @@ -26,6 +25,8 @@ public: void ResizeRenderWindow(s32 new_window_width, s32 new_window_height) override; void DestroyRenderSurface() override; + bool SetPostProcessingChain(const std::string_view& config) override; + bool CreateResources() override; void DestroyResources() override; diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 710f81141..ffedb5e84 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -48,6 +48,12 @@ if(NOT BUILD_LIBRETRO_CORE) imgui_styles.h ini_settings_interface.cpp ini_settings_interface.h + postprocessing_chain.cpp + postprocessing_chain.h + postprocessing_shader.cpp + postprocessing_shader.h + postprocessing_shadergen.cpp + postprocessing_shadergen.h save_state_selector_ui.cpp save_state_selector_ui.h ) diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index 8df599450..3adbb494d 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -123,6 +123,7 @@ void CommonHostInterface::InitializeUserDirectory() result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("inputprofiles").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("savestates").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("screenshots").c_str(), false); + result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("shaders").c_str(), false); result &= FileSystem::CreateDirectory(GetUserDirectoryRelativePath("memcards").c_str(), false); if (!result) @@ -172,6 +173,7 @@ void CommonHostInterface::DestroySystem() { SetTimerResolutionIncreased(false); m_save_state_selector_ui->Close(); + m_display->SetPostProcessingChain({}); HostInterface::DestroySystem(); } @@ -684,6 +686,9 @@ void CommonHostInterface::SetUserDirectory() void CommonHostInterface::OnSystemCreated() { HostInterface::OnSystemCreated(); + + if (!m_display->SetPostProcessingChain(g_settings.display_post_process_chain)) + AddOSDMessage(TranslateStdString("OSDMessage", "Failed to load post processing shader chain."), 20.0f); } void CommonHostInterface::OnSystemPaused(bool paused) @@ -1955,6 +1960,12 @@ void CommonHostInterface::CheckForSettingsChanges(const Settings& old_settings) { UpdateSpeedLimiterState(); } + + if (g_settings.display_post_process_chain != old_settings.display_post_process_chain) + { + if (!m_display->SetPostProcessingChain(g_settings.display_post_process_chain)) + AddOSDMessage(TranslateStdString("OSDMessage", "Failed to load post processing shader chain."), 20.0f); + } } if (g_settings.log_level != old_settings.log_level || g_settings.log_filter != old_settings.log_filter || diff --git a/src/frontend-common/d3d11_host_display.cpp b/src/frontend-common/d3d11_host_display.cpp index 80162f505..25319044f 100644 --- a/src/frontend-common/d3d11_host_display.cpp +++ b/src/frontend-common/d3d11_host_display.cpp @@ -3,10 +3,12 @@ #include "common/d3d11/shader_compiler.h" #include "common/log.h" #include "common/string_util.h" +#include "core/settings.h" #include "display_ps.hlsl.h" #include "display_vs.hlsl.h" #include #ifndef LIBRETRO +#include "frontend-common/postprocessing_shadergen.h" #include #endif #ifdef WITH_IMGUI @@ -524,6 +526,12 @@ bool D3D11HostDisplay::CreateResources() void D3D11HostDisplay::DestroyResources() { +#ifndef LIBRETRO + m_post_processing_chain.ClearStages(); + m_post_processing_input_texture.Destroy(); + m_post_processing_stages.clear(); +#endif + m_display_uniform_buffer.Release(); m_linear_sampler.Reset(); m_point_sampler.Reset(); @@ -580,9 +588,7 @@ bool D3D11HostDisplay::Render() if (ImGui::GetCurrentContext()) ImGui_ImplDX11_NewFrame(); #endif -#else - RenderDisplay(); - RenderSoftwareCursor(); + #endif return true; @@ -598,13 +604,24 @@ void D3D11HostDisplay::RenderImGui() void D3D11HostDisplay::RenderDisplay() { +#ifndef LIBRETRO if (!HasDisplayTexture()) return; const auto [left, top, width, height] = CalculateDrawRect(GetWindowWidth(), GetWindowHeight(), m_display_top_margin); + + if (!m_post_processing_chain.IsEmpty()) + { + ApplyPostProcessingChain(m_swap_chain_rtv.Get(), left, top, width, height, m_display_texture_handle, + m_display_texture_width, m_display_texture_height, m_display_texture_view_x, + m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height); + return; + } + RenderDisplay(left, top, width, height, m_display_texture_handle, m_display_texture_width, m_display_texture_height, m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_width, m_display_texture_view_height, m_display_linear_filtering); +#endif } void D3D11HostDisplay::RenderDisplay(s32 left, s32 top, s32 width, s32 height, void* texture_handle, u32 texture_width, @@ -621,7 +638,7 @@ void D3D11HostDisplay::RenderDisplay(s32 left, s32 top, s32 width, s32 height, v static_cast(texture_view_y) / static_cast(texture_height), (static_cast(texture_view_width) - 0.5f) / static_cast(texture_width), (static_cast(texture_view_height) - 0.5f) / static_cast(texture_height)}; - const auto map = m_display_uniform_buffer.Map(m_context.Get(), sizeof(uniforms), sizeof(uniforms)); + const auto map = m_display_uniform_buffer.Map(m_context.Get(), m_display_uniform_buffer.GetSize(), sizeof(uniforms)); std::memcpy(map.pointer, uniforms, sizeof(uniforms)); m_display_uniform_buffer.Unmap(m_context.Get(), sizeof(uniforms)); m_context->VSSetConstantBuffers(0, 1, m_display_uniform_buffer.GetD3DBufferArray()); @@ -655,7 +672,7 @@ void D3D11HostDisplay::RenderSoftwareCursor(s32 left, s32 top, s32 width, s32 he m_context->PSSetSamplers(0, 1, m_linear_sampler.GetAddressOf()); const float uniforms[4] = {0.0f, 0.0f, 1.0f, 1.0f}; - const auto map = m_display_uniform_buffer.Map(m_context.Get(), sizeof(uniforms), sizeof(uniforms)); + const auto map = m_display_uniform_buffer.Map(m_context.Get(), m_display_uniform_buffer.GetSize(), sizeof(uniforms)); std::memcpy(map.pointer, uniforms, sizeof(uniforms)); m_display_uniform_buffer.Unmap(m_context.Get(), sizeof(uniforms)); m_context->VSSetConstantBuffers(0, 1, m_display_uniform_buffer.GetD3DBufferArray()); @@ -728,6 +745,162 @@ std::vector D3D11HostDisplay::EnumerateAdapterNames(IDXGIFactory* d return adapter_names; } +bool D3D11HostDisplay::SetPostProcessingChain(const std::string_view& config) +{ + if (config.empty()) + { + m_post_processing_input_texture.Destroy(); + m_post_processing_stages.clear(); + m_post_processing_chain.ClearStages(); + return true; + } + + if (!m_post_processing_chain.CreateFromString(config)) + return false; + + m_post_processing_stages.clear(); + + FrontendCommon::PostProcessingShaderGen shadergen(HostDisplay::RenderAPI::D3D11, true); + u32 max_ubo_size = 0; + + for (u32 i = 0; i < m_post_processing_chain.GetStageCount(); i++) + { + const PostProcessingShader& shader = m_post_processing_chain.GetShaderStage(i); + const std::string vs = shadergen.GeneratePostProcessingVertexShader(shader); + const std::string ps = shadergen.GeneratePostProcessingFragmentShader(shader); + + PostProcessingStage stage; + stage.uniforms_size = shader.GetUniformsSize(); + stage.vertex_shader = + D3D11::ShaderCompiler::CompileAndCreateVertexShader(m_device.Get(), vs, g_settings.gpu_use_debug_device); + stage.pixel_shader = + D3D11::ShaderCompiler::CompileAndCreatePixelShader(m_device.Get(), ps, g_settings.gpu_use_debug_device); + if (!stage.vertex_shader || !stage.pixel_shader) + { + Log_ErrorPrintf("Failed to compile one or more post-processing shaders, disabling."); + m_post_processing_stages.clear(); + m_post_processing_chain.ClearStages(); + return false; + } + + max_ubo_size = std::max(max_ubo_size, stage.uniforms_size); + m_post_processing_stages.push_back(std::move(stage)); + } + + if (m_display_uniform_buffer.GetSize() < max_ubo_size && + !m_display_uniform_buffer.Create(m_device.Get(), D3D11_BIND_CONSTANT_BUFFER, max_ubo_size)) + { + Log_ErrorPrintf("Failed to allocate %u byte constant buffer for postprocessing", max_ubo_size); + m_post_processing_stages.clear(); + m_post_processing_chain.ClearStages(); + return false; + } + + return true; +} + +bool D3D11HostDisplay::CheckPostProcessingRenderTargets(u32 target_width, u32 target_height) +{ + DebugAssert(!m_post_processing_stages.empty()); + + const DXGI_FORMAT format = DXGI_FORMAT_R8G8B8A8_UNORM; + const u32 bind_flags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; + + if (m_post_processing_input_texture.GetWidth() != target_width || + m_post_processing_input_texture.GetHeight() != target_height) + { + if (!m_post_processing_input_texture.Create(m_device.Get(), target_width, target_height, format, bind_flags)) + return false; + } + + const u32 target_count = (static_cast(m_post_processing_stages.size()) - 1); + for (u32 i = 0; i < target_count; i++) + { + PostProcessingStage& pps = m_post_processing_stages[i]; + if (pps.output_texture.GetWidth() != target_width || pps.output_texture.GetHeight() != target_height) + { + if (!pps.output_texture.Create(m_device.Get(), target_width, target_height, format, bind_flags)) + return false; + } + } + + return true; +} + +void D3D11HostDisplay::ApplyPostProcessingChain(ID3D11RenderTargetView* final_target, s32 final_left, s32 final_top, + s32 final_width, s32 final_height, void* texture_handle, + u32 texture_width, s32 texture_height, s32 texture_view_x, + s32 texture_view_y, s32 texture_view_width, s32 texture_view_height) +{ + static constexpr std::array clear_color = {0.0f, 0.0f, 0.0f, 1.0f}; + + if (!CheckPostProcessingRenderTargets(GetWindowWidth(), GetWindowHeight())) + { + RenderDisplay(final_left, final_top, final_width, final_height, texture_handle, texture_width, texture_height, + texture_view_x, texture_view_y, texture_view_width, texture_view_height, m_display_linear_filtering); + return; + } + + // downsample/upsample - use same viewport for remainder + m_context->ClearRenderTargetView(m_post_processing_input_texture.GetD3DRTV(), clear_color.data()); + m_context->OMSetRenderTargets(1, m_post_processing_input_texture.GetD3DRTVArray(), nullptr); + RenderDisplay(final_left, final_top, final_width, final_height, texture_handle, texture_width, texture_height, + texture_view_x, texture_view_y, texture_view_width, texture_view_height, m_display_linear_filtering); + + texture_handle = m_post_processing_input_texture.GetD3DSRV(); + texture_width = m_post_processing_input_texture.GetWidth(); + texture_height = m_post_processing_input_texture.GetHeight(); + texture_view_x = final_left; + texture_view_y = final_top; + texture_view_width = final_width; + texture_view_height = final_height; + + const u32 final_stage = static_cast(m_post_processing_stages.size()) - 1u; + for (u32 i = 0; i < static_cast(m_post_processing_stages.size()); i++) + { + PostProcessingStage& pps = m_post_processing_stages[i]; + if (i == final_stage) + { + m_context->OMSetRenderTargets(1, &final_target, nullptr); + } + else + { + m_context->ClearRenderTargetView(pps.output_texture.GetD3DRTV(), clear_color.data()); + m_context->OMSetRenderTargets(1, pps.output_texture.GetD3DRTVArray(), nullptr); + } + + m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + m_context->VSSetShader(pps.vertex_shader.Get(), nullptr, 0); + m_context->PSSetShader(pps.pixel_shader.Get(), nullptr, 0); + m_context->PSSetShaderResources(0, 1, reinterpret_cast(&texture_handle)); + m_context->PSSetSamplers(0, 1, m_point_sampler.GetAddressOf()); + + const auto map = + m_display_uniform_buffer.Map(m_context.Get(), m_display_uniform_buffer.GetSize(), pps.uniforms_size); + m_post_processing_chain.GetShaderStage(i).FillUniformBuffer( + map.pointer, texture_width, texture_height, texture_view_x, texture_view_y, texture_view_width, + texture_view_height, GetWindowWidth(), GetWindowHeight(), 0.0f); + m_display_uniform_buffer.Unmap(m_context.Get(), pps.uniforms_size); + m_context->VSSetConstantBuffers(0, 1, m_display_uniform_buffer.GetD3DBufferArray()); + m_context->PSSetConstantBuffers(0, 1, m_display_uniform_buffer.GetD3DBufferArray()); + + m_context->Draw(3, 0); + + if (i != final_stage) + texture_handle = pps.output_texture.GetD3DSRV(); + } + + ID3D11ShaderResourceView* null_srv = nullptr; + m_context->PSSetShaderResources(0, 1, &null_srv); +} + +#else // LIBRETRO + +bool D3D11HostDisplay::SetPostProcessingChain(const std::string_view& config) +{ + return false; +} + #endif } // namespace FrontendCommon diff --git a/src/frontend-common/d3d11_host_display.h b/src/frontend-common/d3d11_host_display.h index e992c8ac4..d33bf6a8a 100644 --- a/src/frontend-common/d3d11_host_display.h +++ b/src/frontend-common/d3d11_host_display.h @@ -13,6 +13,10 @@ #include #include +#ifndef LIBRETRO +#include "frontend-common/postprocessing_chain.h" +#endif + namespace FrontendCommon { class D3D11HostDisplay : public HostDisplay @@ -42,6 +46,8 @@ public: virtual void ResizeRenderWindow(s32 new_window_width, s32 new_window_height) override; virtual void DestroyRenderSurface() override; + virtual bool SetPostProcessingChain(const std::string_view& config) override; + std::unique_ptr CreateTexture(u32 width, u32 height, const void* initial_data, u32 initial_data_stride, bool dynamic) override; void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* texture_data, @@ -82,6 +88,22 @@ protected: s32 texture_view_height, bool linear_filter); void RenderSoftwareCursor(s32 left, s32 top, s32 width, s32 height, HostDisplayTexture* texture_handle); +#ifndef LIBRETRO + struct PostProcessingStage + { + ComPtr vertex_shader; + ComPtr pixel_shader; + D3D11::Texture output_texture; + u32 uniforms_size; + }; + + bool CheckPostProcessingRenderTargets(u32 target_width, u32 target_height); + void ApplyPostProcessingChain(ID3D11RenderTargetView* final_target, s32 final_left, s32 final_top, s32 final_width, + s32 final_height, void* texture_handle, u32 texture_width, s32 texture_height, + s32 texture_view_x, s32 texture_view_y, s32 texture_view_width, + s32 texture_view_height); +#endif + ComPtr m_device; ComPtr m_context; @@ -109,6 +131,10 @@ protected: bool m_using_flip_model_swap_chain = true; bool m_using_allow_tearing = false; bool m_vsync = true; + + PostProcessingChain m_post_processing_chain; + D3D11::Texture m_post_processing_input_texture; + std::vector m_post_processing_stages; #endif }; diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index 22d7690d0..fc0916b89 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -79,6 +79,9 @@ + + + @@ -99,6 +102,9 @@ + + + diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index 8b9148ca3..d62fed724 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -19,6 +19,9 @@ + + + @@ -39,6 +42,9 @@ + + + diff --git a/src/frontend-common/opengl_host_display.cpp b/src/frontend-common/opengl_host_display.cpp index 200fe7433..4f2b76791 100644 --- a/src/frontend-common/opengl_host_display.cpp +++ b/src/frontend-common/opengl_host_display.cpp @@ -7,6 +7,9 @@ #include "imgui.h" #include "imgui_impl_opengl3.h" #endif +#ifndef LIBRETRO +#include "postprocessing_shadergen.h" +#endif Log_SetChannel(LibretroOpenGLHostDisplay); namespace FrontendCommon { @@ -207,6 +210,8 @@ bool OpenGLHostDisplay::CreateRenderDevice(const WindowInfo& wi, std::string_vie bool OpenGLHostDisplay::InitializeRenderDevice(std::string_view shader_cache_directory, bool debug_device) { + glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, reinterpret_cast(&m_uniform_buffer_alignment)); + if (debug_device && GLAD_GL_KHR_debug) { if (GetRenderAPI() == RenderAPI::OpenGLES) @@ -418,6 +423,13 @@ void main() void OpenGLHostDisplay::DestroyResources() { +#ifndef LIBRETRO + m_post_processing_chain.ClearStages(); + m_post_processing_input_texture.Destroy(); + m_post_processing_ubo.reset(); + m_post_processing_stages.clear(); +#endif + if (m_display_vao != 0) glDeleteVertexArrays(1, &m_display_vao); if (m_display_linear_sampler != 0) @@ -470,9 +482,18 @@ void OpenGLHostDisplay::RenderDisplay() return; const auto [left, top, width, height] = CalculateDrawRect(GetWindowWidth(), GetWindowHeight(), m_display_top_margin); - RenderDisplay(left, GetWindowHeight() - top - height, width, height, m_display_texture_handle, - m_display_texture_width, m_display_texture_height, m_display_texture_view_x, m_display_texture_view_y, - m_display_texture_view_width, m_display_texture_view_height, m_display_linear_filtering); + + if (!m_post_processing_chain.IsEmpty()) + { + ApplyPostProcessingChain(0, left, top, width, height, m_display_texture_handle, m_display_texture_width, + m_display_texture_height, m_display_texture_view_x, m_display_texture_view_y, + m_display_texture_view_width, m_display_texture_view_height); + return; + } + + RenderDisplay(left, top, width, height, m_display_texture_handle, m_display_texture_width, m_display_texture_height, + m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_width, + m_display_texture_view_height, m_display_linear_filtering); } void OpenGLHostDisplay::RenderDisplay(s32 left, s32 bottom, s32 width, s32 height, void* texture_handle, @@ -526,4 +547,181 @@ void OpenGLHostDisplay::RenderSoftwareCursor(s32 left, s32 bottom, s32 width, s3 glBindSampler(0, 0); } +#ifndef LIBRETRO + +bool OpenGLHostDisplay::SetPostProcessingChain(const std::string_view& config) +{ + if (config.empty()) + { + m_post_processing_input_texture.Destroy(); + m_post_processing_stages.clear(); + m_post_processing_chain.ClearStages(); + return true; + } + + if (!m_post_processing_chain.CreateFromString(config)) + return false; + + m_post_processing_stages.clear(); + + FrontendCommon::PostProcessingShaderGen shadergen(HostDisplay::RenderAPI::OpenGL, false); + + for (u32 i = 0; i < m_post_processing_chain.GetStageCount(); i++) + { + const PostProcessingShader& shader = m_post_processing_chain.GetShaderStage(i); + const std::string vs = shadergen.GeneratePostProcessingVertexShader(shader); + const std::string ps = shadergen.GeneratePostProcessingFragmentShader(shader); + + PostProcessingStage stage; + stage.uniforms_size = shader.GetUniformsSize(); + if (!stage.program.Compile(vs, {}, ps)) + { + Log_InfoPrintf("Failed to compile post-processing program, disabling."); + m_post_processing_stages.clear(); + m_post_processing_chain.ClearStages(); + return false; + } + + if (!shadergen.UseGLSLBindingLayout()) + { + stage.program.BindUniformBlock("UBOBlock", 1); + stage.program.Bind(); + stage.program.Uniform1i("samp0", 0); + } + + if (!stage.program.Link()) + { + Log_InfoPrintf("Failed to link post-processing program, disabling."); + m_post_processing_stages.clear(); + m_post_processing_chain.ClearStages(); + return false; + } + + m_post_processing_stages.push_back(std::move(stage)); + } + + if (!m_post_processing_ubo) + { + m_post_processing_ubo = GL::StreamBuffer::Create(GL_UNIFORM_BUFFER, 1 * 1024 * 1024); + if (!m_post_processing_ubo) + { + Log_InfoPrintf("Failed to allocate uniform buffer for postprocessing"); + m_post_processing_stages.clear(); + m_post_processing_chain.ClearStages(); + return false; + } + + m_post_processing_ubo->Unbind(); + } + + return true; +} + +bool OpenGLHostDisplay::CheckPostProcessingRenderTargets(u32 target_width, u32 target_height) +{ + DebugAssert(!m_post_processing_stages.empty()); + + if (m_post_processing_input_texture.GetWidth() != target_width || + m_post_processing_input_texture.GetHeight() != target_height) + { + if (!m_post_processing_input_texture.Create(target_width, target_height, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE) || + !m_post_processing_input_texture.CreateFramebuffer()) + { + return false; + } + } + + const u32 target_count = (static_cast(m_post_processing_stages.size()) - 1); + for (u32 i = 0; i < target_count; i++) + { + PostProcessingStage& pps = m_post_processing_stages[i]; + if (pps.output_texture.GetWidth() != target_width || pps.output_texture.GetHeight() != target_height) + { + if (!pps.output_texture.Create(target_width, target_height, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE) || + !pps.output_texture.CreateFramebuffer()) + { + return false; + } + } + } + + return true; +} + +void OpenGLHostDisplay::ApplyPostProcessingChain(GLuint final_target, s32 final_left, s32 final_top, s32 final_width, + s32 final_height, void* texture_handle, u32 texture_width, + s32 texture_height, s32 texture_view_x, s32 texture_view_y, + s32 texture_view_width, s32 texture_view_height) +{ + static constexpr std::array clear_color = {0.0f, 0.0f, 0.0f, 1.0f}; + + if (!CheckPostProcessingRenderTargets(GetWindowWidth(), GetWindowHeight())) + { + RenderDisplay(final_left, final_top, final_width, final_height, texture_handle, texture_width, texture_height, + texture_view_x, texture_view_y, texture_view_width, texture_view_height, m_display_linear_filtering); + return; + } + + // downsample/upsample - use same viewport for remainder + m_post_processing_input_texture.BindFramebuffer(GL_DRAW_FRAMEBUFFER); + glClear(GL_COLOR_BUFFER_BIT); + RenderDisplay(final_left, final_top, final_width, final_height, texture_handle, texture_width, texture_height, + texture_view_x, texture_view_y, texture_view_width, texture_view_height, m_display_linear_filtering); + + texture_handle = reinterpret_cast(static_cast(m_post_processing_input_texture.GetGLId())); + texture_width = m_post_processing_input_texture.GetWidth(); + texture_height = m_post_processing_input_texture.GetHeight(); + texture_view_x = final_left; + texture_view_y = final_top; + texture_view_width = final_width; + texture_view_height = final_height; + + m_post_processing_ubo->Bind(); + + const u32 final_stage = static_cast(m_post_processing_stages.size()) - 1u; + for (u32 i = 0; i < static_cast(m_post_processing_stages.size()); i++) + { + PostProcessingStage& pps = m_post_processing_stages[i]; + if (i == final_stage) + { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, final_target); + } + else + { + pps.output_texture.BindFramebuffer(GL_DRAW_FRAMEBUFFER); + glClear(GL_COLOR_BUFFER_BIT); + } + + pps.program.Bind(); + glBindSampler(0, m_display_linear_sampler); + glBindTexture(GL_TEXTURE_2D, static_cast(reinterpret_cast(texture_handle))); + glBindSampler(0, m_display_nearest_sampler); + + const auto map_result = m_post_processing_ubo->Map(m_uniform_buffer_alignment, pps.uniforms_size); + m_post_processing_chain.GetShaderStage(i).FillUniformBuffer( + map_result.pointer, texture_width, texture_height, texture_view_x, texture_view_y, texture_view_width, + texture_view_height, GetWindowWidth(), GetWindowHeight(), 0.0f); + m_post_processing_ubo->Unmap(pps.uniforms_size); + glBindBufferRange(GL_UNIFORM_BUFFER, 1, m_post_processing_ubo->GetGLBufferId(), map_result.buffer_offset, + pps.uniforms_size); + + glDrawArrays(GL_TRIANGLES, 0, 3); + + if (i != final_stage) + texture_handle = reinterpret_cast(static_cast(pps.output_texture.GetGLId())); + } + + glBindSampler(0, 0); + m_post_processing_ubo->Unbind(); +} + +#else + +bool OpenGLHostDisplay::SetPostProcessingChain(const std::string_view& config) +{ + return false; +} + +#endif + } // namespace FrontendCommon diff --git a/src/frontend-common/opengl_host_display.h b/src/frontend-common/opengl_host_display.h index 628be37ab..259947e4a 100644 --- a/src/frontend-common/opengl_host_display.h +++ b/src/frontend-common/opengl_host_display.h @@ -10,11 +10,16 @@ #include "common/gl/context.h" #include "common/gl/program.h" +#include "common/gl/stream_buffer.h" #include "common/gl/texture.h" #include "common/window_info.h" #include "core/host_display.h" #include +#ifndef LIBRETRO +#include "postprocessing_chain.h" +#endif + namespace FrontendCommon { class OpenGLHostDisplay : public HostDisplay @@ -41,6 +46,8 @@ public: virtual void ResizeRenderWindow(s32 new_window_width, s32 new_window_height) override; virtual void DestroyRenderSurface() override; + virtual bool SetPostProcessingChain(const std::string_view& config) override; + std::unique_ptr CreateTexture(u32 width, u32 height, const void* initial_data, u32 initial_data_stride, bool dynamic) override; void UpdateTexture(HostDisplayTexture* texture, u32 x, u32 y, u32 width, u32 height, const void* texture_data, @@ -71,6 +78,20 @@ protected: s32 texture_view_height, bool linear_filter); void RenderSoftwareCursor(s32 left, s32 bottom, s32 width, s32 height, HostDisplayTexture* texture_handle); +#ifndef LIBRETRO + struct PostProcessingStage + { + GL::Program program; + GL::Texture output_texture; + u32 uniforms_size; + }; + + bool CheckPostProcessingRenderTargets(u32 target_width, u32 target_height); + void ApplyPostProcessingChain(GLuint final_target, s32 final_left, s32 final_top, s32 final_width, s32 final_height, + void* texture_handle, u32 texture_width, s32 texture_height, s32 texture_view_x, + s32 texture_view_y, s32 texture_view_width, s32 texture_view_height); +#endif + std::unique_ptr m_gl_context; GL::Program m_display_program; @@ -78,6 +99,14 @@ protected: GLuint m_display_vao = 0; GLuint m_display_nearest_sampler = 0; GLuint m_display_linear_sampler = 0; + GLuint m_uniform_buffer_alignment = 1; + +#ifndef LIBRETRO + PostProcessingChain m_post_processing_chain; + GL::Texture m_post_processing_input_texture; + std::unique_ptr m_post_processing_ubo; + std::vector m_post_processing_stages; +#endif }; } // namespace FrontendCommon \ No newline at end of file diff --git a/src/frontend-common/postprocessing_chain.cpp b/src/frontend-common/postprocessing_chain.cpp new file mode 100644 index 000000000..b049bcde7 --- /dev/null +++ b/src/frontend-common/postprocessing_chain.cpp @@ -0,0 +1,203 @@ +#include "postprocessing_chain.h" +#include "common/assert.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/string.h" +#include "core/host_interface.h" +#include +Log_SetChannel(PostProcessingChain); + +namespace FrontendCommon { + +static bool TryLoadingShader(PostProcessingShader* shader, const std::string_view& shader_name) +{ + std::string shader_name_str(shader_name); + + std::string filename = g_host_interface->GetUserDirectoryRelativePath( + "shaders%c%s.glsl", FS_OSPATH_SEPERATOR_CHARACTER, shader_name_str.c_str()); + if (FileSystem::FileExists(filename.c_str())) + { + if (!shader->LoadFromFile(std::move(shader_name_str), filename.c_str())) + { + Log_ErrorPrintf("Failed to load shader from '%s'", filename.c_str()); + return false; + } + } + else + { + filename = g_host_interface->GetProgramDirectoryRelativePath("shaders%c%s.glsl", FS_OSPATH_SEPERATOR_CHARACTER, + shader_name_str.c_str()); + if (FileSystem::FileExists(filename.c_str())) + { + if (!shader->LoadFromFile(std::move(shader_name_str), filename.c_str())) + { + Log_ErrorPrintf("Failed to load shader from '%s'", filename.c_str()); + return false; + } + } + else + { + Log_ErrorPrintf("Could not find shader '%s'", std::string(shader_name).c_str()); + return false; + } + } + + return true; +} + +PostProcessingChain::PostProcessingChain() = default; + +PostProcessingChain::~PostProcessingChain() = default; + +void PostProcessingChain::AddShader(PostProcessingShader shader) +{ + m_shaders.push_back(std::move(shader)); +} + +bool PostProcessingChain::AddStage(const std::string_view& name) +{ + PostProcessingShader shader; + if (!TryLoadingShader(&shader, name)) + return false; + + m_shaders.push_back(std::move(shader)); + return true; +} + +std::string PostProcessingChain::GetConfigString() const +{ + std::stringstream ss; + bool first = true; + + for (const PostProcessingShader& shader : m_shaders) + { + if (!first) + ss << ':'; + else + first = false; + + ss << shader.GetName(); + std::string config_string = shader.GetConfigString(); + if (!config_string.empty()) + ss << ';' << config_string; + } + + return ss.str(); +} + +bool PostProcessingChain::CreateFromString(const std::string_view& chain_config) +{ + std::vector shaders; + + size_t last_sep = 0; + while (last_sep < chain_config.size()) + { + size_t next_sep = chain_config.find(':', last_sep); + if (next_sep == std::string::npos) + next_sep = chain_config.size(); + + const std::string_view shader_config = chain_config.substr(last_sep, next_sep - last_sep); + size_t first_shader_sep = shader_config.find(';'); + if (first_shader_sep == std::string::npos) + first_shader_sep = shader_config.size(); + + const std::string_view shader_name = shader_config.substr(0, first_shader_sep); + if (!shader_name.empty()) + { + PostProcessingShader shader; + if (!TryLoadingShader(&shader, shader_name)) + return false; + + if (first_shader_sep < shader_config.size()) + shader.SetConfigString(shader_config.substr(first_shader_sep + 1)); + + shaders.push_back(std::move(shader)); + } + + last_sep = next_sep + 1; + } + + if (shaders.empty()) + { + Log_ErrorPrintf("Postprocessing chain is empty!"); + return false; + } + + m_shaders = std::move(shaders); + Log_InfoPrintf("Loaded postprocessing chain of %zu shaders", m_shaders.size()); + return true; +} + +std::vector PostProcessingChain::GetAvailableShaderNames() +{ + std::vector names; + + std::string program_dir = g_host_interface->GetProgramDirectoryRelativePath("shaders"); + std::string user_dir = g_host_interface->GetUserDirectoryRelativePath("shaders"); + FileSystem::FindResultsArray results; + FileSystem::FindFiles(user_dir.c_str(), "*.glsl", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RECURSIVE | FILESYSTEM_FIND_RELATIVE_PATHS, &results); + if (program_dir != user_dir) + { + FileSystem::FindFiles(program_dir.c_str(), "*.glsl", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RECURSIVE | FILESYSTEM_FIND_RELATIVE_PATHS | + FILESYSTEM_FIND_KEEP_ARRAY, + &results); + } + + for (FILESYSTEM_FIND_DATA& fd : results) + { + size_t pos = fd.FileName.rfind('.'); + if (pos != std::string::npos && pos > 0) + fd.FileName.erase(pos); + + // swap any backslashes for forward slashes so the config is cross-platform + for (size_t i = 0; i < fd.FileName.size(); i++) + { + if (fd.FileName[i] == '\\') + fd.FileName[i] = '/'; + } + + if (std::none_of(names.begin(), names.end(), [&fd](const std::string& other) { return fd.FileName == other; })) + { + names.push_back(std::move(fd.FileName)); + } + } + + return names; +} + +void PostProcessingChain::RemoveStage(u32 index) +{ + Assert(index < m_shaders.size()); + m_shaders.erase(m_shaders.begin() + index); +} + +void PostProcessingChain::MoveStageUp(u32 index) +{ + Assert(index < m_shaders.size()); + if (index == 0) + return; + + PostProcessingShader shader = std::move(m_shaders[index]); + m_shaders.erase(m_shaders.begin() + index); + m_shaders.insert(m_shaders.begin() + (index - 1u), std::move(shader)); +} + +void PostProcessingChain::MoveStageDown(u32 index) +{ + Assert(index < m_shaders.size()); + if (index == (m_shaders.size() - 1u)) + return; + + PostProcessingShader shader = std::move(m_shaders[index]); + m_shaders.erase(m_shaders.begin() + index); + m_shaders.insert(m_shaders.begin() + (index + 1u), std::move(shader)); +} + +void PostProcessingChain::ClearStages() +{ + m_shaders.clear(); +} + +} // namespace FrontendCommon \ No newline at end of file diff --git a/src/frontend-common/postprocessing_chain.h b/src/frontend-common/postprocessing_chain.h new file mode 100644 index 000000000..6f4f9b93c --- /dev/null +++ b/src/frontend-common/postprocessing_chain.h @@ -0,0 +1,36 @@ +#pragma once +#include "postprocessing_shader.h" +#include +#include + +namespace FrontendCommon { + +class PostProcessingChain +{ +public: + PostProcessingChain(); + ~PostProcessingChain(); + + ALWAYS_INLINE bool IsEmpty() const { return m_shaders.empty(); } + ALWAYS_INLINE const u32 GetStageCount() const { return static_cast(m_shaders.size()); } + ALWAYS_INLINE const PostProcessingShader& GetShaderStage(u32 i) const { return m_shaders[i]; } + ALWAYS_INLINE PostProcessingShader& GetShaderStage(u32 i) { return m_shaders[i]; } + + void AddShader(PostProcessingShader shader); + bool AddStage(const std::string_view& name); + void RemoveStage(u32 index); + void MoveStageUp(u32 index); + void MoveStageDown(u32 index); + void ClearStages(); + + std::string GetConfigString() const; + + bool CreateFromString(const std::string_view& chain_config); + + static std::vector GetAvailableShaderNames(); + +private: + std::vector m_shaders; +}; + +} // namespace FrontendCommon diff --git a/src/frontend-common/postprocessing_shader.cpp b/src/frontend-common/postprocessing_shader.cpp new file mode 100644 index 000000000..c3234a312 --- /dev/null +++ b/src/frontend-common/postprocessing_shader.cpp @@ -0,0 +1,412 @@ +#include "postprocessing_shader.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/string_util.h" +#include "core/shadergen.h" +#include +#include +#include +Log_SetChannel(PostProcessingShader); + +namespace FrontendCommon { + +void ParseKeyValue(const std::string_view& line, std::string_view* key, std::string_view* value) +{ + size_t key_start = 0; + while (key_start < line.size() && std::isspace(line[key_start])) + key_start++; + + size_t key_end = key_start; + while (key_end < line.size() && (!std::isspace(line[key_end]) && line[key_end] != '=')) + key_end++; + + if (key_start == key_end || key_end == line.size()) + return; + + size_t value_start = key_end; + while (value_start < line.size() && std::isspace(line[value_start])) + value_start++; + + if (value_start == line.size() || line[value_start] != '=') + return; + + value_start++; + while (value_start < line.size() && std::isspace(line[value_start])) + value_start++; + + size_t value_end = value_start; + while (value_end < line.size() && !std::isspace(line[value_end])) + value_end++; + + if (value_start == value_end) + return; + + *key = line.substr(key_start, key_end - key_start); + *value = line.substr(value_start, value_end - value_start); +} + +template +u32 ParseVector(const std::string_view& line, PostProcessingShader::Option::ValueVector* values) +{ + u32 index = 0; + size_t start = 0; + while (index < PostProcessingShader::Option::MAX_VECTOR_COMPONENTS) + { + size_t end = line.find(','); + if (end == std::string_view::npos) + end = line.size(); + + T value = StringUtil::FromChars(line.substr(start, end - start)).value_or(static_cast(0)); + if constexpr (std::is_same_v) + (*values)[index++].float_value = value; + else if constexpr (std::is_same_v) + (*values)[index++].int_value = value; + } + + const u32 size = index; + + for (; index < PostProcessingShader::Option::MAX_VECTOR_COMPONENTS; index++) + { + if constexpr (std::is_same_v) + (*values)[index++].float_value = 0.0f; + else if constexpr (std::is_same_v) + (*values)[index++].int_value = 0; + } + + return size; +} + +PostProcessingShader::PostProcessingShader() = default; + +PostProcessingShader::PostProcessingShader(std::string name, std::string code) : m_name(name), m_code(code) +{ + LoadOptions(); +} + +PostProcessingShader::PostProcessingShader(const PostProcessingShader& copy) + : m_name(copy.m_name), m_code(copy.m_code), m_options(copy.m_options) +{ +} + +PostProcessingShader::PostProcessingShader(PostProcessingShader& move) + : m_name(std::move(move.m_name)), m_code(std::move(move.m_code)), m_options(std::move(move.m_options)) +{ +} + +PostProcessingShader::~PostProcessingShader() = default; + +bool PostProcessingShader::LoadFromFile(std::string name, const char* filename) +{ + std::optional code = FileSystem::ReadFileToString(filename); + if (!code.has_value() || code->empty()) + return false; + + m_name = std::move(name); + m_code = std::move(code.value()); + m_options.clear(); + LoadOptions(); + return true; +} + +bool PostProcessingShader::IsValid() const +{ + return !m_name.empty() && !m_code.empty(); +} + +const PostProcessingShader::Option* PostProcessingShader::GetOptionByName(const std::string_view& name) const +{ + for (const Option& option : m_options) + { + if (option.name == name) + return &option; + } + + return nullptr; +} + +FrontendCommon::PostProcessingShader::Option* PostProcessingShader::GetOptionByName(const std::string_view& name) +{ + for (Option& option : m_options) + { + if (option.name == name) + return &option; + } + + return nullptr; +} + +std::string PostProcessingShader::GetConfigString() const +{ + std::stringstream ss; + bool first = true; + for (const Option& option : m_options) + { + if (!first) + ss << ';'; + else + first = false; + + ss << option.name; + ss << '='; + + for (u32 i = 0; i < option.vector_size; i++) + { + if (i > 0) + ss << ","; + + switch (option.type) + { + case Option::Type::Bool: + ss << option.value[i].bool_value ? "true" : "false"; + break; + + case Option::Type::Int: + ss << option.value[i].int_value; + break; + + case Option::Type::Float: + ss << option.value[i].float_value; + break; + + default: + break; + } + } + } + + return ss.str(); +} + +void PostProcessingShader::SetConfigString(const std::string_view& str) +{ + for (Option& option : m_options) + option.value = option.default_value; + + size_t last_sep = 0; + while (last_sep < str.size()) + { + size_t next_sep = str.find(';', last_sep); + if (next_sep == std::string_view::npos) + next_sep = str.size(); + + const std::string_view kv = str.substr(last_sep, next_sep - last_sep); + std::string_view key, value; + ParseKeyValue(kv, &key, &value); + if (!key.empty() && !value.empty()) + { + Option* option = GetOptionByName(key); + if (option) + { + switch (option->type) + { + case Option::Type::Bool: + option->value[0].bool_value = StringUtil::FromChars(value).value_or(false); + break; + + case Option::Type::Int: + ParseVector(value, &option->value); + break; + + case Option::Type::Float: + ParseVector(value, &option->value); + break; + + default: + break; + } + } + } + + last_sep = next_sep + 1; + } +} + +bool PostProcessingShader::UsePushConstants() const +{ + return GetUniformsSize() <= PUSH_CONSTANT_SIZE_THRESHOLD; +} + +u32 PostProcessingShader::GetUniformsSize() const +{ + // lazy packing. todo improve. + return sizeof(CommonUniforms) + (sizeof(Option::ValueVector) * static_cast(m_options.size())); +} + +void PostProcessingShader::FillUniformBuffer(void* buffer, u32 texture_width, s32 texture_height, s32 texture_view_x, + s32 texture_view_y, s32 texture_view_width, s32 texture_view_height, + u32 window_width, u32 window_height, float time) const +{ + CommonUniforms* common = static_cast(buffer); + + // TODO: OpenGL? + const float rcp_texture_width = 1.0f / static_cast(texture_width); + const float rcp_texture_height = 1.0f / static_cast(texture_height); + common->src_rect[0] = static_cast(texture_view_x) * rcp_texture_width; + common->src_rect[1] = static_cast(texture_view_y) * rcp_texture_height; + common->src_rect[2] = (static_cast(texture_view_x + texture_view_width - 1)) * rcp_texture_width; + common->src_rect[3] = (static_cast(texture_view_y + texture_view_height - 1)) * rcp_texture_height; + common->src_size[0] = (static_cast(texture_view_width)) * rcp_texture_width; + common->src_size[1] = (static_cast(texture_view_height)) * rcp_texture_height; + common->resolution[0] = static_cast(texture_width); + common->resolution[1] = static_cast(texture_height); + common->rcp_resolution[0] = rcp_texture_width; + common->rcp_resolution[1] = rcp_texture_height; + common->window_resolution[0] = static_cast(window_width); + common->window_resolution[1] = static_cast(window_height); + common->rcp_window_resolution[0] = 1.0f / static_cast(window_width); + common->rcp_window_resolution[1] = 1.0f / static_cast(window_height); + common->time = time; + + u8* option_values = reinterpret_cast(common + 1); + for (const Option& option : m_options) + { + std::memcpy(option_values, option.value.data(), sizeof(Option::ValueVector)); + option_values += sizeof(Option::ValueVector); + } +} + +FrontendCommon::PostProcessingShader& PostProcessingShader::operator=(const PostProcessingShader& copy) +{ + m_name = copy.m_name; + m_code = copy.m_code; + m_options = copy.m_options; + return *this; +} + +FrontendCommon::PostProcessingShader& PostProcessingShader::operator=(PostProcessingShader& move) +{ + m_name = std::move(move.m_name); + m_code = std::move(move.m_code); + m_options = std::move(move.m_options); + return *this; +} + +void PostProcessingShader::LoadOptions() +{ + // Adapted from Dolphin's PostProcessingConfiguration::LoadOptions(). + constexpr char config_start_delimiter[] = "[configuration]"; + constexpr char config_end_delimiter[] = "[/configuration]"; + size_t configuration_start = m_code.find(config_start_delimiter); + size_t configuration_end = m_code.find(config_end_delimiter); + if (configuration_start == std::string::npos || configuration_end == std::string::npos) + { + // Issue loading configuration or there isn't one. + return; + } + + std::string configuration_string = + m_code.substr(configuration_start + std::strlen(config_start_delimiter), + configuration_end - configuration_start - std::strlen(config_start_delimiter)); + + std::istringstream in(configuration_string); + + Option current_option = {}; + while (!in.eof()) + { + std::string line_str; + if (std::getline(in, line_str)) + { + std::string_view line_view = line_str; + +#ifndef _WIN32 + // Check for CRLF eol and convert it to LF + if (!line_view.empty() && line_view.at(line_view.size() - 1) == '\r') + line_view.remove_suffix(1); +#endif + + if (line_view.empty()) + continue; + + if (line_view[0] == '[') + { + size_t endpos = line_view.find("]"); + if (endpos != std::string::npos) + { + if (current_option.type != Option::Type::Invalid) + { + current_option.value = current_option.default_value; + if (current_option.ui_name.empty()) + current_option.ui_name = current_option.name; + + if (!current_option.name.empty() && current_option.vector_size > 0) + m_options.push_back(std::move(current_option)); + + current_option = {}; + } + + // New section! + std::string_view sub = line_view.substr(1, endpos - 1); + if (sub == "OptionBool") + current_option.type = Option::Type::Bool; + else if (sub == "OptionRangeFloat") + current_option.type = Option::Type::Float; + else if (sub == "OptionRangeInteger") + current_option.type = Option::Type::Int; + else + Log_ErrorPrintf("Invalid option type: '%s'", line_str.c_str()); + } + else + { + if (current_option.type == Option::Type::Invalid) + continue; + + std::string_view key, value; + ParseKeyValue(line_view, &key, &value); + if (!key.empty() && !value.empty()) + { + if (key == "GUIName") + { + current_option.ui_name = value; + } + else if (key == "OptionName") + { + current_option.name = value; + } + else if (key == "DependentOption") + { + current_option.dependent_option = value; + } + else if (key == "MinValue" || key == "MaxValue" || key == "DefaultValue" || key == "StepAmount") + { + Option::ValueVector* dst_array; + if (key == "MinValue") + dst_array = ¤t_option.min_value; + else if (key == "MaxValue") + dst_array = ¤t_option.max_value; + else if (key == "DefaultValue") + dst_array = ¤t_option.default_value; + else // if (key == "StepAmount") + dst_array = ¤t_option.step_value; + + u32 size = 0; + if (current_option.type == Option::Type::Bool) + (*dst_array)[size++].bool_value = StringUtil::FromChars(value).value_or(false); + else if (current_option.type == Option::Type::Float) + size = ParseVector(value, dst_array); + else if (current_option.type == Option::Type::Int) + size = ParseVector(value, dst_array); + + current_option.vector_size = + (current_option.vector_size != 0) ? size : std::min(current_option.vector_size, size); + } + else + { + Log_ErrorPrintf("Invalid option key: '%s'", line_str.c_str()); + } + } + } + } + } + } + + if (current_option.type != Option::Type::Invalid && !current_option.name.empty() && current_option.vector_size > 0) + { + current_option.value = current_option.default_value; + if (current_option.ui_name.empty()) + current_option.ui_name = current_option.name; + + m_options.push_back(std::move(current_option)); + } +} + +} // namespace FrontendCommon \ No newline at end of file diff --git a/src/frontend-common/postprocessing_shader.h b/src/frontend-common/postprocessing_shader.h new file mode 100644 index 000000000..a4c3d46d2 --- /dev/null +++ b/src/frontend-common/postprocessing_shader.h @@ -0,0 +1,106 @@ +#pragma once +#include "common/rectangle.h" +#include "core/types.h" +#include +#include +#include +#include + +namespace FrontendCommon { + +class PostProcessingShader +{ +public: + enum : u32 + { + PUSH_CONSTANT_SIZE_THRESHOLD = 128 + }; + + struct Option + { + enum : u32 + { + MAX_VECTOR_COMPONENTS = 4 + }; + + enum class Type + { + Invalid, + Bool, + Int, + Float + }; + + union Value + { + bool bool_value; + s32 int_value; + float float_value; + }; + static_assert(sizeof(Value) == sizeof(u32)); + + using ValueVector = std::array; + static_assert(sizeof(ValueVector) == sizeof(u32) * MAX_VECTOR_COMPONENTS); + + std::string name; + std::string ui_name; + std::string dependent_option; + Type type; + u32 vector_size; + ValueVector default_value; + ValueVector min_value; + ValueVector max_value; + ValueVector step_value; + ValueVector value; + }; + + PostProcessingShader(); + PostProcessingShader(std::string name, std::string code); + PostProcessingShader(const PostProcessingShader& copy); + PostProcessingShader(PostProcessingShader& move); + ~PostProcessingShader(); + + PostProcessingShader& operator=(const PostProcessingShader& copy); + PostProcessingShader& operator=(PostProcessingShader& move); + + ALWAYS_INLINE const std::string& GetName() const { return m_name; } + ALWAYS_INLINE const std::string& GetCode() const { return m_code; } + ALWAYS_INLINE const std::vector @@ -169,6 +176,8 @@ + + diff --git a/src/duckstation-qt/postprocessingchainconfigwidget.cpp b/src/duckstation-qt/postprocessingchainconfigwidget.cpp new file mode 100644 index 000000000..1968b9140 --- /dev/null +++ b/src/duckstation-qt/postprocessingchainconfigwidget.cpp @@ -0,0 +1,174 @@ +#include "postprocessingchainconfigwidget.h" +#include "frontend-common/postprocessing_chain.h" +#include +#include +#include + +PostProcessingChainConfigWidget::PostProcessingChainConfigWidget(QWidget* parent) : QWidget(parent) +{ + m_ui.setupUi(this); + connectUi(); + updateButtonStates(); +} + +PostProcessingChainConfigWidget::~PostProcessingChainConfigWidget() = default; + +void PostProcessingChainConfigWidget::connectUi() +{ + connect(m_ui.add, &QPushButton::clicked, this, &PostProcessingChainConfigWidget::onAddButtonClicked); + connect(m_ui.remove, &QPushButton::clicked, this, &PostProcessingChainConfigWidget::onRemoveButtonClicked); + connect(m_ui.clear, &QPushButton::clicked, this, &PostProcessingChainConfigWidget::onClearButtonClicked); + connect(m_ui.moveUp, &QPushButton::clicked, this, &PostProcessingChainConfigWidget::onMoveUpButtonClicked); + connect(m_ui.moveDown, &QPushButton::clicked, this, &PostProcessingChainConfigWidget::onMoveDownButtonClicked); + connect(m_ui.shaderSettings, &QPushButton::clicked, this, + &PostProcessingChainConfigWidget::onShaderConfigButtonClicked); + connect(m_ui.shaders, &QListWidget::itemSelectionChanged, this, &PostProcessingChainConfigWidget::updateButtonStates); + + m_ui.loadPreset->setEnabled(false); + m_ui.savePreset->setEnabled(false); +} + +bool PostProcessingChainConfigWidget::setConfigString(const std::string_view& config_string) +{ + if (!m_chain.CreateFromString(config_string)) + return false; + + updateList(); + return true; +} + +std::optional PostProcessingChainConfigWidget::getSelectedIndex() const +{ + QList selected_items = m_ui.shaders->selectedItems(); + return selected_items.empty() ? std::nullopt : + std::optional(selected_items.first()->data(Qt::UserRole).toUInt()); +} + +void PostProcessingChainConfigWidget::updateList() +{ + m_ui.shaders->clear(); + + for (u32 i = 0; i < m_chain.GetStageCount(); i++) + { + const FrontendCommon::PostProcessingShader& shader = m_chain.GetShaderStage(i); + + QListWidgetItem* item = new QListWidgetItem(QString::fromStdString(shader.GetName()), m_ui.shaders); + item->setData(Qt::UserRole, QVariant(i)); + } + + updateButtonStates(); +} + +void PostProcessingChainConfigWidget::configChanged() +{ + if (m_chain.IsEmpty()) + chainConfigStringChanged(std::string()); + else + chainConfigStringChanged(m_chain.GetConfigString()); +} + +void PostProcessingChainConfigWidget::updateButtonStates() +{ + std::optional index = getSelectedIndex(); + m_ui.remove->setEnabled(index.has_value()); + m_ui.clear->setEnabled(!m_chain.IsEmpty()); + m_ui.shaderSettings->setEnabled(index.has_value()); + + if (index.has_value()) + { + m_ui.moveUp->setEnabled(index.value() > 0); + m_ui.moveDown->setEnabled(index.value() < (m_chain.GetStageCount() - 1u)); + } + else + { + m_ui.moveUp->setEnabled(false); + m_ui.moveDown->setEnabled(false); + } +} + +void PostProcessingChainConfigWidget::onAddButtonClicked() +{ + QMenu menu; + + const std::vector shaders(FrontendCommon::PostProcessingChain::GetAvailableShaderNames()); + if (shaders.empty()) + { + menu.addAction(tr("No Shaders Available"))->setEnabled(false); + } + else + { + for (const std::string& shader : shaders) + { + QAction* action = menu.addAction(QString::fromStdString(shader)); + connect(action, &QAction::triggered, [this, &shader]() { + if (!m_chain.AddStage(shader)) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to add shader. The log may contain more information.")); + return; + } + + updateList(); + configChanged(); + }); + } + } + + menu.exec(QCursor::pos()); +} + +void PostProcessingChainConfigWidget::onRemoveButtonClicked() +{ + QList selected_items = m_ui.shaders->selectedItems(); + if (selected_items.empty()) + return; + + QListWidgetItem* item = selected_items.first(); + u32 index = item->data(Qt::UserRole).toUInt(); + if (index < m_chain.GetStageCount()) + { + m_chain.RemoveStage(index); + updateList(); + configChanged(); + } +} + +void PostProcessingChainConfigWidget::onClearButtonClicked() +{ + if (QMessageBox::question(this, tr("Question"), tr("Are you sure you want to clear all shader stages?"), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) + { + m_chain.ClearStages(); + updateList(); + configChanged(); + } +} + +void PostProcessingChainConfigWidget::onMoveUpButtonClicked() +{ + std::optional index = getSelectedIndex(); + if (index.has_value()) + { + m_chain.MoveStageUp(index.value()); + updateList(); + configChanged(); + } +} + +void PostProcessingChainConfigWidget::onMoveDownButtonClicked() +{ + std::optional index = getSelectedIndex(); + if (index.has_value()) + { + m_chain.MoveStageDown(index.value()); + updateList(); + configChanged(); + } +} + +void PostProcessingChainConfigWidget::onShaderConfigButtonClicked() +{ + std::optional index = getSelectedIndex(); + if (index.has_value()) + { + } +} diff --git a/src/duckstation-qt/postprocessingchainconfigwidget.h b/src/duckstation-qt/postprocessingchainconfigwidget.h new file mode 100644 index 000000000..3e15ca14a --- /dev/null +++ b/src/duckstation-qt/postprocessingchainconfigwidget.h @@ -0,0 +1,45 @@ +#pragma once +#include "common/types.h" +#include "ui_postprocessingchainconfigwidget.h" +#include "frontend-common/postprocessing_chain.h" +#include +#include +#include +#include + +namespace FrontendCommon { +class PostProcessingChain; +} + +class PostProcessingChainConfigWidget : public QWidget +{ + Q_OBJECT + +public: + PostProcessingChainConfigWidget(QWidget* parent); + ~PostProcessingChainConfigWidget(); + + bool setConfigString(const std::string_view& config_string); + +Q_SIGNALS: + void chainConfigStringChanged(const std::string& new_config_string); + +private Q_SLOTS: + void onAddButtonClicked(); + void onRemoveButtonClicked(); + void onClearButtonClicked(); + void onMoveUpButtonClicked(); + void onMoveDownButtonClicked(); + void onShaderConfigButtonClicked(); + void updateButtonStates(); + +private: + void connectUi(); + std::optional getSelectedIndex() const; + void updateList(); + void configChanged(); + + Ui::PostProcessingChainConfigWidget m_ui; + + FrontendCommon::PostProcessingChain m_chain; +}; diff --git a/src/duckstation-qt/postprocessingchainconfigwidget.ui b/src/duckstation-qt/postprocessingchainconfigwidget.ui new file mode 100644 index 000000000..2f9647492 --- /dev/null +++ b/src/duckstation-qt/postprocessingchainconfigwidget.ui @@ -0,0 +1,130 @@ + + + PostProcessingChainConfigWidget + + + + 0 + 0 + 497 + 151 + + + + Form + + + + + + + + Add + + + + :/icons/list-add.png:/icons/list-add.png + + + + + + + Remove + + + + :/icons/list-remove.png:/icons/list-remove.png + + + + + + + Clear + + + + :/icons/edit-clear-16.png:/icons/edit-clear-16.png + + + + + + + Move Up + + + + :/icons/go-up-16.png:/icons/go-up-16.png + + + + + + + Move Down + + + + :/icons/go-down-16.png:/icons/go-down-16.png + + + + + + + Shader Settings... + + + + :/icons/preferences-system@2x.png:/icons/preferences-system@2x.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Load Preset + + + + + + + Save Preset + + + + + + + + + + 0 + 50 + + + + + + + + + + + diff --git a/src/duckstation-qt/postprocessingshaderconfigwidget.cpp b/src/duckstation-qt/postprocessingshaderconfigwidget.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/src/duckstation-qt/postprocessingshaderconfigwidget.h b/src/duckstation-qt/postprocessingshaderconfigwidget.h new file mode 100644 index 000000000..e69de29bb diff --git a/src/duckstation-qt/resources/icons/edit-clear-16.png b/src/duckstation-qt/resources/icons/edit-clear-16.png new file mode 100644 index 0000000000000000000000000000000000000000..44bb5d77de80a98828531b8f799c79a9c136a698 GIT binary patch literal 912 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>lgAQBf zNI|#|az-QPy%W|C&j%e_rI-TpKMG)BVK}zl_}W3+6I-48%0a<_2JFokX4Ww{7@-NH zaCi2)9jX&qUoHIhV(t+O?bqiD|Gw~jd)?&QBcC5n0zcnzd2>bO=0TA!w++ACv3!0` z|NsANjk*)i=ID|jzhDNw)xC^`|1z{+A8PsY-9>Nbqnmuoe*HQoy8YkZ z56d}I{`@?z%2@i)((doe6z*3)w=r{wANyR+!!4uT@%QijG<|t~E@|t_zwRydvD0G{ zRj{o0FgMmwmSAG#WZrWjYZ=fQ#w2fdm)d0;1x^Av>?NMQuI!IFE7bkrOeCu{q^%VFf{kC zpI`5g5K!QeF+=#wjSn1$J?3i&a5Jp_D1St`+lwFQZq*Xkh?11Vl2ohYqEsNoU}Ruu zscT@NYitl=Xk=wzWMyKeZD3$!U@$@S$vG4ax%nxXX_dG&JoJbu0%~CJboFyt=akR{ E0JnBZEC2ui literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/edit-clear-16@2x.png b/src/duckstation-qt/resources/icons/edit-clear-16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3450882d73ed6a688f331acea2e96d9948e0fe58 GIT binary patch literal 1788 zcmV004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x1`|m{K~z}7&6aCy6z3I&pSkSJ-n_duwz08|V?wyqj)@C2 zv=j)0NL3UyRRvW85~`Y}Nq%p6@%~2uzC?w^QpOeZlkHYr76` zo;ePoc=6KLSzB6t*Bf;W%c@Mv);{WNYZn5!hrazX_FMso#S0}AdA{zhCDjw@ypT=~ z#Tr*{T<7TWPv_Esn*bz5DfR3evJ%8~ETTa{4#!G&9^Q)dsQ~2l%$aba%8zOnn6D zlLEx+Eml9=IKOG*57*W%-BK#Zk>H%{ft&WjR?=fN{^x3z#g{j4eoUxcvICaL-?LU$gTF1SX{@iPSsaN*T|Ej zd)xLCn^gw@+V)eM8I_N9z7}dO8F0nVM=B{_uz)@sPETC*x9oXPSiU(~R==|B5 z?fhoOEtm=i+Q$W)^Cfs!TS7ekx1GY~?^WYEqjXg&uu=M}0W>0gme;%K@^JBPgnbmDg}Rhg|v!YP4L9RXo`8wG9`S zz0%|#efAXoj2t)u)?V0Kv5b^un>cm`i41vt6j>25vKqeUBMKslIv;K9Jp`dQw6Ns| z+s-WyzTI^gWkvx1*(mO?Wt!-^2EK-Ej1dmYWNsu;gDSdaBZ?W+=-s47-orI7C=D$? z+GL28ZSU-(V#WZTOQ)IF{d`_W&t(7u%NVD;D#1wq2$4iNM#cgEI?>W*lKrPKwUbiA zs$DA+RZZ<@e#iP50XQ~i^I9_FI4ZeZ8bpUwavVwaK@hR?DWu>6EZxF&uTfgFj%0ra z+QeJJf)(3ptCv4^?Ce45$jJkg|4#rT{d8t;^k;psj9ey*;}~Sq8kVV3UbBda!8DOX z1luUVu~K->$5bp@&*We?lh^-1NqJR#@tSA88uS8P(}&ib02Cm)Z4Xf`)qh=5YH_^~ zc|$`|!(>t^95+p(x{3726-w(?lARE-jXqpwjLO9uuuYe-tEU<6IhyPlu(#j81G~8H z?EqvT3X}jbApSwGbHvniQwi6irLzRWF(yYdnE7$SWp(H|1=k)XUfoP?(!00{r;i$C!RH)Jo0CT3X@kXWC0Lv1)vtJR|?N6 zpqeJi>J?hBzJAfYk{psOqnB7^3(3JgWHpBxxd+RzP$P8&WB0m4AO1D_=83*vJ^38_ zZltgQf0{hNyVYGV_2vJipiHQBU+mS--2eE(fv#7ms9S{Vmy;Umptk7=@~Nczao0if z@0YU2pZPU!W^!nN3FL1l=@X8pSY#J~;COgi*1XtQKen;1@tYwt>l2JF_Og?c`D>lW zhmRgRfAHyFST7zuMZ0OE7mAqEWED&JggYWw)Q;k|1b+SIo7HtU>G`)E7 zd%PkDw@`daKL2m=FUy?0uNIG}I{*LxC3HntbYx+4WjbSWWnpw>05UK!IV~_aEiyP% zFf=+bGCDChD=;uRFff1{B#;0A03~!qSaf7zbY(hiZ)9m^c>ppnF*z+TI4v?bR4_C; eF)}(aI4dwPIxsNtMw~1F000014Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>=0S?N*P{4K~vbXIE@;%3EWcvRpNCf!Nf;SrwZcbJrN9 zEK>=aCphI`df7(1oYe-2OBDlVb9U{GD&1_AwO%u7nLx;5&eq*Q)jJ*1Hp#`V=Z)RK z6SJNtay56xI<=gws_|R+l6Uc^>=DS>rI5NuFzbj=&I#dyW6~v;B>(^azmw(XTcDc^ zOM?7@8RT>F$_ol#GQK$W@zc*o&#DA&uF-sYh|TrQ4Ux<*kM&qMr~dg6%g!$@ufDsV zjaOKzm@9lWP%~qax4TQss~Zf{fgJV{PhVH|N9>{^>Q=6i-0y)xlRRA8NyOGjH*r!ca+tEY?aU=TO&Up;$wySl#o zeufB#fD@;0AHROyULinYm5YmshJ}iWijR*>T3bs>g-gnmDJF*$jW$iZSoDcwU(=#V zo4QslVw$pg*}8=*m#$sBbmhWz+=3UcUcPQ%aN)xB%c%-%3=ChF3n#5-cr*{_I@J=_ zh?11Vl2ohYqEsNoU}Ruup=)5SYh)N=WME}%X=Q4mZD3$!V8C SYG*fr(}kz2pUXO@geCyCBl!0K literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/go-down-16@2x.png b/src/duckstation-qt/resources/icons/go-down-16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..84d64c2d73f01308fb7ace3fcb586dff718b981e GIT binary patch literal 1592 zcmZ`&dpOg382<_z)0)c=)f1IlZc{|Vj5fJ6_sf#8#kL4j*l)98${}?stsaVUmto9Y zC!~^1IxeMioTr=a$*FX5o}5aYdCp%wo%4C#@B4Yb@B4n9_j%s$^DPVt*o{OOBLDzM zUmrq*9kt+hx~Un$1r;Kff!|g9CG8s~5J#of@DJvQgFo;K5Tb zzRDuyuXRsWP5@r~)fEb8pU@JU4#Nw8U#@;itt*ZT$ESgS0;BA9e_SSPuh9IoJZe`C z%r#SuS?+lL4vmnD3=dzCE8)t8^V|GL1qPNe|0cfk?iuQZ{SoA%P3Ar;Tlf(k97M5# z&M0%R4q4gWe*%F9I0$YZqxNI9XX>J8^ryPpg6a`|xdJMaA1=9>eD219qHFOdug0Fa z7B8MSF6iEq*-l_fafe!*lbSFLktMCplv2GevO@1*qdl*K$d-CAMONfWbYi1jM7d5* zmw#&Wj)P71(bdLbm)10mAIs_T%V>3HH9IltEogNnL1mhaV+C2AKAbkMG>I##*)g%v zmR@g8t=&kj)DI|yHH_u+JG>z&K23s67H>;zw4sa4Vr$n2U(~E0&4b!;X;K$f>$W6` zO?;zCMD^;>DvgjzjihEf*>7c~Lx)NRlg|w%oVXgr>+*uS+%h_yS?zXI@mg}dR%AUQ zLTQ&ZV;T%uQcQ{jlhkZ~R2EcyiklQ5{8cvJWI zFMel7D2zTm=73JpuwKdt`pBru+3~0Y*L4ocbq|f}^Czr-9`{ci(oG&gr;h4F6Q+5i zZme5IWxvMphA}0R(aX!rcDaS0l<&PiJtQC$aOK_d*VFH3XJ_Z;XBO_IuH ztUKvZSC%5cl(uCvfdIXG+zw4IslrhkaQvC!J0!)a6NNS8O{UyuFB&G9W96lT zm410{F;#)%Ej=5WXWBbCO7o5ue`=ay#q?z@ynn~rKoMJc)WSqioiI%(6=8MuMCO@aNm~V=*cPhrU@x=2bptEW?n2VRGet4Iof}z`Q*;y9cBH! z1@$G}rzI6R_{#2*`hwp6bwXh!wYO*jV?X!d*sy5k{IOJ)q;*2O_lXI?QYHK*R?f{F z;#q9rD0p1?PWh^r`oZhZtbNO1ZhQvROh2(xt{1){yH|@2FfNOR@k{{1DrDp$%RR~n zn57fR>G8C5IyUYwT}faY#=*rF<7A6*2*bEw9d}}#T&yt|ECz#H+j8SOK{6{ofx-QM TK_Jl}TPXm(!~jBz7xl14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>`T@Ck7RDsZl3cW7X6 zDQBoy;So1k)3T7EXt7K2I=hHo1)yy9e4D~mw%)Bm@l*A?cf}Sgw{~dYi0D(9bRaWl zk%dzYgLkXQ^aF(%^Np>`7^3>snm0yNuko$i;+VhGI%|Q2V;#G56HCDwQ`Z)j!u4j( z4J<|L%z(y1fO$4c`Ff{M^SX!|B zgf0B>TXV;e!&cCEOPgkai06r#<=Tq(wx7qT?&8RQ}+IJk$1(B%b)h{D=d0e^sMYT z8<%L^%NMU+y?Ct1$ssJGq$W}2{0L|UW0JSKOMLOgKwTh*y~NYkmHiPb6Qh)+bhJMx zNP0Y7977~7Cnp?Wl$aUt^Nh|KjT*BuZazO1JMryHr<(HXJ2mv*KX&bwpM$_LaiMMH z1tmpgf`Y9djvk41}z_!crpyI+rFAnFahn1BU zFA5c4T4gA>vG8KntAKTF%cgDXTQ_fCcptHiB=@13VJPy>UftDnm{r-UW|x#I>} literal 0 HcmV?d00001 diff --git a/src/duckstation-qt/resources/icons/go-up-16@2x.png b/src/duckstation-qt/resources/icons/go-up-16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..18bbd26a460e0ae74a2715032878ec64c1fc411e GIT binary patch literal 1340 zcmV-C1;hG@P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Rd1pyZh5ei;FwEzGFKuJVFR9M69mQ8F_RTRg6 z=f3x*)RuNEEp0)BKwJ5+e3MeGW&DZ|i3Ut{LyQ|WF$$@P1fvTRHzp<|CT>hjTxnw1 zsUcuU{Adv|7*mtNqERgUu!Je4G8AU!-FsZTnL4%<+gi{C=jP;|#k=?azvukVy$^2N ze{9}t7q+$G#drGeF52k6ANYavZ1*v=K6V=tG+=O0PDg?~#4wCoS|CbsN) zYC&bZX#4%UH@md|=`AZk%K`t9^$lC!uU)h-e&+lM#8Yosb>mX4I~zOKVZXkq1ey;N z)z#JB^VychmW8K>j^l8QUc118^6H>0R=IB8s)65nJGJ|!5NK|vqPn7{bH@YEEa^Vq zfpuA|hB&ocNcFK|(duHGHQP!XM>_hByZ$-Vz@`@nO3UI$cRl#r(w^igtP25<+yLkr z{E9Uz*H%?k)EsJkl_hgZpsXtP@zW2#xPI_*kEFvfK*r$#Lvz6VR$fw^O!cF=5m{jjXcS+`T;fc!Lmt}t_~xO8;Ga#Wrpyn)S4vAtI6eFgMj>slaVay1C4&jTsGur1rz|O1 z$J(=c;|Un_#6f##I`_b|o>2iT8IqY^jIS8aASSAFL_l#i`p|d+lMnju z2q2IOhrw8kCwRVKyhv~;@v~hDfCJaT30u4iJYbs9{FnGyfrxDdLHy z$XsN=2Vy{=mH?-y1LqXUrQF%*3U)5%s-U2X0TuuukOeZicv=MVz%d{IVrnrlQ$o$W z28pC5{OcsyD{vq1o#o1j?@lHKPee5LAR0tf zGBFGcUxhYtm2MP;3TabZ#oSxXD zG7XIs0(&`NGX;UX+X+O%>F9c<{UDRHKa(5iw*6o1Pr4+`=S6;Xw*UYDC3HntbYx+4 zWjbSWWnpw>05UK!I4v+WEif@uGBY|cG&(UiD=;uRFfh&(j4c2F03~!qSaf7zbY(hi yZ)9m^c>ppnF*q$SH7zhPR5CLlF0000icons/duck.png icons/duck_128.png icons/duck_64.png + icons/edit-clear-16.png + icons/edit-clear-16@2x.png icons/edit-find.png icons/flag-eu.png icons/flag-eu@2x.png @@ -43,6 +45,10 @@ icons/flag-us@2x.png icons/folder-open.png icons/folder-open@2x.png + icons/go-down-16.png + icons/go-down-16@2x.png + icons/go-up-16.png + icons/go-up-16@2x.png icons/input-gaming.png icons/input-gaming@2x.png icons/list-add.png