plum
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)
- 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
|
|
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
|
|
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 =
|
|
232
|
+
const VERT_WORDS = 6;
|
|
231
|
-
const VERT_BYTES =
|
|
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 {
|
|
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
|
-
//
|
|
311
|
+
// hold other textures. Sets slotBits for the vertices that follow.
|
|
304
312
|
function useTex(t) {
|
|
305
|
-
if (cur && cur.
|
|
313
|
+
if (!cur || (t.bid !== cur.id && cur.n === SLOTS)) {
|
|
306
|
-
|
|
314
|
+
cur = batches[nb];
|
|
307
|
-
|
|
315
|
+
if (!cur) batches.push((cur = { id: 0, slots: new Array(SLOTS).fill(null), n: 0, start: 0, count: 0 }));
|
|
308
|
-
|
|
316
|
+
nb++;
|
|
317
|
+
cur.id = ++batchSeq;
|
|
309
|
-
|
|
318
|
+
cur.n = 0;
|
|
310
|
-
|
|
319
|
+
cur.start = inn;
|
|
311
|
-
|
|
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
|
-
|
|
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(
|
|
527
|
+
${decl}@group(0) @binding(16) var ns: sampler;
|
|
497
|
-
@group(0) @binding(
|
|
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
|
|
536
|
+
return pick(i.slot, i.uv) * i.c;
|
|
500
537
|
}`;
|
|
538
|
+
}
|
|
539
|
+
const WGSL = makeWGSL();
|
|
501
540
|
|
|
502
|
-
async function makeWebGPU(canvas,
|
|
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
|
|
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
|
-
|
|
598
|
+
bg = device.createBindGroup({ layout, entries });
|
|
549
|
-
|
|
599
|
+
groups.set(key, bg);
|
|
600
|
+
}
|
|
550
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
738
|
+
backend = await makeWebGPU(canvas, b);
|
|
695
739
|
} catch (e) {
|
|
696
740
|
console.error("WebGPU failed to start:", e);
|
|
697
741
|
}
|