HLSL backend (codegen + {D3D11 & D3D12) renderer) - #2897
Conversation
|
This proposal looks very promising, thanks @soufianekhiat, and I'd be interested in thoughts from @ashwinbhat, who has given a good deal of thought to strategies for HLSL integration in MaterialX. Back in 2022, Ashwin presented an overview of different HLSL approaches to the MaterialX TSC, and I'll share the Google Doc from that presentation: |
|
Heh, I was looking at having a Robot Friend do the same thing, but you beat me to it! For my use, I have a very specific requirement, though, because I need to glue my HLSL into some special constraint. I need the the per-node genhlsl/*.hlsl library files are guaranteed to be valid standalone HLSL functions (no entry-point glue, callable in isolation). If yes, that's worth documenting because it opens the door to host integrations that consume MaterialX nodes as fragments rather than full shaders. Will your implementation be able to do that? |
|
Ok I downloaded built and tested this, and it seems to do everything I need already - awesome! |
+1 on the proposal. Great work and I'm glad that this proposal includes MaterialXRenderHlsl for testing purposes to ensure future maintainability. The pipeline I had proposed in 2022 focused on ease of maintainability and leveraged existing transpiling tools so that we would have single shadergen (GLSL), but generate additional targets for Metal, Vulkan and DX. Some downsides of the use of transpiling tools are
Since we have direct implementation for other HW target, I would support a HLSL backend that can be refined to better use new HLSL semantics and feature levels. |
|
@soufianekhiat Would it be a lot of effort to support DX12? Do you think we might need some utilities to support extraction of shader metadata for root signature generation? |
It depends if it's for production readyness I would add a D3D12MemoryAllocator as a dependency. Otherwise I can build a simpler version only for the viewer. |
For production, I think the hlsl shader should suffice. I was suggesting DX12 for the testrenderer and viewer. |
|
Not forgot the PR, just need few time to add the dx12 path. |
Cross-platform HLSL codegen as a sibling of GenGlsl/GenMsl/GenSlang.
Reuses GLSL node implementations via a small post-emit pass: three
regex rewrites (single-arg vector splat, texture() to mx_texture_sample,
ClosureData return constructor) plus a token table (mix->lerp,
dFdx/dFdy->ddx/ddy, mod->fmod, fract->frac, inversesqrt->rsqrt).
Adds:
- source/MaterialXGenHlsl/{HlslShaderGenerator,HlslSyntax}.{h,cpp}
- libraries/{stdlib,pbrlib,nprlib,lights}/genhlsl/*.mtlx
- libraries/stdlib/genhlsl/lib/{mx_math,mx_texture}.hlsl
- libraries/targets/genhlsl.mtlx
- source/MaterialXTest/MaterialXGenHlsl/* (codegen tests)
- CHANGELOG.md entry
Mirrors MslResourceBindingContext. When attached to GenContext, splits uniforms into Private/Public/LightData cbuffers with explicit register annotations and suppresses inline LightData struct emission (the binding context emits it once at the cbuffer level).
…ction Renderer-free compile + reflect entry point. FXC via d3dcompiler_47 (default, SM 5.x); DXC via dxcompiler.dll loaded dynamically (SM 6.x). Reflection unifies on HlslResourceBinding for both DXBC and DXIL. Suitable for headless CI to validate that generated HLSL compiles without requiring a D3D11 device.
HlslContext owns ID3D11Device + DeviceContext with hardware-or-WARP fallback and tryCreateContext() for headless test environments. HlslFramebuffer wraps an RTV + DSV + readback path so tests can verify real pixels. End-to-end draw test compiles a trivial VS+PS, draws a fullscreen triangle, and reads back the centre pixel.
Bridges D3D11's stateful cbuffer model to MaterialX's per-uniform update pattern. One ID3D11Buffer per stage per cbuffer with a CPU mirror so partial writes do not clobber unrelated members. Reflection- driven member offset lookup (lookupVariableOffset) so callers can name 'u_worldMatrix' and get back its byte offset inside vertexCB. Includes a generated-shader test that compiles standard_surface_carpaint, allocates the reflection-driven cbuffer pool, and draws into the framebuffer to verify the pixel shader actually executed.
SRV + sampler cache keyed by Image::getResourceId. Subclassing ImageHandler gives Python clients the full bindImage / unbindImage / releaseRenderResources interface. getBoundSrv / getBoundSampler expose the COM pointers so the renderer can bind t# / s# slots without re-walking the cache.
Per-frame validateRender walks the program's reflected bindings and auto-binds: camera/world matrices into vertexCB; lighting scalars into pixelCB; environment radiance/irradiance via LightHandler; file textures from PUBLIC_UNIFORMS via ImageHandler; per-light parameters via reflection's indexed-name lookup. patchVariable lets the renderer be cbuffer-agnostic so it works with and without an attached HlslResourceBindingContext.
One-line CRTP-style specialisation of TextureBaker<HlslRenderer, HlslShaderGenerator>. All baking machinery comes from the templated base class; this subclass only wires the backend types in.
PyMaterialXGenHlsl: HlslShaderGenerator, HlslResourceBindingContext, HlslSyntax. PyMaterialXRenderHlsl: HlslContext, HlslFramebuffer, HlslProgram, HlslMaterial, HlslTextureHandler (as ImageHandler subclass), HlslRenderer. D3D COM pointers are never crossed into Python.
Embind binding for HlslShaderGenerator, gated on EMSCRIPTEN. Generator only - no renderer in WASM since D3D11 is not portable.
Five tests were asserting raw pixel byte values that no longer hold since the framebuffer defaults to sRGB encoding and the texture handler builds a mip chain by default: - ClearAndReadback, Draw Triangle, Material BindCbufferAndDraw, DrawsMeshWhenAvailable: opt the framebuffer into linear pass-through via setEncodeSrgb(false) before bind so the linear RTV is used. - Texture SampleAndDraw: a 2x2 source under trilinear filtering blends mip 0 with the gray average mip 1, so corner reads don't return the original texels. Set ImageSamplingProperties::filterType = CLOSEST so the test sees raw texel values.
Adds
MaterialXGenHlslandMaterialXRenderHlsl, + Python and JS bindings, plus the matching libraries underlibraries/{stdlib,pbrlib,nprlib,lights}/genhlsl/. Existing GLSL / MSL / Slang code is untouched.The codegen reuses the GLSL
.glslimpl files viafile="../genglsl/..."and runs a small post-emit pass for the GLSL→HLSL deltas (mix→lerp,dFdx→ddx, vector-splat C-cast,texture()→mx_texture_sample, etc.). Two HLSL-native helpers (mx_math.hlsl,mx_texture.hlsl) fill in the rest.The renderer is D3D11. FXC for SM 5.x, DXC loaded dynamically for SM 6+.
HlslMaterialowns per-stage cbuffers with a CPU mirror so partial uniform writes don't clobber neighbours — D3D11 cbuffers are stateful, noglProgramUniformanalogue.HlslTextureHandleris anImageHandlersubclass.HlslRendererauto-binds camera, lights, env, file textures and multi-mesh geometry from reflection.Tests: 7 cases / 3369 assertions in
[genhlsl], 19 / 213 in[renderhlsl]. Headless CI safe - every D3D-touching case skipscleanly when
tryCreateContext()returns null. The full FXC compile sweep (~22 min) is tagged[!slow]so quick CI runs can skip it.Validation: 31 materials rendered side-by-side against GLSL on the shaderball (StandardSurface, OpenPBR, glTF PBR, Disney, SimpleHair), plus the 15-material chess_set scene. All visually identical; 30/31 single-material RMSE under 2 on the 0-255 scale. See Compare.
Build:
MaterialXGenHlslcross-platform,MaterialXRenderHlslgated onWIN32. Stagesdxcompiler.dll+dxil.dllfrom the Windows SDK.Why not "just emit HLSL with Slang"?
Slang would give us HLSL source, not a D3D11 renderer - we'd still need most of
MaterialXRenderHlsl. And FXC/DXC reflection round-trips MaterialX uniform names directly with native emit; through Slang they get mangled, breaking the per-uniformsetVariableAPI that GLSL/MSL already provide.Disclosure: This PR was created assicted with Claude Opus 4.7.