diff --git a/qrenderdoc/Windows/EventBrowser.cpp b/qrenderdoc/Windows/EventBrowser.cpp index bf826b11a..93e028c2b 100644 --- a/qrenderdoc/Windows/EventBrowser.cpp +++ b/qrenderdoc/Windows/EventBrowser.cpp @@ -1501,6 +1501,7 @@ public: MAKE_BUILTIN_FILTER(dispatch); MAKE_BUILTIN_FILTER(childOf); MAKE_BUILTIN_FILTER(parent); + MAKE_BUILTIN_FILTER(annot); /* m_BuiltinFilters[lit("event")].completer = [this](ICaptureContext *ctx, QString name, @@ -1686,11 +1687,11 @@ private: QString text; }; - static QList tokenise(QString parameters) + static QList tokenise(QString parameters, QString extraChars = QString()) { QList ret; - const QString operatorChars = lit("<>=:&"); + const QString operatorChars = lit("<>=:&") + extraChars; int p = 0; while(p < parameters.size()) @@ -1883,6 +1884,300 @@ searched for as a case-insensitive substring. }; } + QString filterDescription_annot() const + { + return tr(R"EOD( +

$annot

+ +
+ + +
$annot(annotation condition)
+$annot(annotation condition)
- passes if a given annotation condition is true.
+ +

+This filter queries the annotations of an event to either exist or match simple conditions. A +condition may be omitted, but an annotation must be specified. If no condition is given then +the filter passes if that annotation is exists. +

+ +

+If the annotation contains spaces, it should be surrounded with double quotes, e.g. "foo bar.sub.key" +

+ +

+For numeric annotations you can compare with the normal comparison operators. For string annotations you +can check exact equality with ==, use the keyword 'contains' to check for a case-insensitive substring +match, or =~ to match with a regex. +

+ +

+Numeric properties will always fail to match the filter when used with a string comparison operator, +and vice-versa. +

+)EOD", + "EventFilterModel"); + } + + IEventBrowser::EventFilterCallback filterFunction_annot(QString name, QString parameters, + ParseTrace &trace) + { + parameters = parameters.trimmed(); + + rdcstr keyPath; + + if(parameters[0] == QLatin1Char('"')) + { + int i = 1; + while(i < parameters.size() && parameters[i] != QLatin1Char('"')) + { + if(parameters[i] == QLatin1Char('\\') && parameters[i] == QLatin1Char('"')) + { + keyPath += parameters[i + 1]; + i++; + } + i++; + } + + if(i >= parameters.size()) + { + trace.setError(tr("Invalid quoted annotation, no closing quote found", "EventFilterModel")); + return NULL; + } + + keyPath = parameters.mid(0, i); + + i++; + + parameters = parameters.mid(i).trimmed(); + } + else + { + int i = parameters.indexOf(QLatin1Char(' ')); + if(i < 0) + { + keyPath = parameters; + parameters.clear(); + } + keyPath = parameters.mid(0, i); + parameters = parameters.mid(i).trimmed(); + } + + QList tokens = tokenise(parameters, lit("~")); + + std::function comparer; + + // no parameters, just return if the annotation exists + if(tokens.isEmpty()) + { + } + else + { + if(tokens.size() == 1) + { + trace.setError(tr("Invalid annotation expression, expected an operator and value", + "EventFilterModel")); + return NULL; + } + + QString op = tokens[0].text; + QString param = parameters.mid(tokens[1].position); + + static const QStringList numericOperators = { + lit("="), lit("=="), lit("!="), lit("<"), lit(">"), lit("<="), lit(">="), + }; + + // any numeric value that can be compared to a number + if(numericOperators.contains(op)) + { + int operatorIdx = numericOperators.indexOf(op); + + // need to be a bit polymorphic here as we don't know what we'll be comparing against so we + // don't know the type. We at least upcast to the biggest type to reduce variations + int64_t s64 = param.toLongLong(); + uint64_t u64 = param.toULongLong(); + double d = param.toDouble(); + + comparer = [operatorIdx, d, u64, s64, param](const SDObject *obj) { + switch(obj->type.basetype) + { + case SDBasic::Chunk: + case SDBasic::Resource: + case SDBasic::GPUAddress: + case SDBasic::Struct: + case SDBasic::Array: + case SDBasic::Null: + case SDBasic::Buffer: + return false; + // ambiguity with string operators here, could be an equality check on strings + case SDBasic::String: + if(operatorIdx <= 1) + return QString(obj->data.str) == param; + return false; + case SDBasic::Boolean: + case SDBasic::Enum: + case SDBasic::UnsignedInteger: + case SDBasic::Character: + { + switch(operatorIdx) + { + case 0: + case 1: return obj->data.basic.u == u64; + case 2: return obj->data.basic.u != u64; + case 3: return obj->data.basic.u < u64; + case 4: return obj->data.basic.u > u64; + case 5: return obj->data.basic.u <= u64; + case 6: return obj->data.basic.u >= u64; + default: return false; + } + } + case SDBasic::SignedInteger: + { + switch(operatorIdx) + { + case 0: + case 1: return obj->data.basic.i == s64; + case 2: return obj->data.basic.i != s64; + case 3: return obj->data.basic.i < s64; + case 4: return obj->data.basic.i > s64; + case 5: return obj->data.basic.i <= s64; + case 6: return obj->data.basic.i >= s64; + default: return false; + } + } + case SDBasic::Float: + { + switch(operatorIdx) + { + case 0: + case 1: return obj->data.basic.d == d; + case 2: return obj->data.basic.d != d; + case 3: return obj->data.basic.d < d; + case 4: return obj->data.basic.d > d; + case 5: return obj->data.basic.d <= d; + case 6: return obj->data.basic.d >= d; + default: return false; + } + } + } + + return false; + }; + } + else if(op == lit("contains") || op == lit("=~")) + { + bool contains = op == lit("contains"); + + QRegularExpression regex; + if(!contains) + { + int end = 0; + + if(param.size() < 2 || param[0] != QLatin1Char('/')) + { + trace.setError(tr("Parameter to =~ should be /regex/", "EventFilterModel")); + return NULL; + } + + for(int i = 1; i < param.size(); i++) + { + if(param[i] == QLatin1Char('\\')) + { + i++; + continue; + } + + if(param[i] == QLatin1Char('/')) + { + end = i; + break; + } + } + + if(end == 0) + { + trace.setError(tr("Unterminated regex", "EventFilterModel")); + return NULL; + } + + QString opts = param.right(param.size() - end - 1); + + QRegularExpression::PatternOptions reOpts = QRegularExpression::NoPatternOption; + + for(QChar c : opts) + { + switch(c.toLatin1()) + { + case 'i': reOpts |= QRegularExpression::CaseInsensitiveOption; break; + case 'x': reOpts |= QRegularExpression::ExtendedPatternSyntaxOption; break; + case 'u': reOpts |= QRegularExpression::UseUnicodePropertiesOption; break; + default: + trace.setError(tr("Unexpected option '%1' after regex", "EventFilterModel").arg(c)); + return NULL; + } + } + + regex = QRegularExpression(param.mid(1, end - 1)); + + if(!regex.isValid()) + { + trace.setError(tr("Invalid regex", "EventFilterModel")); + return NULL; + } + } + + param = param.toLower(); + + comparer = [contains, param, regex](const SDObject *obj) { + if(obj->type.basetype == SDBasic::String) + { + if(contains) + { + QString str = obj->data.str; + str = str.toLower(); + return str.contains(param); + } + else + { + QRegularExpressionMatch match = regex.match(QString(obj->data.str)); + return match.isValid() && match.hasMatch(); + } + } + + return false; + }; + } + else + { + trace.setError(tr("Unrecognised annotation condition", "EventFilterModel")); + return NULL; + } + } + + return [keyPath, comparer](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId, + const SDChunk *, const ActionDescription *action, const rdcstr &) { + if(action) + { + for(const APIEvent &e : action->events) + { + if(e.eventId == eventId) + { + const SDObject *annot = e.annotations ? e.annotations->FindChildByKeyPath(keyPath) : NULL; + + if(annot == NULL) + return false; + + if(comparer) + return comparer(annot); + else + return annot != NULL; + } + } + } + return false; + }; + } + QString filterDescription_event() const { return tr(R"EOD(