diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj
index 7a2e73af..1ec8e7c7 100644
--- a/idd/LGIdd/LGIdd.vcxproj
+++ b/idd/LGIdd/LGIdd.vcxproj
@@ -68,6 +68,7 @@
+
@@ -124,10 +125,12 @@
+
+
@@ -135,6 +138,8 @@
+
+
diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters
index c5ef57c4..d3c4edde 100644
--- a/idd/LGIdd/LGIdd.vcxproj.filters
+++ b/idd/LGIdd/LGIdd.vcxproj.filters
@@ -175,6 +175,9 @@
Transport
+
+ Transport
+
Transport
@@ -187,6 +190,9 @@
Transport
+
+ Transport
+
Transport
@@ -208,6 +214,12 @@
Transport
+
+ Transport
+
+
+ Transport
+
Transport
@@ -357,6 +369,9 @@
Transport
+
+ Transport
+
Transport
diff --git a/idd/LGIdd/capture/CFrameGraph.cpp b/idd/LGIdd/capture/CFrameGraph.cpp
index a6ab2922..f5f7ad1d 100644
--- a/idd/LGIdd/capture/CFrameGraph.cpp
+++ b/idd/LGIdd/capture/CFrameGraph.cpp
@@ -66,11 +66,12 @@ bool Frame::Valid(FrameDamage damage, const RECT * rects, unsigned count,
void CFrameGraph::Reset()
{
- m_cfg = GraphCfg {};
- m_nodeCount = 0;
- m_leafCount = 0;
- m_begun = false;
- m_sealed = false;
+ m_cfg = GraphCfg {};
+ m_generation = 0;
+ m_nodeCount = 0;
+ m_leafCount = 0;
+ m_begun = false;
+ m_sealed = false;
}
bool CFrameGraph::Begin(const GraphCfg& cfg)
@@ -288,6 +289,14 @@ bool CFrameGraph::Seal()
return true;
}
+bool CFrameGraph::Stamp(uint64_t generation)
+{
+ if (!m_sealed || m_generation || !generation)
+ return false;
+ m_generation = generation;
+ return true;
+}
+
bool CFrameGraph::Same(const GraphCfg& cfg) const
{
return m_sealed && Frame::Same(m_cfg, cfg);
diff --git a/idd/LGIdd/capture/CFrameGraph.h b/idd/LGIdd/capture/CFrameGraph.h
index 21fedd18..b513d3de 100644
--- a/idd/LGIdd/capture/CFrameGraph.h
+++ b/idd/LGIdd/capture/CFrameGraph.h
@@ -131,6 +131,7 @@ private:
GraphCfg m_cfg;
GraphNode m_nodes[FRAME_GRAPH_MAX_NODES] = {};
GraphLeaf m_leaves[TRANSPORT_MAX_INSTANCES] = {};
+ uint64_t m_generation = 0;
unsigned m_nodeCount = 0;
unsigned m_leafCount = 0;
bool m_begun = false;
@@ -150,6 +151,7 @@ public:
bool Add(BackendId id, uint32_t epoch, bool required, bool primary,
const FrameCfg& cfg);
bool Seal();
+ bool Stamp(uint64_t generation);
bool Same(const GraphCfg& cfg) const;
bool Want(FrameSignal signal) const;
bool Need(FrameOp op) const;
@@ -158,6 +160,7 @@ public:
LeafDesc& desc) const;
const GraphCfg& Cfg() const { return m_cfg; }
+ uint64_t Generation() const { return m_generation; }
const GraphNode * Nodes(unsigned& count) const;
const GraphLeaf * Leaves(unsigned& count) const;
};
diff --git a/idd/LGIdd/capture/CFrameTex.cpp b/idd/LGIdd/capture/CFrameTex.cpp
index 62909b52..c01472c2 100644
--- a/idd/LGIdd/capture/CFrameTex.cpp
+++ b/idd/LGIdd/capture/CFrameTex.cpp
@@ -48,6 +48,22 @@ static uint64_t NextPool()
return Atomic::Next(next);
}
+static bool ValidTime(const FrameTime& time)
+{
+ if (!time.postStart ||
+ (!time.gpuValid && (time.gpuStart || time.gpuEnd)) ||
+ (!time.readyValid && time.readyAt))
+ return false;
+ if (time.gpuValid && (!time.gpuStart ||
+ time.gpuStart < time.postStart ||
+ time.gpuEnd < time.gpuStart))
+ return false;
+ if (time.readyValid && (!time.readyAt ||
+ time.readyAt < (time.gpuValid ? time.gpuEnd : time.postStart)))
+ return false;
+ return true;
+}
+
static bool ValidFrame(const FrameProfile& profile,
const D3D12_RESOURCE_DESC& resource, const FrameDesc& frame)
{
@@ -279,7 +295,8 @@ struct CTexCore
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 FrameDesc& frameValue, const FrameTime& timeValue,
+ const ComPtr& res,
const D12Sync& sync, D3D12_RESOURCE_STATES state, bool sharedValue) :
m_res(res),
m_sync(sync),
@@ -291,7 +308,8 @@ CFrameTex::CFrameTex(uint64_t graphValue, uint64_t poolValue,
slot(slotValue),
version(versionValue),
profile(profileValue),
- frame(frameValue)
+ frame(frameValue),
+ time(timeValue)
{
}
@@ -359,13 +377,19 @@ ID3D12Resource * TexWrite::Get() const
resource.Get() : nullptr;
}
-bool TexWrite::Seal(const FrameDesc& frame, const D12Sync& sync,
- TexLease& lease)
+bool TexWrite::Seal(const FrameDesc& frame, const FrameTime& time,
+ const D12Sync& sync, TexLease& lease)
{
if (!m_core || !sync.Valid())
return false;
const std::shared_ptr core = m_core;
+ if (!ValidTime(time))
+ {
+ core->Fail(m_index, m_version, sync);
+ Clear();
+ return false;
+ }
ComPtr resource;
FrameDesc desc = frame;
if (!core->Info(m_index, m_version, resource) ||
@@ -390,8 +414,8 @@ bool TexWrite::Seal(const FrameDesc& frame, const D12Sync& sync,
}
CFrameTex * raw = new (std::nothrow) CFrameTex(core->graph, core->id,
- core->node, m_index, m_version, core->profile, desc, resource, sync,
- core->read, core->shared);
+ core->node, m_index, m_version, core->profile, desc, time, resource,
+ sync, core->read, core->shared);
if (!raw)
{
core->Drop(index, generation);
diff --git a/idd/LGIdd/capture/CFrameTex.h b/idd/LGIdd/capture/CFrameTex.h
index 73bc8793..465991b7 100644
--- a/idd/LGIdd/capture/CFrameTex.h
+++ b/idd/LGIdd/capture/CFrameTex.h
@@ -37,6 +37,18 @@ enum class TexResult : uint8_t
FAILED,
};
+// Immutable producer boundaries for one node product. A transport adds its
+// own transfer or encode timing without rewriting these common values.
+struct FrameTime
+{
+ uint64_t postStart = 0;
+ uint64_t gpuStart = 0;
+ uint64_t gpuEnd = 0;
+ uint64_t readyAt = 0;
+ bool gpuValid = false;
+ bool readyValid = false;
+};
+
// An immutable, pool-owned D3D12 graph product. Node zero is deliberately
// excluded because it aliases the acquired IddCx surface.
class CFrameTex final
@@ -51,7 +63,8 @@ private:
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,
+ const FrameTime& time, const ComPtr& res,
+ const D12Sync& sync,
D3D12_RESOURCE_STATES state, bool shared);
public:
@@ -65,6 +78,7 @@ public:
const uint64_t version;
const FrameProfile profile;
const FrameDesc frame;
+ const FrameTime time;
ID3D12Resource * Get() const { return m_res.Get(); }
const D12Sync& Sync() const { return m_sync; }
@@ -126,7 +140,8 @@ public:
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);
+ bool Seal(const FrameDesc& frame, const FrameTime& time,
+ const D12Sync& sync, TexLease& lease);
void Cancel();
};
diff --git a/idd/LGIdd/capture/CSwapChainProcessor.cpp b/idd/LGIdd/capture/CSwapChainProcessor.cpp
index 673d0f5a..fdfaea1f 100644
--- a/idd/LGIdd/capture/CSwapChainProcessor.cpp
+++ b/idd/LGIdd/capture/CSwapChainProcessor.cpp
@@ -533,17 +533,29 @@ void CSwapChainProcessor::QueueGraph(const D12FrameFormat& source,
source, checkpoint, m_dx11Device->IsSoftware(),
m_renderAdapter, cfg))
return;
- if (m_haveGraphCfg && Frame::Same(m_graphCfg, cfg))
+ CTransportManager& transport = m_devContext->GetTransport();
+ const uint64_t revision = transport.FrameRev();
+ if (m_haveGraphCfg && m_graphRev == revision &&
+ Frame::Same(m_graphCfg, cfg))
return;
m_graphCfg = cfg;
m_haveGraphCfg = true;
m_graphPending = true;
+ m_graphRev = revision;
m_graphRetryAt = 0;
}
void CSwapChainProcessor::CfgGraph()
{
+ CTransportManager& transport = m_devContext->GetTransport();
+ const uint64_t revision = transport.FrameRev();
+ if (m_haveGraphCfg && revision != m_graphRev)
+ {
+ m_graphRev = revision;
+ m_graphPending = true;
+ m_graphRetryAt = 0;
+ }
if (!m_graphPending)
return;
@@ -553,7 +565,7 @@ void CSwapChainProcessor::CfgGraph()
m_graphPending = false;
const CfgResult result =
- m_devContext->GetTransport().Cfg(m_graphCfg, m_graph);
+ transport.Cfg(m_graphCfg, m_graphRev, m_graph);
if (result == CfgResult::ACCEPTED)
{
m_graphRetryAt = 0;
diff --git a/idd/LGIdd/capture/CSwapChainProcessor.h b/idd/LGIdd/capture/CSwapChainProcessor.h
index 153a65e1..81c2c615 100644
--- a/idd/LGIdd/capture/CSwapChainProcessor.h
+++ b/idd/LGIdd/capture/CSwapChainProcessor.h
@@ -61,6 +61,7 @@ private:
GraphCfg m_graphCfg;
bool m_haveGraphCfg = false;
bool m_graphPending = false;
+ uint64_t m_graphRev = 0;
uint64_t m_graphRetryAt = 0;
CPostProcessor m_postProcessors[CAPTURE_PIPELINE_SLOTS];
std::unique_ptr m_frameProcessor;
diff --git a/idd/LGIdd/transport/CTexHub.cpp b/idd/LGIdd/transport/CTexHub.cpp
new file mode 100644
index 00000000..35e79c05
--- /dev/null
+++ b/idd/LGIdd/transport/CTexHub.cpp
@@ -0,0 +1,644 @@
+/**
+ * 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 "transport/CTexHub.h"
+
+#include "capture/CFrameGraph.h"
+#include "capture/CFrameTex.h"
+#include "d3d/CInteropPool.h"
+#include "transport/ITexSink.h"
+#include "transport/ITransport.h"
+
+#include
+#include
+#include
+#include
+
+namespace
+{
+ template
+ T& Append(T (&items)[N], unsigned& count)
+ {
+ if (count == N)
+ {
+ for (unsigned i = 1; i < count; ++i)
+ items[i - 1] = items[i];
+ --count;
+ }
+ return items[count++];
+ }
+}
+
+struct CTexSet
+{
+ struct Route
+ {
+ std::shared_ptr owner;
+ ITexSink * sink = nullptr;
+ BackendId id = 0;
+ uint32_t epoch = 0;
+ unsigned node = 0;
+ unsigned leaf = 0;
+ FrameProfile profile;
+ FrameCfg cfg;
+ bool required = false;
+ bool primary = false;
+ bool active = false;
+ bool dropped = false;
+ CfgResult fault = CfgResult::ACCEPTED;
+ unsigned calls = 0;
+ HANDLE idle = nullptr;
+
+ ~Route()
+ {
+ if (idle)
+ CloseHandle(idle);
+ }
+ };
+
+ CSRWLock lock;
+ CFrameGraph graph;
+ uint64_t generation = 0;
+ unsigned count = 0;
+ Route routes[TRANSPORT_MAX_INSTANCES];
+};
+
+static bool SameFrame(const FrameDesc& left, const FrameDesc& right,
+ bool ignoreFlags = false)
+{
+ if (left.content != right.content ||
+ left.damage != right.damage ||
+ left.count != right.count ||
+ left.count > FRAME_DAMAGE_MAX ||
+ !D12::Same(left.format, right.format, ignoreFlags ?
+ D12::FormatCmp::NO_FLAGS : D12::FormatCmp::EXACT))
+ return false;
+ return !left.count || memcmp(left.rects, right.rects,
+ left.count * sizeof(*left.rects)) == 0;
+}
+
+static unsigned Find(const CTexSet& set, const FrameIn& frame)
+{
+ for (unsigned i = 0; i < set.count; ++i)
+ {
+ const CTexSet::Route& route = set.routes[i];
+ if (route.id == frame.desc.id &&
+ route.epoch == frame.desc.epoch &&
+ route.node == frame.desc.node &&
+ Frame::Same(route.profile, frame.desc.profile))
+ return i;
+ }
+ return TRANSPORT_MAX_INSTANCES;
+}
+
+static bool Valid(const CTexSet& set, const CTexSet::Route& route,
+ const FrameIn& frame, const CFrameTex& tex, FrameStorage storage)
+{
+ const FrameProfile product =
+ Frame::Store(route.profile, FrameStorage::D3D12_TEXTURE);
+ LeafDesc expected;
+ if (!set.graph.Desc(route.leaf, frame.desc.frame.content, expected) ||
+ expected.id != frame.desc.id ||
+ expected.epoch != frame.desc.epoch ||
+ expected.node != frame.desc.node ||
+ !Frame::Same(expected.profile, frame.desc.profile) ||
+ !SameFrame(expected.frame, frame.desc.frame) ||
+ !SameFrame(expected.frame, tex.frame, true))
+ return false;
+ return
+ frame.graph != 0 &&
+ frame.desc.node != 0 &&
+ frame.desc.frame.content &&
+ route.profile.storage == storage &&
+ tex.graph == frame.graph &&
+ tex.node == route.node &&
+ Frame::Same(tex.profile, product);
+}
+
+CTexStage::CTexStage(CTexStage&& other) noexcept :
+ m_owner(other.m_owner), m_next(std::move(other.m_next))
+{
+ other.m_owner = nullptr;
+}
+
+CTexStage& CTexStage::operator=(CTexStage&& other) noexcept
+{
+ if (this == &other)
+ return *this;
+ if (m_owner)
+ m_owner->Abort(*this);
+ m_owner = other.m_owner;
+ m_next = std::move(other.m_next);
+ other.m_owner = nullptr;
+ return *this;
+}
+
+CTexStage::~CTexStage()
+{
+ if (m_owner)
+ m_owner->Abort(*this);
+}
+
+CTexHub::CTexHub(std::atomic& rev) : m_rev(rev)
+{
+ m_idle = CreateEvent(nullptr, TRUE, TRUE, nullptr);
+}
+
+CTexHub::~CTexHub()
+{
+ Stop();
+ if (m_idle)
+ CloseHandle(m_idle);
+}
+
+bool CTexHub::Enter(const FrameIn& frame, bool lease,
+ std::shared_ptr& set, unsigned& route, ITexSink *& sink)
+{
+ route = TRANSPORT_MAX_INSTANCES;
+ sink = nullptr;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_open || m_stopped || !m_active)
+ return false;
+ set = m_active;
+ if (!m_calls++)
+ ResetEvent(m_idle);
+ }
+
+ CSRWExclusiveLock lock(set->lock);
+ route = Find(*set, frame);
+ if (route >= set->count || !set->routes[route].active || !lease ||
+ frame.graph != set->generation)
+ return false;
+
+ CTexSet::Route& target = set->routes[route];
+ if (!target.calls++)
+ ResetEvent(target.idle);
+ sink = target.sink;
+ return true;
+}
+
+void CTexHub::Finish(const std::shared_ptr& set,
+ unsigned route, PushResult result)
+{
+ if (result == PushResult::REJECTED || result == PushResult::FAILED)
+ Disable(set, route, result);
+ Leave(set, route);
+}
+
+void CTexHub::Bump()
+{
+ Atomic::Next(m_rev, std::memory_order_release);
+}
+
+bool CTexHub::Match(const FaultRec& fault, BackendId id, uint32_t epoch,
+ const FrameCfg& cfg, bool anyCfg)
+{
+ return
+ fault.id == id &&
+ fault.epoch == epoch &&
+ (anyCfg || fault.result == CfgResult::FAILED ||
+ Frame::Same(fault.cfg, cfg));
+}
+
+CfgResult CTexHub::Faulted(BackendId id, uint32_t epoch,
+ const FrameCfg& cfg) const
+{
+ CSRWSharedLock lock(m_lock);
+ for (unsigned i = 0; i < m_faultCount; ++i)
+ if (Match(m_faults[i], id, epoch, cfg))
+ return m_faults[i].result;
+ return CfgResult::ACCEPTED;
+}
+
+void CTexHub::Rebind(BackendId id, uint32_t epoch)
+{
+ if (!id || !epoch)
+ return;
+ CSRWExclusiveLock lock(m_lock);
+ for (unsigned i = 0; i < m_faultCount;)
+ {
+ if (m_faults[i].id != id || m_faults[i].epoch != epoch)
+ {
+ ++i;
+ continue;
+ }
+ m_faults[i] = m_faults[--m_faultCount];
+ m_faults[m_faultCount] = FaultRec {};
+ }
+}
+
+void CTexHub::Leave(
+ const std::shared_ptr& set, unsigned route)
+{
+ if (set && route < set->count)
+ {
+ CSRWExclusiveLock setLock(set->lock);
+ CTexSet::Route& target = set->routes[route];
+ if (target.calls && !--target.calls && target.idle)
+ SetEvent(target.idle);
+ }
+ CSRWExclusiveLock lock(m_lock);
+ if (m_calls && !--m_calls)
+ SetEvent(m_idle);
+}
+
+void CTexHub::Disable(const std::shared_ptr& set,
+ unsigned route, PushResult result)
+{
+ bool found = false;
+ CTexSet::Route failed;
+ {
+ CSRWExclusiveLock lock(set->lock);
+ if (route < set->count && !set->routes[route].dropped &&
+ (set->routes[route].active ||
+ (result == PushResult::FAILED &&
+ set->routes[route].fault == CfgResult::REJECTED)))
+ {
+ CTexSet::Route& target = set->routes[route];
+ target.active = false;
+ target.fault = result == PushResult::FAILED ?
+ CfgResult::FAILED : CfgResult::REJECTED;
+ failed.id = target.id;
+ failed.epoch = target.epoch;
+ failed.cfg = target.cfg;
+ found = true;
+ }
+ }
+ if (!found)
+ return;
+
+ FaultRec record;
+ record.id = failed.id;
+ record.epoch = failed.epoch;
+ record.cfg = failed.cfg;
+ record.result = result == PushResult::FAILED ?
+ CfgResult::FAILED : CfgResult::REJECTED;
+
+ bool wake = false;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ for (unsigned i = 0; i < m_faultCount; ++i)
+ if (Match(m_faults[i], failed.id, failed.epoch, failed.cfg,
+ result == PushResult::FAILED))
+ {
+ if (result == PushResult::FAILED &&
+ m_faults[i].result != CfgResult::FAILED)
+ {
+ m_faults[i].result = CfgResult::FAILED;
+ wake = true;
+ }
+ if (result != PushResult::FAILED)
+ {
+ lock.Unlock();
+ if (wake)
+ Bump();
+ return;
+ }
+ break;
+ }
+ bool faulted = false;
+ for (unsigned i = 0; i < m_faultCount; ++i)
+ if (Match(m_faults[i], failed.id, failed.epoch, failed.cfg))
+ {
+ faulted = true;
+ break;
+ }
+ if (!faulted)
+ {
+ Append(m_faults, m_faultCount) = record;
+ wake = true;
+ }
+ if (result == PushResult::FAILED)
+ {
+ bool queued = false;
+ for (unsigned i = 0; i < m_failureCount; ++i)
+ if (m_failures[i].id == failed.id &&
+ m_failures[i].epoch == failed.epoch)
+ {
+ queued = true;
+ break;
+ }
+ if (!queued)
+ {
+ Failure& source = Append(m_failures, m_failureCount);
+ source.id = failed.id;
+ source.epoch = failed.epoch;
+ }
+ wake = true;
+ }
+ }
+ if (wake)
+ Bump();
+}
+
+bool CTexHub::TakeFailure(BackendId& id, uint32_t& epoch)
+{
+ CSRWExclusiveLock lock(m_lock);
+ if (!m_failureCount)
+ return false;
+ id = m_failures[0].id;
+ epoch = m_failures[0].epoch;
+ for (unsigned i = 1; i < m_failureCount; ++i)
+ m_failures[i - 1] = m_failures[i];
+ m_failures[--m_failureCount] = Failure {};
+ return true;
+}
+
+CfgResult CTexHub::Hold(CTexStage& stage)
+{
+ if (stage.m_owner)
+ return CfgResult::REJECTED;
+ if (!m_idle)
+ return CfgResult::FAILED;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_stopped || m_changing || m_pending)
+ return CfgResult::RETRY;
+ m_changing = true;
+ m_open = false;
+ stage.m_owner = this;
+ if (m_calls)
+ ResetEvent(m_idle);
+ else
+ SetEvent(m_idle);
+ }
+ return WaitForSingleObject(m_idle, INFINITE) == WAIT_OBJECT_0 ?
+ CfgResult::ACCEPTED : CfgResult::FAILED;
+}
+
+CfgResult CTexHub::Prep(const CFrameGraph& graph,
+ const Bind * binds, unsigned count, CTexStage& stage)
+{
+ if (stage.m_owner != this || stage.m_next || !graph.Generation() ||
+ count > TRANSPORT_MAX_INSTANCES || (count && !binds))
+ return CfgResult::REJECTED;
+
+ unsigned leafCount = 0;
+ const GraphLeaf * leaves = graph.Leaves(leafCount);
+ if (!leaves || leafCount != count)
+ return CfgResult::REJECTED;
+
+ std::shared_ptr next;
+ try
+ {
+ next = std::make_shared();
+ }
+ catch (const std::bad_alloc&)
+ {
+ return CfgResult::FAILED;
+ }
+ next->generation = graph.Generation();
+ next->graph = graph;
+
+ for (unsigned leaf = 0; leaf < leafCount; ++leaf)
+ {
+ const GraphLeaf& route = leaves[leaf];
+ const Bind * binding = nullptr;
+ for (unsigned bind = 0; bind < count; ++bind)
+ if (binds[bind].id == route.id && binds[bind].epoch == route.epoch)
+ {
+ if (binding)
+ return CfgResult::FAILED;
+ binding = &binds[bind];
+ }
+ if (!binding || !binding->owner)
+ return CfgResult::FAILED;
+
+ // Legacy transports deliberately have no texture sink yet. Their normal
+ // frame service remains active while this route table stays dormant.
+ if (!binding->sink)
+ continue;
+
+ CTexSet::Route& target = next->routes[next->count++];
+ target.owner = binding->owner;
+ target.sink = binding->sink;
+ target.id = route.id;
+ target.epoch = route.epoch;
+ target.node = route.node;
+ target.leaf = leaf;
+ target.profile = route.cfg.profile;
+ target.cfg = route.cfg;
+ target.required = route.required;
+ target.primary = route.primary;
+ target.active = true;
+ target.idle = CreateEvent(nullptr, TRUE, TRUE, nullptr);
+ if (!target.idle)
+ return CfgResult::FAILED;
+ }
+
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_stopped || !m_changing || m_open || m_pending)
+ return CfgResult::RETRY;
+ m_pending = next;
+ stage.m_next = next;
+ }
+ return CfgResult::ACCEPTED;
+}
+
+void CTexHub::Commit(CTexStage& stage) noexcept
+{
+ std::shared_ptr old;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (stage.m_owner != this || stage.m_next != m_pending)
+ return;
+ old = std::move(m_active);
+ m_active = std::move(m_pending);
+ m_open = !m_stopped;
+ m_changing = false;
+ stage.m_owner = nullptr;
+ stage.m_next.reset();
+ }
+ old.reset();
+}
+
+void CTexHub::Abort(CTexStage& stage) noexcept
+{
+ std::shared_ptr drop;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (stage.m_owner != this)
+ return;
+ if (stage.m_next == m_pending)
+ {
+ drop = std::move(m_pending);
+ m_open = !m_stopped;
+ m_changing = false;
+ }
+ stage.m_owner = nullptr;
+ stage.m_next.reset();
+ }
+ drop.reset();
+}
+
+PushResult CTexHub::Push(FrameIn frame, TexLease lease) noexcept
+{
+ std::shared_ptr set;
+ unsigned index;
+ ITexSink * sink = nullptr;
+ if (!Enter(frame, !!lease, set, index, sink))
+ {
+ if (set)
+ Leave(set, TRANSPORT_MAX_INSTANCES);
+ return PushResult::STALE;
+ }
+
+ const D12SyncState state = lease->Status();
+ bool valid;
+ {
+ CSRWSharedLock lock(set->lock);
+ valid = sink &&
+ Valid(*set, set->routes[index], frame, *lease.Get(),
+ FrameStorage::D3D12_TEXTURE) &&
+ state != D12SyncState::FAILED &&
+ (!lease->time.readyValid || state == D12SyncState::READY);
+ }
+ if (!valid)
+ {
+ const PushResult result = state == D12SyncState::FAILED && sink ?
+ PushResult::FAILED : PushResult::STALE;
+ Finish(set, index, result);
+ return result;
+ }
+
+ const PushResult result = sink->Push(std::move(frame), std::move(lease));
+ Finish(set, index, result);
+ return result;
+}
+
+PushResult CTexHub::Push(FrameIn frame, D11Lease lease) noexcept
+{
+ std::shared_ptr set;
+ unsigned index;
+ ITexSink * sink = nullptr;
+ if (!Enter(frame, !!lease, set, index, sink))
+ {
+ if (set)
+ Leave(set, TRANSPORT_MAX_INSTANCES);
+ return PushResult::STALE;
+ }
+
+ const D12SyncState state = lease.Status();
+ const CFrameTex * source = lease.Frame();
+ const D12SyncState sourceState = source ?
+ source->Status() : D12SyncState::FAILED;
+ bool valid;
+ {
+ CSRWSharedLock lock(set->lock);
+ const CTexSet::Route& route = set->routes[index];
+ valid = sink &&
+ source &&
+ Frame::Same(lease.Profile(), route.profile) &&
+ Valid(*set, route, frame, *source,
+ FrameStorage::D3D11_TEXTURE) &&
+ state != D12SyncState::FAILED &&
+ sourceState != D12SyncState::FAILED &&
+ (!source->time.readyValid ||
+ (state == D12SyncState::READY &&
+ sourceState == D12SyncState::READY));
+ }
+ if (!valid)
+ {
+ const PushResult result = sink &&
+ (state == D12SyncState::FAILED ||
+ sourceState == D12SyncState::FAILED) ?
+ PushResult::FAILED : PushResult::STALE;
+ Finish(set, index, result);
+ return result;
+ }
+
+ const PushResult result = sink->Push(std::move(frame), std::move(lease));
+ Finish(set, index, result);
+ return result;
+}
+
+void CTexHub::Drop(BackendId id, uint32_t epoch)
+{
+ if (!id || !epoch)
+ return;
+
+ std::shared_ptr set;
+ {
+ CSRWSharedLock lock(m_lock);
+ if (m_stopped)
+ return;
+ set = m_active;
+ }
+ if (!set)
+ {
+ Rebind(id, epoch);
+ return;
+ }
+
+ HANDLE waits[TRANSPORT_MAX_INSTANCES] = {};
+ unsigned waitCount = 0;
+ bool changed = false;
+ {
+ CSRWExclusiveLock lock(set->lock);
+ for (unsigned i = 0; i < set->count; ++i)
+ if (set->routes[i].id == id && set->routes[i].epoch == epoch)
+ {
+ CTexSet::Route& target = set->routes[i];
+ changed |= target.active;
+ target.active = false;
+ target.dropped = true;
+ if (target.calls)
+ {
+ ResetEvent(target.idle);
+ waits[waitCount++] = target.idle;
+ }
+ }
+ }
+ for (unsigned i = 0; i < waitCount; ++i)
+ WaitForSingleObject(waits[i], INFINITE);
+ Rebind(id, epoch);
+ if (changed)
+ Bump();
+}
+
+void CTexHub::Stop()
+{
+ std::shared_ptr active;
+ std::shared_ptr pending;
+ {
+ CSRWExclusiveLock lock(m_lock);
+ if (m_stopped)
+ return;
+ m_stopped = true;
+ m_changing = true;
+ m_open = false;
+ if (m_calls)
+ ResetEvent(m_idle);
+ else if (m_idle)
+ SetEvent(m_idle);
+ }
+
+ if (m_idle)
+ WaitForSingleObject(m_idle, INFINITE);
+ {
+ CSRWExclusiveLock lock(m_lock);
+ active = std::move(m_active);
+ pending = std::move(m_pending);
+ }
+ pending.reset();
+ active.reset();
+}
diff --git a/idd/LGIdd/transport/CTexHub.h b/idd/LGIdd/transport/CTexHub.h
new file mode 100644
index 00000000..01f4b1b2
--- /dev/null
+++ b/idd/LGIdd/transport/CTexHub.h
@@ -0,0 +1,140 @@
+/**
+ * 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 "Atomic.h"
+#include "CSRWLock.h"
+#include "transport/FrameIn.h"
+#include "transport/FrameProfile.h"
+#include "transport/TransportConfig.h"
+
+#include
+#include
+
+class CFrameGraph;
+class CTransportManager;
+class D11Lease;
+class ITransport;
+class ITexSink;
+class TexLease;
+class CTexHub;
+struct CTexSet;
+
+class CTexStage
+{
+ friend class CTexHub;
+
+private:
+ CTexHub * m_owner = nullptr;
+ std::shared_ptr m_next;
+
+public:
+ CTexStage() = default;
+ CTexStage(const CTexStage&) = delete;
+ CTexStage& operator=(const CTexStage&) = delete;
+ CTexStage(CTexStage&& other) noexcept;
+ CTexStage& operator=(CTexStage&& other) noexcept;
+ ~CTexStage();
+};
+
+class CTexHub
+{
+ friend class CTransportManager;
+ friend class CTexStage;
+
+private:
+ struct Bind
+ {
+ std::shared_ptr owner;
+ BackendId id = 0;
+ uint32_t epoch = 0;
+ ITexSink * sink = nullptr;
+ };
+
+ struct FaultRec
+ {
+ BackendId id = 0;
+ uint32_t epoch = 0;
+ FrameCfg cfg;
+ CfgResult result = CfgResult::ACCEPTED;
+ };
+
+ struct Failure
+ {
+ BackendId id = 0;
+ uint32_t epoch = 0;
+ };
+
+ static const unsigned MAX_FAULTS =
+ TRANSPORT_MAX_INSTANCES * FRAME_PROFILE_MAX;
+
+ mutable CSRWLock m_lock;
+ std::shared_ptr m_active;
+ std::shared_ptr m_pending;
+ std::atomic & m_rev;
+ HANDLE m_idle = nullptr;
+ unsigned m_calls = 0;
+ bool m_open = true;
+ bool m_changing = false;
+ bool m_stopped = false;
+ FaultRec m_faults[MAX_FAULTS] = {};
+ unsigned m_faultCount = 0;
+ Failure m_failures[TRANSPORT_MAX_INSTANCES] = {};
+ unsigned m_failureCount = 0;
+
+ static bool Match(const FaultRec& fault, BackendId id, uint32_t epoch,
+ const FrameCfg& cfg, bool anyCfg = false);
+
+ bool Enter(const FrameIn& frame, bool lease,
+ std::shared_ptr& set, unsigned& route, ITexSink *& sink);
+ void Finish(const std::shared_ptr& set, unsigned route,
+ PushResult result);
+ void Leave(const std::shared_ptr& set, unsigned route);
+ void Disable(const std::shared_ptr& set, unsigned route,
+ PushResult result);
+ void Bump();
+
+ explicit CTexHub(std::atomic& rev);
+ ~CTexHub();
+
+ CTexHub(const CTexHub&) = delete;
+ CTexHub& operator=(const CTexHub&) = delete;
+
+ CfgResult Hold(CTexStage& stage);
+ CfgResult Prep(const CFrameGraph& graph,
+ const Bind * binds, unsigned count, CTexStage& stage);
+ void Commit(CTexStage& stage) noexcept;
+ void Abort(CTexStage& stage) noexcept;
+
+ CfgResult Faulted(BackendId id, uint32_t epoch,
+ const FrameCfg& cfg) const;
+ bool TakeFailure(BackendId& id, uint32_t& epoch);
+ void Rebind(BackendId id, uint32_t epoch);
+
+ // Drop closes only this route and drains its calls before the transport can
+ // stop. Accepted asynchronous leases remain the transport's responsibility.
+ void Drop(BackendId id, uint32_t epoch);
+ void Stop();
+
+public:
+ PushResult Push(FrameIn frame, TexLease lease) noexcept;
+ PushResult Push(FrameIn frame, D11Lease lease) noexcept;
+};
diff --git a/idd/LGIdd/transport/CTransportManager.cpp b/idd/LGIdd/transport/CTransportManager.cpp
index 5c87f6e1..1e9d4844 100644
--- a/idd/LGIdd/transport/CTransportManager.cpp
+++ b/idd/LGIdd/transport/CTransportManager.cpp
@@ -20,9 +20,11 @@
#include "transport/CTransportManager.h"
+#include "Atomic.h"
#include "capture/CFrameGraph.h"
#include "CDebug.h"
#include "Seq.h"
+#include "transport/ITexStage.h"
#include
#include
@@ -117,12 +119,27 @@ CTransportManager::Entry::~Entry()
CloseHandle(idleEvent);
}
-CTransportManager::CTransportManager()
+CTransportManager::CTransportManager() : m_tex(m_frameRev)
{
m_phaseIdle = CreateEvent(nullptr, TRUE, TRUE, nullptr);
m_stoppedEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
}
+uint64_t CTransportManager::FrameRev() const
+{
+ return Atomic::Load(m_frameRev, std::memory_order_acquire);
+}
+
+void CTransportManager::BumpFrameRev()
+{
+ Atomic::Next(m_frameRev, std::memory_order_release);
+}
+
+uint64_t CTransportManager::NextGraph()
+{
+ return Seq::Inc(m_graphSerial);
+}
+
CTransportManager::~CTransportManager()
{
Stop();
@@ -469,6 +486,7 @@ bool CTransportManager::AddServices(Entry& entry)
bool inputFailed = false;
bool inputAbsent = false;
bool frameAdded = false;
+ bool frameBound = false;
bool frameAbsent = false;
uint64_t retryAt = 0;
{
@@ -524,11 +542,24 @@ bool CTransportManager::AddServices(Entry& entry)
else if (attach && !frameAdded && !frameAbsent)
{
IFrameSink * frame = transport->FrameSink();
+ ITexSink * tex = transport->TexSink();
if (frame && m_frames.Bind(id, epoch, primary, *frame))
{
CSRWExclusiveLock entryLock(entry.lock);
- entry.frameAdded = true;
- frameAdded = true;
+ entry.frameAdded = true;
+ entry.frameLegacy = true;
+ entry.texSink = nullptr;
+ frameAdded = true;
+ frameBound = true;
+ }
+ else if (!frame && tex)
+ {
+ CSRWExclusiveLock entryLock(entry.lock);
+ entry.frameAdded = true;
+ entry.frameLegacy = false;
+ entry.texSink = tex;
+ frameAdded = true;
+ frameBound = true;
}
else if (frame || primary)
frameRetry = true;
@@ -567,6 +598,12 @@ bool CTransportManager::AddServices(Entry& entry)
entry.serviceRetryAt = now + SERVICE_RETRY_DELAY_MS;
}
+ if (frameBound)
+ {
+ m_tex.Rebind(id, epoch);
+ BumpFrameRev();
+ }
+
const bool servicesReady =
(!(services & TRANSPORT_SERVICE_FRAME) || frameAdded) &&
(!(services & TRANSPORT_SERVICE_CONTROL) || controlAdded) &&
@@ -576,6 +613,37 @@ bool CTransportManager::AddServices(Entry& entry)
void CTransportManager::HandleServiceFailures()
{
+ BackendId frameId = 0;
+ uint32_t frameEpoch = 0;
+ while (m_tex.TakeFailure(frameId, frameEpoch))
+ {
+ Entry * entries[FRAME_MAX_SINKS] = {};
+ const unsigned count = Entries(entries);
+ for (unsigned i = 0; i < count; ++i)
+ {
+ Entry& entry = *entries[i];
+ bool restart = false;
+ {
+ CSRWSharedLock entryLock(entry.lock);
+ if (entry.id != frameId || entry.epoch != frameEpoch ||
+ !entry.frameAdded || !entry.texSink)
+ continue;
+ restart = !entry.exposed;
+ }
+
+ DetachRecovery(entry);
+ RemoveServices(entry);
+ if (restart)
+ ScheduleRetry(entry);
+ else
+ {
+ CSRWExclusiveLock entryLock(entry.lock);
+ entry.state = State::FAILED;
+ }
+ break;
+ }
+ }
+
ControlToken token;
while (m_control.TakeFailure(token))
{
@@ -687,6 +755,7 @@ void CTransportManager::RemoveServices(Entry& entry)
BackendId id = 0;
uint32_t epoch = 0;
bool frameAdded = false;
+ bool frameLegacy = false;
bool controlAdded = false;
bool inputAdded = false;
{
@@ -694,20 +763,28 @@ void CTransportManager::RemoveServices(Entry& entry)
id = entry.id;
epoch = entry.epoch;
frameAdded = entry.frameAdded;
+ frameLegacy = entry.frameLegacy;
controlAdded = entry.controlAdded;
inputAdded = entry.inputAdded;
entry.frameAdded = false;
+ entry.frameLegacy = false;
+ entry.texSink = nullptr;
entry.controlAdded = false;
entry.inputAdded = false;
}
m_input.RevokeInteraction(id, epoch);
+ m_tex.Drop(id, epoch);
if (inputAdded)
m_input.Unbind(id, epoch);
if (controlAdded)
m_control.Remove(id, epoch);
- if (frameAdded)
+ if (frameLegacy)
m_frames.Unbind(id, epoch);
+ if (frameAdded)
+ {
+ BumpFrameRev();
+ }
}
void CTransportManager::RetryEntry(Entry& entry, uint64_t now,
@@ -729,6 +806,8 @@ void CTransportManager::RetryEntry(Entry& entry, uint64_t now,
{
CSRWExclusiveLock entryLock(entry.lock);
+ const bool frameService =
+ (entry.config.services & TRANSPORT_SERVICE_FRAME) != 0;
entry.transport.reset();
entry.directMemory = DirectFrameBufferMemory {};
entry.directMemoryValid = false;
@@ -740,6 +819,8 @@ void CTransportManager::RetryEntry(Entry& entry, uint64_t now,
entry.frameAbsent = false;
entry.serviceRetryAt = 0;
Seq::Inc(entry.epoch);
+ if (frameService)
+ BumpFrameRev();
}
if (OpenEntry(entry) != OpenResult::SUCCESS)
@@ -994,32 +1075,46 @@ bool CTransportManager::Setup(size_t alignment)
return success;
}
-CfgResult CTransportManager::Cfg(
- const GraphCfg& cfg, CFrameGraph& graph)
+CfgResult CTransportManager::Cfg(const GraphCfg& cfg,
+ uint64_t revision, CFrameGraph& graph, ITexStage * activation)
{
+ if (!revision || revision != FrameRev())
+ return CfgResult::RETRY;
+
CFrameGraph next;
if (!next.Begin(cfg))
return CfgResult::REJECTED;
if (!BeginPhase(Phase::CFG, true))
return CfgResult::RETRY;
+ CTexStage texStage;
+ CfgResult held = m_tex.Hold(texStage);
+ if (held != CfgResult::ACCEPTED)
+ {
+ m_tex.Abort(texStage);
+ EndPhase();
+ return held;
+ }
+
struct Route
{
- Entry * entry = nullptr;
+ Entry * entry = nullptr;
std::shared_ptr transport;
- BackendId id = 0;
- uint32_t epoch = 0;
- bool required = false;
- bool primary = false;
- bool prepared = false;
- bool eligible = false;
- FrameProfile profiles[FRAME_PROFILE_MAX] = {};
- unsigned profileCount = 0;
+ BackendId id = 0;
+ uint32_t epoch = 0;
+ bool required = false;
+ bool primary = false;
+ bool prepared = false;
+ bool eligible = false;
+ ITexSink * texSink = nullptr;
+ FrameProfile profiles[FRAME_PROFILE_MAX] = {};
+ unsigned profileCount = 0;
};
Route routes[FRAME_MAX_SINKS];
unsigned routeCount = 0;
- CfgResult result = CfgResult::ACCEPTED;
+ CfgResult result = revision == FrameRev() ?
+ CfgResult::ACCEPTED : CfgResult::RETRY;
Entry * entries[FRAME_MAX_SINKS] = {};
const unsigned count = Entries(entries);
@@ -1051,6 +1146,7 @@ CfgResult CTransportManager::Cfg(
route.entry = &entry;
State state;
bool frameAbsent = false;
+ bool frameAdded = false;
{
CSRWSharedLock entryLock(entry.lock);
route.transport = entry.transport;
@@ -1058,12 +1154,14 @@ CfgResult CTransportManager::Cfg(
route.epoch = entry.epoch;
route.required = entry.required;
route.primary = entry.primary;
+ route.texSink = entry.texSink;
state = entry.state;
frameAbsent = entry.frameAbsent;
+ frameAdded = entry.frameAdded;
}
route.eligible = route.transport && !frameAbsent &&
- (state == State::INITIALIZED || state == State::READY);
+ frameAdded && state == State::READY;
if (!route.eligible)
{
if (!route.required &&
@@ -1073,6 +1171,16 @@ CfgResult CTransportManager::Cfg(
(state == State::FAILED ? CfgResult::FAILED : CfgResult::RETRY);
break;
}
+ if (route.texSink && !activation)
+ {
+ route.eligible = false;
+ if (route.required)
+ {
+ result = CfgResult::REJECTED;
+ break;
+ }
+ continue;
+ }
unsigned profileCount = 0;
const FrameProfile * profiles =
@@ -1114,6 +1222,22 @@ CfgResult CTransportManager::Cfg(
if (!next.Can(candidate))
continue;
+ if (route.texSink)
+ {
+ const CfgResult fault =
+ m_tex.Faulted(route.id, route.epoch, candidate);
+ if (fault == CfgResult::FAILED)
+ {
+ routeResult = fault;
+ break;
+ }
+ if (fault == CfgResult::REJECTED)
+ {
+ routeResult = fault;
+ continue;
+ }
+ }
+
routeResult = route.transport->Probe(candidate);
if (routeResult == CfgResult::NEXT)
continue;
@@ -1160,17 +1284,53 @@ CfgResult CTransportManager::Cfg(
if (result == CfgResult::ACCEPTED && !next.Seal())
result = CfgResult::FAILED;
+ if (result == CfgResult::ACCEPTED &&
+ !next.Stamp(NextGraph()))
+ result = CfgResult::FAILED;
+
+ CTexHub::Bind binds[FRAME_MAX_SINKS];
+ unsigned bindCount = 0;
+ if (result == CfgResult::ACCEPTED)
+ for (unsigned i = 0; i < routeCount; ++i)
+ if (routes[i].prepared)
+ {
+ CTexHub::Bind& bind = binds[bindCount++];
+ bind.owner = routes[i].transport;
+ bind.id = routes[i].id;
+ bind.epoch = routes[i].epoch;
+ bind.sink = routes[i].texSink;
+ }
+
+ bool activationReady = false;
+ if (result == CfgResult::ACCEPTED && activation)
+ {
+ result = activation->Prep(next);
+ activationReady = result == CfgResult::ACCEPTED;
+ }
+ if (result == CfgResult::ACCEPTED)
+ result = m_tex.Prep(next, binds, bindCount, texStage);
+ if (result == CfgResult::ACCEPTED && revision != FrameRev())
+ result = CfgResult::RETRY;
+
if (result == CfgResult::ACCEPTED)
{
for (unsigned i = 0; i < routeCount; ++i)
if (routes[i].prepared)
routes[i].transport->Commit();
graph = next;
+ m_tex.Commit(texStage);
+ if (activationReady)
+ activation->Commit();
}
else
+ {
for (unsigned i = routeCount; i > 0; --i)
if (routes[i - 1].prepared)
routes[i - 1].transport->Abort();
+ m_tex.Abort(texStage);
+ if (activationReady)
+ activation->Abort();
+ }
for (unsigned i = routeCount; i > 0; --i)
EndCall(*routes[i - 1].entry, routes[i - 1].transport);
@@ -1343,6 +1503,8 @@ void CTransportManager::Stop()
return;
}
+ m_tex.Stop();
+
for (unsigned i = count; i > 0; --i)
{
DetachRecovery(*entries[i - 1]);
diff --git a/idd/LGIdd/transport/CTransportManager.h b/idd/LGIdd/transport/CTransportManager.h
index 61b62820..385bdee9 100644
--- a/idd/LGIdd/transport/CTransportManager.h
+++ b/idd/LGIdd/transport/CTransportManager.h
@@ -20,11 +20,13 @@
#pragma once
+#include "Atomic.h"
#include "CSRWLock.h"
#include "transport/CControlHub.h"
#include "transport/CFrameHub.h"
#include "transport/CInputHub.h"
#include "transport/CRecoveryHub.h"
+#include "transport/CTexHub.h"
#include "transport/ITransport.h"
#include "transport/TransportConfig.h"
@@ -34,6 +36,7 @@ static_assert(TRANSPORT_MAX_INSTANCES == FRAME_MAX_SINKS,
"The transport and frame limits must match");
class CFrameGraph;
+class ITexStage;
struct GraphCfg;
class CTransportManager final : public FrameCaps
@@ -106,8 +109,10 @@ private:
bool inputAdded = false;
bool inputFailed = false;
bool inputAbsent = false;
- bool frameAdded = false;
- bool frameAbsent = false;
+ bool frameAdded = false;
+ bool frameLegacy = false;
+ ITexSink * texSink = nullptr;
+ bool frameAbsent = false;
uint64_t serviceRetryAt = 0;
bool exposed = false;
bool setupDone = false;
@@ -121,8 +126,10 @@ private:
std::unique_ptr m_entries[FRAME_MAX_SINKS];
unsigned m_entryCount = 0;
+ std::atomic m_frameRev = 1;
CControlHub m_control;
CFrameHub m_frames;
+ CTexHub m_tex;
CInputHub m_input;
CRecoveryHub m_recovery;
Entry * m_primary = nullptr;
@@ -136,6 +143,7 @@ private:
bool m_started = false;
bool m_stopping = false;
bool m_stopped = false;
+ uint64_t m_graphSerial = 0;
unsigned Entries(Entry * entries[FRAME_MAX_SINKS]) const;
Entry * Primary() const;
@@ -160,6 +168,8 @@ private:
void ScheduleRetry(Entry& entry);
void RemoveServices(Entry& entry);
void Expose(Entry& entry);
+ void BumpFrameRev();
+ uint64_t NextGraph();
public:
CTransportManager();
@@ -173,7 +183,8 @@ public:
OpenResult Open();
bool Initialize();
bool Setup(size_t alignment);
- CfgResult Cfg(const GraphCfg& cfg, CFrameGraph& graph);
+ CfgResult Cfg(const GraphCfg& cfg, uint64_t revision,
+ CFrameGraph& graph, ITexStage * activation = nullptr);
ProcessResult Process(ITransportActions& actions);
void Stop();
void SyncRecovery();
@@ -186,6 +197,8 @@ public:
DirectFrameBufferMemory GetDirectMemory() const;
IFrameTransport& Frames();
+ CTexHub& Tex() { return m_tex; }
+ uint64_t FrameRev() const;
IControlTransport& Control();
IInputTransport& Input();
};
diff --git a/idd/LGIdd/transport/FrameIn.h b/idd/LGIdd/transport/FrameIn.h
new file mode 100644
index 00000000..c3623f4a
--- /dev/null
+++ b/idd/LGIdd/transport/FrameIn.h
@@ -0,0 +1,42 @@
+/**
+ * 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 "capture/CFrameGraph.h"
+
+#include
+
+enum class PushResult : uint8_t
+{
+ ACCEPTED,
+ BUSY,
+ STALE,
+ REJECTED,
+ FAILED,
+};
+
+// The texture is supplied separately as exactly one typed lease. Node zero is
+// never valid because it is the borrowed IddCx acquisition surface.
+struct FrameIn
+{
+ uint64_t graph = 0;
+ LeafDesc desc;
+};
diff --git a/idd/LGIdd/transport/ITexSink.h b/idd/LGIdd/transport/ITexSink.h
new file mode 100644
index 00000000..ae830ed5
--- /dev/null
+++ b/idd/LGIdd/transport/ITexSink.h
@@ -0,0 +1,40 @@
+/**
+ * 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 "capture/CFrameTex.h"
+#include "d3d/CInteropPool.h"
+#include "transport/FrameIn.h"
+
+class ITexSink
+{
+public:
+ virtual ~ITexSink() = default;
+
+ // Push is bounded and nonblocking. ACCEPTED transfers the self-contained
+ // frame and lease to the transport; both remain retained until its
+ // asynchronous GPU use completes. Other results retain neither object and
+ // leave sibling routes and providers untouched. A later executor must make
+ // the next accepted product full-damage after BUSY or another dropped
+ // partial update.
+ virtual PushResult Push(FrameIn frame, TexLease lease) noexcept = 0;
+ virtual PushResult Push(FrameIn frame, D11Lease lease) noexcept = 0;
+};
diff --git a/idd/LGIdd/transport/ITexStage.h b/idd/LGIdd/transport/ITexStage.h
new file mode 100644
index 00000000..2db470ac
--- /dev/null
+++ b/idd/LGIdd/transport/ITexStage.h
@@ -0,0 +1,40 @@
+/**
+ * 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 "transport/FrameProfile.h"
+
+class CFrameGraph;
+
+// The future graph executor stages every fallible pool and interop resource
+// here. Commit and Abort are infallible so transport configuration and graph
+// activation remain one transaction. A non-ACCEPTED Prep leaves no pending
+// state; an ACCEPTED Prep remains dormant until Commit. Commit publishes the
+// new producer only after texture admission has opened. Abort may resume the
+// old producer because the old admission set is reopened first.
+class ITexStage
+{
+public:
+ virtual ~ITexStage() = default;
+ virtual CfgResult Prep(const CFrameGraph& graph) noexcept = 0;
+ virtual void Commit() noexcept = 0;
+ virtual void Abort() noexcept = 0;
+};
diff --git a/idd/LGIdd/transport/ITransport.h b/idd/LGIdd/transport/ITransport.h
index ec2a13a9..e46492d3 100644
--- a/idd/LGIdd/transport/ITransport.h
+++ b/idd/LGIdd/transport/ITransport.h
@@ -32,6 +32,7 @@
class IControlSink;
class IFrameSink;
class IInputSource;
+class ITexSink;
struct SourceKey
{
@@ -151,8 +152,11 @@ public:
// Profiles is an immutable, ordered preference list of at most
// FRAME_PROFILE_MAX entries whose pointer remains valid for the lifetime
// of this instance.
- // Probe has no side effects. Prepare changes pending state only; Commit
- // promotes it without failure, while Abort preserves the active route.
+ // Probe has no side effects. Prepare changes pending state only and is safe
+ // while the active texture sink is admitting frames. Commit promotes it
+ // without failure, while Abort preserves the active route. A texture
+ // transport retains old-generation resources until every accepted lease
+ // completes; Stop drains all accepted leases before destroying its sink.
virtual const FrameProfile * Profiles(unsigned& count) const = 0;
virtual CfgResult Probe(const FrameCfg& cfg) const = 0;
virtual CfgResult Prepare(const FrameCfg& cfg) = 0;
@@ -171,6 +175,7 @@ public:
// Component pointers are fixed after Setup and remain valid until Stop.
virtual IFrameSink * FrameSink() { return nullptr; }
+ virtual ITexSink * TexSink() { return nullptr; }
virtual IControlSink * Control() { return nullptr; }
virtual IInputSource * Input() { return nullptr; }
};