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