carb/container/BufferedObject.h
File members: carb/container/BufferedObject.h
// Copyright (c) 2021-2023, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#pragma once
#include "../Defines.h"
#include "../cpp/Utility.h"
#include "../cpp/Atomic.h"
#include <array>
#include <cstdint>
#include <utility>
namespace carb
{
namespace container
{
template <typename T>
class BufferedObject final
{
CARB_PREVENT_COPY_AND_MOVE(BufferedObject);
enum Flags : uint8_t
{
eFlagsCreate = 0x06, // Field0=0, Field1=0, Field2=1, Field3=2
eFlagsDestroy = 0x00,
eDataAvailable = 0x40, // Field0=1
eField0Mask = 0xc0,
eField1Mask = 0x30,
eField2Mask = 0x0c,
eField3Mask = 0x03,
};
public:
BufferedObject() : m_flags(eFlagsCreate), m_buffer{}
{
}
template <typename... TArgs>
constexpr explicit BufferedObject(carb::cpp::in_place_t, TArgs&&... args)
: m_flags(eFlagsCreate), m_buffer{ std::forward<TArgs>(args)... }
{
}
~BufferedObject()
{
m_flags = eFlagsDestroy;
}
template <typename... TArgs>
void emplace_back(TArgs&&... args)
{
uint8_t flagsNow;
uint8_t newFlags;
// place item in buffer in producer index/slot
m_buffer[(m_flags.load(std::memory_order_acquire) & eField1Mask) >> 4] = { std::forward<TArgs>(args)... };
//
// to produce a new value we need to:
// 1. set field 0 in flags to 1 (using mask eNewDataMask)
// 2. swap fields 1 and 2 ((flagsNow & eField2Mask) << 2) | ((flagsNow & eField1Mask) >> 2)
// 3. leave field 3 unchanged (flagsNow & eField3Mask)
//
do
{
flagsNow = m_flags.load(std::memory_order_acquire);
newFlags = eDataAvailable | ((flagsNow & eField2Mask) << 2) | ((flagsNow & eField1Mask) >> 2) |
(flagsNow & eField3Mask);
} while (
!m_flags.compare_exchange_strong(flagsNow, newFlags, std::memory_order_acq_rel, std::memory_order_relaxed));
}
void push_back(T&& item)
{
emplace_back(std::move(item));
}
void push_back(T const& item)
{
emplace_back(item);
}
T& front()
{
return m_buffer[m_flags.load(std::memory_order_acquire) & eField3Mask];
}
void pop_front()
{
uint8_t flagsNow;
uint8_t newFlags;
//
// to consume a new value:
// 1. check if new data available bit is set, if not just return previous data
// 2. remove the new data available bit
// 3. swap fields 2 and 3 ((flagsNow & eField3Mask) << 2) | ((flagsNow & eField2Mask) >> 2)
// 4. leave field 1 unchanged (flagsNow & eField1Mask)
//
do
{
flagsNow = m_flags.load(std::memory_order_acquire);
// check new data available bit set first
if ((flagsNow & eDataAvailable) == 0)
{
break;
}
newFlags = (flagsNow & eField1Mask) | ((flagsNow & eField3Mask) << 2) | ((flagsNow & eField2Mask) >> 2);
} while (
!m_flags.compare_exchange_strong(flagsNow, newFlags, std::memory_order_acq_rel, std::memory_order_relaxed));
}
private:
/*
* 8-bits of flags (2-bits per field)
*
* 0 1 2 3
* +--+--+--+--+
* |00|00|00|00|
* +--+--+--+--+
*
* Field 0: Is new data available? 0 == no, 1 == yes
* Field 1: Index into m_buffer that new values are pushed/emplaced into via push_back()/emplace_back()
* Field 2: Index into m_buffer that is the buffer between producer and consumer
* Field 3: Index into m_buffer that represents front()
*
* When the producer pushes a new value to m_buffer[field1], it will then atomically swap
* Field 1 and Field 2 and set Field 0 to 1 (to indicate new data is available)
*
* When the consumer calls front(), it just returns m_buffer[field3]. Since the producer
* never changes field3 value, the consumer is safe to call front() without any locks, even
* if the producer is pushing new values.
*
* When the consumer calls pop_front(), it will atomically swap
* Field 3 and Field 2 and set Field 0 back to 0 (to indicate middle buffer was drained)
*
* Producer
* * only ever sets Field 0 to 1
* * only ever writes to m_buffer[field1]
* * only ever swaps Field 1 and Field 2
*
* Consumer
* * only ever sets Field 0 to 0
* * only ever reads to m_buffer[field3]
* * only ever swaps Field 2 and Field 3
*/
std::atomic<uint8_t> m_flags;
std::array<T, 3> m_buffer;
};
} // namespace container
} // namespace carb