plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


73a13f5 — Peter John 2026-11-02T17:23:10+05:30
plum_bridge: 16 texture slots per draw call (per-vertex slot, switch over 16 bindings)
Files changed (1) hide show
  1. plum-game/web/plum_bridge.js +84 -40
plum-game/web/plum_bridge.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // Own batching renderer for a Plum game: plum.wasm calls this file directly (no
2
2
  // second wasm module, no library). Every draw becomes quads or triangle fans
3
3
  // written into one CPU vertex/index array (camera applied on the CPU), flushed
4
- // with one buffer upload per frame and one draw call per run of the same texture.
4
+ // with one buffer upload per frame. A draw call binds up to 16 textures and each
5
+ // vertex carries the slot of its texture, so the draw only ends when a 17th
5
- // Untextured shapes sample a 1x1 white texture. The backend is WebGPU where the
6
+ // texture is needed. Untextured shapes sample a 1x1 white texture. The backend is WebGPU where the
6
7
  // browser has it; there is no fallback (a page without navigator.gpu says so).
7
8
  //
8
9
  // Each std/Game draw extern appends vertices to the batcher directly.
@@ -226,9 +227,12 @@
226
227
 
227
228
  // --- batcher ----------------------------------------------------------------------
228
229
 
229
- // One vertex is 20 bytes: x, y (clip space), u, v (f32) and rgba (u8x4).
230
+ // One vertex is 24 bytes: x, y (clip space), u, v (f32), rgba (u8x4) and a
231
+ // u32 holding the texture slot (bits 0-7) and the linear-filter flag (bit 8).
230
- const VERT_WORDS = 5;
232
+ const VERT_WORDS = 6;
231
- const VERT_BYTES = 20;
233
+ const VERT_BYTES = 24;
234
+ const SLOTS = 16;
235
+ const SMOOTH_BIT = 256;
232
236
  const MAX_SEGMENTS = 96;
233
237
 
234
238
  function clamp01(v) {
@@ -268,11 +272,14 @@
268
272
  let idx = new Uint32Array(icap);
269
273
  let vn = 0; // vertices used
270
274
  let inn = 0; // indices used
271
- const batches = []; // pooled { tex, start, count }
275
+ const batches = []; // pooled { id, slots, n, start, count }
272
276
  let nb = 0;
273
277
  let cur = null;
278
+ let batchSeq = 0;
279
+ let slotBits = 0; // slot | smooth flag of the texture of the quad being written
280
+ let texSeq = 0;
274
- const white = { w: 1, h: 1, white: true };
281
+ const white = { w: 1, h: 1, white: true, uid: texSeq++ };
275
- const textures = []; // handle -> texture object { w, h, ... }
282
+ const textures = []; // handle -> texture object { w, h, uid, smooth, ... }
276
283
  let sw = 1;
277
284
  let sh = 1;
278
285
  const cam = { x: 0, y: 0, s: 1 };
@@ -300,15 +307,24 @@
300
307
  }
301
308
  }
302
309
 
310
+ // Puts `t` in a slot of the current batch; starts a new batch when all 16 slots
303
- // Starts a new batch only when the texture changes.
311
+ // hold other textures. Sets slotBits for the vertices that follow.
304
312
  function useTex(t) {
305
- if (cur && cur.tex === t) return;
313
+ if (!cur || (t.bid !== cur.id && cur.n === SLOTS)) {
306
- cur = batches[nb];
314
+ cur = batches[nb];
307
- if (!cur) batches.push((cur = { tex: t, start: 0, count: 0 }));
315
+ if (!cur) batches.push((cur = { id: 0, slots: new Array(SLOTS).fill(null), n: 0, start: 0, count: 0 }));
308
- nb++;
316
+ nb++;
317
+ cur.id = ++batchSeq;
309
- cur.tex = t;
318
+ cur.n = 0;
310
- cur.start = inn;
319
+ cur.start = inn;
311
- cur.count = 0;
320
+ cur.count = 0;
321
+ }
322
+ if (t.bid !== cur.id) {
323
+ t.bid = cur.id;
324
+ t.slot = cur.n;
325
+ cur.slots[cur.n++] = t;
326
+ }
327
+ slotBits = t.slot | (t.smooth ? SMOOTH_BIT : 0);
312
328
  }
