Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 74 additions & 19 deletions src/Core/Batcher.luau
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
--!native
--!optimize 2
-- This file is part of the Satset networking library and is licensed under MIT License; see LICENSE.txt for details
-- Batcher — handles per-frame packet batching and unreliable transport hardening.
-- Batcher — handles frame-level batching, dynamic segmentation, and rate-limited flushing.

local RunService = game:GetService("RunService")

Expand All @@ -13,8 +13,9 @@ local IS_SERVER = RunService:IsServer()

-- UnreliableRemoteEvent has a limit around 1000 bytes.
-- We cap at 900 to stay safe.
local RELIABLE_SPLIT_THRESHOLD = 45000
local RELIABLE_SPLIT_THRESHOLD = 60000
local UNRELIABLE_SPLIT_THRESHOLD = 900
local MAX_PACKETS_PER_FRAME = 0 -- 0 means no limit (Latency Mode)
local INITIAL_WORKING_SIZE = 65536

type Stream = {
Expand Down Expand Up @@ -50,6 +51,24 @@ function Batcher.registerFixedSize(id: number, size: number)
packetFixedSizes[id] = size
end

export type BatchingConfig = {
reliableThreshold: number?,
unreliableThreshold: number?,
maxPacketsPerFrame: number?,
}

function Batcher.configure(config: BatchingConfig)
if config.reliableThreshold then
RELIABLE_SPLIT_THRESHOLD = config.reliableThreshold
end
if config.unreliableThreshold then
UNRELIABLE_SPLIT_THRESHOLD = config.unreliableThreshold
end
if config.maxPacketsPerFrame ~= nil then
MAX_PACKETS_PER_FRAME = config.maxPacketsPerFrame
end
end

local function commitStream(stream: Stream, reliable: boolean, seqCounter: number?): (buffer?, number?)
if stream.count == 0 then
return nil, seqCounter
Expand Down Expand Up @@ -323,10 +342,18 @@ local function flush()
if exact then
table.insert(batches, exact)
end
for _, batch in batches do
reliableRemote:FireClient(player, batch)

local sentThisFrame = 0
local i = 1
while i <= #batches do
reliableRemote:FireClient(player, batches[i])
sentThisFrame += 1
table.remove(batches, i)

if MAX_PACKETS_PER_FRAME > 0 and sentThisFrame >= MAX_PACKETS_PER_FRAME then
break
end
end
table.clear(batches)
elseif exact then
reliableRemote:FireClient(player, exact)
end
Expand All @@ -342,10 +369,18 @@ local function flush()
if exact then
table.insert(batches, exact)
end
for _, batch in batches do
unreliableRemote:FireClient(player, batch)

local sentThisFrame = 0
local i = 1
while i <= #batches do
unreliableRemote:FireClient(player, batches[i])
sentThisFrame += 1
table.remove(batches, i)

if MAX_PACKETS_PER_FRAME > 0 and sentThisFrame >= MAX_PACKETS_PER_FRAME then
break
end
end
table.clear(batches)
elseif exact then
unreliableRemote:FireClient(player, exact)
end
Expand All @@ -355,32 +390,52 @@ local function flush()
if exact then
table.insert(broadcastReliableBatches, exact)
end
for _, batch in broadcastReliableBatches do
reliableRemote:FireAllClients(batch)

local sentThisFrame = 0
local i = 1
while i <= #broadcastReliableBatches do
reliableRemote:FireAllClients(broadcastReliableBatches[i])
sentThisFrame += 1
table.remove(broadcastReliableBatches, i)

if MAX_PACKETS_PER_FRAME > 0 and sentThisFrame >= MAX_PACKETS_PER_FRAME then
break
end
end
table.clear(broadcastReliableBatches)
else
local exactR, _ = commitStream(serverReliableStream, true)
if exactR then
table.insert(serverReliableBatches, exactR)
end
if #serverReliableBatches > 0 then
for _, batch in serverReliableBatches do
Bridge.getReliable():FireServer(batch)

local sentThisFrame = 0
local i = 1
while i <= #serverReliableBatches do
Bridge.getReliable():FireServer(serverReliableBatches[i])
sentThisFrame += 1
table.remove(serverReliableBatches, i)

if MAX_PACKETS_PER_FRAME > 0 and sentThisFrame >= MAX_PACKETS_PER_FRAME then
break
end
table.clear(serverReliableBatches)
end

local exactU, newSeq = commitStream(serverUnreliableStream, false, clientSeqCounter)
clientSeqCounter = newSeq :: number
if exactU then
table.insert(serverUnreliableBatches, exactU)
end
if #serverUnreliableBatches > 0 then
for _, batch in serverUnreliableBatches do
Bridge.getUnreliable():FireServer(batch)

i = 1
local sentThisFrameU = 0
while i <= #serverUnreliableBatches do
Bridge.getUnreliable():FireServer(serverUnreliableBatches[i])
sentThisFrameU += 1
table.remove(serverUnreliableBatches, i)

if MAX_PACKETS_PER_FRAME > 0 and sentThisFrameU >= MAX_PACKETS_PER_FRAME then
break
end
table.clear(serverUnreliableBatches)
end
end
end
Expand Down
108 changes: 99 additions & 9 deletions src/Types/init.luau
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
--!native
--!optimize 2
-- This file is part of the Satset networking library and is licensed under MIT License; see LICENSE.txt for details
-- Types — defines all primitive and composite networking types.
-- Types — provides optimized serialization primitives and composite types.

-- [ Builtin Localization ]
-- We localize these for GETIMPORT and FASTCALL optimization in the Luau VM.
Expand Down Expand Up @@ -283,6 +283,79 @@ function Types.Vector3Quantized(range: number?): Type<Vector3>
}
end

