Skip to content

ENH: Out-of-core architecture rewrite and filter optimizations - #1568

Open
joeykleingers wants to merge 2 commits into
BlueQuartzSoftware:developfrom
joeykleingers:worktree-ooc-architecture-rewrite
Open

ENH: Out-of-core architecture rewrite and filter optimizations#1568
joeykleingers wants to merge 2 commits into
BlueQuartzSoftware:developfrom
joeykleingers:worktree-ooc-architecture-rewrite

Conversation

@joeykleingers

@joeykleingers joeykleingers commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Summary

This branch does two things at once:

  1. Reworks the core out-of-core (OOC) data-storage architecture in simplnx so that disk-backed storage is supplied by a runtime-registered IO manager through clean interfaces, with the core library holding no reference to any concrete OOC format or symbol. Core gains a bulk-I/O data-store API (copyIntoBuffer / copyFromBuffer), an injectable store-format resolver, IO-manager lifecycle hooks, a tri-state storage-mode preference, a memory-budget manager, and memory-safety guards. The OOC implementation itself (chunked HDF5 stores, the chunk cache, deflate decompression) lives in a separate private SimplnxOoc source set and is not part of this diff — it plugs into core at runtime through these interfaces.

  2. Adds chunk-aware (OOC-optimized) algorithm variants for ~50 filters across the SimplnxCore and OrientationAnalysis plugins, plus shared core utilities that benefit many more. Random-access algorithms that thrash the chunk cache on disk-backed arrays now either stream data with bulk I/O or dispatch to a sequential "scanline / CCL" variant, eliminating per-element virtual dispatch and HDF5 chunk churn. In-core behavior and performance are preserved via a runtime storage-type check, so optimized filters keep two code paths: one for in-memory data, one for disk-backed data.

Scope of this diff

develop…HEAD: 378 files, +42,853 / −13,112.

Area Files + / −
src/simplnx (core library) 74 +6,874 / −1,252
Plugins/SimplnxCore 179 +24,817 / −8,795
Plugins/OrientationAnalysis 103 +8,379 / −2,853
Root / build / top-level tests 22 +2,783 / −212

How to review

