Skip to content

Commit d04f256

Browse files
authored
Optimize drawing (#154)
* Try to fix performance when zoomed in vertically * Optimize signal rendering to improve performance on high zoom levels
1 parent d0236b9 commit d04f256

1 file changed

Lines changed: 115 additions & 16 deletions

File tree

src/gui/signal_browser/signal_graphics_item.cpp

Lines changed: 115 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -290,37 +290,124 @@ void SignalGraphicsItem::paint (QPainter* painter, const QStyleOptionGraphicsIte
290290
// }
291291

292292
painter->translate (0, height_ / 2.0f);
293+
294+
// When y_zoom_ is very large (zoomed in on amplitude), sample values outside
295+
// the visible amplitude window map to screen Y coordinates of millions of
296+
// pixels. We must clamp them before giving them to QPainter, because its
297+
// internal 26.6 fixed-point arithmetic overflows beyond ~33 million px, and
298+
// even well short of that the rasterizer spends significant time processing
299+
// enormous geometry.
300+
//
301+
// We do NOT set a vertical painter clip here: signals are intentionally
302+
// allowed to "overflow" visually into neighbouring channel lanes, so the
303+
// only hard boundary we enforce is a large-but-finite coordinate clamp.
304+
// The clamp value is chosen so that any line crossing the channel boundary
305+
// is rasterised with the correct entry-angle (the clamped end-point is far
306+
// enough off-screen that the intersection geometry is exact).
307+
const double SCENE_HALF_H = 32000.0; // well within QPainter's fixed-point range
308+
const double YMIN = -SCENE_HALF_H;
309+
const double YMAX = SCENE_HALF_H;
310+
// Y_OVERDRAW is unused in the polyline path but kept for the minmax path below.
311+
const double Y_OVERDRAW = (double)height_;
312+
293313
painter->setPen (color_manager_->getChannelColor (id_));
294314

295-
if (pixel_per_sample >= 1.0)
315+
// On HiDPI / Retina displays the same logical canvas is rendered at a higher
316+
// physical pixel density. The polyline path (connected per-sample points)
317+
// only produces a visibly smoother result than the minmax envelope path when
318+
// there is at least one *physical* device pixel per sample. Below that
319+
// density the two paths are indistinguishable — at pixel_per_sample == 1.0
320+
// on a 2× Retina screen every logical-pixel column still contains exactly
321+
// one sample, so min == max and the minmax bar is a single point, identical
322+
// to the polyline point — but the polyline path is more expensive because
323+
// CoreGraphics must compute miter joints for every connected segment at full
324+
// physical resolution. Using the device pixel ratio as the threshold lets
325+
// the cheaper minmax path handle the common "default zoom" case on Retina.
326+
const double dpr = std::max(1.0, painter->device()->devicePixelRatioF());
327+
328+
if (pixel_per_sample >= dpr)
296329
{
297-
// Full-resolution path: accumulate points and draw as a single polyline per
298-
// contiguous (non-NaN) segment. One drawPolyline call replaces O(N) drawLine
299-
// calls, significantly reducing QPainter overhead.
330+
// Full-resolution path: build a polyline per contiguous (non-NaN) segment.
331+
//
332+
// When the signal is zoomed in vertically, many consecutive samples share
333+
// the same clamped OOB Y value (all above or all below the visible range).
334+
// Drawing all such points is wasteful — a run of N identical-Y points is
335+
// just a horizontal line segment that only needs its first and last endpoints.
336+
// We collapse each OOB run down to at most 2 points, keeping the polygon
337+
// size proportional to the number of vertically-visible samples rather than
338+
// the total number of samples in the horizontal clip window.
300339
QPolygonF points;
301340
points.reserve(static_cast<int>(data_block->size()));
302341

342+
const double oob_above_y = YMIN - Y_OVERDRAW; // clamped Y for above-range samples
343+
const double oob_below_y = YMAX + Y_OVERDRAW; // clamped Y for below-range samples
344+
345+
// State for the current OOB run.
346+
bool in_oob_run = false;
347+
double oob_run_y = 0.0; // clamped Y of the active OOB run
348+
double oob_run_end_x = 0.0; // X of the most-recent sample in this run
349+
350+
// Emit the pending OOB run end-point (if different from the run start).
351+
// Called whenever we leave an OOB run: back in-range, direction change, or NaN.
352+
auto flush_oob_endpoint = [&]() {
353+
if (in_oob_run) {
354+
if (!points.empty() && points.last().x() != oob_run_end_x)
355+
points.append(QPointF(oob_run_end_x, oob_run_y));
356+
in_oob_run = false;
357+
}
358+
};
359+
auto flush_segment = [&]() {
360+
flush_oob_endpoint();
361+
if (points.size() > 1)
362+
painter->drawPolyline(points);
363+
else if (points.size() == 1)
364+
painter->drawPoint(points.first());
365+
points.clear();
366+
};
367+
303368
for (int index = 0; index < static_cast<int>(data_block->size()); index++)
304369
{
305370
float64 y = (*data_block)[index];
306371
if (std::isnan(y))
307372
{
308-
if (points.size() > 1)
309-
painter->drawPolyline(points);
310-
else if (points.size() == 1)
311-
painter->drawPoint(points.first());
312-
points.clear();
373+
flush_segment();
313374
}
314375
else
315376
{
316-
points.append(QPointF(last_x, y_offset_ - y_zoom_ * y));
377+
double raw_y = y_offset_ - y_zoom_ * y;
378+
double clamped_y;
379+
if (raw_y < oob_above_y) clamped_y = oob_above_y;
380+
else if (raw_y > oob_below_y) clamped_y = oob_below_y;
381+
else clamped_y = raw_y;
382+
383+
const bool is_oob = (raw_y < oob_above_y || raw_y > oob_below_y);
384+
385+
if (is_oob && in_oob_run && clamped_y == oob_run_y)
386+
{
387+
// Continuing the same OOB run: just slide the pending end-point.
388+
oob_run_end_x = last_x;
389+
}
390+
else if (is_oob)
391+
{
392+
// New OOB run (or direction change): close the previous OOB run
393+
// and open a new one, emitting the first point immediately.
394+
flush_oob_endpoint();
395+
points.append(QPointF(last_x, clamped_y));
396+
in_oob_run = true;
397+
oob_run_y = clamped_y;
398+
oob_run_end_x = last_x;
399+
}
400+
else
401+
{
402+
// In-range point: close any OOB run first (emits its end-point),
403+
// then add this point normally.
404+
flush_oob_endpoint();
405+
points.append(QPointF(last_x, clamped_y));
406+
}
317407
}
318408
last_x += pixel_per_sample;
319409
}
320-
if (points.size() > 1)
321-
painter->drawPolyline(points);
322-
else if (points.size() == 1)
323-
painter->drawPoint(points.first());
410+
flush_segment();
324411
}
325412
else
326413
{
@@ -414,13 +501,25 @@ void SignalGraphicsItem::paint (QPainter* painter, const QStyleOptionGraphicsIte
414501
double sy_max = y_offset_ - y_zoom_ * ymax;
415502
double sy_min = y_offset_ - y_zoom_ * ymin;
416503

504+
// sy_max is the screen-Y of the amplitude maximum (top of bar,
505+
// smaller Y value); sy_min is the screen-Y of the amplitude
506+
// minimum (bottom of bar, larger Y value).
507+
// Clamp to the safe coordinate range to prevent QPainter 26.6
508+
// fixed-point overflow when y_zoom_ is very large.
509+
sy_max = std::max(sy_max, YMIN);
510+
sy_min = std::min(sy_min, YMAX);
511+
417512
// Draw a diagonal connector from the midpoint of the previous
418513
// bar to the midpoint of this bar. This fills any vertical gap
419514
// between adjacent non-overlapping bars without inflating the
420515
// shown amplitude range.
421516
if (!std::isnan(prev_mid))
422-
painter->drawLine(QPointF(screen_x - 1.0, y_offset_ - y_zoom_ * prev_mid),
423-
QPointF(screen_x, y_offset_ - y_zoom_ * cur_mid));
517+
{
518+
double prev_sy = std::max(YMIN, std::min(YMAX, y_offset_ - y_zoom_ * (double)prev_mid));
519+
double cur_sy = std::max(YMIN, std::min(YMAX, y_offset_ - y_zoom_ * (double)cur_mid));
520+
painter->drawLine(QPointF(screen_x - 1.0, prev_sy),
521+
QPointF(screen_x, cur_sy));
522+
}
424523

425524
// Overdraw the precise min–max bar on top.
426525
painter->drawLine(QPointF(screen_x, sy_max),

0 commit comments

Comments
 (0)