Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
96 changes: 96 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,93 @@ 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";

size_t numHashed = 0;
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);
INFO(documentsPaths[docIdx] << ": " << message);
REQUIRE(docValid);

context.getShaderGenerator().registerTypeDefs(doc);

std::vector<mx::TypedElementPtr> elements = mx::findRenderableElements(doc);
for (const mx::TypedElementPtr& element : elements)
{
mx::ShaderPtr shader;
REQUIRE_NOTHROW(shader = context.getShaderGenerator().generate(element->getName(), element, context));
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;
REQUIRE_NOTHROW(shader2 = context.getShaderGenerator().generate(element->getName(), element, context));
REQUIRE(shader2 != nullptr);
size_t hash2 = mx::computeStructuralHash(shader2->getGraph());
REQUIRE(hash1 == hash2);
++numHashed;

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());
REQUIRE(numHashed > 0);
}
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