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

Side by Side Diff: src/gpu/GrDrawState.cpp

Issue 719203002: Add GrProcOptInfo class to track various output information for color and coverage stages. (Closed) Base URL: https://skia.googlesource.com/skia.git@moveIO
Patch Set: Delete old code Created 6 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
OLDNEW
1 /* 1 /*
2 * Copyright 2012 Google Inc. 2 * Copyright 2012 Google Inc.
3 * 3 *
4 * Use of this source code is governed by a BSD-style license that can be 4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file. 5 * found in the LICENSE file.
6 */ 6 */
7 7
8 #include "GrDrawState.h" 8 #include "GrDrawState.h"
9 9
10 #include "GrInvariantOutput.h"
11 #include "GrOptDrawState.h" 10 #include "GrOptDrawState.h"
12 #include "GrPaint.h" 11 #include "GrPaint.h"
12 #include "GrProcOptInfo.h"
13 13
14 //////////////////////////////////////////////////////////////////////////////s 14 //////////////////////////////////////////////////////////////////////////////s
15 15
16 bool GrDrawState::isEqual(const GrDrawState& that) const { 16 bool GrDrawState::isEqual(const GrDrawState& that) const {
17 bool usingVertexColors = this->hasColorVertexAttribute(); 17 bool usingVertexColors = this->hasColorVertexAttribute();
18 if (!usingVertexColors && this->fColor != that.fColor) { 18 if (!usingVertexColors && this->fColor != that.fColor) {
19 return false; 19 return false;
20 } 20 }
21 21
22 if (this->getRenderTarget() != that.getRenderTarget() || 22 if (this->getRenderTarget() != that.getRenderTarget() ||
(...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after
143 if (that.hasGeometryProcessor()) { 143 if (that.hasGeometryProcessor()) {
144 fGeometryProcessor.initAndRef(that.fGeometryProcessor); 144 fGeometryProcessor.initAndRef(that.fGeometryProcessor);
145 } else { 145 } else {
146 fGeometryProcessor.reset(NULL); 146 fGeometryProcessor.reset(NULL);
147 } 147 }
148 fColorStages = that.fColorStages; 148 fColorStages = that.fColorStages;
149 fCoverageStages = that.fCoverageStages; 149 fCoverageStages = that.fCoverageStages;
150 150
151 fHints = that.fHints; 151 fHints = that.fHints;
152 152
153 fColorProcInfoValid = that.fColorProcInfoValid;
154 fCoverageProcInfoValid = that.fCoverageProcInfoValid;
155 if (fColorProcInfoValid) {
156 fColorProcInfo = that.fColorProcInfo;
157 }
158 if (fCoverageProcInfoValid) {
159 fCoverageProcInfo = that.fCoverageProcInfo;
160 }
161
153 memcpy(fFixedFunctionVertexAttribIndices, 162 memcpy(fFixedFunctionVertexAttribIndices,
154 that.fFixedFunctionVertexAttribIndices, 163 that.fFixedFunctionVertexAttribIndices,
155 sizeof(fFixedFunctionVertexAttribIndices)); 164 sizeof(fFixedFunctionVertexAttribIndices));
156 return *this; 165 return *this;
157 } 166 }
158 167
159 void GrDrawState::onReset(const SkMatrix* initialViewMatrix) { 168 void GrDrawState::onReset(const SkMatrix* initialViewMatrix) {
160 SkASSERT(0 == fBlockEffectRemovalCnt || 0 == this->numTotalStages()); 169 SkASSERT(0 == fBlockEffectRemovalCnt || 0 == this->numTotalStages());
161 SkASSERT(!fRenderTarget.ownsPendingIO()); 170 SkASSERT(!fRenderTarget.ownsPendingIO());
162 171
(...skipping 13 matching lines...) Expand all
176 } 185 }
177 fSrcBlend = kOne_GrBlendCoeff; 186 fSrcBlend = kOne_GrBlendCoeff;
178 fDstBlend = kZero_GrBlendCoeff; 187 fDstBlend = kZero_GrBlendCoeff;
179 fBlendConstant = 0x0; 188 fBlendConstant = 0x0;
180 fFlagBits = 0x0; 189 fFlagBits = 0x0;
181 fStencilSettings.setDisabled(); 190 fStencilSettings.setDisabled();
182 fCoverage = 0xff; 191 fCoverage = 0xff;
183 fDrawFace = kBoth_DrawFace; 192 fDrawFace = kBoth_DrawFace;
184 193
185 fHints = 0; 194 fHints = 0;
195
196 fColorProcInfoValid = false;
197 fCoverageProcInfoValid = false;
186 } 198 }
187 199
188 bool GrDrawState::setIdentityViewMatrix() { 200 bool GrDrawState::setIdentityViewMatrix() {
189 if (this->numFragmentStages()) { 201 if (this->numFragmentStages()) {
190 SkMatrix invVM; 202 SkMatrix invVM;
191 if (!fViewMatrix.invert(&invVM)) { 203 if (!fViewMatrix.invert(&invVM)) {
192 // sad trombone sound 204 // sad trombone sound
193 return false; 205 return false;
194 } 206 }
195 for (int s = 0; s < this->numColorStages(); ++s) { 207 for (int s = 0; s < this->numColorStages(); ++s) {
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
231 243
232 // Enable the clip bit 244 // Enable the clip bit
233 this->enableState(GrDrawState::kClip_StateBit); 245 this->enableState(GrDrawState::kClip_StateBit);
234 246
235 this->setColor(paint.getColor()); 247 this->setColor(paint.getColor());
236 this->setState(GrDrawState::kDither_StateBit, paint.isDither()); 248 this->setState(GrDrawState::kDither_StateBit, paint.isDither());
237 this->setState(GrDrawState::kHWAntialias_StateBit, paint.isAntiAlias()); 249 this->setState(GrDrawState::kHWAntialias_StateBit, paint.isAntiAlias());
238 250
239 this->setBlendFunc(paint.getSrcBlendCoeff(), paint.getDstBlendCoeff()); 251 this->setBlendFunc(paint.getSrcBlendCoeff(), paint.getDstBlendCoeff());
240 this->setCoverage(0xFF); 252 this->setCoverage(0xFF);
253 fColorProcInfoValid = false;
254 fCoverageProcInfoValid = false;
241 } 255 }
242 256
243 //////////////////////////////////////////////////////////////////////////////// 257 ////////////////////////////////////////////////////////////////////////////////
244 258
245 bool GrDrawState::validateVertexAttribs() const { 259 bool GrDrawState::validateVertexAttribs() const {
246 // check consistency of effects and attributes 260 // check consistency of effects and attributes
247 GrSLType slTypes[kMaxVertexAttribCnt]; 261 GrSLType slTypes[kMaxVertexAttribCnt];
248 for (int i = 0; i < kMaxVertexAttribCnt; ++i) { 262 for (int i = 0; i < kMaxVertexAttribCnt; ++i) {
249 slTypes[i] = static_cast<GrSLType>(-1); 263 slTypes[i] = static_cast<GrSLType>(-1);
250 } 264 }
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
328 fFixedFunctionVertexAttribIndices[attribs[i].fBinding] = i; 342 fFixedFunctionVertexAttribIndices[attribs[i].fBinding] = i;
329 } 343 }
330 #ifdef SK_DEBUG 344 #ifdef SK_DEBUG
331 size_t dwordCount = GrVertexAttribTypeSize(attribs[i].fType) >> 2; 345 size_t dwordCount = GrVertexAttribTypeSize(attribs[i].fType) >> 2;
332 uint32_t mask = (1 << dwordCount)-1; 346 uint32_t mask = (1 << dwordCount)-1;
333 size_t offsetShift = attribs[i].fOffset >> 2; 347 size_t offsetShift = attribs[i].fOffset >> 2;
334 SkASSERT(!(overlapCheck & (mask << offsetShift))); 348 SkASSERT(!(overlapCheck & (mask << offsetShift)));
335 overlapCheck |= (mask << offsetShift); 349 overlapCheck |= (mask << offsetShift);
336 #endif 350 #endif
337 } 351 }
352 fColorProcInfoValid = false;
353 fCoverageProcInfoValid = false;
338 // Positions must be specified. 354 // Positions must be specified.
339 SkASSERT(-1 != fFixedFunctionVertexAttribIndices[kPosition_GrVertexAttribBin ding]); 355 SkASSERT(-1 != fFixedFunctionVertexAttribIndices[kPosition_GrVertexAttribBin ding]);
340 } 356 }
341 357
342 //////////////////////////////////////////////////////////////////////////////// 358 ////////////////////////////////////////////////////////////////////////////////
343 359
344 void GrDrawState::setDefaultVertexAttribs() { 360 void GrDrawState::setDefaultVertexAttribs() {
345 static const GrVertexAttrib kPositionAttrib = 361 static const GrVertexAttrib kPositionAttrib =
346 {kVec2f_GrVertexAttribType, 0, kPosition_GrVertexAttribBinding}; 362 {kVec2f_GrVertexAttribType, 0, kPosition_GrVertexAttribBinding};
347 363
348 fVAPtr = &kPositionAttrib; 364 fVAPtr = &kPositionAttrib;
349 fVACount = 1; 365 fVACount = 1;
350 fVAStride = GrVertexAttribTypeSize(kVec2f_GrVertexAttribType); 366 fVAStride = GrVertexAttribTypeSize(kVec2f_GrVertexAttribType);
351 367
352 // set all the fixed function indices to -1 except position. 368 // set all the fixed function indices to -1 except position.
353 memset(fFixedFunctionVertexAttribIndices, 369 memset(fFixedFunctionVertexAttribIndices,
354 0xff, 370 0xff,
355 sizeof(fFixedFunctionVertexAttribIndices)); 371 sizeof(fFixedFunctionVertexAttribIndices));
356 fFixedFunctionVertexAttribIndices[kPosition_GrVertexAttribBinding] = 0; 372 fFixedFunctionVertexAttribIndices[kPosition_GrVertexAttribBinding] = 0;
373 fColorProcInfoValid = false;
374 fCoverageProcInfoValid = false;
357 } 375 }
358 376
359 //////////////////////////////////////////////////////////////////////////////// 377 ////////////////////////////////////////////////////////////////////////////////
360 378
361 bool GrDrawState::couldApplyCoverage(const GrDrawTargetCaps& caps) const { 379 bool GrDrawState::couldApplyCoverage(const GrDrawTargetCaps& caps) const {
362 if (caps.dualSourceBlendingSupport()) { 380 if (caps.dualSourceBlendingSupport()) {
363 return true; 381 return true;
364 } 382 }
365 // we can correctly apply coverage if a) we have dual source blending 383 // we can correctly apply coverage if a) we have dual source blending
366 // or b) one of our blend optimizations applies 384 // or b) one of our blend optimizations applies
367 // or c) the src, dst blend coeffs are 1,0 and we will read Dst Color 385 // or c) the src, dst blend coeffs are 1,0 and we will read Dst Color
368 GrBlendCoeff srcCoeff; 386 GrBlendCoeff srcCoeff;
369 GrBlendCoeff dstCoeff; 387 GrBlendCoeff dstCoeff;
370 BlendOptFlags flag = this->getBlendOpts(true, &srcCoeff, &dstCoeff); 388 BlendOptFlags flag = this->getBlendOpts(true, &srcCoeff, &dstCoeff);
371 return GrDrawState::kNone_BlendOpt != flag || 389 return GrDrawState::kNone_BlendOpt != flag ||
372 (this->willEffectReadDstColor() && 390 (this->willEffectReadDstColor() &&
373 kOne_GrBlendCoeff == srcCoeff && kZero_GrBlendCoeff == dstCoeff); 391 kOne_GrBlendCoeff == srcCoeff && kZero_GrBlendCoeff == dstCoeff);
374 } 392 }
375 393
376 bool GrDrawState::hasSolidCoverage() const { 394 bool GrDrawState::hasSolidCoverage() const {
377 // If we're drawing coverage directly then coverage is effectively treated a s color. 395 // If we're drawing coverage directly then coverage is effectively treated a s color.
378 if (this->isCoverageDrawing()) { 396 if (this->isCoverageDrawing()) {
379 return true; 397 return true;
380 } 398 }
381 399
382 if (this->numCoverageStages() > 0) { 400 if (this->numCoverageStages() > 0) {
383 return false; 401 return false;
384 } 402 }
385 403
386 GrColor color; 404 this->calcCoverageInvariantOutput();
387 GrColorComponentFlags flags; 405 return fCoverageProcInfo.isSolidWhite();
388 // Initialize to an unknown starting coverage if per-vertex coverage is spec ified.
389 if (this->hasCoverageVertexAttribute()) {
390 color = 0;
391 flags = static_cast<GrColorComponentFlags>(0);
392 } else {
393 color = this->getCoverageColor();
394 flags = kRGBA_GrColorComponentFlags;
395 }
396 GrInvariantOutput inout(color, flags, true);
397
398 // check the coverage output from the GP
399 if (this->hasGeometryProcessor()) {
400 fGeometryProcessor->computeInvariantOutput(&inout);
401 }
402
403 return inout.isSolidWhite();
404 } 406 }
405 407
406 ////////////////////////////////////////////////////////////////////////////// 408 //////////////////////////////////////////////////////////////////////////////
407 409
408 GrDrawState::AutoVertexAttribRestore::AutoVertexAttribRestore(GrDrawState* drawS tate) { 410 GrDrawState::AutoVertexAttribRestore::AutoVertexAttribRestore(GrDrawState* drawS tate) {
409 SkASSERT(drawState); 411 SkASSERT(drawState);
410 fDrawState = drawState; 412 fDrawState = drawState;
411 fVAPtr = drawState->fVAPtr; 413 fVAPtr = drawState->fVAPtr;
412 fVACount = drawState->fVACount; 414 fVACount = drawState->fVACount;
413 fVAStride = drawState->fVAStride; 415 fVAStride = drawState->fVAStride;
414 fDrawState->setDefaultVertexAttribs(); 416 fDrawState->setDefaultVertexAttribs();
415 } 417 }
416 418
417 //////////////////////////////////////////////////////////////////////////////s 419 //////////////////////////////////////////////////////////////////////////////s
418 420
419 bool GrDrawState::willEffectReadDstColor() const { 421 bool GrDrawState::willEffectReadDstColor() const {
420 if (!this->isColorWriteDisabled()) { 422 if (!this->isColorWriteDisabled()) {
421 for (int s = 0; s < this->numColorStages(); ++s) { 423 this->calcColorInvariantOutput();
422 if (this->getColorStage(s).getProcessor()->willReadDstColor()) { 424 if (fColorProcInfo.readsDst()) {
423 return true;
424 }
425 }
426 }
427 for (int s = 0; s < this->numCoverageStages(); ++s) {
428 if (this->getCoverageStage(s).getProcessor()->willReadDstColor()) {
429 return true; 425 return true;
430 } 426 }
431 } 427 }
432 return false; 428 this->calcCoverageInvariantOutput();
429 return fCoverageProcInfo.readsDst();
433 } 430 }
434 431
435 void GrDrawState::AutoRestoreEffects::set(GrDrawState* ds) { 432 void GrDrawState::AutoRestoreEffects::set(GrDrawState* ds) {
436 if (fDrawState) { 433 if (fDrawState) {
437 // See the big comment on the class definition about GPs. 434 // See the big comment on the class definition about GPs.
438 if (SK_InvalidUniqueID == fOriginalGPID) { 435 if (SK_InvalidUniqueID == fOriginalGPID) {
439 fDrawState->fGeometryProcessor.reset(NULL); 436 fDrawState->fGeometryProcessor.reset(NULL);
440 } else { 437 } else {
441 SkASSERT(fDrawState->getGeometryProcessor()->getUniqueID() == 438 SkASSERT(fDrawState->getGeometryProcessor()->getUniqueID() ==
442 fOriginalGPID); 439 fOriginalGPID);
443 fOriginalGPID = SK_InvalidUniqueID; 440 fOriginalGPID = SK_InvalidUniqueID;
444 } 441 }
445 442
446 int m = fDrawState->numColorStages() - fColorEffectCnt; 443 int m = fDrawState->numColorStages() - fColorEffectCnt;
447 SkASSERT(m >= 0); 444 SkASSERT(m >= 0);
448 fDrawState->fColorStages.pop_back_n(m); 445 fDrawState->fColorStages.pop_back_n(m);
449 446
450 int n = fDrawState->numCoverageStages() - fCoverageEffectCnt; 447 int n = fDrawState->numCoverageStages() - fCoverageEffectCnt;
451 SkASSERT(n >= 0); 448 SkASSERT(n >= 0);
452 fDrawState->fCoverageStages.pop_back_n(n); 449 fDrawState->fCoverageStages.pop_back_n(n);
450 if (m + n > 0) {
451 fDrawState->fColorProcInfoValid = false;
452 fDrawState->fCoverageProcInfoValid = false;
453 }
453 SkDEBUGCODE(--fDrawState->fBlockEffectRemovalCnt;) 454 SkDEBUGCODE(--fDrawState->fBlockEffectRemovalCnt;)
454 } 455 }
455 fDrawState = ds; 456 fDrawState = ds;
456 if (NULL != ds) { 457 if (NULL != ds) {
457 SkASSERT(SK_InvalidUniqueID == fOriginalGPID); 458 SkASSERT(SK_InvalidUniqueID == fOriginalGPID);
458 if (NULL != ds->getGeometryProcessor()) { 459 if (NULL != ds->getGeometryProcessor()) {
459 fOriginalGPID = ds->getGeometryProcessor()->getUniqueID(); 460 fOriginalGPID = ds->getGeometryProcessor()->getUniqueID();
460 } 461 }
461 fColorEffectCnt = ds->numColorStages(); 462 fColorEffectCnt = ds->numColorStages();
462 fCoverageEffectCnt = ds->numCoverageStages(); 463 fCoverageEffectCnt = ds->numCoverageStages();
(...skipping 218 matching lines...) Expand 10 before | Expand all | Expand 10 after
681 // the dst coeff is effectively one so blend works out to: 682 // the dst coeff is effectively one so blend works out to:
682 // cS + (c)(1)D + (1-c)D = cS + D. 683 // cS + (c)(1)D + (1-c)D = cS + D.
683 *dstCoeff = kOne_GrBlendCoeff; 684 *dstCoeff = kOne_GrBlendCoeff;
684 return kCoverageAsAlpha_BlendOptFlag; 685 return kCoverageAsAlpha_BlendOptFlag;
685 } 686 }
686 } 687 }
687 688
688 return kNone_BlendOpt; 689 return kNone_BlendOpt;
689 } 690 }
690 691
691
692 bool GrDrawState::srcAlphaWillBeOne() const { 692 bool GrDrawState::srcAlphaWillBeOne() const {
693 GrColor color; 693 this->calcColorInvariantOutput();
694 GrColorComponentFlags flags; 694 if (this->isCoverageDrawing()) {
695 // Check if per-vertex or constant color may have partial alpha 695 this->calcCoverageInvariantOutput();
696 if (this->hasColorVertexAttribute()) { 696 return (fColorProcInfo.isOpaque() && fCoverageProcInfo.isOpaque());
697 if (fHints & kVertexColorsAreOpaque_Hint) {
698 flags = kA_GrColorComponentFlag;
699 color = 0xFF << GrColor_SHIFT_A;
700 } else {
701 flags = static_cast<GrColorComponentFlags>(0);
702 color = 0;
703 }
704 } else {
705 flags = kRGBA_GrColorComponentFlags;
706 color = this->getColor();
707 } 697 }
708 GrInvariantOutput inoutColor(color, flags, false); 698 return fColorProcInfo.isOpaque();
709
710 // Run through the color stages
711 for (int s = 0; s < this->numColorStages(); ++s) {
712 const GrProcessor* processor = this->getColorStage(s).getProcessor();
713 processor->computeInvariantOutput(&inoutColor);
714 }
715
716 // Check whether coverage is treated as color. If so we run through the cove rage computation.
717 if (this->isCoverageDrawing()) {
718 // The shader generated for coverage drawing runs the full coverage comp utation and then
719 // makes the shader output be the multiplication of color and coverage. We mirror that here.
720 if (this->hasCoverageVertexAttribute()) {
721 flags = static_cast<GrColorComponentFlags>(0);
722 color = 0;
723 } else {
724 flags = kRGBA_GrColorComponentFlags;
725 color = this->getCoverageColor();
726 }
727 GrInvariantOutput inoutCoverage(color, flags, true);
728
729 if (this->hasGeometryProcessor()) {
730 fGeometryProcessor->computeInvariantOutput(&inoutCoverage);
731 }
732
733 // Run through the coverage stages
734 for (int s = 0; s < this->numCoverageStages(); ++s) {
735 const GrProcessor* processor = this->getCoverageStage(s).getProcesso r();
736 processor->computeInvariantOutput(&inoutCoverage);
737 }
738
739 // Since the shader will multiply coverage and color, the only way the f inal A==1 is if
740 // coverage and color both have A==1.
741 return (inoutColor.isOpaque() && inoutCoverage.isOpaque());
742 }
743
744 return inoutColor.isOpaque();
745 } 699 }
746 700
747 bool GrDrawState::willBlendWithDst() const { 701 bool GrDrawState::willBlendWithDst() const {
748 if (!this->hasSolidCoverage()) { 702 if (!this->hasSolidCoverage()) {
749 return true; 703 return true;
750 } 704 }
751 705
752 bool srcAIsOne = this->srcAlphaWillBeOne(); 706 bool srcAIsOne = this->srcAlphaWillBeOne();
753 GrBlendCoeff srcCoeff = this->getSrcBlendCoeff(); 707 GrBlendCoeff srcCoeff = this->getSrcBlendCoeff();
754 GrBlendCoeff dstCoeff = this->getDstBlendCoeff(); 708 GrBlendCoeff dstCoeff = this->getDstBlendCoeff();
755 if (kISA_GrBlendCoeff == dstCoeff && srcAIsOne) { 709 if (kISA_GrBlendCoeff == dstCoeff && srcAIsOne) {
756 dstCoeff = kZero_GrBlendCoeff; 710 dstCoeff = kZero_GrBlendCoeff;
757 } 711 }
758 if (kOne_GrBlendCoeff != srcCoeff || 712 if (kOne_GrBlendCoeff != srcCoeff ||
759 kZero_GrBlendCoeff != dstCoeff || 713 kZero_GrBlendCoeff != dstCoeff ||
760 this->willEffectReadDstColor()) { 714 this->willEffectReadDstColor()) {
761 return true; 715 return true;
762 } 716 }
763 717
764 return false; 718 return false;
765 } 719 }
766 720
721 void GrDrawState::calcColorInvariantOutput() const {
722 if (!fColorProcInfoValid) {
723 GrColor color;
724 GrColorComponentFlags flags;
725 if (this->hasColorVertexAttribute()) {
726 if (fHints & kVertexColorsAreOpaque_Hint) {
727 flags = kA_GrColorComponentFlag;
728 color = 0xFF << GrColor_SHIFT_A;
729 } else {
730 flags = static_cast<GrColorComponentFlags>(0);
731 color = 0;
732 }
733 } else {
734 flags = kRGBA_GrColorComponentFlags;
735 color = this->getColor();
736 }
737 fColorProcInfo.calcWithInitialValues(fColorStages.begin(), this->numColo rStages(),
738 color, flags, false);
739 fColorProcInfoValid = true;
740 }
741 }
742
743 void GrDrawState::calcCoverageInvariantOutput() const {
744 if (!fCoverageProcInfoValid) {
745 GrColor color;
746 GrColorComponentFlags flags;
747 // Check if per-vertex or constant color may have partial alpha
748 if (this->hasCoverageVertexAttribute()) {
749 flags = static_cast<GrColorComponentFlags>(0);
750 color = 0;
751 } else {
752 flags = kRGBA_GrColorComponentFlags;
753 color = this->getCoverageColor();
754 }
755 fCoverageProcInfo.calcWithInitialValues(fCoverageStages.begin(), this->n umCoverageStages(),
756 color, flags, true, fGeometryPro cessor.get());
757 fCoverageProcInfoValid = true;
758 }
759 }
760
OLDNEW
« no previous file with comments | « src/gpu/GrDrawState.h ('k') | src/gpu/GrInvariantOutput.cpp » ('j') | src/gpu/GrProcOptInfo.h » ('J')

Powered by Google App Engine
This is Rietveld 408576698