| // |
| // Copyright 2025 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // FrameCaptureCommon.cpp: |
| // ANGLE Frame capture implementation for both GL and CL. |
| // |
| |
| #include "libANGLE/capture/FrameCapture.h" |
| |
| #define USE_SYSTEM_ZLIB |
| #include "compression_utils_portable.h" |
| |
| namespace angle |
| { |
| |
| std::string GetBinaryDataFilePath(bool compression, const std::string &captureLabel) |
| { |
| std::stringstream fnameStream; |
| fnameStream << FmtCapturePrefix(kNoContextId, captureLabel) << ".angledata"; |
| if (compression) |
| { |
| fnameStream << ".gz"; |
| } |
| return fnameStream.str(); |
| } |
| |
| void SaveBinaryData(bool compression, |
| const std::string &outDir, |
| gl::ContextID contextId, |
| const std::string &captureLabel, |
| const std::vector<uint8_t> &binaryData) |
| { |
| std::string binaryDataFileName = GetBinaryDataFilePath(compression, captureLabel); |
| std::string dataFilepath = outDir + binaryDataFileName; |
| |
| SaveFileHelper saveData(dataFilepath); |
| |
| if (compression) |
| { |
| // Save compressed data. |
| uLong uncompressedSize = static_cast<uLong>(binaryData.size()); |
| uLong expectedCompressedSize = zlib_internal::GzipExpectedCompressedSize(uncompressedSize); |
| |
| std::vector<uint8_t> compressedData(expectedCompressedSize, 0); |
| |
| uLong compressedSize = expectedCompressedSize; |
| int zResult = zlib_internal::GzipCompressHelper(compressedData.data(), &compressedSize, |
| binaryData.data(), uncompressedSize, |
| nullptr, nullptr); |
| |
| if (zResult != Z_OK) |
| { |
| FATAL() << "Error compressing binary data: " << zResult; |
| } |
| |
| saveData.write(compressedData.data(), compressedSize); |
| } |
| else |
| { |
| saveData.write(binaryData.data(), binaryData.size()); |
| } |
| } |
| |
| template <> |
| void WriteInlineData<GLchar>(const std::vector<uint8_t> &vec, std::ostream &out) |
| { |
| const GLchar *data = reinterpret_cast<const GLchar *>(vec.data()); |
| size_t count = vec.size() / sizeof(GLchar); |
| |
| if (data == nullptr || data[0] == '\0') |
| { |
| return; |
| } |
| |
| out << "\""; |
| |
| for (size_t dataIndex = 0; dataIndex < count; ++dataIndex) |
| { |
| if (data[dataIndex] == '\0') |
| break; |
| |
| out << static_cast<GLchar>(data[dataIndex]); |
| } |
| |
| out << "\""; |
| } |
| |
| void WriteBinaryParamReplay(ReplayWriter &replayWriter, |
| std::ostream &out, |
| std::ostream &header, |
| const CallCapture &call, |
| const ParamCapture ¶m, |
| std::vector<uint8_t> *binaryData) |
| { |
| std::string varName = replayWriter.getInlineVariableName(call.entryPoint, param.name); |
| |
| ASSERT(param.data.size() == 1); |
| const std::vector<uint8_t> &data = param.data[0]; |
| |
| // Only inline strings (shaders) to simplify the C code. |
| ParamType overrideType = param.type; |
| if (param.type == ParamType::TvoidConstPointer) |
| { |
| overrideType = ParamType::TGLubyteConstPointer; |
| } |
| if (overrideType == ParamType::TGLcharPointer || overrideType == ParamType::TcharConstPointer) |
| { |
| // Inline if data is of type string |
| std::string paramTypeString = ParamTypeToString(param.type); |
| header << paramTypeString.substr(0, paramTypeString.length() - 1) << varName << "[] = { "; |
| WriteInlineData<GLchar>(data, header); |
| header << " };\n"; |
| out << varName; |
| } |
| else |
| { |
| // Store in binary file if data are not of type string |
| // Round up to 16-byte boundary for cross ABI safety |
| size_t offset = rx::roundUpPow2(binaryData->size(), kBinaryAlignment); |
| binaryData->resize(offset + data.size()); |
| memcpy(binaryData->data() + offset, data.data(), data.size()); |
| out << "(" << ParamTypeToString(overrideType) << ")&gBinaryData[" << offset << "]"; |
| } |
| } |
| |
| void WriteStringPointerParamReplay(ReplayWriter &replayWriter, |
| std::ostream &out, |
| std::ostream &header, |
| const CallCapture &call, |
| const ParamCapture ¶m) |
| { |
| // Concatenate the strings to ensure we get an accurate counter |
| std::vector<std::string> strings; |
| for (const std::vector<uint8_t> &data : param.data) |
| { |
| // null terminate C style string |
| ASSERT(data.size() > 0 && data.back() == '\0'); |
| strings.emplace_back(data.begin(), data.end() - 1); |
| } |
| |
| bool isNewEntry = false; |
| std::string varName = replayWriter.getInlineStringSetVariableName(call.entryPoint, param.name, |
| strings, &isNewEntry); |
| |
| if (isNewEntry) |
| { |
| header << "const char *" << (replayWriter.captureAPI == CaptureAPI::CL ? " " : "const ") |
| << varName << "[] = { \n"; |
| |
| for (const std::string &str : strings) |
| { |
| // Break up long strings for MSVC |
| size_t copyLength = 0; |
| std::string separator; |
| for (size_t i = 0; i < str.length(); i += kStringLengthLimit) |
| { |
| if ((str.length() - i) <= kStringLengthLimit) |
| { |
| copyLength = str.length() - i; |
| separator = ","; |
| } |
| else |
| { |
| copyLength = kStringLengthLimit; |
| separator = ""; |
| } |
| |
| header << FmtMultiLineString(str.substr(i, copyLength)) << separator << "\n"; |
| } |
| } |
| |
| header << "};\n"; |
| } |
| |
| out << varName; |
| } |
| |
| void WriteComment(std::ostream &out, const CallCapture &call) |
| { |
| // Read the string parameter |
| const ParamCapture &stringParam = |
| call.params.getParam("comment", ParamType::TGLcharConstPointer, 0); |
| const std::vector<uint8_t> &data = stringParam.data[0]; |
| ASSERT(data.size() > 0 && data.back() == '\0'); |
| std::string str(data.begin(), data.end() - 1); |
| |
| // Write the string prefixed with single line comment |
| out << "// " << str; |
| } |
| |
| std::string EscapeString(const std::string &string) |
| { |
| std::stringstream strstr; |
| |
| for (char c : string) |
| { |
| if (c == '\"' || c == '\\') |
| { |
| strstr << "\\"; |
| } |
| strstr << c; |
| } |
| |
| return strstr.str(); |
| } |
| |
| std::ostream &operator<<(std::ostream &os, gl::ContextID contextId) |
| { |
| os << static_cast<int>(contextId.value); |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &os, const FmtCapturePrefix &fmt) |
| { |
| if (fmt.captureLabel.empty()) |
| { |
| os << "angle_capture"; |
| } |
| else |
| { |
| os << fmt.captureLabel; |
| } |
| |
| if (fmt.contextId == kSharedContextId) |
| { |
| os << "_shared"; |
| } |
| |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &os, FuncUsage usage) |
| { |
| os << "("; |
| if (usage != FuncUsage::Call) |
| { |
| os << "void"; |
| } |
| os << ")"; |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &os, const FmtReplayFunction &fmt) |
| { |
| os << "Replay"; |
| |
| if (fmt.contextId == kSharedContextId) |
| { |
| os << "Shared"; |
| } |
| |
| os << "Frame" << fmt.frameIndex; |
| |
| if (fmt.partId != kNoPartId) |
| { |
| os << "Part" << fmt.partId; |
| } |
| os << fmt.usage; |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &os, const FmtSetupFunction &fmt) |
| { |
| os << "SetupReplay"; |
| |
| if (fmt.contextId != kNoContextId) |
| { |
| os << "Context"; |
| } |
| |
| if (fmt.contextId == kSharedContextId) |
| { |
| os << "Shared"; |
| } |
| else |
| { |
| os << fmt.contextId; |
| } |
| |
| if (fmt.partId != kNoPartId) |
| { |
| os << "Part" << fmt.partId; |
| } |
| os << fmt.usage; |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &os, const FmtSetupFirstFrameFunction &fmt) |
| { |
| os << "SetupFirstFrame()"; |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &os, const FmtSetupInactiveFunction &fmt) |
| { |
| if ((fmt.usage == FuncUsage::Call) && (fmt.partId == kNoPartId)) |
| { |
| os << "if (gReplayResourceMode == angle::ReplayResourceMode::All)\n {\n "; |
| } |
| os << "SetupReplay"; |
| |
| if (fmt.contextId != kNoContextId) |
| { |
| os << "Context"; |
| } |
| |
| if (fmt.contextId == kSharedContextId) |
| { |
| os << "Shared"; |
| } |
| else |
| { |
| os << fmt.contextId; |
| } |
| |
| os << "Inactive"; |
| |
| if (fmt.partId != kNoPartId) |
| { |
| os << "Part" << fmt.partId; |
| } |
| |
| os << fmt.usage; |
| |
| if ((fmt.usage == FuncUsage::Call) && (fmt.partId == kNoPartId)) |
| { |
| os << ";\n }"; |
| } |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &os, const FmtResetFunction &fmt) |
| { |
| os << "ResetReplayContext"; |
| |
| if (fmt.contextId == kSharedContextId) |
| { |
| os << "Shared"; |
| } |
| else |
| { |
| os << fmt.contextId; |
| } |
| |
| if (fmt.partId != kNoPartId) |
| { |
| os << "Part" << fmt.partId; |
| } |
| os << fmt.usage; |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &os, const FmtFunction &fmt) |
| { |
| switch (fmt.funcType) |
| { |
| case ReplayFunc::Replay: |
| os << FmtReplayFunction(fmt.contextId, fmt.usage, fmt.frameIndex, fmt.partId); |
| break; |
| |
| case ReplayFunc::Setup: |
| os << FmtSetupFunction(fmt.partId, fmt.contextId, fmt.usage); |
| break; |
| |
| case ReplayFunc::SetupInactive: |
| os << FmtSetupInactiveFunction(fmt.partId, fmt.contextId, fmt.usage); |
| break; |
| |
| case ReplayFunc::Reset: |
| os << FmtResetFunction(fmt.partId, fmt.contextId, fmt.usage); |
| break; |
| |
| case ReplayFunc::SetupFirstFrame: |
| os << FmtSetupFirstFrameFunction(fmt.partId); |
| break; |
| |
| default: |
| UNREACHABLE(); |
| break; |
| } |
| |
| return os; |
| } |
| |
| std::ostream &operator<<(std::ostream &ostr, const FmtMultiLineString &fmt) |
| { |
| ASSERT(!fmt.strings.empty()); |
| bool first = true; |
| for (const std::string &string : fmt.strings) |
| { |
| if (first) |
| { |
| first = false; |
| } |
| else |
| { |
| ostr << "\\n\"\n"; |
| } |
| |
| ostr << "\"" << EscapeString(string); |
| } |
| |
| ostr << "\""; |
| |
| return ostr; |
| } |
| |
| std::string GetDefaultOutDirectory() |
| { |
| #if defined(ANGLE_PLATFORM_ANDROID) |
| std::string path = "/sdcard/Android/data/"; |
| |
| // Linux interface to get application id of the running process |
| FILE *cmdline = fopen("/proc/self/cmdline", "r"); |
| char applicationId[512]; |
| if (cmdline) |
| { |
| fread(applicationId, 1, sizeof(applicationId), cmdline); |
| fclose(cmdline); |
| |
| // Some package may have application id as <app_name>:<cmd_name> |
| char *colonSep = strchr(applicationId, ':'); |
| if (colonSep) |
| { |
| *colonSep = '\0'; |
| } |
| } |
| else |
| { |
| ERR() << "not able to lookup application id"; |
| } |
| |
| constexpr char kAndroidOutputSubdir[] = "/angle_capture/"; |
| path += std::string(applicationId) + kAndroidOutputSubdir; |
| |
| // Check for existence of output path |
| struct stat dir_stat; |
| if (stat(path.c_str(), &dir_stat) == -1) |
| { |
| ERR() << "Output directory '" << path |
| << "' does not exist. Create it over adb using mkdir."; |
| } |
| |
| return path; |
| #else |
| return std::string("./"); |
| #endif // defined(ANGLE_PLATFORM_ANDROID) |
| } |
| |
| FrameCapture::FrameCapture() = default; |
| FrameCapture::~FrameCapture() = default; |
| |
| void FrameCapture::reset() |
| { |
| mSetupCalls.clear(); |
| } |
| |
| FrameCaptureShared::FrameCaptureShared() |
| : mEnabled(true), |
| mSerializeStateEnabled(false), |
| mCompression(true), |
| mClientVertexArrayMap{}, |
| mFrameIndex(1), |
| mCaptureStartFrame(1), |
| mCaptureEndFrame(0), |
| mClientArraySizes{}, |
| mReadBufferSize(0), |
| mResourceIDBufferSize(0), |
| mHasResourceType{}, |
| mResourceIDToSetupCalls{}, |
| mMaxAccessedResourceIDs{}, |
| mCaptureTrigger(0), |
| mCaptureActive(false), |
| mWindowSurfaceContextID({0}) |
| { |
| reset(); |
| |
| std::string enabledFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kEnabledVarName, kAndroidEnabled); |
| if (enabledFromEnv == "0") |
| { |
| mEnabled = false; |
| } |
| |
| std::string startFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kFrameStartVarName, kAndroidFrameStart); |
| if (!startFromEnv.empty()) |
| { |
| mCaptureStartFrame = atoi(startFromEnv.c_str()); |
| } |
| if (mCaptureStartFrame < 1) |
| { |
| WARN() << "Cannot use a capture start frame less than 1."; |
| mCaptureStartFrame = 1; |
| } |
| |
| std::string endFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kFrameEndVarName, kAndroidFrameEnd); |
| if (!endFromEnv.empty()) |
| { |
| mCaptureEndFrame = atoi(endFromEnv.c_str()); |
| } |
| |
| std::string captureTriggerFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kTriggerVarName, kAndroidTrigger); |
| if (!captureTriggerFromEnv.empty()) |
| { |
| mCaptureTrigger = atoi(captureTriggerFromEnv.c_str()); |
| |
| // Using capture trigger, initialize frame range variables for MEC |
| resetCaptureStartEndFrames(); |
| } |
| |
| std::string labelFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kCaptureLabelVarName, kAndroidCaptureLabel); |
| // --angle-per-test-capture-label sets the env var, not properties |
| if (labelFromEnv.empty()) |
| { |
| labelFromEnv = GetEnvironmentVar(kCaptureLabelVarName); |
| } |
| if (!labelFromEnv.empty()) |
| { |
| // Optional label to provide unique file names and namespaces |
| mCaptureLabel = labelFromEnv; |
| } |
| |
| std::string compressionFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kCompressionVarName, kAndroidCompression); |
| if (compressionFromEnv == "0") |
| { |
| mCompression = false; |
| } |
| std::string serializeStateFromEnv = angle::GetEnvironmentVar(kSerializeStateVarName); |
| if (serializeStateFromEnv == "1") |
| { |
| mSerializeStateEnabled = true; |
| } |
| |
| std::string validateSerialiedStateFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kValidationVarName, kAndroidValidation); |
| if (validateSerialiedStateFromEnv == "1") |
| { |
| mValidateSerializedState = true; |
| } |
| |
| mValidationExpression = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kValidationExprVarName, kAndroidValidationExpr); |
| |
| if (!mValidationExpression.empty()) |
| { |
| INFO() << "Validation expression is " << kValidationExprVarName; |
| } |
| |
| // TODO: Remove. http://anglebug.com/42266223 |
| std::string sourceExtFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kSourceExtVarName, kAndroidSourceExt); |
| if (!sourceExtFromEnv.empty()) |
| { |
| if (sourceExtFromEnv == "c" || sourceExtFromEnv == "cpp") |
| { |
| mReplayWriter.setSourceFileExtension(sourceExtFromEnv.c_str()); |
| } |
| else |
| { |
| WARN() << "Invalid capture source extension: " << sourceExtFromEnv; |
| } |
| } |
| |
| std::string sourceSizeFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kSourceSizeVarName, kAndroidSourceSize); |
| if (!sourceSizeFromEnv.empty()) |
| { |
| int sourceSize = atoi(sourceSizeFromEnv.c_str()); |
| if (sourceSize < 0) |
| { |
| WARN() << "Invalid capture source size: " << sourceSize; |
| } |
| else |
| { |
| mReplayWriter.setSourceFileSizeThreshold(sourceSize); |
| } |
| } |
| |
| std::string forceShadowFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kForceShadowVarName, kAndroidForceShadow); |
| if (forceShadowFromEnv == "1") |
| { |
| INFO() << "Force enabling shadow memory for coherent buffer tracking."; |
| mCoherentBufferTracker.enableShadowMemory(); |
| } |
| |
| if (mFrameIndex == mCaptureStartFrame) |
| { |
| // Capture is starting from the first frame, so set the capture active to ensure all GLES |
| // commands issued are handled correctly by maybeCapturePreCallUpdates() and |
| // maybeCapturePostCallUpdates(). |
| setCaptureActive(); |
| } |
| |
| if (mCaptureEndFrame < mCaptureStartFrame) |
| { |
| // If we're still in a situation where start frame is after end frame, |
| // capture cannot happen. Consider this a disabled state. |
| // Note: We won't get here if trigger is in use, as it sets them equal but huge. |
| mEnabled = false; |
| } |
| |
| // Special case the output directory |
| if (mEnabled) |
| { |
| // Only perform output directory checks if enabled |
| // - This can avoid some expensive process name and filesystem checks |
| // - We want to emit errors if the directory doesn't exist |
| getOutputDirectory(); |
| } |
| |
| mMaxCLParamsSize[ParamType::Tcl_device_idPointer] = 0; |
| mMaxCLParamsSize[ParamType::Tcl_context] = 0; |
| mMaxCLParamsSize[ParamType::Tcl_platform_idPointer] = 0; |
| mMaxCLParamsSize[ParamType::Tcl_command_queue] = 0; |
| mMaxCLParamsSize[ParamType::Tcl_program] = 0; |
| mMaxCLParamsSize[ParamType::Tcl_kernel] = 0; |
| mMaxCLParamsSize[ParamType::Tcl_mem] = 0; |
| mMaxCLParamsSize[ParamType::Tcl_eventPointer] = 0; |
| mMaxCLParamsSize[ParamType::Tcl_sampler] = 0; |
| mMaxCLParamsSize[ParamType::TvoidPointer] = 0; |
| } |
| |
| FrameCaptureShared::~FrameCaptureShared() {} |
| |
| bool FrameCaptureShared::isCapturing() const |
| { |
| // Currently we will always do a capture up until the last frame. In the future we could improve |
| // mid execution capture by only capturing between the start and end frames. The only necessary |
| // reason we need to capture before the start is for attached program and shader sources. |
| return mEnabled; |
| } |
| |
| uint32_t FrameCaptureShared::getFrameCount() const |
| { |
| return mCaptureEndFrame - mCaptureStartFrame + 1; |
| } |
| |
| uint32_t FrameCaptureShared::getReplayFrameIndex() const |
| { |
| return mFrameIndex - mCaptureStartFrame + 1; |
| } |
| |
| bool FrameCaptureShared::isRuntimeEnabled() |
| { |
| if (!mRuntimeEnabled && mRuntimeInitialized) |
| { |
| return false; |
| } |
| if (mRuntimeEnabled) |
| { |
| return true; |
| } |
| |
| uint32_t mCaptureStartFrame = 1; |
| uint32_t mCaptureEndFrame = 0; |
| std::string enabledFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kEnabledVarName, kAndroidEnabled); |
| |
| std::string startFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kFrameStartVarName, kAndroidFrameStart); |
| if (!startFromEnv.empty()) |
| { |
| mCaptureStartFrame = atoi(startFromEnv.c_str()); |
| } |
| if (mCaptureStartFrame < 1) |
| { |
| mCaptureStartFrame = 1; |
| } |
| |
| std::string endFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kFrameEndVarName, kAndroidFrameEnd); |
| if (!endFromEnv.empty()) |
| { |
| mCaptureEndFrame = atoi(endFromEnv.c_str()); |
| } |
| |
| uint32_t mCaptureTrigger = 0; |
| std::string captureTriggerFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kTriggerVarName, kAndroidTrigger); |
| if (!captureTriggerFromEnv.empty()) |
| { |
| mCaptureTrigger = atoi(captureTriggerFromEnv.c_str()); |
| } |
| |
| mRuntimeEnabled = |
| enabledFromEnv != "0" && |
| (mCaptureTrigger || (mCaptureEndFrame != 0 && mCaptureEndFrame >= mCaptureStartFrame)); |
| |
| mRuntimeInitialized = true; |
| return mRuntimeEnabled; |
| } |
| |
| void FrameCaptureShared::reset() |
| { |
| mFrameCalls.clear(); |
| mClientVertexArrayMap.fill(-1); |
| |
| // Do not reset replay-specific settings like the maximum read buffer size, client array sizes, |
| // or the 'has seen' type map. We could refine this into per-frame and per-capture maximums if |
| // necessary. |
| } |
| |
| // This function will clear FrameCaptureShared state so that mid-execution capture can be |
| // run multiple times. |
| void FrameCaptureShared::resetMidExecutionCapture(gl::Context *context) |
| { |
| for (ResourceIDType resourceID : AllEnums<ResourceIDType>()) |
| { |
| mResourceIDToSetupCalls[resourceID].clear(); |
| } |
| |
| egl::ShareGroup *shareGroup = context->getShareGroup(); |
| for (auto shareContext : shareGroup->getContexts()) |
| { |
| FrameCapture *frameCapture = shareContext.second->getFrameCapture(); |
| frameCapture->reset(); |
| frameCapture->getStateResetHelper().reset(); |
| } |
| |
| mActiveFrameIndices.clear(); |
| mWroteIndexFile = false; |
| std::fill(std::begin(mClientArraySizes), std::end(mClientArraySizes), 0); |
| mReadBufferSize = 0; |
| mResourceIDBufferSize = 0; |
| mHasResourceType.Zero(); |
| mBufferDataMap.clear(); |
| mMaxAccessedResourceIDs.fill(0); |
| mResourceTracker.resetResourceTracking(); |
| mReplayWriter.reset(); |
| mShareGroupSetupCalls.clear(); |
| mDeferredLinkPrograms.clear(); |
| mActiveContexts.clear(); |
| } |
| |
| // ReplayWriter implementation. |
| ReplayWriter::ReplayWriter() |
| : mSourceFileExtension(kDefaultSourceFileExt), |
| mSourceFileSizeThreshold(kDefaultSourceFileSizeThreshold), |
| mFrameIndex(1) |
| {} |
| |
| ReplayWriter::~ReplayWriter() |
| { |
| ASSERT(mPrivateFunctionPrototypes.empty()); |
| ASSERT(mPublicFunctionPrototypes.empty()); |
| ASSERT(mPrivateFunctions.empty()); |
| ASSERT(mPublicFunctions.empty()); |
| ASSERT(mGlobalVariableDeclarations.empty()); |
| ASSERT(mStaticVariableDeclarations.empty()); |
| ASSERT(mReplayHeaders.empty()); |
| } |
| |
| void ReplayWriter::setSourceFileExtension(const char *ext) |
| { |
| mSourceFileExtension = ext; |
| } |
| |
| void ReplayWriter::setSourceFileSizeThreshold(size_t sourceFileSizeThreshold) |
| { |
| mSourceFileSizeThreshold = sourceFileSizeThreshold; |
| } |
| |
| void ReplayWriter::setFilenamePattern(const std::string &pattern) |
| { |
| if (mFilenamePattern != pattern) |
| { |
| mFilenamePattern = pattern; |
| } |
| } |
| |
| void ReplayWriter::setSourcePrologue(const std::string &prologue) |
| { |
| mSourcePrologue = prologue; |
| } |
| |
| void ReplayWriter::setHeaderPrologue(const std::string &prologue) |
| { |
| mHeaderPrologue = prologue; |
| } |
| |
| void ReplayWriter::addPublicFunction(const std::string &functionProto, |
| const std::stringstream &headerStream, |
| const std::stringstream &bodyStream) |
| { |
| mPublicFunctionPrototypes.push_back(functionProto); |
| |
| std::string header = headerStream.str(); |
| std::string body = bodyStream.str(); |
| |
| if (!header.empty()) |
| { |
| mReplayHeaders.emplace_back(header); |
| } |
| |
| if (!body.empty()) |
| { |
| mPublicFunctions.emplace_back(body); |
| } |
| } |
| |
| void ReplayWriter::addPrivateFunction(const std::string &functionProto, |
| const std::stringstream &headerStream, |
| const std::stringstream &bodyStream) |
| { |
| mPrivateFunctionPrototypes.push_back(functionProto); |
| |
| std::string header = headerStream.str(); |
| std::string body = bodyStream.str(); |
| |
| if (!header.empty()) |
| { |
| mReplayHeaders.emplace_back(header); |
| } |
| |
| if (!body.empty()) |
| { |
| mPrivateFunctions.emplace_back(body); |
| } |
| } |
| |
| std::string ReplayWriter::getInlineVariableName(EntryPoint entryPoint, const std::string ¶mName) |
| { |
| int counter = mDataTracker.getCounters().getAndIncrement(entryPoint, paramName); |
| return GetVarName(entryPoint, paramName, counter); |
| } |
| |
| int DataCounters::getAndIncrement(EntryPoint entryPoint, const std::string ¶mName) |
| { |
| Counter counterKey = {entryPoint, paramName}; |
| return mData[counterKey]++; |
| } |
| |
| int StringCounters::getStringCounter(const std::vector<std::string> &strings) |
| { |
| const auto &id = mStringCounterMap.find(strings); |
| if (id == mStringCounterMap.end()) |
| { |
| return kStringsNotFound; |
| } |
| else |
| { |
| return mStringCounterMap[strings]; |
| } |
| } |
| |
| void StringCounters::setStringCounter(const std::vector<std::string> &strings, int &counter) |
| { |
| ASSERT(counter >= 0); |
| mStringCounterMap[strings] = counter; |
| } |
| |
| StringCounters::StringCounters() = default; |
| |
| StringCounters::~StringCounters() = default; |
| |
| DataCounters::DataCounters() = default; |
| |
| DataCounters::~DataCounters() = default; |
| |
| DataTracker::DataTracker() = default; |
| |
| DataTracker::~DataTracker() = default; |
| |
| std::string ReplayWriter::getInlineStringSetVariableName(EntryPoint entryPoint, |
| const std::string ¶mName, |
| const std::vector<std::string> &strings, |
| bool *isNewEntryOut) |
| { |
| int counter = mDataTracker.getStringCounters().getStringCounter(strings); |
| *isNewEntryOut = (counter == kStringsNotFound); |
| if (*isNewEntryOut) |
| { |
| // This is a unique set of strings, so set up their declaration and update the counter |
| counter = mDataTracker.getCounters().getAndIncrement(entryPoint, paramName); |
| mDataTracker.getStringCounters().setStringCounter(strings, counter); |
| |
| std::string varName = GetVarName(entryPoint, paramName, counter); |
| |
| std::stringstream declStream; |
| declStream << "const char *" << (captureAPI == CaptureAPI::CL ? " " : "const ") << varName |
| << "[]"; |
| std::string decl = declStream.str(); |
| |
| mGlobalVariableDeclarations.push_back(decl); |
| |
| return varName; |
| } |
| else |
| { |
| return GetVarName(entryPoint, paramName, counter); |
| } |
| } |
| |
| void ReplayWriter::addStaticVariable(const std::string &customVarType, |
| const std::string &customVarName) |
| { |
| std::string decl = customVarType + " " + customVarName; |
| mStaticVariableDeclarations.push_back(decl); |
| } |
| |
| size_t ReplayWriter::getStoredReplaySourceSize() const |
| { |
| size_t sum = 0; |
| for (const std::string &header : mReplayHeaders) |
| { |
| sum += header.size(); |
| } |
| for (const std::string &publicFunc : mPublicFunctions) |
| { |
| sum += publicFunc.size(); |
| } |
| for (const std::string &privateFunc : mPrivateFunctions) |
| { |
| sum += privateFunc.size(); |
| } |
| return sum; |
| } |
| |
| // static |
| std::string ReplayWriter::GetVarName(EntryPoint entryPoint, |
| const std::string ¶mName, |
| int counter) |
| { |
| std::stringstream strstr; |
| strstr << GetEntryPointName(entryPoint) << "_" << paramName << "_" << counter; |
| return strstr.str(); |
| } |
| |
| void ReplayWriter::saveFrame() |
| { |
| if (mReplayHeaders.empty() && mPublicFunctions.empty() && mPrivateFunctions.empty()) |
| { |
| return; |
| } |
| |
| ASSERT(!mSourceFileExtension.empty()); |
| |
| std::stringstream strstr; |
| strstr << mFilenamePattern << "_" << std::setfill('0') << std::setw(3) << mFrameIndex << "." |
| << mSourceFileExtension; |
| |
| std::string frameFilePath = strstr.str(); |
| |
| if (captureAPI == CaptureAPI::GL) |
| { |
| ++mFrameIndex; |
| } |
| |
| writeReplaySource(frameFilePath); |
| } |
| |
| void ReplayWriter::saveFrameIfFull() |
| { |
| if (getStoredReplaySourceSize() < mSourceFileSizeThreshold) |
| { |
| INFO() << "Merging captured frame: " << getStoredReplaySourceSize() |
| << " less than threshold of " << mSourceFileSizeThreshold << " bytes"; |
| return; |
| } |
| |
| saveFrame(); |
| } |
| |
| void ReplayWriter::saveHeader() |
| { |
| std::stringstream headerPathStream; |
| headerPathStream << mFilenamePattern << ".h"; |
| std::string headerPath = headerPathStream.str(); |
| |
| SaveFileHelper saveH(headerPath); |
| |
| saveH << mHeaderPrologue << "\n"; |
| |
| saveH << "// Public functions are declared in " |
| << (captureAPI == CaptureAPI::GL ? "trace_fixture.h.\n" : "trace_fixture_cl.h.\n"); |
| saveH << "\n"; |
| saveH << "// Private Functions\n"; |
| saveH << "\n"; |
| |
| for (const std::string &proto : mPrivateFunctionPrototypes) |
| { |
| saveH << proto << ";\n"; |
| } |
| |
| saveH << "\n"; |
| saveH << "// Global variables\n"; |
| saveH << "\n"; |
| |
| for (const std::string &globalVar : mGlobalVariableDeclarations) |
| { |
| saveH << "extern " << globalVar << ";\n"; |
| } |
| |
| for (const std::string &staticVar : mStaticVariableDeclarations) |
| { |
| saveH << "static " << staticVar << ";\n"; |
| } |
| |
| mPublicFunctionPrototypes.clear(); |
| mPrivateFunctionPrototypes.clear(); |
| mGlobalVariableDeclarations.clear(); |
| mStaticVariableDeclarations.clear(); |
| |
| addWrittenFile(headerPath); |
| } |
| |
| void ReplayWriter::saveIndexFilesAndHeader() |
| { |
| ASSERT(!mSourceFileExtension.empty()); |
| |
| std::stringstream sourcePathStream; |
| sourcePathStream << mFilenamePattern << "." << mSourceFileExtension; |
| std::string sourcePath = sourcePathStream.str(); |
| |
| writeReplaySource(sourcePath); |
| saveHeader(); |
| } |
| |
| void ReplayWriter::saveSetupFile() |
| { |
| ASSERT(!mSourceFileExtension.empty()); |
| |
| std::stringstream strstr; |
| strstr << mFilenamePattern << "." << mSourceFileExtension; |
| |
| std::string frameFilePath = strstr.str(); |
| |
| writeReplaySource(frameFilePath); |
| } |
| |
| void ReplayWriter::writeReplaySource(const std::string &filename) |
| { |
| SaveFileHelper saveCpp(filename); |
| |
| saveCpp << mSourcePrologue << "\n"; |
| for (const std::string &header : mReplayHeaders) |
| { |
| saveCpp << header << "\n"; |
| } |
| |
| saveCpp << "// Private Functions\n"; |
| saveCpp << "\n"; |
| |
| for (const std::string &func : mPrivateFunctions) |
| { |
| saveCpp << func << "\n"; |
| } |
| |
| saveCpp << "// Public Functions\n"; |
| saveCpp << "\n"; |
| |
| if (mFilenamePattern == "cpp") |
| { |
| saveCpp << "extern \"C\"\n"; |
| saveCpp << "{\n"; |
| } |
| |
| for (const std::string &func : mPublicFunctions) |
| { |
| saveCpp << func << "\n"; |
| } |
| |
| if (mFilenamePattern == "cpp") |
| { |
| saveCpp << "} // extern \"C\"\n"; |
| } |
| |
| mReplayHeaders.clear(); |
| mPrivateFunctions.clear(); |
| mPublicFunctions.clear(); |
| |
| addWrittenFile(filename); |
| } |
| |
| std::string GetBaseName(const std::string &nameWithPath) |
| { |
| std::vector<std::string> result = angle::SplitString( |
| nameWithPath, "/\\", WhitespaceHandling::TRIM_WHITESPACE, SplitResult::SPLIT_WANT_NONEMPTY); |
| ASSERT(!result.empty()); |
| return result.back(); |
| } |
| |
| void ReplayWriter::addWrittenFile(const std::string &filename) |
| { |
| std::string writtenFile = GetBaseName(filename); |
| ASSERT(std::find(mWrittenFiles.begin(), mWrittenFiles.end(), writtenFile) == |
| mWrittenFiles.end()); |
| mWrittenFiles.push_back(writtenFile); |
| } |
| |
| std::vector<std::string> ReplayWriter::getAndResetWrittenFiles() |
| { |
| std::vector<std::string> results = std::move(mWrittenFiles); |
| std::sort(results.begin(), results.end()); |
| ASSERT(mWrittenFiles.empty()); |
| return results; |
| } |
| |
| void AddComment(std::vector<CallCapture> *outCalls, const std::string &comment) |
| { |
| ParamBuffer commentParamBuffer; |
| ParamCapture commentParam("comment", ParamType::TGLcharConstPointer); |
| CaptureString(comment.c_str(), &commentParam); |
| commentParamBuffer.addParam(std::move(commentParam)); |
| outCalls->emplace_back("Comment", std::move(commentParamBuffer)); |
| } |
| |
| bool FrameCaptureShared::mRuntimeEnabled = false; |
| bool FrameCaptureShared::mRuntimeInitialized = false; |
| |
| void FrameCaptureShared::getOutputDirectory() |
| { |
| std::string pathFromEnv = |
| GetEnvironmentVarOrUnCachedAndroidProperty(kOutDirectoryVarName, kAndroidOutDir); |
| if (pathFromEnv.empty()) |
| { |
| mOutDirectory = GetDefaultOutDirectory(); |
| } |
| else |
| { |
| mOutDirectory = pathFromEnv; |
| } |
| |
| // Ensure the capture path ends with a slash. |
| if (mOutDirectory.back() != '\\' && mOutDirectory.back() != '/') |
| { |
| mOutDirectory += '/'; |
| } |
| } |
| |
| void CaptureMemory(const void *source, size_t size, ParamCapture *paramCapture) |
| { |
| std::vector<uint8_t> data(size); |
| memcpy(data.data(), source, size); |
| paramCapture->data.emplace_back(std::move(data)); |
| } |
| |
| void CaptureString(const GLchar *str, ParamCapture *paramCapture) |
| { |
| // include the '\0' suffix |
| CaptureMemory(str, strlen(str) + 1, paramCapture); |
| } |
| |
| TrackedResource::TrackedResource() = default; |
| |
| TrackedResource::~TrackedResource() = default; |
| |
| ResourceTracker::ResourceTracker() = default; |
| |
| ResourceTracker::~ResourceTracker() = default; |
| |
| StateResetHelper::StateResetHelper() = default; |
| |
| StateResetHelper::~StateResetHelper() = default; |
| |
| CoherentBufferTracker::CoherentBufferTracker() |
| : mEnabled(false), mHasBeenReset(false), mShadowMemoryEnabled(false) |
| { |
| mPageSize = GetPageSize(); |
| } |
| |
| CoherentBufferTracker::~CoherentBufferTracker() |
| { |
| disable(); |
| } |
| |
| void CoherentBufferTracker::disable() |
| { |
| if (!mEnabled) |
| { |
| return; |
| } |
| |
| if (mPageFaultHandler->disable()) |
| { |
| mEnabled = false; |
| } |
| else |
| { |
| ERR() << "Could not disable page fault handler."; |
| } |
| |
| if (mShadowMemoryEnabled && mBuffers.size() > 0) |
| { |
| WARN() << "Disabling coherent buffer tracking while leaving shadow memory without " |
| "synchronization. Expect rendering artifacts after capture ends."; |
| } |
| } |
| |
| } // namespace angle |