1 /* wireless_timeline.cpp
2  * GUI to show an 802.11 wireless timeline of packets
3  *
4  * Wireshark - Network traffic analyzer
5  * By Gerald Combs <gerald@wireshark.org>
6  * Copyright 1998 Gerald Combs
7  *
8  * Copyright 2012 Parc Inc and Samsung Electronics
9  * Copyright 2015, 2016 & 2017 Cisco Inc
10  *
11  * SPDX-License-Identifier: GPL-2.0-or-later
12  */
13 
14 #include "wireless_timeline.h"
15 
16 #include <epan/packet.h>
17 #include <epan/prefs.h>
18 #include <epan/proto_data.h>
19 #include <epan/packet_info.h>
20 #include <epan/column-utils.h>
21 #include <epan/tap.h>
22 
23 #include <cmath>
24 
25 #include "globals.h"
26 #include <epan/dissectors/packet-ieee80211-radio.h>
27 
28 #include <epan/color_filters.h>
29 #include "frame_tvbuff.h"
30 
31 #include <ui/qt/utils/color_utils.h>
32 #include <ui/qt/utils/qt_ui_utils.h>
33 #include "wireshark_application.h"
34 #include <wsutil/report_message.h>
35 #include <wsutil/utf8_entities.h>
36 
37 #ifdef Q_OS_WIN
38 #include "wsutil/file_util.h"
39 #include <QSysInfo>
40 #endif
41 
42 #include <QPaintEvent>
43 #include <QPainter>
44 #include <QGraphicsScene>
45 #include <QToolTip>
46 
47 #include "packet_list.h"
48 #include <ui/qt/models/packet_list_model.h>
49 
50 /* we start rendering this number of microseconds left of the left edge - to ensure
51  * NAV lines are drawn correctly, and that small errors in time order don't prevent some
52  * frames from being rendered.
53  * These errors in time order can come from generators that record PHY rate incorrectly
54  * in some circumstances.
55  */
56 #define RENDER_EARLY 40000
57 
58 
59 const float fraction = 0.8F;
60 const float base = 0.1F;
61 class pcolor : public QColor
62 {
63 public:
pcolor(float red,float green,float blue)64     inline pcolor(float red, float green, float blue) : QColor(
65             (int) (255*(red * fraction + base)),
66             (int) (255*(green * fraction + base)),
67             (int) (255*(blue * fraction + base))) { }
68 };
69 
reset_rgb(float rgb[TIMELINE_HEIGHT][3])70 static void reset_rgb(float rgb[TIMELINE_HEIGHT][3])
71 {
72     int i;
73     for (i = 0; i < TIMELINE_HEIGHT; i++)
74         rgb[i][0] = rgb[i][1] = rgb[i][2] = 1.0;
75 }
76 
render_pixels(QPainter & p,gint x,gint width,float rgb[TIMELINE_HEIGHT][3],float ratio)77 static void render_pixels(QPainter &p, gint x, gint width, float rgb[TIMELINE_HEIGHT][3], float ratio)
78 {
79     int previous = 0, i;
80     for (i = 1; i <= TIMELINE_HEIGHT; i++) {
81         if (i != TIMELINE_HEIGHT &&
82                 rgb[previous][0] == rgb[i][0] &&
83                 rgb[previous][1] == rgb[i][1] &&
84                 rgb[previous][2] == rgb[i][2])
85             continue;
86         if (rgb[previous][0] != 1.0 || rgb[previous][1] != 1.0 || rgb[previous][2] != 1.0) {
87           p.fillRect(QRectF(x/ratio, previous, width/ratio, i-previous), pcolor(rgb[previous][0],rgb[previous][1],rgb[previous][2]));
88         }
89         previous = i;
90     }
91     reset_rgb(rgb);
92 }
93 
render_rectangle(QPainter & p,gint x,gint width,guint height,int dfilter,float r,float g,float b,float ratio)94 static void render_rectangle(QPainter &p, gint x, gint width, guint height, int dfilter, float r, float g, float b, float ratio)
95 {
96     p.fillRect(QRectF(x/ratio, TIMELINE_HEIGHT/2-height, width/ratio, dfilter ? height * 2 : height), pcolor(r,g,b));
97 }
98 
accumulate_rgb(float rgb[TIMELINE_HEIGHT][3],int height,int dfilter,float width,float red,float green,float blue)99 static void accumulate_rgb(float rgb[TIMELINE_HEIGHT][3], int height, int dfilter, float width, float red, float green, float blue)
100 {
101     int i;
102     for (i = TIMELINE_HEIGHT/2-height; i < (TIMELINE_HEIGHT/2 + (dfilter ? height : 0)); i++) {
103         rgb[i][0] = rgb[i][0] - width + width * red;
104         rgb[i][1] = rgb[i][1] - width + width * green;
105         rgb[i][2] = rgb[i][2] - width + width * blue;
106     }
107 }
108 
109 
mousePressEvent(QMouseEvent * event)110 void WirelessTimeline::mousePressEvent(QMouseEvent *event)
111 {
112     start_x = last_x = event->localPos().x();
113 }
114 
115 
mouseMoveEvent(QMouseEvent * event)116 void WirelessTimeline::mouseMoveEvent(QMouseEvent *event)
117 {
118     if (event->buttons() == Qt::NoButton)
119         return;
120 
121     qreal offset = event->localPos().x() - last_x;
122     last_x = event->localPos().x();
123 
124     qreal shift = ((qreal) (end_tsf - start_tsf))/width() * offset;
125     start_tsf -= shift;
126     end_tsf -= shift;
127     clip_tsf();
128 
129     // TODO: scroll by moving pixels and redraw only exposed area
130     // render(p, ...)
131     // then update full widget only on release.
132     update();
133 }
134 
135 
mouseReleaseEvent(QMouseEvent * event)136 void WirelessTimeline::mouseReleaseEvent(QMouseEvent *event)
137 {
138     QPointF localPos = event->localPos();
139     qreal offset = localPos.x() - start_x;
140 
141     /* if this was a drag, ignore it */
142     if (std::abs(offset) > 3)
143         return;
144 
145     /* this was a click */
146     guint num = find_packet(localPos.x());
147     if (num == 0)
148         return;
149 
150     frame_data *fdata = frame_data_sequence_find(cfile.provider.frames, num);
151     if (!fdata->passed_dfilter && fdata->prev_dis_num > 0)
152         num = fdata->prev_dis_num;
153 
154     cf_goto_frame(&cfile, num);
155 }
156 
157 
clip_tsf()158 void WirelessTimeline::clip_tsf()
159 {
160     // did we go past the start of the file?
161     if (((gint64) start_tsf) < ((gint64) first->start_tsf)) {
162         // align the start of the file at the left edge
163         guint64 shift = first->start_tsf - start_tsf;
164         start_tsf += shift;
165         end_tsf += shift;
166     }
167     if (end_tsf > last->end_tsf) {
168         guint64 shift = end_tsf - last->end_tsf;
169         start_tsf -= shift;
170         end_tsf -= shift;
171     }
172 }
173 
174 
selectedFrameChanged(QList<int>)175 void WirelessTimeline::selectedFrameChanged(QList<int>)
176 {
177     if (isHidden())
178         return;
179 
180     if (cfile.current_frame) {
181         struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num);
182 
183         guint left_margin = 0.9 * start_tsf + 0.1 * end_tsf;
184         guint right_margin = 0.1 * start_tsf + 0.9 * end_tsf;
185         guint64 half_window = (end_tsf - start_tsf)/2;
186 
187         if (wr) {
188             // are we to the left of the left margin?
189             if (wr->start_tsf < left_margin) {
190                 // scroll the left edge back to the left margin
191                 guint64 offset = left_margin - wr->start_tsf;
192                 if (offset < half_window) {
193                     // small movement; keep packet to margin
194                     start_tsf -= offset;
195                     end_tsf -= offset;
196                 } else {
197                     // large movement; move packet to center of window
198                     guint64 center = (wr->start_tsf + wr->end_tsf)/2;
199                     start_tsf = center - half_window;
200                     end_tsf = center + half_window;
201                 }
202             } else if (wr->end_tsf > right_margin) {
203                 guint64 offset = wr->end_tsf - right_margin;
204                 if (offset < half_window) {
205                     start_tsf += offset;
206                     end_tsf += offset;
207                 } else {
208                     guint64 center = (wr->start_tsf + wr->end_tsf)/2;
209                     start_tsf = center - half_window;
210                     end_tsf = center + half_window;
211                 }
212             }
213             clip_tsf();
214 
215             update();
216         }
217     }
218 }
219 
220 
221 /* given an x position find which packet that corresponds to.
222  * if it's inter frame space the subsequent packet is returned */
223 guint
find_packet(qreal x_position)224 WirelessTimeline::find_packet(qreal x_position)
225 {
226     guint64 x_time = start_tsf + (x_position/width() * (end_tsf - start_tsf));
227 
228     return find_packet_tsf(x_time);
229 }
230 
captureFileReadStarted(capture_file * cf)231 void WirelessTimeline::captureFileReadStarted(capture_file *cf)
232 {
233     capfile = cf;
234     hide();
235     // TODO: hide or grey the toolbar controls
236 }
237 
captureFileReadFinished()238 void WirelessTimeline::captureFileReadFinished()
239 {
240     /* All frames must be included in packet list */
241     if (cfile.count == 0 || g_hash_table_size(radio_packet_list) != cfile.count)
242         return;
243 
244     /* check that all frames have start and end tsf time and are reasonable time order.
245      * packet timing reference seems to be off a little on some generators, which
246      * causes frequent IFS values in the range 0 to -30. Some generators emit excessive
247      * data when an FCS error happens, and this results in the duration calculation for
248      * the error frame being excessively long. This can cause larger negative IFS values
249      * (-30 to -1000) for the subsequent frame. Ignore these cases, as they don't seem
250      * to impact the GUI too badly. If the TSF reference point is set wrong (TSF at
251      * start of frame when it is at the end) then larger negative offsets are often
252      * seen. Don't display the timeline in these cases.
253      */
254     /* TODO: update GUI to handle captures with occasional frames missing TSF data */
255     /* TODO: indicate error message to the user */
256     for (guint32 n = 1; n < cfile.count; n++) {
257         struct wlan_radio *w = get_wlan_radio(n);
258         if (w->start_tsf == 0 || w->end_tsf == 0) {
259             QString err = tr("Packet number %1 does not include TSF timestamp, not showing timeline.").arg(n);
260             wsApp->pushStatus(WiresharkApplication::TemporaryStatus, err);
261             return;
262         }
263         if (w->ifs < -RENDER_EARLY) {
264             QString err = tr("Packet number %u has large negative jump in TSF, not showing timeline. Perhaps TSF reference point is set wrong?").arg(n);
265             wsApp->pushStatus(WiresharkApplication::TemporaryStatus, err);
266             return;
267         }
268     }
269 
270     first = get_wlan_radio(1);
271     last = get_wlan_radio(cfile.count);
272 
273     start_tsf = first->start_tsf;
274     end_tsf = last->end_tsf;
275 
276     /* TODO: only reset the zoom level if the file is changed, not on redissection */
277     zoom_level = 0;
278 
279     show();
280     selectedFrameChanged(QList<int>());
281     // TODO: show or ungrey the toolbar controls
282     update();
283 }
284 
appInitialized()285 void WirelessTimeline::appInitialized()
286 {
287     connect(wsApp->mainWindow(), SIGNAL(framesSelected(QList<int>)), this, SLOT(selectedFrameChanged(QList<int>)));
288 
289     GString *error_string;
290     error_string = register_tap_listener("wlan_radio_timeline", this, NULL, TL_REQUIRES_NOTHING, tap_timeline_reset, tap_timeline_packet, NULL/*tap_draw_cb tap_draw*/, NULL);
291     if (error_string) {
292         report_failure("Wireless Timeline - tap registration failed: %s", error_string->str);
293         g_string_free(error_string, TRUE);
294     }
295 }
296 
resizeEvent(QResizeEvent *)297 void WirelessTimeline::resizeEvent(QResizeEvent*)
298 {
299     // TODO adjust scrollbar
300 }
301 
302 
303 // Calculate the x position on the GUI from the timestamp
position(guint64 tsf,float ratio)304 int WirelessTimeline::position(guint64 tsf, float ratio)
305 {
306     int position = -100;
307 
308     if (tsf != G_MAXUINT64) {
309         position = ((double) tsf - start_tsf)*width()*ratio/(end_tsf-start_tsf);
310     }
311     return position;
312 }
313 
314 
WirelessTimeline(QWidget * parent)315 WirelessTimeline::WirelessTimeline(QWidget *parent) : QWidget(parent)
316 {
317     setHidden(true);
318     zoom_level = 1.0;
319     setFixedHeight(TIMELINE_HEIGHT);
320     first_packet = 1;
321     setMouseTracking(true);
322     start_x = 0;
323     last_x = 0;
324     packet_list = NULL;
325     start_tsf = 0;
326     end_tsf = 0;
327     first = NULL;
328     last = NULL;
329     capfile = NULL;
330 
331     radio_packet_list = g_hash_table_new(g_direct_hash, g_direct_equal);
332     connect(wsApp, SIGNAL(appInitialized()), this, SLOT(appInitialized()));
333 }
334 
~WirelessTimeline()335 WirelessTimeline::~WirelessTimeline()
336 {
337     if (radio_packet_list != NULL)
338     {
339         g_hash_table_destroy(radio_packet_list);
340     }
341 }
342 
setPacketList(PacketList * packet_list)343 void WirelessTimeline::setPacketList(PacketList *packet_list)
344 {
345     this->packet_list = packet_list;
346 }
347 
tap_timeline_reset(void * tapdata)348 void WirelessTimeline::tap_timeline_reset(void* tapdata)
349 {
350     WirelessTimeline* timeline = (WirelessTimeline*)tapdata;
351 
352     if (timeline->radio_packet_list != NULL)
353     {
354         g_hash_table_destroy(timeline->radio_packet_list);
355     }
356     timeline->hide();
357 
358     timeline->radio_packet_list = g_hash_table_new(g_direct_hash, g_direct_equal);
359 }
360 
tap_timeline_packet(void * tapdata,packet_info * pinfo,epan_dissect_t * edt _U_,const void * data)361 tap_packet_status WirelessTimeline::tap_timeline_packet(void *tapdata, packet_info* pinfo, epan_dissect_t* edt _U_, const void *data)
362 {
363     WirelessTimeline* timeline = (WirelessTimeline*)tapdata;
364     const struct wlan_radio *wlan_radio_info = (const struct wlan_radio *)data;
365 
366     /* Save the radio information in our own (GUI) hashtable */
367     g_hash_table_insert(timeline->radio_packet_list, GUINT_TO_POINTER(pinfo->num), (gpointer)wlan_radio_info);
368     return TAP_PACKET_DONT_REDRAW;
369 }
370 
get_wlan_radio(guint32 packet_num)371 struct wlan_radio* WirelessTimeline::get_wlan_radio(guint32 packet_num)
372 {
373     return (struct wlan_radio*)g_hash_table_lookup(radio_packet_list, GUINT_TO_POINTER(packet_num));
374 }
375 
doToolTip(struct wlan_radio * wr,QPoint pos,int x)376 void WirelessTimeline::doToolTip(struct wlan_radio *wr, QPoint pos, int x)
377 {
378     if (x < position(wr->start_tsf, 1.0)) {
379         QToolTip::showText(pos, QString("Inter frame space %1 " UTF8_MICRO_SIGN "s").arg(wr->ifs));
380     } else {
381         QToolTip::showText(pos, QString("Total duration %1 " UTF8_MICRO_SIGN "s\nNAV %2 " UTF8_MICRO_SIGN "s")
382                            .arg(wr->end_tsf-wr->start_tsf).arg(wr->nav));
383     }
384 }
385 
386 
event(QEvent * event)387 bool WirelessTimeline::event(QEvent *event)
388 {
389     if (event->type() == QEvent::ToolTip) {
390         QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
391         guint packet = find_packet(helpEvent->pos().x());
392         if (packet) {
393             doToolTip(get_wlan_radio(packet), helpEvent->globalPos(), helpEvent->x());
394         } else {
395             QToolTip::hideText();
396             event->ignore();
397         }
398         return true;
399     }
400     return QWidget::event(event);
401 }
402 
403 
wheelEvent(QWheelEvent * event)404 void WirelessTimeline::wheelEvent(QWheelEvent *event)
405 {
406     // "Most mouse types work in steps of 15 degrees, in which case the delta
407     // value is a multiple of 120; i.e., 120 units * 1/8 = 15 degrees"
408     double steps = event->angleDelta().y() / 120.0;
409     if (steps != 0.0) {
410         zoom_level += steps;
411         if (zoom_level < 0) zoom_level = 0;
412         if (zoom_level > TIMELINE_MAX_ZOOM) zoom_level = TIMELINE_MAX_ZOOM;
413 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
414         zoom(event->position().x() / width());
415 #else
416         zoom(event->posF().x() / width());
417 #endif
418     }
419 }
420 
421 
bgColorizationProgress(int first,int last)422 void WirelessTimeline::bgColorizationProgress(int first, int last)
423 {
424     if (isHidden()) return;
425 
426     struct wlan_radio *first_wr = get_wlan_radio(first);
427 
428     struct wlan_radio *last_wr = get_wlan_radio(last-1);
429 
430     int x = position(first_wr->start_tsf, 1);
431     int x_end = position(last_wr->end_tsf, 1);
432 
433     update(x, 0, x_end-x+1, height());
434 }
435 
436 
437 // zoom at relative position 0.0 <= x_fraction <= 1.0.
zoom(double x_fraction)438 void WirelessTimeline::zoom(double x_fraction)
439 {
440     /* adjust the zoom around the selected packet */
441     guint64 file_range = last->end_tsf - first->start_tsf;
442     guint64 center = start_tsf + x_fraction * (end_tsf - start_tsf);
443     guint64 span = pow(file_range, 1.0 - zoom_level / TIMELINE_MAX_ZOOM);
444     start_tsf = center - span * x_fraction;
445     end_tsf = center + span * (1.0 - x_fraction);
446     clip_tsf();
447     update();
448 }
449 
find_packet_tsf(guint64 tsf)450 int WirelessTimeline::find_packet_tsf(guint64 tsf)
451 {
452     if (cfile.count < 1)
453         return 0;
454 
455     if (cfile.count < 2)
456         return 1;
457 
458     guint32 min_count = 1;
459     guint32 max_count = cfile.count-1;
460 
461     guint64 min_tsf = get_wlan_radio(min_count)->end_tsf;
462     guint64 max_tsf = get_wlan_radio(max_count)->end_tsf;
463 
464     for (;;) {
465         if (tsf >= max_tsf)
466             return max_count+1;
467 
468         if (tsf < min_tsf)
469             return min_count;
470 
471         guint32 middle = (min_count + max_count)/2;
472         if (middle == min_count)
473             return middle+1;
474 
475         guint64 middle_tsf = get_wlan_radio(middle)->end_tsf;
476 
477         if (tsf >= middle_tsf) {
478             min_count = middle;
479             min_tsf = middle_tsf;
480         } else {
481             max_count = middle;
482             max_tsf = middle_tsf;
483         }
484     };
485 }
486 
487 void
paintEvent(QPaintEvent * qpe)488 WirelessTimeline::paintEvent(QPaintEvent *qpe)
489 {
490     QPainter p(this);
491 
492     // painting is done in device pixels in the x axis, get the ratio here
493     float ratio = p.device()->devicePixelRatio();
494 
495     unsigned int packet;
496     double zoom;
497     int last_x=-1;
498     int left = qpe->rect().left()*ratio;
499     int right = qpe->rect().right()*ratio;
500     float rgb[TIMELINE_HEIGHT][3];
501     reset_rgb(rgb);
502 
503     zoom = ((double) width())/(end_tsf - start_tsf) * ratio;
504 
505     /* background is light grey */
506     p.fillRect(0, 0, width(), TIMELINE_HEIGHT, QColor(240,240,240));
507 
508     /* background of packets visible in packet_list is white */
509     int top = packet_list->indexAt(QPoint(0,0)).row();
510     int bottom = packet_list->indexAt(QPoint(0,packet_list->viewport()->height())).row();
511 
512     frame_data * topData = packet_list->getFDataForRow(top);
513     frame_data * botData = packet_list->getFDataForRow(bottom);
514     if (! topData || ! botData)
515         return;
516 
517     int x1 = top == -1 ? 0 : position(get_wlan_radio(topData->num)->start_tsf, ratio);
518     int x2 = bottom == -1 ? width() : position(get_wlan_radio(botData->num)->end_tsf, ratio);
519     p.fillRect(QRectF(x1/ratio, 0, (x2-x1+1)/ratio, TIMELINE_HEIGHT), Qt::white);
520 
521     /* background of current packet is blue */
522     if (cfile.current_frame) {
523         struct wlan_radio *wr = get_wlan_radio(cfile.current_frame->num);
524         if (wr) {
525             x1 = position(wr->start_tsf, ratio);
526             x2 = position(wr->end_tsf, ratio);
527             p.fillRect(QRectF(x1/ratio, 0, (x2-x1+1)/ratio, TIMELINE_HEIGHT), Qt::blue);
528         }
529     }
530 
531     QGraphicsScene qs;
532     for (packet = find_packet_tsf(start_tsf + left/zoom - RENDER_EARLY); packet <= cfile.count; packet++) {
533         frame_data *fdata = frame_data_sequence_find(cfile.provider.frames, packet);
534         struct wlan_radio *ri = get_wlan_radio(fdata->num);
535         float x, width, red, green, blue;
536 
537         if (ri == NULL) continue;
538 
539         gint8 rssi = ri->aggregate ? ri->aggregate->rssi : ri->rssi;
540         guint height = (rssi+100)/2;
541         gint end_nav;
542 
543         /* leave a margin above the packets so the selected packet can be seen */
544         if (height > TIMELINE_HEIGHT/2-6)
545             height = TIMELINE_HEIGHT/2-6;
546 
547         /* ensure shortest packets are clearly visible */
548         if (height < 2)
549             height = 2;
550 
551         /* skip frames we don't have start and end data for */
552         /* TODO: show something, so it's clear a frame is missing */
553         if (ri->start_tsf == 0 || ri->end_tsf == 0)
554             continue;
555 
556         x = ((gint64) (ri->start_tsf - start_tsf))*zoom;
557         /* is there a previous anti-aliased pixel to output */
558         if (last_x >= 0 && ((int) x) != last_x) {
559             /* write it out now */
560             render_pixels(p, last_x, 1, rgb, ratio);
561             last_x = -1;
562         }
563 
564         /* does this packet start past the right edge of the window? */
565         if (x >= right) {
566             break;
567         }
568 
569         width = (ri->end_tsf - ri->start_tsf)*zoom;
570         if (width < 0) {
571             continue;
572         }
573 
574         /* is this packet completely to the left of the displayed area? */
575         // TODO clip NAV line properly if we are displaying it
576         if ((x + width) < left)
577             continue;
578 
579         /* remember the first displayed packet */
580         if (first_packet < 0)
581             first_packet = packet;
582 
583         if (fdata->color_filter) {
584             const color_t *c = &((const color_filter_t *) fdata->color_filter)->fg_color;
585             red = c->red / 65535.0;
586             green = c->green / 65535.0;
587             blue = c->blue / 65535.0;
588         } else {
589             red = green = blue = 0.0;
590         }
591 
592         /* record NAV field at higher magnifications */
593         end_nav = x + width + ri->nav*zoom;
594         if (zoom >= 0.01 && ri->nav && end_nav > 0) {
595             gint y = 2*(packet % (TIMELINE_HEIGHT/2));
596             qs.addLine(QLineF((x+width)/ratio, y, end_nav/ratio, y), QPen(pcolor(red,green,blue)));
597         }
598 
599         /* does this rectangle fit within one pixel? */
600         if (((int) x) == ((int) (x+width))) {
601             /* accumulate it for later rendering together
602              * with all other sub pixels that fall within this
603              * pixel */
604             last_x = x;
605             accumulate_rgb(rgb, height, fdata->passed_dfilter, width, red, green, blue);
606         } else {
607             /* it spans more than 1 pixel.
608              * first accumulate the part that does fit */
609             float partial = ((int) x) + 1 - x;
610             accumulate_rgb(rgb, height, fdata->passed_dfilter, partial, red, green, blue);
611             /* and render it */
612             render_pixels(p, (int) x, 1, rgb, ratio);
613             last_x = -1;
614             x += partial;
615             width -= partial;
616             /* are there any whole pixels of width left to draw? */
617             if (width > 1.0) {
618                 render_rectangle(p, x, width, height, fdata->passed_dfilter, red, green, blue, ratio);
619                 x += (int) width;
620                 width -= (int) width;
621             }
622             /* is there a partial pixel left */
623             if (width > 0.0) {
624                 last_x = x;
625                 accumulate_rgb(rgb, height, fdata->passed_dfilter, width, red, green, blue);
626             }
627         }
628     }
629 
630     // draw the NAV lines last, so they appear on top of the packets
631     qs.render(&p, rect(), rect());
632 }
633