diff --git a/MMCore/BufferAdapter.cpp b/MMCore/BufferAdapter.cpp new file mode 100644 index 000000000..c0afad55e --- /dev/null +++ b/MMCore/BufferAdapter.cpp @@ -0,0 +1,335 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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 + + +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_) { + v2Buffer_ = new DataBuffer(memorySizeMB); + } else { + circBuffer_ = new CircularBuffer(memorySizeMB); + } +} + +BufferAdapter::~BufferAdapter() +{ + if (useV2_) { + if (v2Buffer_) { + delete v2Buffer_; + } + } else { + if (circBuffer_) { + delete circBuffer_; + } + } +} + +const unsigned char* BufferAdapter::GetLastImage() const +{ + if (useV2_) { + 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::PopNextImage() +{ + if (useV2_) { + Metadata dummyMetadata; + return v2Buffer_->PopNextDataReadPointer(dummyMetadata, nullptr, false); + // TODO: ensure calling code releases the slot after use + } else { + 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_) { + 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&) { + // Optionally log the exception + return false; + } + return true; + } else { + return circBuffer_->Initialize(numChannels, width, height, bytesPerPixel); + } +} + +unsigned BufferAdapter::GetMemorySizeMB() const +{ + if (useV2_) { + return v2Buffer_->GetMemorySizeMB(); + } else { + return circBuffer_->GetMemorySizeMB(); + } +} + +long BufferAdapter::GetRemainingImageCount() const +{ + if (useV2_) { + return v2Buffer_->GetRemainingImageCount(); + } else { + return circBuffer_->GetRemainingImageCount(); + } +} + +void BufferAdapter::Clear() +{ + if (useV2_) { + v2Buffer_->ReleaseBuffer(); + } else { + circBuffer_->Clear(); + } + // Reset image counters when buffer is cleared + imageNumbers_.clear(); +} + +long BufferAdapter::GetSize(long imageSize) const +{ + if (useV2_) { + return v2Buffer_->GetMemorySizeMB() * 1024 * 1024 / imageSize; + } else { + return circBuffer_->GetSize(); + } + +} + +long BufferAdapter::GetFreeSize(long imageSize) const +{ + if (useV2_) { + return static_cast(v2Buffer_->GetFreeMemory()) / imageSize; + } else { + return circBuffer_->GetFreeSize(); + } +} + +bool BufferAdapter::Overflow() const +{ + if (useV2_) { + return v2Buffer_->Overflow(); + } else { + return circBuffer_->Overflow(); + } +} + +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 + 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) +{ + if (useV2_) { + // 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 + 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 const_cast(ptr); + // TODO: make sure calling code releases the slot after use + } else { + 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); + } + } +} + +void* BufferAdapter::GetNthImageMD(unsigned long n, Metadata& md) const throw (CMMError) +{ + if (useV2_) { + size_t dataSize = 0; + 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(ptr); + // TODO: make sure calling code releases the slot after use + } else { + 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); + } + } +} + +void* BufferAdapter::PopNextImageMD(unsigned channel, Metadata& md) throw (CMMError) +{ + if (useV2_) { + // 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* ptr = v2Buffer_->PopNextDataReadPointer(md, &dataSize, false); + if (ptr == nullptr) + throw CMMError("V2 buffer is empty.", MMERR_CircularBufferEmpty); + return const_cast(ptr); + // TODO: ensure that calling code releases the read pointer after use. + } else { + 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); + } + } +} diff --git a/MMCore/BufferAdapter.h b/MMCore/BufferAdapter.h new file mode 100644 index 000000000..97aab6a9c --- /dev/null +++ b/MMCore/BufferAdapter.h @@ -0,0 +1,198 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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 + +#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. +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. + * @param memorySizeMB Memory size for the buffer (in megabytes). + */ + BufferAdapter(bool useV2Buffer, unsigned int memorySizeMB); + ~BufferAdapter(); + + /** + * Get a pointer to the top (most recent) image. + * @return Pointer to image data, or nullptr if unavailable. + */ + 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* PopNextImage(); + + /** + * 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(); + + /** + * 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(long imageSize) const; + + /** + * Get the 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(long imageSize) 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; + + 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 new file mode 100644 index 000000000..3e45d3381 --- /dev/null +++ b/MMCore/Buffer_v2.cpp @@ -0,0 +1,793 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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 (In non-overwriting mode) + +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 GetDataWriteSlot+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 +#include // for std::this_thread::yield if needed +#include +#include +#include +#include +#include +#include + +// New internal header that precedes every slot's data. +struct BufferSlotRecord { + size_t imageSize; + size_t metadataSize; +}; + +/////////////////////////////////////////////////////////////////////////////// +// 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_(true) // The slot is created with write access held by default. +{ + // No readers are active and the slot starts with write access. +} + +BufferSlot::~BufferSlot() { + // No explicit cleanup required here. +} + +/** + * Returns the start offset (in bytes) of the slot from the start of the 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. + */ +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); +} + +/** + * 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); +} + + + +/////////////////////////////////////////////////////////////////////////////// +// 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 unsigned 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; +} + +/** + * Pack the data as [BufferSlotRecord][image data][serialized metadata] + */ +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; + + // 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(&imageDataPointer, metaSize > 0 ? static_cast(metaSize) : -1); +} + +/** + * 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(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); +} + +/** + * 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 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_). + // 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. + // Lock to ensure exclusive allocation. + 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--) { + 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 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 + totalSlotSize <= nextIt->first); + } else if (localCandidate + totalSlotSize > 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 + totalSlotSize <= 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(std::make_unique(localCandidate, totalSlotSize)); + BufferSlot* slot = activeSlotsVector_.back().get(); + activeSlotsByStart_[localCandidate] = slot; + *imageDataPointer = buffer_ + slot->GetStart() + sizeof(BufferSlotRecord); + *metadataPointer = *imageDataPointer + imageSize; + return DEVICE_OK; + } + } + + // If no released candidate fits, fall back to nextAllocOffset_. + size_t candidateStart = nextAllocOffset_; + 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; + *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 < 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)"); + } + + // Remove from both tracking structures + activeSlotsVector_.erase( + std::remove_if(activeSlotsVector_.begin(), activeSlotsVector_.end(), + [targetStart = it->first](const std::unique_ptr& slot) { + return slot->GetStart() == targetStart; + }), + activeSlotsVector_.end()); + it = activeSlotsByStart_.erase(it); + } + } + + activeSlotsVector_.push_back(std::make_unique(candidateStart, totalSlotSize)); + BufferSlot* newSlot = activeSlotsVector_.back().get(); + activeSlotsByStart_[candidateStart] = newSlot; + nextAllocOffset_ = candidateStart + totalSlotSize; + if (nextAllocOffset_ >= bufferSize_) { + nextAllocOffset_ = 0; + } + *imageDataPointer = buffer_ + newSlot->GetStart() + sizeof(BufferSlotRecord); + *metadataPointer = *imageDataPointer + imageSize; + 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(unsigned char** imageDataPointer, int actualMetadataBytes) { + if (imageDataPointer == nullptr || *imageDataPointer == nullptr) + return DEVICE_ERR; + + std::lock_guard lock(slotManagementMutex_); + + // 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_; + + // Locate the slot using the true header offset. + 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(); + + // 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; + } + } + + // Clear the externally provided image data pointer. + *imageDataPointer = nullptr; + + // Notify any waiting threads that new data is available. + dataCV_.notify_all(); + + return DEVICE_OK; +} + + +/** + * ReleaseSlot is called after a slot's content has been fully read. + * + * 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(const unsigned char** imageDataPointer) { + if (imageDataPointer == nullptr || *imageDataPointer == nullptr) + return DEVICE_ERR; + + std::unique_lock lock(slotManagementMutex_); + + // 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; + // Release the previously acquired read access. + slot->ReleaseReadAccess(); + + // 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."); + } + *imageDataPointer = nullptr; + return DEVICE_OK; +} + +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); + } + + // 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); + md.Restore(metaDataStart); + } else { + // If no metadata is available, clear the metadata object. + md.Clear(); + } + + // Consume this slot by advancing the index. + currentSlotIndex_ = (currentSlotIndex_ + 1) % activeSlotsVector_.size(); + + // Unlock and return the pointer to the image data region. + return imageDataPointer; +} + +unsigned int DataBuffer::GetMemorySizeMB() const { + // Convert bytes to MB (1 MB = 1048576 bytes) + return static_cast(bufferSize_ >> 20); +} + +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_); + 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; + + *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* imageDataSize, Metadata &md) { + std::unique_lock lock(slotManagementMutex_); + if (activeSlotsVector_.empty() || (currentSlotIndex_ + n) >= activeSlotsVector_.size()) { + throw std::runtime_error("Not enough unread data available."); + } + + // 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."); + + // 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); + } + + // 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; +} + +/** + * 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(const unsigned char** imageDataPointer) { + if (imageDataPointer == nullptr || *imageDataPointer == nullptr) + return DEVICE_ERR; + + std::lock_guard lock(slotManagementMutex_); + 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); + 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(); + + *imageDataPointer = 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 std::unique_ptr& 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 { + return static_cast(activeSlotsVector_.size()); +} diff --git a/MMCore/Buffer_v2.h b/MMCore/Buffer_v2.h new file mode 100644 index 000000000..2c937f20e --- /dev/null +++ b/MMCore/Buffer_v2.h @@ -0,0 +1,409 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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. +// +// 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. +// 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 +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "../MMDevice/ImageMetadata.h" +#include "../MMDevice/MMDevice.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * BufferSlot represents a contiguous slot in the DataBuffer that holds image + * 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 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. + */ + ~BufferSlot(); + + /** + * 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. + * + * @return The slot's length. + */ + std::size_t GetLength() const; + + /** + * 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 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 from the slot. + */ + void ClearDetails(); + + /** + * 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(); + + /** + * Releases exclusive write access. + * Clears the write flag and notifies waiting readers. + */ + void ReleaseWriteAccess(); + + /** + * 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(); + + /** + * Releases shared read access. + * Decrements the reader count. + * + * @return True if this call released the last active reader; false otherwise. + */ + bool ReleaseReadAccess(); + + /** + * 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; + + /** + * 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: + 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 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. + * + * 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: + /** + * 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(); + + /** + * 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(); + + /** + * 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 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 unsigned char* data, size_t dataSize, const Metadata* pMd); + + /** + * 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 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(unsigned char* dataDestination, size_t* imageDataSize, Metadata &md, bool waitForData); + + /** + * Sets whether the buffer should overwrite older data when full. + * + * @param overwrite True to enable overwriting, false otherwise. + * @return DEVICE_OK on success. + */ + int SetOverwriteData(bool overwrite); + + /** + * 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 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 imageSize, size_t metadataSize, unsigned char** imageDataPointer, unsigned char** metadataPointer); + + /** + * Releases a write slot after data has been written. + * + * @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(unsigned char** imageDataPointer, int actualMetadataBytes = -1); + + /** + * Releases read access for the image data region after its content has been read. + * + * @param imageDataPointer Pointer previously obtained from reading routines. + * @return DEVICE_OK on success. + */ + int ReleaseDataReadPointer(const unsigned char** imageDataPointer); + + /** + * 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 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 start of the image data region, or nullptr if none available. + */ + const unsigned char* PopNextDataReadPointer(Metadata &md, size_t *imageDataSize, bool waitForData); + + /** + * 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 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(const unsigned char** imageDataPointer, size_t* imageDataSize, Metadata &md); + + /** + * Peeks at the nth unread data entry without consuming it. + * (n = 0 is equivalent to PeekNextDataReadPointer.) + * + * @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* imageDataSize, Metadata &md); + + /** + * Releases read access that was acquired by a peek. + * + * @param imageDataPointer Pointer previously obtained from a peek. + * @return DEVICE_OK on success. + */ + int ReleasePeekDataReadPointer(const unsigned char** imageDataPointer); + + /** + * Returns the total buffer memory size (in MB). + * + * @return Buffer size in MB. + */ + unsigned int GetMemorySizeMB() const; + + /** + * Returns the number of occupied slots in the buffer. + * + * @return Occupied slot count. + */ + size_t GetOccupiedSlotCount() const; + + /** + * Returns the total occupied memory (in bytes). + * + * @return Sum of active slot lengths. + */ + size_t GetOccupiedMemory() const; + + /** + * Returns the amount of free memory (in bytes) remaining. + * + * @return Free byte count. + */ + size_t GetFreeMemory() const; + + /** + * 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 slots in the buffer. + * + * @return Unread slot count. + */ + long GetRemainingImageCount() const; + + /** + * Reinitializes the DataBuffer by clearing its structures, releasing the current + * buffer, and allocating a new one. + * + * @param memorySizeMB New buffer size (in MB). + * @return DEVICE_OK on success. + * @throws std::runtime_error if any slot is still actively in use. + */ + int ReinitializeBuffer(unsigned int memorySizeMB); + +private: + // Pointer to the allocated block. + unsigned char* buffer_; + // Total allocated size in bytes. + size_t bufferSize_; + + // Whether to overwrite old data when full. + bool overwriteWhenFull_; + + // Overflow flag (set if insert fails due to full buffer). + bool overflow_; + + // Data structures used to track active slots. + std::vector> activeSlotsVector_; + std::map activeSlotsByStart_; + std::vector releasedSlots_; + + // Next free offset within the buffer. + size_t nextAllocOffset_; + + // Index tracking the next slot for read. + size_t currentSlotIndex_; + + // Synchronization primitives for slot management. + 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/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 19dd2b0cb..26919883a 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -137,10 +137,11 @@ CMMCore::CMMCore() : properties_(0), externalCallback_(0), pixelSizeGroup_(0), - cbuf_(0), + 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; - cbuf_ = new CircularBuffer(seqBufMegabytes); + bufferAdapter_ = new BufferAdapter(useV2Buffer_, seqBufMegabytes); nullAffine_ = new std::vector(6); for (int i = 0; i < 6; i++) { @@ -181,7 +182,7 @@ CMMCore::~CMMCore() delete callback_; delete configGroups_; delete properties_; - delete cbuf_; + delete bufferAdapter_; delete pixelSizeGroup_; delete pPostedErrorsLock_; @@ -2830,12 +2831,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 +2875,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 +2926,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 +2978,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 +3079,7 @@ void* CMMCore::getLastImage() throw (CMMError) } } - unsigned char* pBuf = const_cast(cbuf_->GetTopImage()); + unsigned char* pBuf = const_cast(bufferAdapter_->GetLastImage()); if (pBuf != 0) return pBuf; else @@ -3094,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 = cbuf_->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); } /** @@ -3135,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 = cbuf_->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); } /** @@ -3159,7 +3146,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_->PopNextImage()); if (pBuf != 0) return pBuf; else @@ -3177,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 = cbuf_->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); } /** @@ -3203,7 +3183,7 @@ void* CMMCore::popNextImageMD(Metadata& md) throw (CMMError) */ void CMMCore::clearCircularBuffer() throw (CMMError) { - cbuf_->Clear(); + bufferAdapter_->Clear(); } /** @@ -3212,12 +3192,12 @@ 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); + bufferAdapter_ = new BufferAdapter(useV2Buffer_, sizeMB); } catch (std::bad_alloc& ex) { @@ -3226,7 +3206,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 +3217,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 +3230,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 +3239,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 +3251,9 @@ unsigned CMMCore::getCircularBufferMemoryFootprint() */ long CMMCore::getRemainingImageCount() { - if (cbuf_) + if (bufferAdapter_) { - return cbuf_->GetRemainingImageCount(); + return bufferAdapter_->GetRemainingImageCount(); } return 0; } @@ -3283,9 +3263,12 @@ long CMMCore::getRemainingImageCount() */ long CMMCore::getBufferTotalCapacity() { - if (cbuf_) + if (bufferAdapter_) { - return cbuf_->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; } @@ -3297,9 +3280,11 @@ long CMMCore::getBufferTotalCapacity() */ long CMMCore::getBufferFreeCapacity() { - if (cbuf_) + if (bufferAdapter_) { - return cbuf_->GetFreeSize(); + // Compute image size from the current camera parameters. + long imageSize = getImageWidth() * getImageHeight() * getBytesPerPixel(); + return bufferAdapter_->GetFreeSize(imageSize); } return 0; } @@ -3309,7 +3294,7 @@ long CMMCore::getBufferFreeCapacity() */ bool CMMCore::isBufferOverflowed() const { - return cbuf_->Overflow(); + return bufferAdapter_->Overflow(); } /** @@ -4412,7 +4397,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 +4476,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 +4535,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..9e1f63394 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 @@ -88,7 +89,6 @@ class CPluginManager; -class CircularBuffer; class ConfigGroupCollection; class CoreCallback; class CorePropertyCollection; @@ -658,7 +658,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_; @@ -671,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