@swf2es/render 0.1.18 → 0.1.20

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.
@@ -1,8 +1,12 @@
1
1
  import type { GlState } from "@swf2es/context3d/device";
2
2
  import type { Place } from "./atlas.js";
3
- import { COLOR_AT, DRAW_AT, POSITION_AT, TEXEL_AT } from "./programs.js";
3
+ import { COLOR_AT, DRAW_AT, MERGED_LINE, MERGED_UNIT, POSITION_AT, TEXEL_AT } from "./programs.js";
4
4
 
5
- /** Atlas attributes followed by the packed row: transforms and snapping stay in the shader. */
5
+ /**
6
+ * Atlas attributes followed by where the draw's packed row is in the rows'
7
+ * texture: transforms and snapping stay in the shader, and a row that
8
+ * changes changes no vertex.
9
+ */
6
10
  const WORDS = 6;
7
11
  /** Bound the extra CPU and GPU storage, falling back to the atlas after it fills. */
8
12
  const VERTICES_MOST = 2 << 20;
@@ -13,12 +17,30 @@ const INDICES_LEAST = 3 << 14;
13
17
  /** Frames over which the most used is watched; room four times that is let go of after them. */
14
18
  const SHRINK_FRAMES = 240;
15
19
 
20
+ type Batch = NonNullable<Place["batch"]>;
21
+
22
+ /** A run's geometries copied in a row into the stream, where every run of the same geometries finds them. */
23
+ interface Segment {
24
+ batches: Batch[];
25
+ vertexStart: number;
26
+ vertices: number;
27
+ indexStart: number;
28
+ indices: number;
29
+ /** Where its draws' rows are in the rows' texture, one after another. */
30
+ slot: number;
31
+ /** The last frame a run used it (FillBatches.frame). */
32
+ frame: number;
33
+ }
34
+
16
35
  /**
17
- * Small stable geometries merged in painter's order; unchanged streams reuse
18
- * their GPU buffers. Only what changed since the last frame, from the first
19
- * draw that differs to the stream's end, is staged on the CPU and uploaded
20
- * where it lies: the stream before it stays on the GPU alone, and a buffer
21
- * grown keeps it by a copy there.
36
+ * Small geometries merged run by run in painter's order, each run's copy a
37
+ * segment of one stream that any later run of the same geometries draws
38
+ * again, wherever it comes in the frame: its rows are in a texture of its
39
+ * own, written each frame, not in its vertices. Only the segments a frame adds are staged on the
40
+ * CPU, after the stream's top, and uploaded there at once; what is below
41
+ * stays on the GPU alone, and a buffer grown keeps it by a copy there.
42
+ * Segments no run used in a frame are let go of, and their room once the
43
+ * stream is twice what the last frame used, the stream then made anew.
22
44
  */
