/src/libjxl/lib/jxl/splines.cc
Line | Count | Source |
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 <jxl/memory_manager.h> |
9 | | |
10 | | #include <algorithm> |
11 | | #include <cinttypes> // PRIu64 |
12 | | #include <cmath> |
13 | | #include <cstddef> |
14 | | #include <cstdint> |
15 | | #include <cstring> |
16 | | #include <limits> |
17 | | #include <utility> |
18 | | #include <vector> |
19 | | |
20 | | #include "lib/jxl/base/bits.h" |
21 | | #include "lib/jxl/base/common.h" |
22 | | #include "lib/jxl/base/compiler_specific.h" |
23 | | #include "lib/jxl/base/printf_macros.h" |
24 | | #include "lib/jxl/base/rect.h" |
25 | | #include "lib/jxl/base/status.h" |
26 | | #include "lib/jxl/chroma_from_luma.h" |
27 | | #include "lib/jxl/common.h" // JXL_HIGH_PRECISION |
28 | | #include "lib/jxl/dct_scales.h" |
29 | | #include "lib/jxl/dec_ans.h" |
30 | | #include "lib/jxl/dec_bit_reader.h" |
31 | | #include "lib/jxl/image.h" |
32 | | #include "lib/jxl/memory_manager_internal.h" |
33 | | #include "lib/jxl/pack_signed.h" |
34 | | |
35 | | #undef HWY_TARGET_INCLUDE |
36 | | #define HWY_TARGET_INCLUDE "lib/jxl/splines.cc" |
37 | | #include <hwy/foreach_target.h> |
38 | | #include <hwy/highway.h> |
39 | | |
40 | | #include "lib/jxl/base/fast_math-inl.h" |
41 | | HWY_BEFORE_NAMESPACE(); |
42 | | namespace jxl { |
43 | | namespace HWY_NAMESPACE { |
44 | | namespace { |
45 | | |
46 | | // These templates are not found via ADL. |
47 | | using hwy::HWY_NAMESPACE::Mul; |
48 | | using hwy::HWY_NAMESPACE::MulAdd; |
49 | | using hwy::HWY_NAMESPACE::MulSub; |
50 | | using hwy::HWY_NAMESPACE::Sqrt; |
51 | | using hwy::HWY_NAMESPACE::Sub; |
52 | | |
53 | | // Given a set of DCT coefficients, this returns the result of performing cosine |
54 | | // interpolation on the original samples. |
55 | 6.66k | float ContinuousIDCT(const Dct32& dct, const float t) { |
56 | | // We compute here the DCT-3 of the `dct` vector, rescaled by a factor of |
57 | | // sqrt(32). This is such that an input vector vector {x, 0, ..., 0} produces |
58 | | // a constant result of x. dct[0] was scaled in Dequantize() to allow uniform |
59 | | // treatment of all the coefficients. |
60 | 6.66k | constexpr float kMultipliers[32] = { |
61 | 6.66k | kPi / 32 * 0, kPi / 32 * 1, kPi / 32 * 2, kPi / 32 * 3, kPi / 32 * 4, |
62 | 6.66k | kPi / 32 * 5, kPi / 32 * 6, kPi / 32 * 7, kPi / 32 * 8, kPi / 32 * 9, |
63 | 6.66k | kPi / 32 * 10, kPi / 32 * 11, kPi / 32 * 12, kPi / 32 * 13, kPi / 32 * 14, |
64 | 6.66k | kPi / 32 * 15, kPi / 32 * 16, kPi / 32 * 17, kPi / 32 * 18, kPi / 32 * 19, |
65 | 6.66k | kPi / 32 * 20, kPi / 32 * 21, kPi / 32 * 22, kPi / 32 * 23, kPi / 32 * 24, |
66 | 6.66k | kPi / 32 * 25, kPi / 32 * 26, kPi / 32 * 27, kPi / 32 * 28, kPi / 32 * 29, |
67 | 6.66k | kPi / 32 * 30, kPi / 32 * 31, |
68 | 6.66k | }; |
69 | 6.66k | HWY_CAPPED(float, 32) df; |
70 | 6.66k | auto result = Zero(df); |
71 | 6.66k | const auto tandhalf = Set(df, t + 0.5f); |
72 | 219k | for (int i = 0; i < 32; i += Lanes(df)) { |
73 | 213k | auto cos_arg = Mul(LoadU(df, kMultipliers + i), tandhalf); |
74 | 213k | auto cos = FastCosf(df, cos_arg); |
75 | 213k | auto local_res = Mul(LoadU(df, dct.data() + i), cos); |
76 | 213k | result = MulAdd(Set(df, kSqrt2), local_res, result); |
77 | 213k | } |
78 | 6.66k | return GetLane(SumOfLanes(df, result)); |
79 | 6.66k | } |
80 | | |
81 | | template <typename DF> |
82 | | void DrawSegment(DF df, const SplineSegment& segment, const bool add, |
83 | | const size_t y, const size_t x, const size_t x0, |
84 | 69.8k | float* JXL_RESTRICT rows[3]) { |
85 | 69.8k | Rebind<int32_t, DF> di; |
86 | 69.8k | const auto inv_sigma = Set(df, segment.inv_sigma); |
87 | 69.8k | const auto half = Set(df, 0.5f); |
88 | 69.8k | const auto one_over_2s2 = Set(df, 0.353553391f); |
89 | 69.8k | const auto sigma_over_4_times_intensity = |
90 | 69.8k | Set(df, segment.sigma_over_4_times_intensity); |
91 | 69.8k | const auto dx = |
92 | 69.8k | Sub(ConvertTo(df, Iota(di, x + x0)), Set(df, segment.center_x)); |
93 | 69.8k | const auto dy = Set(df, y - segment.center_y); |
94 | 69.8k | const auto sqd = MulAdd(dx, dx, Mul(dy, dy)); |
95 | 69.8k | const auto distance = Sqrt(sqd); |
96 | 69.8k | const auto one_dimensional_factor = |
97 | 69.8k | Sub(FastErff(df, Mul(MulAdd(distance, half, one_over_2s2), inv_sigma)), |
98 | 69.8k | FastErff(df, Mul(MulSub(distance, half, one_over_2s2), inv_sigma))); |
99 | 69.8k | auto local_intensity = |
100 | 69.8k | Mul(sigma_over_4_times_intensity, |
101 | 69.8k | Mul(one_dimensional_factor, one_dimensional_factor)); |
102 | 279k | for (size_t c = 0; c < 3; ++c) { |
103 | 209k | const auto cm = Set(df, add ? segment.color[c] : -segment.color[c]); |
104 | 209k | const auto in = LoadU(df, rows[c] + x); |
105 | 209k | StoreU(MulAdd(cm, local_intensity, in), df, rows[c] + x); |
106 | 209k | } |
107 | 69.8k | } |
108 | | |
109 | | void DrawSegment(const SplineSegment& segment, const bool add, const size_t y, |
110 | | const size_t x0, const size_t x1, |
111 | 6.59k | float* JXL_RESTRICT rows[3]) { |
112 | 6.59k | ptrdiff_t start = std::llround(segment.center_x - segment.maximum_distance); |
113 | 6.59k | ptrdiff_t end = std::llround(segment.center_x + segment.maximum_distance); |
114 | 6.59k | if (end < static_cast<ptrdiff_t>(x0) || start >= static_cast<ptrdiff_t>(x1)) { |
115 | 0 | return; // span does not intersect scan |
116 | 0 | } |
117 | 6.59k | size_t span_x0 = std::max<ptrdiff_t>(x0, start) - x0; |
118 | 6.59k | size_t span_x1 = std::min<ptrdiff_t>(x1, end + 1) - x0; // exclusive |
119 | 6.59k | HWY_FULL(float) df; |
120 | 6.59k | size_t x = span_x0; |
121 | 76.4k | for (; x + Lanes(df) <= span_x1; x += Lanes(df)) { |
122 | 69.8k | DrawSegment(df, segment, add, y, x, x0, rows); |
123 | 69.8k | } |
124 | 6.59k | for (; x < span_x1; ++x) { |
125 | 0 | DrawSegment(HWY_CAPPED(float, 1)(), segment, add, y, x, x0, rows); |
126 | 0 | } |
127 | 6.59k | } |
128 | | |
129 | | void ComputeSegments(size_t image_ysize, const Spline::Point& center, |
130 | | const float intensity, const float color[3], |
131 | | const float sigma, std::vector<SplineSegment>& segments, |
132 | 1.66k | std::vector<SplineSegmentSpan>& segment_spans) { |
133 | | // Sanity check sigma, inverse sigma and intensity |
134 | 1.66k | if (!(std::isfinite(sigma) && sigma != 0.0f && std::isfinite(1.0f / sigma) && |
135 | 1.62k | std::isfinite(intensity))) { |
136 | 42 | return; |
137 | 42 | } |
138 | 1.62k | #if JXL_HIGH_PRECISION |
139 | 1.62k | constexpr float kDistanceExp = 5; |
140 | | #else |
141 | | // About 30% faster. |
142 | | constexpr float kDistanceExp = 3; |
143 | | #endif |
144 | | // We cap from below colors to at least 0.01. |
145 | 1.62k | float max_color = 0.01f; |
146 | 6.49k | for (size_t c = 0; c < 3; c++) { |
147 | 4.86k | max_color = std::max(max_color, std::abs(color[c] * intensity)); |
148 | 4.86k | } |
149 | | // Distance beyond which max_color*intensity*exp(-d^2 / (2 * sigma^2)) drops |
150 | | // below 10^-kDistanceExp. |
151 | 1.62k | const float maximum_distance = |
152 | 1.62k | std::sqrt(-2.0f * sigma * sigma * |
153 | 1.62k | (std::log(0.1f) * kDistanceExp - std::log(max_color))); |
154 | | |
155 | 1.62k | ptrdiff_t y0 = std::llround(center.y - maximum_distance); |
156 | 1.62k | y0 = std::max<ptrdiff_t>(y0, 0); |
157 | 1.62k | ptrdiff_t y1 = std::llround(center.y + maximum_distance) + 1; |
158 | 1.62k | y1 = std::min<ptrdiff_t>(y1, image_ysize); |
159 | 1.62k | if (y1 <= y0) return; |
160 | | |
161 | 1.62k | SplineSegment segment; |
162 | 1.62k | segment.center_y = center.y; |
163 | 1.62k | segment.center_x = center.x; |
164 | 1.62k | memcpy(segment.color, color, sizeof(segment.color)); |
165 | 1.62k | segment.inv_sigma = 1.0f / sigma; |
166 | 1.62k | segment.sigma_over_4_times_intensity = .25f * sigma * intensity; |
167 | 1.62k | segment.maximum_distance = maximum_distance; |
168 | 1.62k | segments.emplace_back(segment); |
169 | | |
170 | 1.62k | segment_spans.emplace_back(y0, y1); |
171 | 1.62k | } |
172 | | |
173 | | void DrawSegments(float* JXL_RESTRICT row_x, float* JXL_RESTRICT row_y, |
174 | | float* JXL_RESTRICT row_b, size_t y, size_t x0, size_t x1, |
175 | | const bool add, const SplineSegment* segments, |
176 | | const size_t* segment_indices, |
177 | 3.28k | const ptrdiff_t* segment_y_start) { |
178 | 3.28k | float* JXL_RESTRICT rows[3] = {row_x, row_y, row_b}; |
179 | 9.87k | for (ptrdiff_t i = segment_y_start[y]; i < segment_y_start[y + 1]; i++) { |
180 | 6.59k | DrawSegment(segments[segment_indices[i]], add, y, x0, x1, rows); |
181 | 6.59k | } |
182 | 3.28k | } |
183 | | |
184 | | void SegmentsFromPoints( |
185 | | size_t image_ysize, const Spline& spline, |
186 | | const std::vector<std::pair<Spline::Point, float>>& points_to_draw, |
187 | | const float arc_length, std::vector<SplineSegment>& segments, |
188 | 555 | std::vector<SplineSegmentSpan>& segments_spans) { |
189 | 555 | const float inv_arc_length = 1.0f / arc_length; |
190 | 555 | int k = 0; |
191 | 1.66k | for (const auto& point_to_draw : points_to_draw) { |
192 | 1.66k | const Spline::Point& point = point_to_draw.first; |
193 | 1.66k | const float multiplier = point_to_draw.second; |
194 | 1.66k | const float progress_along_arc = |
195 | 1.66k | std::min(1.f, (k * kDesiredRenderingDistance) * inv_arc_length); |
196 | 1.66k | ++k; |
197 | 1.66k | float color[3]; |
198 | 6.66k | for (size_t c = 0; c < 3; ++c) { |
199 | 4.99k | color[c] = |
200 | 4.99k | ContinuousIDCT(spline.color_dct[c], (32 - 1) * progress_along_arc); |
201 | 4.99k | } |
202 | 1.66k | const float sigma = |
203 | 1.66k | ContinuousIDCT(spline.sigma_dct, (32 - 1) * progress_along_arc); |
204 | 1.66k | ComputeSegments(image_ysize, point, multiplier, color, sigma, segments, |
205 | 1.66k | segments_spans); |
206 | 1.66k | } |
207 | 555 | } |
208 | | } // namespace |
209 | | // NOLINTNEXTLINE(google-readability-namespace-comments) |
210 | | } // namespace HWY_NAMESPACE |
211 | | } // namespace jxl |
212 | | HWY_AFTER_NAMESPACE(); |
213 | | |
214 | | #if HWY_ONCE |
215 | | namespace jxl { |
216 | | HWY_EXPORT(SegmentsFromPoints); |
217 | | HWY_EXPORT(DrawSegments); |
218 | | |
219 | | namespace { |
220 | | |
221 | | // It is not in spec, but reasonable limit to avoid overflows. |
222 | | template <typename T> |
223 | 2.23k | Status ValidateSplinePointPos(const T& x, const T& y) { |
224 | 2.23k | constexpr T kSplinePosLimit = 1u << 23; |
225 | 2.23k | if ((x >= kSplinePosLimit) || (x <= -kSplinePosLimit) || |
226 | 2.23k | (y >= kSplinePosLimit) || (y <= -kSplinePosLimit)) { |
227 | 0 | return JXL_FAILURE("Spline coordinates out of bounds"); |
228 | 0 | } |
229 | 2.23k | return true; |
230 | 2.23k | } splines.cc:jxl::Status jxl::(anonymous namespace)::ValidateSplinePointPos<long>(long const&, long const&) Line | Count | Source | 223 | 563 | Status ValidateSplinePointPos(const T& x, const T& y) { | 224 | 563 | constexpr T kSplinePosLimit = 1u << 23; | 225 | 563 | if ((x >= kSplinePosLimit) || (x <= -kSplinePosLimit) || | 226 | 563 | (y >= kSplinePosLimit) || (y <= -kSplinePosLimit)) { | 227 | 0 | return JXL_FAILURE("Spline coordinates out of bounds"); | 228 | 0 | } | 229 | 563 | return true; | 230 | 563 | } |
splines.cc:jxl::Status jxl::(anonymous namespace)::ValidateSplinePointPos<float>(float const&, float const&) Line | Count | Source | 223 | 559 | Status ValidateSplinePointPos(const T& x, const T& y) { | 224 | 559 | constexpr T kSplinePosLimit = 1u << 23; | 225 | 559 | if ((x >= kSplinePosLimit) || (x <= -kSplinePosLimit) || | 226 | 559 | (y >= kSplinePosLimit) || (y <= -kSplinePosLimit)) { | 227 | 0 | return JXL_FAILURE("Spline coordinates out of bounds"); | 228 | 0 | } | 229 | 559 | return true; | 230 | 559 | } |
splines.cc:jxl::Status jxl::(anonymous namespace)::ValidateSplinePointPos<int>(int const&, int const&) Line | Count | Source | 223 | 1.11k | Status ValidateSplinePointPos(const T& x, const T& y) { | 224 | 1.11k | constexpr T kSplinePosLimit = 1u << 23; | 225 | 1.11k | if ((x >= kSplinePosLimit) || (x <= -kSplinePosLimit) || | 226 | 1.11k | (y >= kSplinePosLimit) || (y <= -kSplinePosLimit)) { | 227 | 0 | return JXL_FAILURE("Spline coordinates out of bounds"); | 228 | 0 | } | 229 | 1.11k | return true; | 230 | 1.11k | } |
|
231 | | |
232 | | // Maximum number of spline control points per frame is |
233 | | // std::min(kMaxNumControlPoints, xsize * ysize / 2) |
234 | | constexpr size_t kMaxNumControlPoints = 1u << 20u; |
235 | | constexpr size_t kMaxNumControlPointsPerPixelRatio = 2; |
236 | | |
237 | 0 | float AdjustedQuant(const int32_t adjustment) { |
238 | 0 | return (adjustment >= 0) ? (1.f + .125f * adjustment) |
239 | 0 | : 1.f / (1.f - .125f * adjustment); |
240 | 0 | } |
241 | | |
242 | 559 | float InvAdjustedQuant(const int32_t adjustment) { |
243 | 559 | return (adjustment >= 0) ? 1.f / (1.f + .125f * adjustment) |
244 | 559 | : (1.f - .125f * adjustment); |
245 | 559 | } |
246 | | |
247 | | // X, Y, B, sigma. |
248 | | constexpr float kChannelWeight[] = {0.0042f, 0.075f, 0.07f, .3333f}; |
249 | | |
250 | | Status DecodeAllStartingPoints(std::vector<Spline::Point>* const points, |
251 | | BitReader* const br, ANSSymbolReader* reader, |
252 | | const std::vector<uint8_t>& context_map, |
253 | 563 | const size_t num_splines) { |
254 | 563 | points->clear(); |
255 | 563 | points->reserve(num_splines); |
256 | 563 | int64_t last_x = 0; |
257 | 563 | int64_t last_y = 0; |
258 | 1.12k | for (size_t i = 0; i < num_splines; i++) { |
259 | 563 | size_t dx = |
260 | 563 | reader->ReadHybridUint(kStartingPositionContext, br, context_map); |
261 | 563 | size_t dy = |
262 | 563 | reader->ReadHybridUint(kStartingPositionContext, br, context_map); |
263 | 563 | int64_t x; |
264 | 563 | int64_t y; |
265 | 563 | if (i != 0) { |
266 | 0 | x = UnpackSigned(dx) + last_x; |
267 | 0 | y = UnpackSigned(dy) + last_y; |
268 | 563 | } else { |
269 | 563 | x = dx; |
270 | 563 | y = dy; |
271 | 563 | } |
272 | 563 | JXL_RETURN_IF_ERROR(ValidateSplinePointPos(x, y)); |
273 | 563 | points->emplace_back(static_cast<float>(x), static_cast<float>(y)); |
274 | 563 | last_x = x; |
275 | 563 | last_y = y; |
276 | 563 | } |
277 | 563 | return true; |
278 | 563 | } |
279 | | |
280 | | struct Vector { |
281 | | float x, y; |
282 | 0 | Vector operator-() const { return {-x, -y}; } |
283 | 0 | Vector operator+(const Vector& other) const { |
284 | 0 | return {x + other.x, y + other.y}; |
285 | 0 | } |
286 | 9.99k | float SquaredNorm() const { return x * x + y * y; } |
287 | | }; |
288 | 50.5k | Vector operator*(const float k, const Vector& vec) { |
289 | 50.5k | return {k * vec.x, k * vec.y}; |
290 | 50.5k | } |
291 | | |
292 | 51.6k | Spline::Point operator+(const Spline::Point& p, const Vector& vec) { |
293 | 51.6k | return {p.x + vec.x, p.y + vec.y}; |
294 | 51.6k | } |
295 | 61.6k | Vector operator-(const Spline::Point& a, const Spline::Point& b) { |
296 | 61.6k | return {a.x - b.x, a.y - b.y}; |
297 | 61.6k | } |
298 | | |
299 | | // TODO(eustas): avoid making a copy of "points". |
300 | | void DrawCentripetalCatmullRomSpline(std::vector<Spline::Point> points, |
301 | 559 | std::vector<Spline::Point>& result) { |
302 | 559 | if (points.empty()) return; |
303 | 559 | if (points.size() == 1) { |
304 | 4 | result.push_back(points[0]); |
305 | 4 | return; |
306 | 4 | } |
307 | | // Number of points to compute between each control point. |
308 | 555 | static constexpr int kNumPoints = 16; |
309 | 555 | result.reserve((points.size() - 1) * kNumPoints + 1); |
310 | 555 | points.insert(points.begin(), points[0] + (points[0] - points[1])); |
311 | 555 | points.push_back(points[points.size() - 1] + |
312 | 555 | (points[points.size() - 1] - points[points.size() - 2])); |
313 | | // points has at least 4 elements at this point. |
314 | 1.11k | for (size_t start = 0; start < points.size() - 3; ++start) { |
315 | | // 4 of them are used, and we draw from p[1] to p[2]. |
316 | 555 | const Spline::Point* const p = &points[start]; |
317 | 555 | result.push_back(p[1]); |
318 | 555 | float d[3]; |
319 | 555 | float t[4]; |
320 | 555 | t[0] = 0; |
321 | 2.22k | for (int k = 0; k < 3; ++k) { |
322 | | // TODO(eustas): for each segment delta is calculated 3 times... |
323 | | // TODO(eustas): restrict d[k] with reasonable limit and spec it. |
324 | 1.66k | d[k] = std::sqrt(hypotf(p[k + 1].x - p[k].x, p[k + 1].y - p[k].y)); |
325 | 1.66k | t[k + 1] = t[k] + d[k]; |
326 | 1.66k | } |
327 | 8.88k | for (int i = 1; i < kNumPoints; ++i) { |
328 | 8.32k | const float tt = d[0] + (static_cast<float>(i) / kNumPoints) * d[1]; |
329 | 8.32k | Spline::Point a[3]; |
330 | 33.3k | for (int k = 0; k < 3; ++k) { |
331 | | // TODO(eustas): reciprocal multiplication would be faster. |
332 | 24.9k | a[k] = p[k] + ((tt - t[k]) / d[k]) * (p[k + 1] - p[k]); |
333 | 24.9k | } |
334 | 8.32k | Spline::Point b[2]; |
335 | 24.9k | for (int k = 0; k < 2; ++k) { |
336 | 16.6k | b[k] = a[k] + ((tt - t[k]) / (d[k] + d[k + 1])) * (a[k + 1] - a[k]); |
337 | 16.6k | } |
338 | 8.32k | result.push_back(b[0] + ((tt - t[1]) / d[1]) * (b[1] - b[0])); |
339 | 8.32k | } |
340 | 555 | } |
341 | 555 | result.push_back(points[points.size() - 2]); |
342 | 555 | } |
343 | | |
344 | | // Move along the line segments defined by `points`, `kDesiredRenderingDistance` |
345 | | // pixels at a time, and call `functor` with each point and the actual distance |
346 | | // to the previous point (which will always be kDesiredRenderingDistance except |
347 | | // possibly for the very last point). |
348 | | // TODO(eustas): this method always adds the last point, but never the first |
349 | | // (unless those are one); I believe both ends matter. |
350 | | template <typename Points, typename Functor> |
351 | 559 | Status ForEachEquallySpacedPoint(const Points& points, const Functor& functor) { |
352 | 559 | JXL_ENSURE(!points.empty()); |
353 | 559 | Spline::Point current = points.front(); |
354 | 559 | functor(current, kDesiredRenderingDistance); |
355 | 559 | auto next = points.begin(); |
356 | 1.11k | while (next != points.end()) { |
357 | 1.11k | const Spline::Point* previous = ¤t; |
358 | 1.11k | float arclength_from_previous = 0.f; |
359 | 10.5k | for (;;) { |
360 | 10.5k | if (next == points.end()) { |
361 | 559 | functor(*previous, arclength_from_previous); |
362 | 559 | return true; |
363 | 559 | } |
364 | 9.99k | const float arclength_to_next = |
365 | 9.99k | std::sqrt((*next - *previous).SquaredNorm()); |
366 | 9.99k | if (arclength_from_previous + arclength_to_next >= |
367 | 9.99k | kDesiredRenderingDistance) { |
368 | 555 | current = |
369 | 555 | *previous + ((kDesiredRenderingDistance - arclength_from_previous) / |
370 | 555 | arclength_to_next) * |
371 | 555 | (*next - *previous); |
372 | 555 | functor(current, kDesiredRenderingDistance); |
373 | 555 | break; |
374 | 555 | } |
375 | 9.43k | arclength_from_previous += arclength_to_next; |
376 | 9.43k | previous = &*next; |
377 | 9.43k | ++next; |
378 | 9.43k | } |
379 | 1.11k | } |
380 | 0 | return true; |
381 | 559 | } |
382 | | |
383 | | } // namespace |
384 | | |
385 | | StatusOr<QuantizedSpline> QuantizedSpline::Create( |
386 | | const Spline& original, const int32_t quantization_adjustment, |
387 | 0 | const float y_to_x, const float y_to_b) { |
388 | 0 | JXL_ENSURE(!original.control_points.empty()); |
389 | 0 | QuantizedSpline result; |
390 | 0 | result.control_points_.reserve(original.control_points.size() - 1); |
391 | 0 | const Spline::Point& starting_point = original.control_points.front(); |
392 | 0 | int previous_x = static_cast<int>(std::round(starting_point.x)); |
393 | 0 | int previous_y = static_cast<int>(std::round(starting_point.y)); |
394 | 0 | int previous_delta_x = 0; |
395 | 0 | int previous_delta_y = 0; |
396 | 0 | for (auto it = original.control_points.begin() + 1; |
397 | 0 | it != original.control_points.end(); ++it) { |
398 | 0 | const int new_x = static_cast<int>(std::round(it->x)); |
399 | 0 | const int new_y = static_cast<int>(std::round(it->y)); |
400 | 0 | const int new_delta_x = new_x - previous_x; |
401 | 0 | const int new_delta_y = new_y - previous_y; |
402 | 0 | result.control_points_.emplace_back(new_delta_x - previous_delta_x, |
403 | 0 | new_delta_y - previous_delta_y); |
404 | 0 | previous_delta_x = new_delta_x; |
405 | 0 | previous_delta_y = new_delta_y; |
406 | 0 | previous_x = new_x; |
407 | 0 | previous_y = new_y; |
408 | 0 | } |
409 | |
|
410 | 0 | const auto to_int = [](float v) -> int { |
411 | | // Maximal int representable with float. |
412 | 0 | constexpr float kMax = std::numeric_limits<int>::max() - 127; |
413 | 0 | constexpr float kMin = -kMax; |
414 | 0 | return static_cast<int>(std::round(Clamp1(v, kMin, kMax))); |
415 | 0 | }; |
416 | |
|
417 | 0 | const auto quant = AdjustedQuant(quantization_adjustment); |
418 | 0 | const auto inv_quant = InvAdjustedQuant(quantization_adjustment); |
419 | 0 | for (int c : {1, 0, 2}) { |
420 | 0 | float factor = (c == 0) ? y_to_x : (c == 1) ? 0 : y_to_b; |
421 | 0 | for (int i = 0; i < 32; ++i) { |
422 | 0 | const float dct_factor = (i == 0) ? kSqrt2 : 1.0f; |
423 | 0 | const float inv_dct_factor = (i == 0) ? kSqrt0_5 : 1.0f; |
424 | 0 | auto restored_y = result.color_dct_[1][i] * inv_dct_factor * |
425 | 0 | kChannelWeight[1] * inv_quant; |
426 | 0 | auto decorrelated = original.color_dct[c][i] - factor * restored_y; |
427 | 0 | result.color_dct_[c][i] = |
428 | 0 | to_int(decorrelated * dct_factor * quant / kChannelWeight[c]); |
429 | 0 | } |
430 | 0 | } |
431 | 0 | for (int i = 0; i < 32; ++i) { |
432 | 0 | const float dct_factor = (i == 0) ? kSqrt2 : 1.0f; |
433 | 0 | result.sigma_dct_[i] = |
434 | 0 | to_int(original.sigma_dct[i] * dct_factor * quant / kChannelWeight[3]); |
435 | 0 | } |
436 | 0 | return result; |
437 | 0 | } |
438 | | |
439 | | Status QuantizedSpline::Dequantize(const Spline::Point& starting_point, |
440 | | const int32_t quantization_adjustment, |
441 | | const float y_to_x, const float y_to_b, |
442 | | const uint64_t image_size, |
443 | | uint64_t* total_estimated_area_reached, |
444 | 559 | Spline& result) const { |
445 | 559 | constexpr uint64_t kOne = static_cast<uint64_t>(1); |
446 | 559 | const uint64_t area_limit = |
447 | 559 | std::min(1024 * image_size + (kOne << 32), kOne << 42); |
448 | | |
449 | 559 | result.control_points.clear(); |
450 | 559 | result.control_points.reserve(control_points_.size() + 1); |
451 | 559 | float px = std::round(starting_point.x); |
452 | 559 | float py = std::round(starting_point.y); |
453 | 559 | JXL_RETURN_IF_ERROR(ValidateSplinePointPos(px, py)); |
454 | 559 | int current_x = static_cast<int>(px); |
455 | 559 | int current_y = static_cast<int>(py); |
456 | 559 | result.control_points.emplace_back(static_cast<float>(current_x), |
457 | 559 | static_cast<float>(current_y)); |
458 | 559 | int current_delta_x = 0; |
459 | 559 | int current_delta_y = 0; |
460 | 559 | uint64_t manhattan_distance = 0; |
461 | 559 | for (const auto& point : control_points_) { |
462 | 555 | current_delta_x += point.first; |
463 | 555 | current_delta_y += point.second; |
464 | 555 | manhattan_distance += std::abs(current_delta_x) + std::abs(current_delta_y); |
465 | 555 | if (manhattan_distance > area_limit) { |
466 | 0 | return JXL_FAILURE("Too large manhattan_distance reached: %" PRIu64, |
467 | 0 | manhattan_distance); |
468 | 0 | } |
469 | 555 | JXL_RETURN_IF_ERROR( |
470 | 555 | ValidateSplinePointPos(current_delta_x, current_delta_y)); |
471 | 555 | current_x += current_delta_x; |
472 | 555 | current_y += current_delta_y; |
473 | 555 | JXL_RETURN_IF_ERROR(ValidateSplinePointPos(current_x, current_y)); |
474 | 555 | result.control_points.emplace_back(static_cast<float>(current_x), |
475 | 555 | static_cast<float>(current_y)); |
476 | 555 | } |
477 | | |
478 | 559 | const auto inv_quant = InvAdjustedQuant(quantization_adjustment); |
479 | 2.23k | for (int c = 0; c < 3; ++c) { |
480 | 55.3k | for (int i = 0; i < 32; ++i) { |
481 | 53.6k | const float inv_dct_factor = (i == 0) ? kSqrt0_5 : 1.0f; |
482 | 53.6k | result.color_dct[c][i] = |
483 | 53.6k | color_dct_[c][i] * inv_dct_factor * kChannelWeight[c] * inv_quant; |
484 | 53.6k | } |
485 | 1.67k | } |
486 | 18.4k | for (int i = 0; i < 32; ++i) { |
487 | 17.8k | result.color_dct[0][i] += y_to_x * result.color_dct[1][i]; |
488 | 17.8k | result.color_dct[2][i] += y_to_b * result.color_dct[1][i]; |
489 | 17.8k | } |
490 | 559 | uint64_t width_estimate = 0; |
491 | | |
492 | 559 | uint64_t color[3] = {}; |
493 | 2.23k | for (int c = 0; c < 3; ++c) { |
494 | 55.3k | for (int i = 0; i < 32; ++i) { |
495 | 53.6k | color[c] += static_cast<uint64_t>( |
496 | 53.6k | std::ceil(inv_quant * std::abs(color_dct_[c][i]))); |
497 | 53.6k | } |
498 | 1.67k | } |
499 | 559 | color[0] += static_cast<uint64_t>(std::ceil(std::abs(y_to_x))) * color[1]; |
500 | 559 | color[2] += static_cast<uint64_t>(std::ceil(std::abs(y_to_b))) * color[1]; |
501 | | // This is not taking kChannelWeight into account, but up to constant factors |
502 | | // it gives an indication of the influence of the color values on the area |
503 | | // that will need to be rendered. |
504 | 559 | const uint64_t max_color = std::max({color[1], color[0], color[2]}); |
505 | 559 | uint64_t logcolor = |
506 | 559 | std::max(kOne, static_cast<uint64_t>(CeilLog2Nonzero(kOne + max_color))); |
507 | | |
508 | 559 | const float weight_limit = |
509 | 559 | std::ceil(std::sqrt((static_cast<float>(area_limit) / logcolor) / |
510 | 559 | std::max<size_t>(1, manhattan_distance))); |
511 | | |
512 | 18.4k | for (int i = 0; i < 32; ++i) { |
513 | 17.8k | const float inv_dct_factor = (i == 0) ? kSqrt0_5 : 1.0f; |
514 | 17.8k | result.sigma_dct[i] = |
515 | 17.8k | sigma_dct_[i] * inv_dct_factor * kChannelWeight[3] * inv_quant; |
516 | | // If we include the factor kChannelWeight[3]=.3333f here, we get a |
517 | | // realistic area estimate. We leave it out to simplify the calculations, |
518 | | // and understand that this way we underestimate the area by a factor of |
519 | | // 1/(0.3333*0.3333). This is taken into account in the limits below. |
520 | 17.8k | float weight_f = std::ceil(inv_quant * std::abs(sigma_dct_[i])); |
521 | 17.8k | uint64_t weight = |
522 | 17.8k | static_cast<uint64_t>(std::min(weight_limit, std::max(1.0f, weight_f))); |
523 | 17.8k | width_estimate += weight * weight * logcolor; |
524 | 17.8k | } |
525 | 559 | *total_estimated_area_reached += (width_estimate * manhattan_distance); |
526 | 559 | if (*total_estimated_area_reached > area_limit) { |
527 | 0 | return JXL_FAILURE("Too large total_estimated_area eached: %" PRIu64, |
528 | 0 | *total_estimated_area_reached); |
529 | 0 | } |
530 | | |
531 | 559 | return true; |
532 | 559 | } |
533 | | |
534 | | Status QuantizedSpline::Decode(const std::vector<uint8_t>& context_map, |
535 | | ANSSymbolReader* const decoder, |
536 | | BitReader* const br, |
537 | | const size_t max_control_points, |
538 | 563 | size_t* total_num_control_points) { |
539 | 563 | const size_t num_control_points = |
540 | 563 | decoder->ReadHybridUint(kNumControlPointsContext, br, context_map); |
541 | 563 | if (num_control_points > max_control_points) { |
542 | 0 | return JXL_FAILURE("Too many control points: %" PRIuS, num_control_points); |
543 | 0 | } |
544 | 563 | *total_num_control_points += num_control_points; |
545 | 563 | if (*total_num_control_points > max_control_points) { |
546 | 0 | return JXL_FAILURE("Too many control points: %" PRIuS, |
547 | 0 | *total_num_control_points); |
548 | 0 | } |
549 | 563 | control_points_.resize(num_control_points); |
550 | | // Maximal image dimension. |
551 | 563 | constexpr int64_t kDeltaLimit = 1u << 30; |
552 | 563 | for (std::pair<int64_t, int64_t>& control_point : control_points_) { |
553 | 558 | control_point.first = UnpackSigned( |
554 | 558 | decoder->ReadHybridUint(kControlPointsContext, br, context_map)); |
555 | 558 | control_point.second = UnpackSigned( |
556 | 558 | decoder->ReadHybridUint(kControlPointsContext, br, context_map)); |
557 | | // Check delta-deltas are not outrageous; it is not in spec, but there is |
558 | | // no reason to allow larger values. |
559 | 558 | if ((control_point.first >= kDeltaLimit) || |
560 | 558 | (control_point.first <= -kDeltaLimit) || |
561 | 558 | (control_point.second >= kDeltaLimit) || |
562 | 558 | (control_point.second <= -kDeltaLimit)) { |
563 | 0 | return JXL_FAILURE("Spline delta-delta is out of bounds"); |
564 | 0 | } |
565 | 558 | } |
566 | | |
567 | 2.25k | const auto decode_dct = [decoder, br, &context_map](int dct[32]) -> Status { |
568 | 2.25k | constexpr int kWeirdNumber = std::numeric_limits<int>::min(); |
569 | 74.3k | for (int i = 0; i < 32; ++i) { |
570 | 72.0k | dct[i] = |
571 | 72.0k | UnpackSigned(decoder->ReadHybridUint(kDCTContext, br, context_map)); |
572 | 72.0k | if (dct[i] == kWeirdNumber) { |
573 | 0 | return JXL_FAILURE("The weird number in spline DCT"); |
574 | 0 | } |
575 | 72.0k | } |
576 | 2.25k | return true; |
577 | 2.25k | }; |
578 | 1.68k | for (auto& dct : color_dct_) { |
579 | 1.68k | JXL_RETURN_IF_ERROR(decode_dct(dct)); |
580 | 1.68k | } |
581 | 563 | JXL_RETURN_IF_ERROR(decode_dct(sigma_dct_)); |
582 | 563 | return true; |
583 | 563 | } |
584 | | |
585 | 0 | void Splines::SetData(SplineDataView data) { |
586 | 0 | Clear(); |
587 | 0 | data_ = data; |
588 | 0 | } |
589 | | |
590 | 17.7k | void Splines::Clear() { |
591 | 17.7k | quantization_adjustment_ = 0; |
592 | 17.7k | splines_storage_.clear(); |
593 | 17.7k | starting_points_storage_.clear(); |
594 | 17.7k | data_ = {}; |
595 | 17.7k | segments_.clear(); |
596 | 17.7k | segment_indices_ = AlignedMemory(); |
597 | 17.7k | segment_y_start_ = AlignedMemory(); |
598 | 17.7k | } |
599 | | |
600 | 563 | Status Splines::Decode(jxl::BitReader* br, const size_t num_pixels) { |
601 | 563 | std::vector<uint8_t> context_map; |
602 | 563 | ANSCode code; |
603 | 563 | JXL_RETURN_IF_ERROR(DecodeHistograms(memory_manager_, br, kNumSplineContexts, |
604 | 563 | &code, &context_map)); |
605 | 1.12k | JXL_ASSIGN_OR_RETURN(ANSSymbolReader decoder, |
606 | 1.12k | ANSSymbolReader::Create(&code, br)); |
607 | 1.12k | size_t num_splines = |
608 | 1.12k | decoder.ReadHybridUint(kNumSplinesContext, br, context_map); |
609 | 1.12k | size_t max_control_points = std::min( |
610 | 1.12k | kMaxNumControlPoints, num_pixels / kMaxNumControlPointsPerPixelRatio); |
611 | 1.12k | if (num_splines > max_control_points || |
612 | 563 | num_splines + 1 > max_control_points) { |
613 | 0 | return JXL_FAILURE("Too many splines: %" PRIuS, num_splines); |
614 | 0 | } |
615 | 563 | num_splines++; |
616 | 563 | JXL_RETURN_IF_ERROR(DecodeAllStartingPoints( |
617 | 563 | &starting_points_storage_, br, &decoder, context_map, num_splines)); |
618 | | |
619 | 563 | quantization_adjustment_ = UnpackSigned( |
620 | 563 | decoder.ReadHybridUint(kQuantizationAdjustmentContext, br, context_map)); |
621 | | |
622 | 563 | splines_storage_.clear(); |
623 | 563 | splines_storage_.reserve(num_splines); |
624 | 563 | size_t num_control_points = num_splines; |
625 | 1.12k | for (size_t i = 0; i < num_splines; ++i) { |
626 | 563 | QuantizedSpline spline; |
627 | 563 | JXL_RETURN_IF_ERROR(spline.Decode(context_map, &decoder, br, |
628 | 563 | max_control_points, &num_control_points)); |
629 | 563 | splines_storage_.push_back(std::move(spline)); |
630 | 563 | } |
631 | | |
632 | 563 | JXL_RETURN_IF_ERROR(decoder.CheckANSFinalState()); |
633 | | |
634 | 563 | data_ = SplineDataView{Span<const QuantizedSpline>(splines_storage_), |
635 | 563 | Span<const Spline::Point>(starting_points_storage_)}; |
636 | | |
637 | 563 | if (!HasAny()) { |
638 | 0 | return JXL_FAILURE("Decoded splines but got none"); |
639 | 0 | } |
640 | | |
641 | 563 | return true; |
642 | 563 | } |
643 | | |
644 | 0 | void Splines::AddTo(Image3F* const opsin, const Rect& opsin_rect) const { |
645 | 0 | Apply</*add=*/true>(opsin, opsin_rect); |
646 | 0 | } |
647 | | void Splines::AddToRow(float* JXL_RESTRICT row_x, float* JXL_RESTRICT row_y, |
648 | | float* JXL_RESTRICT row_b, size_t y, size_t x0, |
649 | 4.82k | size_t x1) const { |
650 | 4.82k | ApplyToRow</*add=*/true>(row_x, row_y, row_b, y, x0, x1); |
651 | 4.82k | } |
652 | | |
653 | 0 | void Splines::SubtractFrom(Image3F* const opsin) const { |
654 | 0 | Apply</*add=*/false>(opsin, Rect(*opsin)); |
655 | 0 | } |
656 | | |
657 | | Status Splines::InitializeDrawCache(const size_t image_xsize, |
658 | | const size_t image_ysize, |
659 | 559 | const ColorCorrelation& color_correlation) { |
660 | | // TODO(veluca): avoid storing segments that are entirely outside image |
661 | | // boundaries. |
662 | 559 | segments_.clear(); |
663 | 559 | segment_indices_ = AlignedMemory(); |
664 | 559 | segment_y_start_ = AlignedMemory(); |
665 | 559 | std::vector<SplineSegmentSpan> segments_spans; |
666 | 559 | std::vector<Spline::Point> intermediate_points; |
667 | 559 | uint64_t total_estimated_area_reached = 0; |
668 | 559 | std::vector<Spline> splines; |
669 | 1.11k | for (size_t i = 0; i < data_.splines.size(); ++i) { |
670 | 559 | Spline spline; |
671 | 559 | JXL_RETURN_IF_ERROR(data_.splines[i].Dequantize( |
672 | 559 | data_.starting_points[i], quantization_adjustment_, |
673 | 559 | color_correlation.YtoXRatio(0), color_correlation.YtoBRatio(0), |
674 | 559 | image_xsize * image_ysize, &total_estimated_area_reached, spline)); |
675 | 559 | if (std::adjacent_find(spline.control_points.begin(), |
676 | 559 | spline.control_points.end()) != |
677 | 559 | spline.control_points.end()) { |
678 | | // Otherwise division by zero might occur. Once control points coincide, |
679 | | // the direction of curve is undefined... |
680 | 0 | return JXL_FAILURE( |
681 | 0 | "identical successive control points in spline %" PRIuS, i); |
682 | 0 | } |
683 | 559 | splines.push_back(spline); |
684 | 559 | } |
685 | | // TODO(firsching) Change this into a JXL_FAILURE for level 5 codestreams. |
686 | 559 | if (total_estimated_area_reached > |
687 | 559 | std::min( |
688 | 559 | (8 * image_xsize * image_ysize + (static_cast<uint64_t>(1) << 25)), |
689 | 559 | (static_cast<uint64_t>(1) << 30))) { |
690 | 0 | JXL_WARNING( |
691 | 0 | "Large total_estimated_area_reached, expect slower decoding: %" PRIu64, |
692 | 0 | total_estimated_area_reached); |
693 | 0 | #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION |
694 | 0 | return JXL_FAILURE("Total spline area is too large"); |
695 | 0 | #endif |
696 | 0 | } |
697 | | |
698 | 559 | for (Spline& spline : splines) { |
699 | 559 | std::vector<std::pair<Spline::Point, float>> points_to_draw; |
700 | 1.67k | auto add_point = [&](const Spline::Point& point, const float multiplier) { |
701 | 1.67k | points_to_draw.emplace_back(point, multiplier); |
702 | 1.67k | }; |
703 | 559 | intermediate_points.clear(); |
704 | 559 | DrawCentripetalCatmullRomSpline(spline.control_points, intermediate_points); |
705 | 559 | JXL_RETURN_IF_ERROR( |
706 | 559 | ForEachEquallySpacedPoint(intermediate_points, add_point)); |
707 | 559 | const float arc_length = |
708 | 559 | (points_to_draw.size() - 2) * kDesiredRenderingDistance + |
709 | 559 | points_to_draw.back().second; |
710 | 559 | if (arc_length <= 0.f) { |
711 | | // This spline wouldn't have any effect. |
712 | 4 | continue; |
713 | 4 | } |
714 | 555 | HWY_DYNAMIC_DISPATCH(SegmentsFromPoints) |
715 | 555 | (image_ysize, spline, points_to_draw, arc_length, segments_, |
716 | 555 | segments_spans); |
717 | 555 | } |
718 | | |
719 | 559 | size_t segment_y_start_num_bytes = (image_ysize + 2) * sizeof(ptrdiff_t); |
720 | 559 | JXL_ASSIGN_OR_RETURN( |
721 | 559 | segment_y_start_, |
722 | 559 | AlignedMemory::Create(memory_manager_, segment_y_start_num_bytes)); |
723 | 559 | ptrdiff_t* segment_y_start = segment_y_start_.address<ptrdiff_t>(); |
724 | 559 | memset(segment_y_start, 0, segment_y_start_num_bytes); |
725 | 559 | ptrdiff_t* population = segment_y_start + 1; |
726 | 1.62k | for (const auto& segment_span : segments_spans) { |
727 | 1.62k | population[segment_span.start]++; |
728 | 1.62k | population[segment_span.end]--; |
729 | 1.62k | } |
730 | | // Turn to cumulative. |
731 | 559 | size_t total = 0; |
732 | 559 | ptrdiff_t coverage = 0; |
733 | 35.6k | for (size_t y = 0; y < image_ysize; y++) { |
734 | 35.0k | if (population[y] < 0) { |
735 | 1.39k | JXL_ENSURE(coverage >= -population[y]); |
736 | 1.39k | } |
737 | 35.0k | coverage += population[y]; |
738 | 35.0k | population[y] = total; |
739 | 35.0k | total += coverage; |
740 | 35.0k | } |
741 | | // population[0] == 0 (that is segment_y_start[1]) |
742 | | // We are going to place segments using population[y] as index for elements |
743 | | // of y-th line (and increment them); that way final value of population[y] |
744 | | // is the staring index for (y+1)-th line. Thus segment_y_start will contain |
745 | | // indices without an offset. |
746 | 559 | JXL_ASSIGN_OR_RETURN( |
747 | 559 | segment_indices_, |
748 | 559 | AlignedMemory::Create(memory_manager_, total * sizeof(size_t))); |
749 | 559 | size_t* segment_indices = segment_indices_.address<size_t>(); |
750 | 2.18k | for (size_t i = 0; i < segments_.size(); ++i) { |
751 | 1.62k | const auto& segment_span = segments_spans[i]; |
752 | 13.6k | for (size_t y = segment_span.start; y < segment_span.end; y++) { |
753 | 12.0k | segment_indices[population[y]++] = i; |
754 | 12.0k | } |
755 | 1.62k | } |
756 | | |
757 | 559 | return true; |
758 | 559 | } |
759 | | |
760 | | template <bool add> |
761 | | void Splines::ApplyToRow(float* JXL_RESTRICT row_x, float* JXL_RESTRICT row_y, |
762 | | float* JXL_RESTRICT row_b, size_t y, size_t x0, |
763 | 4.82k | size_t x1) const { |
764 | 4.82k | if (segments_.empty()) return; |
765 | 3.28k | HWY_DYNAMIC_DISPATCH(DrawSegments) |
766 | 3.28k | (row_x, row_y, row_b, y, x0, x1, add, segments_.data(), |
767 | 3.28k | segment_indices_.address<size_t>(), segment_y_start_.address<ptrdiff_t>()); |
768 | 3.28k | } void jxl::Splines::ApplyToRow<true>(float*, float*, float*, unsigned long, unsigned long, unsigned long) const Line | Count | Source | 763 | 4.82k | size_t x1) const { | 764 | 4.82k | if (segments_.empty()) return; | 765 | 3.28k | HWY_DYNAMIC_DISPATCH(DrawSegments) | 766 | 3.28k | (row_x, row_y, row_b, y, x0, x1, add, segments_.data(), | 767 | 3.28k | segment_indices_.address<size_t>(), segment_y_start_.address<ptrdiff_t>()); | 768 | 3.28k | } |
Unexecuted instantiation: void jxl::Splines::ApplyToRow<false>(float*, float*, float*, unsigned long, unsigned long, unsigned long) const |
769 | | |
770 | | template <bool add> |
771 | 0 | void Splines::Apply(Image3F* const opsin, const Rect& opsin_rect) const { |
772 | 0 | if (segments_.empty()) return; |
773 | 0 | const size_t y0 = opsin_rect.y0(); |
774 | 0 | const size_t x0 = opsin_rect.x0(); |
775 | 0 | const size_t x1 = opsin_rect.x1(); |
776 | 0 | for (size_t y = 0; y < opsin_rect.ysize(); y++) { |
777 | 0 | ApplyToRow<add>(opsin->PlaneRow(0, y0 + y) + x0, |
778 | 0 | opsin->PlaneRow(1, y0 + y) + x0, |
779 | 0 | opsin->PlaneRow(2, y0 + y) + x0, y0 + y, x0, x1); |
780 | 0 | } |
781 | 0 | } Unexecuted instantiation: void jxl::Splines::Apply<true>(jxl::Image3<float>*, jxl::RectT<unsigned long> const&) const Unexecuted instantiation: void jxl::Splines::Apply<false>(jxl::Image3<float>*, jxl::RectT<unsigned long> const&) const |
782 | | |
783 | | } // namespace jxl |
784 | | #endif // HWY_ONCE |