Skip to content

Commit 24e2eb9

Browse files
committed
Use uint32 bit-shifting for rgb->rgba interleaving
1 parent d5ae437 commit 24e2eb9

File tree

2 files changed

+108
-8
lines changed

2 files changed

+108
-8
lines changed

addons/addon-image/src/kitty/KittyGraphicsHandler.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -632,18 +632,43 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
632632
}
633633

634634
const pixelCount = width * height;
635+
636+
if (image.format === KittyFormat.RGBA) {
637+
// RGBA: use bytes directly — no copy needed
638+
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * BYTES_PER_PIXEL_RGBA), width, height));
639+
}
640+
641+
// RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
642+
// 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
643+
// Assumes little-endian (all modern browsers/Node.js).
635644
const data = new Uint8ClampedArray(pixelCount * BYTES_PER_PIXEL_RGBA);
636-
const isRgba = image.format === KittyFormat.RGBA;
645+
const src32 = new Uint32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
646+
const dst32 = new Uint32Array(data.buffer);
647+
const alignedPixels = pixelCount & ~3; // round down to multiple of 4
637648

638649
let srcOffset = 0;
639650
let dstOffset = 0;
640-
for (let i = 0; i < pixelCount; i++) {
641-
data[dstOffset] = bytes[srcOffset];
642-
data[dstOffset + 1] = bytes[srcOffset + 1];
643-
data[dstOffset + 2] = bytes[srcOffset + 2];
644-
data[dstOffset + 3] = isRgba ? bytes[srcOffset + 3] : ALPHA_OPAQUE;
645-
srcOffset += bytesPerPixel;
646-
dstOffset += BYTES_PER_PIXEL_RGBA;
651+
for (let i = 0; i < alignedPixels; i += 4) {
652+
const b0 = src32[srcOffset++];
653+
const b1 = src32[srcOffset++];
654+
const b2 = src32[srcOffset++];
655+
// Little-endian: pixel bytes are [R,G,B] → uint32 ABGR layout
656+
dst32[dstOffset++] = (b0 & 0x00FFFFFF) | 0xFF000000;
657+
dst32[dstOffset++] = ((b0 >>> 24) | (b1 << 8)) & 0x00FFFFFF | 0xFF000000;
658+
dst32[dstOffset++] = ((b1 >>> 16) | (b2 << 16)) & 0x00FFFFFF | 0xFF000000;
659+
dst32[dstOffset++] = (b2 >>> 8) | 0xFF000000;
660+
}
661+
662+
// Handle remaining 1–3 pixels
663+
let srcByte = alignedPixels * BYTES_PER_PIXEL_RGB;
664+
let dstByte = alignedPixels * BYTES_PER_PIXEL_RGBA;
665+
for (let i = alignedPixels; i < pixelCount; i++) {
666+
data[dstByte] = bytes[srcByte];
667+
data[dstByte + 1] = bytes[srcByte + 1];
668+
data[dstByte + 2] = bytes[srcByte + 2];
669+
data[dstByte + 3] = ALPHA_OPAQUE;
670+
srcByte += BYTES_PER_PIXEL_RGB;
671+
dstByte += BYTES_PER_PIXEL_RGBA;
647672
}
648673

649674
return createImageBitmap(new ImageData(data, width, height));

addons/addon-image/test/KittyGraphics.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ const RAW_RGB_2X2 = Buffer.from([
5757
255, 0, 0, 0, 255, 0,
5858
0, 0, 255, 255, 255, 0
5959
]).toString('base64');
60+
// 5 pixels (1 uint32 block + 1 remainder) — tests block+tail boundary
61+
const RAW_RGB_5X1 = Buffer.from([
62+
255, 0, 0,
63+
0, 255, 0,
64+
0, 0, 255,
65+
255, 255, 0,
66+
255, 0, 255
67+
]).toString('base64');
68+
// 8 pixels (2 full uint32 blocks, 0 remainder) — tests multi-block path
69+
const RAW_RGB_4X2 = Buffer.from([
70+
255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0,
71+
255, 0, 255, 0, 255, 255, 128, 128, 128, 255, 255, 255
72+
]).toString('base64');
6073

6174
// Raw RGBA pixel data (f=32): 4 bytes per pixel, no header — requires s= and v=
6275
const RAW_RGBA_1X1_WHITE = Buffer.from([255, 255, 255, 255]).toString('base64');
@@ -71,6 +84,14 @@ const RAW_RGBA_2X2 = Buffer.from([
7184
255, 0, 0, 255, 0, 255, 0, 255,
7285
0, 0, 255, 255, 255, 255, 0, 255
7386
]).toString('base64');
87+
// 5 pixels — tests RGBA zero-copy with non-power-of-2 count
88+
const RAW_RGBA_5X1 = Buffer.from([
89+
255, 0, 0, 255,
90+
0, 255, 0, 255,
91+
0, 0, 255, 255,
92+
255, 255, 0, 255,
93+
255, 0, 255, 255
94+
]).toString('base64');
7495

7596
let ctx: ITestContext;
7697
test.beforeAll(async ({ browser }) => {
@@ -1449,6 +1470,29 @@ test.describe('Kitty Graphics Protocol', () => {
14491470
deepStrictEqual(await getPixel(0, 0, 0, 1), [0, 0, 255, 255]);
14501471
deepStrictEqual(await getPixel(0, 0, 1, 1), [255, 255, 0, 255]);
14511472
});
1473+
1474+
test('renders 5x1 row with block+remainder pixel layout', async () => {
1475+
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=5,v=1;${RAW_RGB_5X1}\x1b\\`);
1476+
await timeout(100);
1477+
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
1478+
deepStrictEqual(await getPixel(0, 0, 1, 0), [0, 255, 0, 255]);
1479+
deepStrictEqual(await getPixel(0, 0, 2, 0), [0, 0, 255, 255]);
1480+
deepStrictEqual(await getPixel(0, 0, 3, 0), [255, 255, 0, 255]);
1481+
deepStrictEqual(await getPixel(0, 0, 4, 0), [255, 0, 255, 255]);
1482+
});
1483+
1484+
test('renders 4x2 grid with multi-block pixel layout', async () => {
1485+
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=4,v=2;${RAW_RGB_4X2}\x1b\\`);
1486+
await timeout(100);
1487+
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
1488+
deepStrictEqual(await getPixel(0, 0, 1, 0), [0, 255, 0, 255]);
1489+
deepStrictEqual(await getPixel(0, 0, 2, 0), [0, 0, 255, 255]);
1490+
deepStrictEqual(await getPixel(0, 0, 3, 0), [255, 255, 0, 255]);
1491+
deepStrictEqual(await getPixel(0, 0, 0, 1), [255, 0, 255, 255]);
1492+
deepStrictEqual(await getPixel(0, 0, 1, 1), [0, 255, 255, 255]);
1493+
deepStrictEqual(await getPixel(0, 0, 2, 1), [128, 128, 128, 255]);
1494+
deepStrictEqual(await getPixel(0, 0, 3, 1), [255, 255, 255, 255]);
1495+
});
14521496
});
14531497

