diff --git a/source/ps/CStrInternStatic.h b/source/ps/CStrInternStatic.h index 8c9548e189..99d007f790 100644 --- a/source/ps/CStrInternStatic.h +++ b/source/ps/CStrInternStatic.h @@ -97,6 +97,7 @@ X(color) X(colorAdd) X(colorMul) X(compute_rcas) +X(compute_resolve_pbr) X(compute_skinning) X(compute_upscale_fsr) X(debug_line) @@ -104,6 +105,7 @@ X(debug_overlay) X(delta) X(depthTex) X(dummy) +X(exposure) X(foamTex) X(fogColor) X(fogParams) diff --git a/source/renderer/PostprocManager.cpp b/source/renderer/PostprocManager.cpp index 03ba488d85..084bc12480 100644 --- a/source/renderer/PostprocManager.cpp +++ b/source/renderer/PostprocManager.cpp @@ -128,6 +128,7 @@ void CPostprocManager::Cleanup() m_PingFramebuffer.reset(); m_PongFramebuffer.reset(); + m_PBROutputTexture.reset(); m_ColorTex1.reset(); m_ColorTex2.reset(); m_DepthTex.reset(); @@ -147,6 +148,9 @@ void CPostprocManager::Initialize() if (m_IsInitialized) return; + if (g_ConfigDB.Get("pbr", false)) + InitializePBR(); + const std::array attributes{{ {Renderer::Backend::VertexAttributeStream::POSITION, Renderer::Backend::Format::R32G32_SFLOAT, 0, sizeof(float) * 2, @@ -183,6 +187,40 @@ void CPostprocManager::Initialize() m_DownscaleComputeTech = g_Renderer.GetShaderManager().LoadEffect(CStrIntern("compute_downscale")); } +void CPostprocManager::InitializePBR() +{ + const bool PBRSupported{ + m_Device->GetCapabilities().computeShaders && + m_Device->IsFramebufferFormatSupported(Renderer::Backend::Format::R16G16B16A16_SFLOAT)}; + if (!PBRSupported) + { + m_PBREnabled = false; + LOGWARNING("PBR is unsupported for the current renderer backend"); + return; + } + + m_FramebufferColorFormat = Renderer::Backend::Format::R16G16B16A16_SFLOAT; + const std::string framebufferFormatName{g_ConfigDB.Get("pbr.framebufferformat", std::string{})}; + if (framebufferFormatName == "rgba32") + { + if (m_Device->IsFramebufferFormatSupported(Renderer::Backend::Format::R32G32B32A32_SFLOAT)) + m_FramebufferColorFormat = Renderer::Backend::Format::R32G32B32A32_SFLOAT; + else + LOGWARNING("%s is unsupported", framebufferFormatName); + } + else if (framebufferFormatName == "r11g11b10") + { + if (m_Device->IsFramebufferFormatSupported(Renderer::Backend::Format::B10G11R11_UFLOAT)) + m_FramebufferColorFormat = Renderer::Backend::Format::B10G11R11_UFLOAT; + else + LOGWARNING("%s is unsupported", framebufferFormatName); + } + + m_ResolvePBRComputeTech = g_Renderer.GetShaderManager().LoadEffect(str_compute_resolve_pbr); + if (m_ResolvePBRComputeTech) + m_PBREnabled = true; +} + void CPostprocManager::Resize() { RecalculateSize(g_Renderer.GetWidth(), g_Renderer.GetHeight()); @@ -196,21 +234,33 @@ 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)); + const auto defaultSampler{ + Renderer::Backend::Sampler::MakeDefaultSampler( + Renderer::Backend::Sampler::Filter::LINEAR, + Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)}; + const uint32_t colorTextureUsage{ + Renderer::Backend::ITexture::Usage::SAMPLED | + Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT | + Renderer::Backend::ITexture::Usage::TRANSFER_SRC | + Renderer::Backend::ITexture::Usage::TRANSFER_DST}; + + if (m_PBREnabled) + { + m_PBROutputTexture = m_Device->CreateTexture2D( + "PostProcPBROutput", colorTextureUsage, + m_FramebufferColorFormat, + m_Width, m_Height, defaultSampler); + } // Two fullscreen ping-pong textures. - GEN_BUFFER_RGBA(m_ColorTex1, m_Width, m_Height); - GEN_BUFFER_RGBA(m_ColorTex2, m_Width, m_Height); + m_ColorTex1 = m_Device->CreateTexture2D( + "PostProcColor1", colorTextureUsage | (m_PBREnabled ? Renderer::Backend::ITexture::Usage::STORAGE : 0), + Renderer::Backend::Format::R8G8B8A8_UNORM, + m_Width, m_Height, defaultSampler); + m_ColorTex2 = m_Device->CreateTexture2D( + "PostProcColor2", colorTextureUsage | (m_PBREnabled ? Renderer::Backend::ITexture::Usage::STORAGE : 0), + Renderer::Backend::Format::R8G8B8A8_UNORM, + m_Width, m_Height, defaultSampler); if (m_UnscaledWidth != m_Width && m_Device->GetCapabilities().computeShaders) { @@ -257,7 +307,10 @@ void CPostprocManager::RecreateBuffers() { for (BlurScale::Step& step : scale.steps) { - GEN_BUFFER_RGBA(step.texture, width, height); + step.texture = m_Device->CreateTexture2D( + "PostProcBlurStep", colorTextureUsage, + Renderer::Backend::Format::R8G8B8A8_UNORM, + width, height, defaultSampler); Renderer::Backend::SColorAttachment colorAttachment{}; colorAttachment.texture = step.texture.get(); colorAttachment.loadOp = Renderer::Backend::AttachmentLoadOp::LOAD; @@ -270,8 +323,6 @@ void CPostprocManager::RecreateBuffers() 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 | @@ -285,24 +336,14 @@ void CPostprocManager::RecreateBuffers() Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE)); + RecreateCaptureFramebuffer(); + // 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; + colorAttachment.clearColor = CColor{0.0f, 0.0f, 0.0f, 0.0f}; m_PingFramebuffer = m_Device->CreateFramebuffer("PostprocPingFramebuffer", &colorAttachment, nullptr); @@ -310,7 +351,7 @@ void CPostprocManager::RecreateBuffers() m_PongFramebuffer = m_Device->CreateFramebuffer("PostprocPongFramebuffer", &colorAttachment, nullptr); - if (!m_CaptureFramebuffer || !m_PingFramebuffer || !m_PongFramebuffer) + if (!m_PingFramebuffer || !m_PongFramebuffer) { LOGWARNING("Failed to create postproc framebuffers"); g_RenderingOptions.SetPostProc(false); @@ -323,6 +364,28 @@ void CPostprocManager::RecreateBuffers() } } +void CPostprocManager::RecreateCaptureFramebuffer() +{ + Renderer::Backend::SColorAttachment colorAttachment{}; + colorAttachment.texture = m_PBREnabled ? m_PBROutputTexture.get() : 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, m_UsingMultisampleBuffer ? nullptr : &depthStencilAttachment); + + if (!m_CaptureFramebuffer) + { + LOGWARNING("Failed to create postproc capture framebuffer"); + g_RenderingOptions.SetPostProc(false); + } +} void CPostprocManager::ApplyBlurDownscale2x( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, @@ -698,17 +761,15 @@ void CPostprocManager::ApplyPostproc( { 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 hasAA{m_AATech}; - const bool hasSharp{m_SharpTech}; - if (!hasEffects && !hasAA && !hasSharp) - return; - PROFILE3_GPU(deviceCommandContext, "Render postproc"); GPU_SCOPED_LABEL(deviceCommandContext, "Render postproc"); - if (hasEffects) + if (m_PBREnabled) + { + ResolvePBR(deviceCommandContext); + } + + if (!m_PBREnabled && m_PostProcEffect != L"default") { // 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) @@ -717,13 +778,13 @@ void CPostprocManager::ApplyPostproc( ApplyEffect(deviceCommandContext, m_PostProcTech, pass); } - if (hasAA) + if (m_AATech) { for (int pass = 0; pass < m_AATech->GetNumPasses(); ++pass) ApplyEffect(deviceCommandContext, m_AATech, pass); } - if (hasSharp && !ShouldUpscale()) + if (m_SharpTech && !ShouldUpscale()) { for (int pass = 0; pass < m_SharpTech->GetNumPasses(); ++pass) ApplyEffect(deviceCommandContext, m_SharpTech, pass); @@ -815,6 +876,8 @@ void CPostprocManager::UpdateAntiAliasingTechnique() m_UsingMultisampleBuffer = true; CreateMultisampleBuffer(); } + + RecreateCaptureFramebuffer(); } void CPostprocManager::UpdateSharpeningTechnique() @@ -871,7 +934,7 @@ void CPostprocManager::CreateMultisampleBuffer() 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, + m_FramebufferColorFormat, m_Width, m_Height, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE), 1, m_MultisampleCount); @@ -935,7 +998,42 @@ void CPostprocManager::ResolveMultisampleFramebuffer( GPU_SCOPED_LABEL(deviceCommandContext, "Resolve postproc multisample"); deviceCommandContext->ResolveFramebuffer( - m_MultisampleFramebuffer.get(), m_PingFramebuffer.get()); + m_MultisampleFramebuffer.get(), + m_PBREnabled ? m_CaptureFramebuffer.get() : m_PingFramebuffer.get()); +} + +void CPostprocManager::ResolvePBR( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext) +{ + ENSURE(m_PBREnabled); + ENSURE(m_ResolvePBRComputeTech); + + GPU_SCOPED_LABEL(deviceCommandContext, "Resolve PBR"); + + Renderer::Backend::IShaderProgram* shaderProgram{m_ResolvePBRComputeTech->GetShader()}; + + Renderer::Backend::ITexture* source{m_PBROutputTexture.get()}; + Renderer::Backend::ITexture* destination{m_ColorTex1.get()}; + + const std::array screenSize{{ + static_cast(m_Width), static_cast(m_Height), + 1.0f / static_cast(m_Width), 1.0f / static_cast(m_Height)}}; + + constexpr uint32_t threadGroupWorkRegionDim{8}; + const uint32_t dispatchGroupCountX{DivideRoundUp(m_Width, threadGroupWorkRegionDim)}; + const uint32_t dispatchGroupCountY{DivideRoundUp(m_Height, threadGroupWorkRegionDim)}; + + deviceCommandContext->BeginComputePass(); + deviceCommandContext->SetComputePipelineState( + m_ResolvePBRComputeTech->GetComputePipelineState()); + deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_screenSize), screenSize); + // Mapping [0.0, 1.0] to [-2.0, 2.0]. + deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_exposure), + std::exp2f((g_RenderingOptions.GetPBRBrightness() - 0.5f) * 4.0f)); + deviceCommandContext->SetTexture(shaderProgram->GetBindingSlot(str_inTex), source); + deviceCommandContext->SetStorageTexture(shaderProgram->GetBindingSlot(str_outTex), destination); + deviceCommandContext->Dispatch(dispatchGroupCountX, dispatchGroupCountY, 1); + deviceCommandContext->EndComputePass(); } void CPostprocManager::RecalculateSize(const uint32_t width, const uint32_t height) diff --git a/source/renderer/PostprocManager.h b/source/renderer/PostprocManager.h index a9b97d3984..a7b0dd6f7e 100644 --- a/source/renderer/PostprocManager.h +++ b/source/renderer/PostprocManager.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -95,9 +95,16 @@ public: float GetScale() const { return m_Scale; } + bool IsPBREnabled() const { return m_PBREnabled; } + + Renderer::Backend::Format GetFramebufferColorFormat() const { return m_FramebufferColorFormat; } + private: void CreateMultisampleBuffer(); void DestroyMultisampleBuffer(); + void RecreateCaptureFramebuffer(); + + void InitializePBR(); void RecalculateSize(const uint32_t width, const uint32_t height); @@ -127,8 +134,20 @@ private: Renderer::Backend::ITexture* source, Renderer::Backend::ITexture* destination); + /** + * Resolve PBR HDR output to SDR one. + */ + void ResolvePBR( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext); + Renderer::Backend::IDevice* m_Device = nullptr; + Renderer::Backend::Format m_FramebufferColorFormat{ + Renderer::Backend::Format::R8G8B8A8_UNORM}; + + bool m_PBREnabled{false}; + std::unique_ptr m_PBROutputTexture; + std::unique_ptr m_CaptureFramebuffer; // Two framebuffers, that we flip between at each shader pass. @@ -170,6 +189,8 @@ private: CStrW m_PostProcEffect; CShaderTechniquePtr m_PostProcTech; + CShaderTechniquePtr m_ResolvePBRComputeTech; + CStr m_SharpName; CShaderTechniquePtr m_SharpTech; float m_Sharpness; diff --git a/source/renderer/RenderingOptions.cpp b/source/renderer/RenderingOptions.cpp index 772efa85b1..a035c44ae7 100644 --- a/source/renderer/RenderingOptions.cpp +++ b/source/renderer/RenderingOptions.cpp @@ -271,6 +271,8 @@ void CRenderingOptions::ReadConfigAndSetupHooks() if (CRenderer::IsInitialised()) g_Renderer.GetTextureManager().OnQualityChanged(); }); + + m_ConfigHooks->Setup("pbr.brightness", m_PBRBrightness); } void CRenderingOptions::ClearHooks() diff --git a/source/renderer/RenderingOptions.h b/source/renderer/RenderingOptions.h index 05ab6dc52b..45760b738a 100644 --- a/source/renderer/RenderingOptions.h +++ b/source/renderer/RenderingOptions.h @@ -120,6 +120,8 @@ OPTION_CUSTOM_SETTER(NAME, TYPE); OPTION_GETTER(NAME, TYPE); OPTION_DEF(NAME, TY OPTION(DisplayFrustum, bool); OPTION(DisplayShadowsFrustum, bool); + OPTION(PBRBrightness, float); + // Cutscene Mode: while active, (most) visual overlays aren't rendered in order to give the scene a more "pure" // and real feel, like a movie. // The idea is for it to be inactive during normal gameplay, but enabled while playing cinema paths and diff --git a/source/renderer/WaterManager.cpp b/source/renderer/WaterManager.cpp index 21b0e1e436..1b83343f95 100644 --- a/source/renderer/WaterManager.cpp +++ b/source/renderer/WaterManager.cpp @@ -260,6 +260,11 @@ void WaterManager::RecreateOrLoadTexturesIfNeeded() Renderer::Backend::ITexture::Usage::DEPTH_STENCIL_ATTACHMENT, true, false); + Renderer::Backend::Format framebufferColorFormat{ + g_Renderer.GetPostprocManager().IsEnabled() + ? g_Renderer.GetPostprocManager().GetFramebufferColorFormat() + : Renderer::Backend::Format::R8G8B8A8_UNORM}; + // Create reflection textures. const bool needsReflectionTextures = g_RenderingOptions.GetWaterEffects() && @@ -269,7 +274,7 @@ void WaterManager::RecreateOrLoadTexturesIfNeeded() m_ReflectionTexture = m_Device->CreateTexture2D("WaterReflectionTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, - Renderer::Backend::Format::R8G8B8A8_UNORM, m_RefTextureSize, m_RefTextureSize, + framebufferColorFormat, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT)); @@ -312,7 +317,7 @@ void WaterManager::RecreateOrLoadTexturesIfNeeded() m_RefractionTexture = m_Device->CreateTexture2D("WaterRefractionTexture", Renderer::Backend::ITexture::Usage::SAMPLED | Renderer::Backend::ITexture::Usage::COLOR_ATTACHMENT, - Renderer::Backend::Format::R8G8B8A8_UNORM, m_RefTextureSize, m_RefTextureSize, + framebufferColorFormat, m_RefTextureSize, m_RefTextureSize, Renderer::Backend::Sampler::MakeDefaultSampler( Renderer::Backend::Sampler::Filter::LINEAR, Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT)); diff --git a/source/renderer/backend/gl/Device.cpp b/source/renderer/backend/gl/Device.cpp index 4d59696db0..39d55d2e95 100644 --- a/source/renderer/backend/gl/Device.cpp +++ b/source/renderer/backend/gl/Device.cpp @@ -968,7 +968,7 @@ bool CDevice::IsTextureFormatSupported(const Format format) const case Format::R16G16B16A16_SFLOAT: case Format::R32G32B32A32_SFLOAT: - supported = m_Capabilities.computeShaders && m_Capabilities.storage && ogl_HaveExtension("GL_ARB_texture_float"); + supported = m_Capabilities.computeShaders && m_Capabilities.storage && GLAD_GL_ARB_texture_float; break; default: diff --git a/source/renderer/backend/gl/Texture.cpp b/source/renderer/backend/gl/Texture.cpp index 85f6f19cde..2825acf9de 100644 --- a/source/renderer/backend/gl/Texture.cpp +++ b/source/renderer/backend/gl/Texture.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify