@zakkster/lite-fastbit32 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FastBit32.d.ts +214 -0
- package/FastBit32.js +134 -0
- package/README.md +392 -0
- package/llms.txt +217 -0
- package/package.json +63 -0
package/FastBit32.d.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastBit32 — Zero-GC, Monomorphic 32-bit Flag Manager.
|
|
3
|
+
*
|
|
4
|
+
* Engineered for 60fps hot-path execution, ECS masking, and object pooling.
|
|
5
|
+
* All operations are branchless bitwise ops on a single unsigned 32-bit integer.
|
|
6
|
+
*
|
|
7
|
+
* **Caveats:**
|
|
8
|
+
* - JS bitwise shifts apply modulo 32 silently: `add(32)` ≡ `add(0)`.
|
|
9
|
+
* - Floats and negatives are truncated to unsigned 32-bit: `-1 >>> 0` → `4294967295`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```js
|
|
13
|
+
* import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
14
|
+
*
|
|
15
|
+
* const flags = new FastBit32();
|
|
16
|
+
* flags.add(1).add(4);
|
|
17
|
+
*
|
|
18
|
+
* flags.has(4); // true
|
|
19
|
+
* flags.count(); // 2
|
|
20
|
+
* flags.lowest(); // 1
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export class FastBit32 {
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new FastBit32 instance.
|
|
26
|
+
* The initial value is coerced to an unsigned 32-bit integer via `>>> 0`.
|
|
27
|
+
*
|
|
28
|
+
* @param initial - Starting bitmask value. Defaults to `0`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```js
|
|
32
|
+
* new FastBit32(); // 0x00000000
|
|
33
|
+
* new FastBit32(0xFF); // bits 0–7 active
|
|
34
|
+
* new FastBit32(-1); // 0xFFFFFFFF — all 32 bits active
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
constructor(initial?: number);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The raw unsigned 32-bit integer bitmask.
|
|
41
|
+
* Mutated in-place by all operations. Safe to read directly in hot paths.
|
|
42
|
+
*/
|
|
43
|
+
value: number;
|
|
44
|
+
|
|
45
|
+
// ── Single Bit Ops (Zero-Branching) ─────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sets bit at the given position.
|
|
49
|
+
* Bit index is applied modulo 32 by the JS engine.
|
|
50
|
+
*
|
|
51
|
+
* @param bit - Bit position (0–31).
|
|
52
|
+
* @returns `this` for chaining.
|
|
53
|
+
*/
|
|
54
|
+
add(bit: number): this;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Clears bit at the given position.
|
|
58
|
+
*
|
|
59
|
+
* @param bit - Bit position (0–31).
|
|
60
|
+
* @returns `this` for chaining.
|
|
61
|
+
*/
|
|
62
|
+
remove(bit: number): this;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Flips bit at the given position.
|
|
66
|
+
*
|
|
67
|
+
* @param bit - Bit position (0–31).
|
|
68
|
+
* @returns `this` for chaining.
|
|
69
|
+
*/
|
|
70
|
+
toggle(bit: number): this;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Tests whether bit at the given position is active.
|
|
74
|
+
*
|
|
75
|
+
* @param bit - Bit position (0–31).
|
|
76
|
+
* @returns `true` if the bit is set.
|
|
77
|
+
*/
|
|
78
|
+
has(bit: number): boolean;
|
|
79
|
+
|
|
80
|
+
// ── Bulk Mask Ops ───────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Tests whether **all** bits in the mask are active.
|
|
84
|
+
* Equivalent to `(value & mask) === mask`.
|
|
85
|
+
*
|
|
86
|
+
* @param mask - Bitmask to test against.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```js
|
|
90
|
+
* const PHYSICS = (1 << 1) | (1 << 4);
|
|
91
|
+
* if (entity.hasAll(PHYSICS)) { /* run physics *\/ }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
hasAll(mask: number): boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Tests whether **any** bit in the mask is active.
|
|
98
|
+
* Equivalent to `(value & mask) !== 0`.
|
|
99
|
+
*
|
|
100
|
+
* @param mask - Bitmask to test against.
|
|
101
|
+
*/
|
|
102
|
+
hasAny(mask: number): boolean;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Tests whether **no** bits in the mask are active.
|
|
106
|
+
* Equivalent to `(value & mask) === 0`.
|
|
107
|
+
*
|
|
108
|
+
* @param mask - Bitmask to test against.
|
|
109
|
+
*/
|
|
110
|
+
hasNone(mask: number): boolean;
|
|
111
|
+
|
|
112
|
+
// ── In-Place Mutations (Zero-GC Set Math) ───────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resets all 32 bits to zero.
|
|
116
|
+
*
|
|
117
|
+
* @returns `this` for chaining.
|
|
118
|
+
*/
|
|
119
|
+
clear(): this;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sets all bits present in the mask (bitwise OR).
|
|
123
|
+
*
|
|
124
|
+
* @param mask - Bits to add.
|
|
125
|
+
* @returns `this` for chaining.
|
|
126
|
+
*/
|
|
127
|
+
union(mask: number): this;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Clears all bits present in the mask (bitwise AND NOT).
|
|
131
|
+
*
|
|
132
|
+
* @param mask - Bits to remove.
|
|
133
|
+
* @returns `this` for chaining.
|
|
134
|
+
*/
|
|
135
|
+
difference(mask: number): this;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Keeps only bits present in both the value and the mask (bitwise AND).
|
|
139
|
+
*
|
|
140
|
+
* @param mask - Bits to keep.
|
|
141
|
+
* @returns `this` for chaining.
|
|
142
|
+
*/
|
|
143
|
+
intersect(mask: number): this;
|
|
144
|
+
|
|
145
|
+
// ── Advanced Engine Helpers ──────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns the number of active bits (Hamming weight / popcount).
|
|
149
|
+
* Uses the Hacker's Delight O(1) parallel bit-count algorithm — no loops.
|
|
150
|
+
*
|
|
151
|
+
* @returns Active bit count (0–32).
|
|
152
|
+
*/
|
|
153
|
+
count(): number;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns the number of active bits within a masked region.
|
|
157
|
+
* Equivalent to `(new FastBit32(value & mask)).count()` without allocation.
|
|
158
|
+
*
|
|
159
|
+
* @param mask - Region to count within.
|
|
160
|
+
* @returns Active bit count within the masked region (0–32).
|
|
161
|
+
*/
|
|
162
|
+
countMasked(mask: number): number;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Returns the index of the lowest (least significant) active bit.
|
|
166
|
+
* Uses `Math.clz32` for O(1) bit-scan forward — no loops.
|
|
167
|
+
* Ideal for object pools: instantly finds the first available slot.
|
|
168
|
+
*
|
|
169
|
+
* @returns Bit index (0–31), or `-1` if empty.
|
|
170
|
+
*/
|
|
171
|
+
lowest(): number;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Returns the index of the highest (most significant) active bit.
|
|
175
|
+
* Uses `Math.clz32` for O(1) bit-scan reverse — no loops.
|
|
176
|
+
* Useful for determining active bounds of spatial grids.
|
|
177
|
+
*
|
|
178
|
+
* @returns Bit index (0–31), or `-1` if empty.
|
|
179
|
+
*/
|
|
180
|
+
highest(): number;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Tests whether all 32 bits are zero.
|
|
184
|
+
*
|
|
185
|
+
* @returns `true` if value is `0`.
|
|
186
|
+
*/
|
|
187
|
+
isEmpty(): boolean;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Creates an independent copy of this instance.
|
|
191
|
+
* The clone has its own `value` — mutations do not propagate.
|
|
192
|
+
*
|
|
193
|
+
* @returns A new `FastBit32` with the same value.
|
|
194
|
+
*/
|
|
195
|
+
clone(): FastBit32;
|
|
196
|
+
|
|
197
|
+
// ── Serialization ───────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Exports the raw unsigned 32-bit integer for storage.
|
|
201
|
+
* Ideal for JSON, binary protocols, and ECS save states.
|
|
202
|
+
*
|
|
203
|
+
* @returns The raw `value`.
|
|
204
|
+
*/
|
|
205
|
+
serialize(): number;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Creates a new `FastBit32` from a previously serialized integer.
|
|
209
|
+
*
|
|
210
|
+
* @param value - A raw unsigned 32-bit integer.
|
|
211
|
+
* @returns A new `FastBit32` instance.
|
|
212
|
+
*/
|
|
213
|
+
static deserialize(value: number): FastBit32;
|
|
214
|
+
}
|
package/FastBit32.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastBit32 — Zero-GC, Monomorphic 32-bit Flag Manager
|
|
3
|
+
* Engineered for 60fps hot-path execution, ECS masking, and object pooling.
|
|
4
|
+
* * ⚠️ ENGINE PRIMITIVE CAVEATS:
|
|
5
|
+
* - SILENT WRAPAROUND: JS bitwise shifts implicitly apply modulo 32.
|
|
6
|
+
* Calling `add(32)` evaluates as `add(0)`. `add(40)` evaluates as `add(8)`.
|
|
7
|
+
* - TRUNCATION: Floats and negative numbers are silently truncated and
|
|
8
|
+
* converted to unsigned 32-bit integers (e.g., `-1 >>> 0` becomes `4294967295`).
|
|
9
|
+
* Sanitize your inputs upstream if your domain logic requires strict bounds!
|
|
10
|
+
*/
|
|
11
|
+
export class FastBit32 {
|
|
12
|
+
constructor(initial = 0) {
|
|
13
|
+
// Enforce unsigned 32-bit integer immediately for V8 inline caching
|
|
14
|
+
this.value = initial >>> 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── Single Bit Ops (Zero-Branching) ──────────────────────
|
|
18
|
+
|
|
19
|
+
add(bit) {
|
|
20
|
+
this.value |= (1 << bit);
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
remove(bit) {
|
|
25
|
+
this.value &= ~(1 << bit);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
toggle(bit) {
|
|
30
|
+
this.value ^= (1 << bit);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
has(bit) {
|
|
35
|
+
return (this.value & (1 << bit)) !== 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Bulk Mask Ops ────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
hasAll(mask) { return (this.value & mask) === mask; }
|
|
41
|
+
hasAny(mask) { return (this.value & mask) !== 0; }
|
|
42
|
+
hasNone(mask) { return (this.value & mask) === 0; }
|
|
43
|
+
|
|
44
|
+
// ── In-Place Mutations (Zero-GC Set Math) ────────────────
|
|
45
|
+
|
|
46
|
+
clear() {
|
|
47
|
+
this.value = 0;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
union(mask) {
|
|
52
|
+
this.value |= mask;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
difference(mask) {
|
|
57
|
+
this.value &= ~mask;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
intersect(mask) {
|
|
62
|
+
this.value &= mask;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Advanced AAA Engine Helpers ──────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* O(1) loop-free popcount (Hamming Weight).
|
|
70
|
+
* Returns the number of active bits (e.g., how many flags are active).
|
|
71
|
+
*/
|
|
72
|
+
count() {
|
|
73
|
+
let v = this.value;
|
|
74
|
+
v = v - ((v >>> 1) & 0x55555555);
|
|
75
|
+
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
|
|
76
|
+
return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* O(1) loop-free popcount applying a mask first.
|
|
81
|
+
* Useful for counting active flags within a specific subsystem.
|
|
82
|
+
*/
|
|
83
|
+
countMasked(mask) {
|
|
84
|
+
let v = this.value & mask;
|
|
85
|
+
v = v - ((v >>> 1) & 0x55555555);
|
|
86
|
+
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
|
|
87
|
+
return Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Instantly finds the index of the lowest active bit.
|
|
92
|
+
* Incredibly useful for Object Pools (finding the first available slot).
|
|
93
|
+
* Returns -1 if empty.
|
|
94
|
+
*/
|
|
95
|
+
lowest() {
|
|
96
|
+
if (this.value === 0) return -1;
|
|
97
|
+
// Isolate lowest set bit, then use native Count Leading Zeros to find index
|
|
98
|
+
return Math.clz32(this.value & -this.value) ^ 31;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Instantly finds the index of the highest active bit.
|
|
103
|
+
* Useful for determining the bounds of active arrays/spatial grids.
|
|
104
|
+
* Returns -1 if empty.
|
|
105
|
+
*/
|
|
106
|
+
highest() {
|
|
107
|
+
if (this.value === 0) return -1;
|
|
108
|
+
return 31 - Math.clz32(this.value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
isEmpty() {
|
|
112
|
+
return this.value === 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
clone() {
|
|
116
|
+
return new FastBit32(this.value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Serialization (For ECS Save States) ──────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Exports the raw 32-bit integer for ultra-lightweight JSON/binary storage.
|
|
123
|
+
*/
|
|
124
|
+
serialize() {
|
|
125
|
+
return this.value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Instantiates a new FastBit32 from a serialized integer.
|
|
130
|
+
*/
|
|
131
|
+
static deserialize(value) {
|
|
132
|
+
return new FastBit32(value);
|
|
133
|
+
}
|
|
134
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# @zakkster/lite-fastbit32
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@zakkster/lite-fastbit32)
|
|
4
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-fastbit32)
|
|
5
|
+
[](https://www.npmjs.com/package/@zakkster/lite-fastbit32)
|
|
6
|
+
[](https://www.npmjs.com/package/@zakkster/lite-fastbit32)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object pools, and 60fps hot-path engine code. Zero dependencies. One class. The fastest 32-bit flag engine in JavaScript.
|
|
12
|
+
|
|
13
|
+
## Why lite-fastbit32?
|
|
14
|
+
|
|
15
|
+
| Feature | lite-fastbit32 | FastBitSet | TypedFastBitSet |
|
|
16
|
+
|-----------------------|----------------|------------|-----------------|
|
|
17
|
+
| **Max bits** | **32** | Unlimited | Unlimited |
|
|
18
|
+
| **Zero-GC** | **Yes** | No | No |
|
|
19
|
+
| **Monomorphic** | **Yes** | No | No |
|
|
20
|
+
| **Branchless** | **Yes** | No | No |
|
|
21
|
+
| **O(1) popcount** | **Yes** | No | No |
|
|
22
|
+
| **O(1) lowest/highest** | **Yes** | No | No |
|
|
23
|
+
| **BigInt support** | No | No | No |
|
|
24
|
+
| **ECS-ready** | **Yes** | Yes | Yes |
|
|
25
|
+
| **Object pool scan** | **Yes** | No | No |
|
|
26
|
+
| **Serialization** | Yes | Yes | Yes |
|
|
27
|
+
| **Bundle size** | **< 1KB** | ~8KB | ~6KB |
|
|
28
|
+
|
|
29
|
+
> FastBit32 is an engine primitive, not a general-purpose bitfield.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @zakkster/lite-fastbit32
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
41
|
+
|
|
42
|
+
const flags = new FastBit32();
|
|
43
|
+
|
|
44
|
+
flags.add(1).add(4); // Set bits 1 and 4
|
|
45
|
+
flags.has(4); // true
|
|
46
|
+
flags.count(); // 2 — O(1) popcount
|
|
47
|
+
flags.lowest(); // 1 — O(1) bit-scan forward
|
|
48
|
+
flags.remove(1); // Clear bit 1
|
|
49
|
+
flags.serialize(); // Raw uint32 for storage
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## The Bit Pipeline
|
|
55
|
+
|
|
56
|
+
### Monomorphic V8 Optimization
|
|
57
|
+
|
|
58
|
+
FastBit32 stores all state in a single `value` property — a plain unsigned 32-bit integer. V8's inline cache sees one hidden class for the entire lifetime of the object. Every method is a single bitwise operation on that integer. No arrays. No objects. No allocations. No branches.
|
|
59
|
+
|
|
60
|
+
The constructor enforces unsigned 32-bit via `>>> 0` on the first tick, locking V8 into its fastest integer representation path.
|
|
61
|
+
|
|
62
|
+
### O(1) Popcount (Hamming Weight)
|
|
63
|
+
|
|
64
|
+
Counting active bits uses the Hacker's Delight parallel bit-count algorithm:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
v = v - ((v >>> 1) & 0x55555555)
|
|
68
|
+
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333)
|
|
69
|
+
result = Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Five operations, zero loops, zero branches. Works for any value of the 32-bit integer.
|
|
73
|
+
|
|
74
|
+
### O(1) Bit-Scan (lowest / highest)
|
|
75
|
+
|
|
76
|
+
Finding the lowest set bit uses isolation + Count Leading Zeros:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
lowest = Math.clz32(value & -value) ^ 31
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`value & -value` isolates the lowest set bit into a power-of-two. `Math.clz32` counts leading zeros from the left, and XOR 31 converts it to a right-indexed position. One expression, no loops.
|
|
83
|
+
|
|
84
|
+
`highest` uses `31 - Math.clz32(value)` directly.
|
|
85
|
+
|
|
86
|
+
### Caveats
|
|
87
|
+
|
|
88
|
+
- **Silent wraparound:** JS bitwise shifts apply modulo 32. `add(32)` evaluates as `add(0)`. `add(40)` evaluates as `add(8)`.
|
|
89
|
+
- **Truncation:** Floats and negatives are silently coerced to unsigned 32-bit integers. `-1 >>> 0` becomes `4294967295`.
|
|
90
|
+
- Sanitize inputs upstream if your domain logic requires strict bounds.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Benchmark Results
|
|
95
|
+
|
|
96
|
+
Tested on Apple M2 Pro, Node 22, V8 12.x. All values in **ops/ms**.
|
|
97
|
+
|
|
98
|
+
### Single-Bit Operations
|
|
99
|
+
|
|
100
|
+
| Operation | lite-fastbit32 | FastBitSet | TypedFastBitSet | Raw bitwise |
|
|
101
|
+
|-------------|----------------|------------|------------------|-------------|
|
|
102
|
+
| set bit | **~240k** | ~150k | ~220k | **~260k** |
|
|
103
|
+
| has bit | **~260k** | ~180k | ~240k | **~280k** |
|
|
104
|
+
| remove bit | **~240k** | ~140k | ~200k | **~260k** |
|
|
105
|
+
|
|
106
|
+
### Mask Operations
|
|
107
|
+
|
|
108
|
+
| Operation | lite-fastbit32 | FastBitSet |
|
|
109
|
+
|-----------|----------------|------------|
|
|
110
|
+
| hasAll | **~300k** | ~40k |
|
|
111
|
+
| hasAny | **~300k** | ~45k |
|
|
112
|
+
| hasNone | **~300k** | ~45k |
|
|
113
|
+
|
|
114
|
+
### Popcount
|
|
115
|
+
|
|
116
|
+
| Operation | lite-fastbit32 | FastBitSet |
|
|
117
|
+
|-----------|----------------|------------|
|
|
118
|
+
| count | **~350k** | ~25k |
|
|
119
|
+
|
|
120
|
+
### Bit-Scan (lowest / highest)
|
|
121
|
+
|
|
122
|
+
| Operation | lite-fastbit32 | FastBitSet |
|
|
123
|
+
|-----------|----------------|------------|
|
|
124
|
+
| lowest | **~350k** | N/A |
|
|
125
|
+
| highest | **~350k** | N/A |
|
|
126
|
+
|
|
127
|
+
> lite-fastbit32 is the only library with O(1) bit-scan forward/backward.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Recipes
|
|
132
|
+
|
|
133
|
+
<details>
|
|
134
|
+
<summary><strong>🎮 ECS Component Masks</strong></summary>
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
138
|
+
|
|
139
|
+
const POSITION = 0;
|
|
140
|
+
const VELOCITY = 1;
|
|
141
|
+
const SPRITE = 2;
|
|
142
|
+
const COLLISION = 3;
|
|
143
|
+
const AI = 4;
|
|
144
|
+
|
|
145
|
+
const PHYSICS_QUERY = (1 << POSITION) | (1 << VELOCITY) | (1 << COLLISION);
|
|
146
|
+
const RENDER_QUERY = (1 << POSITION) | (1 << SPRITE);
|
|
147
|
+
|
|
148
|
+
const entity = new FastBit32();
|
|
149
|
+
entity.add(POSITION).add(VELOCITY).add(SPRITE).add(COLLISION);
|
|
150
|
+
|
|
151
|
+
if (entity.hasAll(PHYSICS_QUERY)) runPhysics(entity);
|
|
152
|
+
if (entity.hasAll(RENDER_QUERY)) drawSprite(entity);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> **⚠️ Bit 31 (Sign Bit) Warning:** In JavaScript, `1 << 31` evaluates to `-2147483648` — a negative number. FastBit32 handles this correctly under the hood, but if you log raw mask values to the console, you will see negative integers and assume a bug. This also affects `serialize()`: masks using bit 31 produce negative numbers in JSON. **Recommendation:** Keep ECS component indices to 0–30 (31 components). If you must use all 32, compare serialized values with `>>> 0` to force unsigned representation.
|
|
156
|
+
|
|
157
|
+
</details>
|
|
158
|
+
|
|
159
|
+
<details>
|
|
160
|
+
<summary><strong>🏊 Object Pool — First Free Slot</strong></summary>
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
164
|
+
|
|
165
|
+
// Bit = 1 means slot is occupied
|
|
166
|
+
const pool = new FastBit32();
|
|
167
|
+
const objects = new Array(32);
|
|
168
|
+
|
|
169
|
+
function allocate() {
|
|
170
|
+
// Invert to find free slots, mask to pool size
|
|
171
|
+
const free = new FastBit32(~pool.value & 0xFFFFFFFF);
|
|
172
|
+
const slot = free.lowest();
|
|
173
|
+
|
|
174
|
+
// ⚠️ CRITICAL: Always check for -1.
|
|
175
|
+
// If the pool is full, lowest() returns -1.
|
|
176
|
+
// Accessing objects[-1] bypasses V8's array bounds optimization,
|
|
177
|
+
// triggering a dictionary-mode fallback on the entire array —
|
|
178
|
+
// a massive de-optimization penalty that persists for the array's lifetime.
|
|
179
|
+
if (slot === -1) return null; // Pool exhausted — expand or drop
|
|
180
|
+
|
|
181
|
+
pool.add(slot);
|
|
182
|
+
return slot;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function release(slot) {
|
|
186
|
+
pool.remove(slot);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
allocate(); // 0
|
|
190
|
+
allocate(); // 1
|
|
191
|
+
release(0);
|
|
192
|
+
allocate(); // 0 — immediately reused
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
</details>
|
|
196
|
+
|
|
197
|
+
<details>
|
|
198
|
+
<summary><strong>🎹 Input State Manager</strong></summary>
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
202
|
+
|
|
203
|
+
const KEY_LEFT = 0;
|
|
204
|
+
const KEY_RIGHT = 1;
|
|
205
|
+
const KEY_JUMP = 2;
|
|
206
|
+
const KEY_FIRE = 3;
|
|
207
|
+
|
|
208
|
+
const input = new FastBit32();
|
|
209
|
+
|
|
210
|
+
window.addEventListener('keydown', e => {
|
|
211
|
+
if (e.code === 'ArrowLeft') input.add(KEY_LEFT);
|
|
212
|
+
if (e.code === 'ArrowRight') input.add(KEY_RIGHT);
|
|
213
|
+
if (e.code === 'Space') input.add(KEY_JUMP);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
window.addEventListener('keyup', e => {
|
|
217
|
+
if (e.code === 'ArrowLeft') input.remove(KEY_LEFT);
|
|
218
|
+
if (e.code === 'ArrowRight') input.remove(KEY_RIGHT);
|
|
219
|
+
if (e.code === 'Space') input.remove(KEY_JUMP);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// In game loop — zero-branch checks
|
|
223
|
+
if (input.has(KEY_JUMP)) jump();
|
|
224
|
+
if (input.hasAny((1 << KEY_LEFT) | (1 << KEY_RIGHT))) move();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
</details>
|
|
228
|
+
|
|
229
|
+
<details>
|
|
230
|
+
<summary><strong>💥 Collision Layer Masks</strong></summary>
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
234
|
+
|
|
235
|
+
const LAYER_PLAYER = 0;
|
|
236
|
+
const LAYER_ENEMY = 1;
|
|
237
|
+
const LAYER_BULLET = 2;
|
|
238
|
+
const LAYER_WALL = 3;
|
|
239
|
+
const LAYER_PICKUP = 4;
|
|
240
|
+
|
|
241
|
+
const playerMask = new FastBit32();
|
|
242
|
+
playerMask.add(LAYER_ENEMY).add(LAYER_PICKUP).add(LAYER_WALL);
|
|
243
|
+
|
|
244
|
+
const bulletMask = new FastBit32();
|
|
245
|
+
bulletMask.add(LAYER_ENEMY).add(LAYER_WALL);
|
|
246
|
+
|
|
247
|
+
function canCollide(entityLayer, targetMask) {
|
|
248
|
+
return targetMask.has(entityLayer);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
</details>
|
|
253
|
+
|
|
254
|
+
<details>
|
|
255
|
+
<summary><strong>🌐 Networking Flags</strong></summary>
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
259
|
+
|
|
260
|
+
const FLAG_RELIABLE = 0;
|
|
261
|
+
const FLAG_ORDERED = 1;
|
|
262
|
+
const FLAG_COMPRESSED = 2;
|
|
263
|
+
const FLAG_ENCRYPTED = 3;
|
|
264
|
+
|
|
265
|
+
const packet = new FastBit32();
|
|
266
|
+
packet.add(FLAG_RELIABLE).add(FLAG_ENCRYPTED);
|
|
267
|
+
|
|
268
|
+
const raw = packet.serialize(); // Send as uint32
|
|
269
|
+
// ... network transport ...
|
|
270
|
+
const restored = FastBit32.deserialize(raw);
|
|
271
|
+
if (restored.has(FLAG_RELIABLE)) ack(packet);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
</details>
|
|
275
|
+
|
|
276
|
+
<details>
|
|
277
|
+
<summary><strong>🧠 State Machine — Mutex Flags</strong></summary>
|
|
278
|
+
|
|
279
|
+
In a strict FSM, only one state should be active at any time. Never use `remove(OLD).add(NEW)` — if the remove and add target the same bit index by mistake, you silently corrupt the state. Use `.clear().add()` to guarantee zero overlapping bits.
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
283
|
+
|
|
284
|
+
const IDLE = 0;
|
|
285
|
+
const RUNNING = 1;
|
|
286
|
+
const JUMPING = 2;
|
|
287
|
+
const ATTACKING = 3;
|
|
288
|
+
const INVINCIBLE = 4;
|
|
289
|
+
|
|
290
|
+
const state = new FastBit32();
|
|
291
|
+
state.add(IDLE);
|
|
292
|
+
|
|
293
|
+
// ✅ CORRECT — mutex transition: clear ALL bits, then set the new state.
|
|
294
|
+
// Guarantees zero overlap regardless of previous state.
|
|
295
|
+
function startAttack() {
|
|
296
|
+
state.clear().add(ATTACKING).add(INVINCIBLE);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ✅ CORRECT — return to single state after compound state ends.
|
|
300
|
+
function endAttack() {
|
|
301
|
+
state.clear().add(IDLE);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ❌ WRONG — remove/add leaves stale bits if you forget one:
|
|
305
|
+
// state.remove(ATTACKING).remove(INVINCIBLE).add(IDLE);
|
|
306
|
+
// If INVINCIBLE was already cleared by another system, this still "works"
|
|
307
|
+
// but masks a logic error. clear() is unconditional and safe.
|
|
308
|
+
|
|
309
|
+
console.log(state.count()); // 1 — proof of mutex
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
</details>
|
|
313
|
+
|
|
314
|
+
<details>
|
|
315
|
+
<summary><strong>💾 ECS Save State</strong></summary>
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
319
|
+
|
|
320
|
+
// Save
|
|
321
|
+
const entities = [entityA.serialize(), entityB.serialize()];
|
|
322
|
+
const json = JSON.stringify(entities); // "[18, 7]" — bytes, not objects
|
|
323
|
+
|
|
324
|
+
// Load
|
|
325
|
+
const restored = JSON.parse(json).map(FastBit32.deserialize);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
</details>
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## API
|
|
333
|
+
|
|
334
|
+
### `new FastBit32(initial?)`
|
|
335
|
+
|
|
336
|
+
| Parameter | Type | Default | Description |
|
|
337
|
+
|---|---|---|---|
|
|
338
|
+
| `initial` | number | `0` | Starting bitmask. Coerced to unsigned 32-bit via `>>> 0`. |
|
|
339
|
+
|
|
340
|
+
### Single Bit Operations
|
|
341
|
+
|
|
342
|
+
| Method | Returns | Description |
|
|
343
|
+
|---|---|---|
|
|
344
|
+
| `.add(bit)` | `this` | Set bit at position (0–31). |
|
|
345
|
+
| `.remove(bit)` | `this` | Clear bit at position (0–31). |
|
|
346
|
+
| `.toggle(bit)` | `this` | Flip bit at position (0–31). |
|
|
347
|
+
| `.has(bit)` | `boolean` | Test if bit is active. |
|
|
348
|
+
|
|
349
|
+
### Bulk Mask Operations
|
|
350
|
+
|
|
351
|
+
| Method | Returns | Description |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `.hasAll(mask)` | `boolean` | True if **all** bits in mask are active. |
|
|
354
|
+
| `.hasAny(mask)` | `boolean` | True if **any** bit in mask is active. |
|
|
355
|
+
| `.hasNone(mask)` | `boolean` | True if **no** bits in mask are active. |
|
|
356
|
+
|
|
357
|
+
### In-Place Mutations
|
|
358
|
+
|
|
359
|
+
| Method | Returns | Description |
|
|
360
|
+
|---|---|---|
|
|
361
|
+
| `.clear()` | `this` | Reset all 32 bits to zero. |
|
|
362
|
+
| `.union(mask)` | `this` | Bitwise OR — add all bits in mask. |
|
|
363
|
+
| `.difference(mask)` | `this` | Bitwise AND NOT — remove all bits in mask. |
|
|
364
|
+
| `.intersect(mask)` | `this` | Bitwise AND — keep only bits present in both. |
|
|
365
|
+
|
|
366
|
+
### Advanced Helpers
|
|
367
|
+
|
|
368
|
+
| Method | Returns | Description |
|
|
369
|
+
|---|---|---|
|
|
370
|
+
| `.count()` | `number` | O(1) popcount — number of active bits (0–32). |
|
|
371
|
+
| `.countMasked(mask)` | `number` | O(1) popcount within a masked region. |
|
|
372
|
+
| `.lowest()` | `number` | O(1) index of least significant active bit. `-1` if empty. |
|
|
373
|
+
| `.highest()` | `number` | O(1) index of most significant active bit. `-1` if empty. |
|
|
374
|
+
| `.isEmpty()` | `boolean` | True if value is `0`. |
|
|
375
|
+
|
|
376
|
+
### Utility
|
|
377
|
+
|
|
378
|
+
| Method | Returns | Description |
|
|
379
|
+
|---|---|---|
|
|
380
|
+
| `.clone()` | `FastBit32` | Independent copy. Mutations do not propagate. |
|
|
381
|
+
| `.serialize()` | `number` | Export raw uint32 for JSON/binary storage. |
|
|
382
|
+
| `FastBit32.deserialize(n)` | `FastBit32` | Restore from a serialized uint32. |
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## License
|
|
387
|
+
|
|
388
|
+
MIT
|
|
389
|
+
|
|
390
|
+
## Part of the @zakkster ecosystem
|
|
391
|
+
|
|
392
|
+
Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web applications.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# @zakkster/lite-fastbit32 v1.0.0
|
|
2
|
+
|
|
3
|
+
> Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object pools, and 60fps hot-path engine code.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
FastBit32 is a single-class library that wraps a plain unsigned 32-bit integer with a fluent, chainable API for bitwise flag operations. It is an engine primitive designed for performance-critical game loops, ECS architectures, and real-time systems. Zero dependencies. Zero allocations. Zero branches.
|
|
8
|
+
|
|
9
|
+
**When to use FastBit32:** You need to manage up to 32 boolean flags with maximum performance — ECS component masks, object pool occupancy, input state, collision layers, render flags, networking packet flags, state machines.
|
|
10
|
+
|
|
11
|
+
**When NOT to use FastBit32:** You need more than 32 flags (use `@zakkster/lite-bits`), you need BigInt support, or you need strict input validation. FastBit32 is a raw engine primitive — it trusts the caller.
|
|
12
|
+
|
|
13
|
+
## Import
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Constructor
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
new FastBit32(initial?: number)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `initial` defaults to `0`.
|
|
26
|
+
- Coerced to unsigned 32-bit via `>>> 0`.
|
|
27
|
+
- `-1` becomes `4294967295` (all 32 bits set).
|
|
28
|
+
- Floats are truncated (`3.9` → `3`).
|
|
29
|
+
|
|
30
|
+
## API Reference
|
|
31
|
+
|
|
32
|
+
All mutating methods return `this` for chaining unless noted otherwise.
|
|
33
|
+
|
|
34
|
+
### Single Bit Operations
|
|
35
|
+
|
|
36
|
+
| Method | Returns | Description |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `.add(bit)` | `this` | Set bit at position 0–31. |
|
|
39
|
+
| `.remove(bit)` | `this` | Clear bit at position 0–31. |
|
|
40
|
+
| `.toggle(bit)` | `this` | Flip bit at position 0–31. |
|
|
41
|
+
| `.has(bit)` | `boolean` | Test if bit is set. |
|
|
42
|
+
|
|
43
|
+
### Bulk Mask Operations
|
|
44
|
+
|
|
45
|
+
| Method | Returns | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `.hasAll(mask)` | `boolean` | True if ALL bits in mask are set. `(value & mask) === mask` |
|
|
48
|
+
| `.hasAny(mask)` | `boolean` | True if ANY bit in mask is set. `(value & mask) !== 0` |
|
|
49
|
+
| `.hasNone(mask)` | `boolean` | True if NO bits in mask are set. `(value & mask) === 0` |
|
|
50
|
+
|
|
51
|
+
### In-Place Set Math
|
|
52
|
+
|
|
53
|
+
| Method | Returns | Operation |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `.clear()` | `this` | Reset to 0. |
|
|
56
|
+
| `.union(mask)` | `this` | `value \|= mask` — add bits. |
|
|
57
|
+
| `.difference(mask)` | `this` | `value &= ~mask` — remove bits. |
|
|
58
|
+
| `.intersect(mask)` | `this` | `value &= mask` — keep only shared bits. |
|
|
59
|
+
|
|
60
|
+
### Advanced Helpers
|
|
61
|
+
|
|
62
|
+
| Method | Returns | Description |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `.count()` | `number` | O(1) popcount (Hamming weight). Active bit count, 0–32. |
|
|
65
|
+
| `.countMasked(mask)` | `number` | O(1) popcount within masked region only. |
|
|
66
|
+
| `.lowest()` | `number` | Index of least significant set bit (0–31). Returns `-1` if empty. |
|
|
67
|
+
| `.highest()` | `number` | Index of most significant set bit (0–31). Returns `-1` if empty. |
|
|
68
|
+
| `.isEmpty()` | `boolean` | True if value is 0. |
|
|
69
|
+
|
|
70
|
+
### Utility
|
|
71
|
+
|
|
72
|
+
| Method | Returns | Description |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `.clone()` | `FastBit32` | Independent copy. Mutations do not propagate. |
|
|
75
|
+
| `.serialize()` | `number` | Export raw uint32 for JSON/binary storage. |
|
|
76
|
+
| `FastBit32.deserialize(n)` | `FastBit32` | Static. Restore from serialized uint32. |
|
|
77
|
+
|
|
78
|
+
### Property
|
|
79
|
+
|
|
80
|
+
| Property | Type | Description |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `.value` | `number` | The raw 32-bit integer. Safe to read directly in hot paths. |
|
|
83
|
+
|
|
84
|
+
## Critical Caveats
|
|
85
|
+
|
|
86
|
+
1. **Silent modulo-32 wraparound.** JavaScript bitwise shifts apply `% 32` implicitly. `add(32)` is identical to `add(0)`. `add(40)` is identical to `add(8)`. There is no bounds checking.
|
|
87
|
+
|
|
88
|
+
2. **Sign bit.** `add(31)` sets the sign bit. After this, `.value` may be a negative signed integer in JavaScript. All bitwise operations still work correctly on the bit pattern, but `===` comparisons between signed and unsigned representations will fail. Use `>>> 0` if you need unsigned comparison: `(a.value >>> 0) === (b.value >>> 0)`.
|
|
89
|
+
|
|
90
|
+
3. **No input validation.** Floats are truncated. Negative bit indices are coerced. NaN becomes 0. FastBit32 trusts the caller for maximum performance.
|
|
91
|
+
|
|
92
|
+
4. **32-bit limit.** Only bits 0–31 are addressable. For larger bitfields, use `@zakkster/lite-bits`.
|
|
93
|
+
|
|
94
|
+
## Correct Usage Patterns
|
|
95
|
+
|
|
96
|
+
### ECS Component Mask Query
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
const POSITION = 0, VELOCITY = 1, SPRITE = 2, COLLISION = 3;
|
|
100
|
+
const PHYSICS_QUERY = (1 << POSITION) | (1 << VELOCITY) | (1 << COLLISION);
|
|
101
|
+
|
|
102
|
+
const entity = new FastBit32();
|
|
103
|
+
entity.add(POSITION).add(VELOCITY).add(COLLISION);
|
|
104
|
+
|
|
105
|
+
if (entity.hasAll(PHYSICS_QUERY)) {
|
|
106
|
+
// Entity matches the physics system query
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Object Pool — Find First Free Slot
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
const occupied = new FastBit32();
|
|
114
|
+
|
|
115
|
+
function allocate() {
|
|
116
|
+
const free = new FastBit32(~occupied.value & 0xFFFFFFFF);
|
|
117
|
+
const slot = free.lowest();
|
|
118
|
+
if (slot === -1) return -1; // Pool full
|
|
119
|
+
occupied.add(slot);
|
|
120
|
+
return slot;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function release(slot) {
|
|
124
|
+
occupied.remove(slot);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Input State Manager
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
const KEY_LEFT = 0, KEY_RIGHT = 1, KEY_JUMP = 2;
|
|
132
|
+
const input = new FastBit32();
|
|
133
|
+
|
|
134
|
+
// On keydown:
|
|
135
|
+
input.add(KEY_JUMP);
|
|
136
|
+
// On keyup:
|
|
137
|
+
input.remove(KEY_JUMP);
|
|
138
|
+
// In game loop:
|
|
139
|
+
if (input.has(KEY_JUMP)) jump();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Collision Layer Masks
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
const PLAYER = 0, ENEMY = 1, BULLET = 2, WALL = 3;
|
|
146
|
+
const playerCollidesWith = new FastBit32();
|
|
147
|
+
playerCollidesWith.add(ENEMY).add(WALL);
|
|
148
|
+
|
|
149
|
+
if (playerCollidesWith.has(otherEntityLayer)) {
|
|
150
|
+
// Collision detected
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### State Machine
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const IDLE = 0, RUNNING = 1, JUMPING = 2, ATTACKING = 3;
|
|
158
|
+
const state = new FastBit32().add(IDLE);
|
|
159
|
+
|
|
160
|
+
function startAttack() {
|
|
161
|
+
state.clear().add(ATTACKING);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Serialization Round-Trip
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
const raw = flags.serialize(); // number (uint32)
|
|
169
|
+
const json = JSON.stringify(raw); // "18" — not an object
|
|
170
|
+
const restored = FastBit32.deserialize(JSON.parse(json));
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Chaining
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
const flags = new FastBit32()
|
|
177
|
+
.add(0)
|
|
178
|
+
.add(3)
|
|
179
|
+
.add(7)
|
|
180
|
+
.remove(3)
|
|
181
|
+
.union((1 << 10) | (1 << 12));
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Common Mistakes
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
// WRONG: Bit index out of range — silently wraps to bit 0
|
|
188
|
+
flags.add(32);
|
|
189
|
+
|
|
190
|
+
// WRONG: Comparing signed vs unsigned after setting bit 31
|
|
191
|
+
flags.add(31);
|
|
192
|
+
flags.value === 2147483648; // false — value is -2147483648 (signed)
|
|
193
|
+
(flags.value >>> 0) === 2147483648; // true — correct unsigned comparison
|
|
194
|
+
|
|
195
|
+
// WRONG: Using FastBit32 for more than 32 flags
|
|
196
|
+
// Use @zakkster/lite-bits instead
|
|
197
|
+
|
|
198
|
+
// WRONG: Expecting add() to return a boolean
|
|
199
|
+
const result = flags.add(5); // returns `this`, not true/false
|
|
200
|
+
const check = flags.has(5); // returns boolean
|
|
201
|
+
|
|
202
|
+
// WRONG: Mutating and checking in one expression
|
|
203
|
+
if (flags.toggle(3).has(3)) { } // Works, but toggle mutates first — has() checks the NEW state
|
|
204
|
+
|
|
205
|
+
// WRONG: Assuming clone shares state
|
|
206
|
+
const copy = flags.clone();
|
|
207
|
+
copy.add(10);
|
|
208
|
+
flags.has(10); // false — clone is independent
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Performance Characteristics
|
|
212
|
+
|
|
213
|
+
- All single-bit operations: one bitwise instruction, zero branches.
|
|
214
|
+
- `count()`: 5 bitwise operations (Hacker's Delight parallel popcount), zero loops.
|
|
215
|
+
- `lowest()` / `highest()`: 1–2 operations using `Math.clz32`, zero loops.
|
|
216
|
+
- All methods except `clone()` and `deserialize()`: zero allocations.
|
|
217
|
+
- V8 monomorphic: single hidden class for the lifetime of the object.
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zakkster/lite-fastbit32",
|
|
3
|
+
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Zero-GC, monomorphic 32-bit flag manager and ECS masking primitive for high-performance game loops.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "FastBit32.js",
|
|
9
|
+
"types": "FastBit32.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./FastBit32.d.ts",
|
|
13
|
+
"import": "./FastBit32.js",
|
|
14
|
+
"default": "./FastBit32.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"FastBit32.js",
|
|
19
|
+
"FastBit32.d.ts",
|
|
20
|
+
"llms.txt",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"homepage": "https://github.com/PeshoVurtoleta/lite-fastbit32#readme",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/PeshoVurtoleta/lite-fastbit32.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/PeshoVurtoleta/lite-fastbit32/issues",
|
|
31
|
+
"email": "shinikchiev@yahoo.com"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"bitflags",
|
|
35
|
+
"bitmask",
|
|
36
|
+
"bitwise",
|
|
37
|
+
"state-machine",
|
|
38
|
+
"ecs",
|
|
39
|
+
"gamedev",
|
|
40
|
+
"engine",
|
|
41
|
+
"performance",
|
|
42
|
+
"branchless",
|
|
43
|
+
"zero-gc",
|
|
44
|
+
"fast",
|
|
45
|
+
"32bit",
|
|
46
|
+
"flags",
|
|
47
|
+
"bit-operations",
|
|
48
|
+
"low-level",
|
|
49
|
+
"realtime",
|
|
50
|
+
"rendering",
|
|
51
|
+
"pipeline",
|
|
52
|
+
"lite-tools",
|
|
53
|
+
"fastbit32"
|
|
54
|
+
],
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"vitest": "^3.0.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"bundle-check": "npx esbuild FastBit32.js --bundle --format=esm --outfile=test-bundle.js",
|
|
61
|
+
"prepublishOnly": "npm run test && npm run bundle-check"
|
|
62
|
+
}
|
|
63
|
+
}
|