Skip to content

Commit eb0ea4b

Browse files
committed
REFACTOR: Core out-of-core (OOC) architecture rewrite for simplnx
Replace the chunk-based DataStore API with a plugin-driven hook architecture that cleanly separates OOC policy (in the SimplnxOoc plugin) from mechanism (in the core library). The old API required every caller to understand chunk geometry; the new design hides OOC details behind bulk I/O primitives and plugin-registered callbacks. --- AbstractDataStore / IDataStore API --- Remove the entire chunk API from AbstractDataStore and IDataStore: loadChunk, getNumberOfChunks, getChunkLowerBounds, getChunkUpperBounds, getChunkShape, getChunkSize, getChunkTupleShape, getChunkExtents, and convertChunkToDataStore. Replace with two bulk I/O primitives: copyIntoBuffer(startIndex, span<T>) and copyFromBuffer(startIndex, span<const T>), implemented in DataStore (std::copy on raw memory) and EmptyDataStore (throws). This shifts the abstraction from "load a chunk, then index into it" to "copy a contiguous range into a caller- owned buffer," which works identically for in-core and OOC stores. Simplify StoreType to three values (InMemory, OutOfCore, Empty) by removing EmptyOutOfCore. IsOutOfCore() now checks StoreType instead of testing getChunkShape().has_value(). Add getRecoveryMetadata() virtual to IDataStore for crash-recovery attribute persistence. --- Plugin Hook System (DataIOCollection / IDataIOManager) --- Add three plugin-registered callback hooks to DataIOCollection: FormatResolverFnc: Decides storage format for a given array based on type, shape, and size. Called from DataStoreUtilities::CreateDataStore and CreateListStore. Replaces the removed checkStoreDataFormat() and TryForceLargeDataFormatFromPrefs — format decisions now live entirely in the plugin, with core only calling resolveFormat() when no format is already set. BackfillHandlerFnc: Post-import callback that lets the plugin finalize placeholder stores after all HDF5 objects are read. Called from ImportH5ObjectPathsAction after importing all paths. Replaces the removed backfillReadOnlyOocStores core implementation. WriteArrayOverrideFnc: Intercepts HDF5 writes during recovery file creation, allowing the plugin to write lightweight placeholder datasets instead of full array data. Activated via RAII WriteArrayOverrideGuard, wired into DataStructureWriter. Add factory registration on IDataIOManager for ListStoreRefCreateFnc, StringStoreCreateFnc, and FinalizeStoresFnc, with delegating creation methods on DataIOCollection. Guard against reserved format name "Simplnx-Default-In-Memory" during IO manager registration. --- EmptyStringStore Placeholder --- Add EmptyStringStore, a placeholder class for OOC string array import that stores only tuple shape metadata. All data access methods throw std::runtime_error. isPlaceholder() returns true (vs false for StringStore). StringArrayIO creates EmptyStringStore in OOC mode instead of allocating numValues empty strings. --- HDF5 I/O --- DataStoreIO::ReadDataStore gains two interception paths before the normal in-core load: (1) recovery file detection via OocBackingFilePath HDF5 attributes, creating a read-only reference store pointing at the backing file; (2) OOC format resolution via resolveFormat(), creating a read-only reference store directly from the source .dream3d file with no temp copy. DataArrayIO::writeData always calls WriteDataStore directly — OOC stores materialize their data through the plugin's writeHdf5() method; recovery writes use WriteArrayOverrideFnc. NeighborListIO gains OOC interception: computes total neighbor count, calls resolveFormat(), and creates a read-only ref list store when an OOC format is available. Legacy NeighborList reading passes a preflight flag through the entire call chain (readLegacyNeighborList -> createLegacyNeighborList -> ReadHdf5Data) so legacy .dream3d imports create EmptyListStore placeholders instead of eagerly loading per- element via setList(). DataStructureWriter checks WriteArrayOverrideFnc before normal writes, giving the registered plugin callback first chance to handle each data object. Add explicit template instantiations for DatasetIO::createEmptyDataset and DatasetIO::writeSpanHyperslab for all numeric types plus bool. These are needed by the SimplnxOoc plugin's AbstractOocStore::writeHdf5(), which cannot use writeSpan() because the full array is not in memory. Instead it creates an empty dataset, then fills it region-by-region via hyperslab writes as it streams data from the backing file. --- Preferences --- Add unified oocMemoryBudgetBytes preference (default 8 GB) that the ChunkCache, visualization, and stride cache all use. Add k_InMemoryFormat sentinel constant for explicit in-core format choice. Add migration logic to erase legacy empty-string and "In-Memory" preference values. checkUseOoc() now tests against k_InMemoryFormat. setLargeDataFormat("") removes the key so plugin defaults take effect. --- Algorithm Infrastructure --- AlgorithmDispatch: Add ForceInCoreAlgorithm/ForceOocAlgorithm global flags with RAII guards. Add DispatchAlgorithm template that selects Direct (in-core) vs Scanline (OOC) algorithm variant based on store types and force flags. Add SIMPLNX_TEST_ALGORITHM_PATH CMake option (0=both, 1=OOC-only, 2=InCore-only) for dual-dispatch test control. IParallelAlgorithm: Remove blanket TBB disabling for OOC data — OOC stores are now thread-safe via ChunkCache + HDF5 global mutex. CheckStoresInMemory/CheckArraysInMemory use StoreType instead of getDataFormat(). VtkUtilities: Rewrite binary write path to read into 4096-element buffers via copyIntoBuffer, byte-swap in the buffer, and fwrite — replacing direct DataStore data() pointer access. --- Filter Algorithm Updates --- FillBadData: Rewrite phaseOneCCL and phaseThreeRelabeling to use Z-slab buffered I/O via copyIntoBuffer/copyFromBuffer instead of the removed chunk API (loadChunk, getChunkLowerBounds, etc.). operator()() scans feature counts in 64K-element chunks via copyIntoBuffer. QuickSurfaceMesh: Remove getChunkShape() call in generateTripleLines() that set ParallelData3DAlgorithm chunk size, as the chunk API no longer exists on AbstractDataStore. --- File Import --- ImportH5ObjectPathsAction: Add deferred-load pattern. When a backfill handler is registered, pass preflight=true to create placeholder stores during import, then call runBackfillHandler() after all paths are imported to let the plugin finalize. Dream3dIO: Add WriteRecoveryFile() that wraps WriteFile with WriteArrayOverrideGuard. --- Utility Changes --- DataStoreUtilities: Remove TryForceLargeDataFormatFromPrefs entirely. CreateDataStore and CreateListStore call resolveFormat() on the IO collection. ArrayCreationUtilities: check k_InMemoryFormat sentinel before skipping memory checks. ITKArrayHelper/ITKTestBase: OOC checks use getStoreType() instead of getDataFormat().empty(). IsArrayInMemory simplified from a 40-line DataType switch to a single StoreType check. ArraySelectionParameter: Remove EmptyOutOfCore handling; simplify to just StoreType::Empty. --- Tests --- Add EmptyStringStore tests (6 cases: metadata, zero tuples, throwing access, deep copy placeholder preservation, resize, isPlaceholder). Add DataIOCollection hooks tests (format resolver, backfill handler). Add IOFormat tests (7 cases: InMemory sentinel, empty format, resolveFormat with/without plugin). Add IParallelAlgorithm OOC tests (8 cases with MockOocDataStore: TBB enablement for in-memory, OOC, and mixed arrays/stores). Remove the "Target DataStructure Size" test from IOFormat.cpp — it was a tautology that re-implemented the same arithmetic as updateMemoryDefaults() without testing any edge case or behavior. Fix RodriguesConvertorTest exemplar data: add missing expected values for the 4th tuple (indices 12-15). The old CompareDataArrays broke on the first floating-point mismatch regardless of magnitude, masking this incomplete exemplar. The new chunked comparison correctly continues past epsilon-close differences, exposing the missing data. Signed-off-by: Joey Kleingers <joey.kleingers@bluequartz.net>
1 parent f885a0e commit eb0ea4b

