diff --git a/Common/DataModel/vtkCellArray.cxx b/Common/DataModel/vtkCellArray.cxx index 20f91764..c8e67b2f 100644 --- a/Common/DataModel/vtkCellArray.cxx +++ b/Common/DataModel/vtkCellArray.cxx @@ -568,11 +568,15 @@ vtkCellArray::vtkCellArray() vtkCellArray::~vtkCellArray() = default; vtkStandardNewMacro(vtkCellArray); -#ifdef VTK_USE_64BIT_IDS -bool vtkCellArray::DefaultStorageIs64Bit = true; -#else +// fvtk width-relaxed rule ([[fvtk-int32-default-width-relaxed]]): default cell +// arrays to 32-bit offset/connectivity storage regardless of vtkIdType width. +// This halves the cell-array footprint (and memory bandwidth, the bottleneck on +// large meshes) vs stock VTK's 64-bit default. Every value-writing path +// auto-widens to 64-bit when a value cannot fit in int32, so meshes with +// >2^31 points/cells stay correct. Integer VALUES are identical to stock; only +// the container narrows. Use vtkCellArray::SetDefaultStorageIs64Bit(true) to +// restore stock behaviour. bool vtkCellArray::DefaultStorageIs64Bit = false; -#endif //=================== Begin Legacy Methods =================================== // These should be deprecated at some point as they are confusing or very slow @@ -806,6 +810,25 @@ void vtkCellArray::Append(vtkCellArray* src, vtkIdType pointOffset) { if (src->GetNumberOfCells() > 0) { + // After append, the largest offset = our connectivity size + src's, and the + // largest connectivity id = src's max id + pointOffset. Widen if either + // overflows int32 (GetRange is cached on src; this is a bulk op already). + if (this->StorageType == StorageTypes::Int32) + { + vtkIdType maxVal = this->GetNumberOfConnectivityIds() + src->GetNumberOfConnectivityIds(); + vtkDataArray* srcConn = src->GetConnectivityArray(); + if (srcConn && srcConn->GetNumberOfValues() > 0) + { + double r[2]; + srcConn->GetRange(r, 0); + const vtkIdType m = static_cast(r[1]) + pointOffset; + if (m > maxVal) + { + maxVal = m; + } + } + this->WidenStorageForValue(maxVal); + } this->Dispatch(AppendImpl{}, src, pointOffset); } } @@ -1435,6 +1458,19 @@ void vtkCellArray::ReplaceCellAtId(vtkIdType cellId, vtkIdList* list) void vtkCellArray::ReplaceCellAtId( vtkIdType cellId, vtkIdType cellSize, const vtkIdType cellPoints[]) { + // Replaces connectivity ids in place; widen if any new id overflows int32. + if (this->StorageType == StorageTypes::Int32) + { + vtkIdType maxVal = 0; + for (vtkIdType i = 0; i < cellSize; ++i) + { + if (cellPoints[i] > maxVal) + { + maxVal = cellPoints[i]; + } + } + this->WidenStorageForValue(maxVal); + } this->Dispatch(ReplaceCellAtIdImpl{}, cellId, cellSize, cellPoints); } @@ -1442,6 +1478,7 @@ void vtkCellArray::ReplaceCellAtId( void vtkCellArray::ReplaceCellPointAtId( vtkIdType cellId, vtkIdType cellPointIndex, vtkIdType newPointId) { + this->WidenStorageForValue(newPointId); this->Dispatch(ReplaceCellPointAtIdImpl{}, cellId, cellPointIndex, newPointId); } @@ -1489,6 +1526,27 @@ void vtkCellArray::AppendLegacyFormat(vtkIdTypeArray* data, vtkIdType ptOffset) //------------------------------------------------------------------------------ void vtkCellArray::AppendLegacyFormat(const vtkIdType* data, vtkIdType len, vtkIdType ptOffset) { + // Legacy data is [npts, id0..., npts2, id0...]. Upper-bound the largest value + // this will store: offsets grow by at most `len`, and connectivity ids are + // data values + ptOffset. Scanning `data` for its max (incl. the small count + // entries -- a harmless overestimate) bounds both. Widen once if needed. + if (this->StorageType == StorageTypes::Int32) + { + vtkIdType maxData = 0; + for (vtkIdType i = 0; i < len; ++i) + { + if (data[i] > maxData) + { + maxData = data[i]; + } + } + vtkIdType maxVal = this->GetNumberOfConnectivityIds() + len; + if (maxData + ptOffset > maxVal) + { + maxVal = maxData + ptOffset; + } + this->WidenStorageForValue(maxVal); + } this->Dispatch(AppendLegacyFormatImpl{}, data, len, ptOffset); } diff --git a/Common/DataModel/vtkCellArray.h b/Common/DataModel/vtkCellArray.h index e29b2639..334a0694 100644 --- a/Common/DataModel/vtkCellArray.h +++ b/Common/DataModel/vtkCellArray.h @@ -333,6 +333,7 @@ class VTKCOMMONDATAMODEL_EXPORT VTK_MARSHALMANUAL vtkCellArray : public vtkAbstr */ void SetOffset(vtkIdType cellId, vtkIdType offset) { + this->WidenStorageForValue(offset); this->Offsets->SetComponent(cellId, 0, static_cast(offset)); } @@ -1539,6 +1540,23 @@ class VTKCOMMONDATAMODEL_EXPORT VTK_MARSHALMANUAL vtkCellArray : public vtkAbstr private: vtkCellArray(const vtkCellArray&) = delete; void operator=(const vtkCellArray&) = delete; + + // fvtk width-relaxed rule ([[fvtk-int32-default-width-relaxed]]): cell arrays + // default to 32-bit offset/connectivity storage (half the footprint of stock + // VTK's 64-bit default) and auto-widen to 64-bit only when a value cannot be + // represented in int32. This guard is called by every value-writing path + // before it stores; on the common path it is a single predicted-not-taken + // branch. `maxValue` must be the largest value that path is about to store + // (largest offset and/or connectivity id). Out-of-line ConvertTo64BitStorage + // runs only on the rare overflow. + void WidenStorageForValue(vtkIdType maxValue) + { + if (this->StorageType == StorageTypes::Int32 && + maxValue > static_cast(0x7FFFFFFF)) + { + this->ConvertTo64BitStorage(); + } + } }; template @@ -1841,6 +1859,20 @@ inline vtkIdType vtkCellArray::GetCellPointAtId(vtkIdType cellId, vtkIdType cell inline vtkIdType vtkCellArray::InsertNextCell(vtkIdType npts, const vtkIdType* pts) VTK_SIZEHINT(pts, npts) { + // Widen to 64-bit if any stored value (the new offset = current connectivity + // size + npts, or any point id in pts) would overflow int32. + if (this->StorageType == StorageTypes::Int32) + { + vtkIdType maxVal = this->GetNumberOfConnectivityIds() + npts; + for (vtkIdType i = 0; i < npts; ++i) + { + if (pts[i] > maxVal) + { + maxVal = pts[i]; + } + } + this->WidenStorageForValue(maxVal); + } vtkIdType cellId; this->Dispatch(vtkCellArray_detail::InsertNextCellImpl{}, npts, pts, cellId); return cellId; @@ -1849,6 +1881,9 @@ inline vtkIdType vtkCellArray::InsertNextCell(vtkIdType npts, const vtkIdType* p //---------------------------------------------------------------------------- inline vtkIdType vtkCellArray::InsertNextCell(int npts) { + // Incremental API: this writes only the new offset (= connectivity size + + // npts); connectivity ids arrive later via InsertCellPoint, guarded there. + this->WidenStorageForValue(this->GetNumberOfConnectivityIds() + npts); vtkIdType cellId; this->Dispatch(vtkCellArray_detail::InsertNextCellImpl{}, npts, cellId); return cellId; @@ -1857,32 +1892,29 @@ inline vtkIdType vtkCellArray::InsertNextCell(int npts) //---------------------------------------------------------------------------- inline void vtkCellArray::InsertCellPoint(vtkIdType id) { + this->WidenStorageForValue(id); this->Dispatch(vtkCellArray_detail::InsertCellPointImpl{}, id); } //---------------------------------------------------------------------------- inline void vtkCellArray::UpdateCellCount(int npts) { + // Updates the current cell's end offset (= connectivity size). + this->WidenStorageForValue(this->GetNumberOfConnectivityIds()); this->Dispatch(vtkCellArray_detail::UpdateCellCountImpl{}, npts); } //---------------------------------------------------------------------------- inline vtkIdType vtkCellArray::InsertNextCell(vtkIdList* pts) { - vtkIdType cellId; - this->Dispatch( - vtkCellArray_detail::InsertNextCellImpl{}, pts->GetNumberOfIds(), pts->GetPointer(0), cellId); - return cellId; + return this->InsertNextCell(pts->GetNumberOfIds(), pts->GetPointer(0)); } //---------------------------------------------------------------------------- inline vtkIdType vtkCellArray::InsertNextCell(vtkCell* cell) { vtkIdList* pts = cell->GetPointIds(); - vtkIdType cellId; - this->Dispatch( - vtkCellArray_detail::InsertNextCellImpl{}, pts->GetNumberOfIds(), pts->GetPointer(0), cellId); - return cellId; + return this->InsertNextCell(pts->GetNumberOfIds(), pts->GetPointer(0)); } //---------------------------------------------------------------------------- diff --git a/IO/Geometry/vtkSTLReader.cxx b/IO/Geometry/vtkSTLReader.cxx index 9ae8f2ee..d86fcd29 100644 --- a/IO/Geometry/vtkSTLReader.cxx +++ b/IO/Geometry/vtkSTLReader.cxx @@ -21,6 +21,7 @@ #include "vtkSmartPointer.h" #include "vtkStreamingDemandDrivenPipeline.h" #include "vtkStringScanner.h" +#include "vtkTypeInt32Array.h" #include "vtkUnsignedCharArray.h" #include @@ -493,17 +494,43 @@ int vtkSTLReader::ReadSTLFast(vtkResourceStream* stream, vtkPolyData* output) } // Finalize the cell array: shrink connectivity to the kept triangles and - // synthesize the uniform offset array (0, 3, 6, ...). + // synthesize the uniform offset array (0, 3, 6, ...). Per the fvtk int32- + // default rule ([[fvtk-int32-default-width-relaxed]]), store offsets and + // connectivity as int32 when every value fits -- numFile*3 bounds both the + // largest offset (nKept*3) and the largest vertex index (numPts <= nKept*3) -- + // widening to int64 only for >2^31-element meshes. int32 halves the cell-array + // footprint; the values are identical. connArr->SetNumberOfValues(nKept * 3); - vtkNew offArr; - offArr->SetNumberOfValues(nKept + 1); - vtkIdType* off = offArr->GetPointer(0); - for (vtkIdType i = 0; i <= nKept; ++i) + vtkNew newPolys; + if (numFile <= static_cast(0x7FFFFFFF) / 3) { - off[i] = 3 * i; + vtkNew conn32; + conn32->SetNumberOfValues(nKept * 3); + vtkTypeInt32* c32 = conn32->GetPointer(0); + for (vtkIdType i = 0; i < nKept * 3; ++i) + { + c32[i] = static_cast(conn[i]); + } + vtkNew off32; + off32->SetNumberOfValues(nKept + 1); + vtkTypeInt32* o32 = off32->GetPointer(0); + for (vtkIdType i = 0; i <= nKept; ++i) + { + o32[i] = static_cast(3 * i); + } + newPolys->SetData(off32, conn32); + } + else + { + vtkNew offArr; + offArr->SetNumberOfValues(nKept + 1); + vtkIdType* off = offArr->GetPointer(0); + for (vtkIdType i = 0; i <= nKept; ++i) + { + off[i] = 3 * i; + } + newPolys->SetData(offArr, connArr); } - vtkNew newPolys; - newPolys->SetData(offArr, connArr); const vtkIdType numPts = static_cast(merger.NumberOfVertices()); vtkNew coords; diff --git a/tests/bitexact/compare.py b/tests/bitexact/compare.py index 2ac7dfe3..b71584c7 100644 --- a/tests/bitexact/compare.py +++ b/tests/bitexact/compare.py @@ -52,11 +52,23 @@ def compare_case(stock_dir, fvtk_dir, key): ok = True for name in sorted(names_a): x, y = a[name], b[name] - equal = bool( - x.shape == y.shape - and x.dtype == y.dtype - and np.array_equal(x, y) - ) + # Width-relaxed comparison for INTEGER arrays (fvtk-wide int32-default + # rule): integer VALUES are sacred, but the storage CONTAINER WIDTH is + # negotiable -- fvtk defaults topology/index/offset arrays to int32 while + # stock VTK uses int64. So for integer-kind arrays compare values + # normalized to int64 and ignore the dtype tag. FLOAT (and other) arrays + # stay strict: identical dtype + exact bytes (maxULP=0). + if x.dtype.kind in "iu" and y.dtype.kind in "iu": + equal = bool( + x.shape == y.shape + and np.array_equal(x.astype(np.int64), y.astype(np.int64)) + ) + else: + equal = bool( + x.shape == y.shape + and x.dtype == y.dtype + and np.array_equal(x, y) + ) ulp = None if x.dtype.kind == "f" and x.shape == y.shape and x.dtype == y.dtype: ulp = _ulp_distance(x, y)