From 8a40c7bf944b3a4209d90134a379c9d1f0cc7e20 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 13 Aug 2023 23:23:54 +1000 Subject: [PATCH] PostProcessing: Add ReShade .fx shader support --- README.md | 52 +- dep/CMakeLists.txt | 1 + dep/reshadefx/src/effect_codegen_hlsl.cpp | 4 +- src/util/CMakeLists.txt | 4 +- src/util/postprocessing.cpp | 57 +- src/util/postprocessing_shader_fx.cpp | 1304 +++++++++++++++++++++ src/util/postprocessing_shader_fx.h | 122 ++ src/util/util.vcxproj | 8 +- src/util/util.vcxproj.filters | 4 +- 9 files changed, 1522 insertions(+), 34 deletions(-) create mode 100644 src/util/postprocessing_shader_fx.cpp create mode 100644 src/util/postprocessing_shader_fx.h diff --git a/README.md b/README.md index 416bbb1c00..0ee7a6bba7 100644 --- a/README.md +++ b/README.md @@ -22,37 +22,37 @@ DuckStation features a fully-featured frontend built using Qt, as well as a full Other features include: - - CPU Recompiler/JIT (x86-64, armv7/AArch32 and AArch64) - - Hardware (D3D11, D3D12, OpenGL, Vulkan, Metal) and software rendering - - Upscaling, texture filtering, and true colour (24-bit) in hardware renderers - - PGXP for geometry precision, texture correction, and depth buffer emulation - - Adaptive downsampling filter - - Post processing shader chains - - "Fast boot" for skipping BIOS splash/intro - - Save state support - - Windows, Linux, macOS support + - CPU Recompiler/JIT (x86-64, armv7/AArch32 and AArch64). + - Hardware (D3D11, D3D12, OpenGL, Vulkan, Metal) and software rendering. + - Upscaling, texture filtering, and true colour (24-bit) in hardware renderers. + - PGXP for geometry precision, texture correction, and depth buffer emulation. + - Adaptive downsampling filter. + - Post processing shader chains (GLSL and experimental Reshade FX). + - "Fast boot" for skipping BIOS splash/intro. + - Save state support. + - Windows, Linux, macOS support. - Supports bin/cue images, raw bin/img files, MAME CHD, single-track ECM, MDS/MDF, and unencrypted PBP formats. - - Direct booting of homebrew executables - - Direct loading of Portable Sound Format (psf) files - - Digital and analog controllers for input (rumble is forwarded to host) - - Namco GunCon lightgun support (simulated with mouse) - - NeGcon support - - Qt and "Big Picture" UI - - Automatic updates for Windows builds - - Automatic content scanning - game titles/hashes are provided by redump.org - - Optional automatic switching of memory cards for each game - - Supports loading cheats from existing lists - - Memory card editor and save importer - - Emulated CPU overclocking - - Integrated and remote debugging - - Multitap controllers (up to 8 devices) - - RetroAchievements - - Automatic loading/applying of PPF patches + - Direct booting of homebrew executables. + - Direct loading of Portable Sound Format (psf) files. + - Digital and analog controllers for input (rumble is forwarded to host). + - Namco GunCon lightgun support (simulated with mouse). + - NeGcon support. + - Qt and "Big Picture" UI. + - Automatic updates for Windows builds. + - Automatic content scanning - game titles/hashes are provided by redump.org. + - Optional automatic switching of memory cards for each game. + - Supports loading cheats from existing lists. + - Memory card editor and save importer. + - Emulated CPU overclocking. + - Integrated and remote debugging. + - Multitap controllers (up to 8 devices). + - RetroAchievements. + - Automatic loading/applying of PPF patches. ## System Requirements - A CPU faster than a potato. But it needs to be x86_64, AArch32/armv7, or AArch64/ARMv8, otherwise you won't get a recompiler and it'll be slow. - For the hardware renderers, a GPU capable of OpenGL 3.1/OpenGL ES 3.1/Direct3D 11 Feature Level 10.0 (or Vulkan 1.0) and above. So, basically anything made in the last 10 years or so. - - SDL, XInput or DInput compatible game controller (e.g. XB360/XBOne). DualShock 3 users on Windows will need to install the official DualShock 3 drivers included as part of PlayStation Now. + - SDL, XInput or DInput compatible game controller (e.g. XB360/XBOne/XBSeries). DualShock 3 users on Windows will need to install the official DualShock 3 drivers included as part of PlayStation Now. ## Downloading and running Binaries of DuckStation for Windows x64/ARM64, Linux x86_64 (in AppImage format), and Android ARMv7/ARMv8 are available via GitHub Releases and are automatically built with every commit/push. Binaries or packages distributed through other sources may be out of date and are not supported by the developer, please speak to them for support, not us. diff --git a/dep/CMakeLists.txt b/dep/CMakeLists.txt index 5bbfcb39c7..4c39663c70 100644 --- a/dep/CMakeLists.txt +++ b/dep/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory(soundtouch) add_subdirectory(googletest) add_subdirectory(cpuinfo) add_subdirectory(fast_float) +add_subdirectory(reshadefx) if(ENABLE_CUBEB) add_subdirectory(cubeb) diff --git a/dep/reshadefx/src/effect_codegen_hlsl.cpp b/dep/reshadefx/src/effect_codegen_hlsl.cpp index 7625b454cb..5fad082c24 100644 --- a/dep/reshadefx/src/effect_codegen_hlsl.cpp +++ b/dep/reshadefx/src/effect_codegen_hlsl.cpp @@ -632,9 +632,9 @@ class codegen_hlsl final : public codegen const unsigned int texture_dimension = info.type.texture_dimension(); code += "Texture" + std::to_string(texture_dimension) + "D<"; write_texture_format(code, tex_info.format); - code += "> __" + info.unique_name + "_t : register(t" + std::to_string(info.binding) + "); \n"; + code += "> __" + info.unique_name + "_t : register( t0); \n"; - code += "SamplerState __" + info.unique_name + "_s : register(s" + std::to_string(info.binding) + ");\n"; + code += "SamplerState __" + info.unique_name + "_s : register( s0);\n"; code += "static const "; write_type(code, info.type); diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 5192c67b1f..f9606422e5 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -52,6 +52,8 @@ add_library(util postprocessing.h postprocessing_shader.cpp postprocessing_shader.h + postprocessing_shader_fx.cpp + postprocessing_shader_fx.h postprocessing_shader_glsl.cpp postprocessing_shader_glsl.h shadergen.cpp @@ -69,7 +71,7 @@ add_library(util target_include_directories(util PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") target_include_directories(util PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..") target_link_libraries(util PUBLIC common simpleini imgui) -target_link_libraries(util PRIVATE stb libchdr zlib soundtouch Zstd::Zstd) +target_link_libraries(util PRIVATE stb libchdr zlib soundtouch Zstd::Zstd reshadefx) if(ENABLE_CUBEB) target_sources(util PRIVATE diff --git a/src/util/postprocessing.cpp b/src/util/postprocessing.cpp index 7d255ce030..637644732f 100644 --- a/src/util/postprocessing.cpp +++ b/src/util/postprocessing.cpp @@ -6,6 +6,7 @@ #include "host.h" #include "imgui_manager.h" #include "postprocessing_shader.h" +#include "postprocessing_shader_fx.h" #include "postprocessing_shader_glsl.h" // TODO: Remove me @@ -175,6 +176,32 @@ std::vector> PostProcessing::GetAvailableSha } } + FileSystem::FindFiles(Path::Combine(EmuFolders::Shaders, "reshade" FS_OSPATH_SEPARATOR_STR "Shaders").c_str(), "*.fx", + FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &results); + FileSystem::FindFiles( + Path::Combine(EmuFolders::Resources, "shaders" FS_OSPATH_SEPARATOR_STR "reshade" FS_OSPATH_SEPARATOR_STR "Shaders") + .c_str(), + "*.fx", FILESYSTEM_FIND_FILES | 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 auto& other) { return fd.FileName == other.second; })) + { + std::string display_name = fmt::format(TRANSLATE_FS("PostProcessing", "{} [ReShade]"), fd.FileName); + names.emplace_back(std::move(display_name), std::move(fd.FileName)); + } + } + return names; } @@ -333,7 +360,31 @@ void PostProcessing::SetEnabled(bool enabled) std::unique_ptr PostProcessing::TryLoadingShader(const std::string& shader_name, bool only_config, Error* error) { - std::string filename(Path::Combine(EmuFolders::Shaders, fmt::format("{}.glsl", shader_name))); + std::string filename; + std::optional resource_str; + + // Try reshade first. + filename = Path::Combine( + EmuFolders::Shaders, + fmt::format("reshade" FS_OSPATH_SEPARATOR_STR "Shaders" FS_OSPATH_SEPARATOR_STR "{}.fx", shader_name)); + + // TODO: Won't work on Android. Who cares? All the homies are tired of demanding Android users. + if (!FileSystem::FileExists(filename.c_str())) + { + filename = Path::Combine(EmuFolders::Resources, + fmt::format("shaders" FS_OSPATH_SEPARATOR_STR "reshade" FS_OSPATH_SEPARATOR_STR + "Shaders" FS_OSPATH_SEPARATOR_STR "{}.fx", + shader_name)); + } + + if (FileSystem::FileExists(filename.c_str())) + { + std::unique_ptr shader = std::make_unique(); + if (shader->LoadFromFile(std::string(shader_name), filename.c_str(), only_config, error)) + return shader; + } + + filename = Path::Combine(EmuFolders::Shaders, fmt::format("{}.glsl", shader_name)); if (FileSystem::FileExists(filename.c_str())) { std::unique_ptr shader = std::make_unique(); @@ -341,8 +392,8 @@ std::unique_ptr PostProcessing::TryLoadingShader(const s return shader; } - std::optional resource_str( - Host::ReadResourceFileToString(fmt::format("shaders" FS_OSPATH_SEPARATOR_STR "{}.glsl", shader_name).c_str())); + resource_str = + Host::ReadResourceFileToString(fmt::format("shaders" FS_OSPATH_SEPARATOR_STR "{}.glsl", shader_name).c_str()); if (resource_str.has_value()) { std::unique_ptr shader = std::make_unique(); diff --git a/src/util/postprocessing_shader_fx.cpp b/src/util/postprocessing_shader_fx.cpp new file mode 100644 index 0000000000..18055cf4bc --- /dev/null +++ b/src/util/postprocessing_shader_fx.cpp @@ -0,0 +1,1304 @@ +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "postprocessing_shader_fx.h" +#include "input_manager.h" +#include "shadergen.h" + +// TODO: Remove me +#include "core/settings.h" + +#include "common/assert.h" +#include "common/error.h" +#include "common/file_system.h" +#include "common/image.h" +#include "common/log.h" +#include "common/path.h" +#include "common/string_util.h" + +#include "effect_codegen.hpp" +#include "effect_parser.hpp" +#include "effect_preprocessor.hpp" + +#include "fmt/format.h" + +#include +#include +#include +#include + +Log_SetChannel(ReShadeFXShader); + +static constexpr s32 DEFAULT_BUFFER_WIDTH = 3840; +static constexpr s32 DEFAULT_BUFFER_HEIGHT = 2160; + +static RenderAPI GetRenderAPI() +{ + return g_gpu_device ? g_gpu_device->GetRenderAPI() : RenderAPI::D3D11; +} + +static std::unique_ptr CreateRFXCodegen() +{ + const bool debug_info = g_gpu_device ? g_gpu_device->IsDebugDevice() : false; + const bool uniforms_to_spec_constants = false; + + switch (GetRenderAPI()) + { + case RenderAPI::None: + case RenderAPI::D3D11: + case RenderAPI::D3D12: + return std::unique_ptr( + reshadefx::create_codegen_hlsl(50, debug_info, uniforms_to_spec_constants)); + + case RenderAPI::Vulkan: + case RenderAPI::Metal: + return std::unique_ptr( + reshadefx::create_codegen_glsl(true, debug_info, uniforms_to_spec_constants)); + + case RenderAPI::OpenGL: + case RenderAPI::OpenGLES: + default: + return std::unique_ptr( + reshadefx::create_codegen_glsl(false, debug_info, uniforms_to_spec_constants)); + } +} + +static GPUTexture::Format MapTextureFormat(reshadefx::texture_format format) +{ + static constexpr GPUTexture::Format s_mapping[] = { + GPUTexture::Format::Unknown, // unknown + GPUTexture::Format::R8, // r8 + GPUTexture::Format::R16, // r16 + GPUTexture::Format::R16F, // r16f + GPUTexture::Format::R32I, // r32i + GPUTexture::Format::R32U, // r32u + GPUTexture::Format::R32F, // r32f + GPUTexture::Format::RG8, // rg8 + GPUTexture::Format::RG16, // rg16 + GPUTexture::Format::RG16F, // rg16f + GPUTexture::Format::RG32F, // rg32f + GPUTexture::Format::RGBA8, // rgba8 + GPUTexture::Format::RGBA16, // rgba16 + GPUTexture::Format::RGBA16F, // rgba16f + GPUTexture::Format::RGBA32F, // rgba32f + GPUTexture::Format::RGB10A2, // rgb10a2 + }; + DebugAssert(static_cast(format) < std::size(s_mapping)); + return s_mapping[static_cast(format)]; +} + +static GPUSampler::Config MapSampler(const reshadefx::sampler_info& si) +{ + GPUSampler::Config config = GPUSampler::GetNearestConfig(); + + switch (si.filter) + { + case reshadefx::filter_mode::min_mag_mip_point: + config.min_filter = GPUSampler::Filter::Nearest; + config.mag_filter = GPUSampler::Filter::Nearest; + config.mip_filter = GPUSampler::Filter::Nearest; + break; + + case reshadefx::filter_mode::min_mag_point_mip_linear: + config.min_filter = GPUSampler::Filter::Nearest; + config.mag_filter = GPUSampler::Filter::Nearest; + config.mip_filter = GPUSampler::Filter::Linear; + break; + + case reshadefx::filter_mode::min_point_mag_linear_mip_point: + config.min_filter = GPUSampler::Filter::Linear; + config.mag_filter = GPUSampler::Filter::Linear; + config.mip_filter = GPUSampler::Filter::Nearest; + break; + + case reshadefx::filter_mode::min_point_mag_mip_linear: + config.min_filter = GPUSampler::Filter::Nearest; + config.mag_filter = GPUSampler::Filter::Linear; + config.mip_filter = GPUSampler::Filter::Linear; + break; + + case reshadefx::filter_mode::min_linear_mag_mip_point: + config.min_filter = GPUSampler::Filter::Linear; + config.mag_filter = GPUSampler::Filter::Nearest; + config.mip_filter = GPUSampler::Filter::Nearest; + break; + + case reshadefx::filter_mode::min_linear_mag_point_mip_linear: + config.min_filter = GPUSampler::Filter::Linear; + config.mag_filter = GPUSampler::Filter::Nearest; + config.mip_filter = GPUSampler::Filter::Linear; + break; + + case reshadefx::filter_mode::min_mag_linear_mip_point: + config.min_filter = GPUSampler::Filter::Linear; + config.mag_filter = GPUSampler::Filter::Linear; + config.mip_filter = GPUSampler::Filter::Nearest; + break; + + case reshadefx::filter_mode::min_mag_mip_linear: + config.min_filter = GPUSampler::Filter::Linear; + config.mag_filter = GPUSampler::Filter::Linear; + config.mip_filter = GPUSampler::Filter::Linear; + break; + + default: + break; + } + + static constexpr auto map_address_mode = [](const reshadefx::texture_address_mode m) { + switch (m) + { + case reshadefx::texture_address_mode::wrap: + return GPUSampler::AddressMode::Repeat; + case reshadefx::texture_address_mode::mirror: + Panic("Not implemented"); + return GPUSampler::AddressMode::Repeat; + case reshadefx::texture_address_mode::clamp: + return GPUSampler::AddressMode::ClampToEdge; + case reshadefx::texture_address_mode::border: + default: + return GPUSampler::AddressMode::ClampToBorder; + } + }; + + config.address_u = map_address_mode(si.address_u); + config.address_v = map_address_mode(si.address_v); + config.address_w = map_address_mode(si.address_w); + + return config; +} + +static GPUPipeline::BlendState MapBlendState(const reshadefx::pass_info& pi) +{ + static constexpr auto map_blend_op = [](const reshadefx::pass_blend_op o) { + switch (o) + { + case reshadefx::pass_blend_op::add: + return GPUPipeline::BlendOp::Add; + case reshadefx::pass_blend_op::subtract: + return GPUPipeline::BlendOp::Subtract; + case reshadefx::pass_blend_op::reverse_subtract: + return GPUPipeline::BlendOp::ReverseSubtract; + case reshadefx::pass_blend_op::min: + return GPUPipeline::BlendOp::Min; + case reshadefx::pass_blend_op::max: + default: + return GPUPipeline::BlendOp::Max; + } + }; + static constexpr auto map_blend_factor = [](const reshadefx::pass_blend_factor f) { + switch (f) + { + case reshadefx::pass_blend_factor::zero: + return GPUPipeline::BlendFunc::Zero; + case reshadefx::pass_blend_factor::one: + return GPUPipeline::BlendFunc::One; + case reshadefx::pass_blend_factor::source_color: + return GPUPipeline::BlendFunc::SrcColor; + case reshadefx::pass_blend_factor::one_minus_source_color: + return GPUPipeline::BlendFunc::InvSrcColor; + case reshadefx::pass_blend_factor::dest_color: + return GPUPipeline::BlendFunc::DstColor; + case reshadefx::pass_blend_factor::one_minus_dest_color: + return GPUPipeline::BlendFunc::InvDstColor; + case reshadefx::pass_blend_factor::source_alpha: + return GPUPipeline::BlendFunc::SrcAlpha; + case reshadefx::pass_blend_factor::one_minus_source_alpha: + return GPUPipeline::BlendFunc::InvSrcAlpha; + case reshadefx::pass_blend_factor::dest_alpha: + default: + return GPUPipeline::BlendFunc::DstAlpha; + } + }; + + GPUPipeline::BlendState bs = GPUPipeline::BlendState::GetNoBlendingState(); + bs.enable = (pi.blend_enable[0] != 0); + bs.blend_op = map_blend_op(pi.blend_op[0]); + bs.src_blend = map_blend_factor(pi.src_blend[0]); + bs.dst_blend = map_blend_factor(pi.dest_blend[0]); + bs.alpha_blend_op = map_blend_op(pi.blend_op_alpha[0]); + bs.src_alpha_blend = map_blend_factor(pi.src_blend_alpha[0]); + bs.dst_alpha_blend = map_blend_factor(pi.dest_blend_alpha[0]); + bs.write_mask = pi.color_write_mask[0]; + return bs; +} + +static GPUPipeline::Primitive MapPrimitive(reshadefx::primitive_topology topology) +{ + switch (topology) + { + case reshadefx::primitive_topology::point_list: + return GPUPipeline::Primitive::Points; + case reshadefx::primitive_topology::line_list: + return GPUPipeline::Primitive::Lines; + case reshadefx::primitive_topology::line_strip: + Panic("Unhandled line strip"); + return GPUPipeline::Primitive::Lines; + case reshadefx::primitive_topology::triangle_list: + return GPUPipeline::Primitive::Triangles; + case reshadefx::primitive_topology::triangle_strip: + default: + return GPUPipeline::Primitive::TriangleStrips; + } +} + +PostProcessing::ReShadeFXShader::ReShadeFXShader() = default; + +PostProcessing::ReShadeFXShader::~ReShadeFXShader() = default; + +bool PostProcessing::ReShadeFXShader::LoadFromFile(std::string name, const char* filename, bool only_config, + Error* error) +{ + DebugAssert(only_config || g_gpu_device); + + m_filename = filename; + m_name = std::move(name); + + reshadefx::module temp_module; + if (!CreateModule(only_config ? DEFAULT_BUFFER_WIDTH : g_gpu_device->GetWindowWidth(), + only_config ? DEFAULT_BUFFER_HEIGHT : g_gpu_device->GetWindowHeight(), &temp_module, error)) + return false; + + if (!CreateOptions(temp_module, error)) + return false; + + if (!temp_module.techniques.empty()) + { + u32 max_rt = 0; + bool has_passes = false; + for (const reshadefx::technique_info& tech : temp_module.techniques) + { + for (const reshadefx::pass_info& pi : tech.passes) + { + has_passes = true; + + for (u32 i = 0; i < std::size(pi.render_target_names); i++) + { + if (pi.render_target_names[i].empty()) + break; + + max_rt = std::max(max_rt, i); + } + + if (pi.samplers.size() > GPUDevice::MAX_TEXTURE_SAMPLERS) + { + Error::SetString(error, fmt::format("Too many samplers ({}) in pass {}, only {} are supported.", + pi.samplers.size(), pi.name, GPUDevice::MAX_TEXTURE_SAMPLERS)); + return false; + } + } + } + if (!has_passes) + { + Error::SetString(error, "No passes defined in file."); + return false; + } + else if (max_rt > 0) + { + Error::SetString(error, "Shaders with multiple render targets are currently not supported."); + return false; + } + } + + // Might go invalid when creating pipelines. + m_valid = true; + return true; +} + +bool PostProcessing::ReShadeFXShader::IsValid() const +{ + return m_valid; +} + +bool PostProcessing::ReShadeFXShader::CreateModule(s32 buffer_width, s32 buffer_height, reshadefx::module* mod, + Error* error) +{ + reshadefx::preprocessor pp; + pp.add_include_path(std::filesystem::path(Path::GetDirectory(m_filename))); + pp.add_include_path(std::filesystem::path(Path::Combine( + EmuFolders::Resources, "shaders" FS_OSPATH_SEPARATOR_STR "reshade" FS_OSPATH_SEPARATOR_STR "Shaders"))); + pp.add_macro_definition("__RESHADE__", "50901"); + pp.add_macro_definition("BUFFER_WIDTH", std::to_string(buffer_width)); // TODO: can we make these uniforms? + pp.add_macro_definition("BUFFER_HEIGHT", std::to_string(buffer_height)); + pp.add_macro_definition("BUFFER_RCP_WIDTH", fmt::format("({}.0 / BUFFER_WIDTH)", buffer_width)); + pp.add_macro_definition("BUFFER_RCP_HEIGHT", fmt::format("({}.0 / BUFFER_HEIGHT)", buffer_height)); + + switch (GetRenderAPI()) + { + case RenderAPI::D3D11: + case RenderAPI::D3D12: + pp.add_macro_definition("__RENDERER__", "0x0B000"); + break; + + case RenderAPI::OpenGL: + case RenderAPI::OpenGLES: + case RenderAPI::Vulkan: + case RenderAPI::Metal: + pp.add_macro_definition("__RENDERER__", "0x14300"); + break; + + default: + UnreachableCode(); + break; + } + + if (!pp.append_file(std::filesystem::path(m_filename))) + { + Error::SetString(error, fmt::format("Failed to preprocess:\n{}", pp.errors())); + return false; + } + + std::unique_ptr cg = CreateRFXCodegen(); + if (!cg) + return false; + + reshadefx::parser parser; + if (!parser.parse(pp.output(), cg.get())) + { + Error::SetString(error, fmt::format("Failed to parse:\n{}", parser.errors())); + return false; + } + + cg->write_result(*mod); + + // FileSystem::WriteBinaryFile("D:\\out.txt", mod->code.data(), mod->code.size()); + return true; +} + +static std::string_view GetStringAnnotationValue(const std::vector& annotations, + const std::string_view& annotation_name, + const std::string_view& default_value) +{ + for (const reshadefx::annotation& an : annotations) + { + if (an.name != annotation_name) + continue; + + if (an.type.base != reshadefx::type::t_string) + continue; + + return an.value.string_data; + } + + return default_value; +} + +static bool GetBooleanAnnotationValue(const std::vector& annotations, + const std::string_view& annotation_name, bool default_value) +{ + for (const reshadefx::annotation& an : annotations) + { + if (an.name != annotation_name) + continue; + + if (an.type.base != reshadefx::type::t_bool) + continue; + + return (an.value.as_int[0] != 0); + } + + return default_value; +} + +static PostProcessing::ShaderOption::ValueVector +GetVectorAnnotationValue(const reshadefx::uniform_info& uniform, const std::string_view& annotation_name, + const PostProcessing::ShaderOption::ValueVector& default_value) +{ + PostProcessing::ShaderOption::ValueVector vv = default_value; + for (const reshadefx::annotation& an : uniform.annotations) + { + if (an.name != annotation_name) + continue; + + const u32 components = std::min(an.type.components(), PostProcessing::ShaderOption::MAX_VECTOR_COMPONENTS); + + if (an.type.base == uniform.type.base) + { + if (components > 0) + std::memcpy(&vv[0].float_value, &an.value.as_float[0], sizeof(float) * components); + + break; + } + else if (an.type.base == reshadefx::type::t_string) + { + // Convert from string. + if (uniform.type.base == reshadefx::type::t_float) + { + if (an.value.string_data == "BUFFER_WIDTH") + vv[0].float_value = DEFAULT_BUFFER_WIDTH; + else if (an.value.string_data == "BUFFER_HEIGHT") + vv[0].float_value = DEFAULT_BUFFER_HEIGHT; + else + vv[0].float_value = StringUtil::FromChars(an.value.string_data).value_or(1000.0f); + } + else if (uniform.type.base == reshadefx::type::t_int) + { + if (an.value.string_data == "BUFFER_WIDTH") + vv[0].int_value = DEFAULT_BUFFER_WIDTH; + else if (an.value.string_data == "BUFFER_HEIGHT") + vv[0].int_value = DEFAULT_BUFFER_HEIGHT; + else + vv[0].int_value = StringUtil::FromChars(an.value.string_data).value_or(1000); + } + else + { + Log_ErrorPrint(fmt::format("Unhandled string value for '{}' (annotation type: {}, uniform type {})", + uniform.name, an.type.description(), uniform.type.description()) + .c_str()); + } + + break; + } + else if (an.type.base == reshadefx::type::t_int) + { + // Convert from int. + if (uniform.type.base == reshadefx::type::t_float) + { + for (u32 i = 0; i < components; i++) + vv[i].float_value = static_cast(an.value.as_int[i]); + } + else if (uniform.type.base == reshadefx::type::t_bool) + { + for (u32 i = 0; i < components; i++) + vv[i].int_value = (an.value.as_int[i] != 0) ? 1 : 0; + } + } + else if (an.type.base == reshadefx::type::t_float) + { + // Convert from float. + if (uniform.type.base == reshadefx::type::t_int) + { + for (u32 i = 0; i < components; i++) + vv[i].int_value = static_cast(an.value.as_float[i]); + } + else if (uniform.type.base == reshadefx::type::t_bool) + { + for (u32 i = 0; i < components; i++) + vv[i].int_value = (an.value.as_float[i] != 0.0f) ? 1 : 0; + } + } + + break; + } + + return vv; +} + +bool PostProcessing::ReShadeFXShader::CreateOptions(const reshadefx::module& mod, Error* error) +{ + for (const reshadefx::uniform_info& ui : mod.uniforms) + { + SourceOptionType so; + if (!GetSourceOption(ui, &so, error)) + return false; + if (so != SourceOptionType::None) + { + Log_DevPrintf("Add source based option %u at offset %u (%s)", static_cast(so), ui.offset, ui.name.c_str()); + + SourceOption sopt; + sopt.source = so; + sopt.offset = ui.offset; + + const ShaderOption::ValueVector min = + GetVectorAnnotationValue(ui, "min", ShaderOption::MakeFloatVector(0, 0, 0, 0)); + const ShaderOption::ValueVector max = + GetVectorAnnotationValue(ui, "max", ShaderOption::MakeFloatVector(1, 1, 1, 1)); + const ShaderOption::ValueVector smoothing = + GetVectorAnnotationValue(ui, "smoothing", ShaderOption::MakeFloatVector(0)); + const ShaderOption::ValueVector step = + GetVectorAnnotationValue(ui, "step", ShaderOption::MakeFloatVector(0, 1, 0, 0)); + + sopt.min = min[0].float_value; + sopt.max = max[0].float_value; + sopt.smoothing = smoothing[0].float_value; + std::memcpy(&sopt.step[0], &step[0].float_value, sizeof(sopt.value)); + std::memcpy(&sopt.value[0], &ui.initializer_value.as_float[0], sizeof(sopt.value)); + + m_source_options.push_back(std::move(sopt)); + continue; + } + + ShaderOption opt; + opt.name = ui.name; + + if (!GetBooleanAnnotationValue(ui.annotations, "hidden", false)) + { + opt.ui_name = GetStringAnnotationValue(ui.annotations, "ui_label", std::string_view()); + if (opt.ui_name.empty()) + opt.ui_name = ui.name; + } + + // const std::string_view ui_type = GetStringAnnotationValue(ui.annotations, "ui_type", std::string_view(); + + switch (ui.type.base) + { + case reshadefx::type::t_float: + opt.type = ShaderOption::Type::Float; + break; + + case reshadefx::type::t_int: + case reshadefx::type::t_uint: + opt.type = ShaderOption::Type::Int; + break; + + case reshadefx::type::t_bool: + opt.type = ShaderOption::Type::Bool; + break; + + default: + Error::SetString(error, fmt::format("Unhandled uniform type {} ({})", static_cast(ui.type.base), ui.name)); + return false; + } + + opt.buffer_offset = ui.offset; + opt.buffer_size = ui.size; + opt.vector_size = ui.type.components(); + if (opt.vector_size == 0 || opt.vector_size > ShaderOption::MAX_VECTOR_COMPONENTS) + { + Error::SetString(error, + fmt::format("Unhandled vector size {} ({})", static_cast(ui.type.components()), ui.name)); + return false; + } + + opt.min_value = GetVectorAnnotationValue(ui, "ui_min", {}); + opt.max_value = GetVectorAnnotationValue(ui, "ui_max", {}); + ShaderOption::ValueVector default_step = {}; + switch (opt.type) + { + case ShaderOption::Type::Float: + { + for (u32 i = 0; i < opt.vector_size; i++) + { + const float range = opt.max_value[i].float_value - opt.min_value[i].float_value; + default_step[i].float_value = range / 100.0f; + } + } + break; + + case ShaderOption::Type::Int: + { + for (u32 i = 0; i < opt.vector_size; i++) + { + const s32 range = opt.max_value[i].int_value - opt.min_value[i].int_value; + default_step[i].int_value = std::max(range / 100, 1); + } + } + break; + + default: + break; + } + opt.step_value = GetVectorAnnotationValue(ui, "ui_step", default_step); + + if (ui.has_initializer_value) + { + std::memcpy(&opt.default_value[0].float_value, &ui.initializer_value.as_float[0], + sizeof(float) * opt.vector_size); + } + else + { + opt.default_value = {}; + } + + // Assume default if user doesn't set it. + opt.value = opt.default_value; + + m_options.push_back(std::move(opt)); + } + + m_uniforms_size = mod.total_uniform_size; + Log_DevPrintf("%s: %zu options", m_filename.c_str(), m_options.size()); + return true; +} + +bool PostProcessing::ReShadeFXShader::GetSourceOption(const reshadefx::uniform_info& ui, SourceOptionType* si, + Error* error) +{ + const std::string_view source = GetStringAnnotationValue(ui.annotations, "source", {}); + if (!source.empty()) + { + if (source == "timer") + { + if (ui.type.base != reshadefx::type::t_float || ui.type.components() > 1) + { + Error::SetString( + error, fmt::format("Unexpected type '{}' for timer source in uniform '{}'", ui.type.description(), ui.name)); + return false; + } + + *si = SourceOptionType::Timer; + return true; + } + else if (source == "framecount") + { + if ((!ui.type.is_integral() && !ui.type.is_floating_point()) || ui.type.components() > 1) + { + Error::SetString( + error, fmt::format("Unexpected type '{}' for timer source in uniform '{}'", ui.type.description(), ui.name)); + return false; + } + + *si = (ui.type.base == reshadefx::type::t_float) ? SourceOptionType::FrameCountF : SourceOptionType::FrameCount; + return true; + } + else if (source == "frametime") + { + if (ui.type.base != reshadefx::type::t_float || ui.type.components() > 1) + { + Error::SetString( + error, fmt::format("Unexpected type '{}' for timer source in uniform '{}'", ui.type.description(), ui.name)); + return false; + } + + *si = SourceOptionType::FrameTime; + return true; + } + else if (source == "pingpong") + { + if (!ui.type.is_floating_point() || ui.type.components() < 2) + { + Error::SetString(error, fmt::format("Unexpected type '{}' for pingpong source in uniform '{}'", + ui.type.description(), ui.name)); + return false; + } + + *si = SourceOptionType::PingPong; + return true; + } + else if (source == "mousepoint") + { + if (!ui.type.is_floating_point() || ui.type.components() < 2) + { + Error::SetString(error, fmt::format("Unexpected type '{}' for mousepoint source in uniform '{}'", + ui.type.description(), ui.name)); + return false; + } + + *si = SourceOptionType::MousePoint; + return true; + } + else if (source == "overlay_active" || source == "has_depth") + { + *si = SourceOptionType::Zero; + return true; + } + else if (source == "bufferwidth") + { + *si = (ui.type.base == reshadefx::type::t_float) ? SourceOptionType::BufferWidthF : SourceOptionType::BufferWidth; + return true; + } + else if (source == "bufferheight") + { + *si = + (ui.type.base == reshadefx::type::t_float) ? SourceOptionType::BufferHeightF : SourceOptionType::BufferHeight; + return true; + } + else if (source == "internalwidth") + { + *si = + (ui.type.base == reshadefx::type::t_float) ? SourceOptionType::InternalWidthF : SourceOptionType::InternalWidth; + return true; + } + else if (source == "internalheight") + { + *si = (ui.type.base == reshadefx::type::t_float) ? SourceOptionType::InternalHeightF : + SourceOptionType::InternalHeight; + return true; + } + else + { + Error::SetString(error, fmt::format("Unknown source '{}' in uniform '{}'", source, ui.name)); + return false; + } + } + + if (ui.has_initializer_value) + { + if (ui.initializer_value.string_data == "BUFFER_WIDTH") + { + *si = (ui.type.base == reshadefx::type::t_float) ? SourceOptionType::BufferWidthF : SourceOptionType::BufferWidth; + return true; + } + else if (ui.initializer_value.string_data == "BUFFER_HEIGHT") + { + *si = + (ui.type.base == reshadefx::type::t_float) ? SourceOptionType::BufferHeightF : SourceOptionType::BufferHeight; + return true; + } + } + + *si = SourceOptionType::None; + return true; +} + +bool PostProcessing::ReShadeFXShader::CreatePasses(GPUTexture::Format backbuffer_format, reshadefx::module& mod, + Error* error) +{ + u32 total_passes = 0; + for (const reshadefx::technique_info& tech : mod.techniques) + total_passes += static_cast(tech.passes.size()); + if (total_passes == 0) + { + Error::SetString(error, "No passes defined."); + return false; + } + + m_passes.reserve(total_passes); + + // Named render targets. + for (const reshadefx::texture_info& ti : mod.textures) + { + Texture tex; + + if (!ti.semantic.empty()) + { + Log_DevPrint(fmt::format("Ignoring semantic {} texture {}", ti.semantic, ti.unique_name).c_str()); + continue; + } + if (ti.render_target) + { + tex.rt_scale = 1.0f; + tex.format = MapTextureFormat(ti.format); + Log_DevPrint( + fmt::format("Creating render target '{}' {}", ti.unique_name, GPUTexture::GetFormatName(tex.format)).c_str()); + } + else + { + const std::string_view source = GetStringAnnotationValue(ti.annotations, "source", {}); + if (source.empty()) + { + Error::SetString(error, fmt::format("Non-render target texture '{}' is missing source.", ti.unique_name)); + return false; + } + + const std::string image_path = + Path::Combine(EmuFolders::Shaders, Path::Combine("reshade" FS_OSPATH_SEPARATOR_STR "Textures", source)); + Common::RGBA8Image image; + if (!image.LoadFromFile(image_path.c_str())) + { + Error::SetString(error, fmt::format("Failed to load image '{}' (from '{}')", source, image_path).c_str()); + return false; + } + + tex.rt_scale = 0.0f; + tex.texture = g_gpu_device->CreateTexture(image.GetWidth(), image.GetHeight(), 1, 1, 1, GPUTexture::Type::Texture, + GPUTexture::Format::RGBA8, image.GetPixels(), image.GetPitch()); + if (!tex.texture) + { + Error::SetString( + error, fmt::format("Failed to create {}x{} texture ({})", image.GetWidth(), image.GetHeight(), source)); + return false; + } + + Log_DevPrint(fmt::format("Loaded {}x{} texture ({})", image.GetWidth(), image.GetHeight(), source).c_str()); + } + + tex.reshade_name = ti.unique_name; + m_textures.push_back(std::move(tex)); + } + + TextureID last_output = INPUT_COLOR_TEXTURE; + + for (reshadefx::technique_info& tech : mod.techniques) + { + for (reshadefx::pass_info& pi : tech.passes) + { + const bool is_final = (&tech == &mod.techniques.back() && &pi == &tech.passes.back()); + + Pass pass; + pass.num_vertices = pi.num_vertices; + + if (is_final) + { + pass.render_target = OUTPUT_COLOR_TEXTURE; + } + else if (!pi.render_target_names[0].empty()) + { + pass.render_target = static_cast(m_textures.size()); + for (u32 i = 0; i < static_cast(m_textures.size()); i++) + { + if (m_textures[i].reshade_name == pi.render_target_names[0]) + { + pass.render_target = static_cast(i); + break; + } + } + if (pass.render_target == static_cast(m_textures.size())) + { + Error::SetString(error, fmt::format("Unknown texture '{}' used as render target in pass '{}'", + pi.render_target_names[0], pi.name)); + return false; + } + } + else + { + Texture new_rt; + new_rt.rt_scale = 1.0f; + new_rt.format = backbuffer_format; + pass.render_target = static_cast(m_textures.size()); + m_textures.push_back(std::move(new_rt)); + } + + u32 texture_slot = 0; + for (const reshadefx::sampler_info& si : pi.samplers) + { + Sampler sampler; + sampler.slot = texture_slot++; + sampler.reshade_name = si.unique_name; + + sampler.texture_id = static_cast(m_textures.size()); + for (const reshadefx::texture_info& ti : mod.textures) + { + if (ti.unique_name == si.texture_name) + { + // found the texture, now look for our side of it + if (ti.semantic == "COLOR") + { + sampler.texture_id = INPUT_COLOR_TEXTURE; + break; + } + else if (ti.semantic == "DEPTH") + { + Log_WarningPrint( + fmt::format("Shader '{}' uses input depth as '{}' which is not supported.", m_name, si.texture_name) + .c_str()); + sampler.texture_id = INPUT_DEPTH_TEXTURE; + break; + } + else if (!ti.semantic.empty()) + { + Error::SetString(error, fmt::format("Unknown semantic {} in texture {}", ti.semantic, ti.name)); + return false; + } + + // must be a render target, or another texture + for (u32 i = 0; i < static_cast(m_textures.size()); i++) + { + if (m_textures[i].reshade_name == si.texture_name) + { + // hook it up + sampler.texture_id = static_cast(i); + break; + } + } + + break; + } + } + if (sampler.texture_id == static_cast(m_textures.size())) + { + Error::SetString( + error, fmt::format("Unknown texture {} (sampler {}) in pass {}", si.texture_name, si.name, pi.name)); + return false; + } + + Log_DevPrint(fmt::format("Pass {} Texture {} => {}", pi.name, si.texture_name, sampler.texture_id).c_str()); + + sampler.sampler = GetSampler(MapSampler(si)); + if (!sampler.sampler) + { + Error::SetString(error, "Failed to create sampler."); + return false; + } + + pass.samplers.push_back(std::move(sampler)); + } + +#ifdef _DEBUG + pass.name = std::move(pi.name); +#endif + last_output = pass.render_target; + m_passes.push_back(std::move(pass)); + } + } + + return true; +} + +const char* PostProcessing::ReShadeFXShader::GetTextureNameForID(TextureID id) const +{ + if (id == INPUT_COLOR_TEXTURE) + return "Input Color Texture / Backbuffer"; + else if (id == INPUT_DEPTH_TEXTURE) + return "Input Depth Texture"; + else if (id == OUTPUT_COLOR_TEXTURE) + return "Output Color Texture"; + else if (id < 0 || static_cast(id) >= m_textures.size()) + return "UNKNOWN"; + else + return m_textures[static_cast(id)].reshade_name.c_str(); +} + +GPUTexture* PostProcessing::ReShadeFXShader::GetTextureByID(TextureID id, GPUTexture* input, + GPUFramebuffer* final_target) const +{ + if (id < 0) + { + if (id == INPUT_COLOR_TEXTURE) + { + return input; + } + else if (id == INPUT_DEPTH_TEXTURE) + { + return PostProcessing::GetDummyTexture(); + } + else if (id == OUTPUT_COLOR_TEXTURE) + { + Panic("Wrong state for final target"); + return nullptr; + } + else + { + Panic("Unexpected reserved texture ID"); + return nullptr; + } + } + + if (static_cast(id) >= m_textures.size()) + Panic("Unexpected texture ID"); + + return m_textures[static_cast(id)].texture.get(); +} + +GPUFramebuffer* PostProcessing::ReShadeFXShader::GetFramebufferByID(TextureID id, GPUTexture* input, + GPUFramebuffer* final_target) const +{ + if (id < 0) + { + if (id == OUTPUT_COLOR_TEXTURE) + { + return final_target; + } + else + { + Panic("Unexpected reserved texture ID"); + return nullptr; + } + } + + if (static_cast(id) >= m_textures.size()) + Panic("Unexpected texture ID"); + + const Texture& tex = m_textures[static_cast(id)]; + Assert(tex.framebuffer); + return tex.framebuffer.get(); +} + +bool PostProcessing::ReShadeFXShader::CompilePipeline(GPUTexture::Format format, u32 width, u32 height) +{ + const RenderAPI api = g_gpu_device->GetRenderAPI(); + const bool needs_main_defn = (api != RenderAPI::D3D11 && api != RenderAPI::D3D12); + + if (api != RenderAPI::D3D11) + { + Log_ErrorPrintf("ReShade is currently only supported on Direct3D 11 (due to bugs)"); + return false; + } + + m_valid = false; + m_textures.clear(); + m_passes.clear(); + + Error error; + reshadefx::module mod; + if (!CreateModule(width, height, &mod, &error)) + { + Log_ErrorPrintf("Failed to create module for '%s': %s", m_name.c_str(), error.GetDescription().c_str()); + return false; + } + + DebugAssert(!mod.techniques.empty()); + + if (!CreatePasses(format, mod, &error)) + { + Log_ErrorPrintf("Failed to create passes for '%s': %s", m_name.c_str(), error.GetDescription().c_str()); + return false; + } + + const std::string_view code(mod.code.data(), mod.code.size()); + + auto get_shader = [this, needs_main_defn, &code](const std::string& name, const std::vector& samplers, + GPUShaderStage stage) { + std::string real_code; + if (needs_main_defn) + real_code = fmt::format("#version 460 core\n#define ENTRY_POINT_{}\n{}", name, code); + else + real_code = std::string(code); + + for (const Sampler& sampler : samplers) + { + std::string decl = fmt::format("__{}_t : register( t0);", sampler.reshade_name); + std::string replacement = + fmt::format("__{}_t : register({}t{});", sampler.reshade_name, (sampler.slot < 10) ? " " : "", sampler.slot); + StringUtil::ReplaceAll(&real_code, decl, replacement); + + decl = fmt::format("__{}_s : register( s0);", sampler.reshade_name); + replacement = + fmt::format("__{}_s : register({}s{});", sampler.reshade_name, (sampler.slot < 10) ? " " : "", sampler.slot); + StringUtil::ReplaceAll(&real_code, decl, replacement); + } + + // FileSystem::WriteStringToFile("D:\\foo.txt", real_code); + + std::unique_ptr sshader = + g_gpu_device->CreateShader(stage, real_code, needs_main_defn ? "main" : name.c_str()); + if (!sshader) + Log_ErrorPrintf("Failed to compile function '%s'", name.c_str()); + + return sshader; + }; + + GPUPipeline::GraphicsConfig plconfig; + plconfig.layout = GPUPipeline::Layout::MultiTextureAndUBO; + plconfig.primitive = GPUPipeline::Primitive::Triangles; + plconfig.depth_format = GPUTexture::Format::Unknown; + plconfig.rasterization = GPUPipeline::RasterizationState::GetNoCullState(); + plconfig.depth = GPUPipeline::DepthState::GetNoTestsState(); + plconfig.blend = GPUPipeline::BlendState::GetNoBlendingState(); + plconfig.samples = 1; + plconfig.per_sample_shading = false; + + u32 passnum = 0; + for (const reshadefx::technique_info& tech : mod.techniques) + { + for (const reshadefx::pass_info& info : tech.passes) + { + DebugAssert(passnum < m_passes.size()); + Pass& pass = m_passes[passnum++]; + + auto vs = get_shader(info.vs_entry_point, pass.samplers, GPUShaderStage::Vertex); + auto fs = get_shader(info.ps_entry_point, pass.samplers, GPUShaderStage::Fragment); + if (!vs || !fs) + return false; + + plconfig.color_format = (pass.render_target >= 0) ? m_textures[pass.render_target].format : format; + plconfig.blend = MapBlendState(info); + plconfig.primitive = MapPrimitive(info.topology); + plconfig.vertex_shader = vs.get(); + plconfig.fragment_shader = fs.get(); + if (!plconfig.vertex_shader || !plconfig.fragment_shader) + return false; + + pass.pipeline = g_gpu_device->CreatePipeline(plconfig); + if (!pass.pipeline) + { + Log_ErrorPrintf("Failed to create pipeline for pass '%s'", info.name.c_str()); + return false; + } + } + } + + m_valid = true; + return true; +} + +bool PostProcessing::ReShadeFXShader::ResizeOutput(GPUTexture::Format format, u32 width, u32 height) +{ + m_valid = false; + + for (Texture& tex : m_textures) + { + if (tex.rt_scale == 0.0f) + continue; + + tex.framebuffer.reset(); + tex.texture.reset(); + + const u32 t_width = std::max(static_cast(static_cast(width) * tex.rt_scale), 1u); + const u32 t_height = std::max(static_cast(static_cast(height) * tex.rt_scale), 1u); + tex.texture = g_gpu_device->CreateTexture(t_width, t_height, 1, 1, 1, GPUTexture::Type::RenderTarget, tex.format); + if (!tex.texture) + { + Log_ErrorPrintf("Failed to create %ux%u texture", t_width, t_height); + return {}; + } + + tex.framebuffer = g_gpu_device->CreateFramebuffer(tex.texture.get()); + if (!tex.framebuffer) + { + Log_ErrorPrintf("Failed to create %ux%u texture framebuffer", t_width, t_height); + return {}; + } + } + + m_valid = true; + return true; +} + +bool PostProcessing::ReShadeFXShader::Apply(GPUTexture* input, GPUFramebuffer* final_target, s32 final_left, + s32 final_top, s32 final_width, s32 final_height, s32 orig_width, + s32 orig_height, u32 target_width, u32 target_height) +{ + GL_PUSH("PostProcessingShaderFX %s", m_name.c_str()); + + m_frame_count++; + + // Reshade always draws at full size. + g_gpu_device->SetViewportAndScissor(0, 0, target_width, target_height); + + if (m_uniforms_size > 0) + { + GL_SCOPE("Uniforms: %u bytes", m_uniforms_size); + + u8* uniforms = static_cast(g_gpu_device->MapUniformBuffer(m_uniforms_size)); + for (const ShaderOption& opt : m_options) + { + DebugAssert((opt.buffer_offset + opt.buffer_size) <= m_uniforms_size); + std::memcpy(uniforms + opt.buffer_offset, &opt.value[0].float_value, opt.buffer_size); + } + for (const SourceOption& so : m_source_options) + { + u8* dst = uniforms + so.offset; + switch (so.source) + { + case SourceOptionType::Zero: + { + const u32 value = 0; + std::memcpy(dst, &value, sizeof(value)); + } + break; + + case SourceOptionType::Timer: + { + const float value = static_cast(PostProcessing::GetTimer().GetTimeMilliseconds()); + std::memcpy(dst, &value, sizeof(value)); + } + break; + + case SourceOptionType::FrameTime: + { + const float value = static_cast(m_frame_timer.GetTimeMilliseconds()); + std::memcpy(dst, &value, sizeof(value)); + } + break; + + case SourceOptionType::FrameCount: + { + std::memcpy(dst, &m_frame_count, sizeof(m_frame_count)); + } + break; + + case SourceOptionType::FrameCountF: + { + const float value = static_cast(m_frame_count); + std::memcpy(dst, &value, sizeof(value)); + } + break; + + case SourceOptionType::PingPong: + { + float increment = so.step[1] == 0 ? + so.step[0] : + (so.step[0] + std::fmod(static_cast(std::rand()), so.step[1] - so.step[0] + 1)); + + std::array value = {so.value[0].float_value, so.value[1].float_value}; + if (value[1] >= 0) + { + increment = std::max(increment - std::max(0.0f, so.smoothing - (so.max - value[0])), 0.05f); + increment *= static_cast(m_frame_timer.GetTimeMilliseconds() * 1e-9); + + if ((value[0] += increment) >= so.max) + { + value[0] = so.max; + value[1] = -1; + } + } + else + { + increment = std::max(increment - std::max(0.0f, so.smoothing - (value[0] - so.min)), 0.05f); + increment *= static_cast(m_frame_timer.GetTimeMilliseconds() * 1e-9); + + if ((value[0] -= increment) <= so.min) + { + value[0] = so.min; + value[1] = +1; + } + } + + std::memcpy(dst, value.data(), sizeof(value)); + } + break; + + case SourceOptionType::MousePoint: + { + const std::pair mpos = InputManager::GetPointerAbsolutePosition(0); + std::memcpy(dst, &mpos.first, sizeof(float)); + std::memcpy(dst + sizeof(float), &mpos.second, sizeof(float)); + } + break; + + case SourceOptionType::BufferWidth: + case SourceOptionType::BufferHeight: + { + const s32 value = (so.source == SourceOptionType::BufferWidth) ? static_cast(target_width) : + static_cast(target_height); + std::memcpy(dst, &value, sizeof(value)); + } + break; + + case SourceOptionType::BufferWidthF: + case SourceOptionType::BufferHeightF: + { + const float value = (so.source == SourceOptionType::BufferWidthF) ? static_cast(target_width) : + static_cast(target_height); + std::memcpy(dst, &value, sizeof(value)); + } + break; + + case SourceOptionType::InternalWidth: + case SourceOptionType::InternalHeight: + { + const s32 value = + (so.source == SourceOptionType::BufferWidth) ? static_cast(orig_width) : static_cast(orig_height); + std::memcpy(dst, &value, sizeof(value)); + } + break; + + case SourceOptionType::InternalWidthF: + case SourceOptionType::InternalHeightF: + { + const float value = (so.source == SourceOptionType::BufferWidthF) ? static_cast(orig_width) : + static_cast(orig_height); + std::memcpy(dst, &value, sizeof(value)); + } + break; + + default: + UnreachableCode(); + break; + } + } + g_gpu_device->UnmapUniformBuffer(m_uniforms_size); + } + + for (const Pass& pass : m_passes) + { + GL_SCOPE("Draw pass %s", pass.name.c_str()); + + GL_INS("Render Target: ID %d [%s]", pass.render_target, GetTextureNameForID(pass.render_target)); + GPUFramebuffer* output_fb = GetFramebufferByID(pass.render_target, input, final_target); + g_gpu_device->SetFramebuffer(output_fb); + g_gpu_device->SetPipeline(pass.pipeline.get()); + + // Set all inputs first, before the render pass starts. + for (const Sampler& sampler : pass.samplers) + { + GL_INS("Texture Sampler %u: ID %d [%s]", sampler.slot, sampler.texture_id, + GetTextureNameForID(sampler.texture_id)); + g_gpu_device->SetTextureSampler(sampler.slot, GetTextureByID(sampler.texture_id, input, final_target), + sampler.sampler); + } + + if (!output_fb) + { + // Drawing to final buffer. + if (!g_gpu_device->BeginPresent(false)) + return false; + } + + g_gpu_device->Draw(pass.num_vertices, 0); + } + + m_frame_timer.Reset(); + return true; +} diff --git a/src/util/postprocessing_shader_fx.h b/src/util/postprocessing_shader_fx.h new file mode 100644 index 0000000000..cbbd8f06fa --- /dev/null +++ b/src/util/postprocessing_shader_fx.h @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once + +#include "postprocessing_shader.h" + +#include "common/timer.h" + +// reshadefx +#include "effect_module.hpp" + +class Error; + +namespace PostProcessing { + +class ReShadeFXShader final : public Shader +{ +public: + ReShadeFXShader(); + ~ReShadeFXShader(); + + bool IsValid() const override; + + bool LoadFromFile(std::string name, const char* filename, bool only_config, Error* error); + + bool ResizeOutput(GPUTexture::Format format, u32 width, u32 height) override; + bool CompilePipeline(GPUTexture::Format format, u32 width, u32 height) override; + bool Apply(GPUTexture* input, GPUFramebuffer* final_target, s32 final_left, s32 final_top, s32 final_width, + s32 final_height, s32 orig_width, s32 orig_height, u32 target_width, u32 target_height) override; + +private: + using TextureID = s32; + + static constexpr TextureID INPUT_COLOR_TEXTURE = -1; + static constexpr TextureID INPUT_DEPTH_TEXTURE = -2; + static constexpr TextureID OUTPUT_COLOR_TEXTURE = -3; + + enum class SourceOptionType + { + None, + Zero, + Timer, + FrameTime, + FrameCount, + FrameCountF, + PingPong, + MousePoint, + BufferWidth, + BufferHeight, + BufferWidthF, + BufferHeightF, + InternalWidth, + InternalHeight, + InternalWidthF, + InternalHeightF, + + MaxCount + }; + + struct SourceOption + { + SourceOptionType source; + u32 offset; + float min; + float max; + float smoothing; + std::array step; + ShaderOption::ValueVector value; + }; + + bool CreateModule(s32 buffer_width, s32 buffer_height, reshadefx::module* mod, Error* error); + bool CreateOptions(const reshadefx::module& mod, Error* error); + bool GetSourceOption(const reshadefx::uniform_info& ui, SourceOptionType* si, Error* error); + bool CreatePasses(GPUTexture::Format backbuffer_format, reshadefx::module& mod, Error* error); + + const char* GetTextureNameForID(TextureID id) const; + GPUTexture* GetTextureByID(TextureID id, GPUTexture* input, GPUFramebuffer* final_target) const; + GPUFramebuffer* GetFramebufferByID(TextureID id, GPUTexture* input, GPUFramebuffer* final_target) const; + + std::string m_filename; + + struct Texture + { + std::unique_ptr texture; + std::unique_ptr framebuffer; + std::string reshade_name; // TODO: we might be able to drop this + GPUTexture::Format format; + float rt_scale; + }; + + struct Sampler + { + u32 slot; + TextureID texture_id; + std::string reshade_name; + GPUSampler* sampler; + }; + + struct Pass + { + std::unique_ptr pipeline; + TextureID render_target; + std::vector samplers; + u32 num_vertices; + +#ifdef _DEBUG + std::string name; +#endif + }; + + std::vector m_passes; + std::vector m_textures; + std::vector m_source_options; + u32 m_uniforms_size = 0; + bool m_valid = false; + + Common::Timer m_frame_timer; + u32 m_frame_count = 0; +}; + +} // namespace PostProcessing \ No newline at end of file diff --git a/src/util/util.vcxproj b/src/util/util.vcxproj index d2c1a6f2c3..a6c0bf5ec0 100644 --- a/src/util/util.vcxproj +++ b/src/util/util.vcxproj @@ -64,6 +64,7 @@ + @@ -164,6 +165,7 @@ + @@ -215,6 +217,9 @@ {425d6c99-d1c8-43c2-b8ac-4d7b1d941017} + + {27b8d4bb-4f01-4432-bc14-9bf6ca458eee} + {751d9f62-881c-454e-bce8-cb9cf5f1d22f} @@ -238,8 +243,9 @@ + %(AdditionalIncludeDirectories);$(SolutionDir)dep\reshadefx\include $(IntDir)/%(RelativeDir)/ - + \ No newline at end of file diff --git a/src/util/util.vcxproj.filters b/src/util/util.vcxproj.filters index b0e38a0283..9e02354e78 100644 --- a/src/util/util.vcxproj.filters +++ b/src/util/util.vcxproj.filters @@ -69,6 +69,7 @@ + @@ -144,10 +145,11 @@ + {e637fc5b-2483-4a31-abc3-89a16d45c223} - + \ No newline at end of file