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