Skip to content

Commit 662eefb

Browse files
committed
ye
1 parent fa688f3 commit 662eefb

32 files changed

Lines changed: 3086 additions & 317 deletions

README.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,9 @@ Enable with `Lync.configure({ stats = true })`.
250250
| Codec | Bytes | Notes |
251251
|:---|---:|:---|
252252
| `int(min, max)` | 1 / 2 / 4 | Picks narrowest u8/u16/u32/i8/i16/i32. |
253+
| `zint(min?, max?)` | 1 – 5 | Variable-length signed via zigzag varint. 1 byte for [-96, 95]. |
253254
| `f16` / `f32` / `f64` | 2 / 4 / 8 | `f16` ≈ ±65504, ~3 digits. |
254-
| `float(min, max, precision)` | 1 / 2 / 4 | Quantized; clamped to range. |
255+
| `float(min, max, precision)` | 1 / 2 / 3 / 4 | Quantized; picks u8 / u16 / u24 / u32 wire form. |
255256
| `bool` | 1 | Auto-bitpacked inside `struct` and `array`. |
256257

257258
### Strings & buffers
@@ -284,8 +285,8 @@ Call as a function for compression.
284285

285286
| Codec | Bytes | Notes |
286287
|:---|---:|:---|
287-
| `vec2(min, max, precision)` | 2 / 4 / 8 | Per-component quantization. |
288-
| `vec3(min, max, precision)` | 3 / 6 / 12 | Per-component quantization. |
288+
| `vec2(min, max, precision)` | 2 / 4 / 6 / 8 | Per-component, narrowest fitting width. |
289+
| `vec3(min, max, precision)` | 3 / 6 / 9 / 12 | Per-component, narrowest fitting width. |
289290
| `cframe()` | 16 | Smallest-three quaternion. ≤ 0.16° rotation error. |
290291

291292
### Composites
@@ -301,13 +302,17 @@ Call as a function for compression.
301302

302303
### Delta — reliable transport only
303304

304-
Sends 1 byte when the value is byte-equal to the cached previous frame.
305+
Tracks the previous frame's value and ships only what changed. Rejected on `unreliable = true`.
305306

306-
| Codec | Notes |
307-
|:---|:---|
308-
| `deltaStruct(schema)` | Per-segment dirty bitmap; single-segment fast path. |
309-
| `deltaArray(c, max?)` | Whole-array byte-equality. |
310-
| `deltaMap(k, v, max?)` | Whole-map byte-equality. |
307+
| Codec | Static | Mutation |
308+
|:---|:---:|:---:|
309+
| `deltaStruct(schema)` | 1 B | per-field |
310+
| `deltaArray(c, max?)` | 1 B | per-changed-index |
311+
| `deltaMap(k, v, max?)` | 1 B | per-changed-key |
312+
| `deltaInt(min, max)` | 1 B | 1–5 B |
313+
| `deltaFloat(min, max, precision)` | 1 B | 1–5 B |
314+
| `deltaVec3(min, max, precision)` | 3 B | 3–15 B |
315+
| `deltaCFrame(posMin, posMax, precision)` | 1 B | 4–13 B |
311316

312317
### Meta
313318