23
45
  export class FillBatches {
24
46
  vertexArray!: WebGLVertexArrayObject;
@@ -32,14 +54,36 @@ export class FillBatches {
32
54
  private indexCapacity = 0;
33
55
  private vertexTop = 0;
34
56
  indexTop = 0;
35
- /** Where this frame's staging starts in the stream: the first draw that differs from the last frame's. */
57
+ /** Where this frame's staging starts in the stream: its top as the frame began. */
36
58
  private fromVertex = 0;
37
59
  private fromIndex = 0;
38
- private readonly previous: NonNullable<Place["batch"]>[] = [];
39
- private readonly previousRows: number[] = [];
40
- private cursor = 0;
41
- /** Stays dirty if preparation aborts before upload, as an overflowing run table can do. */
42
- private changed = false;
60
+ /**
61
+ * The segments' rows, slot by slot (Segment.slot), as their texture holds
62
+ * them, the slots taken, and the range of them this frame changed.
63
+ */
64
+ rowsTexture: WebGLTexture | null = null;
65
+ private rows = new Int32Array(0);
66
+ private rowsLines = 0;
67
+ private slotTop = 0;
68
+ private slotsMost = 0;
69
+ private changedFrom = 0;
70
+ private changedTo = 0;
71
+ /** The segments by their first geometry. */
72
+ private readonly segments = new Map<Batch, Segment[]>();
73
+ private frame = 0;
74
+ /** What the frame's runs used of the stream, each segment once. */
75
+ private liveVertices = 0;
76
+ private liveIndices = 0;
77
+ /**
78
+ * The keys of the runs this frame and the last asked for (add's
79
+ * `settled`), so that a run is copied only once it is drawn again.
80
+ */
81
+ private seen = new Set<number>();
82
+ private seenBefore = new Set<number>();
83
+ /** A frame prepared but not uploaded, as an overflowing run table leaves one: its staging lost. */
84
+ private pending = false;
85
+ /** Whether a run found no room this frame, so that the next makes the stream anew. */
86
+ private full = false;
43
87
  /** The frames of the current watch, and the most of the stream and of the staging they used. */
44
88
  private frames = 0;
45
89
  private mostVertices = 0;
@@ -52,8 +96,13 @@ export class FillBatches {
52
96
  private readonly state: GlState,
53
97
  ) {}
54
98
 
55
- setUp(): void {
99
+ /** The buffers and the rows' texture made, `textureMost` lines at most, as the context comes. */
100
+ setUp(textureMost: number): void {
56
101
  const gl = this.gl;
102
+ this.rowsTexture = null;
103
+ this.rowsLines = 0;
104
+ this.rows = new Int32Array(0);
105
+ this.slotsMost = MERGED_LINE * textureMost;
57
106
  this.vertexArray = gl.createVertexArray() as WebGLVertexArrayObject;
58
107
  this.vertexBuffer = gl.createBuffer() as WebGLBuffer;
59
108
  this.indexBuffer = gl.createBuffer() as WebGLBuffer;
@@ -72,69 +121,142 @@ export class FillBatches {
72
121
  gl.vertexAttribI4i(DRAW_AT, -1, 0, 0, 0);
73
122
  this.vertexCapacity = 0;
74
123
  this.indexCapacity = 0;
75
- this.previous.length = 0;
76
- this.previousRows.length = 0;
77
- this.changed = false;
78
- this.reset();
124
+ this.clear();
79
125
  }
80
126
 
81
- reset(): void {
127
+ /** No segment kept: the stream made anew from its start. */
128
+ private clear(): void {
129
+ this.segments.clear();
82
130
  this.vertexTop = 0;
83
131
  this.indexTop = 0;
84
- this.cursor = 0;
85
- // A preparation that aborted left the whole stream to stage again.
86
- this.fromVertex = 0;
87
- this.fromIndex = 0;
132
+ this.slotTop = 0;
133
+ this.pending = false;
134
+ this.full = false;
88
135
  }
89
136
 
90
- /** Append the whole run, or leave it to multi-draw if copying would cost too much. */
91
- add(places: Place[], rows: Int32Array, count: number, frame: number): void {
92
- if (count < 32) {
93
- return;
137
+ /** A frame's preparation begun: staging from the stream's top, or from its start once it is mostly dead. */
138
+ reset(): void {
139
+ if (this.pending) {
140
+ // Rows written and not uploaded: the texture made again, so that the CPU's copy is its own.
141
+ this.rowsLines = 0;
142
+ }
143
+
144
+ if (
145
+ this.pending ||
146
+ // Room let go of where a run found none, unless the last frame used all of the stream.
147
+ (this.full && (this.vertexTop > this.liveVertices || this.indexTop > this.liveIndices)) ||
148
+ (this.vertexTop > VERTICES_LEAST && this.vertexTop > this.liveVertices * 2) ||
149
+ (this.indexTop > INDICES_LEAST && this.indexTop > this.liveIndices * 2)
150
+ ) {
151
+ this.clear();
152
+ }
153
+
154
+ this.full = false;
155
+ const seen = this.seenBefore;
156
+ this.seenBefore = this.seen;
157
+ this.seen = seen;
158
+ seen.clear();
159
+ this.frame++;
160
+ this.fromVertex = this.vertexTop;
161
+ this.fromIndex = this.indexTop;
162
+ this.liveVertices = 0;
163
+ this.liveIndices = 0;
164
+ this.changedFrom = this.slotTop;
165
+ this.changedTo = 0;
166
+ this.pending = true;
167
+ }
168
+
169
+ /**
170
+ * The segment that draws the run, of at least `least` draws: one kept, or
171
+ * one staged now; null leaves it to the atlas, where copying would cost
172
+ * too much: a run short or of geometry added this `frame` is cheaper
173
+ * drawn from it, and with `settled` a run not drawn the frame before
174
+ * too, as a run that changes from frame to frame, as an animation's do,
175
+ * would be copied again each frame. Its vertices carry each draw's slot
176
+ * in the rows' texture, where `rows` are written.
177
+ */
178
+ add(
179
+ places: Place[],
180
+ rows: Int32Array,
181
+ count: number,
182
+ frame: number,
183
+ least: number,
184
+ settled: boolean,
185
+ ): { indexStart: number; indices: number } | null {
186
+ if (count < least) {
187
+ return null;
94
188
  }
95
189
 
96
- let vertices = this.vertexTop;
97
- let indices = this.indexTop;
190
+ let vertices = 0;
191
+ let indices = 0;
192
+ // The run's geometries by where they lie in the atlas, which no two live geometries share.
193
+ let key = count;
98
194
  for (let i = 0; i < count; i++) {
99
195
  const place = places[i];
100
196
  if (!place.batch || place.batch.frame === frame) {
101
- return;
197
+ return null;
102
198
  }
103
199
 
104
200
  vertices += place.vertices;
105
201
  indices += place.indices;
202
+ key = Math.imul(key ^ place.indexStart, 0x9e3779b1) ^ place.indices;
106
203
  }
107
204
 
108
- if (vertices > VERTICES_MOST || indices > INDICES_MOST) {
109
- return;
205
+ const first = places[0].batch as Batch;
206
+ let list = this.segments.get(first);
207
+ if (list) {
208
+ for (const segment of list) {
209
+ // A copy a run drew this frame already serves another only with the same rows.
210
+ if (
211
+ segment.batches.length === count &&
212
+ this.holds(segment, places) &&
213
+ (segment.frame !== this.frame || this.rowsAre(segment.slot, rows, count))
214
+ ) {
215
+ this.use(segment);
216
+ this.writeRows(segment.slot, rows, count);
217
+ return segment;
218
+ }
219
+ }
110
220
  }
111
221
 
112
- for (let i = 0; i < count; i++) {
113
- const place = places[i];
114
- const batch = place.batch as NonNullable<Place["batch"]>;
115
- // An unchanged prefix has the same offsets. After a mismatch, stage the rest in order.
116
- if (
117
- !this.changed &&
118
- (this.previous[this.cursor] !== batch || this.previousRows[this.cursor] !== rows[i])
119
- ) {
120
- this.changed = true;
121
- this.fromVertex = this.vertexTop;
122
- this.fromIndex = this.indexTop;
123
- }
222
+ // A key alike by chance merges a run a frame early, which costs a copy and draws the same.
223
+ this.seen.add(key);
224
+ if (settled && !this.seenBefore.has(key)) {
225
+ return null;
226
+ }
124
227
 
125
- this.previous[this.cursor] = batch;
126
- this.previousRows[this.cursor++] = rows[i];
127
- if (!this.changed) {
128
- this.vertexTop += place.vertices;
129
- this.indexTop += place.indices;
130
- continue;
131
- }
228
+ if (
229
+ this.vertexTop + vertices > VERTICES_MOST ||
230
+ this.indexTop + indices > INDICES_MOST ||
231
+ this.slotTop + count > this.slotsMost
232
+ ) {
233
+ this.full = true;
234
+ return null;
235
+ }
132
236
 
133
- this.stageRoom(vertices - this.fromVertex, indices - this.fromIndex);
237
+ const segment: Segment = {
238
+ batches: new Array<Batch>(count),
239
+ vertexStart: this.vertexTop,
240
+ vertices,
241
+ indexStart: this.indexTop,
242
+ indices,
243
+ slot: this.slotTop,
244
+ frame: -1,
245
+ };
246
+ this.slotTop += count;
247
+ this.stageRoom(
248
+ this.vertexTop + vertices - this.fromVertex,
249
+ this.indexTop + indices - this.fromIndex,
250
+ );
251
+ const floats = this.floats;
252
+ const words = this.words;
253
+ const staged = this.indices;
254
+ for (let i = 0; i < count; i++) {
255
+ const place = places[i];
256
+ const batch = place.batch as Batch;
257
+ segment.batches[i] = batch;
134
258
  const data = batch.data;
135
259
  const base = this.vertexTop;
136
- const floats = this.floats;
137
- const words = this.words;
138
260
  for (let j = 0; j < place.vertices; j++) {
139
261
  const at = (base - this.fromVertex + j) * WORDS;
140
262
  floats[at] = data.positions[j * 2];
@@ -142,10 +264,9 @@ export class FillBatches {
142
264
  floats[at + 2] = data.texels ? data.texels[j * 2] : 0;
143
265
  floats[at + 3] = data.texels ? data.texels[j * 2 + 1] : 0;
144
266
  words[at + 4] = data.colors[j];
145
- words[at + 5] = rows[i];
267
+ words[at + 5] = segment.slot + i;
146
268
  }
147
269
 
148
- const staged = this.indices;
149
270
  let at = this.indexTop - this.fromIndex;
150
271
  for (let j = 0; j < place.indices; j++) {
151
272
  staged[at++] = base + data.indices[j];
@@ -154,22 +275,146 @@ export class FillBatches {
154
275
  this.vertexTop += place.vertices;
155
276
  this.indexTop += place.indices;
156
277
  }
278
+
279
+ if (!list) {
280
+ list = [];
281
+ this.segments.set(first, list);
282
+ }
283
+
284
+ list.push(segment);
285
+ this.use(segment);
286
+ this.writeRows(segment.slot, rows, count);
287
+ return segment;
288
+ }
289
+
290
+ /** Whether the `count` rows from `slot` are `rows`. */
291
+ private rowsAre(slot: number, rows: Int32Array, count: number): boolean {
292
+ for (let i = 0; i < count; i++) {
293
+ if (this.rows[slot + i] !== rows[i]) {
294
+ return false;
295
+ }
296
+ }
297
+
298
+ return true;
299
+ }
300
+
301
+ /** `rows` written from `slot` on, what changes counted for the upload. */
302
+ private writeRows(slot: number, rows: Int32Array, count: number): void {
303
+ if (slot + count > this.rows.length) {
304
+ const lines = Math.ceil(Math.max(slot + count, this.rows.length * 2) / MERGED_LINE);
305
+ const grown = new Int32Array(lines * MERGED_LINE);
306
+ grown.set(this.rows);
307
+ this.rows = grown;
308
+ }
309
+
310
+ const held = this.rows;
311
+ for (let i = 0; i < count; i++) {
312
+ if (held[slot + i] !== rows[i]) {
313
+ held[slot + i] = rows[i];
314
+ this.changedFrom = Math.min(this.changedFrom, slot + i);
315
+ this.changedTo = Math.max(this.changedTo, slot + i + 1);
316
+ }
317
+ }
318
+ }
319
+
320
+ /** The rows' texture made tall enough for the slots taken, and the lines this frame changed uploaded. */
321
+ private uploadRows(): void {
322
+ const gl = this.gl;
323
+ const lines = Math.ceil(this.slotTop / MERGED_LINE);
324
+ if (lines > this.rowsLines) {
325
+ if (this.rowsTexture) {
326
+ this.state.dropTexture(this.rowsTexture);
327
+ gl.deleteTexture(this.rowsTexture);
328
+ }
329
+
330
+ // A new texture holds zeros: the rows taken uploaded again, the CPU's past them zeros too.
331
+ this.rows.fill(0, this.slotTop);
332
+ this.rowsLines = Math.min(this.slotsMost / MERGED_LINE, Math.max(lines, this.rowsLines * 2));
333
+ this.rowsTexture = gl.createTexture();
334
+ this.state.bindTexture(MERGED_UNIT, this.rowsTexture);
335
+ this.state.activeTexture(MERGED_UNIT);
336
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R32I, MERGED_LINE, this.rowsLines);
337
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
338
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
339
+ this.changedFrom = 0;
340
+ this.changedTo = this.slotTop;
341
+ }
342
+
343
+ if (this.changedTo <= this.changedFrom) {
344
+ return;
345
+ }
346
+
347
+ const from = Math.floor(this.changedFrom / MERGED_LINE);
348
+ const to = Math.ceil(this.changedTo / MERGED_LINE);
349
+ this.state.bindTexture(MERGED_UNIT, this.rowsTexture);
350
+ this.state.activeTexture(MERGED_UNIT);
351
+ gl.texSubImage2D(
352
+ gl.TEXTURE_2D,
353
+ 0,
354
+ 0,
355
+ from,
356
+ MERGED_LINE,
357
+ to - from,
358
+ gl.RED_INTEGER,
359
+ gl.INT,
360
+ this.rows,
361
+ from * MERGED_LINE,
362
+ );
363
+ }
364
+
365
+ /** Whether `segment` is of the geometries of `places`, in their order. */
366
+ private holds(segment: Segment, places: Place[]): boolean {
367
+ const batches = segment.batches;
368
+ for (let i = 0; i < batches.length; i++) {
369
+ if (batches[i] !== places[i].batch) {
370
+ return false;
371
+ }
372
+ }
373
+
374
+ return true;
375
+ }
376
+
377
+ /** `segment` used this frame, counted once among what the frame used. */
378
+ private use(segment: Segment): void {
379
+ if (segment.frame !== this.frame) {
380
+ segment.frame = this.frame;
381
+ this.liveVertices += segment.vertices;
382
+ this.liveIndices += segment.indices;
383
+ }
157
384
  }
158
385
 
159
386
  /**
160
- * The staged part of the stream uploaded where it lies, the buffers grown
161
- * to fit it first, keeping what is before it; and, once a watch of
162
- * SHRINK_FRAMES has used a quarter of the room or less, the buffers and
163
- * the staging made smaller.
387
+ * The staged segments uploaded where they lie, the buffers grown to fit
388
+ * them first, keeping what is below; the segments no run used let go of;
389
+ * and, once a watch of SHRINK_FRAMES has used a quarter of the room or
390
+ * less, the buffers and the staging made smaller.
164
391
  */
165
392
  upload(): void {
166
- this.previous.length = this.cursor;
167
- this.previousRows.length = this.cursor;
168
- const staging = this.changed && this.indexTop > 0;
393
+ const staging = this.vertexTop > this.fromVertex;
169
394
  if (staging) {
170
395
  this.uploadStaged();
171
396
  }
172
397
 
398
+ if (this.slotTop > 0) {
399
+ this.uploadRows();
400
+ }
401
+
402
+ for (const [first, list] of this.segments) {
403
+ let kept = 0;
404
+ for (const segment of list) {
405
+ if (segment.frame === this.frame) {
406
+ list[kept++] = segment;
407
+ }
408
+ }
409
+
410
+ if (kept === 0) {
411
+ this.segments.delete(first);
412
+ } else {
413
+ list.length = kept;
414
+ }
415
+ }
416
+
417
+ this.pending = false;
173
418
  this.watch(staging);
174
419
  }
175
420
 
@@ -215,10 +460,15 @@ export class FillBatches {
215
460
  0,
216
461
  this.indexTop - this.fromIndex,
217
462
  );
218
- this.changed = false;
219
463
  }
220
464
 
221
465
  destroy(): void {
466
+ if (this.rowsTexture) {
467
+ this.state.dropTexture(this.rowsTexture);
468
+ this.gl.deleteTexture(this.rowsTexture);
469
+ this.rowsTexture = null;
470
+ }
471
+
222
472
  this.gl.deleteVertexArray(this.vertexArray);
223
473
  this.gl.deleteBuffer(this.vertexBuffer);
224
474
  this.gl.deleteBuffer(this.indexBuffer);
@@ -266,10 +516,8 @@ export class FillBatches {
266
516
  }
267
517
 
268
518
  const gl = this.gl;
269
- // With nothing uploaded for a change, as for a stream of no draws, nothing on the GPU is kept.
270
- const kept = !this.changed;
271
- const keepVertices = kept ? this.vertexTop : 0;
272
- const keepIndices = kept ? this.indexTop : 0;
519
+ const keepVertices = this.vertexTop;
520
+ const keepIndices = this.indexTop;
273
521
  const vertices = Math.max(VERTICES_LEAST, this.mostVertices * 2);
274
522
  if (this.mostVertices * 4 <= this.vertexCapacity && vertices < this.vertexCapacity) {
275
523
  this.state.bindVertexArray(this.vertexArray);