/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2019-2020 Baldur Karlsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. ******************************************************************************/ #include "ShaderViewer.h" #include #include #include #include #include #include #include #include #include #include "Code/ScintillaSyntax.h" #include "Widgets/FindReplace.h" #include "scintilla/include/SciLexer.h" #include "scintilla/include/qt/ScintillaEdit.h" #include "toolwindowmanager/ToolWindowManager.h" #include "toolwindowmanager/ToolWindowManagerArea.h" #include "ui_ShaderViewer.h" namespace { struct VariableTag { VariableTag() : offset(0), globalSourceVar(-1), localSourceVar(-1) {} VariableTag(rdcstr name, uint32_t offs, int32_t globalVar, int32_t localVar) : offset(offs), globalSourceVar(globalVar), localSourceVar(localVar) { debugVar.name = name; } VariableTag(DebugVariableReference var) : offset(0), debugVar(var) {} uint32_t offset; int32_t globalSourceVar; int32_t localSourceVar; DebugVariableReference debugVar; }; struct AccessedResourceTag { AccessedResourceTag() : type(VarType::Unknown), step(0) { bind.bind = -1; } AccessedResourceTag(uint32_t s) : type(VarType::Unknown), step(s) { bind.bind = -1; } AccessedResourceTag(BindpointIndex bp, VarType t) : bind(bp), type(t), step(0) {} AccessedResourceTag(ShaderVariable var) : step(0), type(var.type) { if(var.type == VarType::ReadOnlyResource || var.type == VarType::ReadWriteResource) bind = var.GetBinding(); else bind.bind = -1; } BindpointIndex bind; VarType type; uint32_t step; }; }; Q_DECLARE_METATYPE(VariableTag); Q_DECLARE_METATYPE(AccessedResourceTag); ShaderViewer::ShaderViewer(ICaptureContext &ctx, QWidget *parent) : QFrame(parent), ui(new Ui::ShaderViewer), m_Ctx(ctx) { ui->setupUi(this); ui->constants->setFont(Formatter::PreferredFont()); ui->accessedResources->setFont(Formatter::PreferredFont()); ui->debugVars->setFont(Formatter::PreferredFont()); ui->sourceVars->setFont(Formatter::PreferredFont()); ui->watch->setFont(Formatter::PreferredFont()); ui->inputSig->setFont(Formatter::PreferredFont()); ui->outputSig->setFont(Formatter::PreferredFont()); ui->callstack->setFont(Formatter::PreferredFont()); // we create this up front so its state stays persistent as much as possible. m_FindReplace = new FindReplace(this); m_FindResults = MakeEditor(lit("findresults"), QString(), SCLEX_NULL); m_FindResults->setReadOnly(true); m_FindResults->setWindowTitle(lit("Find Results")); // remove margins m_FindResults->setMarginWidthN(0, 0); m_FindResults->setMarginWidthN(1, 0); m_FindResults->setMarginWidthN(2, 0); QObject::connect(m_FindReplace, &FindReplace::performFind, this, &ShaderViewer::performFind); QObject::connect(m_FindReplace, &FindReplace::performFindAll, this, &ShaderViewer::performFindAll); QObject::connect(m_FindReplace, &FindReplace::performReplace, this, &ShaderViewer::performReplace); QObject::connect(m_FindReplace, &FindReplace::performReplaceAll, this, &ShaderViewer::performReplaceAll); ui->docking->addToolWindow(m_FindReplace, ToolWindowManager::NoArea); ui->docking->setToolWindowProperties(m_FindReplace, ToolWindowManager::HideOnClose); ui->docking->addToolWindow(m_FindResults, ToolWindowManager::NoArea); ui->docking->setToolWindowProperties(m_FindResults, ToolWindowManager::HideOnClose); { m_DisassemblyView = MakeEditor(lit("scintillaDisassem"), QString(), m_Ctx.APIProps().pipelineType == GraphicsAPI::Vulkan ? SCLEX_GLSL : SCLEX_HLSL); m_DisassemblyView->setReadOnly(true); QObject::connect(m_DisassemblyView, &ScintillaEdit::keyPressed, this, &ShaderViewer::readonly_keyPressed); m_Scintillas.push_back(m_DisassemblyView); m_DisassemblyFrame = new QWidget(this); m_DisassemblyFrame->setWindowTitle(tr("Disassembly")); m_DisassemblyToolbar = new QFrame(this); m_DisassemblyToolbar->setFrameShape(QFrame::Panel); m_DisassemblyToolbar->setFrameShadow(QFrame::Raised); QHBoxLayout *toolbarlayout = new QHBoxLayout(m_DisassemblyToolbar); toolbarlayout->setSpacing(2); toolbarlayout->setContentsMargins(3, 3, 3, 3); m_DisassemblyType = new QComboBox(m_DisassemblyToolbar); m_DisassemblyType->setMaxVisibleItems(12); m_DisassemblyType->setSizeAdjustPolicy(QComboBox::AdjustToContents); toolbarlayout->addWidget(new QLabel(tr("Disassembly type:"), m_DisassemblyToolbar)); toolbarlayout->addWidget(m_DisassemblyType); toolbarlayout->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum)); QVBoxLayout *framelayout = new QVBoxLayout(m_DisassemblyFrame); framelayout->setSpacing(0); framelayout->setMargin(0); framelayout->addWidget(m_DisassemblyToolbar); framelayout->addWidget(m_DisassemblyView); ui->docking->addToolWindow(m_DisassemblyFrame, ToolWindowManager::EmptySpace); ui->docking->setToolWindowProperties(m_DisassemblyFrame, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow | ToolWindowManager::AlwaysDisplayFullTabs); } ui->docking->setAllowFloatingWindow(false); { QMenu *snippetsMenu = new QMenu(this); QAction *dim = new QAction(tr("Texture Dimensions Global"), this); QAction *mip = new QAction(tr("Selected Mip Global"), this); QAction *slice = new QAction(tr("Seleted Array Slice / Cubemap Face Global"), this); QAction *sample = new QAction(tr("Selected Sample Global"), this); QAction *type = new QAction(tr("Texture Type Global"), this); QAction *samplers = new QAction(tr("Point && Linear Samplers"), this); QAction *resources = new QAction(tr("Texture Resources"), this); snippetsMenu->addAction(dim); snippetsMenu->addAction(mip); snippetsMenu->addAction(slice); snippetsMenu->addAction(sample); snippetsMenu->addAction(type); snippetsMenu->addSeparator(); snippetsMenu->addAction(samplers); snippetsMenu->addAction(resources); QObject::connect(dim, &QAction::triggered, this, &ShaderViewer::snippet_textureDimensions); QObject::connect(mip, &QAction::triggered, this, &ShaderViewer::snippet_selectedMip); QObject::connect(slice, &QAction::triggered, this, &ShaderViewer::snippet_selectedSlice); QObject::connect(sample, &QAction::triggered, this, &ShaderViewer::snippet_selectedSample); QObject::connect(type, &QAction::triggered, this, &ShaderViewer::snippet_selectedType); QObject::connect(samplers, &QAction::triggered, this, &ShaderViewer::snippet_samplers); QObject::connect(resources, &QAction::triggered, this, &ShaderViewer::snippet_resources); ui->snippets->setMenu(snippetsMenu); } QVBoxLayout *layout = new QVBoxLayout(this); layout->setSpacing(3); layout->setContentsMargins(3, 3, 3, 3); layout->addWidget(ui->toolbar); layout->addWidget(ui->docking); m_Ctx.AddCaptureViewer(this); } void ShaderViewer::editShader(ResourceId id, ShaderStage stage, const QString &entryPoint, const rdcstrpairs &files, ShaderEncoding shaderEncoding, ShaderCompileFlags flags) { m_Scintillas.removeOne(m_DisassemblyView); ui->docking->removeToolWindow(m_DisassemblyFrame); m_DisassemblyView = NULL; m_Stage = stage; m_Flags = flags; m_CustomShader = (id == ResourceId()); m_EditingShader = id; // set up compilation parameters for(ShaderEncoding i : values()) if(IsTextRepresentation(i) || shaderEncoding == i) m_Encodings << i; QStringList strs; strs.clear(); for(ShaderEncoding i : m_Encodings) strs << ToQStr(i); ui->encoding->addItems(strs); ui->encoding->setCurrentIndex(m_Encodings.indexOf(shaderEncoding)); ui->entryFunc->setText(entryPoint); QObject::connect(ui->entryFunc, &QLineEdit::textChanged, [this](const QString &) { MarkModification(); }); QObject::connect(ui->toolCommandLine, &QTextEdit::textChanged, [this]() { MarkModification(); }); PopulateCompileTools(); QObject::connect(ui->encoding, OverloadedSlot::of(&QComboBox::currentIndexChanged), [this](int) { PopulateCompileTools(); MarkModification(); }); QObject::connect(ui->compileTool, OverloadedSlot::of(&QComboBox::currentIndexChanged), [this](int) { PopulateCompileToolParameters(); MarkModification(); }); // if it's a custom shader, hide the group entirely (don't allow customisation of compile // parameters). We can still use it to store the parameters passed in. When visible we collapse it // by default. if(m_CustomShader) ui->compilationGroup->hide(); // hide debugging windows ui->watch->hide(); ui->debugVars->hide(); ui->constants->hide(); ui->resourcesPanel->hide(); ui->callstack->hide(); ui->sourceVars->hide(); ui->snippets->setVisible(m_CustomShader); // hide debugging toolbar buttons ui->debugSep->hide(); ui->runBack->hide(); ui->run->hide(); ui->stepBack->hide(); ui->stepNext->hide(); ui->runToCursor->hide(); ui->runToSample->hide(); ui->runToNaNOrInf->hide(); ui->regFormatSep->hide(); ui->intView->hide(); ui->floatView->hide(); ui->debugToggleSep->hide(); ui->debugToggle->hide(); // hide signatures ui->inputSig->hide(); ui->outputSig->hide(); QString title; QWidget *sel = NULL; for(const rdcstrpair &kv : files) { QString name = QFileInfo(kv.first).fileName(); QString text = kv.second; ScintillaEdit *scintilla = AddFileScintilla(name, text, shaderEncoding); scintilla->setReadOnly(false); QObject::connect(scintilla, &ScintillaEdit::keyPressed, this, &ShaderViewer::editable_keyPressed); QObject::connect(scintilla, &ScintillaEdit::modified, [this](int type, int, int, int, const QByteArray &, int, int, int) { if(type & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT | SC_MOD_BEFOREINSERT | SC_MOD_BEFOREDELETE)) m_FindState = FindState(); MarkModification(); }); m_Ctx.GetMainWindow()->RegisterShortcut(QKeySequence(QKeySequence::Refresh).toString(), this, [this](QWidget *) { on_refresh_clicked(); }); ui->refresh->setToolTip(ui->refresh->toolTip() + lit(" (%1)").arg(QKeySequence(QKeySequence::Refresh).toString())); QWidget *w = (QWidget *)scintilla; w->setProperty("filename", kv.first); if(text.contains(entryPoint)) sel = scintilla; if(sel == scintilla || title.isEmpty()) title = tr(" - %1 - %2()").arg(name).arg(entryPoint); } if(sel != NULL) ToolWindowManager::raiseToolWindow(sel); if(m_CustomShader) title.prepend(tr("Editing %1 Shader").arg(ToQStr(stage, m_Ctx.APIProps().pipelineType))); else title.prepend(tr("Editing %1").arg(m_Ctx.GetResourceNameUnsuffixed(id))); setWindowTitle(title); if(files.count() > 2) addFileList(); m_Errors = MakeEditor(lit("errors"), QString(), SCLEX_NULL); m_Errors->setReadOnly(true); m_Errors->setWindowTitle(lit("Errors")); // remove margins m_Errors->setMarginWidthN(0, 0); m_Errors->setMarginWidthN(1, 0); m_Errors->setMarginWidthN(2, 0); QObject::connect(m_Errors, &ScintillaEdit::keyPressed, this, &ShaderViewer::readonly_keyPressed); ui->docking->addToolWindow( m_Errors, ToolWindowManager::AreaReference(ToolWindowManager::BottomOf, ui->docking->areaOf(m_Scintillas.front()), 0.2f)); ui->docking->setToolWindowProperties( m_Errors, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow); if(!m_CustomShader) { ui->compilationGroup->setWindowTitle(tr("Compilation Settings")); ui->docking->addToolWindow(ui->compilationGroup, ToolWindowManager::AreaReference( ToolWindowManager::LeftOf, ui->docking->areaOf(m_Errors), 0.5f)); ui->docking->setToolWindowProperties( ui->compilationGroup, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow); } } void ShaderViewer::cacheResources() { m_ReadOnlyResources = m_Ctx.CurPipelineState().GetReadOnlyResources(m_Stage, false); m_ReadWriteResources = m_Ctx.CurPipelineState().GetReadWriteResources(m_Stage, false); } void ShaderViewer::debugShader(const ShaderBindpointMapping *bind, const ShaderReflection *shader, ResourceId pipeline, ShaderDebugTrace *trace, const QString &debugContext) { if(bind) m_Mapping = *bind; m_ShaderDetails = shader; m_Pipeline = pipeline; m_Trace = trace; m_Stage = ShaderStage::Vertex; m_DebugContext = debugContext; // no recompilation happening, hide that group ui->compilationGroup->hide(); // no replacing allowed, stay in find mode m_FindReplace->allowUserModeChange(false); if(!bind || !m_ShaderDetails) m_Trace = NULL; if(m_ShaderDetails) { m_Stage = m_ShaderDetails->stage; QPointer me(this); m_Ctx.Replay().AsyncInvoke([me, this](IReplayController *r) { if(!me) return; rdcarray targets = r->GetDisassemblyTargets(m_Pipeline != ResourceId()); if(m_Pipeline == ResourceId()) { rdcarray pipelineTargets = r->GetDisassemblyTargets(true); if(pipelineTargets.size() > targets.size()) { m_PipelineTargets = pipelineTargets; m_PipelineTargets.removeIf([&targets](const rdcstr &t) { return targets.contains(t); }); } } rdcstr disasm = r->DisassembleShader(m_Pipeline, m_ShaderDetails, ""); if(!me) return; GUIInvoke::call(this, [this, targets, disasm]() { QStringList targetNames; for(int i = 0; i < targets.count(); i++) { QString target = targets[i]; targetNames << QString(targets[i]); if(i == 0) { // add any custom decompiling tools we have after the first one for(const ShaderProcessingTool &d : m_Ctx.Config().ShaderProcessors) { if(d.input == m_ShaderDetails->encoding) targetNames << targetName(d); } } } if(!m_PipelineTargets.empty()) targetNames << tr("More disassembly formats..."); m_DisassemblyType->clear(); m_DisassemblyType->addItems(targetNames); m_DisassemblyType->setCurrentIndex(0); QObject::connect(m_DisassemblyType, OverloadedSlot::of(&QComboBox::currentIndexChanged), this, &ShaderViewer::disassemble_typeChanged); // read-only applies to us too! m_DisassemblyView->setReadOnly(false); SetTextAndUpdateMargin0(m_DisassemblyView, disasm); m_DisassemblyView->setReadOnly(true); }); }); } updateWindowTitle(); // we always want to highlight words/registers QObject::connect(m_DisassemblyView, &ScintillaEdit::buttonReleased, this, &ShaderViewer::disassembly_buttonReleased); if(m_Trace) { if(m_Stage == ShaderStage::Vertex) { ANALYTIC_SET(ShaderDebug.Vertex, true); } else if(m_Stage == ShaderStage::Pixel) { ANALYTIC_SET(ShaderDebug.Pixel, true); } else if(m_Stage == ShaderStage::Compute) { ANALYTIC_SET(ShaderDebug.Compute, true); } m_DisassemblyFrame->layout()->removeWidget(m_DisassemblyToolbar); } if(m_ShaderDetails && !m_ShaderDetails->debugInfo.files.isEmpty()) { if(m_Trace) setWindowTitle(QFormatStr("Debug %1() - %2").arg(m_ShaderDetails->entryPoint).arg(debugContext)); else setWindowTitle(m_ShaderDetails->entryPoint); // add all the files, skipping any that have empty contents. We push a NULL in that case so the // indices still match up with what the debug info expects. Debug info *shouldn't* point us at // an empty file, but if it does we'll just bail out when we see NULL m_FileScintillas.reserve(m_ShaderDetails->debugInfo.files.count()); QWidget *sel = NULL; for(const ShaderSourceFile &f : m_ShaderDetails->debugInfo.files) { if(f.contents.isEmpty()) { m_FileScintillas.push_back(NULL); continue; } QString name = QFileInfo(f.filename).fileName(); QString text = f.contents; ScintillaEdit *scintilla = AddFileScintilla(name, text, m_ShaderDetails->debugInfo.encoding); if(sel == NULL) sel = scintilla; m_FileScintillas.push_back(scintilla); } if(m_Trace || sel == NULL) sel = m_DisassemblyFrame; if(m_ShaderDetails->debugInfo.files.size() > 2) addFileList(); ToolWindowManager::raiseToolWindow(sel); } // hide edit buttons ui->editSep->hide(); ui->refresh->hide(); ui->snippets->hide(); if(m_Trace) { // hide signatures ui->inputSig->hide(); ui->outputSig->hide(); if(m_ShaderDetails->debugInfo.files.isEmpty()) { ui->debugToggle->setEnabled(false); ui->debugToggle->setText(tr("Source Unavailable")); } ui->debugVars->setColumns({tr("Name"), tr("Value")}); ui->debugVars->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); ui->debugVars->header()->setSectionResizeMode(1, QHeaderView::Interactive); ui->sourceVars->setColumns({tr("Name"), tr("Register(s)"), tr("Type"), tr("Value")}); ui->sourceVars->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); ui->sourceVars->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->sourceVars->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); ui->sourceVars->header()->setSectionResizeMode(3, QHeaderView::Interactive); ui->constants->setColumns({tr("Name"), tr("Register(s)"), tr("Type"), tr("Value")}); ui->constants->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); ui->constants->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->constants->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); ui->constants->header()->setSectionResizeMode(3, QHeaderView::Interactive); ui->constants->header()->resizeSection(0, 80); ui->accessedResources->setColumns({tr("Location"), tr("Type"), tr("Info")}); ui->accessedResources->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); ui->accessedResources->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->accessedResources->header()->setSectionResizeMode(2, QHeaderView::Interactive); ui->accessedResources->header()->resizeSection(0, 80); ui->debugVars->setTooltipElidedItems(false); ui->constants->setTooltipElidedItems(false); ui->accessedResources->setTooltipElidedItems(false); ToolWindowManager::ToolWindowProperty windowProps = ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow; ui->watch->setWindowTitle(tr("Watch")); ui->docking->addToolWindow( ui->watch, ToolWindowManager::AreaReference(ToolWindowManager::BottomOf, ui->docking->areaOf(m_DisassemblyFrame), 0.25f)); ui->docking->setToolWindowProperties(ui->watch, windowProps); ui->debugVars->setWindowTitle(tr("Variable Values")); ui->docking->addToolWindow( ui->debugVars, ToolWindowManager::AreaReference(ToolWindowManager::AddTo, ui->docking->areaOf(ui->watch))); ui->docking->setToolWindowProperties(ui->debugVars, windowProps); ui->constants->setWindowTitle(tr("Constants && Resources")); ui->docking->addToolWindow( ui->constants, ToolWindowManager::AreaReference(ToolWindowManager::LeftOf, ui->docking->areaOf(ui->debugVars), 0.5f)); ui->docking->setToolWindowProperties(ui->constants, windowProps); ui->resourcesPanel->setWindowTitle(tr("Accessed Resources")); ui->docking->addToolWindow( ui->resourcesPanel, ToolWindowManager::AreaReference(ToolWindowManager::AddTo, ui->docking->areaOf(ui->constants))); ui->docking->setToolWindowProperties(ui->resourcesPanel, windowProps); ui->docking->raiseToolWindow(ui->constants); ui->callstack->setWindowTitle(tr("Callstack")); ui->docking->addToolWindow( ui->callstack, ToolWindowManager::AreaReference(ToolWindowManager::RightOf, ui->docking->areaOf(ui->debugVars), 0.2f)); ui->docking->setToolWindowProperties(ui->callstack, windowProps); ui->sourceVars->setWindowTitle(tr("High-level Variables")); ui->docking->addToolWindow( ui->sourceVars, ToolWindowManager::AreaReference(ToolWindowManager::AddTo, ui->docking->areaOf(ui->debugVars))); ui->docking->setToolWindowProperties(ui->sourceVars, windowProps); m_Line2Insts.resize(m_ShaderDetails->debugInfo.files.count()); bool hasLineInfo = false; for(size_t inst = 0; inst < m_Trace->lineInfo.size(); inst++) { const LineColumnInfo &line = m_Trace->lineInfo[inst]; int disasmLine = (int)line.disassemblyLine; if(disasmLine > 0 && disasmLine >= m_AsmLine2Inst.size()) { int oldSize = m_AsmLine2Inst.size(); m_AsmLine2Inst.resize(disasmLine + 1); for(int i = oldSize; i < disasmLine; i++) m_AsmLine2Inst[i] = -1; } if(disasmLine > 0) m_AsmLine2Inst[disasmLine] = (int)inst; if(line.fileIndex < 0 || line.fileIndex >= m_Line2Insts.count()) continue; hasLineInfo = true; for(uint32_t lineNum = line.lineStart; lineNum <= line.lineEnd; lineNum++) m_Line2Insts[line.fileIndex][lineNum].push_back(inst); } // if we don't have line mapping info, assume we also don't have useful high-level variable // info. Show the debug variables first rather than a potentially empty source variables panel. if(!hasLineInfo) ui->docking->raiseToolWindow(ui->debugVars); QObject::connect(ui->stepBack, &QToolButton::clicked, this, &ShaderViewer::stepBack); QObject::connect(ui->stepNext, &QToolButton::clicked, this, &ShaderViewer::stepNext); QObject::connect(ui->runBack, &QToolButton::clicked, this, &ShaderViewer::runBack); QObject::connect(ui->run, &QToolButton::clicked, this, &ShaderViewer::run); QObject::connect(ui->runToCursor, &QToolButton::clicked, this, &ShaderViewer::runToCursor); QObject::connect(ui->runToSample, &QToolButton::clicked, this, &ShaderViewer::runToSample); QObject::connect(ui->runToNaNOrInf, &QToolButton::clicked, this, &ShaderViewer::runToNanOrInf); for(ScintillaEdit *edit : m_Scintillas) { edit->setMarginWidthN(1, 20.0 * devicePixelRatioF()); // display current line in margin 2, distinct from breakpoint in margin 1 sptr_t markMask = (1 << CURRENT_MARKER) | (1 << FINISHED_MARKER); edit->setMarginMaskN(1, edit->marginMaskN(1) & ~markMask); edit->setMarginMaskN(2, edit->marginMaskN(2) | markMask); // suppress the built-in context menu and hook up our own edit->usePopUp(SC_POPUP_NEVER); edit->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(edit, &ScintillaEdit::customContextMenuRequested, this, &ShaderViewer::debug_contextMenu); edit->setMouseDwellTime(500); QObject::connect(edit, &ScintillaEdit::dwellStart, this, &ShaderViewer::disasm_tooltipShow); QObject::connect(edit, &ScintillaEdit::dwellEnd, this, &ShaderViewer::disasm_tooltipHide); } // register the shortcuts via MainWindow so that it works regardless of the active scintilla but // still handles multiple shader viewers being present (the one with focus will get the input) m_Ctx.GetMainWindow()->RegisterShortcut(QKeySequence(Qt::Key_F10).toString(), this, [this](QWidget *) { stepNext(); }); m_Ctx.GetMainWindow()->RegisterShortcut(QKeySequence(Qt::Key_F10 | Qt::ShiftModifier).toString(), this, [this](QWidget *) { stepBack(); }); m_Ctx.GetMainWindow()->RegisterShortcut( QKeySequence(Qt::Key_F10 | Qt::ControlModifier).toString(), this, [this](QWidget *) { runToCursor(); }); m_Ctx.GetMainWindow()->RegisterShortcut(QKeySequence(Qt::Key_F5).toString(), this, [this](QWidget *) { run(); }); m_Ctx.GetMainWindow()->RegisterShortcut(QKeySequence(Qt::Key_F5 | Qt::ShiftModifier).toString(), this, [this](QWidget *) { runBack(); }); m_Ctx.GetMainWindow()->RegisterShortcut(QKeySequence(Qt::Key_F9).toString(), this, [this](QWidget *) { ToggleBreakpoint(); }); // event filter to pick up tooltip events ui->constants->installEventFilter(this); ui->accessedResources->installEventFilter(this); ui->debugVars->installEventFilter(this); ui->watch->installEventFilter(this); cacheResources(); m_BackgroundRunning.release(); QPointer me(this); m_Ctx.Replay().AsyncInvoke([this, me](IReplayController *r) { if(!me) return; rdcarray states = r->ContinueDebug(m_Trace->debugger); bool finished = false; do { if(!me) return; rdcarray nextStates = r->ContinueDebug(m_Trace->debugger); if(!me) return; states.append(nextStates); finished = nextStates.empty(); } while(!finished && m_BackgroundRunning.available() == 1); if(!me) return; m_BackgroundRunning.tryAcquire(1); r->SetFrameEvent(m_Ctx.CurEvent(), true); if(!me) return; GUIInvoke::call(this, [this, states]() { m_States = states; if(!m_States.empty()) { for(const ShaderVariableChange &c : GetCurrentState().changes) m_Variables.push_back(c.after); } bool preferSourceDebug = false; for(const ShaderCompileFlag &flag : m_ShaderDetails->debugInfo.compileFlags.flags) { if(flag.name == "preferSourceDebug") { preferSourceDebug = true; break; } } updateDebugState(); // we do updateDebugging() again because the first call finds the scintilla for the current // source file, the second time jumps to it. if(preferSourceDebug) { gotoSourceDebugging(); updateDebugState(); } }); }); GUIInvoke::defer(this, [this, debugContext]() { // wait a short while before displaying the progress dialog (which won't show if we're already // done by the time we reach it) for(int i = 0; m_BackgroundRunning.available() == 1 && i < 100; i++) QThread::msleep(5); ShowProgressDialog(this, tr("Debugging %1").arg(debugContext), [this]() { return m_BackgroundRunning.available() == 0; }, NULL, [this]() { m_BackgroundRunning.acquire(); }); }); m_CurrentStateIdx = 0; QObject::connect(ui->watch, &RDTableWidget::keyPress, this, &ShaderViewer::watch_keyPress); ui->watch->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(ui->watch, &RDTableWidget::customContextMenuRequested, this, &ShaderViewer::variables_contextMenu); ui->debugVars->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(ui->debugVars, &RDTreeWidget::customContextMenuRequested, this, &ShaderViewer::variables_contextMenu); ui->sourceVars->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(ui->sourceVars, &RDTreeWidget::customContextMenuRequested, this, &ShaderViewer::variables_contextMenu); ui->accessedResources->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(ui->accessedResources, &RDTreeWidget::customContextMenuRequested, this, &ShaderViewer::accessedResources_contextMenu); ui->watch->insertRow(0); for(int i = 0; i < ui->watch->columnCount(); i++) { QTableWidgetItem *item = new QTableWidgetItem(); if(i > 0) item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->watch->setItem(0, i, item); } ui->watch->resizeRowsToContents(); ToolWindowManager::raiseToolWindow(m_DisassemblyFrame); } else { // hide watch, constants, variables ui->watch->hide(); ui->debugVars->hide(); ui->constants->hide(); ui->resourcesPanel->hide(); ui->sourceVars->hide(); ui->callstack->hide(); // hide debugging toolbar buttons ui->debugSep->hide(); ui->runBack->hide(); ui->run->hide(); ui->stepBack->hide(); ui->stepNext->hide(); ui->runToCursor->hide(); ui->runToSample->hide(); ui->runToNaNOrInf->hide(); ui->regFormatSep->hide(); ui->intView->hide(); ui->floatView->hide(); ui->debugToggleSep->hide(); ui->debugToggle->hide(); // show input and output signatures ui->inputSig->setColumns( {tr("Name"), tr("Index"), tr("Reg"), tr("Type"), tr("SysValue"), tr("Mask"), tr("Used")}); for(int i = 0; i < ui->inputSig->header()->count(); i++) ui->inputSig->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); ui->outputSig->setColumns( {tr("Name"), tr("Index"), tr("Reg"), tr("Type"), tr("SysValue"), tr("Mask"), tr("Used")}); for(int i = 0; i < ui->outputSig->header()->count(); i++) ui->outputSig->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); if(m_ShaderDetails) { for(const SigParameter &s : m_ShaderDetails->inputSignature) { QString name = s.varName.isEmpty() ? QString(s.semanticName) : QFormatStr("%1 (%2)").arg(s.varName).arg(s.semanticName); if(s.semanticName.isEmpty()) name = s.varName; QString semIdx = s.needSemanticIndex ? QString::number(s.semanticIndex) : QString(); QString regIdx = s.systemValue == ShaderBuiltin::Undefined ? QString::number(s.regIndex) : lit("-"); ui->inputSig->addTopLevelItem(new RDTreeWidgetItem( {name, semIdx, regIdx, TypeString(s), ToQStr(s.systemValue), GetComponentString(s.regChannelMask), GetComponentString(s.channelUsedMask)})); } bool multipleStreams = false; for(const SigParameter &s : m_ShaderDetails->outputSignature) { if(s.stream > 0) { multipleStreams = true; break; } } for(const SigParameter &s : m_ShaderDetails->outputSignature) { QString name = s.varName.isEmpty() ? QString(s.semanticName) : QFormatStr("%1 (%2)").arg(s.varName).arg(s.semanticName); if(s.semanticName.isEmpty()) name = s.varName; if(multipleStreams) name = QFormatStr("Stream %1 : %2").arg(s.stream).arg(name); QString semIdx = s.needSemanticIndex ? QString::number(s.semanticIndex) : QString(); QString regIdx = s.systemValue == ShaderBuiltin::Undefined ? QString::number(s.regIndex) : lit("-"); ui->outputSig->addTopLevelItem(new RDTreeWidgetItem( {name, semIdx, regIdx, TypeString(s), ToQStr(s.systemValue), GetComponentString(s.regChannelMask), GetComponentString(s.channelUsedMask)})); } } ui->inputSig->setWindowTitle(tr("Input Signature")); ui->docking->addToolWindow(ui->inputSig, ToolWindowManager::AreaReference( ToolWindowManager::BottomOf, ui->docking->areaOf(m_DisassemblyFrame), 0.2f)); ui->docking->setToolWindowProperties( ui->inputSig, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow); ui->outputSig->setWindowTitle(tr("Output Signature")); ui->docking->addToolWindow( ui->outputSig, ToolWindowManager::AreaReference(ToolWindowManager::RightOf, ui->docking->areaOf(ui->inputSig), 0.5f)); ui->docking->setToolWindowProperties( ui->outputSig, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow); } for(ScintillaEdit *edit : m_Scintillas) { // C# LightCoral edit->markerSetBack(CURRENT_MARKER, SCINTILLA_COLOUR(240, 128, 128)); edit->markerSetBack(CURRENT_MARKER + 1, SCINTILLA_COLOUR(240, 128, 128)); edit->markerDefine(CURRENT_MARKER, SC_MARK_SHORTARROW); edit->markerDefine(CURRENT_MARKER + 1, SC_MARK_BACKGROUND); edit->indicSetFore(CURRENT_INDICATOR, SCINTILLA_COLOUR(240, 128, 128)); edit->indicSetAlpha(CURRENT_INDICATOR, 220); edit->indicSetOutlineAlpha(CURRENT_INDICATOR, 255); edit->indicSetUnder(CURRENT_INDICATOR, true); edit->indicSetStyle(CURRENT_INDICATOR, INDIC_STRAIGHTBOX); edit->indicSetHoverFore(CURRENT_INDICATOR, SCINTILLA_COLOUR(240, 128, 128)); edit->indicSetHoverStyle(CURRENT_INDICATOR, INDIC_STRAIGHTBOX); // C# LightSlateGray edit->markerSetBack(FINISHED_MARKER, SCINTILLA_COLOUR(119, 136, 153)); edit->markerSetBack(FINISHED_MARKER + 1, SCINTILLA_COLOUR(119, 136, 153)); edit->markerDefine(FINISHED_MARKER, SC_MARK_ROUNDRECT); edit->markerDefine(FINISHED_MARKER + 1, SC_MARK_BACKGROUND); edit->indicSetFore(FINISHED_INDICATOR, SCINTILLA_COLOUR(119, 136, 153)); edit->indicSetAlpha(FINISHED_INDICATOR, 220); edit->indicSetOutlineAlpha(FINISHED_INDICATOR, 255); edit->indicSetUnder(FINISHED_INDICATOR, true); edit->indicSetStyle(FINISHED_INDICATOR, INDIC_STRAIGHTBOX); edit->indicSetHoverFore(FINISHED_INDICATOR, SCINTILLA_COLOUR(119, 136, 153)); edit->indicSetHoverStyle(FINISHED_INDICATOR, INDIC_STRAIGHTBOX); // C# Red edit->markerSetBack(BREAKPOINT_MARKER, SCINTILLA_COLOUR(255, 0, 0)); edit->markerSetBack(BREAKPOINT_MARKER + 1, SCINTILLA_COLOUR(255, 0, 0)); edit->markerDefine(BREAKPOINT_MARKER, SC_MARK_CIRCLE); edit->markerDefine(BREAKPOINT_MARKER + 1, SC_MARK_BACKGROUND); } } void ShaderViewer::updateWindowTitle() { if(m_ShaderDetails) { QString shaderName = m_Ctx.GetResourceName(m_ShaderDetails->resourceId); // On D3D12, get the shader name from the pipeline rather than the shader itself // for the benefit of D3D12 which doesn't have separate shader objects if(m_Ctx.CurPipelineState().IsCaptureD3D12()) shaderName = QFormatStr("%1 %2") .arg(m_Ctx.GetResourceName(m_Pipeline)) .arg(m_Ctx.CurPipelineState().Abbrev(m_ShaderDetails->stage)); if(m_Trace) setWindowTitle(QFormatStr("Debugging %1 - %2").arg(shaderName).arg(m_DebugContext)); else setWindowTitle(shaderName); } } void ShaderViewer::gotoSourceDebugging() { if(m_CurInstructionScintilla) { ToolWindowManager::raiseToolWindow(m_CurInstructionScintilla); m_CurInstructionScintilla->setFocus(Qt::MouseFocusReason); } } void ShaderViewer::gotoDisassemblyDebugging() { ToolWindowManager::raiseToolWindow(m_DisassemblyFrame); m_DisassemblyFrame->setFocus(Qt::MouseFocusReason); } ShaderViewer *ShaderViewer::LoadEditor(ICaptureContext &ctx, QVariantMap data, IShaderViewer::SaveCallback saveCallback, IShaderViewer::CloseCallback closeCallback, ModifyCallback modifyCallback, QWidget *parent) { if(data.isEmpty()) return NULL; ResourceId id; { QVariant v = data[lit("id")]; RichResourceTextInitialise(v); id = v.value(); } ShaderStage stage = (ShaderStage)data[lit("stage")].toUInt(); rdcstr entryPoint = data[lit("entryPoint")].toString(); ShaderEncoding encoding = (ShaderEncoding)data[lit("encoding")].toUInt(); QString commandLine = data[lit("commandLine")].toString(); QString toolName = data[lit("tool")].toString(); ShaderCompileFlags flags; { QVariantMap v = data[lit("flags")].toMap(); for(const QString &str : v.keys()) flags.flags.push_back({(rdcstr)str, (rdcstr)v[str].toString()}); } rdcstrpairs files; { QVariantList v = data[lit("files")].toList(); for(QVariant f : v) { QVariantMap file = f.toMap(); files.push_back( {(rdcstr)file[lit("name")].toString(), (rdcstr)file[lit("contents")].toString()}); } } rdcstr errors; if((uint32_t)encoding >= (uint32_t)ShaderEncoding::Count) { errors += tr("Unrecognised shader encoding '%1'").arg(ToStr(encoding)); // with no other information let's guess HLSL as the most likely encoding = ShaderEncoding::HLSL; } ShaderViewer *view = EditShader(ctx, id, stage, entryPoint, files, encoding, flags, saveCallback, closeCallback, modifyCallback, parent); int toolIndex = -1; for(int i = 0; i < view->ui->compileTool->count(); i++) { QString tool = view->ui->compileTool->itemText(i); if(tool == toolName) { toolIndex = i; break; } } if(toolIndex == -1) { errors += tr("Unknown shader tool '%1'").arg(toolName); // let's pick the highest priority tool for the current encoding toolIndex = 0; } view->ui->encoding->setCurrentIndex(view->m_Encodings.indexOf(encoding)); view->ui->compileTool->setCurrentIndex(toolIndex); view->ui->entryFunc->setText(entryPoint); view->ui->toolCommandLine->setText(commandLine); return view; } QVariantMap ShaderViewer::SaveEditor() { QVariantMap ret; if(m_EditingShader != ResourceId()) { ret[lit("id")] = m_EditingShader; ret[lit("stage")] = (uint32_t)m_Stage; ret[lit("entryPoint")] = ui->entryFunc->text(); ret[lit("encoding")] = (uint32_t)currentEncoding(); ret[lit("commandLine")] = ui->toolCommandLine->toPlainText(); ret[lit("tool")] = ui->compileTool->currentText(); { QVariantMap v; for(const ShaderCompileFlag &flag : m_Flags.flags) v[flag.name] = QString(flag.value); ret[lit("flags")] = v; } { QVariantList v; for(ScintillaEdit *edit : m_Scintillas) { QWidget *w = (QWidget *)edit; QVariantMap f; f[lit("name")] = w->property("filename").toString(); f[lit("contents")] = QString::fromUtf8(edit->getText(edit->textLength() + 1)); v.push_back(f); } ret[lit("files")] = v; } } return ret; } ShaderViewer::~ShaderViewer() { delete m_FindResults; m_FindResults = NULL; // don't want to async invoke while using 'this', so save the trace separately ShaderDebugTrace *trace = m_Trace; // unregister any shortcuts on this window m_Ctx.GetMainWindow()->UnregisterShortcut(QString(), this); m_Ctx.Replay().AsyncInvoke([trace](IReplayController *r) { r->FreeTrace(trace); }); if(m_CloseCallback) m_CloseCallback(&m_Ctx, this, m_EditingShader); if(m_ModifyCallback) m_ModifyCallback(this, true); m_Ctx.RemoveCaptureViewer(this); delete ui; } void ShaderViewer::OnCaptureLoaded() { } void ShaderViewer::OnCaptureClosed() { ToolWindowManager::closeToolWindow(this); } void ShaderViewer::OnEventChanged(uint32_t eventId) { updateDebugState(); updateWindowTitle(); } ScintillaEdit *ShaderViewer::AddFileScintilla(const QString &name, const QString &text, ShaderEncoding encoding) { ScintillaEdit *scintilla = MakeEditor(lit("scintilla") + name, text, encoding == ShaderEncoding::HLSL ? SCLEX_HLSL : SCLEX_GLSL); scintilla->setReadOnly(true); scintilla->setWindowTitle(name); ((QWidget *)scintilla)->setProperty("name", name); QObject::connect(scintilla, &ScintillaEdit::keyPressed, this, &ShaderViewer::readonly_keyPressed); ToolWindowManager::AreaReference ref(ToolWindowManager::EmptySpace); if(!m_Scintillas.empty()) ref = ToolWindowManager::AreaReference(ToolWindowManager::AddTo, ui->docking->areaOf(m_Scintillas[0])); ui->docking->addToolWindow(scintilla, ref); ui->docking->setToolWindowProperties(scintilla, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow | ToolWindowManager::AlwaysDisplayFullTabs); m_Scintillas.push_back(scintilla); return scintilla; } ScintillaEdit *ShaderViewer::MakeEditor(const QString &name, const QString &text, int lang) { ScintillaEdit *ret = new ScintillaEdit(this); SetTextAndUpdateMargin0(ret, text); ret->setMarginLeft(4.0 * devicePixelRatioF()); ret->setMarginWidthN(1, 0); ret->setMarginWidthN(2, 16.0 * devicePixelRatioF()); ret->setObjectName(name); ret->styleSetFont(STYLE_DEFAULT, QFontDatabase::systemFont(QFontDatabase::FixedFont).family().toUtf8().data()); // C# DarkGreen ret->indicSetFore(INDICATOR_REGHIGHLIGHT, SCINTILLA_COLOUR(0, 100, 0)); ret->indicSetStyle(INDICATOR_REGHIGHLIGHT, INDIC_ROUNDBOX); // set up find result highlight style ret->indicSetFore(INDICATOR_FINDRESULT, SCINTILLA_COLOUR(200, 200, 127)); ret->indicSetStyle(INDICATOR_FINDRESULT, INDIC_FULLBOX); ret->indicSetAlpha(INDICATOR_FINDRESULT, 50); ret->indicSetOutlineAlpha(INDICATOR_FINDRESULT, 80); ConfigureSyntax(ret, lang); ret->setTabWidth(4); ret->setScrollWidth(1); ret->setScrollWidthTracking(true); ret->colourise(0, -1); ret->emptyUndoBuffer(); return ret; } void ShaderViewer::SetTextAndUpdateMargin0(ScintillaEdit *sc, const QString &text) { sc->setText(text.toUtf8().data()); sptr_t numlines = sc->lineCount(); int margin0width = 30; if(numlines > 1000) margin0width += 6; if(numlines > 10000) margin0width += 6; margin0width = int(margin0width * devicePixelRatioF()); sc->setMarginWidthN(0, margin0width); } void ShaderViewer::readonly_keyPressed(QKeyEvent *event) { if(event->key() == Qt::Key_F && (event->modifiers() & Qt::ControlModifier)) { m_FindReplace->setReplaceMode(false); on_findReplace_clicked(); } if(event->key() == Qt::Key_F3) { find((event->modifiers() & Qt::ShiftModifier) == 0); } } void ShaderViewer::editable_keyPressed(QKeyEvent *event) { if(event->key() == Qt::Key_H && (event->modifiers() & Qt::ControlModifier)) { m_FindReplace->setReplaceMode(true); on_findReplace_clicked(); } } void ShaderViewer::debug_contextMenu(const QPoint &pos) { ScintillaEdit *edit = qobject_cast(QObject::sender()); bool isDisasm = (edit == m_DisassemblyView); int scintillaPos = edit->positionFromPoint(pos.x(), pos.y()); QMenu contextMenu(this); QAction gotoOther(isDisasm ? tr("Go to Source") : tr("Go to Disassembly"), this); QObject::connect(&gotoOther, &QAction::triggered, [this, isDisasm]() { if(isDisasm) gotoSourceDebugging(); else gotoDisassemblyDebugging(); updateDebugState(); }); QAction intDisplay(tr("Integer register display"), this); QAction floatDisplay(tr("Float register display"), this); intDisplay.setCheckable(true); floatDisplay.setCheckable(true); intDisplay.setChecked(ui->intView->isChecked()); floatDisplay.setChecked(ui->floatView->isChecked()); QObject::connect(&intDisplay, &QAction::triggered, this, &ShaderViewer::on_intView_clicked); QObject::connect(&floatDisplay, &QAction::triggered, this, &ShaderViewer::on_floatView_clicked); if(isDisasm && m_CurInstructionScintilla == NULL) gotoOther.setEnabled(false); contextMenu.addAction(&gotoOther); contextMenu.addSeparator(); contextMenu.addAction(&intDisplay); contextMenu.addAction(&floatDisplay); contextMenu.addSeparator(); QAction addBreakpoint(tr("Toggle breakpoint here"), this); QAction runCursor(tr("Run to Cursor"), this); QObject::connect(&addBreakpoint, &QAction::triggered, [this, scintillaPos] { m_DisassemblyView->setSelection(scintillaPos, scintillaPos); ToggleBreakpoint(); }); QObject::connect(&runCursor, &QAction::triggered, [this, scintillaPos] { m_DisassemblyView->setSelection(scintillaPos, scintillaPos); runToCursor(); }); contextMenu.addAction(&addBreakpoint); contextMenu.addAction(&runCursor); contextMenu.addSeparator(); QAction copyText(tr("Copy"), this); QAction selectAll(tr("Select All"), this); copyText.setEnabled(!edit->selectionEmpty()); QObject::connect(©Text, &QAction::triggered, [edit] { edit->copyRange(edit->selectionStart(), edit->selectionEnd()); }); QObject::connect(&selectAll, &QAction::triggered, [edit] { edit->selectAll(); }); contextMenu.addAction(©Text); contextMenu.addAction(&selectAll); contextMenu.addSeparator(); RDDialog::show(&contextMenu, edit->viewport()->mapToGlobal(pos)); } void ShaderViewer::variables_contextMenu(const QPoint &pos) { QAbstractItemView *w = qobject_cast(QObject::sender()); QMenu contextMenu(this); QAction copyValue(tr("Copy"), this); QAction addWatch(tr("Add Watch"), this); QAction deleteWatch(tr("Delete Watch"), this); QAction clearAll(tr("Clear All"), this); contextMenu.addAction(©Value); contextMenu.addSeparator(); contextMenu.addAction(&addWatch); if(QObject::sender() == ui->watch) { QObject::connect(©Value, &QAction::triggered, [this] { ui->watch->copySelection(); }); contextMenu.addAction(&deleteWatch); contextMenu.addSeparator(); contextMenu.addAction(&clearAll); // start with no row selected int selRow = -1; QList items = ui->watch->selectedItems(); for(QTableWidgetItem *item : items) { // if no row is selected, or the same as this item, set selected row to this item's if(selRow == -1 || selRow == item->row()) { selRow = item->row(); } else { // we only get here if we see an item on a different row selected - that means too many rows // so bail out selRow = -1; break; } } // if we have a selected row that isn't the last one, we can add/delete this item deleteWatch.setEnabled(selRow >= 0 && selRow < ui->watch->rowCount() - 1); addWatch.setEnabled(selRow >= 0 && selRow < ui->watch->rowCount() - 1); QObject::connect(&addWatch, &QAction::triggered, [this, selRow] { QTableWidgetItem *item = ui->watch->item(selRow, 0); if(item) AddWatch(item->text()); }); QObject::connect(&deleteWatch, &QAction::triggered, [this, selRow] { ui->watch->removeRow(selRow); }); QObject::connect(&clearAll, &QAction::triggered, [this] { while(ui->watch->rowCount() > 1) ui->watch->removeRow(0); }); } else { RDTreeWidget *tree = qobject_cast(w); QObject::connect(©Value, &QAction::triggered, [tree] { tree->copySelection(); }); addWatch.setEnabled(tree->selectedItem() != NULL); QObject::connect(&addWatch, &QAction::triggered, [this, tree] { if(tree == ui->sourceVars) AddWatch(tree->selectedItem()->tag().value().debugVar.name); else AddWatch(tree->selectedItem()->text(0)); }); } RDDialog::show(&contextMenu, w->viewport()->mapToGlobal(pos)); } void ShaderViewer::accessedResources_contextMenu(const QPoint &pos) { QAbstractItemView *w = qobject_cast(QObject::sender()); RDTreeWidget *tree = qobject_cast(w); if(tree->selectedItem() == NULL) return; const AccessedResourceTag &tag = tree->selectedItem()->tag().value(); if(tag.type == VarType::Unknown) { // Right clicked on an instruction row QMenu contextMenu(this); QAction gotoInstr(tr("Go to Step"), this); contextMenu.addAction(&gotoInstr); QObject::connect(&gotoInstr, &QAction::triggered, [this, tag] { bool forward = (tag.step >= m_CurrentStateIdx); runTo({tag.step}, forward); }); RDDialog::show(&contextMenu, w->viewport()->mapToGlobal(pos)); } else { // Right clicked on a resource row QMenu contextMenu(this); QAction prevAccess(tr("Run to Previous Access"), this); QAction nextAccess(tr("Run to Next Access"), this); contextMenu.addAction(&prevAccess); contextMenu.addAction(&nextAccess); QObject::connect(&prevAccess, &QAction::triggered, [this, tag] { runToResourceAccess(false, tag.type, tag.bind); }); QObject::connect(&nextAccess, &QAction::triggered, [this, tag] { runToResourceAccess(true, tag.type, tag.bind); }); RDDialog::show(&contextMenu, w->viewport()->mapToGlobal(pos)); } } void ShaderViewer::disassembly_buttonReleased(QMouseEvent *event) { if(event->button() == Qt::LeftButton) { sptr_t scintillaPos = m_DisassemblyView->positionFromPoint(event->x(), event->y()); sptr_t start = m_DisassemblyView->wordStartPosition(scintillaPos, true); sptr_t end = m_DisassemblyView->wordEndPosition(scintillaPos, true); QString text = QString::fromUtf8(m_DisassemblyView->textRange(start, end)); QRegularExpression regexp(lit("^[xyzwrgba]+$")); // if we match a swizzle look before that for the variable if(regexp.match(text).hasMatch()) { start--; while(isspace(m_DisassemblyView->charAt(start))) start--; if(m_DisassemblyView->charAt(start) == '.') { end = m_DisassemblyView->wordEndPosition(start - 1, true); start = m_DisassemblyView->wordStartPosition(start - 1, true); text = QString::fromUtf8(m_DisassemblyView->textRange(start, end)); } } if(!text.isEmpty()) { if(findVar(text)) { start = 0; end = m_DisassemblyView->length(); QColor highlightColor = QColor::fromHslF( 0.333f, 1.0f, qBound(0.25, palette().color(QPalette::Base).lightnessF(), 0.85)); highlightMatchingVars(ui->debugVars->invisibleRootItem(), text, highlightColor); highlightMatchingVars(ui->constants->invisibleRootItem(), text, highlightColor); highlightMatchingVars(ui->accessedResources->invisibleRootItem(), text, highlightColor); highlightMatchingVars(ui->sourceVars->invisibleRootItem(), text, highlightColor); m_DisassemblyView->setIndicatorCurrent(INDICATOR_REGHIGHLIGHT); m_DisassemblyView->indicatorClearRange(start, end); sptr_t flags = SCFIND_MATCHCASE | SCFIND_WHOLEWORD | SCFIND_REGEXP | SCFIND_POSIX; text += lit("\\.[xyzwrgba]+"); QByteArray findUtf8 = text.toUtf8(); QPair result; do { result = m_DisassemblyView->findText(flags, findUtf8.data(), start, end); if(result.first >= 0) m_DisassemblyView->indicatorFillRange(result.first, result.second - result.first); start = result.second; } while(result.first >= 0); } } } } void ShaderViewer::disassemble_typeChanged(int index) { if(m_ShaderDetails == NULL) return; QString targetStr = m_DisassemblyType->currentText(); QByteArray target = targetStr.toUtf8(); for(const ShaderProcessingTool &disasm : m_Ctx.Config().ShaderProcessors) { if(targetStr == targetName(disasm)) { ShaderToolOutput out = disasm.DisassembleShader(this, m_ShaderDetails, ""); rdcstr text; if(out.result.isEmpty()) text = out.log; else text.assign((const char *)out.result.data(), out.result.size()); m_DisassemblyView->setReadOnly(false); SetTextAndUpdateMargin0(m_DisassemblyView, text); m_DisassemblyView->setReadOnly(true); m_DisassemblyView->emptyUndoBuffer(); return; } } if(targetStr == tr("More disassembly formats...")) { QString text; text = tr("; More disassembly formats are available with a pipeline. This shader view is not\n" "; associated with any specific pipeline and shows only the shader itself.\n\n" "; Viewing the shader from the pipeline state view with a pipeline bound will expose\n" "; these other formats:\n\n"); for(const rdcstr &t : m_PipelineTargets) text += QFormatStr("%1\n").arg(QString(t)); m_DisassemblyView->setReadOnly(false); SetTextAndUpdateMargin0(m_DisassemblyView, text); m_DisassemblyView->setReadOnly(true); m_DisassemblyView->emptyUndoBuffer(); return; } QPointer me(this); m_Ctx.Replay().AsyncInvoke([me, this, target](IReplayController *r) { if(!me) return; rdcstr disasm = r->DisassembleShader(m_Pipeline, m_ShaderDetails, target.data()); if(!me) return; GUIInvoke::call(this, [this, disasm]() { m_DisassemblyView->setReadOnly(false); SetTextAndUpdateMargin0(m_DisassemblyView, disasm); m_DisassemblyView->setReadOnly(true); m_DisassemblyView->emptyUndoBuffer(); }); }); } void ShaderViewer::watch_keyPress(QKeyEvent *event) { if(event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) { QList items = ui->watch->selectedItems(); if(!items.isEmpty() && items.back()->row() < ui->watch->rowCount() - 1) ui->watch->removeRow(items.back()->row()); } } void ShaderViewer::on_watch_itemChanged(QTableWidgetItem *item) { // ignore changes to the type/value columns. Only look at name changes, which must be by the user if(item->column() != 0) return; static bool recurse = false; if(recurse) return; recurse = true; // if the item is now empty, remove it if(item->text().isEmpty()) ui->watch->removeRow(item->row()); // ensure we have a trailing row for adding new watch items. if(ui->watch->rowCount() == 0 || ui->watch->item(ui->watch->rowCount() - 1, 0) == NULL || !ui->watch->item(ui->watch->rowCount() - 1, 0)->text().isEmpty()) { // add a new row if needed if(ui->watch->rowCount() == 0 || ui->watch->item(ui->watch->rowCount() - 1, 0) != NULL) ui->watch->insertRow(ui->watch->rowCount()); for(int i = 0; i < ui->watch->columnCount(); i++) { QTableWidgetItem *newItem = new QTableWidgetItem(); if(i > 0) newItem->setFlags(newItem->flags() & ~Qt::ItemIsEditable); ui->watch->setItem(ui->watch->rowCount() - 1, i, newItem); } } ui->watch->resizeRowsToContents(); recurse = false; updateDebugState(); } bool ShaderViewer::stepBack() { if(!m_Trace || m_States.empty()) return false; if(IsFirstState()) return false; if(isSourceDebugging()) { LineColumnInfo oldLine = m_Trace->lineInfo[GetCurrentState().nextInstruction]; // first step to the next instruction in a backwards direction that's on a different line from // the current one do { applyBackwardsChange(); if(m_Breakpoints.contains((int)GetCurrentState().nextInstruction)) break; if(IsFirstState()) break; if(m_Trace->lineInfo[GetCurrentState().nextInstruction].SourceEqual(oldLine)) continue; break; } while(true); oldLine = m_Trace->lineInfo[GetCurrentState().nextInstruction]; // now since a line can have multiple instructions, keep stepping (looking forward) until we // reach the first instruction with an identical line info while(!IsFirstState() && m_Trace->lineInfo[GetPreviousState().nextInstruction].SourceEqual(oldLine)) { applyBackwardsChange(); if(m_Breakpoints.contains((int)GetCurrentState().nextInstruction)) break; } updateDebugState(); } else { applyBackwardsChange(); updateDebugState(); } return true; } bool ShaderViewer::stepNext() { if(!m_Trace || m_States.empty()) return false; if(IsLastState()) return false; if(isSourceDebugging()) { LineColumnInfo oldLine = m_Trace->lineInfo[GetCurrentState().nextInstruction]; do { applyForwardsChange(); if(m_Breakpoints.contains((int)GetCurrentState().nextInstruction)) break; if(IsLastState()) break; if(m_Trace->lineInfo[GetCurrentState().nextInstruction].SourceEqual(oldLine)) continue; break; } while(true); updateDebugState(); } else { applyForwardsChange(); updateDebugState(); } return true; } void ShaderViewer::runToCursor() { if(!m_Trace || m_States.empty()) return; ScintillaEdit *cur = currentScintilla(); if(cur != m_DisassemblyView) { int scintillaIndex = m_FileScintillas.indexOf(cur); if(scintillaIndex < 0) return; sptr_t i = cur->lineFromPosition(cur->currentPos()) + 1; QMap> &fileMap = m_Line2Insts[scintillaIndex]; // find the next line that maps to an instruction for(; i < cur->lineCount(); i++) { if(fileMap.contains(i)) { runTo(fileMap[i], true); return; } } // if we didn't find one, just run run(); } else { sptr_t i = m_DisassemblyView->lineFromPosition(m_DisassemblyView->currentPos()); for(; i < m_DisassemblyView->lineCount(); i++) { int line = instructionForDisassemblyLine(i); if(line >= 0) { runTo({(size_t)line}, true); break; } } } } int ShaderViewer::instructionForDisassemblyLine(sptr_t line) { // go from scintilla's lines (0-based) to ours (1-based) line++; if(line < m_AsmLine2Inst.size()) return m_AsmLine2Inst[line]; return -1; } bool ShaderViewer::IsFirstState() const { return m_CurrentStateIdx == 0; } bool ShaderViewer::IsLastState() const { return m_CurrentStateIdx == m_States.size() - 1; } const ShaderDebugState &ShaderViewer::GetPreviousState() const { if(m_CurrentStateIdx > 0) return m_States[m_CurrentStateIdx - 1]; return m_States.front(); } const ShaderDebugState &ShaderViewer::GetCurrentState() const { if(m_CurrentStateIdx < m_States.size()) return m_States[m_CurrentStateIdx]; return m_States.back(); } const ShaderDebugState &ShaderViewer::GetNextState() const { if(m_CurrentStateIdx + 1 < m_States.size()) return m_States[m_CurrentStateIdx + 1]; return m_States.back(); } void ShaderViewer::runToSample() { runTo({}, true, ShaderEvents::SampleLoadGather); } void ShaderViewer::runToNanOrInf() { runTo({}, true, ShaderEvents::GeneratedNanOrInf); } void ShaderViewer::runBack() { runTo({}, false); } void ShaderViewer::run() { runTo({}, true); } void ShaderViewer::runTo(QVector runToInstruction, bool forward, ShaderEvents condition) { if(!m_Trace || m_States.empty()) return; bool firstStep = true; // this is effectively infinite as we break out before moving to next/previous state if that would // be first/last while((forward && !IsLastState()) || (!forward && !IsFirstState())) { // break immediately even on the very first step if it's the one we want to go to if(runToInstruction.contains(GetCurrentState().nextInstruction)) break; // after the first step, break on condition if(!firstStep && (GetCurrentState().flags & condition)) break; // or breakpoint if(!firstStep && m_Breakpoints.contains((int)GetCurrentState().nextInstruction)) break; firstStep = false; if(forward) { if(IsLastState()) break; applyForwardsChange(); } else { if(IsFirstState()) break; applyBackwardsChange(); } } updateDebugState(); } void ShaderViewer::runToResourceAccess(bool forward, VarType type, const BindpointIndex &resource) { if(!m_Trace || m_States.empty()) return; // this is effectively infinite as we break out before moving to next/previous state if that would // be first/last while((forward && !IsLastState()) || (!forward && !IsFirstState())) { if(forward) { if(IsLastState()) break; applyForwardsChange(); } else { if(IsFirstState()) break; applyBackwardsChange(); } // Break if the current state references the specific resource requested bool foundResource = false; for(const ShaderVariableChange &c : GetCurrentState().changes) { if(c.after.type == type && c.after.GetBinding() == resource) { foundResource = true; break; } } if(foundResource) break; // or breakpoint if(m_Breakpoints.contains((int)GetCurrentState().nextInstruction)) break; } updateDebugState(); } void ShaderViewer::applyBackwardsChange() { if(!IsFirstState()) { rdcarray newVariables; for(const ShaderVariableChange &c : GetCurrentState().changes) { // if the before name is empty, this is a variable that came into scope/was created if(c.before.name.empty()) { // delete the matching variable (should only be one) for(size_t i = 0; i < m_Variables.size(); i++) { if(c.after.name == m_Variables[i].name) { m_Variables.erase(i); break; } } } else { ShaderVariable *v = NULL; for(size_t i = 0; i < m_Variables.size(); i++) { if(c.before.name == m_Variables[i].name) { v = &m_Variables[i]; break; } } if(v) *v = c.before; else newVariables.push_back(c.before); } } m_Variables.insert(0, newVariables); m_CurrentStateIdx--; } } void ShaderViewer::applyForwardsChange() { if(!IsLastState()) { m_CurrentStateIdx++; rdcarray newVariables; rdcarray newAccessedResources; for(const ShaderVariableChange &c : GetCurrentState().changes) { // if the after name is empty, this is a variable going out of scope/being deleted if(c.after.name.empty()) { // delete the matching variable (should only be one) for(size_t i = 0; i < m_Variables.size(); i++) { if(c.before.name == m_Variables[i].name) { m_Variables.erase(i); break; } } } else { ShaderVariable *v = NULL; for(size_t i = 0; i < m_Variables.size(); i++) { if(c.after.name == m_Variables[i].name) { v = &m_Variables[i]; break; } } if(v) *v = c.after; else newVariables.push_back(c.after); if(c.after.type == VarType::ReadOnlyResource || c.after.type == VarType::ReadWriteResource) { bool found = false; for(size_t i = 0; i < m_AccessedResources.size(); i++) { if(c.after.GetBinding() == m_AccessedResources[i].resource.GetBinding()) { found = true; if(m_AccessedResources[i].steps.indexOf(m_CurrentStateIdx) < 0) m_AccessedResources[i].steps.push_back(m_CurrentStateIdx); break; } } if(!found) newAccessedResources.push_back({c.after, {m_CurrentStateIdx}}); } } } m_Variables.insert(0, newVariables); m_AccessedResources.insert(0, newAccessedResources); } } QString ShaderViewer::stringRep(const ShaderVariable &var, uint32_t row) { VarType type = var.type; if(type == VarType::Unknown) type = ui->intView->isChecked() ? VarType::SInt : VarType::Float; if(type == VarType::ReadOnlyResource || type == VarType::ReadWriteResource || type == VarType::Sampler) { BindpointIndex varBind = var.GetBinding(); rdcarray resList; if(type == VarType::ReadOnlyResource) resList = m_ReadOnlyResources; else if(type == VarType::ReadWriteResource) resList = m_ReadWriteResources; else if(type == VarType::Sampler) resList = m_Ctx.CurPipelineState().GetSamplers(m_Stage); int32_t bindIdx = resList.indexOf(Bindpoint(varBind)); if(bindIdx < 0) return QString(); BoundResourceArray res = resList[bindIdx]; if(varBind.arrayIndex >= res.resources.size()) return QString(); if(type == VarType::Sampler) return samplerRep(varBind, varBind.arrayIndex, res.resources[varBind.arrayIndex].resourceId); return ToQStr(res.resources[varBind.arrayIndex].resourceId); } return RowString(var, row, type); } QString ShaderViewer::samplerRep(Bindpoint bind, uint32_t arrayIndex, ResourceId id) { if(id == ResourceId()) { QString contents; if(bind.bindset > 0) { // a bit ugly to do an API-specific switch here but we don't have a better way to refer // by binding contents = IsD3D(m_Ctx.APIProps().pipelineType) ? tr("space%1, ") : tr("Set %1, "); contents = contents.arg(bind.bindset); } if(arrayIndex == ~0U) contents += QString::number(bind.bind); else contents += QFormatStr("%1[%2]").arg(bind.bind).arg(arrayIndex); return contents; } else { return ToQStr(id); } } QString ShaderViewer::targetName(const ShaderProcessingTool &disasm) { return lit("%1 (%2)").arg(ToQStr(disasm.output)).arg(disasm.name); } void ShaderViewer::addFileList() { QListWidget *list = new QListWidget(this); list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); list->setSelectionMode(QAbstractItemView::SingleSelection); QObject::connect(list, &QListWidget::currentRowChanged, [this](int idx) { QWidget *raiseWidget = m_Scintillas[idx]; if(m_Scintillas[idx] == m_DisassemblyView) raiseWidget = m_DisassemblyFrame; ToolWindowManager::raiseToolWindow(raiseWidget); }); list->setWindowTitle(tr("File List")); for(ScintillaEdit *s : m_Scintillas) { if(s == m_DisassemblyView) list->addItem(m_DisassemblyFrame->windowTitle()); else list->addItem(s->windowTitle()); } ui->docking->addToolWindow( list, ToolWindowManager::AreaReference(ToolWindowManager::LeftOf, ui->docking->areaOf(m_Scintillas.front()), 0.2f)); ui->docking->setToolWindowProperties( list, ToolWindowManager::HideCloseButton | ToolWindowManager::DisallowFloatWindow); } void ShaderViewer::combineStructures(RDTreeWidgetItem *root, int skipPrefixLength) { RDTreeWidgetItem temp; // we perform a filter moving from root to temp. At each point we check the node: // * if the node has no struct or array prefix, it gets moved // * if the node does have a prefix, we sweep finding all matching elements with the same prefix, // strip the prefix off them and make a combined node, then recurse to combine anything // underneath. We aren't greedy in picking prefixes so this should generate a struct/array tree. // * in the event that a node has no matching elements we move it across as if it had no prefix. // * we iterate from last to first, because when combining elements that may be spread out in the // list of children, we want to combine up to the position of the last item, not the position of // the first. for(int c = root->childCount() - 1; c >= 0;) { RDTreeWidgetItem *child = root->takeChild(c); c--; QString name = child->text(0); int dotIndex = name.indexOf(QLatin1Char('.'), skipPrefixLength); int arrIndex = name.indexOf(QLatin1Char('['), skipPrefixLength); // if this node doesn't have any segments, just move it across. if(dotIndex < 0 && arrIndex < 0) { temp.insertChild(0, child); continue; } // store the index of the first separator int sepIndex = dotIndex; bool isLeafArray = (sepIndex == -1); if(sepIndex == -1 || (arrIndex > 0 && arrIndex < sepIndex)) sepIndex = arrIndex; // we have a valid node to match against, record the prefix (including separator character) QString prefix = name.mid(0, sepIndex + 1); QVector matches = {child}; // iterate down from the next item for(int n = c; n >= 0; n--) { RDTreeWidgetItem *testNode = root->child(n); QString testName = testNode->text(0); QString testprefix = testName.mid(0, sepIndex + 1); // no match - continue if(testprefix != prefix) continue; // match, take this child matches.push_back(root->takeChild(n)); // also decrement c since we're taking a child ahead of where that loop will go. c--; } // Sort the children by offset, then global source var index, then by text. // Using the global source var index allows resource arrays to be presented in index order // rather than by name, so for example arr[2] comes before arr[10] std::sort(matches.begin(), matches.end(), [](const RDTreeWidgetItem *a, const RDTreeWidgetItem *b) { VariableTag at = a->tag().value(); VariableTag bt = b->tag().value(); if(at.offset != bt.offset) return at.offset < bt.offset; if(at.globalSourceVar != bt.globalSourceVar) return at.globalSourceVar < bt.globalSourceVar; return a->text(0) < b->text(0); }); // create a new parent with just the prefix prefix.chop(1); QVariantList values = {prefix}; for(int i = 1; i < child->dataCount(); i++) values.push_back(QVariant()); RDTreeWidgetItem *parent = new RDTreeWidgetItem(values); // add all the children (stripping the prefix from their name) for(RDTreeWidgetItem *item : matches) { if(sepIndex == dotIndex) item->setText(0, item->text(0).mid(sepIndex + 1)); parent->addChild(item); if(item->background().color().isValid()) parent->setBackground(item->background()); if(item->foreground().color().isValid()) parent->setForeground(item->foreground()); } // recurse and combine members of this object if a struct if(!isLeafArray) { if(sepIndex != dotIndex) combineStructures(parent, sepIndex + 1); else combineStructures(parent); } // now add to the list temp.insertChild(0, parent); } if(root->childCount() > 0) qCritical() << "Some objects left on root!"; // move all the children back from the temp object into the parameter while(temp.childCount() > 0) root->addChild(temp.takeChild(0)); } RDTreeWidgetItem *ShaderViewer::findVarInTree(RDTreeWidgetItem *root, QString name, bool fullmatch, int maxDepth) { if(fullmatch) { if(root->tag().value().debugVar.name == rdcstr(name)) return root; } else { if(root->dataCount() > 0 && root->text(0) == name) return root; for(int i = 0; i < root->childCount(); i++) { RDTreeWidgetItem *child = root->child(i); if(child->dataCount() > 0 && child->text(0) == name) return child; } maxDepth--; if(maxDepth <= 0) return NULL; } for(int i = 0; i < root->childCount(); i++) { RDTreeWidgetItem *ret = findVarInTree(root->child(i), name, fullmatch, maxDepth); if(ret) return ret; } return NULL; } bool ShaderViewer::findVar(QString name, ShaderVariable *var) { if(!m_Trace || m_States.empty()) return false; // try source mapped variables first, as if we have ambiguity (a source variable the same as a // debug variable) we'll pick the source variable as 'more desirable' RDTreeWidgetItem *item = findVarInTree(ui->sourceVars->invisibleRootItem(), name, true, -1); // next try constants, which also contains some source mapping if(!item) item = findVarInTree(ui->constants->invisibleRootItem(), name, true, -1); // finally try debug variables if(!item) item = findVarInTree(ui->debugVars->invisibleRootItem(), name, true, -1); // if we didn't find anything, try a non-full match. This will search just on the member name // and might pick up some false positives. We search top-level items (which is equivalent to the // match above so redundant) and their children only, not any further. The idea is to catch // anything under implicit global scopes which don't match the source, e.g. constant buffer names // or struct names which are implicit. if(!item) item = findVarInTree(ui->sourceVars->invisibleRootItem(), name, false, 2); if(!item) item = findVarInTree(ui->constants->invisibleRootItem(), name, false, 2); if(!item) item = findVarInTree(ui->debugVars->invisibleRootItem(), name, false, 2); if(!item) return false; return getVar(item, var, NULL); } bool ShaderViewer::getVar(RDTreeWidgetItem *item, ShaderVariable *var, QString *regNames) { VariableTag tag = item->tag().value(); // if the tag is invalid, it's not a proper match if(tag.globalSourceVar < 0 && tag.localSourceVar < 0 && tag.debugVar.type == DebugVariableType::Undefined) return false; // don't find resource variables if(tag.debugVar.type == DebugVariableType::Sampler || tag.debugVar.type == DebugVariableType::ReadOnlyResource || tag.debugVar.type == DebugVariableType::ReadWriteResource) return false; // if we have a debug var tag then it's easy-mode if(tag.debugVar.type != DebugVariableType::Undefined) { // found a match. If we don't want the variable contents, just return true now if(!var) return true; const ShaderVariable *reg = GetDebugVariable(tag.debugVar); if(reg) { *var = *reg; var->name = tag.debugVar.name; if(regNames) *regNames = reg->name; } else { qCritical() << "Couldn't find expected debug variable!" << ToQStr(tag.debugVar.type) << QString(tag.debugVar.name) << tag.debugVar.component; return false; } return true; } else { SourceVariableMapping mapping; if(tag.globalSourceVar >= 0 && tag.globalSourceVar < m_Trace->sourceVars.count()) mapping = m_Trace->sourceVars[tag.globalSourceVar]; else if(tag.localSourceVar >= 0 && tag.localSourceVar < GetCurrentState().sourceVars.count()) mapping = GetCurrentState().sourceVars[tag.localSourceVar]; else qCritical() << "Couldn't find expected source variable!" << tag.globalSourceVar << tag.localSourceVar; if(mapping.variables.empty()) return false; { // don't find resource variables if(mapping.variables[0].type == DebugVariableType::Sampler || mapping.variables[0].type == DebugVariableType::ReadOnlyResource || mapping.variables[0].type == DebugVariableType::ReadWriteResource) return false; // found a match. If we don't want the variable contents, just return true now if(!var) return true; ShaderVariable &ret = *var; ret.name = tag.debugVar.name; ret.rowMajor = true; ret.rows = mapping.rows; ret.columns = mapping.columns; ret.type = mapping.type; const QString xyzw = lit("xyzw"); for(uint32_t i = 0; i < mapping.variables.size(); i++) { const DebugVariableReference &r = mapping.variables[i]; const ShaderVariable *reg = GetDebugVariable(r); if(regNames && !regNames->isEmpty()) *regNames += lit(", "); if(reg) { if(regNames) { // if the previous register was the same, just append our component if(i > 0 && r.type == mapping.variables[i - 1].type && r.name == mapping.variables[i - 1].name && (r.component / reg->columns) == (mapping.variables[i - 1].component / reg->columns)) { // remove the auto-appended ", " - there must be one because this isn't the first // register regNames->chop(2); *regNames += xyzw[r.component % 4]; } else { if(reg->rows > 1) *regNames += QFormatStr("%1.row%2.%3") .arg(reg->name) .arg(r.component / 4) .arg(xyzw[r.component % 4]); else *regNames += QFormatStr("%1.%2").arg(reg->name).arg(xyzw[r.component % 4]); } } if(mapping.type == VarType::Double || mapping.type == VarType::ULong) ret.value.u64v[i] = reg->value.u64v[r.component]; else ret.value.uv[i] = reg->value.uv[r.component]; } else { if(regNames) *regNames += lit("-"); } } return true; } return false; } } void ShaderViewer::setEditorWindowTitle() { if(m_EditingShader != ResourceId()) { if(!m_Ctx.IsResourceReplaced(m_EditingShader)) m_Modified = true; if(m_Modified) { QString title = windowTitle(); if(title[0] != QLatin1Char('*')) title.prepend(lit("* ")); setWindowTitle(title); } else { QString title = windowTitle(); if(title[0] == QLatin1Char('*')) title.remove(0, 2); setWindowTitle(title); } } } void ShaderViewer::highlightMatchingVars(RDTreeWidgetItem *root, const QString varName, const QColor highlightColor) { for(int i = 0; i < root->childCount(); i++) { RDTreeWidgetItem *item = root->child(i); if(item->tag().value().debugVar.name == rdcstr(varName)) item->setBackgroundColor(highlightColor); else item->setBackground(QBrush()); highlightMatchingVars(item, varName, highlightColor); } } void ShaderViewer::updateAccessedResources() { RDTreeViewExpansionState expansion; ui->accessedResources->saveExpansion(expansion, 0); ui->accessedResources->beginUpdate(); ui->accessedResources->clear(); switch(m_AccessedResourceView) { case AccessedResourceView::SortByResource: { for(size_t i = 0; i < m_AccessedResources.size(); ++i) { // Check if the resource was accessed prior to this step bool accessed = false; for(size_t j = 0; j < m_AccessedResources[i].steps.size(); ++j) { if(m_AccessedResources[i].steps[j] <= m_CurrentStateIdx) { accessed = true; break; } } if(!accessed) continue; bool modified = false; for(const ShaderVariableChange &c : GetCurrentState().changes) { if(c.before.name == m_AccessedResources[i].resource.name || c.after.name == m_AccessedResources[i].resource.name) { modified = true; break; } } RDTreeWidgetItem *resourceNode = makeAccessedResourceNode(m_AccessedResources[i].resource, modified); if(resourceNode) { // Add a child for each step that it was accessed for(size_t j = 0; j < m_AccessedResources[i].steps.size(); ++j) { accessed = m_AccessedResources[i].steps[j] <= m_CurrentStateIdx; if(accessed) { RDTreeWidgetItem *stepNode = new RDTreeWidgetItem( {tr("Step %1").arg(m_AccessedResources[i].steps[j]), lit("Access"), lit("")}); stepNode->setTag(QVariant::fromValue( AccessedResourceTag((uint32_t)m_AccessedResources[i].steps[j]))); if(m_CurrentStateIdx == m_AccessedResources[i].steps[j]) stepNode->setForegroundColor(QColor(Qt::red)); resourceNode->addChild(stepNode); } } ui->accessedResources->addTopLevelItem(resourceNode); } } break; } case AccessedResourceView::SortByStep: { rdcarray> stepNodes; for(size_t i = 0; i < m_AccessedResources.size(); ++i) { bool modified = false; for(const ShaderVariableChange &c : GetCurrentState().changes) { if(c.before.name == m_AccessedResources[i].resource.name || c.after.name == m_AccessedResources[i].resource.name) { modified = true; break; } } // Add a root node for each instruction, and place the resource node as a child for(size_t j = 0; j < m_AccessedResources[i].steps.size(); ++j) { bool accessed = m_AccessedResources[i].steps[j] <= m_CurrentStateIdx; if(accessed) { int32_t nodeIdx = -1; for(int32_t k = 0; k < stepNodes.count(); ++k) { if(stepNodes[k].first == m_AccessedResources[i].steps[j]) { nodeIdx = k; break; } } RDTreeWidgetItem *resourceNode = makeAccessedResourceNode(m_AccessedResources[i].resource, modified); if(nodeIdx == -1) { RDTreeWidgetItem *stepNode = new RDTreeWidgetItem( {tr("Step %1").arg(m_AccessedResources[i].steps[j]), lit("Access"), lit("")}); stepNode->setTag(QVariant::fromValue( AccessedResourceTag((uint32_t)m_AccessedResources[i].steps[j]))); if(m_CurrentStateIdx == m_AccessedResources[i].steps[j]) stepNode->setForegroundColor(QColor(Qt::red)); stepNode->addChild(resourceNode); stepNodes.push_back({m_AccessedResources[i].steps[j], stepNode}); } else { stepNodes[nodeIdx].second->addChild(resourceNode); } } } } std::sort(stepNodes.begin(), stepNodes.end(), [](const rdcpair &a, const rdcpair &b) { return a.first < b.first; }); for(size_t i = 0; i < stepNodes.size(); ++i) ui->accessedResources->addTopLevelItem(stepNodes[i].second); break; } } ui->accessedResources->endUpdate(); ui->accessedResources->applyExpansion(expansion, 0); } void ShaderViewer::updateDebugState() { if(!m_Trace || m_States.empty()) return; if(ui->debugToggle->isEnabled()) { if(isSourceDebugging()) ui->debugToggle->setText(tr("Debug in Assembly")); else ui->debugToggle->setText(tr("Debug in Source")); } const ShaderDebugState &state = GetCurrentState(); const bool done = IsLastState(); // add current instruction marker m_DisassemblyView->markerDeleteAll(CURRENT_MARKER); m_DisassemblyView->markerDeleteAll(CURRENT_MARKER + 1); m_DisassemblyView->markerDeleteAll(FINISHED_MARKER); m_DisassemblyView->markerDeleteAll(FINISHED_MARKER + 1); if(m_CurInstructionScintilla) { m_CurInstructionScintilla->markerDeleteAll(CURRENT_MARKER); m_CurInstructionScintilla->markerDeleteAll(CURRENT_MARKER + 1); m_CurInstructionScintilla->markerDeleteAll(FINISHED_MARKER); m_CurInstructionScintilla->markerDeleteAll(FINISHED_MARKER + 1); m_CurInstructionScintilla->indicatorClearRange(0, m_CurInstructionScintilla->length()); m_CurInstructionScintilla = NULL; } ui->callstack->clear(); for(const rdcstr &s : state.callstack) ui->callstack->insertItem(0, s); if(state.nextInstruction < m_Trace->lineInfo.size()) { LineColumnInfo &lineInfo = m_Trace->lineInfo[state.nextInstruction]; // highlight the current line { m_DisassemblyView->markerAdd(lineInfo.disassemblyLine - 1, done ? FINISHED_MARKER : CURRENT_MARKER); m_DisassemblyView->markerAdd(lineInfo.disassemblyLine - 1, done ? FINISHED_MARKER + 1 : CURRENT_MARKER + 1); int pos = m_DisassemblyView->positionFromLine(lineInfo.disassemblyLine - 1); m_DisassemblyView->setSelection(pos, pos); ensureLineScrolled(m_DisassemblyView, lineInfo.disassemblyLine - 1); } if(lineInfo.fileIndex >= 0 && lineInfo.fileIndex < m_FileScintillas.count()) { m_CurInstructionScintilla = m_FileScintillas[lineInfo.fileIndex]; if(m_CurInstructionScintilla) { for(sptr_t line = lineInfo.lineStart; line <= (sptr_t)lineInfo.lineEnd; line++) { if(line == (sptr_t)lineInfo.lineEnd) m_CurInstructionScintilla->markerAdd(line - 1, done ? FINISHED_MARKER : CURRENT_MARKER); if(lineInfo.colStart == 0) { // with no column info, add a marker on the whole line m_CurInstructionScintilla->markerAdd(line - 1, done ? FINISHED_MARKER + 1 : CURRENT_MARKER + 1); } else { // otherwise add an indicator on the column range. // Start from the full position/length for this line sptr_t pos = m_CurInstructionScintilla->positionFromLine(line - 1); sptr_t len = m_CurInstructionScintilla->lineEndPosition(line - 1) - pos; // if we're on the last line of the range, restrict the length to end on the last column if(line == (sptr_t)lineInfo.lineEnd && lineInfo.colEnd != 0) len = lineInfo.colEnd; // if we're on the start of the range (which may also be the last line above too), shift // inwards towards the first column if(line == (sptr_t)lineInfo.lineStart) { pos += lineInfo.colStart - 1; len -= lineInfo.colStart - 1; } m_CurInstructionScintilla->setIndicatorCurrent(done ? FINISHED_INDICATOR : CURRENT_INDICATOR); m_CurInstructionScintilla->indicatorFillRange(pos, len); } } if(isSourceDebugging() || ui->docking->areaOf(m_CurInstructionScintilla) != ui->docking->areaOf(m_DisassemblyFrame)) ToolWindowManager::raiseToolWindow(m_CurInstructionScintilla); int pos = m_CurInstructionScintilla->positionFromLine(lineInfo.lineStart - 1); m_CurInstructionScintilla->setSelection(pos, pos); ensureLineScrolled(m_CurInstructionScintilla, lineInfo.lineStart - 1); } } } if(ui->constants->topLevelItemCount() == 0) { // track all debug variables that have been mapped by source vars QSet varsMapped; RDTreeWidgetItem fakeroot; for(int globalVarIdx = 0; globalVarIdx < m_Trace->sourceVars.count(); globalVarIdx++) { const SourceVariableMapping &sourceVar = m_Trace->sourceVars[globalVarIdx]; if(!sourceVar.variables.empty() && sourceVar.variables[0].type == DebugVariableType::Variable) continue; for(const DebugVariableReference &r : sourceVar.variables) varsMapped.insert(r.name); if(sourceVar.rows == 0 || sourceVar.columns == 0) continue; fakeroot.addChild(makeSourceVariableNode(sourceVar, globalVarIdx, -1, false)); } // recursively combine nodes with the same prefix together combineStructures(&fakeroot); while(fakeroot.childCount() > 0) ui->constants->addTopLevelItem(fakeroot.takeChild(0)); // add any raw registers that weren't mapped with something better. We assume for inputs that // everything has a source mapping, even if it's faked from reflection info, but just to be sure // we add any remainders here. Constants might be un-touched by reflection info for(int i = 0; i < m_Trace->constantBlocks.count(); i++) { rdcstr name = m_Trace->constantBlocks[i].name; if(varsMapped.contains(name)) continue; RDTreeWidgetItem *node = new RDTreeWidgetItem({name, name, lit("Constant"), QString()}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Constant, name)))); for(int j = 0; j < m_Trace->constantBlocks[i].members.count(); j++) { if(m_Trace->constantBlocks[i].members[j].rows > 0 || m_Trace->constantBlocks[i].members[j].columns > 0) { name = m_Trace->constantBlocks[i].members[j].name; if(!varsMapped.contains(name)) { RDTreeWidgetItem *child = new RDTreeWidgetItem( {name, name, lit("Constant"), stringRep(m_Trace->constantBlocks[i].members[j])}); child->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Constant, name)))); node->addChild(child); } } else { // Check if this is a constant buffer array int arrayCount = m_Trace->constantBlocks[i].members[j].members.count(); for(int k = 0; k < arrayCount; k++) { if(m_Trace->constantBlocks[i].members[j].members[k].rows > 0 || m_Trace->constantBlocks[i].members[j].members[k].columns > 0) { name = m_Trace->constantBlocks[i].members[j].members[k].name; RDTreeWidgetItem *child = new RDTreeWidgetItem({name, name, lit("Constant"), stringRep(m_Trace->constantBlocks[i].members[j].members[k])}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Constant, name)))); node->addChild(child); } } } } if(node->childCount() == 0) { delete node; continue; } ui->constants->addTopLevelItem(node); } for(int i = 0; i < m_Trace->inputs.count(); i++) { const ShaderVariable &input = m_Trace->inputs[i]; if(varsMapped.contains(input.name)) continue; if(input.rows > 0 || input.columns > 0) { RDTreeWidgetItem *node = new RDTreeWidgetItem({input.name, input.name, lit("Input"), stringRep(input)}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Input, input.name)))); ui->constants->addTopLevelItem(node); } } rdcarray &roBinds = m_ReadOnlyResources; for(int i = 0; i < m_Trace->readOnlyResources.count(); i++) { const ShaderVariable &ro = m_Trace->readOnlyResources[i]; if(varsMapped.contains(ro.name)) continue; int32_t idx = m_Mapping.readOnlyResources.indexOf(Bindpoint(ro.GetBinding())); if(idx < 0) continue; Bindpoint bind = m_Mapping.readOnlyResources[idx]; if(!bind.used) continue; int32_t bindIdx = roBinds.indexOf(bind); if(bindIdx < 0) continue; BoundResourceArray &roBind = roBinds[bindIdx]; if(bind.arraySize == 1) { if(!roBind.resources.empty()) { RDTreeWidgetItem *node = new RDTreeWidgetItem({m_ShaderDetails->readOnlyResources[i].name, ro.name, lit("Resource"), ToQStr(roBind.resources[0].resourceId)}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::ReadOnlyResource, ro.name)))); ui->constants->addTopLevelItem(node); } } else if(bind.arraySize == ~0U) { RDTreeWidgetItem *node = new RDTreeWidgetItem( {m_ShaderDetails->readOnlyResources[i].name, ro.name, lit("[unbounded]"), QString()}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::ReadOnlyResource, ro.name)))); ui->constants->addTopLevelItem(node); } else { RDTreeWidgetItem *node = new RDTreeWidgetItem({m_ShaderDetails->readOnlyResources[i].name, ro.name, QFormatStr("[%1]").arg(bind.arraySize), QString()}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::ReadOnlyResource, ro.name)))); uint32_t count = qMin(bind.arraySize, (uint32_t)roBind.resources.size()); for(uint32_t a = 0; a < count; a++) { QString childName = QFormatStr("%1[%2]").arg(ro.name).arg(a); RDTreeWidgetItem *child = new RDTreeWidgetItem({ QFormatStr("%1[%2]").arg(m_ShaderDetails->readOnlyResources[i].name).arg(a), childName, lit("Resource"), ToQStr(roBind.resources[a].resourceId), }); child->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::ReadOnlyResource, childName)))); node->addChild(child); } ui->constants->addTopLevelItem(node); } } rdcarray &rwBinds = m_ReadWriteResources; for(int i = 0; i < m_Trace->readWriteResources.count(); i++) { const ShaderVariable &rw = m_Trace->readWriteResources[i]; if(varsMapped.contains(rw.name)) continue; int32_t idx = m_Mapping.readWriteResources.indexOf(Bindpoint(rw.GetBinding())); if(idx < 0) continue; Bindpoint bind = m_Mapping.readWriteResources[idx]; if(!bind.used) continue; int32_t bindIdx = rwBinds.indexOf(bind); if(bindIdx < 0) continue; BoundResourceArray &rwBind = rwBinds[bindIdx]; if(bind.arraySize == 1) { if(!rwBind.resources.empty()) { RDTreeWidgetItem *node = new RDTreeWidgetItem({m_ShaderDetails->readWriteResources[i].name, rw.name, lit("Resource"), ToQStr(rwBind.resources[0].resourceId)}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::ReadWriteResource, rw.name)))); ui->constants->addTopLevelItem(node); } } else if(bind.arraySize == ~0U) { RDTreeWidgetItem *node = new RDTreeWidgetItem( {m_ShaderDetails->readWriteResources[i].name, rw.name, lit("[unbounded]"), QString()}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::ReadWriteResource, rw.name)))); ui->constants->addTopLevelItem(node); } else { RDTreeWidgetItem *node = new RDTreeWidgetItem({m_ShaderDetails->readWriteResources[i].name, rw.name, QFormatStr("[%1]").arg(bind.arraySize), QString()}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::ReadWriteResource, rw.name)))); uint32_t count = qMin(bind.arraySize, (uint32_t)rwBind.resources.size()); for(uint32_t a = 0; a < count; a++) { QString childName = QFormatStr("%1[%2]").arg(rw.name).arg(a); RDTreeWidgetItem *child = new RDTreeWidgetItem({ QFormatStr("%1[%2]").arg(m_ShaderDetails->readWriteResources[i].name).arg(a), childName, lit("RW Resource"), ToQStr(rwBind.resources[a].resourceId), }); child->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::ReadWriteResource, childName)))); node->addChild(child); } ui->constants->addTopLevelItem(node); } } rdcarray samplers = m_Ctx.CurPipelineState().GetSamplers(m_Stage); for(int i = 0; i < m_Trace->samplers.count(); i++) { const ShaderVariable &s = m_Trace->samplers[i]; if(varsMapped.contains(s.name)) continue; int32_t idx = m_Mapping.samplers.indexOf(Bindpoint(s.GetBinding())); if(idx < 0) continue; Bindpoint bind = m_Mapping.samplers[idx]; if(!bind.used) continue; int32_t bindIdx = samplers.indexOf(bind); if(bindIdx < 0) continue; BoundResourceArray sampBind = samplers[bindIdx]; if(bind.arraySize == 1) { if(!sampBind.resources.empty()) { RDTreeWidgetItem *node = new RDTreeWidgetItem({m_ShaderDetails->samplers[i].name, s.name, lit("Sampler"), samplerRep(bind, ~0U, sampBind.resources[0].resourceId)}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Sampler, s.name)))); ui->constants->addTopLevelItem(node); } } else if(bind.arraySize == ~0U) { RDTreeWidgetItem *node = new RDTreeWidgetItem( {m_ShaderDetails->samplers[i].name, s.name, lit("[unbounded]"), QString()}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Sampler, s.name)))); ui->constants->addTopLevelItem(node); } else { RDTreeWidgetItem *node = new RDTreeWidgetItem({m_ShaderDetails->samplers[i].name, s.name, QFormatStr("[%1]").arg(bind.arraySize), QString()}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Sampler, s.name)))); for(uint32_t a = 0; a < bind.arraySize; a++) { QString childName = QFormatStr("%1[%2]").arg(s.name).arg(a); RDTreeWidgetItem *child = new RDTreeWidgetItem({ QFormatStr("%1[%2]").arg(m_ShaderDetails->samplers[i].name).arg(a), childName, lit("Sampler"), samplerRep(bind, a, sampBind.resources[a].resourceId), }); child->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Sampler, childName)))); node->addChild(child); } ui->constants->addTopLevelItem(node); } } } { RDTreeViewExpansionState expansion; ui->sourceVars->saveExpansion(expansion, 0); ui->sourceVars->beginUpdate(); ui->sourceVars->clear(); RDTreeWidgetItem fakeroot; const rdcarray &sourceVars = state.sourceVars; for(size_t lidx = 0; lidx < sourceVars.size(); lidx++) { int32_t localVarIdx = int32_t(sourceVars.size() - 1 - lidx); // iterate in reverse order, so newest locals tend to end up on top const SourceVariableMapping &l = sourceVars[localVarIdx]; bool modified = false; // don't list any modified variables on the first step when they all come into existance if(l.variables[0].type == DebugVariableType::Variable && !IsFirstState()) { for(const DebugVariableReference &v : l.variables) { rdcstr base = v.name; int offs = base.find_first_of("[."); if(offs > 0) base = v.name.substr(0, offs); for(const ShaderVariableChange &c : GetCurrentState().changes) { if(c.before.name == v.name || c.after.name == v.name || c.before.name == base || c.after.name == base) { modified = true; break; } } if(modified) break; } } RDTreeWidgetItem *node = makeSourceVariableNode(l, -1, localVarIdx, modified); fakeroot.addChild(node); } // recursively combine nodes with the same prefix together combineStructures(&fakeroot); while(fakeroot.childCount() > 0) ui->sourceVars->addTopLevelItem(fakeroot.takeChild(0)); ui->sourceVars->endUpdate(); ui->sourceVars->applyExpansion(expansion, 0); } { RDTreeViewExpansionState expansion; ui->debugVars->saveExpansion(expansion, 0); ui->debugVars->beginUpdate(); ui->debugVars->clear(); for(int i = 0; i < m_Variables.count(); i++) { bool modified = false; for(const ShaderVariableChange &c : GetCurrentState().changes) { if(c.before.name == m_Variables[i].name || c.after.name == m_Variables[i].name) { modified = true; break; } } ui->debugVars->addTopLevelItem(makeDebugVariableNode(m_Variables[i], "", modified)); } ui->debugVars->endUpdate(); ui->debugVars->applyExpansion(expansion, 0); } updateAccessedResources(); updateWatchVariables(); ui->debugVars->resizeColumnToContents(0); ui->debugVars->resizeColumnToContents(1); updateVariableTooltip(); } void ShaderViewer::updateWatchVariables() { ui->watch->setUpdatesEnabled(false); for(int i = 0; i < ui->watch->rowCount() - 1; i++) { QTableWidgetItem *item = ui->watch->item(i, 0); QString expr = item->text().trimmed(); QRegularExpression exprRE( lit("^" // beginning of the line "((" // chained identifiers, captured together "[a-zA-Z_][a-zA-Z_0-9]*" // a named identifier "(\\[[0-9]+\\])?" // a literal-indexed array expression "\\.?" // optional struct dot ")+)" // 1 or more chained identifiers "(,[xfiudb])?" // optional typecast "$")); // end of the line QRegularExpression identifierSliceRE( lit("^" // beginning of the line "\\.?" // possible struct dot "(" // begin capture "[a-zA-Z_][a-zA-Z_0-9]*|" // a named identifier "(\\[[0-9]+\\])" // or a literal-indexed array expression ")")); // end capture QRegularExpression swizzleRE(lit("^\\.[xyzwrgba]+$")); QRegularExpressionMatch match = exprRE.match(expr); QString error = tr("Error evaluating expression"); if(match.hasMatch()) { QString identifiers = match.captured(1); QChar regcast = QLatin1Char(' '); if(!match.captured(4).isEmpty()) regcast = match.captured(4)[1]; expr = identifiers; match = identifierSliceRE.match(identifiers); if(match.hasMatch()) { QString base = match.captured(1); identifiers = identifiers.mid(base.length()); RDTreeWidgetItem *node = findVarInTree(ui->sourceVars->invisibleRootItem(), base, false, 2); if(!node) node = findVarInTree(ui->constants->invisibleRootItem(), base, false, 2); if(!node) node = findVarInTree(ui->debugVars->invisibleRootItem(), base, false, 2); if(!node) error = tr("Couldn't find variable '%1'").arg(base); QString swizzle; // now we have the node, continue while there are still identifiers to resolve while(node && identifiers.length() > 0) { // get the next identifier match = identifierSliceRE.match(identifiers); if(!match.hasMatch()) { error = tr("Parse error at '%1'").arg(identifiers); node = NULL; break; } QString identifier = match.captured(1); identifiers = identifiers.mid(match.capturedEnd(1)); RDTreeWidgetItem *child = NULL; // handle arrays specially, as the child might be foo[0] or [0] if(identifier[0] == QLatin1Char('[')) { child = findVarInTree(node, identifier, false, 1); if(!child) child = findVarInTree(node, node->text(0) + identifier, false, 1); } else { child = findVarInTree(node, identifier, false, 1); } // didn't find a match! if(!child) { // This is OK if this is the final identifier and it's a swizzle, that just looks like a // member if(swizzleRE.match(identifier).hasMatch() && identifiers.isEmpty()) { swizzle = identifier.mid(1); break; } error = tr("Couldn't find '%1' in '%2'").arg(identifier).arg(node->text(0)); // otherwise we've failed to resolve the expression node = NULL; break; } // recurse node = child; } if(node) { ShaderVariable var; QString regNames; if(getVar(node, &var, ®Names)) { if(swizzle.isEmpty()) swizzle = lit("xyzw").left((int)var.columns); if(regcast == QLatin1Char(' ')) { if(var.type == VarType::Double) regcast = QLatin1Char('d'); else if(var.type == VarType::Float || var.type == VarType::Half) regcast = QLatin1Char('f'); else if(var.type == VarType::ULong || var.type == VarType::UInt || var.type == VarType::UShort || var.type == VarType::UByte) regcast = QLatin1Char('u'); else if(var.type == VarType::SLong || var.type == VarType::SInt || var.type == VarType::SShort || var.type == VarType::SByte || var.type == VarType::Bool) regcast = QLatin1Char('i'); else if(var.type == VarType::Unknown) regcast = ui->intView->isChecked() ? QLatin1Char('i') : QLatin1Char('f'); } QString val; for(int s = 0; s < swizzle.count(); s++) { QChar swiz = swizzle[s]; int elindex = 0; if(swiz == QLatin1Char('x') || swiz == QLatin1Char('r')) elindex = 0; if(swiz == QLatin1Char('y') || swiz == QLatin1Char('g')) elindex = 1; if(swiz == QLatin1Char('z') || swiz == QLatin1Char('b')) elindex = 2; if(swiz == QLatin1Char('w') || swiz == QLatin1Char('a')) elindex = 3; if(regcast == QLatin1Char('i')) { val += Formatter::Format(var.value.iv[elindex]); } else if(regcast == QLatin1Char('f')) { val += Formatter::Format(var.value.fv[elindex]); } else if(regcast == QLatin1Char('u')) { val += Formatter::Format(var.value.uv[elindex]); } else if(regcast == QLatin1Char('x')) { val += Formatter::Format(var.value.uv[elindex], true); } else if(regcast == QLatin1Char('b')) { val += QFormatStr("%1").arg(var.value.uv[elindex], 32, 2, QLatin1Char('0')); } else if(regcast == QLatin1Char('d')) { val += Formatter::Format(var.value.dv[elindex]); } if(s < swizzle.count() - 1) val += lit(", "); } item = new QTableWidgetItem(regNames); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->watch->setItem(i, 1, item); item = new QTableWidgetItem(TypeString(var)); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->watch->setItem(i, 2, item); item = new QTableWidgetItem(val); item->setData(Qt::UserRole, node->tag()); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->watch->setItem(i, 3, item); // success! continue continue; } else { error = tr("'%1' not a watchable variable").arg(expr); } } } } // if we got here, something went wrong. item = new QTableWidgetItem(); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->watch->setItem(i, 1, item); item = new QTableWidgetItem(); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->watch->setItem(i, 2, item); item = new QTableWidgetItem(error); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->watch->setItem(i, 3, item); } ui->watch->setUpdatesEnabled(true); } RDTreeWidgetItem *ShaderViewer::makeSourceVariableNode(const ShaderVariable &var, const rdcstr &sourcePath, const rdcstr &debugVarPath, bool modified) { QString typeName; if(var.type == VarType::UInt) typeName = lit("uint"); else if(var.type == VarType::SInt) typeName = lit("int"); else if(var.type == VarType::Float) typeName = lit("float"); else if(var.type == VarType::Double) typeName = lit("double"); else if(var.type == VarType::Bool) typeName = lit("bool"); QString rowTypeName = typeName; if(var.rows > 1) { typeName += QFormatStr("%1x%2").arg(var.rows).arg(var.columns); if(var.columns > 1) rowTypeName += QString::number(var.columns); } else if(var.columns > 1) { typeName += QString::number(var.columns); } QString value = var.rows == 1 && var.members.empty() ? stringRep(var) : QString(); rdcstr sep = var.name[0] == '[' ? "" : "."; rdcstr sourceName = sourcePath + sep + var.name; rdcstr debugName = debugVarPath + sep + var.name; RDTreeWidgetItem *node = new RDTreeWidgetItem({sourceName, debugName, typeName, value}); for(const ShaderVariable &child : var.members) node->addChild(makeSourceVariableNode(child, sourceName, debugName, modified)); // if this is a matrix, even if it has no explicit row members add the rows as children if(var.members.empty() && var.rows > 1) { for(uint32_t row = 0; row < var.rows; row++) { rdcstr rowsuffix = ".row" + ToStr(row); node->addChild(new RDTreeWidgetItem( {sourceName + rowsuffix, debugName + rowsuffix, rowTypeName, stringRep(var, row)})); } } if(modified) node->setForegroundColor(QColor(Qt::red)); return node; } RDTreeWidgetItem *ShaderViewer::makeSourceVariableNode(const SourceVariableMapping &l, int globalVarIdx, int localVarIdx, bool modified) { const QString xyzw = lit("xyzw"); QString localName = l.name; QString regNames, typeName; QString value; if(l.type == VarType::UInt) typeName = lit("uint"); else if(l.type == VarType::SInt) typeName = lit("int"); else if(l.type == VarType::Float) typeName = lit("float"); else if(l.type == VarType::Double) typeName = lit("double"); else if(l.type == VarType::Bool) typeName = lit("bool"); QList children; { if(l.rows > 1) typeName += QFormatStr("%1x%2").arg(l.rows).arg(l.columns); else if(l.columns > 1) typeName += QString::number(l.columns); for(size_t i = 0; i < l.variables.size(); i++) { const DebugVariableReference &r = l.variables[i]; if(!value.isEmpty()) value += lit(", "); if(!regNames.isEmpty()) regNames += lit(", "); if(r.name.empty()) { regNames += lit("-"); value += lit("?"); } else if(r.type == DebugVariableType::Sampler) { const ShaderVariable *reg = GetDebugVariable(r); if(reg == NULL) continue; regNames = r.name; typeName = lit("Sampler"); rdcarray samplers = m_Ctx.CurPipelineState().GetSamplers(m_Stage); int32_t idx = m_Mapping.samplers.indexOf(Bindpoint(reg->GetBinding())); if(idx < 0) continue; Bindpoint bind = m_Mapping.samplers[idx]; int32_t bindIdx = samplers.indexOf(bind); if(bindIdx < 0) continue; BoundResourceArray &res = samplers[bindIdx]; if(bind.arraySize == 1) { if(!res.resources.empty()) value = samplerRep(bind, ~0U, res.resources[0].resourceId); } else if(bind.arraySize == ~0U) { regNames = QString(); typeName = lit("[unbounded]"); value = QString(); } else { for(uint32_t a = 0; a < bind.arraySize; a++) children.push_back(new RDTreeWidgetItem({ QFormatStr("%1[%2]").arg(localName).arg(a), QFormatStr("%1[%2]").arg(regNames).arg(a), typeName, samplerRep(bind, a, res.resources[a].resourceId), })); regNames = QString(); typeName = QFormatStr("[%1]").arg(bind.arraySize); value = QString(); } } else if(r.type == DebugVariableType::ReadOnlyResource || r.type == DebugVariableType::ReadWriteResource) { const bool isReadOnlyResource = r.type == DebugVariableType::ReadOnlyResource; const ShaderVariable *reg = GetDebugVariable(r); if(reg == NULL) continue; regNames = r.name; typeName = isReadOnlyResource ? lit("Resource") : lit("RW Resource"); rdcarray &resList = isReadOnlyResource ? m_ReadOnlyResources : m_ReadWriteResources; int32_t idx = (isReadOnlyResource ? m_Mapping.readOnlyResources : m_Mapping.readWriteResources) .indexOf(Bindpoint(reg->GetBinding())); if(idx < 0) continue; Bindpoint bind = isReadOnlyResource ? m_Mapping.readOnlyResources[idx] : m_Mapping.readWriteResources[idx]; int32_t bindIdx = resList.indexOf(bind); if(bindIdx < 0) continue; BoundResourceArray &res = resList[bindIdx]; if(bind.arraySize == 1) { if(!res.resources.empty()) value = ToQStr(res.resources[0].resourceId); } else if(bind.arraySize == ~0U) { regNames = QString(); typeName = lit("[unbounded]"); value = QString(); } else { uint32_t count = qMin(bind.arraySize, (uint32_t)res.resources.size()); for(uint32_t a = 0; a < count; a++) { children.push_back(new RDTreeWidgetItem({ QFormatStr("%1[%2]").arg(localName).arg(a), QFormatStr("%1[%2]").arg(regNames).arg(a), typeName, ToQStr(res.resources[a].resourceId), })); } regNames = QString(); typeName = QFormatStr("[%1]").arg(bind.arraySize); value = QString(); } } else { const ShaderVariable *reg = GetDebugVariable(r); if(reg) { if(!reg->members.empty()) { // if the register we were pointed at is a complex type (struct/array/etc), embed it as // a child typeName = QString(); value = QString(); for(const ShaderVariable &child : reg->members) children.push_back(makeSourceVariableNode(child, localName, reg->name, modified)); break; } else if(i > 0 && r.name == l.variables[i - 1].name && (r.component / reg->columns) == (l.variables[i - 1].component / reg->columns)) { // if the previous register was the same, just append our component // remove the auto-appended ", " - there must be one because this isn't the first // register regNames.chop(2); regNames += xyzw[r.component % 4]; } else { if(reg->rows > 1) regNames += QFormatStr("%1.row%2.%3") .arg(reg->name) .arg(r.component / reg->columns) .arg(xyzw[r.component % 4]); else regNames += QFormatStr("%1.%2").arg(r.name).arg(xyzw[r.component % 4]); } if(l.type == VarType::UInt) value += Formatter::Format(reg->value.uv[r.component]); else if(l.type == VarType::SInt) value += Formatter::Format(reg->value.iv[r.component]); else if(l.type == VarType::Bool) value += Formatter::Format(reg->value.uv[r.component] ? true : false); else if(l.type == VarType::Float) value += Formatter::Format(reg->value.fv[r.component]); else if(l.type == VarType::Double) value += Formatter::Format(reg->value.dv[r.component]); } else { regNames += lit(""); value += lit(""); } } if(l.rows > 1 && l.variables.size() > l.columns) { if(((i + 1) % l.columns) == 0) { QString localBaseName = localName; int dot = localBaseName.lastIndexOf(QLatin1Char('.')); if(dot >= 0) localBaseName = localBaseName.mid(dot + 1); uint32_t row = (uint32_t)i / l.columns; children.push_back(new RDTreeWidgetItem( {QFormatStr("%1.row%2").arg(localBaseName).arg(row), regNames, typeName, value})); regNames = QString(); value = QString(); } } } } RDTreeWidgetItem *node = new RDTreeWidgetItem({localName, regNames, typeName, value}); for(RDTreeWidgetItem *c : children) node->addChild(c); if(modified) node->setForegroundColor(QColor(Qt::red)); node->setTag(QVariant::fromValue(VariableTag(localName, l.offset, globalVarIdx, localVarIdx))); return node; } RDTreeWidgetItem *ShaderViewer::makeDebugVariableNode(const ShaderVariable &v, rdcstr prefix, bool modified) { rdcstr basename = prefix + v.name; RDTreeWidgetItem *node = new RDTreeWidgetItem({v.name, v.rows == 1 && v.members.empty() ? stringRep(v) : QString()}); node->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Variable, basename)))); for(const ShaderVariable &m : v.members) { rdcstr childprefix = basename + "."; if(m.name.beginsWith(basename + "[")) childprefix = basename; node->addChild(makeDebugVariableNode(m, childprefix, modified)); } // if this is a matrix, even if it has no explicit row members add the rows as children if(v.members.empty() && v.rows > 1) { for(uint32_t row = 0; row < v.rows; row++) { rdcstr rowsuffix = ".row" + ToStr(row); RDTreeWidgetItem *child = new RDTreeWidgetItem({v.name + rowsuffix, stringRep(v, row)}); child->setTag(QVariant::fromValue( VariableTag(DebugVariableReference(DebugVariableType::Variable, basename + rowsuffix)))); node->addChild(child); } } if(modified) node->setForegroundColor(QColor(Qt::red)); return node; } RDTreeWidgetItem *ShaderViewer::makeAccessedResourceNode(const ShaderVariable &v, bool modified) { BindpointIndex bp = v.GetBinding(); ResourceId resId; QString typeName; if(v.type == VarType::ReadOnlyResource) { typeName = lit("Resource"); int32_t idx = m_Mapping.readOnlyResources.indexOf(Bindpoint(bp)); if(idx >= 0) { Bindpoint bind = m_Mapping.readOnlyResources[idx]; if(bind.used) { int32_t bindIdx = m_ReadOnlyResources.indexOf(bind); if(bindIdx >= 0) { BoundResourceArray &roBind = m_ReadOnlyResources[bindIdx]; if(bp.arrayIndex < roBind.resources.size()) resId = roBind.resources[bp.arrayIndex].resourceId; } } } } else if(v.type == VarType::ReadWriteResource) { typeName = lit("RW Resource"); int32_t idx = m_Mapping.readWriteResources.indexOf(Bindpoint(bp)); if(idx >= 0) { Bindpoint bind = m_Mapping.readWriteResources[idx]; if(bind.used) { int32_t bindIdx = m_ReadWriteResources.indexOf(bind); if(bindIdx >= 0) { BoundResourceArray &rwBind = m_ReadWriteResources[bindIdx]; if(bp.arrayIndex < rwBind.resources.size()) resId = rwBind.resources[bp.arrayIndex].resourceId; } } } } RDTreeWidgetItem *node = new RDTreeWidgetItem({v.name, typeName, ToQStr(resId)}); if(resId != ResourceId()) node->setTag(QVariant::fromValue(AccessedResourceTag(bp, v.type))); if(modified) node->setForegroundColor(QColor(Qt::red)); return node; } const ShaderVariable *ShaderViewer::GetDebugVariable(const DebugVariableReference &r) { if(r.type == DebugVariableType::ReadOnlyResource) { for(int i = 0; i < m_Trace->readOnlyResources.count(); i++) if(m_Trace->readOnlyResources[i].name == r.name) return &m_Trace->readOnlyResources[i]; return NULL; } else if(r.type == DebugVariableType::ReadWriteResource) { for(int i = 0; i < m_Trace->readWriteResources.count(); i++) if(m_Trace->readWriteResources[i].name == r.name) return &m_Trace->readWriteResources[i]; return NULL; } else if(r.type == DebugVariableType::Sampler) { for(int i = 0; i < m_Trace->samplers.count(); i++) if(m_Trace->samplers[i].name == r.name) return &m_Trace->samplers[i]; return NULL; } else if(r.type == DebugVariableType::Input || r.type == DebugVariableType::Constant || r.type == DebugVariableType::Variable) { rdcarray *vars = r.type == DebugVariableType::Input ? &m_Trace->inputs : (r.type == DebugVariableType::Constant ? &m_Trace->constantBlocks : &m_Variables); rdcstr path = r.name; while(!path.empty()) { rdcstr elem; // pick out the next element in the path // if this is an array index, grab that if(path[0] == '[') { int idx = path.indexOf(']'); if(idx < 0) break; elem = path.substr(0, idx + 1); // skip past any .s if(path[idx + 1] == '.') idx++; path = path.substr(idx + 1); } else { // otherwise look for the next identifier int idx = path.find_first_of("[."); if(idx < 0) { // no results means that all that's left of the path is an identifier elem.swap(path); } else { elem = path.substr(0, idx); // skip past any .s if(path[idx] == '.') idx++; path = path.substr(idx); } } // look in our current set of vars for a matching variable bool found = false; for(size_t i = 0; i < vars->size(); i++) { if(vars->at(i).name == elem) { // found the match. found = true; // If there's no more path, we've found the exact match, otherwise continue if(path.empty()) return &vars->at(i); vars = &vars->at(i).members; break; } } if(!found) break; } return NULL; } return NULL; } void ShaderViewer::ensureLineScrolled(ScintillaEdit *s, int line) { int firstLine = s->firstVisibleLine(); int linesVisible = s->linesOnScreen(); if(s->isVisible() && (line < firstLine || line > (firstLine + linesVisible - 1))) s->setFirstVisibleLine(qMax(0, line - linesVisible / 2)); } uint32_t ShaderViewer::CurrentStep() { return (uint32_t)m_CurrentStateIdx; } void ShaderViewer::SetCurrentStep(uint32_t step) { if(!m_Trace || m_States.empty()) return; while(GetCurrentState().stepIndex != step) { if(GetCurrentState().stepIndex < step) { // move forward if possible if(!IsLastState()) applyForwardsChange(); else break; } else { // move backward if possible if(!IsFirstState()) applyBackwardsChange(); else break; } } updateDebugState(); } void ShaderViewer::ToggleBreakpoint(int instruction) { if(!m_Trace || m_States.empty()) return; sptr_t instLine = -1; if(instruction == -1) { ScintillaEdit *cur = currentScintilla(); // search forward for an instruction if(cur != m_DisassemblyView) { int scintillaIndex = m_FileScintillas.indexOf(cur); if(scintillaIndex < 0) return; // add one to go from scintilla line numbers (0-based) to ours (1-based) sptr_t i = cur->lineFromPosition(cur->currentPos()) + 1; QMap> &fileMap = m_Line2Insts[scintillaIndex]; // find the next line that maps to an instruction for(; i < cur->lineCount(); i++) { if(fileMap.contains(i)) { for(size_t inst : fileMap[i]) ToggleBreakpoint((int)inst); return; } } } else { instLine = m_DisassemblyView->lineFromPosition(m_DisassemblyView->currentPos()); for(; instLine < m_DisassemblyView->lineCount(); instLine++) { instruction = instructionForDisassemblyLine(instLine); if(instruction >= 0) break; } } } if(instruction < 0 || instruction >= m_Trace->lineInfo.count()) return; if(instLine == -1) { if(instruction < m_Trace->lineInfo.count()) instLine = m_Trace->lineInfo[instruction].disassemblyLine - 1; } if(m_Breakpoints.contains(instruction)) { if(instLine >= 0) { m_DisassemblyView->markerDelete(instLine, BREAKPOINT_MARKER); m_DisassemblyView->markerDelete(instLine, BREAKPOINT_MARKER + 1); const LineColumnInfo &lineInfo = m_Trace->lineInfo[instruction]; if(lineInfo.fileIndex >= 0 && lineInfo.fileIndex < m_FileScintillas.count()) { for(sptr_t line = lineInfo.lineStart; line <= (sptr_t)lineInfo.lineEnd; line++) { ScintillaEdit *s = m_FileScintillas[lineInfo.fileIndex]; if(s) { m_FileScintillas[lineInfo.fileIndex]->markerDelete(line - 1, BREAKPOINT_MARKER); m_FileScintillas[lineInfo.fileIndex]->markerDelete(line - 1, BREAKPOINT_MARKER + 1); } } } } m_Breakpoints.removeOne(instruction); } else { if(instLine >= 0) { m_DisassemblyView->markerAdd(instLine, BREAKPOINT_MARKER); m_DisassemblyView->markerAdd(instLine, BREAKPOINT_MARKER + 1); const LineColumnInfo &lineInfo = m_Trace->lineInfo[instruction]; if(lineInfo.fileIndex >= 0 && lineInfo.fileIndex < m_FileScintillas.count()) { for(sptr_t line = lineInfo.lineStart; line <= (sptr_t)lineInfo.lineEnd; line++) { ScintillaEdit *s = m_FileScintillas[lineInfo.fileIndex]; if(s) { m_FileScintillas[lineInfo.fileIndex]->markerAdd(line - 1, BREAKPOINT_MARKER); m_FileScintillas[lineInfo.fileIndex]->markerAdd(line - 1, BREAKPOINT_MARKER + 1); } } } m_Breakpoints.push_back(instruction); } } } void ShaderViewer::ShowErrors(const rdcstr &errors) { if(m_Errors) { m_Errors->setReadOnly(false); SetTextAndUpdateMargin0(m_Errors, errors); m_Errors->setReadOnly(true); if(!errors.isEmpty()) ToolWindowManager::raiseToolWindow(m_Errors); } setEditorWindowTitle(); } void ShaderViewer::AddWatch(const rdcstr &variable) { int newRow = ui->watch->rowCount() - 1; ui->watch->insertRow(ui->watch->rowCount() - 1); ui->watch->setItem(newRow, 0, new QTableWidgetItem(variable)); ToolWindowManager::raiseToolWindow(ui->watch); ui->watch->activateWindow(); ui->watch->QWidget::setFocus(); } int ShaderViewer::snippetPos() { ShaderEncoding encoding = currentEncoding(); if(encoding != ShaderEncoding::GLSL) return 0; if(m_Scintillas.isEmpty()) return 0; QPair ver = m_Scintillas[0]->findText(SCFIND_REGEXP, "#version.*", 0, m_Scintillas[0]->length()); if(ver.first < 0) return 0; return ver.second + 1; } void ShaderViewer::insertSnippet(const QString &text) { if(text.isEmpty()) return; if(m_Scintillas.isEmpty()) return; m_Scintillas[0]->insertText(snippetPos(), text.toUtf8().data()); m_Scintillas[0]->setSelection(0, 0); } QString ShaderViewer::vulkanUBO() { ShaderEncoding encoding = currentEncoding(); if(encoding == ShaderEncoding::GLSL) { return lit(R"( layout(binding = 0, std140) uniform RENDERDOC_Uniforms { uvec4 TexDim; uint SelectedMip; int TextureType; uint SelectedSliceFace; int SelectedSample; uvec4 YUVDownsampleRate; uvec4 YUVAChannels; } RENDERDOC; )"); } else if(encoding == ShaderEncoding::HLSL) { return lit(R"( cbuffer RENDERDOC_Constants : register(b0) { uint4 RENDERDOC_TexDim; uint RENDERDOC_SelectedMip; int RENDERDOC_TextureType; uint RENDERDOC_SelectedSliceFace; int RENDERDOC_SelectedSample; uint4 RENDERDOC_YUVDownsampleRate; uint4 RENDERDOC_YUVAChannels; }; )"); } else if(encoding == ShaderEncoding::SPIRVAsm) { return lit("; Can't insert snippets for SPIR-V ASM"); } return QString(); } void ShaderViewer::snippet_textureDimensions() { ShaderEncoding encoding = currentEncoding(); GraphicsAPI api = m_Ctx.APIProps().localRenderer; QString text; if(api == GraphicsAPI::Vulkan) { text = vulkanUBO(); } else if(encoding == ShaderEncoding::HLSL) { text = lit(R"( // xyz == width, height, depth. w == # mips uint4 RENDERDOC_TexDim; uint4 RENDERDOC_YUVDownsampleRate; uint4 RENDERDOC_YUVAChannels; )"); } else if(encoding == ShaderEncoding::GLSL) { text = lit(R"( // xyz == width, height, depth. w == # mips uniform uvec4 RENDERDOC_TexDim; )"); } else if(encoding == ShaderEncoding::SPIRVAsm) { text = lit("; Can't insert snippets for SPIR-V ASM"); } insertSnippet(text); } void ShaderViewer::snippet_selectedMip() { ShaderEncoding encoding = currentEncoding(); GraphicsAPI api = m_Ctx.APIProps().localRenderer; QString text; if(api == GraphicsAPI::Vulkan) { text = vulkanUBO(); } else if(encoding == ShaderEncoding::HLSL) { text = lit(R"( // selected mip in UI uint RENDERDOC_SelectedMip; )"); } else if(encoding == ShaderEncoding::GLSL) { text = lit(R"( // selected mip in UI uniform uint RENDERDOC_SelectedMip; )"); } else if(encoding == ShaderEncoding::SPIRVAsm) { text = lit("; Can't insert snippets for SPIR-V ASM"); } insertSnippet(text); } void ShaderViewer::snippet_selectedSlice() { ShaderEncoding encoding = currentEncoding(); GraphicsAPI api = m_Ctx.APIProps().localRenderer; QString text; if(api == GraphicsAPI::Vulkan) { text = vulkanUBO(); } else if(encoding == ShaderEncoding::HLSL) { text = lit(R"( // selected array slice or cubemap face in UI uint RENDERDOC_SelectedSliceFace; )"); } else if(encoding == ShaderEncoding::GLSL) { text = lit(R"( // selected array slice or cubemap face in UI uniform uint RENDERDOC_SelectedSliceFace; )"); } else if(encoding == ShaderEncoding::SPIRVAsm) { text = lit("; Can't insert snippets for SPIR-V ASM"); } insertSnippet(text); } void ShaderViewer::snippet_selectedSample() { ShaderEncoding encoding = currentEncoding(); GraphicsAPI api = m_Ctx.APIProps().localRenderer; QString text; if(api == GraphicsAPI::Vulkan) { text = vulkanUBO(); } else if(encoding == ShaderEncoding::HLSL) { text = lit(R"( // selected MSAA sample or -numSamples for resolve. See docs int RENDERDOC_SelectedSample; )"); } else if(encoding == ShaderEncoding::GLSL) { text = lit(R"( // selected MSAA sample or -numSamples for resolve. See docs uniform int RENDERDOC_SelectedSample; )"); } else if(encoding == ShaderEncoding::SPIRVAsm) { text = lit("; Can't insert snippets for SPIR-V ASM"); } insertSnippet(text); } void ShaderViewer::snippet_selectedType() { ShaderEncoding encoding = currentEncoding(); GraphicsAPI api = m_Ctx.APIProps().localRenderer; QString text; if(api == GraphicsAPI::Vulkan) { text = vulkanUBO(); } else if(encoding == ShaderEncoding::HLSL) { text = lit(R"( // 1 = 1D, 2 = 2D, 3 = 3D, 4 = Depth, 5 = Depth + Stencil // 6 = Depth (MS), 7 = Depth + Stencil (MS) uint RENDERDOC_TextureType; )"); } else if(encoding == ShaderEncoding::GLSL) { text = lit(R"( // 1 = 1D, 2 = 2D, 3 = 3D, 4 = Cube // 5 = 1DArray, 6 = 2DArray, 7 = CubeArray // 8 = Rect, 9 = Buffer, 10 = 2DMS uniform uint RENDERDOC_TextureType; )"); } else if(encoding == ShaderEncoding::SPIRVAsm) { text = lit("; Can't insert snippets for SPIR-V ASM"); } insertSnippet(text); } void ShaderViewer::snippet_samplers() { ShaderEncoding encoding = currentEncoding(); GraphicsAPI api = m_Ctx.APIProps().localRenderer; if(encoding == ShaderEncoding::HLSL) { if(api == GraphicsAPI::Vulkan) { insertSnippet(lit(R"( // Samplers SamplerState pointSampler : register(s50); SamplerState linearSampler : register(s51); // End Samplers )")); } else { insertSnippet(lit(R"( // Samplers SamplerState pointSampler : register(s0); SamplerState linearSampler : register(s1); // End Samplers )")); } } } void ShaderViewer::snippet_resources() { ShaderEncoding encoding = currentEncoding(); GraphicsAPI api = m_Ctx.APIProps().localRenderer; if(encoding == ShaderEncoding::HLSL) { if(api == GraphicsAPI::Vulkan) { insertSnippet(lit(R"( // Textures // Floating point Texture1DArray texDisplayTex1DArray : register(t6); Texture2DArray texDisplayTex2DArray : register(t7); Texture3D texDisplayTex3D : register(t8); Texture2DMSArray texDisplayTex2DMSArray : register(t9); Texture2DArray texDisplayYUVArray : register(t10); // Unsigned int samplers Texture1DArray texDisplayUIntTex1DArray : register(t11); Texture2DArray texDisplayUIntTex2DArray : register(t12); Texture3D texDisplayUIntTex3D : register(t13); Texture2DMSArray texDisplayUIntTex2DMSArray : register(t14); // Int samplers Texture1DArray texDisplayIntTex1DArray : register(t16); Texture2DArray texDisplayIntTex2DArray : register(t17); Texture3D texDisplayIntTex3D : register(t18); Texture2DMSArray texDisplayIntTex2DMSArray : register(t19); // End Textures )")); } else { insertSnippet(lit(R"( // Textures Texture1DArray texDisplayTex1DArray : register(t1); Texture2DArray texDisplayTex2DArray : register(t2); Texture3D texDisplayTex3D : register(t3); Texture2DArray texDisplayTexDepthArray : register(t4); Texture2DArray texDisplayTexStencilArray : register(t5); Texture2DMSArray texDisplayTexDepthMSArray : register(t6); Texture2DMSArray texDisplayTexStencilMSArray : register(t7); Texture2DMSArray texDisplayTex2DMSArray : register(t9); Texture2DArray texDisplayYUVArray : register(t10); // Unsigned int samplers Texture1DArray texDisplayUIntTex1DArray : register(t11); Texture2DArray texDisplayUIntTex2DArray : register(t12); Texture3D texDisplayUIntTex3D : register(t13); Texture2DMSArray texDisplayUIntTex2DMSArray : register(t19); // Int samplers Texture1DArray texDisplayIntTex1DArray : register(t21); Texture2DArray texDisplayIntTex2DArray : register(t22); Texture3D texDisplayIntTex3D : register(t23); Texture2DMSArray texDisplayIntTex2DMSArray : register(t29); // End Textures )")); } } else if(encoding == ShaderEncoding::GLSL) { if(api == GraphicsAPI::Vulkan) { insertSnippet(lit(R"( // Textures // Floating point samplers layout(binding = 6) uniform sampler1DArray tex1DArray; layout(binding = 7) uniform sampler2DArray tex2DArray; layout(binding = 8) uniform sampler3D tex3D; layout(binding = 9) uniform sampler2DMS tex2DMS; layout(binding = 10) uniform sampler2DArray texYUVArray[2]; // Unsigned int samplers layout(binding = 11) uniform usampler1DArray texUInt1DArray; layout(binding = 12) uniform usampler2DArray texUInt2DArray; layout(binding = 13) uniform usampler3D texUInt3D; layout(binding = 14) uniform usampler2DMS texUInt2DMS; // Int samplers layout(binding = 16) uniform isampler1DArray texSInt1DArray; layout(binding = 17) uniform isampler2DArray texSInt2DArray; layout(binding = 18) uniform isampler3D texSInt3D; layout(binding = 19) uniform isampler2DMS texSInt2DMS; // End Textures )")); } else { insertSnippet(lit(R"( // Textures // Unsigned int samplers layout (binding = 1) uniform usampler1D texUInt1D; layout (binding = 2) uniform usampler2D texUInt2D; layout (binding = 3) uniform usampler3D texUInt3D; // cube = 4 layout (binding = 5) uniform usampler1DArray texUInt1DArray; layout (binding = 6) uniform usampler2DArray texUInt2DArray; // cube array = 7 layout (binding = 8) uniform usampler2DRect texUInt2DRect; layout (binding = 9) uniform usamplerBuffer texUIntBuffer; layout (binding = 10) uniform usampler2DMS texUInt2DMS; // Int samplers layout (binding = 1) uniform isampler1D texSInt1D; layout (binding = 2) uniform isampler2D texSInt2D; layout (binding = 3) uniform isampler3D texSInt3D; // cube = 4 layout (binding = 5) uniform isampler1DArray texSInt1DArray; layout (binding = 6) uniform isampler2DArray texSInt2DArray; // cube array = 7 layout (binding = 8) uniform isampler2DRect texSInt2DRect; layout (binding = 9) uniform isamplerBuffer texSIntBuffer; layout (binding = 10) uniform isampler2DMS texSInt2DMS; // Floating point samplers layout (binding = 1) uniform sampler1D tex1D; layout (binding = 2) uniform sampler2D tex2D; layout (binding = 3) uniform sampler3D tex3D; layout (binding = 4) uniform samplerCube texCube; layout (binding = 5) uniform sampler1DArray tex1DArray; layout (binding = 6) uniform sampler2DArray tex2DArray; layout (binding = 7) uniform samplerCubeArray texCubeArray; layout (binding = 8) uniform sampler2DRect tex2DRect; layout (binding = 9) uniform samplerBuffer texBuffer; layout (binding = 10) uniform sampler2DMS tex2DMS; // End Textures )")); } } } bool ShaderViewer::eventFilter(QObject *watched, QEvent *event) { if(event->type() == QEvent::ToolTip) { QHelpEvent *he = (QHelpEvent *)event; RDTreeWidget *tree = qobject_cast(watched); if(tree) { RDTreeWidgetItem *item = tree->itemAt(tree->viewport()->mapFromGlobal(QCursor::pos())); if(item) { VariableTag tag = item->tag().value(); showVariableTooltip(tag.debugVar.name); } } RDTableWidget *table = qobject_cast(watched); if(table) { QTableWidgetItem *item = table->itemAt(table->viewport()->mapFromGlobal(QCursor::pos())); if(item) { item = table->item(item->row(), 2); VariableTag tag = item->data(Qt::UserRole).value(); showVariableTooltip(tag.debugVar.name); } } } if(event->type() == QEvent::MouseMove || event->type() == QEvent::Leave) { hideVariableTooltip(); } return QFrame::eventFilter(watched, event); } void ShaderViewer::MarkModification() { if(m_ModifyCallback) m_ModifyCallback(this, false); m_Modified = true; setEditorWindowTitle(); } void ShaderViewer::disasm_tooltipShow(int x, int y) { // do nothing if there's no trace if(!m_Trace || m_States.empty()) return; ScintillaEdit *sc = qobject_cast(QObject::sender()); if(!sc) return; // ignore any messages if we're already outside the viewport if(!sc->rect().contains(sc->mapFromGlobal(QCursor::pos()))) return; if(sc->isVisible()) { sptr_t scintillaPos = sc->positionFromPoint(x, y); sptr_t start = sc->wordStartPosition(scintillaPos, true); sptr_t end = sc->wordEndPosition(scintillaPos, true); do { // expand leftwards through simple struct . access // TODO handle arrays while(isspace(sc->charAt(start - 1))) start--; if(sc->charAt(start - 1) == '.') start = sc->wordStartPosition(start - 2, true); else break; } while(true); QString text = QString::fromUtf8(sc->textRange(start, end)).trimmed(); if(!text.isEmpty()) showVariableTooltip(text); } } void ShaderViewer::disasm_tooltipHide(int x, int y) { hideVariableTooltip(); } void ShaderViewer::showVariableTooltip(QString name) { m_TooltipName = name; m_TooltipPos = QCursor::pos(); updateVariableTooltip(); } void ShaderViewer::updateVariableTooltip() { if(!m_Trace || m_States.empty()) return; ShaderVariable var; if(!findVar(m_TooltipName, &var)) return; if(var.type != VarType::Unknown) { QString tooltip; if(var.type == VarType::ReadOnlyResource || var.type == VarType::ReadWriteResource) { tooltip = RichResourceTextFormat(m_Ctx, stringRep(var, 0)); } else { tooltip = QFormatStr("
%1: %2\n").arg(var.name).arg(RowString(var, 0));
      QString spacing = QString(var.name.count(), QLatin1Char(' '));
      for(int i = 1; i < var.rows; i++)
        tooltip += QFormatStr("%1  %2\n").arg(spacing).arg(RowString(var, i));
      tooltip += lit("
"); } QToolTip::showText(m_TooltipPos, tooltip); return; } QString text = QFormatStr("
%1\n").arg(var.name);
  text +=
      lit("                  X           Y           Z           W \n"
          "--------------------------------------------------------\n");

  text += QFormatStr("float | %1 %2 %3 %4\n")
              .arg(Formatter::Format(var.value.fv[0]), 11)
              .arg(Formatter::Format(var.value.fv[1]), 11)
              .arg(Formatter::Format(var.value.fv[2]), 11)
              .arg(Formatter::Format(var.value.fv[3]), 11);
  text += QFormatStr("uint  | %1 %2 %3 %4\n")
              .arg(var.value.uv[0], 11, 10, QLatin1Char(' '))
              .arg(var.value.uv[1], 11, 10, QLatin1Char(' '))
              .arg(var.value.uv[2], 11, 10, QLatin1Char(' '))
              .arg(var.value.uv[3], 11, 10, QLatin1Char(' '));
  text += QFormatStr("int   | %1 %2 %3 %4\n")
              .arg(var.value.iv[0], 11, 10, QLatin1Char(' '))
              .arg(var.value.iv[1], 11, 10, QLatin1Char(' '))
              .arg(var.value.iv[2], 11, 10, QLatin1Char(' '))
              .arg(var.value.iv[3], 11, 10, QLatin1Char(' '));
  text += QFormatStr("hex   |    %1    %2    %3    %4")
              .arg(Formatter::HexFormat(var.value.uv[0], 4))
              .arg(Formatter::HexFormat(var.value.uv[1], 4))
              .arg(Formatter::HexFormat(var.value.uv[2], 4))
              .arg(Formatter::HexFormat(var.value.uv[3], 4));
  text += lit("
"); QToolTip::showText(m_TooltipPos, text); } void ShaderViewer::hideVariableTooltip() { QToolTip::hideText(); m_TooltipVarIndex = -1; m_TooltipName = QString(); } bool ShaderViewer::isSourceDebugging() { return !m_DisassemblyFrame->isVisible(); } ShaderEncoding ShaderViewer::currentEncoding() { int idx = ui->encoding->currentIndex(); if(idx >= 0 && idx < m_Encodings.count()) return m_Encodings[idx]; return ShaderEncoding::Unknown; } void ShaderViewer::on_findReplace_clicked() { if(m_FindReplace->isVisible()) { ToolWindowManager::raiseToolWindow(m_FindReplace); } else { ui->docking->moveToolWindow( m_FindReplace, ToolWindowManager::AreaReference(ToolWindowManager::NewFloatingArea)); ui->docking->setToolWindowProperties(m_FindReplace, ToolWindowManager::HideOnClose); } ui->docking->areaOf(m_FindReplace)->parentWidget()->activateWindow(); m_FindReplace->takeFocus(); } void ShaderViewer::PopulateCompileTools() { ShaderEncoding encoding = currentEncoding(); rdcarray accepted = m_Ctx.TargetShaderEncodings(); QStringList strs; strs.clear(); for(const ShaderProcessingTool &tool : m_Ctx.Config().ShaderProcessors) { // skip tools that can't accept our inputs, or doesn't produce a supported output if(tool.input != encoding || accepted.indexOf(tool.output) < 0) continue; strs << tool.name; } // if we can pass in the shader source as-is, add a built-in option if(accepted.indexOf(encoding) >= 0) strs << tr("Builtin"); ui->compileTool->clear(); ui->compileTool->addItems(strs); // pick the first option as highest priority ui->compileTool->setCurrentIndex(0); // fill out parameters PopulateCompileToolParameters(); if(strs.isEmpty()) { ShowErrors(tr("No compilation tool found that takes %1 as input and produces compatible output") .arg(ToQStr(encoding))); } } void ShaderViewer::PopulateCompileToolParameters() { ShaderEncoding encoding = currentEncoding(); rdcarray accepted = m_Ctx.TargetShaderEncodings(); ui->toolCommandLine->clear(); if(accepted.indexOf(encoding) >= 0 && ui->compileTool->currentIndex() == ui->compileTool->count() - 1) { // if we're using the last Builtin tool, there are no default parameters } else { for(const ShaderProcessingTool &tool : m_Ctx.Config().ShaderProcessors) { if(QString(tool.name) == ui->compileTool->currentText()) { ui->toolCommandLine->setPlainText(tool.DefaultArguments()); ui->toolCommandLine->setEnabled(true); break; } } } for(int i = 0; i < m_Flags.flags.count(); i++) { ShaderCompileFlag &flag = m_Flags.flags[i]; if(flag.name == "@cmdline") { // append command line from saved flags ui->toolCommandLine->setPlainText(ui->toolCommandLine->toPlainText() + lit(" %1").arg(flag.value)); break; } } } bool ShaderViewer::ProcessIncludeDirectives(QString &source, const rdcstrpairs &files) { // try and match up #includes against the files that we have. This isn't always // possible as fxc only seems to include the source for files if something in // that file was included in the compiled output. So you might end up with // dangling #includes - we just have to ignore them int offs = source.indexOf(lit("#include")); while(offs >= 0) { // search back to ensure this is a valid #include (ie. not in a comment). // Must only see whitespace before, then a newline. int ws = qMax(0, offs - 1); while(ws >= 0 && (source[ws] == QLatin1Char(' ') || source[ws] == QLatin1Char('\t'))) ws--; // not valid? jump to next. if(ws > 0 && source[ws] != QLatin1Char('\n')) { offs = source.indexOf(lit("#include"), offs + 1); continue; } int start = ws + 1; bool tail = true; int lineEnd = source.indexOf(QLatin1Char('\n'), start + 1); if(lineEnd == -1) { lineEnd = source.length(); tail = false; } ws = offs + sizeof("#include") - 1; while(source[ws] == QLatin1Char(' ') || source[ws] == QLatin1Char('\t')) ws++; QString line = source.mid(offs, lineEnd - offs + 1); if(source[ws] != QLatin1Char('<') && source[ws] != QLatin1Char('"')) { ShowErrors(tr("Invalid #include directive found:\r\n") + line); return false; } // find matching char, either <> or ""; int end = source.indexOf(source[ws] == QLatin1Char('"') ? QLatin1Char('"') : QLatin1Char('>'), ws + 1); if(end == -1) { ShowErrors(tr("Invalid #include directive found:\r\n") + line); return false; } QString fname = source.mid(ws + 1, end - ws - 1); QString fileText; // look for exact match first for(int i = 0; i < files.count(); i++) { if(QString(files[i].first) == fname) { fileText = files[i].second; break; } } if(fileText.isEmpty()) { QString search = QFileInfo(fname).fileName(); // if not, try and find the same filename (this is not proper include handling!) for(const rdcstrpair &kv : files) { if(QFileInfo(kv.first).fileName().compare(search, Qt::CaseInsensitive) == 0) { fileText = kv.second; break; } } if(fileText.isEmpty()) fileText = QFormatStr("// Can't find file %1\n").arg(fname); } source = source.left(offs) + lit("\n\n") + fileText + lit("\n\n") + (tail ? source.mid(lineEnd + 1) : QString()); // need to start searching from the beginning - wasteful but allows nested includes to // work offs = source.indexOf(lit("#include")); } for(const rdcstrpair &kv : files) { if(kv.first == "@cmdline") source = QString(kv.second) + lit("\n\n") + source; } return true; } void ShaderViewer::on_refresh_clicked() { if(m_Trace) { m_Ctx.GetPipelineViewer()->SaveShaderFile(m_ShaderDetails); return; } ShaderEncoding encoding = currentEncoding(); // if we don't have any compile tools - even the 'builtin' one, this compilation is not going to // succeed. if(ui->compileTool->count() == 0 && !m_CustomShader) { ShowErrors(tr("No compilation tool found that takes %1 as input and produces compatible output") .arg(ToQStr(encoding))); } else if(m_SaveCallback) { rdcstrpairs files; for(ScintillaEdit *s : m_Scintillas) { QWidget *w = (QWidget *)s; files.push_back( {w->property("filename").toString(), QString::fromUtf8(s->getText(s->textLength() + 1))}); } if(files.isEmpty()) return; QString source = files[0].second; if(encoding == ShaderEncoding::HLSL || encoding == ShaderEncoding::GLSL) { bool success = ProcessIncludeDirectives(source, files); if(!success) return; } bytebuf shaderBytes(source.toUtf8()); rdcarray accepted = m_Ctx.TargetShaderEncodings(); if(m_CustomShader || (accepted.indexOf(encoding) >= 0 && ui->compileTool->currentIndex() == ui->compileTool->count() - 1)) { // if using the builtin compiler, just pass through } else { for(const ShaderProcessingTool &tool : m_Ctx.Config().ShaderProcessors) { if(QString(tool.name) == ui->compileTool->currentText()) { ShaderToolOutput out = tool.CompileShader(this, source, ui->entryFunc->text(), m_Stage, ui->toolCommandLine->toPlainText()); ShowErrors(out.log); if(out.result.isEmpty()) return; encoding = tool.output; shaderBytes = out.result; break; } } } ShaderCompileFlags flags = m_Flags; bool found = false; for(ShaderCompileFlag &f : flags.flags) { if(f.name == "@cmdline") { f.value = ui->toolCommandLine->toPlainText(); found = true; break; } } if(!found) flags.flags.push_back({"@cmdline", ui->toolCommandLine->toPlainText()}); m_Modified = false; m_SaveCallback(&m_Ctx, this, m_EditingShader, m_Stage, encoding, flags, ui->entryFunc->text(), shaderBytes); } } void ShaderViewer::on_intView_clicked() { ui->intView->setChecked(true); ui->floatView->setChecked(false); updateDebugState(); } void ShaderViewer::on_floatView_clicked() { ui->floatView->setChecked(true); ui->intView->setChecked(false); updateDebugState(); } void ShaderViewer::on_debugToggle_clicked() { if(isSourceDebugging()) gotoDisassemblyDebugging(); else gotoSourceDebugging(); updateDebugState(); } void ShaderViewer::on_resources_sortByStep_clicked() { m_AccessedResourceView = AccessedResourceView::SortByStep; ui->resources_sortByStep->setChecked(true); ui->resources_sortByResource->setChecked(false); updateAccessedResources(); } void ShaderViewer::on_resources_sortByResource_clicked() { m_AccessedResourceView = AccessedResourceView::SortByResource; ui->resources_sortByResource->setChecked(true); ui->resources_sortByStep->setChecked(false); updateAccessedResources(); } ScintillaEdit *ShaderViewer::currentScintilla() { ScintillaEdit *cur = qobject_cast(QApplication::focusWidget()); if(cur == NULL) { for(ScintillaEdit *s : m_Scintillas) { if(s->isVisible()) { cur = s; break; } } } return cur; } ScintillaEdit *ShaderViewer::nextScintilla(ScintillaEdit *cur) { for(int i = 0; i < m_Scintillas.count(); i++) { if(m_Scintillas[i] == cur) { if(i + 1 < m_Scintillas.count()) return m_Scintillas[i + 1]; return m_Scintillas[0]; } } if(!m_Scintillas.isEmpty()) return m_Scintillas[0]; return NULL; } void ShaderViewer::find(bool down) { ScintillaEdit *cur = currentScintilla(); if(!cur) return; QString find = m_FindReplace->findText(); sptr_t flags = 0; if(m_FindReplace->matchCase()) flags |= SCFIND_MATCHCASE; if(m_FindReplace->matchWord()) flags |= SCFIND_WHOLEWORD; if(m_FindReplace->regexp()) flags |= SCFIND_REGEXP | SCFIND_POSIX; FindReplace::SearchContext context = m_FindReplace->context(); QString findHash = QFormatStr("%1%2%3%4").arg(find).arg(flags).arg((int)context).arg(down); if(findHash != m_FindState.hash) { m_FindState.hash = findHash; m_FindState.start = 0; m_FindState.end = cur->length(); m_FindState.offset = cur->currentPos(); if(down && cur->selectionStart() == m_FindState.offset && cur->selectionEnd() - m_FindState.offset == find.length()) m_FindState.offset += find.length(); } int start = m_FindState.start + m_FindState.offset; int end = m_FindState.end; if(!down) end = m_FindState.start; QPair result = cur->findText(flags, find.toUtf8().data(), start, end); m_FindState.prevResult = result; if(result.first == -1) { sptr_t maxOffset = down ? 0 : m_FindState.end; // if we're at offset 0 searching down, there are no results. Same for offset max and searching // up if(m_FindState.offset == maxOffset) return; // otherwise, we can wrap the search around if(context == FindReplace::AllFiles) { cur = nextScintilla(cur); ToolWindowManager::raiseToolWindow(cur); cur->activateWindow(); cur->QWidget::setFocus(); } m_FindState.offset = maxOffset; start = m_FindState.start + m_FindState.offset; end = m_FindState.end; if(!down) end = m_FindState.start; result = cur->findText(flags, find.toUtf8().data(), start, end); m_FindState.prevResult = result; if(result.first == -1) return; } cur->setSelection(result.first, result.second); ensureLineScrolled(cur, cur->lineFromPosition(result.first)); if(down) m_FindState.offset = result.second - m_FindState.start; else m_FindState.offset = result.first - m_FindState.start; } void ShaderViewer::performFind() { find(m_FindReplace->direction() == FindReplace::Down); } void ShaderViewer::performFindAll() { ScintillaEdit *cur = currentScintilla(); if(!cur) return; QString find = m_FindReplace->findText(); sptr_t flags = 0; QString results = tr("Find all \"%1\"").arg(find); if(m_FindReplace->matchCase()) { flags |= SCFIND_MATCHCASE; results += tr(", Match case"); } if(m_FindReplace->matchWord()) { flags |= SCFIND_WHOLEWORD; results += tr(", Match whole word"); } if(m_FindReplace->regexp()) { flags |= SCFIND_REGEXP | SCFIND_POSIX; results += tr(", with Regular Expressions"); } FindReplace::SearchContext context = m_FindReplace->context(); if(context == FindReplace::File) results += tr(", in current file\n"); else results += tr(", in all files\n"); // trash the find state for any incremental finds m_FindState = FindState(); QList scintillas = m_Scintillas; if(context == FindReplace::File) scintillas = {cur}; QList> resultList; QByteArray findUtf8 = find.toUtf8(); for(ScintillaEdit *s : scintillas) { sptr_t start = 0; sptr_t end = s->length(); s->setIndicatorCurrent(INDICATOR_FINDRESULT); s->indicatorClearRange(start, end); if(findUtf8.isEmpty()) continue; QPair result; do { result = s->findText(flags, findUtf8.data(), start, end); if(result.first >= 0) { int line = s->lineFromPosition(result.first); sptr_t lineStart = s->positionFromLine(line); sptr_t lineEnd = s->lineEndPosition(line); s->indicatorFillRange(result.first, result.second - result.first); QString lineText = QString::fromUtf8(s->textRange(lineStart, lineEnd)); results += QFormatStr(" %1(%2): ").arg(s->windowTitle()).arg(line, 4); int startPos = results.length(); results += lineText; results += lit("\n"); resultList.push_back( qMakePair(result.first - lineStart + startPos, result.second - lineStart + startPos)); } start = result.second; } while(result.first >= 0); } if(findUtf8.isEmpty()) return; results += tr("Matching lines: %1").arg(resultList.count()); m_FindResults->setReadOnly(false); m_FindResults->setText(results.toUtf8().data()); m_FindResults->setIndicatorCurrent(INDICATOR_FINDRESULT); for(QPair r : resultList) m_FindResults->indicatorFillRange(r.first, r.second - r.first); m_FindResults->setReadOnly(true); if(m_FindResults->isVisible()) { ToolWindowManager::raiseToolWindow(m_FindResults); } else { ui->docking->moveToolWindow(m_FindResults, ToolWindowManager::AreaReference(ToolWindowManager::BottomOf, ui->docking->areaOf(cur), 0.2f)); ui->docking->setToolWindowProperties(m_FindResults, ToolWindowManager::HideOnClose); } } void ShaderViewer::performReplace() { ScintillaEdit *cur = currentScintilla(); if(!cur) return; QString find = m_FindReplace->findText(); if(find.isEmpty()) return; sptr_t flags = 0; if(m_FindReplace->matchCase()) flags |= SCFIND_MATCHCASE; if(m_FindReplace->matchWord()) flags |= SCFIND_WHOLEWORD; if(m_FindReplace->regexp()) flags |= SCFIND_REGEXP | SCFIND_POSIX; FindReplace::SearchContext context = m_FindReplace->context(); QString findHash = QFormatStr("%1%2%3").arg(find).arg(flags).arg((int)context); // if we didn't have a valid previous find, just do a find and bail if(findHash != m_FindState.hash) { performFind(); return; } if(m_FindState.prevResult.first == -1) return; cur->setTargetRange(m_FindState.prevResult.first, m_FindState.prevResult.second); FindState save = m_FindState; QString replaceText = m_FindReplace->replaceText(); // otherwise we have a valid previous find. Do the replace now // note this will invalidate the find state (as most user operations would), so we save/restore // the state if(m_FindReplace->regexp()) cur->replaceTargetRE(-1, replaceText.toUtf8().data()); else cur->replaceTarget(-1, replaceText.toUtf8().data()); m_FindState = save; // adjust the offset if we replaced text and it went up or down in size m_FindState.offset += (replaceText.count() - find.count()); // move to the next result performFind(); } void ShaderViewer::performReplaceAll() { ScintillaEdit *cur = currentScintilla(); if(!cur) return; QString find = m_FindReplace->findText(); QString replace = m_FindReplace->replaceText(); if(find.isEmpty()) return; sptr_t flags = 0; if(m_FindReplace->matchCase()) flags |= SCFIND_MATCHCASE; if(m_FindReplace->matchWord()) flags |= SCFIND_WHOLEWORD; if(m_FindReplace->regexp()) flags |= SCFIND_REGEXP | SCFIND_POSIX; FindReplace::SearchContext context = m_FindReplace->context(); (void)context; // trash the find state for any incremental finds m_FindState = FindState(); QList scintillas = m_Scintillas; if(context == FindReplace::File) scintillas = {cur}; int numReplacements = 1; for(ScintillaEdit *s : scintillas) { sptr_t start = 0; sptr_t end = s->length(); QPair result; QByteArray findUtf8 = find.toUtf8(); QByteArray replaceUtf8 = replace.toUtf8(); do { result = s->findText(flags, findUtf8.data(), start, end); if(result.first >= 0) { s->setTargetRange(result.first, result.second); if(m_FindReplace->regexp()) s->replaceTargetRE(-1, replaceUtf8.data()); else s->replaceTarget(-1, replaceUtf8.data()); numReplacements++; } start = result.second + (replaceUtf8.count() - findUtf8.count()); } while(result.first >= 0); } RDDialog::information( this, tr("Replace all"), tr("%1 replacements made in %2 files").arg(numReplacements).arg(scintillas.count())); }