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