11package io .github .dfa1 .vortex .encoding ;
22
3+ import com .google .protobuf .InvalidProtocolBufferException ;
4+ import dev .vortex .proto .EncodingProtos ;
35import dev .vortex .proto .ScalarProtos ;
46import io .github .dfa1 .vortex .core .DType ;
57import io .github .dfa1 .vortex .core .PType ;
1012import java .nio .ByteOrder ;
1113import java .util .List ;
1214
13- /// Codec for ` fastlanes.bitpacked` — bit-packing for integer columns .
15+ /// Codec for {@code fastlanes.bitpacked} — spec-compliant FastLanes bit-packing .
1416///
15- /// Metadata (9 bytes): bit_width (u8) | frame_of_reference (i64 LE).
16- /// Values are shifted by the column minimum (frame-of-reference), then packed
17- /// bit_width bits each, LSB-first, contiguous. bit_width=0 means all values
18- /// equal the frame-of-reference.
17+ /// <p>Metadata: protobuf {@code BitPackedMetadata} — {@code bit_width u32} (tag 1),
18+ /// {@code offset u32} (tag 2, element offset within the first 1024-element block).
19+ ///
20+ /// <p>Buffer layout: {@code ceil((len + offset) / 1024)} blocks, each block {@code 128 * bit_width}
21+ /// bytes. Within each block the values are transposed using the FastLanes FL_ORDER permutation so
22+ /// that adjacent bit-planes are contiguous (enables SIMD-friendly decompression).
1923public final class BitpackedCodec implements Codec {
2024
25+ // FL_ORDER permutation from the FastLanes paper / spiraldb/fastlanes-rs.
26+ private static final int [] FL_ORDER = {0 , 4 , 2 , 6 , 1 , 5 , 3 , 7 };
27+
2128 @ Override
2229 public String encodingId () {
2330 return "fastlanes.bitpacked" ;
@@ -41,148 +48,244 @@ public EncodeResult encode(DType dtype, Object data) {
4148 PType ptype = ((DType .Primitive ) dtype ).ptype ();
4249 long [] longs = toLongs (data , ptype );
4350 int n = longs .length ;
51+ int typeBits = ptype .byteSize () * 8 ;
52+ long typeMask = typeMask (typeBits );
4453 boolean unsign = isUnsigned (ptype );
4554
46- long frameOfRef = 0L ;
47- long maxVal = 0L ;
55+ long signedMin = 0L ;
56+ long signedMax = 0L ;
57+ long maxUnsigned = 0L ;
4858 int bitWidth = 0 ;
4959
5060 if (n > 0 ) {
51- long min = longs [0 ];
52- long max = longs [0 ];
53- for (int i = 1 ; i < n ; i ++) {
61+ signedMin = longs [0 ];
62+ signedMax = longs [0 ];
63+ for (int i = 0 ; i < n ; i ++) {
5464 long v = longs [i ];
55- if (unsign ? Long .compareUnsigned (v , min ) < 0 : v < min ) {
56- min = v ;
65+ if (unsign ? Long .compareUnsigned (v , signedMin ) < 0 : v < signedMin ) {
66+ signedMin = v ;
67+ }
68+ if (unsign ? Long .compareUnsigned (v , signedMax ) > 0 : v > signedMax ) {
69+ signedMax = v ;
5770 }
58- if (unsign ? Long .compareUnsigned (v , max ) > 0 : v > max ) {
59- max = v ;
71+ long uv = v & typeMask ;
72+ if (Long .compareUnsigned (uv , maxUnsigned ) > 0 ) {
73+ maxUnsigned = uv ;
6074 }
6175 }
62- frameOfRef = min ;
63- maxVal = max ;
64- long range = max - min ;
65- bitWidth = (range == 0L ) ? 0 : (64 - Long .numberOfLeadingZeros (range ));
76+ bitWidth = maxUnsigned == 0L ? 0 : (Long .SIZE - Long .numberOfLeadingZeros (maxUnsigned ));
6677 }
6778
68- ByteBuffer packed = pack (longs , frameOfRef , bitWidth );
79+ ByteBuffer packed = packFastLanes (longs , n , bitWidth , typeBits );
6980
70- ByteBuffer meta = ByteBuffer .allocate (9 ).order (ByteOrder .LITTLE_ENDIAN );
71- meta .put ((byte ) bitWidth );
72- meta .putLong (frameOfRef );
73- meta .flip ();
81+ byte [] metaBytes = EncodingProtos .BitPackedMetadata .newBuilder ()
82+ .setBitWidth (bitWidth )
83+ .setOffset (0 )
84+ .build ()
85+ .toByteArray ();
7486
75- byte [] statsMin = n > 0 ? statsBytes (ptype , frameOfRef ) : null ;
76- byte [] statsMax = n > 0 ? statsBytes (ptype , maxVal ) : null ;
87+ byte [] statsMin = n > 0 ? statsBytes (ptype , signedMin ) : null ;
88+ byte [] statsMax = n > 0 ? statsBytes (ptype , signedMax ) : null ;
7789
78- EncodeNode root = new EncodeNode (encodingId (), meta , new EncodeNode [0 ], new int []{0 });
90+ EncodeNode root = new EncodeNode (encodingId (), ByteBuffer .wrap (metaBytes ),
91+ new EncodeNode [0 ], new int []{0 });
7992 return new EncodeResult (root , List .of (packed ), statsMin , statsMax );
8093 }
8194
8295 // ── Decode ────────────────────────────────────────────────────────────────
8396
8497 @ Override
85- public Array decode (DecodeContext ctx ) {
98+ public Array decode (DecodeContext ctx ) {
8699 ByteBuffer rawMeta = ctx .metadata ();
87- if (rawMeta == null || rawMeta . capacity () < 2 ) {
88- throw new IllegalStateException ("fastlanes.bitpacked: missing or truncated metadata" );
100+ if (rawMeta == null ) {
101+ throw new IllegalStateException ("fastlanes.bitpacked: missing metadata" );
89102 }
90- if (rawMeta .capacity () == 9 ) {
91- return decodeJava (ctx , rawMeta );
92- }
93- // 2-byte JNI format: [constant:u8, bit_width:u8] — FastLanes block packing
94- return decodeJni (ctx , rawMeta );
95- }
96103
97- private Array decodeJava (DecodeContext ctx , ByteBuffer rawMeta ) {
98- ByteBuffer meta = rawMeta .duplicate ().order (ByteOrder .LITTLE_ENDIAN );
99- int bitWidth = Byte .toUnsignedInt (meta .get (0 ));
100- long frameOfRef = meta .getLong (1 );
101-
102- PType ptype = ((DType .Primitive ) ctx .dtype ()).ptype ();
103- long rowCount = ctx .rowCount ();
104+ EncodingProtos .BitPackedMetadata meta ;
105+ try {
106+ byte [] bytes = new byte [rawMeta .remaining ()];
107+ rawMeta .duplicate ().get (bytes );
108+ meta = EncodingProtos .BitPackedMetadata .parseFrom (bytes );
109+ } catch (InvalidProtocolBufferException e ) {
110+ throw new IllegalStateException ("fastlanes.bitpacked: invalid metadata" , e );
111+ }
104112
105- MemorySegment packed = ctx .buffer (0 );
106- byte [] packedBytes = new byte [(int ) packed .byteSize ()];
107- MemorySegment .copy (packed , ValueLayout .JAVA_BYTE , 0 , packedBytes , 0 , packedBytes .length );
113+ int bitWidth = meta .getBitWidth ();
114+ int offset = meta .getOffset ();
115+ PType ptype = ((DType .Primitive ) ctx .dtype ()).ptype ();
116+ int typeBits = ptype .byteSize () * 8 ;
117+ long rowCount = ctx .rowCount ();
108118
109- long [] longs = unpack (packedBytes , (int ) rowCount , bitWidth , frameOfRef );
119+ MemorySegment packed = ctx .buffer (0 );
120+ long [] longs = fastlanesUnpack (packed , bitWidth , offset , typeBits , rowCount );
110121
111122 return new Array (ctx .dtype (), rowCount ,
112123 new MemorySegment []{fromLongs (longs , ptype )}, new Array [0 ], ArrayStats .empty ());
113124 }
114125
115- // Decode FastLanes block-transposed format (JNI/Rust Vortex writer).
116- // Layout: ceil(rowCount/1024) blocks, each block = bit_width * 16 * 8 bytes.
117- // Word index = bitPos * 16 + lane; bit j = bit bitPos of value at index (lane + j*16) in block.
118- private Array decodeJni (DecodeContext ctx , ByteBuffer rawMeta ) {
119- int bitWidth = Byte .toUnsignedInt (rawMeta .get (1 ));
120-
121- PType ptype = ((DType .Primitive ) ctx .dtype ()).ptype ();
122- long rowCount = ctx .rowCount ();
126+ // ── FastLanes pack ────────────────────────────────────────────────────────
123127
124- MemorySegment packed = ctx .buffer (0 );
125- long [] output = new long [(int ) rowCount ];
128+ private static ByteBuffer packFastLanes (long [] values , int n , int bitWidth , int typeBits ) {
129+ if (bitWidth == 0 || n == 0 ) {
130+ return ByteBuffer .wrap (new byte [0 ]);
131+ }
132+ int LANES = 1024 / typeBits ;
133+ int wordBytes = typeBits / 8 ;
134+ int blockCount = (n + 1023 ) / 1024 ;
135+ long typeMask = typeMask (typeBits );
136+ byte [] buf = new byte [blockCount * 128 * bitWidth ];
126137
127- int blockCount = (int ) ((rowCount + 1023 ) / 1024 );
128138 for (int block = 0 ; block < blockCount ; block ++) {
129- int blockStart = block * 1024 ;
130- long blockByteOff = (long ) block * bitWidth * 16L * 8L ;
131- for (int bitPos = 0 ; bitPos < bitWidth ; bitPos ++) {
132- for (int lane = 0 ; lane < 16 ; lane ++) {
133- long wordByteOff = blockByteOff + ((long ) bitPos * 16 + lane ) * 8L ;
134- long word = packed .get (
135- ValueLayout .JAVA_LONG_UNALIGNED .withOrder (ByteOrder .LITTLE_ENDIAN ),
136- wordByteOff );
137- for (int j = 0 ; j < 64 ; j ++) {
138- int vi = blockStart + lane + j * 16 ;
139- if (vi >= rowCount ) {
140- break ;
141- }
142- output [vi ] |= ((word >>> j ) & 1L ) << bitPos ;
139+ int blockByteOff = block * 128 * bitWidth ;
140+ int blockStart = block * 1024 ;
141+
142+ for (int row = 0 ; row < typeBits ; row ++) {
143+ int currWord = (row * bitWidth ) / typeBits ;
144+ int nextWord = ((row + 1 ) * bitWidth ) / typeBits ;
145+ int shift = (row * bitWidth ) % typeBits ;
146+ int remainingBits = (nextWord > currWord ) ? ((row + 1 ) * bitWidth ) % typeBits : 0 ;
147+ int currentBits = bitWidth - remainingBits ;
148+
149+ for (int lane = 0 ; lane < LANES ; lane ++) {
150+ int o = row / 8 ;
151+ int s = row % 8 ;
152+ int logicalIdx = blockStart + FL_ORDER [o ] * 16 + s * 128 + lane ;
153+ long value = (logicalIdx < n ) ? (values [logicalIdx ] & typeMask ) : 0L ;
154+
155+ int wordOff = blockByteOff + (LANES * currWord + lane ) * wordBytes ;
156+ long existing = readWordFromBuf (buf , wordOff , typeBits );
157+ existing |= (value << shift ) & typeMask ;
158+ writeWordToBuf (buf , wordOff , existing , typeBits );
159+
160+ if (remainingBits > 0 ) {
161+ int hiWordOff = blockByteOff + (LANES * nextWord + lane ) * wordBytes ;
162+ long existingHi = readWordFromBuf (buf , hiWordOff , typeBits );
163+ existingHi |= (value >>> currentBits ) & typeMask ;
164+ writeWordToBuf (buf , hiWordOff , existingHi , typeBits );
143165 }
144166 }
145167 }
146168 }
147-
148- return new Array (ctx .dtype (), rowCount ,
149- new MemorySegment []{fromLongs (output , ptype )}, new Array [0 ], ArrayStats .empty ());
169+ return ByteBuffer .wrap (buf );
150170 }
151171
152- // ── Bit packing ───────────────────────────────────────────────────────────
172+ // ── FastLanes unpack ──────────────────────────────────────────────────────
173+
174+ private static long [] fastlanesUnpack (
175+ MemorySegment buf , int bitWidth , int offset , int typeBits , long rowCount ) {
176+ long [] output = new long [(int ) rowCount ];
177+ if (bitWidth == 0 ) {
178+ return output ;
179+ }
180+
181+ int LANES = 1024 / typeBits ;
182+ int wordBytes = typeBits / 8 ;
183+ long totalElems = rowCount + offset ;
184+ int blockCount = (int ) ((totalElems + 1023 ) / 1024 );
185+ long bitMask = bitWidth == 64 ? -1L : (1L << bitWidth ) - 1L ;
153186
154- private static ByteBuffer pack (long [] values , long frameOfRef , int bitWidth ) {
155- int n = values .length ;
156- int totalBits = n * bitWidth ;
157- byte [] buf = new byte [(totalBits + 7 ) / 8 ];
187+ for (int block = 0 ; block < blockCount ; block ++) {
188+ long blockByteOff = (long ) block * 128L * bitWidth ;
189+ int blockLogicStart = block * 1024 - offset ;
190+
191+ for (int row = 0 ; row < typeBits ; row ++) {
192+ int currWord = (row * bitWidth ) / typeBits ;
193+ int nextWord = ((row + 1 ) * bitWidth ) / typeBits ;
194+ int shift = (row * bitWidth ) % typeBits ;
195+ int remainingBits = (nextWord > currWord ) ? ((row + 1 ) * bitWidth ) % typeBits : 0 ;
196+ int currentBits = bitWidth - remainingBits ;
197+
198+ for (int lane = 0 ; lane < LANES ; lane ++) {
199+ int o = row / 8 ;
200+ int s = row % 8 ;
201+ int logicalIdx = blockLogicStart + FL_ORDER [o ] * 16 + s * 128 + lane ;
202+
203+ if (logicalIdx < 0 || logicalIdx >= rowCount ) {
204+ continue ;
205+ }
206+
207+ long wordOff = blockByteOff + (long ) (LANES * currWord + lane ) * wordBytes ;
208+ long src = readWordFromSeg (buf , wordOff , typeBits );
209+
210+ long value ;
211+ if (remainingBits > 0 ) {
212+ long loMask = (1L << currentBits ) - 1L ;
213+ long lo = (src >>> shift ) & loMask ;
214+ long hiOff = blockByteOff + (long ) (LANES * nextWord + lane ) * wordBytes ;
215+ long hi = readWordFromSeg (buf , hiOff , typeBits )
216+ & ((1L << remainingBits ) - 1L );
217+ value = lo | (hi << currentBits );
218+ } else {
219+ value = (src >>> shift ) & bitMask ;
220+ }
158221
159- for (int i = 0 ; i < n ; i ++) {
160- long shifted = values [i ] - frameOfRef ;
161- int bitPos = i * bitWidth ;
162- for (int b = 0 ; b < bitWidth ; b ++) {
163- if (((shifted >>> b ) & 1L ) == 1L ) {
164- buf [(bitPos + b ) >>> 3 ] |= (byte ) (1 << ((bitPos + b ) & 7 ));
222+ output [logicalIdx ] = value ;
165223 }
166224 }
167225 }
168- return ByteBuffer . wrap ( buf ) ;
226+ return output ;
169227 }
170228
171- private static long [] unpack (byte [] packed , int rowCount , int bitWidth , long frameOfRef ) {
172- long [] out = new long [rowCount ];
173- for (int i = 0 ; i < rowCount ; i ++) {
174- long val = 0L ;
175- int bitPos = i * bitWidth ;
176- for (int b = 0 ; b < bitWidth ; b ++) {
177- int byteIdx = (bitPos + b ) >>> 3 ;
178- int bitIdx = (bitPos + b ) & 7 ;
179- if (byteIdx < packed .length && ((packed [byteIdx ] >>> bitIdx ) & 1 ) == 1 ) {
180- val |= 1L << b ;
181- }
229+ // ── Buffer helpers ────────────────────────────────────────────────────────
230+
231+ private static long readWordFromBuf (byte [] buf , int off , int typeBits ) {
232+ return switch (typeBits ) {
233+ case 8 -> buf [off ] & 0xFFL ;
234+ case 16 -> (buf [off ] & 0xFFL ) | ((buf [off + 1 ] & 0xFFL ) << 8 );
235+ case 32 -> Integer .toUnsignedLong (
236+ (buf [off ] & 0xFF ) | ((buf [off + 1 ] & 0xFF ) << 8 )
237+ | ((buf [off + 2 ] & 0xFF ) << 16 ) | ((buf [off + 3 ] & 0xFF ) << 24 ));
238+ case 64 -> {
239+ long lo = Integer .toUnsignedLong (
240+ (buf [off ] & 0xFF ) | ((buf [off + 1 ] & 0xFF ) << 8 )
241+ | ((buf [off + 2 ] & 0xFF ) << 16 ) | ((buf [off + 3 ] & 0xFF ) << 24 ));
242+ long hi = Integer .toUnsignedLong (
243+ (buf [off + 4 ] & 0xFF ) | ((buf [off + 5 ] & 0xFF ) << 8 )
244+ | ((buf [off + 6 ] & 0xFF ) << 16 ) | ((buf [off + 7 ] & 0xFF ) << 24 ));
245+ yield lo | (hi << 32 );
246+ }
247+ default -> throw new IllegalArgumentException ("unsupported typeBits: " + typeBits );
248+ };
249+ }
250+
251+ private static void writeWordToBuf (byte [] buf , int off , long value , int typeBits ) {
252+ switch (typeBits ) {
253+ case 8 -> buf [off ] = (byte ) value ;
254+ case 16 -> {
255+ buf [off ] = (byte ) value ;
256+ buf [off + 1 ] = (byte ) (value >>> 8 );
257+ }
258+ case 32 -> {
259+ buf [off ] = (byte ) value ;
260+ buf [off + 1 ] = (byte ) (value >>> 8 );
261+ buf [off + 2 ] = (byte ) (value >>> 16 );
262+ buf [off + 3 ] = (byte ) (value >>> 24 );
182263 }
183- out [i ] = val + frameOfRef ;
264+ case 64 -> {
265+ buf [off ] = (byte ) value ;
266+ buf [off + 1 ] = (byte ) (value >>> 8 );
267+ buf [off + 2 ] = (byte ) (value >>> 16 );
268+ buf [off + 3 ] = (byte ) (value >>> 24 );
269+ buf [off + 4 ] = (byte ) (value >>> 32 );
270+ buf [off + 5 ] = (byte ) (value >>> 40 );
271+ buf [off + 6 ] = (byte ) (value >>> 48 );
272+ buf [off + 7 ] = (byte ) (value >>> 56 );
273+ }
274+ default -> throw new IllegalArgumentException ("unsupported typeBits: " + typeBits );
184275 }
185- return out ;
276+ }
277+
278+ private static long readWordFromSeg (MemorySegment seg , long off , int typeBits ) {
279+ return switch (typeBits ) {
280+ case 8 -> Byte .toUnsignedLong (seg .get (ValueLayout .JAVA_BYTE , off ));
281+ case 16 -> Short .toUnsignedLong (
282+ seg .get (ValueLayout .JAVA_SHORT_UNALIGNED .withOrder (ByteOrder .LITTLE_ENDIAN ), off ));
283+ case 32 -> Integer .toUnsignedLong (
284+ seg .get (ValueLayout .JAVA_INT_UNALIGNED .withOrder (ByteOrder .LITTLE_ENDIAN ), off ));
285+ case 64 -> seg .get (
286+ ValueLayout .JAVA_LONG_UNALIGNED .withOrder (ByteOrder .LITTLE_ENDIAN ), off );
287+ default -> throw new IllegalArgumentException ("unsupported typeBits: " + typeBits );
288+ };
186289 }
187290
188291 // ── Type conversion ───────────────────────────────────────────────────────
@@ -247,7 +350,11 @@ private static MemorySegment fromLongs(long[] longs, PType ptype) {
247350 return MemorySegment .ofArray (bytes );
248351 }
249352
250- // ── Stats helpers ─────────────────────────────────────────────────────────
353+ // ── Misc helpers ──────────────────────────────────────────────────────────
354+
355+ private static long typeMask (int typeBits ) {
356+ return typeBits == 64 ? -1L : (1L << typeBits ) - 1L ;
357+ }
251358
252359 private static byte [] statsBytes (PType ptype , long value ) {
253360 if (isUnsigned (ptype )) {
0 commit comments