Files
LookingGlass/idd/LGIdd/capture/CFrameExec.cpp
2026-08-14 02:30:31 +10:00

788 lines
23 KiB
C++

/**
* 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/CFrameExec.h"
#include "d3d/CD3D11Device.h"
#include "d3d/CD3D12Device.h"
#include "d3d/CInteropPool.h"
#include "d3d/CInteropResource.h"
#include "postprocess/effect/CColorTransformEffect.h"
#include "postprocess/effect/CDownsampleEffect.h"
#include "postprocess/effect/CFormatEffect.h"
#include "postprocess/effect/CHDR16to10Effect.h"
#include "transport/CTexHub.h"
#include <cstring>
#include <new>
#include <utility>
namespace
{
static const unsigned EXEC_LANES = 2;
static const unsigned EXEC_SLOTS = 4;
enum class ExecOp : uint8_t
{
COPY,
CAL,
LUT,
SCALE,
HDR10,
FORMAT,
};
D12FrameFormat MakeFormat(const FrameProfile& profile,
unsigned width, unsigned height,
const std::shared_ptr<const D12ColorTransform>& transform,
D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE)
{
D12FrameFormat format = {};
D12::Set(format,
Frame::Store(profile, FrameStorage::D3D12_TEXTURE),
width, height, flags);
format.colorTransform = transform;
return format;
}
bool Copy(ID3D12GraphicsCommandList * list,
ID3D12Resource * src, ID3D12Resource * dst)
{
if (!list || !src || !dst || src == dst)
return false;
const D3D12_RESOURCE_DESC in = src->GetDesc();
const D3D12_RESOURCE_DESC out = dst->GetDesc();
if (!D12::Same(in, out, D12::DescCmp::COPY))
return false;
// Leave the immutable input in COMMON. COPY_SOURCE is promoted
// implicitly, allowing the legacy and texture queues to read it without
// cross-queue state ownership. Only this private destination transitions.
D3D12_RESOURCE_BARRIER barrier = {};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = dst;
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COMMON;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
list->ResourceBarrier(1, &barrier);
list->CopyResource(dst, src);
std::swap(barrier.Transition.StateBefore,
barrier.Transition.StateAfter);
list->ResourceBarrier(1, &barrier);
return true;
}
CfgResult CfgFromTex(TexResult result)
{
switch (result)
{
case TexResult::OK:
return CfgResult::ACCEPTED;
case TexResult::BUSY:
return CfgResult::RETRY;
case TexResult::REJECTED:
return CfgResult::REJECTED;
case TexResult::FAILED:
return CfgResult::FAILED;
}
return CfgResult::FAILED;
}
bool Supports(ID3D12Device3 * device, ExecOp op,
DXGI_FORMAT src, DXGI_FORMAT dst)
{
if (op == ExecOp::COPY)
return true;
D3D12_FEATURE_DATA_FORMAT_SUPPORT input = { src };
D3D12_FEATURE_DATA_FORMAT_SUPPORT output = { dst };
if (!device || FAILED(device->CheckFeatureSupport(
D3D12_FEATURE_FORMAT_SUPPORT, &input, sizeof(input))) ||
FAILED(device->CheckFeatureSupport(
D3D12_FEATURE_FORMAT_SUPPORT, &output, sizeof(output))))
return false;
const D3D12_FORMAT_SUPPORT1 read = op == ExecOp::SCALE ?
D3D12_FORMAT_SUPPORT1_SHADER_SAMPLE :
D3D12_FORMAT_SUPPORT1_SHADER_LOAD;
return (input.Support1 & read) == read &&
(output.Support2 & D3D12_FORMAT_SUPPORT2_UAV_TYPED_STORE) != 0;
}
}
struct CFrameExec::Core : std::enable_shared_from_this<CFrameExec::Core>
{
struct Fx
{
ExecOp op = ExecOp::COPY;
ComPtr<ID3D12Resource> scratch;
std::unique_ptr<CColorTransformEffect> color;
std::unique_ptr<CDownsampleEffect> scale;
std::unique_ptr<CHDR16to10Effect> hdr;
std::unique_ptr<CFormatEffect> convert;
bool Init(const ComPtr<ID3D12Device3>& device, ExecOp selected,
const D12FrameFormat& src, const D12FrameFormat& dst, bool exported)
{
op = selected;
PostProcessStatus status = PostProcessStatus::SUCCESS;
D12FrameFormat output = src;
output.desc.Flags = dst.desc.Flags;
switch (op)
{
case ExecOp::COPY:
if (src.desc.Width != dst.desc.Width ||
src.desc.Height != dst.desc.Height ||
src.desc.Format != dst.desc.Format)
return false;
break;
case ExecOp::CAL:
color.reset(new (std::nothrow)
CColorTransformEffect(CalPart::MATRIX, true));
if (!color || !color->Init(device))
return false;
status = color->Cfg(device, src, output);
break;
case ExecOp::LUT:
color.reset(new (std::nothrow)
CColorTransformEffect(CalPart::LUT, true));
if (!color || !color->Init(device))
return false;
status = color->Cfg(device, src, output);
break;
case ExecOp::SCALE:
scale.reset(new (std::nothrow)
CDownsampleEffect(dst.width, dst.height));
if (!scale || !scale->Init(device))
return false;
status = scale->Cfg(device, src, output);
break;
case ExecOp::HDR10:
hdr.reset(new (std::nothrow) CHDR16to10Effect);
if (!hdr || !hdr->Init(device))
return false;
status = hdr->Cfg(device, src, output);
break;
case ExecOp::FORMAT:
convert.reset(new (std::nothrow) CFormatEffect);
if (!convert || !convert->Init(device))
return false;
status = convert->Cfg(device, src, dst);
output = dst;
break;
}
if (status != PostProcessStatus::SUCCESS ||
!D12::Same(output, dst, D12::FormatCmp::IMAGE))
return false;
if (!exported && !PostProcessUtil::CreateDefaultTexture(
device, dst.desc, scratch))
return false;
return true;
}
bool Run(const ComPtr<ID3D12Device3>& device,
const ComPtr<ID3D12GraphicsCommandList>& list,
ID3D12Resource * src, ID3D12Resource * dst)
{
if (!src || !dst)
return false;
if (op == ExecOp::COPY)
return Copy(list.Get(), src, dst);
ComPtr<ID3D12Resource> input = src;
RECT rect = {};
unsigned count = 0;
switch (op)
{
case ExecOp::CAL:
case ExecOp::LUT:
return color && color->Run(
device, list, input, dst, &rect, &count);
case ExecOp::SCALE:
return scale && scale->Run(
device, list, input, dst, &rect, &count);
case ExecOp::HDR10:
return hdr && hdr->Run(
device, list, input, dst, &rect, &count);
case ExecOp::FORMAT:
return convert && convert->Run(
device, list, input, dst, &rect, &count);
case ExecOp::COPY:
break;
}
return false;
}
};
struct Lane
{
Fx nodes[FRAME_GRAPH_MAX_NODES];
};
CSRWLock runLock;
std::shared_ptr<CD3D11Device> d11;
std::shared_ptr<CD3D12Device> d12;
ComPtr<ID3D12Device3> device;
CTexHub * hub = nullptr;
CFrameGraph graph;
CD3D12CommandQueue queue;
CTexPool pools[FRAME_GRAPH_MAX_NODES];
CInteropPool interop[FRAME_GRAPH_MAX_NODES];
Lane lanes[EXEC_LANES];
D12FrameFormat formats[FRAME_GRAPH_MAX_NODES] = {};
ExecOp ops[FRAME_GRAPH_MAX_NODES] = {};
bool exported[FRAME_GRAPH_MAX_NODES] = {};
bool d11Node[FRAME_GRAPH_MAX_NODES] = {};
unsigned readers[FRAME_GRAPH_MAX_NODES] = {};
unsigned nodeCount = 0;
unsigned leafCount = 0;
unsigned nextLane = 0;
bool live = false;
std::atomic<bool> forceFull { true };
~Core()
{
// Effect descriptor heaps, scratch textures, and pool destinations are
// referenced by submitted lists. Drain before member destruction.
queue.WaitForIdle();
}
static bool Select(const GraphNode& parent,
const GraphNode& node, ExecOp& op)
{
switch (node.op)
{
case FrameOp::CAL:
op = ExecOp::CAL;
return true;
case FrameOp::LUT:
op = ExecOp::LUT;
return true;
case FrameOp::SCALE:
op = ExecOp::SCALE;
return true;
case FrameOp::HDR10:
if (parent.profile.signal == FrameSignal::SCRGB_LINEAR &&
node.profile.signal == FrameSignal::PQ_BT2020)
{
op = ExecOp::HDR10;
return true;
}
break;
case FrameOp::SDR:
case FrameOp::SCRGB:
break;
case FrameOp::SRC:
return false;
}
if (parent.width == node.width && parent.height == node.height &&
Frame::Same(parent.profile, node.profile))
{
op = ExecOp::COPY;
return true;
}
if (parent.width == node.width && parent.height == node.height &&
parent.profile.signal == FrameSignal::SRGB &&
node.profile.signal == FrameSignal::SRGB &&
parent.profile.pixel != node.profile.pixel)
{
op = ExecOp::FORMAT;
return true;
}
return false;
}
CfgResult Init(const CFrameGraph& source,
const std::shared_ptr<CD3D11Device>& d11Device,
const std::shared_ptr<CD3D12Device>& d12Device, CTexHub& texHub,
bool inheritedFull)
{
d11 = d11Device;
d12 = d12Device;
hub = &texHub;
graph = source;
device = d12 ? d12->GetDevice() : nullptr;
if (!d11 || !d12 || !device || !graph.Generation())
return CfgResult::FAILED;
const GraphNode * nodes = graph.Nodes(nodeCount);
const GraphLeaf * leaves = graph.Leaves(leafCount);
if (!nodes || !leaves || !nodeCount ||
nodeCount > FRAME_GRAPH_MAX_NODES ||
leafCount > TRANSPORT_MAX_INSTANCES)
return CfgResult::REJECTED;
unsigned texLeaves = 0;
bool continuous = true;
for (unsigned leaf = 0; leaf < leafCount; ++leaf)
if (leaves[leaf].tex)
{
if (!leaves[leaf].node || leaves[leaf].node >= nodeCount)
return CfgResult::REJECTED;
exported[leaves[leaf].node] = true;
++readers[leaves[leaf].node];
d11Node[leaves[leaf].node] |=
leaves[leaf].cfg.profile.storage ==
FrameStorage::D3D11_TEXTURE;
continuous &= leaves[leaf].continuous;
++texLeaves;
}
if (!texLeaves)
return CfgResult::ACCEPTED;
Atomic::Store(forceFull, inheritedFull || !continuous,
std::memory_order_relaxed);
if (!nodes[0].texRefs || !queue.Init(device.Get(),
D3D12_COMMAND_LIST_TYPE_DIRECT, L"Frame Graph",
CD3D12CommandSlot::FAST, EXEC_LANES, false, true))
return CfgResult::FAILED;
const GraphCfg& cfg = graph.Cfg();
formats[0] = MakeFormat(cfg.src, cfg.srcWidth, cfg.srcHeight,
cfg.transform);
for (unsigned node = 1; node < nodeCount; ++node)
{
if (!nodes[node].texRefs)
continue;
if (nodes[node].texRefs > nodes[node].refs ||
nodes[node].parent >= node ||
!nodes[nodes[node].parent].texRefs ||
!Select(nodes[nodes[node].parent], nodes[node], ops[node]))
return CfgResult::REJECTED;
D3D12_RESOURCE_FLAGS flags = ops[node] == ExecOp::COPY &&
exported[node] ?
D3D12_RESOURCE_FLAG_NONE :
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
if (exported[node] && readers[node] > 1)
flags |= D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS;
formats[node] = MakeFormat(nodes[node].profile,
nodes[node].width, nodes[node].height, cfg.transform, flags);
if (!Supports(device.Get(), ops[node],
formats[nodes[node].parent].desc.Format,
formats[node].desc.Format))
return CfgResult::REJECTED;
if (exported[node])
{
const FrameProfile profile = Frame::Store(
nodes[node].profile, FrameStorage::D3D12_TEXTURE);
const TexResult result = pools[node].Init(device.Get(),
graph.Generation(), node, profile, formats[node].desc,
D3D12_RESOURCE_STATE_COMMON, EXEC_SLOTS, graph.Shared(node));
if (result != TexResult::OK)
return CfgFromTex(result);
if (d11Node[node])
{
const TexResult interopResult = interop[node].Init(d11, d12);
if (interopResult != TexResult::OK)
return CfgFromTex(interopResult);
}
}
}
for (unsigned lane = 0; lane < EXEC_LANES; ++lane)
for (unsigned node = 1; node < nodeCount; ++node)
if (nodes[node].texRefs && !lanes[lane].nodes[node].Init(
device, ops[node], formats[nodes[node].parent], formats[node],
exported[node]))
return CfgResult::REJECTED;
live = true;
return CfgResult::ACCEPTED;
}
bool NeedsFull()
{
CSRWExclusiveLock lock(runLock);
return Atomic::Load(forceFull, std::memory_order_acquire);
}
FrameContentRef Content(uint64_t serial, const D12FrameFormat& format,
uint64_t captureTime, bool full, FrameDamage damage,
const RECT * rects, unsigned count)
{
if (!serial || count > FRAME_DAMAGE_MAX ||
(count && !rects) ||
(damage == FrameDamage::RECTS) != (count != 0))
return {};
FrameContent * raw = new (std::nothrow) FrameContent;
if (!raw)
return {};
raw->serial = serial;
raw->captureTime = captureTime;
raw->damage = full ? FrameDamage::FULL : damage;
raw->count = raw->damage == FrameDamage::RECTS ? count : 0;
if (raw->count)
memcpy(raw->rects, rects, raw->count * sizeof(*rects));
raw->format = format;
try
{
return FrameContentRef(raw);
}
catch (const std::bad_alloc&)
{
return {};
}
}
TexResult Run(CInteropResource& src, const D12FrameFormat& sourceFormat,
uint64_t serial, uint64_t captureTime, uint64_t postStart,
FrameDamage damage, const RECT * rects, unsigned count) noexcept
{
if (!live)
return TexResult::OK;
CSRWExclusiveLock run = CSRWExclusiveLock::Try(runLock);
if (!run)
{
Atomic::Store(forceFull, true, std::memory_order_release);
return TexResult::BUSY;
}
const bool full = Atomic::Swap(forceFull, false, std::memory_order_acq_rel);
ID3D12Resource * source = src.GetRes().Get();
if (!source)
{
Atomic::Store(forceFull, true, std::memory_order_release);
return TexResult::REJECTED;
}
const D3D12_RESOURCE_DESC actual = source->GetDesc();
if (!D12::Same(actual, sourceFormat.desc, D12::DescCmp::COPY))
{
Atomic::Store(forceFull, true, std::memory_order_release);
return TexResult::REJECTED;
}
const FrameContentRef content = Content(
serial, sourceFormat, captureTime, full, damage, rects, count);
FrameDesc check;
unsigned first = 0;
while (++first < nodeCount && !exported[first]) {}
if (!content || first == nodeCount ||
!graph.Desc(first, content, check))
{
Atomic::Store(forceFull, true, std::memory_order_release);
return TexResult::REJECTED;
}
CD3D12CommandSlot * slot = nullptr;
unsigned lane = 0;
for (unsigned attempt = 0; attempt < EXEC_LANES; ++attempt)
{
lane = (nextLane + attempt) % EXEC_LANES;
slot = queue.Try(lane);
if (slot)
{
nextLane = (lane + 1) % EXEC_LANES;
break;
}
}
if (!slot)
{
Atomic::Store(forceFull, true, std::memory_order_release);
return queue.Failed() ? TexResult::FAILED : TexResult::BUSY;
}
TexWrite writes[FRAME_GRAPH_MAX_NODES];
const GraphNode * nodes = graph.Nodes(nodeCount);
TexResult result = TexResult::OK;
for (unsigned node = 1; node < nodeCount; ++node)
if (exported[node])
{
result = pools[node].Try(writes[node]);
if (result != TexResult::OK)
break;
}
if (result != TexResult::OK)
{
for (unsigned node = 1; node < nodeCount; ++node)
writes[node].Cancel();
slot->Cancel();
Atomic::Store(forceFull, true, std::memory_order_release);
return result;
}
if (!src.Signal() || !src.Sync(*slot))
{
for (unsigned node = 1; node < nodeCount; ++node)
writes[node].Cancel();
slot->Cancel();
Atomic::Store(forceFull, true, std::memory_order_release);
return TexResult::FAILED;
}
ComPtr<ID3D12GraphicsCommandList> list = slot->GetGfxList();
ID3D12Resource * outputs[FRAME_GRAPH_MAX_NODES] = {};
outputs[0] = source;
bool recorded = !!list && !!outputs[0];
for (unsigned node = 1; recorded && node < nodeCount; ++node)
if (nodes[node].texRefs)
{
ID3D12Resource * output = exported[node] ?
writes[node].Get() : lanes[lane].nodes[node].scratch.Get();
recorded = output && lanes[lane].nodes[node].Run(
device, list, outputs[nodes[node].parent], output);
outputs[node] = output;
}
if (!recorded)
{
for (unsigned node = 1; node < nodeCount; ++node)
writes[node].Cancel();
slot->Cancel();
Atomic::Store(forceFull, true, std::memory_order_release);
return TexResult::FAILED;
}
D12Sync sync;
if (!slot->Execute(&sync))
{
const bool submitted = slot->HasSubmittedWork();
for (unsigned node = 1; node < nodeCount; ++node)
if (submitted)
writes[node].Fail(sync);
else
writes[node].Cancel();
Atomic::Store(forceFull, true, std::memory_order_release);
return TexResult::FAILED;
}
const FrameTime time = { postStart, 0, 0, 0, false, false };
TexLease leases[FRAME_GRAPH_MAX_NODES];
const std::shared_ptr<void> hold = shared_from_this();
bool sealed = true;
for (unsigned node = 1; node < nodeCount; ++node)
if (exported[node])
{
FrameDesc desc;
if (!graph.Desc(node, content, desc) ||
!writes[node].Seal(desc, time, sync, leases[node], hold))
{
sealed = false;
break;
}
}
if (!sealed)
{
for (unsigned node = 1; node < nodeCount; ++node)
{
if (writes[node])
writes[node].Fail(sync);
leases[node].Reset();
}
Atomic::Store(forceFull, true, std::memory_order_release);
return TexResult::FAILED;
}
bool dropped = false;
bool failed = false;
const GraphLeaf * leaves = graph.Leaves(leafCount);
for (unsigned leaf = 0; leaf < leafCount; ++leaf)
{
if (!leaves[leaf].tex)
continue;
FrameIn frame;
frame.graph = graph.Generation();
if (!graph.Desc(leaf, content, frame.desc))
{
dropped = true;
continue;
}
PushResult pushed = PushResult::STALE;
const unsigned node = leaves[leaf].node;
if (leaves[leaf].cfg.profile.storage ==
FrameStorage::D3D12_TEXTURE)
{
pushed = hub->Push(frame, leases[node]);
}
else
{
D11Lease lease;
const TexResult opened = interop[node].Get(leases[node], lease);
if (opened == TexResult::OK)
pushed = hub->Push(frame, std::move(lease));
else if (opened == TexResult::BUSY)
pushed = PushResult::BUSY;
else
{
pushed = opened == TexResult::REJECTED ?
PushResult::REJECTED : PushResult::FAILED;
hub->Fault(frame, pushed);
}
}
if (pushed == PushResult::BUSY || pushed == PushResult::STALE)
dropped = true;
else if (pushed == PushResult::FAILED)
failed = true;
}
if (dropped)
Atomic::Store(forceFull, true, std::memory_order_release);
if (failed)
return TexResult::FAILED;
return dropped ? TexResult::BUSY : TexResult::OK;
}
};
CFrameExec::~CFrameExec()
{
Reset();
}
bool CFrameExec::Init(const std::shared_ptr<CD3D11Device>& d11,
const std::shared_ptr<CD3D12Device>& d12, CTexHub& hub)
{
if (!d11 || !d12 || !d11->GetDevice() || !d12->GetDevice())
return false;
CSRWExclusiveLock lock(m_lock);
if (m_d11 || m_d12 || m_hub)
return false;
m_d11 = d11;
m_d12 = d12;
m_hub = &hub;
return true;
}
void CFrameExec::Reset()
{
std::shared_ptr<Core> active;
std::shared_ptr<Core> pending;
{
CSRWExclusiveLock lock(m_lock);
active = std::move(m_active);
pending = std::move(m_pending);
m_hub = nullptr;
m_d12.reset();
m_d11.reset();
}
pending.reset();
active.reset();
}
uint64_t CFrameExec::NextSerial()
{
return Atomic::Next(m_serial);
}
CfgResult CFrameExec::Prep(const CFrameGraph& graph) noexcept
{
std::shared_ptr<CD3D11Device> d11;
std::shared_ptr<CD3D12Device> d12;
std::shared_ptr<Core> active;
CTexHub * hub = nullptr;
{
CSRWSharedLock lock(m_lock);
if (m_pending)
return CfgResult::REJECTED;
d11 = m_d11;
d12 = m_d12;
active = m_active;
hub = m_hub;
}
if (!d11 || !d12 || !hub)
return CfgResult::FAILED;
std::shared_ptr<Core> next;
try
{
next.reset(new (std::nothrow) Core);
}
catch (const std::bad_alloc&)
{
return CfgResult::FAILED;
}
if (!next)
return CfgResult::FAILED;
CfgResult result = CfgResult::FAILED;
try
{
result = next->Init(graph, d11, d12, *hub,
!active || active->NeedsFull());
}
catch (...)
{
return CfgResult::FAILED;
}
if (result != CfgResult::ACCEPTED)
return result;
{
CSRWExclusiveLock lock(m_lock);
if (m_pending || m_d11 != d11 || m_d12 != d12 || m_hub != hub)
return CfgResult::RETRY;
m_pending = next;
}
return CfgResult::ACCEPTED;
}
void CFrameExec::Commit() noexcept
{
std::shared_ptr<Core> old;
{
CSRWExclusiveLock lock(m_lock);
if (!m_pending)
return;
old = std::move(m_active);
m_active = std::move(m_pending);
}
old.reset();
}
void CFrameExec::Abort() noexcept
{
std::shared_ptr<Core> drop;
{
CSRWExclusiveLock lock(m_lock);
drop = std::move(m_pending);
}
drop.reset();
}
TexResult CFrameExec::Run(CInteropResource& src,
const D12FrameFormat& format, uint64_t captureTime, uint64_t postStart,
FrameDamage damage, const RECT * rects, unsigned count) noexcept
{
std::shared_ptr<Core> core;
{
CSRWSharedLock lock(m_lock);
core = m_active;
}
if (!core || !core->live)
return TexResult::OK;
return core->Run(src, format, NextSerial(), captureTime, postStart,
damage, rects, count);
}