Skip to content

PeshoVurtoleta/lite-fastbit32

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@zakkster/lite-fastbit32

npm version npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

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.

🚀 Built with Lite-FastBit32: Lite-Tween Pro

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.

👉 Get the Lite-Tween Pro Source Code here

(Are you a solo indie dev or student? Reach out to me directly at shinikchiev@yahoo.com and I will hook you up with an indie discount!)

Why lite-fastbit32?

Feature lite-fastbit32 FastBitSet TypedFastBitSet
Max bits 32 Unlimited Unlimited
Zero-GC Yes No No
Monomorphic Yes No No
Branchless Yes No No
O(1) popcount Yes No No
O(1) lowest/highest Yes No No
O(k) iteration Yes No No
BitMapper Yes No No
BigInt support No No No
ECS-ready Yes Yes Yes
Object pool scan Yes No No
Serialization Yes Yes Yes
Bundle size < 1KB ~8KB ~6KB

FastBit32 is an engine primitive, not a general-purpose bitfield.

Installation

npm install @zakkster/lite-fastbit32

Quick Start

import { FastBit32, BitMapper } from '@zakkster/lite-fastbit32';

const flags = new FastBit32();

flags.add(1).add(4);         // Set bits 1 and 4
flags.has(4);                 // true
flags.count();                // 2 — O(1) popcount
flags.lowest();               // 1 — O(1) bit-scan forward
flags.remove(1);              // Clear bit 1
flags.serialize();            // Raw uint32 for storage

// Human-readable flag management
const mapper = new BitMapper(['Physics', 'Render', 'AI', 'Input']);
const entity = new FastBit32();
entity.add(mapper.get('Physics')).add(mapper.get('Input'));
mapper.getActiveNames(entity); // ['Physics', 'Input']

The Bit Pipeline

Monomorphic V8 Optimization

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.

The constructor enforces unsigned 32-bit via >>> 0 on the first tick, locking V8 into its fastest integer representation path.

O(1) Popcount (Hamming Weight)

Counting active bits uses the Hacker's Delight parallel bit-count algorithm:

v = v - ((v >>> 1) & 0x55555555)
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333)
result = Math.imul((v + (v >>> 4)) & 0x0F0F0F0F, 0x01010101) >>> 24

Five operations, zero loops, zero branches. Works for any value of the 32-bit integer.

O(1) Bit-Scan (lowest / highest)

Finding the lowest set bit uses isolation + Count Leading Zeros:

lowest = Math.clz32(value & -value) ^ 31

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.

highest uses 31 - Math.clz32(value) directly.

O(k) Iteration

All iteration helpers visit only active bits using the v &= v - 1 trick to clear the lowest bit each step. Complexity is O(k) where k is the number of set bits — not 32.

Caveats

  • Silent wraparound: JS bitwise shifts apply modulo 32. add(32) evaluates as add(0). add(40) evaluates as add(8).
  • Truncation: Floats and negatives are silently coerced to unsigned 32-bit integers. -1 >>> 0 becomes 4294967295.
  • Sanitize inputs upstream if your domain logic requires strict bounds.

Benchmark Results

Tested on Apple M2 Pro, Node 22, V8 12.x. All values in ops/ms.

Single-Bit Operations

Operation lite-fastbit32 FastBitSet TypedFastBitSet Raw bitwise
set bit ~240k ~150k ~220k ~260k
has bit ~260k ~180k ~240k ~280k
remove bit ~240k ~140k ~200k ~260k

Mask Operations

Operation lite-fastbit32 FastBitSet
hasAll ~300k ~40k
hasAny ~300k ~45k
hasNone ~300k ~45k

Popcount

Operation lite-fastbit32 FastBitSet
count ~350k ~25k

Bit-Scan (lowest / highest)

Operation lite-fastbit32 FastBitSet
lowest ~350k N/A
highest ~350k N/A

lite-fastbit32 is the only library with O(1) bit-scan forward/backward.


Recipes

ECS Component Masks
import { FastBit32 } from '@zakkster/lite-fastbit32';

const POSITION  = 0;
const VELOCITY  = 1;
const SPRITE    = 2;
const COLLISION = 3;
const AI        = 4;

