blob: 646d39b3af82f6ceaf1bfca11656329474b65ab3 [file] [edit]
/*
* Copyright 2026 Google LLC
*
* 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/SkData.h"
#include "include/core/SkPicture.h"
#include "include/core/SkRect.h"
#include "include/core/SkStream.h"
#include "src/capture/SkCapture.h"
#include "src/core/SkPicturePriv.h"
#include "src/core/SkRecord.h"
#include "src/core/SkRecordCanvas.h"
#include "src/core/SkRecordDraw.h"
#include "tools/flags/CommandLineFlags.h"
#include <cstdio>
#include <cstdlib>
#include <filesystem>
static DEFINE_string2(input, i, "", "Capture file(s) to dump.");
static DEFINE_bool2(assets, a, true, "Include assets in the dump.");
static DEFINE_string2(output, o, "", "Files to output text to");
static std::filesystem::path resolve_path(const char* rawPath) {
std::filesystem::path p(rawPath);
const char* bazel_wd = std::getenv("BUILD_WORKING_DIRECTORY");
if (p.is_relative() && bazel_wd != nullptr) {
p = std::filesystem::path(bazel_wd) / p;
}
return p;
}
// RecordDumper is a functor for inspecting canvas commands via SkRecord::visit()
// (see src/core/SkRecord.h).
//
// Specific overloads (e.g. operator()(const SkRecords::DrawRect&)) customize formatting
// or manage indentation state (Save/Restore). The generic template fallback handles all
// other SkRecords::* types by default via NameOf().
class RecordDumper {
public:
// totalCommands is the number of operations in the record, used to calculate
// the number of digits needed to format the command index column.
RecordDumper(int totalCommands, FILE* output = stdout)
: fDigits(0), fIndent(0), fIndex(0), fOutput(output) {
while (totalCommands > 0) {
totalCommands /= 10;
fDigits++;
}
if (fDigits < 2) fDigits = 2;
}
template <typename T> void operator()(const T& command) { this->printName(NameOf(command)); }
void operator()(const SkRecords::NoOp&) {}
void operator()(const SkRecords::Restore& command) {
if (fIndent > 0) --fIndent;
this->printName(NameOf(command));
}
void operator()(const SkRecords::Save& command) {
this->printName(NameOf(command));
++fIndent;
}
void operator()(const SkRecords::SaveLayer& command) {
this->printName(NameOf(command));
++fIndent;
}
void operator()(const SkRecords::DrawRect& command) {
char buf[128];
snprintf(buf,
sizeof(buf),
"DrawRect [%g, %g, %g, %g]",
command.rect.left(),
command.rect.top(),
command.rect.right(),
command.rect.bottom());
this->printName(buf);
}
void operator()(const SkRecords::DrawOval& command) {
char buf[128];
snprintf(buf,
sizeof(buf),
"DrawOval [%g, %g, %g, %g]",
command.oval.left(),
command.oval.top(),
command.oval.right(),
command.oval.bottom());
this->printName(buf);
}
void operator()(const SkRecords::DrawImage& command) {
char buf[128];
snprintf(buf, sizeof(buf), "DrawImage [%g, %g]", command.left, command.top);
this->printName(buf);
}
void operator()(const SkRecords::DrawImageRect& command) {
char buf[128];
snprintf(buf,
sizeof(buf),
"DrawImageRect dst=[%g, %g, %g, %g]",
command.dst.left(),
command.dst.top(),
command.dst.right(),
command.dst.bottom());
this->printName(buf);
}
void operator()(const SkRecords::DrawPaint& command) {
this->printName("DrawPaint (clear/paint)");
}
void operator()(const SkRecords::ClipRect& command) {
char buf[128];
snprintf(buf,
sizeof(buf),
"ClipRect [%g, %g, %g, %g]",
command.rect.left(),
command.rect.top(),
command.rect.right(),
command.rect.bottom());
this->printName(buf);
}
private:
void printName(const char* name) {
fprintf(fOutput, " [%*d] ", fDigits, fIndex++);
for (int i = 0; i < fIndent; ++i) {
fprintf(fOutput, " ");
}
fprintf(fOutput, "%s\n", name);
return;
}
// NameOf returns the string name of an SkRecord command type.
// It uses the X-macro pattern via SK_RECORD_TYPES (defined in src/core/SkRecord.h):
// SK_RECORD_TYPES(M) expands macro M over all SkRecord command types.
// CASE(U) generates a case label for SkRecords::U##_Type returning its stringified name "#U".
template <typename T> static const char* NameOf(const T&) {
#define CASE(U) \
case SkRecords::U##_Type: \
return #U;
switch (T::kType) { SK_RECORD_TYPES(CASE) }
#undef CASE
return "Unknown";
}
int fDigits; // Number of digits for formatting the command index column.
int fIndent; // Current indentation level to represent Save/SaveLayer nesting depth.
int fIndex; // Zero-based index of the current command being dumped.
FILE* fOutput; // The file to write the output to. If null, print to std-out
};
void dump_assets(SkCapture::Metadata meta, sk_sp<SkCapture> capture, FILE* output = stdout) {
fprintf(output,
"--------------------------------------------------------------------------------\n");
fprintf(output, "ASSETS SECTION (%u assets):\n", meta.numAssets);
fprintf(output,
"--------------------------------------------------------------------------------\n");
for (uint32_t i = 0; i < meta.numAssets; ++i) {
sk_sp<SkPicture> picture = capture->getAsset(i);
if (!picture) {
fprintf(output, " Picture [%u]: <null>\n", i);
continue;
}
SkRect cull = picture->cullRect();
fprintf(output,
" Picture [%u]: Dimensions %dx%d (cull: [%g, %g, %g, %g])\n",
i,
SkScalarCeilToInt(cull.width()),
SkScalarCeilToInt(cull.height()),
cull.left(),
cull.top(),
cull.right(),
cull.bottom());
const int w = SkScalarCeilToInt(cull.width());
const int h = SkScalarCeilToInt(cull.height());
SkRecord record;
SkRecordCanvas recCanvas(&record, w, h);
picture->playback(&recCanvas);
fprintf(output, " Record count: %d operations\n", record.count());
if (record.count() == 0) {
fprintf(output, " (Empty picture - snapped for idle surface)\n");
} else {
RecordDumper dumper(record.count(), output);
for (int j = 0; j < record.count(); ++j) {
record.visit(j, dumper);
}
}
fprintf(output, "\n");
}
}
void dump_timeline(SkCapture::Metadata meta, sk_sp<SkCapture> capture, FILE* output = stdout) {
fprintf(output,
"--------------------------------------------------------------------------------\n");
fprintf(output, "TIMELINE SECTION (%u recordings):\n", meta.numRecordingCaptures);
fprintf(output,
"--------------------------------------------------------------------------------\n");
for (uint32_t i = 0; i < meta.numRecordingCaptures; ++i) {
const SkCapture::RecordingCapture* rec = capture->getRecordingCapture(i);
if (!rec) {
fprintf(output, " Recording [%u]: <null>\n", i);
continue;
}
fprintf(output, " Recording [%u]: %d draw task(s)\n", i, rec->fDrawTasks.size());
for (int j = 0; j < rec->fDrawTasks.size(); ++j) {
uint32_t assetIdx = rec->fDrawTasks[j].fAssetIndex;
sk_sp<SkPicture> pic = capture->getAsset(assetIdx);
if (pic) {
SkRect cull = pic->cullRect();
fprintf(output,
" [Task %d] -> Asset [%u] (Dimensions: %dx%d, cull: [%g, %g, %g, %g], "
"approxOps: %d)\n",
j,
assetIdx,
SkScalarCeilToInt(cull.width()),
SkScalarCeilToInt(cull.height()),
cull.left(),
cull.top(),
cull.right(),
cull.bottom(),
pic->approximateOpCount());
} else {
fprintf(output, " [Task %d] -> Asset [%u] (<null>)\n", j, assetIdx);
}
}
fprintf(output, "\n");
}
}
void dump_capture_file(const char* path, bool include_assets = true, FILE* output = stdout) {
if (!output) output = stdout;
fprintf(output,
"================================================================================\n");
fprintf(output, "CAPTURE FILE: %s\n", path);
fprintf(output,
"================================================================================\n");
sk_sp<SkData> data = SkData::MakeFromFileName(path);
if (!data) {
fprintf(output, "Error: Could not read file '%s'\n\n", path);
return;
}
fprintf(output, "File Size: %zu bytes\n", data->size());
sk_sp<SkCapture> capture = SkCapture::MakeFromData(data);
if (!capture) {
fprintf(output, "Error: Failed to deserialize SkCapture from '%s'\n\n", path);
return;
}
// Metadata Section
SkCapture::Metadata meta = capture->getMetadata();
fprintf(output, "Capture Format Version: %u\n", meta.version);
fprintf(output, "Number of Recorded Assets: %u\n", meta.numAssets);
fprintf(output, "Number of Recording Captures (Timeline): %u\n", meta.numRecordingCaptures);
// Assets Section
if (include_assets) dump_assets(meta, capture, output);
// Timeline Section
dump_timeline(meta, capture, output);
fprintf(output,
"================================================================================\n\n");
}
int main(int argc, char** argv) {
CommandLineFlags::SetUsage(
R"(Dumps the contents and structure of SkCapture files (.capt). dump_capture
Usage:
dump_capture -i <file1.capt> [file2.capt ...]
dump_capture -i <file.capt> --noassets
)");
CommandLineFlags::Parse(argc, argv);
if (FLAGS_input.isEmpty()) {
CommandLineFlags::PrintUsage();
return 1;
}
if (FLAGS_output.isEmpty()) {
for (int i = 0; i < FLAGS_input.size(); ++i) {
std::filesystem::path inPath = resolve_path(FLAGS_input[i]);
dump_capture_file(inPath.c_str(), FLAGS_assets);
}
return 0;
}
if (FLAGS_output.size() != FLAGS_input.size()) {
fprintf(stderr,
"Error: Number of output files must match number of input files : output size "
"'%d' != input size '%d'"
"\n",
FLAGS_output.size(),
FLAGS_input.size());
return 1;
}
for (int i = 0; i < FLAGS_input.size(); ++i) {
std::filesystem::path inPath = resolve_path(FLAGS_input[i]);
std::filesystem::path outPath = resolve_path(FLAGS_output[i]);
FILE* out = fopen(outPath.c_str(), "w");
if (!out) {
fprintf(stderr, "Error: Failed to open output file '%s'\n", outPath.c_str());
return 1;
}
dump_capture_file(inPath.c_str(), FLAGS_assets, out);
if (out != stdout) {
fclose(out);
}
}
return 0;
}