diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ada2df3ce8..f9de29a53b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,7 @@ jobs: compiler: gcc compiler_version: "14" python: 3.13 + extended_build_perfetto: ON - name: Linux_GCC_14_Python314 os: ubuntu-24.04 @@ -206,6 +207,9 @@ jobs: if [ "${{ matrix.extended_build_mdl_sdk }}" == "ON" -a "${{ runner.os }}" == "Windows" ]; then EXTENDED_BUILD_CONFIG="$EXTENDED_BUILD_CONFIG -DVCPKG_TARGET_TRIPLET=x64-windows-release -DMATERIALX_MDL_SDK_DIR=C:/vcpkg/installed/x64-windows-release" fi + if [ "${{ matrix.extended_build_perfetto }}" == "ON" ]; then + EXTENDED_BUILD_CONFIG="$EXTENDED_BUILD_CONFIG -DMATERIALX_BUILD_PERFETTO_TRACING=ON" + fi fi TEST_RENDER_CONFIG="-DMATERIALX_TEST_RENDER=OFF" if [ "${{ matrix.test_render }}" == "ON" ]; then @@ -266,7 +270,7 @@ jobs: run: | sudo apt-get install gcovr mkdir coverage - gcovr --html --html-details --output coverage/index.html --exclude .*\/External\/.* --root .. . + gcovr --html --html-details --output coverage/index.html --exclude .*\/External\/.* --exclude .*perfetto.* --root .. . working-directory: build - name: Static Analysis Tests @@ -277,7 +281,7 @@ jobs: else brew install cppcheck fi - cppcheck --project=build/compile_commands.json --error-exitcode=1 --suppress=normalCheckLevelMaxBranches --suppress=*:*/External/* --suppress=*:*/NanoGUI/* + cppcheck --project=build/compile_commands.json --error-exitcode=1 --suppress=normalCheckLevelMaxBranches --suppress=*:*/External/* --suppress=*:*/NanoGUI/* --suppress=*:*perfetto* - name: Setup Rendering Environment (Linux) if: matrix.test_render == 'ON' && runner.os == 'Linux' @@ -358,6 +362,14 @@ jobs: name: MaterialX_Coverage path: build/coverage + - name: Upload Perfetto Traces + uses: actions/upload-artifact@v4 + if: matrix.extended_build_perfetto == 'ON' && env.IS_EXTENDED_BUILD == 'true' + with: + name: Traces_${{ matrix.name }} + path: build/**/*.perfetto-trace + if-no-files-found: ignore + javascript: name: JavaScript runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a3aea525a..632c1f97f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ option(MATERIALX_BUILD_OCIO "Build OpenColorIO support for shader generators." O option(MATERIALX_BUILD_TESTS "Build unit tests." OFF) option(MATERIALX_BUILD_BENCHMARK_TESTS "Build benchmark tests." OFF) option(MATERIALX_BUILD_OSOS "Build OSL .oso's of standard library shaders for the OSL Network generator" OFF) +option(MATERIALX_BUILD_PERFETTO_TRACING "Build with Perfetto tracing support for performance analysis." OFF) option(MATERIALX_BUILD_SHARED_LIBS "Build MaterialX libraries as shared rather than static." OFF) option(MATERIALX_BUILD_DATA_LIBRARY "Build generated products from the MaterialX data library." OFF) @@ -228,6 +229,41 @@ mark_as_advanced(MATERIALX_MDL_BINARY_TESTRENDER) mark_as_advanced(MATERIALX_MDL_MODULE_PATHS) mark_as_advanced(MATERIALX_MDL_SDK_DIR) mark_as_advanced(MATERIALX_SLANG_RHI_SOURCE_DIR) +mark_as_advanced(MATERIALX_BUILD_PERFETTO_TRACING) + +# Perfetto tracing support +if(MATERIALX_BUILD_PERFETTO_TRACING) + include(FetchContent) + FetchContent_Declare( + perfetto + GIT_REPOSITORY https://android.googlesource.com/platform/external/perfetto + GIT_TAG v43.0 + GIT_SHALLOW TRUE + ) + # Only fetch the SDK, not the full Perfetto source + set(PERFETTO_SDK_INCLUDE_DIR "${CMAKE_BINARY_DIR}/_deps/perfetto-src/sdk") + FetchContent_MakeAvailable(perfetto) + add_definitions(-DMATERIALX_BUILD_PERFETTO_TRACING) + + # Define compile flags for perfetto.cc (Perfetto SDK triggers various warnings) + # These variables are used in source/CMakeLists.txt (monolithic) and MaterialXTrace (non-monolithic) + # Note: set_source_files_properties is directory-scoped, so we define variables here + # but apply them in the directories where targets are created. + if(MSVC) + set(MATERIALX_PERFETTO_COMPILE_DEFINITIONS "NOMINMAX;WIN32_LEAN_AND_MEAN" CACHE INTERNAL "") + # /bigobj: perfetto.cc has too many sections for default MSVC object format + # /W0: disable all warnings for third-party Perfetto SDK code + # /WX-: disable warnings-as-errors (overrides project-level /WX) + set(MATERIALX_PERFETTO_COMPILE_FLAGS "/bigobj /W0 /WX-" CACHE INTERNAL "") + else() + set(MATERIALX_PERFETTO_COMPILE_DEFINITIONS "" CACHE INTERNAL "") + # -Wno-error: Don't treat warnings as errors (Perfetto has shadowing issues) + # -Wno-shadow: Suppress shadow warnings (kDevNull shadows global) + set(MATERIALX_PERFETTO_COMPILE_FLAGS "-Wno-error -Wno-shadow" CACHE INTERNAL "") + endif() + + message(STATUS "Perfetto tracing support enabled") +endif() if (MATERIALX_BUILD_USE_CCACHE) # Setup CCache for C/C++ compilation @@ -491,6 +527,9 @@ endif() # Add core subdirectories add_subdirectory(source/MaterialXCore) add_subdirectory(source/MaterialXFormat) +if(MATERIALX_BUILD_PERFETTO_TRACING) + add_subdirectory(source/MaterialXTrace) +endif() # Add shader generation subdirectories add_subdirectory(source/MaterialXGenShader) diff --git a/resources/Materials/TestSuite/_options.mtlx b/resources/Materials/TestSuite/_options.mtlx index a5d0623f2d..49b0b7cc39 100644 --- a/resources/Materials/TestSuite/_options.mtlx +++ b/resources/Materials/TestSuite/_options.mtlx @@ -70,5 +70,19 @@ but requiring a more powerful GPU and longer CPU render times. --> + + + + + + diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index e1cd929ff9..49c19ee8b6 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -5,6 +5,15 @@ if (MATERIALX_BUILD_MONOLITHIC) # such that the individual module would have been built if not in monolithic build mode add_library(${MATERIALX_MODULE_NAME} "" "" ) + # Apply Perfetto compile flags for monolithic builds + # (set_source_files_properties is directory-scoped, must be called here) + if(MATERIALX_BUILD_PERFETTO_TRACING) + set_source_files_properties("${perfetto_SOURCE_DIR}/sdk/perfetto.cc" + PROPERTIES + COMPILE_DEFINITIONS "${MATERIALX_PERFETTO_COMPILE_DEFINITIONS}" + COMPILE_FLAGS "${MATERIALX_PERFETTO_COMPILE_FLAGS}") + endif() + set_target_properties(${MATERIALX_MODULE_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) set_target_properties(${MATERIALX_MODULE_NAME} PROPERTIES CMAKE_VISIBILITY_INLINES_HIDDEN 1) diff --git a/source/MaterialXTest/CMakeLists.txt b/source/MaterialXTest/CMakeLists.txt index 87acd64f00..9449a9b830 100644 --- a/source/MaterialXTest/CMakeLists.txt +++ b/source/MaterialXTest/CMakeLists.txt @@ -40,6 +40,9 @@ add_subdirectory(MaterialXCore) target_link_libraries(MaterialXTest MaterialXCore) add_subdirectory(MaterialXFormat) target_link_libraries(MaterialXTest MaterialXFormat) +if(MATERIALX_BUILD_PERFETTO_TRACING) + target_link_libraries(MaterialXTest MaterialXTrace) +endif() add_subdirectory(MaterialXGenShader) target_link_libraries(MaterialXTest MaterialXGenShader) diff --git a/source/MaterialXTest/MaterialXGenShader/GenShaderUtil.cpp b/source/MaterialXTest/MaterialXGenShader/GenShaderUtil.cpp index e5e573bb54..e2a197e1cc 100644 --- a/source/MaterialXTest/MaterialXGenShader/GenShaderUtil.cpp +++ b/source/MaterialXTest/MaterialXGenShader/GenShaderUtil.cpp @@ -23,7 +23,10 @@ #include #include +#include + #include +#include namespace mx = MaterialX; @@ -652,24 +655,36 @@ void ShaderGeneratorTester::registerLights(mx::DocumentPtr doc, const std::vecto void ShaderGeneratorTester::validate(const mx::GenOptions& generateOptions, const std::string& optionsFilePath) { - // Start logging - _logFile.open(_logFilePath); - - // Check for an option file + // Check for an option file first (before opening log) so we can use outputDirectory TestSuiteOptions options; if (!options.readOptions(optionsFilePath)) { - _logFile << "Cannot read options file: " << optionsFilePath << ". Skipping test." << std::endl; - _logFile.close(); + std::cerr << "Cannot read options file: " << optionsFilePath << ". Skipping test." << std::endl; return; } // Test has been turned off so just do nothing. if (!runTest(options)) { - _logFile << "Target: " << _targetString << " not set to run. Skipping test." << std::endl; - _logFile.close(); + std::cerr << "Target: " << _targetString << " not set to run. Skipping test." << std::endl; return; } + +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + // Set up Perfetto tracing if enabled + std::optional tracingGuard; + if (options.enableTracing) + { + mx::FilePath tracePath = options.resolveOutputPath(_shaderGenerator->getTarget() + "_gen_trace.perfetto-trace"); + mx::Tracing::Dispatcher::getInstance().setSink( + mx::Tracing::createPerfettoSink(tracePath.asString())); + tracingGuard.emplace(); + } +#endif + + // Start logging - use outputDirectory if set + mx::FilePath logPath = options.resolveOutputPath(_logFilePath); + _logFile.open(logPath.asString()); + options.print(_logFile); // Add files to override the files in the test suite to be examined. @@ -839,7 +854,11 @@ void ShaderGeneratorTester::validate(const mx::GenOptions& generateOptions, cons _logFile << "------------ Run validation with element: " << namePath << "------------" << std::endl; mx::StringVec sourceCode; - const bool generatedCode = generateCode(context, elementName, element, _logFile, _testStages, sourceCode); + bool generatedCode = false; + { + MX_TRACE_SCOPE(mx::Tracing::Category::ShaderGen, elementName.c_str()); + generatedCode = generateCode(context, elementName, element, _logFile, _testStages, sourceCode); + } // Record implementations tested if (options.checkImplCount) @@ -879,6 +898,17 @@ void ShaderGeneratorTester::validate(const mx::GenOptions& generateOptions, cons path = searchPath.isEmpty() ? mx::FilePath() : searchPath[0]; } + // Redirect to outputDirectory if set + if (!options.outputDirectory.isEmpty()) + { + mx::FilePath materialDir = path.getBaseName(); + path = options.outputDirectory / materialDir; + if (!path.exists()) + { + path.createDirectory(); + } + } + std::vector sourceCodePaths; if (sourceCode.size() > 1) { @@ -939,6 +969,12 @@ void ShaderGeneratorTester::validate(const mx::GenOptions& generateOptions, cons { _logFile.close(); } + + // Print effective output directory for easy access (clickable in terminals) + if (!options.outputDirectory.isEmpty()) + { + std::cout << std::endl << "Test artifacts written to: " << options.outputDirectory.asString() << std::endl; + } } void TestSuiteOptions::print(std::ostream& output) const @@ -968,6 +1004,8 @@ void TestSuiteOptions::print(std::ostream& output) const output << "\tExtra library paths: " << extraLibraryPaths.asString() << std::endl; output << "\tRender test paths: " << renderTestPaths.asString() << std::endl; output << "\tEnable Reference Quality: " << enableReferenceQuality << std::endl; + output << "\tOutput Directory: " << (outputDirectory.isEmpty() ? "(default)" : outputDirectory.asString()) << std::endl; + output << "\tEnable Tracing: " << enableTracing << std::endl; } bool TestSuiteOptions::readOptions(const std::string& optionFile) @@ -993,6 +1031,8 @@ bool TestSuiteOptions::readOptions(const std::string& optionFile) const std::string EXTRA_LIBRARY_PATHS("extraLibraryPaths"); const std::string RENDER_TEST_PATHS("renderTestPaths"); const std::string ENABLE_REFERENCE_QUALITY("enableReferenceQuality"); + const std::string OUTPUT_DIRECTORY_STRING("outputDirectory"); + const std::string ENABLE_TRACING_STRING("enableTracing"); overrideFiles.clear(); dumpGeneratedCode = false; @@ -1091,6 +1131,23 @@ bool TestSuiteOptions::readOptions(const std::string& optionFile) { enableReferenceQuality = val->asA(); } + else if (name == OUTPUT_DIRECTORY_STRING) + { + std::string dirPath = p->getValueString(); + if (!dirPath.empty()) + { + outputDirectory = mx::FilePath(dirPath); + // Create the directory if it doesn't exist + if (!outputDirectory.exists()) + { + outputDirectory.createDirectory(); + } + } + } + else if (name == ENABLE_TRACING_STRING) + { + enableTracing = val->asA(); + } } } } diff --git a/source/MaterialXTest/MaterialXGenShader/GenShaderUtil.h b/source/MaterialXTest/MaterialXGenShader/GenShaderUtil.h index 869379f530..e6ae23f19f 100644 --- a/source/MaterialXTest/MaterialXGenShader/GenShaderUtil.h +++ b/source/MaterialXTest/MaterialXGenShader/GenShaderUtil.h @@ -118,6 +118,27 @@ class TestSuiteOptions // Enable reference quality rendering. Default is false. bool enableReferenceQuality; + // Base directory for all test output artifacts (shaders, images, logs). + // If empty, use default locations. If set, all artifacts go to this directory. + mx::FilePath outputDirectory; + + // Enable Perfetto tracing during render tests (requires MATERIALX_BUILD_PERFETTO_TRACING). + // Default is false to avoid overhead when not profiling. + bool enableTracing = false; + + // Helper to resolve output path for an artifact. + // If outputDirectory is set, returns outputDirectory/filename. + // Otherwise returns the original path unchanged. + mx::FilePath resolveOutputPath(const mx::FilePath& path) const + { + if (outputDirectory.isEmpty()) + { + return path; + } + // Extract just the filename and place it in outputDirectory + return outputDirectory / path.getBaseName(); + } + // Bake parameters struct BakeSetting { diff --git a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp index 02b22b4441..e79387f23a 100644 --- a/source/MaterialXTest/MaterialXRender/RenderUtil.cpp +++ b/source/MaterialXTest/MaterialXRender/RenderUtil.cpp @@ -12,6 +12,11 @@ #include #endif +#ifdef MATERIALX_BUILD_PERFETTO_TRACING +#include +#include +#endif + namespace mx = MaterialX; namespace RenderUtil @@ -81,13 +86,42 @@ void ShaderRenderTester::loadDependentLibraries(GenShaderUtil::TestSuiteOptions bool ShaderRenderTester::validate(const mx::FilePath optionsFilePath) { + // Read options first so we can use outputDirectory for log files + GenShaderUtil::TestSuiteOptions options; + if (!options.readOptions(optionsFilePath)) + { + std::cerr << "Can't find options file. Skip test." << std::endl; + return false; + } + if (!runTest(options)) + { + std::cerr << "Target: " << _shaderGenerator->getTarget() << " not set to run. Skip test." << std::endl; + return false; + } + +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + // Initialize tracing with target-specific trace filename (if enabled in options) + std::optional tracingGuard; + if (options.enableTracing) + { + mx::FilePath tracePath = options.resolveOutputPath(_shaderGenerator->getTarget() + "_render_trace.perfetto-trace"); + mx::Tracing::Dispatcher::getInstance().setSink( + mx::Tracing::createPerfettoSink(tracePath.asString())); + // Scope guard ensures tracing is shut down on any exit path (return, exception, etc.) + tracingGuard.emplace(); + } +#endif + #ifdef LOG_TO_FILE - std::ofstream logfile(_shaderGenerator->getTarget() + "_render_log.txt"); + mx::FilePath logPath = options.resolveOutputPath(_shaderGenerator->getTarget() + "_render_log.txt"); + std::ofstream logfile(logPath.asString()); std::ostream& log(logfile); - std::string docValidLogFilename = _shaderGenerator->getTarget() + "_render_doc_validation_log.txt"; + mx::FilePath docValidLogPath = options.resolveOutputPath(_shaderGenerator->getTarget() + "_render_doc_validation_log.txt"); + std::string docValidLogFilename = docValidLogPath.asString(); std::ofstream docValidLogFile(docValidLogFilename); std::ostream& docValidLog(docValidLogFile); - std::ofstream profilingLogfile(_shaderGenerator->getTarget() + "_render_profiling_log.txt"); + mx::FilePath profilingLogPath = options.resolveOutputPath(_shaderGenerator->getTarget() + "_render_profiling_log.txt"); + std::ofstream profilingLogfile(profilingLogPath.asString()); std::ostream& profilingLog(profilingLogfile); #else std::ostream& log(std::cout); @@ -96,20 +130,6 @@ bool ShaderRenderTester::validate(const mx::FilePath optionsFilePath) std::ostream& profilingLog(std::cout); #endif - // Test has been turned off so just do nothing. - // Check for an option file - GenShaderUtil::TestSuiteOptions options; - if (!options.readOptions(optionsFilePath)) - { - log << "Can't find options file. Skip test." << std::endl; - return false; - } - if (!runTest(options)) - { - log << "Target: " << _shaderGenerator->getTarget() << " not set to run. Skip test." << std::endl; - return false; - } - // Profiling times RenderUtil::RenderProfileTimes profileTimes; // Global setup timer @@ -288,6 +308,15 @@ bool ShaderRenderTester::validate(const mx::FilePath optionsFilePath) mx::FilePath outputPath = filename; outputPath.removeExtension(); + + // If outputDirectory is set, redirect output to that directory + // while preserving the material name as a subdirectory + if (!options.outputDirectory.isEmpty()) + { + // Get just the material directory name (e.g., "standard_surface_carpaint") + mx::FilePath materialDir = outputPath.getBaseName(); + outputPath = options.outputDirectory / materialDir; + } renderableSearchTimer.startTimer(); std::vector elements; @@ -313,6 +342,12 @@ bool ShaderRenderTester::validate(const mx::FilePath optionsFilePath) totalTime.endTimer(); printRunLog(profileTimes, options, profilingLog, dependLib); + // Print effective output directory for easy access (clickable in terminals) + if (!options.outputDirectory.isEmpty()) + { + std::cout << std::endl << "Test artifacts written to: " << options.outputDirectory.asString() << std::endl; + } + return true; } diff --git a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp index e28931803c..197c43c93e 100644 --- a/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp +++ b/source/MaterialXTest/MaterialXRenderGlsl/RenderGlsl.cpp @@ -12,6 +12,7 @@ #include #include +#include #if defined(MATERIALX_BUILD_OIIO) #include #endif @@ -161,6 +162,8 @@ bool GlslShaderRenderTester::runRenderer(const std::string& shaderName, const std::string& outputPath, mx::ImageVec* imageVec) { + MX_TRACE_FUNCTION(mx::Tracing::Category::Render); + MX_TRACE_SCOPE(mx::Tracing::Category::Material, shaderName.c_str()); std::cout << "Validating GLSL rendering for: " << doc->getSourceUri() << std::endl; mx::ScopedTimer totalGLSLTime(&profileTimes.languageTimes.totalTime); @@ -180,16 +183,18 @@ bool GlslShaderRenderTester::runRenderer(const std::string& shaderName, profileTimes.elementsTested++; mx::FilePath outputFilePath = outputPath; - // Use separate directory for reduced output - if (options.shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED) - { - outputFilePath = outputFilePath / mx::FilePath("reduced"); - } - + // Note: mkdir will fail if the directory already exists which is ok. { mx::ScopedTimer ioDir(&profileTimes.languageTimes.ioTime); outputFilePath.createDirectory(); + + // Use separate directory for reduced output + if (options.shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED) + { + outputFilePath = outputFilePath / mx::FilePath("reduced"); + outputFilePath.createDirectory(); + } } std::string shaderPath = mx::FilePath(outputFilePath) / mx::FilePath(shaderName); @@ -201,6 +206,7 @@ bool GlslShaderRenderTester::runRenderer(const std::string& shaderName, transpTimer.endTimer(); mx::ScopedTimer generationTimer(&profileTimes.languageTimes.generationTime); + MX_TRACE_SCOPE(mx::Tracing::Category::ShaderGen, "GenerateShader"); mx::GenOptions& contextOptions = context.getOptions(); contextOptions = options; contextOptions.targetColorSpaceOverride = "lin_rec709"; @@ -277,6 +283,7 @@ bool GlslShaderRenderTester::runRenderer(const std::string& shaderName, _renderer->setLightHandler(isShader ? _lightHandler : nullptr); { + MX_TRACE_SCOPE(mx::Tracing::Category::Render, "CompileShader"); mx::ScopedTimer compileTimer(&profileTimes.languageTimes.compileTime); _renderer->createProgram(shader); _renderer->validateInputs(); @@ -343,6 +350,7 @@ bool GlslShaderRenderTester::runRenderer(const std::string& shaderName, int supersampleFactor = testOptions.enableReferenceQuality ? 8 : 1; { + MX_TRACE_SCOPE(mx::Tracing::Category::Render, "RenderMaterial"); mx::ScopedTimer renderTimer(&profileTimes.languageTimes.renderTime); _renderer->getImageHandler()->setSearchPath(imageSearchPath); unsigned int width = (unsigned int) testOptions.renderSize[0] * supersampleFactor; diff --git a/source/MaterialXTest/MaterialXRenderMsl/RenderMsl.mm b/source/MaterialXTest/MaterialXRenderMsl/RenderMsl.mm index 82ff81b96a..c6340101da 100644 --- a/source/MaterialXTest/MaterialXRenderMsl/RenderMsl.mm +++ b/source/MaterialXTest/MaterialXRenderMsl/RenderMsl.mm @@ -188,16 +188,18 @@ bool runRenderer(const std::string& shaderName, profileTimes.elementsTested++; mx::FilePath outputFilePath = outputPath; - // Use separate directory for reduced output - if (options.shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED) - { - outputFilePath = outputFilePath / mx::FilePath("reduced"); - } - + // Note: mkdir will fail if the directory already exists which is ok. { mx::ScopedTimer ioDir(&profileTimes.languageTimes.ioTime); outputFilePath.createDirectory(); + + // Use separate directory for reduced output + if (options.shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED) + { + outputFilePath = outputFilePath / mx::FilePath("reduced"); + outputFilePath.createDirectory(); + } } std::string shaderPath = mx::FilePath(outputFilePath) / mx::FilePath(shaderName); diff --git a/source/MaterialXTest/MaterialXRenderOsl/RenderOsl.cpp b/source/MaterialXTest/MaterialXRenderOsl/RenderOsl.cpp index c8b8d45bbb..b57ed2bfb8 100644 --- a/source/MaterialXTest/MaterialXRenderOsl/RenderOsl.cpp +++ b/source/MaterialXTest/MaterialXRenderOsl/RenderOsl.cpp @@ -254,16 +254,18 @@ bool OslShaderRenderTester::runRenderer(const std::string& shaderName, std::string shaderPath; mx::FilePath outputFilePath = outputPath; - // Use separate directory for reduced output - if (options.shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED) - { - outputFilePath = outputFilePath / mx::FilePath("reduced"); - } - + // Note: mkdir will fail if the directory already exists which is ok. { mx::ScopedTimer ioDir(&profileTimes.languageTimes.ioTime); outputFilePath.createDirectory(); + + // Use separate directory for reduced output + if (options.shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED) + { + outputFilePath = outputFilePath / mx::FilePath("reduced"); + outputFilePath.createDirectory(); + } } shaderPath = mx::FilePath(outputFilePath) / mx::FilePath(shaderName); diff --git a/source/MaterialXTest/MaterialXRenderSlang/RenderSlang.cpp b/source/MaterialXTest/MaterialXRenderSlang/RenderSlang.cpp index 4d47cad68b..7a96b7af78 100644 --- a/source/MaterialXTest/MaterialXRenderSlang/RenderSlang.cpp +++ b/source/MaterialXTest/MaterialXRenderSlang/RenderSlang.cpp @@ -180,16 +180,18 @@ bool SlangShaderRenderTester::runRenderer(const std::string& shaderName, profileTimes.elementsTested++; mx::FilePath outputFilePath = outputPath; - // Use separate directory for reduced output - if (options.shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED) - { - outputFilePath = outputFilePath / mx::FilePath("reduced"); - } - + // Note: mkdir will fail if the directory already exists which is ok. { mx::ScopedTimer ioDir(&profileTimes.languageTimes.ioTime); outputFilePath.createDirectory(); + + // Use separate directory for reduced output + if (options.shaderInterfaceType == mx::SHADER_INTERFACE_REDUCED) + { + outputFilePath = outputFilePath / mx::FilePath("reduced"); + outputFilePath.createDirectory(); + } } std::string shaderPath = mx::FilePath(outputFilePath) / mx::FilePath(shaderName); diff --git a/source/MaterialXTrace/CMakeLists.txt b/source/MaterialXTrace/CMakeLists.txt new file mode 100644 index 0000000000..bbade0aef2 --- /dev/null +++ b/source/MaterialXTrace/CMakeLists.txt @@ -0,0 +1,66 @@ +# MaterialXTrace - Performance tracing infrastructure for MaterialX +# +# This module provides an abstract tracing interface that can be backed by +# different implementations (Perfetto, USD TraceCollector, etc.). + +file(GLOB materialx_source "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") + +# Public headers only - PerfettoSink.h is internal (includes Perfetto SDK headers) +set(materialx_headers + ${CMAKE_CURRENT_SOURCE_DIR}/Export.h + ${CMAKE_CURRENT_SOURCE_DIR}/Tracing.h +) + +# Exclude Perfetto sink when tracing is disabled +if(NOT MATERIALX_BUILD_PERFETTO_TRACING) + list(REMOVE_ITEM materialx_source "${CMAKE_CURRENT_SOURCE_DIR}/Tracing.cpp") + list(REMOVE_ITEM materialx_source "${CMAKE_CURRENT_SOURCE_DIR}/PerfettoSink.cpp") + # Keep headers but they'll be no-ops via macros +endif() + +mx_add_library(MaterialXTrace + SOURCE_FILES + ${materialx_source} + HEADER_FILES + ${materialx_headers} + EXPORT_DEFINE + MATERIALX_TRACE_EXPORTS + MTLX_MODULES + MaterialXCore) + +# Perfetto tracing support +if(MATERIALX_BUILD_PERFETTO_TRACING) + # The Perfetto SDK is distributed as an amalgamated single-file source (sdk/perfetto.cc). + # This is the official recommended integration method per Google's documentation: + # https://perfetto.dev/docs/instrumentation/tracing-sdk + # Compiling directly (vs. linking a library) simplifies integration and enables + # better compiler optimizations within a single translation unit. + # + # NOTE: Compile flags for perfetto.cc are set in root CMakeLists.txt so they apply + # to all targets (including monolithic builds which aggregate sources). + target_sources(${TARGET_NAME} PRIVATE + "${perfetto_SOURCE_DIR}/sdk/perfetto.cc") + # Use generator expression - only needed at build time, not install + target_include_directories(${TARGET_NAME} PUBLIC + $) + target_compile_definitions(${TARGET_NAME} PUBLIC MATERIALX_BUILD_PERFETTO_TRACING) + + # Platform-specific link libraries + if(WIN32) + # ws2_32: Windows Sockets 2 library for Perfetto IPC + target_link_libraries(${TARGET_NAME} PRIVATE ws2_32) + elseif(UNIX AND NOT CMAKE_SYSTEM_NAME STREQUAL "iOS") + # Perfetto requires pthread on Linux/Unix (but not iOS where it's built-in) + find_package(Threads REQUIRED) + target_link_libraries(${TARGET_NAME} PRIVATE Threads::Threads) + endif() + + # Apply Perfetto compile flags (defined in root CMakeLists.txt) + # NOTE: set_source_files_properties is directory-scoped, so we must set them + # here for non-monolithic builds AND in root CMakeLists.txt for monolithic builds. + set_source_files_properties("${perfetto_SOURCE_DIR}/sdk/perfetto.cc" + PROPERTIES + COMPILE_DEFINITIONS "${MATERIALX_PERFETTO_COMPILE_DEFINITIONS}" + COMPILE_FLAGS "${MATERIALX_PERFETTO_COMPILE_FLAGS}") +endif() + diff --git a/source/MaterialXTrace/Export.h b/source/MaterialXTrace/Export.h new file mode 100644 index 0000000000..00105df6ac --- /dev/null +++ b/source/MaterialXTrace/Export.h @@ -0,0 +1,24 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_TRACE_EXPORT_H +#define MATERIALX_TRACE_EXPORT_H + +#include + +/// @file +/// Macros for declaring imported and exported symbols. + +#if defined(MATERIALX_TRACE_EXPORTS) + #define MX_TRACE_API MATERIALX_SYMBOL_EXPORT + #define MX_TRACE_EXTERN_TEMPLATE(...) MATERIALX_EXPORT_EXTERN_TEMPLATE(__VA_ARGS__) +#else + #define MX_TRACE_API MATERIALX_SYMBOL_IMPORT + #define MX_TRACE_EXTERN_TEMPLATE(...) MATERIALX_IMPORT_EXTERN_TEMPLATE(__VA_ARGS__) +#endif + +#endif + + diff --git a/source/MaterialXTrace/PerfettoSink.cpp b/source/MaterialXTrace/PerfettoSink.cpp new file mode 100644 index 0000000000..691c73c08f --- /dev/null +++ b/source/MaterialXTrace/PerfettoSink.cpp @@ -0,0 +1,177 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + +#include +#include + +// Define Perfetto trace categories for MaterialX +// These must be in a .cpp file, not a header +PERFETTO_DEFINE_CATEGORIES( + perfetto::Category("mx.render") + .SetDescription("MaterialX rendering operations"), + perfetto::Category("mx.shadergen") + .SetDescription("MaterialX shader generation"), + perfetto::Category("mx.optimize") + .SetDescription("MaterialX optimization passes"), + perfetto::Category("mx.material") + .SetDescription("MaterialX material identity markers") +); + +// Required for Perfetto SDK - provides static storage for track events +PERFETTO_TRACK_EVENT_STATIC_STORAGE(); + +MATERIALX_NAMESPACE_BEGIN + +namespace Tracing +{ + +PerfettoSink::PerfettoSink(std::string outputPath, size_t bufferSizeKb) + : _outputPath(std::move(outputPath)) +{ + // One-time global Perfetto initialization + static std::once_flag initFlag; + std::call_once(initFlag, []() { + perfetto::TracingInitArgs args; + args.backends |= perfetto::kInProcessBackend; + perfetto::Tracing::Initialize(args); + perfetto::TrackEvent::Register(); + }); + + // Create and start a tracing session + perfetto::TraceConfig cfg; + cfg.add_buffers()->set_size_kb(static_cast(bufferSizeKb)); + + auto* ds_cfg = cfg.add_data_sources()->mutable_config(); + ds_cfg->set_name("track_event"); + + _session = perfetto::Tracing::NewTrace(); + _session->Setup(cfg); + _session->StartBlocking(); +} + +PerfettoSink::~PerfettoSink() +{ + if (!_session) + return; + + // Flush any pending trace data + perfetto::TrackEvent::Flush(); + + // Stop the tracing session + _session->StopBlocking(); + + // Read trace data and write to file + std::vector traceData(_session->ReadTraceBlocking()); + if (!traceData.empty()) + { + std::ofstream output(_outputPath, std::ios::binary); + output.write(traceData.data(), static_cast(traceData.size())); + } +} + +void PerfettoSink::beginEvent(Category category, const char* name) +{ + // Perfetto requires compile-time category names for TRACE_EVENT macros. + // Switch on the enum lets the compiler optimize to a jump table. + switch (category) + { + case Category::Render: + TRACE_EVENT_BEGIN("mx.render", nullptr, [&](perfetto::EventContext ctx) { + ctx.event()->set_name(name); + }); + break; + case Category::ShaderGen: + TRACE_EVENT_BEGIN("mx.shadergen", nullptr, [&](perfetto::EventContext ctx) { + ctx.event()->set_name(name); + }); + break; + case Category::Optimize: + TRACE_EVENT_BEGIN("mx.optimize", nullptr, [&](perfetto::EventContext ctx) { + ctx.event()->set_name(name); + }); + break; + case Category::Material: + TRACE_EVENT_BEGIN("mx.material", nullptr, [&](perfetto::EventContext ctx) { + ctx.event()->set_name(name); + }); + break; + default: + // Fallback for any future categories + TRACE_EVENT_BEGIN("mx.render", nullptr, [&](perfetto::EventContext ctx) { + ctx.event()->set_name(name); + }); + break; + } +} + +void PerfettoSink::endEvent(Category category) +{ + switch (category) + { + case Category::Render: + TRACE_EVENT_END("mx.render"); + break; + case Category::ShaderGen: + TRACE_EVENT_END("mx.shadergen"); + break; + case Category::Optimize: + TRACE_EVENT_END("mx.optimize"); + break; + case Category::Material: + TRACE_EVENT_END("mx.material"); + break; + default: + TRACE_EVENT_END("mx.render"); + break; + } +} + +void PerfettoSink::counter(Category category, const char* name, double value) +{ + auto track = perfetto::CounterTrack(name); + switch (category) + { + case Category::Render: + TRACE_COUNTER("mx.render", track, value); + break; + case Category::ShaderGen: + TRACE_COUNTER("mx.shadergen", track, value); + break; + case Category::Optimize: + TRACE_COUNTER("mx.optimize", track, value); + break; + case Category::Material: + TRACE_COUNTER("mx.material", track, value); + break; + default: + TRACE_COUNTER("mx.render", track, value); + break; + } +} + +void PerfettoSink::setThreadName(const char* name) +{ + // Set thread name for trace visualization + auto track = perfetto::ThreadTrack::Current(); + auto desc = track.Serialize(); + desc.mutable_thread()->set_thread_name(name); + perfetto::TrackEvent::SetTrackDescriptor(track, desc); +} + +// Factory function - the exported entry point +std::unique_ptr createPerfettoSink(const std::string& outputPath, size_t bufferSizeKb) +{ + return std::make_unique(outputPath, bufferSizeKb); +} + +} // namespace Tracing + +MATERIALX_NAMESPACE_END + +#endif // MATERIALX_BUILD_PERFETTO_TRACING diff --git a/source/MaterialXTrace/PerfettoSink.h b/source/MaterialXTrace/PerfettoSink.h new file mode 100644 index 0000000000..b0012e86ec --- /dev/null +++ b/source/MaterialXTrace/PerfettoSink.h @@ -0,0 +1,86 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_PERFETTOSINK_H +#define MATERIALX_PERFETTOSINK_H + +/// @file +/// Perfetto-based implementation of the Tracing::Sink interface. +/// +/// This header is internal to MaterialXTrace. Users should NOT include it +/// directly. Instead, use the createPerfettoSink() factory in Tracing.h: +/// +/// #include +/// auto sink = mx::Tracing::createPerfettoSink("trace.perfetto-trace"); + +#include + +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + +// Suppress verbose warnings from Perfetto SDK templates +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4127) // conditional expression is constant +#pragma warning(disable : 4146) // unary minus on unsigned type +#pragma warning(disable : 4369) // enumerator value cannot be represented +#endif + +#include + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +namespace Tracing +{ + +/// @class PerfettoSink +/// Perfetto-based implementation of Tracing::Sink. +/// +/// Uses Perfetto SDK's in-process tracing backend. The constructor starts +/// a tracing session, and the destructor stops it and writes trace data +/// to a .perfetto-trace file viewable at https://ui.perfetto.dev +/// +/// @note Do not use this class directly. Use createPerfettoSink() factory. +class PerfettoSink : public Sink +{ + public: + /// Construct and start a Perfetto tracing session. + /// @param outputPath Path to write the trace file when destroyed + /// @param bufferSizeKb Size of the trace buffer in KB (default 32MB) + explicit PerfettoSink(std::string outputPath, size_t bufferSizeKb = 32768); + + /// Stop tracing and write the trace to the output path. + ~PerfettoSink() override; + + // Non-copyable, non-movable + PerfettoSink(const PerfettoSink&) = delete; + PerfettoSink& operator=(const PerfettoSink&) = delete; + PerfettoSink(PerfettoSink&&) = delete; + PerfettoSink& operator=(PerfettoSink&&) = delete; + + // Sink interface implementation + void beginEvent(Category category, const char* name) override; + void endEvent(Category category) override; + void counter(Category category, const char* name, double value) override; + void setThreadName(const char* name) override; + + private: + const std::string _outputPath; + std::unique_ptr _session; +}; + +} // namespace Tracing + +MATERIALX_NAMESPACE_END + +#endif // MATERIALX_BUILD_PERFETTO_TRACING + +#endif // MATERIALX_PERFETTOSINK_H diff --git a/source/MaterialXTrace/Tracing.cpp b/source/MaterialXTrace/Tracing.cpp new file mode 100644 index 0000000000..75c00dcdd6 --- /dev/null +++ b/source/MaterialXTrace/Tracing.cpp @@ -0,0 +1,35 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +MATERIALX_NAMESPACE_BEGIN + +namespace Tracing +{ + +Dispatcher& Dispatcher::getInstance() +{ + static Dispatcher instance; + return instance; +} + +void Dispatcher::setSink(std::unique_ptr sink) +{ + // Assert if a sink is already set - caller should shutdownSink() first. + // This catches programming errors; if triggered, the old sink will still + // write its output when destroyed by this assignment. + assert(!_sink && "Sink already set - call shutdownSink() first"); + _sink = std::move(sink); +} + +void Dispatcher::shutdownSink() +{ + _sink.reset(); // Destructor handles writing output +} + +} // namespace Tracing + +MATERIALX_NAMESPACE_END diff --git a/source/MaterialXTrace/Tracing.h b/source/MaterialXTrace/Tracing.h new file mode 100644 index 0000000000..283fd33dca --- /dev/null +++ b/source/MaterialXTrace/Tracing.h @@ -0,0 +1,255 @@ +// +// Copyright Contributors to the MaterialX Project +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef MATERIALX_TRACING_H +#define MATERIALX_TRACING_H + +/// @file +/// Tracing infrastructure for performance analysis. +/// +/// This module provides an abstract tracing interface that can be backed by +/// different implementations (Perfetto, USD TraceCollector, etc.). +/// +/// Design goals: +/// - API similar to USD's TraceCollector/TraceScope for familiarity +/// - Abstract sink allows USD to inject its own tracing when calling MaterialX +/// - Zero overhead when tracing is disabled (macros compile to nothing) +/// - Enum-based categories for type safety and efficient dispatch + +#include + +#include +#include +#include +#include + +MATERIALX_NAMESPACE_BEGIN + +/// @namespace Tracing +/// Tracing infrastructure for performance analysis. +namespace Tracing +{ + +/// @enum Category +/// Trace event categories for filtering and organization. +/// +/// These are used to categorize trace events for filtering in the UI +/// and to enable/disable specific categories at runtime. +enum class Category +{ + /// Rendering operations (GPU commands, frame capture, etc.) + Render = 0, + + /// Shader generation (code generation, optimization passes) + ShaderGen, + + /// Optimization passes (constant folding, dead code elimination) + Optimize, + + /// Material/shader identity markers (for filtering/grouping in traces) + Material, + + /// Number of categories (must be last) + Count +}; + +/// @class Sink +/// Abstract tracing sink interface. +/// +/// Implementations can delegate to Perfetto, USD TraceCollector, or custom systems. +/// This allows USD/Hydra to inject their own tracing when calling MaterialX code. +class MX_TRACE_API Sink +{ + public: + virtual ~Sink() = default; + + /// Begin a trace event with the given category and name. + virtual void beginEvent(Category category, const char* name) = 0; + + /// End the current trace event for the given category. + virtual void endEvent(Category category) = 0; + + /// Record a counter value (e.g., GPU time, memory usage). + virtual void counter(Category category, const char* name, double value) = 0; + + /// Set the current thread's name for trace visualization. + virtual void setThreadName(const char* name) = 0; +}; + +/// @class Dispatcher +/// Global trace dispatcher singleton. +/// +/// Owns the active sink and dispatches trace events to it. +/// The Dispatcher takes ownership of the sink via unique_ptr. +/// +/// Usage: +/// Dispatcher::getInstance().setSink(std::make_unique(...)); +/// // ... traced work ... +/// Dispatcher::getInstance().shutdownSink(); +class MX_TRACE_API Dispatcher +{ + public: + /// Get the singleton instance. + static Dispatcher& getInstance(); + + /// Set the tracing sink. Takes ownership. + /// Asserts if a sink is already set (call shutdownSink() first). + void setSink(std::unique_ptr sink); + + /// Shutdown and destroy the current sink. + /// The sink's destructor handles writing output. + void shutdownSink(); + + /// Scope guard that calls shutdownSink() on destruction. + /// Ensures tracing is properly shut down on any exit path (return, exception, etc.) + /// + /// Usage: + /// Dispatcher::getInstance().setSink(std::make_unique(...)); + /// Dispatcher::ShutdownGuard guard; + /// // ... traced work ... + /// // guard destructor calls shutdownSink() + struct ShutdownGuard + { + ~ShutdownGuard() { Dispatcher::getInstance().shutdownSink(); } + ShutdownGuard() = default; + ShutdownGuard(const ShutdownGuard&) = delete; + ShutdownGuard& operator=(const ShutdownGuard&) = delete; + }; + + /// Check if tracing is currently enabled. + bool isEnabled() const { return _sink != nullptr; } + + /// Begin a trace event. + void beginEvent(Category category, const char* name) + { + if (_sink) + _sink->beginEvent(category, name); + } + + /// End a trace event. + void endEvent(Category category) + { + if (_sink) + _sink->endEvent(category); + } + + /// Record a counter value. + void counter(Category category, const char* name, double value) + { + if (_sink) + _sink->counter(category, name, value); + } + + private: + Dispatcher() = default; + Dispatcher(const Dispatcher&) = delete; + Dispatcher& operator=(const Dispatcher&) = delete; + + std::unique_ptr _sink; +}; + +/// @class Scope +/// RAII scope guard for trace events (similar to USD's TraceScope). +/// +/// Template parameter Cat is the category enum value, known at compile time. +/// This avoids storing the category on the stack. +/// +/// Usage: +/// { +/// Tracing::Scope scope("RenderMaterial"); +/// // ... code to trace ... +/// } // Event automatically ends here +template +class Scope +{ + public: + explicit Scope(const char* name) + { + Dispatcher::getInstance().beginEvent(Cat, name); + } + + ~Scope() + { + Dispatcher::getInstance().endEvent(Cat); + } + + // Non-copyable + Scope(const Scope&) = delete; + Scope& operator=(const Scope&) = delete; +}; + +// ============================================================================ +// Sink Factory Functions +// ============================================================================ + +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + +/// Create a Perfetto-based tracing sink. +/// +/// The returned sink writes trace data to a .perfetto-trace file that can be +/// visualized at https://ui.perfetto.dev +/// +/// @param outputPath Path to write the trace file when the sink is destroyed +/// @param bufferSizeKb Size of the trace buffer in KB (default 32MB) +/// @return A unique_ptr to the Perfetto sink +/// +/// Usage: +/// Dispatcher::getInstance().setSink(createPerfettoSink("trace.perfetto-trace")); +/// Dispatcher::ShutdownGuard guard; +/// // ... traced work ... +/// // guard destructor writes the trace file +MX_TRACE_API std::unique_ptr createPerfettoSink( + const std::string& outputPath, size_t bufferSizeKb = 32768); + +#endif // MATERIALX_BUILD_PERFETTO_TRACING + +} // namespace Tracing + +MATERIALX_NAMESPACE_END + +// ============================================================================ +// Tracing Macros +// ============================================================================ +// When MATERIALX_BUILD_PERFETTO_TRACING is defined, these macros generate trace events. +// Otherwise, they compile to nothing (zero overhead). + +// Helper macros for token pasting with __LINE__ expansion +#define MX_TRACE_CONCAT_IMPL(a, b) a##b +#define MX_TRACE_CONCAT(a, b) MX_TRACE_CONCAT_IMPL(a, b) + +#ifdef MATERIALX_BUILD_PERFETTO_TRACING + +/// Create a scoped trace event. Event ends when scope exits. +/// Category must be a Tracing::Category enum value. +#define MX_TRACE_SCOPE(category, name) \ + MaterialX::Tracing::Scope MX_TRACE_CONCAT(_mxTraceScope_, __LINE__)(name) + +/// Create a scoped trace event using the current function name. +#define MX_TRACE_FUNCTION(category) \ + MaterialX::Tracing::Scope MX_TRACE_CONCAT(_mxTraceFn_, __LINE__)(__FUNCTION__) + +/// Record a counter value. +#define MX_TRACE_COUNTER(category, name, value) \ + MaterialX::Tracing::Dispatcher::getInstance().counter(category, name, value) + +/// Begin a trace event (must be paired with MX_TRACE_END). +#define MX_TRACE_BEGIN(category, name) \ + MaterialX::Tracing::Dispatcher::getInstance().beginEvent(category, name) + +/// End a trace event. +#define MX_TRACE_END(category) \ + MaterialX::Tracing::Dispatcher::getInstance().endEvent(category) + +#else // MATERIALX_BUILD_PERFETTO_TRACING not defined + +#define MX_TRACE_SCOPE(category, name) +#define MX_TRACE_FUNCTION(category) +#define MX_TRACE_COUNTER(category, name, value) +#define MX_TRACE_BEGIN(category, name) +#define MX_TRACE_END(category) + +#endif // MATERIALX_BUILD_PERFETTO_TRACING + +#endif // MATERIALX_TRACING_H