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

Side by Side Diff: tests/ClipStackTest.cpp

Issue 48593003: Avoid re-rendering stencil clip for every draw with reducable clip stack (Closed) Base URL: https://skia.googlecode.com/svn/trunk
Patch Set: gcc-4.2 mac os 10.6 fix Created 7 years, 1 month 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 | « tests/ClipCacheTest.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 2011 Google Inc. 3 * Copyright 2011 Google Inc.
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 #include "Test.h" 8 #include "Test.h"
9 #if SK_SUPPORT_GPU 9 #if SK_SUPPORT_GPU
10 #include "GrReducedClip.h" 10 #include "GrReducedClip.h"
(...skipping 253 matching lines...) Expand 10 before | Expand all | Expand 10 after
264 264
265 if (useRects) { 265 if (useRects) {
266 stack.clipDevRect(rectA, SkRegion::kIntersect_Op, false); 266 stack.clipDevRect(rectA, SkRegion::kIntersect_Op, false);
267 stack.clipDevRect(rectB, gOps[op], false); 267 stack.clipDevRect(rectB, gOps[op], false);
268 } else { 268 } else {
269 stack.clipDevPath(clipA, SkRegion::kIntersect_Op, false); 269 stack.clipDevPath(clipA, SkRegion::kIntersect_Op, false);
270 stack.clipDevPath(clipB, gOps[op], false); 270 stack.clipDevPath(clipB, gOps[op], false);
271 } 271 }
272 272
273 REPORTER_ASSERT(reporter, !stack.isWideOpen()); 273 REPORTER_ASSERT(reporter, !stack.isWideOpen());
274 REPORTER_ASSERT(reporter, SkClipStack::kWideOpenGenID != stack.getTo pmostGenID());
274 275
275 stack.getConservativeBounds(0, 0, 100, 100, &devClipBound, 276 stack.getConservativeBounds(0, 0, 100, 100, &devClipBound,
276 &isIntersectionOfRects); 277 &isIntersectionOfRects);
277 278
278 if (useRects) { 279 if (useRects) {
279 REPORTER_ASSERT(reporter, isIntersectionOfRects == 280 REPORTER_ASSERT(reporter, isIntersectionOfRects ==
280 (gOps[op] == SkRegion::kIntersect_Op)); 281 (gOps[op] == SkRegion::kIntersect_Op));
281 } else { 282 } else {
282 REPORTER_ASSERT(reporter, !isIntersectionOfRects); 283 REPORTER_ASSERT(reporter, !isIntersectionOfRects);
283 } 284 }
284 285
285 SkASSERT(testCase < gNumCases); 286 SkASSERT(testCase < gNumCases);
286 REPORTER_ASSERT(reporter, devClipBound == gAnswerRectsBW[testCase]); 287 REPORTER_ASSERT(reporter, devClipBound == gAnswerRectsBW[testCase]);
287 ++testCase; 288 ++testCase;
288 289
289 stack.restore(); 290 stack.restore();
290 } 291 }
291 } 292 }
292 } 293 }
293 294
294 // Test out 'isWideOpen' entry point 295 // Test out 'isWideOpen' entry point
295 static void test_isWideOpen(skiatest::Reporter* reporter) { 296 static void test_isWideOpen(skiatest::Reporter* reporter) {
297 {
298 // Empty stack is wide open. Wide open stack means that gen id is wide o pen.
299 SkClipStack stack;
300 REPORTER_ASSERT(reporter, stack.isWideOpen());
301 REPORTER_ASSERT(reporter, SkClipStack::kWideOpenGenID == stack.getTopmos tGenID());
302 }
296 303
297 SkRect rectA, rectB; 304 SkRect rectA, rectB;
298 305
299 rectA.iset(10, 10, 40, 40); 306 rectA.iset(10, 10, 40, 40);
300 rectB.iset(50, 50, 80, 80); 307 rectB.iset(50, 50, 80, 80);
301 308
302 // Stack should initially be wide open 309 // Stack should initially be wide open
303 { 310 {
304 SkClipStack stack; 311 SkClipStack stack;
305 312
306 REPORTER_ASSERT(reporter, stack.isWideOpen()); 313 REPORTER_ASSERT(reporter, stack.isWideOpen());
314 REPORTER_ASSERT(reporter, SkClipStack::kWideOpenGenID == stack.getTopmos tGenID());
307 } 315 }
308 316
309 // Test out case where the user specifies a union that includes everything 317 // Test out case where the user specifies a union that includes everything
310 { 318 {
311 SkClipStack stack; 319 SkClipStack stack;
312 320
313 SkPath clipA, clipB; 321 SkPath clipA, clipB;
314 322
315 clipA.addRoundRect(rectA, SkIntToScalar(5), SkIntToScalar(5)); 323 clipA.addRoundRect(rectA, SkIntToScalar(5), SkIntToScalar(5));
316 clipA.setFillType(SkPath::kInverseEvenOdd_FillType); 324 clipA.setFillType(SkPath::kInverseEvenOdd_FillType);
317 325
318 clipB.addRoundRect(rectB, SkIntToScalar(5), SkIntToScalar(5)); 326 clipB.addRoundRect(rectB, SkIntToScalar(5), SkIntToScalar(5));
319 clipB.setFillType(SkPath::kInverseEvenOdd_FillType); 327 clipB.setFillType(SkPath::kInverseEvenOdd_FillType);
320 328
321 stack.clipDevPath(clipA, SkRegion::kReplace_Op, false); 329 stack.clipDevPath(clipA, SkRegion::kReplace_Op, false);
322 stack.clipDevPath(clipB, SkRegion::kUnion_Op, false); 330 stack.clipDevPath(clipB, SkRegion::kUnion_Op, false);
323 331
324 REPORTER_ASSERT(reporter, stack.isWideOpen()); 332 REPORTER_ASSERT(reporter, stack.isWideOpen());
333 REPORTER_ASSERT(reporter, SkClipStack::kWideOpenGenID == stack.getTopmos tGenID());
325 } 334 }
326 335
327 // Test out union w/ a wide open clip 336 // Test out union w/ a wide open clip
328 { 337 {
329 SkClipStack stack; 338 SkClipStack stack;
330 339
331 stack.clipDevRect(rectA, SkRegion::kUnion_Op, false); 340 stack.clipDevRect(rectA, SkRegion::kUnion_Op, false);
332 341
333 REPORTER_ASSERT(reporter, stack.isWideOpen()); 342 REPORTER_ASSERT(reporter, stack.isWideOpen());
343 REPORTER_ASSERT(reporter, SkClipStack::kWideOpenGenID == stack.getTopmos tGenID());
334 } 344 }
335 345
336 // Test out empty difference from a wide open clip 346 // Test out empty difference from a wide open clip
337 { 347 {
338 SkClipStack stack; 348 SkClipStack stack;
339 349
340 SkRect emptyRect; 350 SkRect emptyRect;
341 emptyRect.setEmpty(); 351 emptyRect.setEmpty();
342 352
343 stack.clipDevRect(emptyRect, SkRegion::kDifference_Op, false); 353 stack.clipDevRect(emptyRect, SkRegion::kDifference_Op, false);
344 354
345 REPORTER_ASSERT(reporter, stack.isWideOpen()); 355 REPORTER_ASSERT(reporter, stack.isWideOpen());
356 REPORTER_ASSERT(reporter, SkClipStack::kWideOpenGenID == stack.getTopmos tGenID());
346 } 357 }
347 358
348 // Test out return to wide open 359 // Test out return to wide open
349 { 360 {
350 SkClipStack stack; 361 SkClipStack stack;
351 362
352 stack.save(); 363 stack.save();
353 364
354 stack.clipDevRect(rectA, SkRegion::kReplace_Op, false); 365 stack.clipDevRect(rectA, SkRegion::kReplace_Op, false);
355 366
356 REPORTER_ASSERT(reporter, !stack.isWideOpen()); 367 REPORTER_ASSERT(reporter, !stack.isWideOpen());
368 REPORTER_ASSERT(reporter, SkClipStack::kWideOpenGenID != stack.getTopmos tGenID());
357 369
358 stack.restore(); 370 stack.restore();
359 371
360 REPORTER_ASSERT(reporter, stack.isWideOpen()); 372 REPORTER_ASSERT(reporter, stack.isWideOpen());
373 REPORTER_ASSERT(reporter, SkClipStack::kWideOpenGenID == stack.getTopmos tGenID());
361 } 374 }
362 } 375 }
363 376
364 static int count(const SkClipStack& stack) { 377 static int count(const SkClipStack& stack) {
365 378
366 SkClipStack::Iter iter(stack, SkClipStack::Iter::kTop_IterStart); 379 SkClipStack::Iter iter(stack, SkClipStack::Iter::kTop_IterStart);
367 380
368 const SkClipStack::Element* element = NULL; 381 const SkClipStack::Element* element = NULL;
369 int count = 0; 382 int count = 0;
370 383
(...skipping 562 matching lines...) Expand 10 before | Expand all | Expand 10 after
933 } 946 }
934 947
935 SkRect inflatedBounds = kBounds; 948 SkRect inflatedBounds = kBounds;
936 inflatedBounds.outset(kBounds.width() / 2, kBounds.height() / 2); 949 inflatedBounds.outset(kBounds.width() / 2, kBounds.height() / 2);
937 SkIRect inflatedIBounds; 950 SkIRect inflatedIBounds;
938 inflatedBounds.roundOut(&inflatedIBounds); 951 inflatedBounds.roundOut(&inflatedIBounds);
939 952
940 typedef GrReducedClip::ElementList ElementList; 953 typedef GrReducedClip::ElementList ElementList;
941 // Get the reduced version of the stack. 954 // Get the reduced version of the stack.
942 ElementList reducedClips; 955 ElementList reducedClips;
943 956 int32_t reducedGenID;
944 GrReducedClip::InitialState initial; 957 GrReducedClip::InitialState initial;
945 SkIRect tBounds(inflatedIBounds); 958 SkIRect tBounds(inflatedIBounds);
946 SkIRect* tightBounds = r.nextBool() ? &tBounds : NULL; 959 SkIRect* tightBounds = r.nextBool() ? &tBounds : NULL;
947 GrReducedClip::ReduceClipStack(stack, 960 GrReducedClip::ReduceClipStack(stack,
948 inflatedIBounds, 961 inflatedIBounds,
949 &reducedClips, 962 &reducedClips,
963 &reducedGenID,
950 &initial, 964 &initial,
951 tightBounds); 965 tightBounds);
952 966
967 REPORTER_ASSERT(reporter, SkClipStack::kInvalidGenID != reducedGenID);
968
953 // Build a new clip stack based on the reduced clip elements 969 // Build a new clip stack based on the reduced clip elements
954 SkClipStack reducedStack; 970 SkClipStack reducedStack;
955 if (GrReducedClip::kAllOut_InitialState == initial) { 971 if (GrReducedClip::kAllOut_InitialState == initial) {
956 // whether the result is bounded or not, the whole plane should star t outside the clip. 972 // whether the result is bounded or not, the whole plane should star t outside the clip.
957 reducedStack.clipEmpty(); 973 reducedStack.clipEmpty();
958 } 974 }
959 for (ElementList::Iter iter = reducedClips.headIter(); NULL != iter.get( ); iter.next()) { 975 for (ElementList::Iter iter = reducedClips.headIter(); NULL != iter.get( ); iter.next()) {
960 add_elem_to_stack(*iter.get(), &reducedStack); 976 add_elem_to_stack(*iter.get(), &reducedStack);
961 } 977 }
962 978
(...skipping 16 matching lines...) Expand all
979 reducedRegion.setRect(inflatedIBounds); 995 reducedRegion.setRect(inflatedIBounds);
980 iter.reset(reducedStack, SkClipStack::Iter::kBottom_IterStart); 996 iter.reset(reducedStack, SkClipStack::Iter::kBottom_IterStart);
981 while ((element = iter.next())) { 997 while ((element = iter.next())) {
982 add_elem_to_region(*element, inflatedIBounds, &reducedRegion); 998 add_elem_to_region(*element, inflatedIBounds, &reducedRegion);
983 } 999 }
984 1000
985 REPORTER_ASSERT(reporter, region == reducedRegion); 1001 REPORTER_ASSERT(reporter, region == reducedRegion);
986 } 1002 }
987 } 1003 }
988 1004
1005 #if defined(WIN32)
1006 #define SUPPRESS_VISIBILITY_WARNING
1007 #else
1008 #define SUPPRESS_VISIBILITY_WARNING __attribute__((visibility("hidden")))
1009 #endif
1010
1011 static void test_reduced_clip_stack_genid(skiatest::Reporter* reporter) {
1012 {
1013 SkClipStack stack;
1014 stack.clipDevRect(SkRect::MakeXYWH(0, 0, 100, 100), SkRegion::kReplace_O p, true);
1015 stack.clipDevRect(SkRect::MakeXYWH(0, 0, SkScalar(50.3), SkScalar(50.3)) , SkRegion::kReplace_Op, true);
1016 SkIRect inflatedIBounds = SkIRect::MakeXYWH(0, 0, 100, 100);
1017
1018 GrReducedClip::ElementList reducedClips;
1019 int32_t reducedGenID;
1020 GrReducedClip::InitialState initial;
1021 SkIRect tightBounds;
1022
1023 GrReducedClip::ReduceClipStack(stack,
1024 inflatedIBounds,
1025 &reducedClips,
1026 &reducedGenID,
1027 &initial,
1028 &tightBounds);
1029
1030 REPORTER_ASSERT(reporter, reducedClips.count() == 1);
1031 // Clips will be cached based on the generation id. Make sure the gen id is valid.
1032 REPORTER_ASSERT(reporter, SkClipStack::kInvalidGenID != reducedGenID);
1033 }
1034 {
1035 SkClipStack stack;
1036
1037 // Create a clip with following 25.3, 25.3 boxes which are 25 apart:
1038 // A B
1039 // C D
1040
1041 stack.clipDevRect(SkRect::MakeXYWH(0, 0, SkScalar(25.3), SkScalar(25.3)) , SkRegion::kReplace_Op, true);
1042 int32_t genIDA = stack.getTopmostGenID();
1043 stack.clipDevRect(SkRect::MakeXYWH(50, 0, SkScalar(25.3), SkScalar(25.3) ), SkRegion::kUnion_Op, true);
1044 int32_t genIDB = stack.getTopmostGenID();
1045 stack.clipDevRect(SkRect::MakeXYWH(0, 50, SkScalar(25.3), SkScalar(25.3) ), SkRegion::kUnion_Op, true);
1046 int32_t genIDC = stack.getTopmostGenID();
1047 stack.clipDevRect(SkRect::MakeXYWH(50, 50, SkScalar(25.3), SkScalar(25.3 )), SkRegion::kUnion_Op, true);
1048 int32_t genIDD = stack.getTopmostGenID();
1049
1050
1051 #define XYWH SkIRect::MakeXYWH
1052
1053 SkIRect unused;
1054 unused.setEmpty();
1055 SkIRect stackBounds = XYWH(0, 0, 76, 76);
1056
1057 // The base test is to test each rect in two ways:
1058 // 1) The box dimensions. (Should reduce to "all in", no elements).
1059 // 2) A bit over the box dimensions.
1060 // In the case 2, test that the generation id is what is expected.
1061 // The rects are of fractional size so that case 2 never gets optimized to an empty element
1062 // list.
1063
1064 // Not passing in tighter bounds is tested for consistency.
1065 static const struct SUPPRESS_VISIBILITY_WARNING {
1066 SkIRect testBounds;
1067 int reducedClipCount;
1068 int32_t reducedGenID;
1069 GrReducedClip::InitialState initialState;
1070 SkIRect tighterBounds; // If this is empty, the query will not pass tighter bounds
1071 // parameter.
1072 } testCases[] = {
1073 // Rect A.
1074 { XYWH(0, 0, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip: :kAllIn_InitialState, XYWH(0, 0, 25, 25) },
1075 { XYWH(0, 0, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip: :kAllIn_InitialState, unused },
1076 { XYWH(0, 0, 27, 27), 1, genIDA, GrReducedClip::kAllOut_InitialState , XYWH(0, 0, 27, 27)},
1077 { XYWH(0, 0, 27, 27), 1, genIDA, GrReducedClip::kAllOut_InitialState , unused },
1078
1079 // Rect B.
1080 { XYWH(50, 0, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip ::kAllIn_InitialState, XYWH(50, 0, 25, 25) },
1081 { XYWH(50, 0, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip ::kAllIn_InitialState, unused },
1082 { XYWH(50, 0, 27, 27), 1, genIDB, GrReducedClip::kAllOut_InitialStat e, XYWH(50, 0, 26, 27) },
1083 { XYWH(50, 0, 27, 27), 1, genIDB, GrReducedClip::kAllOut_InitialStat e, unused },
1084
1085 // Rect C.
1086 { XYWH(0, 50, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip ::kAllIn_InitialState, XYWH(0, 50, 25, 25) },
1087 { XYWH(0, 50, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip ::kAllIn_InitialState, unused },
1088 { XYWH(0, 50, 27, 27), 1, genIDC, GrReducedClip::kAllOut_InitialStat e, XYWH(0, 50, 27, 26) },
1089 { XYWH(0, 50, 27, 27), 1, genIDC, GrReducedClip::kAllOut_InitialStat e, unused },
1090
1091 // Rect D.
1092 { XYWH(50, 50, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedCli p::kAllIn_InitialState, unused },
1093 { XYWH(50, 50, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedCli p::kAllIn_InitialState, XYWH(50, 50, 25, 25)},
1094 { XYWH(50, 50, 27, 27), 1, genIDD, GrReducedClip::kAllOut_InitialSta te, unused },
1095 { XYWH(50, 50, 27, 27), 1, genIDD, GrReducedClip::kAllOut_InitialSta te, XYWH(50, 50, 26, 26)},
1096
1097 // Other tests:
1098 { XYWH(0, 0, 100, 100), 4, genIDD, GrReducedClip::kAllOut_InitialSta te, unused },
1099 { XYWH(0, 0, 100, 100), 4, genIDD, GrReducedClip::kAllOut_InitialSta te, stackBounds },
1100
1101 // Rect in the middle, touches none.
1102 { XYWH(26, 26, 24, 24), 0, SkClipStack::kEmptyGenID, GrReducedClip:: kAllOut_InitialState, unused },
1103 { XYWH(26, 26, 24, 24), 0, SkClipStack::kEmptyGenID, GrReducedClip:: kAllOut_InitialState, XYWH(26, 26, 24, 24) },
1104
1105 // Rect in the middle, touches all the rects. GenID is the last rect .
1106 { XYWH(24, 24, 27, 27), 4, genIDD, GrReducedClip::kAllOut_InitialSta te, unused },
1107 { XYWH(24, 24, 27, 27), 4, genIDD, GrReducedClip::kAllOut_InitialSta te, XYWH(24, 24, 27, 27) },
1108 };
1109
1110 #undef XYWH
1111
1112 for (size_t i = 0; i < SK_ARRAY_COUNT(testCases); ++i) {
1113 GrReducedClip::ElementList reducedClips;
1114 int32_t reducedGenID;
1115 GrReducedClip::InitialState initial;
1116 SkIRect tightBounds;
1117
1118 GrReducedClip::ReduceClipStack(stack,
1119 testCases[i].testBounds,
1120 &reducedClips,
1121 &reducedGenID,
1122 &initial,
1123 testCases[i].tighterBounds.isEmpty() ? NULL : &tightBounds);
1124
1125 REPORTER_ASSERT(reporter, reducedClips.count() == testCases[i].reduc edClipCount);
1126 SkASSERT(reducedClips.count() == testCases[i].reducedClipCount);
1127 REPORTER_ASSERT(reporter, reducedGenID == testCases[i].reducedGenID) ;
1128 SkASSERT(reducedGenID == testCases[i].reducedGenID);
1129 REPORTER_ASSERT(reporter, initial == testCases[i].initialState);
1130 SkASSERT(initial == testCases[i].initialState);
1131 if (!testCases[i].tighterBounds.isEmpty()) {
1132 REPORTER_ASSERT(reporter, tightBounds == testCases[i].tighterBou nds);
1133 SkASSERT(tightBounds == testCases[i].tighterBounds);
1134 }
1135 }
1136 }
1137 }
1138
1139 static void test_reduced_clip_stack_no_aa_crash(skiatest::Reporter* reporter) {
1140 SkClipStack stack;
1141 stack.clipDevRect(SkIRect::MakeXYWH(0, 0, 100, 100), SkRegion::kReplace_Op);
1142 stack.clipDevRect(SkIRect::MakeXYWH(0, 0, 50, 50), SkRegion::kReplace_Op);
1143 SkIRect inflatedIBounds = SkIRect::MakeXYWH(0, 0, 100, 100);
1144
1145 GrReducedClip::ElementList reducedClips;
1146 int32_t reducedGenID;
1147 GrReducedClip::InitialState initial;
1148 SkIRect tightBounds;
1149
1150 // At the time, this would crash.
1151 GrReducedClip::ReduceClipStack(stack,
1152 inflatedIBounds,
1153 &reducedClips,
1154 &reducedGenID,
1155 &initial,
1156 &tightBounds);
1157
1158 REPORTER_ASSERT(reporter, 0 == reducedClips.count());
1159 }
1160
989 #endif 1161 #endif
990 //////////////////////////////////////////////////////////////////////////////// /////////////////// 1162 //////////////////////////////////////////////////////////////////////////////// ///////////////////
991 1163
992 static void TestClipStack(skiatest::Reporter* reporter) { 1164 static void TestClipStack(skiatest::Reporter* reporter) {
993 SkClipStack stack; 1165 SkClipStack stack;
994 1166
995 REPORTER_ASSERT(reporter, 0 == stack.getSaveCount()); 1167 REPORTER_ASSERT(reporter, 0 == stack.getSaveCount());
996 assert_count(reporter, stack, 0); 1168 assert_count(reporter, stack, 0);
997 1169
998 static const SkIRect gRects[] = { 1170 static const SkIRect gRects[] = {
(...skipping 28 matching lines...) Expand all
1027 test_bounds(reporter, true); // once with rects 1199 test_bounds(reporter, true); // once with rects
1028 test_bounds(reporter, false); // once with paths 1200 test_bounds(reporter, false); // once with paths
1029 test_isWideOpen(reporter); 1201 test_isWideOpen(reporter);
1030 test_rect_merging(reporter); 1202 test_rect_merging(reporter);
1031 test_rect_replace(reporter); 1203 test_rect_replace(reporter);
1032 test_rect_inverse_fill(reporter); 1204 test_rect_inverse_fill(reporter);
1033 test_path_replace(reporter); 1205 test_path_replace(reporter);
1034 test_quickContains(reporter); 1206 test_quickContains(reporter);
1035 #if SK_SUPPORT_GPU 1207 #if SK_SUPPORT_GPU
1036 test_reduced_clip_stack(reporter); 1208 test_reduced_clip_stack(reporter);
1209 test_reduced_clip_stack_genid(reporter);
1210 test_reduced_clip_stack_no_aa_crash(reporter);
1037 #endif 1211 #endif
1038 } 1212 }
1039 1213
1040 #include "TestClassDef.h" 1214 #include "TestClassDef.h"
1041 DEFINE_TESTCLASS("ClipStack", TestClipStackClass, TestClipStack) 1215 DEFINE_TESTCLASS("ClipStack", TestClipStackClass, TestClipStack)
OLDNEW
« no previous file with comments | « tests/ClipCacheTest.cpp ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698