@zakkster/lite-fastbit32 1.1.3 → 1.2.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 +68 -0
- package/FastBit32.js +86 -0
- package/README.md +30 -7
- package/llms.txt +23 -5
- package/package.json +1 -1
package/FastBit32.d.ts
CHANGED
|
@@ -199,6 +199,74 @@ export class FastBit32 {
|
|
|
199
199
|
*/
|
|
200
200
|
forEach(callback: (bit: number) => void): this;
|
|
201
201
|
|
|
202
|
+
// ── v1.2.0 AAA Engine Primitives ─────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Returns the index of the lowest INACTIVE (0) bit.
|
|
206
|
+
* O(1) via `Math.clz32` on the inverted mask — no loops, no scratch
|
|
207
|
+
* `FastBit32` allocation. Ideal for object pools: instantly finds the
|
|
208
|
+
* first free slot.
|
|
209
|
+
*
|
|
210
|
+
* @returns Bit index (0–31), or `-1` if all 32 bits are active.
|
|
211
|
+
*/
|
|
212
|
+
nextClearBit(): number;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns the index of the highest INACTIVE (0) bit.
|
|
216
|
+
* O(1) via `Math.clz32` on the inverted mask.
|
|
217
|
+
*
|
|
218
|
+
* @returns Bit index (0–31), or `-1` if all 32 bits are active.
|
|
219
|
+
*/
|
|
220
|
+
highestClearBit(): number;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Tests whether all 32 bits are active.
|
|
224
|
+
*
|
|
225
|
+
* @returns `true` if the value has every bit set.
|
|
226
|
+
*/
|
|
227
|
+
isFull(): boolean;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Returns the number of active bits within an inclusive range
|
|
231
|
+
* `[start, end]`. Caller must guarantee `0 <= start <= end <= 31`.
|
|
232
|
+
*
|
|
233
|
+
* @param start - Lowest bit of the range (inclusive).
|
|
234
|
+
* @param end - Highest bit of the range (inclusive).
|
|
235
|
+
* @returns Active bit count within the range (0–32).
|
|
236
|
+
*/
|
|
237
|
+
countRange(start: number, end: number): number;
|
|
238
|
+
|
|
239
|
+
// ── Debug & Init Helpers (allocate — NOT hot-path safe) ──
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns a 32-character binary string of the raw value, LSB on the right.
|
|
243
|
+
* Forces unsigned representation so bit 31 prints as `1`, not a minus sign.
|
|
244
|
+
*
|
|
245
|
+
* ⚠️ Allocates a String. Debug use only.
|
|
246
|
+
*
|
|
247
|
+
* @param padded - Pad to 32 chars with leading zeros. Defaults to `true`.
|
|
248
|
+
*/
|
|
249
|
+
toBinaryString(padded?: boolean): string;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Returns an array of active bit indexes in ascending order.
|
|
253
|
+
*
|
|
254
|
+
* ⚠️ Allocates an Array. Not safe for hot loops.
|
|
255
|
+
*
|
|
256
|
+
* @returns Array of bit indexes (0–31).
|
|
257
|
+
*/
|
|
258
|
+
toArray(): number[];
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Replaces the current value with a bitmask built from an array of bit
|
|
262
|
+
* indexes. Overwrites — does NOT OR into the existing value. Intended
|
|
263
|
+
* for initialization and deserialization, not hot paths.
|
|
264
|
+
*
|
|
265
|
+
* @param bits - Array of bit indexes (0–31).
|
|
266
|
+
* @returns `this` for chaining.
|
|
267
|
+
*/
|
|
268
|
+
fromArray(bits: number[]): this;
|
|
269
|
+
|
|
202
270
|
// ── Serialization ───────────────────────────────────
|
|
203
271
|
|
|
204
272
|
/**
|
package/FastBit32.js
CHANGED
|
@@ -146,6 +146,92 @@ export class FastBit32 {
|
|
|
146
146
|
}
|
|
147
147
|
return this;
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
// ── v1.2.0 AAA Engine Primitives ─────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* O(1) loop-free bit-scan forward for the lowest INACTIVE (0) bit.
|
|
154
|
+
* Essential for Object Pools to find the first available slot WITHOUT
|
|
155
|
+
* allocating a scratch FastBit32 for the inverted mask.
|
|
156
|
+
* Returns -1 if all 32 bits are active.
|
|
157
|
+
*/
|
|
158
|
+
nextClearBit() {
|
|
159
|
+
const inv = ~this.value >>> 0;
|
|
160
|
+
if (inv === 0) return -1;
|
|
161
|
+
return Math.clz32(inv & -inv) ^ 31;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* O(1) loop-free bit-scan reverse for the highest INACTIVE (0) bit.
|
|
166
|
+
* Returns -1 if all 32 bits are active.
|
|
167
|
+
*/
|
|
168
|
+
highestClearBit() {
|
|
169
|
+
const inv = ~this.value >>> 0;
|
|
170
|
+
if (inv === 0) return -1;
|
|
171
|
+
return 31 - Math.clz32(inv);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* O(1) Returns true if all 32 bits are active.
|
|
176
|
+
* Uses `~this.value === 0` because JS bitwise ops yield signed int32,
|
|
177
|
+
* so a fully-set value reads as -1 rather than 0xFFFFFFFF.
|
|
178
|
+
*/
|
|
179
|
+
isFull() {
|
|
180
|
+
return ~this.value === 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* O(1) active bit count within an inclusive range [start, end].
|
|
185
|
+
* Mask is built with `>>>` to avoid the `1 << 32` wraparound trap.
|
|
186
|
+
* Caller must guarantee 0 <= start <= end <= 31.
|
|
187
|
+
*/
|
|
188
|
+
countRange(start, end) {
|
|
189
|
+
const mask = ((0xFFFFFFFF >>> (31 - (end - start))) << start) >>> 0;
|
|
190
|
+
return this.countMasked(mask);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Debug & Init Helpers (allocate — NOT hot-path safe) ──
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns the 32-bit binary string representation (LSB on the right).
|
|
197
|
+
* Forces unsigned via `>>> 0` so the sign bit prints as a leading `1`
|
|
198
|
+
* instead of triggering `toString(2)`'s minus-sign formatting.
|
|
199
|
+
* ⚠️ Allocates a String. Debug use only.
|
|
200
|
+
*/
|
|
201
|
+
toBinaryString(padded = true) {
|
|
202
|
+
const str = (this.value >>> 0).toString(2);
|
|
203
|
+
return padded ? str.padStart(32, '0') : str;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns an array of active bit indexes in ascending order.
|
|
208
|
+
* Inlined O(k) scan — no closure allocation vs. delegating to forEach.
|
|
209
|
+
* ⚠️ Allocates an Array. Do not use in hot loop!
|
|
210
|
+
*/
|
|
211
|
+
toArray() {
|
|
212
|
+
const result = [];
|
|
213
|
+
let v = this.value;
|
|
214
|
+
while (v !== 0) {
|
|
215
|
+
result.push(Math.clz32(v & -v) ^ 31);
|
|
216
|
+
v &= v - 1;
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Replaces the mask from an array of bit indexes. Note: this OVERWRITES
|
|
223
|
+
* the current value — it does not OR into it. Accumulates in a local and
|
|
224
|
+
* writes `this.value` once for monomorphic-friendly init.
|
|
225
|
+
* ⚠️ Intended for initialization / deserialization, not hot paths.
|
|
226
|
+
*/
|
|
227
|
+
fromArray(bits) {
|
|
228
|
+
let v = 0;
|
|
229
|
+
for (let i = 0; i < bits.length; i++) {
|
|
230
|
+
v |= (1 << bits[i]);
|
|
231
|
+
}
|
|
232
|
+
this.value = v >>> 0;
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
149
235
|
}
|
|
150
236
|
|
|
151
237
|
export class BitMapper {
|
package/README.md
CHANGED
|
@@ -12,9 +12,7 @@ Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object poo
|
|
|
12
12
|
|
|
13
13
|
## 🚀 Built with Lite-FastBit32: Lite-Tween Pro
|
|
14
14
|
|
|
15
|
-
If you are building high-performance WebGL applications or JavaScript games, check out **Lite-Tween Pro**. It is a commercial, zero-allocation ECS tweening engine built directly on top of this bitwise architecture.
|
|
16
|
-
|
|
17
|
-
It completely bypasses the JavaScript Garbage Collector to guarantee a flat memory profile and a stable 120fps on mobile devices.
|
|
15
|
+
If you are building high-performance WebGL applications or JavaScript games, check out **Lite-Tween Pro**. It is a commercial, zero-allocation ECS tweening engine built directly on top of this bitwise architecture. It completely bypasses the JavaScript Garbage Collector to guarantee a flat memory profile and a stable 120fps on mobile devices.
|
|
18
16
|
|
|
19
17
|
👉 **[Get the Lite-Tween Pro Source Code here](https://zakkster.lemonsqueezy.com/checkout/buy/05ad56be-4b91-4bef-8218-9e85c65690b4)**
|
|
20
18
|
|
|
@@ -179,7 +177,7 @@ if (entity.hasAll(RENDER_QUERY)) drawSprite(entity);
|
|
|
179
177
|
</details>
|
|
180
178
|
|
|
181
179
|
<details>
|
|
182
|
-
<summary><strong>Object Pool — First Free Slot</strong></summary>
|
|
180
|
+
<summary><strong>Object Pool — First Free Slot (zero-allocation, v1.2.0)</strong></summary>
|
|
183
181
|
|
|
184
182
|
```javascript
|
|
185
183
|
import { FastBit32 } from '@zakkster/lite-fastbit32';
|
|
@@ -188,9 +186,8 @@ const pool = new FastBit32();
|
|
|
188
186
|
const objects = new Array(32);
|
|
189
187
|
|
|
190
188
|
function allocate() {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
if (slot === -1) return null;
|
|
189
|
+
const slot = pool.nextClearBit(); // O(1), zero allocation
|
|
190
|
+
if (slot === -1) return null; // Pool full
|
|
194
191
|
pool.add(slot);
|
|
195
192
|
return slot;
|
|
196
193
|
}
|
|
@@ -205,6 +202,8 @@ release(0);
|
|
|
205
202
|
allocate(); // 0 — immediately reused
|
|
206
203
|
```
|
|
207
204
|
|
|
205
|
+
> Before v1.2.0 this required constructing a scratch `FastBit32` from the inverted mask and calling `.lowest()` on it — one allocation per `allocate()` call. `nextClearBit` collapses that to a single `Math.clz32` on an inverted-and-isolated bit, with no allocations.
|
|
206
|
+
|
|
208
207
|
</details>
|
|
209
208
|
|
|
210
209
|
<details>
|
|
@@ -353,9 +352,13 @@ forEachMaskUnion(required, available, bit => console.log('all:', bit));
|
|
|
353
352
|
|---|---|---|
|
|
354
353
|
| `.count()` | `number` | O(1) popcount — number of active bits (0–32). |
|
|
355
354
|
| `.countMasked(mask)` | `number` | O(1) popcount within a masked region. |
|
|
355
|
+
| `.countRange(start, end)` | `number` | O(1) popcount within inclusive range `[start, end]`. |
|
|
356
356
|
| `.lowest()` | `number` | O(1) index of least significant active bit. `-1` if empty. |
|
|
357
357
|
| `.highest()` | `number` | O(1) index of most significant active bit. `-1` if empty. |
|
|
358
|
+
| `.nextClearBit()` | `number` | O(1) index of least significant **clear** bit. `-1` if full. Zero-alloc pool slot lookup. |
|
|
359
|
+
| `.highestClearBit()` | `number` | O(1) index of most significant **clear** bit. `-1` if full. |
|
|
358
360
|
| `.isEmpty()` | `boolean` | True if value is `0`. |
|
|
361
|
+
| `.isFull()` | `boolean` | True if all 32 bits are active. |
|
|
359
362
|
|
|
360
363
|
### Iteration
|
|
361
364
|
|
|
@@ -371,6 +374,16 @@ forEachMaskUnion(required, available, bit => console.log('all:', bit));
|
|
|
371
374
|
| `.serialize()` | `number` | Export raw uint32 for JSON/binary storage. |
|
|
372
375
|
| `FastBit32.deserialize(n)` | `FastBit32` | Restore from a serialized uint32. |
|
|
373
376
|
|
|
377
|
+
### Debug & Init Helpers
|
|
378
|
+
|
|
379
|
+
> These methods allocate. Do not call them inside hot loops.
|
|
380
|
+
|
|
381
|
+
| Method | Returns | Description |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| `.toBinaryString(padded?)` | `string` | 32-char binary representation, LSB on the right. Sign-bit safe. |
|
|
384
|
+
| `.toArray()` | `number[]` | Active bit indexes in ascending order. Inlined O(k) — no closure allocation. |
|
|
385
|
+
| `.fromArray(bits)` | `this` | **Replaces** the mask from a bit-index array. Init/deserialization only. |
|
|
386
|
+
|
|
374
387
|
### `new BitMapper(names?)`
|
|
375
388
|
|
|
376
389
|
| Method | Returns | Description |
|
|
@@ -396,6 +409,16 @@ forEachMaskUnion(required, available, bit => console.log('all:', bit));
|
|
|
396
409
|
|
|
397
410
|
## Changelog
|
|
398
411
|
|
|
412
|
+
### v1.2.0
|
|
413
|
+
|
|
414
|
+
**New: Clear-bit scans** — `nextClearBit()` and `highestClearBit()`. O(1) bit-scan on the inverted mask via `Math.clz32`. The object-pool slot-lookup pattern is now truly zero-allocation; the prior `new FastBit32(~pool.value & 0xFFFFFFFF).lowest()` workaround is retired.
|
|
415
|
+
|
|
416
|
+
**New: `isFull()`** — Companion to `isEmpty()`. Uses `~this.value === 0` for correctness across both signed (`-1`) and unsigned (`0xFFFFFFFF`) representations of an all-set int32.
|
|
417
|
+
|
|
418
|
+
**New: `countRange(start, end)`** — O(1) popcount within an inclusive bit range. Mask is built with `>>>` to sidestep the `1 << 32` wraparound.
|
|
419
|
+
|
|
420
|
+
**New: Debug & init helpers** — `toBinaryString(padded?)`, `toArray()`, `fromArray(bits)`. Clearly documented as allocating; kept outside the hot-path API surface. `toBinaryString` forces unsigned via `>>> 0` so bit 31 does not trigger `toString(2)`'s minus-sign formatting. `toArray` inlines the `v &= v - 1` loop rather than delegating to `forEach`, avoiding a per-call closure allocation. `fromArray` **replaces** the current value (does not OR into it) and writes `this.value` exactly once.
|
|
421
|
+
|
|
399
422
|
### v1.1.0
|
|
400
423
|
|
|
401
424
|
**New: BitMapper** — Human-to-hardware bridge. Maps semantic string names to bit indices and masks, with O(1) reverse lookup via `getName(bit)`.
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @zakkster/lite-fastbit32 v1.
|
|
1
|
+
# @zakkster/lite-fastbit32 v1.2.0
|
|
2
2
|
|
|
3
3
|
> Zero-GC, monomorphic, branchless 32-bit flag manager for ECS masking, object pools, and 60fps hot-path engine code.
|
|
4
4
|
|
|
@@ -69,9 +69,13 @@ All mutating methods return `this` for chaining unless noted otherwise.
|
|
|
69
69
|
|---|---|---|
|
|
70
70
|
| `.count()` | `number` | O(1) popcount (Hamming weight). Active bit count, 0–32. |
|
|
71
71
|
| `.countMasked(mask)` | `number` | O(1) popcount within masked region only. |
|
|
72
|
+
| `.countRange(start, end)` | `number` | O(1) popcount within inclusive range `[start, end]`. Caller must ensure `0 <= start <= end <= 31`. |
|
|
72
73
|
| `.lowest()` | `number` | Index of least significant set bit (0–31). Returns `-1` if empty. |
|
|
73
74
|
| `.highest()` | `number` | Index of most significant set bit (0–31). Returns `-1` if empty. |
|
|
75
|
+
| `.nextClearBit()` | `number` | Index of least significant **clear** bit (0–31). Returns `-1` if full. Zero-alloc object-pool slot lookup. |
|
|
76
|
+
| `.highestClearBit()` | `number` | Index of most significant **clear** bit (0–31). Returns `-1` if full. |
|
|
74
77
|
| `.isEmpty()` | `boolean` | True if value is 0. |
|
|
78
|
+
| `.isFull()` | `boolean` | True if all 32 bits are set. |
|
|
75
79
|
|
|
76
80
|
### Iteration
|
|
77
81
|
|
|
@@ -87,6 +91,16 @@ All mutating methods return `this` for chaining unless noted otherwise.
|
|
|
87
91
|
| `.serialize()` | `number` | Export raw uint32 for JSON/binary storage. |
|
|
88
92
|
| `FastBit32.deserialize(n)` | `FastBit32` | Static. Restore from serialized uint32. |
|
|
89
93
|
|
|
94
|
+
### Debug & Init Helpers
|
|
95
|
+
|
|
96
|
+
These methods allocate — keep them out of hot loops.
|
|
97
|
+
|
|
98
|
+
| Method | Returns | Description |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `.toBinaryString(padded?)` | `string` | 32-char (or variable) binary string, LSB on the right. Handles the sign bit correctly. Allocates a String. |
|
|
101
|
+
| `.toArray()` | `number[]` | Active bit indexes in ascending order. Inlined O(k) scan (no closure allocation). Allocates an Array. |
|
|
102
|
+
| `.fromArray(bits)` | `this` | **Replaces** the value with a bitmask built from a bit-index array. Intended for init / deserialization. |
|
|
103
|
+
|
|
90
104
|
### Property
|
|
91
105
|
|
|
92
106
|
| Property | Type | Description |
|
|
@@ -183,15 +197,14 @@ if (entity.hasAll(PHYSICS_QUERY)) {
|
|
|
183
197
|
}
|
|
184
198
|
```
|
|
185
199
|
|
|
186
|
-
### Object Pool — Find First Free Slot
|
|
200
|
+
### Object Pool — Find First Free Slot (zero-allocation)
|
|
187
201
|
|
|
188
202
|
```javascript
|
|
189
203
|
const occupied = new FastBit32();
|
|
190
204
|
|
|
191
205
|
function allocate() {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
if (slot === -1) return -1; // Pool full
|
|
206
|
+
const slot = occupied.nextClearBit(); // O(1), zero allocation
|
|
207
|
+
if (slot === -1) return -1; // Pool full
|
|
195
208
|
occupied.add(slot);
|
|
196
209
|
return slot;
|
|
197
210
|
}
|
|
@@ -199,8 +212,13 @@ function allocate() {
|
|
|
199
212
|
function release(slot) {
|
|
200
213
|
occupied.remove(slot);
|
|
201
214
|
}
|
|
215
|
+
|
|
216
|
+
// Bail early when the pool is saturated:
|
|
217
|
+
if (occupied.isFull()) return -1;
|
|
202
218
|
```
|
|
203
219
|
|
|
220
|
+
> Prior to v1.2.0 this pattern required `new FastBit32(~occupied.value & 0xFFFFFFFF)` followed by `.lowest()`, which allocated a scratch instance per `allocate()` call. `nextClearBit` is the direct, zero-GC replacement.
|
|
221
|
+
|
|
204
222
|
### Input State Manager
|
|
205
223
|
|
|
206
224
|
```javascript
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-fastbit32",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "Zero-GC, monomorphic 32-bit flag manager and ECS masking primitive for high-performance game loops.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|