[idd] capture: build frame processing graph

This commit is contained in:
Geoffrey McRae
2026-08-13 13:43:32 +10:00
parent 081dcf8879
commit 43d918b5f3
14 changed files with 824 additions and 2 deletions

View File

@@ -0,0 +1,286 @@
/**
* 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 "capture/CFrameGraph.h"
bool Frame::Same(const GraphCfg& left, const GraphCfg& right)
{
return
left.mode == right.mode &&
Frame::Same(left.adapter, right.adapter) &&
left.srcWidth == right.srcWidth &&
left.srcHeight == right.srcHeight &&
Frame::Same(left.src, right.src) &&
left.width == right.width &&
left.height == right.height &&
Frame::Same(left.checkpoint, right.checkpoint);
}
bool Frame::Valid(FrameDamage damage, const RECT * rects, unsigned count,
unsigned width, unsigned height)
{
switch (damage)
{
case FrameDamage::NONE:
case FrameDamage::FULL:
return count == 0;
case FrameDamage::RECTS:
if (!rects || !count || count > FRAME_DAMAGE_MAX)
return false;
for (unsigned i = 0; i < count; ++i)
{
const RECT& rect = rects[i];
if (rect.left < 0 ||
rect.top < 0 ||
rect.left >= rect.right ||
rect.top >= rect.bottom ||
static_cast<unsigned long>(rect.right) > width ||
static_cast<unsigned long>(rect.bottom) > height)
return false;
}
return true;
}
return false;
}
void CFrameGraph::Reset()
{
m_cfg = GraphCfg {};
m_nodeCount = 0;
m_leafCount = 0;
m_begun = false;
m_sealed = false;
}
bool CFrameGraph::Begin(const GraphCfg& cfg)
{
Reset();
if (!cfg.srcWidth || !cfg.srcHeight || !cfg.width || !cfg.height ||
!Frame::Valid(cfg.mode) ||
cfg.src.storage != FrameStorage::D3D12_TEXTURE ||
cfg.checkpoint.storage != FrameStorage::D3D12_TEXTURE ||
!Frame::Valid(cfg.src) || !Frame::Valid(cfg.checkpoint) ||
!Frame::Can(cfg.src.signal, cfg.checkpoint.signal))
return false;
m_cfg = cfg;
GraphNode& source = m_nodes[m_nodeCount++];
source = GraphNode {};
source.op = FrameOp::SRC;
source.parent = FRAME_GRAPH_ROOT;
source.width = cfg.srcWidth;
source.height = cfg.srcHeight;
source.profile = cfg.src;
m_begun = true;
return true;
}
bool CFrameGraph::Can(const FrameCfg& cfg) const
{
if (!m_begun || m_sealed || !cfg.width || !cfg.height ||
cfg.width != m_cfg.width || cfg.height != m_cfg.height ||
cfg.mode != m_cfg.mode ||
!Frame::Same(cfg.adapter, m_cfg.adapter) ||
!Frame::Valid(cfg.profile))
return false;
return Frame::Can(m_cfg.src.signal, cfg.profile.signal);
}
unsigned CFrameGraph::Find(FrameOp op, unsigned parent,
unsigned width, unsigned height,
const FrameProfile& profile) const
{
for (unsigned i = 1; i < m_nodeCount; ++i)
if (m_nodes[i].op == op && m_nodes[i].parent == parent &&
m_nodes[i].width == width && m_nodes[i].height == height &&
Frame::Same(m_nodes[i].profile, profile))
return i;
return FRAME_GRAPH_ROOT;
}
unsigned CFrameGraph::AddNode(FrameOp op, unsigned parent,
unsigned width, unsigned height,
const FrameProfile& profile)
{
const unsigned found = Find(op, parent, width, height, profile);
if (found != FRAME_GRAPH_ROOT)
return found;
if (m_nodeCount == FRAME_GRAPH_MAX_NODES)
return FRAME_GRAPH_ROOT;
const unsigned index = m_nodeCount++;
GraphNode& node = m_nodes[index];
node = GraphNode {};
node.op = op;
node.parent = parent;
node.width = width;
node.height = height;
node.profile = profile;
return index;
}
unsigned CFrameGraph::Checkpoint(const FrameProfile& requested)
{
const FrameProfile profile =
Frame::Store(requested, FrameStorage::D3D12_TEXTURE);
unsigned parent = 0;
FrameOp op;
switch (profile.signal)
{
case FrameSignal::SRGB:
op = FrameOp::SDR;
parent = AddNode(FrameOp::SDR, 0, m_cfg.width, m_cfg.height,
m_cfg.checkpoint);
if (parent == FRAME_GRAPH_ROOT)
return FRAME_GRAPH_ROOT;
if (Frame::Same(profile, m_cfg.checkpoint))
return parent;
break;
case FrameSignal::SCRGB_LINEAR:
{
op = FrameOp::SCRGB;
FrameProfile scRGB;
scRGB.storage = FrameStorage::D3D12_TEXTURE;
scRGB.pixel = FramePixel::RGBA16F;
scRGB.signal = FrameSignal::SCRGB_LINEAR;
return AddNode(
op, 0, m_cfg.width, m_cfg.height, scRGB);
}
case FrameSignal::PQ_BT2020:
op = FrameOp::HDR10;
if (m_cfg.src.signal == FrameSignal::SCRGB_LINEAR)
{
FrameProfile scRGB;
scRGB.storage = FrameStorage::D3D12_TEXTURE;
scRGB.pixel = FramePixel::RGBA16F;
scRGB.signal = FrameSignal::SCRGB_LINEAR;
parent = AddNode(
FrameOp::SCRGB, 0, m_cfg.width, m_cfg.height, scRGB);
if (parent == FRAME_GRAPH_ROOT)
return FRAME_GRAPH_ROOT;
}
else
{
parent = AddNode(FrameOp::HDR10, 0,
m_cfg.width, m_cfg.height, m_cfg.checkpoint);
if (parent == FRAME_GRAPH_ROOT)
return FRAME_GRAPH_ROOT;
if (Frame::Same(profile, m_cfg.checkpoint))
return parent;
}
break;
default:
return FRAME_GRAPH_ROOT;
}
return AddNode(
op, parent, m_cfg.width, m_cfg.height, profile);
}
bool CFrameGraph::Add(BackendId id, uint32_t epoch, bool required,
bool primary, const FrameCfg& cfg)
{
if (!id || !epoch || !Can(cfg) ||
m_leafCount == TRANSPORT_MAX_INSTANCES)
return false;
for (unsigned i = 0; i < m_leafCount; ++i)
if (m_leaves[i].id == id && m_leaves[i].epoch == epoch)
return false;
const CFrameGraph before = *this;
const unsigned node = Checkpoint(cfg.profile);
if (node == FRAME_GRAPH_ROOT)
{
*this = before;
return false;
}
GraphLeaf& leaf = m_leaves[m_leafCount++];
leaf = GraphLeaf {};
leaf.id = id;
leaf.epoch = epoch;
leaf.node = node;
leaf.required = required;
leaf.primary = primary;
leaf.cfg = cfg;
for (unsigned current = node;
current != FRAME_GRAPH_ROOT; current = m_nodes[current].parent)
++m_nodes[current].refs;
return true;
}
bool CFrameGraph::Seal()
{
if (!m_begun || m_sealed || !m_leafCount || !m_nodeCount ||
!m_nodes[0].refs)
return false;
for (unsigned i = 0; i < m_leafCount; ++i)
if (!m_leaves[i].id || !m_leaves[i].epoch ||
!m_leaves[i].node || m_leaves[i].node >= m_nodeCount)
return false;
m_sealed = true;
return true;
}
bool CFrameGraph::Same(const GraphCfg& cfg) const
{
return m_sealed && Frame::Same(m_cfg, cfg);
}
bool CFrameGraph::Desc(unsigned leaf, const FrameDesc& frame,
LeafDesc& desc) const
{
if (!m_sealed || leaf >= m_leafCount || !frame.serial ||
frame.format.width != m_cfg.srcWidth ||
frame.format.height != m_cfg.srcHeight ||
!Frame::Valid(frame.damage, frame.rects, frame.count,
m_cfg.srcWidth, m_cfg.srcHeight))
return false;
const GraphLeaf& route = m_leaves[leaf];
const GraphNode& node = m_nodes[route.node];
desc.id = route.id;
desc.epoch = route.epoch;
desc.node = route.node;
desc.profile = route.cfg.profile;
desc.frame = frame;
D12FrameFormat& format = desc.frame.format;
return D12::Set(format, node.profile, node.width, node.height);
}
const GraphNode * CFrameGraph::Nodes(unsigned& count) const
{
count = m_sealed ? m_nodeCount : 0;
return count ? m_nodes : nullptr;
}
const GraphLeaf * CFrameGraph::Leaves(unsigned& count) const
{
count = m_sealed ? m_leafCount : 0;
return count ? m_leaves : nullptr;
}

View File

@@ -0,0 +1,142 @@
/**
* 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
*/
#pragma once
#include "postprocess/D12FrameFormat.h"
#include "transport/FrameProfile.h"
#include "transport/TransportConfig.h"
#include <Windows.h>
#include <limits.h>
#include <stdint.h>
static const unsigned FRAME_DAMAGE_MAX = 256;
static const unsigned FRAME_GRAPH_ROOT = UINT_MAX;
static const unsigned FRAME_GRAPH_MAX_NODES =
1 + 2 * TRANSPORT_MAX_INSTANCES;
enum class FrameOp : uint8_t
{
SRC,
SDR,
SCRGB,
HDR10,
};
enum class FrameDamage : uint8_t
{
NONE,
RECTS,
FULL,
};
struct GraphCfg
{
GpuMode mode = GpuMode::HARDWARE;
LUID adapter = {};
unsigned srcWidth = 0;
unsigned srcHeight = 0;
FrameProfile src;
unsigned width = 0;
unsigned height = 0;
FrameProfile checkpoint;
};
namespace Frame
{
bool Same(const GraphCfg& left, const GraphCfg& right);
bool Valid(FrameDamage damage, const RECT * rects, unsigned count,
unsigned width, unsigned height);
}
struct GraphNode
{
FrameOp op = FrameOp::SRC;
unsigned parent = FRAME_GRAPH_ROOT;
unsigned refs = 0;
unsigned width = 0;
unsigned height = 0;
FrameProfile profile;
};
struct GraphLeaf
{
BackendId id = 0;
uint32_t epoch = 0;
unsigned node = 0;
bool required = false;
bool primary = false;
FrameCfg cfg;
};
// FrameDesc is resource-free. It can cross graph nodes without retaining the
// acquired IddCx texture and is paired with owned texture storage separately.
struct FrameDesc
{
uint64_t serial = 0;
uint64_t captureTime = 0;
FrameDamage damage = FrameDamage::NONE;
RECT rects[FRAME_DAMAGE_MAX] = {};
unsigned count = 0;
D12FrameFormat format = {};
};
struct LeafDesc
{
BackendId id = 0;
uint32_t epoch = 0;
unsigned node = 0;
FrameProfile profile;
FrameDesc frame = {};
};
class CFrameGraph
{
private:
GraphCfg m_cfg;
GraphNode m_nodes[FRAME_GRAPH_MAX_NODES] = {};
GraphLeaf m_leaves[TRANSPORT_MAX_INSTANCES] = {};
unsigned m_nodeCount = 0;
unsigned m_leafCount = 0;
bool m_begun = false;
bool m_sealed = false;
unsigned Find(FrameOp op, unsigned parent, unsigned width, unsigned height,
const FrameProfile& profile) const;
unsigned AddNode(FrameOp op, unsigned parent, unsigned width,
unsigned height,
const FrameProfile& profile);
unsigned Checkpoint(const FrameProfile& profile);
public:
void Reset();
bool Begin(const GraphCfg& cfg);
bool Can(const FrameCfg& cfg) const;
bool Add(BackendId id, uint32_t epoch, bool required, bool primary,
const FrameCfg& cfg);
bool Seal();
bool Same(const GraphCfg& cfg) const;
bool Desc(unsigned leaf, const FrameDesc& frame, LeafDesc& desc) const;
const GraphCfg& Cfg() const { return m_cfg; }
const GraphNode * Nodes(unsigned& count) const;
const GraphLeaf * Leaves(unsigned& count) const;
};

