diff --git a/MMCore/BufferManager.cpp b/MMCore/BufferManager.cpp new file mode 100644 index 000000000..5eb6640d0 --- /dev/null +++ b/MMCore/BufferManager.cpp @@ -0,0 +1,402 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: BufferManager.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 "BufferManager.h" +#include + + +BufferManager::BufferManager(bool useNewDataBuffer, unsigned int memorySizeMB) + : useNewDataBuffer_(useNewDataBuffer), circBuffer_(nullptr), newDataBuffer_(nullptr) +{ + if (useNewDataBuffer_.load()) { + newDataBuffer_ = new DataBuffer(memorySizeMB); + } else { + circBuffer_ = new CircularBuffer(memorySizeMB); + } +} + +BufferManager::~BufferManager() +{ + if (useNewDataBuffer_.load()) { + if (newDataBuffer_) { + delete newDataBuffer_; + } + } else { + if (circBuffer_) { + delete circBuffer_; + } + } +} + +void BufferManager::Clear() { + if (useNewDataBuffer_.load()) { + newDataBuffer_->Clear(); + } else { + circBuffer_->Clear(); + } +} + +void BufferManager::ForceReset() { + if (useNewDataBuffer_.load()) { + // This is dangerous with the NewDataBuffer because there may be pointers into the buffer's memory + newDataBuffer_->ReinitializeBuffer(GetMemorySizeMB(), true); + } else { + // This is not dangerous with the circular buffer because it does not give out pointers to its memory + circBuffer_->Initialize(circBuffer_->NumChannels(), circBuffer_->Width(), + circBuffer_->Height(), circBuffer_->Depth()); + } +} + +bool BufferManager::InitializeCircularBuffer(unsigned int numChannels, unsigned int width, unsigned int height, unsigned int depth) { + if (!useNewDataBuffer_.load()) { + return circBuffer_->Initialize(numChannels, width, height, depth); + } + return false; +} + +void BufferManager::ReallocateBuffer(unsigned int memorySizeMB) { + if (useNewDataBuffer_.load()) { + int numOutstanding = newDataBuffer_->NumOutstandingSlots(); + if (numOutstanding > 0) { + throw CMMError("Cannot reallocate NewDataBuffer: " + std::to_string(numOutstanding) + " outstanding active slot(s) detected."); + } + delete newDataBuffer_; + newDataBuffer_ = new DataBuffer(memorySizeMB); + } else { + delete circBuffer_; + circBuffer_ = new CircularBuffer(memorySizeMB); + } +} + +const void* BufferManager::GetLastData() +{ + if (useNewDataBuffer_.load()) { + Metadata dummyMetadata; + // NOTE: ensure calling code releases the slot after use + return newDataBuffer_->PeekDataReadPointerAtIndex(0, dummyMetadata); + } else { + return circBuffer_->GetTopImage(); + } +} + +const void* BufferManager::PopNextData() +{ + if (useNewDataBuffer_.load()) { + Metadata dummyMetadata; + // NOTE: ensure calling code releases the slot after use + return newDataBuffer_->PopNextDataReadPointer(dummyMetadata, false); + } else { + return circBuffer_->PopNextImage(); + } +} + +long BufferManager::GetRemainingDataCount() const +{ + if (useNewDataBuffer_.load()) { + return newDataBuffer_->GetActiveSlotCount(); + } else { + return circBuffer_->GetRemainingImageCount(); + } +} + +unsigned BufferManager::GetMemorySizeMB() const { + if (useNewDataBuffer_.load()) { + return newDataBuffer_->GetMemorySizeMB(); + } else { + return circBuffer_->GetMemorySizeMB(); + } +} + +unsigned BufferManager::GetFreeSizeMB() const { + if (useNewDataBuffer_.load()) { + return (unsigned) newDataBuffer_->GetFreeMemory() / 1024 / 1024; + } else { + return circBuffer_->GetFreeSize() * circBuffer_->GetImageSizeBytes() / 1024 / 1024; + } +} + +bool BufferManager::Overflow() const +{ + if (useNewDataBuffer_.load()) { + return newDataBuffer_->Overflow(); + } else { + return circBuffer_->Overflow(); + } +} + +/** + * @deprecated Use InsertData() instead + */ +int BufferManager::InsertImage(const char* callerLabel, const unsigned char* buf, unsigned width, unsigned height, + unsigned byteDepth, Metadata* pMd) { + return InsertMultiChannel(callerLabel, buf, 1, width, height, byteDepth, pMd); +} + +/** + * @deprecated Use InsertData() instead + */ +int BufferManager::InsertMultiChannel(const char* callerLabel, const unsigned char* buf, + unsigned numChannels, unsigned width, unsigned height, unsigned byteDepth, Metadata* pMd) { + + // Initialize metadata with either provided metadata or create empty + Metadata md = (pMd != nullptr) ? *pMd : Metadata(); + + if (useNewDataBuffer_.load()) { + // All the data needed to interpret the image is in the metadata + // This function will copy data and metadata into the buffer + return newDataBuffer_->InsertData(buf, width * height * byteDepth * numChannels, &md, callerLabel); + } else { + return circBuffer_->InsertMultiChannel(buf, numChannels, width, height, + byteDepth, &md) ? DEVICE_OK : DEVICE_BUFFER_OVERFLOW; + } +} + +int BufferManager::InsertData(const char* callerLabel, const unsigned char* buf, size_t dataSize, Metadata* pMd) { + // Initialize metadata with either provided metadata or create empty + Metadata md = (pMd != nullptr) ? *pMd : Metadata(); + + if (!useNewDataBuffer_.load()) { + throw CMMError("InsertData() not supported with circular buffer. Must use NewDataBuffer."); + } + // All the data needed to interpret the image should be in the metadata + // This function will copy data and metadata into the buffer + return newDataBuffer_->InsertData(buf, dataSize, &md, callerLabel); +} + + +const void* BufferManager::GetLastDataMD(Metadata& md) const +{ + return GetLastDataMD(0, 0, md); // single channel size doesnt matter here +} + +const void* BufferManager::GetLastDataMD(unsigned channel, unsigned singleChannelSizeBytes, Metadata& md) const throw (CMMError) +{ + if (useNewDataBuffer_.load()) { + const void* basePtr = newDataBuffer_->PeekLastDataReadPointer(md); + if (basePtr == nullptr) + throw CMMError("NewDataBuffer is empty.", MMERR_CircularBufferEmpty); + // Add multiples of the number of bytes to get the channel pointer + basePtr = static_cast(basePtr) + channel * singleChannelSizeBytes; + return basePtr; + } else { + const mm::ImgBuffer* pBuf = circBuffer_->GetTopImageBuffer(channel); + if (pBuf != nullptr) { + md = pBuf->GetMetadata(); + return pBuf->GetPixels(); + } else { + throw CMMError("Circular buffer is empty.", MMERR_CircularBufferEmpty); + } + } +} + +const void* BufferManager::GetNthDataMD(unsigned long n, Metadata& md) const throw (CMMError) +{ + if (useNewDataBuffer_.load()) { + // NOTE: make sure calling code releases the slot after use. + return newDataBuffer_->PeekDataReadPointerAtIndex(n, md); + } else { + const mm::ImgBuffer* pBuf = circBuffer_->GetNthFromTopImageBuffer(n); + if (pBuf != nullptr) { + md = pBuf->GetMetadata(); + return pBuf->GetPixels(); + } else { + throw CMMError("Circular buffer is empty.", MMERR_CircularBufferEmpty); + } + } +} + +const void* BufferManager::PopNextDataMD(Metadata& md) throw (CMMError) +{ + return PopNextDataMD(0, 0, md); +} + +/** + * @deprecated Use PopNextDataMD() without channel parameter instead. + * The NewDataBuffer is data type agnostic + */ +const void* BufferManager::PopNextDataMD(unsigned channel, + unsigned singleChannelSizeBytes, Metadata& md) throw (CMMError) +{ + if (useNewDataBuffer_.load()) { + const void* basePtr = newDataBuffer_->PopNextDataReadPointer(md, false); + if (basePtr == nullptr) + throw CMMError("NewDataBuffer is empty.", MMERR_CircularBufferEmpty); + + // Add multiples of the number of bytes to get the channel pointer + basePtr = static_cast(basePtr) + channel * singleChannelSizeBytes; + + return basePtr; + } else { + const mm::ImgBuffer* pBuf = circBuffer_->GetNextImageBuffer(channel); + if (pBuf != nullptr) { + md = pBuf->GetMetadata(); + return pBuf->GetPixels(); + } else { + throw CMMError("Circular buffer is empty.", MMERR_CircularBufferEmpty); + } + } +} + +int BufferManager::EnableNewDataBuffer(bool enable) { + // Don't do anything if we're already in the requested state + if (enable == useNewDataBuffer_.load()) { + return DEVICE_OK; + } + + // Create new buffer of requested type with same memory size + unsigned memorySizeMB = GetMemorySizeMB(); + + try { + if (enable) { + // Switch to V2 buffer + DataBuffer* newBuffer = new DataBuffer(memorySizeMB); + delete circBuffer_; + circBuffer_ = nullptr; + newDataBuffer_ = newBuffer; + } else { + // Switch to circular buffer + int numOutstanding = newDataBuffer_->NumOutstandingSlots(); + if (numOutstanding > 0) { + throw CMMError("Cannot switch to circular buffer: " + std::to_string(numOutstanding) + " outstanding active slot(s) detected."); + } + CircularBuffer* newBuffer = new CircularBuffer(memorySizeMB); + delete newDataBuffer_; + newDataBuffer_ = nullptr; + circBuffer_ = newBuffer; + } + + + useNewDataBuffer_.store(enable); + return DEVICE_OK; + } catch (const std::exception&) { + // If allocation fails, keep the existing buffer + return DEVICE_ERR; + } +} + +bool BufferManager::IsUsingNewDataBuffer() const { + return useNewDataBuffer_.load(); +} + +int BufferManager::ReleaseReadAccess(const void* ptr) { + if (useNewDataBuffer_.load() && ptr) { + return newDataBuffer_->ReleaseDataReadPointer(ptr); + } + return DEVICE_ERR; +} + +unsigned BufferManager::GetDataSize(const void* ptr) const { + if (!useNewDataBuffer_.load()) + return circBuffer_->GetImageSizeBytes(); + else + return static_cast(newDataBuffer_->GetDatumSize(ptr)); +} + +int BufferManager::SetOverwriteData(bool overwrite) { + if (useNewDataBuffer_.load()) { + return newDataBuffer_->SetOverwriteData(overwrite); + } else { + return circBuffer_->SetOverwriteData(overwrite); + } +} + +int BufferManager::AcquireWriteSlot(const char* deviceLabel, size_t dataSize, size_t additionalMetadataSize, + void** dataPointer, void** additionalMetadataPointer, Metadata* pInitialMetadata) { + if (!useNewDataBuffer_.load()) { + // Not supported for circular buffer + return DEVICE_ERR; + } + + // Initialize metadata with either provided metadata or create empty + Metadata md = (pInitialMetadata != nullptr) ? *pInitialMetadata : Metadata(); + + std::string serializedMetadata = md.Serialize(); + int ret = newDataBuffer_->AcquireWriteSlot(dataSize, additionalMetadataSize, + dataPointer, additionalMetadataPointer, serializedMetadata, deviceLabel); + return ret; +} + +int BufferManager::FinalizeWriteSlot(const void* imageDataPointer, size_t actualMetadataBytes) { + if (!useNewDataBuffer_.load()) { + // Not supported for circular buffer + return DEVICE_ERR; + } + return newDataBuffer_->FinalizeWriteSlot(imageDataPointer, actualMetadataBytes); +} + +void BufferManager::ExtractMetadata(const void* dataPtr, Metadata& md) const { + if (!useNewDataBuffer_.load()) { + throw CMMError("ExtractMetadata is only supported with NewDataBuffer enabled"); + } + + if (newDataBuffer_ == nullptr) { + throw CMMError("NewDataBuffer is null"); + } + + int result = newDataBuffer_->ExtractCorrespondingMetadata(dataPtr, md); + if (result != DEVICE_OK) { + throw CMMError("Failed to extract metadata"); + } +} + +const void* BufferManager::GetLastDataFromDevice(const std::string& deviceLabel) throw (CMMError) { + if (!useNewDataBuffer_.load()) { + throw CMMError("NewDataBuffer must be enabled for device-specific data access"); + } + Metadata md; + return GetLastDataMDFromDevice(deviceLabel, md); +} + +const void* BufferManager::GetLastDataMDFromDevice(const std::string& deviceLabel, Metadata& md) throw (CMMError) { + if (!useNewDataBuffer_.load()) { + throw CMMError("NewDataBuffer must be enabled for device-specific data access"); + } + + const void* basePtr = newDataBuffer_->PeekLastDataReadPointerFromDevice(deviceLabel, md); + if (basePtr == nullptr) { + throw CMMError("No data found for device: " + deviceLabel, MMERR_InvalidContents); + } + return basePtr; +} + +bool BufferManager::IsPointerInNewDataBuffer(const void* ptr) const { + if (!useNewDataBuffer_.load()) { + return false; + } + + if (newDataBuffer_ == nullptr) { + return false; + } + + return newDataBuffer_->IsPointerInBuffer(ptr); +} + +bool BufferManager::GetOverwriteData() const { + if (useNewDataBuffer_.load()) { + return newDataBuffer_->GetOverwriteData(); + } else { + return circBuffer_->GetOverwriteData(); + } +} diff --git a/MMCore/BufferManager.h b/MMCore/BufferManager.h new file mode 100644 index 000000000..12908da1f --- /dev/null +++ b/MMCore/BufferManager.h @@ -0,0 +1,316 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: BufferManager.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 BUFFERMANAGER_H +#define BUFFERMANAGER_H + +#include "CircularBuffer.h" +#include "NewDataBuffer.h" +#include "../MMDevice/MMDevice.h" +#include +#include +#include +#include + +/** + * BufferManager provides a generic interface for managing data buffers in MMCore. + * + * This class is designed to handle arbitrary data in a format-agnostic way, with + * metadata for interpretation handled separately. Data can be stored and retrieved + * with associated metadata that describes how to interpret the raw bytes. + * + * The implementation supports two buffer types: + * - The newer NewDataBuffer that handles generic data + * - The legacy circular buffer (for backwards compatibility) that assumes image data + * + * While the preferred usage is through the generic data methods (InsertData, + * GetLastData, etc.), legacy image-specific methods are maintained for compatibility + * with existing code that assumes images captured on a camera. + */ +class BufferManager { +public: + static const char* const DEFAULT_NEW_DATA_BUFFER_NAME; + + /** + * Constructor. + * @param useNewDataBuffer Set to true to use the new DataBuffer (NewDataBuffer); false to use CircularBuffer. + * @param memorySizeMB Memory size for the buffer (in megabytes). + */ + BufferManager(bool useNewDataBuffer, unsigned int memorySizeMB); + ~BufferManager(); + + /** + * Reinitialize the buffer. + * @param memorySizeMB Memory size for the buffer (in megabytes). + */ + void ReallocateBuffer(unsigned int memorySizeMB); + + /** + * Enable or disable NewDataBuffer usage. + * @param enable Set to true to use NewDataBuffer, false to use CircularBuffer. + * @return true if the switch was successful, false otherwise. + */ + int EnableNewDataBuffer(bool enable); + + /** + * Get a pointer to the top (most recent) image. + * @return Pointer to image data, or nullptr if unavailable. + */ + const void* GetLastData(); + + /** + * Get a pointer to the next image from the buffer. + * @return Pointer to image data, or nullptr if unavailable. + */ + const void* PopNextData(); + + /** + * Get the memory size of the buffer in megabytes. + * @return Memory size in MB. + */ + unsigned GetMemorySizeMB() const; + + /** + * Get the free capacity of the buffer. + * @return Free capacity in MB. + */ + unsigned GetFreeSizeMB() const; + + /** + * Get the remaining data entry (e.g. image) count in the buffer. + * @return Number of remaining data entries. + */ + long GetRemainingDataCount() const; + + /** + * 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 pMd Metadata associated with the image. + * @return DEVICE_OK on success, DEVICE_ERR on error. + * @deprecated This method assumes specific image data format. It is provided for backwards + * compatibility with with the circular buffer, which assumes images captured on a camera. + * Use InsertData() instead, which provides format-agnostic data handling with metadata for interpretation. + */ + int InsertImage(const char* deviceLabel, const unsigned char *buf, + unsigned width, unsigned height, unsigned byteDepth, + Metadata *pMd); + + + /** + * Insert a multi-channel image into the buffer + * @param caller The device inserting the image. + * @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 DEVICE_OK on success, DEVICE_ERR on error. + * @deprecated This method is not preferred for the NewDataBuffer. Use InsertData() instead. + * This method assumes specific image data format. It is provided for backwards + * compatibility with with the circular buffer, which assumes images captured on a camera. + */ + int InsertMultiChannel(const char* deviceLabel, const unsigned char *buf, + unsigned numChannels, unsigned width, unsigned height, + unsigned byteDepth, Metadata *pMd); + + /** + * Insert data into the buffer. This method is agnostic to the format of the data + * It is the caller's responsibility to ensure that appropriate metadata is provided + * for interpretation of the data (e.g. for an image: width, height, byteDepth, nComponents) + * + * @param callerLabel The label of the device inserting the data. + * @param buf The data to insert. + * @param dataSize The size of the data to insert. + * @param pMd Metadata associated with the data. + * @return DEVICE_OK on success, DEVICE_ERR on error. + */ + int InsertData(const char* callerLabel, const unsigned char* buf, size_t dataSize, Metadata *pMd); + + + /** + * Check if the buffer is overflowed. + * @return True if overflowed, false otherwise. + */ + bool Overflow() const; + + const void* GetNthDataMD(unsigned long n, Metadata& md) const; + + // Channels are not directly supported in NewDataBuffer, these are for backwards compatibility + // with circular buffer + + /** + * @deprecated This method is not preferred for the NewDataBuffer. Use GetLastDataMD() without channel parameter instead. + * The NewDataBuffer is data type agnostic + */ + const void* GetLastDataMD(unsigned channel, unsigned singleChannelSizeBytes, Metadata& md) const; + /** + * @deprecated This method is not preferred for the NewDataBuffer. Use PopNextDataMD() without channel parameter instead. + * The NewDataBuffer is data type agnostic + */ + const void* PopNextDataMD(unsigned channel, unsigned singleChannelSizeBytes,Metadata& md); + + const void* GetLastDataMD(Metadata& md) const; + const void* PopNextDataMD(Metadata& md); + + /** + * Check if this manager is using the NewDataBuffer implementation. + * @return true if using NewDataBuffer, false if using circular buffer. + */ + bool IsUsingNewDataBuffer() const; + + /** + * Release a pointer obtained from the buffer. + * This is required when using the NewDataBuffer implementation. + * @param ptr The pointer to release. + * @return DEVICE_OK on success, DEVICE_ERR on error. + */ + int ReleaseReadAccess(const void* ptr); + + // Get the size of just the data in this slot + unsigned GetDataSize(const void* ptr) const; + + /** + * Configure whether to overwrite old data when buffer is full. + * @param overwrite If true, overwrite old data when buffer is full. + * @return DEVICE_OK on success, DEVICE_ERR on error. + */ + int SetOverwriteData(bool overwrite); + + /** + * Acquires a write slot large enough to hold the data and metadata. + * @param deviceLabel The label of the device requesting the write slot + * @param dataSize The number of bytes reserved for image or other primary data. + * @param additionalMetadataSize The maximum number of bytes reserved for metadata. + * @param dataPointer On success, receives a pointer to the image data region. + * @param additionalMetadataPointer On success, receives a pointer to the metadata region. + * @param pInitialMetadata Optionally, a pointer to a metadata object whose contents should be pre‐written + * @return DEVICE_OK on success, DEVICE_ERR on error. + */ + int AcquireWriteSlot(const char* deviceLabel, size_t dataSize, size_t additionalMetadataSize, + void** dataPointer, void** additionalMetadataPointer, Metadata* pInitialMetadata); + + /** + * Finalizes (releases) a write slot after data has been written. + * @param dataPointer Pointer previously obtained from AcquireWriteSlot. + * @param actualMetadataBytes The actual number of metadata bytes written. + * @return DEVICE_OK on success, DEVICE_ERR on error. + */ + int FinalizeWriteSlot(const void* imageDataPointer, size_t actualMetadataBytes); + + /** + * Extracts metadata for a given data pointer. + * @param dataPtr Pointer to the data. + * @param md Metadata object to populate. + * @throws CMMError if NewDataBuffer is not enabled or extraction fails. + */ + void ExtractMetadata(const void* dataPtr, Metadata& md) const; + + /** + * Add basic metadata tags for the data source device. + * @param deviceLabel The label of the device inserting the metadata + * @param pMd Optional pointer to existing metadata to merge with + * @return Metadata object containing merged metadata + */ + Metadata AddDeviceLabel(const char* deviceLabel, const Metadata* pMd); + + /** + * Get the last data inserted by a specific device. + * @param deviceLabel The label of the device to get the data from. + * @return Pointer to the data. + * @throws CMMError if no data is found or NewDataBuffer is not enabled. + */ + const void* GetLastDataFromDevice(const std::string& deviceLabel); + + /** + * Get the last data and metadata inserted by a specific device. + * @param deviceLabel The label of the device to get the data from. + * @param md Metadata object to populate. + * @return Pointer to the data. + * @throws CMMError if no data is found or NewDataBuffer is not enabled. + */ + const void* GetLastDataMDFromDevice(const std::string& deviceLabel, Metadata& md); + + /** + * Check if a pointer is currently managed by the buffer. + * @param ptr The pointer to check. + * @return true if the pointer is in the buffer, false otherwise. + */ + bool IsPointerInNewDataBuffer(const void* ptr) const; + + /** + * Get whether the buffer is in overwrite mode. + * @return true if buffer overwrites old data when full, false otherwise. + */ + bool GetOverwriteData() const; + + /** + * Clear the buffer, discarding all data. + */ + void Clear(); + + /** + * Reset the buffer, discarding all data that is not currently held externally. + * When using the NewDataBuffer, this is a dangerous operation because there may be pointers + * into the buffer's memory that will not longer be valid. It can be used to reset the buffer + * without having to restart the application, but its use likely indicates a bug in the + * application or device adapter that is not properly releasing the buffer's memory. + */ + void ForceReset(); + + /** + * Initialize the circular buffer. This has no effect if NewDataBuffer is enabled. This method + * is for backwards compatibility with the circular buffer, which requires knowledge of the + * dimensions of the image to be captured. + * @param numChannels Number of channels in the image. + * @param width Image width. + * @param height Image height. + * @param depth Bytes per pixel. + * @return true if the buffer was initialized, false otherwise. + */ + MM_DEPRECATED(bool InitializeCircularBuffer(unsigned int numChannels, unsigned int width, unsigned int height, unsigned int depth)); + +private: + + std::atomic useNewDataBuffer_; + CircularBuffer* circBuffer_; + DataBuffer* newDataBuffer_; + + + /** + * Get the metadata tags attached to device caller, and merge them with metadata + * in pMd (if not null). Returns a metadata object. + * @param caller The device inserting the metadata + * @param pMd Optional pointer to existing metadata to merge with + * @return Metadata object containing merged metadata + */ + Metadata AddCallerMetadata(const MM::Device* caller, const Metadata* pMd); +}; + +#endif // BUFFERMANAGER_H \ No newline at end of file diff --git a/MMCore/CircularBuffer.cpp b/MMCore/CircularBuffer.cpp index 2453bda49..6e9d107ff 100644 --- a/MMCore/CircularBuffer.cpp +++ b/MMCore/CircularBuffer.cpp @@ -62,6 +62,7 @@ CircularBuffer::CircularBuffer(unsigned int memorySizeMB) : saveIndex_(0), memorySizeMB_(memorySizeMB), overflow_(false), + overwriteData_(false), threadPool_(std::make_shared()), tasksMemCopy_(std::make_shared(threadPool_)) { @@ -69,11 +70,15 @@ CircularBuffer::CircularBuffer(unsigned int memorySizeMB) : CircularBuffer::~CircularBuffer() {} +int CircularBuffer::SetOverwriteData(bool overwrite) { + MMThreadGuard guard(g_bufferLock); + overwriteData_ = overwrite; + return DEVICE_OK; +} + 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; try @@ -97,7 +102,7 @@ bool CircularBuffer::Initialize(unsigned channels, unsigned int w, unsigned int // calculate the size of the entire buffer array once all images get allocated // the actual size at the time of the creation is going to be less, because // images are not allocated until pixels become available - unsigned long frameSizeBytes = width_ * height_ * pixDepth_ * numChannels_; + unsigned long frameSizeBytes = GetImageSizeBytes(); unsigned long cbSize = (unsigned long) ((memorySizeMB_ * bytesInMB) / frameSizeBytes); if (cbSize == 0) @@ -138,8 +143,6 @@ void CircularBuffer::Clear() insertIndex_=0; saveIndex_=0; overflow_ = false; - startTime_ = std::chrono::steady_clock::now(); - imageNumbers_.clear(); } unsigned long CircularBuffer::GetSize() const @@ -164,61 +167,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); @@ -234,14 +188,17 @@ bool CircularBuffer::InsertMultiChannel(const unsigned char* pixArray, unsigned bool overflowed = (insertIndex_ - saveIndex_) >= static_cast(frameArray_.size()); if (overflowed) { - overflow_ = true; - return false; + if (overwriteData_) { + Clear(); + } else { + overflow_ = true; + return false; + } } } 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 +273,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..9d077e55a 100644 --- a/MMCore/CircularBuffer.h +++ b/MMCore/CircularBuffer.h @@ -55,6 +55,8 @@ class CircularBuffer CircularBuffer(unsigned int memorySizeMB); ~CircularBuffer(); + int SetOverwriteData(bool overwrite); + unsigned GetMemorySizeMB() const { return memorySizeMB_; } bool Initialize(unsigned channels, unsigned int xSize, unsigned int ySize, unsigned int pixDepth); @@ -65,13 +67,11 @@ class CircularBuffer unsigned int Width() const {MMThreadGuard guard(g_bufferLock); return width_;} unsigned int Height() const {MMThreadGuard guard(g_bufferLock); return height_;} unsigned int Depth() const {MMThreadGuard guard(g_bufferLock); return pixDepth_;} + unsigned int NumChannels() const {MMThreadGuard guard(g_bufferLock); return numChannels_;} - 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; @@ -80,6 +80,13 @@ class CircularBuffer bool Overflow() {MMThreadGuard guard(g_bufferLock); return overflow_;} + unsigned GetImageSizeBytes() const { + MMThreadGuard guard(g_bufferLock); + return width_ * height_ * pixDepth_ * numChannels_; + } + + bool GetOverwriteData() const { return overwriteData_; } + mutable MMThreadLock g_bufferLock; mutable MMThreadLock g_insertLock; @@ -88,8 +95,6 @@ class CircularBuffer unsigned int height_; unsigned int pixDepth_; long imageCounter_; - std::chrono::time_point startTime_; - std::map imageNumbers_; // Invariants: // 0 <= saveIndex_ <= insertIndex_ @@ -100,10 +105,12 @@ class CircularBuffer unsigned long memorySizeMB_; unsigned int numChannels_; bool overflow_; + bool overwriteData_; std::vector frameArray_; std::shared_ptr threadPool_; std::shared_ptr tasksMemCopy_; + }; #if defined(__GNUC__) && !defined(__clang__) diff --git a/MMCore/CoreCallback.cpp b/MMCore/CoreCallback.cpp index 572f9a0ce..77946b31e 100644 --- a/MMCore/CoreCallback.cpp +++ b/MMCore/CoreCallback.cpp @@ -31,12 +31,14 @@ #include "CircularBuffer.h" #include "CoreCallback.h" #include "DeviceManager.h" +#include "BufferManager.h" #include #include #include #include #include +#include CoreCallback::CoreCallback(CMMCore* c) : @@ -45,6 +47,8 @@ CoreCallback::CoreCallback(CMMCore* c) : { assert(core_); pValueChangeLock_ = new MMThreadLock(); + + } @@ -205,43 +209,6 @@ CoreCallback::Sleep(const MM::Device*, double intervalMs) } -/** - * Get the metadata tags attached to device caller, and merge them with metadata - * in pMd (if not null). Returns a metadata object. - */ -Metadata -CoreCallback::AddCameraMetadata(const MM::Device* caller, const Metadata* pMd) -{ - Metadata newMD; - if (pMd) - { - newMD = *pMd; - } - - std::shared_ptr camera = - std::static_pointer_cast( - core_->deviceManager_->GetDevice(caller)); - - std::string label = camera->GetLabel(); - newMD.put(MM::g_Keyword_Metadata_CameraLabel, label); - - std::string serializedMD; - try - { - serializedMD = camera->GetTags(); - } - catch (const CMMError&) - { - return newMD; - } - - Metadata devMD; - devMD.Restore(serializedMD.c_str()); - newMD.Merge(devMD); - - return newMD; -} - int CoreCallback::InsertImage(const MM::Device* caller, const unsigned char* buf, unsigned width, unsigned height, unsigned byteDepth, const char* serializedMetadata, const bool doProcess) { Metadata md; @@ -251,27 +218,7 @@ int CoreCallback::InsertImage(const MM::Device* caller, const unsigned char* buf int CoreCallback::InsertImage(const MM::Device* caller, const unsigned char* buf, unsigned width, unsigned height, unsigned byteDepth, const Metadata* pMd, bool doProcess) { - try - { - Metadata md = AddCameraMetadata(caller, pMd); - - if(doProcess) - { - MM::ImageProcessor* ip = GetImageProcessor(caller); - if( NULL != ip) - { - ip->Process(const_cast(buf), width, height, byteDepth); - } - } - if (core_->cbuf_->InsertImage(buf, width, height, byteDepth, &md)) - return DEVICE_OK; - else - return DEVICE_BUFFER_OVERFLOW; - } - catch (CMMError& /*e*/) - { - return DEVICE_INCOMPATIBLE_IMAGE; - } + return InsertImage(caller, buf, width, height, byteDepth, pMd, doProcess); } int CoreCallback::InsertImage(const MM::Device* caller, const unsigned char* buf, unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents, const char* serializedMetadata, const bool doProcess) @@ -285,7 +232,23 @@ int CoreCallback::InsertImage(const MM::Device* caller, const unsigned char* buf { try { - Metadata md = AddCameraMetadata(caller, pMd); + Metadata newMD; + if (pMd) + { + newMD = *pMd; + } + std::shared_ptr device = core_->deviceManager_->GetDevice(caller); + + if (device->GetType() == MM::CameraDevice) + { + // convert device to camera + std::shared_ptr camera = std::dynamic_pointer_cast(device); + core_->addCameraMetadata(camera, newMD, width, height, byteDepth, nComponents); + } + + char labelBuffer[MM::MaxStrLength]; + caller->GetLabel(labelBuffer); + std::string callerLabel(labelBuffer); if(doProcess) { @@ -295,10 +258,11 @@ 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)) - return DEVICE_OK; - else + + int ret = core_->bufferManager_->InsertImage(callerLabel.c_str(), buf, width, height, byteDepth, &newMD); + if (ret != DEVICE_OK) return DEVICE_BUFFER_OVERFLOW; + return DEVICE_OK; } catch (CMMError& /*e*/) { @@ -316,14 +280,58 @@ int CoreCallback::InsertImage(const MM::Device* caller, const ImgBuffer & imgBuf ip->Process(p, imgBuf.Width(), imgBuf.Height(), imgBuf.Depth()); } + char labelBuffer[MM::MaxStrLength]; + caller->GetLabel(labelBuffer); + std::string callerLabel(labelBuffer); return InsertImage(caller, imgBuf.GetPixels(), imgBuf.Width(), imgBuf.Height(), imgBuf.Depth(), &md); } -void CoreCallback::ClearImageBuffer(const MM::Device* /*caller*/) -{ - core_->cbuf_->Clear(); -} +// TODO: enable these once we know what to do about backwards compatibility with circular buffer +// // This method is explicitly for camera devices. It requires width, height, byteDepth, and nComponents +// // to be passed in, so that higher level code retrieving data from the buffer knows how to interpret the data +// // Generic data insertion is handled by AcquireDataWriteSlot +// int CoreCallback::AcquireImageWriteSlot(const MM::Camera* caller, size_t dataSize, size_t metadataSize, +// unsigned char** dataPointer, unsigned char** metadataPointer, +// unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents) +// { +// if (core_->deviceManager_->GetDevice(caller)->GetType() == MM::CameraDevice) +// { + +// Metadata md; +// std::shared_ptr camera = std::dynamic_pointer_cast(core_->deviceManager_->GetDevice(caller)); +// // Add the metadata needed for interpreting camera images +// core_->addCameraMetadata(camera, md, width, height, byteDepth, nComponents); + +// char label[MM::MaxStrLength]; +// caller->GetLabel(label); +// return core_->bufferManager_->AcquireWriteSlot(label, dataSize, metadataSize, +// (void**)dataPointer, (void**)metadataPointer, &md); +// } +// // This is only for camera devices, other devices should call AcquireDataWriteSlot +// return DEVICE_ERR; +// } + +// // This method is for generic data insertion. It will not add any metadata for interpretting the data +// int CoreCallback::AcquireDataWriteSlot(const MM::Device* caller, size_t dataSize, size_t metadataSize, +// unsigned char** dataPointer, unsigned char** metadataPointer) +// { +// char label[MM::MaxStrLength]; +// caller->GetLabel(label); +// Metadata md; +// return core_->bufferManager_->AcquireWriteSlot(label, dataSize, metadataSize, +// (void**)dataPointer, (void**)metadataPointer, &md); +// } + +// int CoreCallback::FinalizeWriteSlot(unsigned char* dataPointer, +// size_t actualMetadataBytes) +// { +// if (core_->bufferManager_->FinalizeWriteSlot(dataPointer, actualMetadataBytes)) +// { +// return DEVICE_OK; +// } +// return DEVICE_ERR; +// } bool CoreCallback::InitializeImageBuffer(unsigned channels, unsigned slices, unsigned int w, unsigned int h, unsigned int pixDepth) @@ -332,27 +340,45 @@ bool CoreCallback::InitializeImageBuffer(unsigned channels, unsigned slices, if (slices != 1) return false; - return core_->cbuf_->Initialize(channels, w, h, pixDepth); + + // For backwards compatibility with the circular buffer, but really this should be + // (or is?) handled by higher level code. Something this certainly cannot be applied + // to the v2 buffer, because devices do not have the authority to clear the buffer, + // when application code may hold pointers into the buffer. + if (!core_->bufferManager_->IsUsingNewDataBuffer()) { + return core_->bufferManager_->InitializeCircularBuffer(channels, w, h, pixDepth); + } + return true; } -int CoreCallback::InsertMultiChannel(const MM::Device* caller, - const unsigned char* buf, - unsigned numChannels, - unsigned width, - unsigned height, - unsigned byteDepth, - Metadata* pMd) +int CoreCallback:: InsertMultiChannel(const MM::Device* caller, const unsigned char* buf, + unsigned numChannels, unsigned width, unsigned height, unsigned byteDepth, Metadata* pMd) { try { - Metadata md = AddCameraMetadata(caller, pMd); + std::shared_ptr device = core_->deviceManager_->GetDevice(caller); + + Metadata newMD; + if (pMd) + { + newMD = *pMd; + } + if (device->GetType() == MM::CameraDevice) + { + // convert device to camera + std::shared_ptr camera = std::dynamic_pointer_cast(device); + core_->addCameraMetadata(camera, newMD, width, height, byteDepth, 1); + } MM::ImageProcessor* ip = GetImageProcessor(caller); if( NULL != ip) { ip->Process( const_cast(buf), width, height, byteDepth); } - if (core_->cbuf_->InsertMultiChannel(buf, numChannels, width, height, byteDepth, &md)) + char labelBuffer[MM::MaxStrLength]; + caller->GetLabel(labelBuffer); + std::string callerLabel(labelBuffer); + if (core_->bufferManager_->InsertMultiChannel(callerLabel.c_str(), buf, numChannels, width, height, byteDepth, &newMD)) return DEVICE_OK; else return DEVICE_BUFFER_OVERFLOW; @@ -364,6 +390,27 @@ int CoreCallback::InsertMultiChannel(const MM::Device* caller, } +int CoreCallback::InsertData(const MM::Device* caller, const unsigned char* buf, size_t dataSize, Metadata* pMd) +{ + std::shared_ptr device = core_->deviceManager_->GetDevice(caller); + + if (device->GetType() == MM::CameraDevice) + { + return DEVICE_ERR; + } + + Metadata newMD; + if (pMd) + { + newMD = *pMd; + } + + // Add a metadata tag for the data-producing device + newMD.put(MM::g_Keyword_Metadata_CameraLabel, device->GetLabel()); + + return core_->bufferManager_->InsertData(device->GetLabel().c_str(), buf, dataSize, &newMD); +} + int CoreCallback::AcqFinished(const MM::Device* caller, int /*statusCode*/) { std::shared_ptr camera; diff --git a/MMCore/CoreCallback.h b/MMCore/CoreCallback.h index 275493b42..e2705e79e 100644 --- a/MMCore/CoreCallback.h +++ b/MMCore/CoreCallback.h @@ -91,7 +91,60 @@ class CoreCallback : public MM::Core /*Deprecated*/ int InsertImage(const MM::Device* caller, const unsigned char* buf, unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents, const Metadata* pMd = 0, const bool doProcess = true); /*Deprecated*/ int InsertMultiChannel(const MM::Device* caller, const unsigned char* buf, unsigned numChannels, unsigned width, unsigned height, unsigned byteDepth, Metadata* pMd = 0); - void ClearImageBuffer(const MM::Device* caller); + + // Method for inserting generic non-image data into the buffer + int InsertData(const MM::Device* caller, const unsigned char* buf, size_t dataSize, Metadata* pMd = 0); + +// TODO: enable these when we know what to do about backwards compatibility for circular buffer +// /** +// * Direct writing into V2 buffer instead of using InsertImage (which has to copy the data again). +// * Version with essential parameters for camera devices. +// * @param caller Camera device making the call +// * @param dataSize Size of the image data in bytes +// * @param metadataSize Size of metadata in bytes +// * @param dataPointer Pointer that will be set to allocated image buffer. The caller should write +// * data to this buffer. +// * @param metadataPointer Pointer that will be set to allocated metadata buffer. The caller should write +// * serialized metadata to this buffer. +// * @param width Width of the image in pixels +// * @param height Height of the image in pixels +// * @param byteDepth Number of bytes per pixel +// * @param nComponents Number of components per pixel +// * @return DEVICE_OK on success +// */ +// int AcquireImageWriteSlot(const MM::Camera* caller, size_t dataSize, size_t metadataSize, +// unsigned char** dataPointer, unsigned char** metadataPointer, +// unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents); + +// /** +// * Generic version for inserting data into the buffer. +// * @param caller Device making the call +// * @param dataSize Size of the data in bytes +// * @param metadataSize Size of metadata in bytes +// * @param dataPointer Pointer that will be set to allocated data buffer. The caller should write +// * data to this buffer. +// * @param metadataPointer Pointer that will be set to allocated metadata buffer. The caller should write +// * serialized metadata to this buffer. +// * @return DEVICE_OK on success +// */ +// int AcquireDataWriteSlot(const MM::Device* caller, size_t dataSize, size_t metadataSize, +// unsigned char** dataPointer, unsigned char** metadataPointer); + +// /** +// * Finalizes a write slot after data has been written. +// * @param dataPointer Pointer to the data buffer that was written to +// * @param actualMetadataBytes Actual number of metadata bytes written +// * @return DEVICE_OK on success +// */ +// int FinalizeWriteSlot(unsigned char* dataPointer, size_t actualMetadataBytes); + + // @deprecated This method was previously used by many camera adapters to enforce an overwrite mode on the circular buffer + // -- making it wrap around when running a continuous sequence acquisition. Now we've added an option to do this into the + // circular buffer itself, so this method is no longer required and is not used. higher level code will control when this + // option is activated, making it simpler to develop camera adatpers and giving more consistent behavior. + void ClearImageBuffer(const MM::Device* /*caller*/) {}; + // @deprecated This method is not required for the V2 buffer, and in fact should not be called because + // devices do not have the authority to throw away pointers held by other code. bool InitializeImageBuffer(unsigned channels, unsigned slices, unsigned int w, unsigned int h, unsigned int pixDepth); int AcqFinished(const MM::Device* caller, int statusCode); @@ -136,12 +189,12 @@ class CoreCallback : public MM::Core void GetLoadedDeviceOfType(const MM::Device* caller, MM::DeviceType devType, char* deviceName, const unsigned int deviceIterator); + + private: CMMCore* core_; MMThreadLock* pValueChangeLock_; - Metadata AddCameraMetadata(const MM::Device* caller, const Metadata* pMd); - int OnConfigGroupChanged(const char* groupName, const char* newConfigName); int OnPixelSizeChanged(double newPixelSizeUm); int OnPixelSizeAffineChanged(std::vector newPixelSizeAffine); diff --git a/MMCore/CoreFeatures.h b/MMCore/CoreFeatures.h index f1c180ca5..7cca430e9 100644 --- a/MMCore/CoreFeatures.h +++ b/MMCore/CoreFeatures.h @@ -27,6 +27,7 @@ namespace features { struct Flags { bool strictInitializationChecks = false; bool ParallelDeviceInitialization = true; + bool NewDataBuffer = false; // How to add a new Core feature: see the comment in the .cpp file. }; diff --git a/MMCore/MMCore.cpp b/MMCore/MMCore.cpp index 19dd2b0cb..7f50e525b 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -54,6 +54,7 @@ #include "MMCore.h" #include "MMEventCallback.h" #include "PluginManager.h" +#include "NewDataBufferPointer.h" #include #include @@ -77,6 +78,33 @@ #pragma GCC diagnostic ignored "-Wdeprecated" #endif +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; +} + /* * Important! Read this before changing this file: * @@ -137,10 +165,17 @@ CMMCore::CMMCore() : properties_(0), externalCallback_(0), pixelSizeGroup_(0), - cbuf_(0), + bufferManager_(nullptr), pluginManager_(new CPluginManager()), deviceManager_(new mm::DeviceManager()), - pPostedErrorsLock_(NULL) + pPostedErrorsLock_(NULL), + imageMDIncludeSystemStateCache_(true), + imageMDIncludeBitDepth_(true), + imageMDIncludeCameraParams_(true), + imageMDIncludeCameraTags_(true), + imageMDIncludeTiming_(true), + imageMDIncludeLegacyCalibration_(true), + imageMDIncludeAdditionalLegacy_(false) { configGroups_ = new ConfigGroupCollection(); pixelSizeGroup_ = new PixelSizeConfigGroup(); @@ -151,7 +186,7 @@ CMMCore::CMMCore() : callback_ = new CoreCallback(this); const unsigned seqBufMegabytes = (sizeof(void*) > 4) ? 250 : 25; - cbuf_ = new CircularBuffer(seqBufMegabytes); + bufferManager_ = new BufferManager(false, seqBufMegabytes); nullAffine_ = new std::vector(6); for (int i = 0; i < 6; i++) { @@ -159,6 +194,9 @@ CMMCore::CMMCore() : } CreateCoreProperties(); + + // Initialize the start time for time-stamp calculations + startTime_ = std::chrono::steady_clock::now(); } /** @@ -181,7 +219,7 @@ CMMCore::~CMMCore() delete callback_; delete configGroups_; delete properties_; - delete cbuf_; + delete bufferManager_; delete pixelSizeGroup_; delete pPostedErrorsLock_; @@ -2658,6 +2696,241 @@ bool CMMCore::getShutterOpen() throw (CMMError) return getShutterOpen(shutterLabel.c_str()); } +/** + * Set which metadata categories to include in the image metadata using a bitmask. + * + * Bitmask values: + * - MM::MetadataBitDepth (1): Include bit depth information + * - MM::MetadataCameraParams (2): Include camera parameters + * - MM::MetadataCameraTags (4): Include camera tags + * - MM::MetadataTiming (8): Include timing information + * - MM::MetadataSystemStateCache (16): Include system state cache + * - MM::MetadataLegacyCalibration (32): Include legacy calibration + * - MM::MetadataLegacy (64): Include additional legacy metadata + * + * @param categoryBits Bitmask of metadata categories to include + */ +void CMMCore::setIncludeImageMetadata(unsigned int categoryBits) throw (CMMError) { + imageMDIncludeBitDepth_ = (categoryBits & MM::g_Image_Metadata_Bitmask_BitDepth) != 0; + imageMDIncludeCameraParams_ = (categoryBits & MM::g_Image_Metadata_Bitmask_CameraParams) != 0; + imageMDIncludeCameraTags_ = (categoryBits & MM::g_Image_Metadata_Bitmask_CameraTags) != 0; + imageMDIncludeTiming_ = (categoryBits & MM::g_Image_Metadata_Bitmask_Timing) != 0; + imageMDIncludeSystemStateCache_ = (categoryBits & MM::g_Image_Metadata_Bitmask_SystemStateCache) != 0; + imageMDIncludeLegacyCalibration_ = (categoryBits & MM::g_Image_Metadata_Bitmask_LegacyCalibration) != 0; + imageMDIncludeAdditionalLegacy_ = (categoryBits & MM::g_Image_Metadata_Bitmask_Legacy) != 0; +} + +/** + * Get the current metadata inclusion bitmask. + * + * @return Bitmask indicating which metadata categories are included + */ +unsigned int CMMCore::getIncludeImageMetadata() const throw (CMMError) { + unsigned int result = 0; + + if (imageMDIncludeBitDepth_) + result |= MM::g_Image_Metadata_Bitmask_BitDepth; + if (imageMDIncludeCameraParams_) + result |= MM::g_Image_Metadata_Bitmask_CameraParams; + if (imageMDIncludeCameraTags_) + result |= MM::g_Image_Metadata_Bitmask_CameraTags; + if (imageMDIncludeTiming_) + result |= MM::g_Image_Metadata_Bitmask_Timing; + if (imageMDIncludeSystemStateCache_) + result |= MM::g_Image_Metadata_Bitmask_SystemStateCache; + if (imageMDIncludeLegacyCalibration_) + result |= MM::g_Image_Metadata_Bitmask_LegacyCalibration; + if (imageMDIncludeAdditionalLegacy_) + result |= MM::g_Image_Metadata_Bitmask_Legacy; + + return result; +} + +/** + * A centralized function that adds all the metadata for camera devices. + * + * This was previously spread among the circular buffer, corecallback.h, and + * the SWIG wrapper. + * + * Get the metadata tags attached to device caller, and merge them with metadata + * in pMd (if not null). Returns a metadata object. + * + * The caller should have locked the camera device, or be calling from a thread + * in the camera (e.g. CoreCallback::InsertImage) + */ + +void CMMCore::addCameraMetadata(std::shared_ptr pCam, Metadata& md, unsigned width, unsigned height, + unsigned byteDepth, unsigned nComponents) +{ + // Essential metadata for interpreting the image: Width, height, and pixel type + md.PutImageTag(MM::g_Keyword_Metadata_Width, (unsigned int) width); + md.PutImageTag(MM::g_Keyword_Metadata_Height, (unsigned int) 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); + + // Needed for acquisition engine + if (!md.HasTag(MM::g_Keyword_Metadata_CameraLabel)) { + md.put(MM::g_Keyword_Metadata_CameraLabel, pCam->GetLabel()); + } + + // Optional metadata + if (imageMDIncludeBitDepth_) { + md.put(MM::g_Keyword_Metadata_BitDepth, CDeviceUtils::ConvertToString((long) pCam->GetBitDepth())); + } + + if (imageMDIncludeCameraParams_) { + unsigned x, y, xSize, ySize; + pCam->GetROI(x, y, xSize, ySize); + std::string roiTag = std::to_string(x) + "-" + std::to_string(y) + "-" + + std::to_string(xSize) + "-" + std::to_string(ySize); + md.put("ROI", roiTag); + + try { + std::string binning = pCam->GetProperty("Binning"); + md.put("Binning", binning); + } + catch (const CMMError&) { } + } + + if (imageMDIncludeTiming_) { + std::lock_guard lock(imageNumbersMutex_); + std::string cameraName = md.GetSingleTag(MM::g_Keyword_Metadata_CameraLabel).GetValue(); + if (imageNumbers_.find(cameraName) == imageNumbers_.end()) { + imageNumbers_[cameraName] = 0; + } + md.put(MM::g_Keyword_Metadata_ImageNumber, CDeviceUtils::ConvertToString(imageNumbers_[cameraName])); + ++imageNumbers_[cameraName]; + + if (!md.HasTag(MM::g_Keyword_Elapsed_Time_ms)) { + using namespace std::chrono; + auto elapsed = steady_clock::now() - startTime_; + md.put(MM::g_Keyword_Elapsed_Time_ms, + std::to_string(duration_cast(elapsed).count())); + } + + auto now = std::chrono::system_clock::now(); + md.put(MM::g_Keyword_Metadata_TimeInCore, FormatLocalTime(now)); + } + + if (imageMDIncludeCameraTags_) { + try { + std::string serializedMD = pCam->GetTags(); + Metadata devMD; + devMD.Restore(serializedMD.c_str()); + md.Merge(devMD); + } + catch (const CMMError&) { } + } + + if (imageMDIncludeLegacyCalibration_) { + try { + int binning = pCam->GetBinning(); + md.put("PixelSizeUm", CDeviceUtils::ConvertToString(getPixelSizeUm(true, binning))); + + std::string pixelSizeAffine = ""; + std::vector aff = getPixelSizeAffine(true, binning); + if (aff.size() == 6) { + std::ostringstream oss; + for (size_t i = 0; i < 5; i++) { + oss << aff[i] << ";"; + } + oss << aff[5]; + pixelSizeAffine = oss.str(); + } + md.put("PixelSizeAffine", pixelSizeAffine); + } + catch (const CMMError&) { } + } + + // Metadata that previously was in the Java SWIG layer and I think may be used by the application + // and/or acquisition engine. However, it doesn't really make sense that this would exist in this + // since channel, slice, position, etc. higher level concepts associated with an acquisition engine + if (imageMDIncludeAdditionalLegacy_) { + md.put("Frame", "0"); + md.put("FrameIndex", "0"); + md.put("Position", "Default"); + md.put("PositionIndex", "0"); + md.put("Slice", "0"); + md.put("SliceIndex", "0"); + + // Add channel metadata if not already set + try { + std::string channel = getCurrentConfigFromCache( + getPropertyFromCache(MM::g_Keyword_CoreDevice, MM::g_Keyword_CoreChannelGroup).c_str()); + if (channel.empty()) { + channel = "Default"; + } + md.put("Channel", channel); + md.put("ChannelIndex", "0"); + } + catch (const CMMError&) { + md.put("Channel", "Default"); + md.put("ChannelIndex", "0"); + } + } + + if (imageMDIncludeSystemStateCache_) { + try { + // MMThreadGuard scg(stateCacheLock_); // Needed? + Configuration state = getSystemStateCache(); + for (size_t i = 0; i < state.size(); ++i) { + PropertySetting setting = state.getSetting(i); + std::string key = setting.getDeviceLabel() + "-" + setting.getPropertyName(); + std::string value = setting.getPropertyValue(); + md.put(key, value); + } + } + catch (const CMMError&) { } // ignore + } +} + +void CMMCore::addMultiCameraMetadata(Metadata& md, int cameraChannelIndex = 0) const +{ + // Multi-camera device adapter metadata. This is considered legacy since the NewDataBuffer + // make the multi-camera adapter unnecessary. + + if (!md.HasTag("CameraChannelIndex")) { + md.put("CameraChannelIndex", ToString(cameraChannelIndex)); + md.put("ChannelIndex", ToString(cameraChannelIndex)); + } + + // This whole block seems superfluos now since we always add the "Camera" tag + // in the block above. Also, the NewDataBuffer eliminates the need for the multi-camera + // adapter. Leaving here and commented out for now, because I'm unsure what the upstream + // effects are + // if (!md.HasTag("Camera")) { + // // Get the core camera name + // std::string coreCamera; + // try { + // coreCamera = md.GetSingleTag("Core-Camera").GetValue(); + // // Construct physical camera key (e.g. "Camera-Physical Camera 1") + // std::string physCamKey = coreCamera + "-Physical Camera " + ToString(cameraChannelIndex + 1); + + // // Check if physical camera metadata exists + // if (md.HasTag(physCamKey.c_str())) { + // std::string physicalCamera = md.GetSingleTag(physCamKey.c_str()).GetValue(); + // md.put("Camera", physicalCamera); + // md.put("Channel", physicalCamera); + // } + // } + // catch (const CMMError&) { + // // If core camera metadata not found, ignore + // } + // } +} + /** * Exposes the internal image buffer. * @@ -2780,6 +3053,128 @@ void* CMMCore::getImage(unsigned channelNr) throw (CMMError) } } +// Version of snap that also return metadata, so that metadata generation can be centralized in the Core and migrated +// out of the SWIG wrapper. +void* CMMCore::getImageMD(Metadata& md) throw (CMMError) +{ + void* pBuf = getImage(); + + std::shared_ptr camera = currentCameraDevice_.lock(); + if (!camera) + throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); + + mm::DeviceModuleLockGuard guard(camera); + // Need to do this becuase this data was never inserted into the circular buffer or NewDataBuffer where this + // metadata is added for other images + addCameraMetadata(camera, md, camera->GetImageWidth(), camera->GetImageHeight(), + camera->GetImageBytesPerPixel(), camera->GetNumberOfComponents()); + + return pBuf; +} + +void* CMMCore::getImageMD(unsigned channelNr, Metadata& md) throw (CMMError) +{ + void* pBuf = getImage(channelNr); + std::shared_ptr camera = currentCameraDevice_.lock(); + if (!camera) + throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); + + mm::DeviceModuleLockGuard guard(camera); + // Need to do this becuase this data was never inserted into the circular buffer or NewDataBuffer where this + // metadata is added for other images + addCameraMetadata(camera, md, camera->GetImageWidth(), camera->GetImageHeight(), + camera->GetImageBytesPerPixel(), camera->GetNumberOfComponents()); + addMultiCameraMetadata(md, channelNr); + return pBuf; +} + + +/** + * For use with NewDataBuffer + * + * Get a pointer to the image acquired by snapImage. This will copy the image + * from the camera buffer into the NewDataBuffer so that it can be persistently + * accessed (i.e. subsequent calls snapImage will not overwrite the pointer + * returned by this function). + * + * This method relies on the current Core-Camera to determine which camera + * buffer to copy. + * + * @return a pointer to the internal image buffer. + * @throws CMMError when the camera returns no data + */ +BufferDataPointer* CMMCore::getImagePointer() throw (CMMError) +{ + if (!bufferManager_->IsUsingNewDataBuffer() ) + throw CMMError("Only valid when NewDataBuffer in use"); + + std::shared_ptr camera = currentCameraDevice_.lock(); + if (!camera) + throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); + else + { + if( ! everSnapped_) + { + logError("CMMCore::getImage()", getCoreErrorText(MMERR_InvalidImageSequence).c_str()); + throw CMMError(getCoreErrorText(MMERR_InvalidImageSequence).c_str(), MMERR_InvalidImageSequence); + } + + { + MMThreadGuard g(*pPostedErrorsLock_); + // Copied from above getImage() calls, unclear if needed + if(0 < postedErrors_.size()) + { + std::pair< int, std::string> toThrow(postedErrors_[0]); + // todo, process the collection of posted errors. + postedErrors_.clear(); + throw CMMError( toThrow.second.c_str(), toThrow.first); + } + } + + void* pBuf(0); + try { + mm::DeviceModuleLockGuard guard(camera); + pBuf = const_cast (camera->GetImageBuffer()); + + std::shared_ptr imageProcessor = + currentImageProcessor_.lock(); + if (imageProcessor) + { + imageProcessor->Process((unsigned char*)pBuf, camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel() ); + } + + + Metadata md; + // Add metadata to know how to interpret the data added to NewDataBuffer + addCameraMetadata(camera, md, camera->GetImageWidth(), camera->GetImageHeight(), + camera->GetImageBytesPerPixel(), camera->GetNumberOfComponents()); + unsigned imageSize = camera->GetImageWidth() * camera->GetImageHeight() * camera->GetImageBytesPerPixel(); + int ret = bufferManager_->InsertData(camera->GetLabel().c_str(), (unsigned char*)pBuf, imageSize, &md); + if (ret != DEVICE_OK) { + throw CMMError("NewDataBuffer overflow"); + } + + if (bufferManager_->GetOverwriteData()) { + // If in overwrite mode (e.g. live mode), peek the last data + const void* ptr = bufferManager_->GetLastData(); + return new BufferDataPointer(bufferManager_, ptr); + } else { + // If not in overwrite mode, pop the next data + // TODO: could add a check here that there there is one and only one image to pop + // because otherwise the user may be using this incorrectly. + const void* ptr = bufferManager_->PopNextData(); + return new BufferDataPointer(bufferManager_, ptr); + } + + } catch( CMMError& e){ + throw e; + } catch (...) { + logError("CMMCore::getImage()", getCoreErrorText(MMERR_UnhandledException).c_str()); + throw CMMError(getCoreErrorText(MMERR_UnhandledException).c_str(), MMERR_UnhandledException); + } +} +} + /** * Returns the size of the internal image buffer. * @@ -2828,27 +3223,42 @@ void CMMCore::startSequenceAcquisition(long numImages, double intervalMs, bool s ,MMERR_NotAllowedDuringSequenceAcquisition); } - try - { - if (!cbuf_->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(); + try + { + if (!bufferManager_->IsUsingNewDataBuffer()) { + // Circular buffer is initialized in case of a change in camera settings + try { + bufferManager_->InitializeCircularBuffer(camera->GetNumberOfChannels(), camera->GetImageWidth(), + camera->GetImageHeight(), camera->GetImageBytesPerPixel()); + } catch (...) { + logError(getDeviceName(camera).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); + throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); + } + } + // NewDataBuffer does not need to be initialized or cleared, becuase it does not make + // assumptions about image size and it supports asynchronous sequences from different + // cameras. + + // Disable overwriting for finite sequence acquisition + int ret = bufferManager_->SetOverwriteData(false); + if (ret != DEVICE_OK) { + throw CMMError("Failed to switch to non-overwriting mode in DataBuffer"); + } mm::DeviceModuleLockGuard guard(camera); + startTime_ = std::chrono::steady_clock::now(); + imageNumbers_.clear(); LOG_DEBUG(coreLogger_) << "Will start sequence acquisition from default camera"; - int nRet = camera->StartSequenceAcquisition(numImages, intervalMs, stopOnOverflow); - if (nRet != DEVICE_OK) - throw CMMError(getDeviceErrorText(nRet, camera).c_str(), MMERR_DEVICE_GENERIC); - } - catch (std::bad_alloc& ex) - { - std::ostringstream messs; - messs << getCoreErrorText(MMERR_OutOfMemory).c_str() << " " << ex.what() << '\n'; - throw CMMError(messs.str().c_str() , MMERR_OutOfMemory); - } + int nRet = camera->StartSequenceAcquisition(numImages, intervalMs, stopOnOverflow); + if (nRet != DEVICE_OK) + throw CMMError(getDeviceErrorText(nRet, camera).c_str(), MMERR_DEVICE_GENERIC); + } + catch (std::bad_alloc& ex) + { + std::ostringstream messs; + messs << getCoreErrorText(MMERR_OutOfMemory).c_str() << " " << ex.what() << '\n'; + throw CMMError(messs.str().c_str() , MMERR_OutOfMemory); + } } else { @@ -2874,13 +3284,28 @@ 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())) - { - logError(getDeviceName(pCam).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); - throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); + if (!bufferManager_->IsUsingNewDataBuffer()) { + // Circular buffer is initialized in case of a change in camera settings + try { + bufferManager_->InitializeCircularBuffer(pCam->GetNumberOfChannels(), pCam->GetImageWidth(), + pCam->GetImageHeight(), pCam->GetImageBytesPerPixel()); + } catch (...) { + logError(getDeviceName(pCam).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); + throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); + } + } + // NewDataBuffer does not need to be initialized or cleared, becuase it does not make + // assumptions about image size and it supports asynchronous sequences from different + // cameras. + + // Disable overwriting for finite sequence acquisition + int ret = bufferManager_->SetOverwriteData(false); + if (ret != DEVICE_OK) { + throw CMMError("Failed to switch to non-overwriting mode in DataBuffer"); } - cbuf_->Clear(); - + + startTime_ = std::chrono::steady_clock::now(); + imageNumbers_.clear(); LOG_DEBUG(coreLogger_) << "Will start sequence acquisition from camera " << label; int nRet = pCam->StartSequenceAcquisition(numImages, intervalMs, stopOnOverflow); @@ -2921,22 +3346,25 @@ void CMMCore::prepareSequenceAcquisition(const char* label) throw (CMMError) */ void CMMCore::initializeCircularBuffer() throw (CMMError) { - std::shared_ptr camera = currentCameraDevice_.lock(); - if (camera) - { - mm::DeviceModuleLockGuard guard(camera); - if (!cbuf_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) + if (!bufferManager_->IsUsingNewDataBuffer()) { + std::shared_ptr camera = currentCameraDevice_.lock(); + if (camera) { - logError(getDeviceName(camera).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); - throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); + mm::DeviceModuleLockGuard guard(camera); + try { + bufferManager_->InitializeCircularBuffer(camera->GetNumberOfChannels(), camera->GetImageWidth(), + camera->GetImageHeight(), camera->GetImageBytesPerPixel()); + } catch (...) { + logError(getDeviceName(camera).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); + throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); + } } - cbuf_->Clear(); - } - else - { - throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); + else + { + throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); + } + LOG_DEBUG(coreLogger_) << "Circular buffer initialized based on current camera"; } - LOG_DEBUG(coreLogger_) << "Circular buffer initialized based on current camera"; } /** @@ -2967,6 +3395,14 @@ void CMMCore::stopSequenceAcquisition(const char* label) throw (CMMError) void CMMCore::startContinuousSequenceAcquisition(double intervalMs) throw (CMMError) { std::shared_ptr camera = currentCameraDevice_.lock(); + if (camera) { + startContinuousSequenceAcquisition(camera->GetLabel().c_str(), intervalMs); + } +} + +void CMMCore::startContinuousSequenceAcquisition(const char* cameraLabel, double intervalMs) throw (CMMError) +{ + std::shared_ptr camera = deviceManager_->GetDeviceOfType(cameraLabel); if (camera) { mm::DeviceModuleLockGuard guard(camera); @@ -2977,12 +3413,27 @@ void CMMCore::startContinuousSequenceAcquisition(double intervalMs) throw (CMMEr ,MMERR_NotAllowedDuringSequenceAcquisition); } - if (!cbuf_->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); + if (!bufferManager_->IsUsingNewDataBuffer()) { + // Circular buffer is initialized in case of a change in camera settings + try { + bufferManager_->InitializeCircularBuffer(camera->GetNumberOfChannels(), camera->GetImageWidth(), + camera->GetImageHeight(), camera->GetImageBytesPerPixel()); + } catch (...) { + logError(getDeviceName(camera).c_str(), getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str()); + throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); + } } - cbuf_->Clear(); + // NewDataBuffer does not need to be initialized or cleared, becuase it does not make + // assumptions about image size and it supports asynchronous sequences from different + // cameras. + + // Enable overwriting for continuous sequence acquisition + int ret = bufferManager_->SetOverwriteData(true); + if (ret != DEVICE_OK) { + throw CMMError("Failed to switch to overwriting mode in DataBuffer"); + } + startTime_ = std::chrono::steady_clock::now(); + imageNumbers_.clear(); LOG_DEBUG(coreLogger_) << "Will start continuous sequence acquisition from current camera"; int nRet = camera->StartSequenceAcquisition(intervalMs); if (nRet != DEVICE_OK) @@ -3078,9 +3529,9 @@ void* CMMCore::getLastImage() throw (CMMError) } } - unsigned char* pBuf = const_cast(cbuf_->GetTopImage()); + const void* pBuf = bufferManager_->GetLastData(); if (pBuf != 0) - return pBuf; + return const_cast(pBuf); else { logError("CMMCore::getLastImage", getCoreErrorText(MMERR_CircularBufferEmpty).c_str()); @@ -3094,14 +3545,12 @@ 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); + unsigned bytesPerSingleChannel = const_cast(this)->getImageWidth() * + const_cast(this)->getImageHeight() * + const_cast(this)->getBytesPerPixel(); + void* pixels = const_cast(bufferManager_->GetLastDataMD(channel, bytesPerSingleChannel, md)); + addMultiCameraMetadata(md, channel); + return pixels; } /** @@ -3117,8 +3566,8 @@ void* CMMCore::getLastImageMD(unsigned channel, unsigned slice, Metadata& md) co * (see: https://en.wikipedia.org/wiki/RGBA_color_model). */ void* CMMCore::getLastImageMD(Metadata& md) const throw (CMMError) -{ - return getLastImageMD(0, 0, md); +{ + return const_cast(bufferManager_->GetLastDataMD(md)); } /** @@ -3135,14 +3584,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 const_cast(bufferManager_->GetNthDataMD(n, md)); } /** @@ -3159,9 +3601,9 @@ void* CMMCore::getNBeforeLastImageMD(unsigned long n, Metadata& md) const throw */ void* CMMCore::popNextImage() throw (CMMError) { - unsigned char* pBuf = const_cast(cbuf_->GetNextImage()); + const void* pBuf = bufferManager_->PopNextData(); if (pBuf != 0) - return pBuf; + return const_cast(pBuf); else throw CMMError(getCoreErrorText(MMERR_CircularBufferEmpty).c_str(), MMERR_CircularBufferEmpty); } @@ -3170,6 +3612,8 @@ void* CMMCore::popNextImage() throw (CMMError) * Gets and removes the next image (and metadata) from the circular buffer * channel indicates which cameraChannel image should be retrieved. * slice has not been implement and should always be 0 + * + * @deprecated Use popNextImageMD() without channel parameter instead. */ void* CMMCore::popNextImageMD(unsigned channel, unsigned slice, Metadata& md) throw (CMMError) { @@ -3177,14 +3621,12 @@ 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); + unsigned bytesPerSingleChannel = const_cast(this)->getImageWidth() * + const_cast(this)->getImageHeight() * + const_cast(this)->getBytesPerPixel(); + void* pixels = const_cast(bufferManager_->PopNextDataMD(channel, bytesPerSingleChannel, md)); + addMultiCameraMetadata(md, channel); + return pixels; } /** @@ -3192,7 +3634,43 @@ void* CMMCore::popNextImageMD(unsigned channel, unsigned slice, Metadata& md) th */ void* CMMCore::popNextImageMD(Metadata& md) throw (CMMError) { - return popNextImageMD(0, 0, md); + // Unlike version above, don't add multi-camera metadata + void* pixels = const_cast(bufferManager_->PopNextDataMD(md)); + return pixels; +} + +//// Data pointer access for NewDataBuffer +BufferDataPointer* CMMCore::getLastDataPointer() throw (CMMError) { + if (!bufferManager_->IsUsingNewDataBuffer()) { + throw CMMError("NewDataBuffer must be enabled for pointer-based image access"); + } + const void* rawPtr = bufferManager_->GetLastData(); + if (rawPtr == nullptr) { + throw CMMError("Buffer is empty"); + } + return new BufferDataPointer(bufferManager_, rawPtr); +} + +BufferDataPointer* CMMCore::popNextDataPointer() throw (CMMError) { + if (!bufferManager_->IsUsingNewDataBuffer()) { + throw CMMError("NewDataBuffer must be enabled for pointer-based image access"); + } + const void* rawPtr = bufferManager_->PopNextData(); + if (rawPtr == nullptr) { + throw CMMError("Buffer is empty"); + } + return new BufferDataPointer(bufferManager_, rawPtr); +} + +BufferDataPointer* CMMCore::getLastDataFromDevicePointer(std::string deviceLabel) throw (CMMError) { + if (!bufferManager_->IsUsingNewDataBuffer()) { + throw CMMError("NewDataBuffer must be enabled for pointer-based image access"); + } + const void* rawPtr = bufferManager_->GetLastDataFromDevice(deviceLabel); + if (rawPtr == nullptr) { + throw CMMError("Buffer is empty"); + } + return new BufferDataPointer(bufferManager_, rawPtr); } /** @@ -3203,7 +3681,56 @@ void* CMMCore::popNextImageMD(Metadata& md) throw (CMMError) */ void CMMCore::clearCircularBuffer() throw (CMMError) { - cbuf_->Clear(); + if (!bufferManager_->IsUsingNewDataBuffer()) { + clearBuffer(); + } + // No effect on NewDataBuffer, because it supports asynchronous sequences from different + // cameras and should be cleared more carefully using the clearBuffer() method. +} + +/** + * This method applies to both the circular buffer and the NewDataBuffer. + * A difference between the circular buffer and NewDataBuffer is that the NewDataBuffer + * is not required to be empty of images before a new sequence is started. In other + * words, producers can be adding data to it that is asynchronously consumed. This + * means that this method should not be substituted for clearCircularBuffer() in + * acquisition code. + * + */ +void CMMCore::clearBuffer() throw (CMMError) +{ + try { + bufferManager_->Clear(); + } catch (...) { + throw CMMError("Failed to clear buffer"); + } +} + +/** + * For the circular buffer, this does a non-dangerous re-initialization + * for NewDataBuffer, this is a dangerous operation because there may be pointers into the buffer's memory + * that are not valid anymore. It can be used to reset the buffer without having to restart the + * application, but the need to use it indicates a bug in the application or device adapter that + * is not properly releasing the buffer's memory. + */ +void CMMCore::forceBufferReset() throw (CMMError) +{ + bufferManager_->ForceReset(); +} + +/** + * Enables or disables the NewDataBuffer. + */ +void CMMCore::enableNewDataBuffer(bool enable) throw (CMMError) +{ + int ret = bufferManager_->EnableNewDataBuffer(enable); + if (ret != DEVICE_OK) + throw CMMError("Failed to enable New Data Buffer", ret); + + // Default include circular buffer, exclude new buffer + imageMDIncludeLegacyCalibration_ = !enable; + imageMDIncludeSystemStateCache_ = !enable; + imageMDIncludeCameraTags_ = !enable; } /** @@ -3212,12 +3739,19 @@ void CMMCore::clearCircularBuffer() throw (CMMError) void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes ) throw (CMMError) { - delete cbuf_; // discard old buffer + setBufferMemoryFootprint(sizeMB); +} + +void CMMCore::setBufferMemoryFootprint(unsigned sizeMB) throw (CMMError) +{ + if (isSequenceRunning()) { + stopSequenceAcquisition(); + } LOG_DEBUG(coreLogger_) << "Will set circular buffer size to " << sizeMB << " MB"; try { - cbuf_ = new CircularBuffer(sizeMB); + bufferManager_->ReallocateBuffer(sizeMB); } catch (std::bad_alloc& ex) { @@ -3226,7 +3760,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 == bufferManager_) throw CMMError(getCoreErrorText(MMERR_OutOfMemory).c_str(), MMERR_OutOfMemory); try @@ -3237,8 +3771,13 @@ void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes if (camera) { mm::DeviceModuleLockGuard guard(camera); - if (!cbuf_->Initialize(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) - throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); + if (!bufferManager_->IsUsingNewDataBuffer()) { + // Circular buffer requires initialization specific to the camera + if (!bufferManager_->InitializeCircularBuffer(camera->GetNumberOfChannels(), camera->GetImageWidth(), camera->GetImageHeight(), camera->GetImageBytesPerPixel())) + throw CMMError(getCoreErrorText(MMERR_CircularBufferFailedToInitialize).c_str(), MMERR_CircularBufferFailedToInitialize); + } else { + bufferManager_->ReallocateBuffer(sizeMB); + } } LOG_DEBUG(coreLogger_) << "Did set circular buffer size to " << @@ -3250,7 +3789,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 == bufferManager_) throw CMMError(getCoreErrorText(MMERR_OutOfMemory).c_str(), MMERR_OutOfMemory); } @@ -3259,9 +3798,15 @@ void CMMCore::setCircularBufferMemoryFootprint(unsigned sizeMB ///< n megabytes */ unsigned CMMCore::getCircularBufferMemoryFootprint() { - if (cbuf_) +return getBufferMemoryFootprint(); +} + + +unsigned CMMCore::getBufferMemoryFootprint() const +{ + if (bufferManager_) { - return cbuf_->GetMemorySizeMB(); + return bufferManager_->GetMemorySizeMB(); } return 0; } @@ -3271,9 +3816,9 @@ unsigned CMMCore::getCircularBufferMemoryFootprint() */ long CMMCore::getRemainingImageCount() { - if (cbuf_) + if (bufferManager_) { - return cbuf_->GetRemainingImageCount(); + return bufferManager_->GetRemainingDataCount(); } return 0; } @@ -3283,9 +3828,16 @@ long CMMCore::getRemainingImageCount() */ long CMMCore::getBufferTotalCapacity() { - if (cbuf_) + if (bufferManager_) { - return cbuf_->GetSize(); + // Compute image size from the current camera parameters. + double imageSize = getImageWidth() * getImageHeight() * getBytesPerPixel() / 1024.0 / 1024.0; + // Pass the computed image size as an argument to the adapter. + unsigned int sizeMB = bufferManager_->GetMemorySizeMB(); + if (imageSize > 0) + { + return static_cast(sizeMB / imageSize); + } } return 0; } @@ -3297,9 +3849,15 @@ long CMMCore::getBufferTotalCapacity() */ long CMMCore::getBufferFreeCapacity() { - if (cbuf_) + if (bufferManager_) { - return cbuf_->GetFreeSize(); + // Compute image size from the current camera parameters. + double imageSize = getImageWidth() * getImageHeight() * getBytesPerPixel() / 1024.0 / 1024.0; + unsigned int sizeMB = bufferManager_->GetFreeSizeMB(); + if (imageSize > 0) + { + return static_cast(sizeMB / imageSize); + } } return 0; } @@ -3309,7 +3867,7 @@ long CMMCore::getBufferFreeCapacity() */ bool CMMCore::isBufferOverflowed() const { - return cbuf_->Overflow(); + return bufferManager_->Overflow(); } /** @@ -4248,6 +4806,43 @@ unsigned CMMCore::getNumberOfComponents() return 0; } +void CMMCore::releaseReadAccess(DataPtr ptr) { + if (bufferManager_->IsUsingNewDataBuffer()) { + bufferManager_->ReleaseReadAccess(ptr); + } +} + + +// For the below function that gets properties of the image based on a pointer, +// Cant assume that this a NewDataBuffer pointer, because Java SWIG wrapper +// will call this after copying from a snap buffer. So we'll check if the +// buffer knows about this pointer. If not, it's a snap buffer pointer. +// We don't want want to compare to the snap buffer pointer directly because +// its unclear what the device adapter might do when this is called. +bool CMMCore::getImageProperties(DataPtr ptr, int& width, int& height, + int& byteDepth, int& nComponents) throw (CMMError) { + if (!bufferManager_->IsUsingNewDataBuffer()) { + // Could be snap or circular buffer pointer + width = getImageWidth(); + height = getImageHeight(); + byteDepth = getBytesPerPixel(); + nComponents = getNumberOfComponents(); + } else if (bufferManager_->IsPointerInNewDataBuffer(ptr)) { + // NewDataBuffer pointer + Metadata md; + bufferManager_->ExtractMetadata(ptr, md); + // If it can't return this metadata, then its not a valid image + return parseImageMetadata(md, width, height, byteDepth, nComponents); + } else { + // Snap buffer pointer with NewDataBuffer on + width = getImageWidth(); + height = getImageHeight(); + byteDepth = getBytesPerPixel(); + nComponents = getNumberOfComponents(); + } + return true; +} + /** * Returns the number of simultaneous channels the default camera is returning. */ @@ -4408,11 +5003,13 @@ void CMMCore::setROI(int x, int y, int xSize, int ySize) throw (CMMError) if (nRet != DEVICE_OK) throw CMMError(getDeviceErrorText(nRet, camera).c_str(), MMERR_DEVICE_GENERIC); - // Any images left over in the sequence buffer may have sizes - // 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(); + // When using the circularBuffer: Any images left over in the sequence + // buffer may have sizes 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. + if (! bufferManager_->IsUsingNewDataBuffer()) { + bufferManager_->Clear(); + } } else throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); @@ -4491,7 +5088,9 @@ 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(); + if (!bufferManager_->IsUsingNewDataBuffer()) { + bufferManager_->Clear(); + } } else throw CMMError(getCoreErrorText(MMERR_CameraNotAvailable).c_str(), MMERR_CameraNotAvailable); @@ -4546,11 +5145,13 @@ void CMMCore::clearROI() throw (CMMError) if (nRet != DEVICE_OK) throw CMMError(getDeviceErrorText(nRet, camera).c_str(), MMERR_DEVICE_GENERIC); - // Any images left over in the sequence buffer may have sizes - // 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(); + // When using the circularBuffer: Any images left over in the sequence + // buffer may have sizes 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. + if (!bufferManager_->IsUsingNewDataBuffer()) { + bufferManager_->Clear(); + } } } @@ -5531,6 +6132,25 @@ double CMMCore::getPixelSizeUm() * pixel size preset matches the property values. */ double CMMCore::getPixelSizeUm(bool cached) +{ + int binning = 1; + std::shared_ptr camera = currentCameraDevice_.lock(); + if (camera) + { + try + { + mm::DeviceModuleLockGuard guard(camera); + binning = camera->GetBinning(); + } + catch (const CMMError&) // Possibly uninitialized camera + { + // Assume no binning + } + } + return getPixelSizeUm(cached, binning); +} + +double CMMCore::getPixelSizeUm(bool cached, int binning) { std::string resolutionID; try @@ -5550,20 +6170,7 @@ double CMMCore::getPixelSizeUm(bool cached) double pixSize = pCfg->getPixelSizeUm(); - std::shared_ptr camera = currentCameraDevice_.lock(); - if (camera) - { - try - { - mm::DeviceModuleLockGuard guard(camera); - pixSize *= camera->GetBinning(); - } - catch (const CMMError&) // Possibly uninitialized camera - { - // Assume no binning - } - } - + pixSize *= binning; pixSize /= getMagnificationFactor(); return pixSize; @@ -5574,9 +6181,6 @@ double CMMCore::getPixelSizeUm(bool cached) } } -/** - * Returns the pixel size in um for the requested pixel size group - */ double CMMCore::getPixelSizeUmByID(const char* resolutionID) throw (CMMError) { CheckConfigPresetName(resolutionID); @@ -5603,6 +6207,18 @@ std::vector CMMCore::getPixelSizeAffine() throw (CMMError) * and known magnification devices */ std::vector CMMCore::getPixelSizeAffine(bool cached) throw (CMMError) +{ + std::shared_ptr camera = currentCameraDevice_.lock(); + int binning = 1; + if (camera) + { + mm::DeviceModuleLockGuard guard(camera); + binning = camera->GetBinning(); + } + return getPixelSizeAffine(cached, binning); +} + +std::vector CMMCore::getPixelSizeAffine(bool cached, int binning) throw (CMMError) { std::string resolutionID = getCurrentPixelSizeConfig(cached); if (resolutionID.length() > 0) @@ -5611,14 +6227,6 @@ std::vector CMMCore::getPixelSizeAffine(bool cached) throw (CMMError) PixelSizeConfiguration* pCfg = pixelSizeGroup_->Find(resolutionID.c_str()); std::vector af = pCfg->getPixelConfigAffineMatrix(); - std::shared_ptr camera = currentCameraDevice_.lock(); - int binning = 1; - if (camera) - { - mm::DeviceModuleLockGuard guard(camera); - binning = camera->GetBinning(); - } - double factor = binning / getMagnificationFactor(); if (factor != 1.0) @@ -7710,3 +8318,42 @@ std::string CMMCore::getInstalledDeviceDescription(const char* hubLabel, const c } return description.empty() ? "N/A" : description; } + + +/** + * Get the essential metadata for interpretting image data stored in the buffer. + */ +bool CMMCore::parseImageMetadata(Metadata& md, int& width, int& height, int& byteDepth, int& nComponents) +{ + // Check if required metadata tags exist + if (!md.HasTag(MM::g_Keyword_Metadata_Width) || + !md.HasTag(MM::g_Keyword_Metadata_Height) || + !md.HasTag(MM::g_Keyword_PixelType)) + { + return false; + } + + width = std::stoul(md.GetSingleTag(MM::g_Keyword_Metadata_Width).GetValue()); + height = std::stoul(md.GetSingleTag(MM::g_Keyword_Metadata_Height).GetValue()); + nComponents = 1; // Default to 1 component + std::string pixType = md.GetSingleTag(MM::g_Keyword_PixelType).GetValue(); + + if (pixType == MM::g_Keyword_PixelType_GRAY8) + byteDepth = 1; + else if (pixType == MM::g_Keyword_PixelType_GRAY16) + byteDepth = 2; + else if (pixType == MM::g_Keyword_PixelType_GRAY32) + byteDepth = 4; + else if (pixType == MM::g_Keyword_PixelType_RGB32) { + byteDepth = 4; + nComponents = 4; // ARGB format uses 4 components + } + else if (pixType == MM::g_Keyword_PixelType_RGB64) { + byteDepth = 8; + nComponents = 4; // ARGB format uses 4 components + } else { + byteDepth = 1; // Default to 1 byte depth for unknown types + nComponents = 1; + } + return true; +} \ No newline at end of file diff --git a/MMCore/MMCore.h b/MMCore/MMCore.h index 83d67b0b2..bca279a5b 100644 --- a/MMCore/MMCore.h +++ b/MMCore/MMCore.h @@ -65,6 +65,7 @@ #include "Error.h" #include "ErrorCodes.h" #include "Logging/Logger.h" +#include "BufferManager.h" #include #include @@ -88,13 +89,13 @@ class CPluginManager; -class CircularBuffer; class ConfigGroupCollection; class CoreCallback; class CorePropertyCollection; class MMEventCallback; class Metadata; class PixelSizeConfigGroup; +class BufferDataPointer; class AutoFocusInstance; class CameraInstance; @@ -114,6 +115,10 @@ namespace mm { } // namespace mm typedef unsigned int* imgRGB32; +// This is needed for SWIG Java wrapping because +// void* is converted to Objects to copy arrays of data but we want to be able to +// maps these pointers to longs +typedef const void* DataPtr; enum DeviceInitializationState { Uninitialized, @@ -379,6 +384,8 @@ class CMMCore void snapImage() throw (CMMError); void* getImage() throw (CMMError); void* getImage(unsigned numChannel) throw (CMMError); + void* getImageMD(Metadata& md) throw (CMMError); + void* getImageMD(unsigned numChannel, Metadata& md) throw (CMMError); unsigned getImageWidth(); unsigned getImageHeight(); @@ -402,6 +409,7 @@ class CMMCore double intervalMs, bool stopOnOverflow) throw (CMMError); void prepareSequenceAcquisition(const char* cameraLabel) throw (CMMError); void startContinuousSequenceAcquisition(double intervalMs) throw (CMMError); + void startContinuousSequenceAcquisition(const char* cameraLabel, double intervalMs) throw (CMMError); void stopSequenceAcquisition() throw (CMMError); void stopSequenceAcquisition(const char* cameraLabel) throw (CMMError); bool isSequenceRunning() throw (); @@ -418,15 +426,68 @@ class CMMCore const throw (CMMError); void* popNextImageMD(Metadata& md) throw (CMMError); + long getRemainingImageCount(); long getBufferTotalCapacity(); long getBufferFreeCapacity(); bool isBufferOverflowed() const; + + /** + * @deprecated Use setBufferMemoryFootprint() instead + */ void setCircularBufferMemoryFootprint(unsigned sizeMB) throw (CMMError); + /** + * @deprecated Use getBufferMemoryFootprint() instead + */ unsigned getCircularBufferMemoryFootprint(); + + // This is only needed for the circular buffer, because it needs to match the camera settings. Should it be deprecated? void initializeCircularBuffer() throw (CMMError); + /** + * @deprecated Use clearBuffer() instead, but see note in ClearBuffer() about how it does not + * need to be called as frequently as clearCircularBuffer() was + */ void clearCircularBuffer() throw (CMMError); + void setBufferMemoryFootprint(unsigned sizeMB) throw (CMMError); + unsigned getBufferMemoryFootprint() const; + void clearBuffer() throw (CMMError); + void forceBufferReset() throw (CMMError); + + ///@} + + /** \name NewDataBuffer control. */ + ///@{ + void enableNewDataBuffer(bool enable) throw (CMMError); + bool usesNewDataBuffer() const { return bufferManager_->IsUsingNewDataBuffer(); } + + // These functions are used by the Java SWIG wrapper to get properties of the image + // based on a pointer. The DataPtr alias to void* is so they don't get converted to + // Object in the Java SWIG wrapper. + bool getImageProperties(DataPtr ptr, int& width, int& height, int& byteDepth, int& nComponents) throw (CMMError); + void releaseReadAccess(DataPtr ptr) throw (CMMError); + + + // Same functionality as non pointer versions above, but + // enables alternative wrappers in SWIG for pointer-based access to the image data. + + // This one is "Image" not "Data" because it corresponds to SnapImage()/GetImage(), + // Which does only goes through the NewDataBuffer after coming from the camera device buffer, + // so it is guarenteed to be an image, not a more generic piece of data. + BufferDataPointer* getImagePointer() throw (CMMError); + + // These are "Data" not "Image" because they can be used generically for any data type + // Higher level wrapping code can read their associated metadata to determine their data typ + // These ones don't need the metadata versions (e.g. getLastDataMDPointer) because the metadata + // is accessed through the buffer data pointer. + BufferDataPointer* getLastDataPointer() throw (CMMError); + BufferDataPointer* popNextDataPointer() throw (CMMError); + BufferDataPointer* getLastDataFromDevicePointer(std::string deviceLabel) throw (CMMError); + + ///@} + + /** \name Exposure sequence control. */ + ///@{ bool isExposureSequenceable(const char* cameraLabel) throw (CMMError); void startExposureSequence(const char* cameraLabel) throw (CMMError); void stopExposureSequence(const char* cameraLabel) throw (CMMError); @@ -625,6 +686,38 @@ class CMMCore std::vector getLoadedPeripheralDevices(const char* hubLabel) throw (CMMError); ///@} + /** \name Image metadata. */ + ///@{ + + /** + * Unclear why this is needed, deprecate? + */ + bool getIncludeSystemStateCache() { + return imageMDIncludeSystemStateCache_; + } + + /** + * @deprecated Use setIncludeImageMetadata("SystemStateCache", state) instead + */ + void setIncludeSystemStateCache(bool state) { + imageMDIncludeSystemStateCache_ = state; + } + + /** + * Sets which metadata categories should be included with images. + * @param categoryBits Bitmask of metadata categories to include + */ + void setIncludeImageMetadata(unsigned int categoryBits) throw (CMMError); + + /** + * Gets the current metadata inclusion bitmask. + * @return Bitmask indicating which metadata categories are included + */ + unsigned int getIncludeImageMetadata() const throw (CMMError); + ///@} + + static bool parseImageMetadata(Metadata& md, int& width, int& height, int& byteDepth, int& nComponents); + private: // make object non-copyable CMMCore(const CMMCore&); @@ -658,7 +751,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 NewDataBuffer + BufferManager* bufferManager_; std::shared_ptr pluginManager_; std::shared_ptr deviceManager_; @@ -672,6 +766,18 @@ class CMMCore MMThreadLock* pPostedErrorsLock_; mutable std::deque > postedErrors_; + // For adding camera metadata + std::map imageNumbers_; // Track image numbers per camera + std::mutex imageNumbersMutex_; + std::chrono::steady_clock::time_point startTime_; // Start time for elapsed time calculations in seuqence acquisition + bool imageMDIncludeSystemStateCache_; + bool imageMDIncludeBitDepth_; + bool imageMDIncludeCameraParams_; + bool imageMDIncludeCameraTags_; + bool imageMDIncludeTiming_; + bool imageMDIncludeLegacyCalibration_; + bool imageMDIncludeAdditionalLegacy_; + private: void InitializeErrorMessages(); void CreateCoreProperties(); @@ -699,6 +805,24 @@ class CMMCore void initializeAllDevicesSerial() throw (CMMError); void initializeAllDevicesParallel() throw (CMMError); int initializeVectorOfDevices(std::vector, std::string> > pDevices); + + + // Centralized functions for adding and parsing metadata. This includes required metadta + // for interpretting the data like width, height, pixel type, as well as option nice-to-have + // metadata like elapsed time that may be relied on by higher level code. + // If support for other types of data is added in the future, alternative versions of these functions + // should be added. + void addCameraMetadata(std::shared_ptr pCam, Metadata& md, unsigned width, unsigned height, + unsigned byteDepth, unsigned nComponents); + // Additional metadata for the multi-camera device adapter + void addMultiCameraMetadata(Metadata& md, int cameraChannelIndex) const; + // Want to be able to pass in binning so camera doesn't have to be locked and this can + // be called on a camera thread + double getPixelSizeUm(bool cached, int binning); + std::vector getPixelSizeAffine(bool cached, int binning) throw (CMMError); + + + }; #if defined(__GNUC__) && !defined(__clang__) diff --git a/MMCore/MMCore.vcxproj b/MMCore/MMCore.vcxproj index ebfdcb9ba..44c28d7c7 100644 --- a/MMCore/MMCore.vcxproj +++ b/MMCore/MMCore.vcxproj @@ -75,6 +75,8 @@ + + @@ -113,6 +115,9 @@ + + + @@ -181,4 +186,4 @@ - + \ No newline at end of file diff --git a/MMCore/MMCore.vcxproj.filters b/MMCore/MMCore.vcxproj.filters index 1920583b6..7a8d7e1e3 100644 --- a/MMCore/MMCore.vcxproj.filters +++ b/MMCore/MMCore.vcxproj.filters @@ -141,6 +141,12 @@ Source Files + + Source Files + + + Source Files + @@ -305,5 +311,14 @@ Header Files + + Header Files + + + Header Files + + + Header Files + - + \ No newline at end of file diff --git a/MMCore/NewDataBuffer.cpp b/MMCore/NewDataBuffer.cpp new file mode 100644 index 000000000..3a6e64a67 --- /dev/null +++ b/MMCore/NewDataBuffer.cpp @@ -0,0 +1,854 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: NewDataBuffer.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 "NewDataBuffer.h" +#include +#include // for std::this_thread::yield if needed +#include +#include +#include +#include +#include +#include +#include "TaskSet_CopyMemory.h" +#include + + +/////////////////////////////////////////////////////////////////////////////// +// DataBuffer Implementation +/////////////////////////////////////////////////////////////////////////////// + +namespace { + // Get system page size at runtime + inline size_t GetPageSize() { + #ifdef _WIN32 + SYSTEM_INFO si; + GetSystemInfo(&si); + return si.dwPageSize; + #else + return sysconf(_SC_PAGESIZE); + #endif + } + + // Cache the page size. + const size_t PAGE_SIZE = GetPageSize(); + + // Inline alignment function using bitwise operations. + // For a power-of-two alignment, this computes the smallest multiple + // of 'alignment' that is at least as large as 'value'. + inline size_t Align(size_t value) { + // Use PAGE_SIZE if value is large enough; otherwise use the sizeof(max_align_t) + size_t alignment = (value >= PAGE_SIZE) ? PAGE_SIZE : alignof(std::max_align_t); + return (value + alignment - 1) & ~(alignment - 1); + } +} + +DataBuffer::DataBuffer(unsigned int memorySizeMB) + : buffer_(nullptr), + bufferSize_(0), + overwriteWhenFull_(false), + nextAllocOffset_(0), + currentSlotIndex_(0), + overflow_(false), + threadPool_(std::make_shared()), + tasksMemCopy_(std::make_shared(threadPool_)) +{ + ReinitializeBuffer(memorySizeMB, false); +} + +DataBuffer::~DataBuffer() { + if (buffer_) { + #ifdef _WIN32 + VirtualFree(buffer_, 0, MEM_RELEASE); + #else + munmap(buffer_, bufferSize_); + #endif + buffer_ = nullptr; + } + + std::lock_guard lock(slotManagementMutex_); + for (BufferSlot* bs : slotPool_) { + delete bs; + } +} + +/** + * 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); + + #ifdef _WIN32 + buffer_ = (unsigned char*)VirtualAlloc(nullptr, numBytes, + MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE); + if (!buffer_) { + return DEVICE_ERR; + } + + #else + buffer_ = (unsigned char*)mmap(nullptr, numBytes, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); + if (buffer_ == MAP_FAILED) { + buffer_ = nullptr; + return DEVICE_ERR; + } + + // Advise the kernel that we will need this memory soon. + madvise(buffer_, numBytes, MADV_WILLNEED); + + #endif + + bufferSize_ = numBytes; + overflow_ = false; + freeRegions_.clear(); + freeRegions_[0] = bufferSize_; + freeRegionCursor_ = 0; + 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) { + #ifdef _WIN32 + VirtualFree(buffer_, 0, MEM_RELEASE); + #else + munmap(buffer_, bufferSize_); + #endif + buffer_ = nullptr; + return DEVICE_OK; + } + return DEVICE_ERR; +} + +/** + * Pack the data as [BufferSlotRecord][image data][serialized metadata] + */ +int DataBuffer::InsertData(const void* data, size_t dataSize, const Metadata* pMd, const std::string& deviceLabel) { + + void* dataPointer = nullptr; + void* additionalMetadataPointer = nullptr; + + // Convert metadata to serialized string if provided + std::string serializedMetadata; + if (pMd != nullptr) { + serializedMetadata = pMd->Serialize(); + } + // Initial metadata is all metadata because the image and metadata are already complete + int result = AcquireWriteSlot(dataSize, 0, &dataPointer, &additionalMetadataPointer, + serializedMetadata, deviceLabel); + if (result != DEVICE_OK) { + return result; + } + + tasksMemCopy_->MemCopy((void*)dataPointer, data, dataSize); + + // Finalize the write slot. + return FinalizeWriteSlot(dataPointer, 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) { + if (overwriteWhenFull_ == overwrite) { + return DEVICE_OK; + } + + // You can't change modes when code holds pointers into the buffer + if (GetActiveSlotCount() > 0) { + return DEVICE_ERR; + } + + overwriteWhenFull_ = overwrite; + return DEVICE_OK; +} + +/** + * Get whether the buffer should overwrite old data when full. + * @return True if overwriting is enabled, false otherwise. + */ +bool DataBuffer::GetOverwriteData() const { + return overwriteWhenFull_; +} + +/** + * 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::AcquireWriteSlot(size_t dataSize, size_t additionalMetadataSize, + void** dataPointer, + void** additionalMetadataPointer, + const std::string& serializedInitialMetadata, + const std::string& deviceLabel) +{ + if (buffer_ == nullptr) { + return DEVICE_ERR; + } + + // Total size includes data, initial metadata, and any additional metadata space + size_t rawTotalSize = dataSize + serializedInitialMetadata.size() + additionalMetadataSize; + size_t totalSlotSize = Align(rawTotalSize); + size_t candidateStart = 0; + + if (!overwriteWhenFull_) { + std::lock_guard lock(slotManagementMutex_); + // Look in the free-region list as fallback using a cached cursor. + { + bool found = false; + size_t newCandidate = 0; + // Start search from freeRegionCursor_ + auto it = freeRegions_.lower_bound(freeRegionCursor_); + // Loop over free regions at most once (wrapping around if necessary). + for (size_t count = 0, sz = freeRegions_.size(); count < sz; count++) { + if (it == freeRegions_.end()) + it = freeRegions_.begin(); + size_t alignedCandidate = Align(it->first); + if (it->first + it->second >= alignedCandidate + totalSlotSize) { + newCandidate = alignedCandidate; + found = true; + break; + } + ++it; + } + if (found) { + candidateStart = newCandidate; + // Update the cursor so that next search can start here. + freeRegionCursor_ = candidateStart + totalSlotSize; + return CreateSlot(candidateStart, totalSlotSize, dataSize, additionalMetadataSize, + dataPointer, additionalMetadataPointer, + true, serializedInitialMetadata, deviceLabel); + } + } + + // No recycled slot or free region can satisfy the allocation. + overflow_ = true; + *dataPointer = nullptr; + *additionalMetadataPointer = nullptr; + return DEVICE_ERR; + } else { + // Overwrite mode + size_t prevOffset, newOffset; + do { + prevOffset = nextAllocOffset_.load(std::memory_order_relaxed); + candidateStart = Align(prevOffset); + if (candidateStart + totalSlotSize > bufferSize_) + candidateStart = 0; // Wrap around if needed. + newOffset = candidateStart + totalSlotSize; + } while (!nextAllocOffset_.compare_exchange_weak(prevOffset, newOffset)); + + // Only now grab the lock to register the new slot. + { + std::lock_guard lock(slotManagementMutex_); + return CreateSlot(candidateStart, totalSlotSize, dataSize, additionalMetadataSize, + dataPointer, additionalMetadataPointer, false, serializedInitialMetadata, deviceLabel); + } + } +} + +/** + * @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::FinalizeWriteSlot(const void* dataPointer, size_t actualMetadataBytes) { + if (dataPointer == nullptr) + return DEVICE_ERR; + + BufferSlot* slot = nullptr; + { + std::lock_guard lock(slotManagementMutex_); + slot = FindSlotForPointer(dataPointer); + if (!slot) + return DEVICE_ERR; + + // Update the slot with actual metadata size + slot->UpdateAdditionalMetadataSize(actualMetadataBytes); + } + + slot->ReleaseWriteAccess(); + + // Notify waiting threads under a brief lock + { + std::lock_guard lock(slotManagementMutex_); + 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 void* dataPointer) { + if (dataPointer == nullptr) + return DEVICE_ERR; + + // First find the slot without the global lock + BufferSlot* slot = nullptr; + { + std::lock_guard lock(slotManagementMutex_); + slot = FindSlotForPointer(dataPointer); + if (!slot) + return DEVICE_ERR; + } + const size_t offset = static_cast(dataPointer) - + static_cast(buffer_); + + // Release the read access outside the global lock + slot->ReleaseReadAccess(); + + if (!overwriteWhenFull_) { + std::lock_guard lock(slotManagementMutex_); + // Now check if the slot is not being accessed + if (slot->IsFree()) { + auto it = activeSlotsByStart_.find(offset); + DeleteSlot(offset, it); + } + } + + return DEVICE_OK; +} + +const void* DataBuffer::PopNextDataReadPointer(Metadata &md, bool waitForData) +{ + if (overwriteWhenFull_) { + throw std::runtime_error("PopNextDataReadPointer is not available in overwrite mode"); + } + + BufferSlot* slot = nullptr; + size_t slotStart = 0; + + // First, get the slot under the global lock + { + std::unique_lock lock(slotManagementMutex_); + while (activeSlotsVector_.empty()) { + if (!waitForData) + return nullptr; + dataCV_.wait(lock); + } + // Atomically take the slot and advance the index + slot = activeSlotsVector_[currentSlotIndex_]; + slotStart = slot->GetStart(); + currentSlotIndex_ = (currentSlotIndex_ + 1) % activeSlotsVector_.size(); + } // Release global lock + + // Now acquire read access outside the global lock + slot->AcquireReadAccess(); + + const unsigned char* dataPointer = static_cast(buffer_) + slotStart; + this->ExtractMetadata(dataPointer, slot, md); + + return dataPointer; +} + +const void* DataBuffer::PeekLastDataReadPointer(Metadata &md) { + if (!overwriteWhenFull_) { + throw std::runtime_error("PeekLastDataReadPointer is only available in overwrite mode"); + } + + BufferSlot* currentSlot = nullptr; + { + std::unique_lock lock(slotManagementMutex_); + if (activeSlotsVector_.empty()) { + return nullptr; + } + + // Get the most recent slot (last in vector) + currentSlot = activeSlotsVector_.back(); + } + + currentSlot->AcquireReadAccess(); + + const void* result = static_cast(buffer_) + currentSlot->GetStart(); + + if (ExtractMetadata(result, currentSlot, md) != DEVICE_OK) { + currentSlot->ReleaseReadAccess(); + return nullptr; + } + + return result; +} + +const void* DataBuffer::PeekDataReadPointerAtIndex(size_t n, Metadata &md) { + if (!overwriteWhenFull_) { + throw std::runtime_error("PeekDataReadPointerAtIndex is only available in overwrite mode"); + } + + BufferSlot* currentSlot = nullptr; + { + // Lock the global slot management mutex to safely access the active slots. + std::unique_lock lock(slotManagementMutex_); + if (activeSlotsVector_.empty() || n >= activeSlotsVector_.size()) { + return nullptr; + } + + // Instead of looking ahead from currentSlotIndex_, we look back from the end. + // For n==0, return the most recent slot; for n==1, the one before it; etc. + size_t index = activeSlotsVector_.size() - n - 1; + currentSlot = activeSlotsVector_[index]; + } + + currentSlot->AcquireReadAccess(); + + const unsigned char* dataPointer = static_cast(buffer_) + currentSlot->GetStart(); + this->ExtractMetadata(dataPointer, currentSlot, md); + + return dataPointer; +} + +const void* DataBuffer::PeekLastDataReadPointerFromDevice(const std::string& deviceLabel, Metadata& md) { + if (!overwriteWhenFull_) { + throw std::runtime_error("PeekLastDataReadPointerFromDevice is only available in overwrite mode"); + } + + BufferSlot* matchingSlot = nullptr; + { + std::unique_lock lock(slotManagementMutex_); + + // Search backwards through activeSlotsVector_ to find most recent matching slot + for (auto it = activeSlotsVector_.rbegin(); it != activeSlotsVector_.rend(); ++it) { + if ((*it)->GetDeviceLabel() == deviceLabel) { + matchingSlot = *it; + break; + } + } + + if (!matchingSlot) { + return nullptr; + } + } + + // Acquire read access and get data pointer + matchingSlot->AcquireReadAccess(); + + const void* result = static_cast(buffer_) + matchingSlot->GetStart(); + + if (ExtractMetadata(result, matchingSlot, md) != DEVICE_OK) { + matchingSlot->ReleaseReadAccess(); + return nullptr; + } + + return result; +} + +unsigned int DataBuffer::GetMemorySizeMB() const { + // Convert bytes to MB (1 MB = 1048576 bytes) + return static_cast(bufferSize_ >> 20); +} + +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. + * @param forceReset If true, the buffer will be reset even if there are outstanding active slots. + * This is a dangerous operation operation becuase there may be pointers into the buffer's memory + * that are not valid anymore. It can be used to reset the buffer without having to restart the + * application, but it indicates a bug in the application or device adapter that is not properly + * releasing the buffer's memory. + * @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, bool forceReset) { + std::lock_guard lock(slotManagementMutex_); + + // Ensure no active readers/writers exist. + if (!forceReset) { + for (BufferSlot* slot : activeSlotsVector_) { + if (!slot->IsFree()) { + throw std::runtime_error("Cannot reinitialize DataBuffer: outstanding active slot detected."); + } + } + } + + // Clear internal data structures + activeSlotsVector_.clear(); + activeSlotsByStart_.clear(); + currentSlotIndex_ = 0; + nextAllocOffset_ = 0; + overflow_ = false; + + // Pre-allocate slots (one per MB) and store in both slotPool_ and unusedSlots_ + slotPool_.clear(); + unusedSlots_.clear(); + for (unsigned int i = 0; i < memorySizeMB; i++) { + BufferSlot* bs = new BufferSlot(); + slotPool_.push_back(bs); + unusedSlots_.push_back(bs); + } + + + // Release and reallocate the buffer + if (buffer_ != nullptr) { + ReleaseBuffer(); + } + + return AllocateBuffer(memorySizeMB); +} + +void DataBuffer::Clear() { + if (NumOutstandingSlots() > 0) { + throw std::runtime_error("Cannot clear DataBuffer: outstanding active slot detected."); + } + std::lock_guard lock(slotManagementMutex_); + activeSlotsVector_.clear(); + activeSlotsByStart_.clear(); + currentSlotIndex_ = 0; + nextAllocOffset_ = 0; + // reset the unused slot pool + unusedSlots_.clear(); + for (BufferSlot* bs : slotPool_) { + unusedSlots_.push_back(bs); + } + // Rest freee regions to whole buffer + freeRegions_.clear(); + freeRegions_[0] = bufferSize_; + freeRegionCursor_ = 0; +} + +long DataBuffer::GetActiveSlotCount() const { + return static_cast(activeSlotsVector_.size()); +} + +int DataBuffer::ExtractMetadata(const void* dataPointer, BufferSlot* slot, Metadata &md) { + // No lock is required here because we assume the slot is already locked + + if (!dataPointer || !slot) + return DEVICE_ERR; // Invalid pointer + + // Calculate metadata pointers and sizes from the slot + const unsigned char* initialMetadataPtr = static_cast(dataPointer) + slot->GetDataSize(); + size_t initialMetadataSize = slot->GetInitialMetadataSize(); + const unsigned char* additionalMetadataPtr = initialMetadataPtr + initialMetadataSize; + size_t additionalMetadataSize = slot->GetAdditionalMetadataSize(); + + // Handle initial metadata if present + if (initialMetadataSize > 0) { + Metadata initialMd; + std::string initialMetaStr(reinterpret_cast(initialMetadataPtr), initialMetadataSize); + initialMd.Restore(initialMetaStr.c_str()); + md.Merge(initialMd); + } + + // Handle additional metadata if present + if (additionalMetadataSize > 0) { + Metadata additionalMd; + std::string additionalMetaStr(reinterpret_cast(additionalMetadataPtr), additionalMetadataSize); + additionalMd.Restore(additionalMetaStr.c_str()); + md.Merge(additionalMd); + } + + return DEVICE_OK; +} + +// NOTE: Caller must hold slotManagementMutex_ for thread safety. +BufferSlot* DataBuffer::FindSlotForPointer(const void* dataPointer) { + assert(!slotManagementMutex_.try_lock() && "Caller must hold slotManagementMutex_"); + if (buffer_ == nullptr) + return nullptr; + std::size_t offset = static_cast(dataPointer) - + static_cast(buffer_); + auto it = activeSlotsByStart_.find(offset); + return (it != activeSlotsByStart_.end()) ? it->second : nullptr; +} + +void DataBuffer::MergeFreeRegions(size_t newRegionStart, size_t newRegionEnd) { + assert(!slotManagementMutex_.try_lock() && "Caller must hold slotManagementMutex_"); + // Find the free region that starts at or after newEnd + auto right = freeRegions_.lower_bound(newRegionEnd); + + // Check if there is a free region immediately preceding the new region + auto left = freeRegions_.lower_bound(newRegionStart); + if (left != freeRegions_.begin()) { + auto prev = std::prev(left); + // If the previous region's end matches the new region's start... + if (prev->first + prev->second == newRegionStart) { + newRegionStart = prev->first; + freeRegions_.erase(prev); + } + } + + // Check if the region immediately to the right can be merged + if (right != freeRegions_.end() && right->first == newRegionEnd) { + newRegionEnd = right->first + right->second; + freeRegions_.erase(right); + } + + // Insert the merged (or standalone) free region + size_t newRegionSize = (newRegionEnd > newRegionStart ? newRegionEnd - newRegionStart : 0); + if (newRegionSize > 0) { + freeRegions_[newRegionStart] = newRegionSize; + } +} + +void DataBuffer::RemoveFromActiveTracking(size_t offset, std::unordered_map::iterator it) { + assert(!slotManagementMutex_.try_lock() && "Caller must hold slotManagementMutex_"); + 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 the currentSlotIndex_; if the deleted slot was before it, decrement. + if (currentSlotIndex_ > indexDeleted) + currentSlotIndex_--; + break; + } + } +} + +void DataBuffer::DeleteSlot(size_t offset, std::unordered_map::iterator it) { + assert(!slotManagementMutex_.try_lock() && "Caller must hold slotManagementMutex_"); + + size_t newRegionStart = offset; + size_t newRegionEnd = offset + it->second->GetLength(); + + // Return the slot to the pool before removing from active tracking + ReturnSlotToPool(it->second); + + MergeFreeRegions(newRegionStart, newRegionEnd); + RemoveFromActiveTracking(offset, it); +} + +void DataBuffer::UpdateFreeRegions(size_t candidateStart, size_t totalSlotSize) { + assert(!slotManagementMutex_.try_lock() && "Caller must hold slotManagementMutex_"); + auto it = freeRegions_.upper_bound(candidateStart); + if (it != freeRegions_.begin()) { + --it; + size_t freeRegionStart = it->first; + size_t freeRegionSize = it->second; + size_t freeRegionEnd = freeRegionStart + freeRegionSize; + + if (candidateStart >= freeRegionStart && + candidateStart + totalSlotSize <= freeRegionEnd) { + freeRegions_.erase(it); + + if (candidateStart > freeRegionStart) { + size_t gap = candidateStart - freeRegionStart; + if (gap > 0) + freeRegions_.insert({freeRegionStart, gap}); + } + + if (freeRegionEnd > candidateStart + totalSlotSize) { + size_t gap = freeRegionEnd - (candidateStart + totalSlotSize); + if (gap > 0) + freeRegions_.insert({candidateStart + totalSlotSize, gap}); + } + } + } +} + +int DataBuffer::CreateSlot(size_t candidateStart, size_t totalSlotSize, + size_t dataSize, size_t additionalMetadataSize, + void** dataPointer, void** additionalMetadataPointer, + bool fromFreeRegion, const std::string& serializedInitialMetadata, + const std::string& deviceLabel) +{ + assert(!slotManagementMutex_.try_lock() && "Caller must hold slotManagementMutex_"); + BufferSlot* newSlot = GetSlotFromPool(candidateStart, totalSlotSize, + dataSize, serializedInitialMetadata.size(), + additionalMetadataSize, deviceLabel); + + newSlot->AcquireWriteAccess(); + + // Initialize the data pointer before using it. + *dataPointer = static_cast(buffer_) + newSlot->GetStart(); + + if (!serializedInitialMetadata.empty()) { + std::memcpy(static_cast(*dataPointer) + dataSize, serializedInitialMetadata.data(), + serializedInitialMetadata.size()); + newSlot->SetInitialMetadataSize(serializedInitialMetadata.size()); + } + + *additionalMetadataPointer = static_cast(*dataPointer) + newSlot->GetDataSize() + newSlot->GetInitialMetadataSize(); + + if (fromFreeRegion) { + UpdateFreeRegions(candidateStart, totalSlotSize); + } + + return DEVICE_OK; +} + +BufferSlot* DataBuffer::GetSlotFromPool(size_t start, size_t totalLength, + size_t dataSize, size_t initialMetadataSize, + size_t additionalMetadataSize, + const std::string& deviceLabel) { + assert(!slotManagementMutex_.try_lock() && "Caller must hold slotManagementMutex_"); + + // Grow the pool if needed. + if (unusedSlots_.empty()) { + BufferSlot* newSlot = new BufferSlot(); + slotPool_.push_back(newSlot); + unusedSlots_.push_back(newSlot); + } + + // Get a slot from the front of the deque. + BufferSlot* slot = unusedSlots_.front(); + unusedSlots_.pop_front(); + slot->Reset(start, totalLength, dataSize, initialMetadataSize, additionalMetadataSize, deviceLabel); + + // Add to active tracking. + activeSlotsVector_.push_back(slot); + activeSlotsByStart_[start] = slot; + return slot; +} + +void DataBuffer::ReturnSlotToPool(BufferSlot* slot) { + assert(!slotManagementMutex_.try_lock() && "Caller must hold slotManagementMutex_"); + unusedSlots_.push_back(slot); +} + +int DataBuffer::ExtractCorrespondingMetadata(const void* dataPointer, Metadata &md) { + BufferSlot* slot = nullptr; + { + std::lock_guard lock(slotManagementMutex_); + slot = FindSlotForPointer(dataPointer); + if (!slot) { + return DEVICE_ERR; + } + } + + // Extract metadata (internal method doesn't need lock) + return ExtractMetadata(dataPointer, slot, md); +} + +size_t DataBuffer::GetDatumSize(const void* dataPointer) { + std::lock_guard lock(slotManagementMutex_); + BufferSlot* slot = FindSlotForPointer(dataPointer); + if (!slot) { + throw std::runtime_error("DataBuffer::GetDatumSize: pointer not found in buffer"); + } + return slot->GetDataSize(); +} + +bool DataBuffer::IsPointerInBuffer(const void* ptr) { + if (buffer_ == nullptr || ptr == nullptr) { + return false; + } + // get the mutex + std::lock_guard lock(slotManagementMutex_); + // find the slot + BufferSlot* slot = FindSlotForPointer(ptr); + return slot != nullptr; +} + +int DataBuffer::NumOutstandingSlots() const { + std::lock_guard lock(slotManagementMutex_); + int numOutstanding = 0; + for (const BufferSlot* slot : activeSlotsVector_) { + if (!slot->IsFree()) { + numOutstanding++; + } + } + return numOutstanding; +} diff --git a/MMCore/NewDataBuffer.h b/MMCore/NewDataBuffer.h new file mode 100644 index 000000000..df7601c70 --- /dev/null +++ b/MMCore/NewDataBuffer.h @@ -0,0 +1,503 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: NewDataBuffer.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 tracked via RAII-based synchronization. +// Write access is protected via 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 +#include "TaskSet_CopyMemory.h" +#include +#include + +/** + * BufferSlot represents a contiguous slot in the DataBuffer that holds image + * data and metadata. It uses RAII-based locking with std::shared_timed_mutex to + * support exclusive write access and concurrent shared read access. + */ +class BufferSlot { +public: + /** + * Constructs a BufferSlot with all sizes specified up front. + * + * @param start The starting offset (in bytes) within the buffer. + * @param totalLength The total length (in bytes) reserved for this slot, typically + * an aligned size (which includes image data and metadata). + * @param imageSize The exact number of bytes for the image data. + * @param metadataSize The exact number of bytes for the metadata. + */ + BufferSlot() + : start_(0), + length_(0), + imageSize_(0), + initialMetadataSize_(0), + additionalMetadataSize_(0), + deviceLabel_(), + rwMutex_() + { + } + + /** + * Destructor. + */ + ~BufferSlot() {}; + + /** + * Returns the starting offset (in bytes) of the slot. + * @return The slot's start offset. + */ + std::size_t GetStart() const { return start_; } + + /** + * Returns the length (in bytes) of the slot. + * + * @return The slot's length. + */ + std::size_t GetLength() const { return length_; } + + /** + * Acquires exclusive (write) access (blocking). + */ + void AcquireWriteAccess() { + rwMutex_.lock(); + } + + /** + * Releases exclusive write access. + */ + void ReleaseWriteAccess() { + rwMutex_.unlock(); + } + + /** + * Acquires shared read access (blocking). + */ + void AcquireReadAccess() { + rwMutex_.lock_shared(); + } + + /** + * Releases shared read access. + */ + void ReleaseReadAccess() { + rwMutex_.unlock_shared(); + } + + /** + * Checks if the slot is completely free (no readers, no writers). + * We do this by attempting to lock it exclusively: + * if we succeed, there are no concurrent locks. + */ + bool IsFree() const { + std::unique_lock lk(rwMutex_, std::try_to_lock); + return lk.owns_lock(); + } + + /** + * Resets the slot with new parameters. + * The assertion uses IsFree() as a best-effort check before reinitializing. + */ + void Reset(size_t start, + size_t length, + size_t imageSize, + size_t initialMetadataSize, + size_t additionalMetadataSize, + const std::string& deviceLabel) + { + // If this fails, there's likely an active read or write lock on the slot. + assert(IsFree() && + "BufferSlot mutex still locked during Reset - indicates a bug!"); + + start_ = start; + length_ = length; + imageSize_ = imageSize; + initialMetadataSize_ = initialMetadataSize; + additionalMetadataSize_ = initialMetadataSize + additionalMetadataSize; + deviceLabel_ = deviceLabel; + } + + /** + * Updates the metadata size after writing is complete. + * @param newSize The actual size of the written metadata. + */ + void UpdateAdditionalMetadataSize(size_t newSize) { + additionalMetadataSize_ = newSize; + } + + /** + * Record the number of bytes of the initial metadata that have been written to this slot. + */ + void SetInitialMetadataSize(size_t initialSize) { + initialMetadataSize_ = initialSize; + } + + /** + * Returns the size of the image data in bytes. + * @return The image data size. + */ + std::size_t GetDataSize() const { + return imageSize_; + } + + /** + * Returns the size of the initial metadata in bytes. + * @return The initial metadata size. + */ + std::size_t GetInitialMetadataSize() const { + return initialMetadataSize_; + } + + /** + * Returns the size of the additional metadata in bytes. + * @return The additional metadata size. + */ + std::size_t GetAdditionalMetadataSize() const { + return additionalMetadataSize_; + } + + const std::string& GetDeviceLabel() const { + return deviceLabel_; + } + +private: + std::size_t start_; + std::size_t length_; + size_t imageSize_; + size_t initialMetadataSize_; + size_t additionalMetadataSize_; + std::string deviceLabel_; + + mutable std::shared_timed_mutex rwMutex_; +}; + +/** + * DataBuffer manages a contiguous block of memory divided into BufferSlot objects + * for storing image data and metadata. Each slot in memory holds + * only the image data (followed immediately by metadata), while header information + * is maintained in the BufferSlot objects. + */ +class DataBuffer { +public: + + /** + * Constructor. + * @param memorySizeMB The size (in megabytes) of the buffer. + */ + DataBuffer(unsigned int memorySizeMB); + + /** + * Destructor. + */ + ~DataBuffer(); + + /** + * Inserts image data along with metadata into the buffer. + * @param data Pointer to the raw image data. + * @param dataSize The image data size in bytes. + * @param pMd Pointer to the metadata (can be null if not applicable). + * @param deviceLabel The label of the device that is the source of the data. + * @return DEVICE_OK on success. + */ + int InsertData(const void* data, size_t dataSize, const Metadata* pMd, const std::string& deviceLabel); + + /** + * Sets whether the buffer should overwrite old data when full. + * @param overwrite True to enable overwriting. + * @return DEVICE_OK on success. + */ + int SetOverwriteData(bool overwrite); + + /** + * Returns whether the buffer should overwrite old data when full. + * @return True if overwriting is enabled, false otherwise. + */ + bool GetOverwriteData() const; + + /** + * Acquires a write slot large enough to hold the image data and metadata. + * On success, returns pointers for the image data and metadata regions. + * + * @param dataSize The number of bytes reserved for data. + * @param additionalMetadataSize The maximum number of bytes reserved for additional metadata. + * @param dataPointer On success, receives a pointer to the data region. + * @param additionalMetadataPointer On success, receives a pointer to the additional metadata region. + * @param serializedInitialMetadata Optional string containing initial metadata to write. + * @return DEVICE_OK on success. + */ + int AcquireWriteSlot(size_t dataSize, size_t additionalMetadataSize, + void** dataPointer, + void** additionalMetadataPointer, + const std::string& serializedInitialMetadata, + const std::string& deviceLabel); + + /** + * Finalizes (releases) a write slot after data has been written. + * Requires the actual number of metadata bytes written. + * + * @param imageDataPointer Pointer previously obtained from AcquireWriteSlot. + * @param actualMetadataBytes The actual number of metadata bytes written. + * @return DEVICE_OK on success. + */ + int FinalizeWriteSlot(const void* dataPointer, size_t actualMetadataBytes); + + /** + * Releases read access for the image data after reading. + * @param imageDataPointer Pointer previously obtained from reading routines. + * @return DEVICE_OK on success. + */ + int ReleaseDataReadPointer(const void* dataPointer); + + /** + * Retrieves and consumes the next available data entry for reading, + * populating the provided Metadata object. + * @param md Metadata object to populate. + * @param waitForData If true, blocks until data is available. + * @return Pointer to the image data region, or nullptr if none available. + */ + const void* PopNextDataReadPointer(Metadata &md, bool waitForData); + + /** + * Peeks at the most recently added data entry. + * @param md Metadata object populated from the stored metadata. + * @return Pointer to the start of the data region. + */ + const void* PeekLastDataReadPointer(Metadata &md); + + /** + * Peeks at the nth unread data entry without consuming it. + * (n = 0 is equivalent to PeekNextDataReadPointer). + * @param n Index of the data entry to peek at (0 for next available). + * @param md Metadata object populated from the stored metadata. + * @return Pointer to the start of the data region. + */ + const void* PeekDataReadPointerAtIndex(size_t n, Metadata &md); + + /** + * Get the last image inserted by a specific device. + * @param deviceLabel The label of the device to get the image from. + * @param md Metadata object to populate. + * @return Pointer to the image data, or nullptr if not found. + */ + const void* PeekLastDataReadPointerFromDevice(const std::string& deviceLabel, Metadata& md); + + /** + * Returns the total buffer memory size (in MB). + * @return Buffer size in MB. + */ + unsigned int GetMemorySizeMB() const; + + /** + * Returns the number of occupied buffer slots. + * @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 remaining in bytes. + * @return Free byte count. + */ + size_t GetFreeMemory() const; + + /** + * Indicates whether a buffer overflow has occurred. + * @return True if an insert failed (buffer full), false otherwise. + */ + bool Overflow() const; + + /** + * Returns the number of unread slots in the buffer. + * @return Unread slot count. + */ + long GetActiveSlotCount() const; + + /** + * Extracts metadata for a given image data pointer. + * Thread-safe method that acquires necessary locks to lookup metadata location. + * + * @param dataPtr Pointer to the (usuallyimage data. + * @param md Metadata object to populate. + * @return DEVICE_OK on success, or an error code if extraction fails. + */ + int ExtractCorrespondingMetadata(const void* dataPtr, Metadata &md); + + /** + * Returns the size of the data portion of the slot in bytes. + * @param dataPointer Pointer to the data portion of a slot. + * @return Size in bytes of the data portion, or 0 if pointer is invalid. + */ + size_t GetDatumSize(const void* dataPointer); + + /** + * Check if a pointer is within the buffer's memory range. + * @param ptr The pointer to check. + * @return true if the pointer is within the buffer, false otherwise. + */ + bool IsPointerInBuffer(const void* ptr); + + /** + * Checks if there are any outstanding slots in the buffer. If so, it + * is unsafe to destroy the buffer. + * @return true if there are outstanding slots, false otherwise. + */ + int NumOutstandingSlots() const; + + /** + * Reinitializes the DataBuffer, clearing its structures and allocating a new buffer. + * @param memorySizeMB New buffer size (in MB). + * @param forceReset If true, the buffer will be reset even if there are outstanding active slots. + * This is a dangerous operation operation becuase there may be pointers into the buffer's memory + * that are not valid anymore. It can be used to reset the buffer without having to restart the + * application, but it indicates a bug in the application or device adapter that is not properly + * releasing the buffer's memory. + * @return DEVICE_OK on success. + * @throws std::runtime_error if any slot is still in use. + */ + int ReinitializeBuffer(unsigned int memorySizeMB, bool forceReset); + + /** + * Clears the buffer, discarding all data that does not have outstanding pointers. + */ + void Clear(); + + + +private: + + /** + * Allocates the memory buffer. + * @param memorySizeMB Size in megabytes. + * @return DEVICE_OK on success. + */ + int AllocateBuffer(unsigned int memorySizeMB); + + /** + * Releases the memory buffer. + * @return DEVICE_OK on success. + */ + int ReleaseBuffer(); + + /** + * Internal helper function that finds the slot for a given pointer. + * Returns non-const pointer since slots need to be modified for locking. + * + * @param dataPtr Pointer to the data. + * @return Pointer to the corresponding BufferSlot, or nullptr if not found. + */ + BufferSlot* FindSlotForPointer(const void* dataPtr); + + // Memory managed by the DataBuffer. + void* buffer_; + size_t bufferSize_; + + // Whether to overwrite old data when full. + bool overwriteWhenFull_; + + // Overflow flag (set if insert fails due to full buffer). + bool overflow_; + + // Active slots and their mapping. + std::vector activeSlotsVector_; + std::unordered_map activeSlotsByStart_; + + // Free region list for non-overwrite mode. + // Map from starting offset -> region size (in bytes). + std::map freeRegions_; + + // Cached cursor for scanning free regions in non-overwrite mode. + size_t freeRegionCursor_; + + // Instead of ownership via unique_ptr, store raw pointers + std::deque unusedSlots_; + + // This container holds the ownership; they live for the lifetime of the buffer. + std::vector slotPool_; + + // Next free offset within the buffer. + // In overwrite mode, new allocations will come from this pointer. + std::atomic nextAllocOffset_; + + // Index tracking the next slot for read. + size_t currentSlotIndex_; + + // Synchronization for slot management. + std::condition_variable dataCV_; + mutable std::mutex slotManagementMutex_; + + // Members for multithreaded copying. + std::shared_ptr threadPool_; + std::shared_ptr tasksMemCopy_; + + void DeleteSlot(size_t offset, std::unordered_map::iterator it); + + void MergeFreeRegions(size_t newRegionStart, size_t newRegionEnd); + void RemoveFromActiveTracking(size_t offset, std::unordered_map::iterator it); + + void UpdateFreeRegions(size_t candidateStart, size_t totalSlotSize); + + BufferSlot* GetSlotFromPool(size_t start, size_t totalLength, + size_t dataSize, size_t initialMetadataSize, + size_t additionalMetadataSize, const std::string& deviceLabel); + + + /** + * Creates a new slot with the specified parameters. + * Caller must hold slotManagementMutex_. + */ + int CreateSlot(size_t candidateStart, size_t totalSlotSize, + size_t dataSize, size_t additionalMetadataSize, + void** dataPointer, + void** subsequentMetadataPointer, + bool fromFreeRegion, + const std::string& serializedInitialMetadata, + const std::string& deviceLabel); + + + void ReturnSlotToPool(BufferSlot* slot); + + int ExtractMetadata(const void* dataPointer, + BufferSlot* slot, + Metadata &md); + +}; diff --git a/MMCore/NewDataBufferPointer.h b/MMCore/NewDataBufferPointer.h new file mode 100644 index 000000000..35fc6a9ae --- /dev/null +++ b/MMCore/NewDataBufferPointer.h @@ -0,0 +1,131 @@ +/////////////////////////////////////////////////////////////////////////////// +// FILE: NewDataBufferPointer.h +// PROJECT: Micro-Manager +// SUBSYSTEM: MMCore +//----------------------------------------------------------------------------- +// DESCRIPTION: A read-only wrapper class for accessing image data and metadata +// from a buffer slot. Provides safe access to image data by +// automatically releasing read access when the object is destroyed. +// Includes methods for retrieving pixel data and associated +// metadata.. +// +// 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, 2/16/2025 +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "MMCore.h" +#include "../MMDevice/ImageMetadata.h" +#include +#include + +// This is needed for SWIG Java wrapping to differentiate its void* +// from the void* that MMCore uses for returning data +typedef const void* BufferDataPointerVoidStar; + +/// A read-only wrapper for accessing image data and metadata from a buffer slot. +/// Automatically releases the read access when destroyed. +class BufferDataPointer { + +public: + + BufferDataPointer(BufferManager* bufferManager, DataPtr ptr) + : bufferManager_(bufferManager), ptr_(ptr), mutex_() + { + // check for null pointer + if (!ptr_) { + throw CMMError("Pointer is null"); + } + // check for v2 buffer use + if (!bufferManager_->IsUsingNewDataBuffer()) { + throw CMMError("V2 buffer must be enabled for BufferDataPointer"); + } + // throw an error if the pointer is not in the buffer + if (!bufferManager_->IsPointerInNewDataBuffer(ptr_)) { + throw CMMError("Pointer is not in the buffer"); + } + } + + // Returns a pointer to the pixel data (read-only) + BufferDataPointerVoidStar getData() const { + if (!ptr_) { + return nullptr; + } + return ptr_; + } + + // Same as the above method, but this get wrapped by SWIG differently + // to return the actual + DataPtr getDataPointer() const { + if (!ptr_) { + return nullptr; + } + return ptr_; + } + + bool getImageProperties(int& width, int& height, int& byteDepth, int& nComponents) throw (CMMError) { + if (!bufferManager_ || !ptr_) { + throw CMMError("Invalid buffer manager or pointer"); + } + Metadata md; + bufferManager_->ExtractMetadata(ptr_, md); + return CMMCore::parseImageMetadata(md, width, height, byteDepth, nComponents); + } + + // Fills the provided Metadata object with metadata extracted from the pointer. + // It encapsulates calling the core API function that copies metadata from the buffer. + void getMetadata(Metadata &md) const { + if (bufferManager_ && ptr_) { + bufferManager_->ExtractMetadata(ptr_, md); + } + } + + // Destructor: releases the read access to the pointer if not already released + ~BufferDataPointer() { + release(); + } + + + // Explicitly release the pointer before destruction if needed + void release() { + std::lock_guard lock(mutex_); + if (bufferManager_ && ptr_) { + + int ret = bufferManager_->ReleaseReadAccess(ptr_); + if (ret != DEVICE_OK) { + throw CMMError("Failed to release read access to buffer"); + } + ptr_ = nullptr; // Mark as released + + } + } + + unsigned getSizeBytes() const { + if (bufferManager_ && ptr_) { + return bufferManager_->GetDataSize(ptr_); + } + return 0; + } + +private: + // Disable copy semantics to avoid double releasing the pointer + BufferDataPointer(const BufferDataPointer&); + BufferDataPointer& operator=(const BufferDataPointer&); + + BufferManager* bufferManager_; + const void* ptr_; + mutable std::mutex mutex_; +}; diff --git a/MMCoreJ_wrap/MMCoreJ.i b/MMCoreJ_wrap/MMCoreJ.i index 84a71459b..fdaabde4c 100644 --- a/MMCoreJ_wrap/MMCoreJ.i +++ b/MMCoreJ_wrap/MMCoreJ.i @@ -288,6 +288,10 @@ %apply int &OUTPUT { int &xSize }; %apply int &OUTPUT { int &ySize }; +%apply int &OUTPUT { int &width }; +%apply int &OUTPUT { int &height }; +%apply int &OUTPUT { int &byteDepth }; +%apply int &OUTPUT { int &nComponents }; // Java typemap // change default SWIG mapping of unsigned char* return values @@ -367,14 +371,14 @@ for (int i = 0; i < listSize; ++i) { jbyteArray pixels = (jbyteArray) jenv->CallObjectMethod($input, getMethodID, i); long receivedLength = jenv->GetArrayLength(pixels); - if (receivedLength != expectedLength && receivedLength != expectedLength*4) - { - jclass excep = jenv->FindClass("java/lang/Exception"); - if (excep) - jenv->ThrowNew(excep, "Image dimensions are wrong for this SLM."); - return; - } - inputVector.push_back((unsigned char *) JCALL2(GetByteArrayElements, jenv, pixels, 0)); + if (receivedLength != expectedLength && receivedLength != expectedLength*4) + { + jclass excep = jenv->FindClass("java/lang/Exception"); + if (excep) + jenv->ThrowNew(excep, "Image dimensions are wrong for this SLM."); + return; + } + inputVector.push_back((unsigned char *) JCALL2(GetByteArrayElements, jenv, pixels, 0)); } $1 = inputVector; } @@ -395,20 +399,29 @@ // unsigned GetImageWidth() // unsigned GetImageHeight() -%typemap(jni) void* "jobject" -%typemap(jtype) void* "Object" -%typemap(jstype) void* "Object" + +%typemap(jni) void* "jobject" +%typemap(jtype) void* "Object" +%typemap(jstype) void* "Object" %typemap(javaout) void* { return $jnicall; } %typemap(out) void* { - long lSize = (arg1)->getImageWidth() * (arg1)->getImageHeight(); + if (result == NULL) { + $result = 0; + return $result; + } + + int width, height, bytesPerPixel, numComponents; + (arg1)->getImageProperties(result, width, height, bytesPerPixel, numComponents); + unsigned numPixels = width * height; + - if ((arg1)->getBytesPerPixel() == 1) + if (bytesPerPixel == 1) { // create a new byte[] object in Java - jbyteArray data = JCALL1(NewByteArray, jenv, lSize); + jbyteArray data = JCALL1(NewByteArray, jenv, numPixels); if (data == 0) { jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); @@ -420,14 +433,15 @@ } // copy pixels from the image buffer - JCALL4(SetByteArrayRegion, jenv, data, 0, lSize, (jbyte*)result); + JCALL4(SetByteArrayRegion, jenv, data, 0, numPixels, (jbyte*)result); + (arg1)->releaseReadAccess(result); $result = data; } - else if ((arg1)->getBytesPerPixel() == 2) + else if (bytesPerPixel == 2) { // create a new short[] object in Java - jshortArray data = JCALL1(NewShortArray, jenv, lSize); + jshortArray data = JCALL1(NewShortArray, jenv, numPixels); if (data == 0) { jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); @@ -438,16 +452,17 @@ } // copy pixels from the image buffer - JCALL4(SetShortArrayRegion, jenv, data, 0, lSize, (jshort*)result); + JCALL4(SetShortArrayRegion, jenv, data, 0, numPixels, (jshort*)result); + (arg1)->releaseReadAccess(result); $result = data; } - else if ((arg1)->getBytesPerPixel() == 4) + else if (bytesPerPixel == 4) { - if ((arg1)->getNumberOfComponents() == 1) + if (numComponents == 1) { // create a new float[] object in Java - jfloatArray data = JCALL1(NewFloatArray, jenv, lSize); + jfloatArray data = JCALL1(NewFloatArray, jenv, numPixels); if (data == 0) { jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); @@ -459,14 +474,15 @@ } // copy pixels from the image buffer - JCALL4(SetFloatArrayRegion, jenv, data, 0, lSize, (jfloat*)result); + JCALL4(SetFloatArrayRegion, jenv, data, 0, numPixels, (jfloat*)result); + (arg1)->releaseReadAccess(result); $result = data; } else { // create a new byte[] object in Java - jbyteArray data = JCALL1(NewByteArray, jenv, lSize * 4); + jbyteArray data = JCALL1(NewByteArray, jenv, numPixels * 4); if (data == 0) { jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); @@ -478,15 +494,16 @@ } // copy pixels from the image buffer - JCALL4(SetByteArrayRegion, jenv, data, 0, lSize * 4, (jbyte*)result); + JCALL4(SetByteArrayRegion, jenv, data, 0, numPixels * 4, (jbyte*)result); + (arg1)->releaseReadAccess(result); $result = data; } } - else if ((arg1)->getBytesPerPixel() == 8) + else if (bytesPerPixel == 8) { // create a new short[] object in Java - jshortArray data = JCALL1(NewShortArray, jenv, lSize * 4); + jshortArray data = JCALL1(NewShortArray, jenv, numPixels * 4); if (data == 0) { jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); @@ -497,11 +514,11 @@ } // copy pixels from the image buffer - JCALL4(SetShortArrayRegion, jenv, data, 0, lSize * 4, (jshort*)result); + JCALL4(SetShortArrayRegion, jenv, data, 0, numPixels * 4, (jshort*)result); + (arg1)->releaseReadAccess(result); $result = data; } - else { // don't know how to map @@ -510,33 +527,65 @@ } } -// Java typemap -// change default SWIG mapping of void* return values -// to return CObject containing array of pixel values -// -// Assumes that class has the following methods defined: -// unsigned GetImageWidth() -// unsigned GetImageHeight() -// unsigned GetImageDepth() -// unsigned GetNumberOfComponents() - - -%typemap(jni) unsigned int* "jobject" -%typemap(jtype) unsigned int* "Object" -%typemap(jstype) unsigned int* "Object" -%typemap(javaout) unsigned int* { +// This is conceptually similar to the void* typemap above, +// but requires slightly different calls because BufferDataPointer +// is different from the data-returning void* methods of the Core. +%typemap(jni) BufferDataPointerVoidStar "jobject" +%typemap(jtype) BufferDataPointerVoidStar "Object" +%typemap(jstype) BufferDataPointerVoidStar "Object" +%typemap(javaout) BufferDataPointerVoidStar { return $jnicall; } -%typemap(out) unsigned int* +%typemap(out) BufferDataPointerVoidStar { - long lSize = (arg1)->getImageWidth() * (arg1)->getImageHeight(); - unsigned numComponents = (arg1)->getNumberOfComponents(); + if (result == NULL) { + $result = 0; + return $result; + } - if ((arg1)->getBytesPerPixel() == 1 && numComponents == 4) + unsigned numBytes = (arg1)->getSizeBytes(); + // Return null if no bytes + if (numBytes == 0) { + $result = 0; + return $result; + } + int width, height, bytesPerPixel, numComponents; + bool propertiesOK = (arg1)->getImageProperties(width, height, bytesPerPixel, numComponents); + + unsigned numPixels; + // If getImageProperties fails, its not image data. Assume 1 byte per pixel + // If more data types are supported in the future, could add other + // checks here to return other data types. + if (!propertiesOK) { + bytesPerPixel = 1; + numComponents = 1; + numPixels = numBytes; + } else { + numPixels = width * height; + } + + if (bytesPerPixel == 1) + { + // create a new byte[] object in Java + jbyteArray data = JCALL1(NewByteArray, jenv, numPixels); + if (data == 0) + { + jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); + if (excep) + jenv->ThrowNew(excep, "The system ran out of memory!"); + + $result = 0; + return $result; + } + + // copy pixels from the image buffer + JCALL4(SetByteArrayRegion, jenv, data, 0, numPixels, (jbyte*)result); + $result = data; + } + else if (bytesPerPixel == 2) { - // assuming RGB32 format - // create a new int[] object in Java - jintArray data = JCALL1(NewIntArray, jenv, lSize); + // create a new short[] object in Java + jshortArray data = JCALL1(NewShortArray, jenv, numPixels); if (data == 0) { jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); @@ -547,18 +596,92 @@ } // copy pixels from the image buffer - JCALL4(SetIntArrayRegion, jenv, data, 0, lSize, (jint*)result); + JCALL4(SetShortArrayRegion, jenv, data, 0, numPixels, (jshort*)result); + $result = data; + } + else if (bytesPerPixel == 4) + { + if (numComponents == 1) + { + // create a new float[] object in Java + jfloatArray data = JCALL1(NewFloatArray, jenv, numPixels); + if (data == 0) + { + jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); + if (excep) + jenv->ThrowNew(excep, "The system ran out of memory!"); + $result = 0; + return $result; + } + + // copy pixels from the image buffer + JCALL4(SetFloatArrayRegion, jenv, data, 0, numPixels, (jfloat*)result); + $result = data; + } + else + { + // create a new byte[] object in Java + jbyteArray data = JCALL1(NewByteArray, jenv, numPixels * 4); + if (data == 0) + { + jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); + if (excep) + jenv->ThrowNew(excep, "The system ran out of memory!"); + + $result = 0; + return $result; + } + + // copy pixels from the image buffer + JCALL4(SetByteArrayRegion, jenv, data, 0, numPixels * 4, (jbyte*)result); + $result = data; + } + } + else if (bytesPerPixel == 8) + { + // create a new short[] object in Java + jshortArray data = JCALL1(NewShortArray, jenv, numPixels * 4); + if (data == 0) + { + jclass excep = jenv->FindClass("java/lang/OutOfMemoryError"); + if (excep) + jenv->ThrowNew(excep, "The system ran out of memory!"); + $result = 0; + return $result; + } + + // copy pixels from the image buffer + JCALL4(SetShortArrayRegion, jenv, data, 0, numPixels * 4, (jshort*)result); $result = data; } else { // don't know how to map - // TODO: thow exception? + // TODO: throw exception? $result = 0; } } +%extend BufferDataPointer { + // Trigger immediate release instead of waiting for garbage collection + void dispose() { + $self->release(); + } +} + +// Unlike void* above, this alias to void* is mapped to long so it can be used as a pointer +// address instead of having the data it points to copied +%typemap(jni) DataPtr "jlong" +%typemap(jtype) DataPtr "long" +%typemap(jstype) DataPtr "long" +%typemap(javain) DataPtr "$javainput" +%typemap(javaout) DataPtr { return $jnicall; } +%typemap(out) DataPtr { $result = (jlong)$1; } +%typemap(in) DataPtr { + $1 = (DataPtr)$input; +} + %typemap(jni) imgRGB32 "jintArray" %typemap(jtype) imgRGB32 "int[]" @@ -642,170 +765,97 @@ %} %typemap(javacode) CMMCore %{ - private boolean includeSystemStateCache_ = true; - - public boolean getIncludeSystemStateCache() { - return includeSystemStateCache_; - } - public void setIncludeSystemStateCache(boolean state) { - includeSystemStateCache_ = state; - } - - private JSONObject metadataToMap(Metadata md) { + static JSONObject metadataToMap(Metadata md) { JSONObject tags = new JSONObject(); for (String key:md.GetKeys()) { try { - tags.put(key, md.GetSingleTag(key).GetValue()); + String value = md.GetSingleTag(key).GetValue(); + // Try to convert these to the appropriate type + // since the metadata tags coming from the core + // are all strings + try { + // Try parsing as integer first + tags.put(key, Integer.parseInt(value)); + } catch (NumberFormatException e1) { + try { + // If not integer, try as double/float + tags.put(key, Double.parseDouble(value)); + } catch (NumberFormatException e2) { + // If not a number, keep as string + tags.put(key, value); + } + } } catch (Exception e) {} } return tags; } - private String getROITag() throws java.lang.Exception { - String roi = ""; - int [] x = new int[1]; - int [] y = new int[1]; - int [] xSize = new int[1]; - int [] ySize = new int[1]; - getROI(x, y, xSize, ySize); - roi += x[0] + "-" + y[0] + "-" + xSize[0] + "-" + ySize[0]; - return roi; - } - - private String getPixelType() { - int depth = (int) getBytesPerPixel(); - int numComponents = (int) getNumberOfComponents(); - switch (depth) { - case 1: - return "GRAY8"; - case 2: - return "GRAY16"; - case 4: { - if (numComponents == 1) - return "GRAY32"; - else - return "RGB32"; - } - case 8: - return "RGB64"; - } - return ""; - } - - private String getMultiCameraChannel(JSONObject tags, int cameraChannelIndex) { - try { - String camera = tags.getString("Core-Camera"); - String physCamKey = camera + "-Physical Camera " + (1 + cameraChannelIndex); - if (tags.has(physCamKey)) { - try { - return tags.getString(physCamKey); - } catch (Exception e2) { - return null; - } - } else { - return null; - } - } catch (Exception e) { - return null; - } - - } - - private TaggedImage createTaggedImage(Object pixels, Metadata md, int cameraChannelIndex) throws java.lang.Exception { - TaggedImage image = createTaggedImage(pixels, md); - JSONObject tags = image.tags; - - if (!tags.has("CameraChannelIndex")) { - tags.put("CameraChannelIndex", cameraChannelIndex); - tags.put("ChannelIndex", cameraChannelIndex); - } - if (!tags.has("Camera")) { - String physicalCamera = getMultiCameraChannel(tags, cameraChannelIndex); - if (physicalCamera != null) { - tags.put("Camera", physicalCamera); - tags.put("Channel",physicalCamera); - } - } - return image; - } - - private TaggedImage createTaggedImage(Object pixels, Metadata md) throws java.lang.Exception { - JSONObject tags = metadataToMap(md); - PropertySetting setting; - if (includeSystemStateCache_) { - Configuration config = getSystemStateCache(); - for (int i = 0; i < config.size(); ++i) { - setting = config.getSetting(i); - String key = setting.getDeviceLabel() + "-" + setting.getPropertyName(); - String value = setting.getPropertyValue(); - tags.put(key, value); - } - } - tags.put("BitDepth", getImageBitDepth()); - tags.put("PixelSizeUm", getPixelSizeUm(true)); - tags.put("PixelSizeAffine", getPixelSizeAffineAsString()); - tags.put("ROI", getROITag()); - tags.put("Width", getImageWidth()); - tags.put("Height", getImageHeight()); - tags.put("PixelType", getPixelType()); - tags.put("Frame", 0); - tags.put("FrameIndex", 0); - tags.put("Position", "Default"); - tags.put("PositionIndex", 0); - tags.put("Slice", 0); - tags.put("SliceIndex", 0); - String channel = getCurrentConfigFromCache(getPropertyFromCache("Core","ChannelGroup")); - if ((channel == null) || (channel.length() == 0)) { - channel = "Default"; - } - tags.put("Channel", channel); - tags.put("ChannelIndex", 0); - - - try { - tags.put("Binning", getProperty(getCameraDevice(), "Binning")); - } catch (Exception ex) {} - - return new TaggedImage(pixels, tags); - } + // Snap image functions public TaggedImage getTaggedImage(int cameraChannelIndex) throws java.lang.Exception { Metadata md = new Metadata(); - Object pixels = getImage(cameraChannelIndex); - return createTaggedImage(pixels, md, cameraChannelIndex); + Object pixels = getImageMD(cameraChannelIndex, md); + return new TaggedImage(pixels, metadataToMap(md)); } public TaggedImage getTaggedImage() throws java.lang.Exception { - return getTaggedImage(0); + Metadata md = new Metadata(); + Object pixels = getImageMD(md); + return new TaggedImage(pixels, metadataToMap(md)); } + // sequence acq functions public TaggedImage getLastTaggedImage(int cameraChannelIndex) throws java.lang.Exception { Metadata md = new Metadata(); Object pixels = getLastImageMD(cameraChannelIndex, 0, md); - return createTaggedImage(pixels, md, cameraChannelIndex); + return new TaggedImage(pixels, metadataToMap(md)); } public TaggedImage getLastTaggedImage() throws java.lang.Exception { - return getLastTaggedImage(0); + Metadata md = new Metadata(); + Object pixels = getLastImageMD(md); + return new TaggedImage(pixels, metadataToMap(md)); } public TaggedImage getNBeforeLastTaggedImage(long n) throws java.lang.Exception { Metadata md = new Metadata(); Object pixels = getNBeforeLastImageMD(n, md); - return createTaggedImage(pixels, md); + return new TaggedImage(pixels, metadataToMap(md)); } public TaggedImage popNextTaggedImage(int cameraChannelIndex) throws java.lang.Exception { Metadata md = new Metadata(); Object pixels = popNextImageMD(cameraChannelIndex, 0, md); - return createTaggedImage(pixels, md, cameraChannelIndex); + return new TaggedImage(pixels, metadataToMap(md)); } public TaggedImage popNextTaggedImage() throws java.lang.Exception { - return popNextTaggedImage(0); + Metadata md = new Metadata(); + Object pixels = popNextImageMD(md); + return new TaggedImage(pixels, metadataToMap(md)); } + // BufferDataPointer wrappers + // snap image + public TaggedImagePointer getTaggedImagePointer() throws java.lang.Exception { + return new TaggedImagePointer(getImagePointer()); + } + + // sequence acq + public TaggedImagePointer getLastTaggedImagePointer() throws java.lang.Exception { + return new TaggedImagePointer(getLastDataPointer()); + } + + public TaggedImagePointer popNextTaggedImagePointer() throws java.lang.Exception { + return new TaggedImagePointer(popNextDataPointer()); + } + + public TaggedImagePointer getLastTaggedImagePointerFromDevice(String deviceLabel) throws java.lang.Exception { + return new TaggedImagePointer(getLastDataFromDevicePointer(deviceLabel)); + } + + // convenience functions follow /* @@ -866,9 +916,7 @@ } /** - * Convenience function. Retuns affine transform as a String - * Used in this class and by the acquisition engine - * (rather than duplicating this code there + * Used the acquisition engine, also in AddCameraMetada in core */ public String getPixelSizeAffineAsString() throws java.lang.Exception { String pa = ""; @@ -921,18 +969,19 @@ #include "../MMDevice/ImageMetadata.h" #include "../MMCore/MMEventCallback.h" #include "../MMCore/MMCore.h" +#include "../MMCore/NewDataBufferPointer.h" %} // instantiate STL mappings namespace std { - %typemap(javaimports) vector %{ - import java.lang.Iterable; - import java.util.Iterator; - import java.util.NoSuchElementException; - import java.lang.UnsupportedOperationException; - %} + %typemap(javaimports) vector %{ + import java.lang.Iterable; + import java.util.Iterator; + import java.util.NoSuchElementException; + import java.lang.UnsupportedOperationException; + %} %typemap(javainterfaces) vector %{ Iterable%} @@ -979,11 +1028,11 @@ namespace std { */ %typemap(javaimports) vector %{ - import java.lang.Iterable; - import java.util.Iterator; - import java.util.NoSuchElementException; - import java.lang.UnsupportedOperationException; - %} + import java.lang.Iterable; + import java.util.Iterator; + import java.util.NoSuchElementException; + import java.lang.UnsupportedOperationException; + %} %typemap(javainterfaces) vector %{ Iterable%} @@ -1025,11 +1074,11 @@ namespace std { %} %typemap(javaimports) vector %{ - import java.lang.Iterable; - import java.util.Iterator; - import java.util.NoSuchElementException; - import java.lang.UnsupportedOperationException; - %} + import java.lang.Iterable; + import java.util.Iterator; + import java.util.NoSuchElementException; + import java.lang.UnsupportedOperationException; + %} %typemap(javainterfaces) vector %{ Iterable%} @@ -1071,111 +1120,111 @@ namespace std { %} - %typemap(javaimports) vector %{ - import java.lang.Iterable; - import java.util.Iterator; - import java.util.NoSuchElementException; - import java.lang.UnsupportedOperationException; - %} - - %typemap(javainterfaces) vector %{ Iterable%} - - %typemap(javacode) vector %{ - - public Iterator iterator() { - return new Iterator() { - - private int i_=0; - - public boolean hasNext() { - return (i_ %{ + import java.lang.Iterable; + import java.util.Iterator; + import java.util.NoSuchElementException; + import java.lang.UnsupportedOperationException; + %} + + %typemap(javainterfaces) vector %{ Iterable%} + + %typemap(javacode) vector %{ + + public Iterator iterator() { + return new Iterator() { + + private int i_=0; + + public boolean hasNext() { + return (i_ %{ - import java.lang.Iterable; - import java.util.Iterator; - import java.util.NoSuchElementException; - import java.lang.UnsupportedOperationException; - %} - - %typemap(javainterfaces) vector %{ Iterable%} - - %typemap(javacode) vector %{ - - public Iterator iterator() { - return new Iterator() { - - private int i_=0; - - public boolean hasNext() { - return (i_ %{ - import java.lang.Iterable; - import java.util.Iterator; - import java.util.NoSuchElementException; - import java.lang.UnsupportedOperationException; - %} + %typemap(javaimports) vector %{ + import java.lang.Iterable; + import java.util.Iterator; + import java.util.NoSuchElementException; + import java.lang.UnsupportedOperationException; + %} + + %typemap(javainterfaces) vector %{ Iterable%} + + %typemap(javacode) vector %{ + + public Iterator iterator() { + return new Iterator() { + + private int i_=0; + + public boolean hasNext() { + return (i_ %{ + import java.lang.Iterable; + import java.util.Iterator; + import java.util.NoSuchElementException; + import java.lang.UnsupportedOperationException; + %} %typemap(javainterfaces) vector %{ Iterable%} @@ -1234,9 +1283,16 @@ namespace std { } +// These are needed by the void* typemaps to copy pixels and then +// release them, but they shouldn't be needed by the Java wrap +// because their functionality is handled by the BufferDataPointer class +%ignore CMMCore::getImageProperties(DataPtr, int&, int&, int&, int&); +%ignore CMMCore::releaseReadAccess(DataPtr); + %include "../MMDevice/MMDeviceConstants.h" %include "../MMCore/Configuration.h" %include "../MMCore/MMCore.h" %include "../MMDevice/ImageMetadata.h" %include "../MMCore/MMEventCallback.h" +%include "../MMCore/NewDataBufferPointer.h" diff --git a/MMCoreJ_wrap/src/main/java/mmcorej/LazyJSONObject.java b/MMCoreJ_wrap/src/main/java/mmcorej/LazyJSONObject.java new file mode 100644 index 000000000..26c9f7c50 --- /dev/null +++ b/MMCoreJ_wrap/src/main/java/mmcorej/LazyJSONObject.java @@ -0,0 +1,113 @@ +package mmcorej; + +import java.util.Iterator; +import java.util.Collections; +import mmcorej.BufferDataPointer; +import mmcorej.Metadata; +import mmcorej.org.json.JSONException; +import mmcorej.org.json.JSONObject; +import mmcorej.CMMCore; + +/** + * A JSONObject that lazily initializes its contents from a BufferDataPointer. + */ +class LazyJSONObject extends JSONObject { + private BufferDataPointer dataPointer_; + private boolean initialized_ = false; + + + public LazyJSONObject(BufferDataPointer dataPointer) { + this.dataPointer_ = dataPointer; + } + + /** + * Releases the BufferDataPointer associated with this LazyJSONObject. + + */ + public void releasePointer() { + dataPointer_ = null; + } + + synchronized void initializeIfNeeded() throws JSONException { + if (!initialized_) { + try { + Metadata md = new Metadata(); + dataPointer_.getMetadata(md); + + // This handles some type conversions + JSONObject tags = CMMCore.metadataToMap(md); + Iterator keyIter = tags.keys(); + while (keyIter.hasNext()) { + String key = keyIter.next(); + super.put(key, tags.get(key)); + } + initialized_ = true; + } catch (Exception e) { + throw new JSONException("Failed to initialize metadata"); + } + } + } + + @Override + public Object get(String key) throws JSONException { + initializeIfNeeded(); + return super.get(key); + } + + @Override + public JSONObject put(String key, Object value) throws JSONException { + initializeIfNeeded(); + return super.put(key, value); + } + + @Override + public Object opt(String key) { + try { + initializeIfNeeded(); + } catch (JSONException e) { + return null; // matches parent class behavior for missing keys + } + return super.opt(key); + } + + @Override + public boolean has(String key) { + try { + initializeIfNeeded(); + } catch (JSONException e) { + return false; + } + return super.has(key); + } + + @Override + public Iterator keys() { + try { + initializeIfNeeded(); + } catch (JSONException e) { + // Return empty iterator if initialization fails + return Collections.emptyList().iterator(); + } + return super.keys(); + } + + @Override + public int length() { + try { + initializeIfNeeded(); + } catch (JSONException e) { + return 0; + } + return super.length(); + } + + @Override + public Object remove(String key) { + try { + initializeIfNeeded(); + } catch (JSONException e) { + return null; + } + return super.remove(key); + } +} diff --git a/MMCoreJ_wrap/src/main/java/mmcorej/TaggedImage.java b/MMCoreJ_wrap/src/main/java/mmcorej/TaggedImage.java index 7e85e0da1..7cdc103d5 100644 --- a/MMCoreJ_wrap/src/main/java/mmcorej/TaggedImage.java +++ b/MMCoreJ_wrap/src/main/java/mmcorej/TaggedImage.java @@ -7,11 +7,19 @@ */ public class TaggedImage { - public final Object pix; + public Object pix; public JSONObject tags; public TaggedImage(Object pix, JSONObject tags) { this.pix = pix; this.tags = tags; } + + // This is so that this method can be callled on the + // TaggedImagePointer subclass, so pixels are loaded lazily. + // For regular TaggedImage objects, pixels are already loaded. + public Object getPixels() { + return pix; + } + } diff --git a/MMCoreJ_wrap/src/main/java/mmcorej/TaggedImagePointer.java b/MMCoreJ_wrap/src/main/java/mmcorej/TaggedImagePointer.java new file mode 100644 index 000000000..28c15d155 --- /dev/null +++ b/MMCoreJ_wrap/src/main/java/mmcorej/TaggedImagePointer.java @@ -0,0 +1,97 @@ +package mmcorej; + +import mmcorej.org.json.JSONObject; +import mmcorej.org.json.JSONException; +import java.util.Iterator; +import java.util.Collections; + +/** + * TaggedImagePointer is a wrapper around a pointer to an image in the v2 buffer + * (a BufferDataPointer object). It provides copy-free access to data in the + * C++ layer until the data is actually needed. This class implements lazy loading of + * image data to optimize memory usage and performance. + * + *

