@es3d/es3d 1.1.2 → 1.2.1

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.
@@ -171,12 +171,8 @@ export class WebGL2Backend {
171
171
  this.vao = gl.createVertexArray();
172
172
  gl.bindVertexArray(this.vao);
173
173
  gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
174
- gl.enableVertexAttribArray(0);
175
- gl.vertexAttribPointer(0, 3, gl.FLOAT, false, VERTEX_BYTES, 0);
176
- gl.enableVertexAttribArray(1);
177
- gl.vertexAttribPointer(1, 3, gl.FLOAT, false, VERTEX_BYTES, 12);
178
- gl.enableVertexAttribArray(2);
179
- gl.vertexAttribPointer(2, 2, gl.FLOAT, false, VERTEX_BYTES, 24);
174
+ for (let k = 0; k < 3; k++) gl.enableVertexAttribArray(k);
175
+ this.bindVertexAttributes();
180
176
  gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer);
181
177
  for (let k = 0; k < 4; k++) {
182
178
  gl.enableVertexAttribArray(3 + k);
@@ -215,14 +211,28 @@ export class WebGL2Backend {
215
211
  this.gl.deleteTexture(t);
216
212
  }
217
213
 
218
- /** Upload geometry appended to the pool since the last sync. */
214
+ /** Point the VAO's vertex attributes at the vertex buffer bound to ARRAY_BUFFER. */
215
+ bindVertexAttributes() {
216
+ const gl = this.gl;
217
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, VERTEX_BYTES, 0);
218
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, VERTEX_BYTES, 12);
219
+ gl.vertexAttribPointer(2, 2, gl.FLOAT, false, VERTEX_BYTES, 24);
220
+ }
221
+
222
+ /**
223
+ * Upload geometry appended to the pool since the last sync.
224
+ *
225
+ * A new pool generation (reset, repack, rebuilt context) reallocates both
226
+ * buffers and uploads everything. A pool that only grew keeps its content:
227
+ * the buffer that ran out of room is replaced by a larger one, the bytes
228
+ * already uploaded are copied on the GPU, and only new data is sent.
229
+ */
219
230
  syncPool(pool) {
220
231
  const gl = this.gl;
221
- const vBytes = pool.vertices.byteLength;
222
- const iBytes = pool.indices.byteLength;
223
- const realloc = pool.generation !== this.poolGeneration || vBytes !== this.vertexCapacity || iBytes !== this.indexCapacity;
232
+ const vBytes = pool.vertexCapacityBytes;
233
+ const iBytes = pool.indexCapacityBytes;
224
234
  let uploaded = 0;
225
- if (realloc) {
235
+ if (pool.generation !== this.poolGeneration) {
226
236
  gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
227
237
  gl.bufferData(gl.ARRAY_BUFFER, vBytes, gl.STATIC_DRAW);
228
238
  gl.bindVertexArray(this.vao);
@@ -234,25 +244,67 @@ export class WebGL2Backend {
234
244
  this.poolGeneration = pool.generation;
235
245
  pool.uploadedVertices = 0;
236
246
  pool.uploadedIndices = 0;
247
+ } else {
248
+ if (vBytes > this.vertexCapacity) {
249
+ const old = this.vertexBuffer;
250
+ const next = gl.createBuffer();
251
+ gl.bindBuffer(gl.ARRAY_BUFFER, next);
252
+ gl.bufferData(gl.ARRAY_BUFFER, vBytes, gl.STATIC_DRAW);
253
+ gl.bindVertexArray(this.vao);
254
+ this.bindVertexAttributes();
255
+ gl.bindVertexArray(null);
256
+ this.copyBuffer(old, next, pool.uploadedVertices * VERTEX_BYTES);
257
+ gl.deleteBuffer(old);
258
+ this.vertexBuffer = next;
259
+ this.vertexCapacity = vBytes;
260
+ }
261
+ if (iBytes > this.indexCapacity) {
262
+ // An index buffer must first be bound as ELEMENT_ARRAY_BUFFER (WebGL2
263
+ // types buffers on first bind); bind it through the VAO, which keeps it.
264
+ const old = this.indexBuffer;
265
+ const next = gl.createBuffer();
266
+ gl.bindVertexArray(this.vao);
267
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, next);
268
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, iBytes, gl.STATIC_DRAW);
269
+ gl.bindVertexArray(null);
270
+ this.copyBuffer(old, next, pool.uploadedIndices * 4);
271
+ gl.deleteBuffer(old);
272
+ this.indexBuffer = next;
273
+ this.indexCapacity = iBytes;
274
+ }
237
275
  }
238
276
  const p = pool.pending();
239
277
  if (p.vertexEnd > p.vertexStart) {
240
278
  gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
241
- const f = 8;
242
- gl.bufferSubData(gl.ARRAY_BUFFER, p.vertexStart * VERTEX_BYTES, pool.vertices.subarray(p.vertexStart * f, p.vertexEnd * f));
243
- uploaded += (p.vertexEnd - p.vertexStart) * VERTEX_BYTES;
279
+ pool.forEachVertices(p.vertexStart, p.vertexEnd, (first, data) => {
280
+ gl.bufferSubData(gl.ARRAY_BUFFER, first * VERTEX_BYTES, data);
281
+ uploaded += data.byteLength;
282
+ });
244
283
  }
245
284
  if (p.indexEnd > p.indexStart) {
246
285
  gl.bindVertexArray(this.vao);
247
286
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
248
- gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, p.indexStart * 4, pool.indices.subarray(p.indexStart, p.indexEnd));
287
+ pool.forEachIndices(p.indexStart, p.indexEnd, (first, data) => {
288
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, first * 4, data);
289
+ uploaded += data.byteLength;
290
+ });
249
291
  gl.bindVertexArray(null);
