Skip to content

Repository files navigation

DirectXSplat

License

A C++/Direct3D 12 library for rendering 3D Gaussian Splats in D3D12.

demo.mp4

This is a C++/Direct3D 12 library for rendering scenes captured via the techniques described in 3D Gaussian Splatting for Real-Time Radiance Field Rendering. It lets you load PLY, SPZ, .splat, SOG, and LOD metadata scenes and render them from a host D3D12 application.

Overview

  • Fast rendering: GPU-accelerated D3D12 rasterization for high performance
  • Up to 2.4x faster than gsplat in matched-quality resident-scene benchmarks
  • Native embeddable D3D12 renderer with host-owned device, queue, command list, fences, and render targets
  • Memory-conscious: persistent uploaded scenes and renderer-owned resource reuse
  • No CUDA dependency for DirectXSplat rendering
  • Compatible with trained 3DGS scenes in PLY, SPZ, .splat, SOG, and LOD metadata formats
  • Convenience APIs included: interactive viewer via Show(...) and offscreen image capture via Draw(...)

Requirements

  • Windows 10/11 with a Direct3D 12-capable GPU
  • Visual Studio 2022 with MSVC C++20 tools and Windows SDK
  • CMake >= 3.25
  • Python >= 3.10 only for benchmark scripts
  • Benchmark Python packages via pip install -r benches/requirements.txt

Quick Start

cmake -S .-B build -G "Visual Studio 17 2022"-A x64 `-DDIRECTXSPLAT_BUILD_EXAMPLES=ON `-DDIRECTXSPLAT_BUILD_TESTS=ON
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure

Run the convenience viewer:

.\build\bin\Release\DirectXSplatViewer.exe path\to\point_cloud.ply

Pre-trained Dataset

This project can be used with scenes from Gaussian Splatting. The official pre-trained models are available here.

Download the datasets into the models/ directory. For a scene laid out like:

models/garden/
cameras.json
point_cloud/iteration_30000/point_cloud.ply

The commands below assume the release executables are on your PATH or in the current directory. If you built from source, prefix them with .\build\bin\Release\.

run the C++ viewer with the trained 3DGS scene:

$scene="garden"DirectXSplatViewer.exe"models\$scene\point_cloud\iteration_30000\point_cloud.ply"

To open the same scene with the dataset camera set, build examples and run:

$scene="garden"DirectXSplatCameraViewerExample.exe`"models\$scene\point_cloud\iteration_30000\point_cloud.ply"`"models\$scene\cameras.json"

Replace garden with another downloaded scene, such as bicycle, bonsai, counter, kitchen, room, or stump.

Build Library

cmake -S .-B build-lib-G "Visual Studio 17 2022"-A x64 `-DDIRECTXSPLAT_BUILD_EXAMPLES=OFF `-DDIRECTXSPLAT_BUILD_TESTS=OFF
cmake --build build-lib--config Release --target directxsplat

Use From CMake

Fetch the v0.1.0 release:

include(FetchContent)
FetchContent_Declare(
DirectXSplat
GIT_REPOSITORY https://github.com/pbkx/DirectXSplat.git
GIT_TAG v0.1.0
)
FetchContent_MakeAvailable(DirectXSplat)
target_link_libraries(my_appPRIVATEDirectXSplat::DirectXSplat)

Install:

