From 27e4c8ce0b85b1c6a6af8170db662fa4a209f787 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:06:00 -0800 Subject: [PATCH 1/9] initial data buffer commit --- MMCore/BufferAdapter.cpp | 128 ++++++++++++++++++++++++ MMCore/BufferAdapter.h | 93 ++++++++++++++++++ MMCore/Buffer_v2.cpp | 206 +++++++++++++++++++++++++++++++++++++++ MMCore/Buffer_v2.h | 187 +++++++++++++++++++++++++++++++++++ MMCore/MMCore.cpp | 69 ++++++------- MMCore/MMCore.h | 4 +- 6 files changed, 652 insertions(+), 35 deletions(-) create mode 100644 MMCore/BufferAdapter.cpp create mode 100644 MMCore/BufferAdapter.h create mode 100644 MMCore/Buffer_v2.cpp create mode 100644 MMCore/Buffer_v2.h diff --git a/MMCore/BufferAdapter.cpp b/MMCore/BufferAdapter.cpp new file mode 100644 index 000000000..60d1d3bea --- /dev/null +++ b/MMCore/BufferAdapter.cpp @@ -0,0 +1,128 @@ +#include "BufferAdapter.h" + +// For demonstration, we assume DEVICE_OK and DEVICE_ERR macros are defined in MMCore.h or an included error header. +#ifndef DEVICE_OK + #define DEVICE_OK 0 +#endif +#ifndef DEVICE_ERR + #define DEVICE_ERR -1 +#endif + +BufferAdapter::BufferAdapter(bool useV2Buffer, unsigned int memorySizeMB) + : useV2_(useV2Buffer), circBuffer_(nullptr), v2Buffer_(nullptr) +{ + if (useV2_) { + // Create a new v2 buffer with a total size of memorySizeMB megabytes. + // Multiply by (1 << 20) to convert megabytes to bytes. + size_t bytes = memorySizeMB * (1 << 20); + v2Buffer_ = new DataBuffer(bytes, "DEFAULT"); + } else { + circBuffer_ = new CircularBuffer(memorySizeMB); + // Optionally, perform any necessary initialization for the circular buffer. + } +} + +BufferAdapter::~BufferAdapter() +{ + if (useV2_) { + if (v2Buffer_) { + delete v2Buffer_; + } + } else { + if (circBuffer_) { + delete circBuffer_; + } + } +} + +bool BufferAdapter::InsertImage(const MM::Device *caller, const unsigned char* buf, + unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents, const char* serializedMetadata) +{ + if (useV2_) { + // For demonstration, assume that for a simple image the number of bytes is width * height * byteDepth * nComponents. + size_t dataSize = width * height * byteDepth * nComponents; + int res = v2Buffer_->InsertData(caller, buf, dataSize, serializedMetadata); + return (res == 0); + } else { + // For now, pass a null metadata pointer. + return circBuffer_->InsertImage(buf, width, height, byteDepth, nComponents, nullptr); + } +} + +const unsigned char* BufferAdapter::GetTopImage() const +{ + if (useV2_) { + // Minimal support: the v2Buffer currently does not expose GetTopImage. + return nullptr; + } else { + return circBuffer_->GetTopImage(); + } +} + +const unsigned char* BufferAdapter::GetNextImage() +{ + if (useV2_) { + // Minimal support: return nullptr since v2Buffer does not provide next image retrieval. + return nullptr; + } else { + return circBuffer_->GetNextImage(); + } +} + +const mm::ImgBuffer* BufferAdapter::GetNthFromTopImageBuffer(unsigned long n) const +{ + if (useV2_) { + // Implement logic for v2Buffer if available + return nullptr; // Placeholder + } else { + return circBuffer_->GetNthFromTopImageBuffer(n); + } +} + +const mm::ImgBuffer* BufferAdapter::GetNextImageBuffer(unsigned channel) +{ + if (useV2_) { + // Implement logic for v2Buffer if available + return nullptr; // Placeholder + } else { + return circBuffer_->GetNextImageBuffer(channel); + } +} + +bool BufferAdapter::Initialize(unsigned numChannels, unsigned width, unsigned height, unsigned bytesPerPixel) +{ + if (useV2_) { + // Implement initialization logic for v2Buffer + return true; // Placeholder + } else { + return circBuffer_->Initialize(numChannels, width, height, bytesPerPixel); + } +} + +unsigned BufferAdapter::GetMemorySizeMB() const +{ + if (useV2_) { + return 0; // TODO: need to implement this + } else { + return circBuffer_->GetMemorySizeMB(); + } +} + +long BufferAdapter::GetRemainingImageCount() const +{ + if (useV2_) { + return 0; // TODO: need to implement this + } else { + return circBuffer_->GetRemainingImageCount(); + } +} + +void BufferAdapter::Clear() +{ + if (useV2_) { + // In this basic implementation, we call ReleaseBuffer with the known buffer name. + v2Buffer_->ReleaseBuffer("DEFAULT"); + } else { + circBuffer_->Clear(); + } +} \ No newline at end of file diff --git a/MMCore/BufferAdapter.h b/MMCore/BufferAdapter.h new file mode 100644 index 000000000..e618aea30 --- /dev/null +++ b/MMCore/BufferAdapter.h @@ -0,0 +1,93 @@ +#ifndef BUFFERADAPTER_H +#define BUFFERADAPTER_H + +#include "CircularBuffer.h" +#include "Buffer_v2.h" +#include "../MMDevice/MMDevice.h" + +// BufferAdapter provides a common interface for buffer operations +// used by MMCore. It currently supports only a minimal set of functions. +class BufferAdapter { +public: + /** + * Constructor. + * @param useV2Buffer Set to true to use the new DataBuffer (v2); false to use CircularBuffer. + * @param memorySizeMB Memory size for the buffer (in megabytes). + */ + BufferAdapter(bool useV2Buffer, unsigned int memorySizeMB); + ~BufferAdapter(); + + /** + * Insert an image into the buffer. + * @param caller The device inserting the image. + * @param buf The image data. + * @param width Image width. + * @param height Image height. + * @param byteDepth Bytes per pixel. + * @param nComponents Number of components in the image. + * @param pMd Metadata associated with the image. + * @return true on success, false on error. + */ + bool InsertImage(const MM::Device *caller, const unsigned char* buf, + unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents, Metadata* pMd); + + /** + * Get a pointer to the top (most recent) image. + * @return Pointer to image data, or nullptr if unavailable. + */ + const unsigned char* GetTopImage() const; + + /** + * Get a pointer to the next image from the buffer. + * @return Pointer to image data, or nullptr if unavailable. + */ + const unsigned char* GetNextImage(); + + /** + * Get a pointer to the nth image from the top of the buffer. + * @param n The index from the top. + * @return Pointer to image data, or nullptr if unavailable. + */ + const mm::ImgBuffer* GetNthFromTopImageBuffer(unsigned long n) const; + + /** + * Get a pointer to the next image buffer for a specific channel. + * @param channel The channel number. + * @return Pointer to image data, or nullptr if unavailable. + */ + const mm::ImgBuffer* GetNextImageBuffer(unsigned channel); + + /** + * Initialize the buffer with the given parameters. + * @param numChannels Number of channels. + * @param width Image width. + * @param height Image height. + * @param bytesPerPixel Bytes per pixel. + * @return true on success, false on error. + */ + bool Initialize(unsigned numChannels, unsigned width, unsigned height, unsigned bytesPerPixel); + + /** + * Get the memory size of the buffer in megabytes. + * @return Memory size in MB. + */ + unsigned GetMemorySizeMB() const; + + /** + * Get the remaining image count in the buffer. + * @return Number of remaining images. + */ + long GetRemainingImageCount() const; + + /** + * Clear the entire image buffer. + */ + void Clear(); + +private: + bool useV2_; // if true use DataBuffer, otherwise use CircularBuffer. + CircularBuffer* circBuffer_; + DataBuffer* v2Buffer_; +}; + +#endif // BUFFERADAPTER_H \ No newline at end of file diff --git a/MMCore/Buffer_v2.cpp b/MMCore/Buffer_v2.cpp new file mode 100644 index 000000000..e4ef33750 --- /dev/null +++ b/MMCore/Buffer_v2.cpp @@ -0,0 +1,206 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: Buffer_v2.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: MMCore +//----------------------------------------------------------------------------- +// DESCRIPTION: Generic implementation of a buffer for storing image data and +// metadata. Provides thread-safe access for reading and writing +// with configurable overflow behavior. +// +// COPYRIGHT: Henry Pinkard, 2025 +// +// LICENSE: This file is distributed under the "Lesser GPL" (LGPL) license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Henry Pinkard, 01/31/2025 + + +/* +Design Overview: + +The buffer is designed as a flexible data structure for storing image data and metadata: + +Buffer Structure: +- A large block of contiguous memory divided into slots + +Slots: +- Contiguous sections within the buffer that can vary in size +- Support exclusive write access with shared read access +- Memory management through reference counting: + - Writers get exclusive ownership during writes + - Readers can get shared read-only access + - Slots are recycled when all references are released + +Data Access: +- Two access patterns supported: + 1. Copy-based access + 2. Direct pointer access with explicit release +- Reference counting ensures safe memory management +- Slots become available for recycling when: + - Writing is complete (via Insert or GetWritingSlot+Release) + - All readers have released their references + +Metadata Handling: +- Devices must specify PixelType when adding data +- Device-specific metadata requirements (e.g. image dimensions) are handled at the + device API level rather than in the buffer API to maintain clean separation +*/ + + +#include "Buffer_v2.h" +#include + +DataBuffer::DataBuffer(size_t numBytes, const char* name) { + // Initialize basic buffer with given size + AllocateBuffer(numBytes, name); +} + +DataBuffer::~DataBuffer() { + // Cleanup + delete[] buffer_; +} + +/** + * Allocate a character buffer + * @param numBytes The size of the buffer to allocate. + * @param name The name of the buffer. + * @return Error code (0 on success). + */ +int DataBuffer::AllocateBuffer(size_t numBytes, const char* name) { + buffer_ = new char[numBytes]; + bufferSize_ = numBytes; + bufferName_ = name; + // TODO: Store the buffer in a member variable for later use + return DEVICE_OK; +} + +/** + * Release the buffer if it matches the given name. + * @param name The name of the buffer to release. + * @return Error code (0 on success, error if buffer not found or already released). + */ +int DataBuffer::ReleaseBuffer(const char* name) { + if (buffer_ != nullptr && bufferName_ == name) { + delete[] buffer_; + buffer_ = nullptr; + bufferSize_ = 0; + bufferName_ = nullptr; + return DEVICE_OK; + } + // TODO: throw errors if other code holds pointers on stuff + return DEVICE_ERR; // Return an error if the buffer is not found or already released +} + +/** + * @brief Copy data into the next available slot in the buffer. + * + * Returns the size of the copied data through dataSize. + * Implementing code should check the device type of the caller, and ensure that + * all required metadata for interpreting its image data is there. + * Note: this can be implemented in terms of Get/Release slot + memcopy. + * + * @param caller The device calling this function. + * @param data The data to be copied into the buffer. + * @param dataSize The size of the data to be copied. + * @param serializedMetadata The serialized metadata associated with the data. + * @return Error code (0 on success). + */ +int DataBuffer::InsertData(const MM::Device *caller, const void* data, size_t dataSize, const char* serializedMetadata) { + // Basic implementation - just copy data + // TODO: Add proper buffer management and metadata handling + return 0; +} + +/** + * Check if a new slot has been fully written in this buffer + * @return true if new data is ready, false otherwise + */ +bool DataBuffer::IsNewDataReady() { + // Basic implementation + return false; +} + +/** + * Copy the next available data and metadata from the buffer + * @param dataDestination Destination buffer to copy data into + * @param dataSize Returns the size of the copied data, or 0 if no data available + * @param md Metadata object to populate + * @param waitForData If true, block until data becomes available + * @return Error code (0 on success) + */ +int DataBuffer::CopyNextDataAndMetadata(void* dataDestination, + size_t* dataSize, Metadata &md, bool waitForData) { + // Basic implementation + // TODO: Add proper data copying and metadata handling + return 0; +} + +/** + * Configure whether to overwrite old data when buffer is full. + * + * If true, when there are no more slots available for writing because + * images haven't been read fast enough, then automatically recycle the + * oldest slot(s) in the buffer as needed in order to make space for new images. + * This is suitable for situations when its okay to drop frames, like live + * view when data is not being saved. + * + * If false, then throw an exception if the buffer becomes full. + * + * @param overwrite Whether to enable overwriting of old data + * @return Error code (0 on success) + */ +int DataBuffer::SetOverwriteData(bool overwrite) { + // Basic implementation + return 0; +} + +/** + * @brief Get a pointer to the next available data slot in the buffer for writing. + * + * The caller must release the slot using ReleaseDataSlot after writing is complete. + * Internally this will use a std::unique_ptr. + * + * @param caller The device calling this function. + * @param slot Pointer to the slot where data will be written. + * @param slotSize The size of the slot. + * @param serializedMetadata The serialized metadata associated with the data. + * @return Error code (0 on success). + */ +int DataBuffer::GetWritingSlot(const MM::Device *caller, void** slot, size_t slotSize, const char* serializedMetadata) { + // Basic implementation + return 0; +} + +/** + * @brief Release a data slot after writing is complete. + * + * @param caller The device calling this function. + * @param buffer The buffer to be released. + * @return Error code (0 on success). + */ +int DataBuffer::ReleaseWritingSlot(const MM::Device *caller, void* buffer) { + // Basic implementation + return 0; +} + +/** + * Get a pointer to a heap allocated Metadata object with the required fields filled in + * @param md Pointer to the Metadata object to be created. + * @param width The width of the image. + * @param height The height of the image. + * @param bitDepth The bit depth of the image. + * @return Error code (0 on success). + */ +int DataBuffer::CreateCameraRequiredMetadata(Metadata** md, int width, int height, int bitDepth) { + // Basic implementation + // TODO: Create metadata with required camera fields + return 0; +} diff --git a/MMCore/Buffer_v2.h b/MMCore/Buffer_v2.h new file mode 100644 index 000000000..7cdb02523 --- /dev/null +++ b/MMCore/Buffer_v2.h @@ -0,0 +1,187 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: Buffer_v2.h +// PROJECT: Micro-Manager +// SUBSYSTEM: MMCore +//----------------------------------------------------------------------------- +// DESCRIPTION: Generic implementation of a buffer for storing image data and +// metadata. Provides thread-safe access for reading and writing +// with configurable overflow behavior. +// +// COPYRIGHT: Henry Pinkard, 2025 +// +// LICENSE: This file is distributed under the "Lesser GPL" (LGPL) license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Henry Pinkard, henry.pinkard@gmail.com, 01/31/2025 +// + +#pragma once + +#include "Metadata.h" +#include "../MMDevice/MMDevice.h" +#include +#include + +class DataBuffer { + public: + DataBuffer(size_t numBytes, const char* name); + ~DataBuffer(); + + + /////// Buffer Allocation and Destruction + + // C version + int AllocateBuffer(size_t numBytes, const char* name); + //// Is there a need for a name? + //// Maybe makes sense for Core to assing a unique integer + int ReleaseBuffer(const char* name); + + // TODO: Other versions for allocating buffers Java, Python + + + /////// Monitoring the Buffer /////// + int GetAvailableBytes(void* buffer, size_t* availableBytes); + int GetNumSlotsUsed(void* buffer, size_t* numSlotsUsed); + + + + //// Configuration options + /** + * Configure whether to overwrite old data when buffer is full. + * + * If true, when there are no more slots available for writing because + * images haven't been read fast enough, then automatically recycle the + * oldest slot(s) in the buffer as needed in order to make space for new images. + * This is suitable for situations when its okay to drop frames, like live + * view when data is not being saved. + * + * If false, then throw an exception if the buffer becomes full. + * + * @param overwrite Whether to enable overwriting of old data + * @return Error code (0 on success) + */ + int SetOverwriteData(bool overwrite); + + + + /////// Getting Data Out /////// + + /** + * Check if a new slot has been fully written in this buffer + * @return true if new data is ready, false otherwise + */ + bool IsNewDataReady(); + + /** + * Copy the next available data and metadata from the buffer + * @param dataDestination Destination buffer to copy data into + * @param dataSize Returns the size of the copied data, or 0 if no data available + * @param md Metadata object to populate + * @param waitForData If true, block until data becomes available + * @return Error code (0 on success) + */ + int CopyNextDataAndMetadata(void* dataDestination, + size_t* dataSize, Metadata &md, bool waitForData); + + + /** + * Copy the next available metadata from the buffer + * Returns the size of the copied metadata through metadataSize, + * or 0 if no metadata is available + */ + int CopyNextMetadata(void* buffer, Metadata &md); + + /** + * Get a pointer to the next available data slot in the buffer + * The caller must release the slot using ReleaseNextDataAndMetadata + * If awaitReady is false and the data was inserted using GetWritingSlot, it + * is possible to read the data as it is being written (e.g. to monitor progress) + * of large or slow image being written + * + * Internally this will use a std::shared_ptr + */ + int GetNextSlotPointer(void** slotPointer, size_t* dataSize, + Metadata &md, bool awaitReady=true); + + /** + * Release the next data slot and its associated metadata + */ + int ReleaseNextSlot(void** slotPointer); + + + ////// Writing Data into buffer ////// + + /** + * @brief Copy data into the next available slot in the buffer. + * + * Returns the size of the copied data through dataSize. + * Implementing code should check the device type of the caller, and ensure that + * all required metadata for interpreting its image data is there. + * Note: this can be implemented in terms of Get/Release slot + memcopy. + * + * @param caller The device calling this function. + * @param data The data to be copied into the buffer. + * @param dataSize The size of the data to be copied. + * @param serializedMetadata The serialized metadata associated with the data. + * @return Error code (0 on success). + */ + int InsertData(const MM::Device *caller, const void* data, size_t dataSize, const char* serializedMetadata); + + /** + * @brief Get a pointer to the next available data slot in the buffer for writing. + * + * The caller must release the slot using ReleaseDataSlot after writing is complete. + * Internally this will use a std::unique_ptr. + * + * @param caller The device calling this function. + * @param slot Pointer to the slot where data will be written. + * @param slotSize The size of the slot. + * @param serializedMetadata The serialized metadata associated with the data. + * @return Error code (0 on success). + */ + int GetWritingSlot(const MM::Device *caller, void** slot, size_t slotSize, const char* serializedMetadata); + + /** + * @brief Release a data slot after writing is complete. + * + * @param caller The device calling this function. + * @param buffer The buffer to be released. + * @return Error code (0 on success). + */ + int ReleaseWritingSlot(const MM::Device *caller, void* buffer); + + + + + ////// Camera API ////// + + // Set the buffer for a camera to write into + int SetCameraBuffer(const char* camera, void* buffer); + + // Get a pointer to a heap allocated Metadata object with the required fields filled in + int CreateCameraRequiredMetadata(Metadata**, int width, int height, int bitDepth); + + private: + // Basic buffer management + void* buffer_; + size_t bufferSize_; + std::string bufferName_; + + // Read/write positions + size_t writePos_; + size_t readPos_; + + // Configuration + bool overwriteWhenFull_; + + // Mutex for thread safety + std::mutex mutex_; +}; diff --git a/MMCore/MMCore.cpp b/MMCore/MMCore.cpp index 19dd2b0cb..5164dfea2 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -137,7 +137,7 @@ CMMCore::CMMCore() : properties_(0), externalCallback_(0), pixelSizeGroup_(0), - cbuf_(0), + bufferAdapter_(nullptr), pluginManager_(new CPluginManager()), deviceManager_(new mm::DeviceManager()), pPostedErrorsLock_(NULL) @@ -151,7 +151,7 @@ CMMCore::CMMCore() : callback_ = new CoreCallback(this); const unsigned seqBufMegabytes = (sizeof(void*) > 4) ? 250 : 25; - cbuf_ = new CircularBuffer(seqBufMegabytes); + bufferAdapter_ = new BufferAdapter(false, seqBufMegabytes); nullAffine_ = new std::vector(6); for (int i = 0; i < 6; i++) { @@ -181,7 +181,7 @@ CMMCore::~CMMCore() delete callback_; delete configGroups_; delete properties_; - delete cbuf_; + delete bufferAdapter_; delete pixelSizeGroup_; delete pPostedErrorsLock_; @@ -2830,12 +2830,12 @@ void CMMCore::startSequenceAcquisition(long numImages, double intervalMs, bool s try { - if (!cbuf_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) + if (!bufferAdapter_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) { logError(getDeviceName(camera).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); } - cbuf_->Clear(); + bufferAdapter_->Clear(); mm::DeviceModuleLockGuard guard(camera); LOG_DEBUG(coreLogger_) << "Will start sequence acquisition from default camera"; @@ -2874,12 +2874,12 @@ void CMMCore::startSequenceAcquisition(const char* label, long numImages, double throw CMMError(getCoreErrorText(MMERR_NotAllowedDuringSequenceAcquisition).c_str(), MMERR_NotAllowedDuringSequenceAcquisition); - if (!cbuf_->Initialize(pCam->GetNumberOfChannels(), pCam->GetImageWidth(), pCam->GetImageHeight(), pCam->GetImageBytesPerPixel())) + if (!bufferAdapter_->Initialize(pCam->GetNumberOfChannels(), pCam->GetImageWidth(), pCam->GetImageHeight(), pCam->GetImageBytesPerPixel())) { logError(getDeviceName(pCam).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); } - cbuf_->Clear(); + bufferAdapter_->Clear(); LOG_DEBUG(coreLogger_) << "Will start sequence acquisition from camera " << label; @@ -2925,12 +2925,12 @@ void CMMCore::initializeCircularBuffer() throw (CMMError) if (camera) { mm::DeviceModuleLockGuard guard(camera); - if (!cbuf_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) + if (!bufferAdapter_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) { logError(getDeviceName(camera).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); } - cbuf_->Clear(); + bufferAdapter_->Clear(); } else { @@ -2977,12 +2977,12 @@ void CMMCore::startContinuousSequenceAcquisition(double intervalMs) throw (CMMEr ,MMERR_NotAllowedDuringSequenceAcquisition); } - if (!cbuf_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) + if (!bufferAdapter_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) { logError(getDeviceName(camera).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); } - cbuf_->Clear(); + bufferAdapter_->Clear(); LOG_DEBUG(coreLogger_) << "Will start continuous sequence acquisition from current camera"; int nRet = camera->StartSequenceAcquisition(intervalMs); if (nRet != DEVICE_OK) @@ -3078,7 +3078,7 @@ void* CMMCore::getLastImage() throw (CMMError) } } - unsigned char* pBuf = const_cast(cbuf_->GetTopImage()); + unsigned char* pBuf = const_cast(bufferAdapter_->GetTopImage()); if (pBuf != 0) return pBuf; else @@ -3094,7 +3094,7 @@ void* CMMCore::getLastImageMD(unsigned channel, unsigned slice, Metadata& md) co if (slice != 0) throw CMMError("Slice must be 0"); - const mm::ImgBuffer* pBuf = cbuf_->GetTopImageBuffer(channel); + const mm::ImgBuffer* pBuf = bufferAdapter_->GetTopImageBuffer(channel); if (pBuf != 0) { md = pBuf->GetMetadata(); @@ -3135,7 +3135,7 @@ void* CMMCore::getLastImageMD(Metadata& md) const throw (CMMError) */ void* CMMCore::getNBeforeLastImageMD(unsigned long n, Metadata& md) const throw (CMMError) { - const mm::ImgBuffer* pBuf = cbuf_->GetNthFromTopImageBuffer(n); + const mm::ImgBuffer* pBuf = bufferAdapter_->GetNthFromTopImageBuffer(n); if (pBuf != 0) { md = pBuf->GetMetadata(); @@ -3159,7 +3159,7 @@ void* CMMCore::getNBeforeLastImageMD(unsigned long n, Metadata& md) const throw */ void* CMMCore::popNextImage() throw (CMMError) { - unsigned char* pBuf = const_cast(cbuf_->GetNextImage()); + unsigned char* pBuf = const_cast(bufferAdapter_->GetNextImage()); if (pBuf != 0) return pBuf; else @@ -3177,7 +3177,7 @@ void* CMMCore::popNextImageMD(unsigned channel, unsigned slice, Metadata& md) th if (slice != 0) throw CMMError("Slice must be 0"); - const mm::ImgBuffer* pBuf = cbuf_->GetNextImageBuffer(channel); + const mm::ImgBuffer* pBuf = bufferAdapter_->GetNextImageBuffer(channel); if (pBuf != 0) { md = pBuf->GetMetadata(); @@ -3203,7 +3203,7 @@ void* CMMCore::popNextImageMD(Metadata& md) throw (CMMError) */ void CMMCore::clearCircularBuffer() throw (CMMError) { - cbuf_->Clear(); + bufferAdapter_->Clear(); } /** @@ -3212,12 +3212,13 @@ void CMMCore::clearCircularBuffer() throw (CMMError) void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes ) throw (CMMError) { - delete cbuf_; // discard old buffer + delete bufferAdapter_; // discard old buffer LOG_DEBUG(coreLogger_) << "Will set circular buffer size to " << sizeMB << " MB"; try { - cbuf_ = new CircularBuffer(sizeMB); + // TODO: need to store a flag about which buffer to use + bufferAdapter_ = new BufferAdapter(false, sizeMB); } catch (std::bad_alloc& ex) { @@ -3226,7 +3227,7 @@ void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes messs << getCoreErrorText(MMERR_OutOfMemory).c_str() << " " << ex.what() << '\n'; throw CMMError(messs.str().c_str() , MMERR_OutOfMemory); } - if (NULL == cbuf_) throw CMMError(getCoreErrorText(MMERR_OutOfMemory).c_str(), MMERR_OutOfMemory); + if (NULL == bufferAdapter_) throw CMMError(getCoreErrorText(MMERR_OutOfMemory).c_str(), MMERR_OutOfMemory); try @@ -3237,7 +3238,7 @@ void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes if (camera) { mm::DeviceModuleLockGuard guard(camera); - if (!cbuf_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) + if (!bufferAdapter_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); } @@ -3250,7 +3251,7 @@ void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes messs << getCoreErrorText(MMERR_OutOfMemory).c_str() << " " << ex.what() << '\n'; throw CMMError(messs.str().c_str() , MMERR_OutOfMemory); } - if (NULL == cbuf_) + if (NULL == bufferAdapter_) throw CMMError(getCoreErrorText(MMERR_OutOfMemory).c_str(), MMERR_OutOfMemory); } @@ -3259,9 +3260,9 @@ void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes */ unsigned CMMCore::getCircularBufferMemoryFootprint() { - if (cbuf_) + if (bufferAdapter_) { - return cbuf_->GetMemorySizeMB(); + return bufferAdapter_->GetMemorySizeMB(); } return 0; } @@ -3271,9 +3272,9 @@ unsigned CMMCore::getCircularBufferMemoryFootprint() */ long CMMCore::getRemainingImageCount() { - if (cbuf_) + if (bufferAdapter_) { - return cbuf_->GetRemainingImageCount(); + return bufferAdapter_->GetRemainingImageCount(); } return 0; } @@ -3283,9 +3284,9 @@ long CMMCore::getRemainingImageCount() */ long CMMCore::getBufferTotalCapacity() { - if (cbuf_) + if (bufferAdapter_) { - return cbuf_->GetSize(); + return bufferAdapter_->GetSize(); } return 0; } @@ -3297,9 +3298,9 @@ long CMMCore::getBufferTotalCapacity() */ long CMMCore::getBufferFreeCapacity() { - if (cbuf_) + if (bufferAdapter_) { - return cbuf_->GetFreeSize(); + return bufferAdapter_->GetFreeSize(); } return 0; } @@ -3309,7 +3310,7 @@ long CMMCore::getBufferFreeCapacity() */ bool CMMCore::isBufferOverflowed() const { - return cbuf_->Overflow(); + return bufferAdapter_->Overflow(); } /** @@ -4412,7 +4413,7 @@ void CMMCore::setROI(int x, int y, int xSize, int ySize) throw (CMMError) // inconsistent with the current image size. There is no way to "fix" // popNextImage() to handle this correctly, so we need to make sure we // discard such images. - cbuf_->Clear(); + bufferAdapter_->Clear(); } else throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); @@ -4491,7 +4492,7 @@ void CMMCore::setROI(const char* label, int x, int y, int xSize, int ySize) thro // inconsistent with the current image size. There is no way to "fix" // popNextImage() to handle this correctly, so we need to make sure we // discard such images. - cbuf_->Clear(); + bufferAdapter_->Clear(); } else throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); @@ -4550,7 +4551,7 @@ void CMMCore::clearROI() throw (CMMError) // inconsistent with the current image size. There is no way to "fix" // popNextImage() to handle this correctly, so we need to make sure we // discard such images. - cbuf_->Clear(); + bufferAdapter_->Clear(); } } diff --git a/MMCore/MMCore.h b/MMCore/MMCore.h index 83d67b0b2..f31b8b412 100644 --- a/MMCore/MMCore.h +++ b/MMCore/MMCore.h @@ -65,6 +65,7 @@ #include "Error.h" #include "ErrorCodes.h" #include "Logging/Logger.h" +#include "BufferAdapter.h" #include #include @@ -658,7 +659,8 @@ class CMMCore CorePropertyCollection* properties_; MMEventCallback* externalCallback_; // notification hook to the higher layer (e.g. GUI) PixelSizeConfigGroup* pixelSizeGroup_; - CircularBuffer* cbuf_; + // New adapter to wrap either the circular buffer or the DataBuffer (v2) + BufferAdapter* bufferAdapter_; std::shared_ptr pluginManager_; std::shared_ptr deviceManager_; From f886b12d971687e2e4040913dd831d35895425d9 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Fri, 31 Jan 2025 16:08:13 -0800 Subject: [PATCH 2/9] Factored out circularBuffer behind new API --- MMCore/BufferAdapter.cpp | 84 ++++++++++++++++++++++++++++---- MMCore/BufferAdapter.h | 91 +++++++++++++++++++++++++++++------ MMCore/Buffer_v2.h | 15 ++---- MMCore/CoreCallback.cpp | 10 ++-- MMCore/MMCore.cpp | 7 +-- MMCore/MMCore.h | 2 +- MMCore/MMCore.vcxproj | 6 ++- MMCore/MMCore.vcxproj.filters | 14 +++++- 8 files changed, 185 insertions(+), 44 deletions(-) diff --git a/MMCore/BufferAdapter.cpp b/MMCore/BufferAdapter.cpp index 60d1d3bea..185c04036 100644 --- a/MMCore/BufferAdapter.cpp +++ b/MMCore/BufferAdapter.cpp @@ -35,17 +35,23 @@ BufferAdapter::~BufferAdapter() } } -bool BufferAdapter::InsertImage(const MM::Device *caller, const unsigned char* buf, - unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents, const char* serializedMetadata) -{ +bool BufferAdapter::InsertImage(const unsigned char* buf, + unsigned width, unsigned height, unsigned byteDepth, Metadata* pMd) { if (useV2_) { - // For demonstration, assume that for a simple image the number of bytes is width * height * byteDepth * nComponents. - size_t dataSize = width * height * byteDepth * nComponents; - int res = v2Buffer_->InsertData(caller, buf, dataSize, serializedMetadata); - return (res == 0); + // Implement logic for v2Buffer if available + return false; // Placeholder } else { - // For now, pass a null metadata pointer. - return circBuffer_->InsertImage(buf, width, height, byteDepth, nComponents, nullptr); + return circBuffer_->InsertImage(buf, width, height, byteDepth, pMd); + } +} + +bool BufferAdapter::InsertImage(const unsigned char *buf, unsigned width, unsigned height, + unsigned byteDepth, unsigned nComponents, Metadata *pMd) { + if (useV2_) { + // Implement logic for v2Buffer if available + return false; // Placeholder + } else { + return circBuffer_->InsertImage(buf, width, height, byteDepth, nComponents, pMd); } } @@ -125,4 +131,64 @@ void BufferAdapter::Clear() } else { circBuffer_->Clear(); } +} + +bool BufferAdapter::InsertMultiChannel(const unsigned char *buf, unsigned numChannels, unsigned width, + unsigned height, unsigned byteDepth, Metadata *pMd) { + if (useV2_) { + // Implement logic for v2Buffer if available + return false; // Placeholder + } else { + return circBuffer_->InsertMultiChannel(buf, numChannels, width, height, byteDepth, pMd); + } +} + +bool BufferAdapter::InsertMultiChannel(const unsigned char *buf, unsigned numChannels, unsigned width, + unsigned height, unsigned byteDepth, unsigned nComponents, Metadata *pMd) { + if (useV2_) { + // Implement logic for v2Buffer if available + return false; // Placeholder + } else { + return circBuffer_->InsertMultiChannel(buf, numChannels, width, height, byteDepth, nComponents, pMd); + } +} + +long BufferAdapter::GetSize() const +{ + if (useV2_) { + // Implement logic for v2Buffer if available + return 0; // Placeholder + } else { + return circBuffer_->GetSize(); + } +} + +long BufferAdapter::GetFreeSize() const +{ + if (useV2_) { + // Implement logic for v2Buffer if available + return 0; // Placeholder + } else { + return circBuffer_->GetFreeSize(); + } +} + +bool BufferAdapter::Overflow() const +{ + if (useV2_) { + // Implement logic for v2Buffer if available + return false; // Placeholder + } else { + return circBuffer_->Overflow(); + } +} + +const mm::ImgBuffer* BufferAdapter::GetTopImageBuffer(unsigned channel) const +{ + if (useV2_) { + // Implement logic for v2Buffer if available + return nullptr; // Placeholder + } else { + return circBuffer_->GetTopImageBuffer(channel); + } } \ No newline at end of file diff --git a/MMCore/BufferAdapter.h b/MMCore/BufferAdapter.h index e618aea30..f768e5b8f 100644 --- a/MMCore/BufferAdapter.h +++ b/MMCore/BufferAdapter.h @@ -17,20 +17,6 @@ class BufferAdapter { BufferAdapter(bool useV2Buffer, unsigned int memorySizeMB); ~BufferAdapter(); - /** - * Insert an image into the buffer. - * @param caller The device inserting the image. - * @param buf The image data. - * @param width Image width. - * @param height Image height. - * @param byteDepth Bytes per pixel. - * @param nComponents Number of components in the image. - * @param pMd Metadata associated with the image. - * @return true on success, false on error. - */ - bool InsertImage(const MM::Device *caller, const unsigned char* buf, - unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents, Metadata* pMd); - /** * Get a pointer to the top (most recent) image. * @return Pointer to image data, or nullptr if unavailable. @@ -84,6 +70,83 @@ class BufferAdapter { */ void Clear(); + /** + * Insert an image into the buffer. + * @param buf The image data. + * @param width Image width. + * @param height Image height. + * @param byteDepth Bytes per pixel. + * @param pMd Metadata associated with the image. + * @return true on success, false on error. + */ + bool InsertImage(const unsigned char *buf, unsigned width, unsigned height, + unsigned byteDepth, Metadata *pMd); + + /** + * Insert an image into the buffer with specified number of components. + * @param buf The image data. + * @param width Image width. + * @param height Image height. + * @param byteDepth Bytes per pixel. + * @param nComponents Number of components in the image. + * @param pMd Metadata associated with the image. + * @return true on success, false on error. + */ + bool InsertImage(const unsigned char *buf, unsigned width, unsigned height, + unsigned byteDepth, unsigned nComponents, Metadata *pMd); + + /** + * Insert a multi-channel image into the buffer. + * @param buf The image data. + * @param numChannels Number of channels in the image. + * @param width Image width. + * @param height Image height. + * @param byteDepth Bytes per pixel. + * @param pMd Metadata associated with the image. + * @return true on success, false on error. + */ + bool InsertMultiChannel(const unsigned char *buf, unsigned numChannels, unsigned width, + unsigned height, unsigned byteDepth, Metadata *pMd); + + /** + * Insert a multi-channel image into the buffer with specified number of components. + * @param buf The image data. + * @param numChannels Number of channels in the image. + * @param width Image width. + * @param height Image height. + * @param byteDepth Bytes per pixel. + * @param nComponents Number of components in the image. + * @param pMd Metadata associated with the image. + * @return true on success, false on error. + */ + bool InsertMultiChannel(const unsigned char *buf, unsigned numChannels, unsigned width, + unsigned height, unsigned byteDepth, unsigned nComponents, Metadata *pMd); + + /** + * Get the total capacity of the buffer. + * @return Total capacity of the buffer. + */ + long GetSize() const; + + /** + * Get the free capacity of the buffer. + * @return Free capacity of the buffer. + */ + long GetFreeSize() const; + + /** + * Check if the buffer is overflowed. + * @return True if overflowed, false otherwise. + */ + bool Overflow() const; + + /** + * Get a pointer to the top image buffer for a specific channel. + * @param channel The channel number. + * @return Pointer to image data, or nullptr if unavailable. + */ + const mm::ImgBuffer* GetTopImageBuffer(unsigned channel) const; + private: bool useV2_; // if true use DataBuffer, otherwise use CircularBuffer. CircularBuffer* circBuffer_; diff --git a/MMCore/Buffer_v2.h b/MMCore/Buffer_v2.h index 7cdb02523..cca62f1e2 100644 --- a/MMCore/Buffer_v2.h +++ b/MMCore/Buffer_v2.h @@ -20,12 +20,12 @@ // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. // -// AUTHOR: Henry Pinkard, henry.pinkard@gmail.com, 01/31/2025 -// +// AUTHOR: Henry Pinkard, 01/31/2025 + #pragma once -#include "Metadata.h" +#include "../MMDevice/ImageMetadata.h" #include "../MMDevice/MMDevice.h" #include #include @@ -171,17 +171,12 @@ class DataBuffer { private: // Basic buffer management - void* buffer_; + char* buffer_; size_t bufferSize_; std::string bufferName_; - // Read/write positions - size_t writePos_; - size_t readPos_; - // Configuration bool overwriteWhenFull_; - // Mutex for thread safety - std::mutex mutex_; + }; diff --git a/MMCore/CoreCallback.cpp b/MMCore/CoreCallback.cpp index 572f9a0ce..5dff1539c 100644 --- a/MMCore/CoreCallback.cpp +++ b/MMCore/CoreCallback.cpp @@ -263,7 +263,7 @@ int CoreCallback::InsertImage(const MM::Device* caller, const unsigned char* buf ip->Process(const_cast(buf), width, height, byteDepth); } } - if (core_->cbuf_->InsertImage(buf, width, height, byteDepth, &md)) + if (core_->bufferAdapter_->InsertImage(buf, width, height, byteDepth, &md)) return DEVICE_OK; else return DEVICE_BUFFER_OVERFLOW; @@ -295,7 +295,7 @@ int CoreCallback::InsertImage(const MM::Device* caller, const unsigned char* buf ip->Process(const_cast(buf), width, height, byteDepth); } } - if (core_->cbuf_->InsertImage(buf, width, height, byteDepth, nComponents, &md)) + if (core_->bufferAdapter_->InsertImage(buf, width, height, byteDepth, nComponents, &md)) return DEVICE_OK; else return DEVICE_BUFFER_OVERFLOW; @@ -322,7 +322,7 @@ int CoreCallback::InsertImage(const MM::Device* caller, const ImgBuffer & imgBuf void CoreCallback::ClearImageBuffer(const MM::Device* /*caller*/) { - core_->cbuf_->Clear(); + core_->bufferAdapter_->Clear(); } bool CoreCallback::InitializeImageBuffer(unsigned channels, unsigned slices, @@ -332,7 +332,7 @@ bool CoreCallback::InitializeImageBuffer(unsigned channels, unsigned slices, if (slices != 1) return false; - return core_->cbuf_->Initialize(channels, w, h, pixDepth); + return core_->bufferAdapter_->Initialize(channels, w, h, pixDepth); } int CoreCallback::InsertMultiChannel(const MM::Device* caller, @@ -352,7 +352,7 @@ int CoreCallback::InsertMultiChannel(const MM::Device* caller, { ip->Process( const_cast(buf), width, height, byteDepth); } - if (core_->cbuf_->InsertMultiChannel(buf, numChannels, width, height, byteDepth, &md)) + if (core_->bufferAdapter_->InsertMultiChannel(buf, numChannels, width, height, byteDepth, &md)) return DEVICE_OK; else return DEVICE_BUFFER_OVERFLOW; diff --git a/MMCore/MMCore.cpp b/MMCore/MMCore.cpp index 5164dfea2..b52e4d234 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -140,7 +140,8 @@ CMMCore::CMMCore() : bufferAdapter_(nullptr), pluginManager_(new CPluginManager()), deviceManager_(new mm::DeviceManager()), - pPostedErrorsLock_(NULL) + pPostedErrorsLock_(NULL), + useV2Buffer_(false) { configGroups_ = new ConfigGroupCollection(); pixelSizeGroup_ = new PixelSizeConfigGroup(); @@ -151,7 +152,7 @@ CMMCore::CMMCore() : callback_ = new CoreCallback(this); const unsigned seqBufMegabytes = (sizeof(void*) > 4) ? 250 : 25; - bufferAdapter_ = new BufferAdapter(false, seqBufMegabytes); + bufferAdapter_ = new BufferAdapter(useV2Buffer_, seqBufMegabytes); nullAffine_ = new std::vector(6); for (int i = 0; i < 6; i++) { @@ -3218,7 +3219,7 @@ void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes try { // TODO: need to store a flag about which buffer to use - bufferAdapter_ = new BufferAdapter(false, sizeMB); + bufferAdapter_ = new BufferAdapter(useV2Buffer_, sizeMB); } catch (std::bad_alloc& ex) { diff --git a/MMCore/MMCore.h b/MMCore/MMCore.h index f31b8b412..9e1f63394 100644 --- a/MMCore/MMCore.h +++ b/MMCore/MMCore.h @@ -89,7 +89,6 @@ class CPluginManager; -class CircularBuffer; class ConfigGroupCollection; class CoreCallback; class CorePropertyCollection; @@ -673,6 +672,7 @@ class CMMCore MMThreadLock* pPostedErrorsLock_; mutable std::deque > postedErrors_; + bool useV2Buffer_; // Whether to use the V2 buffer implementation private: void InitializeErrorMessages(); diff --git a/MMCore/MMCore.vcxproj b/MMCore/MMCore.vcxproj index ebfdcb9ba..cefe0ccd2 100644 --- a/MMCore/MMCore.vcxproj +++ b/MMCore/MMCore.vcxproj @@ -75,6 +75,8 @@ + + @@ -113,6 +115,8 @@ + + @@ -181,4 +185,4 @@ - + \ No newline at end of file diff --git a/MMCore/MMCore.vcxproj.filters b/MMCore/MMCore.vcxproj.filters index 1920583b6..77ca63b99 100644 --- a/MMCore/MMCore.vcxproj.filters +++ b/MMCore/MMCore.vcxproj.filters @@ -141,6 +141,12 @@ Source Files + + Source Files + + + Source Files + @@ -305,5 +311,11 @@ Header Files + + Header Files + + + Header Files + - + \ No newline at end of file From ebbc1bda43bc92028252912f91af12a0d330e928 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Sat, 1 Feb 2025 14:02:10 -0800 Subject: [PATCH 3/9] finished concurrency for buffer slots --- MMCore/BufferAdapter.cpp | 14 +-- MMCore/BufferAdapter.h | 4 + MMCore/Buffer_v2.cpp | 230 +++++++++++++++++++++++++++++++++------ MMCore/Buffer_v2.h | 161 +++++++++++++++++++++------ MMCore/MMCore.cpp | 2 +- 5 files changed, 335 insertions(+), 76 deletions(-) diff --git a/MMCore/BufferAdapter.cpp b/MMCore/BufferAdapter.cpp index 185c04036..aec92a2d8 100644 --- a/MMCore/BufferAdapter.cpp +++ b/MMCore/BufferAdapter.cpp @@ -8,17 +8,16 @@ #define DEVICE_ERR -1 #endif +const char* const BufferAdapter::DEFAULT_V2_BUFFER_NAME = "DEFAULT_BUFFER"; + BufferAdapter::BufferAdapter(bool useV2Buffer, unsigned int memorySizeMB) : useV2_(useV2Buffer), circBuffer_(nullptr), v2Buffer_(nullptr) { if (useV2_) { - // Create a new v2 buffer with a total size of memorySizeMB megabytes. - // Multiply by (1 << 20) to convert megabytes to bytes. - size_t bytes = memorySizeMB * (1 << 20); - v2Buffer_ = new DataBuffer(bytes, "DEFAULT"); + // Create a new v2 buffer directly with MB + v2Buffer_ = new DataBuffer(memorySizeMB, DEFAULT_V2_BUFFER_NAME); } else { circBuffer_ = new CircularBuffer(memorySizeMB); - // Optionally, perform any necessary initialization for the circular buffer. } } @@ -108,7 +107,7 @@ bool BufferAdapter::Initialize(unsigned numChannels, unsigned width, unsigned he unsigned BufferAdapter::GetMemorySizeMB() const { if (useV2_) { - return 0; // TODO: need to implement this + return v2Buffer_->GetMemorySizeMB(); } else { return circBuffer_->GetMemorySizeMB(); } @@ -126,8 +125,7 @@ long BufferAdapter::GetRemainingImageCount() const void BufferAdapter::Clear() { if (useV2_) { - // In this basic implementation, we call ReleaseBuffer with the known buffer name. - v2Buffer_->ReleaseBuffer("DEFAULT"); + v2Buffer_->ReleaseBuffer(DEFAULT_V2_BUFFER_NAME); } else { circBuffer_->Clear(); } diff --git a/MMCore/BufferAdapter.h b/MMCore/BufferAdapter.h index f768e5b8f..e0359dedd 100644 --- a/MMCore/BufferAdapter.h +++ b/MMCore/BufferAdapter.h @@ -9,6 +9,8 @@ // used by MMCore. It currently supports only a minimal set of functions. class BufferAdapter { public: + static const char* const DEFAULT_V2_BUFFER_NAME; + /** * Constructor. * @param useV2Buffer Set to true to use the new DataBuffer (v2); false to use CircularBuffer. @@ -151,6 +153,8 @@ class BufferAdapter { bool useV2_; // if true use DataBuffer, otherwise use CircularBuffer. CircularBuffer* circBuffer_; DataBuffer* v2Buffer_; + + }; #endif // BUFFERADAPTER_H \ No newline at end of file diff --git a/MMCore/Buffer_v2.cpp b/MMCore/Buffer_v2.cpp index e4ef33750..afe938ad9 100644 --- a/MMCore/Buffer_v2.cpp +++ b/MMCore/Buffer_v2.cpp @@ -6,7 +6,7 @@ // DESCRIPTION: Generic implementation of a buffer for storing image data and // metadata. Provides thread-safe access for reading and writing // with configurable overflow behavior. -// +//// // COPYRIGHT: Henry Pinkard, 2025 // // LICENSE: This file is distributed under the "Lesser GPL" (LGPL) license. @@ -57,10 +57,15 @@ Metadata Handling: #include "Buffer_v2.h" #include +#include // for std::this_thread::yield if needed -DataBuffer::DataBuffer(size_t numBytes, const char* name) { - // Initialize basic buffer with given size - AllocateBuffer(numBytes, name); +/////////////////////////////////////////////////////////////////////////////// +// DataBuffer Implementation +/////////////////////////////////////////////////////////////////////////////// + +DataBuffer::DataBuffer(unsigned int memorySizeMB, const std::string& name) { + // Convert MB to bytes for internal allocation + AllocateBuffer(memorySizeMB, name); } DataBuffer::~DataBuffer() { @@ -70,15 +75,16 @@ DataBuffer::~DataBuffer() { /** * Allocate a character buffer - * @param numBytes The size of the buffer to allocate. + * @param memorySizeMB The size (in MB) of the buffer to allocate. * @param name The name of the buffer. * @return Error code (0 on success). */ -int DataBuffer::AllocateBuffer(size_t numBytes, const char* name) { +int DataBuffer::AllocateBuffer(unsigned int memorySizeMB, const std::string& name) { + // Convert MB to bytes (1 MB = 1048576 bytes) + size_t numBytes = static_cast(memorySizeMB) * (1ULL << 20); buffer_ = new char[numBytes]; bufferSize_ = numBytes; bufferName_ = name; - // TODO: Store the buffer in a member variable for later use return DEVICE_OK; } @@ -87,44 +93,46 @@ int DataBuffer::AllocateBuffer(size_t numBytes, const char* name) { * @param name The name of the buffer to release. * @return Error code (0 on success, error if buffer not found or already released). */ -int DataBuffer::ReleaseBuffer(const char* name) { +int DataBuffer::ReleaseBuffer(const std::string& name) { if (buffer_ != nullptr && bufferName_ == name) { delete[] buffer_; buffer_ = nullptr; bufferSize_ = 0; - bufferName_ = nullptr; + bufferName_.clear(); return DEVICE_OK; } - // TODO: throw errors if other code holds pointers on stuff - return DEVICE_ERR; // Return an error if the buffer is not found or already released + // TODO: Handle errors if other parts of the system still hold pointers. + return DEVICE_ERR; } /** * @brief Copy data into the next available slot in the buffer. * * Returns the size of the copied data through dataSize. - * Implementing code should check the device type of the caller, and ensure that + * TODO: Implementing code should check the device type of the caller, and ensure that * all required metadata for interpreting its image data is there. * Note: this can be implemented in terms of Get/Release slot + memcopy. * * @param caller The device calling this function. - * @param data The data to be copied into the buffer. - * @param dataSize The size of the data to be copied. - * @param serializedMetadata The serialized metadata associated with the data. + * @param data The data to be copied. + * @param dataSize Size of the data to copy. + * @param serializedMetadata The associated metadata. * @return Error code (0 on success). */ -int DataBuffer::InsertData(const MM::Device *caller, const void* data, size_t dataSize, const char* serializedMetadata) { - // Basic implementation - just copy data - // TODO: Add proper buffer management and metadata handling - return 0; +int DataBuffer::InsertData(const MM::Device *caller, const void* data, + size_t dataSize, const std::string& serializedMetadata) { + // TODO: Create a slot, copy the data into it, then release write access on the slot. + // Also, ensure that a slot is not garbage-collected while data remains available. + return DEVICE_OK; } + /** * Check if a new slot has been fully written in this buffer * @return true if new data is ready, false otherwise */ bool DataBuffer::IsNewDataReady() { - // Basic implementation + // TODO: Implement checking logic based on the slot state. return false; } @@ -138,9 +146,9 @@ bool DataBuffer::IsNewDataReady() { */ int DataBuffer::CopyNextDataAndMetadata(void* dataDestination, size_t* dataSize, Metadata &md, bool waitForData) { - // Basic implementation - // TODO: Add proper data copying and metadata handling - return 0; + // Basic implementation: + // TODO: Use slot management to return data from the next available slot. + return DEVICE_OK; } /** @@ -158,8 +166,8 @@ int DataBuffer::CopyNextDataAndMetadata(void* dataDestination, * @return Error code (0 on success) */ int DataBuffer::SetOverwriteData(bool overwrite) { - // Basic implementation - return 0; + overwriteWhenFull_ = overwrite; + return DEVICE_OK; } /** @@ -174,9 +182,15 @@ int DataBuffer::SetOverwriteData(bool overwrite) { * @param serializedMetadata The serialized metadata associated with the data. * @return Error code (0 on success). */ -int DataBuffer::GetWritingSlot(const MM::Device *caller, void** slot, size_t slotSize, const char* serializedMetadata) { - // Basic implementation - return 0; +int DataBuffer::GetWritingSlot(const MM::Device *caller, void** slot, + size_t slotSize, const std::string& serializedMetadata) { + // TODO: For now, we simply return a pointer into the buffer; a full implementation + // would create and manage a BufferSlot and assign it with exclusive write access. + if (slot == nullptr || slotSize > bufferSize_) + return DEVICE_ERR; + // TODO: understnad this pointer stuff + *slot = static_cast(buffer_); + return DEVICE_OK; } /** @@ -187,8 +201,8 @@ int DataBuffer::GetWritingSlot(const MM::Device *caller, void** slot, size_t slo * @return Error code (0 on success). */ int DataBuffer::ReleaseWritingSlot(const MM::Device *caller, void* buffer) { - // Basic implementation - return 0; + // TODO + return DEVICE_OK; } /** @@ -200,7 +214,157 @@ int DataBuffer::ReleaseWritingSlot(const MM::Device *caller, void* buffer) { * @return Error code (0 on success). */ int DataBuffer::CreateCameraRequiredMetadata(Metadata** md, int width, int height, int bitDepth) { - // Basic implementation - // TODO: Create metadata with required camera fields - return 0; + // TODO: Implement camera-specific metadata creation. + return DEVICE_OK; +} + +unsigned int DataBuffer::GetMemorySizeMB() const { + // Convert bytes to MB (1 MB = 1048576 bytes) + return static_cast(bufferSize_ >> 20); +} + +/////////////////////////////////////////////////////////////////////////////// +// BufferSlot Implementation +/////////////////////////////////////////////////////////////////////////////// + +/** + * Constructor. + * Initializes the slot with the specified starting byte offset and length. + * Also initializes atomic variables that track reader and writer access. + */ +BufferSlot::BufferSlot(std::size_t start, std::size_t length) + : start_(start), length_(length), + readAccessCountAtomicInt_(0), + writeAtomicBool_(false) +{ + // No readers are active and the write lock is free upon construction. +} + +/** + * Destructor. + * Currently no dynamic memory is used inside BufferSlot, so nothing needs to be cleaned up. + */ +BufferSlot::~BufferSlot() { + // No explicit cleanup required here. +} + +/** + * Returns the start offset (in bytes) of the slot within the main buffer. + */ +std::size_t BufferSlot::GetStart() const { + return start_; +} + +/** + * Returns the length (in bytes) of the slot. + */ +std::size_t BufferSlot::GetLength() const { + return length_; +} + +/** + * Sets a detail for this slot using the provided key and value. + * Typically used to store metadata information (e.g. width, height). + */ +void BufferSlot::SetDetail(const std::string &key, std::size_t value) { + details_[key] = value; +} + +/** + * Retrieves a previously set detail. + * Returns 0 if the key is not found. + */ +std::size_t BufferSlot::GetDetail(const std::string &key) const { + auto it = details_.find(key); + return (it != details_.end()) ? it->second : 0; +} + +/** + * Clears all additional details associated with this slot. + */ +void BufferSlot::ClearDetails() { + details_.clear(); +} + +/** + * Attempts to acquire exclusive write access. + * This method first attempts to set the write flag atomically. + * If it fails, that indicates another writer holds the lock. + * Next, it attempts to confirm that no readers are active. + * If there are active readers, it reverts the write flag and returns false. + */ +bool BufferSlot::AcquireWriteAccess() { + bool expected = false; + // Attempt to atomically set the write flag. + if (!writeAtomicBool_.compare_exchange_strong(expected, true, std::memory_order_acquire)) { + // A writer is already active. + return false; + } + // Ensure no readers are active by checking the read counter. + int expectedReaders = 0; + if (!readAccessCountAtomicInt_.compare_exchange_strong(expectedReaders, 0, std::memory_order_acquire)) { + // Active readers are present; revert the write lock. + writeAtomicBool_.store(false, std::memory_order_release); + return false; + } + // Exclusive write access has been acquired. + return true; +} + +/** + * Releases exclusive write access. + * The writer flag is cleared, and waiting readers are notified so that + * they may acquire shared read access once the write is complete. + */ +void BufferSlot::ReleaseWriteAccess() { + // Publish all writes by releasing the writer flag. + writeAtomicBool_.store(false, std::memory_order_release); + // Notify waiting readers (using the condition variable) + // that the slot is now available for read access. + std::lock_guard lock(writeCompleteConditionMutex_); + writeCompleteCondition_.notify_all(); +} + +/** + * Acquires shared read access. + * This is a blocking operation – if a writer is active, + * the calling thread will wait until the writer releases its lock. + * Once unlocked, the method increments the reader count. + */ +bool BufferSlot::AcquireReadAccess() { + // Acquire the mutex associated with the condition variable. + std::unique_lock lock(writeCompleteConditionMutex_); + // Block until no writer is active. + writeCompleteCondition_.wait(lock, [this]() { + return !writeAtomicBool_.load(std::memory_order_acquire); + }); + // Now that there is no writer, increment the reader counter. + readAccessCountAtomicInt_.fetch_add(1, std::memory_order_acquire); + return true; +} + +/** + * Releases shared read access. + * The reader count is decremented using release semantics to ensure that all + * prior read operations complete before the decrement is visible to other threads. + */ +void BufferSlot::ReleaseReadAccess() { + readAccessCountAtomicInt_.fetch_sub(1, std::memory_order_release); +} + +/** + * Checks if the slot is available for acquiring write access. + * A slot is available for writing if there are no active readers and no writer. + */ +bool BufferSlot::IsAvailableForWriting() const { + return (readAccessCountAtomicInt_.load(std::memory_order_acquire) == 0) && + (!writeAtomicBool_.load(std::memory_order_acquire)); +} + +/** + * Checks if the slot is available for acquiring read access. + * A slot is available for reading if no writer currently holds the lock. + */ +bool BufferSlot::IsAvailableForReading() const { + return !writeAtomicBool_.load(std::memory_order_acquire); } diff --git a/MMCore/Buffer_v2.h b/MMCore/Buffer_v2.h index cca62f1e2..69684f3c4 100644 --- a/MMCore/Buffer_v2.h +++ b/MMCore/Buffer_v2.h @@ -6,7 +6,13 @@ // DESCRIPTION: Generic implementation of a buffer for storing image data and // metadata. Provides thread-safe access for reading and writing // with configurable overflow behavior. -// +// +// The buffer is organized into slots (BufferSlot objects), each of which +// supports exclusive write access and shared read access. Read access is +// delivered using const pointers and is counted via an atomic counter, while +// write access requires acquiring an exclusive lock. This ensures that once a +// read pointer is given out it cannot be misused for writing. +// // COPYRIGHT: Henry Pinkard, 2025 // // LICENSE: This file is distributed under the "Lesser GPL" (LGPL) license. @@ -21,7 +27,7 @@ // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. // // AUTHOR: Henry Pinkard, 01/31/2025 - +/////////////////////////////////////////////////////////////////////////////// #pragma once @@ -29,23 +35,108 @@ #include "../MMDevice/MMDevice.h" #include #include +#include +#include +#include +#include +#include + +/** + * BufferSlot represents a contiguous slot in the DataBuffer that holds image + * data and metadata. It manages exclusive (write) and shared (read) access + * using atomics, a mutex, and a condition variable. + */ +class BufferSlot { +public: + // Constructor: Initializes the slot with the given start offset and length. + BufferSlot(std::size_t start, std::size_t length); + // Destructor. + ~BufferSlot(); + + // Returns the start offset (in bytes) of the slot. + std::size_t GetStart() const; + // Returns the length (in bytes) of the slot. + std::size_t GetLength() const; + + // Stores a detail (e.g., width, height) associated with the slot. + void SetDetail(const std::string &key, std::size_t value); + // Retrieves a stored detail; returns 0 if the key is not found. + std::size_t GetDetail(const std::string &key) const; + // Clears all stored details. + void ClearDetails(); + + // --- Methods for synchronizing access --- + + /** + * Try to acquire exclusive write access. + * Returns true on success, false if the slot is already locked for writing + * or if active readers exist. + */ + bool AcquireWriteAccess(); + /** + * Release exclusive write access. + * Clears the write flag and notifies waiting readers. + */ + void ReleaseWriteAccess(); + /** + * Acquire shared read access by blocking until no writer is active. + * Once the waiting condition is met, the reader count is incremented. + * Returns true when read access has been acquired. + */ + bool AcquireReadAccess(); + /** + * Release shared read access. + * Decrements the reader count using release semantics. + */ + void ReleaseReadAccess(); + + /** + * Return true if the slot is available for acquiring write access (i.e., + * no active writer or reader). + */ + bool IsAvailableForWriting() const; + /** + * Return true if the slot is available for acquiring read access + * (no active writer). + */ + bool IsAvailableForReading() const; + +private: + // Basic slot information. + std::size_t start_; // Byte offset within the buffer. + std::size_t length_; // Length of the slot in bytes. + std::map details_; // Additional details (e.g., image dimensions). + + // Synchronization primitives. + std::atomic readAccessCountAtomicInt_; // Count of active readers. + std::atomic writeAtomicBool_; // True if the slot is locked for writing. + mutable std::mutex writeCompleteConditionMutex_; // Mutex for condition variable. + mutable std::condition_variable writeCompleteCondition_; // Condition variable for blocking readers. +}; -class DataBuffer { - public: - DataBuffer(size_t numBytes, const char* name); - ~DataBuffer(); - - /////// Buffer Allocation and Destruction +/** + * DataBuffer manages a large contiguous memory area, divided into BufferSlot + * objects for storing image data and metadata. It supports two data access + * patterns: copy-based access and direct pointer access via retrieval of slots. + */ +class DataBuffer { +public: + DataBuffer(unsigned int memorySizeMB, const std::string& name); + ~DataBuffer(); - // C version - int AllocateBuffer(size_t numBytes, const char* name); - //// Is there a need for a name? - //// Maybe makes sense for Core to assing a unique integer - int ReleaseBuffer(const char* name); + // Buffer Allocation and Destruction + int AllocateBuffer(unsigned int memorySizeMB, const std::string& name); + int ReleaseBuffer(const std::string& name); // TODO: Other versions for allocating buffers Java, Python + + /** + * Get the total memory size of the buffer in megabytes + * @return Size of the buffer in MB + */ + unsigned int GetMemorySizeMB() const; /////// Monitoring the Buffer /////// int GetAvailableBytes(void* buffer, size_t* availableBytes); @@ -120,7 +211,7 @@ class DataBuffer { ////// Writing Data into buffer ////// /** - * @brief Copy data into the next available slot in the buffer. + * Copy data into the next available slot in the buffer. * * Returns the size of the copied data through dataSize. * Implementing code should check the device type of the caller, and ensure that @@ -133,10 +224,11 @@ class DataBuffer { * @param serializedMetadata The serialized metadata associated with the data. * @return Error code (0 on success). */ - int InsertData(const MM::Device *caller, const void* data, size_t dataSize, const char* serializedMetadata); + int InsertData(const MM::Device *caller, const void* data, size_t dataSize, + const std::string& serializedMetadata); /** - * @brief Get a pointer to the next available data slot in the buffer for writing. + * Get a pointer to the next available data slot in the buffer for writing. * * The caller must release the slot using ReleaseDataSlot after writing is complete. * Internally this will use a std::unique_ptr. @@ -147,16 +239,16 @@ class DataBuffer { * @param serializedMetadata The serialized metadata associated with the data. * @return Error code (0 on success). */ - int GetWritingSlot(const MM::Device *caller, void** slot, size_t slotSize, const char* serializedMetadata); + int GetWritingSlot(const MM::Device *caller, void** slot, size_t slotSize, + const std::string& serializedMetadata); - /** - * @brief Release a data slot after writing is complete. - * - * @param caller The device calling this function. - * @param buffer The buffer to be released. - * @return Error code (0 on success). - */ - int ReleaseWritingSlot(const MM::Device *caller, void* buffer); + /** + * Release a data slot after writing is complete. + * @param caller The device calling this function. + * @param buffer The slot to be released. + * @return Error code (0 on success). + */ + int ReleaseWritingSlot(const MM::Device *caller, void* buffer); @@ -164,19 +256,20 @@ class DataBuffer { ////// Camera API ////// // Set the buffer for a camera to write into - int SetCameraBuffer(const char* camera, void* buffer); + int SetCameraBuffer(const std::string& camera, void* buffer); // Get a pointer to a heap allocated Metadata object with the required fields filled in int CreateCameraRequiredMetadata(Metadata**, int width, int height, int bitDepth); - private: - // Basic buffer management - char* buffer_; - size_t bufferSize_; - std::string bufferName_; - - // Configuration - bool overwriteWhenFull_; +private: + // Basic buffer management. + char* buffer_; + size_t bufferSize_; + std::string bufferName_; + // Configuration. + bool overwriteWhenFull_; + // List of active buffer slots. Each slot manages its own read/write access. + std::vector activeSlots_; }; diff --git a/MMCore/MMCore.cpp b/MMCore/MMCore.cpp index b52e4d234..e3867acf5 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -141,7 +141,7 @@ CMMCore::CMMCore() : pluginManager_(new CPluginManager()), deviceManager_(new mm::DeviceManager()), pPostedErrorsLock_(NULL), - useV2Buffer_(false) + useV2Buffer_(true) { configGroups_ = new ConfigGroupCollection(); pixelSizeGroup_ = new PixelSizeConfigGroup(); From 94125ee9e4d7dbd201ec3b7ae344ef601050a37d Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Tue, 4 Feb 2025 13:29:28 -0800 Subject: [PATCH 4/9] most functionality in v2 buffer implemented --- MMCore/BufferAdapter.cpp | 284 ++++++++++----- MMCore/BufferAdapter.h | 25 +- MMCore/Buffer_v2.cpp | 709 +++++++++++++++++++++++++++++--------- MMCore/Buffer_v2.h | 462 ++++++++++++++++--------- MMCore/CircularBuffer.cpp | 114 +----- MMCore/CircularBuffer.h | 6 +- MMCore/MMCore.cpp | 40 +-- 7 files changed, 1072 insertions(+), 568 deletions(-) diff --git a/MMCore/BufferAdapter.cpp b/MMCore/BufferAdapter.cpp index aec92a2d8..2bbcdb306 100644 --- a/MMCore/BufferAdapter.cpp +++ b/MMCore/BufferAdapter.cpp @@ -1,4 +1,5 @@ #include "BufferAdapter.h" +#include // For demonstration, we assume DEVICE_OK and DEVICE_ERR macros are defined in MMCore.h or an included error header. #ifndef DEVICE_OK @@ -8,14 +9,40 @@ #define DEVICE_ERR -1 #endif -const char* const BufferAdapter::DEFAULT_V2_BUFFER_NAME = "DEFAULT_BUFFER"; + +static std::string FormatLocalTime(std::chrono::time_point tp) { + using namespace std::chrono; + auto us = duration_cast(tp.time_since_epoch()); + auto secs = duration_cast(us); + auto whole = duration_cast(secs); + auto frac = static_cast((us - whole).count()); + + // As of C++14/17, it is simpler (and probably faster) to use C functions for + // date-time formatting + + std::time_t t(secs.count()); // time_t is seconds on platforms we support + std::tm *ptm; +#ifdef _WIN32 // Windows localtime() is documented thread-safe + ptm = std::localtime(&t); +#else // POSIX has localtime_r() + std::tm tmstruct; + ptm = localtime_r(&t, &tmstruct); +#endif + + // Format as "yyyy-mm-dd hh:mm:ss.uuuuuu" (26 chars) + const char *timeFmt = "%Y-%m-%d %H:%M:%S"; + char buf[32]; + std::size_t len = std::strftime(buf, sizeof(buf), timeFmt, ptm); + std::snprintf(buf + len, sizeof(buf) - len, ".%06d", frac); + return buf; +} + BufferAdapter::BufferAdapter(bool useV2Buffer, unsigned int memorySizeMB) : useV2_(useV2Buffer), circBuffer_(nullptr), v2Buffer_(nullptr) { if (useV2_) { - // Create a new v2 buffer directly with MB - v2Buffer_ = new DataBuffer(memorySizeMB, DEFAULT_V2_BUFFER_NAME); + v2Buffer_ = new DataBuffer(memorySizeMB); } else { circBuffer_ = new CircularBuffer(memorySizeMB); } @@ -34,71 +61,44 @@ BufferAdapter::~BufferAdapter() } } -bool BufferAdapter::InsertImage(const unsigned char* buf, - unsigned width, unsigned height, unsigned byteDepth, Metadata* pMd) { - if (useV2_) { - // Implement logic for v2Buffer if available - return false; // Placeholder - } else { - return circBuffer_->InsertImage(buf, width, height, byteDepth, pMd); - } -} - -bool BufferAdapter::InsertImage(const unsigned char *buf, unsigned width, unsigned height, - unsigned byteDepth, unsigned nComponents, Metadata *pMd) { - if (useV2_) { - // Implement logic for v2Buffer if available - return false; // Placeholder - } else { - return circBuffer_->InsertImage(buf, width, height, byteDepth, nComponents, pMd); - } -} - -const unsigned char* BufferAdapter::GetTopImage() const +const unsigned char* BufferAdapter::GetLastImage() const { if (useV2_) { - // Minimal support: the v2Buffer currently does not expose GetTopImage. - return nullptr; + Metadata dummyMetadata; + return v2Buffer_->PeekDataReadPointerAtIndex(0, nullptr, dummyMetadata); + // TODO: ensure calling code releases the slot after use } else { return circBuffer_->GetTopImage(); } -} -const unsigned char* BufferAdapter::GetNextImage() -{ - if (useV2_) { - // Minimal support: return nullptr since v2Buffer does not provide next image retrieval. - return nullptr; - } else { - return circBuffer_->GetNextImage(); - } } -const mm::ImgBuffer* BufferAdapter::GetNthFromTopImageBuffer(unsigned long n) const +const unsigned char* BufferAdapter::PopNextImage() { if (useV2_) { - // Implement logic for v2Buffer if available - return nullptr; // Placeholder + Metadata dummyMetadata; + return v2Buffer_->PopNextDataReadPointer(nullptr, dummyMetadata, false); + // TODO: ensure calling code releases the slot after use } else { - return circBuffer_->GetNthFromTopImageBuffer(n); - } -} - -const mm::ImgBuffer* BufferAdapter::GetNextImageBuffer(unsigned channel) -{ - if (useV2_) { - // Implement logic for v2Buffer if available - return nullptr; // Placeholder - } else { - return circBuffer_->GetNextImageBuffer(channel); + return circBuffer_->PopNextImage(); } } bool BufferAdapter::Initialize(unsigned numChannels, unsigned width, unsigned height, unsigned bytesPerPixel) { + startTime_ = std::chrono::steady_clock::now(); // Initialize start time + imageNumbers_.clear(); if (useV2_) { - // Implement initialization logic for v2Buffer - return true; // Placeholder + try { + // Reinitialize the v2Buffer using its current allocated memory size. + int ret = v2Buffer_->ReinitializeBuffer(v2Buffer_->GetMemorySizeMB()); + if (ret != DEVICE_OK) + return false; + } catch (const std::exception& ex) { + // Optionally log the exception + return false; + } + return true; } else { return circBuffer_->Initialize(numChannels, width, height, bytesPerPixel); } @@ -116,7 +116,7 @@ unsigned BufferAdapter::GetMemorySizeMB() const long BufferAdapter::GetRemainingImageCount() const { if (useV2_) { - return 0; // TODO: need to implement this + return v2Buffer_->GetRemainingImageCount(); } else { return circBuffer_->GetRemainingImageCount(); } @@ -125,68 +125,192 @@ long BufferAdapter::GetRemainingImageCount() const void BufferAdapter::Clear() { if (useV2_) { - v2Buffer_->ReleaseBuffer(DEFAULT_V2_BUFFER_NAME); + v2Buffer_->ReleaseBuffer(); } else { circBuffer_->Clear(); } + // Reset image counters when buffer is cleared + imageNumbers_.clear(); } -bool BufferAdapter::InsertMultiChannel(const unsigned char *buf, unsigned numChannels, unsigned width, - unsigned height, unsigned byteDepth, Metadata *pMd) { +long BufferAdapter::GetSize(long imageSize) const +{ if (useV2_) { - // Implement logic for v2Buffer if available - return false; // Placeholder + return v2Buffer_->GetMemorySizeMB() * 1024 * 1024 / imageSize; } else { - return circBuffer_->InsertMultiChannel(buf, numChannels, width, height, byteDepth, pMd); + return circBuffer_->GetSize(); } + } -bool BufferAdapter::InsertMultiChannel(const unsigned char *buf, unsigned numChannels, unsigned width, - unsigned height, unsigned byteDepth, unsigned nComponents, Metadata *pMd) { +long BufferAdapter::GetFreeSize(long imageSize) const +{ if (useV2_) { - // Implement logic for v2Buffer if available - return false; // Placeholder + return v2Buffer_->GetFreeMemory() / imageSize; } else { - return circBuffer_->InsertMultiChannel(buf, numChannels, width, height, byteDepth, nComponents, pMd); + return circBuffer_->GetFreeSize(); } } -long BufferAdapter::GetSize() const +bool BufferAdapter::Overflow() const { if (useV2_) { - // Implement logic for v2Buffer if available - return 0; // Placeholder + return v2Buffer_->Overflow(); } else { - return circBuffer_->GetSize(); + return circBuffer_->Overflow(); } } -long BufferAdapter::GetFreeSize() const +void BufferAdapter::ProcessMetadata(Metadata& md, unsigned width, unsigned height, + unsigned byteDepth, unsigned nComponents) { + // Track image numbers per camera + { + std::lock_guard lock(imageNumbersMutex_); + std::string cameraName = md.GetSingleTag(MM::g_Keyword_Metadata_CameraLabel).GetValue(); + if (imageNumbers_.end() == imageNumbers_.find(cameraName)) + { + imageNumbers_[cameraName] = 0; + } + + // insert image number + md.put(MM::g_Keyword_Metadata_ImageNumber, CDeviceUtils::ConvertToString(imageNumbers_[cameraName])); + ++imageNumbers_[cameraName]; + } + + if (!md.HasTag(MM::g_Keyword_Elapsed_Time_ms)) + { + // if time tag was not supplied by the camera insert current timestamp + using namespace std::chrono; + auto elapsed = steady_clock::now() - startTime_; + md.PutImageTag(MM::g_Keyword_Elapsed_Time_ms, + std::to_string(duration_cast(elapsed).count())); + } + + // Note: It is not ideal to use local time. I think this tag is rarely + // used. Consider replacing with UTC (micro)seconds-since-epoch (with + // different tag key) after addressing current usage. + auto now = std::chrono::system_clock::now(); + md.PutImageTag(MM::g_Keyword_Metadata_TimeInCore, FormatLocalTime(now)); + + md.PutImageTag(MM::g_Keyword_Metadata_Width, width); + md.PutImageTag(MM::g_Keyword_Metadata_Height, height); + if (byteDepth == 1) + md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_GRAY8); + else if (byteDepth == 2) + md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_GRAY16); + else if (byteDepth == 4) + { + if (nComponents == 1) + md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_GRAY32); + else + md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_RGB32); + } + else if (byteDepth == 8) + md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_RGB64); + else + md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_Unknown); +} + +bool BufferAdapter::InsertImage(const unsigned char* buf, + unsigned width, unsigned height, unsigned byteDepth, Metadata* pMd) { + return InsertMultiChannel(buf, 1, width, height, byteDepth, 1, pMd); +} + +bool BufferAdapter::InsertImage(const unsigned char *buf, unsigned width, unsigned height, + unsigned byteDepth, unsigned nComponents, Metadata *pMd) { + return InsertMultiChannel(buf, 1, width, height, byteDepth, nComponents, pMd); +} + + +bool BufferAdapter::InsertMultiChannel(const unsigned char *buf, unsigned numChannels, unsigned width, + unsigned height, unsigned byteDepth, Metadata *pMd) { + return InsertMultiChannel(buf, numChannels, width, height, byteDepth, 1, pMd); +} + +bool BufferAdapter::InsertMultiChannel(const unsigned char* buf, unsigned numChannels, + unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents, Metadata* pMd) { + + // Initialize metadata with either provided metadata or create empty + Metadata md = (pMd != nullptr) ? *pMd : Metadata(); + + // Process common metadata + ProcessMetadata(md, width, height, byteDepth, nComponents); + + if (useV2_) { + // All the data needed to interpret the image is in the metadata + // This function will copy data and metadata into the buffer + v2Buffer_->InsertData(buf, width * height * byteDepth *numChannels, &md); + } else { + return circBuffer_->InsertMultiChannel(buf, numChannels, width, height, + byteDepth, &md); + } +} + +void* BufferAdapter::GetLastImageMD(unsigned channel, Metadata& md) const throw (CMMError) { if (useV2_) { - // Implement logic for v2Buffer if available - return 0; // Placeholder + // In v2, we use PeekNextDataReadPointer (which does not advance the internal pointer) + // Note: the v2 buffer is not channel aware, so the 'channel' parameter is ignored. + // TODO implement the channel aware version + void* slotPtr = nullptr; + size_t dataSize = 0; + int ret = v2Buffer_->PeekNextDataReadPointer(&slotPtr, &dataSize, md); + if (ret != DEVICE_OK || slotPtr == nullptr) + throw CMMError("V2 buffer is empty.", MMERR_CircularBufferEmpty); + return slotPtr; + // TODO: make sure calling code releases the slot after use } else { - return circBuffer_->GetFreeSize(); + const mm::ImgBuffer* pBuf = circBuffer_->GetTopImageBuffer(channel); + if (pBuf != nullptr) { + md = pBuf->GetMetadata(); + return const_cast(pBuf->GetPixels()); + } else { + throw CMMError("Circular buffer is empty.", MMERR_CircularBufferEmpty); + } } } -bool BufferAdapter::Overflow() const +void* BufferAdapter::GetNthImageMD(unsigned long n, Metadata& md) const throw (CMMError) { if (useV2_) { - // Implement logic for v2Buffer if available - return false; // Placeholder + size_t dataSize = 0; + const unsigned char* slotPtr = v2Buffer_->PeekDataReadPointerAtIndex(n, &dataSize, md); + if (slotPtr == nullptr) + throw CMMError("V2 buffer does not contain enough data.", MMERR_CircularBufferEmpty); + // Return a non-const pointer (caller must be careful with the const_cast) + return const_cast(slotPtr); + // TODO: make sure calling code releases the slot after use } else { - return circBuffer_->Overflow(); + const mm::ImgBuffer* pBuf = circBuffer_->GetNthFromTopImageBuffer(n); + if (pBuf != nullptr) { + md = pBuf->GetMetadata(); + return const_cast(pBuf->GetPixels()); + } else { + throw CMMError("Circular buffer is empty.", MMERR_CircularBufferEmpty); + } } } -const mm::ImgBuffer* BufferAdapter::GetTopImageBuffer(unsigned channel) const +void* BufferAdapter::PopNextImageMD(unsigned channel, Metadata& md) throw (CMMError) { if (useV2_) { - // Implement logic for v2Buffer if available - return nullptr; // Placeholder + // For v2, consume the data by calling PopNextDataReadPointer, + // which returns a const unsigned char* or throws an exception on error. + // The caller is expected to call ReleaseDataReadPointer on the returned pointer once done. + // TODO: make channel aware + size_t dataSize = 0; + const unsigned char* slotPtr = v2Buffer_->PopNextDataReadPointer(&dataSize, md, false); + if (slotPtr == nullptr) + throw CMMError("V2 buffer is empty.", MMERR_CircularBufferEmpty); + return const_cast(slotPtr); + // TODO: ensure that calling code releases the read pointer after use. } else { - return circBuffer_->GetTopImageBuffer(channel); + const mm::ImgBuffer* pBuf = circBuffer_->GetNextImageBuffer(channel); + if (pBuf != nullptr) { + md = pBuf->GetMetadata(); + return const_cast(pBuf->GetPixels()); + } else { + throw CMMError("Circular buffer is empty.", MMERR_CircularBufferEmpty); + } } -} \ No newline at end of file +} diff --git a/MMCore/BufferAdapter.h b/MMCore/BufferAdapter.h index e0359dedd..ca3627430 100644 --- a/MMCore/BufferAdapter.h +++ b/MMCore/BufferAdapter.h @@ -4,6 +4,9 @@ #include "CircularBuffer.h" #include "Buffer_v2.h" #include "../MMDevice/MMDevice.h" +#include +#include +#include // BufferAdapter provides a common interface for buffer operations // used by MMCore. It currently supports only a minimal set of functions. @@ -23,13 +26,13 @@ class BufferAdapter { * Get a pointer to the top (most recent) image. * @return Pointer to image data, or nullptr if unavailable. */ - const unsigned char* GetTopImage() const; + const unsigned char* GetLastImage() const; /** * Get a pointer to the next image from the buffer. * @return Pointer to image data, or nullptr if unavailable. */ - const unsigned char* GetNextImage(); + const unsigned char* PopNextImage(); /** * Get a pointer to the nth image from the top of the buffer. @@ -128,13 +131,14 @@ class BufferAdapter { * Get the total capacity of the buffer. * @return Total capacity of the buffer. */ - long GetSize() const; + long GetSize(long imageSize) const; /** * Get the free capacity of the buffer. - * @return Free capacity of the buffer. + * @param imageSize Size of a single image in bytes. + * @return Number of images that can be added without overflowing. */ - long GetFreeSize() const; + long GetFreeSize(long imageSize) const; /** * Check if the buffer is overflowed. @@ -149,12 +153,21 @@ class BufferAdapter { */ const mm::ImgBuffer* GetTopImageBuffer(unsigned channel) const; + void* GetLastImageMD(unsigned channel, Metadata& md) const throw (CMMError); + void* GetNthImageMD(unsigned long n, Metadata& md) const throw (CMMError); + void* PopNextImageMD(unsigned channel, Metadata& md) throw (CMMError); + private: bool useV2_; // if true use DataBuffer, otherwise use CircularBuffer. CircularBuffer* circBuffer_; DataBuffer* v2Buffer_; - + std::chrono::steady_clock::time_point startTime_; + std::map imageNumbers_; // Track image numbers per camera + std::mutex imageNumbersMutex_; // Mutex to protect access to imageNumbers_ + + void ProcessMetadata(Metadata& md, unsigned width, unsigned height, + unsigned byteDepth, unsigned nComponents); }; #endif // BUFFERADAPTER_H \ No newline at end of file diff --git a/MMCore/Buffer_v2.cpp b/MMCore/Buffer_v2.cpp index afe938ad9..69cf38cef 100644 --- a/MMCore/Buffer_v2.cpp +++ b/MMCore/Buffer_v2.cpp @@ -45,7 +45,7 @@ Data Access: 2. Direct pointer access with explicit release - Reference counting ensures safe memory management - Slots become available for recycling when: - - Writing is complete (via Insert or GetWritingSlot+Release) + - Writing is complete (via Insert or GetDataWriteSlot+Release) - All readers have released their references Metadata Handling: @@ -58,170 +58,14 @@ Metadata Handling: #include "Buffer_v2.h" #include #include // for std::this_thread::yield if needed +#include +#include +#include +#include +#include -/////////////////////////////////////////////////////////////////////////////// -// DataBuffer Implementation -/////////////////////////////////////////////////////////////////////////////// - -DataBuffer::DataBuffer(unsigned int memorySizeMB, const std::string& name) { - // Convert MB to bytes for internal allocation - AllocateBuffer(memorySizeMB, name); -} - -DataBuffer::~DataBuffer() { - // Cleanup - delete[] buffer_; -} - -/** - * Allocate a character buffer - * @param memorySizeMB The size (in MB) of the buffer to allocate. - * @param name The name of the buffer. - * @return Error code (0 on success). - */ -int DataBuffer::AllocateBuffer(unsigned int memorySizeMB, const std::string& name) { - // Convert MB to bytes (1 MB = 1048576 bytes) - size_t numBytes = static_cast(memorySizeMB) * (1ULL << 20); - buffer_ = new char[numBytes]; - bufferSize_ = numBytes; - bufferName_ = name; - return DEVICE_OK; -} - -/** - * Release the buffer if it matches the given name. - * @param name The name of the buffer to release. - * @return Error code (0 on success, error if buffer not found or already released). - */ -int DataBuffer::ReleaseBuffer(const std::string& name) { - if (buffer_ != nullptr && bufferName_ == name) { - delete[] buffer_; - buffer_ = nullptr; - bufferSize_ = 0; - bufferName_.clear(); - return DEVICE_OK; - } - // TODO: Handle errors if other parts of the system still hold pointers. - return DEVICE_ERR; -} - -/** - * @brief Copy data into the next available slot in the buffer. - * - * Returns the size of the copied data through dataSize. - * TODO: Implementing code should check the device type of the caller, and ensure that - * all required metadata for interpreting its image data is there. - * Note: this can be implemented in terms of Get/Release slot + memcopy. - * - * @param caller The device calling this function. - * @param data The data to be copied. - * @param dataSize Size of the data to copy. - * @param serializedMetadata The associated metadata. - * @return Error code (0 on success). - */ -int DataBuffer::InsertData(const MM::Device *caller, const void* data, - size_t dataSize, const std::string& serializedMetadata) { - // TODO: Create a slot, copy the data into it, then release write access on the slot. - // Also, ensure that a slot is not garbage-collected while data remains available. - return DEVICE_OK; -} -/** - * Check if a new slot has been fully written in this buffer - * @return true if new data is ready, false otherwise - */ -bool DataBuffer::IsNewDataReady() { - // TODO: Implement checking logic based on the slot state. - return false; -} - -/** - * Copy the next available data and metadata from the buffer - * @param dataDestination Destination buffer to copy data into - * @param dataSize Returns the size of the copied data, or 0 if no data available - * @param md Metadata object to populate - * @param waitForData If true, block until data becomes available - * @return Error code (0 on success) - */ -int DataBuffer::CopyNextDataAndMetadata(void* dataDestination, - size_t* dataSize, Metadata &md, bool waitForData) { - // Basic implementation: - // TODO: Use slot management to return data from the next available slot. - return DEVICE_OK; -} - -/** - * Configure whether to overwrite old data when buffer is full. - * - * If true, when there are no more slots available for writing because - * images haven't been read fast enough, then automatically recycle the - * oldest slot(s) in the buffer as needed in order to make space for new images. - * This is suitable for situations when its okay to drop frames, like live - * view when data is not being saved. - * - * If false, then throw an exception if the buffer becomes full. - * - * @param overwrite Whether to enable overwriting of old data - * @return Error code (0 on success) - */ -int DataBuffer::SetOverwriteData(bool overwrite) { - overwriteWhenFull_ = overwrite; - return DEVICE_OK; -} - -/** - * @brief Get a pointer to the next available data slot in the buffer for writing. - * - * The caller must release the slot using ReleaseDataSlot after writing is complete. - * Internally this will use a std::unique_ptr. - * - * @param caller The device calling this function. - * @param slot Pointer to the slot where data will be written. - * @param slotSize The size of the slot. - * @param serializedMetadata The serialized metadata associated with the data. - * @return Error code (0 on success). - */ -int DataBuffer::GetWritingSlot(const MM::Device *caller, void** slot, - size_t slotSize, const std::string& serializedMetadata) { - // TODO: For now, we simply return a pointer into the buffer; a full implementation - // would create and manage a BufferSlot and assign it with exclusive write access. - if (slot == nullptr || slotSize > bufferSize_) - return DEVICE_ERR; - // TODO: understnad this pointer stuff - *slot = static_cast(buffer_); - return DEVICE_OK; -} - -/** - * @brief Release a data slot after writing is complete. - * - * @param caller The device calling this function. - * @param buffer The buffer to be released. - * @return Error code (0 on success). - */ -int DataBuffer::ReleaseWritingSlot(const MM::Device *caller, void* buffer) { - // TODO - return DEVICE_OK; -} - -/** - * Get a pointer to a heap allocated Metadata object with the required fields filled in - * @param md Pointer to the Metadata object to be created. - * @param width The width of the image. - * @param height The height of the image. - * @param bitDepth The bit depth of the image. - * @return Error code (0 on success). - */ -int DataBuffer::CreateCameraRequiredMetadata(Metadata** md, int width, int height, int bitDepth) { - // TODO: Implement camera-specific metadata creation. - return DEVICE_OK; -} - -unsigned int DataBuffer::GetMemorySizeMB() const { - // Convert bytes to MB (1 MB = 1048576 bytes) - return static_cast(bufferSize_ >> 20); -} /////////////////////////////////////////////////////////////////////////////// // BufferSlot Implementation @@ -235,21 +79,17 @@ unsigned int DataBuffer::GetMemorySizeMB() const { BufferSlot::BufferSlot(std::size_t start, std::size_t length) : start_(start), length_(length), readAccessCountAtomicInt_(0), - writeAtomicBool_(false) + writeAtomicBool_(true) // The slot is created with write access held by default. { - // No readers are active and the write lock is free upon construction. + // No readers are active and the slot starts with write access. } -/** - * Destructor. - * Currently no dynamic memory is used inside BufferSlot, so nothing needs to be cleaned up. - */ BufferSlot::~BufferSlot() { // No explicit cleanup required here. } /** - * Returns the start offset (in bytes) of the slot within the main buffer. + * Returns the start offset (in bytes) of the slot from the start of the buffer. */ std::size_t BufferSlot::GetStart() const { return start_; @@ -348,8 +188,11 @@ bool BufferSlot::AcquireReadAccess() { * The reader count is decremented using release semantics to ensure that all * prior read operations complete before the decrement is visible to other threads. */ -void BufferSlot::ReleaseReadAccess() { - readAccessCountAtomicInt_.fetch_sub(1, std::memory_order_release); +bool BufferSlot::ReleaseReadAccess() { + // fetch_sub returns the previous value. If that value was 1, + // then this call decrements the active reader count to zero. + int prevCount = readAccessCountAtomicInt_.fetch_sub(1, std::memory_order_release); + return (prevCount == 1); } /** @@ -368,3 +211,527 @@ bool BufferSlot::IsAvailableForWriting() const { bool BufferSlot::IsAvailableForReading() const { return !writeAtomicBool_.load(std::memory_order_acquire); } + + + +/////////////////////////////////////////////////////////////////////////////// +// DataBuffer Implementation +/////////////////////////////////////////////////////////////////////////////// + +DataBuffer::DataBuffer(unsigned int memorySizeMB) + : buffer_(nullptr), + bufferSize_(0), + overwriteWhenFull_(false), + nextAllocOffset_(0), + currentSlotIndex_(0), + overflow_(false) +{ + AllocateBuffer(memorySizeMB); +} + +DataBuffer::~DataBuffer() { + delete[] buffer_; +} + +/** + * Allocate a character buffer + * @param memorySizeMB The size (in MB) of the buffer to allocate. + * @return Error code (0 on success). + */ +int DataBuffer::AllocateBuffer(unsigned int memorySizeMB) { + // Convert MB to bytes (1 MB = 1048576 bytes) + size_t numBytes = static_cast(memorySizeMB) * (1ULL << 20); + buffer_ = new char[numBytes]; + bufferSize_ = numBytes; + overflow_ = false; + return DEVICE_OK; +} + +/** + * Release the buffer. + * @return Error code (0 on success, error if buffer not found or already released). + */ +int DataBuffer::ReleaseBuffer() { + if (buffer_ != nullptr) { + delete[] buffer_; + buffer_ = nullptr; + bufferSize_ = 0; + return DEVICE_OK; + } + // TODO: Handle errors if other parts of the system still hold pointers. + return DEVICE_ERR; +} + +/** + * Copy data into the next available slot in the buffer. + * + * Returns the size of the copied data through dataSize. + * all required metadata for interpreting its image data is there. + * Note: this can be implemented in terms of Get/Release slot + memcopy. + * + * @param caller The device calling this function. + * @param data The data to be copied. + * @param dataSize Size of the data to copy. + * @param serializedMetadata The associated metadata. + * @return Error code (0 on success). + */ +int DataBuffer::InsertData(const void* data, + size_t dataSize, const Metadata* pMd) { + // Get a write slot of the required size + void* slotPointer = nullptr; + int result = GetDataWriteSlot(dataSize, &slotPointer); + if (result != DEVICE_OK) + return result; + + // Copy the data into the slot + std::memcpy(slotPointer, data, dataSize); + if (pMd) { + // md.Serialize().c_str() + // TODO: Need a metadata lock and perhaps a map from buffer offset to metadata? + } + + // Release the write slot + return ReleaseDataWriteSlot(&slotPointer); +} + +/** + * Copy the next available data and metadata from the buffer + * @param dataDestination Destination buffer to copy data into + * @param dataSize Returns the size of the copied data, or 0 if no data available + * @param md Metadata object to populate + * @param waitForData If true, block until data becomes available + * @return Error code (0 on success) + */ +int DataBuffer::CopyNextDataAndMetadata(void* dataDestination, + size_t* dataSize, Metadata &md, bool waitForData) { + void* sourcePtr = nullptr; + int result = PopNextDataReadPointer(&sourcePtr, dataSize, md, waitForData); + if (result != DEVICE_OK) + return result; + + // Copy the data from the slot into the user's destination buffer + std::memcpy(dataDestination, sourcePtr, *dataSize); + + // Release the read pointer (this will handle cleanup and index management) + return ReleaseDataReadPointer(&sourcePtr); +} + +/** + * Configure whether to overwrite old data when buffer is full. + * + * If true, when there are no more slots available for writing because + * images haven't been read fast enough, then automatically recycle the + * oldest slot(s) in the buffer as needed in order to make space for new images. + * This is suitable for situations when its okay to drop frames, like live + * view when data is not being saved. + * + * If false, then throw an exception if the buffer becomes full. + * + * @param overwrite Whether to enable overwriting of old data + * @return Error code (0 on success) + */ +int DataBuffer::SetOverwriteData(bool overwrite) { + overwriteWhenFull_ = overwrite; + return DEVICE_OK; +} + + +/** + * Get a pointer to the next available data slot in the buffer for writing. + * + * The caller must release the slot using ReleaseDataSlot after writing is complete. + */ +int DataBuffer::GetDataWriteSlot(size_t slotSize, void** slotPointer) { + // AllocateNextSlot allocates a slot for writing new data of variable size. + // + // First, it checks if there is a recently released slot start (from releasedSlots_). + // For each candidate, it uses the activeSlotMap_ mechanism to + // verify that the candidate start yields a gap large enough to allocate slotSize bytes. + // This is done so to prefer recently released slots, in order to get performance + // boosts from reusing recently freed memory. + // + // If no released slot fits, then it falls back to using nextAllocOffset_ and similar + // collision checks. In overwrite mode, wrap-around is supported. + + // Ensure that only one thread can allocate a slot at a time. + std::lock_guard lock(slotManagementMutex_); + + // First, try using a released slot candidate (FILO order) + for (int i = static_cast(releasedSlots_.size()) - 1; i >= 0; i--) { + size_t candidateStart = releasedSlots_[i]; + size_t localCandidate = candidateStart; + + // Find first slot at or after our candidate position + auto nextIt = activeSlotsByStart_.lower_bound(localCandidate); + + // If there's a previous slot, ensure we don't overlap with it + if (nextIt != activeSlotsByStart_.begin()) { + size_t prevSlotEnd = std::prev(nextIt)->first + std::prev(nextIt)->second->GetLength(); + // If our candidate region [candidateStart, candidateStart+slotSize) overlaps the previous slot, + // bump candidateStart to the end of the conflicting slot and try again. + if (prevSlotEnd > localCandidate) { + localCandidate = prevSlotEnd; + } + } + + // Check if there's space before the next active slot + nextIt = activeSlotsByStart_.lower_bound(localCandidate); + bool candidateValid = true; + + if (nextIt != activeSlotsByStart_.end()) { + // Case 1: There is a next slot + // Check if our proposed slot would overlap with the next slot + candidateValid = (localCandidate + slotSize <= nextIt->first); + } else if (localCandidate + slotSize > bufferSize_) { + // Case 2: No next slot, but we'd exceed buffer size + if (!overwriteWhenFull_) { + candidateValid = false; + } else { + // Try wrapping around to start of buffer + localCandidate = 0; + nextIt = activeSlotsByStart_.lower_bound(localCandidate); + + // If there are any slots, ensure we don't overlap with the last one + if (nextIt != activeSlotsByStart_.begin()) { + auto prevIt = std::prev(nextIt); + size_t prevSlotEnd = prevIt->first + prevIt->second->GetLength(); + if (prevSlotEnd > localCandidate) { + localCandidate = prevSlotEnd; + } + } + + // Check if wrapped position would overlap with first slot + if (nextIt != activeSlotsByStart_.end()) { + candidateValid = (localCandidate + slotSize <= nextIt->first); + } + } + } + + if (candidateValid) { + // Remove the candidate from releasedSlots_ (it was taken from the "back" if available). + releasedSlots_.erase(releasedSlots_.begin() + i); + activeSlotsVector_.push_back(BufferSlot(localCandidate, slotSize)); + BufferSlot* slot = &activeSlotsVector_.back(); + activeSlotsByStart_[localCandidate] = slot; + *slotPointer = buffer_ + slot->GetStart(); + return DEVICE_OK; + } + } + // If no released candidate fits, use nextAllocOffset_ fallback. + size_t candidateStart = nextAllocOffset_; + if (candidateStart + slotSize > bufferSize_) { + // Not enough space in the buffer: if we are not allowed to overwrite then set our overflow flag. + if (!overwriteWhenFull_) { + overflow_ = true; // <-- mark that overflow has happened + *slotPointer = nullptr; + return DEVICE_ERR; + } + candidateStart = 0; // Reset to start of buffer + + // Since we're starting at position 0, remove any slots that start before our requested size + auto it = activeSlotsByStart_.begin(); + while (it != activeSlotsByStart_.end() && it->first < slotSize) { + BufferSlot* slot = it->second; + if (!slot->IsAvailableForWriting() || !slot->IsAvailableForReading()) { + throw std::runtime_error("Cannot overwrite slot that is currently being accessed (has active readers or writers)"); + } + + // Remove from both tracking structures + activeSlotsVector_.erase( + std::remove_if(activeSlotsVector_.begin(), activeSlotsVector_.end(), + [targetStart = it->first](const BufferSlot& slot) { + return slot.GetStart() == targetStart; + }), + activeSlotsVector_.end()); + it = activeSlotsByStart_.erase(it); + } + } + + // Create and track the new slot + activeSlotsVector_.push_back(BufferSlot(candidateStart, slotSize)); + BufferSlot* newSlot = &activeSlotsVector_.back(); + activeSlotsByStart_[candidateStart] = newSlot; + + // Update nextAllocOffset_ for next allocation + nextAllocOffset_ = candidateStart + slotSize; + if (nextAllocOffset_ >= bufferSize_) { + nextAllocOffset_ = 0; + } + + *slotPointer = buffer_ + newSlot->GetStart(); + return DEVICE_OK; +} + +/** + * @brief Release a data slot after writing is complete. + * + * @param caller The device calling this function. + * @param buffer The buffer to be released. + * @return Error code (0 on success). + */ +int DataBuffer::ReleaseDataWriteSlot(void** slotPointer) { + if (slotPointer == nullptr || *slotPointer == nullptr) + return DEVICE_ERR; + + std::lock_guard lock(slotManagementMutex_); + + // Calculate the offset from the buffer start to find the corresponding slot + char* ptr = static_cast(*slotPointer); + size_t offset = ptr - buffer_; + + // Find the slot in activeSlotsByStart_ + auto it = activeSlotsByStart_.find(offset); + if (it == activeSlotsByStart_.end()) { + return DEVICE_ERR; // Slot not found + } + + // Release the write access + BufferSlot* slot = it->second; + slot->ReleaseWriteAccess(); + + // Clear the pointer + *slotPointer = nullptr; + + // Notify any waiting readers that new data is available + dataCV_.notify_all(); + + return DEVICE_OK; +} + + +/** + * ReleaseSlot is called after a slot's content has been fully read. + * It assumes the caller has already released its read access (the slot is free). + * + * This implementation pushes only the start of the released slot onto the FILO + * (releasedSlots_) and removes the slot from the active slot map and activeSlots_. + */ +int DataBuffer::ReleaseDataReadPointer(void** slotPointer) { + if (slotPointer == nullptr || *slotPointer == nullptr) + return DEVICE_ERR; + + std::unique_lock lock(slotManagementMutex_); + + // Compute the slot's start offset. + char* ptr = static_cast(*slotPointer); + size_t offset = ptr - buffer_; + + // Find the slot in activeSlotMap_. + auto it = activeSlotsByStart_.find(offset); + if (it != activeSlotsByStart_.end()) { + BufferSlot* slot = it->second; + // Check if the slot is being accessed by any readers or writers. + if (!slot->IsAvailableForWriting() || !slot->IsAvailableForReading()) { + // TODO: right way to handle exceptions? + throw std::runtime_error("Cannot release slot that is currently being accessed"); + } + + // If we've reached max size, remove the oldest element (front of vector). + if (releasedSlots_.size() >= MAX_RELEASED_SLOTS) { + releasedSlots_.erase(releasedSlots_.begin()); + } + releasedSlots_.push_back(offset); + + // Remove slot from active structures. + activeSlotsByStart_.erase(it); + for (auto vecIt = activeSlotsVector_.begin(); vecIt != activeSlotsVector_.end(); ++vecIt) { + if (vecIt->GetStart() == offset) { + // Determine the index being removed. + size_t indexDeleted = std::distance(activeSlotsVector_.begin(), vecIt); + activeSlotsVector_.erase(vecIt); + // Adjust currentSlotIndex_: + // If the deleted slot was before the current index, decrement it. + if (currentSlotIndex_ > indexDeleted) + currentSlotIndex_--; + + break; + } + } + } + *slotPointer = nullptr; + return DEVICE_OK; +} + +const unsigned char* DataBuffer::PopNextDataReadPointer(size_t* dataSize, Metadata &md, bool waitForData) { + while (true) { + std::unique_lock lock(slotManagementMutex_); + + // If data is available, process it. + if (!activeSlotsVector_.empty() && currentSlotIndex_ < activeSlotsVector_.size()) { + BufferSlot& currentSlot = activeSlotsVector_[currentSlotIndex_]; + + if (!currentSlot.AcquireReadAccess()) { + throw std::runtime_error("Failed to acquire read access for the current slot."); + } + + const unsigned char* slotPointer = reinterpret_cast(buffer_ + currentSlot.GetStart()); + if (dataSize != nullptr) { + *dataSize = currentSlot.GetLength(); + } + + currentSlotIndex_++; + return slotPointer; + } + + // No data available. + if (!waitForData) { + throw std::runtime_error("No data available to read."); + } + + // Wait for notification of new data. + dataCV_.wait(lock); + // When we wake up, the while loop will check again if data is available + } +} + +unsigned int DataBuffer::GetMemorySizeMB() const { + // Convert bytes to MB (1 MB = 1048576 bytes) + return static_cast(bufferSize_ >> 20); +} + +int DataBuffer::PeekNextDataReadPointer(void** slotPointer, size_t* dataSize, + Metadata &md) { + // Immediately check if there is an unread slot without waiting. + std::unique_lock lock(slotManagementMutex_); + if (activeSlotsVector_.empty() || currentSlotIndex_ >= activeSlotsVector_.size()) { + return DEVICE_ERR; // No unread data available. + } + + // Obtain the next available slot *without* advancing currentSlotIndex_. + BufferSlot& currentSlot = activeSlotsVector_[currentSlotIndex_]; + if (!currentSlot.AcquireReadAccess()) + return DEVICE_ERR; + + *slotPointer = buffer_ + currentSlot.GetStart(); + *dataSize = currentSlot.GetLength(); + // (If metadata is stored per slot, populate md here.) + return DEVICE_OK; +} + +const unsigned char* DataBuffer::PeekDataReadPointerAtIndex(size_t n, size_t* dataSize, Metadata &md) { + + std::unique_lock lock(slotManagementMutex_); + if (activeSlotsVector_.empty() || (currentSlotIndex_ + n) >= activeSlotsVector_.size()) { + throw std::runtime_error("Not enough unread data available."); + } + + BufferSlot& slot = activeSlotsVector_[currentSlotIndex_ + n]; + if (!slot.AcquireReadAccess()) + throw std::runtime_error("Failed to acquire read access for the selected slot."); + + // Assign the size from the slot. + if (dataSize != nullptr) { + *dataSize = slot.GetLength(); + } + + // Return a constant pointer to the data. + return reinterpret_cast(buffer_ + slot.GetStart()); +} + +/** + * Releases the read access that was acquired by a peek. + * This is similar to ReleaseDataReadPointer except that it does not + * remove the slot from the active list. This should be used when the + * overwriteWhenFull_ flag is true and the caller wants to release the + * peeked slot for reuse. + */ + +int DataBuffer::ReleasePeekDataReadPointer(void** slotPointer) { + if (slotPointer == nullptr || *slotPointer == nullptr) + return DEVICE_ERR; + + + std::lock_guard lock(slotManagementMutex_); + char* ptr = static_cast(*slotPointer); + size_t offset = ptr - buffer_; + + // Look up the corresponding slot by its buffer offset. + auto it = activeSlotsByStart_.find(offset); + if (it == activeSlotsByStart_.end()) + return DEVICE_ERR; // Slot not found + + BufferSlot* slot = it->second; + // Release the read access (this does NOT remove the slot from the active list) + slot->ReleaseReadAccess(); + + *slotPointer = nullptr; + return DEVICE_OK; +} + +size_t DataBuffer::GetOccupiedSlotCount() const { + std::lock_guard lock(slotManagementMutex_); + return activeSlotsVector_.size(); +} + +size_t DataBuffer::GetOccupiedMemory() const { + std::lock_guard lock(slotManagementMutex_); + size_t usedMemory = 0; + for (const auto& slot : activeSlotsVector_) { + usedMemory += slot.GetLength(); + } + return usedMemory; +} + +size_t DataBuffer::GetFreeMemory() const { + std::lock_guard lock(slotManagementMutex_); + // Free memory is the total buffer size minus the sum of all occupied memory. + size_t usedMemory = 0; + for (const auto& slot : activeSlotsVector_) { + usedMemory += slot.GetLength(); + } + return (bufferSize_ > usedMemory) ? (bufferSize_ - usedMemory) : 0; +} + +bool DataBuffer::Overflow() const { + std::lock_guard lock(slotManagementMutex_); + return overflow_; +} + +/** + * Reinitialize the DataBuffer by clearing all internal data structures, + * releasing the current buffer, and reallocating a new one. + * This method uses the existing slotManagementMutex_ to ensure thread-safety. + * + * @param memorySizeMB New size (in MB) for the buffer. + * @return DEVICE_OK on success. + * @throws std::runtime_error if any slot is still actively being read or written. + */ +int DataBuffer::ReinitializeBuffer(unsigned int memorySizeMB) { + std::lock_guard lock(slotManagementMutex_); + + // Check that there are no outstanding readers or writers. + for (const BufferSlot &slot : activeSlotsVector_) { + if (!slot.IsAvailableForReading() || !slot.IsAvailableForWriting()) { + throw std::runtime_error("Cannot reinitialize DataBuffer: outstanding active slot detected."); + } + } + + // Clear internal data structures. + activeSlotsVector_.clear(); + activeSlotsByStart_.clear(); + releasedSlots_.clear(); + currentSlotIndex_ = 0; + nextAllocOffset_ = 0; + overflow_ = false; + + // Release the old buffer. + if (buffer_ != nullptr) { + delete[] buffer_; + buffer_ = nullptr; + bufferSize_ = 0; + } + + // Allocate a new buffer using the provided memory size. + AllocateBuffer(memorySizeMB); + + return DEVICE_OK; +} + +long DataBuffer::GetRemainingImageCount() const { + std::lock_guard lock(slotManagementMutex_); + // Return the number of unread slots. + // currentSlotIndex_ tracks the next slot to read, + // so unread count is the total slots minus this index. + return (activeSlotsVector_.size() > currentSlotIndex_) ? + static_cast(activeSlotsVector_.size() - currentSlotIndex_) : 0; +} diff --git a/MMCore/Buffer_v2.h b/MMCore/Buffer_v2.h index 69684f3c4..b24bea5ef 100644 --- a/MMCore/Buffer_v2.h +++ b/MMCore/Buffer_v2.h @@ -40,236 +40,354 @@ #include #include #include +#include /** * BufferSlot represents a contiguous slot in the DataBuffer that holds image - * data and metadata. It manages exclusive (write) and shared (read) access + * data and metadata. It supports exclusive write access with shared read access, * using atomics, a mutex, and a condition variable. */ class BufferSlot { public: - // Constructor: Initializes the slot with the given start offset and length. + /** + * Constructor. + * Initializes the slot with the specified starting byte offset and length. + * Also initializes atomic variables that track reader and writer access. + * + * @param start The starting offset (in bytes) within the buffer. + * @param length The length (in bytes) of the slot. + */ BufferSlot(std::size_t start, std::size_t length); - // Destructor. + + /** + * Destructor. + */ ~BufferSlot(); - // Returns the start offset (in bytes) of the slot. + /** + * Returns the starting offset (in bytes) of the slot in the buffer. + * + * @return The slot's start offset. + */ std::size_t GetStart() const; - // Returns the length (in bytes) of the slot. + + /** + * Returns the length (in bytes) of the slot. + * + * @return The slot's length. + */ std::size_t GetLength() const; - // Stores a detail (e.g., width, height) associated with the slot. + /** + * Stores a detail (for example, image width or height) associated with the slot. + * + * @param key The name of the detail. + * @param value The value of the detail. + */ void SetDetail(const std::string &key, std::size_t value); - // Retrieves a stored detail; returns 0 if the key is not found. + + /** + * Retrieves a previously stored detail. + * Returns 0 if the key is not found. + * + * @param key The detail key. + * @return The stored value or 0 if not found. + */ std::size_t GetDetail(const std::string &key) const; - // Clears all stored details. - void ClearDetails(); - // --- Methods for synchronizing access --- + /** + * Clears all stored details from the slot. + */ + void ClearDetails(); /** - * Try to acquire exclusive write access. - * Returns true on success, false if the slot is already locked for writing - * or if active readers exist. + * Attempts to acquire exclusive write access. + * It first tries to atomically set the write flag. + * If another writer is active or if there are active readers, + * the write lock is not acquired. + * + * @return True if the slot is locked for writing; false otherwise. */ bool AcquireWriteAccess(); + /** - * Release exclusive write access. + * Releases exclusive write access. * Clears the write flag and notifies waiting readers. */ void ReleaseWriteAccess(); + /** - * Acquire shared read access by blocking until no writer is active. - * Once the waiting condition is met, the reader count is incremented. - * Returns true when read access has been acquired. + * Acquires shared read access. + * This is a blocking operation — if a writer is active, the caller waits + * until the writer releases its lock. Once available, the reader count is incremented. + * + * @return True when read access has been successfully acquired. */ bool AcquireReadAccess(); + /** - * Release shared read access. - * Decrements the reader count using release semantics. + * Releases shared read access. + * Decrements the reader count. + * + * @return True if this call released the last active reader; false otherwise. */ - void ReleaseReadAccess(); + bool ReleaseReadAccess(); /** - * Return true if the slot is available for acquiring write access (i.e., - * no active writer or reader). + * Checks if the slot is currently available for writing. + * A slot is available if there are no active readers and no active writer. + * + * @return True if available for writing, false otherwise. */ bool IsAvailableForWriting() const; + /** - * Return true if the slot is available for acquiring read access - * (no active writer). + * Checks if the slot is available for acquiring read access. + * A slot is available for reading if no writer presently holds the lock. + * + * @return True if available for reading. */ bool IsAvailableForReading() const; private: - // Basic slot information. - std::size_t start_; // Byte offset within the buffer. - std::size_t length_; // Length of the slot in bytes. - std::map details_; // Additional details (e.g., image dimensions). - - // Synchronization primitives. - std::atomic readAccessCountAtomicInt_; // Count of active readers. - std::atomic writeAtomicBool_; // True if the slot is locked for writing. - mutable std::mutex writeCompleteConditionMutex_; // Mutex for condition variable. - mutable std::condition_variable writeCompleteCondition_; // Condition variable for blocking readers. + std::size_t start_; + std::size_t length_; + std::map details_; + std::atomic readAccessCountAtomicInt_; + std::atomic writeAtomicBool_; + mutable std::mutex writeCompleteConditionMutex_; + mutable std::condition_variable writeCompleteCondition_; }; /** - * DataBuffer manages a large contiguous memory area, divided into BufferSlot - * objects for storing image data and metadata. It supports two data access - * patterns: copy-based access and direct pointer access via retrieval of slots. + * DataBuffer manages a contiguous block of memory divided into BufferSlot objects + * for storing image data and metadata. It provides thread-safe access for both + * reading and writing operations and supports configurable overflow behavior. + * + * Two data access patterns are provided: + * 1. Copy-based access. + * 2. Direct pointer access with an explicit release. + * + * Reference counting is used to ensure that memory is managed safely. A slot + * is recycled when all references (readers and writers) have been released. */ class DataBuffer { public: - DataBuffer(unsigned int memorySizeMB, const std::string& name); + /** + * Maximum number of released slots to track. + */ + static const size_t MAX_RELEASED_SLOTS = 50; + + /** + * Constructor. + * Initializes the DataBuffer with a specified memory size in MB. + * + * @param memorySizeMB The size (in megabytes) of the buffer. + */ + DataBuffer(unsigned int memorySizeMB); + + /** + * Destructor. + */ ~DataBuffer(); - // Buffer Allocation and Destruction - int AllocateBuffer(unsigned int memorySizeMB, const std::string& name); - int ReleaseBuffer(const std::string& name); + /** + * Allocates a contiguous block of memory for the buffer. + * + * @param memorySizeMB The amount of memory (in MB) to allocate. + * @return DEVICE_OK on success. + */ + int AllocateBuffer(unsigned int memorySizeMB); + + /** + * Releases the allocated buffer. + * + * @return DEVICE_OK on success, or an error if the buffer is already released. + */ + int ReleaseBuffer(); + + /** + * Copies data into the next available slot in the buffer along with its metadata. + * The copy-based approach is implemented using a slot acquisition, memory copy, and then + * slot release. + * + * @param data Pointer to the data to be inserted. + * @param dataSize The size of data (in bytes) being inserted. + * @param pMd Pointer to the metadata associated with the data. + * @return DEVICE_OK on success. + */ + int InsertData(const void* data, size_t dataSize, const Metadata* pMd); + + /** + * Copies data and metadata from the next available slot in the buffer into the provided destination. + * + * @param dataDestination Destination buffer into which data will be copied. + * @param dataSize On success, returns the size of the copied data. + * @param md Metadata object to be populated with the data's metadata. + * @param waitForData If true, block until data becomes available. + * @return DEVICE_OK on success. + */ + int CopyNextDataAndMetadata(void* dataDestination, size_t* dataSize, Metadata &md, bool waitForData); + + /** + * Sets whether the buffer should overwrite old data when it is full. + * If true, the buffer will recycle the oldest slot when no free slot is available; + * if false, an error is returned when writing new data fails due to a full buffer. + * + * @param overwrite True to enable overwriting, false to disable. + * @return DEVICE_OK on success. + */ + int SetOverwriteData(bool overwrite); + + /** + * Acquires a pointer to a free slot in the buffer for writing purposes. + * The caller must later call ReleaseDataWriteSlot after finishing writing. + * + * @param slotSize The required size of the write slot. + * @param slotPointer On success, receives a pointer within the buffer where data can be written. + * @return DEVICE_OK on success. + */ + int GetDataWriteSlot(size_t slotSize, void** slotPointer); + + /** + * Releases the write slot after data writing is complete. + * This clears the write lock and notifies any waiting reader threads. + * + * @param slotPointer Pointer previously obtained from GetDataWriteSlot. + * @return DEVICE_OK on success. + */ + int ReleaseDataWriteSlot(void** slotPointer); + + /** + * Releases read access on a data slot after its contents have been completely read. + * This makes the slot available for recycling. + * + * @param slotPointer Pointer previously obtained from GetNextDataReadPointer. + * @return DEVICE_OK on success. + */ + int ReleaseDataReadPointer(void** slotPointer); + + /** + * Retrieves and removes (consumes) the next available data slot for reading. + * This method advances the internal reading index. + * + * @param dataSize On success, returns the size of the data. + * @param md Associated metadata for the data. + * @param waitForData If true, block until data becomes available. + * @return Pointer to the next available data in the buffer. + */ + const unsigned char* PopNextDataReadPointer(size_t* dataSize, Metadata &md, bool waitForData); + + + /** + * Peeks at the next unread data slot without consuming it. + * The slot remains available for subsequent acquisitions. + * + * @param slotPointer On success, receives the pointer to the data. + * @param dataSize On success, returns the size of the data. + * @param md Associated metadata for the data. + * @return DEVICE_OK on success, or an error code if no data is available. + */ + int PeekNextDataReadPointer(void** slotPointer, size_t* dataSize, Metadata &md); - // TODO: Other versions for allocating buffers Java, Python + /** + * Peeks at the nth unread data slot without consuming it. + * (n = 0 is equivalent to PeekNextDataReadPointer.) + * + * @param n The index of the unread slot. + * @param dataSize On success, returns the size of the data. + * @param md Associated metadata for the data. + * @return const pointer to the data. + */ + const unsigned char* PeekDataReadPointerAtIndex(size_t n, size_t* dataSize, Metadata &md); + + + /** + * Releases the read access that was acquired by a peek operation. + * This method releases the temporary read access without consuming the slot. + * + * @param slotPointer Pointer previously obtained from a peek method. + * @return DEVICE_OK on success. + */ + int ReleasePeekDataReadPointer(void** slotPointer); - /** - * Get the total memory size of the buffer in megabytes - * @return Size of the buffer in MB + * Returns the total memory size of the buffer in megabytes. + * + * @return The buffer size in MB. */ unsigned int GetMemorySizeMB() const; - /////// Monitoring the Buffer /////// - int GetAvailableBytes(void* buffer, size_t* availableBytes); - int GetNumSlotsUsed(void* buffer, size_t* numSlotsUsed); - - - - //// Configuration options - /** - * Configure whether to overwrite old data when buffer is full. - * - * If true, when there are no more slots available for writing because - * images haven't been read fast enough, then automatically recycle the - * oldest slot(s) in the buffer as needed in order to make space for new images. - * This is suitable for situations when its okay to drop frames, like live - * view when data is not being saved. - * - * If false, then throw an exception if the buffer becomes full. - * - * @param overwrite Whether to enable overwriting of old data - * @return Error code (0 on success) - */ - int SetOverwriteData(bool overwrite); - - - - /////// Getting Data Out /////// - - /** - * Check if a new slot has been fully written in this buffer - * @return true if new data is ready, false otherwise - */ - bool IsNewDataReady(); - - /** - * Copy the next available data and metadata from the buffer - * @param dataDestination Destination buffer to copy data into - * @param dataSize Returns the size of the copied data, or 0 if no data available - * @param md Metadata object to populate - * @param waitForData If true, block until data becomes available - * @return Error code (0 on success) - */ - int CopyNextDataAndMetadata(void* dataDestination, - size_t* dataSize, Metadata &md, bool waitForData); - - - /** - * Copy the next available metadata from the buffer - * Returns the size of the copied metadata through metadataSize, - * or 0 if no metadata is available - */ - int CopyNextMetadata(void* buffer, Metadata &md); - - /** - * Get a pointer to the next available data slot in the buffer - * The caller must release the slot using ReleaseNextDataAndMetadata - * If awaitReady is false and the data was inserted using GetWritingSlot, it - * is possible to read the data as it is being written (e.g. to monitor progress) - * of large or slow image being written - * - * Internally this will use a std::shared_ptr - */ - int GetNextSlotPointer(void** slotPointer, size_t* dataSize, - Metadata &md, bool awaitReady=true); - - /** - * Release the next data slot and its associated metadata - */ - int ReleaseNextSlot(void** slotPointer); - - - ////// Writing Data into buffer ////// - - /** - * Copy data into the next available slot in the buffer. - * - * Returns the size of the copied data through dataSize. - * Implementing code should check the device type of the caller, and ensure that - * all required metadata for interpreting its image data is there. - * Note: this can be implemented in terms of Get/Release slot + memcopy. - * - * @param caller The device calling this function. - * @param data The data to be copied into the buffer. - * @param dataSize The size of the data to be copied. - * @param serializedMetadata The serialized metadata associated with the data. - * @return Error code (0 on success). - */ - int InsertData(const MM::Device *caller, const void* data, size_t dataSize, - const std::string& serializedMetadata); - - /** - * Get a pointer to the next available data slot in the buffer for writing. - * - * The caller must release the slot using ReleaseDataSlot after writing is complete. - * Internally this will use a std::unique_ptr. - * - * @param caller The device calling this function. - * @param slot Pointer to the slot where data will be written. - * @param slotSize The size of the slot. - * @param serializedMetadata The serialized metadata associated with the data. - * @return Error code (0 on success). - */ - int GetWritingSlot(const MM::Device *caller, void** slot, size_t slotSize, - const std::string& serializedMetadata); - - /** - * Release a data slot after writing is complete. - * @param caller The device calling this function. - * @param buffer The slot to be released. - * @return Error code (0 on success). - */ - int ReleaseWritingSlot(const MM::Device *caller, void* buffer); - - - - - ////// Camera API ////// - - // Set the buffer for a camera to write into - int SetCameraBuffer(const std::string& camera, void* buffer); - - // Get a pointer to a heap allocated Metadata object with the required fields filled in - int CreateCameraRequiredMetadata(Metadata**, int width, int height, int bitDepth); + /** + * Returns the number of currently occupied slots in the buffer. + * + * @return The number of occupied slots. + */ + size_t GetOccupiedSlotCount() const; + + /** + * Returns the total occupied memory (in bytes) within the buffer. + * + * @return The sum of the lengths of all active slots. + */ + size_t GetOccupiedMemory() const; + + /** + * Returns the amount of free memory (in bytes) remaining in the buffer. + * + * @return The number of free bytes available for new data. + */ + size_t GetFreeMemory() const; + + /** + * Returns whether the buffer has been overflowed (i.e. an attempt to + * allocate a write slot failed because there was no available space). + */ + bool Overflow() const; + + /** + * Returns the number of unread data slots in the buffer. + * + * @return The number of unread data slots. + */ + long GetRemainingImageCount() const; + + /** + * Reinitialize the DataBuffer by clearing all internal data structures, + * releasing the current buffer, and reallocating a new one. + * This method uses the existing slotManagementMutex_ to ensure thread safety. + * + * @param memorySizeMB New size (in MB) for the buffer. + * @return DEVICE_OK on success. + * @throws std::runtime_error if any slot is still actively being read or written. + */ + int ReinitializeBuffer(unsigned int memorySizeMB); private: - // Basic buffer management. + // Pointer to the allocated buffer memory. char* buffer_; + // Total size (in bytes) of the allocated buffer. size_t bufferSize_; - std::string bufferName_; - // Configuration. + // Whether the buffer should overwrite older data when full. bool overwriteWhenFull_; - // List of active buffer slots. Each slot manages its own read/write access. - std::vector activeSlots_; + // New: overflow indicator (set to true if an insert fails because of buffer full) + bool overflow_; + + // Data structures for tracking active slot usage. + std::vector activeSlotsVector_; + std::map activeSlotsByStart_; + std::vector releasedSlots_; + + // The next available offset for a new data slot. + size_t nextAllocOffset_; + + // Tracks the current slot index for read operations. + size_t currentSlotIndex_; + + // Synchronization primitives for managing slot access. + std::condition_variable dataCV_; + mutable std::mutex slotManagementMutex_; }; diff --git a/MMCore/CircularBuffer.cpp b/MMCore/CircularBuffer.cpp index 2453bda49..c30a1b58e 100644 --- a/MMCore/CircularBuffer.cpp +++ b/MMCore/CircularBuffer.cpp @@ -72,7 +72,6 @@ CircularBuffer::~CircularBuffer() {} bool CircularBuffer::Initialize(unsigned channels, unsigned int w, unsigned int h, unsigned int pixDepth) { MMThreadGuard guard(g_bufferLock); - imageNumbers_.clear(); startTime_ = std::chrono::steady_clock::now(); bool ret = true; @@ -139,7 +138,6 @@ void CircularBuffer::Clear() saveIndex_=0; overflow_ = false; startTime_ = std::chrono::steady_clock::now(); - imageNumbers_.clear(); } unsigned long CircularBuffer::GetSize() const @@ -164,61 +162,12 @@ unsigned long CircularBuffer::GetRemainingImageCount() const return (unsigned long)(insertIndex_ - saveIndex_); } -static std::string FormatLocalTime(std::chrono::time_point tp) { - using namespace std::chrono; - auto us = duration_cast(tp.time_since_epoch()); - auto secs = duration_cast(us); - auto whole = duration_cast(secs); - auto frac = static_cast((us - whole).count()); - - // As of C++14/17, it is simpler (and probably faster) to use C functions for - // date-time formatting - - std::time_t t(secs.count()); // time_t is seconds on platforms we support - std::tm *ptm; -#ifdef _WIN32 // Windows localtime() is documented thread-safe - ptm = std::localtime(&t); -#else // POSIX has localtime_r() - std::tm tmstruct; - ptm = localtime_r(&t, &tmstruct); -#endif - - // Format as "yyyy-mm-dd hh:mm:ss.uuuuuu" (26 chars) - const char *timeFmt = "%Y-%m-%d %H:%M:%S"; - char buf[32]; - std::size_t len = std::strftime(buf, sizeof(buf), timeFmt, ptm); - std::snprintf(buf + len, sizeof(buf) - len, ".%06d", frac); - return buf; -} - -/** -* Inserts a single image in the buffer. -*/ -bool CircularBuffer::InsertImage(const unsigned char* pixArray, unsigned int width, unsigned int height, unsigned int byteDepth, const Metadata* pMd) throw (CMMError) -{ - return InsertMultiChannel(pixArray, 1, width, height, byteDepth, pMd); -} - -/** -* Inserts a single image, possibly with multiple channels, but with 1 component, in the buffer. -*/ -bool CircularBuffer::InsertMultiChannel(const unsigned char* pixArray, unsigned int numChannels, unsigned int width, unsigned int height, unsigned int byteDepth, const Metadata* pMd) throw (CMMError) -{ - return InsertMultiChannel(pixArray, numChannels, width, height, byteDepth, 1, pMd); -} - -/** -* Inserts a single image, possibly with multiple components, in the buffer. -*/ -bool CircularBuffer::InsertImage(const unsigned char* pixArray, unsigned int width, unsigned int height, unsigned int byteDepth, unsigned int nComponents, const Metadata* pMd) throw (CMMError) -{ - return InsertMultiChannel(pixArray, 1, width, height, byteDepth, nComponents, pMd); -} /** * Inserts a multi-channel frame in the buffer. */ -bool CircularBuffer::InsertMultiChannel(const unsigned char* pixArray, unsigned int numChannels, unsigned int width, unsigned int height, unsigned int byteDepth, unsigned int nComponents, const Metadata* pMd) throw (CMMError) +bool CircularBuffer::InsertMultiChannel(const unsigned char* pixArray, unsigned int numChannels, unsigned int width, unsigned int height, + unsigned int byteDepth, const Metadata* pMd) throw (CMMError) { MMThreadGuard insertGuard(g_insertLock); @@ -241,7 +190,6 @@ bool CircularBuffer::InsertMultiChannel(const unsigned char* pixArray, unsigned for (unsigned i=0; iSetMetadata(*pMd); } - - std::string cameraName = md.GetSingleTag(MM::g_Keyword_Metadata_CameraLabel).GetValue(); - if (imageNumbers_.end() == imageNumbers_.find(cameraName)) - { - imageNumbers_[cameraName] = 0; - } - - // insert image number. - md.put(MM::g_Keyword_Metadata_ImageNumber, CDeviceUtils::ConvertToString(imageNumbers_[cameraName])); - ++imageNumbers_[cameraName]; - } - - if (!md.HasTag(MM::g_Keyword_Elapsed_Time_ms)) - { - // if time tag was not supplied by the camera insert current timestamp - using namespace std::chrono; - auto elapsed = steady_clock::now() - startTime_; - md.PutImageTag(MM::g_Keyword_Elapsed_Time_ms, - std::to_string(duration_cast(elapsed).count())); - } - - // Note: It is not ideal to use local time. I think this tag is rarely - // used. Consider replacing with UTC (micro)seconds-since-epoch (with - // different tag key) after addressing current usage. - auto now = std::chrono::system_clock::now(); - md.PutImageTag(MM::g_Keyword_Metadata_TimeInCore, FormatLocalTime(now)); - - md.PutImageTag(MM::g_Keyword_Metadata_Width, width); - md.PutImageTag(MM::g_Keyword_Metadata_Height, height); - if (byteDepth == 1) - md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_GRAY8); - else if (byteDepth == 2) - md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_GRAY16); - else if (byteDepth == 4) - { - if (nComponents == 1) - md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_GRAY32); - else - md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_RGB32); - } - else if (byteDepth == 8) - md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_RGB64); - else - md.PutImageTag(MM::g_Keyword_PixelType, MM::g_Keyword_PixelType_Unknown); - - pImg->SetMetadata(md); - //pImg->SetPixels(pixArray + i * singleChannelSize); + } + //pImg->SetPixels(pixArray + i * singleChannelSize); // TODO: In MMCore the ImgBuffer::GetPixels() returns const pointer. // It would be better to have something like ImgBuffer::GetPixelsRW() in MMDevice. // Or even better - pass tasksMemCopy_ to ImgBuffer constructor // and utilize parallel copy also in single snap acquisitions. - tasksMemCopy_->MemCopy((void*)pImg->GetPixels(), + tasksMemCopy_->MemCopy((void*)pImg->GetPixels(), pixArray + i * singleChannelSize, singleChannelSize); } @@ -362,7 +264,7 @@ const mm::ImgBuffer* CircularBuffer::GetNthFromTopImageBuffer(long n, return frameArray_[targetIndex].FindImage(channel); } -const unsigned char* CircularBuffer::GetNextImage() +const unsigned char* CircularBuffer::PopNextImage() { const mm::ImgBuffer* img = GetNextImageBuffer(0); if (!img) diff --git a/MMCore/CircularBuffer.h b/MMCore/CircularBuffer.h index 636c02ceb..c3d7b86b0 100644 --- a/MMCore/CircularBuffer.h +++ b/MMCore/CircularBuffer.h @@ -66,12 +66,9 @@ class CircularBuffer unsigned int Height() const {MMThreadGuard guard(g_bufferLock); return height_;} unsigned int Depth() const {MMThreadGuard guard(g_bufferLock); return pixDepth_;} - bool InsertImage(const unsigned char* pixArray, unsigned int width, unsigned int height, unsigned int byteDepth, const Metadata* pMd) throw (CMMError); bool InsertMultiChannel(const unsigned char* pixArray, unsigned int numChannels, unsigned int width, unsigned int height, unsigned int byteDepth, const Metadata* pMd) throw (CMMError); - bool InsertImage(const unsigned char* pixArray, unsigned int width, unsigned int height, unsigned int byteDepth, unsigned int nComponents, const Metadata* pMd) throw (CMMError); - bool InsertMultiChannel(const unsigned char* pixArray, unsigned int numChannels, unsigned int width, unsigned int height, unsigned int byteDepth, unsigned int nComponents, const Metadata* pMd) throw (CMMError); const unsigned char* GetTopImage() const; - const unsigned char* GetNextImage(); + const unsigned char* PopNextImage(); const mm::ImgBuffer* GetTopImageBuffer(unsigned channel) const; const mm::ImgBuffer* GetNthFromTopImageBuffer(unsigned long n) const; const mm::ImgBuffer* GetNthFromTopImageBuffer(long n, unsigned channel) const; @@ -89,7 +86,6 @@ class CircularBuffer unsigned int pixDepth_; long imageCounter_; std::chrono::time_point startTime_; - std::map imageNumbers_; // Invariants: // 0 <= saveIndex_ <= insertIndex_ diff --git a/MMCore/MMCore.cpp b/MMCore/MMCore.cpp index e3867acf5..150316540 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -3079,7 +3079,7 @@ void* CMMCore::getLastImage() throw (CMMError) } } - unsigned char* pBuf = const_cast(bufferAdapter_->GetTopImage()); + unsigned char* pBuf = const_cast(bufferAdapter_->GetLastImage()); if (pBuf != 0) return pBuf; else @@ -3095,14 +3095,7 @@ void* CMMCore::getLastImageMD(unsigned channel, unsigned slice, Metadata& md) co if (slice != 0) throw CMMError("Slice must be 0"); - const mm::ImgBuffer* pBuf = bufferAdapter_->GetTopImageBuffer(channel); - if (pBuf != 0) - { - md = pBuf->GetMetadata(); - return const_cast(pBuf->GetPixels()); - } - else - throw CMMError(getCoreErrorText(MMERR_CircularBufferEmpty).c_str(), MMERR_CircularBufferEmpty); + return bufferAdapter_->GetLastImageMD(channel, md); } /** @@ -3136,14 +3129,7 @@ void* CMMCore::getLastImageMD(Metadata& md) const throw (CMMError) */ void* CMMCore::getNBeforeLastImageMD(unsigned long n, Metadata& md) const throw (CMMError) { - const mm::ImgBuffer* pBuf = bufferAdapter_->GetNthFromTopImageBuffer(n); - if (pBuf != 0) - { - md = pBuf->GetMetadata(); - return const_cast(pBuf->GetPixels()); - } - else - throw CMMError(getCoreErrorText(MMERR_CircularBufferEmpty).c_str(), MMERR_CircularBufferEmpty); + return bufferAdapter_->GetNthImageMD(n, md); } /** @@ -3160,7 +3146,7 @@ void* CMMCore::getNBeforeLastImageMD(unsigned long n, Metadata& md) const throw */ void* CMMCore::popNextImage() throw (CMMError) { - unsigned char* pBuf = const_cast(bufferAdapter_->GetNextImage()); + unsigned char* pBuf = const_cast(bufferAdapter_->PopNextImage()); if (pBuf != 0) return pBuf; else @@ -3178,14 +3164,7 @@ void* CMMCore::popNextImageMD(unsigned channel, unsigned slice, Metadata& md) th if (slice != 0) throw CMMError("Slice must be 0"); - const mm::ImgBuffer* pBuf = bufferAdapter_->GetNextImageBuffer(channel); - if (pBuf != 0) - { - md = pBuf->GetMetadata(); - return const_cast(pBuf->GetPixels()); - } - else - throw CMMError(getCoreErrorText(MMERR_CircularBufferEmpty).c_str(), MMERR_CircularBufferEmpty); + return bufferAdapter_->PopNextImageMD(channel, md); } /** @@ -3287,7 +3266,10 @@ long CMMCore::getBufferTotalCapacity() { if (bufferAdapter_) { - return bufferAdapter_->GetSize(); + // Compute image size from the current camera parameters. + long imageSize = getImageWidth() * getImageHeight() * getBytesPerPixel(); + // Pass the computed image size as an argument to the adapter. + return bufferAdapter_->GetSize(imageSize); } return 0; } @@ -3301,7 +3283,9 @@ long CMMCore::getBufferFreeCapacity() { if (bufferAdapter_) { - return bufferAdapter_->GetFreeSize(); + // Compute image size from the current camera parameters. + long imageSize = getImageWidth() * getImageHeight() * getBytesPerPixel(); + return bufferAdapter_->GetFreeSize(imageSize); } return 0; } From 8efc90d60d23d25c7874442f2219d623345a65a1 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:51:33 -0800 Subject: [PATCH 5/9] added metadata to buffer --- MMCore/BufferAdapter.cpp | 6 +- MMCore/Buffer_v2.cpp | 410 ++++++++++++++++++++++----------------- MMCore/Buffer_v2.h | 184 ++++++++++-------- 3 files changed, 336 insertions(+), 264 deletions(-) diff --git a/MMCore/BufferAdapter.cpp b/MMCore/BufferAdapter.cpp index 2bbcdb306..ac12beb0a 100644 --- a/MMCore/BufferAdapter.cpp +++ b/MMCore/BufferAdapter.cpp @@ -77,7 +77,7 @@ const unsigned char* BufferAdapter::PopNextImage() { if (useV2_) { Metadata dummyMetadata; - return v2Buffer_->PopNextDataReadPointer(nullptr, dummyMetadata, false); + return v2Buffer_->PopNextDataReadPointer(dummyMetadata, nullptr, false); // TODO: ensure calling code releases the slot after use } else { return circBuffer_->PopNextImage(); @@ -252,7 +252,7 @@ void* BufferAdapter::GetLastImageMD(unsigned channel, Metadata& md) const throw // In v2, we use PeekNextDataReadPointer (which does not advance the internal pointer) // Note: the v2 buffer is not channel aware, so the 'channel' parameter is ignored. // TODO implement the channel aware version - void* slotPtr = nullptr; + unsigned char* slotPtr = nullptr; size_t dataSize = 0; int ret = v2Buffer_->PeekNextDataReadPointer(&slotPtr, &dataSize, md); if (ret != DEVICE_OK || slotPtr == nullptr) @@ -299,7 +299,7 @@ void* BufferAdapter::PopNextImageMD(unsigned channel, Metadata& md) throw (CMMEr // The caller is expected to call ReleaseDataReadPointer on the returned pointer once done. // TODO: make channel aware size_t dataSize = 0; - const unsigned char* slotPtr = v2Buffer_->PopNextDataReadPointer(&dataSize, md, false); + const unsigned char* slotPtr = v2Buffer_->PopNextDataReadPointer(md, &dataSize, false); if (slotPtr == nullptr) throw CMMError("V2 buffer is empty.", MMERR_CircularBufferEmpty); return const_cast(slotPtr); diff --git a/MMCore/Buffer_v2.cpp b/MMCore/Buffer_v2.cpp index 69cf38cef..0c61ba83f 100644 --- a/MMCore/Buffer_v2.cpp +++ b/MMCore/Buffer_v2.cpp @@ -63,9 +63,13 @@ Metadata Handling: #include #include #include +#include - - +// New internal header that precedes every slot's data. +struct BufferSlotRecord { + size_t imageSize; + size_t metadataSize; +}; /////////////////////////////////////////////////////////////////////////////// // BufferSlot Implementation @@ -241,7 +245,7 @@ DataBuffer::~DataBuffer() { int DataBuffer::AllocateBuffer(unsigned int memorySizeMB) { // Convert MB to bytes (1 MB = 1048576 bytes) size_t numBytes = static_cast(memorySizeMB) * (1ULL << 20); - buffer_ = new char[numBytes]; + buffer_ = new unsigned char[numBytes]; bufferSize_ = numBytes; overflow_ = false; return DEVICE_OK; @@ -263,57 +267,67 @@ int DataBuffer::ReleaseBuffer() { } /** - * Copy data into the next available slot in the buffer. - * - * Returns the size of the copied data through dataSize. - * all required metadata for interpreting its image data is there. - * Note: this can be implemented in terms of Get/Release slot + memcopy. - * - * @param caller The device calling this function. - * @param data The data to be copied. - * @param dataSize Size of the data to copy. - * @param serializedMetadata The associated metadata. - * @return Error code (0 on success). + * Pack the data as [BufferSlotRecord][image data][serialized metadata] */ -int DataBuffer::InsertData(const void* data, - size_t dataSize, const Metadata* pMd) { - // Get a write slot of the required size - void* slotPointer = nullptr; - int result = GetDataWriteSlot(dataSize, &slotPointer); +int DataBuffer::InsertData(const unsigned char* data, size_t dataSize, const Metadata* pMd) { + size_t metaSize = 0; + std::string metaStr; + if (pMd) { + metaStr = pMd->Serialize(); + metaSize = metaStr.size(); + } + // Total size is header + image data + metadata + size_t totalSize = sizeof(BufferSlotRecord) + dataSize + metaSize; + unsigned char* imageDataPointer = nullptr; + // TOFO: handle metadata pointer + int result = GetDataWriteSlot(totalSize, metaSize, &imageDataPointer, nullptr); if (result != DEVICE_OK) return result; - // Copy the data into the slot - std::memcpy(slotPointer, data, dataSize); - if (pMd) { - // md.Serialize().c_str() - // TODO: Need a metadata lock and perhaps a map from buffer offset to metadata? + // The externally returned imageDataPointer points to the image data. + // Write out the header by subtracting the header size. + BufferSlotRecord* headerPointer = reinterpret_cast(imageDataPointer - sizeof(BufferSlotRecord)); + headerPointer->imageSize = dataSize; + headerPointer->metadataSize = metaSize; + + // Copy the image data into the allocated slot (imageDataPointer is already at the image data). + std::memcpy(imageDataPointer, data, dataSize); + + // If metadata is available, copy it right after the image data. + if (metaSize > 0) { + unsigned char* metaPtr = imageDataPointer + dataSize; + std::memcpy(metaPtr, metaStr.data(), metaSize); } // Release the write slot - return ReleaseDataWriteSlot(&slotPointer); + return ReleaseDataWriteSlot(&imageDataPointer, metaSize > 0 ? static_cast(metaSize) : -1); } /** - * Copy the next available data and metadata from the buffer - * @param dataDestination Destination buffer to copy data into - * @param dataSize Returns the size of the copied data, or 0 if no data available - * @param md Metadata object to populate - * @param waitForData If true, block until data becomes available - * @return Error code (0 on success) + * Reads the header from the slot, then copies the image data into the destination and + * uses the metadata blob (if any) to populate 'md'. */ -int DataBuffer::CopyNextDataAndMetadata(void* dataDestination, - size_t* dataSize, Metadata &md, bool waitForData) { - void* sourcePtr = nullptr; - int result = PopNextDataReadPointer(&sourcePtr, dataSize, md, waitForData); - if (result != DEVICE_OK) - return result; - - // Copy the data from the slot into the user's destination buffer - std::memcpy(dataDestination, sourcePtr, *dataSize); - - // Release the read pointer (this will handle cleanup and index management) - return ReleaseDataReadPointer(&sourcePtr); +int DataBuffer::CopyNextDataAndMetadata(unsigned char* dataDestination, size_t* imageDataSize, Metadata &md, bool waitForData) { + const unsigned char* imageDataPointer = PopNextDataReadPointer(md, imageDataSize, waitForData); + if (imageDataPointer == nullptr) + return DEVICE_ERR; + + const BufferSlotRecord* headerPointer = reinterpret_cast(imageDataPointer - sizeof(BufferSlotRecord)); + *imageDataSize = headerPointer->imageSize; + // imageDataPointer already points to the image data. + std::memcpy(dataDestination, imageDataPointer, headerPointer->imageSize); + + // Extract the metadata (if any) following the image data. + std::string metaStr; + if (headerPointer->metadataSize > 0) { + const char* metaDataStart = reinterpret_cast(imageDataPointer + headerPointer->imageSize); + metaStr.assign(metaDataStart, headerPointer->metadataSize); + } + // Restore the metadata + // This is analogous to what is done in FrameBuffer.cpp: + md.Restore(metaStr.c_str()); + + return ReleaseDataReadPointer(&imageDataPointer); } /** @@ -341,8 +355,8 @@ int DataBuffer::SetOverwriteData(bool overwrite) { * * The caller must release the slot using ReleaseDataSlot after writing is complete. */ -int DataBuffer::GetDataWriteSlot(size_t slotSize, void** slotPointer) { - // AllocateNextSlot allocates a slot for writing new data of variable size. +int DataBuffer::GetDataWriteSlot(size_t imageSize, size_t metadataSize, const unsigned char** imageDataPointer, const unsigned char** metadataPointer) { + // AllocateNextSlot allocates a slot for writing new data of variable size. // // First, it checks if there is a recently released slot start (from releasedSlots_). // For each candidate, it uses the activeSlotMap_ mechanism to @@ -352,9 +366,11 @@ int DataBuffer::GetDataWriteSlot(size_t slotSize, void** slotPointer) { // // If no released slot fits, then it falls back to using nextAllocOffset_ and similar // collision checks. In overwrite mode, wrap-around is supported. + // Lock to ensure exclusive allocation. + std::lock_guard lock(slotManagementMutex_); - // Ensure that only one thread can allocate a slot at a time. - std::lock_guard lock(slotManagementMutex_); + // Total slot size is the header plus the image and metadata lengths. + size_t totalSlotSize = sizeof(BufferSlotRecord) + imageSize + metadataSize; // First, try using a released slot candidate (FILO order) for (int i = static_cast(releasedSlots_.size()) - 1; i >= 0; i--) { @@ -364,26 +380,25 @@ int DataBuffer::GetDataWriteSlot(size_t slotSize, void** slotPointer) { // Find first slot at or after our candidate position auto nextIt = activeSlotsByStart_.lower_bound(localCandidate); - // If there's a previous slot, ensure we don't overlap with it + // If a previous slot exists, adjust to avoid overlap. if (nextIt != activeSlotsByStart_.begin()) { size_t prevSlotEnd = std::prev(nextIt)->first + std::prev(nextIt)->second->GetLength(); // If our candidate region [candidateStart, candidateStart+slotSize) overlaps the previous slot, // bump candidateStart to the end of the conflicting slot and try again. if (prevSlotEnd > localCandidate) { localCandidate = prevSlotEnd; - } + } } // Check if there's space before the next active slot nextIt = activeSlotsByStart_.lower_bound(localCandidate); bool candidateValid = true; - if (nextIt != activeSlotsByStart_.end()) { // Case 1: There is a next slot // Check if our proposed slot would overlap with the next slot - candidateValid = (localCandidate + slotSize <= nextIt->first); - } else if (localCandidate + slotSize > bufferSize_) { - // Case 2: No next slot, but we'd exceed buffer size + candidateValid = (localCandidate + totalSlotSize <= nextIt->first); + } else if (localCandidate + totalSlotSize > bufferSize_) { + // Case 2: No next slot, but we'd exceed buffer size if (!overwriteWhenFull_) { candidateValid = false; } else { @@ -402,7 +417,7 @@ int DataBuffer::GetDataWriteSlot(size_t slotSize, void** slotPointer) { // Check if wrapped position would overlap with first slot if (nextIt != activeSlotsByStart_.end()) { - candidateValid = (localCandidate + slotSize <= nextIt->first); + candidateValid = (localCandidate + totalSlotSize <= nextIt->first); } } } @@ -410,27 +425,30 @@ int DataBuffer::GetDataWriteSlot(size_t slotSize, void** slotPointer) { if (candidateValid) { // Remove the candidate from releasedSlots_ (it was taken from the "back" if available). releasedSlots_.erase(releasedSlots_.begin() + i); - activeSlotsVector_.push_back(BufferSlot(localCandidate, slotSize)); - BufferSlot* slot = &activeSlotsVector_.back(); + activeSlotsVector_.push_back(std::make_unique(localCandidate, totalSlotSize)); + BufferSlot* slot = activeSlotsVector_.back().get(); activeSlotsByStart_[localCandidate] = slot; - *slotPointer = buffer_ + slot->GetStart(); + *imageDataPointer = buffer_ + slot->GetStart() + sizeof(BufferSlotRecord); + *metadataPointer = *imageDataPointer + imageSize; return DEVICE_OK; } } - // If no released candidate fits, use nextAllocOffset_ fallback. + + // If no released candidate fits, fall back to nextAllocOffset_. size_t candidateStart = nextAllocOffset_; - if (candidateStart + slotSize > bufferSize_) { + if (candidateStart + totalSlotSize > bufferSize_) { // Not enough space in the buffer: if we are not allowed to overwrite then set our overflow flag. if (!overwriteWhenFull_) { - overflow_ = true; // <-- mark that overflow has happened - *slotPointer = nullptr; + overflow_ = true; + *imageDataPointer = nullptr; + *metadataPointer = nullptr; return DEVICE_ERR; } candidateStart = 0; // Reset to start of buffer // Since we're starting at position 0, remove any slots that start before our requested size auto it = activeSlotsByStart_.begin(); - while (it != activeSlotsByStart_.end() && it->first < slotSize) { + while (it != activeSlotsByStart_.end() && it->first < totalSlotSize) { BufferSlot* slot = it->second; if (!slot->IsAvailableForWriting() || !slot->IsAvailableForReading()) { throw std::runtime_error("Cannot overwrite slot that is currently being accessed (has active readers or writers)"); @@ -439,26 +457,23 @@ int DataBuffer::GetDataWriteSlot(size_t slotSize, void** slotPointer) { // Remove from both tracking structures activeSlotsVector_.erase( std::remove_if(activeSlotsVector_.begin(), activeSlotsVector_.end(), - [targetStart = it->first](const BufferSlot& slot) { - return slot.GetStart() == targetStart; - }), + [targetStart = it->first](const std::unique_ptr& slot) { + return slot->GetStart() == targetStart; + }), activeSlotsVector_.end()); it = activeSlotsByStart_.erase(it); } } - // Create and track the new slot - activeSlotsVector_.push_back(BufferSlot(candidateStart, slotSize)); - BufferSlot* newSlot = &activeSlotsVector_.back(); + activeSlotsVector_.push_back(std::make_unique(candidateStart, totalSlotSize)); + BufferSlot* newSlot = activeSlotsVector_.back().get(); activeSlotsByStart_[candidateStart] = newSlot; - - // Update nextAllocOffset_ for next allocation - nextAllocOffset_ = candidateStart + slotSize; + nextAllocOffset_ = candidateStart + totalSlotSize; if (nextAllocOffset_ >= bufferSize_) { nextAllocOffset_ = 0; } - - *slotPointer = buffer_ + newSlot->GetStart(); + *imageDataPointer = buffer_ + newSlot->GetStart() + sizeof(BufferSlotRecord); + *metadataPointer = *imageDataPointer + imageSize; return DEVICE_OK; } @@ -469,30 +484,39 @@ int DataBuffer::GetDataWriteSlot(size_t slotSize, void** slotPointer) { * @param buffer The buffer to be released. * @return Error code (0 on success). */ -int DataBuffer::ReleaseDataWriteSlot(void** slotPointer) { - if (slotPointer == nullptr || *slotPointer == nullptr) +int DataBuffer::ReleaseDataWriteSlot(unsigned char** imageDataPointer, int actualMetadataBytes) { + if (imageDataPointer == nullptr || *imageDataPointer == nullptr) return DEVICE_ERR; std::lock_guard lock(slotManagementMutex_); - // Calculate the offset from the buffer start to find the corresponding slot - char* ptr = static_cast(*slotPointer); - size_t offset = ptr - buffer_; + // Convert the externally provided imageDataPointer (which points to the image data) + // to the true slot start (header) by subtracting sizeof(BufferSlotRecord). + unsigned char* headerPointer = *imageDataPointer - sizeof(BufferSlotRecord); + size_t offset = headerPointer - buffer_; - // Find the slot in activeSlotsByStart_ + // Locate the slot using the true header offset. auto it = activeSlotsByStart_.find(offset); - if (it == activeSlotsByStart_.end()) { + if (it == activeSlotsByStart_.end()) return DEVICE_ERR; // Slot not found - } // Release the write access BufferSlot* slot = it->second; slot->ReleaseWriteAccess(); - // Clear the pointer - *slotPointer = nullptr; + // If a valid actual metadata byte count is provided (i.e. not -1), + // update the header->metadataSize to the actual metadata length if it is less. + if (actualMetadataBytes != -1) { + BufferSlotRecord* hdr = reinterpret_cast(headerPointer); + if (static_cast(actualMetadataBytes) < hdr->metadataSize) { + hdr->metadataSize = actualMetadataBytes; + } + } - // Notify any waiting readers that new data is available + // Clear the externally provided image data pointer. + *imageDataPointer = nullptr; + + // Notify any waiting threads that new data is available. dataCV_.notify_all(); return DEVICE_OK; @@ -501,87 +525,101 @@ int DataBuffer::ReleaseDataWriteSlot(void** slotPointer) { /** * ReleaseSlot is called after a slot's content has been fully read. - * It assumes the caller has already released its read access (the slot is free). * * This implementation pushes only the start of the released slot onto the FILO * (releasedSlots_) and removes the slot from the active slot map and activeSlots_. */ -int DataBuffer::ReleaseDataReadPointer(void** slotPointer) { - if (slotPointer == nullptr || *slotPointer == nullptr) +int DataBuffer::ReleaseDataReadPointer(const unsigned char** imageDataPointer) { + if (imageDataPointer == nullptr || *imageDataPointer == nullptr) return DEVICE_ERR; std::unique_lock lock(slotManagementMutex_); - - // Compute the slot's start offset. - char* ptr = static_cast(*slotPointer); - size_t offset = ptr - buffer_; - // Find the slot in activeSlotMap_. + // Compute the header pointer by subtracting the header size. + const unsigned char* headerPointer = *imageDataPointer - sizeof(BufferSlotRecord); + size_t offset = headerPointer - buffer_; + + // Find the slot in activeSlotsByStart_ auto it = activeSlotsByStart_.find(offset); if (it != activeSlotsByStart_.end()) { BufferSlot* slot = it->second; - // Check if the slot is being accessed by any readers or writers. - if (!slot->IsAvailableForWriting() || !slot->IsAvailableForReading()) { - // TODO: right way to handle exceptions? - throw std::runtime_error("Cannot release slot that is currently being accessed"); - } - - // If we've reached max size, remove the oldest element (front of vector). - if (releasedSlots_.size() >= MAX_RELEASED_SLOTS) { - releasedSlots_.erase(releasedSlots_.begin()); - } - releasedSlots_.push_back(offset); + // Release the previously acquired read access. + slot->ReleaseReadAccess(); - // Remove slot from active structures. - activeSlotsByStart_.erase(it); - for (auto vecIt = activeSlotsVector_.begin(); vecIt != activeSlotsVector_.end(); ++vecIt) { - if (vecIt->GetStart() == offset) { - // Determine the index being removed. - size_t indexDeleted = std::distance(activeSlotsVector_.begin(), vecIt); - activeSlotsVector_.erase(vecIt); - // Adjust currentSlotIndex_: - // If the deleted slot was before the current index, decrement it. - if (currentSlotIndex_ > indexDeleted) - currentSlotIndex_--; - - break; + // Now check if the slot is not being accessed + // (i.e. this was the last/readers and no writer holds it) + if (slot->IsAvailableForWriting() && slot->IsAvailableForReading()) { + // Ensure we do not exceed the maximum number of released slots. + if (releasedSlots_.size() >= MAX_RELEASED_SLOTS) + releasedSlots_.erase(releasedSlots_.begin()); + releasedSlots_.push_back(offset); + + // Remove slot from the active tracking structures. + activeSlotsByStart_.erase(it); + for (auto vecIt = activeSlotsVector_.begin(); vecIt != activeSlotsVector_.end(); ++vecIt) { + if (vecIt->get()->GetStart() == offset) { + // Determine the index being removed. + size_t indexDeleted = std::distance(activeSlotsVector_.begin(), vecIt); + activeSlotsVector_.erase(vecIt); + // Adjust currentSlotIndex_: + // If the deleted slot was before the current index, decrement it. + if (currentSlotIndex_ > indexDeleted) + currentSlotIndex_--; + break; + } } } + } else { + throw std::runtime_error("Cannot release slot that is not in the buffer."); } - *slotPointer = nullptr; + *imageDataPointer = nullptr; return DEVICE_OK; } -const unsigned char* DataBuffer::PopNextDataReadPointer(size_t* dataSize, Metadata &md, bool waitForData) { - while (true) { - std::unique_lock lock(slotManagementMutex_); - - // If data is available, process it. - if (!activeSlotsVector_.empty() && currentSlotIndex_ < activeSlotsVector_.size()) { - BufferSlot& currentSlot = activeSlotsVector_[currentSlotIndex_]; - - if (!currentSlot.AcquireReadAccess()) { - throw std::runtime_error("Failed to acquire read access for the current slot."); - } - - const unsigned char* slotPointer = reinterpret_cast(buffer_ + currentSlot.GetStart()); - if (dataSize != nullptr) { - *dataSize = currentSlot.GetLength(); - } - - currentSlotIndex_++; - return slotPointer; - } - - // No data available. - if (!waitForData) { - throw std::runtime_error("No data available to read."); - } - - // Wait for notification of new data. +const unsigned char* DataBuffer::PopNextDataReadPointer(Metadata &md, size_t *imageDataSize, bool waitForData) +{ + std::unique_lock lock(slotManagementMutex_); + + // Wait until there is data available if requested. + // (Here we check whether activeSlotsVector_ has an unread slot. + // Adjust the condition as appropriate for your implementation.) + while (activeSlotsVector_.empty()) { + if (!waitForData) + return nullptr; dataCV_.wait(lock); - // When we wake up, the while loop will check again if data is available } + + // Assume that the next unread slot is at index currentSlotIndex_. + // (Depending on your data structure you might pop from a deque or update an iterator.) + BufferSlot* slot = activeSlotsVector_[currentSlotIndex_].get(); + // Get the starting offset for this slot. + size_t slotStart = slot->GetStart(); + + // The header is stored at the beginning of the slot. + const BufferSlotRecord* header = reinterpret_cast(buffer_ + slotStart); + + // The image data region starts right after the header. + const unsigned char* imageDataPointer = buffer_ + slotStart + sizeof(BufferSlotRecord); + + // Set the output image data size from the header. + *imageDataSize = header->imageSize; + + // Populate the metadata. + if (header->metadataSize > 0) { + const char* metaDataStart = reinterpret_cast(imageDataPointer + header->imageSize); + // Assuming Metadata::Restore takes a null-terminated string or similar. + md.Restore(metaDataStart); + } else { + // If no metadata is available, clear the metadata object. + md.Clear(); + } + + // Mark that this slot has been consumed. + // For this example, we simply move to the next slot. + currentSlotIndex_ = (currentSlotIndex_ + 1) % activeSlotsVector_.size(); + + // Unlock and return the pointer to the image data region. + return imageDataPointer; } unsigned int DataBuffer::GetMemorySizeMB() const { @@ -589,7 +627,7 @@ unsigned int DataBuffer::GetMemorySizeMB() const { return static_cast(bufferSize_ >> 20); } -int DataBuffer::PeekNextDataReadPointer(void** slotPointer, size_t* dataSize, +int DataBuffer::PeekNextDataReadPointer(const unsigned char** imageDataPointer, size_t* imageDataSize, Metadata &md) { // Immediately check if there is an unread slot without waiting. std::unique_lock lock(slotManagementMutex_); @@ -598,34 +636,61 @@ int DataBuffer::PeekNextDataReadPointer(void** slotPointer, size_t* dataSize, } // Obtain the next available slot *without* advancing currentSlotIndex_. - BufferSlot& currentSlot = activeSlotsVector_[currentSlotIndex_]; + BufferSlot& currentSlot = *activeSlotsVector_[currentSlotIndex_]; if (!currentSlot.AcquireReadAccess()) return DEVICE_ERR; - *slotPointer = buffer_ + currentSlot.GetStart(); - *dataSize = currentSlot.GetLength(); - // (If metadata is stored per slot, populate md here.) + *imageDataPointer = buffer_ + currentSlot.GetStart() + sizeof(BufferSlotRecord); + const BufferSlotRecord* headerPointer = reinterpret_cast( (*imageDataPointer) - sizeof(BufferSlotRecord) ); + *imageDataSize = headerPointer->imageSize; + + // Populate the Metadata object from the stored metadata blob. + std::string metaStr; + if (headerPointer->metadataSize > 0) { + const char* metaDataStart = reinterpret_cast(*imageDataPointer + headerPointer->imageSize); + metaStr.assign(metaDataStart, headerPointer->metadataSize); + } + // Restore the metadata + // This is analogous to what is done in FrameBuffer.cpp: + md.Restore(metaStr.c_str()); + return DEVICE_OK; } -const unsigned char* DataBuffer::PeekDataReadPointerAtIndex(size_t n, size_t* dataSize, Metadata &md) { - +const unsigned char* DataBuffer::PeekDataReadPointerAtIndex(size_t n, size_t* imageDataSize, Metadata &md) { std::unique_lock lock(slotManagementMutex_); if (activeSlotsVector_.empty() || (currentSlotIndex_ + n) >= activeSlotsVector_.size()) { throw std::runtime_error("Not enough unread data available."); } - BufferSlot& slot = activeSlotsVector_[currentSlotIndex_ + n]; + // Access the nth slot (without advancing the read index) + BufferSlot& slot = *activeSlotsVector_[currentSlotIndex_ + n]; if (!slot.AcquireReadAccess()) throw std::runtime_error("Failed to acquire read access for the selected slot."); - // Assign the size from the slot. - if (dataSize != nullptr) { - *dataSize = slot.GetLength(); + // Obtain the pointer to the image data (skip the header) + const unsigned char* imageDataPointer = buffer_ + slot.GetStart() + sizeof(BufferSlotRecord); + const BufferSlotRecord* headerPointer = reinterpret_cast(imageDataPointer - sizeof(BufferSlotRecord)); + + // Return the image size via the pointer parameter + if (imageDataSize != nullptr) { + *imageDataSize = headerPointer->imageSize; + } + + // Retrieve the serialized metadata from the slot. + std::string metaStr; + if (headerPointer->metadataSize > 0) { + const char* metaDataStart = reinterpret_cast(imageDataPointer + headerPointer->imageSize); + metaStr.assign(metaDataStart, headerPointer->metadataSize); } - // Return a constant pointer to the data. - return reinterpret_cast(buffer_ + slot.GetStart()); + // Restore the metadata + // This is analogous to what is done in FrameBuffer.cpp: + // metadata_.Restore(md.Serialize().c_str()); + md.Restore(metaStr.c_str()); + + // Return a pointer to the image data only. + return imageDataPointer; } /** @@ -635,15 +700,13 @@ const unsigned char* DataBuffer::PeekDataReadPointerAtIndex(size_t n, size_t* da * overwriteWhenFull_ flag is true and the caller wants to release the * peeked slot for reuse. */ - -int DataBuffer::ReleasePeekDataReadPointer(void** slotPointer) { - if (slotPointer == nullptr || *slotPointer == nullptr) +int DataBuffer::ReleasePeekDataReadPointer(const unsigned char** imageDataPointer) { + if (imageDataPointer == nullptr || *imageDataPointer == nullptr) return DEVICE_ERR; - std::lock_guard lock(slotManagementMutex_); - char* ptr = static_cast(*slotPointer); - size_t offset = ptr - buffer_; + const unsigned char* headerPointer = *imageDataPointer - sizeof(BufferSlotRecord); + size_t offset = headerPointer - buffer_; // Look up the corresponding slot by its buffer offset. auto it = activeSlotsByStart_.find(offset); @@ -654,7 +717,7 @@ int DataBuffer::ReleasePeekDataReadPointer(void** slotPointer) { // Release the read access (this does NOT remove the slot from the active list) slot->ReleaseReadAccess(); - *slotPointer = nullptr; + *imageDataPointer = nullptr; return DEVICE_OK; } @@ -667,7 +730,7 @@ size_t DataBuffer::GetOccupiedMemory() const { std::lock_guard lock(slotManagementMutex_); size_t usedMemory = 0; for (const auto& slot : activeSlotsVector_) { - usedMemory += slot.GetLength(); + usedMemory += slot->GetLength(); } return usedMemory; } @@ -677,7 +740,7 @@ size_t DataBuffer::GetFreeMemory() const { // Free memory is the total buffer size minus the sum of all occupied memory. size_t usedMemory = 0; for (const auto& slot : activeSlotsVector_) { - usedMemory += slot.GetLength(); + usedMemory += slot->GetLength(); } return (bufferSize_ > usedMemory) ? (bufferSize_ - usedMemory) : 0; } @@ -700,8 +763,8 @@ int DataBuffer::ReinitializeBuffer(unsigned int memorySizeMB) { std::lock_guard lock(slotManagementMutex_); // Check that there are no outstanding readers or writers. - for (const BufferSlot &slot : activeSlotsVector_) { - if (!slot.IsAvailableForReading() || !slot.IsAvailableForWriting()) { + for (const std::unique_ptr& slot : activeSlotsVector_) { + if (!slot->IsAvailableForReading() || !slot->IsAvailableForWriting()) { throw std::runtime_error("Cannot reinitialize DataBuffer: outstanding active slot detected."); } } @@ -726,12 +789,3 @@ int DataBuffer::ReinitializeBuffer(unsigned int memorySizeMB) { return DEVICE_OK; } - -long DataBuffer::GetRemainingImageCount() const { - std::lock_guard lock(slotManagementMutex_); - // Return the number of unread slots. - // currentSlotIndex_ tracks the next slot to read, - // so unread count is the total slots minus this index. - return (activeSlotsVector_.size() > currentSlotIndex_) ? - static_cast(activeSlotsVector_.size() - currentSlotIndex_) : 0; -} diff --git a/MMCore/Buffer_v2.h b/MMCore/Buffer_v2.h index b24bea5ef..b00077ede 100644 --- a/MMCore/Buffer_v2.h +++ b/MMCore/Buffer_v2.h @@ -41,6 +41,7 @@ #include #include #include +#include /** * BufferSlot represents a contiguous slot in the DataBuffer that holds image @@ -169,8 +170,13 @@ class BufferSlot { * 1. Copy-based access. * 2. Direct pointer access with an explicit release. * - * Reference counting is used to ensure that memory is managed safely. A slot - * is recycled when all references (readers and writers) have been released. + * Each slot begins with a header (BufferSlotRecord) that stores: + * - The image data length + * - The serialized metadata length (which might be zero) + * + * The user-visible routines (e.g. InsertData and CopyNextDataAndMetadata) + * automatically pack and unpack the header so that the caller need not worry + * about the extra bytes. */ class DataBuffer { public: @@ -208,186 +214,198 @@ class DataBuffer { int ReleaseBuffer(); /** - * Copies data into the next available slot in the buffer along with its metadata. - * The copy-based approach is implemented using a slot acquisition, memory copy, and then - * slot release. + * Inserts data into the next available slot. + * The data is stored together with its metadata and is arranged as: + * [BufferSlotRecord header][image data][serialized metadata] * - * @param data Pointer to the data to be inserted. - * @param dataSize The size of data (in bytes) being inserted. - * @param pMd Pointer to the metadata associated with the data. + * @param data Pointer to the raw image data. + * @param dataSize The image data byte count. + * @param pMd Pointer to the metadata. If null, no metadata is stored. * @return DEVICE_OK on success. */ - int InsertData(const void* data, size_t dataSize, const Metadata* pMd); + int InsertData(const unsigned char* data, size_t dataSize, const Metadata* pMd); /** - * Copies data and metadata from the next available slot in the buffer into the provided destination. + * Copies data and metadata from the next available slot in the buffer. + * The routine examines the header to determine the image byte count + * and the length of the stored metadata. * - * @param dataDestination Destination buffer into which data will be copied. - * @param dataSize On success, returns the size of the copied data. - * @param md Metadata object to be populated with the data's metadata. + * @param dataDestination Destination buffer where image data is copied. + * @param imageDataSize On success, returns the image data size (in bytes). + * @param md Metadata object to be populated (via deserialization of the stored blob). * @param waitForData If true, block until data becomes available. * @return DEVICE_OK on success. */ - int CopyNextDataAndMetadata(void* dataDestination, size_t* dataSize, Metadata &md, bool waitForData); + int CopyNextDataAndMetadata(unsigned char* dataDestination, size_t* imageDataSize, Metadata &md, bool waitForData); /** - * Sets whether the buffer should overwrite old data when it is full. - * If true, the buffer will recycle the oldest slot when no free slot is available; - * if false, an error is returned when writing new data fails due to a full buffer. + * Sets whether the buffer should overwrite older data when full. * - * @param overwrite True to enable overwriting, false to disable. + * @param overwrite True to enable overwriting, false otherwise. * @return DEVICE_OK on success. */ int SetOverwriteData(bool overwrite); /** - * Acquires a pointer to a free slot in the buffer for writing purposes. - * The caller must later call ReleaseDataWriteSlot after finishing writing. + * Acquires a write slot large enough to hold the image data and metadata. + * On success, provides two pointers: one to the image data region and one to the metadata region. + * + * The metadataSize parameter specifies the maximum size to reserve for metadata if the exact + * size is not known at call time. When the slot is released, the metadata will be automatically + * null-terminated at its actual length, which must not exceed the reserved size. * - * @param slotSize The required size of the write slot. - * @param slotPointer On success, receives a pointer within the buffer where data can be written. + * @param imageSize The number of bytes allocated for image data. + * @param metadataSize The maximum number of bytes to reserve for metadata. + * @param imageDataPointer On success, receives a pointer to the image data region. + * @param metadataPointer On success, receives a pointer to the metadata region. * @return DEVICE_OK on success. */ - int GetDataWriteSlot(size_t slotSize, void** slotPointer); + int GetDataWriteSlot(size_t imageSize, size_t metadataSize, + const unsigned char** imageDataPointer, + const unsigned char** metadataPointer); /** - * Releases the write slot after data writing is complete. - * This clears the write lock and notifies any waiting reader threads. + * Releases a write slot after data has been written. * - * @param slotPointer Pointer previously obtained from GetDataWriteSlot. + * @param imageDataPointer Pointer previously obtained from GetDataWriteSlot. + * This pointer references the start of the image data region. + * @param actualMetadataBytes Optionally, the actual number of metadata bytes written. + * If provided and less than the maximum metadata size reserved, this value + * is used to update the header's metadataSize field. + * Defaults to -1, which means no update is performed. * @return DEVICE_OK on success. */ - int ReleaseDataWriteSlot(void** slotPointer); + int ReleaseDataWriteSlot(unsigned char** imageDataPointer, int actualMetadataBytes = -1); /** - * Releases read access on a data slot after its contents have been completely read. - * This makes the slot available for recycling. + * Releases read access for the image data region after its content has been read. * - * @param slotPointer Pointer previously obtained from GetNextDataReadPointer. + * @param imageDataPointer Pointer previously obtained from reading routines. * @return DEVICE_OK on success. */ - int ReleaseDataReadPointer(void** slotPointer); + int ReleaseDataReadPointer(const unsigned char** imageDataPointer); /** - * Retrieves and removes (consumes) the next available data slot for reading. - * This method advances the internal reading index. + * Retrieves and removes (consumes) the next available data entry for reading, + * and populates the provided Metadata object with the associated metadata. + * The returned pointer points to the beginning of the image data region, + * immediately after the header. * - * @param dataSize On success, returns the size of the data. - * @param md Associated metadata for the data. + * @param md Metadata object to be populated from the stored blob. + * @param imageDataSize On success, returns the image data size (in bytes). * @param waitForData If true, block until data becomes available. - * @return Pointer to the next available data in the buffer. + * @return Pointer to the start of the image data region, or nullptr if none available. */ - const unsigned char* PopNextDataReadPointer(size_t* dataSize, Metadata &md, bool waitForData); - + const unsigned char* PopNextDataReadPointer(Metadata &md, size_t *imageDataSize, bool waitForData); /** - * Peeks at the next unread data slot without consuming it. - * The slot remains available for subsequent acquisitions. + * Peeks at the next unread data entry without consuming it. + * The header is examined so that the actual image data size (excluding header) + * is returned. * - * @param slotPointer On success, receives the pointer to the data. - * @param dataSize On success, returns the size of the data. - * @param md Associated metadata for the data. - * @return DEVICE_OK on success, or an error code if no data is available. + * @param imageDataPointer On success, receives a pointer to the image data region. + * @param imageDataSize On success, returns the image data size (in bytes). + * @param md Metadata object populated from the stored metadata blob. + * @return DEVICE_OK on success, error code otherwise. */ - int PeekNextDataReadPointer(void** slotPointer, size_t* dataSize, Metadata &md); + int PeekNextDataReadPointer(const unsigned char** imageDataPointer, size_t* imageDataSize, Metadata &md); /** - * Peeks at the nth unread data slot without consuming it. + * Peeks at the nth unread data entry without consuming it. * (n = 0 is equivalent to PeekNextDataReadPointer.) * - * @param n The index of the unread slot. - * @param dataSize On success, returns the size of the data. - * @param md Associated metadata for the data. - * @return const pointer to the data. + * @param n The index of the data entry to peek at (0 is next available). + * @param imageDataSize On success, returns the image data size (in bytes). + * @param md Metadata object populated from the stored metadata blob. + * @return Pointer to the start of the image data region. */ - const unsigned char* PeekDataReadPointerAtIndex(size_t n, size_t* dataSize, Metadata &md); - + const unsigned char* PeekDataReadPointerAtIndex(size_t n, size_t* imageDataSize, Metadata &md); /** - * Releases the read access that was acquired by a peek operation. - * This method releases the temporary read access without consuming the slot. + * Releases read access that was acquired by a peek. * - * @param slotPointer Pointer previously obtained from a peek method. + * @param imageDataPointer Pointer previously obtained from a peek. * @return DEVICE_OK on success. */ - int ReleasePeekDataReadPointer(void** slotPointer); + int ReleasePeekDataReadPointer(const unsigned char** imageDataPointer); /** - * Returns the total memory size of the buffer in megabytes. + * Returns the total buffer memory size (in MB). * - * @return The buffer size in MB. + * @return Buffer size in MB. */ unsigned int GetMemorySizeMB() const; /** - * Returns the number of currently occupied slots in the buffer. + * Returns the number of occupied slots in the buffer. * - * @return The number of occupied slots. + * @return Occupied slot count. */ size_t GetOccupiedSlotCount() const; /** - * Returns the total occupied memory (in bytes) within the buffer. + * Returns the total occupied memory (in bytes). * - * @return The sum of the lengths of all active slots. + * @return Sum of active slot lengths. */ size_t GetOccupiedMemory() const; /** - * Returns the amount of free memory (in bytes) remaining in the buffer. + * Returns the amount of free memory (in bytes) remaining. * - * @return The number of free bytes available for new data. + * @return Free byte count. */ size_t GetFreeMemory() const; /** - * Returns whether the buffer has been overflowed (i.e. an attempt to - * allocate a write slot failed because there was no available space). + * Indicates whether a buffer overflow occurred (i.e. an insert failed because + * no appropriate slot was available). + * + * @return True if overflow has happened, false otherwise. */ bool Overflow() const; /** - * Returns the number of unread data slots in the buffer. + * Returns the number of unread slots in the buffer. * - * @return The number of unread data slots. + * @return Unread slot count. */ long GetRemainingImageCount() const; /** - * Reinitialize the DataBuffer by clearing all internal data structures, - * releasing the current buffer, and reallocating a new one. - * This method uses the existing slotManagementMutex_ to ensure thread safety. + * Reinitializes the DataBuffer by clearing its structures, releasing the current + * buffer, and allocating a new one. * - * @param memorySizeMB New size (in MB) for the buffer. + * @param memorySizeMB New buffer size (in MB). * @return DEVICE_OK on success. - * @throws std::runtime_error if any slot is still actively being read or written. + * @throws std::runtime_error if any slot is still actively in use. */ int ReinitializeBuffer(unsigned int memorySizeMB); private: - // Pointer to the allocated buffer memory. - char* buffer_; - // Total size (in bytes) of the allocated buffer. + // Pointer to the allocated block. + unsigned char* buffer_; + // Total allocated size in bytes. size_t bufferSize_; - // Whether the buffer should overwrite older data when full. + // Whether to overwrite old data when full. bool overwriteWhenFull_; - // New: overflow indicator (set to true if an insert fails because of buffer full) + // Overflow flag (set if insert fails due to full buffer). bool overflow_; - // Data structures for tracking active slot usage. - std::vector activeSlotsVector_; + // Data structures used to track active slots. + std::vector> activeSlotsVector_; std::map activeSlotsByStart_; std::vector releasedSlots_; - // The next available offset for a new data slot. + // Next free offset within the buffer. size_t nextAllocOffset_; - // Tracks the current slot index for read operations. + // Index tracking the next slot for read. size_t currentSlotIndex_; - // Synchronization primitives for managing slot access. + // Synchronization primitives for slot management. std::condition_variable dataCV_; mutable std::mutex slotManagementMutex_; }; From 0fd0823e2692888ed3e3576917ff498932b55030 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:22:16 -0800 Subject: [PATCH 6/9] fix compiler warnings --- MMCore/BufferAdapter.cpp | 30 ++++++++++++++++-------------- MMCore/Buffer_v2.cpp | 10 ++++++---- MMCore/Buffer_v2.h | 6 ++---- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/MMCore/BufferAdapter.cpp b/MMCore/BufferAdapter.cpp index ac12beb0a..6e8c3b4a3 100644 --- a/MMCore/BufferAdapter.cpp +++ b/MMCore/BufferAdapter.cpp @@ -94,7 +94,7 @@ bool BufferAdapter::Initialize(unsigned numChannels, unsigned width, unsigned he int ret = v2Buffer_->ReinitializeBuffer(v2Buffer_->GetMemorySizeMB()); if (ret != DEVICE_OK) return false; - } catch (const std::exception& ex) { + } catch (const std::exception&) { // Optionally log the exception return false; } @@ -146,7 +146,7 @@ long BufferAdapter::GetSize(long imageSize) const long BufferAdapter::GetFreeSize(long imageSize) const { if (useV2_) { - return v2Buffer_->GetFreeMemory() / imageSize; + return static_cast(v2Buffer_->GetFreeMemory()) / imageSize; } else { return circBuffer_->GetFreeSize(); } @@ -239,11 +239,13 @@ bool BufferAdapter::InsertMultiChannel(const unsigned char* buf, unsigned numCha if (useV2_) { // All the data needed to interpret the image is in the metadata // This function will copy data and metadata into the buffer - v2Buffer_->InsertData(buf, width * height * byteDepth *numChannels, &md); + int ret = v2Buffer_->InsertData(buf, width * height * byteDepth *numChannels, &md); + return ret == DEVICE_OK; } else { return circBuffer_->InsertMultiChannel(buf, numChannels, width, height, byteDepth, &md); } + } void* BufferAdapter::GetLastImageMD(unsigned channel, Metadata& md) const throw (CMMError) @@ -252,12 +254,12 @@ void* BufferAdapter::GetLastImageMD(unsigned channel, Metadata& md) const throw // In v2, we use PeekNextDataReadPointer (which does not advance the internal pointer) // Note: the v2 buffer is not channel aware, so the 'channel' parameter is ignored. // TODO implement the channel aware version - unsigned char* slotPtr = nullptr; - size_t dataSize = 0; - int ret = v2Buffer_->PeekNextDataReadPointer(&slotPtr, &dataSize, md); - if (ret != DEVICE_OK || slotPtr == nullptr) + const unsigned char* ptr = nullptr; + size_t imageDataSize = 0; + int ret = v2Buffer_->PeekNextDataReadPointer(&ptr, &imageDataSize, md); + if (ret != DEVICE_OK || ptr == nullptr) throw CMMError("V2 buffer is empty.", MMERR_CircularBufferEmpty); - return slotPtr; + return const_cast(ptr); // TODO: make sure calling code releases the slot after use } else { const mm::ImgBuffer* pBuf = circBuffer_->GetTopImageBuffer(channel); @@ -274,11 +276,11 @@ void* BufferAdapter::GetNthImageMD(unsigned long n, Metadata& md) const throw (C { if (useV2_) { size_t dataSize = 0; - const unsigned char* slotPtr = v2Buffer_->PeekDataReadPointerAtIndex(n, &dataSize, md); - if (slotPtr == nullptr) + const unsigned char* ptr = v2Buffer_->PeekDataReadPointerAtIndex(n, &dataSize, md); + if (ptr == nullptr) throw CMMError("V2 buffer does not contain enough data.", MMERR_CircularBufferEmpty); // Return a non-const pointer (caller must be careful with the const_cast) - return const_cast(slotPtr); + return const_cast(ptr); // TODO: make sure calling code releases the slot after use } else { const mm::ImgBuffer* pBuf = circBuffer_->GetNthFromTopImageBuffer(n); @@ -299,10 +301,10 @@ void* BufferAdapter::PopNextImageMD(unsigned channel, Metadata& md) throw (CMMEr // The caller is expected to call ReleaseDataReadPointer on the returned pointer once done. // TODO: make channel aware size_t dataSize = 0; - const unsigned char* slotPtr = v2Buffer_->PopNextDataReadPointer(md, &dataSize, false); - if (slotPtr == nullptr) + const unsigned char* ptr = v2Buffer_->PopNextDataReadPointer(md, &dataSize, false); + if (ptr == nullptr) throw CMMError("V2 buffer is empty.", MMERR_CircularBufferEmpty); - return const_cast(slotPtr); + return const_cast(ptr); // TODO: ensure that calling code releases the read pointer after use. } else { const mm::ImgBuffer* pBuf = circBuffer_->GetNextImageBuffer(channel); diff --git a/MMCore/Buffer_v2.cpp b/MMCore/Buffer_v2.cpp index 0c61ba83f..f0d492ac5 100644 --- a/MMCore/Buffer_v2.cpp +++ b/MMCore/Buffer_v2.cpp @@ -355,7 +355,7 @@ int DataBuffer::SetOverwriteData(bool overwrite) { * * The caller must release the slot using ReleaseDataSlot after writing is complete. */ -int DataBuffer::GetDataWriteSlot(size_t imageSize, size_t metadataSize, const unsigned char** imageDataPointer, const unsigned char** metadataPointer) { +int DataBuffer::GetDataWriteSlot(size_t imageSize, size_t metadataSize, unsigned char** imageDataPointer, unsigned char** metadataPointer) { // AllocateNextSlot allocates a slot for writing new data of variable size. // // First, it checks if there is a recently released slot start (from releasedSlots_). @@ -607,15 +607,13 @@ const unsigned char* DataBuffer::PopNextDataReadPointer(Metadata &md, size_t *im // Populate the metadata. if (header->metadataSize > 0) { const char* metaDataStart = reinterpret_cast(imageDataPointer + header->imageSize); - // Assuming Metadata::Restore takes a null-terminated string or similar. md.Restore(metaDataStart); } else { // If no metadata is available, clear the metadata object. md.Clear(); } - // Mark that this slot has been consumed. - // For this example, we simply move to the next slot. + // Consume this slot by advancing the index. currentSlotIndex_ = (currentSlotIndex_ + 1) % activeSlotsVector_.size(); // Unlock and return the pointer to the image data region. @@ -789,3 +787,7 @@ int DataBuffer::ReinitializeBuffer(unsigned int memorySizeMB) { return DEVICE_OK; } + +long DataBuffer::GetRemainingImageCount() const { + return static_cast(activeSlotsVector_.size()); +} diff --git a/MMCore/Buffer_v2.h b/MMCore/Buffer_v2.h index b00077ede..993d99390 100644 --- a/MMCore/Buffer_v2.h +++ b/MMCore/Buffer_v2.h @@ -260,9 +260,7 @@ class DataBuffer { * @param metadataPointer On success, receives a pointer to the metadata region. * @return DEVICE_OK on success. */ - int GetDataWriteSlot(size_t imageSize, size_t metadataSize, - const unsigned char** imageDataPointer, - const unsigned char** metadataPointer); + int GetDataWriteSlot(size_t imageSize, size_t metadataSize, unsigned char** imageDataPointer, unsigned char** metadataPointer); /** * Releases a write slot after data has been written. @@ -296,7 +294,7 @@ class DataBuffer { * @param waitForData If true, block until data becomes available. * @return Pointer to the start of the image data region, or nullptr if none available. */ - const unsigned char* PopNextDataReadPointer(Metadata &md, size_t *imageDataSize, bool waitForData); + const unsigned char* PopNextDataReadPointer(Metadata &md, size_t* imageDataSize, bool waitForData); /** * Peeks at the next unread data entry without consuming it. From 06fe9aed7b3258a2d748be5cd5a8c823119a42d5 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:45:27 -0800 Subject: [PATCH 7/9] cleanup --- MMCore/BufferAdapter.cpp | 33 +++++++++++++++++++++++++-------- MMCore/Buffer_v2.h | 2 +- MMCore/MMCore.cpp | 2 +- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/MMCore/BufferAdapter.cpp b/MMCore/BufferAdapter.cpp index 6e8c3b4a3..c0afad55e 100644 --- a/MMCore/BufferAdapter.cpp +++ b/MMCore/BufferAdapter.cpp @@ -1,14 +1,31 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: BufferAdapter.cpp +// PROJECT: Micro-Manager +// SUBSYSTEM: MMCore +//----------------------------------------------------------------------------- +// DESCRIPTION: Generic implementation of a buffer for storing image data and +// metadata. Provides thread-safe access for reading and writing +// with configurable overflow behavior. +//// +// COPYRIGHT: Henry Pinkard, 2025 +// +// LICENSE: This file is distributed under the "Lesser GPL" (LGPL) license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Henry Pinkard, 01/31/2025 + + #include "BufferAdapter.h" #include -// For demonstration, we assume DEVICE_OK and DEVICE_ERR macros are defined in MMCore.h or an included error header. -#ifndef DEVICE_OK - #define DEVICE_OK 0 -#endif -#ifndef DEVICE_ERR - #define DEVICE_ERR -1 -#endif - static std::string FormatLocalTime(std::chrono::time_point tp) { using namespace std::chrono; diff --git a/MMCore/Buffer_v2.h b/MMCore/Buffer_v2.h index 993d99390..2c937f20e 100644 --- a/MMCore/Buffer_v2.h +++ b/MMCore/Buffer_v2.h @@ -294,7 +294,7 @@ class DataBuffer { * @param waitForData If true, block until data becomes available. * @return Pointer to the start of the image data region, or nullptr if none available. */ - const unsigned char* PopNextDataReadPointer(Metadata &md, size_t* imageDataSize, bool waitForData); + const unsigned char* PopNextDataReadPointer(Metadata &md, size_t *imageDataSize, bool waitForData); /** * Peeks at the next unread data entry without consuming it. diff --git a/MMCore/MMCore.cpp b/MMCore/MMCore.cpp index 150316540..c19765ee0 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -141,7 +141,7 @@ CMMCore::CMMCore() : pluginManager_(new CPluginManager()), deviceManager_(new mm::DeviceManager()), pPostedErrorsLock_(NULL), - useV2Buffer_(true) + useV2Buffer_(false) { configGroups_ = new ConfigGroupCollection(); pixelSizeGroup_ = new PixelSizeConfigGroup(); From e468f0fbb09df3bbf247281e990147992431e21d Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:46:14 -0800 Subject: [PATCH 8/9] cleanup --- MMCore/BufferAdapter.h | 25 +++++++++++++++++++++++++ MMCore/Buffer_v2.cpp | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/MMCore/BufferAdapter.h b/MMCore/BufferAdapter.h index ca3627430..97aab6a9c 100644 --- a/MMCore/BufferAdapter.h +++ b/MMCore/BufferAdapter.h @@ -1,3 +1,28 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: BufferAdapter.h +// PROJECT: Micro-Manager +// SUBSYSTEM: MMCore +//----------------------------------------------------------------------------- +// DESCRIPTION: Generic implementation of a buffer for storing image data and +// metadata. Provides thread-safe access for reading and writing +// with configurable overflow behavior. +//// +// COPYRIGHT: Henry Pinkard, 2025 +// +// LICENSE: This file is distributed under the "Lesser GPL" (LGPL) license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Henry Pinkard, 01/31/2025 + + #ifndef BUFFERADAPTER_H #define BUFFERADAPTER_H diff --git a/MMCore/Buffer_v2.cpp b/MMCore/Buffer_v2.cpp index f0d492ac5..3e45d3381 100644 --- a/MMCore/Buffer_v2.cpp +++ b/MMCore/Buffer_v2.cpp @@ -37,7 +37,7 @@ Buffer Structure: - Memory management through reference counting: - Writers get exclusive ownership during writes - Readers can get shared read-only access - - Slots are recycled when all references are released + - Slots are recycled when all references are released (In non-overwriting mode) Data Access: - Two access patterns supported: From 93860a923339d6c2c1decd69dd70594d2c3f6153 Mon Sep 17 00:00:00 2001 From: Henry Pinkard <7969470+henrypinkard@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:50:17 -0800 Subject: [PATCH 9/9] remove comment --- MMCore/MMCore.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/MMCore/MMCore.cpp b/MMCore/MMCore.cpp index 150316540..95cefcbf8 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -3197,7 +3197,6 @@ void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes sizeMB << " MB"; try { - // TODO: need to store a flag about which buffer to use bufferAdapter_ = new BufferAdapter(useV2Buffer_, sizeMB); } catch (std::bad_alloc& ex)