@@ -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