Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions python/Scripts/generateshader.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def main():
parser.add_argument('--validatorArgs', dest='validatorArgs', nargs='?', const=' ', type=str, help='Optional arguments for code validator.')
parser.add_argument('--vulkanGlsl', dest='vulkanCompliantGlsl', default=False, type=bool, help='Set to True to generate Vulkan-compliant GLSL when using the genglsl target.')
parser.add_argument('--shaderInterfaceType', dest='shaderInterfaceType', default=0, type=int, help='Set the type of shader interface to be generated')
parser.add_argument('--dumpHash', dest='dumpHash', action='store_true', default=False, help='Print the structural graph hash for each generated shader.')
parser.add_argument(dest='inputFilename', help='Path to input document or folder containing input documents.')
opts = parser.parse_args()

Expand Down Expand Up @@ -156,6 +157,10 @@ def main():
elemName = mx.createValidName(elemName)
shader = shadergen.generate(elemName, elem, context)
if shader:
if opts.dumpHash:
structHash = mx_gen_shader.computeStructuralHash(shader)
print(f'--- Structural hash: 0x{structHash:016x}')

# Use extension of .vert and .frag as it's type is
# recognized by glslangValidator
if gentarget in ['glsl', 'essl', 'vulkan', 'msl', 'wgsl']:
Expand Down
148 changes: 148 additions & 0 deletions source/MaterialXGenShader/ShaderGraphHash.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#include <MaterialXGenShader/ShaderGraphHash.h>

#include <functional>

MATERIALX_NAMESPACE_BEGIN

