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