44 files changed

Lines changed: 2269 additions & 618 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ option(SIMPLNX_DOWNLOAD_TEST_FILES "Download the test files" ON)
5353
# ------------------------------------------------------------------------------
5454
option(SIMPLNX_WRITE_TEST_OUTPUT "Write unit test output files" OFF)
5555

56+
# ------------------------------------------------------------------------------
57+
# Controls which algorithm paths are exercised by dual-dispatch unit tests.
58+
# 0 (Both) - tests run with forceOoc=false AND forceOoc=true (default)
59+
# 1 (OocOnly) - tests run with forceOoc=true only (use for OOC builds)
60+
# 2 (InCoreOnly) - tests run with forceOoc=false only (quick validation)
61+
# ------------------------------------------------------------------------------
62+
set(SIMPLNX_TEST_ALGORITHM_PATH "0" CACHE STRING "Algorithm paths to test: 0=Both, 1=OocOnly, 2=InCoreOnly")
63+
5664
# ------------------------------------------------------------------------------
5765
# Is the SimplnxCore Plugin enabled [DEFAULT=ON]
5866
# ------------------------------------------------------------------------------
@@ -255,6 +263,7 @@ if(SIMPLNX_ENABLE_MULTICORE)
255263
target_link_libraries(simplnx PUBLIC TBB::tbb)
256264
endif()
257265