This class extends TaggedImage and manages the lifecycle of image data stored + * in native memory. It ensures proper release of resources when the image data is + * no longer needed.

+ * + *

This class implements AutoCloseable, allowing it to be used with try-with-resources + * statements for automatic resource management.

+ */ +public class TaggedImagePointer extends TaggedImage implements AutoCloseable { + + public LazyJSONObject tags; + + private BufferDataPointer dataPointer_; + private boolean released_ = false; + + /** + * Constructs a new TaggedImagePointer. + * + * @param dataPointer BufferDataPointer to the image data + */ + public TaggedImagePointer(BufferDataPointer dataPointer) { + super(null, null); // Initialize parent with null pix + this.dataPointer_ = dataPointer; + this.tags = new LazyJSONObject(dataPointer); + } + + public Object getPixels() { + loadData(); + return pix; + } + + /** + * Retrieves the pixels and metadata associated with this image. + * + *

The first call to this method will copy the data from native memory + * to Java memory and release the native buffer. Subsequent calls will + * return the cached copy.

+ * + * @throws IllegalStateException if te image has already been released + */ + private synchronized void loadData() throws IllegalStateException { + if (!released_) { + if (this.pix == null) { + try { + this.pix = dataPointer_.getData(); + tags.initializeIfNeeded(); + } catch (Exception e) { + throw new IllegalStateException("Failed to get pixel data", e); + } + release(); + } + } + } + + /** + * Releases the native memory associated with this image. + * + *

This method is synchronized to prevent concurrent access to the + * release mechanism. Once released, the native memory cannot be accessed + * again.

+ * + * @throws IllegalStateException if releasing the read access fails + */ + public synchronized void release() { + if (!released_) { + dataPointer_.dispose(); + tags.releasePointer(); + released_ = true; + dataPointer_ = null; + } + } + + /** + * Closes this resource, relinquishing any underlying resources. + * This method is invoked automatically when used in a try-with-resources statement. + * + *

This implementation calls {@link #release()} to free native memory.

+ */ + @Override + public void close() { + release(); + } + +} + diff --git a/MMDevice/DeviceBase.h b/MMDevice/DeviceBase.h index 84cef7177..2106d1639 100644 --- a/MMDevice/DeviceBase.h +++ b/MMDevice/DeviceBase.h @@ -1542,7 +1542,6 @@ class CCameraBase : public CDeviceBase char label[MM::MaxStrLength]; this->GetLabel(label); Metadata md; - md.put(MM::g_Keyword_Metadata_CameraLabel, label); int ret = GetCoreCallback()->InsertImage(this, GetImageBuffer(), GetImageWidth(), GetImageHeight(), GetImageBytesPerPixel(), md.Serialize().c_str()); diff --git a/MMDevice/MMDevice.h b/MMDevice/MMDevice.h index c674ac0b7..c724ded5e 100644 --- a/MMDevice/MMDevice.h +++ b/MMDevice/MMDevice.h @@ -1389,11 +1389,26 @@ namespace MM { virtual int InsertImage(const Device* caller, const unsigned char* buf, unsigned width, unsigned height, unsigned byteDepth, const Metadata* md = 0, const bool doProcess = true) = 0; /// \deprecated Use the other forms instead. virtual int InsertImage(const Device* caller, const unsigned char* buf, unsigned width, unsigned height, unsigned byteDepth, const char* serializedMetadata, const bool doProcess = true) = 0; - virtual void ClearImageBuffer(const Device* caller) = 0; - virtual bool InitializeImageBuffer(unsigned channels, unsigned slices, unsigned int w, unsigned int h, unsigned int pixDepth) = 0; + + MM_DEPRECATED(virtual void ClearImageBuffer(const Device* caller)) = 0; + MM_DEPRECATED(virtual bool InitializeImageBuffer(unsigned channels, unsigned slices, unsigned int w, unsigned int h, unsigned int pixDepth)) = 0; /// \deprecated Use the other forms instead. virtual int InsertMultiChannel(const Device* caller, const unsigned char* buf, unsigned numChannels, unsigned width, unsigned height, unsigned byteDepth, Metadata* md = 0) = 0; + // Method for inserting generic non-image data into the buffer + virtual int InsertData(const Device* caller, const unsigned char* buf, size_t dataSize, Metadata* pMd = 0) = 0; + + // TODO: enable these when we know what to do about backwards compatibility for circular buffer +// int AcquireImageWriteSlot(const MM::Camera* caller, size_t dataSize, size_t metadataSize, +// unsigned char** dataPointer, unsigned char** metadataPointer, +// unsigned width, unsigned height, unsigned byteDepth, unsigned nComponents); + +// int AcquireDataWriteSlot(const MM::Device* caller, size_t dataSize, size_t metadataSize, +// unsigned char** dataPointer, unsigned char** metadataPointer); + +// int FinalizeWriteSlot(unsigned char* dataPointer, size_t actualMetadataBytes); + + // Formerly intended for use by autofocus MM_DEPRECATED(virtual const char* GetImage()) = 0; MM_DEPRECATED(virtual int GetImageDimensions(int& width, int& height, int& depth)) = 0; diff --git a/MMDevice/MMDeviceConstants.h b/MMDevice/MMDeviceConstants.h index efde9f5d7..4f9cdc0a0 100644 --- a/MMDevice/MMDeviceConstants.h +++ b/MMDevice/MMDeviceConstants.h @@ -165,6 +165,8 @@ namespace MM { const char* const g_Keyword_Metadata_Width = "Width"; const char* const g_Keyword_Metadata_Height = "Height"; const char* const g_Keyword_Metadata_CameraLabel = "Camera"; + const char* const g_Keyword_Metadata_DataProducerLabel = "DataProducer"; + const char* const g_Keyword_Metadata_BitDepth = "BitDepth"; const char* const g_Keyword_Metadata_Exposure = "Exposure-ms"; MM_DEPRECATED(const char* const g_Keyword_Meatdata_Exposure) = "Exposure-ms"; // Typo const char* const g_Keyword_Metadata_Score = "Score"; @@ -174,6 +176,16 @@ namespace MM { const char* const g_Keyword_Metadata_ROI_Y = "ROI-Y-start"; const char* const g_Keyword_Metadata_TimeInCore = "TimeReceivedByCore"; + // Image metadata categories + // Metadata category bitmasks + const unsigned int g_Image_Metadata_Bitmask_BitDepth = 1; + const unsigned int g_Image_Metadata_Bitmask_CameraParams = 2; + const unsigned int g_Image_Metadata_Bitmask_CameraTags = 4; + const unsigned int g_Image_Metadata_Bitmask_Timing = 8; + const unsigned int g_Image_Metadata_Bitmask_SystemStateCache = 16; + const unsigned int g_Image_Metadata_Bitmask_LegacyCalibration = 32; + const unsigned int g_Image_Metadata_Bitmask_Legacy = 64; + // configuration file format constants const char* const g_FieldDelimiters = ","; const char* const g_CFGCommand_Device = "Device";