Skip to content

Introduce a new Buffer type with DX,VK,MTL implementations.#976

Merged
damyanp merged 10 commits intollvm:mainfrom
Traverse-Research:render-backend-api-buffer
Mar 31, 2026
Merged

Introduce a new Buffer type with DX,VK,MTL implementations.#976
damyanp merged 10 commits intollvm:mainfrom
Traverse-Research:render-backend-api-buffer

Conversation

@manon-traverse
Copy link
Copy Markdown
Contributor

A first small step towards creating a Render Backend API layer on top of DX12, Vulkan, and Metal.

This introduces a new Buffer type with an implementation for each API as well as a function on the Device class to create one.

The change is smaller than it looks due to a naming conflict that needed to be resolved. The type previously called Buffer has been renamed to CPUBuffer.


struct BufferCreateDesc {
MemoryLocation Location;
BufferUsage Usage;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't think I see this Usage flag used anywhere. Is the idea that buffer usage describes the buffer as CBV/UAV/SRV (in DX12 parlance)?

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.

The Usage tag will later be used to specify the type of buffer (Storage, AccelerationStructure, Constant, etc). I included it here so that the field should be filled in from the start. Perhaps we should apply a constructor on this type to enforce filling in all fields?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FWIW, I think I'd hold off on adding fields / enums until we can include code demonstrating what they're for, so I'd recommend just removing the Usage field & enum until we have a need for it.

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.

Let's go with that, I have removed the field.

};

enum class MemoryLocation {
GpuOnly,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This may be confusing if later on you want to add CPU-visible memory which is still GPU local (ReBAR).

I wonder if the memory locations should just be GpuLocal or HostLocal and then the usage of Readback, Upload, ShaderMutable, or ShaderVisible then dictates the heap/memory-type in conjunction with "MemoryLocation"

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.

In our closed-source render framework, we use CpuToGpu to express that the resource has to be HOST visible and won't be read from. Whether that resource is actually in GPU Local ReBAR, some form of shared memory or on HOST. I am basically using that as a starting point as it has proven to have worked so far, but I am open for suggestion and changes.

That's why the terms GpuLocal and HostLocal may not fit as well, since it's a detail that the rendering backend should deal with and not necessarily the cross-platform rendering code.

Copy link
Copy Markdown

@scuff3drook scuff3drook Mar 18, 2026

Choose a reason for hiding this comment

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

Sure, but the enum name here is MemoryLocation after all. I think hiding that as an implementation detail totally makes sense, but it may make sense for the type name to then indicate as such. Personally, if the goal is to expose strictly semantics to the user of this API, and the specific memory location is an implementation detail, I would probably opt to just expose the BufferUsage flag alone.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fortunately, we can change code in the future if we decide that the abstraction we've picked doesn't work out well. At this point I think that categorizing memory as GpuOnly, CpuToGpu and GpuToCpu is a reasonable categorization that works well across all the APIs. I do agree that the name MemoryLocation could be tightened up - MemoryType perhaps? I'm biased as I'm more familiar with D3D12_HEAP_TYPE that has 1:1 mappings with these values :)

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.

MemoryType doesn't match with Vulkan memory types, though. The name of this enum is not as important to me as long as I can quickly understand if it's CpuToGpu, GpuOnly, or GpuToCpu.
The reason for MemoryLocation was just purely based on it being called that in our in-house rendering framework.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, trying to pick good names is hard, trying to pick good names that don't conflict with names used by 3 graphics APIs is even harder :)

I don't think we should block this PR going in on getting the right name. I agree that the abstraction here is a good one, and I think we'll have to accept that the name picked here is somewhat arbitrary and the least-obviously-bad one.

}

const D3D12_RESOURCE_FLAGS Flags =
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These flags aren't yet used, but I'm assuming UAV access should be conditional depending on usage

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.

On Buffers UAV access is kind-of the default. There is no special swizzling or compression like on textures. There may be some exceptions when it comes to acceleration structures that according to the spec shouldn't be UAV. However, I would like to solve that problem once we reach that point.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yea that's definitely true (re: difference between buffers and textures), although the semantic difference for buffers is often still meaningful in most engines -- you wouldn't expect to create a UAV descriptor for a read-only buffer for example (unless you live in a BDA-only world)

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.

From my experience, having worked in various game engines, I have never seen an advantage to marking buffers as read/SRV-only at buffer creation time. Just binding it as read or write has been sufficient as that also guides barrier placement.

I would also like to point out what this flag is for. This flag is just purely there to tell the driver what kind of optimizations (swizzling/compression) it can apply on a resource. That's why resources with no flags passed disallow a lot of use cases. However, in the case of buffers (which use the same resource creation API) there are no optimziations to apply. So there is no point to not passing this flag in the rendering backend.

Copy link
Copy Markdown

@scuff3drook scuff3drook Mar 27, 2026

Choose a reason for hiding this comment

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

The advantage just in a dx12 sense is that you can validate against incorrect usage if anyone tries to grab a UAV descriptor basically. Not a hill I'd ever die on. (I've worked on console games for over a decade and 5 different game engines now so I'm familiar with delta color compression and the other concepts you are mentioning FWIW). Some engines map descriptors/addresses persistently and expose different accessors depending on declared usage, so it's a question of style and user preference I suppose.

