This is an automated email from the git hooks/post-receive script.
git pushed a commit to reference refs/pull/51/head
in repository terminology.
View the commit online.
commit f326c4c1af96c62d55fb31678478ed65f430ccc7
Author: Cedric BAIL <[email protected]>
AuthorDate: Mon Aug 3 13:27:46 2026 -0600
simd: add scalar and NEON scanning kernels with a parity test
Two questions the intake path asks constantly: where does this run of plain
printable ASCII end, and widen it. Both are trivially vectorisable, so add
kernels for them under src/bin/simd/ and use them in the UTF-8 decoder.
Intrinsics, not hand-written assembly. EFL's only .S file is ARMv7-only and
its meson gate excludes it from every aarch64 build -- all of EFL's aarch64
SIMD is arm_neon.h intrinsics. For a loop shaped like memchr there is nothing
an .S buys over intrinsics that the compiler does not already do. There is
also no runtime probe: Advanced SIMD is mandatory in ARMv8-A, so a check
would only confirm what the architecture already guarantees.
Both forms are always compiled and both are exported, because the parity test
drives them against each other -- a kernel reachable only through dispatch
cannot be compared against the reference it is meant to match. Modelled on
EFL's evas_test_neon_ops.c: guard-padded buffers so a kernel that writes
outside its range fails even when the in-range bytes are right, every length
and alignment around the vector width, plus an exhaustive sweep of every byte
value at every position. Run as 'tytest simd_parity'.
TERMINOLOGY_SIMD_DISABLE mirrors EVAS_NEON_DISABLE: it turns the vector paths
off without a rebuild, which makes a suspected difference bisectable in the
field. simd_init() runs before tytest's argument loop, since the unit-test
path returns out of it without reaching tytest_common_init(), and the suite
is registered a second time with the variable set so the scalar kernels --
what every non-NEON target runs -- have end-to-end coverage too.
The kernel calls are guarded by an inline test on the first byte and a
minimum run length. Without the guard, input that is mostly non-ASCII pays a
call per character to be told the run is empty, which measured as a net loss.
Co-Authored-By: Claude Opus 5 <[email protected]>
---
src/bin/meson.build | 9 +
src/bin/simd/simd.c | 427 +++++++++++++++++++++++++++++++++++++++++++++
src/bin/simd/simd.h | 64 +++++++
src/bin/simd/simd_neon.c | 166 ++++++++++++++++++
src/bin/simd/simd_scalar.c | 63 +++++++
src/bin/termpty.c | 3 +
src/bin/tytest.c | 8 +
src/bin/tytest_common.c | 2 +
src/bin/unit_tests.h | 1 +
src/bin/utf8.c | 20 +++
tests/meson.build | 15 ++
11 files changed, 778 insertions(+)
diff --git a/src/bin/meson.build b/src/bin/meson.build
index 0972c6d7..b636ddd6 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -1,3 +1,8 @@
+simd_sources = ['simd/simd.c',
+ 'simd/simd_scalar.c',
+ 'simd/simd_neon.c',
+ 'simd/simd.h']
+
terminology_sources = ['private.h',
'about.c', 'about.h',
'colors.c', 'colors.h',
@@ -34,6 +39,7 @@ terminology_sources = ['private.h',
'md5.c', 'md5.h',
'utils.c', 'utils.h',
'utf8.c', 'utf8.h',
+ simd_sources,
'win.c', 'win.h',
'theme.c', 'theme.h',
'extns.c', 'extns.h',
@@ -63,6 +69,7 @@ tyfuzz_sources = ['termptyesc.c', 'termptyesc.h',
'theme.h',
'utils.c', 'utils.h',
'utf8.c', 'utf8.h',
+ simd_sources,
'tytest_common.c', 'tytest_common.h',
'tyfuzz.c']
tytest_sources = ['termptyesc.c', 'termptyesc.h',
@@ -79,6 +86,7 @@ tytest_sources = ['termptyesc.c', 'termptyesc.h',
'extns.c', 'extns.h',
'sb.c', 'sb.h',
'utf8.c', 'utf8.h',
+ simd_sources,
'utils.c', 'utils.h',
'theme.h',
'md5.c', 'md5.h',
@@ -100,6 +108,7 @@ tybench_sources = ['termptyesc.c', 'termptyesc.h',
'theme.h',
'utils.c', 'utils.h',
'utf8.c', 'utf8.h',
+ simd_sources,
'tytest_common.c', 'tytest_common.h',
'tybench.c']
diff --git a/src/bin/simd/simd.c b/src/bin/simd/simd.c
new file mode 100644
index 00000000..6e5a29f4
--- /dev/null
+++ b/src/bin/simd/simd.c
@@ -0,0 +1,427 @@
+/* Kernel selection and the runtime kill-switch. */
+#include "private.h"
+#include "simd.h"
+#include <stdlib.h>
+#include <string.h>
+
+#if defined(TERMINOLOGY_HAVE_NEON)
+static Eina_Bool _use_simd = EINA_TRUE;
+#else
+static Eina_Bool _use_simd = EINA_FALSE;
+#endif
+
+void
+simd_init(void)
+{
+#if defined(TERMINOLOGY_HAVE_NEON)
+ const char *s = getenv("TERMINOLOGY_SIMD_DISABLE");
+
+ /* Any non-empty value other than "0" disables. */
+ if (s && s[0] && strcmp(s, "0") != 0)
+ _use_simd = EINA_FALSE;
+#endif
+}
+
+Eina_Bool
+simd_enabled(void)
+{
+ return _use_simd;
+}
+
+size_t
+simd_scan_plain_ascii(const unsigned char *buf, size_t len)
+{
+#if defined(TERMINOLOGY_HAVE_NEON)
+ if (EINA_LIKELY(_use_simd))
+ return simd_scan_plain_ascii_neon(buf, len);
+#endif
+ return simd_scan_plain_ascii_scalar(buf, len);
+}
+
+size_t
+simd_scan_plain_ascii_u32(const Eina_Unicode *buf, size_t len)
+{
+#if defined(TERMINOLOGY_HAVE_NEON)
+ if (EINA_LIKELY(_use_simd))
+ return simd_scan_plain_ascii_u32_neon(buf, len);
+#endif
+ return simd_scan_plain_ascii_u32_scalar(buf, len);
+}
+
+size_t
+simd_rscan_nonzero(const unsigned char *buf, size_t len)
+{
+#if defined(TERMINOLOGY_HAVE_NEON)
+ if (EINA_LIKELY(_use_simd))
+ return simd_rscan_nonzero_neon(buf, len);
+#endif
+ return simd_rscan_nonzero_scalar(buf, len);
+}
+
+void
+simd_records_or_byte(void *buf, size_t n, size_t rec, size_t off,
+ unsigned char bit)
+{
+#if defined(TERMINOLOGY_HAVE_NEON)
+ if (EINA_LIKELY(_use_simd))
+ {
+ simd_records_or_byte_neon(buf, n, rec, off, bit);
+ return;
+ }
+#endif
+ simd_records_or_byte_scalar(buf, n, rec, off, bit);
+}
+
+void
+simd_widen_ascii(const unsigned char *buf, size_t len, Eina_Unicode *out)
+{
+#if defined(TERMINOLOGY_HAVE_NEON)
+ if (EINA_LIKELY(_use_simd))
+ {
+ simd_widen_ascii_neon(buf, len, out);
+ return;
+ }
+#endif
+ simd_widen_ascii_scalar(buf, len, out);
+}
+/* Parity tests: each vector kernel must agree with its scalar reference.
+ *
+ * Buffers are guard-padded so a kernel writing outside its range fails even
+ * when the in-range bytes are right, and every length and alignment around the
+ * vector width is walked, since the bugs live in the tail and at the seam
+ * between the vector body and the scalar remainder. */
+#if defined(BINARY_TYTEST)
+#include <assert.h>
+
+#define GUARD 32
+#define GUARD_BYTE 0xA5
+
+/* Deterministic: a parity failure has to be reproducible to be debuggable. */
+static unsigned int _seed = 0x9e3779b9;
+
+static unsigned int
+_rnd(void)
+{
+ _seed ^= _seed << 13;
+ _seed ^= _seed >> 17;
+ _seed ^= _seed << 5;
+ return _seed;
+}
+
+static unsigned char *
+_alloc_guarded(size_t len)
+{
+ unsigned char *base = malloc(len + 2 * GUARD);
+
+ assert(base != NULL);
+ memset(base, GUARD_BYTE, len + 2 * GUARD);
+ return base;
+}
+
+static Eina_Bool
+_guards_intact(const unsigned char *base, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < GUARD; i++)
+ {
+ if (base[i] != GUARD_BYTE)
+ return EINA_FALSE;
+ }
+ for (i = 0; i < GUARD; i++)
+ {
+ if (base[GUARD + len + i] != GUARD_BYTE)
+ return EINA_FALSE;
+ }
+ return EINA_TRUE;
+}
+
+/* Mostly printable ASCII so runs reach the vector body, salted with the exact
+ * boundary values the kernels test against. */
+static void
+_fill(unsigned char *p, size_t len, int density)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ {
+ if ((int)(_rnd() % 100) < density)
+ {
+ switch (_rnd() % 6)
+ {
+ case 0: p[i] = 0x00; break;
+ case 1: p[i] = 0x1f; break;
+ case 2: p[i] = 0x7f; break;
+ case 3: p[i] = 0x80; break;
+ case 4: p[i] = 0xff; break;
+ default: p[i] = (unsigned char)(_rnd() % 0x20); break;
+ }
+ }
+ else
+ p[i] = (unsigned char)(0x20 + (_rnd() % 0x5f));
+ }
+}
+
+#if defined(TERMINOLOGY_HAVE_NEON)
+
+static void
+_test_scan(void)
+{
+ size_t len, off;
+ int density;
+
+ for (len = 0; len <= 70; len++)
+ {
+ for (density = 0; density <= 100; density += 10)
+ {
+ for (off = 0; off < 16; off++)
+ {
+ unsigned char *base = _alloc_guarded(off + len);
+ unsigned char *p = base + GUARD + off;
+
+ _fill(p, len, density);
+ assert(simd_scan_plain_ascii_scalar(p, len) ==
+ simd_scan_plain_ascii_neon(p, len));
+ assert(_guards_intact(base, off + len));
+ free(base);
+ }
+ }
+ }
+}
+
+static void
+_test_scan_u32(void)
+{
+ size_t len, off, i;
+ int density;
+
+ for (len = 0; len <= 40; len++)
+ {
+ for (density = 0; density <= 100; density += 10)
+ {
+ for (off = 0; off < 8; off++)
+ {
+ /* Guarded in bytes, so an overread past the payload lands in
+ * guard bytes rather than in slack. */
+ unsigned char *base =
+ _alloc_guarded((off + len) * sizeof(Eina_Unicode));
+ Eina_Unicode *p = (Eina_Unicode *)(base + GUARD) + off;
+
+ for (i = 0; i < len; i++)
+ {
+ if ((int)(_rnd() % 100) < density)
+ {
+ switch (_rnd() % 7)
+ {
+ case 0: p[i] = 0x00; break;
+ case 1: p[i] = 0x1f; break;
+ case 2: p[i] = 0x7f; break;
+ case 3: p[i] = 0x80; break;
+ case 4: p[i] = 0x4e2d; break;
+ case 5: p[i] = 0x1f600; break;
+ default: p[i] = 0x80000000u | 0x1234; break;
+ }
+ }
+ else
+ p[i] = 0x20 + (_rnd() % 0x5f);
+ }
+
+ assert(simd_scan_plain_ascii_u32_scalar(p, len) ==
+ simd_scan_plain_ascii_u32_neon(p, len));
+ assert(_guards_intact(base,
+ (off + len) * sizeof(Eina_Unicode)));
+ free(base);
+ }
+ }
+ }
+}
+
+static void
+_test_rscan(void)
+{
+ size_t len, off, pos;
+ int density;
+
+ for (len = 0; len <= 70; len++)
+ {
+ for (density = 0; density <= 100; density += 25)
+ {
+ for (off = 0; off < 16; off++)
+ {
+ unsigned char *base = _alloc_guarded(off + len);
+ unsigned char *p = base + GUARD + off;
+
+ memset(p, 0, len);
+ for (pos = 0; pos < len; pos++)
+ {
+ if ((int)(_rnd() % 100) < density)
+ p[pos] = (unsigned char)(1 + (_rnd() % 255));
+ }
+
+ assert(simd_rscan_nonzero_scalar(p, len) ==
+ simd_rscan_nonzero_neon(p, len));
+ assert(_guards_intact(base, off + len));
+ free(base);
+ }
+ }
+ }
+
+ /* One non-zero byte walked across every position: pins down the lane
+ * arithmetic rather than trusting the random fill to have covered it. */
+ for (len = 1; len <= 40; len++)
+ {
+ for (pos = 0; pos < len; pos++)
+ {
+ unsigned char buf[48];
+
+ memset(buf, 0, sizeof(buf));
+ buf[pos] = 0x01;
+ assert(simd_rscan_nonzero_scalar(buf, len) == pos + 1);
+ assert(simd_rscan_nonzero_neon(buf, len) == pos + 1);
+ }
+ }
+}
+
+static void
+_test_records_or(void)
+{
+ size_t n, off, rec, i;
+
+ /* Both the vectorised 12-byte stride and one that must fall back. */
+ for (rec = 11; rec <= 12; rec++)
+ {
+ for (n = 0; n <= 40; n++)
+ {
+ for (off = 0; off < rec; off++)
+ {
+ unsigned char *a = _alloc_guarded(n * rec);
+ unsigned char *b = _alloc_guarded(n * rec);
+
+ for (i = 0; i < n * rec; i++)
+ {
+ unsigned char v = (unsigned char)(_rnd() & 0xff);
+
+ a[GUARD + i] = v;
+ b[GUARD + i] = v;
+ }
+
+ simd_records_or_byte_scalar(a + GUARD, n, rec, off, 0x40);
+ simd_records_or_byte_neon(b + GUARD, n, rec, off, 0x40);
+
+ assert(memcmp(a + GUARD, b + GUARD, n * rec) == 0);
+ assert(_guards_intact(a, n * rec));
+ assert(_guards_intact(b, n * rec));
+ free(a);
+ free(b);
+ }
+ }
+ }
+}
+
+static void
+_test_widen(void)
+{
+ size_t len, off, i;
+
+ for (len = 0; len <= 70; len++)
+ {
+ for (off = 0; off < 16; off++)
+ {
+ unsigned char *base = _alloc_guarded(off + len);
+ unsigned char *p = base + GUARD + off;
+ Eina_Unicode *o1 = calloc(len + 8, sizeof(Eina_Unicode));
+ Eina_Unicode *o2 = calloc(len + 8, sizeof(Eina_Unicode));
+
+ assert(o1 != NULL);
+ assert(o2 != NULL);
+ _fill(p, len, 0);
+ for (i = 0; i < 8; i++)
+ {
+ o1[len + i] = 0xdeadbeef;
+ o2[len + i] = 0xdeadbeef;
+ }
+
+ simd_widen_ascii_scalar(p, len, o1);
+ simd_widen_ascii_neon(p, len, o2);
+
+ assert(memcmp(o1, o2, (len + 8) * sizeof(Eina_Unicode)) == 0);
+ for (i = 0; i < 8; i++)
+ assert(o2[len + i] == 0xdeadbeef);
+ assert(_guards_intact(base, off + len));
+
+ free(base);
+ free(o1);
+ free(o2);
+ }
+ }
+}
+
+/* Every byte value, at every position, exhaustively. */
+static void
+_test_every_byte(void)
+{
+ unsigned int v;
+ size_t len, pos;
+
+ for (v = 0; v < 256; v++)
+ {
+ for (len = 1; len <= 40; len++)
+ {
+ for (pos = 0; pos < len; pos++)
+ {
+ unsigned char buf[64];
+
+ memset(buf, 'x', sizeof(buf));
+ buf[pos] = (unsigned char)v;
+ assert(simd_scan_plain_ascii_scalar(buf, len) ==
+ simd_scan_plain_ascii_neon(buf, len));
+ }
+ }
+ }
+}
+
+static void
+_test_every_u32_boundary(void)
+{
+ static const Eina_Unicode vals[] = {
+ 0x00, 0x01, 0x1f, 0x20, 0x21, 0x7d, 0x7e, 0x7f, 0x80, 0xa0,
+ 0x200b, 0x300, 0x4e2d, 0xfe00, 0x1f600, 0x80000000u
+ };
+ size_t v, len, pos, i;
+
+ for (v = 0; v < sizeof(vals) / sizeof(vals[0]); v++)
+ {
+ for (len = 1; len <= 20; len++)
+ {
+ for (pos = 0; pos < len; pos++)
+ {
+ Eina_Unicode buf[24];
+
+ for (i = 0; i < len; i++) buf[i] = 'x';
+ buf[pos] = vals[v];
+ assert(simd_scan_plain_ascii_u32_scalar(buf, len) ==
+ simd_scan_plain_ascii_u32_neon(buf, len));
+ }
+ }
+ }
+}
+
+#endif
+
+int
+tytest_simd_parity(void)
+{
+#if defined(TERMINOLOGY_HAVE_NEON)
+ _test_scan();
+ _test_scan_u32();
+ _test_rscan();
+ _test_records_or();
+ _test_widen();
+ _test_every_byte();
+ _test_every_u32_boundary();
+#endif
+ /* Without a vector kernel the scalar path is the only path. */
+ return 0;
+}
+
+#endif
diff --git a/src/bin/simd/simd.h b/src/bin/simd/simd.h
new file mode 100644
index 00000000..7583d822
--- /dev/null
+++ b/src/bin/simd/simd.h
@@ -0,0 +1,64 @@
+#ifndef TERMINOLOGY_SIMD_H_
+#define TERMINOLOGY_SIMD_H_ 1
+
+#include <Eina.h>
+#include <stddef.h>
+
+/* Scanning kernels for the pty intake path. Each has a scalar form and, where
+ * the architecture provides one, a vector form; both are exported so the
+ * parity test can compare them directly. */
+
+/* Advanced SIMD is mandatory in ARMv8-A, so no runtime probe is needed. */
+#if defined(__aarch64__) && !defined(__ARM_BIG_ENDIAN)
+# define TERMINOLOGY_HAVE_NEON 1
+#endif
+
+/* Index of the first byte that is not plain printable ASCII (0x20..0x7e),
+ * or len. */
+size_t simd_scan_plain_ascii(const unsigned char *buf, size_t len);
+size_t simd_scan_plain_ascii_scalar(const unsigned char *buf, size_t len);
+#if defined(TERMINOLOGY_HAVE_NEON)
+size_t simd_scan_plain_ascii_neon(const unsigned char *buf, size_t len);
+#endif
+
+/* Same, over decoded codepoints. */
+size_t simd_scan_plain_ascii_u32(const Eina_Unicode *buf, size_t len);
+size_t simd_scan_plain_ascii_u32_scalar(const Eina_Unicode *buf, size_t len);
+#if defined(TERMINOLOGY_HAVE_NEON)
+size_t simd_scan_plain_ascii_u32_neon(const Eina_Unicode *buf, size_t len);
+#endif
+
+/* Index one past the last non-zero byte, or 0 if every byte is zero. */
+size_t simd_rscan_nonzero(const unsigned char *buf, size_t len);
+size_t simd_rscan_nonzero_scalar(const unsigned char *buf, size_t len);
+#if defined(TERMINOLOGY_HAVE_NEON)
+size_t simd_rscan_nonzero_neon(const unsigned char *buf, size_t len);
+#endif
+
+/* OR 'bit' into byte 'off' of each of 'n' records of 'rec' bytes. 'off' must
+ * be less than 'rec'. */
+void simd_records_or_byte(void *buf, size_t n, size_t rec, size_t off,
+ unsigned char bit);
+void simd_records_or_byte_scalar(void *buf, size_t n, size_t rec, size_t off,
+ unsigned char bit);
+#if defined(TERMINOLOGY_HAVE_NEON)
+void simd_records_or_byte_neon(void *buf, size_t n, size_t rec, size_t off,
+ unsigned char bit);
+#endif
+
+/* Widen bytes already known to be plain printable ASCII into codepoints. */
+void simd_widen_ascii(const unsigned char *buf, size_t len, Eina_Unicode *out);
+void simd_widen_ascii_scalar(const unsigned char *buf, size_t len,
+ Eina_Unicode *out);
+#if defined(TERMINOLOGY_HAVE_NEON)
+void simd_widen_ascii_neon(const unsigned char *buf, size_t len,
+ Eina_Unicode *out);
+#endif
+
+/* Read TERMINOLOGY_SIMD_DISABLE, which switches the vector kernels off without
+ * a rebuild. Modelled on EFL's EVAS_NEON_DISABLE. */
+void simd_init(void);
+
+Eina_Bool simd_enabled(void);
+
+#endif
diff --git a/src/bin/simd/simd_neon.c b/src/bin/simd/simd_neon.c
new file mode 100644
index 00000000..3806798a
--- /dev/null
+++ b/src/bin/simd/simd_neon.c
@@ -0,0 +1,166 @@
+/* NEON implementations of the intake kernels, using arm_neon.h intrinsics as
+ * EFL's own aarch64 code does. */
+#include "private.h"
+#include "simd.h"
+#include <string.h>
+
+#if defined(TERMINOLOGY_HAVE_NEON)
+
+#include <arm_neon.h>
+
+size_t
+simd_scan_plain_ascii_neon(const unsigned char *buf, size_t len)
+{
+ const uint8x16_t lo = vdupq_n_u8(0x20);
+ const uint8x16_t hi = vdupq_n_u8(0x7f);
+ size_t i = 0;
+
+ for (; i + 16 <= len; i += 16)
+ {
+ uint8x16_t v = vld1q_u8(buf + i);
+ uint8x16_t bad;
+ uint64_t m;
+
+ /* c < 0x20 || c >= 0x7f. The second test folds DEL and every
+ * high-bit-set byte into one comparison, which is why the range is
+ * expressed as ">= 0x7f" rather than "== 0x7f || >= 0x80". */
+ bad = vorrq_u8(vcltq_u8(v, lo), vcgeq_u8(v, hi));
+
+ /* Collapse the 16 lanes into 16 nibbles of one 64-bit word, so the
+ * first offending byte is a trailing-zero count over four. aarch64 has
+ * no PMOVMSKB equivalent. */
+ m = vget_lane_u64(vreinterpret_u64_u8(
+ vshrn_n_u16(vreinterpretq_u16_u8(bad), 4)), 0);
+ if (m) return i + (size_t)(__builtin_ctzll(m) >> 2);
+ }
+
+ /* Tail: fewer than 16 bytes left. */
+ for (; i < len; i++)
+ {
+ unsigned char c = buf[i];
+
+ if ((c < 0x20) || (c >= 0x7f)) return i;
+ }
+ return len;
+}
+
+size_t
+simd_scan_plain_ascii_u32_neon(const Eina_Unicode *buf, size_t len)
+{
+ /* One unsigned compare instead of two: g - 0x20 wraps for anything below
+ * 0x20, pushing it above the 0x5e span that 0x20..0x7e occupies. */
+ const uint32x4_t bias = vdupq_n_u32(0x20);
+ const uint32x4_t span = vdupq_n_u32(0x7e - 0x20);
+ size_t i = 0;
+
+ for (; i + 4 <= len; i += 4)
+ {
+ uint32x4_t v = vld1q_u32((const uint32_t *)(buf + i));
+ uint32x4_t bad = vcgtq_u32(vsubq_u32(v, bias), span);
+ uint64_t m;
+
+ /* Narrow the four 32-bit lanes to four 16-bit ones, so the first
+ * offending lane is a trailing-zero count divided by sixteen. */
+ m = vget_lane_u64(vreinterpret_u64_u16(vmovn_u32(bad)), 0);
+ if (m) return i + (size_t)(__builtin_ctzll(m) >> 4);
+ }
+
+ for (; i < len; i++)
+ {
+ Eina_Unicode g = buf[i];
+
+ if ((g < 0x20) || (g >= 0x7f)) return i;
+ }
+ return len;
+}
+
+size_t
+simd_rscan_nonzero_neon(const unsigned char *buf, size_t len)
+{
+ size_t i = len;
+
+ while (i >= 16)
+ {
+ uint8x16_t v = vld1q_u8(buf + i - 16);
+ uint64_t m;
+
+ /* vtstq_u8(v, v) is 0xff in every lane whose byte is non-zero. */
+ m = vget_lane_u64(vreinterpret_u64_u8(
+ vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(v, v)),
+ 4)), 0);
+ if (m)
+ {
+ /* Highest set nibble is the last non-zero byte of this block. */
+ size_t lane = (size_t)(63 - __builtin_clzll(m)) >> 2;
+
+ return i - 16 + lane + 1;
+ }
+ i -= 16;
+ }
+
+ while (i > 0)
+ {
+ if (buf[i - 1]) return i;
+ i--;
+ }
+ return 0;
+}
+
+void
+simd_records_or_byte_neon(void *buf, size_t n, size_t rec, size_t off,
+ unsigned char bit)
+{
+ unsigned char *p = (unsigned char *)buf;
+ size_t i = 0;
+
+ /* A 12-byte stride lines up with the vector every four records, so the mask
+ * repeats every 48 bytes. Other strides use the scalar form. */
+ if ((rec == 12) && (off < rec))
+ {
+ unsigned char pat[48];
+ uint8x16_t m0, m1, m2;
+
+ memset(pat, 0, sizeof(pat));
+ pat[off] = bit;
+ pat[rec + off] = bit;
+ pat[2 * rec + off] = bit;
+ pat[3 * rec + off] = bit;
+ m0 = vld1q_u8(pat);
+ m1 = vld1q_u8(pat + 16);
+ m2 = vld1q_u8(pat + 32);
+
+ for (; i + 4 <= n; i += 4, p += 48)
+ {
+ vst1q_u8(p, vorrq_u8(vld1q_u8(p), m0));
+ vst1q_u8(p + 16, vorrq_u8(vld1q_u8(p + 16), m1));
+ vst1q_u8(p + 32, vorrq_u8(vld1q_u8(p + 32), m2));
+ }
+ }
+
+ for (; i < n; i++, p += rec)
+ p[off] |= bit;
+}
+
+void
+simd_widen_ascii_neon(const unsigned char *buf, size_t len, Eina_Unicode *out)
+{
+ size_t i = 0;
+
+ for (; i + 16 <= len; i += 16)
+ {
+ uint8x16_t v = vld1q_u8(buf + i);
+ /* Zero-extend 8 -> 16 -> 32 bits in two steps per half. */
+ uint16x8_t w0 = vmovl_u8(vget_low_u8(v));
+ uint16x8_t w1 = vmovl_u8(vget_high_u8(v));
+
+ vst1q_u32((uint32_t *)(out + i + 0), vmovl_u16(vget_low_u16(w0)));
+ vst1q_u32((uint32_t *)(out + i + 4), vmovl_u16(vget_high_u16(w0)));
+ vst1q_u32((uint32_t *)(out + i + 8), vmovl_u16(vget_low_u16(w1)));
+ vst1q_u32((uint32_t *)(out + i + 12), vmovl_u16(vget_high_u16(w1)));
+ }
+
+ for (; i < len; i++)
+ out[i] = buf[i];
+}
+
+#endif
diff --git a/src/bin/simd/simd_scalar.c b/src/bin/simd/simd_scalar.c
new file mode 100644
index 00000000..61c36e96
--- /dev/null
+++ b/src/bin/simd/simd_scalar.c
@@ -0,0 +1,63 @@
+/* Scalar reference implementations. The vector kernels must agree with these
+ * byte for byte; keep them obvious rather than clever. */
+#include "private.h"
+#include "simd.h"
+
+size_t
+simd_scan_plain_ascii_scalar(const unsigned char *buf, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ {
+ unsigned char c = buf[i];
+
+ if ((c < 0x20) || (c >= 0x7f)) return i;
+ }
+ return len;
+}
+
+size_t
+simd_scan_plain_ascii_u32_scalar(const Eina_Unicode *buf, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ {
+ Eina_Unicode g = buf[i];
+
+ if ((g < 0x20) || (g >= 0x7f)) return i;
+ }
+ return len;
+}
+
+size_t
+simd_rscan_nonzero_scalar(const unsigned char *buf, size_t len)
+{
+ while (len > 0)
+ {
+ if (buf[len - 1]) return len;
+ len--;
+ }
+ return 0;
+}
+
+void
+simd_records_or_byte_scalar(void *buf, size_t n, size_t rec, size_t off,
+ unsigned char bit)
+{
+ unsigned char *p = (unsigned char *)buf + off;
+ size_t i;
+
+ for (i = 0; i < n; i++, p += rec)
+ *p |= bit;
+}
+
+void
+simd_widen_ascii_scalar(const unsigned char *buf, size_t len, Eina_Unicode *out)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ out[i] = buf[i];
+}
diff --git a/src/bin/termpty.c b/src/bin/termpty.c
index f07acd80..259db2ff 100644
--- a/src/bin/termpty.c
+++ b/src/bin/termpty.c
@@ -8,6 +8,7 @@
#include "termptyops.h"
#include "backlog.h"
#include "utf8.h"
+#include "simd/simd.h"
#include "keyin.h"
#if !defined(BINARY_TYFUZZ) && !defined(BINARY_TYTEST)
# include "win.h"
@@ -47,6 +48,8 @@ int _termpty_log_dom = -1;
void
termpty_init(void)
{
+ simd_init();
+
if (_termpty_log_dom >= 0) return;
_termpty_log_dom = eina_log_domain_register("termpty", NULL);
diff --git a/src/bin/tytest.c b/src/bin/tytest.c
index dd99b46c..23db0a19 100644
--- a/src/bin/tytest.c
+++ b/src/bin/tytest.c
@@ -15,6 +15,7 @@
#include "termptyops.h"
#include "backlog.h"
#include "utf8.h"
+#include "simd/simd.h"
#include "termiointernals.h"
#include "tytest.h"
#include "unit_tests.h"
@@ -31,6 +32,7 @@ static struct {
tytest_func func;
} _tytests[] = {
{ "dummy", tytest_dummy },
+ { "simd_parity", tytest_simd_parity},
{ "sb_skip", tytest_sb_skip},
{ "sb_trim", tytest_sb_trim},
{ "sb_gap", tytest_sb_gap},
@@ -425,6 +427,12 @@ main(int argc, char **argv)
int chunk = 0;
int i;
+ /* Before the argument loop: the unit-test path returns out of it without
+ * ever reaching tytest_common_init(), and several of those tests drive the
+ * real parser. Leaving the kernels un-dispatched there would make
+ * TERMINOLOGY_SIMD_DISABLE silently ineffective for 'tytest all'. */
+ simd_init();
+
for (i = 1; i < argc; i++)
{
if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
diff --git a/src/bin/tytest_common.c b/src/bin/tytest_common.c
index 14edbe93..67cc7f3e 100644
--- a/src/bin/tytest_common.c
+++ b/src/bin/tytest_common.c
@@ -12,6 +12,7 @@
#include "termptyops.h"
#include "termiointernals.h"
#include "utf8.h"
+#include "simd/simd.h"
#include "tytest_common.h"
#if defined(BINARY_TYTEST)
#include "colors.h"
@@ -589,6 +590,7 @@ tytest_common_main_loop(void)
void
tytest_common_init(void)
{
+ simd_init();
_config = config_new();
_sd.config = _config;
_termpty_init(&_ty, _config);
diff --git a/src/bin/unit_tests.h b/src/bin/unit_tests.h
index aa498069..4743d168 100644
--- a/src/bin/unit_tests.h
+++ b/src/bin/unit_tests.h
@@ -6,6 +6,7 @@ typedef int (*tytest_func)(void);
/* list of tests */
int tytest_dummy(void);
+int tytest_simd_parity(void);
int tytest_sb_skip(void);
int tytest_sb_trim(void);
int tytest_sb_gap(void);
diff --git a/src/bin/utf8.c b/src/bin/utf8.c
index 0a1a1610..f2f82d6d 100644
--- a/src/bin/utf8.c
+++ b/src/bin/utf8.c
@@ -1,5 +1,6 @@
#include "private.h"
#include "utf8.h"
+#include "simd/simd.h"
/* How many bytes the sequence introduced by this lead byte occupies, or 0 if it
* cannot start one (a continuation byte, or a length this decoder will not
@@ -58,6 +59,25 @@ utf8_to_codepoints(const char *buf, int len, Eina_Unicode *codepoints,
while (i < len)
{
Eina_Unicode g;
+ size_t run;
+
+ /* Plain printable ASCII needs none of the per-character work below, so
+ * widen it a run at a time. The inline test on the first byte keeps
+ * mostly-non-ASCII input from calling the scanner per character. */
+ if (((unsigned char)buf[i] >= 0x20) && ((unsigned char)buf[i] < 0x7f))
+ run = simd_scan_plain_ascii((const unsigned char *)buf + i,
+ (size_t)(len - i));
+ else
+ run = 0;
+ /* Below this the scan-then-widen pair costs more than it saves. */
+ if (run >= 4)
+ {
+ simd_widen_ascii((const unsigned char *)buf + i, run,
+ codepoints + j);
+ i += (int)run;
+ j += (int)run;
+ continue;
+ }
if (buf[i])
{
diff --git a/tests/meson.build b/tests/meson.build
index 25ec8267..257e1ba4 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -28,8 +28,23 @@ if tests
timeout: 600)
endforeach
+ # Again with the vector kernels off, so the scalar path -- what non-NEON
+ # targets run -- has end-to-end coverage too.
+ test('escape-codes-scalar',
+ run_tests,
+ args: ['-v',
+ '-t', tytest.full_path(),
+ '-r', meson.current_source_dir() / 'tests.results',
+ '-d', meson.current_source_dir()],
+ env: {'TERMINOLOGY_SIMD_DISABLE': '1'},
+ depends: tytest,
+ workdir: meson.current_source_dir(),
+ timeout: 300)
+
# The in-process C unit tests compiled into tytest itself.
test('unit', tytest, args: ['all'], timeout: 120)
+ test('unit-scalar', tytest, args: ['all'],
+ env: {'TERMINOLOGY_SIMD_DISABLE': '1'}, timeout: 120)
endif
if benchmarks
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.