View File

@@ -40,6 +40,34 @@
static const uint32_t HDR_PQ_MIN_LUMINANCE = 50;
static const uint32_t HDR_PQ_MAX_LUMINANCE = 10000;
static const uint64_t GRAPH_RETRY_NS = 250000000ULL;
static bool MakeGraphCfg(const D12FrameFormat& source,
const D12FrameFormat& checkpoint, bool software,
const LUID& adapter, GraphCfg& cfg)
{
if (!source.width || !source.height ||
source.desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D ||
!checkpoint.width || !checkpoint.height ||
checkpoint.desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D)
return false;
FrameProfile src;
FrameProfile output;
if (!D12::Profile(source, FrameStorage::D3D12_TEXTURE, src) ||
!D12::Profile(checkpoint, FrameStorage::D3D12_TEXTURE, output))
return false;
cfg.mode = software ? GpuMode::SOFTWARE : GpuMode::HARDWARE;
cfg.adapter = adapter;
cfg.srcWidth = source.width;
cfg.srcHeight = source.height;
cfg.src = src;
cfg.width = checkpoint.width;
cfg.height = checkpoint.height;
cfg.checkpoint = output;
return true;
}
CSwapChainProcessor::CSwapChainProcessor(CMonitorContext * monitorContext,
UINT64 assignmentGeneration, IDDCX_MONITOR monitor,
@@ -329,6 +357,8 @@ void CSwapChainProcessor::SwapChainThreadCore()
if (WaitForSingleObject(m_terminateEvent.Get(), 0) == WAIT_OBJECT_0)
break;
CfgGraph();
UINT frameNumber = 0;
UINT dirtyRectCount = 0;
UINT moveRegionCount = 0;
@@ -413,6 +443,7 @@ void CSwapChainProcessor::SwapChainThreadCore()
// Every acquired frame must be finished before the next acquire, even if
// its presentation number was a duplicate and no work was submitted.
hr = IddCxSwapChainFinishedProcessingFrame(m_hSwapChain);
surface.Reset();
if (FAILED(hr))
{
// A lost path is normal (mode change/topology rebuild); Windows
@@ -493,6 +524,46 @@ bool CSwapChainProcessor::GetContentHDRMetadata(D12FrameFormat& format) const
#endif
}
void CSwapChainProcessor::QueueGraph(const D12FrameFormat& source,
const D12FrameFormat& checkpoint)
{
GraphCfg cfg;
if (!MakeGraphCfg(
source, checkpoint, m_dx11Device->IsSoftware(),
m_renderAdapter, cfg))
return;
if (m_haveGraphCfg && Frame::Same(m_graphCfg, cfg))
return;
m_graphCfg = cfg;
m_haveGraphCfg = true;
m_graphPending = true;
m_graphRetryAt = 0;
}
void CSwapChainProcessor::CfgGraph()
{
if (!m_graphPending)
return;
const uint64_t now = CFrameScheduler::Nanotime();
if (now < m_graphRetryAt)
return;
m_graphPending = false;
const CfgResult result =
m_devContext->GetTransport().Cfg(m_graphCfg, m_graph);
if (result == CfgResult::ACCEPTED)
{
m_graphRetryAt = 0;
}
else if (result == CfgResult::RETRY)
{
m_graphPending = true;
m_graphRetryAt = now + GRAPH_RETRY_NS;
}
}
bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer,
unsigned dirtyRectCount, unsigned moveRegionCount,
DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel,
@@ -732,6 +803,10 @@ bool CSwapChainProcessor::SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer
m_postProcessors[0].GetTimingToken(
&timingEffectIndex, &timingToken);
const D12FrameFormat& graphFormat =
m_postProcessors[0].GetTextureFormat();
QueueGraph(srcFormat, graphFormat);
}
if (needsReconfigure || postProcessFormatChanged || frameMetadataChanged)

