@sadhaka/loom-engine 0.11.0 → 0.13.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/COMMERCIAL_LICENSE_TERMS.md +74 -0
- package/README.md +230 -6
- package/dist/components/peer-sprite.d.ts +25 -0
- package/dist/components/peer-sprite.d.ts.map +1 -0
- package/dist/components/peer-sprite.js +48 -0
- package/dist/components/peer-sprite.js.map +1 -0
- package/dist/engine.d.ts +6 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +35 -1
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/network/mock-multiplayer-bridge.d.ts +34 -0
- package/dist/network/mock-multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/mock-multiplayer-bridge.js +91 -0
- package/dist/network/mock-multiplayer-bridge.js.map +1 -0
- package/dist/network/multiplayer-bridge.d.ts +45 -0
- package/dist/network/multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/multiplayer-bridge.js +34 -0
- package/dist/network/multiplayer-bridge.js.map +1 -0
- package/dist/network/peer-pool.d.ts +46 -0
- package/dist/network/peer-pool.d.ts.map +1 -0
- package/dist/network/peer-pool.js +185 -0
- package/dist/network/peer-pool.js.map +1 -0
- package/dist/network/sse-multiplayer-bridge.d.ts +36 -0
- package/dist/network/sse-multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/sse-multiplayer-bridge.js +264 -0
- package/dist/network/sse-multiplayer-bridge.js.map +1 -0
- package/dist/renderer/shaders/sprite-shader-source.d.ts +4 -0
- package/dist/renderer/shaders/sprite-shader-source.d.ts.map +1 -0
- package/dist/renderer/shaders/sprite-shader-source.js +83 -0
- package/dist/renderer/shaders/sprite-shader-source.js.map +1 -0
- package/dist/renderer/sprite-batcher.d.ts +30 -0
- package/dist/renderer/sprite-batcher.d.ts.map +1 -0
- package/dist/renderer/sprite-batcher.js +146 -0
- package/dist/renderer/sprite-batcher.js.map +1 -0
- package/dist/renderer/texture-atlas.d.ts +24 -0
- package/dist/renderer/texture-atlas.d.ts.map +1 -0
- package/dist/renderer/texture-atlas.js +173 -0
- package/dist/renderer/texture-atlas.js.map +1 -0
- package/dist/renderer/webgl2-device.d.ts +53 -0
- package/dist/renderer/webgl2-device.d.ts.map +1 -0
- package/dist/renderer/webgl2-device.js +596 -0
- package/dist/renderer/webgl2-device.js.map +1 -0
- package/dist/systems/peer-presence-system.d.ts +19 -0
- package/dist/systems/peer-presence-system.d.ts.map +1 -0
- package/dist/systems/peer-presence-system.js +118 -0
- package/dist/systems/peer-presence-system.js.map +1 -0
- package/package.json +11 -5
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// SSEMultiplayerBridge - real EventSource subscription to the
|
|
2
|
+
// backend's presence endpoint, paired with a fetch POST for outbound
|
|
3
|
+
// position broadcasts.
|
|
4
|
+
//
|
|
5
|
+
// Wire protocol (paired with Track B server-side):
|
|
6
|
+
// GET <baseUrl>?character_id=...&zone=... opens an SSE stream that
|
|
7
|
+
// emits three event types:
|
|
8
|
+
// - 'presence.snapshot' { peers: [{ character_id, x, y, zone, ts_ms, name? }] }
|
|
9
|
+
// emitted once on connect with the full current peer roster.
|
|
10
|
+
// - 'presence.update' { character_id, x, y, zone, ts_ms, name? }
|
|
11
|
+
// emitted as peers move.
|
|
12
|
+
// - 'presence.depart' { character_id }
|
|
13
|
+
// emitted when a peer disconnects.
|
|
14
|
+
//
|
|
15
|
+
// POST <broadcastUrl> { character_id, x, y, zone, ts_ms }
|
|
16
|
+
// called by broadcastPosition at most BROADCAST_HZ per second.
|
|
17
|
+
// Engine-side rate limit; the bridge silently drops excess calls
|
|
18
|
+
// and increments rateLimitedDrops.
|
|
19
|
+
//
|
|
20
|
+
// Reconnect strategy: EventSource handles transport-layer reconnect
|
|
21
|
+
// internally. We observe via onerror/onopen and surface 'reconnecting'
|
|
22
|
+
// to the consumer. On reconnect the server is expected to re-emit a
|
|
23
|
+
// fresh 'presence.snapshot', which the PeerPool consumes and treats
|
|
24
|
+
// as authoritative (any peer not in the snapshot is dropped).
|
|
25
|
+
//
|
|
26
|
+
// Browser-only. Constructor throws if EventSource is undefined (Node
|
|
27
|
+
// test environment). Tests use MockMultiplayerBridge instead.
|
|
28
|
+
import { BROADCAST_MIN_INTERVAL_MS, } from './multiplayer-bridge.js';
|
|
29
|
+
export class SSEMultiplayerBridge {
|
|
30
|
+
baseUrl;
|
|
31
|
+
broadcastUrl;
|
|
32
|
+
characterId;
|
|
33
|
+
zone;
|
|
34
|
+
eventSourceFactory;
|
|
35
|
+
fetchFn;
|
|
36
|
+
es = null;
|
|
37
|
+
queue = [];
|
|
38
|
+
statusValue = 'idle';
|
|
39
|
+
statsValue = {
|
|
40
|
+
messagesReceived: 0,
|
|
41
|
+
messagesSent: 0,
|
|
42
|
+
rateLimitedDrops: 0,
|
|
43
|
+
reconnects: 0,
|
|
44
|
+
};
|
|
45
|
+
lastBroadcastMs = -Infinity;
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
this.baseUrl = opts.baseUrl;
|
|
48
|
+
this.broadcastUrl = opts.broadcastUrl ?? defaultBroadcastUrl(opts.baseUrl);
|
|
49
|
+
this.characterId = opts.characterId;
|
|
50
|
+
this.zone = opts.zone;
|
|
51
|
+
if (opts.eventSourceFactory) {
|
|
52
|
+
this.eventSourceFactory = opts.eventSourceFactory;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
if (typeof EventSource === 'undefined') {
|
|
56
|
+
throw new Error('SSEMultiplayerBridge: EventSource is not available in this environment. Use MockMultiplayerBridge for tests.');
|
|
57
|
+
}
|
|
58
|
+
const ESCtor = EventSource;
|
|
59
|
+
this.eventSourceFactory = (u) => new ESCtor(u, { withCredentials: true });
|
|
60
|
+
}
|
|
61
|
+
if (opts.fetchFn) {
|
|
62
|
+
this.fetchFn = opts.fetchFn;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
if (typeof fetch === 'undefined') {
|
|
66
|
+
throw new Error('SSEMultiplayerBridge: fetch is not available in this environment.');
|
|
67
|
+
}
|
|
68
|
+
this.fetchFn = fetch.bind(globalThis);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
connect() {
|
|
72
|
+
if (this.es)
|
|
73
|
+
return;
|
|
74
|
+
this.statusValue = 'connecting';
|
|
75
|
+
this.openConnection();
|
|
76
|
+
}
|
|
77
|
+
disconnect() {
|
|
78
|
+
this.statusValue = 'closed';
|
|
79
|
+
this.closeConnection();
|
|
80
|
+
}
|
|
81
|
+
status() {
|
|
82
|
+
return this.statusValue;
|
|
83
|
+
}
|
|
84
|
+
pollMessages() {
|
|
85
|
+
if (this.queue.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const out = this.queue;
|
|
88
|
+
this.queue = [];
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
broadcastPosition(x, y, zone, tsMs) {
|
|
92
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
93
|
+
if (now - this.lastBroadcastMs < BROADCAST_MIN_INTERVAL_MS) {
|
|
94
|
+
this.statsValue.rateLimitedDrops++;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.lastBroadcastMs = now;
|
|
98
|
+
this.statsValue.messagesSent++;
|
|
99
|
+
// Fire-and-forget POST. Errors are surfaced via stats only - the
|
|
100
|
+
// engine doesn't block on the network round trip. The body
|
|
101
|
+
// matches the server contract from the phase 15.1 spec.
|
|
102
|
+
const body = JSON.stringify({
|
|
103
|
+
character_id: this.characterId,
|
|
104
|
+
x,
|
|
105
|
+
y,
|
|
106
|
+
zone,
|
|
107
|
+
ts_ms: tsMs,
|
|
108
|
+
});
|
|
109
|
+
void this.fetchFn(this.broadcastUrl, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
credentials: 'include',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body,
|
|
114
|
+
}).catch(() => { });
|
|
115
|
+
}
|
|
116
|
+
stats() {
|
|
117
|
+
return this.statsValue;
|
|
118
|
+
}
|
|
119
|
+
// ----- Internal -----
|
|
120
|
+
buildUrl() {
|
|
121
|
+
const sep = this.baseUrl.includes('?') ? '&' : '?';
|
|
122
|
+
return (this.baseUrl +
|
|
123
|
+
sep +
|
|
124
|
+
'character_id=' + encodeURIComponent(this.characterId) +
|
|
125
|
+
'&zone=' + encodeURIComponent(this.zone));
|
|
126
|
+
}
|
|
127
|
+
openConnection() {
|
|
128
|
+
const url = this.buildUrl();
|
|
129
|
+
let es;
|
|
130
|
+
try {
|
|
131
|
+
es = this.eventSourceFactory(url);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
this.statusValue = 'closed';
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
this.es = es;
|
|
138
|
+
es.onopen = () => {
|
|
139
|
+
this.statusValue = 'connected';
|
|
140
|
+
};
|
|
141
|
+
es.onerror = () => {
|
|
142
|
+
const closed = es.readyState === 2; // EventSource.CLOSED
|
|
143
|
+
if (closed) {
|
|
144
|
+
this.statusValue = 'closed';
|
|
145
|
+
this.closeConnection();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.statusValue = 'reconnecting';
|
|
149
|
+
this.statsValue.reconnects++;
|
|
150
|
+
};
|
|
151
|
+
es.addEventListener('presence.update', (e) => {
|
|
152
|
+
this.handleUpdate(e);
|
|
153
|
+
});
|
|
154
|
+
es.addEventListener('presence.depart', (e) => {
|
|
155
|
+
this.handleDepart(e);
|
|
156
|
+
});
|
|
157
|
+
es.addEventListener('presence.snapshot', (e) => {
|
|
158
|
+
this.handleSnapshot(e);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
closeConnection() {
|
|
162
|
+
if (!this.es)
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
this.es.close();
|
|
166
|
+
}
|
|
167
|
+
catch { /* ignore */ }
|
|
168
|
+
this.es = null;
|
|
169
|
+
}
|
|
170
|
+
handleUpdate(e) {
|
|
171
|
+
const data = parseJson(e.data);
|
|
172
|
+
if (!data || typeof data !== 'object')
|
|
173
|
+
return;
|
|
174
|
+
const characterId = data.character_id;
|
|
175
|
+
const x = data.x;
|
|
176
|
+
const y = data.y;
|
|
177
|
+
const zone = data.zone;
|
|
178
|
+
const tsMs = data.ts_ms;
|
|
179
|
+
if (typeof characterId !== 'string')
|
|
180
|
+
return;
|
|
181
|
+
if (typeof x !== 'number' || typeof y !== 'number')
|
|
182
|
+
return;
|
|
183
|
+
if (typeof zone !== 'string')
|
|
184
|
+
return;
|
|
185
|
+
if (typeof tsMs !== 'number')
|
|
186
|
+
return;
|
|
187
|
+
const name = data.name;
|
|
188
|
+
this.statsValue.messagesReceived++;
|
|
189
|
+
this.queue.push({
|
|
190
|
+
kind: 'update',
|
|
191
|
+
characterId,
|
|
192
|
+
x,
|
|
193
|
+
y,
|
|
194
|
+
zone,
|
|
195
|
+
tsMs,
|
|
196
|
+
...(typeof name === 'string' ? { name } : {}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
handleDepart(e) {
|
|
200
|
+
const data = parseJson(e.data);
|
|
201
|
+
if (!data || typeof data !== 'object')
|
|
202
|
+
return;
|
|
203
|
+
const characterId = data.character_id;
|
|
204
|
+
if (typeof characterId !== 'string')
|
|
205
|
+
return;
|
|
206
|
+
this.statsValue.messagesReceived++;
|
|
207
|
+
this.queue.push({ kind: 'depart', characterId });
|
|
208
|
+
}
|
|
209
|
+
handleSnapshot(e) {
|
|
210
|
+
const data = parseJson(e.data);
|
|
211
|
+
if (!data || typeof data !== 'object')
|
|
212
|
+
return;
|
|
213
|
+
const peersRaw = data.peers;
|
|
214
|
+
if (!Array.isArray(peersRaw))
|
|
215
|
+
return;
|
|
216
|
+
const peers = [];
|
|
217
|
+
for (let i = 0; i < peersRaw.length; i++) {
|
|
218
|
+
const p = peersRaw[i];
|
|
219
|
+
if (!p || typeof p !== 'object')
|
|
220
|
+
continue;
|
|
221
|
+
const characterId = p.character_id;
|
|
222
|
+
const x = p.x;
|
|
223
|
+
const y = p.y;
|
|
224
|
+
const zone = p.zone;
|
|
225
|
+
const tsMs = p.ts_ms;
|
|
226
|
+
if (typeof characterId !== 'string')
|
|
227
|
+
continue;
|
|
228
|
+
if (typeof x !== 'number' || typeof y !== 'number')
|
|
229
|
+
continue;
|
|
230
|
+
if (typeof zone !== 'string')
|
|
231
|
+
continue;
|
|
232
|
+
if (typeof tsMs !== 'number')
|
|
233
|
+
continue;
|
|
234
|
+
const name = p.name;
|
|
235
|
+
peers.push({
|
|
236
|
+
characterId,
|
|
237
|
+
x,
|
|
238
|
+
y,
|
|
239
|
+
zone,
|
|
240
|
+
tsMs,
|
|
241
|
+
...(typeof name === 'string' ? { name } : {}),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
this.statsValue.messagesReceived++;
|
|
245
|
+
this.queue.push({ kind: 'snapshot', peers });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function parseJson(raw) {
|
|
249
|
+
if (typeof raw !== 'string')
|
|
250
|
+
return null;
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(raw);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function defaultBroadcastUrl(baseUrl) {
|
|
259
|
+
if (baseUrl.endsWith('/events')) {
|
|
260
|
+
return baseUrl.slice(0, -'/events'.length) + '/move';
|
|
261
|
+
}
|
|
262
|
+
return baseUrl + '/move';
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=sse-multiplayer-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-multiplayer-bridge.js","sourceRoot":"","sources":["../../src/network/sse-multiplayer-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprite-shader-source.d.ts","sourceRoot":"","sources":["../../../src/renderer/shaders/sprite-shader-source.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,eAAe,EAAE,MAkClB,CAAC;AAEb,eAAO,MAAM,eAAe,EAAE,MAkBlB,CAAC;AAMb,eAAO,MAAM,kBAAkB,EAAE,YAO/B,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// GLSL ES 3.00 shader source for the WebGL2 sprite batcher.
|
|
2
|
+
//
|
|
3
|
+
// One pair of shaders services every batched draw the WebGL2Device
|
|
4
|
+
// performs - sprites, tiles, particles, glyphs - by treating each
|
|
5
|
+
// quad as an instance with its own screen-space origin, pixel size,
|
|
6
|
+
// atlas UV rect, and RGBA tint. The vertex shader maps a static unit
|
|
7
|
+
// quad onto the per-instance origin/size and forwards UV + tint to
|
|
8
|
+
// the fragment shader, which samples the bound atlas and multiplies
|
|
9
|
+
// by tint.
|
|
10
|
+
//
|
|
11
|
+
// The host (JS) is responsible for iso projection and camera
|
|
12
|
+
// transform, mirroring what Canvas2DDevice does today. Keeping that
|
|
13
|
+
// math out of the shader makes the GL path identical-output to the
|
|
14
|
+
// Canvas2D path - very useful for parity testing and later perf
|
|
15
|
+
// tuning. The trade is per-instance JS work that could in principle
|
|
16
|
+
// move to the GPU; we measure that in phase 14.3.
|
|
17
|
+
export const SPRITE_VERT_SRC = [
|
|
18
|
+
'#version 300 es',
|
|
19
|
+
'precision highp float;',
|
|
20
|
+
'',
|
|
21
|
+
'// Per-vertex unit-quad position. Six vertices forming two',
|
|
22
|
+
'// triangles cover (0,0)-(1,1) in normalized quad space.',
|
|
23
|
+
'layout(location = 0) in vec2 a_quadVertex;',
|
|
24
|
+
'',
|
|
25
|
+
'// Per-instance attributes - vertexAttribDivisor(1).',
|
|
26
|
+
'layout(location = 1) in vec2 a_origin; // screen-pixel top-left of the quad',
|
|
27
|
+
'layout(location = 2) in vec2 a_size; // screen-pixel width, height',
|
|
28
|
+
'layout(location = 3) in vec4 a_uvRect; // u0, v0, u1, v1 within atlas',
|
|
29
|
+
'layout(location = 4) in vec4 a_tint; // r, g, b, a',
|
|
30
|
+
'',
|
|
31
|
+
'uniform vec2 u_viewport; // viewport in pixels',
|
|
32
|
+
'',
|
|
33
|
+
'out vec2 v_uv;',
|
|
34
|
+
'out vec4 v_tint;',
|
|
35
|
+
'',
|
|
36
|
+
'void main() {',
|
|
37
|
+
' vec2 screenPx = a_origin + a_quadVertex * a_size;',
|
|
38
|
+
' // Map screen-pixel to clip-space NDC. Origin is top-left in',
|
|
39
|
+
' // pixel space; flip Y to match GL clip-space.',
|
|
40
|
+
' vec2 ndc;',
|
|
41
|
+
' ndc.x = (screenPx.x / u_viewport.x) * 2.0 - 1.0;',
|
|
42
|
+
' ndc.y = 1.0 - (screenPx.y / u_viewport.y) * 2.0;',
|
|
43
|
+
' gl_Position = vec4(ndc, 0.0, 1.0);',
|
|
44
|
+
'',
|
|
45
|
+
' // UV is linearly interpolated between the rect corners using',
|
|
46
|
+
' // the unit-quad position as the mix factor.',
|
|
47
|
+
' v_uv = mix(a_uvRect.xy, a_uvRect.zw, a_quadVertex);',
|
|
48
|
+
' v_tint = a_tint;',
|
|
49
|
+
'}',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n');
|
|
52
|
+
export const SPRITE_FRAG_SRC = [
|
|
53
|
+
'#version 300 es',
|
|
54
|
+
'precision highp float;',
|
|
55
|
+
'',
|
|
56
|
+
'in vec2 v_uv;',
|
|
57
|
+
'in vec4 v_tint;',
|
|
58
|
+
'',
|
|
59
|
+
'uniform sampler2D u_atlas;',
|
|
60
|
+
'',
|
|
61
|
+
'out vec4 outColor;',
|
|
62
|
+
'',
|
|
63
|
+
'void main() {',
|
|
64
|
+
' vec4 sampled = texture(u_atlas, v_uv);',
|
|
65
|
+
' vec4 c = sampled * v_tint;',
|
|
66
|
+
' if (c.a <= 0.0) discard;',
|
|
67
|
+
' outColor = c;',
|
|
68
|
+
'}',
|
|
69
|
+
'',
|
|
70
|
+
].join('\n');
|
|
71
|
+
// Static unit-quad vertex data: two triangles covering 0..1 in xy.
|
|
72
|
+
// Vertex order top-left, bottom-left, top-right, top-right, bottom-
|
|
73
|
+
// left, bottom-right matches the UV mapping in the vertex shader.
|
|
74
|
+
// Six vertices x 2 floats = 12 floats.
|
|
75
|
+
export const UNIT_QUAD_VERTICES = new Float32Array([
|
|
76
|
+
0, 0, // top-left
|
|
77
|
+
0, 1, // bottom-left
|
|
78
|
+
1, 0, // top-right
|
|
79
|
+
1, 0, // top-right
|
|
80
|
+
0, 1, // bottom-left
|
|
81
|
+
1, 1, // bottom-right
|
|
82
|
+
]);
|
|
83
|
+
//# sourceMappingURL=sprite-shader-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprite-shader-source.js","sourceRoot":"","sources":["../../../src/renderer/shaders/sprite-shader-source.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,mEAAmE;AACnE,kEAAkE;AAClE,oEAAoE;AACpE,qEAAqE;AACrE,mEAAmE;AACnE,oEAAoE;AACpE,WAAW;AACX,EAAE;AACF,6DAA6D;AAC7D,oEAAoE;AACpE,mEAAmE;AACnE,gEAAgE;AAChE,oEAAoE;AACpE,kDAAkD;AAElD,MAAM,CAAC,MAAM,eAAe,GAAW;IACrC,iBAAiB;IACjB,wBAAwB;IACxB,EAAE;IACF,4DAA4D;IAC5D,0DAA0D;IAC1D,4CAA4C;IAC5C,EAAE;IACF,sDAAsD;IACtD,+EAA+E;IAC/E,wEAAwE;IACxE,yEAAyE;IACzE,wDAAwD;IACxD,EAAE;IACF,gDAAgD;IAChD,EAAE;IACF,gBAAgB;IAChB,kBAAkB;IAClB,EAAE;IACF,eAAe;IACf,qDAAqD;IACrD,gEAAgE;IAChE,kDAAkD;IAClD,aAAa;IACb,oDAAoD;IACpD,oDAAoD;IACpD,sCAAsC;IACtC,EAAE;IACF,iEAAiE;IACjE,gDAAgD;IAChD,uDAAuD;IACvD,oBAAoB;IACpB,GAAG;IACH,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,CAAC,MAAM,eAAe,GAAW;IACrC,iBAAiB;IACjB,wBAAwB;IACxB,EAAE;IACF,eAAe;IACf,iBAAiB;IACjB,EAAE;IACF,4BAA4B;IAC5B,EAAE;IACF,oBAAoB;IACpB,EAAE;IACF,eAAe;IACf,0CAA0C;IAC1C,8BAA8B;IAC9B,4BAA4B;IAC5B,iBAAiB;IACjB,GAAG;IACH,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,mEAAmE;AACnE,oEAAoE;AACpE,kEAAkE;AAClE,uCAAuC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAiB,IAAI,YAAY,CAAC;IAC/D,CAAC,EAAE,CAAC,EAAI,WAAW;IACnB,CAAC,EAAE,CAAC,EAAI,cAAc;IACtB,CAAC,EAAE,CAAC,EAAI,YAAY;IACpB,CAAC,EAAE,CAAC,EAAI,YAAY;IACpB,CAAC,EAAE,CAAC,EAAI,cAAc;IACtB,CAAC,EAAE,CAAC,EAAI,eAAe;CACxB,CAAC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TextureAtlas } from './texture-atlas.js';
|
|
2
|
+
export type BlendMode = 'alpha' | 'add';
|
|
3
|
+
export declare const FLOATS_PER_INSTANCE: number;
|
|
4
|
+
export type FlushHandler = (atlas: TextureAtlas, blendMode: BlendMode, data: Float32Array, count: number) => void;
|
|
5
|
+
export declare class SpriteBatcher {
|
|
6
|
+
private buffer;
|
|
7
|
+
private capacity;
|
|
8
|
+
private count;
|
|
9
|
+
private currentAtlas;
|
|
10
|
+
private currentBlend;
|
|
11
|
+
private flushCount;
|
|
12
|
+
private instanceTotal;
|
|
13
|
+
private readonly flushHandler;
|
|
14
|
+
constructor(flushHandler: FlushHandler, initialCapacity?: number);
|
|
15
|
+
beginFrame(): void;
|
|
16
|
+
submit(atlas: TextureAtlas, blendMode: BlendMode, originX: number, originY: number, sizeX: number, sizeY: number, u0: number, v0: number, u1: number, v1: number, tintR: number, tintG: number, tintB: number, tintA: number): void;
|
|
17
|
+
flush(): void;
|
|
18
|
+
endFrame(): void;
|
|
19
|
+
getStats(out: {
|
|
20
|
+
flushCount: number;
|
|
21
|
+
instanceTotal: number;
|
|
22
|
+
capacity: number;
|
|
23
|
+
}): void;
|
|
24
|
+
_peekCount(): number;
|
|
25
|
+
_peekCurrentAtlas(): TextureAtlas | null;
|
|
26
|
+
_peekCurrentBlend(): BlendMode;
|
|
27
|
+
_peekBuffer(): Float32Array;
|
|
28
|
+
private grow;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=sprite-batcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprite-batcher.d.ts","sourceRoot":"","sources":["../../src/renderer/sprite-batcher.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKvD,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;AAExC,eAAO,MAAM,mBAAmB,EAAE,MAAW,CAAC;AAO9C,MAAM,MAAM,YAAY,GAAG,CACzB,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,MAAM,KACV,IAAI,CAAC;AAIV,qBAAa,aAAa;IAIxB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,YAAY,CAAsB;IAI1C,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,aAAa,CAAa;IAElC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;gBAEhC,YAAY,EAAE,YAAY,EAAE,eAAe,GAAE,MAAyB;IAUlF,UAAU,IAAI,IAAI;IAalB,MAAM,CACJ,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,IAAI;IAoCP,KAAK,IAAI,IAAI;IAYb,QAAQ,IAAI,IAAI;IAQhB,QAAQ,CAAC,GAAG,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IASpF,UAAU,IAAI,MAAM;IAGpB,iBAAiB,IAAI,YAAY,GAAG,IAAI;IAGxC,iBAAiB,IAAI,SAAS;IAG9B,WAAW,IAAI,YAAY;IAO3B,OAAO,CAAC,IAAI;CAOb"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// SpriteBatcher - per-frame instance accumulator for the WebGL2 path.
|
|
2
|
+
//
|
|
3
|
+
// Pure CPU-side: holds a growable Float32Array of per-instance data
|
|
4
|
+
// and tracks the current batch key (atlas + blend mode). submit()
|
|
5
|
+
// appends; if the batch key changes it flushes the current run via
|
|
6
|
+
// a handler the device wires up at construction. flush() also runs
|
|
7
|
+
// from endFrame to drain any pending instances.
|
|
8
|
+
//
|
|
9
|
+
// 12 floats per instance:
|
|
10
|
+
// [origin.x, origin.y, size.x, size.y,
|
|
11
|
+
// uv0.x, uv0.y, uv1.x, uv1.y,
|
|
12
|
+
// tint.r, tint.g, tint.b, tint.a]
|
|
13
|
+
//
|
|
14
|
+
// Why: a single drawArraysInstanced call covers the whole batch with
|
|
15
|
+
// one CPU->GPU upload of the instance buffer. Atlas swap forces a
|
|
16
|
+
// flush because the bound texture changes; blend-mode swap forces a
|
|
17
|
+
// flush because gl.blendFunc state changes.
|
|
18
|
+
//
|
|
19
|
+
// Submission order is preserved within a batch. Higher-level systems
|
|
20
|
+
// (SpriteRenderSystem) sort globally before submitting; the batcher
|
|
21
|
+
// trusts that order. Subsequent atlas batches reset the order chain
|
|
22
|
+
// but never mix across atlases - that is intentional, the cost is
|
|
23
|
+
// borne by the consumer (sort by atlas then by depth if global
|
|
24
|
+
// ordering across atlases matters).
|
|
25
|
+
export const FLOATS_PER_INSTANCE = 12;
|
|
26
|
+
const INITIAL_CAPACITY = 1024;
|
|
27
|
+
export class SpriteBatcher {
|
|
28
|
+
// Instance data buffer. Grows by doubling when capacity is hit.
|
|
29
|
+
// Backing typed-array stays referentially stable when count is
|
|
30
|
+
// below capacity to avoid per-frame allocation.
|
|
31
|
+
buffer;
|
|
32
|
+
capacity;
|
|
33
|
+
// Current run state.
|
|
34
|
+
count = 0;
|
|
35
|
+
currentAtlas = null;
|
|
36
|
+
currentBlend = 'alpha';
|
|
37
|
+
// Flush statistics. Reset at beginFrame; surfaced by getStats for
|
|
38
|
+
// tests + diagnostics.
|
|
39
|
+
flushCount = 0;
|
|
40
|
+
instanceTotal = 0;
|
|
41
|
+
flushHandler;
|
|
42
|
+
constructor(flushHandler, initialCapacity = INITIAL_CAPACITY) {
|
|
43
|
+
this.flushHandler = flushHandler;
|
|
44
|
+
this.capacity = Math.max(64, initialCapacity);
|
|
45
|
+
this.buffer = new Float32Array(this.capacity * FLOATS_PER_INSTANCE);
|
|
46
|
+
}
|
|
47
|
+
// Reset for a new frame. Counts go to zero; batch state cleared.
|
|
48
|
+
// Flushes any pending instances first as a safety net (endFrame
|
|
49
|
+
// should have done this, but we belt-and-suspenders to keep the
|
|
50
|
+
// device contract clean).
|
|
51
|
+
beginFrame() {
|
|
52
|
+
if (this.count > 0 && this.currentAtlas) {
|
|
53
|
+
this.flush();
|
|
54
|
+
}
|
|
55
|
+
this.count = 0;
|
|
56
|
+
this.currentAtlas = null;
|
|
57
|
+
this.currentBlend = 'alpha';
|
|
58
|
+
this.flushCount = 0;
|
|
59
|
+
this.instanceTotal = 0;
|
|
60
|
+
}
|
|
61
|
+
// Submit one instance to the batcher. Triggers flush if the
|
|
62
|
+
// (atlas, blendMode) key differs from the current batch.
|
|
63
|
+
submit(atlas, blendMode, originX, originY, sizeX, sizeY, u0, v0, u1, v1, tintR, tintG, tintB, tintA) {
|
|
64
|
+
if (atlas.released)
|
|
65
|
+
return;
|
|
66
|
+
if (this.currentAtlas !== atlas || this.currentBlend !== blendMode) {
|
|
67
|
+
// Atlas / blend change forces a batch break. Flush the current
|
|
68
|
+
// run before installing the new key.
|
|
69
|
+
if (this.count > 0 && this.currentAtlas) {
|
|
70
|
+
this.flush();
|
|
71
|
+
}
|
|
72
|
+
this.currentAtlas = atlas;
|
|
73
|
+
this.currentBlend = blendMode;
|
|
74
|
+
}
|
|
75
|
+
if (this.count >= this.capacity) {
|
|
76
|
+
this.grow();
|
|
77
|
+
}
|
|
78
|
+
var off = this.count * FLOATS_PER_INSTANCE;
|
|
79
|
+
var b = this.buffer;
|
|
80
|
+
b[off + 0] = originX;
|
|
81
|
+
b[off + 1] = originY;
|
|
82
|
+
b[off + 2] = sizeX;
|
|
83
|
+
b[off + 3] = sizeY;
|
|
84
|
+
b[off + 4] = u0;
|
|
85
|
+
b[off + 5] = v0;
|
|
86
|
+
b[off + 6] = u1;
|
|
87
|
+
b[off + 7] = v1;
|
|
88
|
+
b[off + 8] = tintR;
|
|
89
|
+
b[off + 9] = tintG;
|
|
90
|
+
b[off + 10] = tintB;
|
|
91
|
+
b[off + 11] = tintA;
|
|
92
|
+
this.count++;
|
|
93
|
+
}
|
|
94
|
+
// Flush the current run via the device-supplied handler. No-op if
|
|
95
|
+
// nothing pending or no atlas selected.
|
|
96
|
+
flush() {
|
|
97
|
+
if (this.count === 0 || !this.currentAtlas)
|
|
98
|
+
return;
|
|
99
|
+
var atlas = this.currentAtlas;
|
|
100
|
+
var blend = this.currentBlend;
|
|
101
|
+
var n = this.count;
|
|
102
|
+
this.flushHandler(atlas, blend, this.buffer, n);
|
|
103
|
+
this.flushCount++;
|
|
104
|
+
this.instanceTotal += n;
|
|
105
|
+
this.count = 0;
|
|
106
|
+
}
|
|
107
|
+
// Final drain - call from endFrame to push the last partial batch.
|
|
108
|
+
endFrame() {
|
|
109
|
+
if (this.count > 0 && this.currentAtlas) {
|
|
110
|
+
this.flush();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Diagnostics. Returned object is owned by the caller; we copy
|
|
114
|
+
// into it to avoid allocating per-frame.
|
|
115
|
+
getStats(out) {
|
|
116
|
+
out.flushCount = this.flushCount;
|
|
117
|
+
out.instanceTotal = this.instanceTotal;
|
|
118
|
+
out.capacity = this.capacity;
|
|
119
|
+
}
|
|
120
|
+
// Test-only inspection helpers. Not part of the public API but
|
|
121
|
+
// exported so the test file can assert internal state without
|
|
122
|
+
// brittle reflection.
|
|
123
|
+
_peekCount() {
|
|
124
|
+
return this.count;
|
|
125
|
+
}
|
|
126
|
+
_peekCurrentAtlas() {
|
|
127
|
+
return this.currentAtlas;
|
|
128
|
+
}
|
|
129
|
+
_peekCurrentBlend() {
|
|
130
|
+
return this.currentBlend;
|
|
131
|
+
}
|
|
132
|
+
_peekBuffer() {
|
|
133
|
+
return this.buffer;
|
|
134
|
+
}
|
|
135
|
+
// Grow by doubling. Float32Array does not resize in place; we
|
|
136
|
+
// allocate a new buffer and copy the live prefix. Triggered when
|
|
137
|
+
// submit overruns capacity.
|
|
138
|
+
grow() {
|
|
139
|
+
var next = this.capacity * 2;
|
|
140
|
+
var b = new Float32Array(next * FLOATS_PER_INSTANCE);
|
|
141
|
+
b.set(this.buffer.subarray(0, this.count * FLOATS_PER_INSTANCE));
|
|
142
|
+
this.buffer = b;
|
|
143
|
+
this.capacity = next;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=sprite-batcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprite-batcher.js","sourceRoot":"","sources":["../../src/renderer/sprite-batcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AtlasDescriptor } from './graphics-device.js';
|
|
2
|
+
export declare class TextureAtlas {
|
|
3
|
+
texture: WebGLTexture | null;
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
readonly uvRects: Float32Array;
|
|
7
|
+
readonly frameSizes: Float32Array;
|
|
8
|
+
readonly frameCount: number;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
private sourceImage;
|
|
11
|
+
released: boolean;
|
|
12
|
+
constructor(gl: WebGL2RenderingContext | null, desc: AtlasDescriptor);
|
|
13
|
+
upload(gl: WebGL2RenderingContext): void;
|
|
14
|
+
bind(gl: WebGL2RenderingContext): void;
|
|
15
|
+
handleContextLoss(): void;
|
|
16
|
+
dispose(gl: WebGL2RenderingContext | null): void;
|
|
17
|
+
lookupUVRect(frame: number, out: Float32Array, offset: number): boolean;
|
|
18
|
+
lookupFrameSize(frame: number, out: {
|
|
19
|
+
w: number;
|
|
20
|
+
h: number;
|
|
21
|
+
}): boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function makeParticleDiscAtlas(gl: WebGL2RenderingContext | null): TextureAtlas | null;
|
|
24
|
+
//# sourceMappingURL=texture-atlas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"texture-atlas.d.ts","sourceRoot":"","sources":["../../src/renderer/texture-atlas.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,qBAAa,YAAY;IAGvB,OAAO,EAAE,YAAY,GAAG,IAAI,CAAQ;IAKpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAMxB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAI/B,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAC;IAElC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAG5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAItB,OAAO,CAAC,WAAW,CAAqD;IAIxE,QAAQ,EAAE,OAAO,CAAS;gBAEd,EAAE,EAAE,sBAAsB,GAAG,IAAI,EAAE,IAAI,EAAE,eAAe;IAmCpE,MAAM,CAAC,EAAE,EAAE,sBAAsB,GAAG,IAAI;IAkCxC,IAAI,CAAC,EAAE,EAAE,sBAAsB,GAAG,IAAI;IAMtC,iBAAiB,IAAI,IAAI;IAIzB,OAAO,CAAC,EAAE,EAAE,sBAAsB,GAAG,IAAI,GAAG,IAAI;IAWhD,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAWvE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO;CAOvE;AASD,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,sBAAsB,GAAG,IAAI,GAChC,YAAY,GAAG,IAAI,CAqBrB"}
|