Skip to content

Add DirectX 12 GPU backend for automated unit testing on Windows#2271

Open
num3ric wants to merge 8 commits intoAcademySoftwareFoundation:mainfrom
num3ric:dx
Open

Add DirectX 12 GPU backend for automated unit testing on Windows#2271
num3ric wants to merge 8 commits intoAcademySoftwareFoundation:mainfrom
num3ric:dx

Conversation

@num3ric
Copy link
Copy Markdown
Contributor

@num3ric num3ric commented Mar 7, 2026

Introduce a DirectX 12 / HLSL rendering backend alongside the existing OpenGL / GLSL and Metal / MSL backends, enabling the GPU unit test suite to run natively on Windows without requiring an OpenGL context.

Key changes:

  • GraphicalApp abstract interface (graphicalapp.h/cpp)
    • Backend-agnostic base class extracted from OglApp. OglApp and MetalApp now inherit from it.
  • DxApp (dxapp.h/cpp) -- DirectX 12 backend
    • Off-screen RGBA32F render target, full-screen triangle via SV_VertexID, staging readback, SM 6.0 DXC shader compilation.
  • HLSLBuilder (hlsl.h/cpp) -- HLSL shader generation
    • Translates GpuShaderDesc into HLSL pixel shaders with 1D and 3D LUT texture uploads in RGBA32F format.
  • CMake integration
    • OCIO_DIRECTX_ENABLED option, FetchContent for DirectX-Headers, auto-copy of DXC runtime DLLs to the test output directory.
  • Test tolerance adjustments
    • Minor epsilon increases for 4 tests due to DX12/SM6.0 FMA and pow() precision differences.
    • All 263 GPU tests pass on the DirectX 12 backend.

Build and run:

# Configure (OCIO_DIRECTX_ENABLED defaults to ON on Windows)
  cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

  # Build the GPU test binary
  cmake --build build --target test_gpu_exec --config Release

  # Run GPU tests with the DX12 backend
  ctest --test-dir build -C Release -R test_dx

Further Notes:

  • For a few releases now, we have had to manually test HLSL shader generation. This integration would significantly facilitate this process moving forward.
  • While I maintain ownership, this code change was partly AI-assisted. I used Claude Code to help me finish the DirectX implementation (which I had started more than a year ago), debug failing tests during implementation, review the changes, etc.

@num3ric
Copy link
Copy Markdown
Contributor Author

num3ric commented Mar 7, 2026

Links with issue #526. Note that I chose DX12 over DX11 to be forward-looking. It remains backwards compatible with the SM5 shader generation.

EDIT: Would appreciate help with testing other GPU apps to ensure they all continue to function properly.

@doug-walker doug-walker requested a review from cozdas March 7, 2026 20:44
@doug-walker doug-walker added this to the OCIO 2.6.0 milestone Mar 19, 2026
num3ric added 2 commits April 6, 2026 12:35
Introduce a DirectX 12 / HLSL rendering backend alongside the existing OpenGL / GLSL and Metal / MSL backends, enabling the GPU unit test suite to run natively on Windows without requiring an OpenGL context.

Key changes:

  GraphicalApp abstract interface (graphicalapp.h/cpp)

    Backend-agnostic base class extracted from OglApp.  OglApp and MetalApp now inherit from it.

  DxApp (dxapp.h/cpp) -- DirectX 12 backend

    Off-screen RGBA32F render target, full-screen triangle via SV_VertexID, staging readback, SM 6.0 DXC shader compilation.

  HLSLBuilder (hlsl.h/cpp) -- HLSL shader generation

    Translates GpuShaderDesc into HLSL pixel shaders with 1D and 3D LUT texture uploads in RGBA32F format.

  CMake integration

    OCIO_DIRECTX_ENABLED option, FetchContent for DirectX-Headers, auto-copy of DXC runtime DLLs to the test output directory.

  Test tolerance adjustments

    Minor epsilon increases for 4 tests due to DX12/SM6.0 FMA and pow() precision differences.

  All 263 GPU tests pass on the DirectX 12 backend.

