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