View File

@@ -25,6 +25,7 @@
#include "d3d/CD3D12Device.h"
#include "display/IddCxCompat.h"
#include "d3d/CInteropResourcePool.h"
#include "capture/CFrameGraph.h"
#include "capture/CFrameProcessor.h"
#include "postprocess/D12FrameFormat.h"
#include "postprocess/CPostProcessor.h"
@@ -56,6 +57,11 @@ private:
HANDLE m_newFrameEvent;
CInteropResourcePool m_resPool;
CFrameGraph m_graph;
GraphCfg m_graphCfg;
bool m_haveGraphCfg = false;
bool m_graphPending = false;
uint64_t m_graphRetryAt = 0;
CPostProcessor m_postProcessors[CAPTURE_PIPELINE_SLOTS];
std::unique_ptr<CFrameProcessor> m_frameProcessor;
// Reconfiguration is exclusive while per-candidate recording is shared.
@@ -93,6 +99,9 @@ private:
void UpdateHDRMetadata(const IDDCX_METADATA2& metadata);
#endif
bool GetContentHDRMetadata(D12FrameFormat& format) const;
void QueueGraph(const D12FrameFormat& source,
const D12FrameFormat& checkpoint);
void CfgGraph();
bool SwapChainNewFrame(ComPtr<IDXGIResource> acquiredBuffer,
unsigned dirtyRectCount, unsigned moveRegionCount,
DXGI_COLOR_SPACE_TYPE colorSpace, UINT sdrWhiteLevel,