aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/DurationMinimap.java541
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLCallGroups.java202
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLFunctionTraceViewer.java984
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateContentProvider.java75
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateLabelProvider.java103
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateViewPage.java372
6 files changed, 2277 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/DurationMinimap.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/DurationMinimap.java
new file mode 100644
index 000000000..9b4c57cad
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/DurationMinimap.java
@@ -0,0 +1,541 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.gltrace.editors;
+
+import com.android.ide.eclipse.gltrace.GLProtoBuf.GLMessage.Function;
+import com.android.ide.eclipse.gltrace.model.GLCall;
+import com.android.ide.eclipse.gltrace.model.GLTrace;
+
+import org.eclipse.jface.resource.FontRegistry;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseMoveListener;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DurationMinimap extends Canvas {
+ /** Default alpha value. */
+ private static final int DEFAULT_ALPHA = 255;
+
+ /** Alpha value for highlighting visible calls. */
+ private static final int VISIBLE_CALLS_HIGHLIGHT_ALPHA = 50;
+
+ /** Clamp call durations at this value. */
+ private static final long CALL_DURATION_CLAMP = 20000;
+
+ private static final String FONT_KEY = "default.font"; //$NON-NLS-1$
+
+ /** Scale font size by this amount to get the max display length of call duration. */
+ private static final int MAX_DURATION_LENGTH_SCALE = 6;
+
+ /** List of GL Calls in the trace. */
+ private List<GLCall> mCalls;
+
+ /** Number of GL contexts in the trace. */
+ private int mContextCount;
+
+ /** Starting call index of currently displayed frame. */
+ private int mStartCallIndex;
+
+ /** Ending call index of currently displayed frame. */
+ private int mEndCallIndex;
+
+ /** The top index that is currently visible in the table. */
+ private int mVisibleCallTopIndex;
+
+ /** The bottom index that is currently visible in the table. */
+ private int mVisibleCallBottomIndex;
+
+ private Color mBackgroundColor;
+ private Color mDurationLineColor;
+ private Color mGlDrawColor;
+ private Color mGlErrorColor;
+ private Color mContextHeaderColor;
+ private Color mVisibleCallsHighlightColor;
+ private Color mMouseMarkerColor;
+
+ private FontRegistry mFontRegistry;
+ private int mFontWidth;
+ private int mFontHeight;
+
+ // back buffers used for double buffering
+ private Image mBackBufferImage;
+ private GC mBackBufferGC;
+
+ // mouse state
+ private boolean mMouseInSelf;
+ private int mMouseY;
+
+ // helper object used to position various items on screen
+ private final PositionHelper mPositionHelper;
+
+ public DurationMinimap(Composite parent, GLTrace trace) {
+ super(parent, SWT.NO_BACKGROUND);
+
+ setInput(trace);
+
+ initializeColors();
+ initializeFonts();
+
+ mPositionHelper = new PositionHelper(
+ mFontHeight,
+ mContextCount,
+ mFontWidth * MAX_DURATION_LENGTH_SCALE, /* max display length for call. */
+ CALL_DURATION_CLAMP /* max duration */);
+
+ addPaintListener(new PaintListener() {
+ @Override
+ public void paintControl(PaintEvent e) {
+ draw(e.display, e.gc);
+ }
+ });
+
+ addListener(SWT.Resize, new Listener() {
+ @Override
+ public void handleEvent(Event event) {
+ controlResized();
+ }
+ });
+
+ addMouseMoveListener(new MouseMoveListener() {
+ @Override
+ public void mouseMove(MouseEvent e) {
+ mouseMoved(e);
+ }
+ });
+
+ addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseUp(MouseEvent e) {
+ mouseClicked(e);
+ }
+ });
+
+ addMouseTrackListener(new MouseTrackListener() {
+ @Override
+ public void mouseHover(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseExit(MouseEvent e) {
+ mMouseInSelf = false;
+ redraw();
+ }
+
+ @Override
+ public void mouseEnter(MouseEvent e) {
+ mMouseInSelf = true;
+ redraw();
+ }
+ });
+ }
+
+ public void setInput(GLTrace trace) {
+ if (trace != null) {
+ mCalls = trace.getGLCalls();
+ mContextCount = trace.getContexts().size();
+ } else {
+ mCalls = null;
+ mContextCount = 1;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ disposeColors();
+ disposeBackBuffer();
+ super.dispose();
+ }
+
+ private void initializeColors() {
+ mBackgroundColor = new Color(getDisplay(), 0x33, 0x33, 0x33);
+ mDurationLineColor = new Color(getDisplay(), 0x08, 0x51, 0x9c);
+ mGlDrawColor = new Color(getDisplay(), 0x6b, 0xae, 0xd6);
+ mContextHeaderColor = new Color(getDisplay(), 0xd1, 0xe5, 0xf0);
+ mVisibleCallsHighlightColor = new Color(getDisplay(), 0xcc, 0xcc, 0xcc);
+ mMouseMarkerColor = new Color(getDisplay(), 0xaa, 0xaa, 0xaa);
+
+ mGlErrorColor = getDisplay().getSystemColor(SWT.COLOR_RED);
+ }
+
+ private void disposeColors() {
+ mBackgroundColor.dispose();
+ mDurationLineColor.dispose();
+ mGlDrawColor.dispose();
+ mContextHeaderColor.dispose();
+ mVisibleCallsHighlightColor.dispose();
+ mMouseMarkerColor.dispose();
+ }
+
+ private void initializeFonts() {
+ mFontRegistry = new FontRegistry(getDisplay());
+ mFontRegistry.put(FONT_KEY,
+ new FontData[] { new FontData("Arial", 8, SWT.NORMAL) }); //$NON-NLS-1$
+
+ GC gc = new GC(getDisplay());
+ gc.setFont(mFontRegistry.get(FONT_KEY));
+ mFontWidth = gc.getFontMetrics().getAverageCharWidth();
+ mFontHeight = gc.getFontMetrics().getHeight();
+ gc.dispose();
+ }
+
+ private void initializeBackBuffer() {
+ Rectangle clientArea = getClientArea();
+
+ if (clientArea.width == 0 || clientArea.height == 0) {
+ mBackBufferImage = null;
+ mBackBufferGC = null;
+ return;
+ }
+
+ mBackBufferImage = new Image(getDisplay(),
+ clientArea.width,
+ clientArea.height);
+ mBackBufferGC = new GC(mBackBufferImage);
+ }
+
+ private void disposeBackBuffer() {
+ if (mBackBufferImage != null) {
+ mBackBufferImage.dispose();
+ mBackBufferImage = null;
+ }
+
+ if (mBackBufferGC != null) {
+ mBackBufferGC.dispose();
+ mBackBufferGC = null;
+ }
+ }
+
+ private void mouseMoved(MouseEvent e) {
+ mMouseY = e.y;
+ redraw();
+ }
+
+ private void mouseClicked(MouseEvent e) {
+ if (mMouseInSelf) {
+ int selIndex = mPositionHelper.getCallAt(mMouseY);
+ sendCallSelectedEvent(selIndex);
+ redraw();
+ }
+ }
+
+ private void draw(Display display, GC gc) {
+ if (mBackBufferImage == null) {
+ initializeBackBuffer();
+ }
+
+ if (mBackBufferImage == null) {
+ return;
+ }
+
+ // draw contents onto the back buffer
+ drawBackground(mBackBufferGC, mBackBufferImage.getBounds());
+ drawContextHeaders(mBackBufferGC);
+ drawCallDurations(mBackBufferGC);
+ drawVisibleCallHighlights(mBackBufferGC);
+ drawMouseMarkers(mBackBufferGC);
+
+ // finally copy over the rendered back buffer onto screen
+ int width = getClientArea().width;
+ int height = getClientArea().height;
+ gc.drawImage(mBackBufferImage,
+ 0, 0, width, height,
+ 0, 0, width, height);
+ }
+
+ private void drawBackground(GC gc, Rectangle bounds) {
+ gc.setBackground(mBackgroundColor);
+ gc.fillRectangle(bounds);
+ }
+
+ private void drawContextHeaders(GC gc) {
+ if (mContextCount <= 1) {
+ return;
+ }
+
+ gc.setForeground(mContextHeaderColor);
+ gc.setFont(mFontRegistry.get(FONT_KEY));
+ for (int i = 0; i < mContextCount; i++) {
+ Point p = mPositionHelper.getHeaderLocation(i);
+ gc.drawText("CTX" + Integer.toString(i), p.x, p.y);
+ }
+ }
+
+ /** Draw the call durations as a sequence of lines.
+ *
+ * Calls are arranged on the y-axis based on the sequence in which they were originally
+ * called by the application. If the display height is lesser than the number of calls, then
+ * not every call is shown - the calls are underscanned based the height of the display.
+ *
+ * The x-axis shows two pieces of information: the duration of the call, and the context
+ * in which the call was made. The duration controls how long the displayed line is, and
+ * the context controls the starting offset of the line.
+ */
+ private void drawCallDurations(GC gc) {
+ if (mCalls == null || mCalls.size() < mEndCallIndex) {
+ return;
+ }
+
+ gc.setBackground(mDurationLineColor);
+
+ int callUnderScan = mPositionHelper.getCallUnderScanValue();
+ for (int i = mStartCallIndex; i < mEndCallIndex; i += callUnderScan) {
+ boolean resetColor = false;
+ GLCall c = mCalls.get(i);
+
+ long duration = c.getWallDuration();
+
+ if (c.hasErrors()) {
+ gc.setBackground(mGlErrorColor);
+ resetColor = true;
+
+ // If the call has any errors, we want it to be visible in the minimap
+ // regardless of how long it took.
+ duration = mPositionHelper.getMaxDuration();
+ } else if (c.getFunction() == Function.glDrawArrays
+ || c.getFunction() == Function.glDrawElements
+ || c.getFunction() == Function.eglSwapBuffers) {
+ gc.setBackground(mGlDrawColor);
+ resetColor = true;
+
+ // render all draw calls & swap buffer at max length
+ duration = mPositionHelper.getMaxDuration();
+ }
+
+ Rectangle bounds = mPositionHelper.getDurationBounds(
+ i - mStartCallIndex,
+ c.getContextId(),
+ duration);
+ gc.fillRectangle(bounds);
+
+ if (resetColor) {
+ gc.setBackground(mDurationLineColor);
+ }
+ }
+ }
+
+ /**
+ * Draw a bounding box that highlights the currently visible range of calls in the
+ * {@link GLFunctionTraceViewer} table.
+ */
+ private void drawVisibleCallHighlights(GC gc) {
+ gc.setAlpha(VISIBLE_CALLS_HIGHLIGHT_ALPHA);
+ gc.setBackground(mVisibleCallsHighlightColor);
+ gc.fillRectangle(mPositionHelper.getBoundsFramingCalls(
+ mVisibleCallTopIndex - mStartCallIndex,
+ mVisibleCallBottomIndex - mStartCallIndex));
+ gc.setAlpha(DEFAULT_ALPHA);
+ }
+
+ private void drawMouseMarkers(GC gc) {
+ if (!mMouseInSelf) {
+ return;
+ }
+
+ if (mPositionHelper.getCallAt(mMouseY) < 0) {
+ return;
+ }
+
+ gc.setForeground(mMouseMarkerColor);
+ gc.drawLine(0, mMouseY, getClientArea().width, mMouseY);
+ }
+
+ private void controlResized() {
+ // regenerate back buffer on size changes
+ disposeBackBuffer();
+ initializeBackBuffer();
+
+ redraw();
+ }
+
+ public int getMinimumWidth() {
+ return mPositionHelper.getMinimumWidth();
+ }
+
+ /** Set the GL Call start and end indices for currently displayed frame. */
+ public void setCallRangeForCurrentFrame(int startCallIndex, int endCallIndex) {
+ mStartCallIndex = startCallIndex;
+ mEndCallIndex = endCallIndex;
+ mPositionHelper.updateCallDensity(mEndCallIndex - mStartCallIndex, getClientArea().height);
+ redraw();
+ }
+
+ /**
+ * Set the call range that is currently visible in the {@link GLFunctionTraceViewer} table.
+ * @param visibleTopIndex index of call currently visible at the top of the table.
+ * @param visibleBottomIndex index of call currently visible at the bottom of the table.
+ */
+ public void setVisibleCallRange(int visibleTopIndex, int visibleBottomIndex) {
+ mVisibleCallTopIndex = visibleTopIndex;
+ mVisibleCallBottomIndex = visibleBottomIndex;
+ redraw();
+ }
+
+ public interface ICallSelectionListener {
+ void callSelected(int selectedCallIndex);
+ }
+
+ private List<ICallSelectionListener> mListeners = new ArrayList<ICallSelectionListener>();
+
+ public void addCallSelectionListener(ICallSelectionListener l) {
+ mListeners.add(l);
+ }
+
+ private void sendCallSelectedEvent(int selectedCall) {
+ for (ICallSelectionListener l : mListeners) {
+ l.callSelected(selectedCall);
+ }
+ }
+
+ /** Utility class to help with the positioning and sizes of elements in the canvas. */
+ private static class PositionHelper {
+ /** Left Margin after which duration lines are drawn. */
+ private static final int LEFT_MARGIN = 5;
+
+ /** Top margin after which header is drawn. */
+ private static final int TOP_MARGIN = 5;
+
+ /** # of pixels of padding between duration markers for different contexts. */
+ private static final int CONTEXT_PADDING = 10;
+
+ private final int mHeaderMargin;
+ private final int mContextCount;
+ private final int mMaxDurationLength;
+ private final long mMaxDuration;
+ private final double mScale;
+
+ private int mCallCount;
+ private int mNumCallsPerPixel = 1;
+
+ public PositionHelper(int fontHeight, int contextCount,
+ int maxDurationLength, long maxDuration) {
+ mContextCount = contextCount;
+ mMaxDurationLength = maxDurationLength;
+ mMaxDuration = maxDuration;
+ mScale = (double) maxDurationLength / maxDuration;
+
+ // header region is present only there are multiple contexts
+ if (mContextCount > 1) {
+ mHeaderMargin = fontHeight * 3;
+ } else {
+ mHeaderMargin = 0;
+ }
+ }
+
+ /** Get the minimum width of the canvas. */
+ public int getMinimumWidth() {
+ return LEFT_MARGIN + (mMaxDurationLength + CONTEXT_PADDING) * mContextCount;
+ }
+
+ /** Get the bounds for a call duration line. */
+ public Rectangle getDurationBounds(int callIndex, int context, long duration) {
+ if (duration <= 0) {
+ duration = 1;
+ } else if (duration > mMaxDuration) {
+ duration = mMaxDuration;
+ }
+
+ int x = LEFT_MARGIN + ((mMaxDurationLength + CONTEXT_PADDING) * context);
+ int y = (callIndex/mNumCallsPerPixel) + TOP_MARGIN + mHeaderMargin;
+ int w = (int) (duration * mScale);
+ int h = 1;
+
+ return new Rectangle(x, y, w, h);
+ }
+
+ public long getMaxDuration() {
+ return mMaxDuration;
+ }
+
+ /** Get the bounds for calls spanning given range. */
+ public Rectangle getBoundsFramingCalls(int startCallIndex, int endCallIndex) {
+ if (startCallIndex >= 0 && endCallIndex >= startCallIndex
+ && endCallIndex <= mCallCount) {
+ int x = LEFT_MARGIN;
+ int y = (startCallIndex/mNumCallsPerPixel) + TOP_MARGIN + mHeaderMargin;
+ int w = ((mMaxDurationLength + CONTEXT_PADDING) * mContextCount);
+ int h = (endCallIndex - startCallIndex)/mNumCallsPerPixel;
+ return new Rectangle(x, y, w, h);
+ } else {
+ return new Rectangle(0, 0, 0, 0);
+ }
+ }
+
+ public Point getHeaderLocation(int context) {
+ int x = LEFT_MARGIN + ((mMaxDurationLength + CONTEXT_PADDING) * context);
+ return new Point(x, TOP_MARGIN);
+ }
+
+ /** Update the call density based on the number of calls to be displayed and
+ * the available height to display them in. */
+ public void updateCallDensity(int callCount, int displayHeight) {
+ mCallCount = callCount;
+
+ if (displayHeight <= 0) {
+ displayHeight = callCount + 1;
+ }
+
+ mNumCallsPerPixel = (callCount / displayHeight) + 1;
+ }
+
+ /** Get the underscan value. In cases where there are more calls to be displayed
+ * than there are availble pixels, we only display 1 out of every underscan calls. */
+ public int getCallUnderScanValue() {
+ return mNumCallsPerPixel;
+ }
+
+ /** Get the index of the call at given y offset. */
+ public int getCallAt(int y) {
+ if (!isWithinBounds(y)) {
+ return -1;
+ }
+
+ Rectangle displayBounds = getBoundsFramingCalls(0, mCallCount);
+ return (y - displayBounds.y) * mNumCallsPerPixel;
+ }
+
+ /** Does the provided y offset map to a valid call? */
+ private boolean isWithinBounds(int y) {
+ Rectangle displayBounds = getBoundsFramingCalls(0, mCallCount);
+ if (y < displayBounds.y) {
+ return false;
+ }
+
+ if (y > (displayBounds.y + displayBounds.height)) {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLCallGroups.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLCallGroups.java
new file mode 100644
index 000000000..226d4831a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLCallGroups.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.gltrace.editors;
+
+import com.android.ide.eclipse.gltrace.GLProtoBuf.GLMessage.Function;
+import com.android.ide.eclipse.gltrace.model.GLCall;
+import com.android.ide.eclipse.gltrace.model.GLTrace;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Stack;
+
+public class GLCallGroups {
+ /**
+ * A {@link GLCallNode} is a simple wrapper around a {@link GLCall} that
+ * adds the notion of hierarchy.
+ */
+ public interface GLCallNode {
+ /** Does this call have child nodes? */
+ boolean hasChildren();
+
+ /** Returns a list of child nodes of this call. */
+ List<GLCallNode> getChildren();
+
+ /** Returns the {@link GLCall} that is wrapped by this node. */
+ GLCall getCall();
+
+ /** Returns the parent of this node, the parent is null if this is a top level node */
+ GLCallNode getParent();
+
+ /** Set the parent node. */
+ void setParent(GLCallNode parent);
+ }
+
+ private static class GLTreeNode implements GLCallNode {
+ private final GLCall mCall;
+ private GLCallNode mParent;
+ private List<GLCallNode> mGLCallNodes;
+
+ public GLTreeNode(GLCall call) {
+ mCall = call;
+ mGLCallNodes = new ArrayList<GLCallNode>();
+ }
+
+ @Override
+ public boolean hasChildren() {
+ return true;
+ }
+
+ @Override
+ public GLCallNode getParent() {
+ return mParent;
+ }
+
+ @Override
+ public void setParent(GLCallNode parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public List<GLCallNode> getChildren() {
+ return mGLCallNodes;
+ }
+
+ public void addChild(GLCallNode n) {
+ mGLCallNodes.add(n);
+ n.setParent(this);
+ }
+
+ @Override
+ public GLCall getCall() {
+ return mCall;
+ }
+ }
+
+ private static class GLLeafNode implements GLCallNode {
+ private final GLCall mCall;
+ private GLCallNode mParent;
+
+ public GLLeafNode(GLCall call) {
+ mCall = call;
+ }
+
+ @Override
+ public boolean hasChildren() {
+ return false;
+ }
+
+ @Override
+ public List<GLCallNode> getChildren() {
+ return null;
+ }
+
+ @Override
+ public GLCallNode getParent() {
+ return mParent;
+ }
+
+ @Override
+ public void setParent(GLCallNode parent) {
+ mParent = parent;
+ }
+
+ @Override
+ public GLCall getCall() {
+ return mCall;
+ }
+ }
+
+ /**
+ * Impose a hierarchy on a list of {@link GLCall}'s based on the presence of
+ * {@link Function#glPushGroupMarkerEXT} and {@link Function#glPopGroupMarkerEXT} calls.
+ * Such a hierarchy is possible only if calls from a single context are considered.
+ * @param trace trace to look at
+ * @param start starting call index
+ * @param end ending call index
+ * @param contextToGroup context from which calls should be grouped. If no such context
+ * is present, then all calls in the given range will be returned back as a flat
+ * list.
+ * @return a tree structured list of {@link GLCallNode} objects
+ */
+ public static List<GLCallNode> constructCallHierarchy(GLTrace trace, int start, int end,
+ int contextToGroup) {
+ if (trace == null) {
+ return Collections.emptyList();
+ }
+
+ if (contextToGroup < 0 || contextToGroup > trace.getContexts().size()) {
+ return flatHierarchy(trace, start, end);
+ }
+
+ List<GLCall> calls = trace.getGLCalls();
+
+ Stack<GLTreeNode> hierarchyStack = new Stack<GLTreeNode>();
+ List<GLCallNode> items = new ArrayList<GLCallNode>();
+
+ for (int i = start; i < end; i++) {
+ GLCall c = calls.get(i);
+ if (c.getContextId() != contextToGroup) {
+ // skip this call if it is not part of the context we need to display
+ continue;
+ }
+
+ if (c.getFunction() == Function.glPushGroupMarkerEXT) {
+ GLTreeNode group = new GLTreeNode(c);
+ if (hierarchyStack.size() > 0) {
+ hierarchyStack.peek().addChild(group);
+ } else {
+ items.add(group);
+ }
+ hierarchyStack.push(group);
+ } else if (c.getFunction() == Function.glPopGroupMarkerEXT) {
+ if (hierarchyStack.size() > 0) {
+ hierarchyStack.pop();
+ } else {
+ // FIXME: If we are attempting to pop from an empty stack,
+ // that implies that a push marker was seen in a prior frame
+ // (in a call before @start). In such a case, we simply continue
+ // adding further calls to the root of the hierarchy rather than
+ // searching backwards in the call list for the corresponding
+ // push markers.
+ items.add(new GLLeafNode(c));
+ }
+ } else {
+ GLLeafNode leaf = new GLLeafNode(c);
+ if (hierarchyStack.size() > 0) {
+ hierarchyStack.peek().addChild(leaf);
+ } else {
+ items.add(leaf);
+ }
+ }
+ }
+
+ return items;
+ }
+
+ private static List<GLCallNode> flatHierarchy(GLTrace trace, int start, int end) {
+ List<GLCallNode> items = new ArrayList<GLCallNode>();
+
+ List<GLCall> calls = trace.getGLCalls();
+ for (int i = start; i < end; i++) {
+ items.add(new GLLeafNode(calls.get(i)));
+ }
+
+ return items;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLFunctionTraceViewer.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLFunctionTraceViewer.java
new file mode 100644
index 000000000..b809ddddf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/GLFunctionTraceViewer.java
@@ -0,0 +1,984 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.gltrace.editors;
+
+import com.android.ddmuilib.AbstractBufferFindTarget;
+import com.android.ddmuilib.FindDialog;
+import com.android.ide.eclipse.gltrace.GLProtoBuf.GLMessage.Function;
+import com.android.ide.eclipse.gltrace.GlTracePlugin;
+import com.android.ide.eclipse.gltrace.SwtUtils;
+import com.android.ide.eclipse.gltrace.TraceFileParserTask;
+import com.android.ide.eclipse.gltrace.editors.DurationMinimap.ICallSelectionListener;
+import com.android.ide.eclipse.gltrace.editors.GLCallGroups.GLCallNode;
+import com.android.ide.eclipse.gltrace.model.GLCall;
+import com.android.ide.eclipse.gltrace.model.GLFrame;
+import com.android.ide.eclipse.gltrace.model.GLTrace;
+import com.android.ide.eclipse.gltrace.views.FrameSummaryViewPage;
+import com.android.ide.eclipse.gltrace.views.detail.DetailsPage;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.ProgressMonitorDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.CellLabelProvider;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Scale;
+import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Spinner;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.IURIEditorInput;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.part.EditorPart;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Display OpenGL function trace in a tabular view. */
+public class GLFunctionTraceViewer extends EditorPart implements ISelectionProvider {
+ public static final String ID = "com.android.ide.eclipse.gltrace.GLFunctionTrace"; //$NON-NLS-1$
+
+ private static final String DEFAULT_FILTER_MESSAGE = "Filter list of OpenGL calls. Accepts Java regexes.";
+ private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$
+
+ private static Image sExpandAllIcon;
+
+ private static String sLastExportedToFolder;
+
+ private String mFilePath;
+ private Scale mFrameSelectionScale;
+ private Spinner mFrameSelectionSpinner;
+
+ private GLTrace mTrace;
+
+ private TreeViewer mFrameTreeViewer;
+ private List<GLCallNode> mTreeViewerNodes;
+
+ private Text mFilterText;
+ private GLCallFilter mGLCallFilter;
+
+ private Color mGldrawTextColor;
+ private Color mGlCallErrorColor;
+
+ /**
+ * Job to refresh the tree view & frame summary view.
+ *
+ * When the currently displayed frame is changed, either via the {@link #mFrameSelectionScale}
+ * or via {@link #mFrameSelectionSpinner}, we need to update the displayed tree of calls for
+ * that frame, and the frame summary view. Both these operations need to happen on the UI
+ * thread, but are time consuming. This works out ok if the frame selection is not changing
+ * rapidly (i.e., when the spinner or scale is moved to the target frame in a single action).
+ * However, if the spinner is constantly pressed, then the user is scrolling through a sequence
+ * of frames, and rather than refreshing the details for each of the intermediate frames,
+ * we create a job to refresh the details and schedule the job after a short interval
+ * {@link #TREE_REFRESH_INTERVAL}. This allows us to stay responsive to the spinner/scale,
+ * and not do the costly refresh for each of the intermediate frames.
+ */
+ private Job mTreeRefresherJob;
+ private final Object mTreeRefresherLock = new Object();
+ private static final int TREE_REFRESH_INTERVAL_MS = 250;
+
+ private int mCurrentFrame;
+
+ // Currently displayed frame's start and end call indices.
+ private int mCallStartIndex;
+ private int mCallEndIndex;
+
+ private DurationMinimap mDurationMinimap;
+ private ScrollBar mVerticalScrollBar;
+
+ private Combo mContextSwitchCombo;
+ private boolean mShowContextSwitcher;
+ private int mCurrentlyDisplayedContext = -1;
+
+ private StateViewPage mStateViewPage;
+ private FrameSummaryViewPage mFrameSummaryViewPage;
+ private DetailsPage mDetailsPage;
+
+ private ToolItem mExpandAllToolItem;
+ private ToolItem mCollapseAllToolItem;
+ private ToolItem mSaveAsToolItem;
+
+ public GLFunctionTraceViewer() {
+ mGldrawTextColor = Display.getDefault().getSystemColor(SWT.COLOR_BLUE);
+ mGlCallErrorColor = Display.getDefault().getSystemColor(SWT.COLOR_RED);
+ }
+
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ }
+
+ @Override
+ public void doSaveAs() {
+ }
+
+ @Override
+ public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+ // we use a IURIEditorInput to allow opening files not within the workspace
+ if (!(input instanceof IURIEditorInput)) {
+ throw new PartInitException("GL Function Trace View: unsupported input type.");
+ }
+
+ setSite(site);
+ setInput(input);
+ mFilePath = ((IURIEditorInput) input).getURI().getPath();
+
+ // set the editor part name to be the name of the file.
+ File f = new File(mFilePath);
+ setPartName(f.getName());
+ }
+
+ @Override
+ public boolean isDirty() {
+ return false;
+ }
+
+ @Override
+ public boolean isSaveAsAllowed() {
+ return false;
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(1, false));
+ GridData gd = new GridData(GridData.FILL_BOTH);
+ c.setLayoutData(gd);
+
+ setInput(parent.getShell(), mFilePath);
+
+ createFrameSelectionControls(c);
+ createOptionsBar(c);
+ createFrameTraceView(c);
+
+ getSite().setSelectionProvider(mFrameTreeViewer);
+
+ IActionBars actionBars = getEditorSite().getActionBars();
+ actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(),
+ new Action("Copy") {
+ @Override
+ public void run() {
+ copySelectionToClipboard();
+ }
+ });
+
+ actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
+ new Action("Select All") {
+ @Override
+ public void run() {
+ selectAll();
+ }
+ });
+
+ actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(),
+ new Action("Find") {
+ @Override
+ public void run() {
+ showFindDialog();
+ }
+ });
+ }
+
+ public void setInput(Shell shell, String tracePath) {
+ ProgressMonitorDialog dlg = new ProgressMonitorDialog(shell);
+ TraceFileParserTask parser = new TraceFileParserTask(mFilePath);
+ try {
+ dlg.run(true, true, parser);
+ } catch (InvocationTargetException e) {
+ // exception while parsing, display error to user
+ MessageDialog.openError(shell,
+ "Error parsing OpenGL Trace File",
+ e.getCause().getMessage());
+ return;
+ } catch (InterruptedException e) {
+ // operation canceled by user, just return
+ return;
+ }
+
+ mTrace = parser.getTrace();
+ mShowContextSwitcher = (mTrace == null) ? false : mTrace.getContexts().size() > 1;
+ if (mStateViewPage != null) {
+ mStateViewPage.setInput(mTrace);
+ }
+ if (mFrameSummaryViewPage != null) {
+ mFrameSummaryViewPage.setInput(mTrace);
+ }
+ if (mDetailsPage != null) {
+ mDetailsPage.setInput(mTrace);
+ }
+ if (mDurationMinimap != null) {
+ mDurationMinimap.setInput(mTrace);
+ }
+
+ Display.getDefault().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ refreshUI();
+ }
+ });
+ }
+
+ private void refreshUI() {
+ if (mTrace == null || mTrace.getGLCalls().size() == 0) {
+ setFrameCount(0);
+ return;
+ }
+
+ setFrameCount(mTrace.getFrames().size());
+ selectFrame(1);
+ }
+
+ private void createFrameSelectionControls(Composite parent) {
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(3, false));
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ c.setLayoutData(gd);
+
+ Label l = new Label(c, SWT.NONE);
+ l.setText("Select Frame:");
+
+ mFrameSelectionScale = new Scale(c, SWT.HORIZONTAL);
+ mFrameSelectionScale.setMinimum(1);
+ mFrameSelectionScale.setMaximum(1);
+ mFrameSelectionScale.setSelection(0);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ mFrameSelectionScale.setLayoutData(gd);
+
+ mFrameSelectionScale.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int selectedFrame = mFrameSelectionScale.getSelection();
+ mFrameSelectionSpinner.setSelection(selectedFrame);
+ selectFrame(selectedFrame);
+ }
+ });
+
+ mFrameSelectionSpinner = new Spinner(c, SWT.BORDER);
+ gd = new GridData();
+ // width to hold atleast 6 digits
+ gd.widthHint = SwtUtils.getApproximateFontWidth(mFrameSelectionSpinner) * 6;
+ mFrameSelectionSpinner.setLayoutData(gd);
+
+ mFrameSelectionSpinner.setMinimum(1);
+ mFrameSelectionSpinner.setMaximum(1);
+ mFrameSelectionSpinner.setSelection(0);
+ mFrameSelectionSpinner.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ int selectedFrame = mFrameSelectionSpinner.getSelection();
+ mFrameSelectionScale.setSelection(selectedFrame);
+ selectFrame(selectedFrame);
+ }
+ });
+ }
+
+ private void setFrameCount(int nFrames) {
+ boolean en = nFrames > 0;
+ mFrameSelectionScale.setEnabled(en);
+ mFrameSelectionSpinner.setEnabled(en);
+
+ mFrameSelectionScale.setMaximum(nFrames);
+ mFrameSelectionSpinner.setMaximum(nFrames);
+ }
+
+ private void selectFrame(int selectedFrame) {
+ mFrameSelectionScale.setSelection(selectedFrame);
+ mFrameSelectionSpinner.setSelection(selectedFrame);
+
+ synchronized (mTreeRefresherLock) {
+ if (mTrace != null) {
+ GLFrame f = mTrace.getFrame(selectedFrame - 1);
+ mCallStartIndex = f.getStartIndex();
+ mCallEndIndex = f.getEndIndex();
+ } else {
+ mCallStartIndex = mCallEndIndex = 0;
+ }
+
+ mCurrentFrame = selectedFrame - 1;
+
+ scheduleNewRefreshJob();
+ }
+
+ // update minimap view
+ mDurationMinimap.setCallRangeForCurrentFrame(mCallStartIndex, mCallEndIndex);
+ }
+
+ /**
+ * Show only calls from the given context
+ * @param context context id whose calls should be displayed. Illegal values will result in
+ * calls from all contexts being displayed.
+ */
+ private void selectContext(int context) {
+ if (mCurrentlyDisplayedContext == context) {
+ return;
+ }
+
+ synchronized (mTreeRefresherLock) {
+ mCurrentlyDisplayedContext = context;
+ scheduleNewRefreshJob();
+ }
+ }
+
+ private void scheduleNewRefreshJob() {
+ if (mTreeRefresherJob != null) {
+ return;
+ }
+
+ mTreeRefresherJob = new Job("Refresh GL Trace View Tree") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ final int start, end, context;
+
+ synchronized (mTreeRefresherLock) {
+ start = mCallStartIndex;
+ end = mCallEndIndex;
+ context = mCurrentlyDisplayedContext;
+
+ mTreeRefresherJob = null;
+ }
+
+ // update tree view in the editor
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ refreshTree(start, end, context);
+
+ // update the frame summary view
+ if (mFrameSummaryViewPage != null) {
+ mFrameSummaryViewPage.setSelectedFrame(mCurrentFrame);
+ }
+ }
+ });
+ return Status.OK_STATUS;
+ }
+ };
+ mTreeRefresherJob.setPriority(Job.SHORT);
+ mTreeRefresherJob.schedule(TREE_REFRESH_INTERVAL_MS);
+ }
+
+ private void refreshTree(int startCallIndex, int endCallIndex, int contextToDisplay) {
+ mTreeViewerNodes = GLCallGroups.constructCallHierarchy(mTrace,
+ startCallIndex, endCallIndex,
+ contextToDisplay);
+ mFrameTreeViewer.setInput(mTreeViewerNodes);
+ mFrameTreeViewer.refresh();
+ mFrameTreeViewer.expandAll();
+ }
+
+ private void createOptionsBar(Composite parent) {
+ int numColumns = mShowContextSwitcher ? 4 : 3;
+
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(numColumns, false));
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ c.setLayoutData(gd);
+
+ Label l = new Label(c, SWT.NONE);
+ l.setText("Filter:");
+
+ mFilterText = new Text(c, SWT.BORDER | SWT.ICON_SEARCH | SWT.SEARCH | SWT.ICON_CANCEL);
+ mFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mFilterText.setMessage(DEFAULT_FILTER_MESSAGE);
+ mFilterText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ updateAppliedFilters();
+ }
+ });
+
+ if (mShowContextSwitcher) {
+ mContextSwitchCombo = new Combo(c, SWT.BORDER | SWT.READ_ONLY);
+
+ // Setup the combo such that "All Contexts" is the first item,
+ // and then we have an item for each context.
+ mContextSwitchCombo.add("All Contexts");
+ mContextSwitchCombo.select(0);
+ mCurrentlyDisplayedContext = -1; // showing all contexts
+ for (int i = 0; i < mTrace.getContexts().size(); i++) {
+ mContextSwitchCombo.add("Context " + i);
+ }
+
+ mContextSwitchCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ selectContext(mContextSwitchCombo.getSelectionIndex() - 1);
+ }
+ });
+ } else {
+ mCurrentlyDisplayedContext = 0;
+ }
+
+ ToolBar toolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER);
+
+ mExpandAllToolItem = new ToolItem(toolBar, SWT.PUSH);
+ mExpandAllToolItem.setToolTipText("Expand All");
+ if (sExpandAllIcon == null) {
+ ImageDescriptor id = GlTracePlugin.getImageDescriptor("/icons/expandall.png");
+ sExpandAllIcon = id.createImage();
+ }
+ if (sExpandAllIcon != null) {
+ mExpandAllToolItem.setImage(sExpandAllIcon);
+ }
+
+ mCollapseAllToolItem = new ToolItem(toolBar, SWT.PUSH);
+ mCollapseAllToolItem.setToolTipText("Collapse All");
+ mCollapseAllToolItem.setImage(
+ PlatformUI.getWorkbench().getSharedImages().getImage(
+ ISharedImages.IMG_ELCL_COLLAPSEALL));
+
+ mSaveAsToolItem = new ToolItem(toolBar, SWT.PUSH);
+ mSaveAsToolItem.setToolTipText("Export Trace");
+ mSaveAsToolItem.setImage(
+ PlatformUI.getWorkbench().getSharedImages().getImage(
+ ISharedImages.IMG_ETOOL_SAVEAS_EDIT));
+
+ SelectionListener toolbarSelectionListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (e.getSource() == mCollapseAllToolItem) {
+ setTreeItemsExpanded(false);
+ } else if (e.getSource() == mExpandAllToolItem) {
+ setTreeItemsExpanded(true);
+ } else if (e.getSource() == mSaveAsToolItem) {
+ exportTrace();
+ }
+ }
+ };
+ mExpandAllToolItem.addSelectionListener(toolbarSelectionListener);
+ mCollapseAllToolItem.addSelectionListener(toolbarSelectionListener);
+ mSaveAsToolItem.addSelectionListener(toolbarSelectionListener);
+ }
+
+ private void updateAppliedFilters() {
+ mGLCallFilter.setFilters(mFilterText.getText().trim());
+ mFrameTreeViewer.refresh();
+ }
+
+ private void createFrameTraceView(Composite parent) {
+ Composite c = new Composite(parent, SWT.NONE);
+ c.setLayout(new GridLayout(2, false));
+ GridData gd = new GridData(GridData.FILL_BOTH);
+ c.setLayoutData(gd);
+
+ final Tree tree = new Tree(c, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI);
+ gd = new GridData(GridData.FILL_BOTH);
+ tree.setLayoutData(gd);
+ tree.setLinesVisible(true);
+ tree.setHeaderVisible(true);
+
+ mFrameTreeViewer = new TreeViewer(tree);
+ CellLabelProvider labelProvider = new GLFrameLabelProvider();
+
+ // column showing the GL context id
+ TreeViewerColumn tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE);
+ tvc.setLabelProvider(labelProvider);
+ TreeColumn column = tvc.getColumn();
+ column.setText("Function");
+ column.setWidth(500);
+
+ // column showing the GL function duration (wall clock time)
+ tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE);
+ tvc.setLabelProvider(labelProvider);
+ column = tvc.getColumn();
+ column.setText("Wall Time (ns)");
+ column.setWidth(150);
+ column.setAlignment(SWT.RIGHT);
+
+ // column showing the GL function duration (thread time)
+ tvc = new TreeViewerColumn(mFrameTreeViewer, SWT.NONE);
+ tvc.setLabelProvider(labelProvider);
+ column = tvc.getColumn();
+ column.setText("Thread Time (ns)");
+ column.setWidth(150);
+ column.setAlignment(SWT.RIGHT);
+
+ mFrameTreeViewer.setContentProvider(new GLFrameContentProvider());
+
+ mGLCallFilter = new GLCallFilter();
+ mFrameTreeViewer.addFilter(mGLCallFilter);
+
+ // when the control is resized, give all the additional space
+ // to the function name column.
+ tree.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ int w = mFrameTreeViewer.getTree().getClientArea().width;
+ if (w > 200) {
+ mFrameTreeViewer.getTree().getColumn(2).setWidth(100);
+ mFrameTreeViewer.getTree().getColumn(1).setWidth(100);
+ mFrameTreeViewer.getTree().getColumn(0).setWidth(w - 200);
+ }
+ }
+ });
+
+ mDurationMinimap = new DurationMinimap(c, mTrace);
+ gd = new GridData(GridData.FILL_VERTICAL);
+ gd.widthHint = gd.minimumWidth = mDurationMinimap.getMinimumWidth();
+ mDurationMinimap.setLayoutData(gd);
+ mDurationMinimap.addCallSelectionListener(new ICallSelectionListener() {
+ @Override
+ public void callSelected(int selectedCallIndex) {
+ if (selectedCallIndex > 0 && selectedCallIndex < mTreeViewerNodes.size()) {
+ TreeItem item = tree.getItem(selectedCallIndex);
+ tree.select(item);
+ tree.setTopItem(item);
+ }
+ }
+ });
+
+ mVerticalScrollBar = tree.getVerticalBar();
+ mVerticalScrollBar.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateVisibleRange();
+ }
+ });
+ }
+
+ private void updateVisibleRange() {
+ int visibleCallTopIndex = mCallStartIndex;
+ int visibleCallBottomIndex = mCallEndIndex;
+
+ if (mVerticalScrollBar.isEnabled()) {
+ int selection = mVerticalScrollBar.getSelection();
+ int thumb = mVerticalScrollBar.getThumb();
+ int max = mVerticalScrollBar.getMaximum();
+
+ // from the scrollbar values, compute the visible fraction
+ double top = (double) selection / max;
+ double bottom = (double) (selection + thumb) / max;
+
+ // map the fraction to the call indices
+ int range = mCallEndIndex - mCallStartIndex;
+ visibleCallTopIndex = mCallStartIndex + (int) Math.floor(range * top);
+ visibleCallBottomIndex = mCallStartIndex + (int) Math.ceil(range * bottom);
+ }
+
+ mDurationMinimap.setVisibleCallRange(visibleCallTopIndex, visibleCallBottomIndex);
+ }
+
+ @Override
+ public void setFocus() {
+ mFrameTreeViewer.getTree().setFocus();
+ }
+
+ private static class GLFrameContentProvider implements ITreeContentProvider {
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return getChildren(inputElement);
+ }
+
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof List<?>) {
+ return ((List<?>) parentElement).toArray();
+ }
+
+ if (!(parentElement instanceof GLCallNode)) {
+ return null;
+ }
+
+ GLCallNode parent = (GLCallNode) parentElement;
+ if (parent.hasChildren()) {
+ return parent.getChildren().toArray();
+ } else {
+ return new Object[0];
+ }
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ if (!(element instanceof GLCallNode)) {
+ return null;
+ }
+
+ return ((GLCallNode) element).getParent();
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (!(element instanceof GLCallNode)) {
+ return false;
+ }
+
+ return ((GLCallNode) element).hasChildren();
+ }
+ }
+
+ private class GLFrameLabelProvider extends ColumnLabelProvider {
+ @Override
+ public void update(ViewerCell cell) {
+ Object element = cell.getElement();
+ if (!(element instanceof GLCallNode)) {
+ return;
+ }
+
+ GLCall c = ((GLCallNode) element).getCall();
+
+ if (c.getFunction() == Function.glDrawArrays
+ || c.getFunction() == Function.glDrawElements) {
+ cell.setForeground(mGldrawTextColor);
+ }
+
+ if (c.hasErrors()) {
+ cell.setForeground(mGlCallErrorColor);
+ }
+
+ cell.setText(getColumnText(c, cell.getColumnIndex()));
+ }
+
+ private String getColumnText(GLCall c, int columnIndex) {
+ switch (columnIndex) {
+ case 0:
+ if (c.getFunction() == Function.glPushGroupMarkerEXT) {
+ Object marker = c.getProperty(GLCall.PROPERTY_MARKERNAME);
+ if (marker instanceof String) {
+ return ((String) marker);
+ }
+ }
+ return c.toString();
+ case 1:
+ return formatDuration(c.getWallDuration());
+ case 2:
+ return formatDuration(c.getThreadDuration());
+ default:
+ return Integer.toString(c.getContextId());
+ }
+ }
+
+ private String formatDuration(int time) {
+ // Max duration is in the 10s of milliseconds, so xx,xxx,xxx ns
+ // So we require a format specifier that is 10 characters wide
+ return String.format("%,10d", time); //$NON-NLS-1$
+ }
+ }
+
+ private static class GLCallFilter extends ViewerFilter {
+ private final List<Pattern> mPatterns = new ArrayList<Pattern>();
+
+ public void setFilters(String filter) {
+ mPatterns.clear();
+
+ // split the user input into multiple regexes
+ // we assume that the regexes are OR'ed together i.e., all text that matches
+ // any one of the regexes will be displayed
+ for (String regex : filter.split(" ")) {
+ mPatterns.add(Pattern.compile(regex, Pattern.CASE_INSENSITIVE));
+ }
+ }
+
+ @Override
+ public boolean select(Viewer viewer, Object parentElement, Object element) {
+ if (!(element instanceof GLCallNode)) {
+ return true;
+ }
+
+ String text = getTextUnderNode((GLCallNode) element);
+
+ if (mPatterns.size() == 0) {
+ // match if there are no regex filters
+ return true;
+ }
+
+ for (Pattern p : mPatterns) {
+ Matcher matcher = p.matcher(text);
+ if (matcher.find()) {
+ // match if atleast one of the regexes matches this text
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** Obtain a string representation of all functions under a given tree node. */
+ private String getTextUnderNode(GLCallNode element) {
+ String func = element.getCall().getFunction().toString();
+ if (!element.hasChildren()) {
+ return func;
+ }
+
+ StringBuilder sb = new StringBuilder(100);
+ sb.append(func);
+
+ for (GLCallNode child : element.getChildren()) {
+ sb.append(getTextUnderNode(child));
+ }
+
+ return sb.toString();
+ }
+ }
+
+ @Override
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ if (mFrameTreeViewer != null) {
+ mFrameTreeViewer.addSelectionChangedListener(listener);
+ }
+ }
+
+ @Override
+ public ISelection getSelection() {
+ if (mFrameTreeViewer != null) {
+ return mFrameTreeViewer.getSelection();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ if (mFrameTreeViewer != null) {
+ mFrameTreeViewer.removeSelectionChangedListener(listener);
+ }
+ }
+
+ @Override
+ public void setSelection(ISelection selection) {
+ if (mFrameTreeViewer != null) {
+ mFrameTreeViewer.setSelection(selection);
+ }
+ }
+
+ public GLTrace getTrace() {
+ return mTrace;
+ }
+
+ public StateViewPage getStateViewPage() {
+ if (mStateViewPage == null) {
+ mStateViewPage = new StateViewPage(mTrace);
+ }
+
+ return mStateViewPage;
+ }
+
+ public FrameSummaryViewPage getFrameSummaryViewPage() {
+ if (mFrameSummaryViewPage == null) {
+ mFrameSummaryViewPage = new FrameSummaryViewPage(mTrace);
+ }
+
+ return mFrameSummaryViewPage;
+ }
+
+ public DetailsPage getDetailsPage() {
+ if (mDetailsPage == null) {
+ mDetailsPage = new DetailsPage(mTrace);
+ }
+
+ return mDetailsPage;
+ }
+
+ private void copySelectionToClipboard() {
+ if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) {
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ for (TreeItem it: mFrameTreeViewer.getTree().getSelection()) {
+ Object data = it.getData();
+ if (data instanceof GLCallNode) {
+ sb.append(((GLCallNode) data).getCall());
+ sb.append(NEWLINE);
+ }
+ }
+
+ if (sb.length() > 0) {
+ Clipboard cb = new Clipboard(Display.getDefault());
+ cb.setContents(
+ new Object[] { sb.toString() },
+ new Transfer[] { TextTransfer.getInstance() });
+ cb.dispose();
+ }
+ }
+
+ private void selectAll() {
+ if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) {
+ return;
+ }
+
+ mFrameTreeViewer.getTree().selectAll();
+ }
+
+ private void exportTrace() {
+ if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) {
+ return;
+ }
+
+ if (mCallEndIndex == 0) {
+ return;
+ }
+
+ FileDialog fd = new FileDialog(mFrameTreeViewer.getTree().getShell(), SWT.SAVE);
+ fd.setFilterExtensions(new String[] { "*.txt" });
+ if (sLastExportedToFolder != null) {
+ fd.setFilterPath(sLastExportedToFolder);
+ }
+
+ String path = fd.open();
+ if (path == null) {
+ return;
+ }
+
+ File f = new File(path);
+ sLastExportedToFolder = f.getParent();
+ try {
+ exportFrameTo(f);
+ } catch (IOException e) {
+ ErrorDialog.openError(mFrameTreeViewer.getTree().getShell(),
+ "Export trace file.",
+ "Unexpected error exporting trace file.",
+ new Status(Status.ERROR, GlTracePlugin.PLUGIN_ID, e.toString()));
+ }
+ }
+
+ private void exportFrameTo(File f) throws IOException {
+ String glCalls = serializeGlCalls(mTrace.getGLCalls(), mCallStartIndex, mCallEndIndex);
+ Files.write(glCalls, f, Charsets.UTF_8);
+ }
+
+ private String serializeGlCalls(List<GLCall> glCalls, int start, int end) {
+ StringBuilder sb = new StringBuilder();
+ while (start < end) {
+ sb.append(glCalls.get(start).toString());
+ sb.append("\n"); //$NON-NLS-1$
+ start++;
+ }
+
+ return sb.toString();
+ }
+
+ private void setTreeItemsExpanded(boolean expand) {
+ if (mFrameTreeViewer == null || mFrameTreeViewer.getTree().isDisposed()) {
+ return;
+ }
+
+ if (expand) {
+ mFrameTreeViewer.expandAll();
+ } else {
+ mFrameTreeViewer.collapseAll();
+ }
+ }
+
+ private class TraceViewerFindTarget extends AbstractBufferFindTarget {
+ @Override
+ public int getItemCount() {
+ return mFrameTreeViewer.getTree().getItemCount();
+ }
+
+ @Override
+ public String getItem(int index) {
+ Object data = mFrameTreeViewer.getTree().getItem(index).getData();
+ if (data instanceof GLCallNode) {
+ return ((GLCallNode) data).getCall().toString();
+ }
+ return null;
+ }
+
+ @Override
+ public void selectAndReveal(int index) {
+ Tree t = mFrameTreeViewer.getTree();
+ t.deselectAll();
+ t.select(t.getItem(index));
+ t.showSelection();
+ }
+
+ @Override
+ public int getStartingIndex() {
+ return 0;
+ }
+ };
+
+ private FindDialog mFindDialog;
+ private TraceViewerFindTarget mFindTarget = new TraceViewerFindTarget();
+
+ private void showFindDialog() {
+ if (mFindDialog != null) {
+ // the dialog is already displayed
+ return;
+ }
+
+ mFindDialog = new FindDialog(Display.getDefault().getActiveShell(),
+ mFindTarget,
+ FindDialog.FIND_NEXT_ID);
+ mFindDialog.open(); // blocks until find dialog is closed
+ mFindDialog = null;
+ }
+
+ public String getInputPath() {
+ return mFilePath;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateContentProvider.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateContentProvider.java
new file mode 100644
index 000000000..7bff168fc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateContentProvider.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.gltrace.editors;
+
+import com.android.ide.eclipse.gltrace.state.GLCompositeProperty;
+import com.android.ide.eclipse.gltrace.state.GLListProperty;
+import com.android.ide.eclipse.gltrace.state.GLSparseArrayProperty;
+import com.android.ide.eclipse.gltrace.state.IGLProperty;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+public class StateContentProvider implements ITreeContentProvider {
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ }
+
+ @Override
+ public Object[] getElements(Object inputElement) {
+ return getChildren(inputElement);
+ }
+
+ @Override
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof GLListProperty) {
+ return ((GLListProperty) parentElement).getList().toArray();
+ }
+
+ if (parentElement instanceof GLCompositeProperty) {
+ return ((GLCompositeProperty) parentElement).getProperties().toArray();
+ }
+
+ if (parentElement instanceof GLSparseArrayProperty) {
+ return ((GLSparseArrayProperty) parentElement).getValues().toArray();
+ }
+
+ return null;
+ }
+
+ @Override
+ public Object getParent(Object element) {
+ if (element instanceof IGLProperty) {
+ return ((IGLProperty) element).getParent();
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean hasChildren(Object element) {
+ if (element instanceof IGLProperty) {
+ return ((IGLProperty) element).isComposite();
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateLabelProvider.java
new file mode 100644
index 000000000..e37ea77fd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateLabelProvider.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.gltrace.editors;
+
+import com.android.ide.eclipse.gltrace.state.GLListProperty;
+import com.android.ide.eclipse.gltrace.state.GLSparseArrayProperty;
+import com.android.ide.eclipse.gltrace.state.GLStateType;
+import com.android.ide.eclipse.gltrace.state.IGLProperty;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.Set;
+
+public class StateLabelProvider extends ColumnLabelProvider {
+ private Set<IGLProperty> mChangedProperties;
+
+ private Color mHighlightForegroundColor;
+ private Color mNormalForegroundColor;
+
+ public StateLabelProvider() {
+ mHighlightForegroundColor = Display.getDefault().getSystemColor(SWT.COLOR_BLUE);
+ mNormalForegroundColor = Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
+ }
+
+ public String getColumnText(IGLProperty property, int columnIndex) {
+ switch (columnIndex) {
+ case 0:
+ return getName(property);
+ case 1:
+ return getValue(property);
+ default:
+ return "";
+ }
+ }
+
+ private String getValue(IGLProperty element) {
+ return element.getStringValue();
+ }
+
+ private String getName(IGLProperty element) {
+ IGLProperty parent = element.getParent();
+ if (parent instanceof GLListProperty) {
+ // For members of list, use the index in the list as the name as opposed to
+ // the property type
+ int index = ((GLListProperty) parent).indexOf(element);
+ if (element.getType() == GLStateType.GL_STATE_ES1) {
+ return String.format("Context %d (ES1)", index);
+ } else if (element.getType() == GLStateType.GL_STATE_ES2) {
+ return String.format("Context %d (ES2)", index);
+ } else {
+ return Integer.toString(index);
+ }
+ } else if (parent instanceof GLSparseArrayProperty) {
+ // For members of sparse array, use the key as the name as opposed to
+ // the property type
+ int index = ((GLSparseArrayProperty) parent).keyFor(element);
+ return Integer.toString(index);
+ }
+
+ return element.getType().getDescription();
+ }
+
+ @Override
+ public void update(ViewerCell cell) {
+ Object element = cell.getElement();
+ if (!(element instanceof IGLProperty)) {
+ return;
+ }
+
+ IGLProperty prop = (IGLProperty) element;
+
+ String text = getColumnText(prop, cell.getColumnIndex());
+ cell.setText(text);
+
+ if (mChangedProperties != null && mChangedProperties.contains(prop)) {
+ cell.setForeground(mHighlightForegroundColor);
+ } else {
+ cell.setForeground(mNormalForegroundColor);
+ }
+ }
+
+ public void setChangedProperties(Set<IGLProperty> changedProperties) {
+ mChangedProperties = changedProperties;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateViewPage.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateViewPage.java
new file mode 100644
index 000000000..faa9561cb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/editors/StateViewPage.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.gltrace.editors;
+
+import com.android.ide.eclipse.gltrace.GlTracePlugin;
+import com.android.ide.eclipse.gltrace.editors.GLCallGroups.GLCallNode;
+import com.android.ide.eclipse.gltrace.model.GLCall;
+import com.android.ide.eclipse.gltrace.model.GLTrace;
+import com.android.ide.eclipse.gltrace.state.GLState;
+import com.android.ide.eclipse.gltrace.state.IGLProperty;
+import com.android.ide.eclipse.gltrace.state.StatePrettyPrinter;
+import com.android.ide.eclipse.gltrace.state.transforms.IStateTransform;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.ILock;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ISelectionProvider;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.ui.ISelectionListener;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.part.IPageSite;
+import org.eclipse.ui.part.Page;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A tree view of the OpenGL state. It listens to the current GLCall that is selected
+ * in the Function Trace view, and updates its view to reflect the state as of the selected call.
+ */
+public class StateViewPage extends Page implements ISelectionListener, ISelectionProvider {
+ public static final String ID = "com.android.ide.eclipse.gltrace.views.GLState"; //$NON-NLS-1$
+ private static String sLastUsedPath;
+ private static final ILock sGlStateLock = Job.getJobManager().newLock();
+
+ private GLTrace mTrace;
+ private List<GLCall> mGLCalls;
+
+ /** OpenGL State as of call {@link #mCurrentStateIndex}. */
+ private IGLProperty mState;
+ private int mCurrentStateIndex;
+
+ private String[] TREE_PROPERTIES = { "Name", "Value" };
+ private TreeViewer mTreeViewer;
+ private StateLabelProvider mLabelProvider;
+
+ public StateViewPage(GLTrace trace) {
+ setInput(trace);
+ }
+
+ public void setInput(GLTrace trace) {
+ mTrace = trace;
+ if (trace != null) {
+ mGLCalls = trace.getGLCalls();
+ } else {
+ mGLCalls = null;
+ }
+
+ mState = GLState.createDefaultState();
+ mCurrentStateIndex = -1;
+
+ if (mTreeViewer != null) {
+ mTreeViewer.setInput(mState);
+ mTreeViewer.refresh();
+ }
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ final Tree tree = new Tree(parent, SWT.VIRTUAL | SWT.H_SCROLL | SWT.V_SCROLL);
+ GridDataFactory.fillDefaults().grab(true, true).applyTo(tree);
+
+ tree.setHeaderVisible(true);
+ tree.setLinesVisible(true);
+ tree.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ TreeColumn col1 = new TreeColumn(tree, SWT.LEFT);
+ col1.setText(TREE_PROPERTIES[0]);
+ col1.setWidth(200);
+
+ TreeColumn col2 = new TreeColumn(tree, SWT.LEFT);
+ col2.setText(TREE_PROPERTIES[1]);
+ col2.setWidth(200);
+
+ mTreeViewer = new TreeViewer(tree);
+ mTreeViewer.setContentProvider(new StateContentProvider());
+ mLabelProvider = new StateLabelProvider();
+ mTreeViewer.setLabelProvider(mLabelProvider);
+ mTreeViewer.setInput(mState);
+ mTreeViewer.refresh();
+
+ final IToolBarManager manager = getSite().getActionBars().getToolBarManager();
+ manager.add(new Action("Save to File",
+ PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(
+ ISharedImages.IMG_ETOOL_SAVEAS_EDIT)) {
+ @Override
+ public void run() {
+ saveCurrentState();
+ }
+ });
+ }
+
+ private void saveCurrentState() {
+ final Shell shell = mTreeViewer.getTree().getShell();
+ FileDialog fd = new FileDialog(shell, SWT.SAVE);
+ fd.setFilterExtensions(new String[] { "*.txt" });
+ if (sLastUsedPath != null) {
+ fd.setFilterPath(sLastUsedPath);
+ }
+
+ String path = fd.open();
+ if (path == null) {
+ return;
+ }
+
+ File f = new File(path);
+ sLastUsedPath = f.getParent();
+
+ // export state to f
+ StatePrettyPrinter pp = new StatePrettyPrinter();
+ synchronized (sGlStateLock) {
+ mState.prettyPrint(pp);
+ }
+
+ try {
+ Files.write(pp.toString(), f, Charsets.UTF_8);
+ } catch (IOException e) {
+ ErrorDialog.openError(shell,
+ "Export GL State",
+ "Unexpected error while writing GL state to file.",
+ new Status(Status.ERROR, GlTracePlugin.PLUGIN_ID, e.toString()));
+ }
+ }
+
+ @Override
+ public void init(IPageSite pageSite) {
+ super.init(pageSite);
+ pageSite.getPage().addSelectionListener(this);
+ }
+
+ @Override
+ public void dispose() {
+ getSite().getPage().removeSelectionListener(this);
+ super.dispose();
+ }
+
+ @Override
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ if (!(part instanceof GLFunctionTraceViewer)) {
+ return;
+ }
+
+ if (((GLFunctionTraceViewer) part).getTrace() != mTrace) {
+ return;
+ }
+
+ if (!(selection instanceof TreeSelection)) {
+ return;
+ }
+
+ GLCall selectedCall = null;
+
+ Object data = ((TreeSelection) selection).getFirstElement();
+ if (data instanceof GLCallNode) {
+ selectedCall = ((GLCallNode) data).getCall();
+ }
+
+ if (selectedCall == null) {
+ return;
+ }
+
+ final int selectedCallIndex = selectedCall.getIndex();
+
+ // Creation of texture images takes a few seconds on the first run. So run
+ // the update task as an Eclipse job.
+ Job job = new Job("Updating GL State") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ Set<IGLProperty> changedProperties = null;
+
+ try {
+ sGlStateLock.acquire();
+ changedProperties = updateState(mCurrentStateIndex,
+ selectedCallIndex);
+ mCurrentStateIndex = selectedCallIndex;
+ } catch (Exception e) {
+ GlTracePlugin.getDefault().logMessage(
+ "Unexpected error while updating GL State.");
+ GlTracePlugin.getDefault().logMessage(e.getMessage());
+ return new Status(Status.ERROR,
+ GlTracePlugin.PLUGIN_ID,
+ "Unexpected error while updating GL State.",
+ e);
+ } finally {
+ sGlStateLock.release();
+ }
+
+ mLabelProvider.setChangedProperties(changedProperties);
+ Display.getDefault().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ if (!mTreeViewer.getTree().isDisposed()) {
+ mTreeViewer.refresh();
+ }
+ }
+ });
+
+ return Status.OK_STATUS;
+ }
+ };
+ job.setPriority(Job.SHORT);
+ job.schedule();
+ }
+
+ @Override
+ public Control getControl() {
+ if (mTreeViewer == null) {
+ return null;
+ }
+
+ return mTreeViewer.getControl();
+ }
+
+ @Override
+ public void setFocus() {
+ }
+
+ /**
+ * Update GL state from GL call at fromIndex to the call at toIndex.
+ * If fromIndex < toIndex, the GL state will be updated by applying all the transformations
+ * corresponding to calls from (fromIndex + 1) to toIndex (inclusive).
+ * If fromIndex > toIndex, the GL state will be updated by reverting all the calls from
+ * fromIndex (inclusive) to (toIndex + 1).
+ * @return GL state properties that changed as a result of this update.
+ */
+ private Set<IGLProperty> updateState(int fromIndex, int toIndex) {
+ assert fromIndex >= -1 && fromIndex < mGLCalls.size();
+ assert toIndex >= 0 && toIndex < mGLCalls.size();
+
+ if (fromIndex < toIndex) {
+ return applyTransformations(fromIndex, toIndex);
+ } else if (fromIndex > toIndex) {
+ return revertTransformations(fromIndex, toIndex);
+ } else {
+ return Collections.emptySet();
+ }
+ }
+
+ private Set<IGLProperty> applyTransformations(int fromIndex, int toIndex) {
+ int setSizeHint = 3 * (toIndex - fromIndex) + 10;
+ Set<IGLProperty> changedProperties = new HashSet<IGLProperty>(setSizeHint);
+
+ for (int i = fromIndex + 1; i <= toIndex; i++) {
+ GLCall call = mGLCalls.get(i);
+ for (IStateTransform f : call.getStateTransformations()) {
+ try {
+ f.apply(mState);
+ IGLProperty changedProperty = f.getChangedProperty(mState);
+ if (changedProperty != null) {
+ changedProperties.addAll(getHierarchy(changedProperty));
+ }
+ } catch (Exception e) {
+ GlTracePlugin.getDefault().logMessage("Error applying transformations for "
+ + call);
+ GlTracePlugin.getDefault().logMessage(e.toString());
+ }
+ }
+ }
+
+ return changedProperties;
+ }
+
+ private Set<IGLProperty> revertTransformations(int fromIndex, int toIndex) {
+ int setSizeHint = 3 * (fromIndex - toIndex) + 10;
+ Set<IGLProperty> changedProperties = new HashSet<IGLProperty>(setSizeHint);
+
+ for (int i = fromIndex; i > toIndex; i--) {
+ List<IStateTransform> transforms = mGLCalls.get(i).getStateTransformations();
+ // When reverting transformations, iterate from the last to first so that the reversals
+ // are performed in the correct sequence.
+ for (int j = transforms.size() - 1; j >= 0; j--) {
+ IStateTransform f = transforms.get(j);
+ f.revert(mState);
+
+ IGLProperty changedProperty = f.getChangedProperty(mState);
+ if (changedProperty != null) {
+ changedProperties.addAll(getHierarchy(changedProperty));
+ }
+ }
+ }
+
+ return changedProperties;
+ }
+
+ /**
+ * Obtain the list of properties starting from the provided property up to
+ * the root of GL state.
+ */
+ private List<IGLProperty> getHierarchy(IGLProperty changedProperty) {
+ List<IGLProperty> changedProperties = new ArrayList<IGLProperty>(5);
+ changedProperties.add(changedProperty);
+
+ // add the entire parent chain until we reach the root
+ IGLProperty prop = changedProperty;
+ while ((prop = prop.getParent()) != null) {
+ changedProperties.add(prop);
+ }
+
+ return changedProperties;
+ }
+
+ @Override
+ public void addSelectionChangedListener(ISelectionChangedListener listener) {
+ mTreeViewer.addSelectionChangedListener(listener);
+ }
+
+ @Override
+ public ISelection getSelection() {
+ return mTreeViewer.getSelection();
+ }
+
+ @Override
+ public void removeSelectionChangedListener(ISelectionChangedListener listener) {
+ mTreeViewer.removeSelectionChangedListener(listener);
+ }
+
+ @Override
+ public void setSelection(ISelection selection) {
+ mTreeViewer.setSelection(selection);
+ }
+}