14541498
test.describe('Storage and dimensions', () => {
@@ -1465,6 +1509,20 @@ test.describe('Kitty Graphics Protocol', () => {
14651509
strictEqual(await getImageStorageLength(), 1);
14661510
deepStrictEqual(await getOrigSize(1), [2, 2]);
14671511
});
1512+
1513+
test('stores image with correct original dimensions (5x1)', async () => {
1514+
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=5,v=1;${RAW_RGB_5X1}\x1b\\`);
1515+
await timeout(100);
1516+
strictEqual(await getImageStorageLength(), 1);
1517+
deepStrictEqual(await getOrigSize(1), [5, 1]);
1518+
});
1519+
1520+
test('stores image with correct original dimensions (4x2)', async () => {
1521+
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=4,v=2;${RAW_RGB_4X2}\x1b\\`);
1522+
await timeout(100);
1523+
strictEqual(await getImageStorageLength(), 1);
1524+
deepStrictEqual(await getOrigSize(1), [4, 2]);
1525+
});
14681526
});
14691527

14701528
test.describe('Validation', () => {
@@ -1566,6 +1624,16 @@ test.describe('Kitty Graphics Protocol', () => {
15661624
deepStrictEqual(await getPixel(0, 0, 0, 1), [0, 0, 255, 255]);
15671625
deepStrictEqual(await getPixel(0, 0, 1, 1), [255, 255, 0, 255]);
15681626
});
1627+
1628+
test('renders 5x1 row with zero-copy pixel layout', async () => {
1629+
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=5,v=1;${RAW_RGBA_5X1}\x1b\\`);
1630+
await timeout(100);
1631+
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
1632+
deepStrictEqual(await getPixel(0, 0, 1, 0), [0, 255, 0, 255]);
1633+
deepStrictEqual(await getPixel(0, 0, 2, 0), [0, 0, 255, 255]);
1634+
deepStrictEqual(await getPixel(0, 0, 3, 0), [255, 255, 0, 255]);
1635+
deepStrictEqual(await getPixel(0, 0, 4, 0), [255, 0, 255, 255]);
1636+
});
15691637
});
15701638

15711639
test.describe('Storage and dimensions', () => {
@@ -1582,6 +1650,13 @@ test.describe('Kitty Graphics Protocol', () => {
15821650
strictEqual(await getImageStorageLength(), 1);
15831651
deepStrictEqual(await getOrigSize(1), [2, 2]);
15841652
});
1653+
1654+
test('stores image with correct original dimensions (5x1)', async () => {
1655+
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=5,v=1;${RAW_RGBA_5X1}\x1b\\`);
1656+
await timeout(100);
1657+
strictEqual(await getImageStorageLength(), 1);
1658+
deepStrictEqual(await getOrigSize(1), [5, 1]);
1659+
});
15851660
});
15861661

15871662
test.describe('Validation', () => {

0 commit comments

Comments
 (0)