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