cmake -S .-B build-install-G "Visual Studio 17 2022"-A x64 `-DDIRECTXSPLAT_BUILD_EXAMPLES=OFF `-DDIRECTXSPLAT_BUILD_TESTS=OFF `-DCMAKE_INSTALL_PREFIX="$PWD\install"
cmake --build build-install--config Release --target install

Consume:

find_package(DirectXSplatCONFIGREQUIRED)
add_executable(my_appmain.cpp)
target_link_libraries(my_appPRIVATEDirectXSplat::DirectXSplat)
target_compile_features(my_appPRIVATEcxx_std_20)

Configure the consuming project with the DirectXSplat install prefix:

cmake -S my-app -B my-app-build -DCMAKE_PREFIX_PATH="$PWD\install"
cmake --build my-app-build --config Release

As a subproject, examples and tests are off by default:

add_subdirectory(external/directxsplat)
target_link_libraries(my_appPRIVATEDirectXSplat::DirectXSplat)

Primary API

Use the renderer API when DirectXSplat is embedded into an existing D3D12 renderer. Your application provides the D3D12 device, direct queue, command lists, fences, render targets, and frame loop. DirectXSplat records splat rendering commands into the command list you provide.

#include<directxsplat/context.h>
#include<directxsplat/io.h>
#include<directxsplat/renderer.h>structDirectXSplatSession {
directxsplat::D3D12Context context;
directxsplat::Renderer renderer;
directxsplat::UploadedSceneHandle scene;
};
directxsplat::Status CreateDirectXSplatSession(DirectXSplatSession& session,
ID3D12Device* device,
ID3D12CommandQueue* directQueue,
ID3D12Fence* directFence,
constchar* scenePath) {
directxsplat::Status status = session.context.Initialize(device, directQueue, directFence);
if (!status.ok) {
return status;
}
status = session.renderer.Initialize(session.context);
if (!status.ok) {
return status;
}
directxsplat::StatusOr<directxsplat::Scene> loaded = directxsplat::LoadSceneFromFile(scenePath);
if (!loaded.ok()) {
return loaded.status;
}
return session.renderer.CreateUploadedScene(loaded.value, session.scene);
}

Pass a reset, open direct command list to the frame function. It records the DirectXSplat work, closes the command list, submits it on the same direct queue used to initialize the context, and signals the frame fence.

directxsplat::Status RenderDirectXSplatFrame(DirectXSplatSession& session,
ID3D12GraphicsCommandList* commandList,
ID3D12CommandQueue* directQueue,
ID3D12Fence* directFence,
ID3D12Resource* colorTarget,
D3D12_CPU_DESCRIPTOR_HANDLE colorRtv,
DXGI_FORMAT colorFormat,
D3D12_RESOURCE_STATES colorStateBefore,
D3D12_RESOURCE_STATES colorStateAfter,
D3D12_VIEWPORT viewport,
D3D12_RECT scissor,
const directxsplat::RenderInput& input,
uint64_t submissionFenceValue) {
directxsplat::RenderFrameContext frameContext{};
frameContext.fence = directFence;
frameContext.completedFenceValue = directFence->GetCompletedValue();
frameContext.submissionFenceValue = submissionFenceValue;
frameContext.frameIndex = input.frameIndex;
directxsplat::RenderPreparationResult preparation{};
directxsplat::Status status =
session.renderer.PrepareSceneForRender(session.scene, input, frameContext, &preparation);
if (!status.ok) {
return status;
}
directxsplat::RenderTargetBinding target{};
target.colorTarget = colorTarget;
target.colorRtv = colorRtv;
target.colorFormat = colorFormat;
target.colorStateBefore = colorStateBefore;
target.colorStateAfter = colorStateAfter;
target.transitionMode = directxsplat::ResourceTransitionMode::LibraryManaged;
target.viewport = viewport;
target.scissor = scissor;
directxsplat::RenderResult result{};
status = session.renderer.Render(commandList, target, session.scene, input, frameContext, result);
if (!status.ok && !result.submission.submissionRequired) {
return status;
}
if (result.submission.uploadSyncPoint.IsValid()) {
HRESULT hr = directQueue->Wait(result.submission.uploadSyncPoint.fence,
result.submission.uploadSyncPoint.value);
if (FAILED(hr)) {
session.renderer.NotifyDeviceLost();
returndirectxsplat::Status::Error("failed waiting for DirectXSplat upload sync point");
}
}
HRESULT hr = commandList->Close();
if (FAILED(hr)) {
session.renderer.NotifyDeviceLost();
returndirectxsplat::Status::Error("failed closing DirectXSplat command list");
}
ID3D12CommandList* commandLists[] = {commandList};
directQueue->ExecuteCommandLists(1, commandLists);
hr = directQueue->Signal(directFence, frameContext.submissionFenceValue);
if (FAILED(hr)) {
session.renderer.NotifyDeviceLost();
returndirectxsplat::Status::Error("failed signaling DirectXSplat frame fence");
}
return status;
}

Shutdown is explicit:

session.renderer.DestroyUploadedScene(session.scene);
session.renderer.Shutdown();
session.context.Shutdown();

Ownership and Threading

  • D3D12Context stores non-owning pointers and does not retain COM references. Keep the supplied device, queues, and fences alive until Renderer::Shutdown() and D3D12Context::Shutdown() complete.
  • Renderer::Initialize(...) retains internal COM references to the D3D12 objects it uses. The host still owns command allocators, command lists, queue submission order, fence values, render targets, and device-lost recovery.
  • Renderer calls may come from separate host threads, but the host must synchronize its own D3D12 objects. Never record into the same command list or reset the same command allocator concurrently.
  • Rendering and mutation are mutually exclusive for the same uploaded scene. BeginSceneMutation(...) may wait for active render encoding to finish, and every successful call must be paired with EndSceneMutation(...).
  • Multiple Renderer instances may share one context when the host coordinates command lists, queue ordering, and unique fence values across all instances.

Frame Submission Contract

RenderFrameContext is required for rendering. DirectXSplat uses it to track when renderer-owned GPU resources can be reused or released.

Required fields:

  • fence must be the same direct-queue fence passed to D3D12Context::Initialize(...).
  • completedFenceValue must be the latest known completed value for that fence, normally read from directFence->GetCompletedValue().
  • submissionFenceValue must be nonzero, unique for the submitted frame, greater than every value previously signaled on that fence, and greater than completedFenceValue.
  • frameIndex should be the host frame index for the render input.

Submission rules:

  • Call PrepareSceneForRender(...) before Render(...) for the same scene, input, and frame context.
  • Pass a reset, open command list to Render(...), then close it before submission.
  • If RenderResult::submission.uploadSyncPoint is valid, make the direct queue wait on it before executing the command list.
  • Execute the command list and signal submissionFenceValue on the same direct queue used to initialize the context. Signal only after all work for that frame has been submitted.
  • Pass the fence's resulting completion progress back through completedFenceValue on future frames.
  • If Render returns an error with submissionRequired == true, command-list work may already be recorded. Submit/signal it or enter device-lost cleanup.
  • If queue execution or signaling fails after renderer work was recorded, call renderer.NotifyDeviceLost() before shutdown.
  • Use ResourceTransitionMode::LibraryManaged when DirectXSplat should transition the target from colorStateBefore to colorStateAfter. Use CallerManaged when your renderer handles those transitions.

Render Target Contract

  • colorRtv must describe colorTarget, and colorFormat must match the RTV's single-sampled render-target format. Unsupported formats fail pipeline creation.
  • With ResourceTransitionMode::LibraryManaged, the resource must be in colorStateBefore when rendering begins; DirectXSplat transitions it to colorStateAfter. With CallerManaged, the host must perform the required color, depth, and motion-vector transitions.
  • viewport and scissor must be valid for the bound target and stay within its dimensions.
  • Color output uses source-alpha-over blending. The host owns render-target color-space selection and presentation.
  • Depth output is optional and contains approximate splat depth. When RenderSettings::outputDepth is enabled, provide a matching single-sampled depthTarget, depthDsv, depthFormat, and valid resource states.

Renderer Configuration

directxsplat::RendererConfig config{};
config.residencyBudgetGaussians = 16ull * 1024ull * 1024ull;
config.uploadBudgetGaussians = 650000;
config.warmUploadBudgetGaussians = 1200000;
config.maxUploadsPerFrame = 16;
config.chunkTargetSize = 65536;
config.lod0ScreenRadius = 72.0f;
config.lod1ScreenRadius = 24.0f;
config.cullScreenRadius = 0.35f;
config.residencyCacheFrames = 120;
config.enableGpuTiming = true;
renderer.Initialize(context, config);

Most apps can start with defaults.

Scene Loading

Advanced renderer integrations load directxsplat::Scene directly:

directxsplat::StatusOr<directxsplat::Scene> LoadSceneForDirectXSplat(constchar* scenePath,
constchar* sourceImagesPath) {
directxsplat::SceneLoadOptions options{};
if (sourceImagesPath != nullptr) {
options.sourceImageDirectory = sourceImagesPath;
}
returndirectxsplat::LoadSceneFromFile(scenePath, options);
}

LoadSceneFromFile(...) detects PLY, compressed PLY, SPZ, .splat, SOG, and hierarchical LOD metadata. The convenience loaders LoadFromFile(...), LoadFromPly(...), and LoadFromSpz(...) return GaussianSplats for the convenience wrappers.

Scene Updates

Whole scene:

directxsplat::Status status = renderer.UpdateUploadedScene(sceneHandle, newScene);
if (!status.ok) {
return status;
}

Chunk mutation:

directxsplat::SceneMutationToken token;
directxsplat::Status status = renderer.BeginSceneMutation(sceneHandle, token);
if (!status.ok) {
return status;
}
status = renderer.AddUploadedChunk(token, gaussianSet, outChunkHandle);
directxsplat::Status endStatus = renderer.EndSceneMutation(token);
if (!status.ok) {
return status;
}
return endStatus;

Available chunk operations:

  • AddUploadedChunk
  • UpdateUploadedChunk
  • RemoveUploadedChunk
  • SetUploadedChunkEnabled
  • SetUploadedChunkScalingModifier
  • GetUploadedSceneChunks
  • GetUploadedChunkInfo

GPU Resource Interop

GetUploadedSceneGpuResources(...) returns a non-leasing snapshot. AcquireUploadedSceneGpuResources(...) returns a lease for external GPU work that references renderer-owned resources. Reset and open externalCommandList before recording external work. Submit every command that references the leased resources before signaling the lease fence.

directxsplat::UploadedSceneGpuResources resources{};
directxsplat::Status status =
renderer.AcquireUploadedSceneGpuResources(sceneHandle, frameContext, resources);
if (!status.ok) return status;
if (resources.submission.uploadSyncPoint.IsValid()) {
HRESULT hr = directQueue->Wait(resources.submission.uploadSyncPoint.fence,
resources.submission.uploadSyncPoint.value);
if (FAILED(hr)) {
renderer.NotifyDeviceLost();
returndirectxsplat::Status::Error("failed waiting for DirectXSplat upload sync point");
}
}
// Record external commands that reference resources into externalCommandList.HRESULT hr = externalCommandList->Close();
if (FAILED(hr)) {
renderer.NotifyDeviceLost();
returndirectxsplat::Status::Error("failed closing external command list");
}
ID3D12CommandList* commandLists[] = {externalCommandList};
directQueue->ExecuteCommandLists(1, commandLists);
// Release the lease only after submitting every command that uses its resources.
hr = directQueue->Signal(resources.leaseFence, resources.leaseFenceValue);
if (FAILED(hr)) {
renderer.NotifyDeviceLost();
returndirectxsplat::Status::Error("failed signaling DirectXSplat resource lease fence");
}

Returned resources are renderer-owned. Respect callerMayTransition and callerMayWrite, and do not cache raw resource pointers beyond the snapshot or lease lifetime. Use GetUploadedSceneGpuResources only for a non-leasing snapshot; use AcquireUploadedSceneGpuResources before recording external GPU work that references the returned resources. After submitting that external GPU work, signal resources.leaseFence with resources.leaseFenceValue.

Convenience Wrappers

The convenience API creates the viewer or an owned runtime for you. It is useful for tools, smoke tests, screenshots, and simple applications that do not need to integrate with an existing D3D12 frame loop.

#include<directxsplat/directxsplat.h>auto splats = directxsplat::LoadFromPly("point_cloud.ply");
if (!splats.ok()) {
return splats.status;
}
directxsplat::Status status = directxsplat::Show(splats.value);
if (!status.ok) {
return status;
}

Draw(...) is an owned-runtime convenience capture path. It creates its own D3D12 runtime, renders one image, reads pixels back to the CPU, and returns ImageRgba8. It is not the production render loop and is not the benchmark timing path.

#include<directxsplat/directxsplat.h>auto splats = directxsplat::LoadFromPly("point_cloud.ply");
if (!splats.ok()) {
return splats.status;
}
directxsplat::CameraSet cameras = directxsplat::MakeOrbitCameraSet(splats.value, 1, 512, 512);
directxsplat::DrawOptions options{};
options.width = 512;
options.height = 512;
auto image = directxsplat::Draw(splats.value, cameras.cameras[0], options);
if (!image.ok()) {
return image.status;
}

Examples

TargetExecutablePurpose
DirectXSplatHostD3D12RenderExampleDirectXSplatHostD3D12RenderExample.exeHost-owned D3D12 render submission.
DirectXSplatOffscreenCaptureExampleDirectXSplatOffscreenCaptureExample.exeDirect renderer capture to PPM.
DirectXSplatSceneUpdatesExampleDirectXSplatSceneUpdatesExample.exeUploaded scene/chunk mutation.
DirectXSplatGpuResourceInteropExampleDirectXSplatGpuResourceInteropExample.exeRenderer-owned GPU resource lease.
directxsplat_viewerDirectXSplatViewer.exeConvenience interactive viewer.
DirectXSplatBasicViewerExampleDirectXSplatBasicViewerExample.exeConvenience scene viewer.
DirectXSplatCameraViewerExampleDirectXSplatCameraViewerExample.exeConvenience viewer with camera set.
DirectXSplatBasicDrawExampleDirectXSplatBasicDrawExample.exeConvenience one-image capture.

Benchmarks

Tested on NVIDIA GeForce RTX 4070 SUPER, Windows 11. FPS measures resident-scene, single-camera GPU render throughput; scene loading, initial upload, warmup, GPU-to-CPU readback, UI, and presentation are excluded.

ImplementationDataset#imgsResolution#splatsPSNRFPS
DirectXSplatbicycle1941237x8226,131,95419.17 +/- 1.71382.51
gsplatbicycle1941237x8226,131,95419.17 +/- 1.60165.82
DirectXSplatgarden1851297x8405,834,78418.99 +/- 0.75319.14
gsplatgarden1851297x8405,834,78418.96 +/- 0.74131.68
DirectXSplattreehill1411267x8323,783,76120.24 +/- 2.64491.86
gsplattreehill1411267x8323,783,76120.20 +/- 2.69224.86

See benches/README.md for setup, methodology, full results, and gsplat tuning details.

Build Options

OptionDefaultDescription
DIRECTXSPLAT_BUILD_TESTSON top-level, OFF as subprojectBuild tests.
DIRECTXSPLAT_BUILD_VIEWERON top-level, OFF as subprojectBuild the DirectXSplat viewer executable.
DIRECTXSPLAT_BUILD_EXAMPLESOFFBuild examples.
DIRECTXSPLAT_BUILD_BENCHESOFFBuild benchmark harnesses.
DIRECTXSPLAT_ENABLE_GPU_TESTSONEnable renderer GPU tests when supported.
DIRECTXSPLAT_ENABLE_WARNINGSONEnable warning flags.
DIRECTXSPLAT_WARNINGS_AS_ERRORSOFFTreat warnings as errors.

The previous DXSPLAT_* option names are still accepted as compatibility aliases.

License

DirectXSplat is licensed under the MIT License, copyright (c) 2026 pbkx.

3rd-Party Licenses

LibraryURLLicense
Microsoft D3DX12 helper headershttps://github.com/microsoft/DirectX-HeadersMIT
ghc::filesystemhttps://github.com/gulrak/filesystemMIT
minizhttps://github.com/richgel999/minizUnlicense / Public Domain
stb_imagehttps://github.com/nothings/stbMIT or Public Domain
GPUSortinghttps://github.com/b0nes164/GPUSortingMIT

The bundled GPUSorting package also includes notices for incorporated third-party components:

ProjectURLLicense
FidelityFX SDKhttps://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDKMIT
CUBhttps://github.com/NVIDIA/cubBSD 3-Clause
DirectStoragehttps://github.com/microsoft/DirectStorageMIT
bb_segsorthttps://github.com/vtsynergy/bb_segsortLGPL 2.1

Additional dependencies fetched by CMake:

LibraryURLLicense
nlohmann/jsonhttps://github.com/nlohmann/jsonMIT
Dear ImGuihttps://github.com/ocornut/imguiMIT
doctesthttps://github.com/doctest/doctestMIT

About

A C++/Direct3D 12 library for rendering 3D Gaussian Splats in D3D12.

Resources

Stars

18 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages