| /* |
| * Copyright 2020 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "include/core/SkTypes.h" |
| |
| #if !defined(SK_BUILD_FOR_GOOGLE3) // Google3 doesn't have etc1.h |
| |
| #include "gm/gm.h" |
| #include "include/core/SkBitmap.h" |
| #include "include/core/SkCanvas.h" |
| #include "include/core/SkColor.h" |
| #include "include/core/SkData.h" |
| #include "include/core/SkImage.h" |
| #include "include/core/SkImageInfo.h" |
| #include "include/core/SkPath.h" |
| #include "include/core/SkRect.h" |
| #include "include/core/SkRefCnt.h" |
| #include "include/core/SkSize.h" |
| #include "include/core/SkString.h" |
| #include "include/core/SkTextureCompressionType.h" |
| #include "include/gpu/GrDirectContext.h" |
| #include "include/gpu/GrRecordingContext.h" |
| #include "include/gpu/ganesh/SkImageGanesh.h" |
| #include "src/core/SkCompressedDataUtils.h" |
| #include "src/core/SkMipmap.h" |
| #include "src/gpu/ganesh/GrCaps.h" |
| #include "src/gpu/ganesh/GrDataUtils.h" |
| #include "src/gpu/ganesh/GrImageContextPriv.h" |
| #include "src/gpu/ganesh/GrRecordingContextPriv.h" |
| #include "src/gpu/ganesh/image/SkImage_GaneshBase.h" |
| #include "src/image/SkImage_Base.h" |
| #include "third_party/etc1/etc1.h" |
| #include "tools/gpu/ProxyUtils.h" |
| |
| #if defined(SK_GRAPHITE) |
| #include "include/gpu/graphite/Image.h" |
| #include "include/gpu/graphite/Recorder.h" |
| #include "src/gpu/GpuTypesPriv.h" |
| #include "src/gpu/graphite/Caps.h" |
| #include "src/gpu/graphite/RecorderPriv.h" |
| #include "tools/gpu/ManagedBackendTexture.h" |
| #endif |
| |
| static SkPoint gen_pt(float angle, const SkVector& scale) { |
| SkScalar s = SkScalarSin(angle); |
| SkScalar c = SkScalarCos(angle); |
| |
| return { scale.fX * c, scale.fY * s }; |
| } |
| |
| // The resulting path will be centered at (0,0) and its size will match 'dimensions' |
| static SkPath make_gear(SkISize dimensions, int numTeeth) { |
| SkVector outerRad{ dimensions.fWidth / 2.0f, dimensions.fHeight / 2.0f }; |
| SkVector innerRad{ dimensions.fWidth / 2.5f, dimensions.fHeight / 2.5f }; |
| const float kAnglePerTooth = 2.0f * SK_ScalarPI / (3 * numTeeth); |
| |
| float angle = 0.0f; |
| |
| SkPath tmp; |
| tmp.setFillType(SkPathFillType::kWinding); |
| |
| tmp.moveTo(gen_pt(angle, outerRad)); |
| |
| for (int i = 0; i < numTeeth; ++i, angle += 3*kAnglePerTooth) { |
| tmp.lineTo(gen_pt(angle+kAnglePerTooth, outerRad)); |
| tmp.lineTo(gen_pt(angle+(1.5f*kAnglePerTooth), innerRad)); |
| tmp.lineTo(gen_pt(angle+(2.5f*kAnglePerTooth), innerRad)); |
| tmp.lineTo(gen_pt(angle+(3.0f*kAnglePerTooth), outerRad)); |
| } |
| |
| tmp.close(); |
| |
| float fInnerRad = 0.1f * std::min(dimensions.fWidth, dimensions.fHeight); |
| if (fInnerRad > 0.5f) { |
| tmp.addCircle(0.0f, 0.0f, fInnerRad, SkPathDirection::kCCW); |
| } |
| |
| return tmp; |
| } |
| |
| // Render one level of a mipmap |
| SkBitmap render_level(SkISize dimensions, SkColor color, SkColorType colorType, bool opaque) { |
| SkPath path = make_gear(dimensions, 9); |
| |
| SkImageInfo ii = SkImageInfo::Make(dimensions.width(), dimensions.height(), |
| colorType, opaque ? kOpaque_SkAlphaType |
| : kPremul_SkAlphaType); |
| SkBitmap bm; |
| bm.allocPixels(ii); |
| |
| bm.eraseColor(opaque ? SK_ColorBLACK : SK_ColorTRANSPARENT); |
| |
| SkCanvas c(bm); |
| |
| SkPaint paint; |
| paint.setColor(color | 0xFF000000); |
| paint.setAntiAlias(false); |
| |
| c.translate(dimensions.width() / 2.0f, dimensions.height() / 2.0f); |
| c.drawPath(path, paint); |
| |
| return bm; |
| } |
| |
| struct CompressedImageObjects { |
| sk_sp<SkImage> fImage; |
| #if defined(SK_GRAPHITE) |
| sk_sp<sk_gpu_test::ManagedGraphiteTexture> fGraphiteTexture; |
| #else |
| void* fGraphiteTexture = nullptr; |
| #endif |
| }; |
| |
| // Create the compressed data blob needed to represent a mipmapped 2-color texture of the specified |
| // compression format. In this case 2-color means either opaque black or transparent black plus |
| // one other color. |
| // Note that ETC1/ETC2_RGB8_UNORM only supports 565 opaque textures. |
| static CompressedImageObjects make_compressed_image(SkCanvas* canvas, |
| const SkISize dimensions, |
| SkColorType colorType, |
| bool opaque, |
| SkTextureCompressionType compression) { |
| size_t totalSize = SkCompressedDataSize(compression, dimensions, nullptr, true); |
| |
| sk_sp<SkData> tmp = SkData::MakeUninitialized(totalSize); |
| char* pixels = (char*) tmp->writable_data(); |
| |
| int numMipLevels = SkMipmap::ComputeLevelCount(dimensions.width(), dimensions.height()) + 1; |
| |
| size_t offset = 0; |
| |
| // Use a different color for each mipmap level so we can visually evaluate the draws |
| static const SkColor kColors[] = { |
| SK_ColorRED, |
| SK_ColorGREEN, |
| SK_ColorBLUE, |
| SK_ColorCYAN, |
| SK_ColorMAGENTA, |
| SK_ColorYELLOW, |
| SK_ColorWHITE, |
| }; |
| |
| SkISize levelDims = dimensions; |
| for (int i = 0; i < numMipLevels; ++i) { |
| size_t levelSize = SkCompressedDataSize(compression, levelDims, nullptr, false); |
| |
| SkBitmap bm = render_level(levelDims, kColors[i%7], colorType, opaque); |
| if (compression == SkTextureCompressionType::kETC2_RGB8_UNORM) { |
| SkASSERT(bm.colorType() == kRGB_565_SkColorType); |
| SkASSERT(opaque); |
| |
| if (etc1_encode_image((unsigned char*)bm.getAddr16(0, 0), |
| bm.width(), bm.height(), 2, bm.rowBytes(), |
| (unsigned char*) &pixels[offset])) { |
| return {nullptr, nullptr}; |
| } |
| } else { |
| GrTwoColorBC1Compress(bm.pixmap(), kColors[i%7], &pixels[offset]); |
| } |
| |
| offset += levelSize; |
| levelDims = {std::max(1, levelDims.width()/2), std::max(1, levelDims.height()/2)}; |
| } |
| |
| sk_sp<SkImage> image; |
| #if defined(SK_GRAPHITE) |
| skgpu::graphite::Recorder* recorder = canvas->recorder(); |
| if (recorder) { |
| sk_sp<sk_gpu_test::ManagedGraphiteTexture> texture = |
| sk_gpu_test::ManagedGraphiteTexture::MakeFromCompressedData(recorder, |
| dimensions, |
| compression, |
| tmp, |
| skgpu::Mipmapped::kYes); |
| if (texture) { |
| image = SkImages::WrapTexture(recorder, |
| texture->texture(), |
| skgpu::CompressionTypeToSkColorType(compression), |
| kPremul_SkAlphaType, |
| /*colorSpace=*/nullptr); |
| if (image) { |
| return {image, texture}; |
| } |
| } |
| } |
| #endif |
| auto dContext = GrAsDirectContext(canvas->recordingContext()); |
| if (dContext) { |
| image = SkImages::TextureFromCompressedTextureData(dContext, |
| std::move(tmp), |
| dimensions.width(), |
| dimensions.height(), |
| compression, |
| skgpu::Mipmapped::kYes); |
| } else { |
| image = SkImages::RasterFromCompressedTextureData( |
| std::move(tmp), dimensions.width(), dimensions.height(), compression); |
| } |
| return {image, nullptr}; |
| } |
| |
| // Basic test of Ganesh's ETC1 and BC1 support |
| // The layout is: |
| // ETC2 BC1 |
| // -------------------------------------- |
| // RGB8 | kETC2_RGB8_UNORM | kBC1_RGB8_UNORM | |
| // |--------------------------------------| |
| // RGBA8 | | kBC1_RGBA8_UNORM | |
| // -------------------------------------- |
| // |
| // The nonPowerOfTwo and nonMultipleOfFour cases exercise some compression edge cases. |
| class CompressedTexturesGM : public skiagm::GM { |
| public: |
| enum class Type { |
| kNormal, |
| kNonPowerOfTwo, |
| kNonMultipleOfFour |
| }; |
| |
| CompressedTexturesGM(Type type) : fType(type) { |
| this->setBGColor(0xFFCCCCCC); |
| |
| switch (fType) { |
| case Type::kNonPowerOfTwo: |
| // These dimensions force the top two mip levels to be 1x3 and 1x1 |
| fImgDimensions.set(20, 60); |
| break; |
| case Type::kNonMultipleOfFour: |
| // These dimensions force the top three mip levels to be 1x7, 1x3 and 1x1 |
| fImgDimensions.set(13, 61); // prime numbers - just bc |
| break; |
| default: |
| fImgDimensions.set(kBaseTexWidth, kBaseTexHeight); |
| break; |
| } |
| |
| } |
| |
| protected: |
| SkString getName() const override { |
| SkString name("compressed_textures"); |
| |
| if (fType == Type::kNonPowerOfTwo) { |
| name.append("_npot"); |
| } else if (fType == Type::kNonMultipleOfFour) { |
| name.append("_nmof"); |
| } |
| |
| return name; |
| } |
| |
| SkISize getISize() override { |
| return SkISize::Make(2*kCellWidth + 3*kPad, 2*kBaseTexHeight + 3*kPad); |
| } |
| |
| DrawResult onGpuSetup(SkCanvas* canvas, SkString* errorMsg, |
| GraphiteTestContext* graphiteTestContext) override { |
| auto dContext = GrAsDirectContext(canvas->recordingContext()); |
| if (dContext && dContext->abandoned()) { |
| // This isn't a GpuGM so a null 'context' is okay but an abandoned context |
| // if forbidden. |
| return DrawResult::kSkip; |
| } |
| |
| if (dContext && |
| dContext->backend() == GrBackendApi::kDirect3D && fType == Type::kNonMultipleOfFour) { |
| // skbug.com/10541 - Are non-multiple-of-four BC1 textures supported in D3D? |
| return DrawResult::kSkip; |
| } |
| |
| fOpaqueETC2Image = make_compressed_image(canvas, fImgDimensions, |
| kRGB_565_SkColorType, true, |
| SkTextureCompressionType::kETC2_RGB8_UNORM); |
| |
| fOpaqueBC1Image = make_compressed_image(canvas, fImgDimensions, |
| kRGBA_8888_SkColorType, true, |
| SkTextureCompressionType::kBC1_RGB8_UNORM); |
| |
| fTransparentBC1Image = make_compressed_image(canvas, fImgDimensions, |
| kRGBA_8888_SkColorType, false, |
| SkTextureCompressionType::kBC1_RGBA8_UNORM); |
| |
| if (!fOpaqueETC2Image.fImage || !fOpaqueBC1Image.fImage || !fTransparentBC1Image.fImage) { |
| *errorMsg = "Failed to create compressed images."; |
| return DrawResult::kFail; |
| } |
| |
| return DrawResult::kOk; |
| } |
| |
| void onGpuTeardown() override { |
| fOpaqueETC2Image.fImage = nullptr; |
| fOpaqueBC1Image.fImage = nullptr; |
| fTransparentBC1Image.fImage = nullptr; |
| fOpaqueETC2Image.fGraphiteTexture = nullptr; |
| fOpaqueBC1Image.fGraphiteTexture = nullptr; |
| fTransparentBC1Image.fGraphiteTexture = nullptr; |
| } |
| |
| void onDraw(SkCanvas* canvas) override { |
| this->drawCell(canvas, fOpaqueETC2Image.fImage.get(), { kPad, kPad }); |
| |
| this->drawCell(canvas, fOpaqueBC1Image.fImage.get(), { 2*kPad + kCellWidth, kPad }); |
| |
| this->drawCell(canvas, fTransparentBC1Image.fImage.get(), |
| { 2*kPad + kCellWidth, 2*kPad + kBaseTexHeight }); |
| } |
| |
| private: |
| void drawCell(SkCanvas* canvas, SkImage* image, SkIVector offset) { |
| |
| SkISize levelDimensions = fImgDimensions; |
| int numMipLevels = SkMipmap::ComputeLevelCount(levelDimensions.width(), |
| levelDimensions.height()) + 1; |
| |
| SkSamplingOptions sampling(SkCubicResampler::Mitchell()); |
| |
| bool isCompressed = false; |
| if (image->isTextureBacked()) { |
| auto dContext = GrAsDirectContext(canvas->recordingContext()); |
| if (dContext) { |
| const GrCaps* caps = as_IB(image)->context()->priv().caps(); |
| GrTextureProxy* proxy = sk_gpu_test::GetTextureImageProxy( |
| image, canvas->recordingContext()); |
| isCompressed = caps->isFormatCompressed(proxy->backendFormat()); |
| } else { |
| // Graphite has no fallback to upload the compressed data to a non-compressed |
| // format. So if the image is texture backed and graphite then it will be a |
| // compressed format. |
| isCompressed = true; |
| } |
| } |
| |
| SkPaint redStrokePaint; |
| redStrokePaint.setColor(SK_ColorRED); |
| redStrokePaint.setStyle(SkPaint::kStroke_Style); |
| |
| for (int i = 0; i < numMipLevels; ++i) { |
| SkRect r = SkRect::MakeXYWH(offset.fX, offset.fY, |
| levelDimensions.width(), levelDimensions.height()); |
| |
| canvas->drawImageRect(image, r, sampling); |
| if (!isCompressed) { |
| // Make it obvious which drawImages used decompressed images |
| canvas->drawRect(r, redStrokePaint); |
| } |
| |
| if (i == 0) { |
| offset.fX += levelDimensions.width()+1; |
| } else { |
| offset.fY += levelDimensions.height()+1; |
| } |
| |
| levelDimensions = {std::max(1, levelDimensions.width()/2), |
| std::max(1, levelDimensions.height()/2)}; |
| } |
| } |
| |
| static const int kPad = 8; |
| static const int kBaseTexWidth = 64; |
| static const int kCellWidth = 1.5f * kBaseTexWidth; |
| static const int kBaseTexHeight = 64; |
| |
| Type fType; |
| SkISize fImgDimensions; |
| |
| |
| CompressedImageObjects fOpaqueETC2Image; |
| CompressedImageObjects fOpaqueBC1Image; |
| CompressedImageObjects fTransparentBC1Image; |
| |
| using INHERITED = GM; |
| }; |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| |
| DEF_GM(return new CompressedTexturesGM(CompressedTexturesGM::Type::kNormal);) |
| DEF_GM(return new CompressedTexturesGM(CompressedTexturesGM::Type::kNonPowerOfTwo);) |
| DEF_GM(return new CompressedTexturesGM(CompressedTexturesGM::Type::kNonMultipleOfFour);) |
| |
| #endif |