@@ -351,21 +356,23 @@ Global per-player cap: `Lync.configure({ globalRateLimit = { maxPerSecond = N }
351356
| Tool | `array<entity>[100]` | `array<bool>[1000]` |
352357
|:---|:---|:---|
353358
| roblox | 16 fps · 559,364 Kbps | 21 fps · 353,107 Kbps |
354-
| **lync** | **60 fps · 3.44 Kbps** | **60 fps · 2.46 Kbps** |
359+
| **lync** | **59 fps · 3.37 Kbps** | **61 fps · 2.45 Kbps** |
355360
| blink | 42 fps · 41.81 Kbps | 97 fps · 7.91 Kbps |
356361
| zap | 39 fps · 41.71 Kbps | 52 fps · 8.10 Kbps |
357362
| bytenet | 32 fps · 41.64 Kbps | 35 fps · 8.11 Kbps |
358363

359364
### Network bandwidth — 100 fires/frame, 8 s
360365

361-
| Workload | Kbps |
362-
|:---|---:|
363-
| `array<entity>[100]` randomised | 3,608 |
364-
| `array<entity>[100]` reused | **2.4** |
365-
| `array<bool>[1000]` randomised | 763 |
366-
| `array<bool>[1000]` 1 bit flipped | **20.5** |
367-
| `struct(state)` randomised | 201 |
368-
| `deltaStruct(state)` 1 field mutated | **29.2** |
366+
| Workload | Naive Kbps | Optimized | Savings |
367+
|:---|---:|:---|---:|
368+
| `array<entity>[100]` random | 3,607 | `deltaArray` 3 of 100 mutated | **154** (–96%) |
369+
| `array<entity>[100]` reused | 3,607 | XOR baseline (identical frames) | **2.4** (–99.9%) |
370+
| `array<bool>[1000]` random | 762 | XOR baseline (1 bit flipped) | **20.4** (–97%) |
371+
| `struct(state)` random | 201 | `deltaStruct` 1 field mutated | **29.0** (–86%) |
372+
| `map<id, vec3>[200]` 5 keys mutated | 657 | `deltaMap` 5 keys mutated | **393** (–40%) |
373+
| `array<cframe>[50]` random | 4,585 |||
374+
| `vec3` walking motion (continuous diff) || `deltaVec3` | **19.5** |
375+
| `CFrame` walking pose (pos + rot) || `deltaCFrame` | **41.1** |
369376

370377
## License
371378

bench/Run.server.luau

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ end
2323

2424
-- Delta wire size --------------------------------------------------------
2525

26-
Harness.header("Delta Wire Size (deltaStruct[12 fields], bytes)")
26+
Harness.header("Delta Wire Size (per-codec, bytes)")
2727
for _, c in Scenarios.deltaCases do
28-
Harness.deltaSize(c.label, Scenarios.stateDelta, c.seed, c.value)
28+
Harness.deltaSize(c.label, c.codec, c.seed, c.value)
2929
end
3030

3131
-- CPU round-trip ---------------------------------------------------------

bench/Scenarios.luau

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,13 @@ local stateSchema = {
3939

4040
local entityCodec = Lync.struct(entitySchema)
4141
local entityArray = Lync.array(entityCodec)
42+
local entityDeltaArray = Lync.deltaArray(entityCodec)
4243
local boolArray = Lync.array(Lync.bool)
4344
local cframeArray = Lync.array(Lync.cframe())
4445
local stateCodec = Lync.struct(stateSchema)
4546
local stateDelta = Lync.deltaStruct(stateSchema)
47+
local positionMap = Lync.map(Lync.int(0, 1023), Lync.vec3)
48+
local positionDeltaMap = Lync.deltaMap(Lync.int(0, 1023), Lync.vec3)
4649

4750
-- Generators -------------------------------------------------------------
4851

@@ -242,6 +245,14 @@ local wireCases = table.freeze({
242245
}),
243246
value = { a = true, b = false, c = true, d = false, tier = 7, level = 99 },
244247
},
248+
-- zint and delta scalars are stateful for the delta variants; the
249+
-- single-value wire size below is the FIRST-frame size.
250+
{ label = "zint(0)", codec = Lync.zint(), value = 0 },
251+
{ label = "zint(95)", codec = Lync.zint(), value = 95 },
252+
{ label = "zint(-96)", codec = Lync.zint(), value = -96 },
253+
{ label = "zint(1000)", codec = Lync.zint(), value = 1000 },
254+
{ label = "zint(-1000)", codec = Lync.zint(), value = -1000 },
255+
{ label = "zint(i32_max)", codec = Lync.zint(), value = 0x7FFFFFFF },
245256
})
246257

247258
-- Delta wire-size catalogue ----------------------------------------------
@@ -258,9 +269,67 @@ stateMultiMut.velY = 5
258269
stateMultiMut.velZ = 6
259270

260271
local deltaCases = table.freeze({
261-
{ label = "delta unchanged", seed = stateBase, value = stateBase },
262-
{ label = "delta 1-field-mut", seed = stateBase, value = stateOneMut },
263-
{ label = "delta 6-fields-mut", seed = stateBase, value = stateMultiMut },
272+
{ label = "deltaStruct unchanged", codec = stateDelta, seed = stateBase, value = stateBase },
273+
{
274+
label = "deltaStruct 1-field-mut",
275+
codec = stateDelta,
276+
seed = stateBase,
277+
value = stateOneMut,
278+
},
279+
{
280+
label = "deltaStruct 6-fields-mut",
281+
codec = stateDelta,
282+
seed = stateBase,
283+
value = stateMultiMut,
284+
},
285+
{
286+
label = "deltaInt unchanged",
287+
codec = Lync.deltaInt(0, 1000000),
288+
seed = 12345,
289+
value = 12345,
290+
},
291+
{
292+
label = "deltaInt small-mut(+1)",
293+
codec = Lync.deltaInt(0, 1000000),
294+
seed = 12345,
295+
value = 12346,
296+
},
297+
{
298+
label = "deltaInt large-mut",
299+
codec = Lync.deltaInt(0, 1000000),
300+
seed = 12345,
301+
value = 999999,
302+
},
303+
{
304+
label = "deltaVec3 unchanged",
305+
codec = Lync.deltaVec3(-1000, 1000, 0.01),
306+
seed = Vector3.new(50, 60, 70),
307+
value = Vector3.new(50, 60, 70),
308+
},
309+
{
310+
label = "deltaVec3 small-mut(+0.1 stud)",
311+
codec = Lync.deltaVec3(-1000, 1000, 0.01),
312+
seed = Vector3.new(50, 60, 70),
313+
value = Vector3.new(50.1, 60.1, 70.1),
314+
},
315+
{
316+
label = "deltaCFrame static",
317+
codec = Lync.deltaCFrame(-1000, 1000, 0.01),
318+
seed = CFrame.new(10, 20, 30),
319+
value = CFrame.new(10, 20, 30),
320+
},
321+
{
322+
label = "deltaCFrame pos-only",
323+
codec = Lync.deltaCFrame(-1000, 1000, 0.01),
324+
seed = CFrame.new(10, 20, 30),
325+
value = CFrame.new(10.5, 20, 30),
326+
},
327+
{
328+
label = "deltaCFrame full pose",
329+
codec = Lync.deltaCFrame(-1000, 1000, 0.01),
330+
seed = CFrame.new(10, 20, 30),
331+
value = CFrame.new(11, 21, 31) * CFrame.Angles(1, 0, 0),
332+
},
264333
})
265334

266335
-- Round-trip catalogue (CPU only) ----------------------------------------
@@ -325,10 +394,80 @@ local blinkCases: { NetCase } = table.freeze({
325394
),
326395
})
327396

397+
--[[
398+
Pool builders for the new delta workloads. Each returns a frozen list
399+
of POOL_SIZE values forming a realistic mutation stream so the bench
400+
measures steady-state cache behavior, not first-frame FULL cost.
401+
]]
402+
local function poolEntityArrSparseMut(): { { any } }
403+
-- Stable array shape; 3 random indices flip per frame to exercise PATCH.
404+
local current = entityArr(ENT_COUNT)
405+
local p = table.create(POOL_SIZE)
406+
p[1] = table.clone(current)
407+
for frame = 2, POOL_SIZE do
408+
for _ = 1, 3 do
409+
local idx = rng:NextInteger(1, ENT_COUNT)
410+
current[idx] = entity()
411+
end
412+
p[frame] = table.clone(current)
413+
end
414+
return table.freeze(p) :: { { any } }
415+
end
416+
417+
local function poolPositionMap(): { { [number]: Vector3 } }
418+
-- 200-entry map; ~5 keys mutate per frame to exercise DIFF.
419+
local current: { [number]: Vector3 } = {}
420+
for i = 1, 200 do
421+
current[i] = Vector3.new(rng:NextNumber(-100, 100), 0, rng:NextNumber(-100, 100))
422+
end
423+
local p = table.create(POOL_SIZE)
424+
p[1] = table.clone(current)
425+
for frame = 2, POOL_SIZE do
426+
for _ = 1, 5 do
427+
local k = rng:NextInteger(1, 200)
428+
current[k] = Vector3.new(rng:NextNumber(-100, 100), 0, rng:NextNumber(-100, 100))
429+
end
430+
p[frame] = table.clone(current)
431+
end
432+
return table.freeze(p) :: { { [number]: Vector3 } }
433+
end
434+
435+
local function poolPositionStream(): { Vector3 }
436+
-- Continuous motion: + ~0.5 stud per axis per frame. Hot path for deltaVec3.
437+
local current = Vector3.new(0, 0, 0)
438+
local p = table.create(POOL_SIZE)
439+
for frame = 1, POOL_SIZE do
440+
p[frame] = current
441+
current = current
442+
+ Vector3.new(
443+
rng:NextNumber(-0.5, 0.5),
444+
rng:NextNumber(-0.5, 0.5),
445+
rng:NextNumber(-0.5, 0.5)
446+
)
447+
end
448+
return table.freeze(p) :: { Vector3 }
449+
end
450+
451+
local function poolCFrameStream(): { CFrame }
452+
-- Walking-character pose: position drifts, rotation slowly turns.
453+
local pos = Vector3.new(0, 0, 0)
454+
local yaw = 0
455+
local p = table.create(POOL_SIZE)
456+
for frame = 1, POOL_SIZE do
457+
p[frame] = CFrame.new(pos) * CFrame.Angles(0, yaw, 0)
458+
pos = pos + Vector3.new(rng:NextNumber(-0.3, 0.3), 0, rng:NextNumber(-0.3, 0.3))
459+
yaw = yaw + rng:NextNumber(-0.05, 0.05)
460+
end
461+
return table.freeze(p) :: { CFrame }
462+
end
463+
464+
local positionsCodec = Lync.deltaVec3(-1000, 1000, 0.01)
465+
local cframeStreamCodec = Lync.deltaCFrame(-1000, 1000, 0.01)
466+
328467
--[[
329468
Extended workloads at 100 fires/frame. Pairs vary/stable cases on the
330469
same codec to expose XOR + delta savings: vary = worst case, stable =
331-
same array reused so XOR collapses, delta = single field flipping.
470+
same array reused so XOR collapses, delta = sparse mutations.
332471
]]
333472
local extendedCases: { NetCase } = table.freeze({
334473
defCase(
@@ -345,6 +484,7 @@ local extendedCases: { NetCase } = table.freeze({
345484
return entityArr(ENT_COUNT)
346485
end)
347486
),
487+
defCase("entity_deltaArr_100__3mut", entityDeltaArray, poolEntityArrSparseMut()),
348488
defCase(
349489
"bool_arr_1000__vary",
350490
boolArray,
@@ -362,6 +502,10 @@ local extendedCases: { NetCase } = table.freeze({
362502
),
363503
defCase("state_full__vary", stateCodec, poolVarying(stateOne)),
364504
defCase("state_delta__1mut", stateDelta, poolStateOneMut()),
505+
defCase("position_map_200__5mut", positionMap, poolPositionMap()),
506+
defCase("position_deltaMap_200__5mut", positionDeltaMap, poolPositionMap()),
507+
defCase("vec3_walking_motion", positionsCodec, poolPositionStream()),
508+
defCase("cframe_walking_pose", cframeStreamCodec, poolCFrameStream()),
365509
})
366510

367511
-- Handshake --------------------------------------------------------------

src/Types.luau

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@ export type ChannelState = {
1212
itemCount: number,
1313
singleMode: boolean,
1414
singlePos: number,
15-
deltas: { [number]: DeltaCacheEntry },
15+
--[[
16+
Per-codec write-side delta state, keyed by deltaId. Shape is opaque
17+
and codec-internal: deltaStruct stores `{raw, len, segOff, segLen}`,
18+
deltaArray stores `{perIdx, length}`, deltaMap stores `{perKey}`,
19+
delta scalars store quantized previous values.
20+
]]
21+
deltas: { [number]: any },
1622
prevDump: buffer?,
1723
prevDumpLen: number,
1824
currentPacket: string?,
1925
}
2026

21-
export type DeltaCacheEntry = {
22-
raw: buffer,
23-
len: number,
24-
}
25-
2627
export type Codec<T> = {
2728
write: (ch: ChannelState, value: T) -> (),
2829
read: (src: buffer, pos: number, refs: { Instance }?) -> (T, number),
@@ -39,6 +40,11 @@ export type InternalCodec<T> = {
3940
_directWrite: ((b: buffer, offset: number, value: T) -> ())?,
4041
_directRead: ((b: buffer, offset: number) -> T)?,
4142
_isDelta: boolean?,
43+
--[[
44+
True if any nested codec is delta. Lets the broadcast hot path skip
45+
encode-once when delta state would diverge per-receiver.
46+
]]
47+
_hasDelta: boolean?,
4248
_isBool: boolean?,
4349
_hasUnknown: boolean?,
4450
_min: number?,

0 commit comments

Comments
 (0)