Skip to content

Commit b5b9976

Browse files
luigi-rossoluigi-rosso
andcommitted
fix: ore buffer per-frame update race (#12976) cc34cea963
* add ore GM exposing the per-frame buffer update race * fix Metal ore buffer update race with per-write versioning * add ore_buffer_update_between_draws goldens for metal d3d11 and gl * fix Vulkan ore buffer update race with per-write versioning * fix D3D12 ore buffer update race with per-write versioning * fix WebGPU ore buffer update race with per-write versioning * add ore_buffer_update_between_draws goldens for vulkan d3d12 and webgpu * address review notes and tighten comments for the ore buffer race fix * fix wgpu partial-update shadow seed and harden ore buffer race edge cases * add multi-frame ore buffer race regression unit test * scope ore buffer race test to racy backends, fixing linux headless GL abort * free manager-less GPUResources on release instead of leaking them * harden ore buffer orphaning against gpu allocation failure on metal, vulkan and wgpu * make d3d12 buffer orphaning retry on alloc failure, matching the other backends * wgpu: don't cache a failed bind group creation and correct the shadow comment * wgpu/vulkan: mark UBOs bound only after a successful resolve, and restore the wgpu bind group label * fail makeBuffer cleanly on gpu allocation failure (metal, vulkan, wgpu) * harden ore bind group OOM handling and orphan-failure diagnostics across vulkan/metal/wgpu Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
1 parent a3e67e6 commit b5b9976

35 files changed

Lines changed: 1529 additions & 176 deletions

.rive_head

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
9f3eef86340a3ef53568116c197fd343a9e1d4e3
1+
cc34cea96329320072dcc92cc4d302db44369dbe

renderer/include/rive/renderer/ore/ore_context_d3d12.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ class ContextD3D12 : public Context
4444
void endFrame() override;
4545
void waitForGPU() override;
4646

47+
// Frame numbers from the host, used by BufferD3D12 to version backings:
48+
// a backing bound in `currentFrameNumber` is reclaimable once
49+
// `safeFrameNumber` reaches it.
50+
uint64_t currentFrameNumber() const { return m_currentFrameNumber; }
51+
uint64_t safeFrameNumber() const { return m_safeFrameNumber; }
52+
4753
rcp<TextureView> wrapCanvasTexture(gpu::RenderCanvas* canvas) override;
4854
rcp<TextureView> wrapRiveTexture(gpu::Texture* gpuTex,
4955
uint32_t width,
@@ -58,13 +64,20 @@ class ContextD3D12 : public Context
5864
friend class RenderPassD3D12;
5965
friend class BindGroupD3D12;
6066
friend class TextureD3D12;
67+
friend class BufferD3D12;
6168

6269
ContextD3D12(rcp<rive::gpu::GPUResourceManager> manager) :
6370
Context(std::move(manager))
6471
{}
6572

6673
// D3D12 implementation helpers — defined in ore_context_d3d12.cpp.
6774
rcp<Buffer> d3d12MakeBuffer(const BufferDesc& desc);
75+
// Create + persistently map one UPLOAD-heap backing of exactly
76+
// `alignedSize` bytes. Shared by buffer creation and orphan-on-bound.
77+
bool d3d12AllocBufferBacking(
78+
UINT64 alignedSize,
79+
Microsoft::WRL::ComPtr<ID3D12Resource>& outResource,
80+
void** outMapped);
6881
rcp<Texture> d3d12MakeTexture(const TextureDesc& desc);
6982
rcp<TextureView> d3d12MakeTextureView(const TextureViewDesc& desc);
7083
rcp<Sampler> d3d12MakeSampler(const SamplerDesc& desc);
@@ -109,6 +122,9 @@ class ContextD3D12 : public Context
109122
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> m_d3dGpuSamplerHeap;
110123
UINT m_d3dGpuSrvAllocated = 0;
111124
UINT m_d3dGpuSamplerAllocated = 0;
125+
// Host frame numbers captured at beginFrame for backing reclamation.
126+
uint64_t m_currentFrameNumber = 0;
127+
uint64_t m_safeFrameNumber = 0;
112128
// Helpers called by RenderPass to allocate and resolve descriptor handles.
113129
UINT d3d12AllocGpuSrvSlots(UINT count);
114130
UINT d3d12AllocGpuSamplerSlots(UINT count);

renderer/include/rive/renderer/ore/ore_context_metal.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
#import <Metal/Metal.h>
1010

11+
#include <atomic>
12+
#include <memory>
13+
1114
namespace rive::ore
1215
{
1316

@@ -49,6 +52,16 @@ class ContextMetal : public Context
4952

5053
ShaderTarget shaderTarget() const override { return ShaderTarget::msl; }
5154

55+
// Buffer versioning serials. currentSerial is the command buffer being
56+
// recorded. completedSerial is the highest the GPU has finished, so a
57+
// backing at or below it can be recycled. See ore_buffer_metal.mm.
58+
uint64_t currentSerial() const { return m_currentSerial; }
59+
uint64_t completedSerial() const
60+
{
61+
return m_completedSerial->load(std::memory_order_relaxed);
62+
}
63+
id<MTLDevice> device() const { return m_mtlDevice; }
64+
5265
ContextMetal(const ContextMetal&) = delete;
5366
ContextMetal& operator=(const ContextMetal&) = delete;
5467

@@ -79,6 +92,14 @@ class ContextMetal : public Context
7992
id<MTLCommandBuffer> m_mtlCommandBuffer = nil;
8093

8194
std::vector<rcp<BindGroup>> m_deferredBindGroups;
95+
96+
// Serial of the command buffer being recorded, bumped each frame.
97+
uint64_t m_currentSerial = 0;
98+
// Highest serial the GPU has finished. Written on the completion handler
99+
// thread, read on the recording thread, hence atomic. shared_ptr so the
100+
// handler stays valid even if the context dies first.
101+
std::shared_ptr<std::atomic<uint64_t>> m_completedSerial =
102+
std::make_shared<std::atomic<uint64_t>>(0);
82103
};
83104

84105
} // namespace rive::ore

renderer/include/rive/renderer/ore/ore_context_vulkan.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ class ContextVulkan : public Context
158158
// `vkDeferDestroy` if needed.
159159
VkDescriptorSetLayout m_vkEmptyDSL = VK_NULL_HANDLE;
160160
VkDescriptorSetLayout vkGetOrCreateEmptyDSL();
161+
162+
// Allocate a descriptor set from the active generation, rolling a fresh one
163+
// on capacity. The returned pool ref keeps the set alive. Used by
164+
// makeBindGroup and by BindGroupVulkan when a UBO orphans onto a new
165+
// backing and needs a freshly written set.
166+
struct DescriptorSetAllocation
167+
{
168+
VkDescriptorSet set = VK_NULL_HANDLE;
169+
rcp<DescriptorPoolGeneration> pool;
170+
};
171+
DescriptorSetAllocation vkAllocateDescriptorSet(VkDescriptorSetLayout dsl);
161172
// Render pass cache — typically <10 entries, keyed on format+load/store
162173
// ops.
163174
std::vector<std::pair<VKRenderPassKey, VkRenderPass>> m_vkRenderPassCache;

renderer/include/rive/renderer/ore/ore_context_wgpu.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ class ContextWGPU : public Context
6868
// Use this to select between GLES and Vulkan GLSL shader source.
6969
bool isGLES() const { return m_wgpuBackend == WGPUBackend::OpenGLES; }
7070

71+
// Monotonic frame serial for buffer versioning. A backing bound this frame
72+
// is not reused until a later frame, when its WriteBuffer is ordered after
73+
// this frame's submit. The host does not thread frame numbers to the wgpu
74+
// backend, so the context owns the serial.
75+
uint64_t currentFrameSerial() const { return m_frameSerial; }
76+
7177
ContextWGPU(const ContextWGPU&) = delete;
7278
ContextWGPU& operator=(const ContextWGPU&) = delete;
7379

@@ -87,6 +93,7 @@ class ContextWGPU : public Context
8793
wgpu::Device m_wgpuDevice;
8894
wgpu::Queue m_wgpuQueue;
8995
wgpu::CommandEncoder m_wgpuCommandEncoder;
96+
uint64_t m_frameSerial = 0;
9097
};
9198

