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/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(HAVE_VIDEO_ENCODER)
30 #include "experimental/ffmpeg/SkVideoEncoder.h"
31 const char* formats_help = "Output format (png, skp, mp4, or null)";
32 #else
33 const char* formats_help = "Output format (png, skp, or null)";
34 #endif
35
36 static DEFINE_string2(input , i, nullptr, "Input .json file.");
37 static DEFINE_string2(writePath, w, nullptr, "Output directory. Frames are names [0-9]{6}.png.");
38 static DEFINE_string2(format , f, "png" , formats_help);
39
40 static DEFINE_double(t0, 0, "Timeline start [0..1].");
41 static DEFINE_double(t1, 1, "Timeline stop [0..1].");
42 static DEFINE_double(fps, 0, "Decode frames per second (default is animation native fps).");
43
44 static DEFINE_int(width , 800, "Render width.");
45 static DEFINE_int(height, 600, "Render height.");
46 static DEFINE_int(threads, 0, "Number of worker threads (0 -> cores count).");
47
48 namespace {
49
50 static constexpr SkColor kClearColor = SK_ColorWHITE;
51
MakeFrameStream(size_t idx,const char * ext)52 std::unique_ptr<SkFILEWStream> MakeFrameStream(size_t idx, const char* ext) {
53 const auto frame_file = SkStringPrintf("0%06zu.%s", idx, ext);
54 auto stream = std::make_unique<SkFILEWStream>(SkOSPath::Join(FLAGS_writePath[0],
55 frame_file.c_str()).c_str());
56 if (!stream->isValid()) {
57 return nullptr;
58 }
59
60 return stream;
61 }
62
63 class Sink {
64 public:
65 Sink() = default;
66 virtual ~Sink() = default;
67 Sink(const Sink&) = delete;
68 Sink& operator=(const Sink&) = delete;
69
70 virtual SkCanvas* beginFrame(size_t idx) = 0;
71 virtual bool endFrame(size_t idx) = 0;
72 };
73
74 class PNGSink final : public Sink {
75 public:
Make(const SkMatrix & scale_matrix)76 static std::unique_ptr<Sink> Make(const SkMatrix& scale_matrix) {
77 auto surface = SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height);
78 if (!surface) {
79 SkDebugf("Could not allocate a %d x %d surface.\n", FLAGS_width, FLAGS_height);
80 return nullptr;
81 }
82
83 return std::unique_ptr<Sink>(new PNGSink(std::move(surface), scale_matrix));
84 }
85
86 private:
PNGSink(sk_sp<SkSurface> surface,const SkMatrix & scale_matrix)87 PNGSink(sk_sp<SkSurface> surface, const SkMatrix& scale_matrix)
88 : fSurface(std::move(surface)) {
89 fSurface->getCanvas()->concat(scale_matrix);
90 }
91
beginFrame(size_t)92 SkCanvas* beginFrame(size_t) override {
93 auto* canvas = fSurface->getCanvas();
94 canvas->clear(kClearColor);
95 return canvas;
96 }
97
endFrame(size_t idx)98 bool endFrame(size_t idx) override {
99 auto stream = MakeFrameStream(idx, "png");
100 if (!stream) {
101 return false;
102 }
103
104 // Set encoding options to favor speed over size.
105 SkPngEncoder::Options options;
106 options.fZLibLevel = 1;
107 options.fFilterFlags = SkPngEncoder::FilterFlag::kNone;
108
109 sk_sp<SkImage> img = fSurface->makeImageSnapshot();
110 SkPixmap pixmap;
111 return img->peekPixels(&pixmap)
112 && SkPngEncoder::Encode(stream.get(), pixmap, options);
113 }
114
115 const sk_sp<SkSurface> fSurface;
116 };
117
118 class SKPSink final : public Sink {
119 public:
Make(const SkMatrix & scale_matrix)120 static std::unique_ptr<Sink> Make(const SkMatrix& scale_matrix) {
121 return std::unique_ptr<Sink>(new SKPSink(scale_matrix));
122 }
123
124 private:
SKPSink(const SkMatrix & scale_matrix)125 explicit SKPSink(const SkMatrix& scale_matrix)
126 : fScaleMatrix(scale_matrix) {}
127
beginFrame(size_t)128 SkCanvas* beginFrame(size_t) override {
129 auto canvas = fRecorder.beginRecording(FLAGS_width, FLAGS_height);
130 canvas->concat(fScaleMatrix);
131 return canvas;
132 }
133
endFrame(size_t idx)134 bool endFrame(size_t idx) override {
135 auto stream = MakeFrameStream(idx, "skp");
136 if (!stream) {
137 return false;
138 }
139
140 fRecorder.finishRecordingAsPicture()->serialize(stream.get());
141 return true;
142 }
143
144 const SkMatrix fScaleMatrix;
145 SkPictureRecorder fRecorder;
146 };
147
148 class NullSink final : public Sink {
149 public:
Make(const SkMatrix & scale_matrix)150 static std::unique_ptr<Sink> Make(const SkMatrix& scale_matrix) {
151 auto surface = SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height);
152 if (!surface) {
153 SkDebugf("Could not allocate a %d x %d surface.\n", FLAGS_width, FLAGS_height);
154 return nullptr;
155 }
156
157 return std::unique_ptr<Sink>(new NullSink(std::move(surface), scale_matrix));
158 }
159
160 private:
NullSink(sk_sp<SkSurface> surface,const SkMatrix & scale_matrix)161 NullSink(sk_sp<SkSurface> surface, const SkMatrix& scale_matrix)
162 : fSurface(std::move(surface)) {
163 fSurface->getCanvas()->concat(scale_matrix);
164 }
165
beginFrame(size_t)166 SkCanvas* beginFrame(size_t) override {
167 auto* canvas = fSurface->getCanvas();
168 canvas->clear(kClearColor);
169 return canvas;
170 }
171
endFrame(size_t)172 bool endFrame(size_t) override {
173 return true;
174 }
175
176 const sk_sp<SkSurface> fSurface;
177 };
178
179 static std::vector<std::promise<sk_sp<SkImage>>> gMP4Frames;
180
181 struct MP4Sink final : public Sink {
MP4Sink__anon886712ab0111::MP4Sink182 explicit MP4Sink(const SkMatrix& scale_matrix)
183 : fSurface(SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height)) {
184 fSurface->getCanvas()->concat(scale_matrix);
185 }
186
beginFrame__anon886712ab0111::MP4Sink187 SkCanvas* beginFrame(size_t) override {
188 SkCanvas* canvas = fSurface->getCanvas();
189 canvas->clear(kClearColor);
190 return canvas;
191 }
192
endFrame__anon886712ab0111::MP4Sink193 bool endFrame(size_t i) override {
194 if (sk_sp<SkImage> img = fSurface->makeImageSnapshot()) {
195 gMP4Frames[i].set_value(std::move(img));
196 return true;
197 }
198 return false;
199 }
200
201 const sk_sp<SkSurface> fSurface;
202 };
203
204 class Logger final : public skottie::Logger {
205 public:
206 struct LogEntry {
207 SkString fMessage,
208 fJSON;
209 };
210
log(skottie::Logger::Level lvl,const char message[],const char json[])211 void log(skottie::Logger::Level lvl, const char message[], const char json[]) override {
212 auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings;
213 log.push_back({ SkString(message), json ? SkString(json) : SkString() });
214 }
215
report() const216 void report() const {
217 SkDebugf("Animation loaded with %lu error%s, %lu warning%s.\n",
218 fErrors.size(), fErrors.size() == 1 ? "" : "s",
219 fWarnings.size(), fWarnings.size() == 1 ? "" : "s");
220
221 const auto& show = [](const LogEntry& log, const char prefix[]) {
222 SkDebugf("%s%s", prefix, log.fMessage.c_str());
223 if (!log.fJSON.isEmpty())
224 SkDebugf(" : %s", log.fJSON.c_str());
225 SkDebugf("\n");
226 };
227
228 for (const auto& err : fErrors) show(err, " !! ");
229 for (const auto& wrn : fWarnings) show(wrn, " ?? ");
230 }
231
232 private:
233 std::vector<LogEntry> fErrors,
234 fWarnings;
235 };
236
MakeSink(const char * fmt,const SkMatrix & scale_matrix)237 std::unique_ptr<Sink> MakeSink(const char* fmt, const SkMatrix& scale_matrix) {
238 if (0 == strcmp(fmt, "png")) return PNGSink::Make(scale_matrix);
239 if (0 == strcmp(fmt, "skp")) return SKPSink::Make(scale_matrix);
240 if (0 == strcmp(fmt, "null")) return NullSink::Make(scale_matrix);
241 if (0 == strcmp(fmt, "mp4")) return std::make_unique<MP4Sink>(scale_matrix);
242
243 SkDebugf("Unknown format: %s\n", FLAGS_format[0]);
244 return nullptr;
245 }
246
247 } // namespace
248
249 extern bool gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental;
250
main(int argc,char ** argv)251 int main(int argc, char** argv) {
252 gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental = true;
253 CommandLineFlags::Parse(argc, argv);
254 SkAutoGraphics ag;
255
256 if (FLAGS_input.isEmpty() || FLAGS_writePath.isEmpty()) {
257 SkDebugf("Missing required 'input' and 'writePath' args.\n");
258 return 1;
259 }
260
261 if (!FLAGS_format.contains("mp4") && !sk_mkdir(FLAGS_writePath[0])) {
262 return 1;
263 }
264
265 auto logger = sk_make_sp<Logger>();
266 auto rp = skresources::CachingResourceProvider::Make(
267 skresources::DataURIResourceProviderProxy::Make(
268 skresources::FileResourceProvider::Make(SkOSPath::Dirname(FLAGS_input[0]),
269 /*predecode=*/true),
270 /*predecode=*/true));
271 auto data = SkData::MakeFromFileName(FLAGS_input[0]);
272 auto precomp_interceptor =
273 sk_make_sp<skottie_utils::ExternalAnimationPrecompInterceptor>(rp, "__");
274
275 if (!data) {
276 SkDebugf("Could not load %s.\n", FLAGS_input[0]);
277 return 1;
278 }
279
280 // Instantiate an animation on the main thread for two reasons:
281 // - we need to know its duration upfront
282 // - we want to only report parsing errors once
283 auto anim = skottie::Animation::Builder()
284 .setLogger(logger)
285 .setResourceProvider(rp)
286 .make(static_cast<const char*>(data->data()), data->size());
287 if (!anim) {
288 SkDebugf("Could not parse animation: '%s'.\n", FLAGS_input[0]);
289 return 1;
290 }
291
292 const auto scale_matrix = SkMatrix::RectToRect(SkRect::MakeSize(anim->size()),
293 SkRect::MakeIWH(FLAGS_width, FLAGS_height),
294 SkMatrix::kCenter_ScaleToFit);
295 logger->report();
296
297 const auto t0 = SkTPin(FLAGS_t0, 0.0, 1.0),
298 t1 = SkTPin(FLAGS_t1, t0, 1.0),
299 native_fps = anim->fps(),
300 frame0 = anim->duration() * t0 * native_fps,
301 duration = anim->duration() * (t1 - t0);
302
303 double fps = FLAGS_fps > 0 ? FLAGS_fps : native_fps;
304 if (fps <= 0) {
305 SkDebugf("Invalid fps: %f.\n", fps);
306 return 1;
307 }
308
309 auto frame_count = static_cast<int>(duration * fps);
310 static constexpr int kMaxFrames = 10000;
311 if (frame_count > kMaxFrames) {
312 frame_count = kMaxFrames;
313 fps = frame_count / duration;
314 }
315 const auto fps_scale = native_fps / fps;
316
317 SkDebugf("Rendering %f seconds (%d frames @%f fps).\n", duration, frame_count, fps);
318
319 if (FLAGS_format.contains("mp4")) {
320 gMP4Frames.resize(frame_count);
321 }
322
323 std::vector<double> frames_ms(frame_count);
324
325 auto ms_since = [](auto start) {
326 const auto elapsed = std::chrono::steady_clock::now() - start;
327 return std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
328 };
329
330 SkTaskGroup::Enabler enabler(FLAGS_threads - 1);
331
332 SkTaskGroup tg;
333 tg.batch(frame_count, [&](int i) {
334 // SkTaskGroup::Enabler creates a LIFO work pool,
335 // but we want our early frames to start first.
336 i = frame_count - 1 - i;
337
338 const auto start = std::chrono::steady_clock::now();
339 #if defined(SK_BUILD_FOR_IOS)
340 // iOS doesn't support thread_local on versions less than 9.0.
341 auto anim = skottie::Animation::Builder()
342 .setResourceProvider(rp)
343 .setPrecompInterceptor(precomp_interceptor)
344 .make(static_cast<const char*>(data->data()), data->size());
345 auto sink = MakeSink(FLAGS_format[0], scale_matrix);
346 #else
347 thread_local static auto* anim =
348 skottie::Animation::Builder()
349 .setResourceProvider(rp)
350 .setPrecompInterceptor(precomp_interceptor)
351 .make(static_cast<const char*>(data->data()), data->size())
352 .release();
353 thread_local static auto* sink = MakeSink(FLAGS_format[0], scale_matrix).release();
354 #endif
355
356 if (sink && anim) {
357 anim->seekFrame(frame0 + i * fps_scale);
358 anim->render(sink->beginFrame(i));
359 sink->endFrame(i);
360 }
361
362 frames_ms[i] = ms_since(start);
363 });
364
365 #if defined(HAVE_VIDEO_ENCODER)
366 if (FLAGS_format.contains("mp4")) {
367 SkVideoEncoder enc;
368 if (!enc.beginRecording({FLAGS_width, FLAGS_height}, fps)) {
369 SkDEBUGF("Invalid video stream configuration.\n");
370 return -1;
371 }
372
373 std::vector<double> starved_ms;
374 for (std::promise<sk_sp<SkImage>>& frame : gMP4Frames) {
375 const auto start = std::chrono::steady_clock::now();
376 sk_sp<SkImage> img = frame.get_future().get();
377 starved_ms.push_back(ms_since(start));
378
379 SkPixmap pm;
380 SkAssertResult(img->peekPixels(&pm));
381 enc.addFrame(pm);
382 }
383 sk_sp<SkData> mp4 = enc.endRecording();
384
385 SkFILEWStream{FLAGS_writePath[0]}
386 .write(mp4->data(), mp4->size());
387
388 // If everything's going well, the first frame should account for the most,
389 // and ideally nearly all, starvation.
390 double first = starved_ms[0];
391 std::sort(starved_ms.begin(), starved_ms.end());
392 double sum = std::accumulate(starved_ms.begin(), starved_ms.end(), 0);
393 SkDebugf("starved min %gms, med %gms, avg %gms, max %gms, sum %gms, first %gms (%s)\n",
394 starved_ms[0], starved_ms[frame_count/2], sum/frame_count, starved_ms.back(), sum,
395 first, first == starved_ms.back() ? "ok" : "BAD");
396 }
397 #endif
398 tg.wait();
399
400 std::sort(frames_ms.begin(), frames_ms.end());
401 double sum = std::accumulate(frames_ms.begin(), frames_ms.end(), 0);
402 SkDebugf("frame time min %gms, med %gms, avg %gms, max %gms, sum %gms\n",
403 frames_ms[0], frames_ms[frame_count/2], sum/frame_count, frames_ms.back(), sum);
404 return 0;
405 }
406