The diff is large but highly patterned, and the two layers are independent:

  • Core architecture lives in src/simplnx/DataStructure/IO/Generic/*, Core/Preferences.*, Utilities/{MemoryBudgetManager,AlgorithmDispatch,SegmentFeatures,UnionFind,SliceBufferedTransfer}.*, and Filter/IFilter.cpp (Part 1).
  • Filter work is summarized in the inventory tables in Part 2. Every optimization is one of four patterns — reviewing one example of each pattern transfers to the rest. Each optimized algorithm's original file was renamed to the in-core variant and the OOC variant added beside it, so the diffs read as edits against the original code rather than wholesale new files.

Part 1 — Core out-of-core architecture (src/simplnx)

1.1 Bulk-I/O data-store API

  • copyIntoBuffer / copyFromBuffer on AbstractDataStore<T> (a flat range form and a tuple-addressed form) are the single mechanism for bulk reads/writes, implemented by DataStore<T> and EmptyDataStore<T>; the OOC implementation supplies its own override.
  • The storage-kind signal is IDataStore::StoreType { InMemory, OutOfCore, Empty }, queried via getStoreType(). The chunk-traversal API (loadChunk, getNumberOfChunks, getChunkLowerBounds, getChunkUpperBounds, getChunkShape) and the chunk-shape-based OOC detection are removed — bulk I/O plus getStoreType() replace them entirely.
  • Empty/placeholder string storage: EmptyStringStore, AbstractStringStore/StringStore placeholder support, and StringArray::isPlaceholder().

1.2 Runtime store-format resolution and IO-manager hooks

OOC capability is supplied entirely at runtime; core never names the concrete format.

  • IDataStoreFormatResolver: a const, thread-safe policy interface deciding which registered format a soon-to-be-created array uses ("" = in-memory). InMemoryFormatResolver is the default policy.
  • ArrayCreationUtilities::ResolveStorageFormat is the single decision point for every array-creation call site, with a fixed precedence: the unstructured/poly-geometry gate (ParentGeometrySupportsOoc) forces in-core, then an explicit per-filter format override, then the DataStructure's resolver. DataStructure carries a per-instance resolver plus a lazily-seeded process-wide default (neither serialized).
  • IDataIOManager lifecycle hooks (default no-ops): finalizesImport, onImportFinalize, onRecoveryWrite, onFinalizeStores, setBaseDirectory, shutdownManager. DataIOCollection fans these out to every registered manager, so an OOC manager participates in import finalization, recovery writes, store read-only transition, and shutdown without core knowing the specifics.
  • CoreDataIOManager: the always-present default manager that registers the in-memory data-store/list-store factories; an OOC manager registers on top at runtime.
  • Store creation goes through resolver-aware CreateDataStore / CreateListStore single entry points; CreateNeighborListAction / CreateNeighbors thread an explicit dataFormat override through to the store.

1.3 Storage-mode preference (+ legacy migration)

  • DataStorageMode { Adaptive, ForceInCore, ForceOutOfCore } (persisted as data_storage_mode) is the single source of truth for storage placement, replacing the large_data_format / force_ooc_data preferences. The enum is deliberately OOC-vocabulary-free — core states user intent; the registered manager maps it to a concrete format. dataStorageMode() migrates older preference files from the retained legacy keys; useOocData() is a convenience view (true unless ForceInCore).
  • Preferences seeds the OOC base directory and default format on startup via the IO-collection hooks, and gains a removeValue helper.

1.4 Memory budget + memory safety

  • MemoryBudgetManager: tracks the budget governing in-core vs OOC placement. defaultBudgetBytes() (50% of RAM, ≥1 GiB, clamped to the cap), maxBudgetBytes() (max(min(total−6 GiB, 0.95·total), 1 GiB)), and setBudgetBytes() which clamps to the cap and reports whether it clamped. Total-RAM detection is centralized in Memory::GetTotalMemory(). The default is clamped to the cap so it never exceeds it on <12 GiB machines (e.g. CI runners).
  • nxrunner --memory-budget <GB>: CLI flag wired through to the budget manager so the override takes effect in headless runs, with parsing hardened against NaN/inf/trailing garbage.
  • Memory-safety guards (additive to the existing -264 total-RAM hard block):
    • -271 — non-blocking preflight warning when an in-core array would exceed currently-available RAM. OOC arrays are excluded (EmptyDataStore::memoryUsage() reports 0 for OOC placeholders; the format is resolved in preflight via the shared ResolveStorageFormat helper).
    • -272 — a std::bad_alloc safety net at the single IFilter::execute → executeImpl boundary, turning an out-of-memory condition into a clean pipeline error instead of a crash.

1.5 HDF5 streaming write + compression

  • DatasetIO provides the streaming OOC write path — createEmptyDataset + writeSpanHyperslab for arrays too large to be resident — alongside the single-shot writeSpan; reads use readChunk / readChunkIntoSpan.
  • The streaming path honors requested compression: createEmptyDataset builds the chunked-deflate creation property list (BuildChunkedDeflateDcpl), so a large OOC array taking the two-step streaming write is compressed identically to the single-shot path. Contiguous storage is still used for compression level 0 and for arrays below the small-array threshold.

1.6 .dream3d loader API

  • Dream3dIO exposes a LoadDataStructure family: LoadDataStructure, LoadDataStructureMetadata (metadata-only for preflight), LoadDataStructureArrays (array subset), plus resolver-aware overloads that stamp a per-DataStructure resolver before import finalization (so a read-only visualization load can direct arrays to disk for fast first-show). Import is eager or deferred based on anyManagerFinalizesImport(); writes run under a recovery-write guard supplied by the registered manager.
  • ImportH5ObjectPathsAction uses this API: metadata-only on preflight, full load on execute, with a selective shortest-path-first merge of only the requested paths.

1.7 Core algorithm infrastructure

  • AlgorithmDispatch.hppDispatchAlgorithm<InCoreAlgo, OocAlgo>(arrays, args…), a free function template selecting between an in-core and an OOC algorithm class at runtime. Priority: ForceInCoreAlgorithm() > any array OOC > ForceOocAlgorithm() > in-core default. RAII test guards ForceOocAlgorithmGuard(bool) and ForceInCoreAlgorithmGuard let a single build exercise both paths.
  • SegmentFeatures OOC pathexecuteCCL(): Z-slice connected-component labeling with a 2-slice rolling buffer and UnionFind equivalence tracking (Face and FaceEdgeVertex connectivity, optional periodic BCs), replacing random-access BFS/DFS flood-fill on disk-backed data.
  • UnionFind — vector-based disjoint set with union-by-rank and path-halving.
  • SliceBufferedTransfer — type-dispatched Z-slice buffered tuple copy for morphological / neighbor-replacement transfer phases.
  • Extent — region/range math helper (with unit tests).
  • AlignSections OOC path — bulk slice read/write transfer for the align-sections family.

1.8 Core utility bulk-I/O conversions

These live in core utilities and benefit every caller; each is guarded by a runtime storage-type check that preserves the original in-core code path:

  • DataArrayUtilitiesImportFromBinaryFile, AppendData, CopyData, and the mirror swap_ranges ops route through chunked bulk I/O when OOC. (Powers ReadRawBinary and AppendImageGeometry's mirror.)
  • DataGroupUtilities::RemoveInactiveObjects — chunked featureIds renumbering via copyIntoBuffer/copyFromBuffer.
  • ClusteringUtilities::RandomizeFeatureIds — chunked bulk I/O (both overloads; benefits segmentation filters, SharedFeatureFace, MergeTwins).
  • GeometryHelpersFindElementsContainingVert / FindElementNeighbors use 65K-element chunked passes with a current-chunk cache-hit check before falling back to per-element reads.
  • ImageRotationUtilities — Z-slab source cache for nearest-neighbor and a ±2-slice trilinear margin, sliding-window slab updates (memmove + delta reads), and intra-slice parallelism. This is how ApplyTransformationToGeometry and RotateSampleRefFrame get their OOC speedups (no plugin algorithm file changes for ApplyTransformation).
  • TriangleUtilities — bulk-load triangles/labels for winding repair.
  • H5DataStore — streaming row-batch FillOocDataStore replacing full-dataset allocation on import.
  • RectGridGeom / ImageGeom findElementSizes — route through the resolver-aware CreateDataStore (voxel-sizes array can go OOC); the RectGrid inner loop refactored to per-axis precompute + Z-slice copyFromBuffer.

Part 2 — Filter optimizations

Optimization patterns

Every filter optimization is one of these four shapes:

  • (a) Dispatch splitDispatchAlgorithm<…Direct, …Scanline>: an in-core Direct class (unchanged or parallel) and an OOC Scanline class that streams Z-slices / chunks. The original Foo.cpp becomes a thin dispatcher.
  • (b) CCL splitDispatchAlgorithm<…BFS, …CCL> (or an in-file branch): random-access flood-fill in-core, sequential Z-slice connected-component labeling for OOC.
  • (c) Single-implementation bulk I/O — one algorithm that reads/writes in chunks and caches feature/ensemble-level arrays in local std::vectors, with a runtime storage-type check so in-core stays optimal.
  • (d) Slice-buffered / safety — rolling Z-slice buffers via SliceBufferedTransfer, or an OOC-correctness guard (e.g. disabling threading for OOC stores) / progress-and-cancel additions.

Algorithm structure: in-core + out-of-core variants

For dispatch-split filters the original algorithm file is renamed to the in-core variant and the OOC variant is added beside it; the original filename remains as a thin dispatcher:

Filter In-core variant OOC variant
FillBadData FillBadDataBFS FillBadDataCCL
IdentifySample IdentifySampleBFS IdentifySampleCCL
ComputeBoundaryCells …Direct …Scanline
ComputeFeatureNeighbors …Direct …Scanline
ComputeSurfaceFeatures …Direct …Scanline
ComputeSurfaceAreaToVolume …Direct …Scanline
ComputeFeatureSizes …Direct …Scanline
MultiThresholdObjects …Direct …Scanline
DBSCAN …Direct …Scanline
ComputeKMedoids …Direct …Scanline
QuickSurfaceMesh …Direct …Scanline
SurfaceNets …Direct …Scanline
BadDataNeighborOrientationCheck (OA) …Worklist …Scanline
ComputeGBCDPoleFigure (OA) …Direct …Scanline

New shared header IdentifySampleCommon.hpp (SimplnxCore) provides the VectorUnionFind and per-slice functor shared by the BFS/CCL variants; TupleTransfer.hpp gains quickSurfaceTransferBatch / surfaceNetsTransferBatch bulk APIs used by the mesh Scanline variants.

SimplnxCore inventory

Filter Pattern Technique
FillBadData (b) DispatchAlgorithm<FillBadDataBFS, FillBadDataCCL>; CCL streams Z-slices with core UnionFind
IdentifySample (b) DispatchAlgorithm<IdentifySampleBFS, IdentifySampleCCL>; scanline labeling via VectorUnionFind
ScalarSegmentFeatures (b) In-file OOC branch to SegmentFeatures::executeCCL() (Z-slice CCL)
ComputeBoundaryCells (a) Scanline Z-slice rolling-window neighbor reads
ComputeSurfaceFeatures (a) Scanline Z-slice rolling window
ComputeFeatureNeighbors (a) Scanline Z-slice rolling window
ComputeSurfaceAreaToVolume (a) Scanline Z-slice rolling window
ComputeFeatureSizes (a) Direct = tbb parallel Kahan accumulation; Scanline = chunked copyIntoBuffer (256K-tuple)
MultiThresholdObjects (a) Scanline eliminates the O(n) temp result vector
DBSCAN (a) Chunked grid build + on-demand per-cell coordinate reads in canMerge
ComputeKMedoids (a) Chunked findClusters; bounded per-cluster peak memory
QuickSurfaceMesh (a) Scanline drops the O(volume) nodeIds for rolling 2-plane node buffers; quickSurfaceTransferBatch
SurfaceNets (a) Scanline reimplements with a hash-map surface store; surfaceNetsTransferBatch
ComputeFeatureCentroids (c) Plain std::vector accumulators (no DataStore); 64K-tuple chunked featureIds
ComputeFeatureClustering (c) Feature-level array caching; RDF in local vectors
ComputeEuclideanDistMap (c) Bulk-read into local vectors, flood-fill in RAM, bulk-write
ComputeFeaturePhases (c) 65K-tuple chunked featureIds + cellPhases; feature-level vectors replace per-cell map
RequireMinimumSizeFeatures (c) Chunked feature removal + 3-slice rolling slab for assignBadVoxels voting; sparse changed-voxel tracking
CropImageGeometry (c) K=32 Z-slice batched slab I/O (O(n^⅔) working set)
ExtractInternalSurfacesFromTriangleGeometry (c) triMask bitset + sparse triPrefixSum popcount (~6.4× less memory); 65K-element streamed passes
ComputeTriangleAreas (c) Filter-level chunked triangle connectivity + span-bounded vertex loads; parallel compute on local buffers
RegularGridSampleSurfaceMesh (c/d) ZSliceWorker parallel Z-slice rasterize → mutex-guarded bulk copyFromBuffer
ErodeDilateBadData (d) SliceBufferedTransfer per-Z commit + Z-slice neighbor reads
ErodeDilateCoordinationNumber (d) SliceBufferedTransfer per-Z commit
ErodeDilateMask (c/d) Slice-based bulk mask erosion/dilation
ReplaceElementAttributesWithNeighborValues (d) SliceBufferedTransfer per-Z best-neighbor commit
AlignSectionsFeatureCentroid (d) In-file OOC branch findShiftsOoc() with per-Z-slice mask reads
WriteAvizoRectilinearCoordinate / WriteAvizoUniformCoordinate (c) Bulk copyIntoBuffer for coordinate/data output
ComputeArrayStatistics (d) OOC-safety: parallelization disabled when stores are OOC
ReadHDF5Dataset / ReadStlFile (d) Cancel checks + throttled progress

OrientationAnalysis inventory

Filter Pattern Technique
ComputeIPFColors (a) DispatchAlgorithm<…Direct, …Scanline>; Direct keeps parallel in-core, Scanline 65K-tuple chunks + cached crystal structures; color key forwarded to the OOC path
ComputeGBCDPoleFigure (a) Dispatched at the filter level; Scanline caches only the phase-of-interest GBCD slice
BadDataNeighborOrientationCheck (a) DispatchAlgorithm<…Worklist, …Scanline>; bool-mask reads routed through bulk I/O
EBSDSegmentFeatures (b) SegmentFeatures::executeCCL() slice-by-slice, replacing DFS flood-fill
CAxisSegmentFeatures (b) Same CCL architecture as EBSDSegmentFeatures
AlignSectionsMisorientation (c) findShiftsOoc(), 2-slice buffered quats/phases/mask + cached crystal structures
AlignSectionsMutualInformation (c) Per-slice bulk reads of phases/quats/mask + cached crystal structures
NeighborOrientationCorrelation (c/d) 3-slice rolling window via SliceBufferedTransfer; cached crystal structures
ComputeAvgOrientations (c) Chunked featureIds/phases/quats; cached crystal structures + avgQuats; bulk write
ComputeFeatureReferenceMisorientations (c) Chunked cell-level arrays; cached crystal structures, avgQuats, center quats
ComputeKernelAvgMisorientations (c) Per-Z-plane [plane−kZ, plane+kZ] slab reads; cached crystal structures
ComputeAvgCAxes (c) 4096-tuple chunked reads; feature-level avgCAxes + crystal structures cached
ComputeCAxisLocations (c) 64K-tuple chunked read/process/write; cached crystal structures
ComputeFeatureReferenceCAxisMisorientations (c) Z-slice buffered cell-level I/O; cached crystalStructures + avgCAxes
ComputeFeatureNeighborCAxisMisalignments (c) Bulk-read feature-level arrays; buffered output
ComputeTwinBoundaries (c) Bulk-read all face/feature/ensemble arrays into local vectors
MergeTwins (c) Chunked voxel parent-ID fill + assignment; feature-level parentIds cached
ConvertOrientations (c) Macro-generated convertors process each range in 4096-tuple chunks (bulk read→convert→bulk write)
RotateEulerRefFrame (c) 64K-tuple in-place read-modify-write chunks
ComputeGBCD (c) Feature-level caching; chunked 50K-triangle reads; GBCD accumulated locally then copyFromBuffer
ComputeGBCDMetricBased (c) Per-chunk sequential area accumulation (no O(n) triIncluded); feature-level caching; raw-pointer parallel selector
ComputeGBPDMetricBased (c) Caches feature eulers/phases + ensemble crystal structures; chunked triangle reads (distinct filter from GBCD MetricBased)
WriteGBCDGMTFile (c) Caches phase-of-interest GBCD slice; crystal structures cached
WriteGBCDTriangleData (c) 8K-triangle chunked reads; feature-level Euler cache; output buffered via fmt::memory_buffer
ReadAngData / ReadCtfData (c) Bulk copyFromBuffer for all cell arrays; chunked Euler interleave; in-place phase validation
ReadH5Ebsd (c) copyFromBuffer in CopyData template, phase copy, Euler interleave
ReadH5EspritData (c) Bulk copyFromBuffer from raw HDF5 reader buffers
WritePoleFigure (c) Per-phase chunked input reads with bounded buffers; bulk copyFromBuffer for intensity/image outputs

Cancel + progress

In-core and OOC variants gained m_ShouldCancel checks at the top of major loops and ThrottledMessenger-based progress reporting with per-phase messages and percent-complete.


Part 3 — Performance results

All benchmarks use arm64 Release builds. OOC measurements force the disk-backed path through DataStorageMode::ForceOutOfCore or ForceOocAlgorithmGuard.

CRITICAL filter optimizations (existing unit-test data, filter.execute() only)

Filter IC Before (s) IC After (s) IC Speedup OOC Before (s) OOC After (s) OOC Speedup
ComputeShapesTriangleGeom 0.012920 0.012610 1.02x 0.012910 0.012560 1.03x
RemoveFlaggedFeatures 0.000890 0.000990 0.90x 0.003230 0.003450 0.94x
ComputeKMeans 0.000980 0.000970 1.01x 0.020480 0.020290 1.01x
ComputeArrayStatistics 0.000790 0.000700 1.13x 0.004800 0.004670 1.03x
ComputeArrayHistogramByFeature 0.000710 0.000850 0.84x 0.003890 0.004020 0.97x
ResampleRectGridToImageGeom 0.000890 0.000780 1.14x 0.178960 0.009790 18.28x
ResampleImageGeom 0.002170 0.000940 2.31x 1.658850 0.012980 127.80x
UncertainRegularGridSampleSurfaceMesh 0.003190 0.001850 1.72x 0.487270 0.003690 132.05x
ArrayCalculator 0.000240 0.000240 1.00x 0.000250 0.000230 1.09x
FlyingEdges3D 0.000420 ~0.000100 ~4.20x 0.004830 0.000150 32.20x
ConvertOrientationsToVertexGeometry 0.016570 0.014700 1.13x 0.311490 0.020160 15.45x
ExtractVertexGeometry 0.001500 0.000690 2.17x 0.601910 0.001680 358.28x
ComputeVertexToTriangleDistances 0.000600 0.000750 0.80x 0.000620 0.002030 0.31x
WriteAbaqusHexahedron 0.018990 0.016520 1.15x 4.294920 0.201020 21.37x
WriteStlFile 0.000790 0.000840 0.94x 0.000720 0.001440 0.50x
VerifyTriangleWinding 0.003020 0.003250 0.93x 0.003010 0.003490 0.86x
CreateAMScanPaths 0.020630 0.002070 9.97x 0.020620 0.002180 9.46x

Small existing datasets do not expose every asymptotic win: mesh bucketing/pruning and bounded-memory streaming can add fixed overhead at this scale. All 17 filters passed their affected suites in both configurations; the table reports measured values rather than hiding those small-test regressions.

Large synthetic filter benchmarks (filter.execute() only)

Tier Filter IC Before (s) IC After (s) IC Speedup OOC Before (s) OOC After (s) OOC Speedup
HIGH ComputeArrayHistogram 0.088506 0.053227 1.66x 487.917981 0.814006 599x
HIGH ComputeFeatureBounds 0.053695 0.002626 20.45x 65.637232 0.186534 352x
HIGH ComputeFeatureRect 0.071993 0.017759 4.05x 715.698738 0.090509 7,907x
HIGH ExtractFeatureBoundaries2D 0.028188 0.011352 2.48x 63.905878 0.024392 2,620x
HIGH PartitionGeometry 0.005694 0.001530 3.72x 107.960858 0.054839 1,969x
HIGH ReadChannel5Data 0.408973 0.345354 1.18x >300 0.368083 >815x
HIGH CreateColorMap 0.029480 0.010684 2.76x >120 0.182033 >659x
HIGH ComputeBoundingBoxStats 0.019897 0.014888 1.34x >60 1.075362 >55.8x
HIGH CombineAttributeArrays 0.062550 0.056931 1.10x >60 0.391489 >153x
HIGH ComputeCoordinateThreshold 0.090332 0.000675 133.8x 7.620420 0.090790 83.9x
HIGH ComputeDifferencesMap 0.022731 0.002642 8.60x >60 0.184405 >325x
HIGH ComputeLargestCrossSections (XY) 0.029501 0.026580 1.11x 24.694420 0.032494 760x
HIGH ComputeLargestCrossSections (XZ) 0.024886 0.006156 4.04x 24.816181 0.107269 231x
HIGH ComputeLargestCrossSections (YZ) 0.029908 0.020359 1.47x 26.939749 0.302016 89.2x
HIGH ComputeVectorColors 0.116860 0.062063 1.88x >60 0.248597 >241x
HIGH ComputeFZQuaternions 0.051898 0.038698 1.34x >60 0.595947 >100x
HIGH ComputeMisorientations 0.080476 0.052000 1.55x >60 0.062403 >961x
HIGH ComputeFeaturePhasesBinary 0.027152 0.007749 3.50x >60 0.009101 >6,592x
HIGH ConditionalSetValue 0.009228 0.001509 6.12x >60 0.165667 >362x
HIGH ConvertColorToGrayScale 0.004248 0.001848 2.30x >60 0.101111 >593x
HIGH ConvertQuaternion 0.008270 0.001548 5.34x >60 0.127112 >472x
HIGH ChangeAngleRepresentation 0.002260 0.001425 1.59x >60 0.052921 >1,133x
HIGH ComputeCoordinatesImageGeom 0.006337 0.001267 5.00x >60 0.019143 >3,134x
HIGH AddBadData 0.092763 0.009174 10.11x 49.366814 0.183687 268.76x
HIGH WriteStatsGenOdfAngleFile 0.022509 0.004826 4.66x >60 0.227642 >263x
HIGH WriteVtkStructuredPoints 0.026717 0.015289 1.75x >60 0.016252 >3,691x
HIGH SplitDataArrayByComponent 0.031228 0.002297 13.60x >60 0.198318 >302x
HIGH SplitDataArrayByTuple 0.055733 0.001667 33.43x >60 0.102487 >585x
MEDIUM ComputeMomentInvariants2D 0.001834 0.001560 1.18x 13.974384 0.012929 1,080.86x
MEDIUM CreateFeatureArrayFromElementArray 0.204728 0.026567 7.71x >60 0.251625 >238.45x
MEDIUM ExtractComponentAsArray 0.055706 0.031082 1.79x >60 0.192867 >311.10x
MEDIUM ConvertData 0.005581 0.005187 1.08x >60 0.206812 >290.12x
MEDIUM GroupMicroTextureRegions 0.015163 0.003478 4.36x >60 0.101423 >591.58x
MEDIUM ComputeBoundaryElementFractions 0.013681 0.008264 1.66x 51.947201 0.159685 325.31x
MEDIUM ComputeQuaternionConjugate 0.008203 0.007182 1.14x >60 0.128279 >467.73x
MEDIUM ReadBinaryCTNorthstar 0.018764 0.007421 2.53x >60 0.036908 >1,625.66x
MEDIUM ReadCSVFile 1.126726 1.101959 1.02x >60 1.177329 >50.96x
MEDIUM ReadVtkStructuredPoints 0.300159 0.265677 1.13x >60 0.298817 >200.79x
MEDIUM WriteINLFile 7.112353 7.050428 1.01x >60 7.331228 >8.18x
MEDIUM WriteLosAlamosFFT 1.727152 1.578432 1.09x >60 2.181956 >27.50x

HIGH and MEDIUM benchmarks use 8,000,000 cells (200³) except the 2D-only tests (2048²), the generated Channel5 reader dataset (8,000 × 1,000), and CSV's 8,000,000 rows. Figures are medians of five execute-only runs for the MEDIUM batches and repeated runs for HIGH where practical; > values are conservative timeout bounds. Every listed filter passed its normal affected suite and hidden large benchmark in both configurations.

Additional 200³ execute-only OOC benchmarks

Group Filter Before (s) After (s) Speedup
Groups B–E ComputeBoundaryCells 6.69 0.25 27x
Groups B–E ComputeSurfaceFeatures 4.01 0.28 14x
Groups B–E ComputeFeatureNeighbors 8.93 0.81 11x
Groups B–E ComputeSurfaceAreaToVolume 8.59 0.24 36x
Groups B–E BadDataNeighborOrientationCheck 97.1 5.25 18x
Groups B–E ErodeDilateBadData 25.09 3.80 7x
Groups B–E ErodeDilateCoordinationNumber 12.43 2.30 5x
Groups B–E ErodeDilateMask 6.43 0.40 16x
Groups B–E ReplaceElementAttrsWithNeighborValues 6.05 4.00 1.5x
Groups B–E NeighborOrientationCorrelation 67.94 5.70 12x
Groups B–E ScalarSegmentFeatures 708.3 1.77 400x
Groups B–E EBSDSegmentFeatures 972.6 2.10 463x
Groups B–E CAxisSegmentFeatures 824.1 1.39 593x
Groups B–E FillBadData 8.6 2.26 4x
Groups B–E IdentifySample 825.0 0.27 3056x
Groups B–E AlignSectionsMisorientation 32.89 0.80 41x
Groups B–E AlignSectionsMutualInformation 15.61 0.81 19x
Groups B–E AlignSectionsFeatureCentroid 8.41 0.39 22x
Groups B–E AlignSectionsListFilter 7.50 0.39 19x
Pipeline-critical ComputeFeatureCentroids 39.700 0.025 1,589x
Pipeline-critical RequireMinimumSizeFeatures 20.200 0.210 96x
Pipeline-critical ComputeIPFColors 1.940 0.090 21.5x
Pipeline-critical ComputeFeatureSizes 0.813 0.028 29x
Pipeline-critical ComputeFeatureReferenceMisorientations (AvgOri) 0.106 0.001 106x
Pipeline-critical ComputeFeatureReferenceMisorientations (EuclDist) 0.136 0.001 136x

Full-test wall-clock OOC benchmarks

Group Test Before (s) After (s) Speedup
Mesh generation QuickSurfaceMesh: Base 11.30 0.19 59x
Mesh generation QuickSurfaceMesh: Winding 22.70 0.22 103x
Mesh generation QuickSurfaceMesh: Problem Voxels 11.18 0.19 59x
Mesh generation QuickSurfaceMesh: Winding+PV 21.96 0.22 100x
Mesh generation SurfaceNets: Default 176 2.40 73x
Mesh generation SurfaceNets: Smoothing 224 2.62 85x
Mesh generation SurfaceNets: Winding 515 2.86 180x
Mesh generation SurfaceNets: Winding Smoothing 416 3.22 129x
OrientationAnalysis ComputeFeatureReferenceCAxisMisorientations 196 5.4 36x
OrientationAnalysis ComputeEuclideanDistMap 116 1.1 105x
GBCD ComputeGBCDPoleFigure 833 (fail) 2.4 350x
GBCD ComputeGBCD 1500 (timeout) ~10 150x
GBCD WriteGBCDGMTFile 162 (fail) 6.0 27x
GBCD ComputeGBCDMetricBased 38.1 28.9 1.3x
GBCD WriteGBCDTriangleData 23.5 19.2 1.2x
HDF5 / pole figure WritePoleFigure (3 tests) 4500 (timeout) 11.7 385x
HDF5 / pole figure ReadH5EspritData (3 tests) 2060 (timeout) 6.8 303x
HDF5 / pole figure ReadHDF5Dataset 1500 (timeout) 6.7 224x
Additional ReadRawBinary (Case1) 1076 29 37x
Additional ComputeGBCDPoleFigure 853 0.9 948x
Additional DBSCAN 3D 653 12 54x
Additional AlignSectionsMisorientation Pipeline 635 5.9 107x
Additional ReadH5Ebsd 463 2.1 220x
Additional ReadCtfData 231 0.25 924x
Additional AppendImageGeometry 469 113 4.2x
Additional ComputeFeatureClustering 203 77 2.6x
Additional ComputeTwinBoundaries 179 44 4x
Additional MergeTwins 67 1.8 37x
Additional ComputeKMedoids 74 13 5.7x
Additional CropImageGeometry (X) 27 2.6 10x
Additional WriteAvizoRectilinear 22.8 2.3 10x
Additional WriteAvizoUniform 22.3 2.0 11x

Geometry / Mesh / Phase Filters

Filter Before After Note
ApplyTransformationToGeometry (trilinear, CT_align 1.97 B-voxel) 133s 20s ~6.6x; via ImageRotationUtilities slab cache
ComputeTriangleAreas (CT_align mesh) 26s <1s ~26x
ComputeFeaturePhases (748,800 cells, disk-backed) 11.7s 35ms feature-level vectors + chunked reads
ComputeGBPDMetricBased 20.5s 6.3s ~3.3x; feature/ensemble caching
ExtractInternalSurfacesFromTriangleGeometry ~6.4x less triangle-side memory (bitset + popcount)

Part 4 — Test infrastructure

  • Comparison functions rewritten for chunked bulk I/O (UnitTestCommon.hpp): CompareDataArrays, CompareDataArraysByComponent, CompareArrays, CompareFloatArraysWithNans stream both arrays in 40K-element chunks via copyIntoBuffer instead of per-element operator[] (per-element access on OOC arrays is pathologically slow).
  • New store-type helpers: ExpectedStoreType() (derives the expected StoreType from the active DataStorageMode + whether an OOC manager is registered) and RequireExpectedStoreType().
  • PreferencesSentinel takes a DataStorageMode and restores the original preferences on destruction.
  • TestFileSentinel reference-counts archive extraction via per-process holder files, so parallel test runs don't delete shared decompressed data prematurely.
  • LoadDataStructure test helper calls DREAM3D::LoadDataStructure(path) directly.
  • New SegmentFeaturesTestUtils.hpp (~638 lines): shared builders/verifiers for the SegmentFeatures family.
  • Dual-path testing: ForceOocAlgorithmGuard + GENERATE(from_range(k_ForceOocTestValues)) runs each case in both in-core and forced-OOC modes — adopted by 23 plugin test files (16 SimplnxCore, 7 OrientationAnalysis).
  • 8 new top-level tests in test/: DataStoreFormatResolverTest, DataStorageModeMigrationTest, Dream3dLoadingApiTest, EmptyStringStoreTest, ExtentTest, MemoryBudgetManagerTest, MemorySafetyTest, IParallelAlgorithmTest.
  • RotateSampleRefFrame / RotateEulerRefFrame test paths use slab/chunked bulk I/O.

Part 5 — Build system

  • OOC is a runtime capability — it is selected entirely by the registered IO manager; there is no compile-time OOC switch. cmake/SimplnxConfig.hpp.in is a generated PUBLIC, ODR-safe config header.
  • SIMPLNX_TEST_ALGORITHM_PATH cache option (default 0): 0=Both, 1=OOC-only, 2=InCore-only; plumbed into every plugin test as a compile definition (cmake/Plugin.cmake). An in-core build can validate both algorithm paths (forcing the Scanline path against in-core data still runs correctly and fast).
  • SIMPLNX_UNIT_TEST_TARGETS global propertycreate_simplnx_plugin_unit_test records each test target so consumer (add_subdirectory) builds can attach extra sources/settings.
  • zlib as a direct vcpkg dependency — the OOC layer compiled into consumers uses it directly for parallel deflate-chunk decompression off the global HDF5 mutex (and UnitTestCommon uses it for the in-house tar.gz extractor).
  • /bigobj for the MSVC build of Dream3dIO.cpp (exceeds the COMDAT limit / C1128 in Debug).
  • New core headers/sources registered in CMake (Extent, EmptyStringStore, IDataStoreFormatResolver, InMemoryFormatResolver, MemoryBudgetManager, AlgorithmDispatch, SliceBufferedTransfer, UnionFind, IdentifySampleCommon).

Part 6 — Documentation

  • 48 filter docs updated under src/Plugins/*/docs/ (27 SimplnxCore, 21 OrientationAnalysis; ~+1,177 lines), each adding an ## Algorithm section with ### Performance and paired In-Core / Out-of-Core subsections that explain the dual implementation, memory footprint, and chunk/slab streaming strategy. No docs deleted.