9299
} // namespace rive::ore

renderer/src/gpu_resource.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ GPUResource::~GPUResource() {}
1111
void GPUResource::onRefCntReachedZero() const
1212
{
1313
// GPUResourceManager will hold off on deleting "this" until its safe frame
14-
// number has been reached.
14+
// number has been reached. With no manager (the Metal Ore backend keeps
15+
// native objects alive via ARC and command-buffer retention rather than a
16+
// manager) there is no deferred reclaim, so delete now instead of leaking.
1517
if (m_manager)
1618
m_manager->onRenderingResourceReleased(const_cast<GPUResource*>(this));
19+
else
20+
delete const_cast<GPUResource*>(this);
1721
}
1822

1923
GPUResourceManager::~GPUResourceManager()

renderer/src/ore/d3d12/ore_bind_group_d3d12.hpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace rive::ore
88
{
99
class ContextD3D12;
10+
class BufferD3D12;
1011

1112
class BindGroupD3D12 : public LITE_RTTI_OVERRIDE(BindGroup, BindGroupD3D12)
1213
{
@@ -19,17 +20,21 @@ class BindGroupD3D12 : public LITE_RTTI_OVERRIDE(BindGroup, BindGroupD3D12)
1920
private:
2021
friend class ContextD3D12;
2122
friend class RenderPassD3D12;
22-
// UBO (CBV) slots — GPU virtual addresses and sizes indexed by native slot.
23-
D3D12_GPU_VIRTUAL_ADDRESS m_d3dUBOAddresses[8] = {};
23+
// UBO slots. Live buffer back-refs, offsets and sizes indexed by native
24+
// slot. The GPU-VA is resolved from the buffer at apply time so a post-bind
25+
// update that orphaned the backing is seen. Buffers kept alive by
26+
// m_retainedBuffers.
27+
BufferD3D12* m_d3dUBOBuffers[8] = {};
28+
uint32_t m_d3dUBOOffsets[8] = {};
2429
uint32_t m_d3dUBOSizes[8] = {};
2530
uint8_t m_d3dUBOSlotMask = 0;
2631
uint8_t m_d3dUBODynamicOffsetMask = 0;
27-
// SRV (texture) slots CPU descriptor handles and texture back-refs.
32+
// SRV texture slots. CPU descriptor handles and texture back-refs.
2833
D3D12_CPU_DESCRIPTOR_HANDLE m_d3dSrvHandles[8] = {};
2934
Texture* m_d3dSrvTextures[8] =
30-
{}; // Weak refs; kept alive by m_retainedViews.
35+
{}; // Weak refs kept alive by m_retainedViews
3136
uint8_t m_d3dSrvSlotMask = 0;
32-
// Sampler slots CPU descriptor handles.
37+
// Sampler slots. CPU descriptor handles.
3338
D3D12_CPU_DESCRIPTOR_HANDLE m_d3dSamplerHandles[8] = {};
3439
uint8_t m_d3dSamplerSlotMask = 0;
3540
};

renderer/src/ore/d3d12/ore_buffer_d3d12.cpp

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,81 @@
1313
namespace rive::ore
1414
{
1515

16-
// --- Public method definitions (D3D12-only builds) ---
17-
// When both D3D11 and D3D12 are compiled, the combined
18-
// ore_context_d3d11_d3d12.cpp file provides these methods with dispatch.
16+
// Guarded by ORE_BACKEND_D3D12. The Windows build compiles both the d3d11 and
17+
// d3d12 dirs with both backend macros defined; BufferD3D11 and BufferD3D12 are
18+
// distinct classes under their own guards and the context factory selects the
19+
// concrete type at runtime, so there is no shared dispatch file.
1920
#if defined(ORE_BACKEND_D3D12)
2021

22+
void BufferD3D12::markBound()
23+
{
24+
m_currentSerial = m_ctx->currentFrameNumber();
25+
m_boundSinceUpdate = true;
26+
}
27+
28+
bool BufferD3D12::acquireFreshBacking(uint32_t writeOffset, uint32_t writeSize)
29+
{
30+
// Retire the current backing since an in flight bind may still read it.
31+
void* oldMapped = m_d3dMappedPtr;
32+
m_retired.push_back(
33+
{std::move(m_d3dBuffer), m_d3dMappedPtr, m_currentSerial});
34+
35+
// Reuse a retired backing the GPU has finished with, else allocate one.
36+
// Require the bind to predate the current frame, not just safeFrameNumber,
37+
// so a host that ever reports safe == current cannot hand back an in flight
38+
// backing.
39+
const uint64_t safe = m_ctx->safeFrameNumber();
40+
const uint64_t current = m_ctx->currentFrameNumber();
41+
for (auto it = m_retired.begin(); it != m_retired.end(); ++it)
42+
{
43+
if (it->serial <= safe && it->serial < current)
44+
{
45+
m_d3dBuffer = std::move(it->resource);
46+
m_d3dMappedPtr = it->mapped;
47+
m_retired.erase(it);
48+
break;
49+
}
50+
}
51+
if (m_d3dBuffer == nullptr &&
52+
!m_ctx->d3d12AllocBufferBacking(m_allocatedSize,
53+
m_d3dBuffer,
54+
&m_d3dMappedPtr))
55+
{
56+
// Allocation failed. Keep the backing we just retired so we do not
57+
// write through a null map. This reraces this one update but the data
58+
// is already there, and it avoids a crash.
59+
m_ctx->setLastError("ore: D3D12 buffer backing allocation failed; "
60+
"reusing in flight backing for this update");
61+
Backing& old = m_retired.back();
62+
m_d3dBuffer = std::move(old.resource);
63+
m_d3dMappedPtr = old.mapped;
64+
m_retired.pop_back();
65+
return false; // Kept the current backing; retry orphaning next update.
66+
}
67+
68+
m_currentSerial = 0;
69+
// Carry contents forward so a partial update keeps untouched bytes. Skip
70+
// when this update covers the whole buffer, and skip the self copy when the
71+
// reused backing is the one we just retired.
72+
if (!(writeOffset == 0 && writeSize == m_size) &&
73+
m_d3dMappedPtr != oldMapped)
74+
memcpy(m_d3dMappedPtr, oldMapped, m_size);
75+
return true;
76+
}
77+
2178
void BufferD3D12::update(const void* data, uint32_t size, uint32_t offset)
2279
{
2380
assert(offset + size <= m_size);
2481
assert(m_d3dBuffer != nullptr);
2582
assert(m_d3dMappedPtr != nullptr);
2683

27-
// UPLOAD heap buffer is persistently mapped — write directly.
84+
if (m_boundSinceUpdate)
85+
{
86+
// On allocation failure keep the current backing and retry next update.
87+
if (acquireFreshBacking(offset, size))
88+
m_boundSinceUpdate = false;
89+
}
90+
// UPLOAD heap buffer is persistently mapped so write directly.
2891
memcpy(static_cast<uint8_t*>(m_d3dMappedPtr) + offset, data, size);
2992
}
3093

renderer/src/ore/d3d12/ore_buffer_d3d12.hpp

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
#include "rive/renderer/ore/ore_buffer.hpp"
33
#include <d3d12.h>
44
#include <wrl/client.h>
5+
#include <vector>
56

67
namespace rive::ore
78
{
89
class ContextD3D12;
910

11+
// A write after a bind orphans onto a fresh backing instead of racing the GPU
12+
// still reading the bound one. Bindings resolve the live backing at apply time.
13+
// Immutable or never rebound buffers stay at a single backing.
1014
class BufferD3D12 : public LITE_RTTI_OVERRIDE(Buffer, BufferD3D12)
1115
{
1216
public:
@@ -18,13 +22,42 @@ class BufferD3D12 : public LITE_RTTI_OVERRIDE(Buffer, BufferD3D12)
1822
~BufferD3D12() override = default; // ComPtr released automatically
1923
void update(const void* data, uint32_t size, uint32_t offset) override;
2024

25+
// GPU-VA of the live backing. Call at apply time, not creation time.
26+
D3D12_GPU_VIRTUAL_ADDRESS currentGpuVA() const
27+
{
28+
return m_d3dBuffer->GetGPUVirtualAddress();
29+
}
30+
31+
// Stamp the live backing with this frame so a later update() orphans
32+
// instead of overwriting memory the GPU may still read.
33+
void markBound();
34+
2135
private:
2236
friend class ContextD3D12;
2337
friend class RenderPassD3D12;
2438
friend class TextureD3D12; // for staging buffer access during texture
2539
// uploads
26-
// UPLOAD heap — persistently mapped; GPU reads directly.
40+
41+
// Move to a fresh backing, copying current contents so a partial update
42+
// keeps untouched bytes. The pending write's range lets a full-buffer
43+
// update skip the copy. Returns false if a fresh backing could not be
44+
// allocated, in which case the current backing is kept.
45+
bool acquireFreshBacking(uint32_t writeOffset, uint32_t writeSize);
46+
47+
// Live backing in an UPLOAD heap, persistently mapped so the GPU reads it.
2748
Microsoft::WRL::ComPtr<ID3D12Resource> m_d3dBuffer;
2849
void* m_d3dMappedPtr = nullptr;
50+
51+
struct Backing
52+
{
53+
Microsoft::WRL::ComPtr<ID3D12Resource> resource;
54+
void* mapped = nullptr;
55+
uint64_t serial = 0; // frame it was last bound in
56+
};
57+
std::vector<Backing> m_retired; // rotated-out backings awaiting GPU retire
58+
uint64_t m_currentSerial = 0; // frame the live backing was last bound in
59+
bool m_boundSinceUpdate = false;
60+
UINT64 m_allocatedSize = 0; // aligned size for fresh backings
61+
ContextD3D12* m_ctx = nullptr;
2962
};
3063
} // namespace rive::ore

0 commit comments

Comments
 (0)