266+
258267
target_link_libraries(simplnx
259268
PUBLIC
260269
fmt::fmt
@@ -458,6 +467,7 @@ set(SIMPLNX_HDRS
458467
${SIMPLNX_SOURCE_DIR}/DataStructure/DynamicListArray.hpp
459468
${SIMPLNX_SOURCE_DIR}/DataStructure/EmptyDataStore.hpp
460469
${SIMPLNX_SOURCE_DIR}/DataStructure/EmptyListStore.hpp
470+
${SIMPLNX_SOURCE_DIR}/DataStructure/EmptyStringStore.hpp
461471
${SIMPLNX_SOURCE_DIR}/DataStructure/IArray.hpp
462472
${SIMPLNX_SOURCE_DIR}/DataStructure/IDataArray.hpp
463473
${SIMPLNX_SOURCE_DIR}/DataStructure/IDataStore.hpp
@@ -539,6 +549,7 @@ set(SIMPLNX_HDRS
539549
${SIMPLNX_SOURCE_DIR}/Utilities/DataGroupUtilities.hpp
540550
${SIMPLNX_SOURCE_DIR}/Utilities/DataObjectUtilities.hpp
541551
${SIMPLNX_SOURCE_DIR}/Utilities/DataStoreUtilities.hpp
552+
${SIMPLNX_SOURCE_DIR}/Utilities/AlgorithmDispatch.hpp
542553
${SIMPLNX_SOURCE_DIR}/Utilities/FilePathGenerator.hpp
543554
${SIMPLNX_SOURCE_DIR}/Utilities/ColorTableUtilities.hpp
544555
${SIMPLNX_SOURCE_DIR}/Utilities/FileUtilities.hpp

cmake/Plugin.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ function(create_simplnx_plugin_unit_test)
383383
target_compile_definitions(${UNIT_TEST_TARGET}
384384
PRIVATE
385385
SIMPLNX_BUILD_DIR="$<TARGET_FILE_DIR:simplnx_test>"
386+
SIMPLNX_TEST_ALGORITHM_PATH=${SIMPLNX_TEST_ALGORITHM_PATH}
386387
)
387388

388389
target_compile_options(${UNIT_TEST_TARGET}

src/Plugins/ITKImageProcessing/src/ITKImageProcessing/Common/ITKArrayHelper.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,7 @@ Result<OutputActions> DataCheck(const DataStructure& dataStructure, const DataPa
855855
const auto& inputArray = dataStructure.getDataRefAs<IDataArray>(inputArrayPath);
856856
const auto& inputDataStore = inputArray.getIDataStoreRef();
857857

858-
if(!inputArray.getDataFormat().empty())
858+
if(inputArray.getStoreType() == IDataStore::StoreType::OutOfCore)
859859
{
860860
return MakeErrorResult<OutputActions>(Constants::k_OutOfCoreDataNotSupported,
861861
fmt::format("Input Array '{}' utilizes out-of-core data. This is not supported within ITK filters.", inputArrayPath.toString()));
@@ -877,7 +877,7 @@ Result<detail::ITKFilterFunctorResult_t<FilterCreationFunctorT>> Execute(DataStr
877877

878878
using ResultT = detail::ITKFilterFunctorResult_t<FilterCreationFunctorT>;
879879

880-
if(!inputArray.getDataFormat().empty())
880+
if(inputArray.getStoreType() == IDataStore::StoreType::OutOfCore)
881881
{
882882
return MakeErrorResult(Constants::k_OutOfCoreDataNotSupported, fmt::format("Input Array '{}' utilizes out-of-core data. This is not supported within ITK filters.", inputArrayPath.toString()));
883883
}

src/Plugins/ITKImageProcessing/test/ITKTestBase.cpp

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ std::string ComputeMD5HashTyped(const IDataArray& outputDataArray)
3030
usize arraySize = dataArray.getSize();
3131

3232
MD5 md5;
33-
if(outputDataArray.getDataFormat().empty())
33+
if(outputDataArray.getIDataStoreRef().getStoreType() != IDataStore::StoreType::OutOfCore)
3434
{
3535
const T* dataPtr = dataArray.template getIDataStoreRefAs<DataStore<T>>().data();
3636
md5.update(reinterpret_cast<const uint8*>(dataPtr), arraySize * sizeof(T));
@@ -135,47 +135,7 @@ namespace ITKTestBase
135135
bool IsArrayInMemory(DataStructure& dataStructure, const DataPath& outputDataPath)
136136
{
137137
const auto& outputDataArray = dataStructure.getDataRefAs<IDataArray>(outputDataPath);
138-
DataType outputDataType = outputDataArray.getDataType();
139-
140-
switch(outputDataType)
141-
{
142-
case DataType::float32: {
143-
return dynamic_cast<const DataArray<float32>&>(outputDataArray).getDataFormat().empty();
144-
}
145-
case DataType::float64: {
146-
return dynamic_cast<const DataArray<float64>&>(outputDataArray).getDataFormat().empty();
147-
}
148-
case DataType::int8: {
149-
return dynamic_cast<const DataArray<int8>&>(outputDataArray).getDataFormat().empty();
150-
}
151-
case DataType::uint8: {
152-
return dynamic_cast<const DataArray<uint8>&>(outputDataArray).getDataFormat().empty();
153-
}
154-
case DataType::int16: {
155-
return dynamic_cast<const DataArray<int16>&>(outputDataArray).getDataFormat().empty();
156-
}
157-
case DataType::uint16: {
158-
return dynamic_cast<const DataArray<uint16>&>(outputDataArray).getDataFormat().empty();
159-
}
160-
case DataType::int32: {
161-
return dynamic_cast<const DataArray<int32>&>(outputDataArray).getDataFormat().empty();
162-
}
163-
case DataType::uint32: {
164-
return dynamic_cast<const DataArray<uint32>&>(outputDataArray).getDataFormat().empty();
165-
}
166-
case DataType::int64: {
167-
return dynamic_cast<const DataArray<int64>&>(outputDataArray).getDataFormat().empty();
168-
}
169-
case DataType::uint64: {
170-
return dynamic_cast<const DataArray<uint64>&>(outputDataArray).getDataFormat().empty();
171-
}
172-
case DataType::boolean: {
173-
[[fallthrough]];
174-
}
175-
default: {
176-
return {};
177-
}
178-
}
138+
return outputDataArray.getIDataStoreRef().getStoreType() != IDataStore::StoreType::OutOfCore;
179139
}
180140
//------------------------------------------------------------------------------
181141
std::string ComputeMd5Hash(DataStructure& dataStructure, const DataPath& outputDataPath)

src/Plugins/OrientationAnalysis/test/RodriguesConvertorTest.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ TEST_CASE("OrientationAnalysis::RodriguesConvertorFilter", "[OrientationAnalysis
4444
(*exemplarData)[9] = 0.573462F;
4545
(*exemplarData)[10] = 0.655386F;
4646
(*exemplarData)[11] = 12.2066F;
47+
(*exemplarData)[12] = 0.517892F;
48+
(*exemplarData)[13] = 0.575435F;
49+
(*exemplarData)[14] = 0.632979F;
50+
(*exemplarData)[15] = 17.37815F;
4751
{
4852
// Instantiate the filter, a DataStructure object and an Arguments Object
4953
const RodriguesConvertorFilter filter;

src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ComputeArrayStatistics.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "ComputeArrayStatistics.hpp"
22

33
#include "simplnx/DataStructure/AttributeMatrix.hpp"
4+
#include "simplnx/DataStructure/IDataStore.hpp"
45
#include "simplnx/Utilities/DataArrayUtilities.hpp"
56
#include "simplnx/Utilities/FilterUtilities.hpp"
67
#include "simplnx/Utilities/HistogramUtilities.hpp"
@@ -31,7 +32,7 @@ bool CheckArraysInMemory(const nx::core::IParallelAlgorithm::AlgorithmArrays& ar
3132
continue;
3233
}
3334

34-
if(!arrayPtr->getIDataStoreRef().getDataFormat().empty())
35+
if(arrayPtr->getIDataStoreRef().getStoreType() == nx::core::IDataStore::StoreType::OutOfCore)
3536
{
3637
return false;
3738
}

0 commit comments

Comments
 (0)