Part 7 — Test data archives

Three download_test_data() entries added:

  • fill_bad_data_exemplars.tar.gz (SimplnxCore)
  • identify_sample_exemplars.tar.gz (SimplnxCore)
  • segment_features_exemplars.tar.gz (referenced by both SimplnxCore and OrientationAnalysis)

No existing archive entries were removed.


Related PR

  • BlueQuartzSoftware/DREAM3DNX#1121 — DREAM3D-NX OOC visualization architecture; consumes the runtime resolver/loader APIs added here and depends on this PR merging first.

Test Plan

  • In-core build (SIMPLNX_TEST_ALGORITHM_PATH=2) passes
  • Out-of-core build (SIMPLNX_TEST_ALGORITHM_PATH=1) passes
  • Both algorithm paths (SIMPLNX_TEST_ALGORITHM_PATH=0) pass
  • All optimized filters produce identical results on both algorithm paths
  • In-core performance verified: no regression on the core-utility changes (CopyData, AppendData, mirror swaps)

@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch from b4ef97f to 99b49ed Compare March 24, 2026 18:13
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 2 times, most recently from b4ef97f to bb09048 Compare March 24, 2026 18:51
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 4 times, most recently from 102c436 to b4c1358 Compare April 2, 2026 00:55
@joeykleingers joeykleingers changed the title WIP: OOC architecture rewrite — new bulk I/O API, SimplnxOoc plugin, and filter optimizations ENH: OOC architecture rewrite — new bulk I/O API and infrastructure Apr 2, 2026
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 6 times, most recently from 2bd614a to 110c054 Compare April 8, 2026 17:41
@joeykleingers joeykleingers changed the title ENH: OOC architecture rewrite — new bulk I/O API and infrastructure WIP: ENH: OOC architecture rewrite — new bulk I/O API and infrastructure Apr 8, 2026
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 4 times, most recently from 35aecd0 to 3a88bbf Compare April 16, 2026 13:03
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch from bdfed87 to 6fbfc8d Compare April 27, 2026 18:18
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch from ed64934 to a56f168 Compare June 25, 2026 17:25
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 3 times, most recently from c6bb7fe to ee6b370 Compare July 13, 2026 18:29
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 2 times, most recently from 5a8c281 to b61d3fd Compare July 15, 2026 17:22
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 5 times, most recently from 9e11033 to 03ac304 Compare July 30, 2026 14:22
@joeykleingers
joeykleingers marked this pull request as ready for review July 30, 2026 14:27
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 9 times, most recently from ec2e32c to 09d552a Compare August 13, 2026 18:45
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch from 1d31c6b to a4cd160 Compare August 17, 2026 13:55
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch from 4baad02 to 36d674d Compare August 24, 2026 14:32
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch 2 times, most recently from 2aaa5ff to 66a06fa Compare September 1, 2026 17:49
Add storage-neutral array creation, chunk-aware bulk I/O, and runtime dispatch
between direct and scanline algorithm implementations. Extend simplnx core,
SimplnxCore, OrientationAnalysis, ITK integration, nxrunner, documentation,
and tests for bounded-memory workflows. 741 files changed, +105089 / -32743
lines.

