From 8c523ed03919d6c5ba747283d6724eabaaa2b27f Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Fri, 10 Jul 2026 09:24:05 -0500 Subject: [PATCH] terminal: vectorize APC payload scanning Kitty graphics payloads are dispatched in bulk, but finding each slice boundary still examines every byte with a scalar loop. This leaves large direct base64 image transmissions parser-bound. Scan ordinary APC bytes using the vector width recommended for the compile target. Keep the scalar scan both as the tail and as the full fallback when the target has no recommended vector width. Test state-machine boundaries against byte-at-a-time parsing. A ReleaseFast APC parser benchmark over the same 64 MiB Kitty graphics corpus, with 10 warmups and 30 measured runs, produced: mean median scalar 37.6 ms 32.6 ms vectorized 22.3 ms 19.0 ms Hyperfine reports the vectorized version as 1.69 times faster overall, with the median runtime improving by approximately 42 percent. --- src/terminal/stream.zig | 45 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 0572015f2..efb06ecdc 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -793,6 +793,18 @@ pub fn Stream(comptime H: type) type { assert(self.parser.state == .sos_pm_apc_string); var end: usize = 0; + if (comptime std.simd.suggestVectorLength(u8)) |vector_len| { + const ByteVector = @Vector(vector_len, u8); + while (end + vector_len <= input.len) { + const bytes: ByteVector = input[end..][0..vector_len].*; + const invalid = (bytes == @as(ByteVector, @splat(0x18))) | + (bytes == @as(ByteVector, @splat(0x1A))) | + (bytes == @as(ByteVector, @splat(0x1B))) | + (bytes >= @as(ByteVector, @splat(0x80))); + if (@reduce(.Or, invalid)) break; + end += vector_len; + } + } while (end < input.len) { switch (input[end]) { // Not apc_put bytes: CAN/SUB/ESC and most C1 exit @@ -3899,11 +3911,14 @@ test "stream: apc aborted by CAN" { // CAN (0x18) aborts the APC string via the anywhere => ground // transition. Exiting the sos_pm_apc_string state emits apc_end, // and the trailing bytes are printed, not treated as APC data. - s.nextSlice("\x1b_Gabc\x18def"); + s.nextSlice("\x1b_Gabcdefghijklmnopqrstuvwxyz0123456789\x18def"); try testing.expectEqual(@as(usize, 1), s.handler.started); try testing.expectEqual(@as(usize, 1), s.handler.ended); - try testing.expectEqualStrings("Gabc", s.handler.buf[0..s.handler.len]); + try testing.expectEqualStrings( + "Gabcdefghijklmnopqrstuvwxyz0123456789", + s.handler.buf[0..s.handler.len], + ); } test "stream: apc scalar path matches" { @@ -3917,3 +3932,29 @@ test "stream: apc scalar path matches" { s.handler.buf[0..s.handler.len], ); } + +test "stream: apc vector boundaries match scalar path" { + const positions = [_]usize{ 15, 16, 17, 31, 32, 33, 63, 64, 65 }; + const controls = [_]u8{ 0x18, 0x1A, 0x1B, 0x80, 0xFF }; + + for (positions) |position| for (controls) |control| { + var input: [96]u8 = undefined; + input[0..3].* = "\x1b_G".*; + @memset(input[3 .. 3 + position], 'a'); + input[3 + position] = control; + input[4 + position] = '\\'; + const bytes = input[0 .. 5 + position]; + + var bulk: Stream(ApcTestHandler) = .init(.{}); + bulk.nextSlice(bytes); + var scalar: Stream(ApcTestHandler) = .init(.{}); + for (bytes) |byte| scalar.next(byte); + + try testing.expectEqual(scalar.handler.started, bulk.handler.started); + try testing.expectEqual(scalar.handler.ended, bulk.handler.ended); + try testing.expectEqualStrings( + scalar.handler.buf[0..scalar.handler.len], + bulk.handler.buf[0..bulk.handler.len], + ); + }; +}