Build and run:

  # Configure (OCIO_DIRECTX_ENABLED defaults to ON on Windows)

  cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

  # Build the GPU test binary

  cmake --build build --target test_gpu_exec --config Release

  # Run GPU tests with the DX12 backend

  ctest --test-dir build -C Release -R test_dx

Signed-off-by: Eric Renaud-Houde <eric.renaud.houde@gmail.com>
  - HeadlessOglApp::printGraphicsInfo() was calling pure virtual base (crash on headless EGL)
  - graphicalapp.cpp included oglapp.h unconditionally; guard under OCIO_GL_ENABLED
  - tests/gpu/CMakeLists.txt early-return guard excluded Vulkan-only builds
  - Add missing test_vulkan ctest entry

Signed-off-by: Eric Renaud-Houde <eric.renaud.houde@gmail.com>
Signed-off-by: Eric Renaud-Houde <eric.renaud.houde@gmail.com>
Copy link
Copy Markdown
Collaborator

@doug-walker doug-walker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome @num3ric !

I was able to compile this on Windows and run all three GPU test suites: GL, Vulkan, and DX successfully!

One thing that surprised me is that the DX tests seem to take a lot longer to run than the GL or Vulkan tests, any thoughts about that? Here's what I see on my machine:

      Start 10: test_gpu
10/13 Test #10: test_gpu .........................   Passed   12.12 sec
      Start 11: test_dx
11/13 Test #11: test_dx ..........................   Passed   19.55 sec
      Start 12: test_vulkan
12/13 Test #12: test_vulkan ......................   Passed    9.75 sec

Here's the GPU info for my machine:

GL Vendor:    NVIDIA Corporation
GL Renderer:  Tesla T4/PCIe/SSE2
GL Version:   4.6.0 NVIDIA 528.24
GLSL Version: 4.60 NVIDIA

I tested that ociochecklut, ocioconvert, and ociodisplay still run. These still use the GL path, but at least nothing broke.

I compiled and ran the tests and apps on macOS as well, just to check nothing broke.

Great work!

Comment thread tests/gpu/GPUUnitTest.cpp
bool printHelp = false;
bool useMetalRenderer = false;
bool useVulkanRenderer = false;
bool useDxRenderer = false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When compiling on Mac, I get unused variable warnings on lines 572 and 573. I'm surprised that the CI passed given that it is supposed to handle warnings as errors. But please either put these in ifdef or remove the ifdefs for the command-line arguments.

Comment thread tests/gpu/CMakeLists.txt
endif()
if(WIN32 AND OCIO_DIRECTX_ENABLED)
set_tests_properties(test_dx PROPERTIES ENVIRONMENT PATH=${NEW_PATH})
endif()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to add the following for the Vulkan GPU tests to run successfully:

    if(OCIO_VULKAN_ENABLED)
        set_tests_properties(test_vulkan PROPERTIES ENVIRONMENT PATH=${NEW_PATH})
    endif()

if(NOT OCIO_GL_ENABLED)
message(WARNING "GL component missing. Skipping oglapphelpers.")
if(NOT OCIO_GL_ENABLED AND NOT (WIN32 AND OCIO_DIRECTX_ENABLED) AND NOT OCIO_VULKAN_ENABLED)
message(WARNING "GL component missing, DirectX disabled, and Vulkan disabled. Skipping oglapphelpers.")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a description of OCIO_DIRECTX_ENABLED to the list of CMake variables in:
OpenColorIO/docs/quick_start/installation.rst

And, if you could do me a favor, please add OCIO_VULKAN_ENABLED at the same time.

Copy link
Copy Markdown
Collaborator

@remia remia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @num3ric , I like the cleanup you did adding GraphicalApp. I cannot comment on the DX12 implementation details but this otherwise looks good to me (left a couple of minor comments).

Tangent but maybe time to remove the GPU_LANGUAGE_HLSL_DX11 duplicated enum for the next version?

