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)