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