]> git.cworth.org Git - apitrace/blobdiff - gui/timelinewidget.cpp
Combine timeline and histogram tabs.
[apitrace] / gui / timelinewidget.cpp
index d9f1b5809ebf63246dba4c4c7f7cb2aeb53c9bca..0f30225d268c9bf72515ea91fea5fa2562d8354f 100644 (file)
@@ -1,24 +1,23 @@
 #include "timelinewidget.h"
+#include "profiledialog.h"
 #include "trace_profiler.hpp"
 
-#include <math.h>
+#include <qmath.h>
 #include <QColor>
+#include <QLocale>
 #include <QPainter>
 #include <QToolTip>
 #include <QMouseEvent>
 #include <QWheelEvent>
 #include <QApplication>
 
+typedef trace::Profile::Call Call;
 typedef trace::Profile::Frame Frame;
 typedef trace::Profile::Program Program;
-typedef trace::Profile::CpuCall CpuCall;
-typedef trace::Profile::DrawCall DrawCall;
 
 TimelineWidget::TimelineWidget(QWidget *parent)
     : QWidget(parent),
       m_profile(NULL),
-      m_timeSelectionStart(0),
-      m_timeSelectionEnd(0),
       m_rowHeight(20),
       m_axisWidth(50),
       m_axisHeight(30),
@@ -27,8 +26,12 @@ TimelineWidget::TimelineWidget(QWidget *parent)
       m_axisForeground(Qt::black),
       m_axisBackground(QColor(210, 210, 210)),
       m_itemBorder(Qt::red),
-      m_itemForeground(Qt::cyan),
-      m_itemBackground(Qt::red),
+      m_itemGpuForeground(Qt::cyan),
+      m_itemGpuBackground(Qt::red),
+      m_itemCpuForeground(QColor(255, 255, 0)),
+      m_itemCpuBackground(QColor(0, 0, 255)),
+      m_itemDeselectedForeground(Qt::white),
+      m_itemDeselectedBackground(QColor(155, 155, 155)),
       m_selectionBorder(Qt::green),
       m_selectionBackground(QColor(100, 255, 100, 8)),
       m_zoomBorder(QColor(255, 0, 255)),
@@ -37,6 +40,8 @@ TimelineWidget::TimelineWidget(QWidget *parent)
     setBackgroundRole(QPalette::Base);
     setAutoFillBackground(true);
     setMouseTracking(true);
+
+    m_selection.type = SelectNone;
 }
 
 
@@ -73,15 +78,47 @@ void TimelineWidget::setVerticalScrollValue(int value)
 
 
 /**
- * Update the time selection
+ * Set selection to nothing
+ */
+void TimelineWidget::selectNone(bool notify)
+{
+    m_selection.type = SelectNone;
+
+    if (notify) {
+        emit selectedNone();
+    }
+
+    update();
+}
+
+
+/**
+ * Set selection to a program
+ */
+void TimelineWidget::selectProgram(unsigned program, bool notify)
+{
+    m_selection.program = program;
+    m_selection.type = SelectProgram;
+
+    if (notify) {
+        emit selectedProgram(program);
+    }
+
+    update();
+}
+
+
+/**
+ * Set selection to a period of time
  */
-void TimelineWidget::setSelection(int64_t start, int64_t end, bool notify)
+void TimelineWidget::selectTime(int64_t start, int64_t end, bool notify)
 {
-    m_timeSelectionStart = start;
-    m_timeSelectionEnd = end;
+    m_selection.timeStart = start;
+    m_selection.timeEnd = end;
+    m_selection.type = SelectTime;
 
     if (notify) {
-        emit selectionChanged(m_timeSelectionStart, m_timeSelectionEnd);
+        emit selectedTime(start, end);
     }
 
     update();
@@ -139,17 +176,58 @@ typename std::vector<val_ty>::const_iterator binarySearchTimespan(
         itr = begin + pos;
     }
 
-    if (lower <= upper)
+    if (lower <= upper) {
+        return itr;
+    } else {
+        return end;
+    }
+}
+
+
+/**
+ * Binary Search for a time in start+durations on an array of indices
+ */
+std::vector<unsigned>::const_iterator binarySearchTimespanIndexed(
+        const std::vector<Call>& calls,
+        std::vector<unsigned>::const_iterator begin,
+        std::vector<unsigned>::const_iterator end,
+        int64_t time)
+{
+    int lower = 0;
+    int upper = end - begin - 1;
+    int pos = (lower + upper) / 2;
+    std::vector<unsigned>::const_iterator itr = begin + pos;
+
+    while (lower <= upper) {
+        const Call& call = calls[*itr];
+
+        if (call.gpuStart <= time && call.gpuStart + call.gpuDuration > time) {
+            break;
+        }
+
+        if (call.gpuStart > time) {
+            upper = pos - 1;
+        } else {
+            lower = pos + 1;
+        }
+
+        pos = (lower + upper) / 2;
+        itr = begin + pos;
+    }
+
+    if (lower <= upper) {
         return itr;
-    else
+    } else {
         return end;
+    }
 }
 
 
 /**
  * Find the frame at time
  */
