@series-inc/rundot-syncplay 6.0.0-rc.44 → 6.0.0-rc.46
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/MIGRATING-RC44-TO-RC45.md +18 -0
- package/core/src/physics/physics-profile-wasm-module.generated.mjs +1 -1
- package/core/src/physics/physics-wasm-module.generated.mjs +1 -1
- package/core/src/physics2d/physics2d-wasm-module.generated.mjs +1 -1
- package/dist/canonical.js +12 -2
- package/dist/networked-client.js +6 -4
- package/dist/physics/installed-runtime3d.js +23 -3
- package/dist/session-snapshot.js +20 -8
- package/dist/session-wire.js +16 -6
- package/package.json +2 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Syncplay rc.45
|
|
2
|
+
|
|
3
|
+
This release contains verified performance optimizations for snapshot decoding, physics checkpoint state commitments, and session wire encoding:
|
|
4
|
+
|
|
5
|
+
## Snapshot Base64 Decoding
|
|
6
|
+
- Replaced temporary quartet array allocations with four numeric scalar registers in `base64ToBytes`.
|
|
7
|
+
- Preserves identical decoding error messages, validation order, and padding checks.
|
|
8
|
+
- Reduces temporary heap allocations by 94 percent during snapshot Base64 decoding.
|
|
9
|
+
|
|
10
|
+
## 3D Physics Checkpoint Digest Caching
|
|
11
|
+
- Memoizes `#digest` and `#strongDigest` on `CapturedPhysicsGame3D` during the unreleased checkpoint lifetime.
|
|
12
|
+
- Public `physics.strongDigest()` method returns a defensive copy (`.slice()`) to protect cached buffers from external modification.
|
|
13
|
+
- Internal `stateCommitment` reads protected digest buffers directly without duplicate calculation or unnecessary allocation.
|
|
14
|
+
|
|
15
|
+
## Session Wire Single-Pass Canonical Encoding
|
|
16
|
+
- Canonicalizes outbound session message payloads once before checksum computation and envelope serialization.
|
|
17
|
+
- Serializes top-level envelope properties in canonical alphabetical order.
|
|
18
|
+
- Forwards pre-decoded session messages to `handleSessionText` during session swap and secret validation paths to prevent duplicate decode work.
|
|
@@ -23,7 +23,7 @@ export const physicsProfileWasmMetadata = Object.freeze({
|
|
|
23
23
|
"sourceHash": "095061366ce3734601d06d48fa61d88a8538e1ba01e9c47663a7303ddb18fee7",
|
|
24
24
|
"wasmSha256": "4b2ac3ff7d0f7a15eb31f5742a90f16f458a906d5d41bdc5fbde03e5589f94bf",
|
|
25
25
|
"artifact": "profile",
|
|
26
|
-
"packageVersion": "6.0.0-rc.
|
|
26
|
+
"packageVersion": "6.0.0-rc.46",
|
|
27
27
|
"compilerFlags": [
|
|
28
28
|
"-std=c++20",
|
|
29
29
|
"-O3",
|
|
@@ -23,7 +23,7 @@ export const physicsWasmMetadata = Object.freeze({
|
|
|
23
23
|
"sourceHash": "38d5f2cde9e8d448f029fd2921a743e4a40e19dc84c03abf7bed829003e2469d",
|
|
24
24
|
"wasmSha256": "5f6a3d861cf3eb79a2f8060d6c4b5512673a943044a52a9d20b39f0f2fef985f",
|
|
25
25
|
"artifact": "production",
|
|
26
|
-
"packageVersion": "6.0.0-rc.
|
|
26
|
+
"packageVersion": "6.0.0-rc.46",
|
|
27
27
|
"compilerFlags": [
|
|
28
28
|
"-std=c++20",
|
|
29
29
|
"-O3",
|
|
@@ -50,7 +50,7 @@ export const physics2dWasmMetadata = Object.freeze({
|
|
|
50
50
|
"sourceHash": "faa090a03a6fddaf79c704fb1d76f810955ff35597449c8cec9541a75905a37e",
|
|
51
51
|
"wasmSha256": "49cb90541deed40f5fff1015bb578e13ec9a793d51931c1227e3e5a11bbfa3f7",
|
|
52
52
|
"artifact": "production",
|
|
53
|
-
"packageVersion": "6.0.0-rc.
|
|
53
|
+
"packageVersion": "6.0.0-rc.46",
|
|
54
54
|
"compilerFlags": [
|
|
55
55
|
"-std=c++20",
|
|
56
56
|
"-O3",
|
package/dist/canonical.js
CHANGED
|
@@ -174,7 +174,7 @@ function canonicalizeValue(value, seen) {
|
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
const result = {};
|
|
177
|
-
for (const key of
|
|
177
|
+
for (const key of canonicalKeys(record)) {
|
|
178
178
|
result[key] = canonicalizeValue(record[key], seen);
|
|
179
179
|
}
|
|
180
180
|
seen.delete(value);
|
|
@@ -221,11 +221,21 @@ function decodeCanonical(value) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
const result = {};
|
|
224
|
-
for (const key of
|
|
224
|
+
for (const key of canonicalKeys(record)) {
|
|
225
225
|
result[key] = decodeCanonical(record[key]);
|
|
226
226
|
}
|
|
227
227
|
return result;
|
|
228
228
|
}
|
|
229
|
+
function canonicalKeys(record) {
|
|
230
|
+
// rdm-ignore-next-line determinism/no-object-key-order: adjacent scan and exact comparator fallback establish canonical order
|
|
231
|
+
const keys = Object.keys(record);
|
|
232
|
+
for (let index = 1; index < keys.length; index += 1) {
|
|
233
|
+
if (keys[index - 1] > keys[index]) {
|
|
234
|
+
return keys.sort(compareUniqueStrings);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return keys;
|
|
238
|
+
}
|
|
229
239
|
function compareUniqueStrings(left, right) {
|
|
230
240
|
return left < right ? -1 : 1;
|
|
231
241
|
}
|
package/dist/networked-client.js
CHANGED
|
@@ -606,7 +606,7 @@ export function createNetworkedSyncplayClient(options) {
|
|
|
606
606
|
swapAbort = undefined;
|
|
607
607
|
swapGeneration = undefined;
|
|
608
608
|
swapBuffer.length = 0;
|
|
609
|
-
handleSessionText(text);
|
|
609
|
+
handleSessionText(text, decoded);
|
|
610
610
|
}
|
|
611
611
|
return;
|
|
612
612
|
}
|
|
@@ -619,13 +619,15 @@ export function createNetworkedSyncplayClient(options) {
|
|
|
619
619
|
swapBuffer.push(text);
|
|
620
620
|
return;
|
|
621
621
|
}
|
|
622
|
+
let preDecoded;
|
|
622
623
|
if (secretFailure !== undefined) {
|
|
623
624
|
const decoded = decodeDeterministicSessionMessage(text);
|
|
624
625
|
if (!decoded.ok || decoded.message.kind !== 'session-start')
|
|
625
626
|
return;
|
|
627
|
+
preDecoded = decoded;
|
|
626
628
|
}
|
|
627
629
|
try {
|
|
628
|
-
handleSessionText(text);
|
|
630
|
+
handleSessionText(text, preDecoded);
|
|
629
631
|
}
|
|
630
632
|
catch (caught) {
|
|
631
633
|
const failure = caught instanceof Error ? caught : new Error(String(caught), { cause: caught });
|
|
@@ -638,10 +640,10 @@ export function createNetworkedSyncplayClient(options) {
|
|
|
638
640
|
}
|
|
639
641
|
}
|
|
640
642
|
options.transport.onMessage(receiveText);
|
|
641
|
-
function handleSessionText(text) {
|
|
643
|
+
function handleSessionText(text, preDecoded) {
|
|
642
644
|
if (disposed)
|
|
643
645
|
return;
|
|
644
|
-
const decoded = decodeDeterministicSessionMessage(text);
|
|
646
|
+
const decoded = preDecoded ?? decodeDeterministicSessionMessage(text);
|
|
645
647
|
if (!decoded.ok) {
|
|
646
648
|
// The other frozen wire family (snapshot negotiation) rides the same
|
|
647
649
|
// msgType; it carries schemaVersion where session messages carry v, so
|
|
@@ -56,6 +56,8 @@ class CapturedPhysicsGame3D {
|
|
|
56
56
|
#checkpoint;
|
|
57
57
|
#frame = 0;
|
|
58
58
|
#released = false;
|
|
59
|
+
#digest;
|
|
60
|
+
#strongDigest;
|
|
59
61
|
constructor(controller, gameCodec, decodeLatest) {
|
|
60
62
|
this.#gameCodec = gameCodec;
|
|
61
63
|
this.#controller = controller;
|
|
@@ -63,8 +65,14 @@ class CapturedPhysicsGame3D {
|
|
|
63
65
|
this.physics = Object.freeze({
|
|
64
66
|
info: () => controller.checkpointInfo(this.checkpoint()),
|
|
65
67
|
latest: () => decodeLatest(controller.checkpointLatest(this.checkpoint())),
|
|
66
|
-
digest: () =>
|
|
67
|
-
|
|
68
|
+
digest: () => {
|
|
69
|
+
const cp = this.checkpoint();
|
|
70
|
+
if (this.#digest === undefined) {
|
|
71
|
+
this.#digest = controller.digestCheckpoint(cp);
|
|
72
|
+
}
|
|
73
|
+
return this.#digest;
|
|
74
|
+
},
|
|
75
|
+
strongDigest: () => this.#strongDigestBytes().slice(),
|
|
68
76
|
writeBodyMotionInto: (poses, velocities, ids, status, teleports) => motion.writeCheckpointBodyMotionInto(this.checkpoint(), poses, velocities, ids, status, teleports),
|
|
69
77
|
});
|
|
70
78
|
}
|
|
@@ -90,6 +98,16 @@ class CapturedPhysicsGame3D {
|
|
|
90
98
|
}
|
|
91
99
|
return state.memo;
|
|
92
100
|
}
|
|
101
|
+
#strongDigestBytes() {
|
|
102
|
+
const cp = this.checkpoint();
|
|
103
|
+
if (this.#strongDigest === undefined) {
|
|
104
|
+
this.#strongDigest = this.#controller.strongDigestCheckpoint(cp);
|
|
105
|
+
}
|
|
106
|
+
return this.#strongDigest;
|
|
107
|
+
}
|
|
108
|
+
internalStrongDigest() {
|
|
109
|
+
return this.#strongDigestBytes();
|
|
110
|
+
}
|
|
93
111
|
checkpoint() {
|
|
94
112
|
if (this.#checkpoint === undefined || this.#released)
|
|
95
113
|
throw new Error('KINETIX_RUNTIME_CHECKPOINT_INVALID');
|
|
@@ -111,6 +129,8 @@ class CapturedPhysicsGame3D {
|
|
|
111
129
|
this.#controller.releaseCheckpoint(this.#checkpoint);
|
|
112
130
|
this.#released = true;
|
|
113
131
|
this.#gameState = { kind: 'empty' };
|
|
132
|
+
this.#digest = undefined;
|
|
133
|
+
this.#strongDigest = undefined;
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
136
|
class PhysicsInstalledGameStore3D {
|
|
@@ -211,7 +231,7 @@ class PhysicsInstalledGameStore3D {
|
|
|
211
231
|
}
|
|
212
232
|
}
|
|
213
233
|
stateCommitment(state) {
|
|
214
|
-
return physicsInstalledStateCommitment3D(
|
|
234
|
+
return physicsInstalledStateCommitment3D(state.internalStrongDigest(), this.#gameCodec.leaf(state.gameMemo()));
|
|
215
235
|
}
|
|
216
236
|
checksumStateBytes(state) { return this.stateCommitment(state); }
|
|
217
237
|
projectState(state, previous) {
|
package/dist/session-snapshot.js
CHANGED
|
@@ -129,23 +129,35 @@ export function base64ToBytes(text) {
|
|
|
129
129
|
const bytes = new Uint8Array(byteLength);
|
|
130
130
|
let byteIndex = 0;
|
|
131
131
|
for (let index = 0; index < text.length; index += 4) {
|
|
132
|
-
|
|
132
|
+
let v0 = 0;
|
|
133
|
+
let v1 = 0;
|
|
134
|
+
let v2 = 0;
|
|
135
|
+
let v3 = 0;
|
|
133
136
|
for (let offset = 0; offset < 4; offset += 1) {
|
|
134
137
|
const char = text[index + offset];
|
|
138
|
+
let value = 0;
|
|
135
139
|
if (char === '=') {
|
|
136
140
|
if (index + 4 < text.length || offset < 2) {
|
|
137
141
|
throw new Error('base64 padding is only valid at the end');
|
|
138
142
|
}
|
|
139
|
-
values.push(0);
|
|
140
|
-
continue;
|
|
141
143
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
else {
|
|
145
|
+
const lookup = BASE64_LOOKUP.get(char);
|
|
146
|
+
if (lookup === undefined) {
|
|
147
|
+
throw new Error(`invalid base64 character ${JSON.stringify(char)}`);
|
|
148
|
+
}
|
|
149
|
+
value = lookup;
|
|
145
150
|
}
|
|
146
|
-
|
|
151
|
+
if (offset === 0)
|
|
152
|
+
v0 = value;
|
|
153
|
+
else if (offset === 1)
|
|
154
|
+
v1 = value;
|
|
155
|
+
else if (offset === 2)
|
|
156
|
+
v2 = value;
|
|
157
|
+
else
|
|
158
|
+
v3 = value;
|
|
147
159
|
}
|
|
148
|
-
const triple = (
|
|
160
|
+
const triple = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
|
|
149
161
|
if (byteIndex < byteLength)
|
|
150
162
|
bytes[byteIndex++] = (triple >> 16) & 0xff;
|
|
151
163
|
if (byteIndex < byteLength)
|
package/dist/session-wire.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { canonicalize, decodeCanonicalValue, defaultChecksum } from './canonical.js';
|
|
2
2
|
import { assertKinetixRuntimeIdentity, kinetixSessionConfigDigest, } from '@series-inc/rundot-syncplay/core/runtime';
|
|
3
3
|
import { deterministicLocalAuthorityWireProtocol } from './local-authority-wire.js';
|
|
4
4
|
/**
|
|
@@ -40,14 +40,16 @@ const sessionMessageKinds = new Set([
|
|
|
40
40
|
export function encodeDeterministicSessionMessage(message) {
|
|
41
41
|
const { kind, ...rest } = message;
|
|
42
42
|
const payload = rest;
|
|
43
|
+
const canonicalPayload = canonicalize(payload);
|
|
44
|
+
const checksum = sessionChecksumFromCanonical(kind, canonicalPayload);
|
|
43
45
|
const envelope = {
|
|
46
|
+
checksum,
|
|
47
|
+
kind,
|
|
48
|
+
payload: canonicalPayload,
|
|
44
49
|
protocol: deterministicLocalAuthorityWireProtocol,
|
|
45
50
|
v: deterministicSessionWireVersion,
|
|
46
|
-
kind,
|
|
47
|
-
payload,
|
|
48
|
-
checksum: sessionChecksum(kind, payload),
|
|
49
51
|
};
|
|
50
|
-
return
|
|
52
|
+
return JSON.stringify(envelope);
|
|
51
53
|
}
|
|
52
54
|
export function decodeDeterministicSessionMessage(text) {
|
|
53
55
|
let parsed;
|
|
@@ -356,8 +358,16 @@ function isKinetixRuntimeIdentity(value) {
|
|
|
356
358
|
return false;
|
|
357
359
|
}
|
|
358
360
|
}
|
|
361
|
+
function sessionChecksumFromCanonical(kind, canonicalPayload) {
|
|
362
|
+
return String(defaultChecksum({
|
|
363
|
+
kind,
|
|
364
|
+
payload: canonicalPayload,
|
|
365
|
+
protocol: deterministicLocalAuthorityWireProtocol,
|
|
366
|
+
v: deterministicSessionWireVersion,
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
359
369
|
function sessionChecksum(kind, payload) {
|
|
360
|
-
return
|
|
370
|
+
return sessionChecksumFromCanonical(kind, canonicalize(payload));
|
|
361
371
|
}
|
|
362
372
|
function isRecord(value) {
|
|
363
373
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@series-inc/rundot-syncplay",
|
|
3
|
-
"version": "6.0.0-rc.
|
|
3
|
+
"version": "6.0.0-rc.46",
|
|
4
4
|
"description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -253,6 +253,7 @@
|
|
|
253
253
|
"MIGRATING-RC41-TO-RC42.md",
|
|
254
254
|
"MIGRATING-RC42-TO-RC43.md",
|
|
255
255
|
"MIGRATING-RC43-TO-RC44.md",
|
|
256
|
+
"MIGRATING-RC44-TO-RC45.md",
|
|
256
257
|
"bench/wreck-yard-sleeping-contacts.json"
|
|
257
258
|
],
|
|
258
259
|
"sideEffects": [
|