Skip to content

Commit e1e0e9b

Browse files
Radient: add reference-counted data blob API
- Support shared read and exclusive write access - Notify clients when the last reader releases access - Allow optional initial data copied during creation - Add C/C++ interfaces, documentation, and concurrency tests
1 parent 081e84b commit e1e0e9b

9 files changed

Lines changed: 1087 additions & 0 deletions

File tree

‎Radient/CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ set(SOURCE
3737
src/Assets/RadientTextureAssetManager.cpp
3838
src/Assets/RadientTextureSource.cpp
3939
src/Assets/RadientVertexLayout.cpp
40+
src/Core/RadientDataBlob.cpp
4041
src/Core/RadientBackendImpl.cpp
4142
src/Core/RadientEngineImpl.cpp
4243
src/Core/RadientViewImpl.cpp
@@ -145,6 +146,7 @@ set(INCLUDE
145146

146147
set(INTERFACE
147148
interface/Radient.h
149+
interface/RadientDataBlob.h
148150
interface/RadientAnimation.h
149151
interface/RadientAssetResolver.h
150152
interface/RadientAssets.h

‎Radient/docs/DataBlobs.md‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Data blobs
2+
3+
`IRadientDataBlob` owns a fixed-size allocation of CPU bytes. It is
4+
reference-counted and can be created without an engine or graphics device.
5+
Its public methods expose Radient types and status codes.
6+
7+
Set `RadientDataBlobCreateInfo::pInitialData` to initialize the blob from `Size`
8+
readable bytes. Creation copies those bytes before returning, so the caller can
9+
immediately modify or release the source memory. A null pointer selects zero
10+
initialization; when `Size` is zero, the pointer is ignored. Initialization
11+
does not acquire access or invoke the recycling callback.
12+
13+
```cpp
14+
const float Position[] = {1.f, 2.f, 3.f};
15+
RadientDataBlobCreateInfo CI;
16+
CI.Size = sizeof(Position);
17+
CI.pInitialData = Position;
18+
RefCntAutoPtr<IRadientDataBlob> Blob;
19+
if (CreateRadientDataBlob(CI, &Blob) != RADIENT_STATUS_OK)
20+
return;
21+
```
22+
23+
## Access
24+
25+
`BeginRead` permits multiple simultaneous readers. `BeginWrite` permits a
26+
single writer when there are no readers or other writers. Conflicting attempts
27+
return `RADIENT_STATUS_INVALID_OPERATION` without waiting for active access to
28+
finish. A successful acquisition is paired with exactly one corresponding
29+
`EndRead` or `EndWrite`. Null output parameters return
30+
`RADIENT_STATUS_INVALID_ARGUMENT`; other acquisition failures clear the output
31+
pointer and leave access state unchanged.
32+
33+
Pointers are used only within their access scope. Clients hold a strong blob
34+
reference until that scope ends; acquiring access does not retain the object.
35+
Access can be handed between threads with caller-provided synchronization.
36+
Writes completed before `EndWrite` are visible to subsequent successful access.
37+
The allocation never moves or resizes. Empty blobs are valid and return null
38+
pointers even on successful acquisition, so callers check the status.
39+
40+
```cpp
41+
RadientDataBlobCreateInfo CI;
42+
CI.Size = sizeof(float) * 3;
43+
RefCntAutoPtr<IRadientDataBlob> Blob;
44+
if (CreateRadientDataBlob(CI, &Blob) != RADIENT_STATUS_OK)
45+
return;
46+
47+
void* WriteData = nullptr;
48+
if (Blob->BeginWrite(&WriteData) == RADIENT_STATUS_OK)
49+
{
50+
const float Position[] = {1.f, 2.f, 3.f};
51+
std::memcpy(WriteData, Position, sizeof(Position));
52+
Blob->EndWrite();
53+
}
54+
55+
const void* ReadData = nullptr;
56+
if (Blob->BeginRead(&ReadData) == RADIENT_STATUS_OK)
57+
{
58+
// Consume ReadData while retaining Blob.
59+
Blob->EndRead();
60+
}
61+
```
62+
63+
## Recycling notification
64+
65+
Set `RadientDataBlobCreateInfo::OnLastReaderReleased` and `pUserData` to register
66+
a callback. Registration is copied at creation and remains fixed. The callback
67+
runs once for each reader-count transition from one to zero, including for
68+
empty blobs. Construction, writes, failed operations, and destruction do not
69+
trigger it. The context is not owned by the blob and stays valid for its lifetime.
70+
71+
The callback runs synchronously on the thread performing the final `EndRead`,
72+
outside the internal access lock. The blob stays alive throughout the callback,
73+
even if the callback releases the caller's reference. A pool keeping the blob
74+
afterward acquires a separate strong reference. Clients can queue notifications
75+
onto their own thread and retain the blob while queued.
76+
77+
Notification does not reserve access. Another reader or writer may already be
78+
active, so recycling still starts with a successful `BeginWrite`. Callbacks can
79+
call blob methods. New read cycles can produce nested callbacks or concurrent
80+
callbacks on different threads; callback state accounts for both. A callback
81+
exception is caught and makes `EndRead` return `RADIENT_STATUS_FAILED`, with
82+
the read access already released.
83+
84+
C callers use `Diligent_CreateRadientDataBlob` with a pointer to the creation
85+
descriptor and the `IRadientDataBlob_*` access macros.

‎Radient/interface/Radient.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
#include "RadientMath.h"
3333
#include "RadientTypes.h"
34+
#include "RadientDataBlob.h"
3435
#include "RadientVertexLayout.h"
3536
#include "RadientAssets.h"
3637
#include "RadientMorphTargets.h"
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright 2026 Diligent Graphics LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* In no event and under no legal theory, whether in tort (including negligence),
17+
* contract, or otherwise, unless required by applicable law (such as deliberate
18+
* and grossly negligent acts) or agreed to in writing, shall any Contributor be
19+
* liable for any damages, including any direct, indirect, special, incidental,
20+
* or consequential damages of any character arising as a result of this License or
21+
* out of the use or inability to use the software (including but not limited to damages
22+
* for loss of goodwill, work stoppage, computer failure or malfunction, or any and
23+
* all other commercial damages or losses), even if such Contributor has been advised
24+
* of the possibility of such damages.
25+
*/
26+
27+
#pragma once
28+
29+
/// \file
30+
/// Defines reference-counted CPU data storage with shared read and exclusive write access.
31+
32+
#include "RadientTypes.h"
33+
#include "../../../DiligentCore/Primitives/interface/Object.h"
34+
35+
DILIGENT_BEGIN_NAMESPACE(Diligent)
36+
37+
typedef struct IRadientDataBlob IRadientDataBlob;
38+
39+
/// Notification that a blob's reader count has changed from one to zero.
40+
///
41+
/// \param [in] pBlob - Blob whose final reader released access. The blob stays
42+
/// alive throughout the callback. Retaining it after the
43+
/// callback requires acquiring a separate strong reference.
44+
/// \param [in] pUserData - The value supplied in RadientDataBlobCreateInfo::pUserData.
45+
///
46+
/// Called synchronously by EndRead on the thread releasing the final reader,
47+
/// outside the internal access lock. The callback can call blob methods and
48+
/// acquire its own read or write access. This notification does not reserve
49+
/// write access: another thread may already have acquired access when it runs.
50+
/// New read cycles can cause callbacks to overlap across threads or nest on the
51+
/// same thread, so callback code and its user data account for this concurrency.
52+
/// If a C++ callback throws, EndRead catches the exception and returns
53+
/// RADIENT_STATUS_FAILED; the read access has already been released.
54+
typedef void (*RadientDataBlobReadReleaseCallbackType)(IRadientDataBlob* pBlob, void* pUserData);
55+
56+
/// Creation parameters for a fixed-size CPU data blob.
57+
/// The size, callback, and user-data pointer are copied during creation.
58+
/// Optional initial bytes are copied before CreateRadientDataBlob returns.
59+
struct RadientDataBlobCreateInfo
60+
{
61+
/// Storage size in bytes. The allocation keeps this size for the blob's
62+
/// lifetime. Initial contents come from pInitialData when provided, otherwise
63+
/// the allocation is zero-filled. Zero creates a valid empty blob, whose
64+
/// successful BeginRead and BeginWrite calls return null data pointers.
65+
/// Sizes beyond the implementation's addressable allocation range are
66+
/// rejected with RADIENT_STATUS_INVALID_ARGUMENT. The default is zero.
67+
Uint64 Size DEFAULT_INITIALIZER(0);
68+
69+
/// Optional pointer to at least Size readable bytes used to initialize the
70+
/// allocation. CreateRadientDataBlob copies these bytes before returning;
71+
/// the pointer is not retained, and the caller can then modify or release
72+
/// the source memory. Initialization does not acquire an access scope or
73+
/// invoke OnLastReaderReleased. Ignored when Size is zero. The default is
74+
/// nullptr, which initializes the allocation to zero.
75+
const void* pInitialData DEFAULT_INITIALIZER(nullptr);
76+
77+
/// Optional callback invoked once for each reader-count transition from one
78+
/// to zero. Creation, EndWrite, failed access attempts, and destruction do
79+
/// not invoke it. Registration remains fixed for the blob's lifetime.
80+
/// The default is nullptr, which disables notifications.
81+
RadientDataBlobReadReleaseCallbackType OnLastReaderReleased DEFAULT_INITIALIZER(nullptr);
82+
83+
/// Opaque callback context, passed unchanged to OnLastReaderReleased.
84+
/// The blob stores the pointer without copying or owning the pointed-to data.
85+
/// When a callback is registered, this context remains valid throughout the
86+
/// blob's lifetime, including callback execution. May be null; defaults to
87+
/// nullptr. Ignored when OnLastReaderReleased is null.
88+
void* pUserData DEFAULT_INITIALIZER(nullptr);
89+
};
90+
typedef struct RadientDataBlobCreateInfo RadientDataBlobCreateInfo;
91+
92+
// {8A49B839-9F71-45AF-B8C0-058E75274F7B}
93+
static DILIGENT_CONSTEXPR INTERFACE_ID IID_RadientDataBlob =
94+
{0x8a49b839, 0x9f71, 0x45af, {0xb8, 0xc0, 0x05, 0x8e, 0x75, 0x27, 0x4f, 0x7b}};
95+
96+
#define DILIGENT_INTERFACE_NAME IRadientDataBlob
97+
#include "../../../DiligentCore/Primitives/interface/DefineInterfaceHelperMacros.h"
98+
99+
#define IRadientDataBlobInclusiveMethods \
100+
IObjectInclusiveMethods; \
101+
IRadientDataBlobMethods RadientDataBlob
102+
103+
// clang-format off
104+
105+
/// Reference-counted CPU bytes with concurrent readers or one exclusive writer.
106+
///
107+
/// All methods are thread-safe. Access acquisition returns an error on conflict
108+
/// without waiting for active readers or a writer to finish. Successful Begin
109+
/// calls are paired with exactly one corresponding End call. Access scopes are
110+
/// not tied to a thread; a caller can hand a scope to another thread using its
111+
/// own synchronization. Ending another caller's scope is invalid usage.
112+
///
113+
/// Data pointers are usable only within their corresponding access scope. A
114+
/// caller retains a strong object reference until it finishes accessing the
115+
/// bytes and ends the scope; BeginRead and BeginWrite do not retain the blob.
116+
/// Read access does not permit modifying the bytes. Writes completed before
117+
/// EndWrite are visible to subsequent successful BeginRead and BeginWrite calls.
118+
/// Access methods synchronize scopes; they do not make stale pointer use safe.
119+
DILIGENT_BEGIN_INTERFACE(IRadientDataBlob, IObject)
120+
{
121+
/// Returns the immutable allocation size in bytes, including during active
122+
/// read or write access. Does not acquire access or invoke the callback.
123+
VIRTUAL Uint64 METHOD(GetSize)(THIS) CONST PURE;
124+
125+
/// Acquires one shared read scope and returns the first byte through ppData.
126+
/// Additional readers are allowed. Returns RADIENT_STATUS_INVALID_OPERATION
127+
/// while a writer is active or if the reader counter cannot be incremented.
128+
/// A null ppData returns RADIENT_STATUS_INVALID_ARGUMENT. On any failure,
129+
/// a non-null ppData is set to nullptr and access state is unchanged.
130+
/// Returns RADIENT_STATUS_OK on success, including for an empty blob.
131+
VIRTUAL RADIENT_STATUS METHOD(BeginRead)(THIS_
132+
const void** ppData) PURE;
133+
134+
/// Releases one read scope after the caller has finished reading its bytes.
135+
/// Returns RADIENT_STATUS_INVALID_OPERATION without changing state when
136+
/// there are no readers. Otherwise returns RADIENT_STATUS_OK, invoking
137+
/// OnLastReaderReleased if this releases the final reader. The callback
138+
/// completes before EndRead returns. A callback exception produces
139+
/// RADIENT_STATUS_FAILED with the read scope already released.
140+
VIRTUAL RADIENT_STATUS METHOD(EndRead)(THIS) PURE;
141+
142+
/// Acquires exclusive write access and returns the first byte through ppData.
143+
/// Returns RADIENT_STATUS_INVALID_OPERATION while any reader or writer is
144+
/// active, including a second BeginWrite from the current writer. A null
145+
/// ppData returns RADIENT_STATUS_INVALID_ARGUMENT. On any failure, a non-null
146+
/// ppData is set to nullptr and access state is unchanged. Returns
147+
/// RADIENT_STATUS_OK on success, including for an empty blob.
148+
VIRTUAL RADIENT_STATUS METHOD(BeginWrite)(THIS_
149+
void** ppData) PURE;
150+
151+
/// Releases exclusive access after the caller has finished writing its bytes.
152+
/// Returns RADIENT_STATUS_OK when a writer was active, making those writes
153+
/// visible to subsequent access scopes. Otherwise returns
154+
/// RADIENT_STATUS_INVALID_OPERATION without changing state. Does not invoke
155+
/// OnLastReaderReleased.
156+
VIRTUAL RADIENT_STATUS METHOD(EndWrite)(THIS) PURE;
157+
};
158+
DILIGENT_END_INTERFACE
159+
160+
#include "../../../DiligentCore/Primitives/interface/UndefInterfaceHelperMacros.h"
161+
162+
#if DILIGENT_C_INTERFACE
163+
# define IRadientDataBlob_GetSize(This) CALL_IFACE_METHOD(RadientDataBlob, GetSize, This)
164+
# define IRadientDataBlob_BeginRead(This, ...) CALL_IFACE_METHOD(RadientDataBlob, BeginRead, This, __VA_ARGS__)
165+
# define IRadientDataBlob_EndRead(This) CALL_IFACE_METHOD(RadientDataBlob, EndRead, This)
166+
# define IRadientDataBlob_BeginWrite(This, ...) CALL_IFACE_METHOD(RadientDataBlob, BeginWrite, This, __VA_ARGS__)
167+
# define IRadientDataBlob_EndWrite(This) CALL_IFACE_METHOD(RadientDataBlob, EndWrite, This)
168+
#endif
169+
170+
// clang-format on
171+
172+
#include "../../../DiligentCore/Primitives/interface/DefineGlobalFuncHelperMacros.h"
173+
174+
/// Creates a CPU data blob without requiring a Radient engine or graphics device.
175+
///
176+
/// \param [in] CreateInfo - Size, optional initial data, and last-reader notification.
177+
/// \param [out] ppBlob - Receives an owning reference on success. Must be
178+
/// non-null and point to a null interface pointer.
179+
///
180+
/// Returns RADIENT_STATUS_OK on success, RADIENT_STATUS_INVALID_ARGUMENT for a
181+
/// null output pointer or an unsupported allocation size, or RADIENT_STATUS_FAILED
182+
/// if allocation fails. On failure, a non-null ppBlob is set to nullptr.
183+
/// C callers use Diligent_CreateRadientDataBlob and pass a CreateInfo pointer;
184+
/// a null CreateInfo pointer returns RADIENT_STATUS_INVALID_ARGUMENT.
185+
RADIENT_STATUS DILIGENT_GLOBAL_FUNCTION(CreateRadientDataBlob)(const RadientDataBlobCreateInfo REF CreateInfo,
186+
IRadientDataBlob** ppBlob);
187+
188+
#include "../../../DiligentCore/Primitives/interface/UndefGlobalFuncHelperMacros.h"
189+
190+
DILIGENT_END_NAMESPACE // namespace Diligent

0 commit comments

Comments
 (0)