namespace
{

void hashCombine(size_t& seed, size_t value)
{
seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

void hashString(size_t& seed, const string& str)
{
hashCombine(seed, std::hash<string>()(str));
}

void hashUint32(size_t& seed, uint32_t value)
{
hashCombine(seed, std::hash<uint32_t>()(value));
}

void hashSize(size_t& seed, size_t value)
{
hashCombine(seed, std::hash<size_t>()(value));
}

void hashPortStructure(size_t& seed, const ShaderPort* port)
{
hashString(seed, port->getType().getName());
hashString(seed, port->getSemantic());
hashString(seed, port->getColorSpace());
hashString(seed, port->getUnit());
hashString(seed, port->getGeomProp());

uint32_t structuralFlags = port->getFlags() &
(ShaderPortFlag::UNIFORM | ShaderPortFlag::BIND_INPUT);
hashUint32(seed, structuralFlags);
}

size_t findOutputIndex(const ShaderOutput* output)
{
const ShaderNode* node = output->getNode();
for (size_t i = 0; i < node->numOutputs(); ++i)
{
if (node->getOutput(i) == output)
return i;
}
return SIZE_MAX;
}

} // anonymous namespace

size_t computeStructuralHash(const ShaderGraph& graph)
{
size_t seed = 0;

// 1. Hash graph-level input sockets (count + structural type info, no names or values)
hashSize(seed, graph.numInputSockets());
for (const ShaderGraphInputSocket* socket : graph.getInputSockets())
{
hashPortStructure(seed, socket);
}

// 2. Hash graph-level output sockets (count + structural type info)
hashSize(seed, graph.numOutputSockets());
for (const ShaderGraphOutputSocket* socket : graph.getOutputSockets())
{
hashPortStructure(seed, socket);
}

// 3. Build a stable index for each node using topological order
const auto& nodes = graph.getNodes();
std::unordered_map<const ShaderNode*, size_t> nodeIndex;
nodeIndex.reserve(nodes.size() + 1);
for (size_t i = 0; i < nodes.size(); ++i)
{
nodeIndex[nodes[i]] = i;
}
// The graph itself can appear as a connection source (for graph input sockets)
constexpr size_t GRAPH_SELF_INDEX = SIZE_MAX;
constexpr size_t NO_CONNECTION_SENTINEL = SIZE_MAX - 1;
constexpr size_t UNKNOWN_NODE_SENTINEL = SIZE_MAX - 2;
nodeIndex[&graph] = GRAPH_SELF_INDEX;

// 4. Hash each node in topological order
hashSize(seed, nodes.size());
for (const ShaderNode* node : nodes)
{
hashSize(seed, node->getImplementation().getHash());
hashUint32(seed, node->getClassification());

// Node inputs
hashSize(seed, node->numInputs());
for (const ShaderInput* input : node->getInputs())
{
hashPortStructure(seed, input);

const ShaderOutput* conn = input->getConnection();
if (conn)
{
auto it = nodeIndex.find(conn->getNode());
size_t srcIdx = (it != nodeIndex.end()) ? it->second : UNKNOWN_NODE_SENTINEL;
hashSize(seed, srcIdx);
hashSize(seed, findOutputIndex(conn));
}
else
{
hashSize(seed, NO_CONNECTION_SENTINEL);
}
}

// Node outputs
hashSize(seed, node->numOutputs());
for (const ShaderOutput* output : node->getOutputs())
{
hashPortStructure(seed, output);
}
}

// 5. Hash graph output socket connections
for (const ShaderGraphOutputSocket* socket : graph.getOutputSockets())
{
const ShaderOutput* conn = socket->getConnection();
if (conn)
{
auto it = nodeIndex.find(conn->getNode());
size_t srcIdx = (it != nodeIndex.end()) ? it->second : UNKNOWN_NODE_SENTINEL;
hashSize(seed, srcIdx);
hashSize(seed, findOutputIndex(conn));
}
else
{
hashSize(seed, NO_CONNECTION_SENTINEL);
}
}

return seed;
}

MATERIALX_NAMESPACE_END
38 changes: 38 additions & 0 deletions source/MaterialXGenShader/ShaderGraphHash.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#ifndef MATERIALX_SHADERGRAPHHASH_H
#define MATERIALX_SHADERGRAPHHASH_H

/// @file
/// Structural hash computation for shader graphs

#include <MaterialXGenShader/Export.h>
#include <MaterialXGenShader/ShaderGraph.h>

MATERIALX_NAMESPACE_BEGIN

/// Compute a pure structural hash of a shader graph that captures its
/// topology and node types, independent of instance names and values.
///
/// Two graphs with identical structure (same node implementations,
/// same connection pattern, same port types) will produce the same
/// hash even if they differ in node names, variable names, or uniform
/// values. The hash is computed over the finalized graph and includes:
/// - Graph input/output socket counts and types
/// - Node implementation hashes and classifications
/// - Connection topology (source node topological index + output index)
/// - Structurally-significant port attributes: type, semantic,
/// colorspace, unit, geomprop, uniform/bind-input flags
///
/// Explicitly excluded: node names, port names, port values/defaults.
///
/// This is an external visitor function that only uses the public API
/// of ShaderGraph, ShaderNode, and ShaderPort.
MX_GENSHADER_API size_t computeStructuralHash(const ShaderGraph& graph);

MATERIALX_NAMESPACE_END

#endif
112 changes: 112 additions & 0 deletions source/MaterialXTest/MaterialXGenGlsl/GenGlsl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
#include <MaterialXGenGlsl/VkShaderGenerator.h>
#include <MaterialXGenGlsl/WgslShaderGenerator.h>
#include <MaterialXGenHw/HwConstants.h>
#include <MaterialXGenShader/ShaderGraphHash.h>
#include <MaterialXGenShader/Shader.h>
#include <MaterialXFormat/Util.h>

#include <iomanip>
#include <sstream>

namespace mx = MaterialX;

Expand Down Expand Up @@ -220,3 +226,109 @@ TEST_CASE("GenShader: Wgsl GLSL Shader Generation", "[genglsl]")
{
generateGlslCode(GlslType::GlslWgsl);
}

TEST_CASE("GenShader: GLSL Structural Hash", "[genglsl]")
{
mx::DocumentPtr nodeLibrary = mx::createDocument();
const mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath();

loadLibraries({ "libraries" }, searchPath, nodeLibrary);

mx::GenContext context(mx::GlslShaderGenerator::create());
context.registerSourceCodeSearchPath(searchPath);

mx::DefaultColorManagementSystemPtr colorManagementSystem =
mx::DefaultColorManagementSystem::create(context.getShaderGenerator().getTarget());
REQUIRE(colorManagementSystem);
context.getShaderGenerator().setColorManagementSystem(colorManagementSystem);
colorManagementSystem->loadLibrary(nodeLibrary);

mx::UnitSystemPtr unitSystem = mx::UnitSystem::create(context.getShaderGenerator().getTarget());
REQUIRE(unitSystem);
context.getShaderGenerator().setUnitSystem(unitSystem);
unitSystem->loadLibrary(nodeLibrary);
unitSystem->setUnitConverterRegistry(mx::UnitConverterRegistry::create());
mx::UnitTypeDefPtr distanceTypeDef = nodeLibrary->getUnitTypeDef("distance");
unitSystem->getUnitConverterRegistry()->addUnitConverter(distanceTypeDef, mx::LinearUnitConverter::create(distanceTypeDef));
mx::UnitTypeDefPtr angleTypeDef = nodeLibrary->getUnitTypeDef("angle");
unitSystem->getUnitConverterRegistry()->addUnitConverter(angleTypeDef, mx::LinearUnitConverter::create(angleTypeDef));
context.getOptions().targetDistanceUnit = "meter";

mx::FilePathVec testRootPaths;
testRootPaths.push_back(searchPath.find("resources/Materials/Examples/StandardSurface"));

std::vector<mx::DocumentPtr> loadedDocuments;
mx::StringVec documentsPaths;
mx::StringVec errorLog;

for (const auto& testRoot : testRootPaths)
{
mx::loadDocuments(testRoot, searchPath, {}, {}, loadedDocuments, documentsPaths,
nullptr, &errorLog);
}

REQUIRE(loadedDocuments.size() > 0);

std::ostringstream hashLog;
hashLog << std::hex << std::setfill('0');
hashLog << "\n=== Structural Hash Results ===\n";

for (size_t docIdx = 0; docIdx < loadedDocuments.size(); ++docIdx)
{
mx::DocumentPtr doc = loadedDocuments[docIdx];
doc->setDataLibrary(nodeLibrary);

std::string message;
bool docValid = doc->validate(&message);
if (!docValid)
{
continue;
}
Comment on lines +281 to +286

context.getShaderGenerator().registerTypeDefs(doc);

std::vector<mx::TypedElementPtr> elements = mx::findRenderableElements(doc);
for (const mx::TypedElementPtr& element : elements)
{
mx::ShaderPtr shader;
try
{
shader = context.getShaderGenerator().generate(element->getName(), element, context);
}
catch (const std::exception&)
{
continue;
}
Comment on lines +293 to +301

REQUIRE(shader != nullptr);

size_t hash1 = mx::computeStructuralHash(shader->getGraph());
REQUIRE(hash1 != 0);

// Determinism check: generate the same shader again and verify the hash matches.
mx::ShaderPtr shader2;
try
{
shader2 = context.getShaderGenerator().generate(element->getName(), element, context);
}
catch (const std::exception&)
{
continue;
}
Comment on lines +309 to +317

REQUIRE(shader2 != nullptr);
size_t hash2 = mx::computeStructuralHash(shader2->getGraph());
REQUIRE(hash1 == hash2);

hashLog << " " << documentsPaths[docIdx] << " | "
<< element->getName() << " | 0x"
<< std::setw(sizeof(size_t) * 2) << hash1 << "\n";
}
}

hashLog << "=== End Structural Hash Results ===\n";

// Output to Catch2 INFO so it appears with -s flag
INFO(hashLog.str());
SUCCEED();
}
Comment on lines +331 to +334
8 changes: 8 additions & 0 deletions source/PyMaterialX/PyMaterialXGenShader/PyUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@
#include <PyMaterialX/PyMaterialX.h>

#include <MaterialXGenShader/Util.h>
#include <MaterialXGenShader/Shader.h>
#include <MaterialXGenShader/ShaderGenerator.h>
#include <MaterialXGenShader/ShaderGraphHash.h>

namespace py = pybind11;
namespace mx = MaterialX;

size_t computeStructuralHashFromShader(const mx::Shader& shader)
{
return mx::computeStructuralHash(shader.getGraph());
}

std::vector<mx::TypedElementPtr> findRenderableMaterialNodes(mx::ConstDocumentPtr doc)
{
return mx::findRenderableMaterialNodes(doc);
Expand All @@ -36,4 +43,5 @@ void bindPyUtil(py::module& mod)
mod.def("getUdimScaleAndOffset", &mx::getUdimScaleAndOffset);
mod.def("connectsToWorldSpaceNode", &mx::connectsToWorldSpaceNode);
mod.def("hasElementAttributes", &mx::hasElementAttributes);
mod.def("computeStructuralHash", &computeStructuralHashFromShader);
}
Loading