/** * Looking Glass * Copyright © 2017-2026 The Looking Glass Authors * https://looking-glass.io * * This program 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. * * This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "CPostProcessor.h" #include "CD3D12Device.h" #include "CDebug.h" #include "effect/CColorTransformEffect.h" #include "effect/CDownsampleEffect.h" #include "effect/CHDR16to10Effect.h" #include "effect/CRGB24Effect.h" #include #include #include namespace { bool NearlyEqual(float a, float b, float tolerance) { const float delta = a - b; return delta >= -tolerance && delta <= tolerance; } } bool IsIdentityColorTransform(const D12ColorTransform& transform) { static const float matrixTolerance = 1.0f / 1048576.0f; static const float lutTolerance = 1.0f / 65535.0f; if (transform.matrixEnabled) { for (unsigned row = 0; row < 3; ++row) for (unsigned column = 0; column < 4; ++column) { const float expected = row == column ? 1.0f : 0.0f; const float effective = transform.matrix[row][column] * transform.scalar; if (!NearlyEqual(effective, expected, matrixTolerance)) return false; } } if (transform.lutEnabled) for (unsigned i = 0; i < 4096; ++i) { const float expected = (float)i / 4095.0f; if (!NearlyEqual(transform.lut[i][0], expected, lutTolerance) || !NearlyEqual(transform.lut[i][1], expected, lutTolerance) || !NearlyEqual(transform.lut[i][2], expected, lutTolerance)) return false; } return true; } static void CopyHDRMetadata(D12FrameFormat& dst, const D12FrameFormat& src) { dst.hdrMetadata = src.hdrMetadata; dst.sdrWhiteLevel = src.sdrWhiteLevel; std::memcpy(dst.displayPrimary, src.displayPrimary, sizeof(dst.displayPrimary)); std::memcpy(dst.whitePoint, src.whitePoint, sizeof(dst.whitePoint)); dst.maxDisplayLuminance = src.maxDisplayLuminance; dst.minDisplayLuminance = src.minDisplayLuminance; dst.maxContentLightLevel = src.maxContentLightLevel; dst.maxFrameAverageLightLevel = src.maxFrameAverageLightLevel; } bool CPostProcessor::Init(std::shared_ptr dx12Device, bool enableEffects) { m_dx12Device = dx12Device; m_device = dx12Device->GetDevice(); m_effects.clear(); if (!enableEffects) return true; std::unique_ptr colorTransform(new CColorTransformEffect()); if (colorTransform->Init(m_device)) { DEBUG_INFO("Created post-processing effect: %s", colorTransform->GetName()); m_effects.push_back(std::move(colorTransform)); } else return false; std::unique_ptr downsample(new CDownsampleEffect()); if (downsample->Init(m_device)) { DEBUG_INFO("Created post-processing effect: %s", downsample->GetName()); m_effects.push_back(std::move(downsample)); } std::unique_ptr hdr16to10(new CHDR16to10Effect()); if (hdr16to10->Init(m_device)) { DEBUG_INFO("Created post-processing effect: %s", hdr16to10->GetName()); m_effects.push_back(std::move(hdr16to10)); } else return false; std::unique_ptr rgb24(new CRGB24Effect()); if (rgb24->Init(m_device)) { DEBUG_INFO("Created post-processing effect: %s", rgb24->GetName()); m_effects.push_back(std::move(rgb24)); } return true; } void CPostProcessor::Reset() { m_effects.clear(); m_dx12Device.reset(); m_device.Reset(); m_srcFormat = {}; m_dstFormat = {}; m_copyLayout = {}; m_copyEffect = nullptr; m_frameSize = 0; m_pitch = 0; m_effectsActive = false; m_configured = false; } bool CPostProcessor::HasSameEffectChain(const CPostProcessor& other) const { if (m_effects.size() != other.m_effects.size()) return false; for (size_t i = 0; i < m_effects.size(); ++i) if (std::strcmp(m_effects[i]->GetName(), other.m_effects[i]->GetName()) != 0) return false; return true; } bool CPostProcessor::ShareEffectState(const CPostProcessor& other) { if (!HasSameEffectChain(other)) return false; for (size_t i = 0; i < m_effects.size(); ++i) m_effects[i]->ShareState(*other.m_effects[i]); return true; } void CPostProcessor::Update(const D12FrameFormat& srcFormat) { for (const auto& effect : m_effects) effect->Update(srcFormat); } bool CPostProcessor::NeedsReconfigure(const D12FrameFormat& srcFormat) const { if (!m_configured || srcFormat.desc.Width != m_srcFormat.desc.Width || srcFormat.desc.Height != m_srcFormat.desc.Height || srcFormat.desc.Format != m_srcFormat.desc.Format || srcFormat.format != m_srcFormat.format || srcFormat.width != m_srcFormat.width || srcFormat.height != m_srcFormat.height || srcFormat.hdr != m_srcFormat.hdr || srcFormat.hdrPQ != m_srcFormat.hdrPQ || srcFormat.colorTransform != m_srcFormat.colorTransform) return true; for (const auto& effect : m_effects) if (effect->NeedsReconfigure()) return true; return false; } bool CPostProcessor::RequiresFullDamage() const { for (const auto& effect : m_effects) if (effect->RequiresFullDamage()) return true; return false; } bool CPostProcessor::Configure(const D12FrameFormat& srcFormat, bool * formatChanged) { if (formatChanged) *formatChanged = false; if (!NeedsReconfigure(srcFormat)) { // Static HDR metadata may change independently of the resource format. // Propagate it without recreating resources or post-processing state. CopyHDRMetadata(m_srcFormat, srcFormat); CopyHDRMetadata(m_dstFormat, srcFormat); return true; } D12FrameFormat oldDst = m_dstFormat; D12FrameFormat cur = srcFormat; CPostProcessEffect * outputEffect = nullptr; bool effectsActive = false; for (const auto& effect : m_effects) { D12FrameFormat dst = cur; switch (effect->SetFormat(m_device, cur, dst)) { case PostProcessStatus::SUCCESS: effect->Enabled = true; effectsActive = true; cur = dst; outputEffect = effect.get(); DEBUG_INFO("Post-processing effect active: %s", effect->GetName()); break; case PostProcessStatus::BYPASS_EFFECT: effect->Enabled = false; break; case PostProcessStatus::FAILED: DEBUG_ERROR("Failed to configure post-processing effect: %s", effect->GetName()); return false; } } D3D12_PLACED_SUBRESOURCE_FOOTPRINT copyLayout = {}; CPostProcessEffect * copyEffect = nullptr; unsigned pitch = 0; unsigned dataHeight = 0; if (outputEffect && outputEffect->GetCopyLayout(&pitch, &dataHeight)) copyEffect = outputEffect; else { if (cur.desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D) { DEBUG_ERROR("Post-processing output has no copy implementation"); return false; } m_device->GetCopyableFootprints( &cur.desc, 0, 1, 0, ©Layout, nullptr, nullptr, nullptr); pitch = copyLayout.Footprint.RowPitch; dataHeight = cur.desc.Height; } if (!pitch || !dataHeight || pitch > (std::numeric_limits::max)() / dataHeight) { DEBUG_ERROR("Invalid post-processing output layout"); return false; } const size_t frameSize = (size_t)pitch * dataHeight; if (copyEffect && cur.desc.Width < frameSize) { DEBUG_ERROR("Post-processing output buffer is too small"); return false; } m_srcFormat = srcFormat; m_dstFormat = cur; m_copyLayout = copyLayout; m_copyEffect = copyEffect; m_frameSize = frameSize; m_pitch = pitch; m_effectsActive = effectsActive; m_configured = true; if (formatChanged) *formatChanged = oldDst.desc.Width != m_dstFormat.desc.Width || oldDst.desc.Height != m_dstFormat.desc.Height || oldDst.desc.Format != m_dstFormat.desc.Format || oldDst.dataWidth != m_dstFormat.dataWidth || oldDst.dataHeight != m_dstFormat.dataHeight || oldDst.pitch != m_dstFormat.pitch || oldDst.format != m_dstFormat.format || oldDst.width != m_dstFormat.width || oldDst.height != m_dstFormat.height || oldDst.hdr != m_dstFormat.hdr || oldDst.hdrPQ != m_dstFormat.hdrPQ || oldDst.sdrWhiteLevel != m_dstFormat.sdrWhiteLevel || oldDst.colorTransform != m_dstFormat.colorTransform; return true; } void CPostProcessor::GetTimingToken( unsigned * effectIndex, uint64_t * token) const { if (effectIndex) *effectIndex = 0; if (token) *token = 0; for (size_t i = 0; i < m_effects.size(); ++i) { const uint64_t value = m_effects[i]->GetTimingToken(); if (!value) continue; if (effectIndex) *effectIndex = (unsigned)i; if (token) *token = value; return; } } void CPostProcessor::RecordTiming( unsigned effectIndex, uint64_t token, bool fullCopy, uint64_t totalTime) { if (!token || effectIndex >= m_effects.size()) return; m_effects[effectIndex]->RecordTiming(token, fullCopy, totalTime); } bool CPostProcessor::ShouldCopyFully( const RECT dirtyRects[], unsigned nbDirtyRects) const { return m_copyEffect && m_copyEffect->ShouldCopyFully(dirtyRects, nbDirtyRects); } void CPostProcessor::CopyFrame( const ComPtr& commandList, ID3D12Resource * dst, ID3D12Resource * src, const RECT dirtyRects[], unsigned nbDirtyRects, bool fullCopy) const { if (m_copyEffect) { m_copyEffect->CopyFrame( commandList, dst, src, dirtyRects, nbDirtyRects, fullCopy); return; } D3D12_TEXTURE_COPY_LOCATION srcLoc = {}; srcLoc.pResource = src; srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; srcLoc.SubresourceIndex = 0; D3D12_TEXTURE_COPY_LOCATION dstLoc = {}; dstLoc.pResource = dst; dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; dstLoc.PlacedFootprint = m_copyLayout; if (fullCopy) { commandList->CopyTextureRegion( &dstLoc, 0, 0, 0, &srcLoc, nullptr); return; } for (const RECT * rect = dirtyRects; rect < dirtyRects + nbDirtyRects; ++rect) { D3D12_BOX box = {}; box.left = rect->left; box.top = rect->top; box.front = 0; box.right = rect->right; box.bottom = rect->bottom; box.back = 1; commandList->CopyTextureRegion( &dstLoc, box.left, box.top, 0, &srcLoc, &box); } } void CPostProcessor::AdjustFrameDamage(RECT dirtyRects[], unsigned * nbDirtyRects) { for (const auto& effect : m_effects) if (effect->Enabled) effect->AdjustDamage(dirtyRects, nbDirtyRects); } ComPtr CPostProcessor::Run( const ComPtr& commandList, const ComPtr& src, RECT dirtyRects[], unsigned * nbDirtyRects) { ComPtr next = src; for (const auto& effect : m_effects) { if (!effect->Enabled) continue; //DEBUG_TRACE("Run post-processing effect: %s", effect->GetName()); effect->AdjustDamage(dirtyRects, nbDirtyRects); next = effect->Run(m_device, commandList, next, dirtyRects, nbDirtyRects); } return next; }