From d71827d6a1c69e177a4ff99a0a0b40515a32fdad Mon Sep 17 00:00:00 2001 From: baldurk Date: Thu, 12 Mar 2026 12:42:44 +0000 Subject: [PATCH] Load debugpy when possible to allow python debugging in-program --- qrenderdoc/Code/Interface/PersistantConfig.h | 15 + qrenderdoc/Code/ReplayManager.cpp | 5 + qrenderdoc/Code/pyrenderdoc/PythonContext.cpp | 402 +++++++++++++++++- qrenderdoc/Code/pyrenderdoc/PythonContext.h | 11 + .../Code/pyrenderdoc/function_conversion.h | 3 +- .../Code/pyrenderdoc/pyrenderdoc_stub.cpp | 5 + qrenderdoc/Windows/Dialogs/SettingsDialog.cpp | 33 ++ qrenderdoc/Windows/Dialogs/SettingsDialog.h | 3 + qrenderdoc/Windows/Dialogs/SettingsDialog.ui | 33 +- qrenderdoc/Windows/PythonShell.cpp | 4 + qrenderdoc/qrenderdoc_local.vcxproj | 15 + 11 files changed, 523 insertions(+), 6 deletions(-) diff --git a/qrenderdoc/Code/Interface/PersistantConfig.h b/qrenderdoc/Code/Interface/PersistantConfig.h index 951599746..eaf670735 100644 --- a/qrenderdoc/Code/Interface/PersistantConfig.h +++ b/qrenderdoc/Code/Interface/PersistantConfig.h @@ -364,6 +364,21 @@ DECLARE_REFLECTION_STRUCT(BugReport); "type: List[str]"); \ CONFIG_SETTING(public, QVariantList, rdcarray, TextureViewer_ShaderDirs) \ \ + DOCUMENT( \ + "``True`` if debugging should be enabled at startup for python, including UI extensions " \ + "and scripts in the python shell.\n" \ + "\n" \ + "Defaults to ``True``." \ + "" \ + ":type: bool"); \ + CONFIG_SETTING_VAL(public, bool, bool, Python_DebugEnabled, true) \ + \ + DOCUMENT( \ + "The path to an copy of the ``debugpy`` module which should be loaded for debugging." \ + "" \ + ":type: str"); \ + CONFIG_SETTING_VAL(public, QString, rdcstr, Python_DebugPyDir, "") \ + \ DOCUMENT( \ "List of extra directories to write python stubs into.\n" \ "\n:" \ diff --git a/qrenderdoc/Code/ReplayManager.cpp b/qrenderdoc/Code/ReplayManager.cpp index 07b5a2880..0fad56efc 100644 --- a/qrenderdoc/Code/ReplayManager.cpp +++ b/qrenderdoc/Code/ReplayManager.cpp @@ -26,6 +26,7 @@ #include #include #include +#include "Code/pyrenderdoc/PythonContext.h" #include "CaptureContext.h" #include "QRDUtils.h" @@ -55,7 +56,11 @@ void ReplayManager::OpenCapture(const QString &capturefile, const ReplayOptions int proxyRenderer = -1; m_Thread = new LambdaThread([this, proxyRenderer, capturefile, opts, progress]() { + PythonContext::AddDebuggableThread(); + run(proxyRenderer, capturefile, opts, progress); + + PythonContext::RemoveDebuggableThread(); }); m_Thread->setName(lit("ReplayManager")); m_Thread->start(QThread::HighestPriority); diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp index f67f3e017..0e8491e7f 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp @@ -29,6 +29,7 @@ // must be included first #include +#include #include "3rdparty/pythoncapi_compat.h" @@ -67,6 +68,7 @@ PyTypeObject **SbkPySide2_QtWidgetsTypes = NULL; #include #include #include +#include #include #include #include @@ -139,6 +141,9 @@ static PyMethodDef OutputRedirector_methods[] = { {NULL}}; PyObject *PythonContext::main_dict = NULL; +PyObject *PythonContext::m_DebugPy = NULL; +PyObject *PythonContext::m_CallWrapper = NULL; +PyObject *PythonContext::m_CallWrapperGlobals = NULL; QMap PythonContext::extensions; static PyObject *current_global_handle = NULL; @@ -146,6 +151,7 @@ static PyObject *current_global_handle = NULL; static QMutex decrefQueueMutex; static QList decrefQueue; extern "C" void ProcessDecRefQueue(); +extern "C" void HandleException(PyObject *global_handle); // helper overload to give us the semantics we want - return NULL without an exception if the attr doesn't exist PyObject *PyObject_SafeGetAttrString(PyObject *obj, const char *string) @@ -405,6 +411,16 @@ void PythonContext::GenerateStubs(const rdcarray &extraPaths) qInfo() << "Stubs generation processed in" << timer.elapsed() << "ms"; } +void PythonContext::PrepareDebugTracing() +{ + // prep the frame for debugpy if we have it enabled, as if we're calling straight from C++ debugpy + // won't be enabled and breakpoints won't get hit + if(m_DebugPy && m_CallWrapper && m_CallWrapperGlobals) + { + Py_XDECREF(PyEval_EvalCode(m_CallWrapper, m_CallWrapperGlobals, NULL)); + } +} + void PythonContext::GlobalInit(PersistantConfig &config) { // must happen on the UI thread @@ -444,6 +460,12 @@ void PythonContext::GlobalInit(PersistantConfig &config) } #endif + // pydev complains about the zip of standard library files, but that's fine + // and the recommendation is to ignore this warning: https://github.com/microsoft/debugpy/issues/890 + // + // we need to set this early so it's snapshotted by python for os.getenv/os.environ + qputenv("PYDEVD_DISABLE_FILE_VALIDATION", lit("1").toUtf8()); + #if PY_VERSION_HEX > 0x030B0000 pyconfig.program_name = program_name; @@ -537,6 +559,240 @@ void PythonContext::GlobalInit(PersistantConfig &config) output->block = false; } + // load debugpy from installed vscode if we find it. This requires a minimum of python 3.8 +#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 8 + if(config.Python_DebugEnabled) + { + QString debugpy_path; + + // use the user's specified debugpy path if it exists + if(QDir(config.Python_DebugPyDir).exists() && + QDir(config.Python_DebugPyDir).exists(lit("__init__.py"))) + { + debugpy_path = config.Python_DebugPyDir; + } + + // next load debugpy from VS Code, if it exists + if(debugpy_path.isEmpty()) + { + QString homePath = QStandardPaths::standardLocations(QStandardPaths::HomeLocation)[0]; + QDir homeDir(homePath); + homeDir.cd(lit(".vscode/extensions")); + + if(homeDir.exists()) + { + QStringList debugpy_exts = homeDir.entryList(QStringList() << lit("ms-python.debugpy*")); + + if(debugpy_exts.size() >= 1) + { + // assume that sorting works by version, so if there are multiple versions of the + // extension we will pick the latest + debugpy_exts.sort(); + + QDir debugpy_libs = homeDir; + debugpy_libs.cd(debugpy_exts.last()); + debugpy_libs.cd(lit("bundled/libs")); + + if(debugpy_libs.exists()) + { + debugpy_path = debugpy_libs.absolutePath(); + } + } + } + } + + // next try pycharm, if we can find it + if(debugpy_path.isEmpty()) + { +#ifdef Q_OS_WIN32 + QDir programBase(lit("C:/Program Files/JetBrains")); +#else + QDir programBase(lit("/opt")); +#endif + + QString pycharm; + + QStringList pycharms = + programBase.entryList(QStringList() << lit("PyCharm*") << lit("pycharm-*")); + + if(!pycharms.empty()) + { + // assume sorting will pick the latest + pycharms.sort(); + + pycharm = programBase.absoluteFilePath(pycharms.last()); + } + +#ifdef Q_OS_WIN32 + if(pycharm.isEmpty()) + { + // if we didn't find it in the default location, check in the registry on windows + QSettings pycharm_reg(lit("HKEY_LOCAL_MACHINE\\SOFTWARE\\JetBrains\\PyCharm"), + QSettings::NativeFormat); + + QStringList groups = pycharm_reg.childGroups(); + + if(!groups.empty()) + { + // assume sorting will pick the latest + groups.sort(); + + pycharm = pycharm_reg.value(groups.last() + lit("/Default")).toString(); + } + } +#endif + + if(!pycharm.isEmpty()) + { + QDir helpers(pycharm); + helpers.cd(lit("plugins/python-ce/helpers")); + + if(helpers.exists() && helpers.exists(lit("debugpy"))) + { + debugpy_path = helpers.absolutePath(); + } + } + } + + // if we got a path to debugpy, try to load it + if(!debugpy_path.isEmpty()) + { + PyObject *syspath = PyObject_SafeGetAttrString(sysobj, "path"); + + if(!syspath) + { + qCritical() << "couldn't get sys.path"; + } + else + { + qInfo() << "Importing debugpy from" << debugpy_path; + + PyObject *str = PyUnicode_FromString(debugpy_path.toUtf8().data()); + + PyList_Append(syspath, str); + Py_DecRef(str); + + m_DebugPy = PyImport_ImportModule("debugpy"); + + if(!m_DebugPy) + { + qCritical() << "Failed to import debugpy"; + HandleException(NULL); + } + else + { + PyObject *configure = PyObject_SafeGetAttrString(m_DebugPy, "configure"); + + // don't let debugpy create a subprocess, for obvious reasons + if(configure) + { + PyObject *props = PyDict_New(); + PyDict_SetItemString(props, "subProcess", Py_False); + PyObject *ret = PyObject_CallFunction(configure, "O", props); + + if(!ret) + { + qCritical() << "Failed calling debugpy.configure"; + HandleException(NULL); + Py_XDECREF(m_DebugPy); + m_DebugPy = NULL; + } + + Py_XDECREF(ret); + Py_XDECREF(props); + } + else + { + qCritical() << "Couldn't find debugpy.configure"; + } + + Py_XDECREF(configure); + + if(m_DebugPy) + { + PyObject *listen = PyObject_SafeGetAttrString(m_DebugPy, "listen"); + + if(listen) + { + // listen on default port + PyObject *args = PyTuple_Pack(1, PyLong_FromLong(5678)); + PyObject *kwargs = PyDict_New(); + PyDict_SetItemString(kwargs, "in_process_debug_adapter", Py_True); + + PyObject *ret = PyObject_Call(listen, args, kwargs); + + if(!ret) + { + qCritical() << "Failed calling debugpy.listen"; + HandleException(NULL); + Py_XDECREF(m_DebugPy); + m_DebugPy = NULL; + } + + Py_XDECREF(ret); + + Py_XDECREF(args); + Py_XDECREF(kwargs); + } + else + { + qCritical() << "Couldn't find debugpy.listen"; + } + + Py_XDECREF(listen); + } + } + + // remove the search path from sys.path now + Py_XDECREF(PyObject_CallMethod(syspath, "pop", NULL)); + Py_DecRef(syspath); + + AddDebuggableThread(); + + if(m_DebugPy) + { + m_CallWrapper = + Py_CompileString("debugpy.trace_this_thread(True)", "__callwrapper.py", Py_eval_input); + m_CallWrapperGlobals = PyDict_Copy(main_dict); + PyDict_SetItemString(m_CallWrapperGlobals, "debugpy", m_DebugPy); + + // don't pollute the globals + PyObject *tmpGlobals = PyDict_Copy(main_dict); + + // monkey patch to get around a bug in debugpy/pydevd: https://github.com/microsoft/debugpy/issues/2011 + PyObject *ret = PyRun_String(R"( +try: + monkey_class = sys.modules['_pydevd_bundle'].pydevd_process_net_command_json.PyDevJsonCommandProcessor + orig = monkey_class.on_continue_request + + def on_continue_request_hack(self, py_db, request): + request.arguments.threadId = '*' + orig(self, py_db, request) + + monkey_class.on_continue_request = on_continue_request_hack + + # while we're here, disable termination. No clean way to do this + # otherwise + def _request_terminate_process_hack(self, py_db): + self.api.request_resume_thread('*') + + monkey_class._request_terminate_process = _request_terminate_process_hack + + print("Monkey-patched debugpy successfully") +except: + print("Failed to monkey-patch debugpy") + pass +)", + Py_file_input, tmpGlobals, NULL); + + Py_XDECREF(ret); + Py_XDECREF(tmpGlobals); + } + } + } + } +#endif + // if we need to append to sys.path to locate PySide2, do that now #if defined(PYSIDE2_SYS_PATH) { @@ -822,6 +1078,8 @@ void PythonContext::ProcessExtensionWork(std::function callback) { PyGILState_STATE gil = PyGILState_Ensure(); + PrepareDebugTracing(); + callback(); PyGILState_Release(gil); @@ -960,6 +1218,8 @@ QString PythonContext::LoadExtension(ICaptureContext &ctx, const rdcstr &extensi if(ext) { + PrepareDebugTracing(); + // if import succeeded, call register() PyObject *register_func = PyObject_SafeGetAttrString(ext, "register"); @@ -1132,13 +1392,38 @@ void PythonContext::executeString(const QString &filename, const QString &source return; } - location.file = filename; + QString tempFilename = filename; + + if(filename.isEmpty()) + { + tempFilename = lit(""); + } + else if(!QFile::exists(filename)) + { + for(QString path : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)) + { + QDir tmpDir(path); + tmpDir.mkpath(lit("pytmp")); + tmpDir.cd(lit("pytmp")); + if(tmpDir.exists()) + { + tempFilename = tmpDir.absoluteFilePath(filename); + QFile f(tempFilename); + f.open(QFile::Truncate | QFile::WriteOnly); + f.write(source.toUtf8().data()); + f.close(); + break; + } + } + } + + location.file = tempFilename; location.line = 1; PyGILState_STATE gil = PyGILState_Ensure(); PyObject *compiled = - Py_CompileString(source.toUtf8().data(), filename.toUtf8().data(), + Py_CompileString(source.toUtf8().data(), tempFilename.toUtf8().data(), source.count(QLatin1Char('\n')) == 0 ? Py_single_input : Py_file_input); bool caughtException = false; @@ -1149,6 +1434,8 @@ void PythonContext::executeString(const QString &filename, const QString &source if(compiled) { + PrepareDebugTracing(); + PyObject *sys = PyImport_ImportModule("sys"); PyObject *settrace = NULL; PyObject *gettrace = NULL; @@ -1214,7 +1501,7 @@ void PythonContext::executeString(const QString &filename, const QString &source void PythonContext::executeString(const QString &source) { - executeString(lit(""), source); + executeString(QString(), source); } void PythonContext::executeFile(const QString &filename) @@ -1386,6 +1673,105 @@ QStringList PythonContext::completionOptions(QString base) return ret; } +void PythonContext::AddDebuggableThread() +{ + if(!m_DebugPy) + return; + + PyGILState_STATE gil = PyGILState_Ensure(); + + PyObject *debug_this_thread = PyObject_SafeGetAttrString(m_DebugPy, "debug_this_thread"); + + if(debug_this_thread) + { + PyObject *ret = PyObject_CallNoArgs(debug_this_thread); + + if(!ret) + qCritical() << "Failed calling debugpy.debug_this_thread"; + + Py_XDECREF(ret); + } + else + { + qCritical() << "Couldn't find debugpy.debug_this_thread"; + } + + Py_XDECREF(debug_this_thread); + + QString threadName = QThread::currentThread()->objectName(); + + if(!threadName.isEmpty()) + { + PyObject *sys = PyImport_ImportModule("sys"); + Q_ASSERT(sys); + PyObject *modules = PyObject_SafeGetAttrString(sys, "modules"); + Q_ASSERT(modules); + + PyObject *threading = PyDict_GetItemString(modules, "threading"); + + // expect to load threading + if(threading) + { + PyObject *current_thread = PyObject_SafeGetAttrString(threading, "current_thread"); + + Q_ASSERT(current_thread); + + PyObject *cur = PyObject_CallNoArgs(current_thread); + + if(cur) + { + if(PyObject_HasAttrString(cur, "name")) + PyObject_SetAttrString(cur, "name", PyUnicode_FromString(threadName.toUtf8().data())); + + Py_XDECREF(cur); + } + + Py_XDECREF(current_thread); + } + + Py_XDECREF(modules); + Py_XDECREF(sys); + } + + PyGILState_Release(gil); +} + +void PythonContext::RemoveDebuggableThread() +{ + if(!m_DebugPy) + return; + + // 3.13 fixes this to track the lifetime of dummy/external threads, before that we do it manually +#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 13 + PyGILState_STATE gil = PyGILState_Ensure(); + + PyObject *sys = PyImport_ImportModule("sys"); + Q_ASSERT(sys); + PyObject *modules = PyObject_SafeGetAttrString(sys, "modules"); + Q_ASSERT(modules); + + PyObject *threading = PyDict_GetItemString(modules, "threading"); + + // expect to load threading + if(threading) + { + PyObject *_active = PyObject_SafeGetAttrString(threading, "_active"); + + if(_active) + { + PyObject *key = PyLong_FromInt32(PyThread_get_thread_ident()); + if(PyDict_Contains(_active, key) == 1) + PyDict_DelItem(_active, key); + Py_XDECREF(key); + } + + Py_XDECREF(_active); + } + + PyGILState_Release(gil); +#endif +} + PyObject *PythonContext::QtObjectToPython(const char *typeName, QObject *object) { #if PYSIDE2_ENABLED @@ -1726,7 +2112,7 @@ extern "C" void HandleException(PyObject *global_handle) { emit redirector->context->exception(typeStr, valueStr, finalLine, frames); } - else if(redirector && !redirector->context) + else { // if still NULL we're running in the extension context rdcstr exString; @@ -1786,6 +2172,14 @@ extern "C" void QueueDecRef(PyObject *obj) decrefQueue.push_back(obj); } +extern "C" PyObject *DoFunctionCall(PyObject *object, PyObject *args) +{ + if(!PyCFunction_Check(object)) + PythonContext::PrepareDebugTracing(); + + return PyObject_Call(object, args, NULL); +} + extern "C" void ProcessDecRefQueue() { QMutexLocker lock(&decrefQueueMutex); diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.h b/qrenderdoc/Code/pyrenderdoc/PythonContext.h index 99dbacb26..2e24048b7 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.h +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.h @@ -67,6 +67,8 @@ public: static void GenerateStubs(const rdcarray &extraPaths); + static void PrepareDebugTracing(); + bool CheckInterfaces(rdcstr &log); QString versionString(); @@ -104,6 +106,8 @@ public: bool shouldAbort() { return m_Abort; } QString currentFile() { return location.file; } int currentLine() { return location.line; } + static void AddDebuggableThread(); + static void RemoveDebuggableThread(); signals: void traceLine(const QString &file, int line); void exception(const QString &type, const QString &value, int finalLine, QList frames); @@ -121,6 +125,13 @@ private: // python context static PyObject *main_dict; + // this is the debugpy module + static PyObject *m_DebugPy; + // these are used for callbacks where we have no python frame (e.g. C code on the UI calling into + // a registered python callback) so need to quickly set up things for debugging if enabled + static PyObject *m_CallWrapper; + static PyObject *m_CallWrapperGlobals; + // the list of extension objects, to be able to reload them static QMap extensions; diff --git a/qrenderdoc/Code/pyrenderdoc/function_conversion.h b/qrenderdoc/Code/pyrenderdoc/function_conversion.h index 4d15a62f8..33344c865 100644 --- a/qrenderdoc/Code/pyrenderdoc/function_conversion.h +++ b/qrenderdoc/Code/pyrenderdoc/function_conversion.h @@ -36,6 +36,7 @@ extern "C" bool IsThreadBlocking(PyObject *global_handle); extern "C" void SetThreadBlocking(PyObject *global_handle, bool block); extern "C" void QueueDecRef(PyObject *obj); extern "C" void ProcessDecRefQueue(); +extern "C" PyObject *DoFunctionCall(PyObject *object, PyObject *args); struct ExceptionData { @@ -246,7 +247,7 @@ struct varfunc ProcessDecRefQueue(); - PyObject *result = PyObject_Call(func, args, 0); + PyObject *result = DoFunctionCall(func, args); Py_DECREF(args); diff --git a/qrenderdoc/Code/pyrenderdoc/pyrenderdoc_stub.cpp b/qrenderdoc/Code/pyrenderdoc/pyrenderdoc_stub.cpp index 2d9bd2c7d..6f43de08a 100644 --- a/qrenderdoc/Code/pyrenderdoc/pyrenderdoc_stub.cpp +++ b/qrenderdoc/Code/pyrenderdoc/pyrenderdoc_stub.cpp @@ -72,6 +72,11 @@ extern "C" void ProcessDecRefQueue() { } +extern "C" PyObject *DoFunctionCall(PyObject *object, PyObject *args) +{ + return PyObject_Call(object, args, NULL); +} + extern "C" RENDERDOC_EXPORT_API int _rd_python_minor_version() { return PY_MINOR_VERSION; diff --git a/qrenderdoc/Windows/Dialogs/SettingsDialog.cpp b/qrenderdoc/Windows/Dialogs/SettingsDialog.cpp index be2455f25..aa61eeada 100644 --- a/qrenderdoc/Windows/Dialogs/SettingsDialog.cpp +++ b/qrenderdoc/Windows/Dialogs/SettingsDialog.cpp @@ -307,6 +307,10 @@ SettingsDialog::SettingsDialog(ICaptureContext &ctx, QWidget *parent) ui->EventBrowser_ApplyColors->setChecked(m_Ctx.Config().EventBrowser_ApplyColors); ui->EventBrowser_ColorEventRow->setChecked(m_Ctx.Config().EventBrowser_ColorEventRow); + ui->Python_DebugEnabled->setChecked(m_Ctx.Config().Python_DebugEnabled); + + ui->Python_DebugPyDir->setText(m_Ctx.Config().Python_DebugPyDir); + ui->Comments_ShowOnLoad->setChecked(m_Ctx.Config().Comments_ShowOnLoad); ui->Formatter_MinFigures->setValue(m_Ctx.Config().Formatter_MinFigures); @@ -845,6 +849,35 @@ void SettingsDialog::on_Python_StubPaths_clicked() } } +void SettingsDialog::on_Python_DebugPyDirBrowse_clicked() +{ + QString dir = RDDialog::getExistingDirectory(this, tr("Choose location of debugpy module"), + m_Ctx.Config().Python_DebugPyDir); + + if(!dir.isEmpty() && QDir(dir).exists() && QDir(dir).exists(lit("__init__.py"))) + { + m_Ctx.Config().Python_DebugPyDir = dir; + ui->Python_DebugPyDir->setText(dir); + } + + m_Ctx.Config().Save(); +} + +void SettingsDialog::on_Python_DebugPyDir_textEdited(const QString &dir) +{ + if((QDir(dir).exists() && QDir(dir).exists(lit("__init__.py"))) || dir.isEmpty()) + m_Ctx.Config().Python_DebugPyDir = dir; + + m_Ctx.Config().Save(); +} + +void SettingsDialog::on_Python_DebugEnabled_toggled(bool checked) +{ + m_Ctx.Config().Python_DebugEnabled = ui->Python_DebugEnabled->isChecked(); + + m_Ctx.Config().Save(); +} + // texture viewer void SettingsDialog::on_TextureViewer_PerTexSettings_toggled(bool checked) { diff --git a/qrenderdoc/Windows/Dialogs/SettingsDialog.h b/qrenderdoc/Windows/Dialogs/SettingsDialog.h index c6fd0e1f0..6f6f1761c 100644 --- a/qrenderdoc/Windows/Dialogs/SettingsDialog.h +++ b/qrenderdoc/Windows/Dialogs/SettingsDialog.h @@ -83,6 +83,9 @@ private slots: // python void on_Python_StubPaths_clicked(); + void on_Python_DebugPyDirBrowse_clicked(); + void on_Python_DebugPyDir_textEdited(const QString &dir); + void on_Python_DebugEnabled_toggled(bool checked); // texture viewer void on_TextureViewer_PerTexSettings_toggled(bool checked); diff --git a/qrenderdoc/Windows/Dialogs/SettingsDialog.ui b/qrenderdoc/Windows/Dialogs/SettingsDialog.ui index d88f9f5c9..8dddc36fd 100644 --- a/qrenderdoc/Windows/Dialogs/SettingsDialog.ui +++ b/qrenderdoc/Windows/Dialogs/SettingsDialog.ui @@ -720,7 +720,38 @@ After interop is enabled you will need to reload any capture. - + + + + Enable python debugging (restart required) + + + + + + + + + + + + + + Location of custom debugpy module for debugging + + + + + + + + + + Browse + + + + Qt::Vertical diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index f90031231..9fc4e9816 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -1074,6 +1074,8 @@ void PythonShell::RunScript() enableButtons(false); LambdaThread *thread = new LambdaThread([this, script, context]() { + PythonContext::AddDebuggableThread(); + scriptContext = context; context->executeString(lit("script.py"), script); scriptContext = NULL; @@ -1082,6 +1084,8 @@ void PythonShell::RunScript() context->Finish(); enableButtons(true); }); + + PythonContext::RemoveDebuggableThread(); }); thread->setName(lit("Python script")); diff --git a/qrenderdoc/qrenderdoc_local.vcxproj b/qrenderdoc/qrenderdoc_local.vcxproj index d67da2051..24cd1ab9f 100644 --- a/qrenderdoc/qrenderdoc_local.vcxproj +++ b/qrenderdoc/qrenderdoc_local.vcxproj @@ -1843,6 +1843,21 @@ Always false + + pyexpat.pyd + Always + false + + + _socket.pyd + Always + false + + + _queue.pyd + Always + false + unicodedata.pyd Always