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