const PHYSICS_QUERY = (1 << POSITION) | (1 << VELOCITY) | (1 << COLLISION);
const RENDER_QUERY  = (1 << POSITION) | (1 << SPRITE);

const entity = new FastBit32();
entity.add(POSITION).add(VELOCITY).add(SPRITE).add(COLLISION);

if (entity.hasAll(PHYSICS_QUERY)) runPhysics(entity);
if (entity.hasAll(RENDER_QUERY))  drawSprite(entity);

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.

Object Pool — First Free Slot (zero-allocation, v1.2.0)
import { FastBit32 } from '@zakkster/lite-fastbit32';

const pool = new FastBit32();
const objects = new Array(32);

function allocate() {
    const slot = pool.nextClearBit();   // O(1), zero allocation
    if (slot === -1) return null;        // Pool full
    pool.add(slot);
    return slot;
}

function release(slot) {
    pool.remove(slot);
}

allocate(); // 0
allocate(); // 1
release(0);
allocate(); // 0 — immediately reused

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.

Input State Manager
import { FastBit32 } from '@zakkster/lite-fastbit32';

const KEY_LEFT  = 0;
const KEY_RIGHT = 1;
const KEY_JUMP  = 2;
const KEY_FIRE  = 3;

const input = new FastBit32();

window.addEventListener('keydown', e => {
    if (e.code === 'ArrowLeft')  input.add(KEY_LEFT);
    if (e.code === 'ArrowRight') input.add(KEY_RIGHT);
    if (e.code === 'Space')      input.add(KEY_JUMP);
});

window.addEventListener('keyup', e => {
    if (e.code === 'ArrowLeft')  input.remove(KEY_LEFT);
    if (e.code === 'ArrowRight') input.remove(KEY_RIGHT);
    if (e.code === 'Space')      input.remove(KEY_JUMP);
});

if (input.has(KEY_JUMP)) jump();
if (input.hasAny((1 << KEY_LEFT) | (1 << KEY_RIGHT))) move();
State Machine — Mutex Flags
import { FastBit32 } from '@zakkster/lite-fastbit32';

const IDLE       = 0;
const RUNNING    = 1;
const JUMPING    = 2;
const ATTACKING  = 3;
const INVINCIBLE = 4;

const state = new FastBit32();
state.add(IDLE);

function startAttack() {
    state.clear().add(ATTACKING).add(INVINCIBLE);
}

function endAttack() {
    state.clear().add(IDLE);
}

console.log(state.count()); // 1 — proof of mutex
BitMapper + Iteration (v1.1.0)
import { FastBit32, BitMapper, forEachMapped, forEachMappedObject } from '@zakkster/lite-fastbit32';

const components = new BitMapper(['Position', 'Velocity', 'Sprite', 'AI']);
const entity = new FastBit32();
entity.add(components.get('Position')).add(components.get('AI'));

console.log(components.getActiveNames(entity)); // ['Position', 'AI']

forEachMapped(entity, components, (name, bit) => {
    console.log(`Component ${name} at bit ${bit}`);
});

const systems = { Position: updatePos, Velocity: updateVel, Sprite: draw, AI: think };
forEachMappedObject(entity, components, systems, (system, name) => {
    system(entity);
});
Mask Set Operations (v1.1.0)
import { FastBit32, forEachMaskPair, forEachMaskDiff, forEachMaskUnion } from '@zakkster/lite-fastbit32';

const required = new FastBit32().add(0).add(1).add(3);
const available = new FastBit32().add(0).add(3).add(5);

forEachMaskPair(required, available, bit => console.log('matched:', bit));
// matched: 0, matched: 3

forEachMaskDiff(required, available, bit => console.log('missing:', bit));
// missing: 1

forEachMaskUnion(required, available, bit => console.log('all:', bit));
// all: 0, all: 1, all: 3, all: 5

API

new FastBit32(initial?)

Parameter Type Default Description
initial number 0 Starting bitmask. Coerced to unsigned 32-bit via >>> 0.

Single Bit Operations

Method Returns Description
.add(bit) this Set bit at position (0–31).
.remove(bit) this Clear bit at position (0–31).
.toggle(bit) this Flip bit at position (0–31).
.has(bit) boolean Test if bit is active.

Bulk Mask Operations

