1 /*
2  * Copyright 2020 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "include/core/SkTypes.h" // IWYU pragma: keep
9 
10 #if !defined(SK_BUILD_FOR_GOOGLE3)  // Google3 doesn't have etc1.h
11 
12 #include "gm/gm.h"
13 #include "include/core/SkBitmap.h"
14 #include "include/core/SkCanvas.h"
15 #include "include/core/SkColor.h"
16 #include "include/core/SkData.h"
17 #include "include/core/SkImage.h"
18 #include "include/core/SkImageInfo.h"
19 #include "include/core/SkPath.h"
20 #include "include/core/SkRect.h"
21 #include "include/core/SkRefCnt.h"
22 #include "include/core/SkSize.h"
23 #include "include/core/SkString.h"
24 #include "include/gpu/GrDirectContext.h"
25 #include "include/gpu/GrRecordingContext.h"
26 #include "src/core/SkCompressedDataUtils.h"
27 #include "src/core/SkMipmap.h"
28 #include "src/gpu/GrDataUtils.h"
29 #include "src/gpu/GrImageContextPriv.h"
30 #include "src/gpu/GrRecordingContextPriv.h"
31 #include "src/image/SkImage_Base.h"
32 #include "src/image/SkImage_GpuBase.h"
33 #include "third_party/etc1/etc1.h"
34 #include "tools/gpu/ProxyUtils.h"
35 
36 class GrSurfaceDrawContext;
37 
gen_pt(float angle,const SkVector & scale)38 static SkPoint gen_pt(float angle, const SkVector& scale) {
39     SkScalar s = SkScalarSin(angle);
40     SkScalar c = SkScalarCos(angle);
41 
42     return { scale.fX * c, scale.fY * s };
43 }
44 
45 // The resulting path will be centered at (0,0) and its size will match 'dimensions'
make_gear(SkISize dimensions,int numTeeth)46 static SkPath make_gear(SkISize dimensions, int numTeeth) {
47     SkVector outerRad{ dimensions.fWidth / 2.0f, dimensions.fHeight / 2.0f };
48     SkVector innerRad{ dimensions.fWidth / 2.5f, dimensions.fHeight / 2.5f };
49     const float kAnglePerTooth = 2.0f * SK_ScalarPI / (3 * numTeeth);
50 
51     float angle = 0.0f;
52 
53     SkPath tmp;
54     tmp.setFillType(SkPathFillType::kWinding);
55 
56     tmp.moveTo(gen_pt(angle, outerRad));
57 
58     for (int i = 0; i < numTeeth; ++i, angle += 3*kAnglePerTooth) {
59         tmp.lineTo(gen_pt(angle+kAnglePerTooth, outerRad));
60         tmp.lineTo(gen_pt(angle+(1.5f*kAnglePerTooth), innerRad));
61         tmp.lineTo(gen_pt(angle+(2.5f*kAnglePerTooth), innerRad));
62         tmp.lineTo(gen_pt(angle+(3.0f*kAnglePerTooth), outerRad));
63     }
64 
65     tmp.close();
66 
67     float fInnerRad = 0.1f * std::min(dimensions.fWidth, dimensions.fHeight);
68     if (fInnerRad > 0.5f) {
69         tmp.addCircle(0.0f, 0.0f, fInnerRad, SkPathDirection::kCCW);
70     }
71 
72     return tmp;
73 }
74 
75 // Render one level of a mipmap
render_level(SkISize dimensions,SkColor color,SkColorType colorType,bool opaque)76 SkBitmap render_level(SkISize dimensions, SkColor color, SkColorType colorType, bool opaque) {
77     SkPath path = make_gear(dimensions, 9);
78 
79     SkImageInfo ii = SkImageInfo::Make(dimensions.width(), dimensions.height(),
80                                        colorType, opaque ? kOpaque_SkAlphaType
81                                                          : kPremul_SkAlphaType);
82     SkBitmap bm;
83     bm.allocPixels(ii);
84 
85     bm.eraseColor(opaque ? SK_ColorBLACK : SK_ColorTRANSPARENT);
86 
87     SkCanvas c(bm);
88 
89     SkPaint paint;
90     paint.setColor(color | 0xFF000000);
91     paint.setAntiAlias(false);
92 
93     c.translate(dimensions.width() / 2.0f, dimensions.height() / 2.0f);
94     c.drawPath(path, paint);
95 
96     return bm;
97 }
98 
99 // Create the compressed data blob needed to represent a mipmapped 2-color texture of the specified
100 // compression format. In this case 2-color means either opaque black or transparent black plus
101 // one other color.
102 // Note that ETC1/ETC2_RGB8_UNORM only supports 565 opaque textures.
make_compressed_image(GrDirectContext * dContext,const SkISize dimensions,SkColorType colorType,bool opaque,SkImage::CompressionType compression)103 static sk_sp<SkImage> make_compressed_image(GrDirectContext* dContext,
104                                             const SkISize dimensions,
105                                             SkColorType colorType,
106                                             bool opaque,
107                                             SkImage::CompressionType compression) {
108     size_t totalSize = SkCompressedDataSize(compression, dimensions, nullptr, true);
109 
110     sk_sp<SkData> tmp = SkData::MakeUninitialized(totalSize);
111     char* pixels = (char*) tmp->writable_data();
112 
113     int numMipLevels = SkMipmap::ComputeLevelCount(dimensions.width(), dimensions.height()) + 1;
114 
115     size_t offset = 0;
116 
117     // Use a different color for each mipmap level so we can visually evaluate the draws
118     static const SkColor kColors[] = {
119         SK_ColorRED,
120         SK_ColorGREEN,
121         SK_ColorBLUE,
122         SK_ColorCYAN,
123         SK_ColorMAGENTA,
124         SK_ColorYELLOW,
125         SK_ColorWHITE,
126     };
127 
128     SkISize levelDims = dimensions;
129     for (int i = 0; i < numMipLevels; ++i) {
130         size_t levelSize = SkCompressedDataSize(compression, levelDims, nullptr, false);
131 
132         SkBitmap bm = render_level(levelDims, kColors[i%7], colorType, opaque);
133         if (compression == SkImage::CompressionType::kETC2_RGB8_UNORM) {
134             SkASSERT(bm.colorType() == kRGB_565_SkColorType);
135             SkASSERT(opaque);
136 
137             if (etc1_encode_image((unsigned char*)bm.getAddr16(0, 0),
138                                   bm.width(), bm.height(), 2, bm.rowBytes(),
139                                   (unsigned char*) &pixels[offset])) {
140                 return nullptr;
141             }
142         } else {
143             GrTwoColorBC1Compress(bm.pixmap(), kColors[i%7], &pixels[offset]);
144         }
145 
146         offset += levelSize;
147         levelDims = {std::max(1, levelDims.width()/2), std::max(1, levelDims.height()/2)};
148     }
149 
150     sk_sp<SkImage> image;
151     if (dContext) {
152         image = SkImage::MakeTextureFromCompressed(dContext, std::move(tmp),
153                                                    dimensions.width(),
154                                                    dimensions.height(),
155                                                    compression, GrMipmapped::kYes);
156     } else {
157         image = SkImage::MakeRasterFromCompressed(std::move(tmp),
158                                                   dimensions.width(),
159                                                   dimensions.height(),
160                                                   compression);
161     }
162     return image;
163 }
164 
165 // Basic test of Ganesh's ETC1 and BC1 support
166 // The layout is:
167 //               ETC2                BC1
168 //         --------------------------------------
169 //  RGB8  | kETC2_RGB8_UNORM  | kBC1_RGB8_UNORM  |
170 //        |--------------------------------------|
171 //  RGBA8 |                   | kBC1_RGBA8_UNORM |
172 //         --------------------------------------
173 //
174 // The nonPowerOfTwo and nonMultipleOfFour cases exercise some compression edge cases.
175 class CompressedTexturesGM : public skiagm::GM {
176 public:
177     enum class Type {
178         kNormal,
179         kNonPowerOfTwo,
180         kNonMultipleOfFour
181     };
182 
CompressedTexturesGM(Type type)183     CompressedTexturesGM(Type type) : fType(type) {
184         this->setBGColor(0xFFCCCCCC);
185 
186         switch (fType) {
187             case Type::kNonPowerOfTwo:
188                 // These dimensions force the top two mip levels to be 1x3 and 1x1
189                 fImgDimensions.set(20, 60);
190                 break;
191             case Type::kNonMultipleOfFour:
192                 // These dimensions force the top three mip levels to be 1x7, 1x3 and 1x1
193                 fImgDimensions.set(13, 61); // prime numbers - just bc
194                 break;
195             default:
196                 fImgDimensions.set(kBaseTexWidth, kBaseTexHeight);
197                 break;
198         }
199 
200     }
201 
202 protected:
onShortName()203     SkString onShortName() override {
204         SkString name("compressed_textures");
205 
206         if (fType == Type::kNonPowerOfTwo) {
207             name.append("_npot");
208         } else if (fType == Type::kNonMultipleOfFour) {
209             name.append("_nmof");
210         }
211 
212         return name;
213     }
214 
onISize()215     SkISize onISize() override {
216         return SkISize::Make(2*kCellWidth + 3*kPad, 2*kBaseTexHeight + 3*kPad);
217     }
218 
onGpuSetup(GrDirectContext * dContext,SkString * errorMsg)219     DrawResult onGpuSetup(GrDirectContext* dContext, SkString* errorMsg) override {
220         if (dContext && dContext->abandoned()) {
221             // This isn't a GpuGM so a null 'context' is okay but an abandoned context
222             // if forbidden.
223             return DrawResult::kSkip;
224         }
225 
226         if (dContext &&
227             dContext->backend() == GrBackendApi::kDirect3D && fType == Type::kNonMultipleOfFour) {
228             // skbug.com/10541 - Are non-multiple-of-four BC1 textures supported in D3D?
229             return DrawResult::kSkip;
230         }
231 
232         fOpaqueETC2Image = make_compressed_image(dContext, fImgDimensions,
233                                                  kRGB_565_SkColorType, true,
234                                                  SkImage::CompressionType::kETC2_RGB8_UNORM);
235 
236         fOpaqueBC1Image = make_compressed_image(dContext, fImgDimensions,
237                                                 kRGBA_8888_SkColorType, true,
238                                                 SkImage::CompressionType::kBC1_RGB8_UNORM);
239 
240         fTransparentBC1Image = make_compressed_image(dContext, fImgDimensions,
241                                                      kRGBA_8888_SkColorType, false,
242                                                      SkImage::CompressionType::kBC1_RGBA8_UNORM);
243 
244         if (!fOpaqueETC2Image || !fOpaqueBC1Image || !fTransparentBC1Image) {
245             *errorMsg = "Failed to create compressed images.";
246             return DrawResult::kFail;
247         }
248 
249         return DrawResult::kOk;
250     }
251 
onGpuTeardown()252     void onGpuTeardown() override {
253         fOpaqueETC2Image = nullptr;
254         fOpaqueBC1Image = nullptr;
255         fTransparentBC1Image = nullptr;
256     }
257 
onDraw(SkCanvas * canvas)258     void onDraw(SkCanvas* canvas) override {
259         this->drawCell(canvas, fOpaqueETC2Image.get(), { kPad, kPad });
260 
261         this->drawCell(canvas, fOpaqueBC1Image.get(), { 2*kPad + kCellWidth, kPad });
262 
263         this->drawCell(canvas, fTransparentBC1Image.get(),
264                        { 2*kPad + kCellWidth, 2*kPad + kBaseTexHeight });
265     }
266 
267 private:
drawCell(SkCanvas * canvas,SkImage * image,SkIVector offset)268     void drawCell(SkCanvas* canvas, SkImage* image, SkIVector offset) {
269 
270         SkISize levelDimensions = fImgDimensions;
271         int numMipLevels = SkMipmap::ComputeLevelCount(levelDimensions.width(),
272                                                        levelDimensions.height()) + 1;
273 
274         SkSamplingOptions sampling(SkCubicResampler::Mitchell());
275 
276         bool isCompressed = false;
277         if (image->isTextureBacked()) {
278             const GrCaps* caps = as_IB(image)->context()->priv().caps();
279             GrTextureProxy* proxy = sk_gpu_test::GetTextureImageProxy(image,
280                                                                       canvas->recordingContext());
281             isCompressed = caps->isFormatCompressed(proxy->backendFormat());
282         }
283 
284         SkPaint redStrokePaint;
285         redStrokePaint.setColor(SK_ColorRED);
286         redStrokePaint.setStyle(SkPaint::kStroke_Style);
287 
288         for (int i = 0; i < numMipLevels; ++i) {
289             SkRect r = SkRect::MakeXYWH(offset.fX, offset.fY,
290                                         levelDimensions.width(), levelDimensions.height());
291 
292             canvas->drawImageRect(image, r, sampling);
293             if (!isCompressed) {
294                 // Make it obvious which drawImages used decompressed images
295                 canvas->drawRect(r, redStrokePaint);
296             }
297 
298             if (i == 0) {
299                 offset.fX += levelDimensions.width()+1;
300             } else {
301                 offset.fY += levelDimensions.height()+1;
302             }
303 
304             levelDimensions = {std::max(1, levelDimensions.width()/2),
305                                std::max(1, levelDimensions.height()/2)};
306         }
307     }
308 
309     static const int kPad = 8;
310     static const int kBaseTexWidth = 64;
311     static const int kCellWidth = 1.5f * kBaseTexWidth;
312     static const int kBaseTexHeight = 64;
313 
314     Type           fType;
315     SkISize        fImgDimensions;
316 
317     sk_sp<SkImage> fOpaqueETC2Image;
318     sk_sp<SkImage> fOpaqueBC1Image;
319     sk_sp<SkImage> fTransparentBC1Image;
320 
321     using INHERITED = GM;
322 };
323 
324 //////////////////////////////////////////////////////////////////////////////
325 
326 DEF_GM(return new CompressedTexturesGM(CompressedTexturesGM::Type::kNormal);)
327 DEF_GM(return new CompressedTexturesGM(CompressedTexturesGM::Type::kNonPowerOfTwo);)
328 DEF_GM(return new CompressedTexturesGM(CompressedTexturesGM::Type::kNonMultipleOfFour);)
329 
330 #endif
331