Skip to content

Commit a3b5e99

Browse files
MarijnS95claude
andcommitted
Add ComputeEncoder::batchBuildAS() and shared AS-build orchestration helper
Move acceleration-structure build commands behind the abstract ComputeEncoder interface so the orchestration (data upload, build-request creation, AS allocation, build recording) can live in one place rather than splitting across three backends. ComputeEncoder gains a single batchBuildAS(ArrayRef<ASBuildItem>) method. Each item carries an AccelerationStructure plus a BLAS or TLAS build request via PointerUnion. The caller guarantees no inter-item memory dependencies inside a batch — backends record the whole batch with one barrier slot, no per-element barriers. - Vulkan: single vkCmdBuildAccelerationStructuresKHR call covering the whole batch. TLAS items serialize VkAccelerationStructureInstanceKHR into a device-address upload buffer, BLAS items pull addresses from each VulkanBuffer (new getDeviceAddress accessor). Storage buffers transparently gain SHADER_DEVICE_ADDRESS + ACCEL_BUILD_INPUT_READ_ONLY flags when ray tracing is supported, with the matching VkMemoryAllocateFlagsInfo chained on every allocation. - DX12: loop calling BuildRaytracingAccelerationStructure per item with no intermediate barriers; D3D12_RAYTRACING_INSTANCE_DESC is bit-identical to the Vulkan instance struct. - Metal: lazy transition to MTL::AccelerationStructureCommandEncoder, deduplicates BLAS handles into the MTL::InstanceAccelerationStructureDescriptor's instancedAccelera- tionStructures array (Metal references BLASes by index, not GPU address). Each backend's CommandBuffer now carries a back-pointer to its owning Device so the encoder can reach device-loaded entry points and helpers, plus a keep-alive list for AS scratch and instance buffers. A shared helper buildPipelineAccelerationStructures in lib/API/Device.cpp walks Pipeline::AccelStructs, uploads vertex/index data via the new createBufferWithData, builds requests, allocates AS objects, and issues two batchBuildAS calls (BLAS batch then TLAS batch — VUID-03403 forbids referencing a sibling dstAccelerationStructure in one command). Each backend's executeProgram calls this helper to build the pipeline's AS objects. Descriptor binding for AS resources is intentionally still missing — the existing InlineRT tests still fail at the descriptor-creation step (an unbound AS resource trips createSRV / equivalents) and stay caught by their XFAIL: * annotation; PR #1245 wires the AS descriptor write and flips them green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bc20b5c commit a3b5e99

6 files changed

Lines changed: 840 additions & 10 deletions

File tree

include/API/Device.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,21 @@ createBufferWithData(Device &Dev, std::string Name,
281281
size_t SizeInBytes, ComputeEncoder *Encoder,
282282
std::unique_ptr<offloadtest::Buffer> *OutUploadBuffer);
283283

284+
// Builds all BLAS / TLAS objects defined in `P.AccelStructs` using the
285+
// supplied compute encoder. Uploads each BLAS's vertex/index data, creates
286+
// the BLASBuildRequest + AS object via `Dev`, and records the build via
287+
// `Enc.batchBuildAS`. Then resolves TLAS instance references and records the
288+
// TLAS batch with a separate call (so the AS-build-write barrier between
289+
// BLAS and TLAS is automatic).
290+
//
291+
// Built AS objects are pushed to `OutAS` (in declaration order: BLASes first,
292+
// then TLASes). Vertex/index buffers used as build inputs are pushed to
293+
// `OutInputBuffers`; both must outlive command-buffer submission.
294+
llvm::Error buildPipelineAccelerationStructures(
295+
Device &Dev, ComputeEncoder &Enc, Pipeline &P,
296+
llvm::SmallVectorImpl<std::unique_ptr<AccelerationStructure>> &OutAS,
297+
llvm::SmallVectorImpl<std::unique_ptr<Buffer>> &OutInputBuffers);
298+
284299
} // namespace offloadtest
285300

286301
#endif // OFFLOADTEST_API_DEVICE_H

include/API/Encoder.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
#include "API/API.h"
1313