Storage selection and data structures
=====================================
Files: IDataStoreFormatResolver.*, DataIOCollection.*, DataArray.*,
DataStoreFormatParameter.*, Create*Action.*, EmptyStringStore.*, and geometry
creation actions

Resolve array, string, neighbor-list, and geometry support-array storage when
callers do not specify a format. Preserve explicit formats, apply application
preferences during creation and import, and let node geometry coordinates and
connectivity use registered out-of-core stores.

Bulk datastore and HDF5 I/O
===========================
Files: AbstractDataStore.*, DataStoreUtilities.*, DataStoreIO.*,
DatasetIO.*, ParallelChunkCodec.*, ChunkShapePolicy.*,
DeflateEligibility.*, ChunkIndex.*, and PositionalFileIO.*

Add caller-owned bulk transfer APIs and chunk-aware read and write paths.
Parallelize eligible deflate work outside the serialized HDF5 API, retain
serial fallbacks for unsupported filters and byte orders, preserve raw chunk
filter masks on Windows, and use shared chunk-shape policies.

Algorithm dispatch and bounded utilities
========================================
Files: AlgorithmDispatch.*, Extent.hpp, IExternalSort.*,
ITemporaryRecordStore.*, BoundedRecordPageCache.*,
CacheMemoryBudgetManager.*, SliceBufferedTransfer.*, ExternalEquivalence.*,
and UnionFind.hpp