313
329
 
314
330
  function vert(x, y, u, v, c) {
@@ -318,6 +334,7 @@
318
334
  f32[o + 2] = u;
319
335
  f32[o + 3] = v;
320
336
  u32[o + 4] = c;
337
+ u32[o + 5] = slotBits;
321
338
  vn++;
322
339
  }
323
340
 
@@ -352,6 +369,7 @@
352
369
 
353
370
  const self = {
354
371
  textures,
372
+ newTextureId: () => texSeq++,
355
373
  white,
356
374
  cam,
357
375
  clearColor,
@@ -480,26 +498,48 @@
480
498
  // A backend: { name, createTexture(source, w, h, smooth) -> { w, h, ...gpu fields },
481
499
  // setSmooth(tex, smooth), destroyTexture(tex), submit(batcher, w, h) -> draw calls issued }.
482
500
 
501
+ // Sixteen texture bindings (0-15) picked by a switch on the vertex's slot, then a
502
+ // nearest (16) and a linear (17) sampler. A binding array is not in core WebGPU,
503
+ // so this is the portable form. textureSampleLevel is used because the switch is
504
+ // not uniform control flow.
483
- const WGSL = `
505
+ function makeWGSL() {
506
+ let decl = "";
507
+ let cases = "";
508
+ for (let i = 0; i < SLOTS; i++) {
509
+ decl += `@group(0) @binding(${i}) var t${i}: texture_2d<f32>;\n`;
510
+ cases += ` case ${i}u: { if (lin) { return textureSampleLevel(t${i}, ls, uv, 0.0); } return textureSampleLevel(t${i}, ns, uv, 0.0); }\n`;
511
+ }
512
+ return `
484
513
  struct VOut {
485
514
  @builtin(position) p: vec4f,
486
515
  @location(0) uv: vec2f,
487
516
  @location(1) c: vec4f,
517
+ @location(2) @interpolate(flat) slot: u32,
488
518
  };
489
- @vertex fn vs(@location(0) p: vec2f, @location(1) uv: vec2f, @location(2) c: vec4f) -> VOut {
519
+ @vertex fn vs(@location(0) p: vec2f, @location(1) uv: vec2f, @location(2) c: vec4f, @location(3) slot: u32) -> VOut {
490
520
  var o: VOut;
491
521
  o.p = vec4f(p, 0.0, 1.0);
492
522
  o.uv = uv;
493
523
  o.c = c;
524
+ o.slot = slot;
494
525
  return o;
495
526
  }
496
- @group(0) @binding(0) var tex: texture_2d<f32>;
527
+ ${decl}@group(0) @binding(16) var ns: sampler;
497
- @group(0) @binding(1) var smp: sampler;
528
+ @group(0) @binding(17) var ls: sampler;
529
+ fn pick(slot: u32, uv: vec2f) -> vec4f {
530
+ let lin = (slot & ${SMOOTH_BIT}u) != 0u;
531
+ switch (slot & 255u) {
532
+ ${cases} default: { return vec4f(1.0); }
533
+ }
534
+ }
498
535
  @fragment fn fs(i: VOut) -> @location(0) vec4f {
499
- return textureSample(tex, smp, i.uv) * i.c;
536
+ return pick(i.slot, i.uv) * i.c;
500
537
  }`;
538
+ }
539
+ const WGSL = makeWGSL();
501
540
 
502
- async function makeWebGPU(canvas, white) {
541
+ async function makeWebGPU(canvas, b) {
542
+ const white = b.white;
503
543
  if (!navigator.gpu) return null;
504
544
  const adapter = await navigator.gpu.requestAdapter();
505
545
  if (!adapter) return null;
@@ -520,6 +560,7 @@ struct VOut {
520
560
  { shaderLocation: 0, format: "float32x2", offset: 0 },
521
561
  { shaderLocation: 1, format: "float32x2", offset: 8 },
522
562
  { shaderLocation: 2, format: "unorm8x4", offset: 16 },
563
+ { shaderLocation: 3, format: "uint32", offset: 20 },
523
564
  ],
524
565
  }],
525
566
  },
@@ -544,16 +585,22 @@ struct VOut {
544
585
  let ibuf = null;
545
586
  let isize = 0;
546
587
 
588
+ // Bind groups by the textures of a draw's slots (unused slots hold the white texture).
589
+ const groups = new Map();
547
- function bind(t) {
590
+ function bindGroup(batch, white) {
591
+ let key = "";
592
+ for (let i = 0; i < SLOTS; i++) key += (i < batch.n ? batch.slots[i].uid : white.uid) + ",";
593
+ let bg = groups.get(key);
594
+ if (!bg) {
595
+ const entries = [];
596
+ for (let i = 0; i < SLOTS; i++) entries.push({ binding: i, resource: (i < batch.n ? batch.slots[i] : white).view });
597
+ entries.push({ binding: 16, resource: nearest }, { binding: 17, resource: linear });
548
- t.bg = device.createBindGroup({
598
+ bg = device.createBindGroup({ layout, entries });
549
- layout,
599
+ groups.set(key, bg);
600
+ }
550
- entries: [
601
+ return bg;
551
- { binding: 0, resource: t.view },
552
- { binding: 1, resource: t.smooth ? linear : nearest },
553
- ],
554
- });
555
602
  }
556
- function createTexture(source, w, h, smooth) {
603
+ function createTexture(source, w, h, smooth, uid) {
557
604
  const gpu = device.createTexture({
558
605
  size: [w, h],
559
606
  format: "rgba8unorm",
@@ -562,21 +609,18 @@ struct VOut {
562
609
  if (source && source.data) device.queue.writeTexture({ texture: gpu }, source.data, { bytesPerRow: w * 4 }, [w, h]);
563
610
  else if (source) device.queue.copyExternalImageToTexture({ source }, { texture: gpu }, [w, h]);
564
611
  else device.queue.writeTexture({ texture: gpu }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
565
- const t = { w, h, gpu, view: gpu.createView(), smooth: !!smooth, bg: null };
612
+ return { w, h, uid, gpu, view: gpu.createView(), smooth: !!smooth, bid: 0, slot: 0 };
566
- bind(t);
567
- return t;
568
613
  }
569
- Object.assign(white, createTexture(null, 1, 1, false));
614
+ Object.assign(white, createTexture(null, 1, 1, false, white.uid));
570
615
 
571
616
  return {
572
617
  name: "gpu-webgpu",
573
- createTexture,
618
+ createTexture: (source, w, h, smooth) => createTexture(source, w, h, smooth, b.newTextureId()),
574
619
  setSmooth(t, smooth) {
575
- if (t.smooth === !!smooth) return;
576
620
  t.smooth = !!smooth;
577
- bind(t);
578
621
  },
579
622
  destroyTexture(t) {
623
+ groups.clear();
580
624
  t.gpu.destroy();
581
625
  },
582
626
  submit(b) {
@@ -612,7 +656,7 @@ struct VOut {
612
656
  pass.setIndexBuffer(ibuf, "uint32");
613
657
  for (let i = 0; i < b.batchCount; i++) {
614
658
  const bt = b.batches[i];
615
- pass.setBindGroup(0, bt.tex.bg);
659
+ pass.setBindGroup(0, bindGroup(bt, white));
616
660
  pass.drawIndexed(bt.count, 1, bt.start);
617
661
  }
618
662
  }
@@ -691,7 +735,7 @@ struct VOut {
691
735
  const text = makeTextCache(() => backend);
692
736
  const b = makeBatcher({ textTexture: text.textTexture });
693
737
  try {
694
- backend = await makeWebGPU(canvas, b.white);
738
+ backend = await makeWebGPU(canvas, b);
695
739
  } catch (e) {
696
740
  console.error("WebGPU failed to start:", e);
697
741
  }