diff --git a/src/common/d3d11/texture.h b/src/common/d3d11/texture.h index 0fc6f0180..939e7a8f9 100644 --- a/src/common/d3d11/texture.h +++ b/src/common/d3d11/texture.h @@ -20,6 +20,7 @@ public: ALWAYS_INLINE ID3D11RenderTargetView* GetD3DRTV() const { return m_rtv.Get(); } ALWAYS_INLINE ID3D11ShaderResourceView* const* GetD3DSRVArray() const { return m_srv.GetAddressOf(); } ALWAYS_INLINE ID3D11RenderTargetView* const* GetD3DRTVArray() const { return m_rtv.GetAddressOf(); } + ALWAYS_INLINE bool IsValid() const { return static_cast(m_texture); } ALWAYS_INLINE u32 GetWidth() const { return m_width; } ALWAYS_INLINE u32 GetHeight() const { return m_height; } diff --git a/src/frontend-common/d3d11_host_display.cpp b/src/frontend-common/d3d11_host_display.cpp index be183dd70..f9d637966 100644 --- a/src/frontend-common/d3d11_host_display.cpp +++ b/src/frontend-common/d3d11_host_display.cpp @@ -960,6 +960,10 @@ bool D3D11HostDisplay::SetPostProcessingChain(const std::string_view& config) PostProcessingStage stage; stage.uniforms_size = shader.GetUniformsSize(); + stage.output_texture_scale = shader.GetOutputScale(); + stage.sampler_state = (shader.GetTextureFilter() == PostProcessingShader::TextureFilter::Linear) ? + m_linear_sampler.Get() : + m_point_sampler.Get(); stage.vertex_shader = shader_cache.GetVertexShader(m_device.Get(), vs); stage.pixel_shader = shader_cache.GetPixelShader(m_device.Get(), ps); if (!stage.vertex_shader || !stage.pixel_shader) @@ -1000,13 +1004,17 @@ bool D3D11HostDisplay::CheckPostProcessingRenderTargets(u32 target_width, u32 ta return false; } - const u32 target_count = (static_cast(m_post_processing_stages.size()) - 1); - for (u32 i = 0; i < target_count; i++) + for (u32 i = 0; i < static_cast(m_post_processing_stages.size()); i++) { PostProcessingStage& pps = m_post_processing_stages[i]; - if (pps.output_texture.GetWidth() != target_width || pps.output_texture.GetHeight() != target_height) + if (i == static_cast(m_post_processing_stages.size() - 1) && pps.output_texture_scale == 1.0f) + break; + + const u32 stage_width = static_cast(static_cast(target_width) * pps.output_texture_scale); + const u32 stage_height = static_cast(static_cast(target_height) * pps.output_texture_scale); + if (pps.output_texture.GetWidth() != stage_width || pps.output_texture.GetHeight() != stage_height) { - if (!pps.output_texture.Create(m_device.Get(), target_width, target_height, 1, format, bind_flags)) + if (!pps.output_texture.Create(m_device.Get(), stage_width, stage_height, 1, format, bind_flags)) return false; } } @@ -1042,25 +1050,32 @@ void D3D11HostDisplay::ApplyPostProcessingChain(ID3D11RenderTargetView* final_ta 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 + if (pps.output_texture.IsValid()) { m_context->ClearRenderTargetView(pps.output_texture.GetD3DRTV(), clear_color.data()); m_context->OMSetRenderTargets(1, pps.output_texture.GetD3DRTVArray(), nullptr); + + CD3D11_VIEWPORT vp(0.0f, 0.0f, static_cast(pps.output_texture.GetWidth()), + static_cast(pps.output_texture.GetHeight()), 0.0f, 1.0f); + m_context->RSSetViewports(1, &vp); + } + else + { + m_context->OMSetRenderTargets(1, &final_target, nullptr); + + CD3D11_VIEWPORT vp(0.0f, 0.0f, static_cast(GetWindowWidth()), static_cast(GetWindowHeight()), 0.0f, + 1.0f); + m_context->RSSetViewports(1, &vp); } 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()); + m_context->PSSetSamplers(0, 1, &pps.sampler_state); const auto map = m_display_uniform_buffer.Map(m_context.Get(), m_display_uniform_buffer.GetSize(), pps.uniforms_size); @@ -1073,10 +1088,28 @@ void D3D11HostDisplay::ApplyPostProcessingChain(ID3D11RenderTargetView* final_ta m_context->Draw(3, 0); - if (i != final_stage) + if (pps.output_texture.IsValid()) texture_handle = pps.output_texture.GetD3DSRV(); } + // final blit when upscaling shaders are used + const PostProcessingStage& final_stage = m_post_processing_stages.back(); + if (final_stage.output_texture.IsValid()) + { + m_context->OMSetRenderTargets(1, &final_target, nullptr); + + CD3D11_VIEWPORT vp(0.0f, 0.0f, static_cast(GetWindowWidth()), static_cast(GetWindowHeight()), 0.0f, + 1.0f); + m_context->RSSetViewports(1, &vp); + + m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + m_context->VSSetShader(m_display_vertex_shader.Get(), nullptr, 0); + m_context->PSSetShader(m_display_pixel_shader.Get(), nullptr, 0); + m_context->PSSetShaderResources(0, 1, final_stage.output_texture.GetD3DSRVArray()); + m_context->PSSetSamplers(0, 1, m_linear_sampler.GetAddressOf()); + m_context->Draw(3, 0); + } + ID3D11ShaderResourceView* null_srv = nullptr; m_context->PSSetShaderResources(0, 1, &null_srv); } diff --git a/src/frontend-common/d3d11_host_display.h b/src/frontend-common/d3d11_host_display.h index 5e48112e8..dd9fd5b23 100644 --- a/src/frontend-common/d3d11_host_display.h +++ b/src/frontend-common/d3d11_host_display.h @@ -107,7 +107,9 @@ protected: { ComPtr vertex_shader; ComPtr pixel_shader; + ID3D11SamplerState* sampler_state; D3D11::Texture output_texture; + float output_texture_scale; u32 uniforms_size; }; diff --git a/src/frontend-common/postprocessing_shader.cpp b/src/frontend-common/postprocessing_shader.cpp index fe38c0fb6..7bf6ed984 100644 --- a/src/frontend-common/postprocessing_shader.cpp +++ b/src/frontend-common/postprocessing_shader.cpp @@ -87,39 +87,54 @@ u32 ParseVector(const std::string_view& line, PostProcessingShader::Option::Valu PostProcessingShader::PostProcessingShader() = default; -PostProcessingShader::PostProcessingShader(std::string name, std::string code) : m_name(name), m_code(code) +PostProcessingShader::PostProcessingShader(std::string name, std::string code) : m_name(name) { - LoadOptions(); + Pass pass; + pass.code = std::move(code); + m_passes.push_back(std::move(pass)); + LoadLegacyOptions(); } PostProcessingShader::PostProcessingShader(const PostProcessingShader& copy) - : m_name(copy.m_name), m_code(copy.m_code), m_options(copy.m_options) + : m_name(copy.m_name), m_passes(copy.m_passes), 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)) + : m_name(std::move(move.m_name)), m_passes(std::move(move.m_passes)), m_options(std::move(move.m_options)) { } PostProcessingShader::~PostProcessingShader() = default; bool PostProcessingShader::LoadFromFile(std::string name, const char* filename) +{ + const char* extension = std::strrchr(filename, '.'); + if (extension && StringUtil::Strcasecmp(extension, ".glslp") == 0) + return LoadFromPassFile(std::move(name), filename); + else + return LoadFromLegacyFile(std::move(name), filename); +} + +bool PostProcessingShader::LoadFromLegacyFile(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()); + + Pass pass; + pass.code = std::move(code.value()); + m_passes.push_back(std::move(pass)); m_options.clear(); - LoadOptions(); + LoadLegacyOptions(); return true; } bool PostProcessingShader::IsValid() const { - return !m_name.empty() && !m_code.empty(); + return !m_name.empty() && !m_passes.empty(); } const PostProcessingShader::Option* PostProcessingShader::GetOptionByName(const std::string_view& name) const @@ -277,7 +292,7 @@ void PostProcessingShader::FillUniformBuffer(void* buffer, u32 texture_width, s3 FrontendCommon::PostProcessingShader& PostProcessingShader::operator=(const PostProcessingShader& copy) { m_name = copy.m_name; - m_code = copy.m_code; + m_passes = copy.m_passes; m_options = copy.m_options; return *this; } @@ -285,18 +300,18 @@ FrontendCommon::PostProcessingShader& PostProcessingShader::operator=(const Post FrontendCommon::PostProcessingShader& PostProcessingShader::operator=(PostProcessingShader& move) { m_name = std::move(move.m_name); - m_code = std::move(move.m_code); + m_passes = std::move(move.m_passes); m_options = std::move(move.m_options); return *this; } -void PostProcessingShader::LoadOptions() +void PostProcessingShader::LoadLegacyOptions() { // 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); + size_t configuration_start = m_passes.front().code.find(config_start_delimiter); + size_t configuration_end = m_passes.front().code.find(config_end_delimiter); if (configuration_start == std::string::npos || configuration_end == std::string::npos) { // Issue loading configuration or there isn't one. @@ -304,8 +319,8 @@ void PostProcessingShader::LoadOptions() } std::string configuration_string = - m_code.substr(configuration_start + std::strlen(config_start_delimiter), - configuration_end - configuration_start - std::strlen(config_start_delimiter)); + m_passes.front().code.substr(configuration_start + std::strlen(config_start_delimiter), + configuration_end - configuration_start - std::strlen(config_start_delimiter)); std::istringstream in(configuration_string); @@ -416,4 +431,181 @@ void PostProcessingShader::LoadOptions() } } +bool PostProcessingShader::LoadFromPassFile(std::string name, const char* filename) +{ + std::optional code = FileSystem::ReadFileToString(filename); + if (!code.has_value() || code->empty()) + return false; + + std::istringstream in(code.value()); + + std::string current_section; + Option current_option = {}; + Pass current_pass; + + auto CompleteSection = [&]() { + 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 = {}; + return true; + } + else if (current_section == "Pass") + { + if (current_pass.code.empty()) + { + Log_ErrorPrintf("Pass %zu has no code", m_passes.size() + 1); + return false; + } + + return true; + } + else + { + return true; + } + }; + + while (!in.eof()) + { + std::string line_str; + if (std::getline(in, line_str)) + { + std::string_view line_view = line_str; + + // 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); + + if (line_view.empty()) + continue; + + if (line_view[0] == '[') + { + size_t endpos = line_view.find("]"); + if (endpos != std::string::npos) + { + if (!CompleteSection()) + return false; + + // New section! + current_section = line_view.substr(1, endpos - 1); + if (current_section == "OptionBool") + current_option.type = Option::Type::Bool; + else if (current_section == "OptionRangeFloat") + current_option.type = Option::Type::Float; + else if (current_section == "OptionRangeInteger") + current_option.type = Option::Type::Int; + else if (current_section != "Pass") + Log_ErrorPrintf("Invalid option type: '%s'", line_str.c_str()); + + continue; + } + } + + std::string_view key, value; + ParseKeyValue(line_view, &key, &value); + if (!key.empty() && !value.empty()) + { + if (current_option.type != Option::Type::Invalid) + { + 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++].int_value = StringUtil::FromChars(value).value_or(false) ? 1 : 0; + 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()); + } + } + else if (current_section == "Pass") + { + if (key == "OutputScale") + { + current_pass.output_scale = StringUtil::FromChars(value).value_or(1.0f); + if (current_pass.output_scale <= 0.0f) + { + Log_ErrorPrintf("Invalid output scale: %f", current_pass.output_scale); + current_pass.output_scale = 1.0f; + } + } + else if (key == "TextureFilter") + { + if (value == "Nearest") + current_pass.texture_filter = TextureFilter::Nearest; + else if (value == "Linear") + current_pass.texture_filter = TextureFilter::Linear; + else + Log_ErrorPrintf("Invalid texture filter: '%s'", line_str.c_str()); + } + else if (key == "File") + { + const String source_file(FileSystem::BuildPathRelativeToFile(filename, SmallString(value))); + std::optional source_code(FileSystem::ReadFileToString(source_file)); + if (!source_code.has_value() || source_code->empty()) + { + Log_ErrorPrintf("Failed to load shader source from '%s'", source_file.GetCharArray()); + return false; + } + + current_pass.code = std::move(source_code.value()); + } + else + { + Log_ErrorPrintf("Invalid pass setting: '%s'", line_str.c_str()); + } + } + } + } + } + + if (!CompleteSection()) + return false; + + if (m_passes.empty()) + { + Log_ErrorPrintf("No passes defined in %s", filename); + return false; + } + + return true; +} + } // namespace FrontendCommon diff --git a/src/frontend-common/postprocessing_shader.h b/src/frontend-common/postprocessing_shader.h index 4f6905900..f0d68981b 100644 --- a/src/frontend-common/postprocessing_shader.h +++ b/src/frontend-common/postprocessing_shader.h @@ -16,6 +16,12 @@ public: PUSH_CONSTANT_SIZE_THRESHOLD = 128 }; + enum class TextureFilter + { + Nearest, + Linear + }; + struct Option { enum : u32 @@ -53,6 +59,13 @@ public: ValueVector value; }; + struct Pass + { + std::string code; + float output_scale = 1.0f; + TextureFilter texture_filter = TextureFilter::Linear; + }; + PostProcessingShader(); PostProcessingShader(std::string name, std::string code); PostProcessingShader(const PostProcessingShader& copy); @@ -63,10 +76,14 @@ public: 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::string& GetCode(u32 pass) const { return m_passes[pass].code; } + ALWAYS_INLINE float GetOutputScale(u32 pass) const { return m_passes[pass].output_scale; } + ALWAYS_INLINE TextureFilter GetTextureFilter(u32 pass) const { return m_passes[pass].texture_filter; } + ALWAYS_INLINE u32 GetNumPasses() const { return static_cast(m_passes.size()); } ALWAYS_INLINE const std::vector