/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "renderer/PostprocManager.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/ShaderManager.h" #include "lib/bits.h" #include "maths/MathUtil.h" #include "ps/ConfigDB.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/World.h" #include "renderer/backend/IDevice.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "tools/atlas/GameInterface/GameLoop.h" #include namespace { void DrawFullscreenQuad( Renderer::Backend::IVertexInputLayout* vertexInputLayout, Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { float quadVerts[] = { 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f }; const bool flip = deviceCommandContext->GetDevice()->GetBackend() == Renderer::Backend::Backend::VULKAN; const float bottomV = flip ? 1.0 : 0.0f; const float topV = flip ? 0.0f : 1.0f; float quadTex[] = { 1.0f, topV, 0.0f, topV, 0.0f, bottomV, 0.0f, bottomV, 1.0f, bottomV, 1.0f, topV }; deviceCommandContext->SetVertexInputLayout(vertexInputLayout); deviceCommandContext->SetVertexBufferData( 0, quadVerts, std::size(quadVerts) * sizeof(quadVerts[0])); deviceCommandContext->SetVertexBufferData( 1, quadTex, std::size(quadTex) * sizeof(quadTex[0])); deviceCommandContext->Draw(0, 6); } } // anonymous namespace CPostprocManager::CPostprocManager(Renderer::Backend::IDevice* device) : m_Device(device), m_IsInitialized(false), m_PostProcEffect(L"default"), m_WhichBuffer(true), m_Sharpness(0.3f), m_UsingMultisampleBuffer(false), m_MultisampleCount(0) { } CPostprocManager::~CPostprocManager() { Cleanup(); } bool CPostprocManager::IsEnabled() const { const bool isDepthStencilFormatPresent = m_Device->GetPreferredDepthStencilFormat( Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, true, true) != Renderer::Backend::Format::UNDEFINED; return g_RenderingOptions.GetPostProc() && m_Device->GetBackend() != Renderer::Backend::Backend::GL_ARB && isDepthStencilFormatPresent; } void CPostprocManager::Cleanup() { if (!m_IsInitialized) // Only cleanup if previously used return; m_CaptureFramebuffer.reset(); m_PingFramebuffer.reset(); m_PongFramebuffer.reset(); m_ColorTex1.reset(); m_ColorTex2.reset(); m_DepthTex.reset(); for (BlurScale& scale : m_BlurScales) { for (BlurScale::Step& step : scale.steps) { step.framebuffer.reset(); step.texture.reset(); } } } void CPostprocManager::Initialize() { if (m_IsInitialized) return; const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, {Renderer::Backend::VertexAttributeStream::UV0, Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1}, }}; m_VertexInputLayout = g_Renderer.GetVertexInputLayout(attributes); const uint32_t maxSamples = m_Device->GetCapabilities().maxSampleCount; const uint32_t possibleSampleCounts[] = {2, 4, 8, 16}; std::copy_if( std::begin(possibleSampleCounts), std::end(possibleSampleCounts), std::back_inserter(m_AllowedSampleCounts), [maxSamples](const uint32_t sampleCount) { return sampleCount <= maxSamples; } ); // The screen size starts out correct and then must be updated with Resize() RecalculateSize(g_Renderer.GetWidth(), g_Renderer.GetHeight()); RecreateBuffers(); m_IsInitialized = true; // Once we have initialised the buffers, we can update the techniques. UpdateAntiAliasingTechnique(); UpdateSharpeningTechnique(); UpdateSharpnessFactor(); CStr upscaleName; CFG_GET_VAL("renderer.upscale.technique", upscaleName); SetUpscaleTechnique(upscaleName); // This might happen after the map is loaded and the effect chosen SetPostEffect(m_PostProcEffect); if (m_Device->GetCapabilities().computeShaders) m_DownscaleComputeTech = g_Renderer.GetShaderManager().LoadEffect(CStrIntern("compute_downscale")); } void CPostprocManager::Resize() { RecalculateSize(g_Renderer.GetWidth(), g_Renderer.GetHeight()); // If the buffers were intialized, recreate them to the new size. if (m_IsInitialized) RecreateBuffers(); } void CPostprocManager::RecreateBuffers() { Cleanup(); #define GEN_BUFFER_RGBA(name, w, h) \ name = m_Device->CreateTexture2D( \ "PostProc" #name, \ Renderer::Backend::ITexture::Usage::SAMPLED | \ Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT | \ Renderer::Backend::ITexture::Usage::TRANSFER_SRC | \ Renderer::Backend::ITexture::Usage::TRANSFER_DST, \ Renderer::Backend::Format::R8G8B8A8_UNORM, w, h, \ Renderer::Backend::Sampler::MakeDefaultSampler( \ Renderer::Backend::Sampler::Filter::LINEAR, \ Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)); // Two fullscreen ping-pong textures. GEN_BUFFER_RGBA(m_ColorTex1, m_Width, m_Height); GEN_BUFFER_RGBA(m_ColorTex2, m_Width, m_Height); if (m_UnscaledWidth != m_Width && m_Device->GetCapabilities().computeShaders) { const uint32_t usage = Renderer::Backend::ITexture::Usage::TRANSFER_SRC | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT | Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::STORAGE; m_UnscaledTexture1 = m_Device->CreateTexture2D( "PostProcUnscaledTexture1", usage, Renderer::Backend::Format::R8G8B8A8_UNORM, m_UnscaledWidth, m_UnscaledHeight, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)); m_UnscaledTexture2 = m_Device->CreateTexture2D( "PostProcUnscaledTexture2", usage, Renderer::Backend::Format::R8G8B8A8_UNORM, m_UnscaledWidth, m_UnscaledHeight, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::LOAD; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.texture = m_UnscaledTexture1.get(); m_UnscaledFramebuffer1 = m_Device->CreateFramebuffer("PostprocUnscaledFramebuffer1", &colorAttachment, nullptr); colorAttachment.texture = m_UnscaledTexture2.get(); m_UnscaledFramebuffer2 = m_Device->CreateFramebuffer("PostprocUnscaledFramebuffer2", &colorAttachment, nullptr); } // Textures for several blur sizes. It would be possible to reuse // m_BlurTex2b, thus avoiding the need for m_BlurTex4b and m_BlurTex8b, though given // that these are fairly small it's probably not worth complicating the coordinates passed // to the blur helper functions. uint32_t width = m_Width / 2, height = m_Height / 2; for (BlurScale& scale : m_BlurScales) { for (BlurScale::Step& step : scale.steps) { GEN_BUFFER_RGBA(step.texture, width, height); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = step.texture.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::LOAD; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; step.framebuffer = m_Device->CreateFramebuffer( "BlurScaleStepFramebuffer", &colorAttachment, nullptr); } width = std::max(1u, width / 2); height = std::max(1u, height / 2); } #undef GEN_BUFFER_RGBA // Allocate the Depth/Stencil texture. m_DepthTex = m_Device->CreateTexture2D("PostProcDepthTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, m_Device->GetPreferredDepthStencilFormat( Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, true, true), m_Width, m_Height, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)); // Set up the framebuffers with some initial textures. Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_ColorTex1.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::DONT_CARE; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment depthStencilAttachment{}; depthStencilAttachment.texture = m_DepthTex.get(); depthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; depthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_CaptureFramebuffer = m_Device->CreateFramebuffer("PostprocCaptureFramebuffer", &colorAttachment, &depthStencilAttachment); colorAttachment.texture = m_ColorTex1.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::LOAD; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_PingFramebuffer = m_Device->CreateFramebuffer("PostprocPingFramebuffer", &colorAttachment, nullptr); colorAttachment.texture = m_ColorTex2.get(); m_PongFramebuffer = m_Device->CreateFramebuffer("PostprocPongFramebuffer", &colorAttachment, nullptr); if (!m_CaptureFramebuffer || !m_PingFramebuffer || !m_PongFramebuffer) { LOGWARNING("Failed to create postproc framebuffers"); g_RenderingOptions.SetPostProc(false); } if (m_UsingMultisampleBuffer) { DestroyMultisampleBuffer(); CreateMultisampleBuffer(); } } void CPostprocManager::ApplyBlurDownscale2x( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IFramebuffer* framebuffer, Renderer::Backend::ITexture* inTex, int inWidth, int inHeight) { deviceCommandContext->BeginFramebufferPass(framebuffer); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = inWidth / 2; viewportRect.height = inHeight / 2; deviceCommandContext->SetViewports(1, &viewportRect); // Get bloom shader with instructions to simply copy texels. CShaderDefines defines; defines.Add(str_BLOOM_NOP, str_1); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_bloom, defines); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = tech->GetShader(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_renderedTex), inTex); DrawFullscreenQuad(m_VertexInputLayout, deviceCommandContext); deviceCommandContext->EndPass(); deviceCommandContext->EndFramebufferPass(); } void CPostprocManager::ApplyBlurGauss( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::ITexture* inTex, Renderer::Backend::ITexture* tempTex, Renderer::Backend::IFramebuffer* tempFramebuffer, Renderer::Backend::IFramebuffer* outFramebuffer, int inWidth, int inHeight) { deviceCommandContext->BeginFramebufferPass(tempFramebuffer); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = inWidth; viewportRect.height = inHeight; deviceCommandContext->SetViewports(1, &viewportRect); // Get bloom shader, for a horizontal Gaussian blur pass. CShaderDefines defines2; defines2.Add(str_BLOOM_PASS_H, str_1); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_bloom, defines2); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = tech->GetShader(); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_renderedTex), inTex); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_texSize), inWidth, inHeight); DrawFullscreenQuad(m_VertexInputLayout, deviceCommandContext); deviceCommandContext->EndPass(); deviceCommandContext->EndFramebufferPass(); deviceCommandContext->BeginFramebufferPass(outFramebuffer); deviceCommandContext->SetViewports(1, &viewportRect); // Get bloom shader, for a vertical Gaussian blur pass. CShaderDefines defines3; defines3.Add(str_BLOOM_PASS_V, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_bloom, defines3); deviceCommandContext->SetGraphicsPipelineState( tech->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); shader = tech->GetShader(); // Our input texture to the shader is the output of the horizontal pass. deviceCommandContext->SetTexture( shader->GetBindingSlot(str_renderedTex), tempTex); deviceCommandContext->SetUniform( shader->GetBindingSlot(str_texSize), inWidth, inHeight); DrawFullscreenQuad(m_VertexInputLayout, deviceCommandContext); deviceCommandContext->EndPass(); deviceCommandContext->EndFramebufferPass(); } void CPostprocManager::ApplyBlur( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { uint32_t width = m_Width, height = m_Height; Renderer::Backend::ITexture* previousTexture = (m_WhichBuffer ? m_ColorTex1 : m_ColorTex2).get(); for (BlurScale& scale : m_BlurScales) { ApplyBlurDownscale2x(deviceCommandContext, scale.steps[0].framebuffer.get(), previousTexture, width, height); width /= 2; height /= 2; ApplyBlurGauss(deviceCommandContext, scale.steps[0].texture.get(), scale.steps[1].texture.get(), scale.steps[1].framebuffer.get(), scale.steps[0].framebuffer.get(), width, height); } } Renderer::Backend::IFramebuffer* CPostprocManager::PrepareAndGetOutputFramebuffer() { ENSURE(m_IsInitialized); // Leaves m_PingFramebuffer selected for rendering; m_WhichBuffer stays true at this point. m_WhichBuffer = true; return m_UsingMultisampleBuffer ? m_MultisampleFramebuffer.get() : m_CaptureFramebuffer.get(); } void CPostprocManager::UpscaleTextureByCompute( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, CShaderTechnique* shaderTechnique, Renderer::Backend::ITexture* source, Renderer::Backend::ITexture* destination) { Renderer::Backend::IShaderProgram* shaderProgram = shaderTechnique->GetShader(); const std::array screenSize{{ static_cast(m_Width), static_cast(m_Height), static_cast(m_UnscaledWidth), static_cast(m_UnscaledHeight)}}; constexpr uint32_t threadGroupWorkRegionDim = 16; const uint32_t dispatchGroupCountX = DivideRoundUp(m_UnscaledWidth, threadGroupWorkRegionDim); const uint32_t dispatchGroupCountY = DivideRoundUp(m_UnscaledHeight, threadGroupWorkRegionDim); deviceCommandContext->BeginComputePass(); deviceCommandContext->SetComputePipelineState( shaderTechnique->GetComputePipelineState()); deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_screenSize), screenSize); deviceCommandContext->SetTexture(shaderProgram->GetBindingSlot(str_inTex), source); deviceCommandContext->SetStorageTexture(shaderProgram->GetBindingSlot(str_outTex), destination); deviceCommandContext->Dispatch(dispatchGroupCountX, dispatchGroupCountY, 1); deviceCommandContext->EndComputePass(); } void CPostprocManager::UpscaleTextureByFullscreenQuad( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, CShaderTechnique* shaderTechnique, Renderer::Backend::ITexture* source, Renderer::Backend::IFramebuffer* destination) { Renderer::Backend::IShaderProgram* shaderProgram = shaderTechnique->GetShader(); const std::array screenSize{{ static_cast(m_Width), static_cast(m_Height), static_cast(m_UnscaledWidth), static_cast(m_UnscaledHeight)}}; deviceCommandContext->BeginFramebufferPass(destination); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = destination->GetWidth(); viewportRect.height = destination->GetHeight(); deviceCommandContext->SetViewports(1, &viewportRect); deviceCommandContext->SetGraphicsPipelineState( shaderTechnique->GetGraphicsPipelineState()); deviceCommandContext->BeginPass(); deviceCommandContext->SetTexture( shaderProgram->GetBindingSlot(str_inTex), source); deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_screenSize), screenSize); DrawFullscreenQuad(m_VertexInputLayout, deviceCommandContext); deviceCommandContext->EndPass(); deviceCommandContext->EndFramebufferPass(); } void CPostprocManager::ApplySharpnessAfterScale( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, CShaderTechnique* shaderTechnique, Renderer::Backend::ITexture* source, Renderer::Backend::ITexture* destination) { Renderer::Backend::IShaderProgram* shaderProgram = shaderTechnique->GetShader(); // Recommended sharpness for RCAS. constexpr float sharpness = 0.2f; const std::array screenSize{ { static_cast(m_Width), static_cast(m_Height), static_cast(m_UnscaledWidth), static_cast(m_UnscaledHeight)} }; constexpr uint32_t threadGroupWorkRegionDim = 16; const uint32_t dispatchGroupCountX = DivideRoundUp(m_UnscaledWidth, threadGroupWorkRegionDim); const uint32_t dispatchGroupCountY = DivideRoundUp(m_UnscaledHeight, threadGroupWorkRegionDim); deviceCommandContext->BeginComputePass(); deviceCommandContext->SetComputePipelineState( shaderTechnique->GetComputePipelineState()); deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_sharpness), sharpness); deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_screenSize), screenSize); deviceCommandContext->SetTexture(shaderProgram->GetBindingSlot(str_inTex), source); deviceCommandContext->SetStorageTexture( shaderProgram->GetBindingSlot(str_outTex), destination); deviceCommandContext->Dispatch(dispatchGroupCountX, dispatchGroupCountY, 1); deviceCommandContext->EndComputePass(); } void CPostprocManager::DownscaleTextureByCompute( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, CShaderTechnique* shaderTechnique, Renderer::Backend::ITexture* source, Renderer::Backend::ITexture* destination) { Renderer::Backend::IShaderProgram* shaderProgram = shaderTechnique->GetShader(); const std::array screenSize{{ static_cast(m_Width), static_cast(m_Height), static_cast(m_UnscaledWidth), static_cast(m_UnscaledHeight)}}; constexpr uint32_t threadGroupWorkRegionDim = 8; const uint32_t dispatchGroupCountX = DivideRoundUp(m_UnscaledWidth, threadGroupWorkRegionDim); const uint32_t dispatchGroupCountY = DivideRoundUp(m_UnscaledHeight, threadGroupWorkRegionDim); deviceCommandContext->BeginComputePass(); deviceCommandContext->SetComputePipelineState( shaderTechnique->GetComputePipelineState()); deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_screenSize), screenSize); deviceCommandContext->SetTexture(shaderProgram->GetBindingSlot(str_inTex), source); deviceCommandContext->SetStorageTexture(shaderProgram->GetBindingSlot(str_outTex), destination); deviceCommandContext->Dispatch(dispatchGroupCountX, dispatchGroupCountY, 1); deviceCommandContext->EndComputePass(); } void CPostprocManager::BlitOutputFramebuffer( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Renderer::Backend::IFramebuffer* destination) { ENSURE(m_IsInitialized); GPU_SCOPED_LABEL(deviceCommandContext, "Copy postproc to backbuffer"); Renderer::Backend::ITexture* previousTexture = (m_WhichBuffer ? m_ColorTex1 : m_ColorTex2).get(); if (ShouldUpscale()) { if (m_UpscaleComputeTech) { Renderer::Backend::ITexture* unscaledTexture = m_RCASComputeTech ? m_UnscaledTexture1.get() : m_UnscaledTexture2.get(); UpscaleTextureByCompute(deviceCommandContext, m_UpscaleComputeTech.get(), previousTexture, unscaledTexture); if (m_RCASComputeTech) ApplySharpnessAfterScale(deviceCommandContext, m_RCASComputeTech.get(), m_UnscaledTexture1.get(), m_UnscaledTexture2.get()); Renderer::Backend::IDeviceCommandContext::Rect sourceRegion{}, destinationRegion{}; sourceRegion.width = m_UnscaledTexture2->GetWidth(); sourceRegion.height = m_UnscaledTexture2->GetHeight(); destinationRegion.width = destination->GetWidth(); destinationRegion.height = destination->GetHeight(); deviceCommandContext->BlitFramebuffer( m_UnscaledFramebuffer2.get(), destination, sourceRegion, destinationRegion, Renderer::Backend::Sampler::Filter::NEAREST); } else { UpscaleTextureByFullscreenQuad(deviceCommandContext, m_UpscaleTech.get(), previousTexture, destination); } } else if (ShouldDownscale()) { Renderer::Backend::IDeviceCommandContext::Rect sourceRegion{}; Renderer::Backend::Sampler::Filter samplerFilter{ Renderer::Backend::Sampler::Filter::NEAREST}; Renderer::Backend::IFramebuffer* source{nullptr}; if (m_DownscaleComputeTech) { DownscaleTextureByCompute(deviceCommandContext, m_DownscaleComputeTech.get(), previousTexture, m_UnscaledTexture1.get()); source = m_UnscaledFramebuffer1.get(); sourceRegion.width = m_UnscaledTexture1->GetWidth(); sourceRegion.height = m_UnscaledTexture1->GetHeight(); } else { source = (m_WhichBuffer ? m_PingFramebuffer : m_PongFramebuffer).get(); sourceRegion.width = source->GetWidth(); sourceRegion.height = source->GetHeight(); samplerFilter = Renderer::Backend::Sampler::Filter::LINEAR; } Renderer::Backend::IDeviceCommandContext::Rect destinationRegion{}; destinationRegion.width = destination->GetWidth(); destinationRegion.height = destination->GetHeight(); deviceCommandContext->BlitFramebuffer( source, destination, sourceRegion, destinationRegion, samplerFilter); } else { Renderer::Backend::IFramebuffer* source = (m_WhichBuffer ? m_PingFramebuffer : m_PongFramebuffer).get(); // We blit to the backbuffer from the previous active buffer. Renderer::Backend::IDeviceCommandContext::Rect region{}; region.width = std::min(source->GetWidth(), destination->GetWidth()); region.height = std::min(source->GetHeight(), destination->GetHeight()); deviceCommandContext->BlitFramebuffer( source, destination, region, region, Renderer::Backend::Sampler::Filter::NEAREST); } } void CPostprocManager::ApplyEffect( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderTechniquePtr& shaderTech, int pass) { // Select the other framebuffer for rendering. Renderer::Backend::IFramebuffer* framebuffer = (m_WhichBuffer ? m_PongFramebuffer : m_PingFramebuffer).get(); deviceCommandContext->BeginFramebufferPass(framebuffer); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = framebuffer->GetWidth(); viewportRect.height = framebuffer->GetHeight(); deviceCommandContext->SetViewports(1, &viewportRect); deviceCommandContext->SetGraphicsPipelineState( shaderTech->GetGraphicsPipelineState(pass)); deviceCommandContext->BeginPass(); Renderer::Backend::IShaderProgram* shader = shaderTech->GetShader(pass); // Use the textures from the current framebuffer as input to the shader. // We also bind a bunch of other textures and parameters, but since // this only happens once per frame the overhead is negligible. deviceCommandContext->SetTexture( shader->GetBindingSlot(str_renderedTex), m_WhichBuffer ? m_ColorTex1.get() : m_ColorTex2.get()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_depthTex), m_DepthTex.get()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_blurTex2), m_BlurScales[0].steps[0].texture.get()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_blurTex4), m_BlurScales[1].steps[0].texture.get()); deviceCommandContext->SetTexture( shader->GetBindingSlot(str_blurTex8), m_BlurScales[2].steps[0].texture.get()); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_width), m_Width); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_height), m_Height); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_zNear), m_NearPlane); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_zFar), m_FarPlane); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_sharpness), m_Sharpness); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_brightness), g_LightEnv.m_Brightness); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_hdr), g_LightEnv.m_Contrast); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_saturation), g_LightEnv.m_Saturation); deviceCommandContext->SetUniform(shader->GetBindingSlot(str_bloom), g_LightEnv.m_Bloom); DrawFullscreenQuad(m_VertexInputLayout, deviceCommandContext); deviceCommandContext->EndPass(); deviceCommandContext->EndFramebufferPass(); m_WhichBuffer = !m_WhichBuffer; } void CPostprocManager::ApplyPostproc( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { ENSURE(m_IsInitialized); // Don't do anything if we are using the default effect and no AA. const bool hasEffects = m_PostProcEffect != L"default"; const bool hasARB = m_Device->GetBackend() == Renderer::Backend::Backend::GL_ARB; const bool hasAA = m_AATech && !hasARB; const bool hasSharp = m_SharpTech && !hasARB; if (!hasEffects && !hasAA && !hasSharp) return; GPU_SCOPED_LABEL(deviceCommandContext, "Render postproc"); if (hasEffects) { // First render blur textures. Note that this only happens ONLY ONCE, before any effects are applied! // (This may need to change depending on future usage, however that will have a fps hit) ApplyBlur(deviceCommandContext); for (int pass = 0; pass < m_PostProcTech->GetNumPasses(); ++pass) ApplyEffect(deviceCommandContext, m_PostProcTech, pass); } if (hasAA) { for (int pass = 0; pass < m_AATech->GetNumPasses(); ++pass) ApplyEffect(deviceCommandContext, m_AATech, pass); } if (hasSharp && !ShouldUpscale()) { for (int pass = 0; pass < m_SharpTech->GetNumPasses(); ++pass) ApplyEffect(deviceCommandContext, m_SharpTech, pass); } } // Generate list of available effect-sets std::vector CPostprocManager::GetPostEffects() { std::vector effects; const VfsPath folder(L"shaders/effects/postproc/"); VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, folder, 0, pathnames) < 0) LOGERROR("Error finding Post effects in '%s'", folder.string8()); for (const VfsPath& path : pathnames) if (path.Extension() == L".xml") effects.push_back(path.Basename().string()); // Add the default "null" effect to the list. effects.push_back(L"default"); sort(effects.begin(), effects.end()); return effects; } void CPostprocManager::SetPostEffect(const CStrW& name) { if (m_IsInitialized) { if (name != L"default") { CStrW n = L"postproc/" + name; m_PostProcTech = g_Renderer.GetShaderManager().LoadEffect(CStrIntern(n.ToUTF8())); } } m_PostProcEffect = name; } void CPostprocManager::UpdateAntiAliasingTechnique() { if (m_Device->GetBackend() == Renderer::Backend::Backend::GL_ARB || !m_IsInitialized) return; CStr newAAName; CFG_GET_VAL("antialiasing", newAAName); if (m_AAName == newAAName) return; m_AAName = newAAName; m_AATech.reset(); if (m_UsingMultisampleBuffer) { m_UsingMultisampleBuffer = false; DestroyMultisampleBuffer(); } // We have to hardcode names in the engine, because anti-aliasing // techinques strongly depend on the graphics pipeline. // We might use enums in future though. constexpr std::string_view msaaPrefix{"msaa"}; if (m_AAName == str_fxaa.string()) { m_AATech = g_Renderer.GetShaderManager().LoadEffect(str_fxaa); } else if (m_AAName.size() > msaaPrefix.size() && std::string_view{m_AAName}.substr(0, msaaPrefix.size()) == msaaPrefix) { // We don't want to enable MSAA in Atlas, because it uses wxWidgets and its canvas. if (g_AtlasGameLoop && g_AtlasGameLoop->running) return; if (!m_Device->GetCapabilities().multisampling || m_AllowedSampleCounts.empty()) { LOGWARNING("MSAA is unsupported."); return; } std::stringstream ss(m_AAName.substr(msaaPrefix.size())); ss >> m_MultisampleCount; if (std::find(std::begin(m_AllowedSampleCounts), std::end(m_AllowedSampleCounts), m_MultisampleCount) == std::end(m_AllowedSampleCounts)) { m_MultisampleCount = std::min(4u, m_Device->GetCapabilities().maxSampleCount); LOGWARNING("Wrong MSAA sample count: %s.", m_AAName.EscapeToPrintableASCII().c_str()); } m_UsingMultisampleBuffer = true; CreateMultisampleBuffer(); } } void CPostprocManager::UpdateSharpeningTechnique() { if (m_Device->GetBackend() == Renderer::Backend::Backend::GL_ARB || !m_IsInitialized) return; CStr newSharpName; CFG_GET_VAL("sharpening", newSharpName); if (m_SharpName == newSharpName) return; m_SharpName = newSharpName; m_SharpTech.reset(); if (m_SharpName == "cas") { m_SharpTech = g_Renderer.GetShaderManager().LoadEffect(CStrIntern(m_SharpName)); } } void CPostprocManager::UpdateSharpnessFactor() { CFG_GET_VAL("sharpness", m_Sharpness); } void CPostprocManager::SetUpscaleTechnique(const CStr& upscaleName) { m_UpscaleTech.reset(); m_UpscaleComputeTech.reset(); m_RCASComputeTech.reset(); if (m_Device->GetCapabilities().computeShaders && upscaleName == "fsr") { m_UpscaleComputeTech = g_Renderer.GetShaderManager().LoadEffect(str_compute_upscale_fsr); m_RCASComputeTech = g_Renderer.GetShaderManager().LoadEffect(str_compute_rcas); } else if (upscaleName == "pixelated") { m_UpscaleTech = g_Renderer.GetShaderManager().LoadEffect(str_upscale_nearest); } else { m_UpscaleTech = g_Renderer.GetShaderManager().LoadEffect(str_upscale_bilinear); } } void CPostprocManager::SetDepthBufferClipPlanes(float nearPlane, float farPlane) { m_NearPlane = nearPlane; m_FarPlane = farPlane; } void CPostprocManager::CreateMultisampleBuffer() { m_MultisampleColorTex = m_Device->CreateTexture("PostProcColorMS", Renderer::Backend::ITexture::Type::TEXTURE_2D_MULTISAMPLE, Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT | Renderer::Backend::ITexture::Usage::TRANSFER_SRC, Renderer::Backend::Format::R8G8B8A8_UNORM, m_Width, m_Height, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE), 1, m_MultisampleCount); // Allocate the Depth/Stencil texture. m_MultisampleDepthTex = m_Device->CreateTexture("PostProcDepthMS", Renderer::Backend::ITexture::Type::TEXTURE_2D_MULTISAMPLE, Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT | Renderer::Backend::ITexture::Usage::TRANSFER_SRC, m_Device->GetPreferredDepthStencilFormat( Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT | Renderer::Backend::ITexture::Usage::TRANSFER_SRC, true, true), m_Width, m_Height, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE), 1, m_MultisampleCount); // Set up the framebuffers with some initial textures. Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = m_MultisampleColorTex.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::DONT_CARE; colorAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; Renderer::Backend::SDepthStencilAttachment depthStencilAttachment{}; depthStencilAttachment.texture = m_MultisampleDepthTex.get(); depthStencilAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::CLEAR; depthStencilAttachment.storeOp = Renderer::Backend::AttachmentStoreOp::STORE; m_MultisampleFramebuffer = m_Device->CreateFramebuffer( "PostprocMultisampleFramebuffer", &colorAttachment, &depthStencilAttachment); if (!m_MultisampleFramebuffer) { LOGERROR("Failed to create postproc multisample framebuffer"); m_UsingMultisampleBuffer = false; DestroyMultisampleBuffer(); } } void CPostprocManager::DestroyMultisampleBuffer() { if (m_UsingMultisampleBuffer) return; m_MultisampleFramebuffer.reset(); m_MultisampleColorTex.reset(); m_MultisampleDepthTex.reset(); } bool CPostprocManager::IsMultisampleEnabled() const { return m_UsingMultisampleBuffer; } void CPostprocManager::ResolveMultisampleFramebuffer( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (!m_UsingMultisampleBuffer) return; GPU_SCOPED_LABEL(deviceCommandContext, "Resolve postproc multisample"); deviceCommandContext->ResolveFramebuffer( m_MultisampleFramebuffer.get(), m_PingFramebuffer.get()); } void CPostprocManager::RecalculateSize(const uint32_t width, const uint32_t height) { if (m_Device->GetBackend() == Renderer::Backend::Backend::GL_ARB) { m_Scale = 1.0f; return; } CFG_GET_VAL("renderer.scale", m_Scale); if (m_Scale < 0.25f || m_Scale > 2.0f) { LOGWARNING("Invalid renderer scale: %0.2f", m_Scale); m_Scale = 1.0f; } m_UnscaledWidth = width; m_UnscaledHeight = height; m_Width = m_UnscaledWidth * m_Scale; m_Height = m_UnscaledHeight * m_Scale; } bool CPostprocManager::ShouldUpscale() const { return m_Width < m_UnscaledWidth; } bool CPostprocManager::ShouldDownscale() const { return m_Width > m_UnscaledWidth; }