diff --git a/profiles/vcs/config/ProperShaders.ini b/profiles/vcs/config/ProperShaders.ini index 1d10fbb..590a080 100644 --- a/profiles/vcs/config/ProperShaders.ini +++ b/profiles/vcs/config/ProperShaders.ini @@ -4,8 +4,8 @@ ; until their VCS guest-side sources are connected. [VolumetricClouds] Enabled=true -DownscaleDiv=2 -Layers=3 +DownscaleDiv=4 +Layers=2 ShadowSteps=4 Opacity=1.0 Speed=0.0 diff --git a/profiles/vcs/host/ge_cloudworks_present_shader.hpp b/profiles/vcs/host/ge_cloudworks_present_shader.hpp index 3eb25c9..8427786 100644 --- a/profiles/vcs/host/ge_cloudworks_present_shader.hpp +++ b/profiles/vcs/host/ge_cloudworks_present_shader.hpp @@ -9,8 +9,14 @@ namespace vcs { // fullscreen input/ray contract used by VCSNative's DX12 backend. // CloudWorks: Brian Tu (RTU), CC BY-NC-SA 3.0. inline constexpr char kCloudWorksPresentShaderHlsl[] = R"CLOUD_HLSL( -Texture2D SourceTexture : register(t0); -SamplerState SourceSampler : register(s0); +// Cloud pass descriptor contract: +// t0 = sparse march texture for CloudTemporalResolvePS, or resolved cloud +// history for CloudCompositePS/PresentPS +// t1 = previous resolved cloud history for CloudTemporalResolvePS +// s0 = clamp sampler (linear for resolve/composite) +Texture2D CloudTexture0 : register(t0); +Texture2D CloudTexture1 : register(t1); +SamplerState CloudSampler : register(s0); cbuffer CloudState : register(b0) { float3 CloudRayRight; float g_Time; @@ -25,6 +31,24 @@ cbuffer CloudState : register(b0) { float g_Brightness; float3 CloudPadding; }; +// Temporal pass contract (20 DWORDs). Together with CloudState's 40 DWORDs, +// a two-SRV descriptor table and one sampler table this consumes 62 of the +// D3D12 root signature's 64 DWORD budget. +// +// Translation invalidates history on the CPU. The previous ray basis below +// therefore only has to reproject camera rotation; it is the same world-ray +// basis used by CloudState and avoids importing matrix-layout conventions from +// either the PSP GE or D3D9 ProperShaders implementation. +cbuffer CloudTemporalState : register(b2) { + float3 PrevCloudRayRight; float CloudHistoryValid; + float3 PrevCloudRayUp; float CloudTemporalBlend; + float3 PrevCloudRayForward; float CloudSpatialMix; + float2 CloudTexelSize; float2 CloudSubPixel; + float CloudFullResolutionMarch; + float CloudClampExpand; + float2 CloudTemporalPadding; +}; + struct PresentVertexOutput { float4 position : SV_POSITION; float2 uv : TEXCOORD0; }; PresentVertexOutput PresentVS(uint id : SV_VertexID) { PresentVertexOutput o; @@ -34,7 +58,7 @@ PresentVertexOutput PresentVS(uint id : SV_VertexID) { return o; } float4 PresentPS(PresentVertexOutput i) : SV_TARGET { - return SourceTexture.SampleLevel(SourceSampler, i.uv, 0.0); + return CloudTexture0.SampleLevel(CloudSampler, i.uv, 0.0); } static const float VC_HASH_MUL = 1332.03398875; @@ -110,6 +134,8 @@ float3 atmosphere_scattering(float strength,float3 color,float3 camera,float3 ra float3 scattered=AtmosphereScattering(color,Game2Atm_Alt(camera),step,ray,light,lightDir,1.0,sphere); return lerp(color,scattered,fade); } +)CLOUD_HLSL" +R"CLOUD_HLSL( struct CloudBaseColor {float3 BaseColor;float3 BaseColor_Day;float3 BaseColor_Sunset;}; struct CloudProfile { @@ -152,6 +178,9 @@ float4 CloudAtRay(CloudProfile a,CloudBaseColor b,float3 dir,float3 cam,float3 l if(d.x>=distance)cd*=d.z/d.w;if(cd>0)d.y=d.y*(1-fx.z)+fx.z*d.x;fx.y+=cd;fx.z=(exp(-fx.y)-a.cutoff.y)/(1-a.cutoff.y);d.z=distance-d.x; if(fx.y<2.3)fx.x+=cd*exp(-ShadowMarching(cd,p,a,a.range,lightDir)-fx.y);} d.w=clampMap(2*df-pdf,cs.z*0.85,a.cutoff.x,a.march.x,a.march.y);d.w*=clampMap(d.x,0,a.fade,1,a.march.z); + // The stochastic step offset is intentional. ProperShaders averages it in + // the reprojected temporal history instead of exposing one undersampled + // march directly on screen. d.w+=noise2d(p+g_Time)*a.march.x;pdf=df;p+=dir*d.w;d.x+=d.w;} if(fx.z<1){fx=saturate(fx);float3 z=float3(0,0,cam.z);float3 cbright=SunLight(light,Game2Atm(z+dir*d.y),lightDir,earth)*a.brightness; float3 C=cbright*fx.x*MiePhase(dot(lightDir,dir))+(b.BaseColor*g_vCloudBaseColor)*(1-fx.z); @@ -190,11 +219,111 @@ float4 RenderClouds(float3 dir,float3 cam){float time=gameTime();CloudBaseColor if(layers>=2u&&result.w>.01){float4 high=CloudAtRay(BuildProfile2(time,g_CloudCoverage.z),base,dir,cam,light,lightDir,time,distance);result.rgb+=high.rgb*result.w;result.w*=high.w;} return result;} float3 WorldRay(float2 uv){float2 ndc=float2(uv.x*2-1,1-uv.y*2);return normalize(CloudRayForward+CloudRayRight*ndc.x+CloudRayUp*ndc.y);} -float4 CloudTargetPS(PresentVertexOutput i):SV_TARGET {if((g_Settings&0x10000u)==0u)discard;float3 dir=WorldRay(i.uv);if(dir.z<=1e-6)discard; - float4 clouds=RenderClouds(dir,CloudCameraPosition);float alpha=saturate((1-clouds.a)*g_Opacity);if(alpha<=1e-4)discard; - return float4(clouds.rgb*g_Opacity*g_Brightness,alpha);} + +static const float VC_TEMPORAL_DIV=2.0; +static const float VC_INV_TEMPORAL_DIV=0.5; +static const float VC_RAIL_FLOOR=4.0; + +// A sparse march target contains one texel for each 2x2 block of the resolved +// cloud history. SV_POSITION identifies the sparse texel exactly; deriving UV +// from it avoids interpolation and half-pixel disagreements between APIs. +float2 CloudMarchUV(float2 position){ + float2 pixel=floor(position); + float2 sparseUV=(pixel*VC_TEMPORAL_DIV+CloudSubPixel+0.5)*CloudTexelSize; + float2 fullUV=(pixel+0.5)*CloudTexelSize; + return lerp(sparseUV,fullUV,saturate(CloudFullResolutionMarch)); +} + +// History stores CloudWorks' native representation: premultiplied scattered +// light in rgb and TRANSMITTANCE in alpha (clear sky is 0,0,0,1). +float4 CloudMarchPS(PresentVertexOutput i):SV_TARGET { + if((g_Settings&0x10000u)==0u)return float4(0,0,0,1); + float3 dir=WorldRay(CloudMarchUV(i.position.xy)); + return RenderClouds(dir,CloudCameraPosition); +} + +// Kept as an entry-point alias while the backend moves from its former direct +// cloud target to the full/sparse march paths. +float4 CloudTargetPS(PresentVertexOutput i):SV_TARGET { + return CloudMarchPS(i); +} + +// Reproject a current world direction into the previous camera's ray basis. +// If d = q.x*R + q.y*U + q.z*F, previous NDC is q.xy/q.z. Cramer's rule keeps +// this exact for asymmetric projections and avoids a separate 4x4 matrix. +float3 PreviousCloudNdc(float3 dir){ + float det=dot(PrevCloudRayRight,cross(PrevCloudRayUp,PrevCloudRayForward)); + float safeDet=(abs(det)>1e-8)?det:((det<0)?-1e-8:1e-8); + float3 q=float3( + dot(dir,cross(PrevCloudRayUp,PrevCloudRayForward)), + dot(PrevCloudRayRight,cross(dir,PrevCloudRayForward)), + dot(PrevCloudRayRight,cross(PrevCloudRayUp,dir)))/safeDet; + return float3(q.xy/max(q.z,1e-8),q.z); +} + +// Literal SM5 adaptation of ProperShaders' PS_TemporalResolve. The march fills +// one Bayer slot per 2x2 block; every other pixel keeps its own reprojected +// history and leaks slightly toward the smooth current-frame reconstruction. +float4 CloudTemporalResolvePS(PresentVertexOutput i):SV_TARGET { + float2 pixel=floor(i.position.xy); + float2 uv=(pixel+0.5)*CloudTexelSize; + float2 marchTexel=CloudTexelSize*VC_TEMPORAL_DIV; + + float2 block=floor(pixel*VC_INV_TEMPORAL_DIV); + float2 slot=pixel-block*VC_TEMPORAL_DIV; + float2 blockUV=(block+0.5)*marchTexel; + float4 c=CloudTexture0.SampleLevel(CloudSampler,blockUV,0.0); + + float2 freshDelta=abs(slot-CloudSubPixel); + float fresh=step(freshDelta.x+freshDelta.y,0.5); + + float2 spatialUV=uv-(CloudSubPixel-(VC_TEMPORAL_DIV-1.0)*0.5)*CloudTexelSize; + float2 sp=spatialUV/marchTexel-0.5; + float2 sf=frac(sp); + float2 sb=(floor(sp)+0.5)*marchTexel; + float4 t00=CloudTexture0.SampleLevel(CloudSampler,sb,0.0); + float4 t10=CloudTexture0.SampleLevel(CloudSampler,sb+float2(marchTexel.x,0),0.0); + float4 t01=CloudTexture0.SampleLevel(CloudSampler,sb+float2(0,marchTexel.y),0.0); + float4 t11=CloudTexture0.SampleLevel(CloudSampler,sb+marchTexel,0.0); + float4 spatial=lerp(lerp(t00,t10,sf.x),lerp(t01,t11,sf.x),sf.y); + + float3 previous=PreviousCloudNdc(WorldRay(uv)); + float2 prevUV=float2(previous.x*0.5+0.5,0.5-previous.y*0.5); + float2 inside=step(float2(0,0),prevUV)*step(prevUV,float2(1,1)); + float valid=CloudHistoryValid*inside.x*inside.y*step(1e-8,previous.z); + float4 hist=CloudTexture1.SampleLevel(CloudSampler,prevUV,0.0); + + // Loose safety rail from ProperShaders. RGB and transmittance must move as a + // single value; clamping channels independently creates black pinholes. + float4 mn=min(min(min(t00,t10),min(t01,t11)),c); + float4 mx=max(max(max(t00,t10),max(t01,t11)),c); + float4 mid=(mn+mx)*0.5; + float3 spanRGB=mx.rgb-mn.rgb; + float span=max(max(spanRGB.r,spanRGB.g),max(spanRGB.b,mx.a-mn.a)); + float ext=max(span*CloudClampExpand,VC_RAIL_FLOOR); + float4 dev=abs(hist-mid)-ext; + float over=max(max(dev.r,dev.g),max(dev.b,dev.a)); + hist=lerp(hist,mid,saturate(over)); + + float4 src=lerp(spatial,c,fresh); + float w=lerp(1.0-CloudSpatialMix,CloudTemporalBlend,fresh)*valid; + return lerp(src,hist,w); +} + +// Manual four-tap bilinear upscale matches ProperShaders even if the bound +// sampler is point-filtered. The backend blends this premultiplied result over +// the world target with ONE / INV_SRC_ALPHA. float4 CloudCompositePS(PresentVertexOutput i):SV_TARGET { - return SourceTexture.SampleLevel(SourceSampler,i.uv,0.0); + float2 texelPos=i.uv/CloudTexelSize-0.5; + float2 f=frac(texelPos); + float2 base=(floor(texelPos)+0.5)*CloudTexelSize; + float4 c00=CloudTexture0.SampleLevel(CloudSampler,base,0.0); + float4 c10=CloudTexture0.SampleLevel(CloudSampler,base+float2(CloudTexelSize.x,0),0.0); + float4 c01=CloudTexture0.SampleLevel(CloudSampler,base+float2(0,CloudTexelSize.y),0.0); + float4 c11=CloudTexture0.SampleLevel(CloudSampler,base+CloudTexelSize,0.0); + float4 clouds=lerp(lerp(c00,c10,f.x),lerp(c01,c11,f.x),f.y); + float alpha=saturate((1.0-clouds.a)*g_Opacity); + return float4(clouds.rgb*g_Opacity*g_Brightness,alpha); } )CLOUD_HLSL"; diff --git a/profiles/vcs/host/ge_gpu_backend_dx12.cpp b/profiles/vcs/host/ge_gpu_backend_dx12.cpp index 4b0609c..dd3375e 100644 --- a/profiles/vcs/host/ge_gpu_backend_dx12.cpp +++ b/profiles/vcs/host/ge_gpu_backend_dx12.cpp @@ -148,6 +148,15 @@ struct CloudShaderConstants { }; static_assert(sizeof(CloudShaderConstants) == 40u * sizeof(std::uint32_t)); +struct CloudTemporalConstants { + std::array previous_right_history{}; + std::array previous_up_blend{}; + std::array previous_forward_spatial{}; + std::array texel_subpixel{}; + std::array control{}; +}; +static_assert(sizeof(CloudTemporalConstants) == 20u * sizeof(std::uint32_t)); + struct Dx12FrameResources { ComPtr allocator; ComPtr upload_buffer; @@ -292,12 +301,21 @@ struct Dx12GeState { std::uint32_t swap_height{}; ComPtr present_pipeline; ComPtr cloud_target_pipeline; + ComPtr cloud_resolve_pipeline; ComPtr cloud_composite_pipeline; ComPtr present_vertex_shader; ComPtr present_pixel_shader; ComPtr cloud_target_pixel_shader; + ComPtr cloud_resolve_pixel_shader; ComPtr cloud_composite_pixel_shader; - CloudRenderTarget cloud_render_target; + std::array cloud_history; + CloudRenderTarget cloud_march; + std::array cloud_resolve_srv_base{}; + std::uint32_t cloud_history_index{}; + std::uint32_t cloud_temporal_frame{}; + std::array cloud_previous_camera{}; + std::array cloud_previous_ray_basis{}; + bool cloud_history_valid{}; bool direct_present_ok{}; std::uint32_t presented_framebuffer{}; std::uint32_t missed_display_intervals{}; @@ -1103,8 +1121,8 @@ float4 PSMain(VSOut input) : SV_TARGET { // but compile this faithful large port with the same optimisation and let // genuine HLSL errors remain fatal. const UINT cloud_flags = D3DCOMPILE_OPTIMIZATION_LEVEL3; - hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GECloudTarget", - nullptr, nullptr, "CloudTargetPS", "ps_5_1", cloud_flags, 0u, + hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GECloudMarch", + nullptr, nullptr, "CloudMarchPS", "ps_5_1", cloud_flags, 0u, &s.cloud_target_pixel_shader, &errors); if (FAILED(hr)) { error = errors ? std::string(static_cast(errors->GetBufferPointer()), @@ -1113,6 +1131,16 @@ float4 PSMain(VSOut input) : SV_TARGET { return false; } errors.Reset(); + hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GECloudTemporalResolve", + nullptr, nullptr, "CloudTemporalResolvePS", "ps_5_1", cloud_flags, 0u, + &s.cloud_resolve_pixel_shader, &errors); + if (FAILED(hr)) { + error = errors ? std::string(static_cast(errors->GetBufferPointer()), + errors->GetBufferSize()) + : hr_text(hr, "D3DCompile(DX12 GE cloud temporal resolve PS)"); + return false; + } + errors.Reset(); hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GECloudComposite", nullptr, nullptr, "CloudCompositePS", "ps_5_1", flags, 0u, &s.cloud_composite_pixel_shader, &errors); @@ -1274,49 +1302,78 @@ bool create_targets(Dx12GeState &s, std::string &error) noexcept { if (vcs_configuration().volumetric_clouds.enabled) { const std::uint32_t divisor = std::clamp( vcs_configuration().volumetric_clouds.downscale_div, 1u, 8u); - CloudRenderTarget &cloud = s.cloud_render_target; - cloud.width = std::max(1u, (s.target_width + divisor - 1u) / divisor); - cloud.height = std::max(1u, (s.target_height + divisor - 1u) / divisor); - if (s.next_rtv >= kFramebufferTargetCapacity || s.next_srv >= kSrvCapacity) { - error = "DX12 GE descriptor capacity exhausted by CloudWorks target"; + std::uint32_t cloud_width = std::max(2u, (s.target_width + divisor - 1u) / divisor); + std::uint32_t cloud_height = std::max(2u, (s.target_height + divisor - 1u) / divisor); + cloud_width = (cloud_width + 1u) & ~1u; + cloud_height = (cloud_height + 1u) & ~1u; + if (s.next_rtv + 3u > kFramebufferTargetCapacity || + s.next_srv + 7u > kSrvCapacity) { + error = "DX12 GE descriptor capacity exhausted by CloudWorks temporal targets"; return false; } - cloud.rtv_index = s.next_rtv++; - cloud.srv_index = s.next_srv++; - D3D12_RESOURCE_DESC image{}; - image.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; - image.Width = cloud.width; - image.Height = cloud.height; - image.DepthOrArraySize = 1u; - image.MipLevels = 1u; - image.Format = kCloudFormat; - image.SampleDesc.Count = 1u; - image.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; - image.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; - D3D12_HEAP_PROPERTIES heap_properties{}; - heap_properties.Type = D3D12_HEAP_TYPE_DEFAULT; - D3D12_CLEAR_VALUE clear{}; - clear.Format = kCloudFormat; - clear.Color[0] = clear.Color[1] = clear.Color[2] = clear.Color[3] = 0.0f; - hr = s.device->CreateCommittedResource( - &heap_properties, D3D12_HEAP_FLAG_NONE, &image, - D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, &clear, - IID_PPV_ARGS(&cloud.image)); - if (FAILED(hr)) { - error = hr_text(hr, "CreateCommittedResource(DX12 GE CloudWorks target)"); - return false; - } - s.device->CreateRenderTargetView(cloud.image.Get(), nullptr, - rtv_cpu(s, cloud.rtv_index)); D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srv.Format = kCloudFormat; srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srv.Texture2D.MipLevels = 1u; - s.device->CreateShaderResourceView(cloud.image.Get(), &srv, - srv_cpu(s, cloud.srv_index)); - runtime_log_line("CloudWorks render target " + std::to_string(cloud.width) + "x" + - std::to_string(cloud.height) + " (DownscaleDiv=" + + const auto create_cloud_target = [&](CloudRenderTarget &target, + std::uint32_t width, + std::uint32_t height, + const char *name) -> bool { + target.width = width; + target.height = height; + target.rtv_index = s.next_rtv++; + target.srv_index = s.next_srv++; + D3D12_RESOURCE_DESC image{}; + image.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + image.Width = width; + image.Height = height; + image.DepthOrArraySize = 1u; + image.MipLevels = 1u; + image.Format = kCloudFormat; + image.SampleDesc.Count = 1u; + image.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + image.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_CLEAR_VALUE clear{}; + clear.Format = kCloudFormat; + // Cloud buffers store premultiplied light + remaining transmittance. + clear.Color[3] = 1.0f; + const HRESULT create_hr = s.device->CreateCommittedResource( + &heap, D3D12_HEAP_FLAG_NONE, &image, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, &clear, + IID_PPV_ARGS(&target.image)); + if (FAILED(create_hr)) { + error = hr_text(create_hr, name); + return false; + } + s.device->CreateRenderTargetView(target.image.Get(), nullptr, + rtv_cpu(s, target.rtv_index)); + s.device->CreateShaderResourceView(target.image.Get(), &srv, + srv_cpu(s, target.srv_index)); + return true; + }; + if (!create_cloud_target(s.cloud_history[0], cloud_width, cloud_height, + "Create CloudWorks history 0") || + !create_cloud_target(s.cloud_history[1], cloud_width, cloud_height, + "Create CloudWorks history 1") || + !create_cloud_target(s.cloud_march, cloud_width / 2u, cloud_height / 2u, + "Create CloudWorks sparse march")) + return false; + // Resolve needs t0=march and t1=the selected previous history in one + // contiguous descriptor table. Duplicate the SRVs for each ping-pong side. + for (std::uint32_t index = 0u; index < 2u; ++index) { + s.cloud_resolve_srv_base[index] = s.next_srv; + s.device->CreateShaderResourceView(s.cloud_march.image.Get(), &srv, + srv_cpu(s, s.next_srv++)); + s.device->CreateShaderResourceView(s.cloud_history[index].image.Get(), &srv, + srv_cpu(s, s.next_srv++)); + } + runtime_log_line("CloudWorks temporal buffers history=" + + std::to_string(cloud_width) + "x" + std::to_string(cloud_height) + + " march=" + std::to_string(cloud_width / 2u) + "x" + + std::to_string(cloud_height / 2u) + " (DownscaleDiv=" + std::to_string(divisor) + ")"); } return true; @@ -1787,16 +1844,44 @@ std::uint32_t present_sampler(Dx12GeState &s) noexcept { return ensure_sampler(s, draw); } +std::uint32_t cloud_linear_sampler(Dx12GeState &s) noexcept { + GeGpuDrawDescriptor draw{}; + draw.texture_min_linear = true; + draw.texture_mag_linear = true; + draw.texture_clamp_u = true; + draw.texture_clamp_v = true; + return ensure_sampler(s, draw); +} + bool create_cloud_root_signature(Dx12GeState &s, std::string &error) noexcept { - D3D12_ROOT_PARAMETER parameter{}; - parameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; - parameter.Constants.ShaderRegister = 0u; - parameter.Constants.RegisterSpace = 0u; - parameter.Constants.Num32BitValues = 40u; - parameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + D3D12_DESCRIPTOR_RANGE srv_range{}; + srv_range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + srv_range.NumDescriptors = 2u; + srv_range.BaseShaderRegister = 0u; + D3D12_DESCRIPTOR_RANGE sampler_range{}; + sampler_range.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER; + sampler_range.NumDescriptors = 1u; + sampler_range.BaseShaderRegister = 0u; + std::array parameters{}; + parameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; + parameters[0].Constants.ShaderRegister = 0u; + parameters[0].Constants.Num32BitValues = 40u; + parameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + parameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + parameters[1].DescriptorTable.NumDescriptorRanges = 1u; + parameters[1].DescriptorTable.pDescriptorRanges = &srv_range; + parameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + parameters[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + parameters[2].DescriptorTable.NumDescriptorRanges = 1u; + parameters[2].DescriptorTable.pDescriptorRanges = &sampler_range; + parameters[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + parameters[3].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; + parameters[3].Constants.ShaderRegister = 2u; + parameters[3].Constants.Num32BitValues = 20u; + parameters[3].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; D3D12_ROOT_SIGNATURE_DESC desc{}; - desc.NumParameters = 1u; - desc.pParameters = ¶meter; + desc.NumParameters = static_cast(parameters.size()); + desc.pParameters = parameters.data(); desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; ComPtr blob, errors; HRESULT hr = D3D12SerializeRootSignature(&desc, D3D_ROOT_SIGNATURE_VERSION_1, @@ -1894,9 +1979,36 @@ bool create_cloud_target_pipeline(Dx12GeState &s, std::string &error) noexcept { return true; } +bool create_cloud_resolve_pipeline(Dx12GeState &s, std::string &error) noexcept { + D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{}; + pso.pRootSignature = s.cloud_root_signature.Get(); + pso.VS = {s.present_vertex_shader->GetBufferPointer(), + s.present_vertex_shader->GetBufferSize()}; + pso.PS = {s.cloud_resolve_pixel_shader->GetBufferPointer(), + s.cloud_resolve_pixel_shader->GetBufferSize()}; + pso.SampleMask = UINT_MAX; + pso.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + pso.RasterizerState.DepthClipEnable = TRUE; + pso.BlendState.RenderTarget[0].BlendEnable = FALSE; + pso.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + pso.DepthStencilState.DepthEnable = FALSE; + pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + pso.NumRenderTargets = 1u; + pso.RTVFormats[0] = kCloudFormat; + pso.SampleDesc.Count = 1u; + const HRESULT hr = s.device->CreateGraphicsPipelineState( + &pso, IID_PPV_ARGS(&s.cloud_resolve_pipeline)); + if (FAILED(hr)) { + error = hr_text(hr, "CreateGraphicsPipelineState(DX12 GE cloud temporal resolve)"); + return false; + } + return true; +} + bool create_cloud_composite_pipeline(Dx12GeState &s, std::string &error) noexcept { D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{}; - pso.pRootSignature = s.root_signature.Get(); + pso.pRootSignature = s.cloud_root_signature.Get(); pso.VS = {s.present_vertex_shader->GetBufferPointer(), s.present_vertex_shader->GetBufferSize()}; pso.PS = {s.cloud_composite_pixel_shader->GetBufferPointer(), @@ -1910,10 +2022,15 @@ bool create_cloud_composite_pipeline(Dx12GeState &s, std::string &error) noexcep blend.SrcBlend = D3D12_BLEND_ONE; blend.DestBlend = D3D12_BLEND_INV_SRC_ALPHA; blend.BlendOp = D3D12_BLEND_OP_ADD; - blend.SrcBlendAlpha = D3D12_BLEND_ONE; - blend.DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA; + // Alpha in the PSP world framebuffer is composition metadata, not spare + // storage for cloud opacity. Preserve it exactly; changing it makes the + // later framebuffer/HUD composite treat clouds as foreground content. + blend.SrcBlendAlpha = D3D12_BLEND_ZERO; + blend.DestBlendAlpha = D3D12_BLEND_ONE; blend.BlendOpAlpha = D3D12_BLEND_OP_ADD; - blend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + blend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_RED | + D3D12_COLOR_WRITE_ENABLE_GREEN | + D3D12_COLOR_WRITE_ENABLE_BLUE; pso.DepthStencilState.DepthEnable = TRUE; pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; pso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_EQUAL; @@ -1956,20 +2073,25 @@ const CloudCameraCandidate *select_cloud_camera(const Dx12GeState &s) noexcept { } } - const CloudCameraCandidate *best = nullptr; + // VCS' main 3D world surface is 0x00088000. Prefer it strictly over the + // later display/HUD target; choosing the latter makes clouds cover the HUD + // and every transparent entity regardless of the insertion boundary. + constexpr std::uint32_t kVcsWorldFramebuffer = 0x00088000u; + const CloudCameraCandidate *best_world = nullptr; for (const CloudCameraCandidate &candidate : s.cloud_cameras) { - if (!contains(candidate.target) || candidate.occluding_weight == 0u) continue; - // Match the camera selector already proven by Project2DFX: the camera - // with the most genuinely occluding geometry wins, then total weight. - // Prioritising depth writes selected auxiliary/reflection passes in VCS. - if (best == nullptr || - candidate.occluding_weight > best->occluding_weight || - (candidate.occluding_weight == best->occluding_weight && - candidate.weight > best->weight)) { - best = &candidate; - } + if ((candidate.target & 0x001FFFF0u) != kVcsWorldFramebuffer || + candidate.occluding_weight == 0u) + continue; + if (best_world == nullptr || + candidate.occluding_weight > best_world->occluding_weight || + (candidate.occluding_weight == best_world->occluding_weight && + candidate.weight > best_world->weight)) + best_world = &candidate; } - return best; + // Never fall back to a later framebuffer. Missing clouds are safer and + // correctable; clouds composited over HUD/transparency are categorically + // the wrong render phase. + return best_world; } CloudShaderConstants cloud_present_constants(const Dx12GeState &s) noexcept { @@ -2076,46 +2198,158 @@ CloudShaderConstants cloud_present_constants(const Dx12GeState &s) noexcept { void record_clouds_into_world_target(Dx12GeState &s, Dx12FramebufferTarget &target, const CloudShaderConstants &clouds) noexcept { - CloudRenderTarget &cloud = s.cloud_render_target; - if (!cloud.image) return; - transition(s.list.Get(), cloud.image.Get(), cloud.state, - D3D12_RESOURCE_STATE_RENDER_TARGET); - cloud.state = D3D12_RESOURCE_STATE_RENDER_TARGET; - const D3D12_CPU_DESCRIPTOR_HANDLE cloud_rtv = rtv_cpu(s, cloud.rtv_index); - s.list->OMSetRenderTargets(1u, &cloud_rtv, FALSE, nullptr); - constexpr float transparent[4]{0.0f, 0.0f, 0.0f, 0.0f}; - s.list->ClearRenderTargetView(cloud_rtv, transparent, 0u, nullptr); - D3D12_VIEWPORT cloud_viewport{0.0f, 0.0f, static_cast(cloud.width), - static_cast(cloud.height), 0.0f, 1.0f}; - D3D12_RECT cloud_scissor{0, 0, static_cast(cloud.width), - static_cast(cloud.height)}; - s.list->RSSetViewports(1u, &cloud_viewport); - s.list->RSSetScissorRects(1u, &cloud_scissor); - s.list->SetPipelineState(s.cloud_target_pipeline.Get()); + if (!s.cloud_history[0].image || !s.cloud_history[1].image || + !s.cloud_march.image || !s.cloud_target_pipeline || + !s.cloud_resolve_pipeline || !s.cloud_composite_pipeline) + return; + const std::uint32_t settings = std::bit_cast(clouds.camera_settings[3]); + if ((settings & 0x10000u) == 0u) return; + + const std::uint32_t previous_index = s.cloud_history_index & 1u; + const std::uint32_t current_index = 1u - previous_index; + CloudRenderTarget &previous = s.cloud_history[previous_index]; + CloudRenderTarget ¤t = s.cloud_history[current_index]; + const auto &config = vcs_configuration().volumetric_clouds; + + float camera_delta_squared = 0.0f; + if (s.cloud_history_valid) { + for (std::size_t axis = 0u; axis < 3u; ++axis) { + const float delta = clouds.camera_settings[axis] - s.cloud_previous_camera[axis]; + camera_delta_squared += delta * delta; + } + } + // Like ProperShaders, rotation is reprojected while positional movement + // gets one complete current-frame march. A composite of several cloud + // depths cannot be translated correctly with a single motion vector. + const bool camera_translated = s.cloud_history_valid && + camera_delta_squared > 0.0004f; + const bool full_current_frame = !s.cloud_history_valid || camera_translated; + constexpr std::array, 4> kBayerSlots{{ + {{0.0f, 0.0f}}, {{1.0f, 1.0f}}, {{1.0f, 0.0f}}, {{0.0f, 1.0f}}}}; + const auto &subpixel = kBayerSlots[s.cloud_temporal_frame & 3u]; + + CloudTemporalConstants temporal{}; + const auto copy_previous_basis = [&](std::array &destination, + std::size_t offset) { + for (std::size_t axis = 0u; axis < 3u; ++axis) + destination[axis] = s.cloud_history_valid + ? s.cloud_previous_ray_basis[offset + axis] + : (offset == 0u ? clouds.ray_right_time[axis] + : offset == 3u ? clouds.ray_up_seed[axis] + : clouds.ray_forward_opacity[axis]); + }; + copy_previous_basis(temporal.previous_right_history, 0u); + copy_previous_basis(temporal.previous_up_blend, 3u); + copy_previous_basis(temporal.previous_forward_spatial, 6u); + temporal.previous_right_history[3] = + s.cloud_history_valid && !camera_translated ? 1.0f : 0.0f; + temporal.previous_up_blend[3] = std::clamp(config.temporal_blend, 0.0f, 0.95f); + temporal.previous_forward_spatial[3] = + std::clamp(config.temporal_denoise * 0.012f, 0.0f, 0.25f); + temporal.texel_subpixel = { + 1.0f / static_cast(current.width), + 1.0f / static_cast(current.height), subpixel[0], subpixel[1]}; + temporal.control = { + full_current_frame ? 1.0f : 0.0f, + config.temporal_clamp <= 0.0f ? 1000.0f + : std::clamp(config.temporal_clamp * 2.0f, + 0.5f, 16.0f), + 0.0f, 0.0f}; + + ID3D12DescriptorHeap *heaps[]{s.srv_heap.Get(), s.sampler_heap.Get()}; + s.list->SetDescriptorHeaps(2u, heaps); s.list->SetGraphicsRootSignature(s.cloud_root_signature.Get()); s.list->SetGraphicsRoot32BitConstants(0u, 40u, &clouds, 0u); + s.list->SetGraphicsRoot32BitConstants(3u, 20u, &temporal, 0u); s.list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - s.list->DrawInstanced(3u, 1u, 0u, 0u); - transition(s.list.Get(), cloud.image.Get(), cloud.state, - D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); - cloud.state = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + constexpr float clear_cloud[4]{0.0f, 0.0f, 0.0f, 1.0f}; + const auto bind_cloud_target = [&](CloudRenderTarget &destination, + bool clear) { + transition(s.list.Get(), destination.image.Get(), destination.state, + D3D12_RESOURCE_STATE_RENDER_TARGET); + destination.state = D3D12_RESOURCE_STATE_RENDER_TARGET; + const D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_cpu(s, destination.rtv_index); + s.list->OMSetRenderTargets(1u, &rtv, FALSE, nullptr); + if (clear) s.list->ClearRenderTargetView(rtv, clear_cloud, 0u, nullptr); + const D3D12_VIEWPORT viewport{ + 0.0f, 0.0f, static_cast(destination.width), + static_cast(destination.height), 0.0f, 1.0f}; + const D3D12_RECT scissor{0, 0, static_cast(destination.width), + static_cast(destination.height)}; + s.list->RSSetViewports(1u, &viewport); + s.list->RSSetScissorRects(1u, &scissor); + }; + const auto make_shader_readable = [&](CloudRenderTarget &source) { + transition(s.list.Get(), source.image.Get(), source.state, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + source.state = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; + }; + + if (full_current_frame) { + bind_cloud_target(current, true); + s.list->SetPipelineState(s.cloud_target_pipeline.Get()); + s.list->DrawInstanced(3u, 1u, 0u, 0u); + make_shader_readable(current); + } else { + temporal.control[0] = 0.0f; + s.list->SetGraphicsRoot32BitConstants(3u, 20u, &temporal, 0u); + bind_cloud_target(s.cloud_march, true); + s.list->SetPipelineState(s.cloud_target_pipeline.Get()); + s.list->DrawInstanced(3u, 1u, 0u, 0u); + make_shader_readable(s.cloud_march); + + bind_cloud_target(current, true); + s.list->SetPipelineState(s.cloud_resolve_pipeline.Get()); + s.list->SetGraphicsRootDescriptorTable( + 1u, srv_gpu(s, s.cloud_resolve_srv_base[previous_index])); + s.list->SetGraphicsRootDescriptorTable( + 2u, sampler_gpu(s, cloud_linear_sampler(s))); + s.list->DrawInstanced(3u, 1u, 0u, 0u); + make_shader_readable(current); + } + + static std::uint32_t temporal_trace_count = 0u; + if (temporal_trace_count < 8u) { + runtime_log_line("CLOUD_TEMPORAL frame=" + std::to_string(s.frame_epoch) + + " mode=" + (full_current_frame ? std::string("full") + : std::string("sparse")) + + " delta2=" + std::to_string(camera_delta_squared) + + " history=" + std::to_string(previous_index) + "->" + + std::to_string(current_index) + " bayer=" + + std::to_string(static_cast(subpixel[0])) + "," + + std::to_string(static_cast(subpixel[1]))); + ++temporal_trace_count; + } + + s.cloud_history_index = current_index; + ++s.cloud_temporal_frame; + for (std::size_t axis = 0u; axis < 3u; ++axis) { + s.cloud_previous_camera[axis] = clouds.camera_settings[axis]; + s.cloud_previous_ray_basis[axis] = clouds.ray_right_time[axis]; + s.cloud_previous_ray_basis[3u + axis] = clouds.ray_up_seed[axis]; + s.cloud_previous_ray_basis[6u + axis] = clouds.ray_forward_opacity[axis]; + } + s.cloud_history_valid = true; prepare_target_for_render(s, target); const D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_cpu(s, target.rtv_index); const D3D12_CPU_DESCRIPTOR_HANDLE dsv = dsv_cpu(s, target.dsv_index); s.list->OMSetRenderTargets(1u, &rtv, FALSE, &dsv); - D3D12_VIEWPORT viewport{0.0f, 0.0f, static_cast(s.target_width), - static_cast(s.target_height), 0.0f, 1.0f}; - D3D12_RECT scissor{0, 0, static_cast(s.target_width), - static_cast(s.target_height)}; + const D3D12_VIEWPORT viewport{ + 0.0f, 0.0f, static_cast(s.target_width), + static_cast(s.target_height), 0.0f, 1.0f}; + const D3D12_RECT scissor{0, 0, static_cast(s.target_width), + static_cast(s.target_height)}; s.list->RSSetViewports(1u, &viewport); s.list->RSSetScissorRects(1u, &scissor); s.list->SetPipelineState(s.cloud_composite_pipeline.Get()); - s.list->SetGraphicsRootSignature(s.root_signature.Get()); - ID3D12DescriptorHeap *heaps[]{s.srv_heap.Get(), s.sampler_heap.Get()}; - s.list->SetDescriptorHeaps(2u, heaps); - s.list->SetGraphicsRootDescriptorTable(0u, srv_gpu(s, cloud.srv_index)); - s.list->SetGraphicsRootDescriptorTable(1u, sampler_gpu(s, present_sampler(s))); + s.list->SetGraphicsRootSignature(s.cloud_root_signature.Get()); + s.list->SetGraphicsRoot32BitConstants(0u, 40u, &clouds, 0u); + temporal.control[0] = 0.0f; + s.list->SetGraphicsRoot32BitConstants(3u, 20u, &temporal, 0u); + s.list->SetGraphicsRootDescriptorTable(1u, srv_gpu(s, current.srv_index)); + s.list->SetGraphicsRootDescriptorTable( + 2u, sampler_gpu(s, cloud_linear_sampler(s))); s.list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); s.list->DrawInstanced(3u, 1u, 0u, 0u); } @@ -2635,6 +2869,7 @@ bool create_backend(Dx12GeState &s, std::string &error) noexcept { if (!create_targets(s, error)) return false; if (!create_present_pipeline(s, error)) return false; if (!create_cloud_target_pipeline(s, error)) return false; + if (!create_cloud_resolve_pipeline(s, error)) return false; if (!create_cloud_composite_pipeline(s, error)) return false; s.vertices.reserve(262144u); s.packed_0115_vertices.reserve(2621440u); @@ -2677,10 +2912,19 @@ void destroy_backend(Dx12GeState &s) noexcept { s.swap_rtv_heap.Reset(); s.present_pipeline.Reset(); s.cloud_target_pipeline.Reset(); + s.cloud_resolve_pipeline.Reset(); s.cloud_composite_pipeline.Reset(); s.cloud_target_pixel_shader.Reset(); + s.cloud_resolve_pixel_shader.Reset(); s.cloud_composite_pixel_shader.Reset(); - s.cloud_render_target = {}; + s.cloud_history = {}; + s.cloud_march = {}; + s.cloud_resolve_srv_base = {}; + s.cloud_history_index = 0u; + s.cloud_temporal_frame = 0u; + s.cloud_previous_camera = {}; + s.cloud_previous_ray_basis = {}; + s.cloud_history_valid = false; s.present_pixel_shader.Reset(); s.present_vertex_shader.Reset(); s.pipelines.clear(); @@ -3492,38 +3736,61 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept { const std::uint32_t cloud_target_address = cloud_camera != nullptr ? cloud_camera->target : 0u; bool clouds_injected = false; - bool cloud_opaque_seen = false; + bool cloud_depth_writing_world_seen = false; + static bool cloud_trace_done = false; + const bool trace_cloud_frame = !cloud_trace_done && cloud_camera != nullptr; + std::size_t cloud_batch_index = 0u; + if (trace_cloud_frame) { + std::ostringstream line; + line << "CLOUD_TRACE build=temporal-fading-v4 frame=" << s.frame_epoch + << " selected=0x" << std::hex << cloud_target_address + << " display=0x" << s.display_framebuffer << std::dec + << " batches=" << s.batches.size(); + runtime_log_line(line.str()); + } constexpr float black[4]{0.0f, 0.0f, 0.0f, 1.0f}; for (const Dx12Batch &batch : s.batches) { const std::uint32_t address = batch.draw.framebuffer_address & 0x001FFFF0u; const bool cloud_target_batch = address == cloud_target_address; - const bool opaque_world_batch = cloud_target_batch && !batch.draw.clear_mode && - batch.draw.depth_test_enabled && batch.draw.depth_write_enabled && - !batch.draw.blend_enabled && !batch.draw.alpha_test_enabled; - if (opaque_world_batch) cloud_opaque_seen = true; - // PSP has no named RenderFadingEntities marker in its GE stream, but - // the same boundary is visible in render state: after depth-writing, - // non-blended world geometry, fading/foliage starts using blend, alpha - // test, or depth-test without depth writes. Inject before that first - // batch so every transparent object composites over the clouds. - const bool fading_entities_boundary = cloud_target_batch && cloud_opaque_seen && - !batch.draw.clear_mode && - (batch.draw.blend_enabled || batch.draw.alpha_test_enabled || - (batch.draw.depth_test_enabled && !batch.draw.depth_write_enabled)); - // Conservative fallback for unusual frames without a recognizable - // fading pass: inject before the world target is sampled by composition. - const bool world_target_consumer = batch.framebuffer_feedback && - (batch.feedback_address & 0x001FFFF0u) == cloud_target_address && - address != cloud_target_address; + if (trace_cloud_frame && (cloud_target_batch || address == s.display_framebuffer)) { + std::ostringstream line; + line << "CLOUD_BATCH i=" << cloud_batch_index << " target=0x" << std::hex + << address << std::dec << " clear=" << batch.draw.clear_mode + << " depth_test=" << batch.draw.depth_test_enabled + << " depth_write=" << batch.draw.depth_write_enabled + << " blend=" << batch.draw.blend_enabled + << " alpha_test=" << batch.draw.alpha_test_enabled + << " feedback=" << batch.framebuffer_feedback; + runtime_log_line(line.str()); + } + // Captured VCS frame: opaque/alpha-tested world geometry is the run + // with depth writes (batches 2..92); FadingEntities starts at the first + // depth-tested draw without depth writes (batch 93). Blend and alpha + // test stay enabled throughout and cannot identify this boundary. + if (cloud_target_batch && !batch.draw.clear_mode && + batch.draw.depth_test_enabled && batch.draw.depth_write_enabled) + cloud_depth_writing_world_seen = true; + const bool fading_entities_boundary = cloud_target_batch && + cloud_depth_writing_world_seen && !batch.draw.clear_mode && + batch.draw.depth_test_enabled && !batch.draw.depth_write_enabled; if (!clouds_injected && cloud_camera != nullptr && - (fading_entities_boundary || world_target_consumer)) { + fading_entities_boundary) { + if (trace_cloud_frame) { + runtime_log_line("CLOUD_INJECT before_batch=" + + std::to_string(cloud_batch_index) + + " reason=fading_entities"); + } if (Dx12FramebufferTarget *cloud_target = find_framebuffer_target(s, cloud_target_address); cloud_target != nullptr && cloud_target->color && cloud_target->depth) { if (current_target != nullptr && current_target != cloud_target) resolve_target_for_sampling(s, *current_target, false); record_clouds_into_world_target(s, *cloud_target, clouds); + // The cloud passes use their own 62-DWORD root layout. Restore + // the GE layout before recording the first FadingEntities draw; + // all cached bindings below are invalidated and repopulated. + s.list->SetGraphicsRootSignature(s.root_signature.Get()); current_target = cloud_target; current_address = cloud_target_address; clouds_injected = true; @@ -3538,6 +3805,7 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept { active_topology = D3D_PRIMITIVE_TOPOLOGY_UNDEFINED; } } + ++cloud_batch_index; Dx12FramebufferTarget *target = address == current_address ? current_target : find_framebuffer_target(s, address); if (target == nullptr || !target->color || !target->depth) continue; @@ -3765,6 +4033,11 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept { } } } + if (trace_cloud_frame) { + runtime_log_line(std::string("CLOUD_TRACE_END injected=") + + (clouds_injected ? "1" : "0")); + cloud_trace_done = true; + } if (current_target != nullptr) resolve_target_for_sampling(s, *current_target, false); diff --git a/profiles/vcs/host/vcs_config.hpp b/profiles/vcs/host/vcs_config.hpp index be06c21..1d5a36e 100644 --- a/profiles/vcs/host/vcs_config.hpp +++ b/profiles/vcs/host/vcs_config.hpp @@ -180,8 +180,8 @@ struct WidescreenConfiguration { // materially changes CloudWorks' appearance. struct VolumetricCloudsConfiguration { bool enabled{false}; - std::uint32_t downscale_div{2u}; - std::uint32_t layers{3u}; + std::uint32_t downscale_div{4u}; + std::uint32_t layers{2u}; std::uint32_t shadow_steps{4u}; float coverage_low{0.35f}; float coverage_mid{0.25f};