diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj
index ff0c48c7..76eba21a 100644
--- a/idd/LGIdd/LGIdd.vcxproj
+++ b/idd/LGIdd/LGIdd.vcxproj
@@ -44,6 +44,7 @@
+
@@ -97,6 +98,7 @@
+
diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters
index 8a651f56..29f7ab2e 100644
--- a/idd/LGIdd/LGIdd.vcxproj.filters
+++ b/idd/LGIdd/LGIdd.vcxproj.filters
@@ -100,6 +100,9 @@
Capture
+
+ Capture
+
Capture
@@ -279,6 +282,9 @@
Capture
+
+ Capture
+
Capture
diff --git a/idd/LGIdd/capture/CFrameGraph.cpp b/idd/LGIdd/capture/CFrameGraph.cpp
index 2ec5f70a..8dda16a1 100644
--- a/idd/LGIdd/capture/CFrameGraph.cpp
+++ b/idd/LGIdd/capture/CFrameGraph.cpp
@@ -20,6 +20,8 @@
#include "capture/CFrameGraph.h"
+#include
+
bool Frame::Same(const GraphCfg& left, const GraphCfg& right)
{
return
@@ -311,13 +313,35 @@ bool CFrameGraph::Want(FrameSignal signal) const
return false;
}
-bool CFrameGraph::Desc(unsigned leaf, const FrameDesc& frame,
+bool CFrameGraph::Desc(unsigned leaf, const FrameContentRef& content,
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,
+ if (!content)
+ return false;
+
+ const D12FrameFormat& source = content->format;
+ FrameProfile sourceProfile;
+ const bool validProfile = D12::Profile(source,
+ FrameStorage::D3D12_TEXTURE, sourceProfile);
+ if (!validProfile || !Frame::Same(sourceProfile, m_cfg.src))
+ return false;
+
+ if (!m_sealed ||
+ leaf >= m_leafCount ||
+ !content->serial ||
+ source.width != m_cfg.srcWidth ||
+ source.height != m_cfg.srcHeight ||
+ source.dataWidth != m_cfg.srcWidth ||
+ source.dataHeight != m_cfg.srcHeight ||
+ source.pitch != 0 ||
+ source.desc.Dimension !=
+ D3D12_RESOURCE_DIMENSION_TEXTURE2D ||
+ source.desc.Width != m_cfg.srcWidth ||
+ source.desc.Height != m_cfg.srcHeight ||
+ source.desc.DepthOrArraySize != 1 ||
+ source.desc.MipLevels != 1 ||
+ source.desc.SampleDesc.Count != 1 ||
+ !Frame::Valid(content->damage, content->rects, content->count,
m_cfg.srcWidth, m_cfg.srcHeight))
return false;
@@ -327,10 +351,32 @@ bool CFrameGraph::Desc(unsigned leaf, const FrameDesc& frame,
desc.epoch = route.epoch;
desc.node = route.node;
desc.profile = route.cfg.profile;
- desc.frame = frame;
+
+ desc.frame = FrameDesc {};
+ desc.frame.content = content;
+ desc.frame.damage = content->damage;
+ desc.frame.count = content->count;
+ if (content->count)
+ memcpy(desc.frame.rects, content->rects,
+ content->count * sizeof(*content->rects));
+ desc.frame.format = content->format;
+
+ // A scaled checkpoint has a different damage coordinate space. Until the
+ // executor owns exact edge transforms, preserve correctness by making any
+ // partial source damage a full node update.
+ if (desc.frame.damage == FrameDamage::RECTS &&
+ (node.width != m_cfg.srcWidth || node.height != m_cfg.srcHeight))
+ {
+ desc.frame.damage = FrameDamage::FULL;
+ desc.frame.count = 0;
+ }
D12FrameFormat& format = desc.frame.format;
- return D12::Set(format, node.profile, node.width, node.height);
+ if (!D12::Set(format, node.profile, node.width, node.height))
+ return false;
+ // Calibration/LUT nodes have already consumed this transform.
+ format.colorTransform.reset();
+ return true;
}
const GraphNode * CFrameGraph::Nodes(unsigned& count) const
diff --git a/idd/LGIdd/capture/CFrameGraph.h b/idd/LGIdd/capture/CFrameGraph.h
index 9e2ba60a..1f4f78e7 100644
--- a/idd/LGIdd/capture/CFrameGraph.h
+++ b/idd/LGIdd/capture/CFrameGraph.h
@@ -91,9 +91,9 @@ struct GraphLeaf
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
+// All products derived from one capture retain the same immutable content.
+// It contains no acquired IddCx or Direct3D resource.
+struct FrameContent
{
uint64_t serial = 0;
uint64_t captureTime = 0;
@@ -103,6 +103,19 @@ struct FrameDesc
D12FrameFormat format = {};
};
+using FrameContentRef = std::shared_ptr;
+
+// FrameDesc adds one graph node's representation and damage to the shared
+// content identity. It remains resource-free.
+struct FrameDesc
+{
+ FrameContentRef content;
+ FrameDamage damage = FrameDamage::NONE;
+ RECT rects[FRAME_DAMAGE_MAX] = {};
+ unsigned count = 0;
+ D12FrameFormat format = {};
+};
+
struct LeafDesc
{
BackendId id = 0;
@@ -140,7 +153,8 @@ public:
bool Same(const GraphCfg& cfg) const;
bool Want(FrameSignal signal) const;
bool Need(FrameOp op) const;
- bool Desc(unsigned leaf, const FrameDesc& frame, LeafDesc& desc) const;
+ bool Desc(unsigned leaf, const FrameContentRef& content,
+ LeafDesc& desc) const;
const GraphCfg& Cfg() const { return m_cfg; }
const GraphNode * Nodes(unsigned& count) const;
diff --git a/idd/LGIdd/capture/CFrameTex.cpp b/idd/LGIdd/capture/CFrameTex.cpp
new file mode 100644
index 00000000..c3dcbb5f
--- /dev/null
+++ b/idd/LGIdd/capture/CFrameTex.cpp
@@ -0,0 +1,537 @@
+/**
+ * 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/CFrameTex.h"
+
+#include "Atomic.h"
+#include "CDebug.h"
+#include "CSRWLock.h"
+#include "Seq.h"
+
+#include
+
+enum class TexState : uint8_t
+{
+ FREE,
+ WRITE,
+ LEASED,
+ WAIT,
+ DEAD,
+};
+
+static void WaitSync(const D12Sync& sync)
+{
+ while (sync.State() == D12SyncState::PENDING)
+ Sleep(1);
+}
+
+static uint64_t NextPool()
+{
+ static std::atomic next { 0 };
+ return Atomic::Next(next);
+}
+
+static bool ValidFrame(const FrameProfile& profile,
+ const D3D12_RESOURCE_DESC& resource, const FrameDesc& frame)
+{
+ const D12FrameFormat& format = frame.format;
+ FrameProfile formatProfile;
+ const bool validProfile = D12::Profile(format,
+ FrameStorage::D3D12_TEXTURE, formatProfile);
+ if (!validProfile || !Frame::Same(formatProfile, profile))
+ return false;
+
+ const bool validDamage = Frame::Valid(frame.damage, frame.rects,
+ frame.count, format.width, format.height);
+ return
+ frame.content &&
+ frame.content->serial &&
+ validDamage &&
+ resource.Dimension == D3D12_RESOURCE_DIMENSION_TEXTURE2D &&
+ resource.Width == format.width &&
+ resource.Height == format.height &&
+ resource.DepthOrArraySize == 1 &&
+ resource.MipLevels == 1 &&
+ resource.SampleDesc.Count == 1 &&
+ D12::Same(resource, format.desc, D12::DescCmp::NO_FLAGS) &&
+ format.pitch == 0 &&
+ format.dataWidth == resource.Width &&
+ format.dataHeight == resource.Height;
+}
+
+struct CTexCore
+{
+ struct Slot
+ {
+ ComPtr res;
+ D12Sync sync;
+ TexState state = TexState::FREE;
+ uint64_t gen = 0;
+ };
+
+ mutable CSRWLock lock;
+ bool open = true;
+ bool closed = false;
+ bool failed = false;
+ uint64_t graph = 0;
+ uint64_t id = 0;
+ unsigned node = 0;
+ unsigned count = 0;
+ FrameProfile profile;
+ D3D12_RESOURCE_DESC desc = {};
+ D3D12_RESOURCE_STATES read = D3D12_RESOURCE_STATE_COMMON;
+ Slot slots[CTexPool::MAX_SLOTS];
+
+ TexResult Acquire(unsigned& index, uint64_t& generation)
+ {
+ CSRWExclusiveLock guard(lock);
+ if (!open || failed)
+ return TexResult::FAILED;
+
+ for (unsigned i = 0; i < count; ++i)
+ if (slots[i].state == TexState::WAIT)
+ {
+ const D12SyncState state = slots[i].sync.State();
+ if (state == D12SyncState::FAILED)
+ {
+ slots[i].state = TexState::DEAD;
+ failed = true;
+ return TexResult::FAILED;
+ }
+ else if (state == D12SyncState::READY)
+ {
+ slots[i].sync = D12Sync {};
+ slots[i].state = TexState::FREE;
+ }
+ }
+
+ for (unsigned i = 0; i < count; ++i)
+ if (slots[i].state == TexState::FREE)
+ {
+ Seq::Inc(slots[i].gen);
+ slots[i].state = TexState::WRITE;
+ index = i;
+ generation = slots[i].gen;
+ return TexResult::OK;
+ }
+
+ for (unsigned i = 0; i < count; ++i)
+ if (slots[i].state != TexState::DEAD)
+ return TexResult::BUSY;
+ return TexResult::FAILED;
+ }
+
+ bool Info(unsigned index, uint64_t generation,
+ ComPtr& resource) const
+ {
+ CSRWSharedLock guard(lock);
+ if (!open || failed || index >= count ||
+ slots[index].gen != generation ||
+ slots[index].state != TexState::WRITE)
+ return false;
+ resource = slots[index].res;
+ return true;
+ }
+
+ bool Seal(unsigned index, uint64_t generation, const D12Sync& sync)
+ {
+ CSRWExclusiveLock guard(lock);
+ if (!open || failed || index >= count ||
+ slots[index].gen != generation ||
+ slots[index].state != TexState::WRITE || !sync.Valid())
+ return false;
+ slots[index].sync = sync;
+ slots[index].state = TexState::LEASED;
+ return true;
+ }
+
+ void Fail(unsigned index, uint64_t generation, const D12Sync& sync)
+ {
+ D12Sync retired;
+ {
+ CSRWExclusiveLock guard(lock);
+ if (index >= count || slots[index].gen != generation ||
+ slots[index].state != TexState::WRITE)
+ return;
+ slots[index].sync = sync;
+ if (open)
+ slots[index].state = TexState::WAIT;
+ else
+ {
+ retired = sync;
+ slots[index].state = TexState::DEAD;
+ }
+ }
+ if (retired.Valid())
+ WaitSync(retired);
+ }
+
+ void Drop(unsigned index, uint64_t generation)
+ {
+ D12Sync retired;
+ {
+ CSRWExclusiveLock guard(lock);
+ if (index >= count || slots[index].gen != generation)
+ return;
+
+ Slot& slot = slots[index];
+ if (slot.state == TexState::WRITE)
+ {
+ slot.state = TexState::DEAD;
+ return;
+ }
+ if (slot.state != TexState::LEASED)
+ return;
+
+ const D12SyncState state = slot.sync.State();
+ if (!open)
+ {
+ retired = slot.sync;
+ slot.state = TexState::DEAD;
+ }
+ else if (state == D12SyncState::FAILED)
+ {
+ slot.state = TexState::DEAD;
+ failed = true;
+ }
+ else if (state == D12SyncState::READY)
+ {
+ slot.sync = D12Sync {};
+ slot.state = TexState::FREE;
+ }
+ else
+ slot.state = TexState::WAIT;
+ }
+ if (retired.Valid())
+ WaitSync(retired);
+ }
+
+ void Cancel(unsigned index, uint64_t generation)
+ {
+ CSRWExclusiveLock guard(lock);
+ if (index >= count || slots[index].gen != generation ||
+ slots[index].state != TexState::WRITE)
+ return;
+ slots[index].state = open ? TexState::FREE : TexState::DEAD;
+ }
+
+ void Retire(unsigned index, uint64_t generation)
+ {
+ CSRWExclusiveLock guard(lock);
+ if (index < count && slots[index].gen == generation &&
+ slots[index].state == TexState::WRITE)
+ slots[index].state = TexState::DEAD;
+ }
+
+ void Close()
+ {
+ D12Sync waits[CTexPool::MAX_SLOTS];
+ unsigned waitCount = 0;
+ {
+ CSRWExclusiveLock guard(lock);
+ if (closed)
+ return;
+ open = false;
+ closed = true;
+ for (unsigned i = 0; i < count; ++i)
+ {
+ if ((slots[i].state == TexState::LEASED ||
+ slots[i].state == TexState::WAIT) &&
+ slots[i].sync.Valid())
+ waits[waitCount++] = slots[i].sync;
+ if (slots[i].state != TexState::LEASED &&
+ slots[i].state != TexState::WRITE)
+ slots[i].state = TexState::DEAD;
+ }
+ }
+
+ // Reset is infrequent and cannot release a texture still referenced by
+ // producer commands. Outstanding consumers keep their own core alive.
+ for (unsigned i = 0; i < waitCount; ++i)
+ WaitSync(waits[i]);
+ }
+
+ void Stop()
+ {
+ CSRWExclusiveLock guard(lock);
+ open = false;
+ }
+};
+
+CFrameTex::CFrameTex(uint64_t graphValue, uint64_t poolValue,
+ unsigned nodeValue, unsigned slotValue, uint64_t versionValue,
+ const FrameProfile& profileValue,
+ const FrameDesc& frameValue, const ComPtr& res,
+ const D12Sync& sync, D3D12_RESOURCE_STATES state) :
+ m_res(res),
+ m_sync(sync),
+ m_state(state),
+ graph(graphValue),
+ pool(poolValue),
+ node(nodeValue),
+ slot(slotValue),
+ version(versionValue),
+ profile(profileValue),
+ frame(frameValue)
+{
+}
+
+bool CFrameTex::Wait(CD3D12CommandSlot& cmd) const
+{
+ if (!m_sync.Valid())
+ return false;
+ const UINT64 completed = m_sync.fence->GetCompletedValue();
+ if (completed == UINT64_MAX)
+ return false;
+ if (completed >= m_sync.value)
+ return true;
+ return cmd.WaitFor(m_sync.fence.Get(), m_sync.value);
+}
+
+TexWrite::TexWrite(const std::shared_ptr& core,
+ unsigned index, uint64_t version) :
+ m_core(core), m_index(index), m_version(version)
+{
+}
+
+TexWrite::TexWrite(TexWrite&& other) noexcept :
+ m_core(std::move(other.m_core)),
+ m_index(other.m_index),
+ m_version(other.m_version)
+{
+ other.Clear();
+}
+
+TexWrite& TexWrite::operator=(TexWrite&& other) noexcept
+{
+ if (this == &other)
+ return *this;
+ Retire();
+ m_core = std::move(other.m_core);
+ m_index = other.m_index;
+ m_version = other.m_version;
+ other.Clear();
+ return *this;
+}
+
+TexWrite::~TexWrite()
+{
+ Retire();
+}
+
+void TexWrite::Retire()
+{
+ if (m_core)
+ m_core->Retire(m_index, m_version);
+ Clear();
+}
+
+void TexWrite::Clear()
+{
+ m_core.reset();
+ m_index = 0;
+ m_version = 0;
+}
+
+ID3D12Resource * TexWrite::Get() const
+{
+ ComPtr resource;
+ return m_core && m_core->Info(m_index, m_version, resource) ?
+ resource.Get() : nullptr;
+}
+
+bool TexWrite::Seal(const FrameDesc& frame, const D12Sync& sync,
+ TexLease& lease)
+{
+ if (!m_core || !sync.Valid())
+ return false;
+
+ const std::shared_ptr core = m_core;
+ ComPtr resource;
+ FrameDesc desc = frame;
+ if (!core->Info(m_index, m_version, resource) ||
+ !ValidFrame(core->profile, resource->GetDesc(), desc))
+ {
+ core->Fail(m_index, m_version, sync);
+ Clear();
+ return false;
+ }
+
+ // The graph descriptor is resource-free and canonical. Publish the exact
+ // pool descriptor, including producer-only UAV creation capability.
+ desc.format.desc = resource->GetDesc();
+
+ const unsigned index = m_index;
+ const uint64_t generation = m_version;
+ if (!core->Seal(index, generation, sync))
+ {
+ core->Fail(index, generation, sync);
+ Clear();
+ return false;
+ }
+
+ CFrameTex * raw = new (std::nothrow) CFrameTex(core->graph, core->id,
+ core->node, m_index, m_version, core->profile, desc, resource, sync,
+ core->read);
+ if (!raw)
+ {
+ core->Drop(index, generation);
+ Clear();
+ return false;
+ }
+
+ std::shared_ptr product;
+ try
+ {
+ product = std::shared_ptr(raw,
+ [core, index, generation](const CFrameTex * texture)
+ {
+ delete texture;
+ core->Drop(index, generation);
+ });
+ }
+ catch (const std::bad_alloc&)
+ {
+ // The shared_ptr constructor invokes the supplied deleter on failure.
+ Clear();
+ return false;
+ }
+
+ Clear();
+ lease = TexLease(product);
+ return true;
+}
+
+void TexWrite::Cancel()
+{
+ if (m_core)
+ m_core->Cancel(m_index, m_version);
+ Clear();
+}
+
+TexResult CTexPool::Init(ID3D12Device3 * device, uint64_t graph,
+ unsigned node, const FrameProfile& profile,
+ const D3D12_RESOURCE_DESC& desc, D3D12_RESOURCE_STATES state,
+ unsigned slots)
+{
+ if (!device || !graph || !node || !slots || slots > MAX_SLOTS ||
+ profile.storage != FrameStorage::D3D12_TEXTURE ||
+ !Frame::Valid(profile) ||
+ desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D ||
+ !desc.Width || !desc.Height || desc.Width > UINT_MAX ||
+ desc.Format != D12::Dxgi(profile.pixel) ||
+ desc.DepthOrArraySize != 1 ||
+ desc.MipLevels != 1 ||
+ desc.SampleDesc.Count != 1 ||
+ desc.SampleDesc.Quality != 0 ||
+ desc.Alignment != 0 ||
+ desc.Layout != D3D12_TEXTURE_LAYOUT_UNKNOWN ||
+ (static_cast(desc.Flags) &
+ ~static_cast(D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS |
+ D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS)) != 0 ||
+ node >= FRAME_GRAPH_MAX_NODES ||
+ state != D3D12_RESOURCE_STATE_COMMON)
+ return TexResult::REJECTED;
+
+ std::shared_ptr core;
+ try
+ {
+ core = std::shared_ptr(new (std::nothrow) CTexCore);
+ }
+ catch (const std::bad_alloc&)
+ {
+ return TexResult::FAILED;
+ }
+ if (!core)
+ return TexResult::FAILED;
+ core->graph = graph;
+ core->id = NextPool();
+ core->node = node;
+ core->count = slots;
+ core->profile = profile;
+ core->desc = desc;
+ core->read = state;
+
+ D3D12_HEAP_PROPERTIES heap = {};
+ heap.Type = D3D12_HEAP_TYPE_DEFAULT;
+ heap.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
+ heap.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
+ heap.CreationNodeMask = 1;
+ heap.VisibleNodeMask = 1;
+
+ for (unsigned i = 0; i < slots; ++i)
+ {
+ const HRESULT hr = device->CreateCommittedResource(&heap,
+ D3D12_HEAP_FLAG_CREATE_NOT_ZEROED, &desc, state, nullptr,
+ IID_PPV_ARGS(&core->slots[i].res));
+ if (FAILED(hr))
+ {
+ DEBUG_ERROR_HR(hr, "Failed to create frame texture");
+ core->Close();
+ return TexResult::FAILED;
+ }
+ core->slots[i].res->SetName(L"Frame Texture");
+ }
+
+ std::shared_ptr old;
+ {
+ CSRWExclusiveLock guard(m_lock);
+ old = std::move(m_core);
+ if (old)
+ old->Stop();
+ m_core = core;
+ }
+ if (old)
+ old->Close();
+ return TexResult::OK;
+}
+
+void CTexPool::Reset()
+{
+ std::shared_ptr core;
+ {
+ CSRWExclusiveLock guard(m_lock);
+ core = std::move(m_core);
+ if (core)
+ core->Stop();
+ }
+ if (core)
+ core->Close();
+}
+
+TexResult CTexPool::Try(TexWrite& write)
+{
+ if (write)
+ return TexResult::REJECTED;
+
+ std::shared_ptr core;
+ {
+ CSRWSharedLock guard(m_lock);
+ core = m_core;
+ }
+ if (!core)
+ return TexResult::FAILED;
+ unsigned index;
+ uint64_t generation;
+ const TexResult result = core->Acquire(index, generation);
+ if (result == TexResult::OK)
+ write = TexWrite(core, index, generation);
+ return result;
+}
diff --git a/idd/LGIdd/capture/CFrameTex.h b/idd/LGIdd/capture/CFrameTex.h
new file mode 100644
index 00000000..7a6dee31
--- /dev/null
+++ b/idd/LGIdd/capture/CFrameTex.h
@@ -0,0 +1,158 @@
+/**
+ * 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 "CSRWLock.h"
+#include "capture/CFrameGraph.h"
+#include "d3d/CD3D12CommandQueue.h"
+
+#include
+
+struct CTexCore;
+class TexWrite;
+
+enum class TexResult : uint8_t
+{
+ OK,
+ BUSY,
+ REJECTED,
+ FAILED,
+};
+
+// An immutable, pool-owned D3D12 graph product. Node zero is deliberately
+// excluded because it aliases the acquired IddCx surface.
+class CFrameTex final
+{
+ friend class TexWrite;
+
+private:
+ ComPtr m_res;
+ D12Sync m_sync;
+ D3D12_RESOURCE_STATES m_state;
+
+ CFrameTex(uint64_t graph, uint64_t pool, unsigned node, unsigned slot,
+ uint64_t version, const FrameProfile& profile, const FrameDesc& frame,
+ const ComPtr& res, const D12Sync& sync,
+ D3D12_RESOURCE_STATES state);
+
+public:
+ CFrameTex(const CFrameTex&) = delete;
+ CFrameTex& operator=(const CFrameTex&) = delete;
+
+ const uint64_t graph;
+ const uint64_t pool;
+ const unsigned node;
+ const unsigned slot;
+ const uint64_t version;
+ const FrameProfile profile;
+ const FrameDesc frame;
+
+ ID3D12Resource * Get() const { return m_res.Get(); }
+ const D12Sync& Sync() const { return m_sync; }
+ D3D12_RESOURCE_STATES State() const { return m_state; }
+ bool Wait(CD3D12CommandSlot& cmd) const;
+ D12SyncState Status() const { return m_sync.State(); }
+ bool Ready() const { return m_sync.Done(); }
+};
+
+// Consumers retain a lease until their own GPU use of the texture has
+// completed. Dropping the CPU call's local copy is not sufficient.
+class TexLease
+{
+ friend class TexWrite;
+
+private:
+ std::shared_ptr m_tex;
+ explicit TexLease(std::shared_ptr tex) : m_tex(tex) {}
+
+public:
+ TexLease() = default;
+ TexLease(const TexLease&) = default;
+ TexLease& operator=(const TexLease&) = default;
+ TexLease(TexLease&&) noexcept = default;
+ TexLease& operator=(TexLease&&) noexcept = default;
+
+ explicit operator bool() const { return !!m_tex; }
+ const CFrameTex * Get() const { return m_tex.get(); }
+ const CFrameTex * operator->() const { return m_tex.get(); }
+ void Reset() { m_tex.reset(); }
+};
+
+// A move-only producer reservation. Abandoning a writer retires its slot;
+// Cancel may be used only before any command referencing the texture submits.
+class TexWrite
+{
+ friend class CTexPool;
+
+private:
+ std::shared_ptr m_core;
+ unsigned m_index = 0;
+ uint64_t m_version = 0;
+
+ TexWrite(const std::shared_ptr& core,
+ unsigned index, uint64_t version);
+ void Clear();
+ void Retire();
+
+public:
+ TexWrite() = default;
+ TexWrite(const TexWrite&) = delete;
+ TexWrite& operator=(const TexWrite&) = delete;
+ TexWrite(TexWrite&& other) noexcept;
+ TexWrite& operator=(TexWrite&& other) noexcept;
+ ~TexWrite();
+
+ explicit operator bool() const { return !!m_core; }
+ ID3D12Resource * Get() const;
+ // Seal only after producer commands restore the texture to the pool's
+ // configured immutable state and Execute returns this exact sync point.
+ bool Seal(const FrameDesc& frame, const D12Sync& sync, TexLease& lease);
+ void Cancel();
+};
+
+// One pool owns interchangeable textures for a single graph node. Reset stops
+// acquisition and drains every sealed producer point. Existing writers and
+// leases keep the retired core alive. Reset invalidates writers; a writer
+// which already submitted must still Seal so its point is safely drained,
+// but it will not publish a product from the retired generation.
+// The configured state is the promised state presented to every consumer;
+// it is not queried from D3D12. Consumers may read but never transition or
+// write a shared product.
+class CTexPool
+{
+private:
+ mutable CSRWLock m_lock;
+ std::shared_ptr m_core;
+
+public:
+ static const unsigned MAX_SLOTS = 4;
+
+ CTexPool() = default;
+ CTexPool(const CTexPool&) = delete;
+ CTexPool& operator=(const CTexPool&) = delete;
+ ~CTexPool() { Reset(); }
+
+ TexResult Init(ID3D12Device3 * device, uint64_t graph, unsigned node,
+ const FrameProfile& profile, const D3D12_RESOURCE_DESC& desc,
+ D3D12_RESOURCE_STATES state, unsigned slots);
+ void Reset();
+ TexResult Try(TexWrite& write);
+};
diff --git a/idd/LGIdd/d3d/CD3D12CommandQueue.cpp b/idd/LGIdd/d3d/CD3D12CommandQueue.cpp
index 0e205db7..fd7f3a16 100644
--- a/idd/LGIdd/d3d/CD3D12CommandQueue.cpp
+++ b/idd/LGIdd/d3d/CD3D12CommandQueue.cpp
@@ -236,8 +236,11 @@ void CD3D12CommandSlot::Cancel()
SetEvent(m_availableEvent.Get());
}
-bool CD3D12CommandSlot::Execute()
+bool CD3D12CommandSlot::Execute(D12Sync * sync)
{
+ if (sync)
+ *sync = D12Sync {};
+
State expected = STATE_RECORDING;
if (!Atomic::CAS(m_state, expected, STATE_SUBMITTED,
std::memory_order_acq_rel))
@@ -256,7 +259,7 @@ bool CD3D12CommandSlot::Execute()
return false;
}
- if (m_queue->Submit(*this))
+ if (m_queue->Submit(*this, sync))
return true;
if (!Atomic::Load(m_submitted, std::memory_order_acquire))
@@ -572,7 +575,7 @@ void CD3D12CommandQueue::WaitForIdle()
}
}
-bool CD3D12CommandQueue::Submit(CD3D12CommandSlot& slot)
+bool CD3D12CommandQueue::Submit(CD3D12CommandSlot& slot, D12Sync * sync)
{
bool result = false;
CSRWExclusiveLock lock(m_submitLock);
@@ -612,6 +615,14 @@ bool CD3D12CommandQueue::Submit(CD3D12CommandSlot& slot)
break;
}
+ // Capture the immutable point while submission is serialized. The slot
+ // may complete and be acquired again as soon as we leave this scope.
+ if (sync)
+ {
+ sync->fence = m_fence;
+ sync->value = fenceTarget;
+ }
+
hr = m_fence->SetEventOnCompletion(fenceTarget, slot.m_event.Get());
if (FAILED(hr))
{
diff --git a/idd/LGIdd/d3d/CD3D12CommandQueue.h b/idd/LGIdd/d3d/CD3D12CommandQueue.h
index 24f9bb0d..7ed20f0b 100644
--- a/idd/LGIdd/d3d/CD3D12CommandQueue.h
+++ b/idd/LGIdd/d3d/CD3D12CommandQueue.h
@@ -35,6 +35,32 @@ using namespace Microsoft::WRL::Wrappers::HandleTraits;
class CD3D12CommandQueue;
+enum class D12SyncState : uint8_t
+{
+ PENDING,
+ READY,
+ FAILED,
+};
+
+struct D12Sync
+{
+ ComPtr fence;
+ UINT64 value = 0;
+
+ bool Valid() const { return fence && value; }
+ D12SyncState State() const
+ {
+ if (!Valid())
+ return D12SyncState::FAILED;
+ const UINT64 completed = fence->GetCompletedValue();
+ if (completed == UINT64_MAX)
+ return D12SyncState::FAILED;
+ return completed >= value ?
+ D12SyncState::READY : D12SyncState::PENDING;
+ }
+ bool Done() const { return State() == D12SyncState::READY; }
+};
+
class CD3D12CommandSlot
{
friend class CD3D12CommandQueue;
@@ -104,7 +130,7 @@ class CD3D12CommandSlot
bool Acquire();
void Cancel();
- bool Execute();
+ bool Execute(D12Sync * sync = nullptr);
bool BeginTiming();
void EndTiming();
@@ -162,7 +188,7 @@ class CD3D12CommandQueue
bool m_timingSupported = false;
bool InitTiming(ID3D12Device3 * device, UINT slotCount);
- bool Submit(CD3D12CommandSlot& slot);
+ bool Submit(CD3D12CommandSlot& slot, D12Sync * sync);
bool SnapshotTiming(CD3D12CommandSlot& slot) const;
bool GetGPUTimes(const CD3D12CommandSlot& slot,
uint64_t& start, uint64_t& end) const;