Comment thread tests/gpu/CMakeLists.txt
# Copy dxcompiler.dll and dxil.dll to the test output directory.
# These are required at runtime when DXC (IDxcCompiler3) is used for SM6.0 shader compilation.
# The Redist/D3D path is the stable, version-independent redistribution location.
if(WIN32 AND OCIO_DIRECTX_ENABLED)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my test workstation the unit tests crash, I checked dxdiag and it advertise DX 12_1 support but the dxcompiler.dll found here seems to be around 5y old. When I manually copy the DLL from dxcompiler latest release, the tests run successfully. The workstation is stuck on Win10 and not updated so probably not expecting it to work flawlessly but maybe there is a way to improve the error. Just a note for now.

Comment thread tests/gpu/CMakeLists.txt Outdated

add_test(NAME test_gpu COMMAND test_gpu_exec)
if(OCIO_GL_ENABLED)
add_test(NAME test_gpu COMMAND test_gpu_exec)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point could we rename test_gpu to test_opengl to make it less confusing?

Comment thread CMakeLists.txt
message(STATUS "Checking for GPU configuration...")
include(CheckSupportGL)

# DirectX 12 is only available on Windows.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not from this PR but maybe we should use this opportunity to also add the same option for OCIO_VULKAN_ENABLED.

bool g_gpulegacy = false;
bool g_gpuinfo = false;
#if __APPLE__
bool g_useMetal = false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct to say adding support for DirectX and/or Vulkan here would be more work than for Metal due to the use of GLUT (which we should probably move away from)? And for DX12 specifically, it currently only support headless rendering and more work would be needed for windowing support?

It would be nice to have these modes available to use ociodisplay with debugger/profilers like Nvidia NSight / Renderdoc (like you can already do with XCode Metal debugger).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-GLUT & metal, that's correct. An alternative framework would be something like GLFW, which apparently can also support DX12 (though I've never tried).

The DX12 implementation is not headless currently, since my starting point was the separate app I'd made for testing purposes (similar to ociodisplay). With this PR, we still create a full window (see CreateWindowA), swapchain, present loop, but we render the OCIO transforms to an offscreen RT for test readback. I'd initially picked this approach since it's closer to practical use cases (what most users are going to be implementing), and mirrors the OpenGL implementation. Though as I learned through recent updates, the new Vulkan path is indeed different being fully headless/compute.

We could add an headless option here too, but I'd suggest that being a separate line of work/PR if needed.

set(DIRECTX_HEADERS_BUILD_TEST OFF CACHE BOOL "" FORCE)

