From 39be707fde0c5e3adcc434b8bdf86a6a98a2f4d2 Mon Sep 17 00:00:00 2001 From: baldurk Date: Fri, 23 Oct 2026 13:19:09 +0100 Subject: [PATCH] Ensure consistent thread usage of ICaptureAccess in the UI * We could put everything onto the replay thread but that's unnecessary back- and-forth pingponging for a local capture. * This is safe to use on the UI thread if it's local, but if it's remote it uses the same connection as the replay controller so must happen on the replay thread. * We add a new helper (not exposed to swig) that handles this check, and for python we wrap the returned ICaptureAccess in an invoker that ensures we are on one of those threads. --- qrenderdoc/Code/CaptureContext.cpp | 47 +++-- qrenderdoc/Code/Interface/QRDInterface.h | 38 ++++ .../Code/pyrenderdoc/PythonInvokers.cpp | 192 +++++++++++++++++- qrenderdoc/Widgets/ReplayOptionsSelector.cpp | 16 +- qrenderdoc/Windows/APIInspector.cpp | 13 +- qrenderdoc/Windows/MainWindow.cpp | 130 ++++++------ 6 files changed, 333 insertions(+), 103 deletions(-) diff --git a/qrenderdoc/Code/CaptureContext.cpp b/qrenderdoc/Code/CaptureContext.cpp index 4ad3d88f8..263752434 100644 --- a/qrenderdoc/Code/CaptureContext.cpp +++ b/qrenderdoc/Code/CaptureContext.cpp @@ -1228,29 +1228,32 @@ void CaptureContext::LoadCaptureThreaded(const QString &captureFile, const Repla .arg(origFilename)); } - ICaptureAccess *access = Replay().GetCaptureAccess(); - - if(access) - { + Replay().BlockAccessInvoke([this](ICaptureAccess *access) { int idx = access->FindSectionByType(SectionType::ResourceRenames); if(idx >= 0) { bytebuf buf = access->GetSectionContents(idx); - LoadRenames(QString::fromUtf8((const char *)buf.data(), buf.count())); + GUIInvoke::call(m_MainWindow, [this, buf]() { + LoadRenames(QString::fromUtf8((const char *)buf.data(), buf.count())); + }); } idx = access->FindSectionByType(SectionType::Bookmarks); if(idx >= 0) { bytebuf buf = access->GetSectionContents(idx); - LoadBookmarks(QString::fromUtf8((const char *)buf.data(), buf.count())); + GUIInvoke::call(m_MainWindow, [this, buf]() { + LoadBookmarks(QString::fromUtf8((const char *)buf.data(), buf.count())); + }); } idx = access->FindSectionByType(SectionType::Notes); if(idx >= 0) { bytebuf buf = access->GetSectionContents(idx); - LoadNotes(QString::fromUtf8((const char *)buf.data(), buf.count())); + GUIInvoke::call(m_MainWindow, [this, buf]() { + LoadNotes(QString::fromUtf8((const char *)buf.data(), buf.count())); + }); } idx = access->FindSectionByType(SectionType::EditedShaders); @@ -1271,7 +1274,7 @@ void CaptureContext::LoadCaptureThreaded(const QString &captureFile, const Repla { ANALYTIC_ADDUNIQ(APIs, driver); } - } + }); m_LoadInProgress = false; m_CaptureLoaded = true; @@ -3119,17 +3122,17 @@ void CaptureContext::AddDockWindow(QWidget *newWindow, DockReference ref, QWidge void CaptureContext::EmbedDependentFiles() { - if(!m_Replay.GetCaptureAccess()) - return; - // Always operate on the capture access (local or remote) QString tag = lit("replayEmbedDependenciesIntoCapture"); bool done = false; - Replay().AsyncInvoke(tag, [this, &done](IReplayController *) { - m_Replay.GetCaptureAccess()->EmbedDependenciesIntoCapture(); - done = true; - }); + if(!Replay().AccessInvoke( + [this, &done](ICaptureAccess *access) { + access->EmbedDependenciesIntoCapture(); + done = true; + }, + false, tag)) + return; // wait a short while before displaying the progress dialog for(int i = 0; !done && (i < 100 || m_Replay.GetCurrentProcessingTag().isEmpty() || @@ -3164,17 +3167,17 @@ void CaptureContext::EmbedDependentFiles() void CaptureContext::RemoveDependentFiles() { - if(!m_Replay.GetCaptureAccess()) - return; - // Always operate on the capture access (local or remote) QString tag = lit("replayRemoveDependenciesFromCapture"); bool done = false; - Replay().AsyncInvoke(tag, [this, &done](IReplayController *) { - m_Replay.GetCaptureAccess()->RemoveDependenciesFromCapture(); - done = true; - }); + if(!Replay().AccessInvoke( + [this, &done](ICaptureAccess *access) { + access->RemoveDependenciesFromCapture(); + done = true; + }, + false, tag)) + return; // wait a short while before displaying the progress dialog for(int i = 0; !done && (i < 100 || m_Replay.GetCurrentProcessingTag().isEmpty() || diff --git a/qrenderdoc/Code/Interface/QRDInterface.h b/qrenderdoc/Code/Interface/QRDInterface.h index ab09e87ce..5dcd9e8c7 100644 --- a/qrenderdoc/Code/Interface/QRDInterface.h +++ b/qrenderdoc/Code/Interface/QRDInterface.h @@ -2098,6 +2098,44 @@ If no tag is present, the callback will be processed as normal. )"); virtual void BlockInvoke(ReplayInvokeCallback method) = 0; +#if !defined(SWIG) && !defined(SWIG_GENERATED) + // helper for ICaptureAccess - if we are replaying remotely we need to invoke onto the replay + // thread (and given the interface normally blocking) to use it, but if not it is safe to access + // directly on the UI thread so we can skip the invoke. + // + // returns false if no capture is open so the callback isn't invoked + // + // this is hidden from swig as for python the threading is less controlled there so we wrap the + // ICaptureAccess with a force-blocking invoker + bool AccessInvoke(std::function callback, bool blocking, rdcstr tag) + { + if(!GetCaptureAccess()) + return false; + + if(CurrentRemote().IsConnected()) + { + if(blocking) + BlockInvoke([this, callback](IReplayController *) { callback(GetCaptureAccess()); }); + else + AsyncInvoke([this, callback](IReplayController *) { callback(GetCaptureAccess()); }, tag); + } + else + { + callback(GetCaptureAccess()); + } + + return true; + } + bool BlockAccessInvoke(std::function callback) + { + return AccessInvoke(callback, true, ""); + } + bool AsyncAccessInvoke(std::function callback) + { + return AccessInvoke(callback, false, ""); + } +#endif + protected: IReplayManager() = default; ~IReplayManager() = default; diff --git a/qrenderdoc/Code/pyrenderdoc/PythonInvokers.cpp b/qrenderdoc/Code/pyrenderdoc/PythonInvokers.cpp index 97d0b8a58..a965c2f6e 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonInvokers.cpp +++ b/qrenderdoc/Code/pyrenderdoc/PythonInvokers.cpp @@ -452,7 +452,6 @@ struct ReplayControllerInvoker : IReplayController { ReplayControllerInvoker(ICaptureContext &ctx) : m_Ctx(ctx) {} virtual ~ReplayControllerInvoker() {} - PythonShell *m_Shell; ICaptureContext &m_Ctx; template @@ -845,6 +844,191 @@ struct ReplayControllerInvoker : IReplayController } }; +struct CaptureAccessInvoker : ICaptureAccess +{ + CaptureAccessInvoker(ICaptureContext &ctx) : m_Ctx(ctx), m_Replay(m_Ctx.Replay()) {} + virtual ~CaptureAccessInvoker() {} + ICaptureContext &m_Ctx; + IReplayManager &m_Replay; + + template + R InvokeRetFunction(F ptr, paramTypes... params) + { + R ret = R(); + + if(!m_Replay.GetCaptureAccess()) + return ret; + + void *ctx = PythonContext::PausePythonThreading(); + + auto callback = [&ret, ptr, params...](ICaptureAccess *access) { + ret = (access->*ptr)(params...); + }; + + // ensure we either call on replay thread (if needed) or UI thread. Capture access isn't thread + // safe so we don't want it to race otherwise + if(m_Replay.CurrentRemote().IsConnected()) + m_Replay.BlockInvoke( + [this, &callback](IReplayController *) { callback(m_Replay.GetCaptureAccess()); }); + else + GUIInvoke::blockcall(m_Ctx.GetMainWindow()->Widget(), + [this, callback]() { callback(m_Replay.GetCaptureAccess()); }); + + PythonContext::ResumePythonThreading(ctx); + return ret; + } + + rdcarray GetAvailableGPUs() + { + return InvokeRetFunction>(&ICaptureAccess::GetAvailableGPUs); + } + + int32_t GetSectionCount() { return InvokeRetFunction(&ICaptureAccess::GetSectionCount); } + + int32_t FindSectionByName(const rdcstr &name) + { + return InvokeRetFunction(&ICaptureAccess::FindSectionByName, name); + } + + int32_t FindSectionByType(SectionType type) + { + return InvokeRetFunction(&ICaptureAccess::FindSectionByType, type); + } + + SectionProperties GetSectionProperties(int32_t index) + { + return InvokeRetFunction(&ICaptureAccess::GetSectionProperties, index); + } + + bytebuf GetSectionContents(int32_t index) + { + return InvokeRetFunction(&ICaptureAccess::GetSectionContents, index); + } + + ResultDetails WriteSection(const SectionProperties &props, const bytebuf &contents) + { + return InvokeRetFunction(&ICaptureAccess::WriteSection, props, contents); + } + + bool HasCallstacks() { return InvokeRetFunction(&ICaptureAccess::HasCallstacks); } + + ResultDetails InitResolver(bool interactive, RENDERDOC_ProgressCallback progress = NULL) + { + return InvokeRetFunction(&ICaptureAccess::InitResolver, interactive, progress); + } + + rdcarray GetResolve(const rdcarray &callstack) + { + return InvokeRetFunction>(&ICaptureAccess::GetResolve, callstack); + } + + rdcstr DriverName() { return InvokeRetFunction(&ICaptureAccess::DriverName); } + + ResultDetails EmbedDependenciesIntoCapture() + { + return InvokeRetFunction(&ICaptureAccess::EmbedDependenciesIntoCapture); + } + + ResultDetails RemoveDependenciesFromCapture() + { + return InvokeRetFunction(&ICaptureAccess::RemoveDependenciesFromCapture); + } + + bool HasEmbeddedDependencies() + { + return InvokeRetFunction(&ICaptureAccess::HasEmbeddedDependencies); + } + + bool HasPendingDependencies() + { + return InvokeRetFunction(&ICaptureAccess::HasPendingDependencies); + } + + rdcarray GetPendingDependenciesNicknames() + { + return InvokeRetFunction>(&ICaptureAccess::GetPendingDependenciesNicknames); + } +}; + +// annoyingly we only need this to be able to return the capture access invoker +struct ReplayInvoker : IReplayManager +{ + CaptureAccessInvoker m_Access; + + IReplayManager &m_Replay; + + ReplayInvoker(ICaptureContext &ctx) : m_Access(ctx), m_Replay(ctx.Replay()) {} + virtual ~ReplayInvoker() {} + + void DeleteCapture(const rdcstr &capturefile, bool local) + { + return m_Replay.DeleteCapture(capturefile, local); + } + + ResultDetails ConnectToRemoteServer(RemoteHost host) + { + return m_Replay.ConnectToRemoteServer(host); + } + + void DisconnectFromRemoteServer() { return m_Replay.DisconnectFromRemoteServer(); } + + void ShutdownServer() { return m_Replay.ShutdownServer(); } + + void PingRemote() { return m_Replay.PingRemote(); } + + void CancelReplayLoop() { return m_Replay.CancelReplayLoop(); } + + RemoteHost CurrentRemote() { return m_Replay.CurrentRemote(); } + + ICaptureAccess *GetCaptureAccess() + { + if(m_Replay.GetCaptureAccess()) + return &m_Access; + + return NULL; + } + + ICaptureFile *GetCaptureFile() { return m_Replay.GetCaptureFile(); } + + ExecuteResult ExecuteAndInject(const rdcstr &exe, const rdcstr &workingDir, const rdcstr &cmdLine, + const rdcarray &env, + const rdcstr &capturefile, CaptureOptions opts) + { + return m_Replay.ExecuteAndInject(exe, workingDir, cmdLine, env, capturefile, opts); + } + + rdcarray GetRemoteSupport() { return m_Replay.GetRemoteSupport(); } + + void GetHomeFolder(bool synchronous, DirectoryBrowseCallback callback) + { + return m_Replay.GetHomeFolder(synchronous, callback); + } + + void ListFolder(const rdcstr &path, bool synchronous, DirectoryBrowseCallback callback) + { + return m_Replay.ListFolder(path, synchronous, callback); + } + + rdcstr CopyCaptureToRemote(const rdcstr &localpath, QWidget *window) + { + return m_Replay.CopyCaptureToRemote(localpath, window); + } + + void CopyCaptureFromRemote(const rdcstr &remotepath, const rdcstr &localpath, QWidget *window) + { + return m_Replay.CopyCaptureFromRemote(remotepath, localpath, window); + } + + float GetCurrentProcessingTime() { return m_Replay.GetCurrentProcessingTime(); } + + void AsyncInvoke(ReplayInvokeCallback method, rdcstr tag) + { + return m_Replay.AsyncInvoke(method, tag); + } + + void BlockInvoke(ReplayInvokeCallback method) { return m_Replay.BlockInvoke(method); } +}; + struct IMainWindowInvoker : UIThreadInvoker { IMainWindowInvoker(ICaptureContext &ctx, IMainWindow &obj) : UIThreadInvoker(ctx, obj) {} @@ -1350,6 +1534,7 @@ struct CaptureContextInvoker : public UIThreadInvoker { ExtensionInvoker *m_Ext; ReplayControllerInvoker m_ReplayController; + ReplayInvoker m_Replay; #define WINDOW_INVOKER(iface) \ iface##Invoker *m_invoker##iface = NULL; \ @@ -1380,7 +1565,8 @@ struct CaptureContextInvoker : public UIThreadInvoker WINDOW_INVOKER(IPythonShell); WINDOW_INVOKER(IResourceInspector); - CaptureContextInvoker(ICaptureContext &ctx) : UIThreadInvoker(ctx, ctx), m_ReplayController(ctx) + CaptureContextInvoker(ICaptureContext &ctx) + : UIThreadInvoker(ctx, ctx), m_ReplayController(ctx), m_Replay(ctx) { m_Ext = new ExtensionInvoker(ctx, ctx.Extensions()); } @@ -1399,7 +1585,7 @@ struct CaptureContextInvoker : public UIThreadInvoker return m_Obj.TempCaptureFilename(appname); } virtual IExtensionManager &Extensions() override { return *m_Ext; } - virtual IReplayManager &Replay() override { return m_Obj.Replay(); } + virtual IReplayManager &Replay() override { return m_Replay; } virtual bool IsCaptureLoaded() override { return m_Obj.IsCaptureLoaded(); } virtual bool IsCaptureLocal() override { return m_Obj.IsCaptureLocal(); } virtual bool IsCaptureTemporary() override { return m_Obj.IsCaptureTemporary(); } diff --git a/qrenderdoc/Widgets/ReplayOptionsSelector.cpp b/qrenderdoc/Widgets/ReplayOptionsSelector.cpp index d18c27d64..839dcfca1 100644 --- a/qrenderdoc/Widgets/ReplayOptionsSelector.cpp +++ b/qrenderdoc/Widgets/ReplayOptionsSelector.cpp @@ -43,16 +43,16 @@ ReplayOptionsSelector::ReplayOptionsSelector(ICaptureContext &ctx, bool actions, // try to use the remote capture access to enumerate remote GPUs, but if it's not available open a // local capture access - ICaptureFile *dummy = RENDERDOC_OpenCaptureFile(); - ICaptureAccess *capture = m_Ctx.Replay().GetCaptureAccess(); + if(!m_Ctx.Replay().BlockAccessInvoke( + [this](ICaptureAccess *capture) { m_GPUs = capture->GetAvailableGPUs(); })) + { + ICaptureFile *dummy = RENDERDOC_OpenCaptureFile(); - if(!capture) - capture = dummy; + // fetch available GPUs + m_GPUs = dummy->GetAvailableGPUs(); - // fetch available GPUs - m_GPUs = capture->GetAvailableGPUs(); - - dummy->Shutdown(); + dummy->Shutdown(); + } // this is always available ui->gpuOverride->addItem(tr("Default GPU selection")); diff --git a/qrenderdoc/Windows/APIInspector.cpp b/qrenderdoc/Windows/APIInspector.cpp index ad0b05a81..9df833c35 100644 --- a/qrenderdoc/Windows/APIInspector.cpp +++ b/qrenderdoc/Windows/APIInspector.cpp @@ -180,16 +180,11 @@ void APIInspector::on_apiEvents_itemSelectionChanged() if(chunk && !chunk->metadata.callstack.isEmpty()) { - if(m_Ctx.Replay().GetCaptureAccess()) - { - m_Ctx.Replay().AsyncInvoke([this, chunk](IReplayController *) { - rdcarray stack = - m_Ctx.Replay().GetCaptureAccess()->GetResolve(chunk->metadata.callstack); + if(!m_Ctx.Replay().AsyncAccessInvoke([this, chunk](ICaptureAccess *access) { + rdcarray stack = access->GetResolve(chunk->metadata.callstack); - GUIInvoke::call(this, [this, stack]() { addCallstack(stack); }); - }); - } - else + GUIInvoke::call(this, [this, stack]() { addCallstack(stack); }); + })) { ui->callstack->setUpdatesEnabled(false); ui->callstack->clear(); diff --git a/qrenderdoc/Windows/MainWindow.cpp b/qrenderdoc/Windows/MainWindow.cpp index b110a0050..24d27fc78 100644 --- a/qrenderdoc/Windows/MainWindow.cpp +++ b/qrenderdoc/Windows/MainWindow.cpp @@ -2312,7 +2312,9 @@ void MainWindow::OnCaptureLoaded() statusProgress->setVisible(false); // don't allow capture recompress on opened images - QString driver = m_Ctx.Replay().GetCaptureAccess()->DriverName(); + QString driver; + m_Ctx.Replay().BlockAccessInvoke( + [&driver](ICaptureAccess *access) { driver = access->DriverName(); }); bool is_image = driver == lit("Image"); ui->action_Recompress_Capture->setEnabled(!is_image); @@ -2322,9 +2324,8 @@ void MainWindow::OnCaptureLoaded() ui->action_Open_RGP_Profile->setEnabled(false); ui->action_Create_RGP_Profile->setEnabled(m_Ctx.APIProps().rgpCapture && m_Ctx.IsCaptureLocal()); - m_Ctx.Replay().AsyncInvoke([this](IReplayController *) { - bool hasAMDGRPPorfile = - (m_Ctx.Replay().GetCaptureAccess()->FindSectionByType(SectionType::AMDRGPProfile) >= 0); + m_Ctx.Replay().AsyncAccessInvoke([this](ICaptureAccess *access) { + bool hasAMDGRPPorfile = (access->FindSectionByType(SectionType::AMDRGPProfile) >= 0); GUIInvoke::call(this, [this, hasAMDGRPPorfile]() { ui->action_Open_RGP_Profile->setEnabled(hasAMDGRPPorfile); @@ -2335,8 +2336,8 @@ void MainWindow::OnCaptureLoaded() ui->action_Resolve_Symbols->setEnabled(false); - m_Ctx.Replay().AsyncInvoke([this](IReplayController *) { - bool hasResolver = m_Ctx.Replay().GetCaptureAccess()->HasCallstacks(); + m_Ctx.Replay().AsyncAccessInvoke([this](ICaptureAccess *access) { + bool hasResolver = access->HasCallstacks(); GUIInvoke::call(this, [this, hasResolver]() { ui->action_Resolve_Symbols->setEnabled(hasResolver); @@ -2741,7 +2742,21 @@ void MainWindow::on_action_Resolve_Symbols_triggered() { ANALYTIC_SET(UIFeatures.CallstackResolve, true); - if(!m_Ctx.Replay().GetCaptureAccess()) + float progress = 0.0f; + bool finished = false; + + if(!m_Ctx.Replay().AsyncAccessInvoke([this, &progress, &finished](ICaptureAccess *access) { + ResultDetails success = access->InitResolver(true, [&progress](float p) { progress = p; }); + + if(!success.OK()) + { + RDDialog::critical( + this, tr("Error loading symbols"), + tr("Couldn't load symbols for callstack resolution.\n\n%1").arg(success.Message())); + } + + finished = true; + })) { RDDialog::critical( this, tr("Not Available"), @@ -2749,23 +2764,6 @@ void MainWindow::on_action_Resolve_Symbols_triggered() return; } - float progress = 0.0f; - bool finished = false; - - m_Ctx.Replay().AsyncInvoke([this, &progress, &finished](IReplayController *) { - ResultDetails success = m_Ctx.Replay().GetCaptureAccess()->InitResolver( - true, [&progress](float p) { progress = p; }); - - if(!success.OK()) - { - RDDialog::critical( - this, tr("Error loading symbols"), - tr("Couldn't load symbols for callstack resolution.\n\n%1").arg(success.Message())); - } - - finished = true; - }); - ShowProgressDialog( this, tr("Resolving symbols, please wait..."), [&finished]() { return finished; }, [&progress]() { return progress; }); @@ -2874,27 +2872,34 @@ void MainWindow::on_action_Open_RGP_Profile_triggered() if(!m_Ctx.IsCaptureLoaded()) return; - int idx = m_Ctx.Replay().GetCaptureAccess()->FindSectionByType(SectionType::AMDRGPProfile); + QString path; + m_Ctx.Replay().BlockAccessInvoke([this, &path](ICaptureAccess *access) { + int idx = access->FindSectionByType(SectionType::AMDRGPProfile); - if(idx < 0) + if(idx < 0) + return; + + path = QDir::temp().absoluteFilePath(lit("renderdoc_extracted.rgp")); + + QFile f(path); + if(f.open(QIODevice::WriteOnly | QIODevice::Truncate)) + { + bytebuf buf = access->GetSectionContents(idx); + + f.write((const char *)buf.data(), (qint64)buf.size()); + f.flush(); + } + else + { + qCritical() << "Couldn't open temporary file " << path << " for write"; + path = QString(); + return; + } + }); + + if(path.isEmpty()) return; - QString path = QDir::temp().absoluteFilePath(lit("renderdoc_extracted.rgp")); - - QFile f(path); - if(f.open(QIODevice::WriteOnly | QIODevice::Truncate)) - { - bytebuf buf = m_Ctx.Replay().GetCaptureAccess()->GetSectionContents(idx); - - f.write((const char *)buf.data(), (qint64)buf.size()); - f.flush(); - } - else - { - qCritical() << "Couldn't open temporary file " << path << " for write"; - return; - } - m_Ctx.OpenRGPProfile(path); } @@ -2903,15 +2908,20 @@ void MainWindow::on_action_Create_RGP_Profile_triggered() if(!m_Ctx.IsCaptureLoaded()) return; - if(m_Ctx.Replay().GetCaptureAccess()->FindSectionByType(SectionType::AMDRGPProfile) >= 0) - { - QMessageBox::StandardButton res = RDDialog::question( - this, tr("Existing RGP profile"), tr("Capture already contains an RGP profile. Overwrite?"), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + bool abort = false; - if(res != QMessageBox::Yes) - return; - } + m_Ctx.Replay().BlockAccessInvoke([this, &abort](ICaptureAccess *access) { + if(access->FindSectionByType(SectionType::AMDRGPProfile) >= 0) + { + QMessageBox::StandardButton res = + RDDialog::question(this, tr("Existing RGP profile"), + tr("Capture already contains an RGP profile. Overwrite?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + + if(res != QMessageBox::Yes) + abort = true; + } + }); QDialog popup; popup.setWindowFlags(popup.windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -2949,7 +2959,8 @@ void MainWindow::on_action_Create_RGP_Profile_triggered() props.version = 1; props.flags = SectionFlags::ZstdCompressed; - m_Ctx.Replay().GetCaptureAccess()->WriteSection(props, buf); + m_Ctx.Replay().BlockAccessInvoke( + [props, &buf](ICaptureAccess *access) { access->WriteSection(props, buf); }); ui->action_Open_RGP_Profile->setEnabled(true); } @@ -3148,18 +3159,15 @@ void MainWindow::loadLayout_triggered() void MainWindow::updateToolsMenuOptions() { - if(m_Ctx.Replay().GetCaptureAccess()) - { - m_Ctx.Replay().AsyncInvoke([this](IReplayController *) { - bool hasEmbeddedDependencies = m_Ctx.Replay().GetCaptureAccess()->HasEmbeddedDependencies(); - bool hasPendingDependencies = m_Ctx.Replay().GetCaptureAccess()->HasPendingDependencies(); + m_Ctx.Replay().AsyncAccessInvoke([this](ICaptureAccess *access) { + bool hasEmbeddedDependencies = access->HasEmbeddedDependencies(); + bool hasPendingDependencies = access->HasPendingDependencies(); - GUIInvoke::call(this, [this, hasEmbeddedDependencies, hasPendingDependencies]() { - ui->action_EmbedExternalFiles->setEnabled(!hasEmbeddedDependencies && hasPendingDependencies); - ui->action_RemoveExternalFiles->setEnabled(hasEmbeddedDependencies); - }); + GUIInvoke::call(this, [this, hasEmbeddedDependencies, hasPendingDependencies]() { + ui->action_EmbedExternalFiles->setEnabled(!hasEmbeddedDependencies && hasPendingDependencies); + ui->action_RemoveExternalFiles->setEnabled(hasEmbeddedDependencies); }); - } + }); } void MainWindow::closeEvent(QCloseEvent *event)