14 constexpr int kMinCanvasHeight = 110;
15 constexpr float kLineWidth = 2.0f;
16 constexpr float kHoverLineWidth = 1.0f;
17 constexpr float kHoverPointRadius = 4.0f;
18 constexpr int kPlotMarginLeft = 8;
19 constexpr int kPlotMarginRight = 8;
20 constexpr int kPlotMarginTop = 6;
21 constexpr int kPlotMarginBottomOffset = 4;
22 constexpr int kLabelOffset = 2;
23 constexpr int kGridLines = 3;
24 constexpr int kGraphBucketsPerPixel = 2;
26 int nearestIndexByX(
const std::vector<QPointF>& pts, qreal mx) {
27 if (pts.empty())
return -1;
28 const auto it = std::lower_bound(pts.begin(), pts.end(), mx,
29 [](
const QPointF& p, qreal x) { return p.x() < x; });
30 if (it == pts.begin()) {
33 if (it == pts.end()) {
34 return static_cast<int>(pts.size() - 1);
36 const int i1 =
static_cast<int>(it - pts.begin());
37 const int i0 = i1 - 1;
38 const qreal d0 = std::abs(pts[
static_cast<size_t>(i0)].x() - mx);
39 const qreal d1 = std::abs(pts[
static_cast<size_t>(i1)].x() - mx);
40 return d0 <= d1 ? i0 : i1;
46 setMouseTracking(
true);
47 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
48 setMinimumHeight(kMinCanvasHeight);
49 resizeRebuildTimer.setSingleShot(
true);
50 resizeRebuildTimer.setInterval(250);
51 connect(&resizeRebuildTimer, &QTimer::timeout,
this, [
this]() {
52 requestPointRebuild();
57 const std::array<std::pair<float, float>,
kPlottableMetricCount>& valueMinMax,
float tMinP,
float tMaxP) {
58 framesData = (frames && !frames->empty()) ? std::move(frames) :
nullptr;
60 valueMinMaxPerMetric = valueMinMax;
64 valueMinMaxPerMetric = {};
68 requestPointRebuild();
73 valueMinMaxPerMetric = {};
77 graphPointFrameIndices.clear();
79 rebuildGeneration.fetch_add(1, std::memory_order_relaxed);
86 currentMetric = metric;
87 requestPointRebuild();
91 QWidget::paintEvent(event);
92 if (cacheDirty || baseCache.isNull()) {
96 QPainter painter(
this);
97 painter.drawPixmap(0, 0, baseCache);
99 if (hoverIndex >= 0 && hoverIndex <
static_cast<int>(graphPoints.size())) {
100 painter.setRenderHint(QPainter::Antialiasing,
true);
101 const QPalette pal = palette();
102 const QRect rect = plotRect();
103 const QColor hoverColor = pal.color(QPalette::Highlight);
104 const QPointF point = graphPoints[hoverIndex];
105 painter.setPen(QPen(hoverColor, kHoverLineWidth, Qt::DashLine));
106 painter.drawLine(QPointF(point.x(), rect.top()), QPointF(point.x(), rect.bottom()));
107 painter.setBrush(hoverColor);
108 painter.setPen(Qt::NoPen);
109 painter.drawEllipse(point, kHoverPointRadius, kHoverPointRadius);
113void FrameGraphCanvas::rebuildBaseCache() {
114 const qreal ratio = devicePixelRatioF();
115 baseCache = QPixmap(size() * ratio);
116 baseCache.setDevicePixelRatio(ratio);
117 baseCache.fill(Qt::transparent);
119 QPainter painter(&baseCache);
120 painter.setRenderHint(QPainter::Antialiasing,
true);
121 const QRect rect = plotRect();
122 const QPalette pal = palette();
123 const QColor panelColor = pal.color(QPalette::Window);
124 const QColor plotColor = pal.color(QPalette::Base);
125 const QColor borderColor = pal.color(QPalette::Mid);
126 const QColor textColor = pal.color(QPalette::Text);
127 const QColor mutedText = pal.color(QPalette::Midlight);
128 const QColor lineColor = pal.color(QPalette::Highlight);
129 painter.fillRect(this->rect(), panelColor);
130 painter.fillRect(rect, plotColor);
131 painter.setPen(borderColor);
132 painter.drawRect(rect);
133 painter.setPen(mutedText);
134 for (
int i = 1; i < kGridLines; ++i) {
135 const int y = rect.top() + (rect.height() * i) / kGridLines;
136 painter.drawLine(rect.left(), y, rect.right(), y);
138 painter.setPen(textColor);
139 painter.drawText(QRect(rect.left(), rect.bottom() + kLabelOffset, rect.width(), bottomLabelHeight()),
140 Qt::AlignRight | Qt::AlignVCenter,
142 if (graphPoints.empty() || !framesData) {
143 painter.setPen(mutedText);
144 painter.drawText(rect, Qt::AlignCenter, tr(
"Select a simulated object to view its history."));
150 path.moveTo(graphPoints.front());
151 for (
size_t i = 1; i < graphPoints.size(); ++i) {
152 path.lineTo(graphPoints[i]);
154 painter.setPen(QPen(lineColor, kLineWidth));
155 painter.drawPath(path);
160 const QRect rect = plotRect();
161 if (graphPoints.empty() || !rect.contains(event->position().toPoint())) {
162 if (hoverIndex != -1) {
164 QToolTip::hideText();
169 const int nearestIndex = nearestIndexByX(graphPoints, event->position().x());
170 if (nearestIndex < 0)
return;
171 if (nearestIndex == hoverIndex)
return;
173 hoverIndex = nearestIndex;
174 const ObjectSnapshot& sample = (*framesData)[graphPointFrameIndices[
static_cast<size_t>(nearestIndex)]];
176 QToolTip::showText(event->globalPosition().toPoint(),
178 .arg(sample.
time, 0,
'f', 3)
180 .arg(value, 0,
'f', 4),
187 QWidget::leaveEvent(event);
189 QToolTip::hideText();
194 QWidget::resizeEvent(event);
196 QToolTip::hideText();
197 resizeRebuildTimer.start();
200int FrameGraphCanvas::bottomLabelHeight()
const {
201 return fontMetrics().height() + kLabelOffset;
204QRect FrameGraphCanvas::plotRect()
const {
205 const int bottom = bottomLabelHeight() + kPlotMarginBottomOffset;
206 return rect().adjusted(kPlotMarginLeft, kPlotMarginTop, -kPlotMarginRight, -bottom);
209void FrameGraphCanvas::requestPointRebuild() {
211 const auto frames = framesData;
212 const uint64_t generation = rebuildGeneration.fetch_add(1, std::memory_order_relaxed) + 1;
213 const QRect rect = plotRect();
214 if (!frames || frames->empty() || rect.width() <= 1 || rect.height() <= 1) {
215 applyRebuiltPoints(generation, {}, {});
219 const int m =
static_cast<int>(currentMetric);
221 applyRebuiltPoints(generation, {}, {});
225 const auto valueRange = valueMinMaxPerMetric;
226 const float minTime = tMin;
227 const float maxTime = tMax;
228 const Metric metric = currentMetric;
229 const QPointer<FrameGraphCanvas> self(
this);
231 std::thread([self, frames, valueRange, minTime, maxTime, metric, m, rect, generation]() {
232 std::vector<QPointF> points;
233 std::vector<size_t> frameIndices;
235 const float minValue = valueRange[
static_cast<size_t>(m)].first;
236 const float maxValue = valueRange[
static_cast<size_t>(m)].second;
237 const float invTime = maxTime > minTime ? 1.0f / (maxTime - minTime) : 0.0f;
238 const float invValue = maxValue > minValue ? 1.0f / (maxValue - minValue) : 0.0f;
242 qreal minY = std::numeric_limits<qreal>::max();
243 qreal maxY = std::numeric_limits<qreal>::lowest();
248 const int bucketCount = std::max(1, rect.width() * kGraphBucketsPerPixel);
249 std::vector<Bucket> buckets(
static_cast<size_t>(bucketCount));
251 for (
size_t i = 0; i < frames->size(); ++i) {
252 const auto& frame = (*frames)[i];
253 const float timeAlpha = (frame.time - minTime) * invTime;
255 if (!std::isfinite(timeAlpha) || !std::isfinite(valueAlpha))
continue;
257 const qreal x = rect.left() +
static_cast<qreal
>(timeAlpha) * rect.width();
258 const qreal y = rect.bottom() -
static_cast<qreal
>(valueAlpha) * rect.height();
259 if (!std::isfinite(x) || !std::isfinite(y))
continue;
261 const int bucketIndex = std::clamp(
static_cast<int>(timeAlpha *
static_cast<float>(bucketCount - 1)), 0, bucketCount - 1);
262 Bucket& bucket = buckets[
static_cast<size_t>(bucketIndex)];
265 if (y < bucket.minY) {
269 if (y > bucket.maxY) {
275 points.reserve(
static_cast<size_t>(bucketCount * 2));
276 frameIndices.reserve(points.capacity());
278 for (
const Bucket& bucket : buckets) {
279 if (!bucket.used)
continue;
280 if (bucket.minIndex <= bucket.maxIndex) {
281 points.emplace_back(bucket.x, bucket.minY);
282 frameIndices.push_back(bucket.minIndex);
283 if (bucket.maxY != bucket.minY) {
284 points.emplace_back(bucket.x, bucket.maxY);
285 frameIndices.push_back(bucket.maxIndex);
288 points.emplace_back(bucket.x, bucket.maxY);
289 frameIndices.push_back(bucket.maxIndex);
290 if (bucket.maxY != bucket.minY) {
291 points.emplace_back(bucket.x, bucket.minY);
292 frameIndices.push_back(bucket.minIndex);
298 QMetaObject::invokeMethod(self.data(), [self, generation, points = std::move(points), frameIndices = std::move(frameIndices)]()
mutable {
300 self->applyRebuiltPoints(generation, std::move(points), std::move(frameIndices));
301 }, Qt::QueuedConnection);
305void FrameGraphCanvas::invalidateCache() {
309void FrameGraphCanvas::applyRebuiltPoints(uint64_t generation, std::vector<QPointF> points, std::vector<size_t> frameIndices) {
310 if (generation != rebuildGeneration.load(std::memory_order_relaxed))
return;
311 graphPoints = std::move(points);
312 graphPointFrameIndices = std::move(frameIndices);
constexpr std::size_t kPlottableMetricCount
QString metricLabel(Metric metric)
float objectSnapshotValue(Metric metric, const ObjectSnapshot &s)
void mouseMoveEvent(QMouseEvent *event) override
void setSharedData(std::shared_ptr< const std::vector< ObjectSnapshot > > frames, const std::array< std::pair< float, float >, kPlottableMetricCount > &valueMinMax, float tMin, float tMax)
void resizeEvent(QResizeEvent *event) override
FrameGraphCanvas(QWidget *parent=nullptr)
void leaveEvent(QEvent *event) override
void setMetric(Metric metric)
void paintEvent(QPaintEvent *event) override