From 8203d5b90ec312901843f0a9a9d6c8123421d128 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 19 Jun 2026 14:05:48 -0600 Subject: [PATCH] perf(geometry): width-relaxed int32 vtkOriginalPointIds + width-agnostic selection vtkGeometryFilter::PassPointIds now stores the vtkOriginalPointIds passthrough array in an int32 container when every input point id fits in 0x7FFFFFFF (else int64), halving its footprint on large extracted surfaces. Values are sacred; only the container width changes. This is only safe because the render hardware-selection path reads point/cell id passthrough arrays width-agnostically: vtkOpenGLPolyDataMapper (and the Batched, LowMemory, and LowMemoryBatched variants) previously did vtkArrayDownCast(...) which returns NULL on an int32 array, silently skipping the picked-id remap. They now fetch the array as vtkDataArray and read values via GetComponent (exact for ids < 2^53, identical to the former vtkIdType GetValue), including the UpdateMaximumPointId/CellId id-bit-sizing paths. Validation: * bitexact: op_geometry / op_geometry_ugrid / op_geometry_ugrid_mixed now enable PassThroughPointIdsOn, so the int32 vtkOriginalPointIds array is compared against stock's int64 (width-normalized -> values must match). * NEW selection gate (tests/renderexact/run_select.py + compare_select.py, wired into ci/run-renderexact.sh): renders the surface of a hex lattice with the mapper remapping picked point ids through vtkOriginalPointIds, runs a full-viewport vtkHardwareSelector POINT selection, and asserts fvtk's selected original-id set equals stock's. The lattice makes original ids a non-identity map of surface ids, so a dropped int32 remap would change the result -- the gate is discriminating. Co-Authored-By: Claude Opus 4.8 --- Filters/Geometry/vtkGeometryFilter.cxx | 50 +++-- .../vtkOpenGLBatchedPolyDataMapper.cxx | 30 +-- ...tkOpenGLLowMemoryBatchedPolyDataMapper.cxx | 30 +-- .../vtkOpenGLLowMemoryPolyDataMapper.cxx | 40 ++-- Rendering/OpenGL2/vtkOpenGLPolyDataMapper.cxx | 49 +++-- ci/run-renderexact.sh | 9 + tests/bitexact/ops.py | 6 + tests/renderexact/compare_select.py | 75 +++++++ tests/renderexact/run_select.py | 202 ++++++++++++++++++ 9 files changed, 422 insertions(+), 69 deletions(-) create mode 100644 tests/renderexact/compare_select.py create mode 100644 tests/renderexact/run_select.py diff --git a/Filters/Geometry/vtkGeometryFilter.cxx b/Filters/Geometry/vtkGeometryFilter.cxx index 9af1e5a5..921b1258 100644 --- a/Filters/Geometry/vtkGeometryFilter.cxx +++ b/Filters/Geometry/vtkGeometryFilter.cxx @@ -32,6 +32,7 @@ #include "vtkStructuredData.h" #include "vtkStructuredGrid.h" #include "vtkTetra.h" +#include "vtkTypeInt32Array.h" // fvtk: width-relaxed int32 OriginalPointIds storage #include "vtkUniformGrid.h" #include "vtkUnsignedCharArray.h" #include "vtkUnstructuredGrid.h" @@ -2808,19 +2809,11 @@ struct CharacterizeGrid }; //------------------------------------------------------------------------------ -// Threaded creation to generate array of originating point ids. -template -void PassPointIds(const char* name, vtkIdType numInputPts, vtkIdType numOutputPts, - TInputIdType* ptMap, vtkPointData* outPD) +// Threaded populate of the originating-point-id array (templated on the output +// container's value type so the same scatter drives int32 or int64 storage). +template +void PassPointIdsFill(TOutId* origIds, vtkIdType numInputPts, TInputIdType* ptMap) { - vtkNew origPtIds; - origPtIds->SetName(name); - origPtIds->SetNumberOfComponents(1); - origPtIds->SetNumberOfTuples(numOutputPts); - outPD->AddArray(origPtIds); - vtkIdType* origIds = origPtIds->GetPointer(0); - - // Now threaded populate the array vtkSMPTools::For(0, numInputPts, [&origIds, &ptMap](vtkIdType ptId, vtkIdType endPtId) { @@ -2828,12 +2821,43 @@ void PassPointIds(const char* name, vtkIdType numInputPts, vtkIdType numOutputPt { if (ptMap[ptId] >= 0) { - origIds[ptMap[ptId]] = ptId; + origIds[ptMap[ptId]] = static_cast(ptId); } } }); } +//------------------------------------------------------------------------------ +// Threaded creation to generate array of originating point ids. +template +void PassPointIds(const char* name, vtkIdType numInputPts, vtkIdType numOutputPts, + TInputIdType* ptMap, vtkPointData* outPD) +{ + // fvtk: width-relaxed storage. The values are input point ids (sacred); the + // CONTAINER is int32 when every id fits in 0x7FFFFFFF, else int64. Halves the + // array footprint on large extracted surfaces. The bitexact gate width- + // normalizes integer arrays, and the render hardware-selection path reads this + // passthrough array width-agnostically (vtkOpenGL*PolyDataMapper). + if (numInputPts <= static_cast(0x7FFFFFFF)) + { + vtkNew origPtIds; + origPtIds->SetName(name); + origPtIds->SetNumberOfComponents(1); + origPtIds->SetNumberOfTuples(numOutputPts); + outPD->AddArray(origPtIds); + PassPointIdsFill(origPtIds->GetPointer(0), numInputPts, ptMap); + } + else + { + vtkNew origPtIds; + origPtIds->SetName(name); + origPtIds->SetNumberOfComponents(1); + origPtIds->SetNumberOfTuples(numOutputPts); + outPD->AddArray(origPtIds); + PassPointIdsFill(origPtIds->GetPointer(0), numInputPts, ptMap); + } +} + //------------------------------------------------------------------------------ // Threaded compositing of originating cell ids. template diff --git a/Rendering/OpenGL2/vtkOpenGLBatchedPolyDataMapper.cxx b/Rendering/OpenGL2/vtkOpenGLBatchedPolyDataMapper.cxx index c89eaef0..3c4d56b7 100644 --- a/Rendering/OpenGL2/vtkOpenGLBatchedPolyDataMapper.cxx +++ b/Rendering/OpenGL2/vtkOpenGLBatchedPolyDataMapper.cxx @@ -489,8 +489,10 @@ void vtkOpenGLBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHardwareSel // do we need to do anything to the point id data? if (currPass == vtkHardwareSelector::POINT_ID_LOW24) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(pd->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id read (int32-or-int64 passthrough array); see + // vtkOpenGLPolyDataMapper for the bit-exactness argument. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(pd->GetArray(this->PointIdArrayName)) : nullptr; // do we need to do anything to the point id data? @@ -521,7 +523,7 @@ void vtkOpenGLBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHardwareSel vtkIdType outval = inval; if (pointArrayId && static_cast(inval) <= pointArrayId->GetMaxId()) { - outval = pointArrayId->GetValue(inval); + outval = static_cast(pointArrayId->GetComponent(inval, 0)); } plowdata[pos] = outval & 0xff; plowdata[pos + 1] = (outval & 0xff00) >> 8; @@ -533,8 +535,10 @@ void vtkOpenGLBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHardwareSel if (currPass == vtkHardwareSelector::POINT_ID_HIGH24) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(pd->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id read (int32-or-int64 passthrough array); see + // vtkOpenGLPolyDataMapper for the bit-exactness argument. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(pd->GetArray(this->PointIdArrayName)) : nullptr; // do we need to do anything to the point id data? @@ -557,7 +561,7 @@ void vtkOpenGLBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHardwareSel vtkIdType outval = inval; if (pointArrayId) { - outval = pointArrayId->GetValue(inval); + outval = static_cast(pointArrayId->GetComponent(inval, 0)); } phighdata[pos] = (outval & 0xff000000) >> 24; phighdata[pos + 1] = (outval & 0xff00000000) >> 32; @@ -606,8 +610,9 @@ void vtkOpenGLBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHardwareSel if (currPass == vtkHardwareSelector::CELL_ID_LOW24) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(cd->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id read (see point-id note above). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(cd->GetArray(this->CellIdArrayName)) : nullptr; unsigned char* clowdata = sel->GetPixelBuffer(vtkHardwareSelector::CELL_ID_LOW24); bool hasHighCellIds = sel->HasHighCellIds(); @@ -639,7 +644,7 @@ void vtkOpenGLBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHardwareSel glBatchElement->CellCellMap->ConvertOpenGLCellIdToVTKCellId(pointPicking, inval); if (cellArrayId && outval <= cellArrayId->GetMaxId()) { - outval = cellArrayId->GetValue(outval); + outval = static_cast(cellArrayId->GetComponent(outval, 0)); } clowdata[pos] = outval & 0xff; clowdata[pos + 1] = (outval & 0xff00) >> 8; @@ -651,8 +656,9 @@ void vtkOpenGLBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHardwareSel if (currPass == vtkHardwareSelector::CELL_ID_HIGH24) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(cd->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id read (see point-id note above). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(cd->GetArray(this->CellIdArrayName)) : nullptr; unsigned char* chighdata = sel->GetPixelBuffer(vtkHardwareSelector::CELL_ID_HIGH24); @@ -675,7 +681,7 @@ void vtkOpenGLBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHardwareSel glBatchElement->CellCellMap->ConvertOpenGLCellIdToVTKCellId(pointPicking, inval); if (cellArrayId) { - outval = cellArrayId->GetValue(outval); + outval = static_cast(cellArrayId->GetComponent(outval, 0)); } chighdata[pos] = (outval & 0xff000000) >> 24; chighdata[pos + 1] = (outval & 0xff00000000) >> 32; diff --git a/Rendering/OpenGL2/vtkOpenGLLowMemoryBatchedPolyDataMapper.cxx b/Rendering/OpenGL2/vtkOpenGLLowMemoryBatchedPolyDataMapper.cxx index d32622d3..fbae4f71 100644 --- a/Rendering/OpenGL2/vtkOpenGLLowMemoryBatchedPolyDataMapper.cxx +++ b/Rendering/OpenGL2/vtkOpenGLLowMemoryBatchedPolyDataMapper.cxx @@ -514,8 +514,10 @@ void vtkOpenGLLowMemoryBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHa // do we need to do anything to the point id data? if (currPass == vtkHardwareSelector::POINT_ID_LOW24) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(pd->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id read (int32-or-int64 passthrough array); see + // vtkOpenGLPolyDataMapper for the bit-exactness argument. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(pd->GetArray(this->PointIdArrayName)) : nullptr; // do we need to do anything to the point id data? @@ -545,7 +547,7 @@ void vtkOpenGLLowMemoryBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHa vtkIdType outval = inval; if (pointArrayId && static_cast(inval) <= pointArrayId->GetMaxId()) { - outval = pointArrayId->GetValue(inval); + outval = static_cast(pointArrayId->GetComponent(inval, 0)); } plowdata[pos] = outval & 0xff; plowdata[pos + 1] = (outval & 0xff00) >> 8; @@ -557,8 +559,10 @@ void vtkOpenGLLowMemoryBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHa if (currPass == vtkHardwareSelector::POINT_ID_HIGH24) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(pd->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id read (int32-or-int64 passthrough array); see + // vtkOpenGLPolyDataMapper for the bit-exactness argument. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(pd->GetArray(this->PointIdArrayName)) : nullptr; // do we need to do anything to the point id data? @@ -580,7 +584,7 @@ void vtkOpenGLLowMemoryBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHa vtkIdType outval = inval; if (pointArrayId) { - outval = pointArrayId->GetValue(inval); + outval = static_cast(pointArrayId->GetComponent(inval, 0)); } phighdata[pos] = (outval & 0xff000000) >> 24; phighdata[pos + 1] = (outval & 0xff00000000) >> 32; @@ -617,8 +621,9 @@ void vtkOpenGLLowMemoryBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHa if (currPass == vtkHardwareSelector::CELL_ID_LOW24) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(cd->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id read (see point-id note above). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(cd->GetArray(this->CellIdArrayName)) : nullptr; unsigned char* clowdata = sel->GetPixelBuffer(vtkHardwareSelector::CELL_ID_LOW24); bool hasHighCellIds = sel->HasHighCellIds(); @@ -647,7 +652,7 @@ void vtkOpenGLLowMemoryBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHa vtkIdType outval = inval; if (cellArrayId && outval <= cellArrayId->GetMaxId()) { - outval = cellArrayId->GetValue(outval); + outval = static_cast(cellArrayId->GetComponent(outval, 0)); } clowdata[pos] = outval & 0xff; clowdata[pos + 1] = (outval & 0xff00) >> 8; @@ -659,8 +664,9 @@ void vtkOpenGLLowMemoryBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHa if (currPass == vtkHardwareSelector::CELL_ID_HIGH24) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(cd->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id read (see point-id note above). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(cd->GetArray(this->CellIdArrayName)) : nullptr; unsigned char* chighdata = sel->GetPixelBuffer(vtkHardwareSelector::CELL_ID_HIGH24); @@ -680,7 +686,7 @@ void vtkOpenGLLowMemoryBatchedPolyDataMapper::ProcessCompositePixelBuffers(vtkHa vtkIdType outval = inval; if (cellArrayId) { - outval = cellArrayId->GetValue(outval); + outval = static_cast(cellArrayId->GetComponent(outval, 0)); } chighdata[pos] = (outval & 0xff000000) >> 24; chighdata[pos + 1] = (outval & 0xff00000000) >> 32; diff --git a/Rendering/OpenGL2/vtkOpenGLLowMemoryPolyDataMapper.cxx b/Rendering/OpenGL2/vtkOpenGLLowMemoryPolyDataMapper.cxx index d0a1370d..dbdf40ff 100644 --- a/Rendering/OpenGL2/vtkOpenGLLowMemoryPolyDataMapper.cxx +++ b/Rendering/OpenGL2/vtkOpenGLLowMemoryPolyDataMapper.cxx @@ -3268,8 +3268,10 @@ void vtkOpenGLLowMemoryPolyDataMapper::ProcessSelectorPixelBuffers( if (currPass == vtkHardwareSelector::POINT_ID_LOW24) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(pd->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id read (int32-or-int64 passthrough array); see + // vtkOpenGLPolyDataMapper for the bit-exactness argument. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(pd->GetArray(this->PointIdArrayName)) : nullptr; // do we need to do anything to the point id data? @@ -3290,7 +3292,7 @@ void vtkOpenGLLowMemoryPolyDataMapper::ProcessSelectorPixelBuffers( inval |= rawplowdata[pos + 1]; inval = inval << 8; inval |= rawplowdata[pos]; - vtkIdType outval = pointArrayId->GetValue(inval); + vtkIdType outval = static_cast(pointArrayId->GetComponent(inval, 0)); plowdata[pos] = outval & 0xff; plowdata[pos + 1] = (outval & 0xff00) >> 8; plowdata[pos + 2] = (outval & 0xff0000) >> 16; @@ -3300,8 +3302,10 @@ void vtkOpenGLLowMemoryPolyDataMapper::ProcessSelectorPixelBuffers( if (currPass == vtkHardwareSelector::POINT_ID_HIGH24) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(pd->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id read (int32-or-int64 passthrough array); see + // vtkOpenGLPolyDataMapper for the bit-exactness argument. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(pd->GetArray(this->PointIdArrayName)) : nullptr; // do we need to do anything to the point id data? @@ -3319,7 +3323,7 @@ void vtkOpenGLLowMemoryPolyDataMapper::ProcessSelectorPixelBuffers( inval |= rawplowdata[pos + 1]; inval = inval << 8; inval |= rawplowdata[pos]; - vtkIdType outval = pointArrayId->GetValue(inval); + vtkIdType outval = static_cast(pointArrayId->GetComponent(inval, 0)); phighdata[pos] = (outval & 0xff000000) >> 24; phighdata[pos + 1] = (outval & 0xff00000000) >> 32; phighdata[pos + 2] = (outval & 0xff0000000000) >> 40; @@ -3356,8 +3360,9 @@ void vtkOpenGLLowMemoryPolyDataMapper::ProcessSelectorPixelBuffers( // process the cellid array? if (currPass == vtkHardwareSelector::CELL_ID_LOW24) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(cd->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id read (see point-id note above). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(cd->GetArray(this->CellIdArrayName)) : nullptr; unsigned char* clowdata = sel->GetPixelBuffer(vtkHardwareSelector::CELL_ID_LOW24); @@ -3379,7 +3384,7 @@ void vtkOpenGLLowMemoryPolyDataMapper::ProcessSelectorPixelBuffers( vtkIdType outval = inval; if (cellArrayId) { - outval = cellArrayId->GetValue(outval); + outval = static_cast(cellArrayId->GetComponent(outval, 0)); } clowdata[pos] = outval & 0xff; clowdata[pos + 1] = (outval & 0xff00) >> 8; @@ -3390,8 +3395,9 @@ void vtkOpenGLLowMemoryPolyDataMapper::ProcessSelectorPixelBuffers( if (currPass == vtkHardwareSelector::CELL_ID_HIGH24) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(cd->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id read (see point-id note above). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(cd->GetArray(this->CellIdArrayName)) : nullptr; unsigned char* chighdata = sel->GetPixelBuffer(vtkHardwareSelector::CELL_ID_HIGH24); @@ -3410,7 +3416,7 @@ void vtkOpenGLLowMemoryPolyDataMapper::ProcessSelectorPixelBuffers( vtkIdType outval = inval; if (cellArrayId) { - outval = cellArrayId->GetValue(outval); + outval = static_cast(cellArrayId->GetComponent(outval, 0)); } chighdata[pos] = (outval & 0xff000000) >> 24; chighdata[pos + 1] = (outval & 0xff00000000) >> 32; @@ -3434,8 +3440,9 @@ void vtkOpenGLLowMemoryPolyDataMapper::UpdateMaximumPointCellIds(vtkRenderer* re vtkIdType maxPointId = mesh->GetPoints()->GetNumberOfPoints() - 1; if (mesh && mesh->GetPointData()) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(mesh->GetPointData()->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id read (GetRange works on any vtkDataArray). + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(mesh->GetPointData()->GetArray(this->PointIdArrayName)) : nullptr; if (pointArrayId) { @@ -3451,8 +3458,9 @@ void vtkOpenGLLowMemoryPolyDataMapper::UpdateMaximumPointCellIds(vtkRenderer* re vtkIdType maxCellId = mesh->GetNumberOfCells() - 1; if (mesh && mesh->GetCellData()) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(mesh->GetCellData()->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id read (GetRange works on any vtkDataArray). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(mesh->GetCellData()->GetArray(this->CellIdArrayName)) : nullptr; if (cellArrayId) { diff --git a/Rendering/OpenGL2/vtkOpenGLPolyDataMapper.cxx b/Rendering/OpenGL2/vtkOpenGLPolyDataMapper.cxx index c0c0156d..84f88d02 100644 --- a/Rendering/OpenGL2/vtkOpenGLPolyDataMapper.cxx +++ b/Rendering/OpenGL2/vtkOpenGLPolyDataMapper.cxx @@ -3408,8 +3408,10 @@ void vtkOpenGLPolyDataMapper::UpdateMaximumPointCellIds(vtkRenderer* ren, vtkAct vtkIdType maxPointId = this->CurrentInput->GetPoints()->GetNumberOfPoints() - 1; if (this->CurrentInput && this->CurrentInput->GetPointData()) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast( + // fvtk: width-agnostic id read (GetRange works on any vtkDataArray); the + // int32-or-int64 passthrough array must size the id-bit allocation correctly. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast( this->CurrentInput->GetPointData()->GetArray(this->PointIdArrayName)) : nullptr; if (pointArrayId) @@ -3444,8 +3446,9 @@ void vtkOpenGLPolyDataMapper::UpdateMaximumPointCellIds(vtkRenderer* ren, vtkAct if (this->CurrentInput && this->CurrentInput->GetCellData()) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast( + // fvtk: width-agnostic id read (GetRange works on any vtkDataArray). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast( this->CurrentInput->GetCellData()->GetArray(this->CellIdArrayName)) : nullptr; if (cellArrayId) @@ -4746,8 +4749,14 @@ void vtkOpenGLPolyDataMapper::ProcessSelectorPixelBuffers( if (currPass == vtkHardwareSelector::POINT_ID_LOW24) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(pd->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id-array read. The point-id passthrough array may be + // stored as int32 (width-relaxed) or int64; fetch as vtkDataArray and read + // values via GetComponent so an int32 container is NOT silently dropped (a + // vtkArrayDownCast on an int32 array returns null -> the id + // remap would be skipped). Point/cell indices are < 2^53 so the double round + // trip is exact, identical to the former vtkIdType GetValue. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(pd->GetArray(this->PointIdArrayName)) : nullptr; // do we need to do anything to the point id data? @@ -4768,7 +4777,7 @@ void vtkOpenGLPolyDataMapper::ProcessSelectorPixelBuffers( inval |= rawplowdata[pos + 1]; inval = inval << 8; inval |= rawplowdata[pos]; - vtkIdType outval = pointArrayId->GetValue(inval); + vtkIdType outval = static_cast(pointArrayId->GetComponent(inval, 0)); plowdata[pos] = outval & 0xff; plowdata[pos + 1] = (outval & 0xff00) >> 8; plowdata[pos + 2] = (outval & 0xff0000) >> 16; @@ -4778,8 +4787,14 @@ void vtkOpenGLPolyDataMapper::ProcessSelectorPixelBuffers( if (currPass == vtkHardwareSelector::POINT_ID_HIGH24) { - vtkIdTypeArray* pointArrayId = this->PointIdArrayName - ? vtkArrayDownCast(pd->GetArray(this->PointIdArrayName)) + // fvtk: width-agnostic id-array read. The point-id passthrough array may be + // stored as int32 (width-relaxed) or int64; fetch as vtkDataArray and read + // values via GetComponent so an int32 container is NOT silently dropped (a + // vtkArrayDownCast on an int32 array returns null -> the id + // remap would be skipped). Point/cell indices are < 2^53 so the double round + // trip is exact, identical to the former vtkIdType GetValue. + vtkDataArray* pointArrayId = this->PointIdArrayName + ? vtkDataArray::SafeDownCast(pd->GetArray(this->PointIdArrayName)) : nullptr; // do we need to do anything to the point id data? @@ -4797,7 +4812,7 @@ void vtkOpenGLPolyDataMapper::ProcessSelectorPixelBuffers( inval |= rawplowdata[pos + 1]; inval = inval << 8; inval |= rawplowdata[pos]; - vtkIdType outval = pointArrayId->GetValue(inval); + vtkIdType outval = static_cast(pointArrayId->GetComponent(inval, 0)); phighdata[pos] = (outval & 0xff000000) >> 24; phighdata[pos + 1] = (outval & 0xff00000000) >> 32; phighdata[pos + 2] = (outval & 0xff0000000000) >> 40; @@ -4846,8 +4861,9 @@ void vtkOpenGLPolyDataMapper::ProcessSelectorPixelBuffers( // process the cellid array? if (currPass == vtkHardwareSelector::CELL_ID_LOW24) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(cd->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id-array read (see point-id note above). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(cd->GetArray(this->CellIdArrayName)) : nullptr; unsigned char* clowdata = sel->GetPixelBuffer(vtkHardwareSelector::CELL_ID_LOW24); @@ -4872,7 +4888,7 @@ void vtkOpenGLPolyDataMapper::ProcessSelectorPixelBuffers( this->CellCellMap->ConvertOpenGLCellIdToVTKCellId(this->PointPicking, inval); if (cellArrayId) { - outval = cellArrayId->GetValue(outval); + outval = static_cast(cellArrayId->GetComponent(outval, 0)); } clowdata[pos] = outval & 0xff; clowdata[pos + 1] = (outval & 0xff00) >> 8; @@ -4883,8 +4899,9 @@ void vtkOpenGLPolyDataMapper::ProcessSelectorPixelBuffers( if (currPass == vtkHardwareSelector::CELL_ID_HIGH24) { - vtkIdTypeArray* cellArrayId = this->CellIdArrayName - ? vtkArrayDownCast(cd->GetArray(this->CellIdArrayName)) + // fvtk: width-agnostic id-array read (see point-id note above). + vtkDataArray* cellArrayId = this->CellIdArrayName + ? vtkDataArray::SafeDownCast(cd->GetArray(this->CellIdArrayName)) : nullptr; unsigned char* chighdata = sel->GetPixelBuffer(vtkHardwareSelector::CELL_ID_HIGH24); @@ -4906,7 +4923,7 @@ void vtkOpenGLPolyDataMapper::ProcessSelectorPixelBuffers( this->CellCellMap->ConvertOpenGLCellIdToVTKCellId(this->PointPicking, inval); if (cellArrayId) { - outval = cellArrayId->GetValue(outval); + outval = static_cast(cellArrayId->GetComponent(outval, 0)); } chighdata[pos] = (outval & 0xff000000) >> 24; chighdata[pos + 1] = (outval & 0xff00000000) >> 32; diff --git a/ci/run-renderexact.sh b/ci/run-renderexact.sh index c83682ba..0acbd34d 100755 --- a/ci/run-renderexact.sh +++ b/ci/run-renderexact.sh @@ -58,3 +58,12 @@ cd "$SRC/tests/renderexact" # Pixel-exact diff + GL-driver-match gate (exits non-zero on any diff / mismatch). # Use the stock venv's python — compare_render.py needs numpy. /tmp/rx-stock/bin/python compare_render.py "$OUT/stock" "$OUT/fvtk" + +# Hardware-selection (picking) parity: prove the GPU selection path remaps picked +# point ids through the width-relaxed int32 vtkOriginalPointIds array to the SAME +# original ids as stock VTK's int64 array. This path is invisible to the pixel +# gate above, so it gets its own selected-id comparison under the same EGL driver. +mkdir -p "$OUT/sel-stock" "$OUT/sel-fvtk" +/tmp/rx-stock/bin/python run_select.py "$OUT/sel-stock" +/tmp/rx-fvtk/bin/python run_select.py "$OUT/sel-fvtk" +/tmp/rx-stock/bin/python compare_select.py "$OUT/sel-stock" "$OUT/sel-fvtk" diff --git a/tests/bitexact/ops.py b/tests/bitexact/ops.py index c5b153c7..f13ee53e 100644 --- a/tests/bitexact/ops.py +++ b/tests/bitexact/ops.py @@ -1195,6 +1195,10 @@ def op_triangle(dtype, size): def op_geometry(dtype, size): g = vtkGeometryFilter() g.SetInputData(make_volume(size, dtype)) + # fvtk: emit vtkOriginalPointIds so the width-relaxed int32 id-array storage + # (vtkGeometryFilter::PassPointIds) is validated against stock (values match, + # int32 vs int64 container normalized by the compare gate). + g.PassThroughPointIdsOn() g.Update() return g.GetOutput() @@ -1664,6 +1668,7 @@ def op_geometry_ugrid(dtype, size): # inline operator[] connectivity-read optimization in vtkCellArray.h). g = vtkGeometryFilter() g.SetInputData(make_hex_ugrid(size, dtype)) + g.PassThroughPointIdsOn() # fvtk: validate int32 vtkOriginalPointIds storage g.Update() return g.GetOutput() @@ -1678,6 +1683,7 @@ def op_geometry_ugrid_mixed(dtype, size): # Run on float32 AND float64 to cover both typed point-copy paths. g = vtkGeometryFilter() g.SetInputData(make_mixed_ugrid(size, dtype)) + g.PassThroughPointIdsOn() # fvtk: validate int32 vtkOriginalPointIds storage g.Update() return g.GetOutput() diff --git a/tests/renderexact/compare_select.py b/tests/renderexact/compare_select.py new file mode 100644 index 00000000..ab07bfde --- /dev/null +++ b/tests/renderexact/compare_select.py @@ -0,0 +1,75 @@ +"""Compare hardware-selection (picking) output between two run_select.py dirs. + +Asserts that the set of selected ORIGINAL point ids is identical between stock +VTK and fvtk for every scene. The ids are integer values (width-relaxed): fvtk's +source vtkOriginalPointIds array is int32, stock's is int64, but the selected +VALUES must match exactly. A mismatch means fvtk's mapper failed to remap picked +ids through the int32 passthrough array (the regression this gate guards). + +Also enforces: + * the selection is non-empty (an empty selection would trivially "match"), + * neither backend errored, + * (informational) the fvtk source array really is int32 -- proving the + discriminating condition holds; a warning, not a failure, so the gate keeps + working if the storage policy ever changes. + +Usage: python compare_select.py (exit 1 on any diff) +""" +from __future__ import annotations + +import json +import os +import sys + +import numpy as np + + +def _load(d): + with open(os.path.join(d, "manifest.json")) as fh: + return json.load(fh) + + +def main(): + stock_dir, fvtk_dir = sys.argv[1], sys.argv[2] + sm = _load(stock_dir) + fm = _load(fvtk_dir) + + scenes = sorted(set(sm["cases"]) | set(fm["cases"])) + n_fail = 0 + for name in scenes: + sc = sm["cases"].get(name, {}) + fc = fm["cases"].get(name, {}) + if "error" in sc or "error" in fc: + print(f"FAIL {name}: backend error stock={sc.get('error')} fvtk={fc.get('error')}") + n_fail += 1 + continue + s_ids = np.load(os.path.join(stock_dir, name + ".npz"))["selected_point_ids"] + f_ids = np.load(os.path.join(fvtk_dir, name + ".npz"))["selected_point_ids"] + if s_ids.size == 0: + print(f"FAIL {name}: stock selection is EMPTY (non-discriminating)") + n_fail += 1 + continue + if s_ids.shape != f_ids.shape or not np.array_equal( + s_ids.astype(np.int64), f_ids.astype(np.int64) + ): + only_s = np.setdiff1d(s_ids, f_ids) + only_f = np.setdiff1d(f_ids, s_ids) + print( + f"FAIL {name}: selected original point ids differ " + f"(stock n={s_ids.size}, fvtk n={f_ids.size}; " + f"only-stock={only_s[:8].tolist()}, only-fvtk={only_f[:8].tolist()})" + ) + n_fail += 1 + continue + # Informational: confirm the discriminating int32 storage is in effect. + fdt = fc.get("orig_pointids_dtype") + note = "" if fdt == "int32" else f" [warn: fvtk dtype={fdt}, expected int32]" + print(f"OK {name}: {s_ids.size} selected ids match " + f"(stock dtype={sc.get('orig_pointids_dtype')}, fvtk dtype={fdt}){note}") + + print(f"[compare_select] {len(scenes) - n_fail}/{len(scenes)} scenes matched") + return 1 if n_fail else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/renderexact/run_select.py b/tests/renderexact/run_select.py new file mode 100644 index 00000000..2b6b69d2 --- /dev/null +++ b/tests/renderexact/run_select.py @@ -0,0 +1,202 @@ +"""Standalone driver: hardware-selection (picking) parity for fvtk vs stock VTK. + +Sister to ``run_render.py``. Where that gate proves pixels match, this one proves +the GPU **hardware-selection** path maps picked pixels back to the SAME original +point ids on both backends -- the path that ``run_render.py`` never exercises. + +Why this gate exists: fvtk stores the ``vtkOriginalPointIds`` passthrough array of +``vtkGeometryFilter`` in an int32 container (width-relaxed) when the ids fit, +where stock VTK uses int64. The render hardware-selector remaps a picked pixel's +raw VTK point id to the value in that array. If a mapper fetched the array with a +width-specific ``vtkArrayDownCast`` (null on int32), the remap +would be silently skipped and picking would return the WRONG ids -- a break that +neither the bit-exact nor the pixel-exact gate can see. The fvtk mappers were +reworked to read the array width-agnostically; this driver is the gate for that. + +Scene design mirrors run_render.py's determinism rules (fixed offscreen EGL +window, fixed camera, deterministic integer-algebra geometry, no MSAA), so both +backends drive an identical GL command stream and the only thing that can differ +is the id-array read. The scene extracts the surface of a hex lattice so the +surviving surface points are a NON-IDENTITY subset of the input points -- i.e. +``vtkOriginalPointIds[surfaceId] != surfaceId`` -- which makes the test +discriminating: a dropped int32 remap yields surface ids, not original ids. + +Usage: python run_select.py [scene_filter] +""" +from __future__ import annotations + +import json +import os +import sys + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import render_ops # noqa: E402 (reuse _renderer/_fixed_camera/_new_window) + +from vtkmodules.vtkCommonCore import vtkPoints # noqa: E402 +from vtkmodules.vtkCommonDataModel import ( # noqa: E402 + vtkUnstructuredGrid, + vtkDataObject, + VTK_HEXAHEDRON, +) +from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter # noqa: E402 +from vtkmodules.vtkRenderingCore import ( # noqa: E402 + vtkActor, + vtkPolyDataMapper, + vtkHardwareSelector, +) +from vtkmodules.util.numpy_support import vtk_to_numpy # noqa: E402 + +# Register the GL backend factory overrides (same as run_render.py). +import vtkmodules.vtkRenderingOpenGL2 # noqa: F401,E402 + + +def _hex_lattice(n): + """An n x n x n point lattice meshed with (n-1)^3 hexahedra. The surface + extraction will drop the interior points, so vtkOriginalPointIds becomes a + non-identity map (the property this gate relies on).""" + grid = vtkUnstructuredGrid() + pts = vtkPoints() + for k in range(n): + for j in range(n): + for i in range(n): + # centered, unit-ish extent; pure integer algebra -> identical + # coordinates on both backends. + pts.InsertNextPoint( + (i - (n - 1) / 2.0) * 0.4, + (j - (n - 1) / 2.0) * 0.4, + (k - (n - 1) / 2.0) * 0.4, + ) + grid.SetPoints(pts) + + def pid(i, j, k): + return (k * n + j) * n + i + + grid.Allocate((n - 1) ** 3) + for k in range(n - 1): + for j in range(n - 1): + for i in range(n - 1): + ids = [ + pid(i, j, k), + pid(i + 1, j, k), + pid(i + 1, j + 1, k), + pid(i, j + 1, k), + pid(i, j, k + 1), + pid(i + 1, j, k + 1), + pid(i + 1, j + 1, k + 1), + pid(i, j + 1, k + 1), + ] + grid.InsertNextCell(VTK_HEXAHEDRON, 8, ids) + return grid + + +def scene_surface_pointpick(): + """Surface of a hex lattice, with the mapper wired to remap picked point ids + through the vtkOriginalPointIds passthrough array.""" + grid = _hex_lattice(6) + geom = vtkGeometryFilter() + geom.SetInputData(grid) + geom.PassThroughPointIdsOn() + geom.PassThroughCellIdsOn() + geom.Update() + surf = geom.GetOutput() + + m = vtkPolyDataMapper() + m.SetInputData(surf) + m.SetPointIdArrayName("vtkOriginalPointIds") + m.SetCellIdArrayName("vtkOriginalCellIds") + a = vtkActor() + a.SetMapper(m) + a.GetProperty().SetPointSize(3.0) + + ren = render_ops._renderer() + ren.AddActor(a) + render_ops._fixed_camera(ren, dist=3.0) + rw = render_ops._new_window(ren) + return ren, rw, surf + + +SCENES = { + "surface_pointpick": scene_surface_pointpick, +} + + +def _select_point_ids(ren, rw): + """Run a full-viewport hardware POINT selection; return the sorted-unique set + of selected ids (these are the REMAPPED original ids because the mapper has a + PointIdArrayName set).""" + w, h = rw.GetSize() + sel = vtkHardwareSelector() + sel.SetRenderer(ren) + sel.SetArea(0, 0, w - 1, h - 1) + sel.SetFieldAssociation(vtkDataObject.FIELD_ASSOCIATION_POINTS) + result = sel.Select() + ids = [] + n_nodes = result.GetNumberOfNodes() + for i in range(n_nodes): + node = result.GetNode(i) + sl = node.GetSelectionList() + if sl is not None and sl.GetNumberOfTuples() > 0: + ids.append(np.asarray(vtk_to_numpy(sl)).astype(np.int64).ravel()) + if ids: + allids = np.concatenate(ids) + else: + allids = np.empty(0, dtype=np.int64) + return np.unique(allids), n_nodes + + +def main(): + out = sys.argv[1] + scene_filter = sys.argv[2] if len(sys.argv) > 2 else None + os.makedirs(out, exist_ok=True) + + from vtkmodules.vtkCommonCore import vtkVersion + import vtkmodules + + manifest = {"cases": {}} + n_ok = n_err = 0 + + for name, fn in SCENES.items(): + if scene_filter and scene_filter not in name: + continue + try: + ren, rw, surf = fn() + rw.Render() + ids, n_nodes = _select_point_ids(ren, rw) + orig = surf.GetPointData().GetArray("vtkOriginalPointIds") + orig_dtype = str(vtk_to_numpy(orig).dtype) if orig is not None else "MISSING" + np.savez(os.path.join(out, name + ".npz"), selected_point_ids=ids) + manifest["cases"][name] = { + "scene": name, + "n_selected": int(ids.size), + "n_nodes": int(n_nodes), + "orig_pointids_dtype": orig_dtype, + "selected_min": int(ids.min()) if ids.size else -1, + "selected_max": int(ids.max()) if ids.size else -1, + } + rw.Finalize() + n_ok += 1 + except Exception as e: # noqa: BLE001 + manifest["cases"][name] = {"scene": name, "error": repr(e)} + n_err += 1 + print(f"ERROR {name}: {e!r}", file=sys.stderr) + + manifest["provenance"] = { + "numpy": np.__version__, + "vtk_version": vtkVersion.GetVTKVersion(), + "vtkmodules_file": getattr(vtkmodules, "__file__", "?"), + } + with open(os.path.join(out, "manifest.json"), "w") as fh: + json.dump(manifest, fh, indent=2, sort_keys=True) + + print( + f"[run_select] backend={manifest['provenance']['vtkmodules_file']} " + f"vtk={manifest['provenance']['vtk_version']}" + ) + print(f"[run_select] wrote {n_ok} scenes, {n_err} errors -> {out}") + return 1 if n_err else 0 + + +if __name__ == "__main__": + sys.exit(main())