| /* |
| * Copyright 2022 Google LLC |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "tests/Test.h" |
| |
| #ifdef SK_GRAPHITE_ENABLED |
| |
| #include "include/core/SkImage.h" |
| #include "include/gpu/GpuTypes.h" |
| #include "include/gpu/graphite/BackendTexture.h" |
| #include "include/gpu/graphite/Context.h" |
| #include "include/gpu/graphite/Recorder.h" |
| #include "include/gpu/graphite/Recording.h" |
| #include "src/core/SkAutoPixmapStorage.h" |
| #include "src/gpu/graphite/Caps.h" |
| #include "src/gpu/graphite/ContextPriv.h" |
| #include "src/gpu/graphite/Surface_Graphite.h" |
| #include "src/gpu/graphite/Texture.h" |
| #include "src/gpu/graphite/TextureProxy.h" |
| #include "tests/TestUtils.h" |
| #include "tools/ToolUtils.h" |
| |
| using namespace skgpu::graphite; |
| |
| namespace { |
| |
| // We draw the larger image into the smaller surface to force mipmapping |
| const SkISize kImageSize = { 32, 32 }; |
| SkDEBUGCODE(constexpr int kNumMipLevels = 6;) |
| const SkISize kSurfaceSize = { 16, 16 }; |
| |
| constexpr int kNumMutations = 2; |
| constexpr SkColor4f kInitialColor = SkColors::kRed; |
| constexpr SkColor4f kMutationColors[kNumMutations] = { |
| SkColors::kGreen, |
| SkColors::kBlue |
| }; |
| |
| /* |
| * We have 3 use cases. In each case there is a mutating task which changes the contents of an |
| * image (somehow) and a shared redraw task which just creates a single Recording which draws the |
| * image that is being mutated. The mutator's image must start off being 'kInitialColor' and |
| * then cycle through 'kMutationColors'. The mutation tasks are: |
| |
| * 1) (AHBs) The client has wrapped a backend texture in an image and is changing the backend |
| * texture's contents. |
| * 2) (Volatile Promise Images) The client has a pool of backend textures and updates both the |
| * contents of the backend textures and which one backs the image every frame |
| * 3) (Surface/Image pair) The client has a surface and has snapped an image w/o a copy but |
| * keeps drawing to the surface |
| * |
| * There are also two scenarios for the mutation and redrawing tasks: |
| * a) Both use the same recorder |
| * b) They use separate recorders |
| * The latter, obviously, requires more synchronization. |
| */ |
| |
| // Base class for the 3 mutation methods. |
| // init - should create the SkImage that is going to be changing |
| // mutate - should change the contents of the SkImage |
| class Mutator { |
| public: |
| Mutator(skiatest::Reporter* reporter, Recorder* recorder, bool withMips) |
| : fReporter(reporter) |
| , fRecorder(recorder) |
| , fWithMips(withMips) { |
| } |
| virtual ~Mutator() = default; |
| |
| virtual std::unique_ptr<Recording> init(const Caps*) = 0; |
| virtual std::unique_ptr<Recording> mutate(int mutationIndex) = 0; |
| virtual int getCase() const = 0; |
| |
| SkImage* getMutatingImage() { |
| return fMutatingImg.get(); |
| } |
| |
| protected: |
| skiatest::Reporter* fReporter; |
| Recorder* fRecorder; |
| bool fWithMips; |
| |
| sk_sp<SkImage> fMutatingImg; // needs to be created in the 'init' method |
| }; |
| |
| // This class puts the 3 mutation use cases through their paces. |
| // init - creates the single Recording that draws the mutator's image |
| // checkResult - verifies that replaying the Recording results in the expected/mutated color |
| class Redrawer { |
| public: |
| Redrawer(skiatest::Reporter* reporter, Recorder* recorder) |
| : fReporter(reporter) |
| , fRecorder(recorder) { |
| SkImageInfo ii = SkImageInfo::Make(kSurfaceSize, |
| kRGBA_8888_SkColorType, |
| kPremul_SkAlphaType); |
| fReadbackPM.alloc(ii); |
| } |
| |
| void init(SkImage* imageToDraw) { |
| SkImageInfo ii = SkImageInfo::Make(kSurfaceSize, |
| kRGBA_8888_SkColorType, |
| kPremul_SkAlphaType); |
| fImgDrawSurface = SkSurface::MakeGraphite(fRecorder, ii, Mipmapped::kNo); |
| REPORTER_ASSERT(fReporter, fImgDrawSurface); |
| |
| fImgDrawRecording = MakeRedrawRecording(fRecorder, fImgDrawSurface.get(), imageToDraw); |
| } |
| |
| Recording* imgDrawRecording() { |
| return fImgDrawRecording.get(); |
| } |
| |
| // This is here bc it uses a lot from the Redrawer (i.e., its recorder, its surface, etc.). |
| void checkResult(Context* context, |
| int testcaseID, |
| bool useTwoRecorders, |
| bool withMips, |
| const SkColor4f& expectedColor) { |
| |
| fReadbackPM.erase(SkColors::kTransparent); |
| |
| if (!fImgDrawSurface->readPixels(fReadbackPM, 0, 0)) { |
| ERRORF(fReporter, "readPixels failed"); |
| } |
| |
| auto error = std::function<ComparePixmapsErrorReporter>( |
| [&](int x, int y, const float diffs[4]) { |
| ERRORF(fReporter, |
| "case %d%c - %s: " |
| "expected (%.1f %.1f %.1f %.1f) " |
| "- diffs (%.1f, %.1f, %.1f, %.1f)", |
| testcaseID, useTwoRecorders ? 'b' : 'a', |
| withMips ? "mipmapped" : "not-mipmapped", |
| expectedColor.fR, expectedColor.fG, expectedColor.fB, expectedColor.fA, |
| diffs[0], diffs[1], diffs[2], diffs[3]); |
| }); |
| |
| static constexpr float kTol[] = {0, 0, 0, 0}; |
| CheckSolidPixels(expectedColor, fReadbackPM, kTol, error); |
| } |
| |
| private: |
| static std::unique_ptr<Recording> MakeRedrawRecording(Recorder* recorder, |
| SkSurface* surfaceToDrawTo, |
| SkImage* imageToDraw) { |
| SkSamplingOptions sampling = SkSamplingOptions(SkFilterMode::kLinear, |
| SkMipmapMode::kNearest); |
| |
| SkCanvas* canvas = surfaceToDrawTo->getCanvas(); |
| |
| canvas->clear(SkColors::kTransparent); |
| canvas->drawImageRect(imageToDraw, |
| SkRect::MakeWH(kSurfaceSize.width(), kSurfaceSize.height()), |
| sampling); |
| |
| return recorder->snap(); |
| } |
| |
| skiatest::Reporter* fReporter; |
| Recorder* fRecorder; |
| |
| sk_sp<SkSurface> fImgDrawSurface; |
| std::unique_ptr<Recording> fImgDrawRecording; |
| |
| SkAutoPixmapStorage fReadbackPM; |
| }; |
| |
| void update_backend_texture(skiatest::Reporter* reporter, |
| Recorder* recorder, |
| const BackendTexture& backendTex, |
| SkColorType ct, |
| bool withMips, |
| SkColor4f color) { |
| SkPixmap pixmaps[6]; |
| std::unique_ptr<char[]> memForPixmaps; |
| |
| const SkColor4f colors[6] = { color, color, color, color, color, color }; |
| |
| int numMipLevels = ToolUtils::make_pixmaps(ct, kPremul_SkAlphaType, withMips, colors, pixmaps, |
| &memForPixmaps); |
| SkASSERT(numMipLevels == 1 || numMipLevels == kNumMipLevels); |
| SkASSERT(kImageSize == pixmaps[0].dimensions()); |
| |
| REPORTER_ASSERT(reporter, recorder->updateBackendTexture(backendTex, pixmaps, numMipLevels)); |
| } |
| |
| // case 1 (AHBs) |
| // To simulate the AHB use case this Mutator creates a BackendTexture and an SkImage that wraps |
| // it. To mutate the SkImage it simply updates the BackendTexture. |
| class UpdateBackendTextureMutator : public Mutator { |
| public: |
| static std::unique_ptr<Mutator> Make(skiatest::Reporter* reporter, |
| Recorder* recorder, |
| bool withMips) { |
| return std::make_unique<UpdateBackendTextureMutator>(reporter, recorder, withMips); |
| } |
| |
| UpdateBackendTextureMutator(skiatest::Reporter* reporter, Recorder* recorder, bool withMips) |
| : Mutator(reporter, recorder, withMips) { |
| } |
| ~UpdateBackendTextureMutator() override { |
| fRecorder->deleteBackendTexture(fBETexture); |
| } |
| |
| std::unique_ptr<Recording> init(const Caps* caps) override { |
| // Note: not renderable |
| TextureInfo info = caps->getDefaultSampledTextureInfo(kRGBA_8888_SkColorType, |
| fWithMips ? Mipmapped::kYes |
| : Mipmapped::kNo, |
| skgpu::Protected::kNo, |
| Renderable::kNo); |
| REPORTER_ASSERT(fReporter, info.isValid()); |
| |
| fBETexture = fRecorder->createBackendTexture(kImageSize, info); |
| REPORTER_ASSERT(fReporter, fBETexture.isValid()); |
| |
| update_backend_texture(fReporter, fRecorder, fBETexture, kRGBA_8888_SkColorType, |
| fWithMips, kInitialColor); |
| |
| fMutatingImg = SkImage::MakeGraphiteFromBackendTexture(fRecorder, |
| fBETexture, |
| kRGBA_8888_SkColorType, |
| kPremul_SkAlphaType, |
| /* colorSpace= */ nullptr); |
| REPORTER_ASSERT(fReporter, fMutatingImg); |
| |
| return fRecorder->snap(); |
| } |
| |
| std::unique_ptr<Recording> mutate(int mutationIndex) override { |
| update_backend_texture(fReporter, fRecorder, fBETexture, kRGBA_8888_SkColorType, |
| fWithMips, kMutationColors[mutationIndex]); |
| return fRecorder->snap(); |
| } |
| |
| int getCase() const override { return 1; } |
| |
| private: |
| BackendTexture fBETexture; |
| }; |
| |
| // case 2 (Volatile Promise Images) |
| // To simulate the hardware video decoder use case this Mutator creates a set of BackendTextures |
| // and fills them w/ different colors. A single volatile Promise Image is created and is |
| // fulfilled by the different BackendTextures. |
| class VolatilePromiseImageMutator : public Mutator { |
| public: |
| static std::unique_ptr<Mutator> Make(skiatest::Reporter* reporter, |
| Recorder* recorder, |
| bool withMips) { |
| return std::make_unique<VolatilePromiseImageMutator>(reporter, recorder, withMips); |
| } |
| |
| VolatilePromiseImageMutator(skiatest::Reporter* reporter, Recorder* recorder, bool withMips) |
| : Mutator(reporter, recorder, withMips) { |
| } |
| |
| ~VolatilePromiseImageMutator() override { |
| // We need to delete the mutating image first since it holds onto the backend texture |
| // that was last used to fulfill the volatile promise image. |
| fMutatingImg.reset(); |
| |
| fCallbackTracker.finishedTest(); |
| |
| for (int i = 0; i < kNumMutations+1; ++i) { |
| fRecorder->deleteBackendTexture(fBETextures[i]); |
| } |
| } |
| |
| static std::tuple<BackendTexture, void*> fulfill(void* ctx) { |
| VolatilePromiseImageMutator* mutator = reinterpret_cast<VolatilePromiseImageMutator*>(ctx); |
| |
| int index = mutator->fCallbackTracker.onFulfillCB(); |
| |
| return { mutator->fBETextures[index], &mutator->fCallbackTracker }; |
| } |
| |
| static void imageRelease(void* ctx) { |
| VolatilePromiseImageMutator* mutator = reinterpret_cast<VolatilePromiseImageMutator*>(ctx); |
| |
| mutator->fCallbackTracker.onImageReleaseCB(); |
| } |
| |
| static void textureRelease(void* ctx) { |
| CallbackTracker* callbackTracker = reinterpret_cast<CallbackTracker*>(ctx); |
| |
| callbackTracker->onTextureReleaseCB(); |
| } |
| |
| std::unique_ptr<Recording> init(const Caps* caps) override { |
| // Note: not renderable |
| TextureInfo info = caps->getDefaultSampledTextureInfo(kRGBA_8888_SkColorType, |
| fWithMips ? Mipmapped::kYes |
| : Mipmapped::kNo, |
| skgpu::Protected::kNo, |
| Renderable::kNo); |
| REPORTER_ASSERT(fReporter, info.isValid()); |
| |
| fBETextures[0] = fRecorder->createBackendTexture(kImageSize, info); |
| REPORTER_ASSERT(fReporter, fBETextures[0].isValid()); |
| |
| update_backend_texture(fReporter, fRecorder, fBETextures[0], kRGBA_8888_SkColorType, |
| fWithMips, kInitialColor); |
| |
| for (int i = 0; i < kNumMutations; ++i) { |
| fBETextures[i+1] = fRecorder->createBackendTexture(kImageSize, info); |
| REPORTER_ASSERT(fReporter, fBETextures[i+1].isValid()); |
| |
| update_backend_texture(fReporter, fRecorder, fBETextures[i+1], kRGBA_8888_SkColorType, |
| fWithMips, kMutationColors[i]); |
| } |
| |
| fMutatingImg = SkImage::MakeGraphitePromiseTexture(fRecorder, |
| kImageSize, |
| info, |
| SkColorInfo(kRGBA_8888_SkColorType, |
| kPremul_SkAlphaType, |
| /* colorSpace= */ nullptr), |
| Volatile::kYes, |
| fulfill, |
| imageRelease, |
| textureRelease, |
| this); |
| REPORTER_ASSERT(fReporter, fMutatingImg); |
| |
| return fRecorder->snap(); |
| } |
| |
| std::unique_ptr<Recording> mutate(int mutationIndex) override { |
| fCallbackTracker.onMutation(); |
| return nullptr; |
| } |
| |
| int getCase() const override { return 2; } |
| |
| private: |
| class CallbackTracker { |
| public: |
| CallbackTracker() { |
| for (int i = 0; i < kNumMutations+1; ++i) { |
| fFulfilled[i] = false; |
| fReleased[i] = false; |
| } |
| } |
| |
| void onMutation() { |
| // In this use case, the active mutation occurs in the volatile promise image callbacks. |
| ++fMutationCount; |
| } |
| |
| int onFulfillCB() { |
| SkASSERT(fMutationCount < kNumMutations+1); |
| SkASSERT(fFulfilledCount == fMutationCount); |
| // For this unit test we should only be fulfilling with each backend texture only once |
| SkASSERT(!fFulfilled[fFulfilledCount]); |
| SkASSERT(!fReleased[fFulfilledCount]); |
| |
| fFulfilled[fFulfilledCount] = true; |
| return fFulfilledCount++; |
| } |
| |
| void onImageReleaseCB() { |
| SkASSERT(!fImageReleased); |
| fImageReleased = true; |
| } |
| |
| void onTextureReleaseCB() { |
| SkASSERT(fReleasedCount >= 0 && fReleasedCount < kNumMutations+1); |
| |
| SkASSERT(fFulfilled[fReleasedCount]); |
| SkASSERT(!fReleased[fReleasedCount]); |
| fReleased[fReleasedCount] = true; |
| fReleasedCount++; |
| } |
| |
| void finishedTest() const { |
| SkASSERT(fMutationCount == kNumMutations); |
| SkASSERT(fImageReleased); |
| |
| for (int i = 0; i < kNumMutations+1; ++i) { |
| SkASSERT(fFulfilled[i]); |
| SkASSERT(fReleased[i]); |
| } |
| } |
| |
| private: |
| int fMutationCount = 0; |
| int fFulfilledCount = 0; |
| bool fImageReleased = false; |
| int fReleasedCount = 0; |
| bool fFulfilled[kNumMutations+1]; |
| bool fReleased[kNumMutations+1]; |
| }; |
| |
| CallbackTracker fCallbackTracker; |
| |
| BackendTexture fBETextures[kNumMutations+1]; |
| }; |
| |
| // case 3 (Surface/Image pair) |
| // This mutator creates an SkSurface/SkImage pair that share the same backend object. |
| // Mutation is accomplished by simply drawing to the SkSurface. |
| class SurfaceMutator : public Mutator { |
| public: |
| static std::unique_ptr<Mutator> Make(skiatest::Reporter* reporter, |
| Recorder* recorder, |
| bool withMips) { |
| return std::make_unique<SurfaceMutator>(reporter, recorder, withMips); |
| } |
| |
| SurfaceMutator(skiatest::Reporter* reporter, Recorder* recorder, bool withMips) |
| : Mutator(reporter, recorder, withMips) { |
| } |
| |
| std::unique_ptr<Recording> init(const Caps* /* caps */) override { |
| SkImageInfo ii = SkImageInfo::Make(kImageSize, kRGBA_8888_SkColorType, kPremul_SkAlphaType); |
| |
| fMutatingSurface = SkSurface::MakeGraphite(fRecorder, ii, |
| fWithMips ? Mipmapped::kYes : Mipmapped::kNo); |
| REPORTER_ASSERT(fReporter, fMutatingSurface); |
| |
| fMutatingSurface->getCanvas()->clear(kInitialColor); |
| |
| fMutatingImg = fMutatingSurface->asImage(); |
| REPORTER_ASSERT(fReporter, fMutatingImg); |
| |
| return fRecorder->snap(); |
| } |
| |
| std::unique_ptr<Recording> mutate(int mutationIndex) override { |
| fMutatingSurface->getCanvas()->clear(kMutationColors[mutationIndex]); |
| return fRecorder->snap(); |
| } |
| |
| int getCase() const override { return 3; } |
| |
| private: |
| sk_sp<SkSurface> fMutatingSurface; |
| }; |
| |
| using MutatorFactoryT = std::unique_ptr<Mutator> (*)(skiatest::Reporter*, Recorder*, bool withMips); |
| |
| void run_test(skiatest::Reporter* reporter, |
| Context* context, |
| bool useTwoRecorders, |
| bool withMips, |
| MutatorFactoryT createMutator) { |
| const Caps* caps = context->priv().caps(); |
| |
| std::unique_ptr<Recorder> recorders[2]; |
| recorders[0] = context->makeRecorder(); |
| |
| Recorder* mutatorRecorder = recorders[0].get(); |
| Recorder* redrawerRecorder = recorders[0].get(); |
| |
| if (useTwoRecorders) { |
| recorders[1] = context->makeRecorder(); |
| redrawerRecorder = recorders[1].get(); |
| } |
| |
| std::unique_ptr<Mutator> mutator = createMutator(reporter, mutatorRecorder, withMips); |
| |
| { |
| std::unique_ptr<Recording> imgCreationRecording = mutator->init(caps); |
| REPORTER_ASSERT(reporter, context->insertRecording({ imgCreationRecording.get() })); |
| } |
| |
| { |
| Redrawer redrawer(reporter, redrawerRecorder); |
| |
| redrawer.init(mutator->getMutatingImage()); |
| |
| REPORTER_ASSERT(reporter, context->insertRecording({ redrawer.imgDrawRecording() })); |
| redrawer.checkResult(context, mutator->getCase(), |
| useTwoRecorders, withMips, kInitialColor); |
| |
| for (int i = 0; i < kNumMutations; ++i) { |
| { |
| std::unique_ptr<Recording> imgMutationRecording = mutator->mutate(i); |
| if (imgMutationRecording) { |
| REPORTER_ASSERT(reporter, |
| context->insertRecording({imgMutationRecording.get()})); |
| } |
| } |
| |
| REPORTER_ASSERT(reporter, context->insertRecording({ redrawer.imgDrawRecording() })); |
| redrawer.checkResult(context, mutator->getCase(), |
| useTwoRecorders, withMips, kMutationColors[i]); |
| } |
| } |
| } |
| |
| } // anonymous namespace |
| |
| DEF_GRAPHITE_TEST_FOR_RENDERING_CONTEXTS(MutableImagesTest, reporter, context) { |
| |
| for (bool useTwoRecorders : { false, true }) { |
| for (bool withMips : { false, true }) { |
| // case 1 (AHBs) |
| run_test(reporter, context, useTwoRecorders, withMips, |
| UpdateBackendTextureMutator::Make); |
| |
| // case 2 (Volatile Promise Images) |
| run_test(reporter, context, useTwoRecorders, withMips, |
| VolatilePromiseImageMutator::Make); |
| |
| // case 3 (Surface/Image pair) |
| if (!withMips) { |
| // TODO: allow the mipmapped version when we can automatically regenerate mipmaps |
| run_test(reporter, context, useTwoRecorders, withMips, |
| SurfaceMutator::Make); |
| } |
| } |
| } |
| } |
| |
| #endif // SK_GRAPHITE_ENABLED |