From f9a58bd8e00b4ed25406173878fd8be76da7ebed Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Thu, 5 Nov 2026 12:15:57 -0500 Subject: [PATCH] Depth fade Acroarts billboards --- MarathonRecomp/CMakeLists.txt | 1 + MarathonRecomp/gpu/guest_shader.cpp | 8 ++ .../shader/hlsl/particle_depth_fade_ps.hlsl | 44 +++++++ .../shader/msl/particle_depth_fade_ps.metal | 34 +++++ MarathonRecomp/gpu/video.cpp | 124 +++++++++++++++++- MarathonRecomp/locale/config_locale.cpp | 56 ++++++++ MarathonRecomp/ui/options_menu.cpp | 1 + MarathonRecomp/user/config.cpp | 6 + MarathonRecomp/user/config.h | 6 + MarathonRecomp/user/config_def.h | 1 + 10 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 MarathonRecomp/gpu/shader/hlsl/particle_depth_fade_ps.hlsl create mode 100644 MarathonRecomp/gpu/shader/msl/particle_depth_fade_ps.metal diff --git a/MarathonRecomp/CMakeLists.txt b/MarathonRecomp/CMakeLists.txt index 34ed11a2f..f47544e2c 100644 --- a/MarathonRecomp/CMakeLists.txt +++ b/MarathonRecomp/CMakeLists.txt @@ -493,6 +493,7 @@ compile_pixel_shader(copy_depth_ps) compile_pixel_shader(downsample_ps) compile_vertex_shader(enhanced_burnout_blur_vs) compile_pixel_shader(enhanced_burnout_blur_ps) +compile_pixel_shader(particle_depth_fade_ps) compile_pixel_shader(movie_ps) compile_pixel_shader(gamma_correction_ps) compile_pixel_shader(imgui_ps) diff --git a/MarathonRecomp/gpu/guest_shader.cpp b/MarathonRecomp/gpu/guest_shader.cpp index bb65d1ebb..e18277b48 100644 --- a/MarathonRecomp/gpu/guest_shader.cpp +++ b/MarathonRecomp/gpu/guest_shader.cpp @@ -19,6 +19,8 @@ static std::unique_ptr g_downsamplePSShader; std::unique_ptr g_conditionalSurveyPSShader; +static std::unique_ptr g_particleDepthFadePSShader; + void LoadEmbeddedResources() { switch (g_backend) @@ -54,6 +56,8 @@ void CreateReplacementShaders() g_conditionalSurveyPSShader = std::make_unique(); g_conditionalSurveyPSShader->type = ResourceType::PixelShader; g_conditionalSurveyPSShader->shader = CREATE_SHADER(conditional_survey_ps); + + g_particleDepthFadePSShader = CREATE_SHADER(particle_depth_fade_ps); } RenderShader* GetReplacementShader(const GuestShader* guestShader) @@ -79,6 +83,10 @@ RenderShader* GetReplacementShader(const GuestShader* guestShader) return g_MoviePSShader.get(); case 0x62573AB4C9EC44EE: return g_downsamplePSShader.get(); + case 0x402F6C3CD7D5B52E: + if (Config::ParticleFade == EParticleFade::Soft) + return g_particleDepthFadePSShader.get(); + break; default: break; } diff --git a/MarathonRecomp/gpu/shader/hlsl/particle_depth_fade_ps.hlsl b/MarathonRecomp/gpu/shader/hlsl/particle_depth_fade_ps.hlsl new file mode 100644 index 000000000..17033c259 --- /dev/null +++ b/MarathonRecomp/gpu/shader/hlsl/particle_depth_fade_ps.hlsl @@ -0,0 +1,44 @@ +#include "../../../../tools/XenosRecomp/XenosRecomp/shader_common.h" + +#ifdef __spirv__ + +#define s0_Texture2DDescriptorIndex vk::RawBufferLoad(g_PushConstants.SharedConstants + 0) +#define s0_SamplerDescriptorIndex vk::RawBufferLoad(g_PushConstants.SharedConstants + 192) +#define g_DepthFadeParams vk::RawBufferLoad(g_PushConstants.SharedConstants + 320) +#define g_DepthFadeTextureIndex vk::RawBufferLoad(g_PushConstants.SharedConstants + 336) + +#else + +cbuffer SharedConstants : register(b2, space4) +{ + uint s0_Texture2DDescriptorIndex : packoffset(c0.x); + uint s0_SamplerDescriptorIndex : packoffset(c12.x); + DEFINE_SHARED_CONSTANTS(); + float4 g_DepthFadeParams : packoffset(c20); + uint g_DepthFadeTextureIndex : packoffset(c21.x); +}; + +#endif + +struct Interpolators +{ + float4 iPos : SV_Position; + float4 iTexCoord0 : TEXCOORD0; + float4 iTexCoord1 : TEXCOORD1; +}; + +float4 shaderMain(Interpolators input) : SV_Target0 +{ + float4 color = tfetch2D(s0_Texture2DDescriptorIndex, s0_SamplerDescriptorIndex, input.iTexCoord1.xy, float2(0, 0)) * input.iTexCoord0; + + float4 depthFadeParams = g_DepthFadeParams; + float sceneDepth = g_Texture2DDescriptorHeap[g_DepthFadeTextureIndex].Load(int3(input.iPos.xy, 0)).x; + float sceneZ = depthFadeParams.y / (sceneDepth - depthFadeParams.x); + float fragmentZ = depthFadeParams.y / (input.iPos.z - depthFadeParams.x); + // https://developer.download.nvidia.com/whitepapers/2007/SDK10/SoftParticles_hi.pdf + float d = saturate((sceneZ - fragmentZ) * depthFadeParams.z); + float c = 0.5 * pow(saturate(2.0 * (d > 0.5 ? 1.0 - d : d)), 2.0); + color.w *= d > 0.5 ? 1.0 - c : c; + + return color; +} diff --git a/MarathonRecomp/gpu/shader/msl/particle_depth_fade_ps.metal b/MarathonRecomp/gpu/shader/msl/particle_depth_fade_ps.metal new file mode 100644 index 000000000..d0c896d9f --- /dev/null +++ b/MarathonRecomp/gpu/shader/msl/particle_depth_fade_ps.metal @@ -0,0 +1,34 @@ +#include "../../../../tools/XenosRecomp/XenosRecomp/shader_common.h" + +#define s0_Texture2DDescriptorIndex (*(reinterpret_cast(g_PushConstants.SharedConstants + 0))) +#define s0_SamplerDescriptorIndex (*(reinterpret_cast(g_PushConstants.SharedConstants + 192))) +#define g_DepthFadeParams (*(reinterpret_cast(g_PushConstants.SharedConstants + 320))) +#define g_DepthFadeTextureIndex (*(reinterpret_cast(g_PushConstants.SharedConstants + 336))) + +struct Interpolators +{ + float4 iPos [[position]]; + float4 iTexCoord0 [[user(TEXCOORD0)]]; + float4 iTexCoord1 [[user(TEXCOORD1)]]; +}; + +[[fragment]] +float4 shaderMain(Interpolators input [[stage_in]], + constant Texture2DDescriptorHeap* g_Texture2DDescriptorHeap [[buffer(0)]], + constant SamplerDescriptorHeap* g_SamplerDescriptorHeap [[buffer(3)]], + constant PushConstants& g_PushConstants [[buffer(8)]]) +{ + float4 color = tfetch2D(g_Texture2DDescriptorHeap, g_SamplerDescriptorHeap, + s0_Texture2DDescriptorIndex, s0_SamplerDescriptorIndex, input.iTexCoord1.xy, float2(0, 0)) * input.iTexCoord0; + + float4 depthFadeParams = g_DepthFadeParams; + float sceneDepth = g_Texture2DDescriptorHeap[g_DepthFadeTextureIndex].tex.read(uint2(input.iPos.xy)).x; + float sceneZ = depthFadeParams.y / (sceneDepth - depthFadeParams.x); + float fragmentZ = depthFadeParams.y / (input.iPos.z - depthFadeParams.x); + // https://developer.download.nvidia.com/whitepapers/2007/SDK10/SoftParticles_hi.pdf + float d = saturate((sceneZ - fragmentZ) * depthFadeParams.z); + float c = 0.5 * pow(saturate(2.0 * (d > 0.5 ? 1.0 - d : d)), 2.0); + color.w *= d > 0.5 ? 1.0 - c : c; + + return color; +} diff --git a/MarathonRecomp/gpu/video.cpp b/MarathonRecomp/gpu/video.cpp index 714735707..ce9f0c1b5 100644 --- a/MarathonRecomp/gpu/video.cpp +++ b/MarathonRecomp/gpu/video.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -95,6 +96,8 @@ struct SharedConstants float alphaThreshold{}; uint32_t conditionalSurveyIndex{}; uint32_t conditionalRenderingIndex{}; + float depthFadeParams[4]{}; + uint32_t depthFadeTextureIndex{}; }; // Depth bias values here are only used when the render device has @@ -277,6 +280,10 @@ TextureDescriptorAllocator g_textureDescriptorAllocator; std::unique_ptr g_pipelineLayout; static xxHashMap> g_pipelines; +static constexpr float DEPTH_FADE_DISTANCE = 100.0f; +static bool g_depthFadeTextureDirty = true; +static GuestTexture* g_depthFadeTexture; + #ifdef ASYNC_PSO_DEBUG static std::atomic g_pipelinesCreatedInRenderThread; static std::atomic g_pipelinesCreatedAsynchronously; @@ -2967,6 +2974,7 @@ static void ProcRenderCommand(const RenderCommands::SetDepthStencilSurface& args { SetDirtyValue(g_dirtyStates.renderTargetAndDepthStencil, g_depthStencil, args.depthStencil); SetDirtyValue(g_dirtyStates.pipelineState, g_pipelineState.depthStencilFormat, args.depthStencil != nullptr ? args.depthStencil->format : RenderFormat::UNKNOWN); + g_depthFadeTextureDirty = true; } static bool UseShaderResolve(GuestSurface* surface) @@ -3289,6 +3297,7 @@ static void ProcRenderCommand(const RenderCommands::Clear& args) } commandList->clearDepthStencil(clearDepth, clearStencil, args.z, args.stencil); + g_depthFadeTextureDirty = true; } PropagateDeferredClear(g_renderTarget, g_depthStencil, args); @@ -3580,11 +3589,108 @@ static void ProcRenderCommand(const RenderCommands::AddPipeline& args) static constexpr int32_t COMMON_DEPTH_BIAS_VALUE = static_cast((1 << 24) * 0.002f); static constexpr float COMMON_SLOPE_SCALED_DEPTH_BIAS_VALUE = 1.0f; +static void CopyDepthForDepthFade(GuestSurface* depthStencil) +{ + if (g_depthFadeTexture == nullptr || + g_depthFadeTexture->width != depthStencil->width || + g_depthFadeTexture->height != depthStencil->height) + { + if (g_depthFadeTexture != nullptr) + g_tempResources[g_renderFrame].push_back(g_depthFadeTexture); + + g_depthFadeTexture = CreateTexture(depthStencil->width, depthStencil->height, 1, 1, 0, D3DFMT_D24FS8, 0, 3); + } + + AddBarrier(depthStencil, RenderTextureLayout::SHADER_READ); + AddBarrier(g_depthFadeTexture, RenderTextureLayout::DEPTH_WRITE); + FlushBarriers(); + + RenderPipeline* pipeline; + + switch (depthStencil->sampleCount) + { + case RenderSampleCount::COUNT_2: + pipeline = g_resolveMsaaDepthPipelines[0].get(); + break; + case RenderSampleCount::COUNT_4: + pipeline = g_resolveMsaaDepthPipelines[1].get(); + break; + case RenderSampleCount::COUNT_8: + pipeline = g_resolveMsaaDepthPipelines[2].get(); + break; + default: + pipeline = g_copyDepthPipeline.get(); + break; + } + + auto& framebuffer = g_depthFadeTexture->framebuffers[0]; + if (framebuffer == nullptr) + { + RenderFramebufferDesc desc; + desc.depthAttachment = g_depthFadeTexture->texture; + framebuffer = g_device->createFramebuffer(desc); + } + + auto& commandList = g_commandLists[g_frame]; + + if (g_framebuffer != framebuffer.get()) + { + commandList->setFramebuffer(framebuffer.get()); + g_framebuffer = framebuffer.get(); + } + + commandList->setPipeline(pipeline); + commandList->setViewports(RenderViewport(0.0f, 0.0f, float(g_depthFadeTexture->width), float(g_depthFadeTexture->height), 0.0f, 1.0f)); + commandList->setScissors(RenderRect(0, 0, g_depthFadeTexture->width, g_depthFadeTexture->height)); + commandList->setGraphicsPushConstants(0, &depthStencil->descriptorIndex, 0, sizeof(uint32_t)); + commandList->drawInstanced(6, 1, 0, 0); + + g_dirtyStates.renderTargetAndDepthStencil = true; + g_dirtyStates.viewport = true; + g_dirtyStates.pipelineState = true; + g_dirtyStates.scissorRect = true; + + if (g_backend != Backend::D3D12) + { + g_dirtyStates.vertexShaderConstants = true; // The push constant call invalidates vertex shader constants. + g_dirtyStates.depthBias = true; // Static depth bias in copy pipeline invalidates dynamic depth bias. + } +} + +static void SetDepthFadeConstants() +{ + // Clip z and clip w rows of g_MatVP (c90, c91) in the particle vertex shader. + auto clipZ = [](uint32_t i) { return std::bit_cast(ByteSwap(g_vertexShaderConstants[90 * 4 + i])); }; + auto clipW = [](uint32_t i) { return std::bit_cast(ByteSwap(g_vertexShaderConstants[91 * 4 + i])); }; + + // clip.z = A * clip.w + B, take A from the largest component for precision. + uint32_t component = 0; + for (uint32_t i = 1; i < 3; i++) + { + if (std::abs(clipW(i)) > std::abs(clipW(component))) + component = i; + } + + float a = clipZ(component) / clipW(component); + float b = clipZ(3) - a * clipW(3); + + // Fold the viewport depth range in so the shader can linearize window depth directly. + float depthRange = g_viewport.maxDepth - g_viewport.minDepth; + + SetDirtyValue(g_dirtyStates.sharedConstants, g_sharedConstants.depthFadeParams[0], g_viewport.minDepth + a * depthRange); + SetDirtyValue(g_dirtyStates.sharedConstants, g_sharedConstants.depthFadeParams[1], b * depthRange); + SetDirtyValue(g_dirtyStates.sharedConstants, g_sharedConstants.depthFadeParams[2], 1.0f / DEPTH_FADE_DISTANCE); + SetDirtyValue(g_dirtyStates.sharedConstants, g_sharedConstants.depthFadeTextureIndex, g_depthFadeTexture->descriptorIndex); +} + static void FlushRenderStateForRenderThread() { auto renderTarget = g_pipelineState.colorWriteEnable ? g_renderTarget : nullptr; auto depthStencil = g_pipelineState.zEnable || g_pipelineState.stencilEnable ? g_depthStencil : nullptr; + if (depthStencil != nullptr && g_pipelineState.zWriteEnable) + g_depthFadeTextureDirty = true; + bool foundAny = PopulateBarriersForStretchRect(renderTarget, depthStencil); auto aliasesBoundStorage = [&](const GuestSurface* surface) @@ -3632,6 +3738,21 @@ static void FlushRenderStateForRenderThread() if (!g_pendingResolves.empty()) g_pendingResolves.clear(); + + if (Config::ParticleFade == EParticleFade::Soft && + g_pipelineState.pixelShader != nullptr && + g_pipelineState.pixelShader->shaderCacheEntry->hash == 0x402F6C3CD7D5B52E) + { + if (g_depthFadeTextureDirty) + { + CopyDepthForDepthFade(depthStencil); + g_depthFadeTextureDirty = false; + } + + AddBarrier(g_depthFadeTexture, RenderTextureLayout::SHADER_READ); + SetDepthFadeConstants(); + } + AddBarrier(renderTarget, RenderTextureLayout::COLOR_WRITE); AddBarrier(depthStencil, RenderTextureLayout::DEPTH_WRITE); @@ -3884,7 +4005,8 @@ void VideoConfigValueChangedCallback(const IConfigDef* config) bool shouldRecompile = config == &Config::AntiAliasing || config == &Config::TransparencyAntiAliasing || - config == &Config::RadialBlur; + config == &Config::RadialBlur || + config == &Config::ParticleFade; if (shouldRecompile) EnqueuePipelineRecompilation(); diff --git a/MarathonRecomp/locale/config_locale.cpp b/MarathonRecomp/locale/config_locale.cpp index 963fb7a7e..0bf860c08 100644 --- a/MarathonRecomp/locale/config_locale.cpp +++ b/MarathonRecomp/locale/config_locale.cpp @@ -837,6 +837,62 @@ CONFIG_DEFINE_ENUM_LOCALE(ERadialBlur) } }; +CONFIG_DEFINE_LOCALE(ParticleFade) +{ + { ELanguage::English, { "Particle Fade", "Change how particle edges blend." } }, + { ELanguage::Japanese, { "Particle Fade", "Change how particle edges blend." } }, + { ELanguage::German, { "Particle Fade", "Change how particle edges blend." } }, + { ELanguage::French, { "Particle Fade", "Change how particle edges blend." } }, + { ELanguage::Spanish, { "Particle Fade", "Change how particle edges blend." } }, + { ELanguage::Italian, { "Particle Fade", "Change how particle edges blend." } }, +}; + +CONFIG_DEFINE_ENUM_LOCALE(EParticleFade) +{ + { + ELanguage::English, + { + { EParticleFade::Original, { "Original", "" } }, + { EParticleFade::Soft, { "Soft", "Soft: softens the edges of particles to reduce clipping artifacts." } } + } + }, + { + ELanguage::Japanese, + { + { EParticleFade::Original, { "Original", "" } }, + { EParticleFade::Soft, { "Soft", "Soft: softens the edges of particles to reduce clipping artifacts." } } + } + }, + { + ELanguage::German, + { + { EParticleFade::Original, { "Original", "" } }, + { EParticleFade::Soft, { "Soft", "Soft: softens the edges of particles to reduce clipping artifacts." } } + } + }, + { + ELanguage::French, + { + { EParticleFade::Original, { "Original", "" } }, + { EParticleFade::Soft, { "Soft", "Soft: softens the edges of particles to reduce clipping artifacts." } } + } + }, + { + ELanguage::Spanish, + { + { EParticleFade::Original, { "Original", "" } }, + { EParticleFade::Soft, { "Soft", "Soft: softens the edges of particles to reduce clipping artifacts." } } + } + }, + { + ELanguage::Italian, + { + { EParticleFade::Original, { "Original", "" } }, + { EParticleFade::Soft, { "Soft", "Soft: softens the edges of particles to reduce clipping artifacts." } } + } + } +}; + CONFIG_DEFINE_LOCALE(CutsceneAspectRatio) { { ELanguage::English, { "Cutscene Aspect Ratio", "Change the aspect ratio of the real-time cutscenes." } }, diff --git a/MarathonRecomp/ui/options_menu.cpp b/MarathonRecomp/ui/options_menu.cpp index 8257c8f1a..ed72bb557 100644 --- a/MarathonRecomp/ui/options_menu.cpp +++ b/MarathonRecomp/ui/options_menu.cpp @@ -867,6 +867,7 @@ static void DrawOptions(ImVec2 min, ImVec2 max) DrawOption(rowCount++, &Config::ShadowResolution, true); DrawOption(rowCount++, &Config::ReflectionResolution, true); DrawOption(rowCount++, &Config::RadialBlur, true); + DrawOption(rowCount++, &Config::ParticleFade, true); DrawOption(rowCount++, &Config::CutsceneAspectRatio, true); DrawOption(rowCount++, &Config::UIAlignmentMode, true); diff --git a/MarathonRecomp/user/config.cpp b/MarathonRecomp/user/config.cpp index b21cb4eea..799fd53b0 100644 --- a/MarathonRecomp/user/config.cpp +++ b/MarathonRecomp/user/config.cpp @@ -372,6 +372,12 @@ CONFIG_DEFINE_ENUM_TEMPLATE(ERadialBlur) { "Enhanced", ERadialBlur::Enhanced } }; +CONFIG_DEFINE_ENUM_TEMPLATE(EParticleFade) +{ + { "Original", EParticleFade::Original }, + { "Soft", EParticleFade::Soft }, +}; + CONFIG_DEFINE_ENUM_TEMPLATE(ECutsceneAspectRatio) { { "Original", ECutsceneAspectRatio::Original }, diff --git a/MarathonRecomp/user/config.h b/MarathonRecomp/user/config.h index e4df549cd..c02f5acf7 100644 --- a/MarathonRecomp/user/config.h +++ b/MarathonRecomp/user/config.h @@ -141,6 +141,12 @@ enum class ERadialBlur : uint32_t Enhanced }; +enum class EParticleFade : uint32_t +{ + Original, + Soft +}; + enum class ECutsceneAspectRatio : uint32_t { Original, diff --git a/MarathonRecomp/user/config_def.h b/MarathonRecomp/user/config_def.h index 4bb8834f2..f3600a405 100644 --- a/MarathonRecomp/user/config_def.h +++ b/MarathonRecomp/user/config_def.h @@ -101,6 +101,7 @@ CONFIG_DEFINE("Video", uint32_t, AnisotropicFiltering, 16, false); CONFIG_DEFINE_ENUM_LOCALISED("Video", EShadowResolution, ShadowResolution, EShadowResolution::x4096, false); CONFIG_DEFINE_ENUM_LOCALISED("Video", EReflectionResolution, ReflectionResolution, EReflectionResolution::Half, false); CONFIG_DEFINE_ENUM_LOCALISED("Video", ERadialBlur, RadialBlur, ERadialBlur::Original, false); +CONFIG_DEFINE_ENUM_LOCALISED("Video", EParticleFade, ParticleFade, EParticleFade::Original, false); CONFIG_DEFINE_ENUM_LOCALISED("Video", ECutsceneAspectRatio, CutsceneAspectRatio, ECutsceneAspectRatio::Original, false); CONFIG_DEFINE_ENUM_LOCALISED("Video", EUIAlignmentMode, UIAlignmentMode, EUIAlignmentMode::Edge, false);