14+
#include "llvm/ADT/ArrayRef.h"
15+
#include "llvm/ADT/PointerUnion.h"
1416
#include "llvm/ADT/StringRef.h"
1517
#include "llvm/Support/Error.h"
1618

@@ -21,6 +23,19 @@ namespace offloadtest {
2123

2224
class Buffer;
2325
class PipelineState;
26+
class AccelerationStructure;
27+
struct BLASBuildRequest;
28+
struct TLASBuildRequest;
29+
30+
/// One acceleration-structure build to record in a batch. The caller is
31+
/// responsible for ensuring no item in a batch has a memory dependency on
32+
/// another (e.g. a TLAS that reads a BLAS being built in the same batch must
33+
/// be in a separate batch — that barrier is inserted between batchBuildAS
34+
/// calls automatically).
35+
struct ASBuildItem {
36+
AccelerationStructure *AS;
37+
llvm::PointerUnion<const BLASBuildRequest *, const TLASBuildRequest *> Req;
38+
};
2439

2540
/// Base class for all command encoders. An encoder records commands into a
2641
/// command buffer. Call endEncoding() when done recording. Barriers are
@@ -82,6 +97,14 @@ class ComputeEncoder : public CommandEncoder {
8297
virtual llvm::Error copyBufferToBuffer(Buffer &Src, size_t SrcOffset,
8398
Buffer &Dst, size_t DstOffset,
8499
size_t Size) = 0;
100+
101+
/// Build a batch of acceleration structures in a single barrier slot. All
102+
/// items in `Items` must be independent — no item may depend on another's
103+
/// build output. Backends may issue this as one native batch call (Vulkan)
104+
/// or as a sequence of single-AS calls without intermediate barriers (DX12,
105+
/// Metal). A barrier covering AS-build writes is implicitly emitted before
106+
/// any subsequent command that reads from the freshly-built structures.
107+
virtual llvm::Error batchBuildAS(llvm::ArrayRef<ASBuildItem> Items) = 0;
85108
};
86109

87110
struct Viewport {

lib/API/DX/Device.cpp

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,12 +571,20 @@ class DXQueue : public offloadtest::Queue {
571571
override;
572572
};
573573

574+
class DXDevice; // forward decl — defined below in this same anon ns
575+
574576
class DXCommandBuffer : public offloadtest::CommandBuffer {
575577
public:
576578
ComPtr<ID3D12CommandAllocator> Allocator;
577579
ComPtr<ID3D12GraphicsCommandListX> CmdList;
580+
/// Back-pointer to the owning device. Used by encoders that need access to
581+
/// device-level resources (e.g. allocating AS scratch buffers).
582+
DXDevice *Dev = nullptr;
578583
/// Whether a UAV barrier is pending from a prior compute command.
579584
bool PendingUAVBarrier = false;
585+
/// Buffers that must outlive command-buffer submission (e.g. AS scratch
586+
/// and TLAS instance buffers used during builds).
587+
llvm::SmallVector<std::unique_ptr<offloadtest::Buffer>> KeepAliveOwned;
580588

581589
static llvm::Expected<std::unique_ptr<DXCommandBuffer>>
582590
create(ComPtr<ID3D12DeviceX> Device) {
@@ -717,6 +725,10 @@ class DXComputeEncoder : public offloadtest::ComputeEncoder {
717725
return llvm::Error::success();
718726
}
719727

728+
// Defined out-of-line below — needs DXDevice's full type for access to the
729+
// ID3D12Device5 entry point and helper allocators.
730+
llvm::Error batchBuildAS(llvm::ArrayRef<ASBuildItem> Items) override;
731+
720732
void endEncodingImpl() override { popDebugGroup(); }
721733
};
722734

@@ -947,6 +959,10 @@ DXCommandBuffer::createRenderEncoder(
947959
}
948960

949961
class DXDevice : public offloadtest::Device {
962+
// DXComputeEncoder needs access to Device5 for AS build commands and to the
963+
// raw ID3D12Device for scratch buffer allocation.
964+
friend class DXComputeEncoder;
965+
950966
private:
951967
ComPtr<IDXCoreAdapter> Adapter;
952968
ComPtr<ID3D12DeviceX> Device;
@@ -1002,6 +1018,12 @@ class DXDevice : public offloadtest::Device {
10021018

10031019
llvm::SmallVector<DescriptorTable> DescTables;
10041020
llvm::SmallVector<ResourcePair> RootResources;
1021+
1022+
// Built acceleration structures, kept alive for the pipeline lifetime.
1023+
llvm::SmallVector<std::unique_ptr<offloadtest::AccelerationStructure>>
1024+
AccelStructs;
1025+
// Vertex/index buffers consumed during AS builds; must outlive submission.
1026+
llvm::SmallVector<std::unique_ptr<offloadtest::Buffer>> ASInputBuffers;
10051027
};
10061028

10071029
public:
@@ -1588,7 +1610,11 @@ class DXDevice : public offloadtest::Device {
15881610

15891611
llvm::Expected<std::unique_ptr<offloadtest::CommandBuffer>>
15901612
createCommandBuffer() override {
1591-
return DXCommandBuffer::create(Device);
1613+
auto CBOrErr = DXCommandBuffer::create(Device);
1614+
if (!CBOrErr)
1615+
return CBOrErr.takeError();
1616+
(*CBOrErr)->Dev = this;
1617+
return std::unique_ptr<offloadtest::CommandBuffer>(std::move(*CBOrErr));
15921618
}
15931619

15941620
llvm::Expected<std::unique_ptr<offloadtest::RenderPass>>
@@ -2630,8 +2656,19 @@ class DXDevice : public offloadtest::Device {
26302656
if (!CBOrErr)
26312657
return CBOrErr.takeError();
26322658
State.CB = std::move(*CBOrErr);
2659+
State.CB->Dev = this;
26332660
llvm::outs() << "Command buffer created.\n";
26342661

2662+
if (!P.AccelStructs.BLAS.empty() || !P.AccelStructs.TLAS.empty()) {
2663+
auto EncOrErr = State.CB->createComputeEncoder();
2664+
if (!EncOrErr)
2665+
return EncOrErr.takeError();
2666+
if (auto Err = offloadtest::buildPipelineAccelerationStructures(
2667+
*this, **EncOrErr, P, State.AccelStructs, State.ASInputBuffers))
2668+
return Err;
2669+
(*EncOrErr)->endEncoding();
2670+
}
2671+
26352672
if (auto Err = createBuffers(P, State))
26362673
return Err;
26372674
llvm::outs() << "Buffers created.\n";
@@ -2822,6 +2859,157 @@ class DXDevice : public offloadtest::Device {
28222859
return llvm::Error::success();
28232860
}
28242861
};
2862+
2863+
llvm::Error DXComputeEncoder::batchBuildAS(llvm::ArrayRef<ASBuildItem> Items) {
2864+
if (Items.empty())
2865+
return llvm::Error::success();
2866+
if (!CB.Dev || !CB.Dev->Device)
2867+
return llvm::createStringError(
2868+
std::errc::not_supported,
2869+
"Ray tracing not supported on this command buffer's device.");
2870+
DXDevice *Dev = CB.Dev;
2871+
2872+
// BuildRaytracingAccelerationStructure lives on ID3D12GraphicsCommandList4.
2873+
ComPtr<ID3D12GraphicsCommandList4> CmdList4;
2874+
if (auto Err = HR::toError(CB.CmdList.As(&CmdList4),
2875+
"Failed to query ID3D12GraphicsCommandList4."))
2876+
return Err;
2877+
2878+
// Flush a pending barrier before reading, like dispatch(): a TLAS build must
2879+
// observe BLASes built in the previous batch.
2880+
CB.flushBarrier();
2881+
2882+
// Per the ComputeEncoder::batchBuildAS contract, the caller guarantees no
2883+
// inter-item memory dependencies within a batch (BLAS and TLAS go in
2884+
// separate batches, so a TLAS never sees BLASes from the same call). Each
2885+
// item also gets its own scratch resource, so there's no aliasing between
2886+
// the builds — no intra-loop UAV barrier is needed.
2887+
for (const auto &Item : Items) {
2888+
auto *DXAS = llvm::cast<DXAccelerationStructure>(Item.AS);
2889+
2890+
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC Desc = {};
2891+
Desc.DestAccelerationStructureData = DXAS->getGPUVirtualAddress();
2892+
2893+
llvm::SmallVector<D3D12_RAYTRACING_GEOMETRY_DESC> GeomDescs;
2894+
uint64_t ScratchSize = 0;
2895+
2896+
if (const auto *BLAS = llvm::dyn_cast<const BLASBuildRequest *>(Item.Req)) {
2897+
Desc.Inputs.Type =
2898+
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL;
2899+
Desc.Inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
2900+
2901+
if (const auto *Tris =
2902+
std::get_if<llvm::SmallVector<TriangleGeometryDesc>>(
2903+
&BLAS->Geometry)) {
2904+
GeomDescs.reserve(Tris->size());
2905+
for (const auto &T : *Tris) {
2906+
D3D12_RAYTRACING_GEOMETRY_DESC GD = {};
2907+
GD.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES;
2908+
if (T.Opaque)
2909+
GD.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE;
2910+
auto *VB = llvm::cast<DXBuffer>(T.VertexBuffer);
2911+
GD.Triangles.VertexBuffer.StartAddress =
2912+
VB->Buffer->GetGPUVirtualAddress() + T.VertexBufferOffset;
2913+
GD.Triangles.VertexBuffer.StrideInBytes = T.VertexStride;
2914+
GD.Triangles.VertexCount = T.VertexCount;
2915+
GD.Triangles.VertexFormat = getDXGIFormat(T.VertexFormat);
2916+
if (T.IndexBuffer) {
2917+
auto *IB = llvm::cast<DXBuffer>(T.IndexBuffer);
2918+
GD.Triangles.IndexBuffer =
2919+
IB->Buffer->GetGPUVirtualAddress() + T.IndexBufferOffset;
2920+
GD.Triangles.IndexCount = T.IndexCount;
2921+
GD.Triangles.IndexFormat = getDXGIIndexFormat(T.IdxFormat);
2922+
}
2923+
GeomDescs.push_back(GD);
2924+
}
2925+
} else {
2926+
const auto &AABBs =
2927+
std::get<llvm::SmallVector<AABBGeometryDesc>>(BLAS->Geometry);
2928+
GeomDescs.reserve(AABBs.size());
2929+
for (const auto &A : AABBs) {
2930+
D3D12_RAYTRACING_GEOMETRY_DESC GD = {};
2931+
GD.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_PROCEDURAL_PRIMITIVE_AABBS;
2932+
if (A.Opaque)
2933+
GD.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE;
2934+
auto *AB = llvm::cast<DXBuffer>(A.AABBBuffer);
2935+
GD.AABBs.AABBs.StartAddress =
2936+
AB->Buffer->GetGPUVirtualAddress() + A.AABBBufferOffset;
2937+
GD.AABBs.AABBs.StrideInBytes = A.AABBStride;
2938+
GD.AABBs.AABBCount = A.AABBCount;
2939+
GeomDescs.push_back(GD);
2940+
}
2941+
}
2942+
Desc.Inputs.NumDescs = static_cast<UINT>(GeomDescs.size());
2943+
Desc.Inputs.pGeometryDescs = GeomDescs.data();
2944+
ScratchSize = BLAS->Sizes.ScratchDataSizeInBytes;
2945+
} else {
2946+
const auto *TLAS = llvm::cast<const TLASBuildRequest *>(Item.Req);
2947+
Desc.Inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL;
2948+
Desc.Inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY;
2949+
Desc.Inputs.NumDescs = static_cast<UINT>(TLAS->Instances.size());
2950+
2951+
// D3D12_RAYTRACING_INSTANCE_DESC has the same byte layout as
2952+
// VkAccelerationStructureInstanceKHR. Serialize and upload via the
2953+
// shared abstract-API helper using an upload-heap (CpuToGpu) buffer.
2954+
llvm::SmallVector<D3D12_RAYTRACING_INSTANCE_DESC> Native;
2955+
Native.reserve(TLAS->Instances.size());
2956+
for (const auto &Inst : TLAS->Instances) {
2957+
D3D12_RAYTRACING_INSTANCE_DESC NI = {};
2958+
static_assert(sizeof(NI.Transform) == sizeof(Inst.Transform),
2959+
"Transform layout mismatch");
2960+
memcpy(&NI.Transform, Inst.Transform, sizeof(Inst.Transform));
2961+
// D3D12_RAYTRACING_INSTANCE_DESC packs InstanceID into a 24-bit
2962+
// bitfield; truncate explicitly so the value matches the VK path
2963+
// (vkInstanceCustomIndex is likewise 24-bit) instead of relying on
2964+
// silent narrowing.
2965+
NI.InstanceID = Inst.InstanceID & 0xFFFFFFu;
2966+
NI.InstanceMask = Inst.InstanceMask;
2967+
NI.InstanceContributionToHitGroupIndex = 0;
2968+
NI.Flags = D3D12_RAYTRACING_INSTANCE_FLAG_NONE;
2969+
auto *BLASPtr = llvm::cast<DXAccelerationStructure>(Inst.BLAS);
2970+
NI.AccelerationStructure = BLASPtr->getGPUVirtualAddress();
2971+
Native.push_back(NI);
2972+
}
2973+
const size_t Bytes =
2974+
Native.size() * sizeof(D3D12_RAYTRACING_INSTANCE_DESC);
2975+
2976+
const BufferCreateDesc UploadDesc{MemoryLocation::CpuToGpu,
2977+
BufferUsage::Storage};
2978+
auto InstBufOrErr = offloadtest::createBufferWithData(
2979+
*Dev, "TLAS-Instances", UploadDesc, Native.data(), Bytes, nullptr,
2980+
nullptr);
2981+
if (!InstBufOrErr)
2982+
return InstBufOrErr.takeError();
2983+
auto *DXInstBuf = llvm::cast<DXBuffer>(InstBufOrErr->get());
2984+
Desc.Inputs.InstanceDescs = DXInstBuf->Buffer->GetGPUVirtualAddress();
2985+
2986+
CB.KeepAliveOwned.push_back(std::move(*InstBufOrErr));
2987+
ScratchSize = TLAS->Sizes.ScratchDataSizeInBytes;
2988+
}
2989+
2990+
// Allocate scratch in the default heap; createBuffer applies
2991+
// ALLOW_UNORDERED_ACCESS for default-heap allocations and creates the
2992+
// resource in COMMON state, which D3D12 implicitly promotes to
2993+
// UNORDERED_ACCESS on first GPU access during the AS build.
2994+
const BufferCreateDesc ScratchDesc{MemoryLocation::GpuOnly,
2995+
BufferUsage::Storage};
2996+
auto ScratchOrErr =
2997+
Dev->createBuffer("AS-Scratch", ScratchDesc, ScratchSize);
2998+
if (!ScratchOrErr)
2999+
return ScratchOrErr.takeError();
3000+
auto *DXScratchBuf = llvm::cast<DXBuffer>(ScratchOrErr->get());
3001+
Desc.ScratchAccelerationStructureData =
3002+
DXScratchBuf->Buffer->GetGPUVirtualAddress();
3003+
CB.KeepAliveOwned.push_back(std::move(*ScratchOrErr));
3004+
3005+
insertDebugSignpost("BuildRaytracingAccelerationStructure");
3006+
CmdList4->BuildRaytracingAccelerationStructure(&Desc, 0, nullptr);
3007+
}
3008+
3009+
// Signal that this batch's AS writes need a barrier before the next reader.
3010+
CB.addPendingUAVBarrier();
3011+
return llvm::Error::success();
3012+
}
28253013
} // namespace
28263014

28273015
llvm::Expected<offloadtest::SubmitResult> DXQueue::submit(

0 commit comments

Comments
 (0)