Copy link
Copy Markdown
Collaborator

@llvm-beanz llvm-beanz left a comment

Choose a reason for hiding this comment

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

A couple comments. As a meta-comment, we do use clang-tidy to enforce consistent style and coding standards. That is run with -Werror in our bots, so some of the CI failures you're seeing as build failures are Clang-Tidy failures.

Our ReadMe documents setting up Clang-Tidy once you have it installed: https://github.com/llvm/offload-test-suite?tab=readme-ov-file#enabling-clang-tidy

class DXBuffer : public offloadtest::Buffer {
public:
ComPtr<ID3D12Resource> Buffer;
std::string Name;
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.

Should we put the Name and SizeInBytes in the base class? Seems like all the sub-classes have those fields.

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.

I am not sure! I am a big fan of using Pure Virtual Classes and only doing functionality inheritance without any data inheritance, much like traits in Rust. What is the LLVM approach to this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think LLVM has a strict opinion here, both patterns are present depending on the context. I think the functionality inheritance here is reasonable.

@manon-traverse
Copy link
Copy Markdown
Contributor Author

A couple comments. As a meta-comment, we do use clang-tidy to enforce consistent style and coding standards. That is run with -Werror in our bots, so some of the CI failures you're seeing as build failures are Clang-Tidy failures.

Our ReadMe documents setting up Clang-Tidy once you have it installed: https://github.com/llvm/offload-test-suite?tab=readme-ov-file#enabling-clang-tidy

Yes I saw something along the lines of that. I'll check it out! <3

I have some local changes to the README with steps and pointers that I found difficult to figure out to improve the onboarding in the future.

Copy link
Copy Markdown
Contributor

@damyanp damyanp left a comment

Choose a reason for hiding this comment

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

LGTM, just a couple of observations.

Comment on lines +275 to +278
ComPtr<ID3D12Resource> Buffer;
std::string Name;
BufferCreateDesc Desc;
size_t SizeInBytes;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did we turn off the clang-tidy rule about public data members?

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.

clang-tidy doesn't seem to mind. Personally, I don't see the advantage of private data members and using getters and setters for everything. It adds a lot of boilerplate to solve issues that are these days solved by tools like clang-tidy (for example: if (MyObject->Field = 3). On top of that, if you write MyObject->GetField() everywhere, assuming that that is a cheap (should-be-free) operation, you can get in the situation where GetField() is refactored to be an expensive operation, you suddenly have introduced a lot of performance overhead. By just accessing the fields directly you are force to refactor the call site as well when changes are made and can catch these types of issues more easily.

If, however, using private fields with getters and setters is the LLVM way, we will adapt to that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I tend to agree with you here - I only mentioned it because I've had to fight with clang-tidy over this in the past :)

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.

Our clang-tidy rules are setup to match LLVM's coding standards, and LLVM allows public data members in classes.

Checks: '-*,clang-diagnostic-*,llvm-*,misc-*,-misc-use-anonymous-namespace,-misc-include-cleaner,-misc-non-private-member-variables-in-classes,readability-identifier-naming'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It functionally 100% equivalent, so I'm happy with whatever you do here, but typically LLVM uses struct for all-public objects like this. Saves a line of code since it's the default and there's a very weak argument that it makes things clearer.

@manon-traverse manon-traverse force-pushed the render-backend-api-buffer branch from deed800 to 5d60f9f Compare March 30, 2026 16:05
@damyanp
Copy link
Copy Markdown
Contributor

damyanp commented Mar 31, 2026

Can you fix the code formatting errors? Then I can merge this.

@damyanp damyanp merged commit 60b8173 into llvm:main Mar 31, 2026
9 of 12 checks passed
Copy link
Copy Markdown
Contributor

@bogner bogner left a comment

Choose a reason for hiding this comment

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

This generally looks reasonable, but for future reference it's always nice if we can add some usage and/or tests whenever we add API surface like this. This both helps in clarifying the motivation and makes it easier to review when you can see the usage in context.

class DXBuffer : public offloadtest::Buffer {
public:
ComPtr<ID3D12Resource> Buffer;
std::string Name;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think LLVM has a strict opinion here, both patterns are present depending on the context. I think the functionality inheritance here is reasonable.

Comment on lines +275 to +278
ComPtr<ID3D12Resource> Buffer;
std::string Name;
BufferCreateDesc Desc;
size_t SizeInBytes;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It functionally 100% equivalent, so I'm happy with whatever you do here, but typically LLVM uses struct for all-public objects like this. Saves a line of code since it's the default and there's a very weak argument that it makes things clearer.


virtual Queue &getGraphicsQueue() = 0;

virtual llvm::Expected<std::shared_ptr<Buffer>>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd prefer to avoid shared_ptr unless we really need it. Since this code just introduces the APIs and doesn't use them yet it's hard for me to tell if we do, but generally I think shared_ptr can be used in ways that obscure ownership and we should use it sparingly.

@bogner
Copy link
Copy Markdown
Contributor

bogner commented Apr 1, 2026

bah - github is failing to refresh these pages lately. I didn't realize my review was post commit.

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.

5 participants