FetchContent_Declare(DirectX-Headers
GIT_REPOSITORY https://github.com/microsoft/DirectX-Headers.git
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker I think but may be nice to offer a way to specify a path manually for Windows systems without internet access (unless you already did and I missed it in the PR).

num3ric added 4 commits April 18, 2026 21:55
  The DX12 test suite was noticeably slower than the OpenGL and Vulkan backends. Profiling the run showed the gap was almost entirely in DXC shader
  compilation, not in Present, fence waits, or DxcCreateInstance as initially suspected.

  Three low-risk changes:

  - Cache IDxcUtils and IDxcCompiler3 as DxApp members instead of recreating them on every setShader() call. The COM instances are thread-safe and
  perfectly reusable; recreating them per test added no value.
  - Compile the full-screen-triangle vertex shader exactly once and reuse the bytecode across all tests. The VSMain HLSL is a hard-coded
  SV_VertexID-driven triangle with no test-specific state — the bytecode is identical every time. Extracted into a new ensureVertexShaderCompiled()
  helper. This alone eliminated the biggest redundancy (263 duplicate VS compiles).
  - Present(1, 0) → Present(0, 0). VSync is meaningless for an off-screen test harness that reads back from a float render target. Locally the win shows
  up mostly in waitForPreviousFrame, which was being throttled by the swap-chain pipeline even on an invisible window.

  All 263/263 tests still pass; no tolerance changes, no DXIL codegen changes (except for a UTF8 fix), no precision risk.

Signed-off-by: Eric Renaud-Houde <eric.renaud.houde@gmail.com>
…ture.

  - Fix unused-variable warnings (fatal on macOS with warnings-as-errors): guard useDxRenderer and useVulkanRenderer declarations with the same ifdefs as their usage sites. useMetalRenderer
   stays unconditional because it's referenced on all platforms.
  - Propagate the MSVC+shared-libs PATH workaround to test_vulkan so it can find OpenColorIO_*.dll at runtime, matching what's already done for test_dx.
  - Upgrade the dxcompiler.dll detection message from STATUS to WARNING and rewrite it to name OCIO_DIRECTX_ENABLED and offer concrete recovery paths. The previous STATUS message was easy
  to miss, leaving users with a silent degradation until test_dx failed at runtime.
  - Rename the OpenGL ctest from test_gpu to test_opengl now that sibling backend-specific tests (test_dx, test_vulkan, test_metal) exist. The test_gpu_exec binary keeps its name since it's
   backend-agnostic and selects via CLI flags.
  - Declare OCIO_VULKAN_ENABLED as a first-class CMake option with mark_as_advanced, matching the existing OCIO_DIRECTX_ENABLED. It was previously used in conditionals without ever being
  declared, so it never appeared as a toggle in ccmake/cmake-gui.
  - Document both OCIO_DIRECTX_ENABLED and OCIO_VULKAN_ENABLED in docs/quick_start/installation.rst, noting that Vulkan requires an external SDK.

Signed-off-by: Eric Renaud-Houde <eric.renaud.houde@gmail.com>
  Previously InstallDirectXHeaders.cmake was included unconditionally from oglapphelpers/CMakeLists.txt, so DirectX-Headers was always fetched from GitHub regardless of whether the user had
   a local copy installed. There was no way to use a system install, a vendored copy, or an air-gapped build, and the dep didn't respect OCIO_INSTALL_EXT_PACKAGES. DirectX-Headers is now a
  first-class OCIO dependency, handled the same way as Imath, ZLIB, yaml-cpp, etc.: try find_package first, fall back to FetchContent only if not found and OCIO_INSTALL_EXT_PACKAGES allows
  it.

  Changes:
  - New share/cmake/modules/FindDirectX-Headers.cmake, modeled on FindImath.cmake.
  - InstallDirectXHeaders.cmake → InstallDirectX-Headers.cmake (the hyphen matches OCIO's Install convention).
  - oglapphelpers/CMakeLists.txt now calls ocio_handle_dependency(DirectX-Headers ...) with MIN_VERSION 1.606.0 (Windows SDK 22H2 era — old enough to cover most installed copies) and
  RECOMMENDED_VERSION 1.619.1 (the version OCIO pins and validates).

  For users: a local DirectX-Headers install can now be supplied via any of the standard CMake mechanisms — -DDirectX-Headers_DIR, -DDirectX-Headers_ROOT, -DDirectX-Headers_INCLUDE_DIR, or
  globally with -DOCIO_INSTALL_EXT_PACKAGES=NONE to forbid any network fetch.

Signed-off-by: Eric Renaud-Houde <eric.renaud.houde@gmail.com>
  Addresses test crashes seen on stuck Windows 10 hosts caused by an old dxcompiler.dll shipped in that host's Windows SDK Redist.

  - Print the version of the found dxcompiler.dll at configure time so crash reports identify the exact DXC build without follow-up diagnostics.
  - Emit a standing hint pointing at the DirectX Shader Compiler releases page, which is the documented workaround.
  - New -DOCIO_DXCOMPILER_DLL=<path> overrides the Windows SDK Redist search, letting users supply a newer DLL pre-build instead of copying it by hand after.
  - Extracted the DXC-runtime logic into share/cmake/utils/LocateDXCompilerRuntime.cmake so tests/gpu/CMakeLists.txt stays focused on the test target.

Signed-off-by: Eric Renaud-Houde <eric.renaud.houde@gmail.com>
@num3ric
Copy link
Copy Markdown
Contributor Author

num3ric commented Apr 19, 2026

Addressed all notes thus far. Please refer to each commit message for detailed descriptions regarding improvements, fixes, and/or findings.

Signed-off-by: Eric Renaud-Houde <eric.renaud.houde@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants