blob: 9a80b0154e14e35bb4ef79c1e2cbe7d58f7b0eef [file] [log] [blame]
robertphillips@google.com105a4a52014-02-11 15:10:40 +00001/*
2 * Copyright 2014 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7#ifndef SkMatrixClipStateMgr_DEFINED
8#define SkMatrixClipStateMgr_DEFINED
9
10#include "SkCanvas.h"
11#include "SkMatrix.h"
12#include "SkRegion.h"
13#include "SkRRect.h"
14#include "SkTypes.h"
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000015#include "SkTDArray.h"
robertphillips@google.com105a4a52014-02-11 15:10:40 +000016
17class SkPictureRecord;
18class SkWriter32;
19
20// The SkMatrixClipStateMgr collapses the matrix/clip state of an SkPicture into
21// a series of save/restore blocks of consistent matrix clip state, e.g.:
22//
23// save
24// clip(s)
25// concat
26// ... draw ops ...
27// restore
28//
29// SaveLayers simply add another level, e.g.:
30//
31// save
32// clip(s)
33// concat
34// ... draw ops ...
35// saveLayer
36// save
37// clip(s)
38// concat
39// ... draw ops ...
40// restore
41// restore
42// restore
43//
44// As a side effect of this process all saves and saveLayers will become
45// kMatrixClip_SaveFlag style saves/saveLayers.
46
47// The SkMatrixClipStateMgr works by intercepting all the save*, restore, clip*,
48// and matrix calls sent to SkCanvas in order to track the current matrix/clip
skia.committer@gmail.com877c4492014-02-12 03:02:04 +000049// state. All the other canvas calls get funnelled into a generic "call" entry
robertphillips@google.com105a4a52014-02-11 15:10:40 +000050// point that signals that a state block is required.
51class SkMatrixClipStateMgr {
52public:
53 static const int32_t kIdentityWideOpenStateID = 0;
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000054 static const int kIdentityMatID = 0;
robertphillips@google.com105a4a52014-02-11 15:10:40 +000055
commit-bot@chromium.orge3beb6b2014-04-07 19:34:38 +000056 class MatrixClipState : SkNoncopyable {
robertphillips@google.com105a4a52014-02-11 15:10:40 +000057 public:
58 class MatrixInfo {
59 public:
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000060 void reset() {
61 fMatrixID = kIdentityMatID;
62 fMatrix.reset();
63 }
64
commit-bot@chromium.org92362382014-03-18 12:51:48 +000065 void preTranslate(SkScalar dx, SkScalar dy) {
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000066 fMatrixID = -1;
commit-bot@chromium.org92362382014-03-18 12:51:48 +000067 fMatrix.preTranslate(dx, dy);
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000068 }
69
commit-bot@chromium.org92362382014-03-18 12:51:48 +000070 void preScale(SkScalar sx, SkScalar sy) {
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000071 fMatrixID = -1;
commit-bot@chromium.org92362382014-03-18 12:51:48 +000072 fMatrix.preScale(sx, sy);
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000073 }
74
commit-bot@chromium.org92362382014-03-18 12:51:48 +000075 void preRotate(SkScalar degrees) {
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000076 fMatrixID = -1;
commit-bot@chromium.org92362382014-03-18 12:51:48 +000077 fMatrix.preRotate(degrees);
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000078 }
79
commit-bot@chromium.org92362382014-03-18 12:51:48 +000080 void preSkew(SkScalar sx, SkScalar sy) {
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000081 fMatrixID = -1;
commit-bot@chromium.org92362382014-03-18 12:51:48 +000082 fMatrix.preSkew(sx, sy);
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000083 }
84
commit-bot@chromium.org92362382014-03-18 12:51:48 +000085 void preConcat(const SkMatrix& matrix) {
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000086 fMatrixID = -1;
commit-bot@chromium.org92362382014-03-18 12:51:48 +000087 fMatrix.preConcat(matrix);
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +000088 }
89
90 void setMatrix(const SkMatrix& matrix) {
91 fMatrixID = -1;
92 fMatrix = matrix;
93 }
94
95 int getID(SkMatrixClipStateMgr* mgr) {
96 if (fMatrixID >= 0) {
97 return fMatrixID;
98 }
99
100 fMatrixID = mgr->addMatToDict(fMatrix);
101 return fMatrixID;
102 }
103
104 private:
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000105 SkMatrix fMatrix;
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000106 int fMatrixID;
commit-bot@chromium.org6c5f1fd2014-02-24 17:24:15 +0000107
108 typedef SkNoncopyable INHERITED;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000109 };
110
commit-bot@chromium.orge3beb6b2014-04-07 19:34:38 +0000111 class ClipInfo : SkNoncopyable {
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000112 public:
113 ClipInfo() {}
114
skia.committer@gmail.com877c4492014-02-12 03:02:04 +0000115 bool clipRect(const SkRect& rect,
116 SkRegion::Op op,
117 bool doAA,
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000118 int matrixID) {
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000119 ClipOp* newClip = fClips.append();
120 newClip->fClipType = kRect_ClipType;
121 newClip->fGeom.fRRect.setRect(rect); // storing the clipRect in the RRect
122 newClip->fOp = op;
123 newClip->fDoAA = doAA;
124 newClip->fMatrixID = matrixID;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000125 return false;
126 }
127
skia.committer@gmail.com877c4492014-02-12 03:02:04 +0000128 bool clipRRect(const SkRRect& rrect,
129 SkRegion::Op op,
130 bool doAA,
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000131 int matrixID) {
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000132 ClipOp* newClip = fClips.append();
133 newClip->fClipType = kRRect_ClipType;
134 newClip->fGeom.fRRect = rrect;
135 newClip->fOp = op;
136 newClip->fDoAA = doAA;
137 newClip->fMatrixID = matrixID;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000138 return false;
139 }
140
skia.committer@gmail.com877c4492014-02-12 03:02:04 +0000141 bool clipPath(SkPictureRecord* picRecord,
142 const SkPath& path,
143 SkRegion::Op op,
144 bool doAA,
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000145 int matrixID);
skia.committer@gmail.com877c4492014-02-12 03:02:04 +0000146 bool clipRegion(SkPictureRecord* picRecord,
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000147 int regionID,
skia.committer@gmail.com877c4492014-02-12 03:02:04 +0000148 SkRegion::Op op,
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000149 int matrixID);
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000150 void writeClip(int* curMatID, SkMatrixClipStateMgr* mgr);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000151
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000152 SkDEBUGCODE(int numClips() const { return fClips.count(); })
153
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000154 private:
155 enum ClipType {
156 kRect_ClipType,
157 kRRect_ClipType,
158 kPath_ClipType,
159 kRegion_ClipType
160 };
161
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000162 class ClipOp {
163 public:
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000164 ClipType fClipType;
165
166 union {
commit-bot@chromium.org6c5f1fd2014-02-24 17:24:15 +0000167 SkRRect fRRect; // also stores clip rect
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000168 int fPathID;
169 int fRegionID;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000170 } fGeom;
171
172 bool fDoAA;
173 SkRegion::Op fOp;
174
175 // The CTM in effect when this clip call was issued
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000176 int fMatrixID;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000177 };
178
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000179 SkTDArray<ClipOp> fClips;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000180
181 typedef SkNoncopyable INHERITED;
182 };
183
Florin Malita5f6102d2014-06-30 10:13:28 -0400184 MatrixClipState(MatrixClipState* prev)
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000185 : fPrev(prev)
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000186 {
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000187 fHasOpen = false;
188
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000189 if (NULL == prev) {
190 fLayerID = 0;
191
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000192 fMatrixInfoStorage.reset();
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000193 fMatrixInfo = &fMatrixInfoStorage;
194 fClipInfo = &fClipInfoStorage; // ctor handles init of fClipInfoStorage
195
196 // The identity/wide-open-clip state is current by default
197 fMCStateID = kIdentityWideOpenStateID;
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000198#ifdef SK_DEBUG
199 fExpectedDepth = 1;
200#endif
skia.committer@gmail.com877c4492014-02-12 03:02:04 +0000201 }
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000202 else {
203 fLayerID = prev->fLayerID;
204
Florin Malita5f6102d2014-06-30 10:13:28 -0400205 fMatrixInfoStorage = *prev->fMatrixInfo;
206 fMatrixInfo = &fMatrixInfoStorage;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000207
Florin Malita5f6102d2014-06-30 10:13:28 -0400208 // We don't copy the ClipOps of the previous clip states
209 fClipInfo = &fClipInfoStorage;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000210
211 // Initially a new save/saveLayer represents the same MC state
212 // as its predecessor.
213 fMCStateID = prev->fMCStateID;
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000214#ifdef SK_DEBUG
215 fExpectedDepth = prev->fExpectedDepth;
216#endif
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000217 }
218
219 fIsSaveLayer = false;
220 }
221
222 MatrixInfo* fMatrixInfo;
223 MatrixInfo fMatrixInfoStorage;
224
225 ClipInfo* fClipInfo;
226 ClipInfo fClipInfoStorage;
227
228 // Tracks the current depth of saveLayers to support the isDrawingToLayer call
229 int fLayerID;
230 // Does this MC state represent a saveLayer call?
231 bool fIsSaveLayer;
232
commit-bot@chromium.org6c5f1fd2014-02-24 17:24:15 +0000233 // The next field is only valid when fIsSaveLayer is set.
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000234 SkTDArray<int>* fSavedSkipOffsets;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000235
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000236 // Does the MC state have an open block in the skp?
237 bool fHasOpen;
238
skia.committer@gmail.com7911ade2014-02-22 03:02:18 +0000239 MatrixClipState* fPrev;
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000240
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000241#ifdef SK_DEBUG
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000242 int fExpectedDepth; // debugging aid
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000243#endif
244
245 int32_t fMCStateID;
246 };
247
248 enum CallType {
249 kMatrix_CallType,
250 kClip_CallType,
251 kOther_CallType
252 };
253
254 SkMatrixClipStateMgr();
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000255 ~SkMatrixClipStateMgr();
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000256
257 void init(SkPictureRecord* picRecord) {
258 // Note: we're not taking a ref here. It is expected that the SkMatrixClipStateMgr
259 // is owned by the SkPictureRecord object
skia.committer@gmail.com877c4492014-02-12 03:02:04 +0000260 fPicRecord = picRecord;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000261 }
262
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000263 SkPictureRecord* getPicRecord() { return fPicRecord; }
264
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000265 // TODO: need to override canvas' getSaveCount. Right now we pass the
266 // save* and restore calls on to the base SkCanvas in SkPictureRecord but
267 // this duplicates effort.
268 int getSaveCount() const { return fMatrixClipStack.count(); }
269
Florin Malita5f6102d2014-06-30 10:13:28 -0400270 int save();
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000271
272 int saveLayer(const SkRect* bounds, const SkPaint* paint, SkCanvas::SaveFlags flags);
273
274 bool isDrawingToLayer() const {
275 return fCurMCState->fLayerID > 0;
276 }
277
278 void restore();
279
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000280 void translate(SkScalar dx, SkScalar dy) {
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000281 this->call(kMatrix_CallType);
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000282 fCurMCState->fMatrixInfo->preTranslate(dx, dy);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000283 }
284
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000285 void scale(SkScalar sx, SkScalar sy) {
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000286 this->call(kMatrix_CallType);
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000287 fCurMCState->fMatrixInfo->preScale(sx, sy);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000288 }
289
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000290 void rotate(SkScalar degrees) {
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000291 this->call(kMatrix_CallType);
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000292 fCurMCState->fMatrixInfo->preRotate(degrees);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000293 }
294
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000295 void skew(SkScalar sx, SkScalar sy) {
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000296 this->call(kMatrix_CallType);
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000297 fCurMCState->fMatrixInfo->preSkew(sx, sy);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000298 }
299
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000300 void concat(const SkMatrix& matrix) {
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000301 this->call(kMatrix_CallType);
commit-bot@chromium.org92362382014-03-18 12:51:48 +0000302 fCurMCState->fMatrixInfo->preConcat(matrix);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000303 }
304
305 void setMatrix(const SkMatrix& matrix) {
306 this->call(kMatrix_CallType);
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000307 fCurMCState->fMatrixInfo->setMatrix(matrix);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000308 }
309
310 bool clipRect(const SkRect& rect, SkRegion::Op op, bool doAA) {
311 this->call(SkMatrixClipStateMgr::kClip_CallType);
312 return fCurMCState->fClipInfo->clipRect(rect, op, doAA,
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000313 fCurMCState->fMatrixInfo->getID(this));
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000314 }
315
316 bool clipRRect(const SkRRect& rrect, SkRegion::Op op, bool doAA) {
317 this->call(SkMatrixClipStateMgr::kClip_CallType);
318 return fCurMCState->fClipInfo->clipRRect(rrect, op, doAA,
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000319 fCurMCState->fMatrixInfo->getID(this));
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000320 }
321
322 bool clipPath(const SkPath& path, SkRegion::Op op, bool doAA) {
323 this->call(SkMatrixClipStateMgr::kClip_CallType);
324 return fCurMCState->fClipInfo->clipPath(fPicRecord, path, op, doAA,
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000325 fCurMCState->fMatrixInfo->getID(this));
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000326 }
327
328 bool clipRegion(const SkRegion& region, SkRegion::Op op) {
329 this->call(SkMatrixClipStateMgr::kClip_CallType);
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000330 int regionID = this->addRegionToDict(region);
331 return fCurMCState->fClipInfo->clipRegion(fPicRecord, regionID, op,
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000332 fCurMCState->fMatrixInfo->getID(this));
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000333 }
334
335 bool call(CallType callType);
336
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000337 void fillInSkips(SkWriter32* writer, int32_t restoreOffset);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000338
339 void finish();
340
341protected:
342 SkPictureRecord* fPicRecord;
343
344 uint32_t fMatrixClipStackStorage[43]; // sized to fit 2 clip states
345 SkDeque fMatrixClipStack;
346 MatrixClipState* fCurMCState;
347
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000348 // This dictionary doesn't actually de-duplicate the matrices (except for the
349 // identity matrix). It merely stores the matrices and allows them to be looked
skia.committer@gmail.com22ef2c32014-02-13 03:01:57 +0000350 // up by ID later. The de-duplication mainly falls upon the matrix/clip stack
351 // which stores the ID so a revisited clip/matrix (via popping the stack) will
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000352 // use the same ID.
353 SkTDArray<SkMatrix> fMatrixDict;
354
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000355 SkTDArray<SkRegion*> fRegionDict;
356
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000357 // The MCStateID of the state currently in effect in the byte stream. 0 if none.
358 int32_t fCurOpenStateID;
skia.committer@gmail.comc05d2852014-02-20 03:01:56 +0000359 // The skip offsets for the current open state. These are the locations in the
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000360 // skp that must be filled in when the current open state is closed. These are
361 // here rather then distributed across the MatrixClipState's because saveLayers
362 // can cause MC states to be nested.
commit-bot@chromium.orgdcecb162014-04-22 17:54:29 +0000363 SkTDArray<int32_t> *fSkipOffsets; // TODO: should we store u32 or size_t instead?
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000364
365 SkDEBUGCODE(void validate();)
366
Florin Malita5f6102d2014-06-30 10:13:28 -0400367 int MCStackPush();
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000368
commit-bot@chromium.orgdcecb162014-04-22 17:54:29 +0000369 void addClipOffset(size_t offset) {
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000370 SkASSERT(NULL != fSkipOffsets);
371 SkASSERT(kIdentityWideOpenStateID != fCurOpenStateID);
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000372 SkASSERT(fCurMCState->fHasOpen);
373 SkASSERT(!fCurMCState->fIsSaveLayer);
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000374
commit-bot@chromium.orgdcecb162014-04-22 17:54:29 +0000375 *fSkipOffsets->append() = SkToS32(offset);
commit-bot@chromium.org92da60c2014-02-19 15:11:23 +0000376 }
377
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000378 void writeDeltaMat(int currentMatID, int desiredMatID);
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000379 static int32_t NewMCStateID();
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000380
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000381 int addRegionToDict(const SkRegion& region);
382 const SkRegion* lookupRegion(int index) {
383 SkASSERT(index >= 0 && index < fRegionDict.count());
skia.committer@gmail.com22ef2c32014-02-13 03:01:57 +0000384 return fRegionDict[index];
commit-bot@chromium.org9a67a7e2014-02-12 23:22:15 +0000385 }
386
commit-bot@chromium.orgc5dada82014-02-12 15:48:04 +0000387 // TODO: add stats to check if the dictionary really does
388 // reduce the size of the SkPicture.
389 int addMatToDict(const SkMatrix& mat);
390 const SkMatrix& lookupMat(int index) {
391 SkASSERT(index >= 0 && index < fMatrixDict.count());
392 return fMatrixDict[index];
393 }
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000394
commit-bot@chromium.org6c5f1fd2014-02-24 17:24:15 +0000395 bool isNestingMCState(int stateID);
robertphillips@google.comfc4ded92014-02-21 17:46:37 +0000396
397#ifdef SK_DEBUG
398 int fActualDepth;
399#endif
commit-bot@chromium.org6c5f1fd2014-02-24 17:24:15 +0000400
401 // save layers are nested within a specific MC state. This stack tracks
402 // the nesting MC state's ID as save layers are pushed and popped.
403 SkTDArray<int> fStateIDStack;
robertphillips@google.com105a4a52014-02-11 15:10:40 +0000404};
405
robertphillips@google.com3cd6fdf2014-02-11 15:40:54 +0000406#endif