Conversation
GCPnts_UniformAbscissa::NbPoints() is not bounded by the requested count. initialize() sizes myParams at theNbPoints + 5 and the walk fills it until it reaches the end parameter or runs out of room, so a caller that sizes its own buffer from the requested count rather than from NbPoints() is handed more points than it asked for. GCPnts_QuasiUniformAbscissa inherits this for every curve that is neither Bezier nor BSpline, since it forwards to GCPnts_UniformAbscissa for those, so the same class returns exactly theNbPoints on a Bezier and possibly more on an ellipse. The cause is a tolerance mismatch. Perform() terminates on abs(aUi - aUU2) <= theEPSILON, where theEPSILON is theC.Resolution(theTol): a parametric tolerance derived from a 3D one using the curve's largest derivative. On an ellipse with major radius 1e6 and minor radius 1e-3 that is about 1e-13, while the local derivative at the end of that curve is 1e-3, so the parametric tolerance actually corresponding to 1e-7 in 3D there is about 1e-4. The walk stops 1.557e-08 short of the end, does not treat that as done, takes one more step and snaps it to the end. The appended point is a duplicate: 1.175e-10 from its neighbour in 3D. On that curve, 22 of the counts from 2 to 60 return one point more than requested. - Perform() also accepts a point that coincides with the end within the caller's 3D tolerance, not only one that is close in parameter. The tolerance is squared once, outside the loop, and compared against SquareDistance() rather than calling Distance() on every iteration; the parameter is named theTol rather than theTol3d, since Perform() also instantiates on Adaptor2d_Curve2d. The end point is evaluated once before the walk, and the distance test is gated behind aUU2 - aUi < aDelta so it runs on the final step rather than on every step. Clamping myNbPoints to theNbPoints was the other option and is worse: the surplus point is the one carrying the exact end parameter, so clamping leaves the distribution stopping short of the curve. Separately, a point count below 2 stores out of bounds. Both classes document theNbPoints >= 2 and previously enforced it with Standard_ConstructionError_Raise_if, which compiles to nothing when No_Exception is defined, as it is for Release builds with BUILD_RELEASE_DISABLE_EXCEPTIONS. GCPnts_QuasiUniformAbscissa::initialize() then allocates NCollection_HArray1<double>(1, theNbPoints), an empty range for such a count, and the next statement is an unconditional myParams->SetValue(1, theU1). SetValue()'s own bounds check is a Raise_if too, so the store lands out of bounds. Reproduced on a 4-pole Bezier and an 8-point BSpline fit with theNbPoints = 0 and with a negative count. - Both classes now leave the object not done for a count below 2 by construction, not by a macro that a build configuration can compile away: the ordinary if replaces Standard_ConstructionError_Raise_if rather than duplicating its condition alongside it, so the result is the same whether or not No_Exception is defined. GCPnts_UniformAbscissa had the same missing precondition without the out-of-bounds store, answering a request for zero points with five, and is guarded the same way. No public API signature changes. Measured across 17 curve types (line, circles of radius 1e-6 to 1e7, two ellipses, hyperbola, parabola, two Beziers, two BSplines, two offsets, a trimmed circle) and counts 2 to 200 for both classes, 6766 configurations: 232 results change from the unpatched baseline and they are exactly the 232 that were returning more points than requested. Every other result is identical parameter for parameter, and on the changed ones the last parameter is still exactly the end. Re-measured after the count-below-2 guard stopped duplicating Standard_ConstructionError_Raise_if: identical 232/6766, and a degenerate count now answers IsDone() == false in every build rather than only when No_Exception is defined.
|
Thank you for the review. Pushed an update addressing all three points. 1. The We build with We also checked whether anything in the tree calls the count-based Given that, we preferred (b) over (a) for a reason external to our own build: an explicit 2. 3. Re-measured the full 6766-configuration equivalence sweep (17 curve types x point counts 2-200, |
Resubmission of #1417, which was closed by an accidental force-push (a bad rebase on our side left
the branch with no commit in common with
IR, and GitHub auto-closes a PR when that happens) ratherthan by any review outcome. Same fix, same branch, now including the three changes @gkv311 requested
on #1417:
SquareDistance()instead ofDistance()in the loop,theTol3drenamed totheTol, andthe duplicated
Standard_ConstructionError_Raise_ifcheck dropped in favor of a single unconditionalif. Full response to that review is in the first comment below.Description
Two defects in the
GCPntsarc-length samplers, both about the requested point count.NbPoints()is not bounded by the requested countGCPnts_UniformAbscissa::initializesizesmyParamsattheNbPoints + 5and the walk fills ituntil it reaches the end parameter or runs out of room, setting
myNbPointsto whatever it reached.A caller that sizes its own buffer from the requested count rather than from
NbPoints()is handedmore points than it asked for.
GCPnts_QuasiUniformAbscissainherits this for every curve that isneither Bezier nor BSpline, since it forwards to
GCPnts_UniformAbscissafor those, so the sameclass returns exactly
theNbPointson a Bezier and possibly more on an ellipse.The cause is a tolerance mismatch rather than an off-by-one.
Performterminates onif (std::abs(aUi - aUU2) <= theEPSILON)where
theEPSILONistheC.Resolution(theTol), a parametric tolerance derived from a 3D one usingthe curve's largest derivative. On an ellipse with major radius 1e6 and minor radius 1e-3 that is
about 1e-13, while the local derivative at the end of that curve is 1e-3, so the parametric tolerance
that actually corresponds to 1e-7 in 3D there is about 1e-4. The test is around nine orders of
magnitude too tight at that end of the curve. The walk stops 1.557e-08 short, does not treat that as
done, takes one more step and snaps it to the end.
The appended point is a duplicate: 1.175e-10 from its neighbour in 3D. On that curve, 22 of the
counts from 2 to 60 return one point more than requested, for both classes.
Performnow also accepts a point that coincides with the end within the caller's 3D tolerance, notonly one close in parameter. The tolerance is squared once outside the loop (
aTol2) and comparedvia
SquareDistance()rather than callingDistance()on every candidate step; the parameter isnamed
theTolrather thantheTol3d, sincePerformalso instantiates onAdaptor2d_Curve2d. Theend point is evaluated once before the walk, and the distance test is gated behind
aUU2 - aUi < aDeltaso it runs on the final step rather than on every step.Clamping
myNbPointstotheNbPointswas the other option and is worse: the surplus point is theone carrying the exact end parameter, so clamping leaves the distribution stopping short of the
curve.
A point count below 2 stores out of bounds
Both classes document
theNbPoints >= 2and enforced it withStandard_ConstructionError_Raise_if,which compiles to nothing when
No_Exceptionis defined, as it is for Release builds withBUILD_RELEASE_DISABLE_EXCEPTIONS(the default).GCPnts_QuasiUniformAbscissa::initializethenallocates
NCollection_HArray1<double>(1, theNbPoints), an empty range for such a count, and thenext statement is an unconditional
myParams->SetValue(1, theU1).SetValue's own bounds check is aRaise_iftoo, so the store lands out of bounds.Reproduced with
theNbPoints = 0and with a negative count, on a 4-pole Bezier and an 8-pointBSpline fit.
GCPnts_UniformAbscissahas the same missing precondition without the out-of-boundsstore: it answers a request for zero points with five.
Both classes now leave the object not done for a count below 2, unconditionally: the ordinary
ifreplaces
Standard_ConstructionError_Raise_ifrather than duplicating its condition alongside it, sothe result no longer depends on whether
No_Exceptionis defined.No public API signature changes.
Type of change
How Has This Been Tested?
A harness fingerprints both classes across 17 curve types (a line, circles of radius 1e-6 to 1e7, a
1e6 x 1e-3 ellipse, a 5 x 2 ellipse, a hyperbola, a parabola, a 2-pole and a 4-pole Bezier, an
8-point and a 40-point BSpline, an offset circle, an offset BSpline, a trimmed circle, and a
half-period slice of the pathological ellipse) and counts 2 to 200, recording the point count, the
first and last parameter and a digest of the whole parameter list, so a stock run and a patched run
can be diffed result by result.
returning more points than requested. Every other result is identical parameter for parameter,
and on the changed ones the last parameter is still exactly the end.
IsDone() == falsefor both classes,in every build configuration, in place of an out-of-bounds store, a five-point answer or a
one-point answer depending on which curve and which class was called.
clang-formatclean on both touched files.Also built into a downstream project's kernel and run against its full suite: clean.
Reproducers and the full write-up:
https://github.com/SecondMouseAU/OCCTSwift/tree/main/Scripts/repro/555-gcpnts-count-contract
Checklist:
clang-formatclean on every touched file)