blob: 80c91d4a3608c0edab557a30fc73b10b4f5e6561 [file] [log] [blame]
/*
* Copyright 2018 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkCanvas.h"
#include "include/core/SkGraphics.h"
#include "include/core/SkPictureRecorder.h"
#include "include/core/SkStream.h"
#include "include/core/SkSurface.h"
#include "include/encode/SkPngEncoder.h"
#include "include/private/SkTPin.h"
#include "modules/skottie/include/Skottie.h"
#include "modules/skottie/utils/SkottieUtils.h"
#include "modules/skresources/include/SkResources.h"
#include "src/core/SkOSFile.h"
#include "src/core/SkTaskGroup.h"
#include "src/utils/SkOSPath.h"
#include "tools/flags/CommandLineFlags.h"
#include <algorithm>
#include <chrono>
#include <future>
#include <numeric>
#include <vector>
#if defined(HAVE_VIDEO_ENCODER)
#include "experimental/ffmpeg/SkVideoEncoder.h"
const char* formats_help = "Output format (png, skp, mp4, or null)";
#else
const char* formats_help = "Output format (png, skp, or null)";
#endif
static DEFINE_string2(input , i, nullptr, "Input .json file.");
static DEFINE_string2(writePath, w, nullptr, "Output directory. Frames are names [0-9]{6}.png.");
static DEFINE_string2(format , f, "png" , formats_help);
static DEFINE_double(t0, 0, "Timeline start [0..1].");
static DEFINE_double(t1, 1, "Timeline stop [0..1].");
static DEFINE_double(fps, 0, "Decode frames per second (default is animation native fps).");
static DEFINE_int(width , 800, "Render width.");
static DEFINE_int(height, 600, "Render height.");
static DEFINE_int(threads, 0, "Number of worker threads (0 -> cores count).");
namespace {
static constexpr SkColor kClearColor = SK_ColorWHITE;
std::unique_ptr<SkFILEWStream> MakeFrameStream(size_t idx, const char* ext) {
const auto frame_file = SkStringPrintf("0%06zu.%s", idx, ext);
auto stream = std::make_unique<SkFILEWStream>(SkOSPath::Join(FLAGS_writePath[0],
frame_file.c_str()).c_str());
if (!stream->isValid()) {
return nullptr;
}
return stream;
}
class Sink {
public:
Sink() = default;
virtual ~Sink() = default;
Sink(const Sink&) = delete;
Sink& operator=(const Sink&) = delete;
virtual SkCanvas* beginFrame(size_t idx) = 0;
virtual bool endFrame(size_t idx) = 0;
};
class PNGSink final : public Sink {
public:
static std::unique_ptr<Sink> Make(const SkMatrix& scale_matrix) {
auto surface = SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height);
if (!surface) {
SkDebugf("Could not allocate a %d x %d surface.\n", FLAGS_width, FLAGS_height);
return nullptr;
}
return std::unique_ptr<Sink>(new PNGSink(std::move(surface), scale_matrix));
}
private:
PNGSink(sk_sp<SkSurface> surface, const SkMatrix& scale_matrix)
: fSurface(std::move(surface)) {
fSurface->getCanvas()->concat(scale_matrix);
}
SkCanvas* beginFrame(size_t) override {
auto* canvas = fSurface->getCanvas();
canvas->clear(kClearColor);
return canvas;
}
bool endFrame(size_t idx) override {
auto stream = MakeFrameStream(idx, "png");
if (!stream) {
return false;
}
// Set encoding options to favor speed over size.
SkPngEncoder::Options options;
options.fZLibLevel = 1;
options.fFilterFlags = SkPngEncoder::FilterFlag::kNone;
sk_sp<SkImage> img = fSurface->makeImageSnapshot();
SkPixmap pixmap;
return img->peekPixels(&pixmap)
&& SkPngEncoder::Encode(stream.get(), pixmap, options);
}
const sk_sp<SkSurface> fSurface;
};
class SKPSink final : public Sink {
public:
static std::unique_ptr<Sink> Make(const SkMatrix& scale_matrix) {
return std::unique_ptr<Sink>(new SKPSink(scale_matrix));
}
private:
explicit SKPSink(const SkMatrix& scale_matrix)
: fScaleMatrix(scale_matrix) {}
SkCanvas* beginFrame(size_t) override {
auto canvas = fRecorder.beginRecording(FLAGS_width, FLAGS_height);
canvas->concat(fScaleMatrix);
return canvas;
}
bool endFrame(size_t idx) override {
auto stream = MakeFrameStream(idx, "skp");
if (!stream) {
return false;
}
fRecorder.finishRecordingAsPicture()->serialize(stream.get());
return true;
}
const SkMatrix fScaleMatrix;
SkPictureRecorder fRecorder;
};
class NullSink final : public Sink {
public:
static std::unique_ptr<Sink> Make(const SkMatrix& scale_matrix) {
auto surface = SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height);
if (!surface) {
SkDebugf("Could not allocate a %d x %d surface.\n", FLAGS_width, FLAGS_height);
return nullptr;
}
return std::unique_ptr<Sink>(new NullSink(std::move(surface), scale_matrix));
}
private:
NullSink(sk_sp<SkSurface> surface, const SkMatrix& scale_matrix)
: fSurface(std::move(surface)) {
fSurface->getCanvas()->concat(scale_matrix);
}
SkCanvas* beginFrame(size_t) override {
auto* canvas = fSurface->getCanvas();
canvas->clear(kClearColor);
return canvas;
}
bool endFrame(size_t) override {
return true;
}
const sk_sp<SkSurface> fSurface;
};
static std::vector<std::promise<sk_sp<SkImage>>> gMP4Frames;
struct MP4Sink final : public Sink {
explicit MP4Sink(const SkMatrix& scale_matrix)
: fSurface(SkSurface::MakeRasterN32Premul(FLAGS_width, FLAGS_height)) {
fSurface->getCanvas()->concat(scale_matrix);
}
SkCanvas* beginFrame(size_t) override {
SkCanvas* canvas = fSurface->getCanvas();
canvas->clear(kClearColor);
return canvas;
}
bool endFrame(size_t i) override {
if (sk_sp<SkImage> img = fSurface->makeImageSnapshot()) {
gMP4Frames[i].set_value(std::move(img));
return true;
}
return false;
}
const sk_sp<SkSurface> fSurface;
};
class Logger final : public skottie::Logger {
public:
struct LogEntry {
SkString fMessage,
fJSON;
};
void log(skottie::Logger::Level lvl, const char message[], const char json[]) override {
auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings;
log.push_back({ SkString(message), json ? SkString(json) : SkString() });
}
void report() const {
SkDebugf("Animation loaded with %lu error%s, %lu warning%s.\n",
fErrors.size(), fErrors.size() == 1 ? "" : "s",
fWarnings.size(), fWarnings.size() == 1 ? "" : "s");
const auto& show = [](const LogEntry& log, const char prefix[]) {
SkDebugf("%s%s", prefix, log.fMessage.c_str());
if (!log.fJSON.isEmpty())
SkDebugf(" : %s", log.fJSON.c_str());
SkDebugf("\n");
};
for (const auto& err : fErrors) show(err, " !! ");
for (const auto& wrn : fWarnings) show(wrn, " ?? ");
}
private:
std::vector<LogEntry> fErrors,
fWarnings;
};
std::unique_ptr<Sink> MakeSink(const char* fmt, const SkMatrix& scale_matrix) {
if (0 == strcmp(fmt, "png")) return PNGSink::Make(scale_matrix);
if (0 == strcmp(fmt, "skp")) return SKPSink::Make(scale_matrix);
if (0 == strcmp(fmt, "null")) return NullSink::Make(scale_matrix);
if (0 == strcmp(fmt, "mp4")) return std::make_unique<MP4Sink>(scale_matrix);
SkDebugf("Unknown format: %s\n", FLAGS_format[0]);
return nullptr;
}
} // namespace
extern bool gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental;
int main(int argc, char** argv) {
gSkUseThreadLocalStrikeCaches_IAcknowledgeThisIsIncrediblyExperimental = true;
CommandLineFlags::Parse(argc, argv);
SkAutoGraphics ag;
if (FLAGS_input.isEmpty() || FLAGS_writePath.isEmpty()) {
SkDebugf("Missing required 'input' and 'writePath' args.\n");
return 1;
}
if (!FLAGS_format.contains("mp4") && !sk_mkdir(FLAGS_writePath[0])) {
return 1;
}
auto logger = sk_make_sp<Logger>();
auto rp = skresources::CachingResourceProvider::Make(
skresources::DataURIResourceProviderProxy::Make(
skresources::FileResourceProvider::Make(SkOSPath::Dirname(FLAGS_input[0]),
/*predecode=*/true),
/*predecode=*/true));
auto data = SkData::MakeFromFileName(FLAGS_input[0]);
auto precomp_interceptor =
sk_make_sp<skottie_utils::ExternalAnimationPrecompInterceptor>(rp, "__");
if (!data) {
SkDebugf("Could not load %s.\n", FLAGS_input[0]);
return 1;
}
// Instantiate an animation on the main thread for two reasons:
// - we need to know its duration upfront
// - we want to only report parsing errors once
auto anim = skottie::Animation::Builder()
.setLogger(logger)
.setResourceProvider(rp)
.make(static_cast<const char*>(data->data()), data->size());
if (!anim) {
SkDebugf("Could not parse animation: '%s'.\n", FLAGS_input[0]);
return 1;
}
const auto scale_matrix = SkMatrix::RectToRect(SkRect::MakeSize(anim->size()),
SkRect::MakeIWH(FLAGS_width, FLAGS_height),
SkMatrix::kCenter_ScaleToFit);
logger->report();
const auto t0 = SkTPin(FLAGS_t0, 0.0, 1.0),
t1 = SkTPin(FLAGS_t1, t0, 1.0),
native_fps = anim->fps(),
frame0 = anim->duration() * t0 * native_fps,
duration = anim->duration() * (t1 - t0);
double fps = FLAGS_fps > 0 ? FLAGS_fps : native_fps;
if (fps <= 0) {
SkDebugf("Invalid fps: %f.\n", fps);
return 1;
}
auto frame_count = static_cast<int>(duration * fps);
static constexpr int kMaxFrames = 10000;
if (frame_count > kMaxFrames) {
frame_count = kMaxFrames;
fps = frame_count / duration;
}
const auto fps_scale = native_fps / fps;
SkDebugf("Rendering %f seconds (%d frames @%f fps).\n", duration, frame_count, fps);
if (FLAGS_format.contains("mp4")) {
gMP4Frames.resize(frame_count);
}
std::vector<double> frames_ms(frame_count);
auto ms_since = [](auto start) {
const auto elapsed = std::chrono::steady_clock::now() - start;
return std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
};
SkTaskGroup::Enabler enabler(FLAGS_threads - 1);
SkTaskGroup tg;
tg.batch(frame_count, [&](int i) {
// SkTaskGroup::Enabler creates a LIFO work pool,
// but we want our early frames to start first.
i = frame_count - 1 - i;
const auto start = std::chrono::steady_clock::now();
#if defined(SK_BUILD_FOR_IOS)
// iOS doesn't support thread_local on versions less than 9.0.
auto anim = skottie::Animation::Builder()
.setResourceProvider(rp)
.setPrecompInterceptor(precomp_interceptor)
.make(static_cast<const char*>(data->data()), data->size());
auto sink = MakeSink(FLAGS_format[0], scale_matrix);
#else
thread_local static auto* anim =
skottie::Animation::Builder()
.setResourceProvider(rp)
.setPrecompInterceptor(precomp_interceptor)
.make(static_cast<const char*>(data->data()), data->size())
.release();
thread_local static auto* sink = MakeSink(FLAGS_format[0], scale_matrix).release();
#endif
if (sink && anim) {
anim->seekFrame(frame0 + i * fps_scale);
anim->render(sink->beginFrame(i));
sink->endFrame(i);
}
frames_ms[i] = ms_since(start);
});
#if defined(HAVE_VIDEO_ENCODER)
if (FLAGS_format.contains("mp4")) {
SkVideoEncoder enc;
if (!enc.beginRecording({FLAGS_width, FLAGS_height}, fps)) {
SkDEBUGF("Invalid video stream configuration.\n");
return -1;
}
std::vector<double> starved_ms;
for (std::promise<sk_sp<SkImage>>& frame : gMP4Frames) {
const auto start = std::chrono::steady_clock::now();
sk_sp<SkImage> img = frame.get_future().get();
starved_ms.push_back(ms_since(start));
SkPixmap pm;
SkAssertResult(img->peekPixels(&pm));
enc.addFrame(pm);
}
sk_sp<SkData> mp4 = enc.endRecording();
SkFILEWStream{FLAGS_writePath[0]}
.write(mp4->data(), mp4->size());
// If everything's going well, the first frame should account for the most,
// and ideally nearly all, starvation.
double first = starved_ms[0];
std::sort(starved_ms.begin(), starved_ms.end());
double sum = std::accumulate(starved_ms.begin(), starved_ms.end(), 0);
SkDebugf("starved min %gms, med %gms, avg %gms, max %gms, sum %gms, first %gms (%s)\n",
starved_ms[0], starved_ms[frame_count/2], sum/frame_count, starved_ms.back(), sum,
first, first == starved_ms.back() ? "ok" : "BAD");
}
#endif
tg.wait();
std::sort(frames_ms.begin(), frames_ms.end());
double sum = std::accumulate(frames_ms.begin(), frames_ms.end(), 0);
SkDebugf("frame time min %gms, med %gms, avg %gms, max %gms, sum %gms\n",
frames_ms[0], frames_ms[frame_count/2], sum/frame_count, frames_ms.back(), sum);
return 0;
}