• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 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/codec/SkCodec.h"
9 #include "include/codec/SkJpegDecoder.h"
10 #include "include/codec/SkPngDecoder.h"
11 #include "include/codec/SkWebpDecoder.h"
12 #include "include/core/SkCanvas.h"
13 #include "include/core/SkColor.h"
14 #include "include/core/SkData.h"
15 #include "include/core/SkFontMgr.h"
16 #include "include/core/SkGraphics.h"
17 #include "include/core/SkImage.h"
18 #include "include/core/SkImageInfo.h"
19 #include "include/core/SkMatrix.h"
20 #include "include/core/SkPixmap.h"
21 #include "include/core/SkRect.h"
22 #include "include/core/SkRefCnt.h"
23 #include "include/core/SkStream.h"
24 #include "include/core/SkString.h"
25 #include "include/core/SkSurface.h"
26 #include "include/core/SkTypes.h"
27 #include "include/encode/SkPngEncoder.h"
28 #include "include/private/base/SkDebug.h"
29 #include "include/private/base/SkTPin.h"
30 #include "include/private/base/SkTo.h"
31 #include "modules/skottie/include/ExternalLayer.h"
32 #include "modules/skottie/include/Skottie.h"
33 #include "modules/skottie/utils/SkottieUtils.h"
34 #include "modules/skresources/include/SkResources.h"
35 #include "modules/skshaper/utils/FactoryHelpers.h"
36 #include "src/core/SkOSFile.h"
37 #include "src/core/SkTaskGroup.h"
38 #include "src/utils/SkOSPath.h"
39 #include "tools/flags/CommandLineFlags.h"
40 
41 #if !defined(CPU_ONLY)
42 #include "include/gpu/GpuTypes.h"
43 #include "include/gpu/GrDirectContext.h"
44 #include "include/gpu/GrTypes.h"
45 #include "include/gpu/ganesh/SkSurfaceGanesh.h"
46 #include "tools/gpu/ContextType.h"
47 #include "tools/gpu/GrContextFactory.h"
48 #endif
49 
50 #if !defined(CPU_ONLY) && !defined(GPU_ONLY)
51 #include "include/core/SkPicture.h"
52 #include "include/core/SkPictureRecorder.h"
53 #include "include/core/SkSerialProcs.h"
54 #include "src/image/SkImage_Base.h"
55 #endif
56 
57 #include <algorithm>
58 #include <chrono>
59 #include <cstdio>
60 #include <cstring>
61 #include <functional>
62 #include <memory>
63 #include <numeric>
64 #include <utility>
65 #include <vector>
66 
67 #if defined(HAVE_VIDEO_ENCODER)
68     #include <future>
69     #include "experimental/ffmpeg/SkVideoEncoder.h"
70     const char* formats_help = "Output format (png, skp, mp4, or null)";
71 #else
72     const char* formats_help = "Output format (png, skp, or null)";
73 #endif
74 
75 #if defined(SK_BUILD_FOR_MAC) && defined(SK_FONTMGR_CORETEXT_AVAILABLE)
76 #include "include/ports/SkFontMgr_mac_ct.h"
77 #elif defined(SK_BUILD_FOR_ANDROID) && defined(SK_FONTMGR_ANDROID_AVAILABLE)
78 #include "include/ports/SkFontMgr_android.h"
79 #include "src/ports/SkTypeface_FreeType.h"
80 #elif defined(SK_BUILD_FOR_UNIX) && defined(SK_FONTMGR_FONTCONFIG_AVAILABLE)
81 #include "include/ports/SkFontMgr_fontconfig.h"
82 #else
83 #include "include/ports/SkFontMgr_empty.h"
84 #endif
85 
86 static DEFINE_string2(input    , i, nullptr, "Input .json file.");
87 static DEFINE_string2(writePath, w, nullptr, "Output directory.  Frames are names [0-9]{6}.png.");
88 static DEFINE_string2(format   , f, "png"  , formats_help);
89 
90 static DEFINE_double(t0,    0, "Timeline start [0..1].");
91 static DEFINE_double(t1,    1, "Timeline stop [0..1].");
92 static DEFINE_double(fps,   0, "Decode frames per second (default is animation native fps).");
93 
94 static DEFINE_int(width , 800, "Render width.");
95 static DEFINE_int(height, 600, "Render height.");
96 static DEFINE_int(threads,  0, "Number of worker threads (0 -> cores count).");
97 
98 static DEFINE_bool2(gpu, g, false, "Enable GPU rasterization.");
99 
100 namespace {
101 
102 static constexpr SkColor kClearColor = SK_ColorWHITE;
103 
104 enum class OutputFormat {
105     kPNG,
106     kSKP,
107     kNull,
108     kMP4,
109 };
110 
111 
ms_since(std::chrono::steady_clock::time_point start)112 auto ms_since(std::chrono::steady_clock::time_point start) {
113     const auto elapsed = std::chrono::steady_clock::now() - start;
114     return std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
115 }
116 
make_file_stream(size_t frame_index,const char * extension)117 std::unique_ptr<SkFILEWStream> make_file_stream(size_t frame_index, const char* extension) {
118     const auto file = SkStringPrintf("0%06zu.%s", frame_index, extension);
119     const auto path = SkOSPath::Join(FLAGS_writePath[0], file.c_str());
120 
121     auto stream = std::make_unique<SkFILEWStream>(path.c_str());
122 
123     return stream->isValid() ? std::move(stream) : nullptr;
124 }
125 
126 class FrameSink {
127 public:
128     virtual ~FrameSink() = default;
129 
130     static std::unique_ptr<FrameSink> Make(OutputFormat fmt, size_t frame_count);
131 
132     virtual void writeFrame(sk_sp<SkImage> frame, size_t frame_index) = 0;
133 
finalize(double fps)134     virtual void finalize(double fps) {}
135 
136 protected:
137     FrameSink() = default;
138 
139 private:
140     FrameSink(const FrameSink&)            = delete;
141     FrameSink& operator=(const FrameSink&) = delete;
142 };
143 
144 class PNGSink final : public FrameSink {
145 public:
writeFrame(sk_sp<SkImage> frame,size_t frame_index)146     void writeFrame(sk_sp<SkImage> frame, size_t frame_index) override {
147         auto stream = make_file_stream(frame_index, "png");
148 
149         if (!frame || !stream) {
150             return;
151         }
152 
153         // Set encoding options to favor speed over size.
154         SkPngEncoder::Options options;
155         options.fZLibLevel   = 1;
156         options.fFilterFlags = SkPngEncoder::FilterFlag::kNone;
157 
158         SkPixmap pixmap;
159         SkAssertResult(frame->peekPixels(&pixmap));
160 
161         SkPngEncoder::Encode(stream.get(), pixmap, options);
162     }
163 };
164 
165 class NullSink final : public FrameSink {
166 public:
writeFrame(sk_sp<SkImage>,size_t)167     void writeFrame(sk_sp<SkImage>, size_t) override {}
168 };
169 
170 #if defined(HAVE_VIDEO_ENCODER)
171 class MP4Sink final : public FrameSink {
172 public:
MP4Sink(size_t frame_count)173     explicit MP4Sink(size_t frame_count) {
174         fFrames.resize(frame_count);
175     }
176 
writeFrame(sk_sp<SkImage> frame,size_t frame_index)177     void writeFrame(sk_sp<SkImage> frame, size_t frame_index) override {
178         fFrames[frame_index].set_value(std::move(frame));
179     }
180 
finalize(double fps)181     void finalize(double fps) override {
182         SkVideoEncoder encoder;
183         if (!encoder.beginRecording({FLAGS_width, FLAGS_height}, sk_double_round2int(fps))) {
184             fprintf(stderr, "Invalid video stream configuration.\n");
185         }
186 
187         std::vector<double> starved_ms;
188         starved_ms.reserve(fFrames.size());
189 
190         for (auto& frame_promise : fFrames) {
191             const auto start = std::chrono::steady_clock::now();
192             auto frame = frame_promise.get_future().get();
193             starved_ms.push_back(ms_since(start));
194 
195             if (!frame) continue;
196 
197             SkPixmap pixmap;
198             SkAssertResult(frame->peekPixels(&pixmap));
199             encoder.addFrame(pixmap);
200         }
201 
202         auto mp4 = encoder.endRecording();
203 
204         SkFILEWStream{FLAGS_writePath[0]}
205             .write(mp4->data(), mp4->size());
206 
207         // If everything's going well, the first frame should account for the most,
208         // and ideally nearly all, starvation.
209         double first = starved_ms[0];
210         std::sort(starved_ms.begin(), starved_ms.end());
211         double sum = std::accumulate(starved_ms.begin(), starved_ms.end(), 0);
212         printf("Encoder starved stats: "
213                "min %gms, med %gms, avg %gms, max %gms, sum %gms, first %gms (%s)\n",
214                starved_ms[0], starved_ms[fFrames.size()/2], sum/fFrames.size(), starved_ms.back(),
215                sum, first, first == starved_ms.back() ? "ok" : "BAD");
216 
217     }
218 
219     std::vector<std::promise<sk_sp<SkImage>>> fFrames;
220 };
221 #endif // HAVE_VIDEO_ENCODER
222 
Make(OutputFormat fmt,size_t frame_count)223 std::unique_ptr<FrameSink> FrameSink::Make(OutputFormat fmt, size_t frame_count) {
224     switch (fmt) {
225     case OutputFormat::kPNG:
226         return std::make_unique<PNGSink>();
227     case OutputFormat::kSKP:
228         // The SKP generator does not use a sink.
229         [[fallthrough]];
230     case OutputFormat::kNull:
231         return std::make_unique<NullSink>();
232     case OutputFormat::kMP4:
233 #if defined(HAVE_VIDEO_ENCODER)
234         return std::make_unique<MP4Sink>(frame_count);
235 #else
236         return nullptr;
237 #endif
238     }
239 
240     SkUNREACHABLE;
241 }
242 
243 class FrameGenerator {
244 public:
245     virtual ~FrameGenerator() = default;
246 
247     static std::unique_ptr<FrameGenerator> Make(FrameSink*, OutputFormat, const SkMatrix&);
248 
generateFrame(const skottie::Animation *,size_t frame_index)249     virtual void generateFrame(const skottie::Animation*, size_t frame_index) {}
250 
251 protected:
FrameGenerator(FrameSink * sink)252     explicit FrameGenerator(FrameSink* sink) : fSink(sink) {}
253 
254     FrameSink* fSink;
255 
256 private:
257     FrameGenerator(const FrameGenerator&)            = delete;
258     FrameGenerator& operator=(const FrameGenerator&) = delete;
259 };
260 
261 class CPUGenerator final : public FrameGenerator {
262 public:
263 #if defined(GPU_ONLY)
Make(FrameSink * sink,const SkMatrix & matrix)264     static std::unique_ptr<FrameGenerator> Make(FrameSink* sink, const SkMatrix& matrix) {
265         return nullptr;
266     }
267 #else
268     static std::unique_ptr<FrameGenerator> Make(FrameSink* sink, const SkMatrix& matrix) {
269         auto surface = SkSurfaces::Raster(SkImageInfo::MakeN32Premul(FLAGS_width, FLAGS_height));
270         if (!surface) {
271             SkDebugf("Could not allocate a %d x %d surface.\n", FLAGS_width, FLAGS_height);
272             return nullptr;
273         }
274 
275         return std::unique_ptr<FrameGenerator>(new CPUGenerator(sink, std::move(surface), matrix));
276     }
277 
278     void generateFrame(const skottie::Animation* anim, size_t frame_index) override {
279         fSurface->getCanvas()->clear(kClearColor);
280         anim->render(fSurface->getCanvas());
281 
282         fSink->writeFrame(fSurface->makeImageSnapshot(), frame_index);
283     }
284 
285 private:
286     CPUGenerator(FrameSink* sink, sk_sp<SkSurface> surface, const SkMatrix& scale_matrix)
287         : FrameGenerator(sink)
288         , fSurface(std::move(surface))
289     {
290         fSurface->getCanvas()->concat(scale_matrix);
291     }
292 
293     const sk_sp<SkSurface> fSurface;
294 #endif // !GPU_ONLY
295 };
296 
297 class SKPGenerator final : public FrameGenerator {
298 public:
299 #if defined(CPU_ONLY) || defined(GPU_ONLY)
Make(FrameSink * sink,const SkMatrix & matrix)300     static std::unique_ptr<FrameGenerator> Make(FrameSink* sink, const SkMatrix& matrix) {
301         return nullptr;
302     }
303 #else
304     static std::unique_ptr<FrameGenerator> Make(FrameSink* sink, const SkMatrix& scale_matrix) {
305         return std::unique_ptr<FrameGenerator>(new SKPGenerator(sink, scale_matrix));
306     }
307 
308     void generateFrame(const skottie::Animation* anim, size_t frame_index) override {
309         auto* canvas = fRecorder.beginRecording(FLAGS_width, FLAGS_height);
310         canvas->concat(fScaleMatrix);
311         anim->render(canvas);
312 
313         auto frame  = fRecorder.finishRecordingAsPicture();
314         auto stream = make_file_stream(frame_index, "skp");
315 
316         if (frame && stream) {
317             SkSerialProcs sProcs;
318             sProcs.fImageProc = [](SkImage* img, void*) -> sk_sp<SkData> {
319                 return SkPngEncoder::Encode(as_IB(img)->directContext(), img,
320                                             SkPngEncoder::Options{});
321             };
322             frame->serialize(stream.get(), &sProcs);
323         }
324     }
325 
326 private:
327     SKPGenerator(FrameSink* sink, const SkMatrix& scale_matrix)
328         : FrameGenerator(sink)
329         , fScaleMatrix(scale_matrix)
330     {}
331 
332     const SkMatrix    fScaleMatrix;
333     SkPictureRecorder fRecorder;
334 #endif // !CPU_ONLY && !GPU_ONLY
335 };
336 
337 class GPUGenerator final : public FrameGenerator {
338 public:
339 #if defined(CPU_ONLY)
Make(FrameSink * sink,const SkMatrix & matrix)340     static std::unique_ptr<FrameGenerator> Make(FrameSink* sink, const SkMatrix& matrix) {
341         return nullptr;
342     }
343 #else
344     static std::unique_ptr<FrameGenerator> Make(FrameSink* sink, const SkMatrix& matrix) {
345         auto gpu_generator = std::unique_ptr<GPUGenerator>(new GPUGenerator(sink, matrix));
346 
347         return gpu_generator->isValid()
348                 ? std::unique_ptr<FrameGenerator>(gpu_generator.release())
349                 : nullptr;
350     }
351 
352     ~GPUGenerator() override {
353         // ensure all pending reads are completed
354         fCtx->flushAndSubmit(GrSyncCpu::kYes);
355     }
356 
357     void generateFrame(const skottie::Animation* anim, size_t frame_index) override {
358         fSurface->getCanvas()->clear(kClearColor);
359         anim->render(fSurface->getCanvas());
360 
361         auto rec = std::make_unique<AsyncRec>(fSink, frame_index);
362         fSurface->asyncRescaleAndReadPixels(SkImageInfo::MakeN32Premul(FLAGS_width, FLAGS_height),
363                                             {0, 0, FLAGS_width, FLAGS_height},
364                                             SkSurface::RescaleGamma::kSrc,
365                                             SkImage::RescaleMode::kNearest,
366                                             AsyncCallback, rec.release());
367 
368         fCtx->submit();
369     }
370 
371 private:
372     GPUGenerator(FrameSink* sink, const SkMatrix& matrix)
373         : FrameGenerator(sink)
374     {
375         fCtx = fFactory.getContextInfo(skgpu::ContextType::kGL).directContext();
376         fSurface = SkSurfaces::RenderTarget(fCtx,
377                                             skgpu::Budgeted::kNo,
378                                             SkImageInfo::MakeN32Premul(FLAGS_width, FLAGS_height),
379                                             0,
380                                             GrSurfaceOrigin::kTopLeft_GrSurfaceOrigin,
381                                             nullptr);
382         if (fSurface) {
383             fSurface->getCanvas()->concat(matrix);
384         } else {
385             fprintf(stderr, "Could not initialize GL context.\n");
386         }
387     }
388 
389     bool isValid() const { return !!fSurface; }
390 
391     struct AsyncRec {
392         FrameSink* sink;
393         size_t     index;
394 
395         AsyncRec(FrameSink* sink, size_t index) : sink(sink), index(index) {}
396     };
397 
398     static void AsyncCallback(SkSurface::ReadPixelsContext ctx,
399                               std::unique_ptr<const SkSurface::AsyncReadResult> result) {
400         std::unique_ptr<const AsyncRec> rec(reinterpret_cast<const AsyncRec*>(ctx));
401         if (result && result->count() == 1) {
402             SkPixmap pm(SkImageInfo::MakeN32Premul(FLAGS_width, FLAGS_height),
403                         result->data(0), result->rowBytes(0));
404 
405             auto release_proc = [](const void*, SkImages::ReleaseContext ctx) {
406                 std::unique_ptr<const SkSurface::AsyncReadResult>
407                         adopted(reinterpret_cast<const SkSurface::AsyncReadResult*>(ctx));
408             };
409 
410             auto frame_image =
411                     SkImages::RasterFromPixmap(pm, release_proc, (void*)result.release());
412 
413             rec->sink->writeFrame(std::move(frame_image), rec->index);
414         }
415     }
416 
417     sk_gpu_test::GrContextFactory fFactory;
418     GrDirectContext*              fCtx;
419     sk_sp<SkSurface>              fSurface;
420 #endif // !CPU_ONLY
421 };
422 
Make(FrameSink * sink,OutputFormat fmt,const SkMatrix & matrix)423 std::unique_ptr<FrameGenerator> FrameGenerator::Make(FrameSink* sink,
424                                                      OutputFormat fmt,
425                                                      const SkMatrix& matrix) {
426     if (fmt == OutputFormat::kSKP) {
427         return SKPGenerator::Make(sink, matrix);
428     }
429 
430     return FLAGS_gpu
431             ? GPUGenerator::Make(sink, matrix)
432             : CPUGenerator::Make(sink, matrix);
433 }
434 
435 class Logger final : public skottie::Logger {
436 public:
437     struct LogEntry {
438         SkString fMessage,
439                  fJSON;
440     };
441 
log(skottie::Logger::Level lvl,const char message[],const char json[])442     void log(skottie::Logger::Level lvl, const char message[], const char json[]) override {
443         auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings;
444         log.push_back({ SkString(message), json ? SkString(json) : SkString() });
445     }
446 
report() const447     void report() const {
448         SkDebugf("Animation loaded with %zu error%s, %zu warning%s.\n",
449                  fErrors.size(), fErrors.size() == 1 ? "" : "s",
450                  fWarnings.size(), fWarnings.size() == 1 ? "" : "s");
451 
452         const auto& show = [](const LogEntry& log, const char prefix[]) {
453             SkDebugf("%s%s", prefix, log.fMessage.c_str());
454             if (!log.fJSON.isEmpty())
455                 SkDebugf(" : %s", log.fJSON.c_str());
456             SkDebugf("\n");
457         };
458 
459         for (const auto& err : fErrors)   show(err, "  !! ");
460         for (const auto& wrn : fWarnings) show(wrn, "  ?? ");
461     }
462 
463 private:
464     std::vector<LogEntry> fErrors,
465                           fWarnings;
466 };
467 
468 } // namespace
469 
470 extern bool gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental;
471 
main(int argc,char ** argv)472 int main(int argc, char** argv) {
473     gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental = true;
474     CommandLineFlags::Parse(argc, argv);
475     SkGraphics::Init();
476 
477     if (FLAGS_input.isEmpty() || FLAGS_writePath.isEmpty()) {
478         SkDebugf("Missing required 'input' and 'writePath' args.\n");
479         return 1;
480     }
481 
482     OutputFormat fmt;
483     if (0 == std::strcmp(FLAGS_format[0], "png")) {
484         fmt = OutputFormat::kPNG;
485     } else if (0 == std::strcmp(FLAGS_format[0], "skp")) {
486         fmt = OutputFormat::kSKP;
487     } else if (0 == std::strcmp(FLAGS_format[0], "null")) {
488         fmt = OutputFormat::kNull;
489 #if defined(HAVE_VIDEO_ENCODER)
490     } else if (0 == std::strcmp(FLAGS_format[0], "mp4")) {
491         fmt = OutputFormat::kMP4;
492 #endif
493     } else {
494         fprintf(stderr, "Unknown format: %s\n", FLAGS_format[0]);
495         return 1;
496     }
497 
498     if (fmt != OutputFormat::kMP4 && !sk_mkdir(FLAGS_writePath[0])) {
499         return 1;
500     }
501 
502     SkCodecs::Register(SkPngDecoder::Decoder());
503     SkCodecs::Register(SkJpegDecoder::Decoder());
504     SkCodecs::Register(SkWebpDecoder::Decoder());
505 
506     // If necessary, clients should use a font manager that would load fonts from the system.
507 #if defined(SK_BUILD_FOR_MAC) && defined(SK_FONTMGR_CORETEXT_AVAILABLE)
508     sk_sp<SkFontMgr> fontMgr = SkFontMgr_New_CoreText(nullptr);
509 #elif defined(SK_BUILD_FOR_ANDROID) && defined(SK_FONTMGR_ANDROID_AVAILABLE)
510     sk_sp<SkFontMgr> fontMgr = SkFontMgr_New_Android(nullptr, std::make_unique<SkFontScanner_FreeType>());
511 #elif defined(SK_BUILD_FOR_UNIX) && defined(SK_FONTMGR_FONTCONFIG_AVAILABLE)
512     sk_sp<SkFontMgr> fontMgr = SkFontMgr_New_FontConfig(nullptr);
513 #else
514     sk_sp<SkFontMgr> fontMgr = SkFontMgr_New_Custom_Empty();
515 #endif
516 
517     auto predecode = skresources::ImageDecodeStrategy::kPreDecode;
518     auto logger = sk_make_sp<Logger>();
519     auto rp = skresources::CachingResourceProvider::Make(
520             skresources::DataURIResourceProviderProxy::Make(
521                     skresources::FileResourceProvider::Make(SkOSPath::Dirname(FLAGS_input[0]),
522                                                             predecode),
523                     predecode,
524                     fontMgr));
525     auto data   = SkData::MakeFromFileName(FLAGS_input[0]);
526     auto precomp_interceptor =
527             sk_make_sp<skottie_utils::ExternalAnimationPrecompInterceptor>(rp, "__");
528 
529     if (!data) {
530         SkDebugf("Could not load %s.\n", FLAGS_input[0]);
531         return 1;
532     }
533 
534     // Instantiate an animation on the main thread for two reasons:
535     //   - we need to know its duration upfront
536     //   - we want to only report parsing errors once
537     auto anim = skottie::Animation::Builder()
538             .setFontManager(fontMgr)
539             .setLogger(logger)
540             .setResourceProvider(rp)
541             .setTextShapingFactory(SkShapers::BestAvailable())
542             .make(static_cast<const char*>(data->data()), data->size());
543     if (!anim) {
544         SkDebugf("Could not parse animation: '%s'.\n", FLAGS_input[0]);
545         return 1;
546     }
547 
548     const auto scale_matrix = SkMatrix::RectToRect(SkRect::MakeSize(anim->size()),
549                                                    SkRect::MakeIWH(FLAGS_width, FLAGS_height),
550                                                    SkMatrix::kCenter_ScaleToFit);
551     logger->report();
552 
553     const auto t0 = SkTPin(FLAGS_t0, 0.0, 1.0),
554                t1 = SkTPin(FLAGS_t1,  t0, 1.0),
555        native_fps = anim->fps(),
556            frame0 = anim->duration() * t0 * native_fps,
557          duration = anim->duration() * (t1 - t0);
558 
559     double fps = FLAGS_fps > 0 ? FLAGS_fps : native_fps;
560     if (fps <= 0) {
561         SkDebugf("Invalid fps: %f.\n", fps);
562         return 1;
563     }
564 
565     auto frame_count = static_cast<int>(duration * fps);
566     static constexpr int kMaxFrames = 10000;
567     if (frame_count > kMaxFrames) {
568         frame_count = kMaxFrames;
569         fps = frame_count / duration;
570     }
571     const auto fps_scale = native_fps / fps;
572 
573     printf("Rendering %f seconds (%d frames @%f fps).\n", duration, frame_count, fps);
574 
575     const auto sink = FrameSink::Make(fmt, frame_count);
576 
577     std::vector<double> frames_ms(frame_count);
578 
579     const auto thread_count = FLAGS_gpu ? 0 : FLAGS_threads - 1;
580     SkTaskGroup::Enabler enabler(thread_count);
581 
582     SkTaskGroup tg;
583     {
584         // Depending on type (gpu vs. everything else), we use either a single generator
585         // or one generator per worker thread, respectively.
586         // Scoping is important for the single generator case because we want its destructor to
587         // flush out any pending async operations.
588         std::unique_ptr<FrameGenerator> singleton_generator;
589         if (FLAGS_gpu) {
590             singleton_generator = FrameGenerator::Make(sink.get(), fmt, scale_matrix);
591         }
592 
593         tg.batch(frame_count, [&](int i) {
594             // SkTaskGroup::Enabler creates a LIFO work pool,
595             // but we want our early frames to start first.
596             i = frame_count - 1 - i;
597 
598             const auto start = std::chrono::steady_clock::now();
599             thread_local static auto* anim =
600                     skottie::Animation::Builder()
601                         .setResourceProvider(rp)
602                         .setPrecompInterceptor(precomp_interceptor)
603                         .make(static_cast<const char*>(data->data()), data->size())
604                         .release();
605             thread_local static auto* gen = singleton_generator
606                     ? singleton_generator.get()
607                     : FrameGenerator::Make(sink.get(), fmt, scale_matrix).release();
608 
609             if (gen && anim) {
610                 anim->seekFrame(frame0 + i * fps_scale);
611                 gen->generateFrame(anim, SkToSizeT(i));
612             } else {
613                 sink->writeFrame(nullptr, SkToSizeT(i));
614             }
615 
616             frames_ms[i] = ms_since(start);
617         });
618     }
619 
620     sink->finalize(fps);
621     tg.wait();
622 
623 
624     std::sort(frames_ms.begin(), frames_ms.end());
625     double sum = std::accumulate(frames_ms.begin(), frames_ms.end(), 0);
626     printf("Frame time stats: min %gms, med %gms, avg %gms, max %gms, sum %gms\n",
627            frames_ms[0], frames_ms[frame_count/2], sum/frame_count, frames_ms.back(), sum);
628 
629     return 0;
630 }
631