Select direct or scanline implementations from target-array residency and
record test witnesses for the selected path and store type. Provide bounded
scratch storage, stable external sorting, paged records, slice transfers,
shared cache budgeting, and storage-neutral equivalence and union-find tools.

Storage-aware filter implementations
====================================
Files: SimplnxCore/Filters/Algorithms/*Direct.*,
SimplnxCore/Filters/Algorithms/*Scanline.*, segmentation BFS and CCL files,
and OrientationAnalysis/Filters/Algorithms/*Direct.* and *Scanline.*

Add storage-specific paths for morphology, segmentation, clustering,
statistics, feature transfer, meshing, array transforms, and orientation
calculations. Keep dispatch wrappers shared while scanline, worklist, and
bounded-record implementations avoid unbounded per-value out-of-core access.

Import, persistence, and command-line integration
=================================================
Files: Dream3dIO.*, Dream3dPreflightCache.*, HDF5 DataStructureWriter.*,
ImportH5ObjectPathsAction.*, ITKImageWriterFilter.*, Application.*,
Preferences.*, and nxrunner.*

Apply the storage resolver to imported and preflight arrays, preserve storage
metadata through DREAM3D I/O, and make writers consume storage-neutral arrays.
Expose data-storage mode and cache-memory budget settings through application
preferences and nxrunner configuration.

Tests, build integration, and documentation
===========================================
Files: test/, SimplnxCore/test/, OrientationAnalysis/test/,
UnitTest/AlgorithmTestScope.*, plugin CMake files, docs/, and vcpkg.json

Add in-core and out-of-core dispatch scenarios, execution witnesses, HDF5
codec and chunk-policy tests, memory-safety checks, storage migration tests,
filter regressions, and V&V cases. Register the new sources and update filter
and algorithm documentation for storage-neutral parameter and execution
behavior.

Verification
============
Clang-format 19.1.1 passes for all 682 changed C/C++ files. Git diff --check
passes. No build or test suite is run for this history-only squash.

Signed-off-by: Joey Kleingers <joey.kleingers@bluequartz.net>
@joeykleingers
joeykleingers force-pushed the worktree-ooc-architecture-rewrite branch from 66a06fa to fe36633 Compare September 1, 2026 19:25
@imikejackson

Copy link
Copy Markdown
Contributor

Remove/Extract Flagged Features: the Direct/Scanline rewrite reintroduces the fill-loop hangs fixed on develop

Cross-reference from the V&V of RemoveFlaggedFeaturesFilter (issue #1698, branch topic/remove_extract_feature_hang, not yet merged). That branch fixes the fill/extract defects below in utils/FeatureRemovalUtilities.cpp and Algorithms/RemoveFlaggedFeatures.cpp, with tests that hang or crash on the old code. This PR replaces RemoveFlaggedFeatures.cpp with DispatchAlgorithm<RemoveFlaggedFeaturesDirect, RemoveFlaggedFeaturesScanline> and no longer calls FeatureRemovalUtilities, and both variants were copied from the pre-#1700 algorithm, so every one of those defects is present again on this branch (checked at fe3663391). KeepRemoveRankedFeatures is unaffected here because it still calls the shared utility.

The OOC work itself (bulk copyIntoBuffer/copyFromBuffer, 65,536-tuple chunked marking and renumbering, the three-slice fill window in place of the O(cells) neighbor buffer) is exactly what the CPU/memory/OOC review of the V&V branch asked for. The problem is only that the correctness fixes did not come along.

Defects present in both RemoveFlaggedFeaturesDirect.cpp and RemoveFlaggedFeaturesScanline.cpp

  • Issue BUG: Remove/Extract Flagged Features hangs when Fill-in Removed Features is enabled #1698 hang (first sighting not counted). Direct.cpp:116 pushes a newly seen feature onto discoveredFeatures without a hit; Scanline.cpp:270 does the same in considerNeighbor. A vacated cell whose valid neighbors are all distinct features, or which has a single valid neighbor, never gets a source and the do { } while(shouldLoop) at Direct.cpp:441 / Scanline.cpp:706 never exits. PR FILT: Add Keep/Remove Ranked Features filter #1700 fixed this in FeatureRemovalUtilities.cpp (shipped in 7.4.2); the fix must be carried into both variants. Repro: 5x2x1, FeatureIds = 1 1 1 2 3 / 1 4 4 4 3, remove 2 and 3 with fill on.
  • Background-cell hang. Direct.cpp:75 and Scanline.cpp:234 test featureName > 0, so every FeatureId 0 cell sets shouldLoop on every pass but is never filled (legacy and develop use < 0 / >= 0 -> skip). Any input with background cells and fill on runs forever. The parity test works around this instead of exposing it: RemoveFlaggedFeaturesTest.cpp:93 ReplaceBackgroundForFillTest rewrites every 0 to 1 before running (:247, :255). That helper should be deleted and the test should run on the fixture as-is once the loop is fixed.
  • No no-progress guard. When every cell belongs to a flagged feature and the unflagged features own no cells, no pass fills anything and the loop spins. develop returns -45436; FillBadVoxels.cpp:438 on this branch returns -55572 for the same condition. The guard must count cells actually filled, not sources chosen: listing the Feature Ids array itself in the ignored arrays makes sources get chosen but nothing written, which is the second route to the same hang. develop strips that path from the list with warning -45438.
  • No FeatureId validation before marking. Direct.cpp:157 indexes activeObjects[featureIds[i]] with unvalidated values (negative or >= tuple count reads out of bounds). FlagFeaturesScanline skips such values silently, which leaves them in the output. develop validates the range (-45435) and the tuple count against the geometry (-45437) in a read-only pass before anything is modified; FillBadVoxels.cpp:158 already has the range check (-55567).
  • Extract path ignores sub-filter failures and throws. Direct.cpp:267,278,344,356 and Scanline.cpp:524,535,601,613 test preflightResult after filter.execute() and throw std::runtime_error instead of returning a Result<>. A flagged feature that owns no cell has min > max bounds, fails the crop preflight, and throws out of the filter. develop returns -53901..-53904 with the sub-filter message and skips empty features with one aggregated warning (-53905).
  • tempBounds is copied into every extracted geometry and leaks on error. Direct.cpp:360 / Scanline.cpp:617 read the bounds through a DataArray copy and leave the array in the feature Attribute Matrix, so CropImageGeometryFilter (feature renumbering off) copies it into each extracted geometry and no error path deletes it. develop copies the six bounds per feature into a local buffer and removes the array before the first crop.

Suggested resolution

  • Route both variants' fill through Algorithms/FillBadVoxels.{hpp,cpp}, which already has the correct first-sighting vote (:52-82), negative-only targets, -55567 and -55572, and bulk I/O, rather than keeping a third copy of the loop. Add the tuple-count check and the Feature-Ids-cannot-be-ignored rule there so RequireMinNumNeighbors gets them too.
  • After topic/remove_extract_feature_hang lands, run its RemoveFlaggedFeaturesTest.cpp (18 cases; the fixtures pin the copy source of every filled cell via a unique-per-cell CellValue array) under both AlgorithmTestScenarios. The -45435/-45436/-45437/-45438 and -53905 expectations will need to be reconciled with whatever codes the shared helper reports.
  • Reconcile the extract path with develop's RemoveFlaggedFeatures.cpp (local bounds copy, immediate tempBounds deletion, Result<> errors, aggregated empty-feature warning); that part is storage-neutral and can be shared verbatim by both variants.

Full analysis: src/Plugins/SimplnxCore/vv/RemoveFlaggedFeaturesFilter.md and vv/deviations/RemoveFlaggedFeaturesFilter.md on the V&V branch (deviations D1 through D7).

Extend the shared cache budget manager with explicit pin residency and bounded
temporary reservations. Preserve storage-neutral filter behavior while keeping
visible cache entries out of normal eviction and avoiding Windows stack
overflow in M3C. 4 files changed, +602 / -13 lines.

Pinned cache accounting
=======================
Files: CacheMemoryBudgetManager.hpp and CacheMemoryBudgetManager.cpp

Add initially pinned registrations, nested pin and unpin operations,
budget-aware pin admission, temporary pinned reservations, and unique pinned
byte reporting. Exclude pinned entries from direct LRU eviction, preserve
recency when the final pin is released, and keep delegated-subsystem behavior
and over-budget accounting explicit. Use subtraction-based budget checks to
avoid overflow at size limits.

M3C bounded-memory safety
=========================
File: SimplnxCore/Filters/Algorithms/M3CSurfaceMeshing.cpp

Move six fixed OOC batch buffers from the thread stack to equal-size
heap-backed vectors. Keep the existing types, batch sizes, initialization,
processing order, and output behavior while remaining below the default
Windows stack limit.

Tests
=====
File: CacheMemoryBudgetManagerTest.cpp

Cover nested pin balance, pinned-byte accounting, LRU selection, recency,
budget reductions, temporary reservations, delegated subsystems, cleared
handles, concurrent pin use, and pin-operation overhead. Exercise all-pinned
over-budget behavior without allowing pinned entries to become a persistent
unbounded policy.

Verification
============
Windows/MSVC Release builds pass for in-core and out-of-core configurations.
The sequential in-core CTest suite passes 2251 of 2251 tests. The sequential
out-of-core-compatible suite passes 2357 of 2357 tests with ITK excluded.
Focused and complete M3C tests pass in both configurations.

Signed-off-by: Jessica Marquis <jessica.marquis@bluequartz.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants