Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(155)

Side by Side Diff: src/effects/SkBlurMaskFilter.cpp

Issue 286273002: Optimize CSS box-shadow performance by caching the SkMask of the blur effect. (Closed) Base URL: https://skia.googlecode.com/svn/trunk
Patch Set: Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « src/core/SkMaskFilter.cpp ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 1
2 /* 2 /*
3 * Copyright 2006 The Android Open Source Project 3 * Copyright 2006 The Android Open Source Project
4 * 4 *
5 * Use of this source code is governed by a BSD-style license that can be 5 * Use of this source code is governed by a BSD-style license that can be
6 * found in the LICENSE file. 6 * found in the LICENSE file.
7 */ 7 */
8 8
9 #include <list>
10
9 #include "SkBlurMaskFilter.h" 11 #include "SkBlurMaskFilter.h"
10 #include "SkBlurMask.h" 12 #include "SkBlurMask.h"
11 #include "SkGpuBlurUtils.h" 13 #include "SkGpuBlurUtils.h"
12 #include "SkReadBuffer.h" 14 #include "SkReadBuffer.h"
13 #include "SkWriteBuffer.h" 15 #include "SkWriteBuffer.h"
14 #include "SkMaskFilter.h" 16 #include "SkMaskFilter.h"
15 #include "SkRRect.h" 17 #include "SkRRect.h"
16 #include "SkRTConf.h" 18 #include "SkRTConf.h"
17 #include "SkStringUtils.h" 19 #include "SkStringUtils.h"
18 #include "SkStrokeRec.h" 20 #include "SkStrokeRec.h"
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
58 virtual bool filterMaskGPU(GrTexture* src, 60 virtual bool filterMaskGPU(GrTexture* src,
59 const SkMatrix& ctm, 61 const SkMatrix& ctm,
60 const SkRect& maskRect, 62 const SkRect& maskRect,
61 GrTexture** result, 63 GrTexture** result,
62 bool canOverwriteSrc) const SK_OVERRIDE; 64 bool canOverwriteSrc) const SK_OVERRIDE;
63 #endif 65 #endif
64 66
65 virtual void computeFastBounds(const SkRect&, SkRect*) const SK_OVERRIDE; 67 virtual void computeFastBounds(const SkRect&, SkRect*) const SK_OVERRIDE;
66 virtual bool asABlur(BlurRec*) const SK_OVERRIDE; 68 virtual bool asABlur(BlurRec*) const SK_OVERRIDE;
67 69
70 class BlurMaskRecord {
71 public:
72 BlurMaskRecord(): fSigma(0),
73 fRectCount(0),
74 fRects(NULL),
75 fMask(NULL) {}
76
77 BlurMaskRecord(SkScalar sigma, unsigned rectCount, SkRect** rects, SkMas k* mask)
78 : fSigma(sigma),
79 fRectCount(rectCount),
80 fRects(rects),
81 fMask(mask) {}
82
83 ~BlurMaskRecord() {
84 if (fMask) {
85 if (fMask->fImage)
86 SkMask::FreeImage(fMask->fImage);
87 delete fMask;
88 }
89 if (fRects) {
90 for(int i = 0; i < fRectCount; i++) {
91 if (fRects[i])
92 delete fRects[i];
93 }
94 delete []fRects;
95 }
96 }
97
98 SkScalar fSigma;
99 unsigned fRectCount;
100 SkRect** fRects;
101 SkMask* fMask;
102 };
103
104 static bool getBlurMaskRecord(SkScalar sigma,
105 unsigned rectCount,
106 const SkRect rects[],
107 SkMask** mask);
108 static bool addBlurMaskRecord(BlurMaskRecord* blurMaskRecord);
109 static void clearBlurMaskRecordList();
110 static unsigned getBlurMaskRecordsMemoryUsage() { return fCachedBlurMaskImag eMemorySize; }
111 static void setBlurMaskRecordsLimit(unsigned, unsigned);
112
68 SK_TO_STRING_OVERRIDE() 113 SK_TO_STRING_OVERRIDE()
69 SK_DECLARE_PUBLIC_FLATTENABLE_DESERIALIZATION_PROCS(SkBlurMaskFilterImpl) 114 SK_DECLARE_PUBLIC_FLATTENABLE_DESERIALIZATION_PROCS(SkBlurMaskFilterImpl)
70 115
71 protected: 116 protected:
72 virtual FilterReturn filterRectsToNine(const SkRect[], int count, const SkMa trix&, 117 virtual FilterReturn filterRectsToNine(const SkRect[], int count, const SkMa trix&,
73 const SkIRect& clipBounds, 118 const SkIRect& clipBounds,
74 NinePatch*) const SK_OVERRIDE; 119 NinePatch*) const SK_OVERRIDE;
75 120
76 virtual FilterReturn filterRRectToNine(const SkRRect&, const SkMatrix&, 121 virtual FilterReturn filterRRectToNine(const SkRRect&, const SkMatrix&,
77 const SkIRect& clipBounds, 122 const SkIRect& clipBounds,
(...skipping 22 matching lines...) Expand all
100 SkBlurMaskFilterImpl(SkReadBuffer&); 145 SkBlurMaskFilterImpl(SkReadBuffer&);
101 virtual void flatten(SkWriteBuffer&) const SK_OVERRIDE; 146 virtual void flatten(SkWriteBuffer&) const SK_OVERRIDE;
102 147
103 SkScalar computeXformedSigma(const SkMatrix& ctm) const { 148 SkScalar computeXformedSigma(const SkMatrix& ctm) const {
104 bool ignoreTransform = SkToBool(fBlurFlags & SkBlurMaskFilter::kIgnoreTr ansform_BlurFlag); 149 bool ignoreTransform = SkToBool(fBlurFlags & SkBlurMaskFilter::kIgnoreTr ansform_BlurFlag);
105 150
106 SkScalar xformedSigma = ignoreTransform ? fSigma : ctm.mapRadius(fSigma) ; 151 SkScalar xformedSigma = ignoreTransform ? fSigma : ctm.mapRadius(fSigma) ;
107 return SkMinScalar(xformedSigma, kMAX_BLUR_SIGMA); 152 return SkMinScalar(xformedSigma, kMAX_BLUR_SIGMA);
108 } 153 }
109 154
155 typedef std::list<BlurMaskRecord*> BlurMaskRecordList;
156 static BlurMaskRecordList fCachedBlurMaskRecordList;
157 static unsigned fCachedBlurMaskImageMemorySize;
158 static unsigned fMaxBlurMaskListSize;
159 static unsigned fMaxImageMemorySize;
160
110 typedef SkMaskFilter INHERITED; 161 typedef SkMaskFilter INHERITED;
111 }; 162 };
112 163
113 const SkScalar SkBlurMaskFilterImpl::kMAX_BLUR_SIGMA = SkIntToScalar(128); 164 const SkScalar SkBlurMaskFilterImpl::kMAX_BLUR_SIGMA = SkIntToScalar(128);
114 165
166 SkBlurMaskFilterImpl::BlurMaskRecordList SkBlurMaskFilterImpl::fCachedBlurMaskRe cordList;
reveman 2014/05/16 18:49:16 I'm failing to see how the use of this is thread s
junj 2014/05/19 12:54:49 Yes, I should add a mutex to make it thread-safe.
167 unsigned SkBlurMaskFilterImpl::fCachedBlurMaskImageMemorySize = 0;
168 unsigned SkBlurMaskFilterImpl::fMaxBlurMaskListSize = 50;
169 unsigned SkBlurMaskFilterImpl::fMaxImageMemorySize = 2 * 1024 * 1024;
reveman 2014/05/16 18:49:16 Please allow SkDiscardableMemory to be used instea
junj 2014/05/19 12:54:49 I have had a look at the interface of SkScaledImag
reveman 2014/05/19 14:09:37 Yes, I'm hoping to enable it this week and remove
170
171 bool SkBlurMaskFilterImpl::getBlurMaskRecord(SkScalar sigma,
172 unsigned rectCount,
173 const SkRect rects[],
174 SkMask** mask) {
175 if (!fCachedBlurMaskRecordList.empty()) {
176 for (BlurMaskRecordList::reverse_iterator it = fCachedBlurMaskRecordList .rbegin();
177 it != fCachedBlurMaskRecordList.rend(); ++it) {
178 BlurMaskRecord* cachedBlurMaskRecord = *it;
179 if (cachedBlurMaskRecord->fSigma == sigma
180 && cachedBlurMaskRecord->fRectCount == rectCount) {
181 bool found = true;
182 SkRect* tempRects[2] = {NULL, NULL};
183 for (int i = 0; i < rectCount; i++) {
184 tempRects[i] = *(cachedBlurMaskRecord->fRects + i);
185 if (tempRects[i]->width() != rects[i].width() ||
186 tempRects[i]->height() != rects[i].height()) {
187 found = false;
188 break;
189 }
190 }
191 if (found && rectCount == 2) {
192 if ((tempRects[0]->x() - rects[0].x()) != (tempRects[1]->x() - rects[1].x()) ||
193 (tempRects[0]->y() - rects[0].y()) != (tempRects[1]->y() - rects[1].y()))
194 found = false;
195 }
196 if (found) {
197 *mask = cachedBlurMaskRecord->fMask;
198 return true;
199 }
200 }
201 }
202 }
203 return false;
204 }
205
206 bool SkBlurMaskFilterImpl::addBlurMaskRecord(BlurMaskRecord* blurMaskRecord) {
207 while (fCachedBlurMaskRecordList.size() >= fMaxBlurMaskListSize
208 || fCachedBlurMaskImageMemorySize >= fMaxImageMemorySize) {
209 BlurMaskRecord* cachedBlurMaskRecord = fCachedBlurMaskRecordList.front() ;
210 fCachedBlurMaskRecordList.pop_front();
211 if (cachedBlurMaskRecord) {
212 fCachedBlurMaskImageMemorySize -= cachedBlurMaskRecord->fMask->compu teImageSize();
213 delete cachedBlurMaskRecord;
214 cachedBlurMaskRecord = NULL;
215 }
216 }
217 fCachedBlurMaskRecordList.push_back(blurMaskRecord);
218 fCachedBlurMaskImageMemorySize += blurMaskRecord->fMask->computeImageSize();
219 return true;
220 }
221
222 void SkBlurMaskFilterImpl::clearBlurMaskRecordList() {
223 while (!fCachedBlurMaskRecordList.empty()) {
224 BlurMaskRecord* cachedBlurMaskRecord = fCachedBlurMaskRecordList.front() ;
225 fCachedBlurMaskRecordList.pop_front();
226 if (cachedBlurMaskRecord) {
227 delete cachedBlurMaskRecord;
228 cachedBlurMaskRecord = NULL;
229 }
230 }
231 fCachedBlurMaskImageMemorySize = 0;
232 }
233
234 void SkBlurMaskFilterImpl::setBlurMaskRecordsLimit(unsigned listSize, unsigned m emSize) {
235 fMaxBlurMaskListSize = listSize;
236 fMaxImageMemorySize = memSize;
237 }
238
115 SkMaskFilter* SkBlurMaskFilter::Create(SkBlurStyle style, SkScalar sigma, uint32 _t flags) { 239 SkMaskFilter* SkBlurMaskFilter::Create(SkBlurStyle style, SkScalar sigma, uint32 _t flags) {
116 if (!SkScalarIsFinite(sigma) || sigma <= 0) { 240 if (!SkScalarIsFinite(sigma) || sigma <= 0) {
117 return NULL; 241 return NULL;
118 } 242 }
119 if ((unsigned)style > (unsigned)kLastEnum_SkBlurStyle) { 243 if ((unsigned)style > (unsigned)kLastEnum_SkBlurStyle) {
120 return NULL; 244 return NULL;
121 } 245 }
122 if (flags > SkBlurMaskFilter::kAll_BlurFlag) { 246 if (flags > SkBlurMaskFilter::kAll_BlurFlag) {
123 return NULL; 247 return NULL;
124 } 248 }
(...skipping 248 matching lines...) Expand 10 before | Expand all | Expand 10 after
373 SkRect smallR = SkRect::MakeWH(totalSmallWidth, totalSmallHeight); 497 SkRect smallR = SkRect::MakeWH(totalSmallWidth, totalSmallHeight);
374 498
375 SkRRect smallRR; 499 SkRRect smallRR;
376 SkVector radii[4]; 500 SkVector radii[4];
377 radii[SkRRect::kUpperLeft_Corner] = UL; 501 radii[SkRRect::kUpperLeft_Corner] = UL;
378 radii[SkRRect::kUpperRight_Corner] = UR; 502 radii[SkRRect::kUpperRight_Corner] = UR;
379 radii[SkRRect::kLowerRight_Corner] = LR; 503 radii[SkRRect::kLowerRight_Corner] = LR;
380 radii[SkRRect::kLowerLeft_Corner] = LL; 504 radii[SkRRect::kLowerLeft_Corner] = LL;
381 smallRR.setRectRadii(smallR, radii); 505 smallRR.setRectRadii(smallR, radii);
382 506
507 SkScalar sigma = this->computeXformedSigma(matrix);
508 SkMask* cachedBlurMask = NULL;
509 SkRect rects[1];
510 rects[0] = rrect.rect();
511 if (getBlurMaskRecord(sigma, 1, rects, &cachedBlurMask)) {
512 patch->fMask = *cachedBlurMask;
513 patch->fOuterRect = dstM.fBounds;
514 patch->fCenter.fX = SkScalarCeilToInt(leftUnstretched) + 1;
515 patch->fCenter.fY = SkScalarCeilToInt(topUnstretched) + 1;
516 return kTrue_FilterReturn;
517 }
518
383 bool analyticBlurWorked = false; 519 bool analyticBlurWorked = false;
384 if (c_analyticBlurRRect) { 520 if (c_analyticBlurRRect) {
385 analyticBlurWorked = 521 analyticBlurWorked =
386 this->filterRRectMask(&patch->fMask, smallRR, matrix, &margin, 522 this->filterRRectMask(&patch->fMask, smallRR, matrix, &margin,
387 SkMask::kComputeBoundsAndRenderImage_CreateMod e); 523 SkMask::kComputeBoundsAndRenderImage_CreateMod e);
388 } 524 }
389 525
390 if (!analyticBlurWorked) { 526 if (!analyticBlurWorked) {
391 if (!draw_rrect_into_mask(smallRR, &srcM)) { 527 if (!draw_rrect_into_mask(smallRR, &srcM)) {
392 return kFalse_FilterReturn; 528 return kFalse_FilterReturn;
393 } 529 }
394 530
395 SkAutoMaskFreeImage amf(srcM.fImage); 531 SkAutoMaskFreeImage amf(srcM.fImage);
396 532
397 if (!this->filterMask(&patch->fMask, srcM, matrix, &margin)) { 533 if (!this->filterMask(&patch->fMask, srcM, matrix, &margin)) {
398 return kFalse_FilterReturn; 534 return kFalse_FilterReturn;
399 } 535 }
400 } 536 }
401 537
402 patch->fMask.fBounds.offsetTo(0, 0); 538 patch->fMask.fBounds.offsetTo(0, 0);
403 patch->fOuterRect = dstM.fBounds; 539 patch->fOuterRect = dstM.fBounds;
404 patch->fCenter.fX = SkScalarCeilToInt(leftUnstretched) + 1; 540 patch->fCenter.fX = SkScalarCeilToInt(leftUnstretched) + 1;
405 patch->fCenter.fY = SkScalarCeilToInt(topUnstretched) + 1; 541 patch->fCenter.fY = SkScalarCeilToInt(topUnstretched) + 1;
542 if (!cachedBlurMask) {
543 SkMask* tempMask = new SkMask;
544 *tempMask = patch->fMask;
545 SkRect** tempRects = new SkRect* [1];
546 tempRects[0] = new SkRect;
547 *(tempRects[0]) = rrect.rect();
548 BlurMaskRecord * blurMaskRecord = new BlurMaskRecord(sigma, 1, tempRects , tempMask);
549 if (blurMaskRecord)
550 addBlurMaskRecord(blurMaskRecord);
551 }
552
406 return kTrue_FilterReturn; 553 return kTrue_FilterReturn;
407 } 554 }
408 555
409 SK_CONF_DECLARE( bool, c_analyticBlurNinepatch, "mask.filter.analyticNinePatch", true, "Use the faster analytic blur approach for ninepatch rects" ); 556 SK_CONF_DECLARE( bool, c_analyticBlurNinepatch, "mask.filter.analyticNinePatch", true, "Use the faster analytic blur approach for ninepatch rects" );
410 557
411 SkMaskFilter::FilterReturn 558 SkMaskFilter::FilterReturn
412 SkBlurMaskFilterImpl::filterRectsToNine(const SkRect rects[], int count, 559 SkBlurMaskFilterImpl::filterRectsToNine(const SkRect rects[], int count,
413 const SkMatrix& matrix, 560 const SkMatrix& matrix,
414 const SkIRect& clipBounds, 561 const SkIRect& clipBounds,
415 NinePatch* patch) const { 562 NinePatch* patch) const {
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after
475 if (1 == count) { 622 if (1 == count) {
476 innerIR = srcM.fBounds; 623 innerIR = srcM.fBounds;
477 center.set(smallW, smallH); 624 center.set(smallW, smallH);
478 } else { 625 } else {
479 SkASSERT(2 == count); 626 SkASSERT(2 == count);
480 rects[1].roundIn(&innerIR); 627 rects[1].roundIn(&innerIR);
481 center.set(smallW + (innerIR.left() - srcM.fBounds.left()), 628 center.set(smallW + (innerIR.left() - srcM.fBounds.left()),
482 smallH + (innerIR.top() - srcM.fBounds.top())); 629 smallH + (innerIR.top() - srcM.fBounds.top()));
483 } 630 }
484 631
632 SkScalar sigma = this->computeXformedSigma(matrix);
633 SkMask* cachedBlurMask = NULL;
634 if (getBlurMaskRecord(sigma, count, rects, &cachedBlurMask)) {
635 patch->fMask = *cachedBlurMask;
636 patch->fOuterRect = dstM.fBounds;
637 patch->fCenter = center;
638 return kTrue_FilterReturn;
639 }
640
485 // +1 so we get a clean, stretchable, center row/col 641 // +1 so we get a clean, stretchable, center row/col
486 smallW += 1; 642 smallW += 1;
487 smallH += 1; 643 smallH += 1;
488 644
489 // we want the inset amounts to be integral, so we don't change any 645 // we want the inset amounts to be integral, so we don't change any
490 // fractional phase on the fRight or fBottom of our smallR. 646 // fractional phase on the fRight or fBottom of our smallR.
491 const SkScalar dx = SkIntToScalar(innerIR.width() - smallW); 647 const SkScalar dx = SkIntToScalar(innerIR.width() - smallW);
492 const SkScalar dy = SkIntToScalar(innerIR.height() - smallH); 648 const SkScalar dy = SkIntToScalar(innerIR.height() - smallH);
493 if (dx < 0 || dy < 0) { 649 if (dx < 0 || dy < 0) {
494 // we're too small, relative to our blur, to break into nine-patch, 650 // we're too small, relative to our blur, to break into nine-patch,
495 // so we ask to have our normal filterMask() be called. 651 // so we ask to have our normal filterMask() be called.
496 return kUnimplemented_FilterReturn; 652 return kUnimplemented_FilterReturn;
497 } 653 }
498 654
499 smallR[0].set(rects[0].left(), rects[0].top(), rects[0].right() - dx, rects[ 0].bottom() - dy); 655 smallR[0].set(rects[0].left(), rects[0].top(), rects[0].right() - dx, rects[ 0].bottom() - dy);
500 if (smallR[0].width() < 2 || smallR[0].height() < 2) { 656 if (smallR[0].width() < 2 || smallR[0].height() < 2) {
501 return kUnimplemented_FilterReturn; 657 return kUnimplemented_FilterReturn;
502 } 658 }
503 if (2 == count) { 659 if (2 == count) {
504 smallR[1].set(rects[1].left(), rects[1].top(), 660 smallR[1].set(rects[1].left(), rects[1].top(),
505 rects[1].right() - dx, rects[1].bottom() - dy); 661 rects[1].right() - dx, rects[1].bottom() - dy);
506 SkASSERT(!smallR[1].isEmpty()); 662 SkASSERT(!smallR[1].isEmpty());
507 } 663 }
508 664
509 if (count > 1 || !c_analyticBlurNinepatch) { 665 if (count > 1 || !c_analyticBlurNinepatch) {
510 if (!draw_rects_into_mask(smallR, count, &srcM)) { 666 if (!draw_rects_into_mask(smallR, count, &srcM)) {
511 return kFalse_FilterReturn; 667 return kFalse_FilterReturn;
512 } 668 }
513
514 SkAutoMaskFreeImage amf(srcM.fImage); 669 SkAutoMaskFreeImage amf(srcM.fImage);
515 670
516 if (!this->filterMask(&patch->fMask, srcM, matrix, &margin)) { 671 if (!this->filterMask(&patch->fMask, srcM, matrix, &margin)) {
517 return kFalse_FilterReturn; 672 return kFalse_FilterReturn;
518 } 673 }
519 } else { 674 } else {
520 if (!this->filterRectMask(&patch->fMask, smallR[0], matrix, &margin, 675 if (!this->filterRectMask(&patch->fMask, smallR[0], matrix, &margin,
521 SkMask::kComputeBoundsAndRenderImage_CreateMod e)) { 676 SkMask::kComputeBoundsAndRenderImage_CreateMod e)) {
522 return kFalse_FilterReturn; 677 return kFalse_FilterReturn;
523 } 678 }
524 } 679 }
525 patch->fMask.fBounds.offsetTo(0, 0); 680 patch->fMask.fBounds.offsetTo(0, 0);
526 patch->fOuterRect = dstM.fBounds; 681 patch->fOuterRect = dstM.fBounds;
527 patch->fCenter = center; 682 patch->fCenter = center;
683
684 if (!cachedBlurMask) {
685 SkMask* tempMask = new SkMask;
686 *tempMask = patch->fMask;
687 SkRect** tempRects = new SkRect* [count];
688 for (int i = 0; i < count; i++) {
689 tempRects[i] = new SkRect;
690 *(tempRects[i]) = rects[i];
691 }
692 BlurMaskRecord * blurMaskRecord = new BlurMaskRecord(sigma, count, tempR ects, tempMask);
693 if (blurMaskRecord)
694 addBlurMaskRecord(blurMaskRecord);
695 }
696
528 return kTrue_FilterReturn; 697 return kTrue_FilterReturn;
529 } 698 }
530 699
531 void SkBlurMaskFilterImpl::computeFastBounds(const SkRect& src, 700 void SkBlurMaskFilterImpl::computeFastBounds(const SkRect& src,
532 SkRect* dst) const { 701 SkRect* dst) const {
533 SkScalar pad = 3.0f * fSigma; 702 SkScalar pad = 3.0f * fSigma;
534 703
535 dst->set(src.fLeft - pad, src.fTop - pad, 704 dst->set(src.fLeft - pad, src.fTop - pad,
536 src.fRight + pad, src.fBottom + pad); 705 src.fRight + pad, src.fBottom + pad);
537 } 706 }
(...skipping 687 matching lines...) Expand 10 before | Expand all | Expand 10 after
1225 } else { 1394 } else {
1226 str->append("None"); 1395 str->append("None");
1227 } 1396 }
1228 str->append("))"); 1397 str->append("))");
1229 } 1398 }
1230 #endif 1399 #endif
1231 1400
1232 SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_START(SkBlurMaskFilter) 1401 SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_START(SkBlurMaskFilter)
1233 SK_DEFINE_FLATTENABLE_REGISTRAR_ENTRY(SkBlurMaskFilterImpl) 1402 SK_DEFINE_FLATTENABLE_REGISTRAR_ENTRY(SkBlurMaskFilterImpl)
1234 SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_END 1403 SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_END
OLDNEW
« no previous file with comments | « src/core/SkMaskFilter.cpp ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698