| /* |
| * 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; |
| } |
| } |