diff --git a/testsuite/gsk/curve-special-cases.c b/testsuite/gsk/curve-special-cases.c
new file mode 100644
index 0000000000..8c4bd131bc
--- /dev/null
+++ b/testsuite/gsk/curve-special-cases.c
@@ -0,0 +1,159 @@
+/*
+ * Copyright © 2020 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see .
+ *
+ * Authors: Benjamin Otte
+ */
+
+#include
+
+#include "gsk/gskcurveprivate.h"
+
+static void
+test_curve_tangents (void)
+{
+ GskCurve c;
+ graphene_point_t p[4];
+ graphene_vec2_t t;
+
+ graphene_point_init (&p[0], 0, 0);
+ graphene_point_init (&p[1], 100, 0);
+ gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_LINE, p));
+
+ gsk_curve_get_start_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
+ gsk_curve_get_end_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
+
+ graphene_point_init (&p[0], 0, 0);
+ graphene_point_init (&p[1], 0, 100);
+ gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_LINE, p));
+
+ gsk_curve_get_start_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001));
+ gsk_curve_get_end_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001));
+
+ graphene_point_init (&p[0], 0, 0);
+ graphene_point_init (&p[1], 50, 0);
+ graphene_point_init (&p[2], 100, 50);
+ graphene_point_init (&p[3], 100, 100);
+ gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p));
+
+ gsk_curve_get_start_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
+ gsk_curve_get_end_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001));
+}
+
+static void
+test_curve_degenerate_tangents (void)
+{
+ GskCurve c;
+ graphene_point_t p[4];
+ graphene_vec2_t t;
+
+ graphene_point_init (&p[0], 0, 0);
+ graphene_point_init (&p[1], 0, 0);
+ graphene_point_init (&p[2], 100, 0);
+ graphene_point_init (&p[3], 100, 0);
+ gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p));
+
+ gsk_curve_get_start_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
+ gsk_curve_get_end_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
+
+ graphene_point_init (&p[0], 0, 0);
+ graphene_point_init (&p[1], 50, 0);
+ graphene_point_init (&p[2], 50, 0);
+ graphene_point_init (&p[3], 100, 0);
+ gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p));
+
+ gsk_curve_get_start_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
+ gsk_curve_get_end_tangent (&c, &t);
+ g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
+}
+
+static gboolean
+pathop_cb (GskPathOperation op,
+ const graphene_point_t *pts,
+ gsize n_pts,
+ gpointer user_data)
+{
+ GskCurve *curve = user_data;
+
+ g_assert (op != GSK_PATH_CLOSE);
+
+ if (op == GSK_PATH_MOVE)
+ return TRUE;
+
+ gsk_curve_init_foreach (curve, op, pts, n_pts);
+ return FALSE;
+}
+
+static void
+parse_curve (GskCurve *c,
+ const char *str)
+{
+ GskPath *path = gsk_path_parse (str);
+
+ gsk_path_foreach (path, -1, pathop_cb, c);
+
+ gsk_path_unref (path);
+}
+
+static void
+test_curve_crossing (void)
+{
+ struct {
+ const char *c;
+ const graphene_point_t p;
+ int crossing;
+ } tests[] = {
+ { "M 0 0 L 200 200", { 200, 100 }, 0 },
+ { "M 0 0 L 200 200", { 0, 100 }, 1 },
+ { "M 0 200 L 200 0", { 0, 100 }, -1 },
+ { "M 0 0 C 100 100 200 200 300 300", { 200, 100 }, 0 },
+ { "M 0 0 C 100 100 200 200 300 300", { 0, 100 }, 1 },
+ { "M 0 300 C 100 200 200 100 300 0", { 0, 100 }, -1 },
+ { "M 0 0 C 100 600 200 -300 300 300", { 0, 150 }, 1 },
+ { "M 0 0 C 100 600 200 -300 300 300", { 100, 150 }, 0 },
+ { "M 0 0 C 100 600 200 -300 300 300", { 200, 150 }, 1 },
+ };
+
+ for (unsigned int i = 0; i < G_N_ELEMENTS (tests); i++)
+ {
+ GskCurve c;
+
+ parse_curve (&c, tests[i].c);
+
+ g_assert_true (gsk_curve_get_crossing (&c, &tests[i].p) == tests[i].crossing);
+ }
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/curve/special/tangents", test_curve_tangents);
+ g_test_add_func ("/curve/special/degenerate-tangents", test_curve_degenerate_tangents);
+ g_test_add_func ("/curve/special/crossing", test_curve_crossing);
+
+ return g_test_run ();
+}
diff --git a/testsuite/gsk/curve.c b/testsuite/gsk/curve.c
new file mode 100644
index 0000000000..5310324c93
--- /dev/null
+++ b/testsuite/gsk/curve.c
@@ -0,0 +1,337 @@
+#include
+#include "gsk/gskcurveprivate.h"
+
+static void
+init_random_point (graphene_point_t *p)
+{
+ p->x = g_test_rand_double_range (0, 1000);
+ p->y = g_test_rand_double_range (0, 1000);
+}
+
+static void
+init_random_curve_with_op (GskCurve *curve,
+ GskPathOperation min_op,
+ GskPathOperation max_op)
+{
+ switch (g_test_rand_int_range (min_op, max_op + 1))
+ {
+ case GSK_PATH_LINE:
+ {
+ graphene_point_t p[2];
+
+ init_random_point (&p[0]);
+ init_random_point (&p[1]);
+ gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_LINE, p));
+ }
+ break;
+
+ case GSK_PATH_QUAD:
+ {
+ graphene_point_t p[3];
+
+ init_random_point (&p[0]);
+ init_random_point (&p[1]);
+ init_random_point (&p[2]);
+ gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_QUAD, p));
+ }
+ break;
+
+ case GSK_PATH_CUBIC:
+ {
+ graphene_point_t p[4];
+
+ init_random_point (&p[0]);
+ init_random_point (&p[1]);
+ init_random_point (&p[2]);
+ init_random_point (&p[3]);
+ gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_CUBIC, p));
+ }
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+init_random_curve (GskCurve *curve)
+{
+ init_random_curve_with_op (curve, GSK_PATH_LINE, GSK_PATH_CUBIC);
+}
+
+static void
+test_curve_tangents (void)
+{
+ for (int i = 0; i < 100; i++)
+ {
+ GskCurve c;
+ graphene_vec2_t vec, exact;
+
+ init_random_curve (&c);
+
+ gsk_curve_get_tangent (&c, 0, &vec);
+ g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&vec), 1.0f, 0.00001);
+ gsk_curve_get_start_tangent (&c, &exact);
+ g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&exact), 1.0f, 0.00001);
+ g_assert_true (graphene_vec2_near (&vec, &exact, 0.05));
+
+ gsk_curve_get_tangent (&c, 1, &vec);
+ g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&vec), 1.0f, 0.00001);
+ gsk_curve_get_end_tangent (&c, &exact);
+ g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&exact), 1.0f, 0.00001);
+ g_assert_true (graphene_vec2_near (&vec, &exact, 0.05));
+ }
+}
+
+static void
+test_curve_points (void)
+{
+ for (int i = 0; i < 100; i++)
+ {
+ GskCurve c;
+ graphene_point_t p;
+
+ init_random_curve (&c);
+
+ /* We can assert equality here because evaluating the polynomials with 0
+ * has no effect on accuracy.
+ */
+ gsk_curve_get_point (&c, 0, &p);
+ g_assert_true (graphene_point_equal (gsk_curve_get_start_point (&c), &p));
+ /* But here we evaluate the polynomials with 1 which gives the highest possible
+ * accuracy error. So we'll just be generous here.
+ */
+ gsk_curve_get_point (&c, 1, &p);
+ g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c), &p, 0.05));
+ }
+}
+
+/* at this point the subdivision stops and the decomposer
+ * violates tolerance rules
+ */
+#define MIN_PROGRESS (1/1024.f)
+
+typedef struct
+{
+ graphene_point_t p;
+ float t;
+} PointOnLine;
+
+static gboolean
+add_line_to_array (const graphene_point_t *from,
+ const graphene_point_t *to,
+ float from_progress,
+ float to_progress,
+ GskCurveLineReason reason,
+ gpointer user_data)
+{
+ GArray *array = user_data;
+ PointOnLine *last = &g_array_index (array, PointOnLine, array->len - 1);
+
+ g_assert_true (array->len > 0);
+ g_assert_cmpfloat (from_progress, >=, 0.0f);
+ g_assert_cmpfloat (from_progress, <, to_progress);
+ g_assert_cmpfloat (to_progress, <=, 1.0f);
+
+ g_assert_true (graphene_point_equal (&last->p, from));
+ g_assert_cmpfloat (last->t, ==, from_progress);
+
+ g_array_append_vals (array, (PointOnLine[1]) { { *to, to_progress } }, 1);
+
+ return TRUE;
+}
+
+static void
+test_curve_decompose (void)
+{
+ static const float tolerance = 0.5;
+
+ for (int i = 0; i < 100; i++)
+ {
+ GArray *array;
+ GskCurve c;
+
+ init_random_curve (&c);
+
+ array = g_array_new (FALSE, FALSE, sizeof (PointOnLine));
+ g_array_append_vals (array, (PointOnLine[1]) { { *gsk_curve_get_start_point (&c), 0.f } }, 1);
+
+ g_assert_true (gsk_curve_decompose (&c, tolerance, add_line_to_array, array));
+
+ g_assert_cmpint (array->len, >=, 2); /* We at least got a line to the end */
+ g_assert_cmpfloat (g_array_index (array, PointOnLine, array->len - 1).t, ==, 1.0);
+
+ for (int j = 0; j < array->len; j++)
+ {
+ PointOnLine *pol = &g_array_index (array, PointOnLine, j);
+ graphene_point_t p;
+
+ /* Check that the points we got are actually on the line */
+ gsk_curve_get_point (&c, pol->t, &p);
+ g_assert_true (graphene_point_near (&pol->p, &p, 0.05));
+
+ /* Check that the mid point is not further than the tolerance */
+ if (j > 0)
+ {
+ PointOnLine *last = &g_array_index (array, PointOnLine, j - 1);
+ graphene_point_t mid;
+
+ if (pol->t - last->t > MIN_PROGRESS)
+ {
+ graphene_point_interpolate (&last->p, &pol->p, 0.5, &mid);
+ gsk_curve_get_point (&c, (pol->t + last->t) / 2, &p);
+ /* The decomposer does this cheaper Manhattan distance test,
+ * so graphene_point_near() does not work */
+ g_assert_cmpfloat (fabs (mid.x - p.x), <=, tolerance);
+ g_assert_cmpfloat (fabs (mid.y - p.y), <=, tolerance);
+ }
+ }
+ }
+
+ g_array_unref (array);
+ }
+}
+
+static gboolean
+add_curve_to_array (GskPathOperation op,
+ const graphene_point_t *pts,
+ gsize n_pts,
+ gpointer user_data)
+{
+ GArray *array = user_data;
+ GskCurve c;
+
+ gsk_curve_init_foreach (&c, op, pts, n_pts);
+ g_array_append_val (array, c);
+
+ return TRUE;
+}
+
+static void
+test_curve_decompose_into (GskPathForeachFlags flags)
+{
+ for (int i = 0; i < 100; i++)
+ {
+ GskCurve c;
+ GskPathBuilder *builder;
+ const graphene_point_t *s;
+ GskPath *path;
+ GArray *array;
+
+ init_random_curve (&c);
+
+ builder = gsk_path_builder_new ();
+
+ s = gsk_curve_get_start_point (&c);
+ gsk_path_builder_move_to (builder, s->x, s->y);
+ gsk_curve_builder_to (&c, builder);
+ path = gsk_path_builder_free_to_path (builder);
+
+ array = g_array_new (FALSE, FALSE, sizeof (GskCurve));
+
+ g_assert_true (gsk_curve_decompose_curve (&c, flags, 0.1, add_curve_to_array, array));
+
+ g_assert_cmpint (array->len, >=, 1);
+
+ for (int j = 0; j < array->len; j++)
+ {
+ GskCurve *c2 = &g_array_index (array, GskCurve, j);
+
+ switch (c2->op)
+ {
+ case GSK_PATH_MOVE:
+ case GSK_PATH_CLOSE:
+ case GSK_PATH_LINE:
+ break;
+ case GSK_PATH_QUAD:
+ g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_QUAD);
+ break;
+ case GSK_PATH_CUBIC:
+ g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_CUBIC);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+ }
+
+ g_array_unref (array);
+ gsk_path_unref (path);
+ }
+}
+
+static void
+test_curve_decompose_into_line (void)
+{
+ test_curve_decompose_into (0);
+}
+
+static void
+test_curve_decompose_into_quad (void)
+{
+ test_curve_decompose_into (GSK_PATH_FOREACH_ALLOW_QUAD);
+}
+
+static void
+test_curve_decompose_into_cubic (void)
+{
+ test_curve_decompose_into (GSK_PATH_FOREACH_ALLOW_CUBIC);
+}
+
+/* Some sanity checks for splitting curves. */
+static void
+test_curve_split (void)
+{
+ for (int i = 0; i < 100; i++)
+ {
+ GskCurve c;
+ GskCurve c1, c2;
+ graphene_point_t p;
+ graphene_vec2_t t, t1, t2;
+
+ init_random_curve (&c);
+
+ gsk_curve_split (&c, 0.5, &c1, &c2);
+
+ g_assert_true (c1.op == c.op);
+ g_assert_true (c2.op == c.op);
+
+ g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c),
+ gsk_curve_get_start_point (&c1), 0.005));
+ g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c1),
+ gsk_curve_get_start_point (&c2), 0.005));
+ g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c),
+ gsk_curve_get_end_point (&c2), 0.005));
+ gsk_curve_get_point (&c, 0.5, &p);
+ gsk_curve_get_tangent (&c, 0.5, &t);
+ g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c1), &p, 0.005));
+ g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c2), &p, 0.005));
+
+ gsk_curve_get_start_tangent (&c, &t1);
+ gsk_curve_get_start_tangent (&c1, &t2);
+ g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));
+ gsk_curve_get_end_tangent (&c1, &t1);
+ gsk_curve_get_start_tangent (&c2, &t2);
+ g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));
+ g_assert_true (graphene_vec2_near (&t, &t1, 0.005));
+ g_assert_true (graphene_vec2_near (&t, &t2, 0.005));
+ gsk_curve_get_end_tangent (&c, &t1);
+ gsk_curve_get_end_tangent (&c2, &t2);
+ g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));
+ }
+}
+
+int
+main (int argc, char *argv[])
+{
+ (g_test_init) (&argc, &argv, NULL);
+
+ g_test_add_func ("/curve/points", test_curve_points);
+ g_test_add_func ("/curve/tangents", test_curve_tangents);
+ g_test_add_func ("/curve/decompose", test_curve_decompose);
+ g_test_add_func ("/curve/decompose/into/line", test_curve_decompose_into_line);
+ g_test_add_func ("/curve/decompose/into/quad", test_curve_decompose_into_quad);
+ g_test_add_func ("/curve/decompose/into/cubic", test_curve_decompose_into_cubic);
+ g_test_add_func ("/curve/split", test_curve_split);
+
+ return g_test_run ();
+}
diff --git a/testsuite/gsk/meson.build b/testsuite/gsk/meson.build
index 9e104e664e..2ae7bbb22f 100644
--- a/testsuite/gsk/meson.build
+++ b/testsuite/gsk/meson.build
@@ -389,6 +389,8 @@ foreach t : tests
endforeach
internal_tests = [
+ [ 'curve' ],
+ [ 'curve-special-cases' ],
[ 'diff' ],
[ 'half-float' ],
['rounded-rect'],