From af1103040ead0fb374b4a114f0159d4dd3461a2a Mon Sep 17 00:00:00 2001 From: John Stiles Date: Mon, 20 Jul 2020 09:45:51 -0400 Subject: [PATCH] Factor out random fragment-processor generation into a helper class. The helper class allows us to regenerate a "random" fragment processor with the same seed as many times as desired. This lets us check the `compatibleWithCoverageAsAlpha` optimization without needing to clone the input FP; instead, we can actually generate the FP under test three times in a row, with the same random seed. In a followup CL (http://review.skia.org/303479/) we will also leverage this helper class to regenerate the FP under test with the same random seed, but with a different input. Change-Id: I1cd83082a949d555f7898970c8a1cc3002818286 Bug: skia:10384 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/303657 Reviewed-by: Brian Salomon Commit-Queue: John Stiles Auto-Submit: John Stiles --- tests/ProcessorTest.cpp | 277 +++++++++++++++++++++++----------------- 1 file changed, 162 insertions(+), 115 deletions(-) diff --git a/tests/ProcessorTest.cpp b/tests/ProcessorTest.cpp index 4142e562e6..fff6e08a89 100644 --- a/tests/ProcessorTest.cpp +++ b/tests/ProcessorTest.cpp @@ -257,62 +257,112 @@ void render_fp(GrContext* context, buffer, 0, {0, 0}); } -/** Initializes the two test texture proxies that are available to the FP test factories. */ -bool init_test_textures(GrResourceProvider* resourceProvider, - GrRecordingContext* context, - SkRandom* random, - GrProcessorTestData::ViewInfo views[2]) { - static const int kTestTextureSize = 256; +// This class is responsible for reproducibly generating a random fragment processor. +// An identical randomly-designed FP can be generated as many times as needed. +class TestFPGenerator { + public: + TestFPGenerator() = delete; + TestFPGenerator(GrDirectContext* context, GrResourceProvider* resourceProvider) + : fContext(context) + , fResourceProvider(resourceProvider) + , fInitialSeed(synthesizeInitialSeed()) + , fRandomSeed(fInitialSeed) {} - { - // Put premul data into the RGBA texture that the test FPs can optionally use. - GrColor* rgbaData = new GrColor[kTestTextureSize * kTestTextureSize]; - for (int y = 0; y < kTestTextureSize; ++y) { - for (int x = 0; x < kTestTextureSize; ++x) { - rgbaData[kTestTextureSize * y + x] = input_texel_color( - random->nextULessThan(256), random->nextULessThan(256), 0.0f); + uint32_t initialSeed() { return fInitialSeed; } + + bool init() { + // Initializes the two test texture proxies that are available to the FP test factories. + SkRandom random{fRandomSeed}; + static constexpr int kTestTextureSize = 256; + + { + // Put premul data into the RGBA texture that the test FPs can optionally use. + GrColor* rgbaData = new GrColor[kTestTextureSize * kTestTextureSize]; + for (int y = 0; y < kTestTextureSize; ++y) { + for (int x = 0; x < kTestTextureSize; ++x) { + rgbaData[kTestTextureSize * y + x] = input_texel_color( + random.nextULessThan(256), random.nextULessThan(256), 0.0f); + } + } + + SkImageInfo ii = SkImageInfo::Make(kTestTextureSize, kTestTextureSize, + kRGBA_8888_SkColorType, kPremul_SkAlphaType); + SkBitmap bitmap; + bitmap.installPixels( + ii, rgbaData, ii.minRowBytes(), + [](void* addr, void* context) { delete[](GrColor*) addr; }, nullptr); + bitmap.setImmutable(); + GrBitmapTextureMaker maker(fContext, bitmap, + GrImageTexGenPolicy::kNew_Uncached_Budgeted); + GrSurfaceProxyView view = maker.view(GrMipMapped::kNo); + if (!view.proxy() || !view.proxy()->instantiate(fResourceProvider)) { + SkDebugf("Unable to instantiate RGBA8888 test texture."); + return false; + } + fTestViews[0] = GrProcessorTestData::ViewInfo{view, GrColorType::kRGBA_8888, + kPremul_SkAlphaType}; + } + + { + // Put random values into the alpha texture that the test FPs can optionally use. + uint8_t* alphaData = new uint8_t[kTestTextureSize * kTestTextureSize]; + for (int y = 0; y < kTestTextureSize; ++y) { + for (int x = 0; x < kTestTextureSize; ++x) { + alphaData[kTestTextureSize * y + x] = random.nextULessThan(256); + } + } + + SkImageInfo ii = SkImageInfo::Make(kTestTextureSize, kTestTextureSize, + kAlpha_8_SkColorType, kPremul_SkAlphaType); + SkBitmap bitmap; + bitmap.installPixels( + ii, alphaData, ii.minRowBytes(), + [](void* addr, void* context) { delete[](uint8_t*) addr; }, nullptr); + bitmap.setImmutable(); + GrBitmapTextureMaker maker(fContext, bitmap, + GrImageTexGenPolicy::kNew_Uncached_Budgeted); + GrSurfaceProxyView view = maker.view(GrMipMapped::kNo); + if (!view.proxy() || !view.proxy()->instantiate(fResourceProvider)) { + SkDebugf("Unable to instantiate A8 test texture."); + return false; + } + fTestViews[1] = GrProcessorTestData::ViewInfo{view, GrColorType::kAlpha_8, + kPremul_SkAlphaType}; + } + + return true; + } + + void reroll() { + // Feed our current random seed into SkRandom to generate a new seed. + SkRandom random{fRandomSeed}; + fRandomSeed = random.nextU(); + } + + std::unique_ptr make(int type) { + // This will generate the exact same randomized FP (of each requested type) each time + // it's called. Call `reroll` to get a different FP. + SkRandom random{fRandomSeed}; + GrProcessorTestData testData{&random, fContext, SK_ARRAY_COUNT(fTestViews), fTestViews}; + return GrFragmentProcessorTestFactory::MakeIdx(type, &testData); + } + + private: + static uint32_t synthesizeInitialSeed() { + if (FLAGS_randomProcessorTest) { + std::random_device rd; + return rd(); + } else { + return FLAGS_processorSeed; } } - SkImageInfo ii = SkImageInfo::Make(kTestTextureSize, kTestTextureSize, - kRGBA_8888_SkColorType, kPremul_SkAlphaType); - SkBitmap bitmap; - bitmap.installPixels(ii, rgbaData, ii.minRowBytes(), - [](void* addr, void* context) { delete[] (GrColor*)addr; }, nullptr); - bitmap.setImmutable(); - GrBitmapTextureMaker maker(context, bitmap, GrImageTexGenPolicy::kNew_Uncached_Budgeted); - auto view = maker.view(GrMipMapped::kNo); - if (!view.proxy() || !view.proxy()->instantiate(resourceProvider)) { - return false; - } - views[0] = {view, GrColorType::kRGBA_8888, kPremul_SkAlphaType}; - } - - { - // Put random values into the alpha texture that the test FPs can optionally use. - uint8_t* alphaData = new uint8_t[kTestTextureSize * kTestTextureSize]; - for (int y = 0; y < kTestTextureSize; ++y) { - for (int x = 0; x < kTestTextureSize; ++x) { - alphaData[kTestTextureSize * y + x] = random->nextULessThan(256); - } - } - - SkImageInfo ii = SkImageInfo::Make(kTestTextureSize, kTestTextureSize, - kAlpha_8_SkColorType, kPremul_SkAlphaType); - SkBitmap bitmap; - bitmap.installPixels(ii, alphaData, ii.minRowBytes(), - [](void* addr, void* context) { delete[] (uint8_t*)addr; }, nullptr); - bitmap.setImmutable(); - GrBitmapTextureMaker maker(context, bitmap, GrImageTexGenPolicy::kNew_Uncached_Budgeted); - auto view = maker.view(GrMipMapped::kNo); - if (!view.proxy() || !view.proxy()->instantiate(resourceProvider)) { - return false; - } - views[1] = {view, GrColorType::kAlpha_8, kPremul_SkAlphaType}; - } - - return true; -} + GrDirectContext* fContext; // owned by caller + GrResourceProvider* fResourceProvider; // owned by caller + const uint32_t fInitialSeed; + uint32_t fRandomSeed; + GrProcessorTestData::ViewInfo fTestViews[2]; +}; // Creates a texture of premul colors used as the output of the fragment processor that precedes // the fragment processor under test. Color values are those provided by input_texel_color(). @@ -334,7 +384,7 @@ GrSurfaceProxyView make_input_texture(GrRecordingContext* context, int width, in return maker.view(GrMipMapped::kNo); } -// We tag logged data as unpremul to avoid conversion when encoding as PNG. The input texture +// We tag logged data as unpremul to avoid conversion when encoding as PNG. The input texture // actually contains unpremul data. Also, even though we made the result data by rendering into // a "unpremul" GrRenderTargetContext, our input texture is unpremul and outside of the random // effect configuration, we didn't do anything to ensure the output is actually premul. We just @@ -342,7 +392,8 @@ GrSurfaceProxyView make_input_texture(GrRecordingContext* context, int width, in static constexpr auto kLogAlphaType = kUnpremul_SkAlphaType; bool log_pixels(GrColor* pixels, int widthHeight, SkString* dst) { - auto info = SkImageInfo::Make(widthHeight, widthHeight, kRGBA_8888_SkColorType, kLogAlphaType); + SkImageInfo info = + SkImageInfo::Make(widthHeight, widthHeight, kRGBA_8888_SkColorType, kLogAlphaType); SkBitmap bmp; bmp.installPixels(info, pixels, widthHeight * sizeof(GrColor)); return BipmapToBase64DataURI(bmp, dst); @@ -476,18 +527,15 @@ bool legal_modulation(const GrColor in[3], const GrColor out[3]) { } DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, reporter, ctxInfo) { - auto context = ctxInfo.directContext(); - auto resourceProvider = context->priv().resourceProvider(); + GrDirectContext* context = ctxInfo.directContext(); + GrResourceProvider* resourceProvider = context->priv().resourceProvider(); using FPFactory = GrFragmentProcessorTestFactory; - uint32_t seed = FLAGS_processorSeed; - if (FLAGS_randomProcessorTest) { - std::random_device rd; - seed = rd(); + TestFPGenerator fpGenerator{context, resourceProvider}; + if (!fpGenerator.init()) { + ERRORF(reporter, "Could not initialize TestFPGenerator"); + return; } - // If a non-deterministic bot fails this test, check the output to see what seed it used, then - // use --processorSeed (without --randomProcessorTest) to reproduce. - SkRandom random(seed); // Make the destination context for the test. static constexpr int kRenderSize = 256; @@ -495,13 +543,6 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, repor context, GrColorType::kRGBA_8888, nullptr, SkBackingFit::kExact, {kRenderSize, kRenderSize}); - GrProcessorTestData::ViewInfo views[2]; - if (!init_test_textures(resourceProvider, context, &random, views)) { - ERRORF(reporter, "Could not create test textures"); - return; - } - GrProcessorTestData testData(&random, context, 2, views); - // Coverage optimization uses three frames with a linearly transformed input texture. The first // frame has no offset, second frames add .2 and .4, which should then be present as a fixed // difference between the frame outputs if the FP is properly following the modulation @@ -516,49 +557,55 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, repor bool loggedFirstFailure = false; bool loggedFirstWarning = false; - // Storage for the three frames required for coverage compatibility optimization. Each frame - // uses the correspondingly numbered inputTextureX. - std::unique_ptr readData1(new GrColor[kRenderSize * kRenderSize]); - std::unique_ptr readData2(new GrColor[kRenderSize * kRenderSize]); - std::unique_ptr readData3(new GrColor[kRenderSize * kRenderSize]); + // Storage for the three frames required for coverage compatibility optimization testing. + // Each frame uses the correspondingly numbered inputTextureX. + std::vector readData1(kRenderSize * kRenderSize); + std::vector readData2(kRenderSize * kRenderSize); + std::vector readData3(kRenderSize * kRenderSize); // Because processor factories configure themselves in random ways, this is not exhaustive. for (int i = 0; i < FPFactory::Count(); ++i) { - int timesToInvokeFactory = 5; - // Increase the number of attempts if the FP has child FPs since optimizations likely depend + // Create a temp FP of this type just to see the number of children that it uses. Then + // increase the number of attempts if the FP has child FPs, since optimizations will depend // on child optimizations being present. - std::unique_ptr fp = FPFactory::MakeIdx(i, &testData); + std::unique_ptr fp = fpGenerator.make(i); + + int timesToInvokeFactory = 5; for (int j = 0; j < fp->numChildProcessors(); ++j) { // This value made a reasonable trade off between time and coverage when this test was // written. timesToInvokeFactory *= FPFactory::Count() / 2; } + #if defined(__MSVC_RUNTIME_CHECKS) // This test is infuriatingly slow with MSVC runtime checks enabled timesToInvokeFactory = 1; #endif - for (int j = 0; j < timesToInvokeFactory; ++j) { - fp = FPFactory::MakeIdx(i, &testData); + for (int j = 0; j < timesToInvokeFactory; ++j) { + // Create a randomly-configured FP. + fpGenerator.reroll(); + fp = fpGenerator.make(i); + + // Skip further testing if it has no optimization bits enabled. if (!fp->hasConstantOutputForConstantInput() && !fp->preservesOpaqueInput() && !fp->compatibleWithCoverageAsAlpha()) { continue; } - // All draws use a clone so that we can continue to query fp. ProcessorCloneTest should - // validate that clones are equivalent to the original. + // We can make identical copies of the test FP in order to test coverage-as-alpha. if (fp->compatibleWithCoverageAsAlpha()) { // 2nd and 3rd frames are only used when checking coverage optimization - render_fp(context, rtc.get(), fp->clone(), inputTexture2, kPremul_SkAlphaType, - readData2.get()); - render_fp(context, rtc.get(), fp->clone(), inputTexture3, kPremul_SkAlphaType, - readData3.get()); + render_fp(context, rtc.get(), fpGenerator.make(i), inputTexture2, + kPremul_SkAlphaType, readData2.data()); + render_fp(context, rtc.get(), fpGenerator.make(i), inputTexture3, + kPremul_SkAlphaType, readData3.data()); } // Draw base frame last so that rtc holds the original FP behavior if we need to // dump the image to the log. render_fp(context, rtc.get(), fp->clone(), inputTexture1, kPremul_SkAlphaType, - readData1.get()); + readData1.data()); // This test has a history of being flaky on a number of devices. If an FP is logically // violating the optimizations, it's reasonable to expect it to violate requirements on @@ -570,9 +617,9 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, repor static constexpr int kMaxAcceptableFailedPixels = 2 * kRenderSize; // ~0.7% of the image #endif - int failedPixelCount = 0; // Collect first optimization failure message, to be output later as a warning or an // error depending on whether the rendering "passed" or failed. + int failedPixelCount = 0; SkString coverageMessage; SkString opaqueMessage; SkString constMessage; @@ -580,7 +627,7 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, repor for (int x = 0; x < kRenderSize; ++x) { bool passing = true; GrColor input = input_texel_color(x, y, 0.0f); - GrColor output = readData1.get()[y * kRenderSize + x]; + GrColor output = readData1[y * kRenderSize + x]; if (fp->compatibleWithCoverageAsAlpha()) { GrColor ins[3]; @@ -590,8 +637,8 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, repor GrColor outs[3]; outs[0] = output; - outs[1] = readData2.get()[y * kRenderSize + x]; - outs[2] = readData3.get()[y * kRenderSize + x]; + outs[1] = readData2[y * kRenderSize + x]; + outs[2] = readData3[y * kRenderSize + x]; if (!legal_modulation(ins, outs)) { passing = false; @@ -652,7 +699,7 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, repor if (failedPixelCount > kMaxAcceptableFailedPixels) { ERRORF(reporter, "Processor violated %d of %d pixels, seed: 0x%08x, processor: %s" ", first failing pixel details are below:", - failedPixelCount, kRenderSize * kRenderSize, seed, + failedPixelCount, kRenderSize * kRenderSize, fpGenerator.initialSeed(), fp->dumpInfo().c_str()); // Print first failing pixel's details. @@ -671,17 +718,17 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, repor SkString input; log_texture_view(context, inputTexture1, &input); SkString output; - log_pixels(readData1.get(), kRenderSize, &output); + log_pixels(readData1.data(), kRenderSize, &output); ERRORF(reporter, "Input image: %s\n\n" "===========================================================\n\n" "Output image: %s\n", input.c_str(), output.c_str()); loggedFirstFailure = true; } - } else if(failedPixelCount > 0) { + } else if (failedPixelCount > 0) { // Don't trigger an error, but don't just hide the failures either. INFOF(reporter, "Processor violated %d of %d pixels (below error threshold), seed: " "0x%08x, processor: %s", failedPixelCount, kRenderSize * kRenderSize, - seed, fp->dumpInfo().c_str()); + fpGenerator.initialSeed(), fp->dumpInfo().c_str()); if (!coverageMessage.isEmpty()) { INFOF(reporter, coverageMessage.c_str()); } @@ -695,7 +742,7 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorOptimizationValidationTest, repor SkString input; log_texture_view(context, inputTexture1, &input); SkString output; - log_pixels(readData1.get(), kRenderSize, &output); + log_pixels(readData1.data(), kRenderSize, &output); INFOF(reporter, "Input image: %s\n\n" "===========================================================\n\n" "Output image: %s\n", input.c_str(), output.c_str()); @@ -728,10 +775,14 @@ static SkString describe_fp(const GrFragmentProcessor& fp) { // Tests that a fragment processor returned by GrFragmentProcessor::clone() is equivalent to its // progenitor. DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorCloneTest, reporter, ctxInfo) { - auto context = ctxInfo.directContext(); - auto resourceProvider = context->priv().resourceProvider(); + GrDirectContext* context = ctxInfo.directContext(); + GrResourceProvider* resourceProvider = context->priv().resourceProvider(); - SkRandom random; + TestFPGenerator fpGenerator{context, resourceProvider}; + if (!fpGenerator.init()) { + ERRORF(reporter, "Could not initialize TestFPGenerator"); + return; + } // Make the destination context for the test. static constexpr int kRenderSize = 1024; @@ -739,22 +790,18 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorCloneTest, reporter, ctxInfo) { context, GrColorType::kRGBA_8888, nullptr, SkBackingFit::kExact, {kRenderSize, kRenderSize}); - GrProcessorTestData::ViewInfo views[2]; - if (!init_test_textures(resourceProvider, context, &random, views)) { - ERRORF(reporter, "Could not create test textures"); - return; - } - GrProcessorTestData testData(&random, context, 2, views); - auto inputTexture = make_input_texture(context, kRenderSize, kRenderSize, 0.0f); - std::unique_ptr readData1(new GrColor[kRenderSize * kRenderSize]); - std::unique_ptr readData2(new GrColor[kRenderSize * kRenderSize]); + // On failure we write out images, but just write the first failing set as the print is very // large. bool loggedFirstFailure = false; + // Storage for the original frame's readback and the readback of its clone. + std::vector readData1(kRenderSize * kRenderSize); + std::vector readData2(kRenderSize * kRenderSize); + // This test has a history of being flaky on a number of devices. If an FP clone is logically - // wrong, it's reasonable to expect it produce a large number of pixel differences in the image + // wrong, it's reasonable to expect it produce a large number of pixel differences in the image. // Sporadic pixel violations are more indicative device errors and represents a separate // problem. #if defined(SK_BUILD_FOR_SKQP) @@ -767,8 +814,8 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorCloneTest, reporter, ctxInfo) { for (int i = 0; i < GrFragmentProcessorTestFactory::Count(); ++i) { static constexpr int kTimesToInvokeFactory = 10; for (int j = 0; j < kTimesToInvokeFactory; ++j) { - std::unique_ptr fp = - GrFragmentProcessorTestFactory::MakeIdx(i, &testData); + fpGenerator.reroll(); + std::unique_ptr fp = fpGenerator.make(i); std::unique_ptr clone = fp->clone(); if (!clone) { ERRORF(reporter, "Clone of processor %s failed.", fp->name()); @@ -796,11 +843,11 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorCloneTest, reporter, ctxInfo) { "%s\n", describe_fp(*fp).c_str()); // Draw with original and read back the results. render_fp(context, rtc.get(), std::move(fp), inputTexture, kPremul_SkAlphaType, - readData1.get()); + readData1.data()); // Draw with clone and read back the results. render_fp(context, rtc.get(), std::move(clone), inputTexture, kPremul_SkAlphaType, - readData2.get()); + readData2.data()); // Check that the results are the same. bool passing = true; @@ -838,8 +885,8 @@ DEF_GPUTEST_FOR_GL_RENDERING_CONTEXTS(ProcessorCloneTest, reporter, ctxInfo) { kUnpremul_SkAlphaType); SkString inputURL, origURL, cloneURL; if (log_texture_view(context, inputTexture, &inputURL) && - log_pixels(readData1.get(), kRenderSize, &origURL) && - log_pixels(readData2.get(), kRenderSize, &cloneURL)) { + log_pixels(readData1.data(), kRenderSize, &origURL) && + log_pixels(readData2.data(), kRenderSize, &cloneURL)) { ERRORF(reporter, "\nInput image:\n%s\n\n" "==========================================================="