1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file.
5 
6 #include "lib/jxl/splines.h"
7 
8 #include <algorithm>
9 
10 #include "lib/jxl/ans_params.h"
11 #include "lib/jxl/base/status.h"
12 #include "lib/jxl/chroma_from_luma.h"
13 #include "lib/jxl/common.h"
14 #include "lib/jxl/dct_scales.h"
15 #include "lib/jxl/entropy_coder.h"
16 #include "lib/jxl/opsin_params.h"
17 
18 #undef HWY_TARGET_INCLUDE
19 #define HWY_TARGET_INCLUDE "lib/jxl/splines.cc"
20 #include <hwy/foreach_target.h>
21 #include <hwy/highway.h>
22 
23 #include "lib/jxl/fast_math-inl.h"
24 HWY_BEFORE_NAMESPACE();
25 namespace jxl {
26 namespace HWY_NAMESPACE {
27 namespace {
28 
29 // Given a set of DCT coefficients, this returns the result of performing cosine
30 // interpolation on the original samples.
ContinuousIDCT(const float dct[32],float t)31 float ContinuousIDCT(const float dct[32], float t) {
32   // We compute here the DCT-3 of the `dct` vector, rescaled by a factor of
33   // sqrt(32). This is such that an input vector vector {x, 0, ..., 0} produces
34   // a constant result of x. dct[0] was scaled in Dequantize() to allow uniform
35   // treatment of all the coefficients.
36   constexpr float kMultipliers[32] = {
37       kPi / 32 * 0,  kPi / 32 * 1,  kPi / 32 * 2,  kPi / 32 * 3,  kPi / 32 * 4,
38       kPi / 32 * 5,  kPi / 32 * 6,  kPi / 32 * 7,  kPi / 32 * 8,  kPi / 32 * 9,
39       kPi / 32 * 10, kPi / 32 * 11, kPi / 32 * 12, kPi / 32 * 13, kPi / 32 * 14,
40       kPi / 32 * 15, kPi / 32 * 16, kPi / 32 * 17, kPi / 32 * 18, kPi / 32 * 19,
41       kPi / 32 * 20, kPi / 32 * 21, kPi / 32 * 22, kPi / 32 * 23, kPi / 32 * 24,
42       kPi / 32 * 25, kPi / 32 * 26, kPi / 32 * 27, kPi / 32 * 28, kPi / 32 * 29,
43       kPi / 32 * 30, kPi / 32 * 31,
44   };
45   HWY_CAPPED(float, 32) df;
46   auto result = Zero(df);
47   const auto tandhalf = Set(df, t + 0.5f);
48   for (int i = 0; i < 32; i += Lanes(df)) {
49     auto cos_arg = LoadU(df, kMultipliers + i) * tandhalf;
50     auto cos = FastCosf(df, cos_arg);
51     auto local_res = LoadU(df, dct + i) * cos;
52     result = MulAdd(Set(df, square_root<2>::value), local_res, result);
53   }
54   return GetLane(SumOfLanes(result));
55 }
56 
57 // Splats a single Gaussian on the image.
DrawGaussian(Image3F * const opsin,const Rect & opsin_rect,const Rect & image_rect,const Spline::Point & center,const float intensity,const float color[3],const float sigma,std::vector<int32_t> & xs,std::vector<int32_t> & ys,std::vector<float> & local_intensity_storage)58 void DrawGaussian(Image3F* const opsin, const Rect& opsin_rect,
59                   const Rect& image_rect, const Spline::Point& center,
60                   const float intensity, const float color[3],
61                   const float sigma, std::vector<int32_t>& xs,
62                   std::vector<int32_t>& ys,
63                   std::vector<float>& local_intensity_storage) {
64   constexpr float kDistanceMultiplier = 4.605170185988091f;  // -2 * log(0.1)
65   // Distance beyond which exp(-d^2 / (2 * sigma^2)) drops below 0.1.
66   const float maximum_distance = sigma * sigma * kDistanceMultiplier;
67   const auto xbegin_s =
68       std::max<ssize_t>(image_rect.x0(), center.x - maximum_distance + .5f);
69   const auto xend_s =
70       std::min<ssize_t>(center.x + maximum_distance + .5f,
71                         image_rect.x0() + image_rect.xsize() - 1);
72   const auto ybegin_s =
73       std::max<ssize_t>(image_rect.y0(), center.y - maximum_distance + .5f);
74   const auto yend_s =
75       std::min<ssize_t>(center.y + maximum_distance + .5f,
76                         image_rect.y0() + image_rect.ysize() - 1);
77   if ((xend_s) <= 0 || (xend_s < xbegin_s)) return;
78   const size_t xbegin = xbegin_s;
79   const size_t xend = xend_s;
80   if ((yend_s <= 0) || (yend_s < ybegin_s)) return;
81   const size_t ybegin = ybegin_s;
82   const size_t yend = yend_s;
83   const size_t opsin_stride = opsin->PixelsPerRow();
84   float* JXL_RESTRICT rows[3] = {
85       opsin_rect.PlaneRow(opsin, 0, ybegin - image_rect.y0()),
86       opsin_rect.PlaneRow(opsin, 1, ybegin - image_rect.y0()),
87       opsin_rect.PlaneRow(opsin, 2, ybegin - image_rect.y0()),
88   };
89   const size_t nx = xend + 1 - xbegin;
90   const size_t ny = yend + 1 - ybegin;
91   HWY_FULL(float) df;
92   if (xs.size() < nx * ny) {
93     size_t sz = DivCeil(nx * ny, Lanes(df)) * Lanes(df);
94     xs.resize(sz);
95     ys.resize(sz);
96     local_intensity_storage.resize(sz);
97   }
98   for (size_t y = ybegin; y <= yend; ++y) {
99     for (size_t x = xbegin; x <= xend; ++x) {
100       xs[(y - ybegin) * nx + (x - xbegin)] = x;
101       ys[(y - ybegin) * nx + (x - xbegin)] = y;
102     }
103   }
104   Rebind<int32_t, decltype(df)> di;
105   const auto inv_sigma = Set(df, 1.0f / sigma);
106   const auto half = Set(df, 0.5f);
107   const auto one_over_2s2 = Set(df, 0.353553391f);
108   const auto sigma_over_4_times_intensity = Set(df, .25f * sigma * intensity);
109   for (size_t i = 0; i < nx * ny; i += Lanes(df)) {
110     const auto x = ConvertTo(df, LoadU(di, &xs[i]));
111     const auto y = ConvertTo(df, LoadU(di, &ys[i]));
112     const auto dx = x - Set(df, center.x);
113     const auto dy = y - Set(df, center.y);
114     const auto sqd = MulAdd(dx, dx, dy * dy);
115     const auto distance = Sqrt(sqd);
116     const auto one_dimensional_factor =
117         FastErff(df, MulAdd(distance, half, one_over_2s2) * inv_sigma) -
118         FastErff(df, MulSub(distance, half, one_over_2s2) * inv_sigma);
119     const auto local_intensity = sigma_over_4_times_intensity *
120                                  one_dimensional_factor *
121                                  one_dimensional_factor;
122     StoreU(local_intensity, df, &local_intensity_storage[i]);
123   }
124   ssize_t off = -static_cast<ssize_t>(image_rect.x0());
125   for (size_t y = ybegin; y <= yend; ++y) {
126     HWY_CAPPED(float, 1) df;
127     for (size_t x = xbegin; x <= xend; ++x) {
128       const auto local_intensity = Load(
129           df, local_intensity_storage.data() + (y - ybegin) * nx + x - xbegin);
130       for (size_t c = 0; c < 3; ++c) {
131         const auto cm = Set(df, color[c]);
132         const auto in = LoadU(df, rows[c] + x + off);
133         StoreU(MulAdd(cm, local_intensity, in), df, rows[c] + x + off);
134       }
135     }
136     off += opsin_stride;
137   }
138 }
139 
DrawFromPoints(Image3F * const opsin,const Rect & opsin_rect,const Rect & image_rect,const Spline & spline,bool add,const std::vector<std::pair<Spline::Point,float>> & points_to_draw,float arc_length)140 void DrawFromPoints(
141     Image3F* const opsin, const Rect& opsin_rect, const Rect& image_rect,
142     const Spline& spline, bool add,
143     const std::vector<std::pair<Spline::Point, float>>& points_to_draw,
144     float arc_length) {
145   float inv_arc_length = 1.0f / arc_length;
146   int k = 0;
147   std::vector<int32_t> xs, ys;
148   std::vector<float> local_intensity_storage;
149   for (const auto& point_to_draw : points_to_draw) {
150     const Spline::Point& point = point_to_draw.first;
151     const float multiplier = add ? point_to_draw.second : -point_to_draw.second;
152     const float progress_along_arc =
153         std::min(1.f, (k * kDesiredRenderingDistance) * inv_arc_length);
154     ++k;
155     float color[3];
156     for (size_t c = 0; c < 3; ++c) {
157       color[c] =
158           ContinuousIDCT(spline.color_dct[c], (32 - 1) * progress_along_arc);
159     }
160     const float sigma =
161         ContinuousIDCT(spline.sigma_dct, (32 - 1) * progress_along_arc);
162     DrawGaussian(opsin, opsin_rect, image_rect, point, multiplier, color, sigma,
163                  xs, ys, local_intensity_storage);
164   }
165 }
166 }  // namespace
167 // NOLINTNEXTLINE(google-readability-namespace-comments)
168 }  // namespace HWY_NAMESPACE
169 }  // namespace jxl
170 HWY_AFTER_NAMESPACE();
171 
172 #if HWY_ONCE
173 namespace jxl {
174 HWY_EXPORT(DrawFromPoints);
175 
176 namespace {
177 
178 // Maximum number of spline control points per frame is
179 //   std::min(kMaxNumControlPoints, xsize * ysize / 2)
180 constexpr size_t kMaxNumControlPoints = 1u << 20u;
181 constexpr size_t kMaxNumControlPointsPerPixelRatio = 2;
182 
183 // X, Y, B, sigma.
ColorQuantizationWeight(const int32_t adjustment,const int channel,const int i)184 float ColorQuantizationWeight(const int32_t adjustment, const int channel,
185                               const int i) {
186   const float multiplier = adjustment >= 0 ? 1.f + .125f * adjustment
187                                            : 1.f / (1.f + .125f * -adjustment);
188 
189   static constexpr float kChannelWeight[] = {0.0042f, 0.075f, 0.07f, .3333f};
190 
191   return multiplier / kChannelWeight[channel];
192 }
193 
DecodeAllStartingPoints(std::vector<Spline::Point> * const points,BitReader * const br,ANSSymbolReader * reader,const std::vector<uint8_t> & context_map,size_t num_splines)194 Status DecodeAllStartingPoints(std::vector<Spline::Point>* const points,
195                                BitReader* const br, ANSSymbolReader* reader,
196                                const std::vector<uint8_t>& context_map,
197                                size_t num_splines) {
198   points->clear();
199   points->reserve(num_splines);
200   int64_t last_x = 0;
201   int64_t last_y = 0;
202   for (size_t i = 0; i < num_splines; i++) {
203     int64_t x =
204         reader->ReadHybridUint(kStartingPositionContext, br, context_map);
205     int64_t y =
206         reader->ReadHybridUint(kStartingPositionContext, br, context_map);
207     if (i != 0) {
208       x = UnpackSigned(x) + last_x;
209       y = UnpackSigned(y) + last_y;
210     }
211     points->emplace_back(static_cast<float>(x), static_cast<float>(y));
212     last_x = x;
213     last_y = y;
214   }
215   return true;
216 }
217 
218 struct Vector {
219   float x, y;
operator -jxl::__anon1da3897f0211::Vector220   Vector operator-() const { return {-x, -y}; }
operator +jxl::__anon1da3897f0211::Vector221   Vector operator+(const Vector& other) const {
222     return {x + other.x, y + other.y};
223   }
SquaredNormjxl::__anon1da3897f0211::Vector224   float SquaredNorm() const { return x * x + y * y; }
225 };
operator *(const float k,const Vector & vec)226 Vector operator*(const float k, const Vector& vec) {
227   return {k * vec.x, k * vec.y};
228 }
229 
operator +(const Spline::Point & p,const Vector & vec)230 Spline::Point operator+(const Spline::Point& p, const Vector& vec) {
231   return {p.x + vec.x, p.y + vec.y};
232 }
operator -(const Spline::Point & p,const Vector & vec)233 Spline::Point operator-(const Spline::Point& p, const Vector& vec) {
234   return p + -vec;
235 }
operator -(const Spline::Point & a,const Spline::Point & b)236 Vector operator-(const Spline::Point& a, const Spline::Point& b) {
237   return {a.x - b.x, a.y - b.y};
238 }
239 
DrawCentripetalCatmullRomSpline(std::vector<Spline::Point> points)240 std::vector<Spline::Point> DrawCentripetalCatmullRomSpline(
241     std::vector<Spline::Point> points) {
242   if (points.size() <= 1) return points;
243   // Number of points to compute between each control point.
244   static constexpr int kNumPoints = 16;
245   std::vector<Spline::Point> result;
246   result.reserve((points.size() - 1) * kNumPoints + 1);
247   points.insert(points.begin(), points[0] + (points[0] - points[1]));
248   points.push_back(points[points.size() - 1] +
249                    (points[points.size() - 1] - points[points.size() - 2]));
250   // points has at least 4 elements at this point.
251   for (size_t start = 0; start < points.size() - 3; ++start) {
252     // 4 of them are used, and we draw from p[1] to p[2].
253     const Spline::Point* const p = &points[start];
254     result.push_back(p[1]);
255     float t[4] = {0};
256     for (int k = 1; k < 4; ++k) {
257       t[k] = std::sqrt(hypotf(p[k].x - p[k - 1].x, p[k].y - p[k - 1].y)) +
258              t[k - 1];
259     }
260     for (int i = 1; i < kNumPoints; ++i) {
261       const float tt =
262           t[1] + (static_cast<float>(i) / kNumPoints) * (t[2] - t[1]);
263       Spline::Point a[3];
264       for (int k = 0; k < 3; ++k) {
265         a[k] = p[k] + ((tt - t[k]) / (t[k + 1] - t[k])) * (p[k + 1] - p[k]);
266       }
267       Spline::Point b[2];
268       for (int k = 0; k < 2; ++k) {
269         b[k] = a[k] + ((tt - t[k]) / (t[k + 2] - t[k])) * (a[k + 1] - a[k]);
270       }
271       result.push_back(b[0] + ((tt - t[1]) / (t[2] - t[1])) * (b[1] - b[0]));
272     }
273   }
274   result.push_back(points[points.size() - 2]);
275   return result;
276 }
277 
278 // Move along the line segments defined by `points`, `kDesiredRenderingDistance`
279 // pixels at a time, and call `functor` with each point and the actual distance
280 // to the previous point (which will always be kDesiredRenderingDistance except
281 // possibly for the very last point).
282 template <typename Points, typename Functor>
ForEachEquallySpacedPoint(const Points & points,const Functor & functor)283 void ForEachEquallySpacedPoint(const Points& points, const Functor& functor) {
284   JXL_ASSERT(!points.empty());
285   Spline::Point current = points.front();
286   functor(current, kDesiredRenderingDistance);
287   auto next = points.begin();
288   while (next != points.end()) {
289     const Spline::Point* previous = &current;
290     float arclength_from_previous = 0.f;
291     for (;;) {
292       if (next == points.end()) {
293         functor(*previous, arclength_from_previous);
294         return;
295       }
296       const float arclength_to_next =
297           std::sqrt((*next - *previous).SquaredNorm());
298       if (arclength_from_previous + arclength_to_next >=
299           kDesiredRenderingDistance) {
300         current =
301             *previous + ((kDesiredRenderingDistance - arclength_from_previous) /
302                          arclength_to_next) *
303                             (*next - *previous);
304         functor(current, kDesiredRenderingDistance);
305         break;
306       }
307       arclength_from_previous += arclength_to_next;
308       previous = &*next;
309       ++next;
310     }
311   }
312 }
313 
314 }  // namespace
315 
QuantizedSpline(const Spline & original,const int32_t quantization_adjustment,float ytox,float ytob)316 QuantizedSpline::QuantizedSpline(const Spline& original,
317                                  const int32_t quantization_adjustment,
318                                  float ytox, float ytob) {
319   JXL_ASSERT(!original.control_points.empty());
320   control_points_.reserve(original.control_points.size() - 1);
321   const Spline::Point& starting_point = original.control_points.front();
322   int previous_x = static_cast<int>(roundf(starting_point.x)),
323       previous_y = static_cast<int>(roundf(starting_point.y));
324   int previous_delta_x = 0, previous_delta_y = 0;
325   for (auto it = original.control_points.begin() + 1;
326        it != original.control_points.end(); ++it) {
327     const int new_x = static_cast<int>(roundf(it->x));
328     const int new_y = static_cast<int>(roundf(it->y));
329     const int new_delta_x = new_x - previous_x;
330     const int new_delta_y = new_y - previous_y;
331     control_points_.emplace_back(new_delta_x - previous_delta_x,
332                                  new_delta_y - previous_delta_y);
333     previous_delta_x = new_delta_x;
334     previous_delta_y = new_delta_y;
335     previous_x = new_x;
336     previous_y = new_y;
337   }
338 
339   for (int c = 0; c < 3; ++c) {
340     float factor = c == 0 ? ytox : c == 1 ? 0 : ytob;
341     for (int i = 0; i < 32; ++i) {
342       const float coefficient =
343           original.color_dct[c][i] -
344           factor * color_dct_[1][i] /
345               ColorQuantizationWeight(quantization_adjustment, 1, i);
346       color_dct_[c][i] = static_cast<int>(
347           roundf(coefficient *
348                  ColorQuantizationWeight(quantization_adjustment, c, i)));
349     }
350   }
351   for (int i = 0; i < 32; ++i) {
352     sigma_dct_[i] = static_cast<int>(
353         roundf(original.sigma_dct[i] *
354                ColorQuantizationWeight(quantization_adjustment, 3, i)));
355   }
356 }
357 
Dequantize(const Spline::Point & starting_point,const int32_t quantization_adjustment,float ytox,float ytob) const358 Spline QuantizedSpline::Dequantize(const Spline::Point& starting_point,
359                                    const int32_t quantization_adjustment,
360                                    float ytox, float ytob) const {
361   Spline result;
362 
363   result.control_points.reserve(control_points_.size() + 1);
364   int current_x = static_cast<int>(roundf(starting_point.x)),
365       current_y = static_cast<int>(roundf(starting_point.y));
366   result.control_points.push_back(Spline::Point{static_cast<float>(current_x),
367                                                 static_cast<float>(current_y)});
368   int current_delta_x = 0, current_delta_y = 0;
369   for (const auto& point : control_points_) {
370     current_delta_x += point.first;
371     current_delta_y += point.second;
372     current_x += current_delta_x;
373     current_y += current_delta_y;
374     result.control_points.push_back(Spline::Point{
375         static_cast<float>(current_x), static_cast<float>(current_y)});
376   }
377 
378   for (int c = 0; c < 3; ++c) {
379     for (int i = 0; i < 32; ++i) {
380       result.color_dct[c][i] =
381           color_dct_[c][i] * (i == 0 ? 1.0f / square_root<2>::value : 1.0f) /
382           ColorQuantizationWeight(quantization_adjustment, c, i);
383     }
384   }
385   for (int i = 0; i < 32; ++i) {
386     result.color_dct[0][i] += ytox * result.color_dct[1][i];
387     result.color_dct[2][i] += ytob * result.color_dct[1][i];
388   }
389   for (int i = 0; i < 32; ++i) {
390     result.sigma_dct[i] =
391         sigma_dct_[i] * (i == 0 ? 1.0f / square_root<2>::value : 1.0f) /
392         ColorQuantizationWeight(quantization_adjustment, 3, i);
393   }
394 
395   return result;
396 }
397 
Decode(const std::vector<uint8_t> & context_map,ANSSymbolReader * const decoder,BitReader * const br,size_t max_control_points,size_t * total_num_control_points)398 Status QuantizedSpline::Decode(const std::vector<uint8_t>& context_map,
399                                ANSSymbolReader* const decoder,
400                                BitReader* const br, size_t max_control_points,
401                                size_t* total_num_control_points) {
402   const size_t num_control_points =
403       decoder->ReadHybridUint(kNumControlPointsContext, br, context_map);
404   *total_num_control_points += num_control_points;
405   if (*total_num_control_points > max_control_points) {
406     return JXL_FAILURE("Too many control points: %zu",
407                        *total_num_control_points);
408   }
409   control_points_.resize(num_control_points);
410   for (std::pair<int64_t, int64_t>& control_point : control_points_) {
411     control_point.first = UnpackSigned(
412         decoder->ReadHybridUint(kControlPointsContext, br, context_map));
413     control_point.second = UnpackSigned(
414         decoder->ReadHybridUint(kControlPointsContext, br, context_map));
415   }
416 
417   const auto decode_dct = [decoder, br, &context_map](int dct[32]) -> Status {
418     for (int i = 0; i < 32; ++i) {
419       dct[i] =
420           UnpackSigned(decoder->ReadHybridUint(kDCTContext, br, context_map));
421     }
422     return true;
423   };
424   for (int c = 0; c < 3; ++c) {
425     JXL_RETURN_IF_ERROR(decode_dct(color_dct_[c]));
426   }
427   JXL_RETURN_IF_ERROR(decode_dct(sigma_dct_));
428   return true;
429 }
430 
Decode(jxl::BitReader * br,size_t num_pixels)431 Status Splines::Decode(jxl::BitReader* br, size_t num_pixels) {
432   std::vector<uint8_t> context_map;
433   ANSCode code;
434   JXL_RETURN_IF_ERROR(
435       DecodeHistograms(br, kNumSplineContexts, &code, &context_map));
436   ANSSymbolReader decoder(&code, br);
437   const size_t num_splines =
438       1 + decoder.ReadHybridUint(kNumSplinesContext, br, context_map);
439   size_t max_control_points = std::min(
440       kMaxNumControlPoints, num_pixels / kMaxNumControlPointsPerPixelRatio);
441   if (num_splines > max_control_points) {
442     return JXL_FAILURE("Too many splines: %zu", num_splines);
443   }
444   JXL_RETURN_IF_ERROR(DecodeAllStartingPoints(&starting_points_, br, &decoder,
445                                               context_map, num_splines));
446 
447   quantization_adjustment_ = UnpackSigned(
448       decoder.ReadHybridUint(kQuantizationAdjustmentContext, br, context_map));
449 
450   splines_.clear();
451   splines_.reserve(num_splines);
452   size_t num_control_points = num_splines;
453   for (size_t i = 0; i < num_splines; ++i) {
454     QuantizedSpline spline;
455     JXL_RETURN_IF_ERROR(spline.Decode(context_map, &decoder, br,
456                                       max_control_points, &num_control_points));
457     splines_.push_back(std::move(spline));
458   }
459 
460   JXL_RETURN_IF_ERROR(decoder.CheckANSFinalState());
461 
462   if (!HasAny()) {
463     return JXL_FAILURE("Decoded splines but got none");
464   }
465 
466   return true;
467 }
468 
AddTo(Image3F * const opsin,const Rect & opsin_rect,const Rect & image_rect,const ColorCorrelationMap & cmap) const469 Status Splines::AddTo(Image3F* const opsin, const Rect& opsin_rect,
470                       const Rect& image_rect,
471                       const ColorCorrelationMap& cmap) const {
472   return Apply</*add=*/true>(opsin, opsin_rect, image_rect, cmap);
473 }
474 
SubtractFrom(Image3F * const opsin,const ColorCorrelationMap & cmap) const475 Status Splines::SubtractFrom(Image3F* const opsin,
476                              const ColorCorrelationMap& cmap) const {
477   return Apply</*add=*/false>(opsin, Rect(*opsin), Rect(*opsin), cmap);
478 }
479 
480 template <bool add>
Apply(Image3F * const opsin,const Rect & opsin_rect,const Rect & image_rect,const ColorCorrelationMap & cmap) const481 Status Splines::Apply(Image3F* const opsin, const Rect& opsin_rect,
482                       const Rect& image_rect,
483                       const ColorCorrelationMap& cmap) const {
484   for (size_t i = 0; i < splines_.size(); ++i) {
485     const Spline spline =
486         splines_[i].Dequantize(starting_points_[i], quantization_adjustment_,
487                                cmap.YtoXRatio(0), cmap.YtoBRatio(0));
488     if (std::adjacent_find(spline.control_points.begin(),
489                            spline.control_points.end()) !=
490         spline.control_points.end()) {
491       return JXL_FAILURE("identical successive control points in spline %zu",
492                          i);
493     }
494     std::vector<std::pair<Spline::Point, float>> points_to_draw;
495     ForEachEquallySpacedPoint(
496         DrawCentripetalCatmullRomSpline(spline.control_points),
497         [&](const Spline::Point& point, const float multiplier) {
498           points_to_draw.emplace_back(point, multiplier);
499         });
500     const float arc_length =
501         (points_to_draw.size() - 2) * kDesiredRenderingDistance +
502         points_to_draw.back().second;
503     if (arc_length <= 0.f) {
504       // This spline wouldn't have any effect.
505       continue;
506     }
507     HWY_DYNAMIC_DISPATCH(DrawFromPoints)
508     (opsin, opsin_rect, image_rect, spline, add, points_to_draw, arc_length);
509   }
510   return true;
511 }
512 
513 }  // namespace jxl
514 #endif  // HWY_ONCE
515