blob: 9666cab056987c13d21d6d7b3d981dbf4bcdc2f4 [file]
/*
* Copyright 2025 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "tools/viewer/CaptureSlide.h"
#include "imgui.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkClipOp.h"
#include "include/core/SkData.h"
#include "include/core/SkPicture.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkStream.h"
#include "include/core/SkString.h"
#include "include/private/SkDebug.h"
#include "include/private/SkLog.h"
#include "src/capture/SkCapture.h"
#include "src/core/SkRecord.h"
#include "src/utils/SkJSONWriter.h"
#include "tools/ProcsUtils.h"
#include "tools/UrlDataManager.h"
#include "tools/debugger/DebugCanvas.h"
#include "tools/debugger/DrawCommand.h"
#include <string>
const char* RecordTypeNames[] = {
#define CASE(T) #T,
SK_RECORD_TYPES(CASE)
#undef CASE
};
const int JSON_TEXT_BOX_HEIGHT = 200;
// A width of -1 means use all available width
const int JSON_TEXT_BOX_WIDTH = -1;
const float EXPLORER_DEFAULT_WIDTH = 450.0f;
const float EXPLORER_DEFAULT_HEIGHT = 500.0f;
const float EXPLORER_DEFAULT_POS_X = 20.0f;
const float EXPLORER_DEFAULT_POS_Y = 20.0f;
CaptureSlide::CaptureSlide(const SkString& name, const SkString& path) {
auto data = SkData::MakeFromFileName(path.c_str());
fCapture = SkCapture::MakeFromData(data);
if (fCapture) {
fMetadata = fCapture->getMetadata();
this->updateActiveAssetIndex();
} else {
SKIA_LOG_E("Couldn't load capture %s", path.c_str());
}
}
CaptureSlide::~CaptureSlide() {}
void drawCommandHistory(const DebugCanvas* debugCanvas) {
ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_Appearing);
if (!ImGui::Begin("Command History")) {
ImGui::End();
return;
}
ImGui::Text("Total Commands: %d", debugCanvas->getSize());
ImGui::Separator();
ImGui::BeginChild("CommandList", ImVec2(0, 0), true);
for (int i = 0; i < debugCanvas->getSize(); ++i) {
DrawCommand* cmd = debugCanvas->getDrawCommandAt(i);
const char* commandName = DrawCommand::GetCommandString(cmd->getOpType());
ImGui::PushID(i);
if (ImGui::TreeNode(commandName, "%d: %s", i, commandName)) {
SkDynamicMemoryWStream stream;
SkJSONWriter writer(
&stream, ToolUtils::default_serial_procs(), SkJSONWriter::Mode::kPretty);
UrlDataManager urlDataManager(SkString("data"));
writer.beginObject();
cmd->toJSON(writer, urlDataManager);
writer.endObject();
writer.flush();
stream.write8(0);
sk_sp<SkData> data = stream.detachAsData();
char* dataString = const_cast<char*>(static_cast<const char*>(data->data()));
ImGui::InputTextMultiline("###json_output",
dataString,
data->size(),
ImVec2(JSON_TEXT_BOX_WIDTH, JSON_TEXT_BOX_HEIGHT),
ImGuiInputTextFlags_ReadOnly);
ImGui::TreePop();
}
ImGui::PopID();
}
ImGui::EndChild();
ImGui::End();
}
void drawRecordingTimelineTab(SkCapture* capture,
int& currentAssetIndex,
int& currentRecordingCaptureIndex,
int& currentDrawTaskIndex,
bool& invalidate) {
ImGui::Text("Total Recordings: %u", capture->getMetadata().numRecordingCaptures);
ImGui::Separator();
ImGui::Columns(2, "TimelineSplit", true);
// Column 0: Recordings List
ImGui::Text("Recordings:");
ImGui::BeginChild("RecordingList", ImVec2(0, 300), true);
for (int i = 0; i < (int)capture->getMetadata().numRecordingCaptures; ++i) {
const auto* rec = capture->getRecordingCapture(i);
if (!rec) continue;
SkString label = SkStringPrintf("Recording #%d", i);
const bool isSelected = (currentRecordingCaptureIndex == i);
if (ImGui::Selectable(label.c_str(), isSelected)) {
if (currentRecordingCaptureIndex != i) {
currentRecordingCaptureIndex = i;
currentDrawTaskIndex = 0; // reset draw task selection
invalidate = true;
}
}
}
ImGui::EndChild();
ImGui::NextColumn();
// Column 1: DrawTasks List
const auto* activeRec = capture->getRecordingCapture(currentRecordingCaptureIndex);
if (activeRec) {
ImGui::Text("DrawTasks (%d):", (int)activeRec->fDrawTasks.size());
ImGui::BeginChild("DrawTaskList", ImVec2(0, 300), true);
for (int j = 0; j < (int)activeRec->fDrawTasks.size(); ++j) {
const auto& task = activeRec->fDrawTasks[j];
uint32_t assetIdx = task.fAssetIndex;
auto pic = capture->getAsset(assetIdx);
SkString label;
if (pic) {
label = SkStringPrintf("Task #%d: Asset %u (%dx%d)",
j, assetIdx, (int)pic->cullRect().width(), (int)pic->cullRect().height());
} else {
label = SkStringPrintf("Task #%d: Asset %u (Null)", j, assetIdx);
}
const bool isSelected = (currentDrawTaskIndex == j);
if (ImGui::Selectable(label.c_str(), isSelected)) {
if (currentDrawTaskIndex != j) {
currentDrawTaskIndex = j;
invalidate = true;
}
}
}
ImGui::EndChild();
// Update currentAssetIndex to point to the selected sub-picture's asset index
if (currentDrawTaskIndex >= 0 && currentDrawTaskIndex < (int)activeRec->fDrawTasks.size()) {
currentAssetIndex = activeRec->fDrawTasks[currentDrawTaskIndex].fAssetIndex;
} else {
currentAssetIndex = -1;
}
} else {
ImGui::Text("No active recording selected.");
}
ImGui::Columns(1); // Reset back to single column
}
void drawFlatAssetCatalogTab(SkCapture* capture,
int& currentAssetIndex,
bool& invalidate) {
ImGui::Text("Total Picture Assets: %u", capture->getMetadata().numAssets);
ImGui::Separator();
ImGui::BeginChild("FlatCatalogList", ImVec2(0, 0), true);
for (int i = 0; i < (int)capture->getMetadata().numAssets; ++i) {
auto pic = capture->getAsset(i);
SkString label;
if (pic) {
label = SkStringPrintf("Asset Index %d (%dx%d)",
i, (int)pic->cullRect().width(), (int)pic->cullRect().height());
} else {
label = SkStringPrintf("Asset Index %d (Null)", i);
}
const bool isSelected = (currentAssetIndex == i);
if (ImGui::Selectable(label.c_str(), isSelected)) {
if (currentAssetIndex != i) {
currentAssetIndex = i;
invalidate = true;
}
}
}
ImGui::EndChild();
}
void drawCaptureExplorer(SkCapture* capture,
int& currentAssetIndex,
int& currentRecordingCaptureIndex,
int& currentDrawTaskIndex,
bool& invalidate) {
ImGui::SetNextWindowSize(ImVec2(EXPLORER_DEFAULT_WIDTH, EXPLORER_DEFAULT_HEIGHT),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(EXPLORER_DEFAULT_POS_X, EXPLORER_DEFAULT_POS_Y),
ImGuiCond_FirstUseEver);
if (!ImGui::Begin("SkCapture Explorer")) {
ImGui::End();
return;
}
if (ImGui::BeginTabBar("ExplorerTabBar")) {
if (ImGui::BeginTabItem("Recording Timeline")) {
drawRecordingTimelineTab(capture,
currentAssetIndex,
currentRecordingCaptureIndex,
currentDrawTaskIndex,
invalidate);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Flat Asset Catalog")) {
drawFlatAssetCatalogTab(capture, currentAssetIndex, invalidate);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
}
void CaptureSlide::draw(SkCanvas* canvas) {
if (!fCapture) {
return;
}
// drawCaptureExplorer may update fCurrentAssetIndex if the user clicks on a different asset.
// If so, fDebugCanvas must be reset with the newly focused draw commands.
int prevAssetIndex = fCurrentAssetIndex;
drawCaptureExplorer(fCapture.get(),
fCurrentAssetIndex,
fCurrentRecordingCaptureIndex,
fCurrentDrawTaskIndex,
fInvalidate);
if (prevAssetIndex != fCurrentAssetIndex) {
fDebugCanvas.reset();
}
auto focusPicture = fCapture->getAsset(fCurrentAssetIndex);
if (!focusPicture) {
// TODO handle this case with an elegant UI popup.
return;
}
auto bounds = focusPicture->cullRect().roundOut();
if (!fDebugCanvas) {
fDebugCanvas = std::make_unique<DebugCanvas>(bounds.width(), bounds.height());
focusPicture->playback(fDebugCanvas.get());
}
canvas->save();
canvas->clipIRect(bounds, SkClipOp::kIntersect);
canvas->drawPicture(focusPicture);
canvas->restore();
drawCommandHistory(fDebugCanvas.get());
}
bool CaptureSlide::animate(double) {
if (fInvalidate) {
fInvalidate = false;
return true;
}
return fInvalidate;
}
void CaptureSlide::load(SkScalar, SkScalar) {}
void CaptureSlide::unload() { fCapture.reset(nullptr); }
SkISize CaptureSlide::getDimensions() const { return {0, 0}; }
bool CaptureSlide::onChar(SkUnichar c) {
switch (c) {
case 'N': {
int numSubs = fMetadata.numRecordingCaptures;
if (numSubs > 0) {
fCurrentRecordingCaptureIndex = (fCurrentRecordingCaptureIndex + 1) % numSubs;
fCurrentDrawTaskIndex = 0;
this->updateActiveAssetIndex();
fInvalidate = true;
fDebugCanvas.reset();
}
return true;
}
case 'P': {
int numSubs = fMetadata.numRecordingCaptures;
if (numSubs > 0) {
fCurrentRecordingCaptureIndex = (fCurrentRecordingCaptureIndex + numSubs - 1)
% numSubs;
fCurrentDrawTaskIndex = 0;
this->updateActiveAssetIndex();
fInvalidate = true;
fDebugCanvas.reset();
}
return true;
}
case 'n': {
const auto* activeRec = fCapture->getRecordingCapture(fCurrentRecordingCaptureIndex);
if (activeRec && activeRec->fDrawTasks.size() > 0) {
int numPics = activeRec->fDrawTasks.size();
fCurrentDrawTaskIndex = (fCurrentDrawTaskIndex + 1) % numPics;
this->updateActiveAssetIndex();
fInvalidate = true;
fDebugCanvas.reset();
}
return true;
}
case 'p': {
const auto* activeRec = fCapture->getRecordingCapture(fCurrentRecordingCaptureIndex);
if (activeRec && activeRec->fDrawTasks.size() > 0) {
int numPics = activeRec->fDrawTasks.size();
fCurrentDrawTaskIndex = (fCurrentDrawTaskIndex + numPics - 1) % numPics;
this->updateActiveAssetIndex();
fInvalidate = true;
fDebugCanvas.reset();
}
return true;
}
case 'F':
ImGui::GetIO().FontGlobalScale = ImGui::GetIO().FontGlobalScale + 0.1f;
return true;
case 'S':
ImGui::GetIO().FontGlobalScale = ImGui::GetIO().FontGlobalScale - 0.1f;
return true;
}
return Slide::onChar(c);
}
void CaptureSlide::updateActiveAssetIndex() {
if (!fCapture) {
return;
}
const auto* activeRec = fCapture->getRecordingCapture(fCurrentRecordingCaptureIndex);
if (activeRec && !activeRec->fDrawTasks.empty()) {
fCurrentDrawTaskIndex = SkTPin(fCurrentDrawTaskIndex, 0, activeRec->fDrawTasks.size() - 1);
fCurrentAssetIndex = activeRec->fDrawTasks[fCurrentDrawTaskIndex].fAssetIndex;
} else {
fCurrentAssetIndex = -1;
}
}