250
- uploaded += (p.indexEnd - p.indexStart) * 4;
251
292
  }
252
293
  pool.markUploaded();
253
294
  return uploaded;
254
295
  }
255
296
 
297
+ /** Copy the first `bytes` of one buffer into another, on the GPU. */
298
+ copyBuffer(from, to, bytes) {
299
+ if (bytes <= 0) return;
300
+ const gl = this.gl;
301
+ gl.bindBuffer(gl.COPY_READ_BUFFER, from);
302
+ gl.bindBuffer(gl.COPY_WRITE_BUFFER, to);
303
+ gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, bytes);
304
+ gl.bindBuffer(gl.COPY_READ_BUFFER, null);
305
+ gl.bindBuffer(gl.COPY_WRITE_BUFFER, null);
306
+ }
307
+
256
308
  uploadInstances(data, count) {
257
309
  const gl = this.gl;
258
310
  const bytes = count * 64;
@@ -254,30 +254,61 @@ export class WebGPUBackend {
254
254
  });
255
255
  }
256
256
 
257
+ /**
258
+ * Upload geometry appended to the pool since the last sync.
259
+ *
260
+ * A new pool generation (reset, repack) reallocates both buffers and uploads
261
+ * everything. A pool that only grew keeps its content: the buffer that ran
262
+ * out of room is replaced by a larger one, the bytes already uploaded are
263
+ * copied on the GPU, and only new data is sent.
264
+ */
257
265
  syncPool(pool) {
258
266
  const d = this.device;
259
- const vBytes = pool.vertices.byteLength;
260
- const iBytes = pool.indices.byteLength;
267
+ const vBytes = pool.vertexCapacityBytes;
268
+ const iBytes = pool.indexCapacityBytes;
269
+ const vUsage = GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
270
+ const iUsage = GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
261
271
  let uploaded = 0;
262
- if (pool.generation !== this.poolGeneration || !this.vertexBuffer || this.vertexBuffer.size < vBytes || this.indexBuffer.size < iBytes) {
272
+ if (pool.generation !== this.poolGeneration || !this.vertexBuffer) {
263
273
  if (this.vertexBuffer) this.vertexBuffer.destroy();
264
274
  if (this.indexBuffer) this.indexBuffer.destroy();
265
- this.vertexBuffer = d.createBuffer({ size: pow2(vBytes, 1 << 16), usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
266
- this.indexBuffer = d.createBuffer({ size: pow2(iBytes, 1 << 16), usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
275
+ this.vertexBuffer = d.createBuffer({ size: pow2(vBytes, 1 << 16), usage: vUsage });
276
+ this.indexBuffer = d.createBuffer({ size: pow2(iBytes, 1 << 16), usage: iUsage });
267
277
  this.poolGeneration = pool.generation;
268
278
  pool.uploadedVertices = 0;
269
279
  pool.uploadedIndices = 0;
280
+ } else {
281
+ let encoder = null;
282
+ const grow = (old, bytes, usage, keep) => {
283
+ const next = d.createBuffer({ size: pow2(bytes, 1 << 16), usage });
284
+ if (keep > 0) {
285
+ encoder ||= d.createCommandEncoder();
286
+ encoder.copyBufferToBuffer(old, 0, next, 0, keep);
287
+ }
288
+ return next;
289
+ };
290
+ const oldV = this.vertexBuffer.size < vBytes ? this.vertexBuffer : null;
291
+ const oldI = this.indexBuffer.size < iBytes ? this.indexBuffer : null;
292
+ if (oldV) this.vertexBuffer = grow(oldV, vBytes, vUsage, pool.uploadedVertices * VERTEX_BYTES);
293
+ if (oldI) this.indexBuffer = grow(oldI, iBytes, iUsage, pool.uploadedIndices * 4);
294
+ // the copy is queued before the writes below, so the new data lands after it;
295
+ // destroying a buffer waits for submitted work that reads it
296
+ if (encoder) d.queue.submit([encoder.finish()]);
297
+ if (oldV) oldV.destroy();
298
+ if (oldI) oldI.destroy();
270
299
  }
271
300
  const p = pool.pending();
272
301
  if (p.vertexEnd > p.vertexStart) {
273
- const src = pool.vertices.subarray(p.vertexStart * 8, p.vertexEnd * 8);
274
- d.queue.writeBuffer(this.vertexBuffer, p.vertexStart * VERTEX_BYTES, src.buffer, src.byteOffset, src.byteLength);
275
- uploaded += src.byteLength;
302
+ pool.forEachVertices(p.vertexStart, p.vertexEnd, (first, src) => {
303
+ d.queue.writeBuffer(this.vertexBuffer, first * VERTEX_BYTES, src.buffer, src.byteOffset, src.byteLength);
304
+ uploaded += src.byteLength;
305
+ });
276
306
  }
277
307
  if (p.indexEnd > p.indexStart) {
278
- const src = pool.indices.subarray(p.indexStart, p.indexEnd);
279
- d.queue.writeBuffer(this.indexBuffer, p.indexStart * 4, src.buffer, src.byteOffset, src.byteLength);
280
- uploaded += src.byteLength;
308
+ pool.forEachIndices(p.indexStart, p.indexEnd, (first, src) => {
309
+ d.queue.writeBuffer(this.indexBuffer, first * 4, src.buffer, src.byteOffset, src.byteLength);
310
+ uploaded += src.byteLength;
311
+ });
281
312
  }
282
313
  pool.markUploaded();
283
314
  return uploaded;
@@ -0,0 +1,14 @@
1
+ // The first module the stage evaluates (render/stage.js imports it before
2
+ // anything else). In the compiler worker it keeps every message that arrives
3
+ // before the compiler is ready: the WebAssembly kernels (wasm/kernels.js) are
4
+ // instantiated with a top-level await, and a module worker delivers messages
5
+ // while it waits, so a handler installed after that would miss the first
6
+ // requests. compile-worker.js answers the kept messages once it takes over.
7
+
8
+ /** True inside a dedicated worker. */
9
+ export const inWorker = typeof WorkerGlobalScope !== 'undefined' && typeof self !== 'undefined' && self instanceof WorkerGlobalScope;
10
+
11
+ /** Messages that arrived before the compiler was ready, oldest first. */
12
+ export const early = [];
13
+
14
+ if (inWorker) self.onmessage = (e) => early.push(e);