--[[
Float16: 2-byte floating point.
Provides lower precision than f32 but significantly saves bandwidth.
Based on Half-Precision (IEEE 754-2008).
]]
Types.f16 = {
read = function(b: buffer, cursor: number)
local raw = readu16(b, cursor)
local sign = (bit32.band(raw, 0x8000) ~= 0) and -1 or 1
local exp = bit32.rshift(bit32.band(raw, 0x7C00), 10)
local frac = bit32.band(raw, 0x03FF)
if exp == 0x1F then
return (frac == 0) and (sign * huge) or 0 -- NaN as 0 for safety
elseif exp == 0 then
return sign * (2 ^ -14) * (frac / 1024)
else
return sign * (2 ^ (exp - 15)) * (1 + frac / 1024)
end
end,
write = function(b: buffer, cursor: number, value: number)
if value ~= value then
writeu16(b, cursor, 0)
return
end
local sign = 0
if value < 0 then
sign = 0x8000
value = -value
end
local exp, frac
if value > 65504 then
exp, frac = 0x1F, 0
elseif value <= 2 ^ -24 then
exp, frac = 0, 0
elseif value < 2 ^ -14 then
exp, frac = 0, round(value / (2 ^ -24))
else
local log2 = math.log(value, 2)
exp = floor(log2)
frac = round((value / (2 ^ exp) - 1) * 1024)
if frac == 1024 then
exp += 1
frac = 0
end
exp += 15
end
writeu16(b, cursor, bit32.bor(sign, bit32.lshift(exp, 10), frac))
end,
size = 2,
getSize = fixedSize(2),
isFixed = true,
}

--[[
Vector3F16: 6-byte Vector3 using Float16 for each component.
]]
Types.Vector3F16 = {
read = function(b: buffer, cursor: number)
local x = Types.f16.read(b, cursor)
local y = Types.f16.read(b, cursor + 2)
local z = Types.f16.read(b, cursor + 4)
return Vector3.new(x, y, z)
end,
write = function(b: buffer, cursor: number, value: Vector3)
Types.f16.write(b, cursor, value.X)
Types.f16.write(b, cursor + 2, value.Y)
Types.f16.write(b, cursor + 4, value.Z)
end,
size = 6,
getSize = fixedSize(6),
isFixed = true,
}

--[[
Vector2Quantized: compressed 4-byte Vector2.

Expand Down Expand Up @@ -515,27 +588,44 @@ function Types.array(elementType: Type<any>): Type<{ any }>
read = function(b: buffer, cursor: number)
local bufLen = buffer.len(b)
local len = readu16(b, cursor)
local maxPossible = bufLen - cursor - 2
local maxPossible = (bufLen - cursor - 2) * 8
len = min(len, max(maxPossible, 0))
local arr, pos = table.create(len), cursor + 2
for i = 1, len do
arr[i] = readu8(b, pos) ~= 0
pos += 1

local byteLen = floor((len + 7) / 8)
for i = 0, byteLen - 1 do
local byte = readu8(b, pos + i)
local startIdx = i * 8 + 1
local endIdx = min(startIdx + 7, len)

for j = startIdx, endIdx do
arr[j] = bit32.band(bit32.rshift(byte, (j - 1) % 8), 1) ~= 0
end
end
return arr
end,
write = function(b: buffer, cursor: number, value: { any })
local len = #value
writeu16(b, cursor, len)
local pos = cursor + 2
for i = 1, len do
writeu8(b, pos, value[i] and 1 or 0)
pos += 1
local byteLen = floor((len + 7) / 8)

for i = 0, byteLen - 1 do
local byte = 0
local startIdx = i * 8 + 1
local endIdx = min(startIdx + 7, len)

for j = startIdx, endIdx do
if value[j] then
byte = bit32.bor(byte, bit32.lshift(1, (j - 1) % 8))
end
end
writeu8(b, pos + i, byte)
end
end,
size = 2,
getSize = function(value: { any })
return 2 + #value
return 2 + floor((#value + 7) / 8)
end,
isFixed = false,
}
Expand Down
10 changes: 8 additions & 2 deletions src/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Satset.defineChannel = Channel.defineChannel

export type SatsetConfig = {
guard: Guard.GuardConfig?,
batching: Batcher.BatchingConfig?,
}

--[[
Expand All @@ -58,14 +59,19 @@ function Satset.start(config: SatsetConfig?)
local context = if IS_SERVER then "Server" else "Client"
print(`[Satset v{VERSION}] Initializing on {context}`)

local cfg: SatsetConfig = config or {} :: SatsetConfig

-- Apply custom batching configuration if provided.
if cfg.batching then
Batcher.configure(cfg.batching)
end

-- We initialize the bridge first to ensure remotes are ready.
Bridge.init()

-- The batcher hooks into PostSimulation to flush outgoing data.
Batcher.init()

local cfg: SatsetConfig = config or {} :: SatsetConfig

-- Dedicated channel folder for organization.
local bridgeFolder = game:GetService("ReplicatedStorage"):FindFirstChild("__SatsetBridge")

Expand Down
Loading