optimizations for hair particle system (#1298)

This commit is contained in:
Turánszki János
2025-11-14 09:04:49 +01:00
committed by GitHub
parent 36c0b96c58
commit 359ade8d06
10 changed files with 155 additions and 46 deletions
+11
View File
@@ -377,6 +377,17 @@ void HairParticleWindow::UpdateData()
ss += "Tip: To use hair particle system, first you must select a surface mesh to spawn particles on, and then increase particle count to grow particles. The particles will get their texture from the Material that is created on the current entity.\n\n";
ss += "Position format: " + std::string(wi::graphics::GetFormatString(hair->position_format)) + "\n";
ss += "Memory usage: " + wi::helper::GetMemorySizeText(hair->GetMemorySizeInBytes()) + "\n";
ss += "\nSuballocation offset: ";
if (hair->generalBufferOffsetAllocation.IsValid())
{
ss += wi::helper::GetMemorySizeText(hair->generalBufferOffsetAllocation.byte_offset);
}
else
{
ss += "suballocation is not used for this hair particle system";
}
infoLabel.SetText(ss);
meshComboBox.ClearItems();
+3 -3
View File
@@ -172,7 +172,7 @@ namespace wi::allocator
{
Reset();
allocator = std::move(other.allocator);
internal_state = other.internal_state;
internal_state = std::move(other.internal_state);
byte_offset = other.byte_offset;
other.allocator = nullptr;
other.internal_state = nullptr;
@@ -197,7 +197,7 @@ namespace wi::allocator
{
Reset();
allocator = std::move(other.allocator);
internal_state = other.internal_state;
internal_state = std::move(other.internal_state);
byte_offset = other.byte_offset;
other.allocator = nullptr;
other.internal_state = nullptr;
@@ -248,7 +248,7 @@ namespace wi::allocator
}
// returns true if no pages are allocated
inline bool is_empty()
inline bool is_empty() const
{
return allocator->allocator.storageReport().totalFreeSpace == page_count;
}
+4
View File
@@ -431,6 +431,10 @@ namespace wi::ecs
// Returns the tightly packed [read only] component array
inline const wi::vector<Component>& GetComponentArray() const { return components; }
// Returns the raw data pointer of components:
inline const Component* GetData() const { return components.data(); }
inline Component* GetData() { return components.data(); }
private:
// This is a linear array of alive components
wi::vector<Component> components;
+5
View File
@@ -241,6 +241,7 @@ namespace wi::graphics
// Some useful helpers:
// This can be used to create a buffer filled from CPU data pointer, the copy will be done on the CPU (and additional GPU copy for non-UMA)
bool CreateBuffer(const GPUBufferDesc* desc, const void* initial_data, GPUBuffer* buffer, const GPUResource* alias = nullptr, uint64_t alias_offset = 0ull) const
{
if (initial_data == nullptr)
@@ -250,16 +251,19 @@ namespace wi::graphics
return CreateBuffer2(desc, [&](void* dest) { std::memcpy(dest, initial_data, desc->size); }, buffer, alias, alias_offset);
}
// This can be used to create a buffer filled with a single value, the data initialization will be done on the CPU (and additional GPU copy for non-UMA)
bool CreateBufferCleared(const GPUBufferDesc* desc, uint8_t value, GPUBuffer* buffer, const GPUResource* alias = nullptr, uint64_t alias_offset = 0ull) const
{
return CreateBuffer2(desc, [&](void* dest) { std::memset(dest, value, desc->size); }, buffer, alias, alias_offset);
}
// This can be used to create a buffer filled with zeroes, the data initialization will be done on the CPU (and additional GPU copy for non-UMA)
bool CreateBufferZeroed(const GPUBufferDesc* desc, GPUBuffer* buffer, const GPUResource* alias = nullptr, uint64_t alias_offset = 0ull) const
{
return CreateBufferCleared(desc, 0, buffer, alias, alias_offset);
}
// Execute a single GPU barrier
void Barrier(const GPUBarrier& barrier, CommandList cmd)
{
Barrier(&barrier, 1, cmd);
@@ -347,6 +351,7 @@ namespace wi::graphics
BindConstantBuffer(&allocation.buffer, slot, cmd, allocation.offset);
}
// Simplified renderpass beginning for a single render target and optional clear (if clear is false, it will use load operation)
void RenderPassBegin(const Texture* rendertarget, CommandList cmd, bool clear = true)
{
RenderPassImage rp[] = {
+67 -2
View File
@@ -89,8 +89,31 @@ namespace wi
_flags |= REBUILD_BUFFERS;
}
void HairParticleSystem::DeleteRenderData()
{
constantBuffer = {};
generalBuffer = {};
generalBufferOffsetAllocation = {};
generalBufferOffsetAllocationAlias = {};
simulation_view = {};
vb_pos[0] = {};
vb_pos[1] = {};
vb_nor = {};
vb_pos_raytracing = {};
vb_uvs = {};
wetmap = {};
ib_culled = {};
indirect_view = {};
prim_view = {};
indexBuffer = {};
vertexBuffer_length = {};
BLAS = {};
}
void HairParticleSystem::CreateRenderData()
{
DeleteRenderData();
GraphicsDevice* device = wi::graphics::GetDevice();
BLAS = {};
@@ -144,8 +167,24 @@ namespace wi
AlignTo(prim_view.size, alignment) +
AlignTo(vb_pos_raytracing.size, alignment)
;
device->CreateBufferZeroed(&bd, &generalBuffer);
device->SetName(&generalBuffer, "HairParticleSystem::generalBuffer");
#if 0 // Suballocation is disabled for hair particle system for now, there is some memory overwrite-like issue somewhere
wi::renderer::BufferSuballocation suballoc = wi::renderer::SuballocateGPUBuffer(bd.size);
if (suballoc.IsValid())
{
bool success = device->CreateBuffer(&bd, nullptr, &generalBuffer, &suballoc.alias, suballoc.allocation.byte_offset);
assert(success);
device->SetName(&generalBuffer, "HairParticleSystem::generalBuffer (suballocated)");
generalBufferOffsetAllocation = std::move(suballoc.allocation);
generalBufferOffsetAllocationAlias = std::move(suballoc.alias);
}
else
#endif
{
// If suballocation was not successful, a standalone buffer can be created instead:
bool success = device->CreateBuffer(&bd, nullptr, &generalBuffer);
assert(success);
device->SetName(&generalBuffer, "HairParticleSystem::generalBuffer");
}
gpu_initialized = false;
uint64_t buffer_offset = 0ull;
@@ -347,6 +386,32 @@ namespace wi
std::swap(vb_pos[0], vb_pos[1]);
}
void HairParticleSystem::InitializeGPUBuffersIfNeeded(
const HairParticleSystem* hairs,
size_t hairCount,
CommandList cmd
)
{
GraphicsDevice* device = wi::graphics::GetDevice();
device->EventBegin("HairParticleSystem - InitializeGPUBuffersIfNeeded", cmd);
for (uint32_t i = 0; i < hairCount; ++i)
{
const HairParticleSystem& hair = hairs[i];
if (hair.strandCount == 0 || !hair.generalBuffer.IsValid())
{
continue;
}
if (!hair.gpu_initialized)
{
hair.gpu_initialized = true;
device->ClearUAV(&hair.generalBuffer, 0, cmd);
wi::renderer::PushBarrier(GPUBarrier::Buffer(&hair.generalBuffer, ResourceState::UNORDERED_ACCESS, ResourceState::COPY_DST));
}
}
wi::renderer::FlushBarriers(cmd);
device->EventEnd(cmd);
}
void HairParticleSystem::UpdateGPU(
const UpdateGPUItem* items,
uint32_t itemCount,
+9
View File
@@ -21,6 +21,8 @@ namespace wi
public:
wi::graphics::GPUBuffer constantBuffer;
wi::graphics::GPUBuffer generalBuffer;
wi::allocator::PageAllocator::Allocation generalBufferOffsetAllocation;
wi::graphics::GPUBuffer generalBufferOffsetAllocationAlias;
wi::scene::MeshComponent::BufferView simulation_view;
wi::scene::MeshComponent::BufferView vb_pos[2];
wi::scene::MeshComponent::BufferView vb_nor;
@@ -37,6 +39,7 @@ namespace wi
wi::graphics::RaytracingAccelerationStructure BLAS;
void CreateFromMesh(const wi::scene::MeshComponent& mesh);
void DeleteRenderData();
void CreateRenderData();
void CreateRaytracingRenderData();
@@ -46,6 +49,12 @@ namespace wi
float dt
);
static void InitializeGPUBuffersIfNeeded(
const HairParticleSystem* hairs,
size_t hairCount,
wi::graphics::CommandList cmd
);
struct UpdateGPUItem
{
const HairParticleSystem* hair = nullptr;
+50 -37
View File
@@ -2731,46 +2731,54 @@ BufferSuballocation SuballocateGPUBuffer(uint64_t size)
if (size > GPUSubAllocator::blocksize / 2)
return {}; // invalid, larger allocations than half block size will not be suballocated
// scoped for locker
std::scoped_lock lock(suballocator.locker);
// See if any of the large blocks can fulfill the allocation request:
BufferSuballocation allocation;
for (auto& block : suballocator.blocks)
{
std::scoped_lock lock(suballocator.locker);
// See if any of the large blocks can fulfill the allocation request:
BufferSuballocation allocation;
for (auto& block : suballocator.blocks)
allocation.allocation = block.allocator.allocate(size);
if (allocation.allocation.IsValid())
{
allocation.allocation = block.allocator.allocate(size);
if (allocation.allocation.IsValid())
{
allocation.alias = block.buffer;
//wilog("SuballocateGPUBuffer allocated size: %s, pages: %d, free space remaining: %s", wi::helper::GetMemorySizeText(size).c_str(), block.allocator.page_count_from_bytes(size), wi::helper::GetMemorySizeText(allocation.allocation.allocator->allocator.storageReport().totalFreeSpace * block.allocator.page_size).c_str());
return allocation;
}
allocation.alias = block.buffer;
//wilog("SuballocateGPUBuffer allocated size: %s, pages: %d, free space remaining: %s", wi::helper::GetMemorySizeText(size).c_str(), block.allocator.page_count_from_bytes(size), wi::helper::GetMemorySizeText(allocation.allocation.allocator->allocator.storageReport().totalFreeSpace * block.allocator.page_size).c_str());
return allocation;
}
// Allocation couldn't be fulfilled, create new block:
GPUBufferDesc desc;
desc.size = GPUSubAllocator::blocksize;
if (device->CheckCapability(GraphicsDeviceCapability::CACHE_COHERENT_UMA))
{
// In UMA mode, it is better to create UPLOAD buffer, this avoids one copy from UPLOAD to DEFAULT
desc.usage = Usage::UPLOAD;
}
else
{
desc.usage = Usage::DEFAULT;
}
desc.bind_flags = BindFlag::SHADER_RESOURCE | BindFlag::VERTEX_BUFFER | BindFlag::INDEX_BUFFER;
desc.misc_flags = ResourceMiscFlag::ALIASING_BUFFER | ResourceMiscFlag::NO_DEFAULT_DESCRIPTORS;
desc.alignment = device->GetMinOffsetAlignment(&desc);
auto& block = suballocator.blocks.emplace_back();
bool success = device->CreateBuffer(&desc, nullptr, &block.buffer);
assert(success);
device->SetName(&block.buffer, "GPUSubAllocator");
block.allocator.init(desc.size, (uint32_t)desc.alignment, true);
wilog("SuballocateGPUBuffer created buffer block with size: %s, with page size: %s, page count: %d", wi::helper::GetMemorySizeText(block.allocator.total_size_in_bytes()).c_str(), wi::helper::GetMemorySizeText(block.allocator.page_size).c_str(), (int)block.allocator.page_count);
}
return SuballocateGPUBuffer(size); // retry
// Allocation couldn't be fulfilled, create new block:
GPUBufferDesc desc;
desc.size = GPUSubAllocator::blocksize;
if (device->CheckCapability(GraphicsDeviceCapability::CACHE_COHERENT_UMA))
{
// In UMA mode, it is better to create UPLOAD buffer, this avoids one copy from UPLOAD to DEFAULT
desc.usage = Usage::UPLOAD;
}
else
{
desc.usage = Usage::DEFAULT;
}
desc.bind_flags = BindFlag::SHADER_RESOURCE | BindFlag::VERTEX_BUFFER | BindFlag::INDEX_BUFFER | BindFlag::UNORDERED_ACCESS;
desc.misc_flags = ResourceMiscFlag::ALIASING_BUFFER | ResourceMiscFlag::NO_DEFAULT_DESCRIPTORS;
if (device->CheckCapability(GraphicsDeviceCapability::RAYTRACING))
{
desc.misc_flags |= ResourceMiscFlag::RAY_TRACING;
}
desc.alignment = device->GetMinOffsetAlignment(&desc);
auto& block = suballocator.blocks.emplace_back();
bool success = device->CreateBuffer(&desc, nullptr, &block.buffer);
assert(success);
device->SetName(&block.buffer, "GPUSubAllocator");
block.allocator.init(desc.size, (uint32_t)desc.alignment, true);
wilog("SuballocateGPUBuffer created buffer block with size: %s, with page size: %s, page count: %d", wi::helper::GetMemorySizeText(block.allocator.total_size_in_bytes()).c_str(), wi::helper::GetMemorySizeText(block.allocator.page_size).c_str(), (int)block.allocator.page_count);
allocation.allocation = block.allocator.allocate(size);
if (allocation.allocation.IsValid())
{
allocation.alias = block.buffer;
//wilog("SuballocateGPUBuffer allocated size: %s, pages: %d, free space remaining: %s", wi::helper::GetMemorySizeText(size).c_str(), block.allocator.page_count_from_bytes(size), wi::helper::GetMemorySizeText(allocation.allocation.allocator->allocator.storageReport().totalFreeSpace * block.allocator.page_size).c_str());
}
return allocation;
}
void UpdateGPUSuballocator()
{
@@ -2781,8 +2789,10 @@ void UpdateGPUSuballocator()
}
for (size_t i = 0; i < suballocator.blocks.size(); ++i)
{
if (suballocator.blocks[i].allocator.is_empty())
const auto& block = suballocator.blocks[i];
if (block.allocator.is_empty())
{
wilog("deleted suballocation buffer block with size: %s, with page size: %s, page count: %d", wi::helper::GetMemorySizeText(block.allocator.total_size_in_bytes()).c_str(), wi::helper::GetMemorySizeText(block.allocator.page_size).c_str(), (int)block.allocator.page_count);
suballocator.blocks.erase(suballocator.blocks.begin() + i);
break;
}
@@ -5275,6 +5285,9 @@ void UpdateRenderData(
FlushBarriers(cmd); // wind/skinning flush
// Hair particle systems clearing (needs to be for everything, not just visible):
HairParticleSystem::InitializeGPUBuffersIfNeeded(vis.scene->hairs.GetData(), vis.scene->hairs.GetCount(), cmd);
// Hair particle systems GPU simulation:
// (This must be non-async too, as prepass will render hairs!)
static thread_local wi::vector<HairParticleSystem::UpdateGPUItem> hair_updates;
+2 -1
View File
@@ -75,9 +75,10 @@ namespace wi::renderer
{
wi::graphics::GPUBuffer alias;
wi::allocator::PageAllocator::Allocation allocation;
inline bool IsValid() const { return allocation.IsValid(); }
};
// Sub-allocate (thread-safe) from a global GPU buffer for memory aliasing purpose:
// The buffer will be DEFAULT usage, useable as vertex buffer, index buffer and shader resource
// The buffer will be DEFAULT usage, useable as vertex buffer, index buffer, shader resource and unordered access
// The purpose is to suballocate smaller GPUBuffers inside a larger GPUBuffer and bind the large GPUBuffer once as index buffer,
// while the small buffers can be allocated/deallocated from it with memory aliasing and also used regularly by themselves
BufferSuballocation SuballocateGPUBuffer(uint64_t size);
+2 -1
View File
@@ -591,6 +591,7 @@ namespace wi::scene
void MeshComponent::DeleteRenderData()
{
generalBufferOffsetAllocation = {};
generalBufferOffsetAllocationAlias = {};
generalBuffer = {};
streamoutBuffer = {};
ib_provoke = {};
@@ -1352,7 +1353,7 @@ namespace wi::scene
// With this we can avoid rebinding the index buffer for every mesh and can work with purely offsets
// Though the index buffer will still need to be rebound if the index format changes, but that happens less frequently
wi::renderer::BufferSuballocation suballoc = wi::renderer::SuballocateGPUBuffer(bd.size);
if (suballoc.allocation.IsValid())
if (suballoc.IsValid())
{
bool success = device->CreateBuffer2(&bd, init_callback, &generalBuffer, &suballoc.alias, suballoc.allocation.byte_offset);
assert(success);
+2 -2
View File
@@ -9,7 +9,7 @@ namespace wi::version
// minor features, major updates, breaking compatibility changes
const int minor = 71;
// minor bug fixes, alterations, refactors, updates
const int revision = 854;
const int revision = 855;
const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision);
@@ -39,7 +39,7 @@ namespace wi::version
"Aldo, lokimx, K. Osterman, Nomad, ykl, Alex Krokos, Timmy, Avaflow, mat, Hexegonel Samael Michael, Joe Spataro, soru, GeniokV, Mammoth, Ignacio, datae, Jason Rice, " \
"MarsBEKET, Tim, Twisty, Zelf ieats kiezen, Romildo Franco, zNachoh, Dmitriy, Alex Minerva, Stefan Kent, Natty, Sunny Krishna, Vilmos Malárik, Ferrata, Rossakis, " \
"Stefana Andrei, Taylor, Gunnar Kriik, 赟 杨, Rex, Lemon Brother, flxy, meta_leap, Edik, jusik5348, Agnares, Fred Naar, Saki Asui, DarkRaVen, Ray, Russell Searle, Alexandr Dem'yanenko, "\
"Portaloffreedom, nxrighthere, わさび わさび "
"Portaloffreedom, nxrighthere, わさび わさび, Durak, ipso"
const char* GetCreditsString()
{