-const Frame* TimelineWidget::frameAtTime(int64_t time) {
+const Frame* TimelineWidget::frameAtTime(int64_t time)
+{
     if (!m_profile) {
         return NULL;
     }
@@ -161,8 +239,7 @@ const Frame* TimelineWidget::frameAtTime(int64_t time) {
                 time);
 
     if (res != m_profile->frames.end()) {
-        const Frame& frame = *res;
-        return &frame;
+        return &*res;
     }
 
     return NULL;
@@ -172,20 +249,20 @@ const Frame* TimelineWidget::frameAtTime(int64_t time) {
 /**
  * Find the CPU call at time
  */
-const CpuCall* TimelineWidget::cpuCallAtTime(int64_t time) {
+const Call* TimelineWidget::cpuCallAtTime(int64_t time)
+{
     if (!m_profile) {
         return NULL;
     }
 
-    std::vector<CpuCall>::const_iterator res
-            = binarySearchTimespan<CpuCall, &CpuCall::cpuStart, &CpuCall::cpuDuration>(
-                m_profile->cpuCalls.begin(),
-                m_profile->cpuCalls.end(),
+    std::vector<Call>::const_iterator res
+            = binarySearchTimespan<Call, &Call::cpuStart, &Call::cpuDuration>(
+                m_profile->calls.begin(),
+                m_profile->calls.end(),
                 time);
 
-    if (res != m_profile->cpuCalls.end()) {
-        const CpuCall& call = *res;
-        return &call;
+    if (res != m_profile->calls.end()) {
+        return &*res;
     }
 
     return NULL;
@@ -195,22 +272,42 @@ const CpuCall* TimelineWidget::cpuCallAtTime(int64_t time) {
 /**
  * Find the draw call at time
  */
-const DrawCall* TimelineWidget::drawCallAtTime(int program, int64_t time) {
+const Call* TimelineWidget::drawCallAtTime(int64_t time)
+{
     if (!m_profile) {
         return NULL;
     }
 
-    std::vector<DrawCall>& drawCalls = m_profile->programs[program].drawCalls;
+    for (int i = 0; i < m_rowPrograms.size(); ++i) {
+        const Call* call = drawCallAtTime(time, m_rowPrograms[i]);
+
+        if (call) {
+            return call;
+        }
+    }
+
+    return NULL;
+}
+
+
+/**
+ * Find the draw call at time for a selected program
+ */
+const Call* TimelineWidget::drawCallAtTime(int64_t time, int program)
+{
+    if (!m_profile) {
+        return NULL;
+    }
 
-    std::vector<DrawCall>::const_iterator res
-            = binarySearchTimespan<DrawCall, &DrawCall::gpuStart, &DrawCall::gpuDuration>(
-                drawCalls.begin(),
-                drawCalls.end(),
+    std::vector<unsigned>::const_iterator res
+            = binarySearchTimespanIndexed(
+                m_profile->calls,
+                m_profile->programs[program].calls.begin(),
+                m_profile->programs[program].calls.end(),
                 time);
 
-    if (res != drawCalls.end()) {
-        const DrawCall& call = *res;
-        return &call;
+    if (res != m_profile->programs[program].calls.end()) {
+        return &m_profile->calls[*res];
     }
 
     return NULL;
@@ -274,7 +371,7 @@ void TimelineWidget::setProfile(trace::Profile* profile)
 
     setTimeScroll(m_time);
     setRowScroll(0);
-
+    selectNone();
     update();
 }
 
@@ -331,7 +428,7 @@ void TimelineWidget::resizeEvent(QResizeEvent *e)
 {
     /* Update viewport size */
     m_viewWidth = qMax(0, width() - m_axisWidth);
-    m_viewHeight = qMax(0, height() - m_axisHeight - m_rowHeight);
+    m_viewHeight = qMax(0, height() - m_axisHeight - m_rowHeight * 2);
 
     /* Update vertical scroll bar */
     if (m_profile) {
@@ -358,39 +455,61 @@ void TimelineWidget::mouseMoveEvent(QMouseEvent *e)
             int y = m_mousePosition.y() - m_axisHeight;
 
             if (y < m_rowHeight) {
-                const CpuCall* call = cpuCallAtTime(time);
+                const Call* call = cpuCallAtTime(time);
 
                 if (call) {
                     QString text;
                     text  = QString::fromStdString(call->name);
                     text += QString("\nCall: %1").arg(call->no);
-                    text += QString("\nCPU Start: %1").arg(call->cpuStart);
-                    text += QString("\nCPU Duration: %1").arg(call->cpuDuration);
+                    text += QString("\nCPU Start: %1").arg(getTimeString(call->cpuStart));
+                    text += QString("\nCPU Duration: %1").arg(getTimeString(call->cpuDuration));
 
                     QToolTip::showText(e->globalPos(), text);
                     tooltip = true;
                 }
             } else {
-                int row = (y - m_rowHeight + m_scrollY) / m_rowHeight;
+                const Call* call = NULL;
 
-                if (row < m_rowPrograms.size()) {
-                    int program = m_rowPrograms[row];
-                    const DrawCall* call = drawCallAtTime(program, time);
-
-                    if (call) {
-                        QString text;
-                        text  = QString::fromStdString(call->name);
-                        text += QString("\nCall: %1").arg(call->no);
-                        text += QString("\nGPU Start: %1").arg(call->gpuStart);
-                        text += QString("\nCPU Start: %1").arg(call->cpuStart);
-                        text += QString("\nGPU Duration: %1").arg(call->gpuDuration);
-                        text += QString("\nCPU Duration: %1").arg(call->cpuDuration);
-                        text += QString("\nPixels Drawn: %1").arg(call->pixels);
-
-                        QToolTip::showText(e->globalPos(), text);
-                        tooltip = true;
+                if (y < m_rowHeight * 2) {
+                    call = drawCallAtTime(time);
+                } else {
+                    int row = (y - m_rowHeight * 2 + m_scrollY) / m_rowHeight;
+
+                    if (row < m_rowPrograms.size()) {
+                        call = drawCallAtTime(time, m_rowPrograms[row]);
                     }
                 }
+
+                if (call) {
+                    QString text;
+                    text  = QString::fromStdString(call->name);
+                    text += QString("\nCall: %1").arg(call->no);
+                    text += QString("\nCPU Start: %1").arg(getTimeString(call->cpuStart));
+                    text += QString("\nGPU Start: %1").arg(getTimeString(call->gpuStart));
+                    text += QString("\nCPU Duration: %1").arg(getTimeString(call->cpuDuration));
+                    text += QString("\nGPU Duration: %1").arg(getTimeString(call->gpuDuration));
+                    text += QString("\nPixels Drawn: %1").arg(QLocale::system().toString((qlonglong)call->pixels));
+
+                    QToolTip::showText(e->globalPos(), text);
+                    tooltip = true;
+                }
+            }
+        } else if (m_mousePosition.x() < m_axisWidth && m_mousePosition.y() > m_axisHeight) {
+            int y = m_mousePosition.y() - m_axisHeight;
+
+            if (y < m_rowHeight) {
+                QToolTip::showText(e->globalPos(), "All CPU calls");
+                tooltip = true;
+            } else if (y < m_rowHeight * 2) {
+                QToolTip::showText(e->globalPos(), "All GPU calls");
+                tooltip = true;
+            } else {
+                int row = (y - m_rowHeight * 2 + m_scrollY) / m_rowHeight;
+
+                if (row < m_rowPrograms.size()) {
+                    QToolTip::showText(e->globalPos(), QString("All calls in Shader Program %1").arg(m_rowPrograms[row]));
+                    tooltip = true;
+                }
             }
         }
     } else if (e->buttons().testFlag(Qt::LeftButton)) {
@@ -409,7 +528,7 @@ void TimelineWidget::mouseMoveEvent(QMouseEvent *e)
             int64_t down  = positionToTime(m_mousePressPosition.x() - m_axisWidth);
             int64_t up    = positionToTime(qMax(e->pos().x() - m_axisWidth, 0));
 
-            setSelection(qMin(down, up), qMax(down, up));
+            selectTime(qMin(down, up), qMax(down, up), true);
         }
 
         update();
@@ -429,18 +548,16 @@ void TimelineWidget::mousePressEvent(QMouseEvent *e)
                 m_mousePressMode = RulerZoom;
             } else {
                 m_mousePressMode = RulerSelect;
-
-                int64_t time = positionToTime(e->pos().x() - m_axisWidth);
-                m_timeSelectionStart = time;
-                m_timeSelectionEnd = time;
             }
-        } else {
+        } else if (e->pos().x() >= m_axisWidth) {
             m_mousePressMode = DragView;
+        } else {
+            m_mousePressMode = NoMousePress;
         }
 
         m_mousePressPosition  = e->pos();
         m_mousePressTime = m_time;
-        m_mousePressRow   = m_scrollY;
+        m_mousePressRow  = m_scrollY;
 
         update();
     }
@@ -454,6 +571,8 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent *e)
     }
 
     /* Calculate new time view based on selected area */
+    int dxy = qAbs(m_mousePressPosition.x() - e->pos().x()) + qAbs(m_mousePressPosition.y() - e->pos().y());
+
     int64_t down  = positionToTime(m_mousePressPosition.x() - m_axisWidth);
     int64_t up    = positionToTime(qMax(e->pos().x() - m_axisWidth, 0));
 
@@ -466,8 +585,23 @@ void TimelineWidget::mouseReleaseEvent(QMouseEvent *e)
 
         m_mousePressMode = NoMousePress;
         setTimeScroll(left);
-    } else if (m_mousePressMode == RulerSelect) {
-        setSelection(m_timeSelectionStart, m_timeSelectionEnd, true);
+    } else {
+        if (dxy <= 2) {
+            if (m_selection.type == SelectTime) {
+                if (left < m_selection.timeStart || right > m_selection.timeEnd || e->pos().x() < m_axisWidth) {
+                    selectNone(true);
+                }
+            } else if (m_selection.type == SelectProgram) {
+                int y = e->pos().y() - m_axisHeight;
+                int row = (y - m_rowHeight * 2 + m_scrollY) / m_rowHeight;
+
+                if (row < 0 || m_rowPrograms[row] != m_selection.program) {
+                    selectNone(true);
+                }
+            }
+        } else if (m_mousePressMode == RulerSelect) {
+            selectTime(left, right, true);
+        }
     }
 }
 
@@ -484,12 +618,12 @@ void TimelineWidget::mouseDoubleClickEvent(QMouseEvent *e)
             const Frame* frame = frameAtTime(time);
 
             if (frame) {
-                setSelection(frame->cpuStart, frame->cpuStart + frame->cpuDuration, true);
+                selectTime(frame->cpuStart, frame->cpuStart + frame->cpuDuration, true);
                 return;
             }
         } else if (row == 0) {
             /* CPU Calls */
-            const CpuCall* call = cpuCallAtTime(time);
+            const Call* call = cpuCallAtTime(time);
 
             if (call) {
                 emit jumpToCall(call->no);
@@ -497,18 +631,20 @@ void TimelineWidget::mouseDoubleClickEvent(QMouseEvent *e)
             }
         } else if (row > 0) {
             /* Draw Calls */
-            int program = m_rowPrograms[row - 1 + m_row];
-            const DrawCall* call = drawCallAtTime(program, time);
+            const Call* call = drawCallAtTime(time, 0);
 
             if (call) {
                 emit jumpToCall(call->no);
                 return;
             }
         }
-    }
+    } else {
+        int y = e->pos().y() - m_axisHeight;
+        int row = (y - m_rowHeight * 2 + m_scrollY) / m_rowHeight;
 
-    if (time < m_timeSelectionStart || time > m_timeSelectionEnd) {
-        setSelection(0, 0, true);
+        if (row >= 0 && row < m_rowPrograms.size()) {
+            selectProgram(m_rowPrograms[row], true);
+        }
     }
 }
 
@@ -519,6 +655,10 @@ void TimelineWidget::wheelEvent(QWheelEvent *e)
         return;
     }
 
+    if (e->pos().x() < m_axisWidth) {
+        return;
+    }
+
     int zoomPercent = 10;
 
     /* If holding Ctrl key then zoom 2x faster */
@@ -550,22 +690,31 @@ void TimelineWidget::wheelEvent(QWheelEvent *e)
 /**
  * Paints a single pixel column of the heat map
  */
-void TimelineWidget::drawHeat(QPainter& painter, int x, int64_t heat, bool isCpu)
+void TimelineWidget::drawHeat(QPainter& painter, int x, int64_t heat, bool gpu, bool selected)
 {
     if (heat == 0) {
         return;
     }
 
-    double timePerPixel = m_timeWidth;
-    timePerPixel /= m_viewWidth;
+    if (m_selection.type == SelectTime) {
+        selected = x >= m_selectionLeft && x <= m_selectionRight;
+    }
 
+    double timePerPixel = m_timeWidth / (double)m_viewWidth;
     double colour = heat / timePerPixel;
-    colour = qBound(0.0, colour * 255.0, 255.0);
 
-    if (isCpu) {
-        painter.setPen(QColor(255 - colour, 255 - colour, 255));
-    } else {
+    /* Gamma correction */
+    colour = qPow(colour, 1.0 / 2.0);
+
+    if (!selected) {
+        colour = qBound(0.0, colour * 100.0, 100.0);
+        painter.setPen(QColor(255 - colour, 255 - colour, 255 - colour));
+    } else if (gpu) {
+        colour = qBound(0.0, colour * 255.0, 255.0);
         painter.setPen(QColor(255, 255 - colour, 255 - colour));
+    } else {
+        colour = qBound(0.0, colour * 255.0, 255.0);
+        painter.setPen(QColor(255 - colour, 255 - colour, 255));
     }
 
     painter.drawLine(x, 0, x, m_rowHeight - 1);
@@ -573,156 +722,176 @@ void TimelineWidget::drawHeat(QPainter& painter, int x, int64_t heat, bool isCpu
 
 
 /**
- * Render the whole widget
+ * Draws a call on the heatmap
  */
-void TimelineWidget::paintEvent(QPaintEvent *e)
+bool TimelineWidget::drawCall(QPainter& painter, const trace::Profile::Call& call, int& lastX, int64_t& heat, bool gpu)
 {
-    if (!m_profile)
-        return;
+    int64_t start, duration, end;
 
-    QPainter painter(this);
+    if (gpu) {
+        start = call.gpuStart;
+        duration = call.gpuDuration;
+    } else {
+        start = call.cpuStart;
+        duration = call.cpuDuration;
+    }
 
-    int rowEnd = qMin(m_row + (m_viewHeight / m_rowHeight) + 1, m_rowCount);
-    int64_t timeEnd = m_time + m_timeWidth;
-    int64_t heat = 0;
-    int lastX = 0;
-    int widgetHeight = height();
-    int widgetWidth = width();
+    end = start + duration;
 
-    /* Draw GPU rows */
-    painter.translate(m_axisWidth, m_axisHeight + m_rowHeight - (m_scrollY % m_rowHeight));
+    if (start > m_timeEnd) {
+        return false;
+    }
 
-    for (int row = m_row; row < rowEnd; ++row) {
-        Program& program = m_profile->programs[m_rowPrograms[row]];
-        lastX = 0;
-        heat = 0;
+    if (end < m_time) {
+        return true;
+    }
 
-        for (std::vector<DrawCall>::const_iterator itr = program.drawCalls.begin(); itr != program.drawCalls.end(); ++itr) {
-            const DrawCall& call = *itr;
-            int64_t gpuEnd = call.gpuStart + call.gpuDuration;
+    double left  = timeToPosition(start);
+    double right = timeToPosition(end);
 
-            if (call.gpuStart > timeEnd) {
-                break;
-            }
+    int leftX  = left;
+    int rightX = right;
 
-            if (gpuEnd < m_time) {
-                continue;
-            }
+    bool selected = true;
 
-            double left  = timeToPosition(call.gpuStart);
-            double right = timeToPosition(gpuEnd);
+    if (m_selection.type == SelectProgram) {
+        selected = call.program == m_selection.program;
+    }
 
-            int leftX  = left;
-            int rightX = right;
+    /* Draw last heat if needed */
+    if (leftX != lastX) {
+        drawHeat(painter, lastX, heat, gpu, selected);
+        lastX = leftX;
+        heat = 0;
+    }
 
-            /* Draw last heat if needed */
-            if (leftX != lastX) {
-                drawHeat(painter, lastX, heat, false);
-                lastX = leftX;
-                heat = 0;
-            }
+    if (rightX <= leftX + 1) {
+        if (rightX == lastX) {
+            /* Fully contained in this X */
+            heat += duration;
+        } else {
+            /* Split call time between the two pixels it occupies */
+            int64_t time = positionToTime(rightX);
+            heat += time - start;
 
-            if (rightX <= leftX + 1) {
-                if (rightX == lastX) {
-                    /* Fully contained in this X */
-                    heat += call.gpuDuration;
-                } else {
-                    /* Split call time between the two pixels it occupies */
-                    int64_t time = positionToTime(rightX);
+            drawHeat(painter, lastX, heat, gpu, selected);
 
-                    heat += time - call.gpuStart;
-                    drawHeat(painter, lastX, heat, false);
+            heat = end - time;
+            lastX = rightX;
+        }
+    } else {
+        QRect rect;
+        rect.setLeft(left + 0.5);
+        rect.setWidth(right - left);
+        rect.setTop(0);
+        rect.setHeight(m_rowHeight);
+
+        if (m_selection.type == SelectTime) {
+            selected = (start >= m_selection.timeStart && start <= m_selection.timeEnd)
+                    || (end >= m_selection.timeStart && end <= m_selection.timeEnd);
+        }
 
-                    heat = gpuEnd - time;
-                    lastX = rightX;
-                }
+        /* Draw background rect */
+        if (selected) {
+            if (gpu) {
+                painter.fillRect(rect, m_itemGpuBackground);
             } else {
-                QRect rect;
-                rect.setLeft(left + 0.5);
-                rect.setWidth(right - left);
-                rect.setTop(0);
-                rect.setHeight(m_rowHeight);
-
-                painter.fillRect(rect, m_itemBackground);
+                painter.fillRect(rect, m_itemCpuBackground);
+            }
+        } else {
+            painter.fillRect(rect, m_itemDeselectedBackground);
+        }
 
-                if (rect.width() > 6) {
-                    rect.adjust(1, 0, -1, -2);
-                    painter.setPen(m_itemForeground);
+        /* If wide enough, draw text */
+        if (rect.width() > 6) {
+            rect.adjust(1, 0, -1, -2);
 
-                    painter.drawText(rect,
-                                     Qt::AlignLeft | Qt::AlignVCenter,
-                                     painter.fontMetrics().elidedText(QString::fromStdString(call.name), Qt::ElideRight, rect.width()));
+            if (selected) {
+                if (gpu) {
+                    painter.setPen(m_itemGpuForeground);
+                } else {
+                    painter.setPen(m_itemCpuForeground);
                 }
+            } else {
+                painter.setPen(m_itemDeselectedForeground);
             }
-        }
 
-        painter.translate(0, m_rowHeight);
+            painter.drawText(rect,
+                             Qt::AlignLeft | Qt::AlignVCenter,
+                             painter.fontMetrics().elidedText(QString::fromStdString(call.name), Qt::ElideRight, rect.width()));
+        }
     }
 
-    /* Draw CPU row */
-    painter.resetTransform();
-    painter.translate(m_axisWidth, m_axisHeight);
-    painter.fillRect(0, 0, m_viewWidth, m_rowHeight, Qt::white);
+    return true;
+}
 
-    for (std::vector<CpuCall>::const_iterator itr = m_profile->cpuCalls.begin(); itr != m_profile->cpuCalls.end(); ++itr) {
-        const CpuCall& call = *itr;
-        int64_t cpuEnd = call.cpuStart + call.cpuDuration;
 
-        if (call.cpuStart > timeEnd) {
-            continue;
-        }
+/**
+ * Render the whole widget
+ */
+void TimelineWidget::paintEvent(QPaintEvent *e)
+{
+    if (!m_profile)
+        return;
 
-        if (cpuEnd < m_time) {
-            continue;
-        }
+    QPainter painter(this);
 
-        double left = timeToPosition(call.cpuStart);
-        double right = timeToPosition(cpuEnd);
+    int rowEnd = qMin(m_row + qCeil(m_viewHeight / (double)m_rowHeight) + 1, m_rowCount);
+    int64_t heatGPU = 0, heatCPU = 0;
+    int lastCpuX = 0, lastGpuX = 0;
+    int widgetHeight = height();
+    int widgetWidth = width();
 
-        int leftX = left;
-        int rightX = right;
+    m_timeEnd = m_time + m_timeWidth;
+    m_selectionLeft  = timeToPosition(m_selection.timeStart);
+    m_selectionRight = (timeToPosition(m_selection.timeEnd) + 0.5);
 
-        /* Draw last heat if needed */
-        if (leftX != lastX) {
-            drawHeat(painter, lastX, heat, true);
-            lastX = leftX;
-            heat = 0;
-        }
 
-        if (rightX <= leftX + 1) {
-            if (rightX == lastX) {
-                /* Fully contained in this X */
-                heat += call.cpuDuration;
-            } else {
-                /* Split call time between the two pixels it occupies */
-                int64_t time = positionToTime(rightX);
+    /* Draw program rows */
+    painter.translate(m_axisWidth, m_axisHeight + m_rowHeight * 2 - (m_scrollY % m_rowHeight));
 
-                heat += time - call.cpuStart;
-                drawHeat(painter, lastX, heat, true);
+    for (int row = m_row; row < rowEnd; ++row) {
+        Program& program = m_profile->programs[m_rowPrograms[row]];
+        lastGpuX = 0;
+        heatGPU = 0;
 
-                heat = cpuEnd - time;
-                lastX = rightX;
+        for (std::vector<unsigned>::const_iterator itr = program.calls.begin(); itr != program.calls.end(); ++itr) {
+            const Call& call = m_profile->calls[*itr];
+
+            if (!drawCall(painter, call, lastGpuX, heatGPU, true)) {
+                break;
             }
-        } else {
-            QRect rect;
-            rect.setLeft(left + 0.5);
-            rect.setWidth(right - left);
-            rect.setTop(0);
-            rect.setHeight(m_rowHeight);
+        }
 
-            painter.fillRect(rect, QColor(0, 0, 255));
+        painter.translate(0, m_rowHeight);
+    }
 
-            if (rect.width() > 6) {
-                rect.adjust(1, 0, -1, -2);
-                painter.setPen(QColor(255, 255, 0));
 
-                painter.drawText(rect,
-                                 Qt::AlignLeft | Qt::AlignVCenter,
-                                 painter.fontMetrics().elidedText(QString::fromStdString(call.name), Qt::ElideRight, rect.width()));
-            }
+    /* Draw CPU/GPU rows */
+    painter.resetTransform();
+    painter.translate(m_axisWidth, m_axisHeight);
+    painter.fillRect(0, 0, m_viewWidth, m_rowHeight * 2, Qt::white);
+
+    lastCpuX = lastGpuX = 0;
+    heatCPU = heatGPU = 0;
+
+    for (std::vector<Call>::const_iterator itr = m_profile->calls.begin(); itr != m_profile->calls.end(); ++itr) {
+        const Call& call = *itr;
+
+        /* Draw gpu row */
+        if (call.pixels >= 0) {
+            painter.translate(0, m_rowHeight);
+            drawCall(painter, call, lastGpuX, heatGPU, true);
+            painter.translate(0, -m_rowHeight);
+        }
+
+        /* Draw cpu row */
+        if (!drawCall(painter, call, lastCpuX, heatCPU, false)) {
+            break;
         }
     }
 
+
     /* Draw axis */
     painter.resetTransform();
     painter.setPen(m_axisBorder);
@@ -735,8 +904,9 @@ void TimelineWidget::paintEvent(QPaintEvent *e)
     painter.fillRect(0, m_axisHeight - 1, m_axisWidth - 1, widgetHeight, m_axisBackground);
     painter.drawLine(m_axisWidth - 1, 0, m_axisWidth - 1, widgetHeight);
 
+
     /* Draw the program numbers */
-    painter.translate(0, m_axisHeight + m_rowHeight);
+    painter.translate(0, m_axisHeight + m_rowHeight * 2);
 
     for (int row = m_row; row < rowEnd; ++row) {
         int y = (row - m_row) * m_rowHeight - (m_scrollY % m_rowHeight);
@@ -744,13 +914,21 @@ void TimelineWidget::paintEvent(QPaintEvent *e)
         painter.setPen(m_axisForeground);
         painter.drawText(0, y, m_axisWidth, m_rowHeight, Qt::AlignHCenter | Qt::AlignVCenter, QString("%1").arg(m_rowPrograms[row]));
 
-        painter.setPen(m_axisBorder);
-        painter.drawLine(0, y + m_rowHeight - 1, m_axisWidth - 1, y + m_rowHeight - 1);
+        if (m_selection.type == SelectProgram && m_selection.program == m_rowPrograms[row]) {
+            painter.setPen(m_selectionBorder);
+            painter.drawLine(0, qMax(0, y - 1), widgetWidth, qMax(0, y - 1));
+            painter.drawLine(0, y + m_rowHeight - 1, widgetWidth, y + m_rowHeight - 1);
+            painter.drawLine(m_axisWidth - 1, y - 1, m_axisWidth - 1, y + m_rowHeight - 1);
+        } else {
+            painter.setPen(m_axisBorder);
+            painter.drawLine(0, y + m_rowHeight - 1, m_axisWidth - 1, y + m_rowHeight - 1);
 
-        painter.setPen(m_axisLine);
-        painter.drawLine(m_axisWidth, y + m_rowHeight - 1, widgetWidth, y + m_rowHeight - 1);
+            painter.setPen(m_axisLine);
+            painter.drawLine(m_axisWidth, y + m_rowHeight - 1, widgetWidth, y + m_rowHeight - 1);
+        }
     }
 
+
     /* Draw the "CPU" axis label */
     painter.resetTransform();
     painter.translate(0, m_axisHeight);
@@ -766,13 +944,27 @@ void TimelineWidget::paintEvent(QPaintEvent *e)
     painter.drawLine(m_axisWidth, m_rowHeight - 1, widgetWidth, m_rowHeight - 1);
 
 
+    /* Draw the "GPU" axis label */
+    painter.translate(0, m_rowHeight);
+
+    painter.setPen(m_axisBorder);
+    painter.setBrush(m_axisBackground);
+    painter.drawRect(-1, -1, m_axisWidth, m_rowHeight);
+
+    painter.setPen(m_axisForeground);
+    painter.drawText(0, 0, m_axisWidth - 1, m_rowHeight - 1, Qt::AlignHCenter | Qt::AlignVCenter, "GPU");
+
+    painter.setPen(m_axisBorder);
+    painter.drawLine(m_axisWidth, m_rowHeight - 1, widgetWidth, m_rowHeight - 1);
+
+
     /* Draw the frame numbers */
     painter.resetTransform();
 
     painter.setPen(m_axisForeground);
     painter.translate(m_axisWidth, 0);
 
-    int lastLabel = -9999;
+    int lastLabel = -999; /* Ensure first label gets drawn */
 
     double scroll = m_time;
     scroll /= m_timeWidth;
@@ -784,7 +976,7 @@ void TimelineWidget::paintEvent(QPaintEvent *e)
         bool draw = true;
         int width;
 
-        if (frame.cpuStart > timeEnd) {
+        if (frame.cpuStart > m_timeEnd) {
             break;
         }
 
@@ -835,6 +1027,7 @@ void TimelineWidget::paintEvent(QPaintEvent *e)
         }
     }
 
+
     /* Draw "Frame" axis label */
     painter.resetTransform();
 
@@ -845,28 +1038,30 @@ void TimelineWidget::paintEvent(QPaintEvent *e)
     painter.setPen(m_axisForeground);
     painter.drawText(0, 0, m_axisWidth - 1, m_axisHeight - 1, Qt::AlignHCenter | Qt::AlignVCenter, "Frame");
 
-    /* Draw the active selection border */
-    if (m_timeSelectionStart != m_timeSelectionEnd) {
-        int selectionLeft  = timeToPosition(m_timeSelectionStart) + m_axisWidth;
-        int selectionRight = (timeToPosition(m_timeSelectionEnd) + 0.5) + m_axisWidth;
 
+    /* Draw the active selection border */
+    if (m_selection.type == SelectTime) {
         painter.setPen(m_selectionBorder);
 
-        if (selectionLeft >= m_axisWidth && selectionLeft < widgetWidth) {
-            painter.drawLine(selectionLeft, 0, selectionLeft, widgetHeight);
+        m_selectionLeft += m_axisWidth;
+        m_selectionRight += m_axisWidth;
+
+        if (m_selectionLeft >= m_axisWidth && m_selectionLeft < widgetWidth) {
+            painter.drawLine(m_selectionLeft, 0, m_selectionLeft, widgetHeight);
         }
 
-        if (selectionRight >= m_axisWidth && selectionRight < widgetWidth) {
-            painter.drawLine(selectionRight, 0, selectionRight, widgetHeight);
+        if (m_selectionRight >= m_axisWidth && m_selectionRight < widgetWidth) {
+            painter.drawLine(m_selectionRight, 0, m_selectionRight, widgetHeight);
         }
 
-        selectionLeft = qBound(m_axisWidth, selectionLeft, widgetWidth);
-        selectionRight = qBound(m_axisWidth, selectionRight, widgetWidth);
+        m_selectionLeft = qBound(m_axisWidth, m_selectionLeft, widgetWidth);
+        m_selectionRight = qBound(m_axisWidth, m_selectionRight, widgetWidth);
 
-        painter.drawLine(selectionLeft, m_axisHeight - 1, selectionRight, m_axisHeight - 1);
-        painter.fillRect(selectionLeft, 0, selectionRight - selectionLeft, widgetHeight, m_selectionBackground);
+        painter.drawLine(m_selectionLeft, m_axisHeight - 1, m_selectionRight, m_axisHeight - 1);
+        painter.fillRect(m_selectionLeft, 0, m_selectionRight - m_selectionLeft, widgetHeight, m_selectionBackground);
     }
 
+
     /* Draw the ruler zoom */
     if (m_mousePressMode == RulerZoom) {
         int x1 = m_mousePressPosition.x();