// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Vectorised kernels for whole uuencode lines, kept beside the scalar code //! in `uuencode.zig` and `uudecode.zig` so the two can be compared. //! //! uuencode suits SIMD better than base64 does. Base64 needs a lookup to map //! a six-bit group onto its alphabet; uuencode's alphabet is contiguous, so //! the map is `group + 0x20` and the only special case is the zero group, //! which is one blend. What is left is a shuffle and a handful of shifts. //! //! Everything here works on a full 45 byte line, which is exactly 15 groups //! of three bytes and 60 characters. Short lines, meaning the last one, stay //! on the scalar path where the padding rules live. const std = @import("std"); const builtin = @import("builtin"); const build_options = @import("build_options"); const assert = std.debug.assert; const uuencode = @import("uuencode.zig"); const uudecode = @import("uudecode.zig"); /// Whether the vector kernels are worth using on the target being compiled /// for. /// /// `@Vector` compiles everywhere: on a target with no vector registers it /// lowers to scalar code that is no faster than the ordinary loop, and can be /// slower once the shuffles are expanded by hand. So this is a question about /// the machine rather than about the language, and the answer is comptime, /// which folds the unused path away entirely. /// /// The thresholds here were measured rather than guessed, on a Ryzen 7 5800X /// with `zig build bench -Dcpu=...`. Encoding a 16 MiB payload against the /// scalar path: /// /// baseline (SSE2 only) 2.25x /// x86_64_v2 (SSE4.2) 2.07x /// x86_64_v3 (AVX2) 2.58x /// /// SSE2 alone already pays for itself, and SSE2 is part of the x86_64 /// baseline, so there is nothing to gate on there. Architectures that have /// not been measured default to the scalar path. /// /// `-Dsimd=false` forces the scalar path regardless. The build flag can only /// take the kernels away, never add them to a target that cannot run them. pub const available = build_options.simd and target_supported; /// Whether the target could use the kernels, ignoring the build flag. pub const target_supported = switch (builtin.cpu.arch) { // SSE2 is guaranteed by the x86_64 baseline. .x86_64 => true, .x86 => std.Target.x86.featureSetHas(builtin.cpu.features, .sse2), .aarch64, .aarch64_be => std.Target.aarch64.featureSetHas(builtin.cpu.features, .neon), .wasm32, .wasm64 => std.Target.wasm.featureSetHas(builtin.cpu.features, .simd128), else => false, }; pub const bytes_per_line = uuencode.bytes_per_line; pub const chars_per_full_line = 4 * (bytes_per_line / 3); const Line = @Vector(bytes_per_line, u8); const Chars = @Vector(chars_per_full_line, u8); /// For output character `p`, the byte of its three byte group at offset `k`. fn encodeGather(comptime k: i32) @Vector(chars_per_full_line, i32) { var mask: [chars_per_full_line]i32 = undefined; for (&mask, 0..) |*entry, p| entry.* = 3 * @as(i32, @intCast(p / 4)) + k; return mask; } /// Lanes holding the `k`th character of each four character group. fn encodeLanes(comptime k: usize) @Vector(chars_per_full_line, bool) { var mask: [chars_per_full_line]bool = undefined; for (&mask, 0..) |*entry, p| entry.* = p % 4 == k; return mask; } /// Encodes exactly one full line. `charset` is comptime so the zero group /// blend folds away entirely for `.space`, where no blend is needed at all. pub fn encodeFullLine( dest: *[chars_per_full_line]u8, source: *const [bytes_per_line]u8, comptime charset: uuencode.Charset, ) void { const in: Line = source.*; // Three copies of the input, aligned so that lane `p` of each holds the // first, second and third byte of the group that character `p` comes from. const a = @shuffle(u8, in, undefined, encodeGather(0)); const b = @shuffle(u8, in, undefined, encodeGather(1)); const c = @shuffle(u8, in, undefined, encodeGather(2)); const two: @Vector(chars_per_full_line, u3) = @splat(2); const four: @Vector(chars_per_full_line, u3) = @splat(4); const six: @Vector(chars_per_full_line, u3) = @splat(6); // Every lane computes all four possible characters; the blends below keep // the one that belongs to it. Branchless, and the masks are comptime. const first = a >> two; const second = ((a & @as(Chars, @splat(0x03))) << four) | (b >> four); const third = ((b & @as(Chars, @splat(0x0f))) << two) | (c >> six); const fourth = c & @as(Chars, @splat(0x3f)); const sextets = @select( u8, comptime encodeLanes(0), first, @select( u8, comptime encodeLanes(1), second, @select(u8, comptime encodeLanes(2), third, fourth), ), ); const shifted = sextets + @as(Chars, @splat(' ')); const chars = switch (charset) { .space => shifted, .backtick => @select( u8, sextets == @as(Chars, @splat(0)), @as(Chars, @splat('`')), shifted, ), }; dest.* = chars; } /// For output byte `q`, the sextet contributing its high bits, and then the /// one contributing its low bits. fn decodeGather(comptime k: i32) @Vector(bytes_per_line, i32) { var mask: [bytes_per_line]i32 = undefined; for (&mask, 0..) |*entry, q| { const group: i32 = @intCast(q / 3); const within: i32 = @intCast(q % 3); entry.* = 4 * group + within + k; } return mask; } /// How far the high and low contributors are shifted for each output byte. fn decodeShifts(comptime which: enum { high, low }) @Vector(bytes_per_line, u3) { var mask: [bytes_per_line]u3 = undefined; for (&mask, 0..) |*entry, q| { const within = q % 3; entry.* = switch (which) { .high => @intCast(2 * (within + 1)), .low => @intCast(4 - 2 * within), }; } return mask; } /// Decodes exactly one full line, returning false without touching `dest` if /// any character is outside the uuencode alphabet. A false return means the /// caller should rerun the line on the scalar path, which reports which /// character was wrong. pub fn decodeFullLine( dest: *[bytes_per_line]u8, source: *const [chars_per_full_line]u8, ) bool { const in: Chars = source.*; // One wrapping subtract turns the range check into a single unsigned // comparison: characters below a space wrap up past 0xc0, and anything // above a backtick lands above 0x40, so both fail the same test. const offset = in -% @as(Chars, @splat(' ')); if (@reduce(.Or, offset > @as(Chars, @splat(0x40)))) return false; // Masking to six bits is what makes the backtick and the space both mean // zero: 0x60 - 0x20 is 64, which is 0 once the seventh bit is dropped. const sextets = offset & @as(Chars, @splat(0x3f)); const high = @shuffle(u8, sextets, undefined, decodeGather(0)); const low = @shuffle(u8, sextets, undefined, decodeGather(1)); dest.* = (high << comptime decodeShifts(.high)) | (low >> comptime decodeShifts(.low)); return true; } const testing = std.testing; test "encodeFullLine matches the scalar encoder" { var prng: std.Random.DefaultPrng = .init(0x5114_D000); const random = prng.random(); var source: [bytes_per_line]u8 = undefined; var wide: [chars_per_full_line]u8 = undefined; var scalar: [uuencode.chars_per_line]u8 = undefined; for (0..2000) |i| { switch (i % 4) { 0 => random.bytes(&source), 1 => @memset(&source, 0), 2 => @memset(&source, 0xff), else => for (&source) |*byte| { byte.* = if (random.uintLessThan(u8, 4) == 0) random.int(u8) else 0; }, } inline for (.{ .backtick, .space }) |charset| { encodeFullLine(&wide, &source, charset); const expected = uuencode.encodeLineScalar(&scalar, &source, .{ .charset = charset }); // The scalar encoder also writes the length character first. try testing.expectEqualSlices(u8, expected[1..], &wide); } } } test "decodeFullLine matches the scalar decoder" { var prng: std.Random.DefaultPrng = .init(0xD3C0_DE00); const random = prng.random(); var source: [bytes_per_line]u8 = undefined; var encoded: [uuencode.chars_per_line]u8 = undefined; var wide: [bytes_per_line]u8 = undefined; var scalar: [bytes_per_line]u8 = undefined; for (0..2000) |_| { random.bytes(&source); const line = uuencode.encodeLineScalar(&encoded, &source, .{ .charset = if (random.boolean()) .backtick else .space, }); try testing.expect(decodeFullLine(&wide, line[1..][0..chars_per_full_line])); try testing.expectEqualSlices(u8, &source, &wide); const written = try uudecode.decodeLineScalar(&scalar, line, .{}); try testing.expectEqual(bytes_per_line, written); try testing.expectEqualSlices(u8, &scalar, &wide); } } test "decodeFullLine rejects characters outside the alphabet" { var source: [bytes_per_line]u8 = undefined; @memset(&source, 'x'); var encoded: [uuencode.chars_per_line]u8 = undefined; const line = uuencode.encodeLineScalar(&encoded, &source, .{}); var dest: [bytes_per_line]u8 = undefined; try testing.expect(decodeFullLine(&dest, line[1..][0..chars_per_full_line])); for ([_]u8{ 0, '\n', 0x1f, 'a', 0x7f, 0xff }) |bad| { var broken = line[1..][0..chars_per_full_line].*; broken[7] = bad; try testing.expect(!decodeFullLine(&dest, &broken)); } }