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