Method Returns Description
.hasAll(mask) boolean True if all bits in mask are active.
.hasAny(mask) boolean True if any bit in mask is active.
.hasNone(mask) boolean True if no bits in mask are active.

In-Place Mutations

Method Returns Description
.clear() this Reset all 32 bits to zero.
.union(mask) this Bitwise OR — add all bits in mask.
.difference(mask) this Bitwise AND NOT — remove all bits in mask.
.intersect(mask) this Bitwise AND — keep only bits present in both.

Advanced Helpers

Method Returns Description
.count() number O(1) popcount — number of active bits (0–32).
.countMasked(mask) number O(1) popcount within a masked region.
.countRange(start, end) number O(1) popcount within inclusive range [start, end].
.lowest() number O(1) index of least significant active bit. -1 if empty.
.highest() number O(1) index of most significant active bit. -1 if empty.
.nextClearBit() number O(1) index of least significant clear bit. -1 if full. Zero-alloc pool slot lookup.
.highestClearBit() number O(1) index of most significant clear bit. -1 if full.
.isEmpty() boolean True if value is 0.
.isFull() boolean True if all 32 bits are active.

Iteration

Method Returns Description
.forEach(callback) this O(k) iteration over active bits. callback(bit).

Utility

Method Returns Description
.clone() FastBit32 Independent copy. Mutations do not propagate.
.serialize() number Export raw uint32 for JSON/binary storage.
FastBit32.deserialize(n) FastBit32 Restore from a serialized uint32.

Debug & Init Helpers

These methods allocate. Do not call them inside hot loops.

Method Returns Description
.toBinaryString(padded?) string 32-char binary representation, LSB on the right. Sign-bit safe.
.toArray() number[] Active bit indexes in ascending order. Inlined O(k) — no closure allocation.
.fromArray(bits) this Replaces the mask from a bit-index array. Init/deserialization only.

new BitMapper(names?)

Method Returns Description
.get(name) number Bit index for a flag name. Throws if unknown.
.getMask(names) number Combined uint32 mask from flag name array.
.getActiveNames(fb32) string[] Active flag names from a FastBit32 instance.
.getName(bit) string | undefined O(1) reverse lookup — bit index to name.

Standalone Iteration Helpers

Function Description
forEachArray(mask, array, cb) cb(element, bit) for each active bit.
forEachObject(mask, keys, obj, cb) cb(value, key, bit) via keys array.
forEachMapped(mask, mapper, cb) cb(name, bit) via BitMapper.
forEachMappedObject(mask, mapper, obj, cb) cb(value, key, bit) via BitMapper + object.
forEachMaskPair(maskA, maskB, cb) cb(bit) for intersection (A & B).
forEachMaskDiff(maskA, maskB, cb) cb(bit) for difference (A & ~B).
forEachMaskUnion(maskA, maskB, cb) cb(bit) for union (A | B).

Changelog

v1.2.0

New: Clear-bit scansnextClearBit() 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.

New: isFull() — Companion to isEmpty(). Uses ~this.value === 0 for correctness across both signed (-1) and unsigned (0xFFFFFFFF) representations of an all-set int32.

New: countRange(start, end) — O(1) popcount within an inclusive bit range. Mask is built with >>> to sidestep the 1 << 32 wraparound.

New: Debug & init helperstoBinaryString(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.

v1.1.0

New: BitMapper — Human-to-hardware bridge. Maps semantic string names to bit indices and masks, with O(1) reverse lookup via getName(bit).

New: forEach(callback) — O(k) iteration on FastBit32 instances. Visits only active bits in ascending order using v &= v - 1 bit-clearing. Returns this for chaining.

New: 7 standalone iteration helpersforEachArray, forEachObject, forEachMapped, forEachMappedObject, forEachMaskPair, forEachMaskDiff, forEachMaskUnion. All O(k). Connect masks directly to arrays, objects, BitMapper dictionaries, and mask set operations without intermediate allocations.

v1.0.0

Initial release. FastBit32 core: single-bit ops, bulk mask ops, in-place set math, O(1) popcount, O(1) bit-scan (lowest/highest), clone, serialize/deserialize.


License

MIT

Part of the @zakkster ecosystem

Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web applications.

About

Zero-GC, monomorphic 32-bit flag manager and ECS masking primitive for high-performance game loops.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors