Repository navigation
Expand file tree
/
Copy pathwgpu_worker.js
More file actions
2969 lines (2882 loc) · 187 KB
/
Copy pathwgpu_worker.js
File metadata and controls
2969 lines (2882 loc) · 187 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// wasm/platform/graphics/wgpu_worker.js
//
// The GPU worker of the wasm port: a plain JS worker (Node worker_threads or a browser dedicated worker) that owns the
// WebGPU device and executes the command stream the game writes into a ring buffer in the shared wasm memory.
//
// Why a separate worker and not calls from the game's render pthread:
// - WebGPU objects cannot cross threads, but D3D11 resources are created by any engine thread (streaming, main, render).
// The game threads only emit commands; every WebGPU object lives here.
// - WebGPU is asynchronous (device creation, mapAsync, onSubmittedWorkDone); this worker has a normal event loop, the
// game's blocking C++ threads never have to yield to JS. They wait on a futex/fence word in the ring when they need a
// result (readback, occlusion query).
//
// Protocol (see wgpu_backend.h for the C++ producer; keep both in sync):
// ring header, int32 words at the ring base address:
// [0] head byte offset of the next free byte (written by the producer(s) under a lock, published with release)
// [1] tail byte offset of the next unread command (written by this worker)
// [2] state 0 = starting, 1 = running, 2 = failed (commands are drained and dropped)
// [3] executed command counter (statistics)
// [4] error counter (WebGPU validation errors reported by the device)
// commands start at byte offset RING_DATA; each command = 2 header words (opcode, payload word count) + payload words.
// opcode WRAP: the producer continues at RING_DATA.
//
// Node needs the `webgpu` npm package (Dawn). Its location: env WGPU_NODE_MODULE, default D:/wasm_build/shaders/node/node_modules/webgpu.
'use strict';
const RING_DATA = 64;
// keep in sync with wgpu_backend.h
const OP = {
NOP: 0, WRAP: 1, INIT: 2, CREATE_TEXTURE: 3, DESTROY_OBJECT: 4, CLEAR_RT: 5, CLEAR_DS: 6, PRESENT: 7, FENCE: 8,
SCREENSHOT: 9, LOG: 10, CREATE_BUFFER: 11, UPLOAD_BUFFER: 12, UPLOAD_TEXTURE: 13, CREATE_SHADER: 14, CREATE_LAYOUT: 15,
CREATE_STATE: 16, CREATE_SRV: 17, SET_SHADER: 18, SET_INPUT_LAYOUT: 19, SET_TOPOLOGY: 20, SET_VERTEX_BUFFERS: 21,
SET_INDEX_BUFFER: 22, SET_CBUFFERS: 23, SET_SRVS: 24, SET_SAMPLERS: 25, SET_RENDER_TARGETS: 26, SET_BLEND: 27,
SET_DEPTH_STENCIL: 28, SET_RASTER: 29, SET_VIEWPORTS: 30, SET_SCISSORS: 31, DRAW: 32, CLEAR_STATE: 33,
COPY_REGION: 34, COPY_RESOURCE: 35, RESOLVE: 36, CREATE_UAV: 37, SET_UAVS: 38, DISPATCH: 39, CLEAR_UAV: 40, GENERATE_MIPS: 41, READBACK: 42, COPY_COUNTER: 43, CREATE_QUERY: 44, BEGIN_QUERY: 45, END_QUERY: 46, UPLOAD_BUFFER_NO_OVERWRITE: 47,
};
const isNode = typeof process !== 'undefined' && !!(process.versions && process.versions.node);
let parentPort = null;
if (isNode) parentPort = require('worker_threads').parentPort;
function post(msg) { if (parentPort) parentPort.postMessage(msg); else self.postMessage(msg); }
// fs.writeSync: process.stderr in a worker is proxied through the main thread, whose event loop the blocked game never runs
function logLine(s) {
if (isNode) { require('fs').writeSync(2, '[wgpu] ' + s + '\n'); return; }
console.log('[wgpu] ' + s);
// with ?log=1 on the page (loader.js puts it in this worker's URL) the page forwards it to the dev server (index.html, 'game-log'): a synchronous POST here stalled
// this worker for a network round trip (remote players: tens of ms)
if (logLine.remote === undefined) logLine.remote = new URLSearchParams(self.location.search).get('log') === '1';
if (logLine.remote) try { (logLine.bc || (logLine.bc = new BroadcastChannel('game-log'))).postMessage('[wgpu] ' + s); } catch (e) { /* no channel */ }
}
// ---- D3D11 (DXGI) -> WebGPU formats ----------------------------------------------------------------------------------
// DXGI_FORMAT numbers. `ds` = the texture is bound as a depth-stencil target. Typeless formats pick the format the game
// most plausibly views them as; `views` are extra view formats (sRGB reinterpretation).
const DXGI = {
R32G32B32A32_FLOAT: 2, R32G32B32_FLOAT: 6, R16G16B16A16_TYPELESS: 9, R16G16B16A16_FLOAT: 10, R16G16B16A16_UNORM: 11,
R32G32_FLOAT: 16, R32G8X24_TYPELESS: 19, D32_FLOAT_S8X24_UINT: 20, R10G10B10A2_TYPELESS: 23, R10G10B10A2_UNORM: 24,
R11G11B10_FLOAT: 26, R8G8B8A8_TYPELESS: 27, R8G8B8A8_UNORM: 28, R8G8B8A8_UNORM_SRGB: 29, R16G16_TYPELESS: 33,
R16G16_FLOAT: 34, R16G16_UNORM: 35, R32_TYPELESS: 39, D32_FLOAT: 40, R32_FLOAT: 41, R24G8_TYPELESS: 44,
D24_UNORM_S8_UINT: 45, R8G8_TYPELESS: 48, R8G8_UNORM: 49, R16_TYPELESS: 53, R16_FLOAT: 54, D16_UNORM: 55,
R8_TYPELESS: 60, R8_UNORM: 61, BC1_TYPELESS: 70, BC1_UNORM: 71, BC1_UNORM_SRGB: 72, BC2_TYPELESS: 73, BC2_UNORM: 74,
BC2_UNORM_SRGB: 75, BC3_TYPELESS: 76, BC3_UNORM: 77, BC3_UNORM_SRGB: 78, BC4_TYPELESS: 79, BC4_UNORM: 80,
BC5_TYPELESS: 82, BC5_UNORM: 83, B8G8R8A8_UNORM: 87, B8G8R8X8_UNORM: 88, B8G8R8A8_TYPELESS: 90,
B8G8R8A8_UNORM_SRGB: 91, BC6H_TYPELESS: 94, BC6H_UF16: 95, BC7_TYPELESS: 97, BC7_UNORM: 98, BC7_UNORM_SRGB: 99,
};
const BIND_SRV = 0x8, BIND_RT = 0x20, BIND_DS = 0x40, BIND_UAV = 0x80;
function mapFormat(f, bind) {
const ds = (bind & BIND_DS) !== 0;
switch (f) {
case DXGI.R32G32B32A32_FLOAT: return { format: 'rgba32float' };
case DXGI.R16G16B16A16_TYPELESS: case DXGI.R16G16B16A16_FLOAT: return { format: 'rgba16float' };
case DXGI.R16G16B16A16_UNORM: return { format: 'rgba16float' }; // no core rgba16unorm: TODO texture-formats-tier1
case DXGI.R32G32_FLOAT: return { format: 'rg32float' };
case DXGI.R10G10B10A2_TYPELESS: case DXGI.R10G10B10A2_UNORM: return { format: 'rgb10a2unorm' };
case DXGI.R11G11B10_FLOAT: return { format: 'rg11b10ufloat' };
case DXGI.R8G8B8A8_TYPELESS: case DXGI.R8G8B8A8_UNORM: return { format: 'rgba8unorm', views: ['rgba8unorm-srgb'] };
case DXGI.R8G8B8A8_UNORM_SRGB: return { format: 'rgba8unorm-srgb', views: ['rgba8unorm'] };
case DXGI.B8G8R8A8_TYPELESS: case DXGI.B8G8R8A8_UNORM: case DXGI.B8G8R8X8_UNORM: return { format: 'bgra8unorm', views: ['bgra8unorm-srgb'] };
case DXGI.B8G8R8A8_UNORM_SRGB: return { format: 'bgra8unorm-srgb', views: ['bgra8unorm'] };
case DXGI.R16G16_TYPELESS: case DXGI.R16G16_FLOAT: case DXGI.R16G16_UNORM: return { format: 'rg16float' };
case DXGI.R32_TYPELESS: return { format: ds ? 'depth32float' : 'r32float' };
case DXGI.D32_FLOAT: return { format: 'depth32float' };
case DXGI.R32_FLOAT: return { format: 'r32float' };
case DXGI.R32G8X24_TYPELESS: case DXGI.D32_FLOAT_S8X24_UINT: return { format: 'depth32float-stencil8' };
case DXGI.R24G8_TYPELESS: case DXGI.D24_UNORM_S8_UINT: return { format: 'depth24plus-stencil8' };
case DXGI.R8G8_TYPELESS: case DXGI.R8G8_UNORM: return { format: 'rg8unorm' };
case DXGI.R16_TYPELESS: return { format: ds ? 'depth16unorm' : 'r16float' };
case DXGI.D16_UNORM: return { format: 'depth16unorm' };
case DXGI.R16_FLOAT: return { format: 'r16float' };
case DXGI.R8_TYPELESS: case DXGI.R8_UNORM: return { format: 'r8unorm' };
case 63: return { format: 'r8snorm' };
case 51: return { format: 'rg8snorm' };
case 31: return { format: 'rgba8snorm' };
case 67: return { format: 'rgb9e5ufloat' };
case 65: return { format: 'rgba8unorm', a8: true }; // A8_UNORM: sampled as (0,0,0,a); expanded to rgba8 on upload (no texture swizzle in WebGPU)
case 56: return { format: 'depth16unorm' };
case 21: case 22: case 46: case 47: return { format: 'depth24plus-stencil8' }; // depth/stencil views of a depth texture: the view aspect selects the plane
case DXGI.BC1_TYPELESS: case DXGI.BC1_UNORM: return { format: 'bc1-rgba-unorm', views: ['bc1-rgba-unorm-srgb'] };
case DXGI.BC1_UNORM_SRGB: return { format: 'bc1-rgba-unorm-srgb', views: ['bc1-rgba-unorm'] };
case DXGI.BC2_TYPELESS: case DXGI.BC2_UNORM: return { format: 'bc2-rgba-unorm', views: ['bc2-rgba-unorm-srgb'] };
case DXGI.BC2_UNORM_SRGB: return { format: 'bc2-rgba-unorm-srgb', views: ['bc2-rgba-unorm'] };
case DXGI.BC3_TYPELESS: case DXGI.BC3_UNORM: return { format: 'bc3-rgba-unorm', views: ['bc3-rgba-unorm-srgb'] };
case DXGI.BC3_UNORM_SRGB: return { format: 'bc3-rgba-unorm-srgb', views: ['bc3-rgba-unorm'] };
case DXGI.BC4_TYPELESS: case DXGI.BC4_UNORM: return { format: 'bc4-r-unorm' };
case DXGI.BC5_TYPELESS: case DXGI.BC5_UNORM: return { format: 'bc5-rg-unorm' };
case DXGI.BC6H_TYPELESS: case DXGI.BC6H_UF16: return { format: 'bc6h-rgb-ufloat' };
case DXGI.BC7_TYPELESS: case DXGI.BC7_UNORM: return { format: 'bc7-rgba-unorm', views: ['bc7-rgba-unorm-srgb'] };
case DXGI.BC7_UNORM_SRGB: return { format: 'bc7-rgba-unorm-srgb', views: ['bc7-rgba-unorm'] };
default: logOnce('fmt' + f, 'no WebGPU format for DXGI ' + f + ' (using rgba8unorm)'); return { format: 'rgba8unorm' };
}
}
const hasStencil = (format) => format === 'depth24plus-stencil8' || format === 'depth32float-stencil8' || format === 'stencil8';
const isDepth = (format) => format.startsWith('depth') || format === 'stencil8';
// ---- state --------------------------------------------------------------------------------------------------------------
let gpu = null, adapter = null, device = null;
let mem = null, memBuffer = null, i32 = null, u32 = null, f32 = null, u8 = null, ringBase = 0, ringSize = 0;
let canvas = null, canvasCtx = null, canvasFormat = null, blit = null, canvasConfigured = false;
let fpsChannel = null, fpsOverlayN = 0, fpsOverlayT = 0;
let encoder = null, pass = null, passKey = '';
let errorCount = 0, errorCount0 = 0, clampedDraws = 0, clampedDraws0 = 0;
const errorKinds = new Map(); // normalised message -> { n, msg }
function dumpErrors() { for (const [k, e] of [...errorKinds].sort((a, b) => b[1].n - a[1].n).slice(0, 15)) logLine(' error x' + e.n + ': ' + e.msg.slice(0, 300)); }
let drawCount = 0, skippedDraws = 0;
let lastPresentDraws = 0, progressChannel = null, worldSent = false;
let worldSince = 0, worldClean = 0, worldSkips0 = 0, worldNoteT = 0, skipsAtPresent = 0; // present(): the title screen stays up until frames stop skipping draws
// id -> object (texture, buffer, shader, layout, srv, state); ids are unique across kinds. Ids come from one counter in wgpu_backend.cpp (AllocId) and are never reused, so a plain
// array indexed by id holds them (dense, small gaps): a draw looks up ~15 objects, and an element load is several times cheaper than Map.get.
class IdTable {
constructor() { this.a = []; this.n = 0; }
get(id) { return this.a[id]; }
set(id, v) { if (this.a[id] === undefined) this.n++; this.a[id] = v; return this; }
delete(id) { if (this.a[id] === undefined) return false; this.a[id] = undefined; this.n--; return true; }
has(id) { return this.a[id] !== undefined; }
get size() { return this.n; }
*values() { for (const v of this.a) if (v !== undefined) yield v; }
}
const objs = new IdTable();
// Index into i32/u32 of a wasm memory address. Not a right shift by 2: JS shifts work on signed 32 bits, so every address at or above 2 GB (the memory starts at 3 GB and objects
// created later in a session live up there) became a negative index; event queries (the engine's GPU fences), occlusion queries and readback flags then never completed.
function w32(addr) { return Math.floor(addr / 4); }
let ringW = 0; // w32(ringBase)
function refreshViews() {
if (memBuffer === mem.buffer) return;
memBuffer = mem.buffer;
i32 = new Int32Array(memBuffer);
u32 = new Uint32Array(memBuffer);
f32 = new Float32Array(memBuffer);
u8 = new Uint8Array(memBuffer);
}
function getEncoder() { return encoder || (encoder = device.createCommandEncoder()); }
function endPass() { if (pass) { endOcclusionInPass(); pass.end(); pass = null; passKey = ''; applied = {}; passSt = newPassSt(); } }
// What the current pass already has set: the same call twice is skipped (every WebGPU call costs a few microseconds of validation + serialisation)
function newPassSt() { return { pl: null, bg0: null, bg1: null, dyn0: -2, dyn1: -2, vb: [], vbo: [], ib: null, ibFmt: '', ibOff: -1, vp: null, sc: null, bf: null, sref: -1 }; }
let passSt = newPassSt();
// Buffer/texture objects referenced by commands recorded since the last submit carry `enc === encEpoch`; flush() starts a new epoch. (A Set cost a hash insert for every bound
// buffer of every draw; nothing iterates it.)
let encEpoch = 1, submits = 0, submits0 = 0;
const usedInEncoder = { add(o) { if (o) o.enc = encEpoch; }, has(o) { return !!o && o.enc === encEpoch; }, clear() { encEpoch++; } };
const flushReasons = new Map(); // submits per reason since the last fps line
function flush(reason) {
const tf0 = performance.now();
if (encoder || pass) { const r = reason || 'other'; flushReasons.set(r, (flushReasons.get(r) || 0) + 1); }
endPass();
if (dirtyChunks.size) flushUniforms();
const resolves = pendingResolve.length ? recordResolves() : null; // into this encoder: no command buffer of its own
if (encoder) { device.queue.submit([encoder.finish()]); encoder = null; onSubmitted(); submits++; counterSlot = 0; }
if (resolves) mapResolves(resolves);
fr.flush += performance.now() - tf0;
usedInEncoder.clear();
}
function logOnce(key, msg) { if (!logOnce.seen) logOnce.seen = new Set(); if (logOnce.seen.has(key)) return; logOnce.seen.add(key); logLine(msg); }
// ---- D3D11 enums -> WebGPU ----------------------------------------------------------------------------------------------
const CMP = [null, 'never', 'less', 'equal', 'less-equal', 'greater', 'not-equal', 'greater-equal', 'always'];
const STENCIL_OP = [null, 'keep', 'zero', 'replace', 'increment-clamp', 'decrement-clamp', 'invert', 'increment-wrap', 'decrement-wrap'];
const BLEND_FACTOR = [null, 'zero', 'one', 'src', 'one-minus-src', 'src-alpha', 'one-minus-src-alpha', 'dst-alpha', 'one-minus-dst-alpha',
'dst', 'one-minus-dst', 'src-alpha-saturated', null, null, 'constant', 'one-minus-constant', 'src1', 'one-minus-src1', 'src1-alpha', 'one-minus-src1-alpha'];
const BLEND_OP = [null, 'add', 'subtract', 'reverse-subtract', 'min', 'max'];
const TOPOLOGY = { 1: ['point-list', false], 2: ['line-list', false], 3: ['line-strip', true], 4: ['triangle-list', false], 5: ['triangle-strip', true] };
const ADDRESS = [null, 'repeat', 'mirror-repeat', 'clamp-to-edge', 'clamp-to-edge', 'mirror-repeat'];
function vertexFormat(dxgi) {
switch (dxgi) {
case 2: return ['float32x4', 16, 'f']; case 6: return ['float32x3', 12, 'f']; case 16: return ['float32x2', 8, 'f']; case 41: return ['float32', 4, 'f'];
case 3: return ['uint32x4', 16, 'u']; case 7: return ['uint32x3', 12, 'u']; case 17: return ['uint32x2', 8, 'u']; case 42: return ['uint32', 4, 'u'];
case 4: return ['sint32x4', 16, 'i']; case 8: return ['sint32x3', 12, 'i']; case 18: return ['sint32x2', 8, 'i']; case 43: return ['sint32', 4, 'i'];
case 10: return ['float16x4', 8, 'f']; case 34: return ['float16x2', 4, 'f'];
case 11: return ['unorm16x4', 8, 'f']; case 35: return ['unorm16x2', 4, 'f'];
case 12: return ['uint16x4', 8, 'u']; case 36: return ['uint16x2', 4, 'u'];
case 13: return ['snorm16x4', 8, 'f']; case 37: return ['snorm16x2', 4, 'f'];
case 14: return ['sint16x4', 8, 'i']; case 38: return ['sint16x2', 4, 'i'];
case 28: return ['unorm8x4', 4, 'f']; case 30: return ['uint8x4', 4, 'u']; case 31: return ['snorm8x4', 4, 'f']; case 32: return ['sint8x4', 4, 'i'];
case 49: return ['unorm8x2', 2, 'f']; case 50: return ['uint8x2', 2, 'u']; case 51: return ['snorm8x2', 2, 'f']; case 52: return ['sint8x2', 2, 'i'];
case 87: return ['unorm8x4-bgra', 4, 'f']; case 24: return ['unorm10-10-10-2', 4, 'f'];
default: return null;
}
}
// D3D11_FILTER: bits 4-5 min, 2-3 mag, 0-1 mip (bit set = linear); 0x80 = comparison; 0x55 with aniso.
function makeSampler(words) {
if ([1, 2, 3].some((i) => words[i] === 4)) noteApprox('sampler border addressing', 'border colour ' + [7, 8, 9, 10].map((i) => wf(words[i]).toFixed(2)).join(',') + (words[0] & 0x80 ? ' (comparison)' : ''));
if (wf(words[4]) !== 0) noteApprox('sampler MipLODBias', String(wf(words[4]).toFixed(2)));
// D3D11_SAMPLER_DESC: Filter, AddressU, AddressV, AddressW, MipLODBias, MaxAnisotropy, ComparisonFunc, BorderColor[4], MinLOD, MaxLOD
const filter = words[0], aniso = ((filter & 0x7f) === 0x55);
const d = {
addressModeU: ADDRESS[words[1]] || 'repeat', addressModeV: ADDRESS[words[2]] || 'repeat', addressModeW: ADDRESS[words[3]] || 'repeat',
magFilter: (filter >> 2) & 1 ? 'linear' : 'nearest', minFilter: (filter >> 4) & 1 ? 'linear' : 'nearest',
mipmapFilter: filter & 1 ? 'linear' : 'nearest',
lodMinClamp: Math.max(0, wf(words[11])), lodMaxClamp: Math.min(32, Math.max(0, wf(words[12]))),
};
if (filter & 0x80) d.compare = CMP[words[6]] || 'always';
if (aniso) { d.magFilter = d.minFilter = d.mipmapFilter = 'linear'; d.maxAnisotropy = Math.max(1, Math.min(16, words[5])); }
if (d.lodMaxClamp < d.lodMinClamp) d.lodMaxClamp = d.lodMinClamp;
return device.createSampler(d);
}
const wfBuf = new Float32Array(1), wuBuf = new Uint32Array(wfBuf.buffer);
function wf(w) { wuBuf[0] = w; return wfBuf[0]; }
// ---- WGSL reflection (the translator emits plain declarations) ----------------------------------------------------------------
const DECL = /@group\((\d+)u?\)\s*@binding\((\d+)u?\)\s*var(?:<([^>]*)>)?\s+(\w+)\s*:\s*([^;]+);/g;
function parseWgsl(text) {
const bindings = [];
for (const m of text.matchAll(DECL)) {
const group = +m[1], binding = +m[2], space = (m[3] || '').replace(/\s+/g, ''), type = m[5].trim();
const b = { group, binding, name: m[4], type };
if (space === 'uniform') b.kind = 'uniform';
else if (space.startsWith('storage')) b.kind = space.includes('read_write') ? 'storage' : 'read-only-storage';
else if (type === 'sampler') b.kind = 'sampler';
else if (type === 'sampler_comparison') b.kind = 'sampler-comparison';
else {
b.kind = 'texture';
const t = /^texture_(\w+?)(?:<([^>]*)>)?$/.exec(type);
const name = t ? t[1] : '2d', arg = t && t[2] ? t[2] : 'f32';
if (name.startsWith('storage_')) { b.kind = 'storage-texture'; b.dim = name.replace('storage_', ''); b.format = arg.split(',')[0].trim(); b.access = arg.includes('read_write') ? 'read-write' : (arg.includes('read') ? 'read-only' : 'write-only'); }
else if (name.startsWith('depth_')) { b.dim = name.replace('depth_', ''); b.sample = 'depth'; }
else if (name.startsWith('multisampled_')) { b.dim = '2d'; b.ms = true; b.sample = 'unfilterable-float'; }
else { b.dim = name; b.sample = arg === 'u32' ? 'uint' : arg === 'i32' ? 'sint' : 'float'; }
if (b.dim === 'depth_2d') b.dim = '2d';
b.dim = ({ '2d': '2d', '2d_array': '2d-array', '3d': '3d', 'cube': 'cube', 'cube_array': 'cube-array', '1d': '1d' })[b.dim] || b.dim;
}
bindings.push(b);
}
// fragment outputs: locations of the return value of `fn main`
const outputs = [];
const sig = /@fragment\s*fn\s+main\s*\((?:[^()]|\([^)]*\))*\)\s*->\s*([^{]+)\{/.exec(text); // parameters contain @location(n)
if (sig) {
const r = sig[1];
const inl = /@location\((\d+)u?\)/.exec(r);
if (inl) outputs.push(+inl[1]);
else {
const st = new RegExp('struct\\s+' + r.trim() + '\\s*\\{([^}]*)\\}').exec(text);
if (st) for (const m of st[1].matchAll(/@location\((\d+)u?\)/g)) outputs.push(+m[1]);
}
}
return { bindings, outputs };
}
const STAGE_VIS = { 0: 1 /*VERTEX*/, 1: 2 /*FRAGMENT*/, 2: 4 /*COMPUTE*/ };
function layoutEntry(b, stage) {
const visibility = STAGE_VIS[stage];
const e = { binding: b.binding, visibility };
switch (b.kind) {
case 'uniform': e.buffer = { type: 'uniform', hasDynamicOffset: b.binding !== 15 }; break; // binding 0 = the packed constant buffers
case 'storage': e.buffer = { type: 'storage' }; break;
case 'read-only-storage': e.buffer = { type: 'read-only-storage' }; break;
case 'sampler': e.sampler = { type: 'filtering' }; break;
case 'sampler-comparison': e.sampler = { type: 'comparison' }; break;
case 'texture': e.texture = { sampleType: b.sample, viewDimension: b.dim, multisampled: !!b.ms }; break;
case 'storage-texture': e.storageTexture = { access: b.access, format: b.format, viewDimension: b.dim }; break;
}
return e;
}
// D3D11 shaders use up to 7 constant buffers per stage, WebGPU allows 10 dynamic-offset uniform bindings per pipeline layout.
// Every stage's constant buffers (all the `var<uniform>` except the special-constant table at binding 15) are therefore
// merged into ONE struct bound at binding 0 with one dynamic offset: `cbN_x` -> `cbpack_v.cbN_x`. The runtime copies the
// bound D3D constant buffers back to back into the uniform ring, in binding order, one 16-byte aligned block per member.
function packUniforms(text) {
const decl = /@group\((\d+)u?\)\s*@binding\((\d+)u?\)\s*var<uniform>\s+(\w+)\s*:\s*(\w+)\s*;/g;
const members = [];
let group = 0;
let out = text.replace(decl, (m, g, b, name, type) => {
if (+b === 15) return m;
group = +g;
const st = new RegExp('struct\\s+' + type + '\\s*\\{([^}]*)\\}').exec(text);
const arr = st ? /array<[^>]*>,\s*(\d+)u?>/.exec(st[1]) : null;
members.push({ binding: +b, name, type, size: arr ? +arr[1] * 16 : 16, offset: 0 });
return '';
});
if (!members.length) return { text, pack: null };
members.sort((a, b) => a.binding - b.binding);
let off = 0;
for (const m of members) { m.offset = off; off += m.size; }
for (const m of members) out = out.replace(new RegExp('\\b' + m.name + '\\b', 'g'), 'cbpack_v.' + m.name);
const struct = 'struct cbpack_t {\n' + members.map((m) => ' ' + m.name + ' : ' + m.type + ',').join('\n') + '\n}\n@group(' + group + 'u) @binding(0u) var<uniform> cbpack_v : cbpack_t;\n';
// the struct must follow the member type definitions: insert before the first function / entry point
const at = out.search(/\n(?:@\w+|fn)\s/);
out = at < 0 ? out + struct : out.slice(0, at) + '\n' + struct + out.slice(at);
return { text: out, pack: { members, size: off, group } };
}
// ---- shaders ------------------------------------------------------------------------------------------------------------------
let shaderIndex = null; // fnv1a64 hex -> { stage, effect, program, ok, consts }
let shaderDir = null;
let shaderUrl = './shaders'; // browser: base URL of the translated shaders (index.json, <hash>.wgsl, <hash>.consts.json)
function loadShaderIndex() {
if (shaderIndex) return;
shaderIndex = {};
shaderDir = isNode ? (process.env.WGPU_SHADER_DIR || 'D:/wasm_build/web/shaders') : shaderUrl;
if (isNode) {
try { shaderIndex = JSON.parse(require('fs').readFileSync(shaderDir + '/index.json', 'utf8')); }
catch (e) { logLine('no shader index at ' + shaderDir + ': ' + e.message); }
}
}
// Browser: fetch is asynchronous, so the sources of the shaders a draw/dispatch needs are awaited before it is executed.
// An overloaded host answers 503/500/429 (2026-10-06): a shader pack that came back as an error left every draw that needs it unshaded for good. Such an answer, or none, is retried up
// to 8 times with growing, jittered waits (~20 s in all).
async function fetchRetry(url, init) {
for (let attempt = 0; ; attempt++) {
let res = null, err = null;
try { res = await fetch(url, init); } catch (e) { err = e; }
if (res && res.status < 500 && res.status !== 429) return res;
if (attempt >= 7) { if (res) return res; throw err; }
await new Promise((r) => setTimeout(r, Math.min(5000, 300 * 2 ** attempt) * (0.5 + Math.random())));
}
}
async function fetchText(url) { const r = await fetchRetry(url); return r.ok ? r.text() : null; }
// true when the shaders bound now need nothing fetched (always, once they have been seen): draws and dispatches then skip `await ensureShaderSources()`, which suspended the
// whole command loop for a microtask turn on every draw
function shaderSourcesReady() {
if (!shaderIndex) return false;
for (let i = 0; i < 3; i++) { const id = S.shader[i]; if (id) { const o = objs.get(id); if (o && o.fetched === undefined) return false; } }
return true;
}
// The index is loaded once (one promise shared by every fetch: shaderIndex is only set when it is complete).
let shaderIndexP = null;
function shaderIndexReady() {
return shaderIndexP || (shaderIndexP = (async () => {
let idx = {};
try { idx = JSON.parse(await fetchText(shaderUrl + '/index.json')); } catch (e) { logLine('no shader index at ' + shaderUrl + ': ' + e.message); }
shaderDir = shaderUrl;
shaderIndex = idx;
})());
}
// Shader packs (phase3/make_shader_pack.py): the translated shaders bundled into ~4 MB files grouped by effect (index.json: "p": [pack, offset, wgsl bytes, constants bytes]), so a
// browser makes ~20 requests instead of one or two per shader (~6,000 in a session, each a round trip: minutes against a distant host). A pack is dropped from memory 8 s after
// its last use (the engine creates a burst of shaders per effect; ~70 MB if all stayed); a shader asked for later refetches its pack, or falls back to its own files.
const packs = new Map(), fetchedByHash = new Map(); // pack number -> { p: Promise<Uint8Array | null>, last }; shader hash -> { text, consts } (shared by shader objects with the same hash)
let packDecoder = null, packSweep = 0;
function packBytes(k) {
let c = packs.get(k);
if (!c) {
c = { p: null, last: 0, done: false };
c.p = fetchRetry(shaderUrl + '/' + (shaderIndex._packs && shaderIndex._packs[k] ? shaderIndex._packs[k].file : 'pack' + k + '.bin')).then((r) => (r.ok ? r.arrayBuffer() : null)).then((b) => (b ? new Uint8Array(b) : null)).catch(() => null).then((u8) => { c.done = true; c.last = performance.now(); return u8; });
packs.set(k, c);
if (!packSweep) packSweep = setInterval(() => { const now = performance.now(); for (const [n, x] of packs) if (x.done && now - x.last > 8000) packs.delete(n); }, 4000);
}
c.last = performance.now();
return c.p;
}
async function packShader(rec) {
const [k, off, wl, cl] = rec.p;
const u8 = await packBytes(k);
if (!u8 || u8.length < off + wl + cl) return null;
packs.get(k) && (packs.get(k).last = performance.now());
const dec = packDecoder || (packDecoder = new TextDecoder());
return { text: dec.decode(u8.subarray(off, off + wl)), consts: cl ? JSON.parse(dec.decode(u8.subarray(off + wl, off + wl + cl))) : null };
}
// A shader's sources (WGSL, constant table); sh.fetched is set only when they are here (null: none). Started at most once per shader.
function fetchShaderSource(sh) {
if (!sh.fetchP) sh.fetchP = (async () => {
await shaderIndexReady();
const rec = shaderIndex[sh.hash];
let fetched = null;
if (rec && rec.ok) {
fetched = fetchedByHash.get(sh.hash) || null;
if (!fetched && rec.p && !(!isNode && new URLSearchParams(self.location.search).get('nopack') === '1')) { // ?nopack=1 (diagnostics): one request per shader file
try { fetched = await packShader(rec); } catch (e) { logOnce('packfail' + rec.p[0], 'shader pack ' + rec.p[0] + ' unusable (' + (e && e.message) + '): fetching shaders one by one'); }
}
if (!fetched) {
try {
const text = await fetchText(shaderDir + '/' + sh.hash + '.wgsl');
let consts = null;
if (rec.consts) consts = JSON.parse(await fetchText(shaderDir + '/' + sh.hash + '.consts.json'));
fetched = { text, consts };
} catch (e) { logOnce('shaderfetch' + sh.hash, 'shader ' + sh.hash + ' could not be fetched: ' + (e && e.message)); }
}
if (fetched) fetchedByHash.set(sh.hash, fetched);
}
sh.fetched = fetched;
shadersReady++;
})();
return sh.fetchP;
}
// Prefetch: effects create their shaders long before the first draw that uses them, so the source is requested at creation (CREATE_SHADER) instead of by that draw,
// which suspended the command loop for a whole request per new shader (a network round trip each for remote players, hundreds in the first frames). At most
// SHADER_FETCHES are in flight; a draw that needs a shader not started yet starts it at once (ensureShaderSources).
const SHADER_FETCHES = 16, shaderQueue = [];
let shaderQueueHead = 0, shaderFetching = 0;
function pumpShaderQueue() {
while (shaderFetching < SHADER_FETCHES && shaderQueueHead < shaderQueue.length) {
const sh = shaderQueue[shaderQueueHead++];
if (sh.fetchP) continue;
shaderFetching++;
fetchShaderSource(sh).finally(() => { shaderFetching--; pumpShaderQueue(); });
}
if (shaderQueueHead === shaderQueue.length) { shaderQueue.length = 0; shaderQueueHead = 0; }
}
function prefetchShader(sh) {
if (isNode || sh.fetchP) return;
shaderQueue.push(sh);
pumpShaderQueue();
}
async function ensureShaderSources() {
if (isNode) return;
if (!shaderIndex) await shaderIndexReady();
for (const id of S.shader) {
const sh = id ? objs.get(id) : null;
if (!sh || sh.fetched !== undefined) continue;
await fetchShaderSource(sh);
}
}
function a2cModule(prog) {
if (prog.a2c !== undefined) return prog.a2c;
prog.a2c = null;
const re = /(\n\s*main_inner\([^;]*\);)/;
if (re.test(prog.text)) {
try { prog.a2c = device.createShaderModule({ code: prog.text.replace(re, '$1\n if (o0.w < 0.5f) { discard; }') }); } catch (e) { logOnce('a2c' + prog.label, 'alpha-to-coverage variant of ' + prog.label + ' failed: ' + e.message); }
} else logOnce('a2c' + prog.label, 'alpha-to-coverage: no main_inner call to patch in ' + prog.label);
return prog.a2c;
}
function shaderProgram(sh) {
// lazily load + compile the WGSL of a shader object; returns { module, info, consts } or null
if (sh.program !== undefined) return sh.program;
sh.program = null;
loadShaderIndex();
const rec = shaderIndex[sh.hash];
if (!rec || !rec.ok) { logOnce('noshader' + sh.hash, 'no WGSL for ' + STAGE_NAME[sh.stage] + ' shader ' + sh.hash + (rec ? ' (' + rec.effect + '::' + rec.program + ': ' + (rec.error || rec.stage_failed || 'untranslated') + ')' : ' (not in the index)')); return null; }
const fs = isNode ? require('fs') : null;
const source = isNode ? fs.readFileSync(shaderDir + '/' + sh.hash + '.wgsl', 'utf8') : (sh.fetched && sh.fetched.text);
if (!source) { logOnce('nosrc' + sh.hash, 'shader source of ' + sh.hash + ' was not fetched'); return null; }
const { text, pack } = packUniforms(source);
shaderModuleCount++;
const tm0 = performance.now();
const module = device.createShaderModule({ code: text, label: rec.effect + '::' + rec.program });
fr.mod += performance.now() - tm0;
const info = parseWgsl(text);
let consts = null;
if (rec.consts) {
const arr = isNode ? JSON.parse(fs.readFileSync(shaderDir + '/' + sh.hash + '.consts.json', 'utf8')) : sh.fetched.consts;
const n = Math.max(1, arr.length);
const data = new Uint32Array(n * 4);
arr.forEach((v, i) => { data[i * 4] = v >>> 0; });
consts = device.createBuffer({ size: Math.max(16, data.byteLength), usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(consts, 0, data);
}
const flat = [];
for (const m of text.matchAll(/@location\((\d+)u?\)\s*@interpolate\(flat[^)]*\)/g)) flat.push(+m[1]);
const groupBindings = {};
for (const b of info.bindings) (groupBindings[b.group] || (groupBindings[b.group] = [])).push(b);
for (const g in groupBindings) groupBindings[g].sort((a, b) => a.binding - b.binding);
// vid/iid: the shader reads vertex_index / instance_index (doDraw folds the draw's start into the buffer offsets only for those: 6 and 93 of ~4,800 shaders)
sh.program = { module, info, consts, pack, text, flat, groupBindings, variants: new Map(), bgls: [], label: rec.effect + '::' + rec.program, vid: /\bvertex_index\b/.test(text), iid: /\binstance_index\b/.test(text) };
return sh.program;
}
const STAGE_NAME = ['vs', 'ps', 'cs'];
async function initDevice(msg) {
if (isNode) {
const modPath = process.env.WGPU_NODE_MODULE || 'D:/wasm_build/shaders/node/node_modules/webgpu';
const dawn = require(modPath);
Object.assign(globalThis, dawn.globals);
// FXC (the shader compiler of Dawn's D3D12 backend in this npm build; it has no DXC) rejects several translated compute kernels
// with X3694/X3695 "race condition" errors that come from its optimiser. emit_hlsl_debug_symbols compiles without optimisation
// (the driver still optimises). Chrome uses DXC where available, so this is a dev-loop workaround only.
gpu = dawn.create(process.env.WGPU_DAWN_FLAGS ? process.env.WGPU_DAWN_FLAGS.split(',') : ['enable-dawn-features=emit_hlsl_debug_symbols']);
} else {
gpu = self.navigator.gpu;
}
if (!gpu) throw new Error('no WebGPU');
adapter = await gpu.requestAdapter({ powerPreference: 'high-performance' });
if (!adapter) throw new Error('no WebGPU adapter');
const want = ['texture-compression-bc', 'texture-compression-bc-sliced-3d', 'depth32float-stencil8', 'float32-filterable', 'indirect-first-instance', 'rg11b10ufloat-renderable', 'bgra8unorm-storage', 'depth-clip-control', 'clip-distances', 'texture-formats-tier1', 'dual-source-blending'];
const requiredFeatures = want.filter((f) => adapter.features.has(f));
const limits = {};
for (const k of ['maxBufferSize', 'maxStorageBufferBindingSize', 'maxUniformBufferBindingSize', 'maxTextureDimension2D', 'maxTextureArrayLayers',
'maxColorAttachmentBytesPerSample', 'maxBindGroups', 'maxStorageBuffersPerShaderStage', 'maxSampledTexturesPerShaderStage', 'maxSamplersPerShaderStage',
'maxStorageTexturesPerShaderStage', 'maxUniformBuffersPerShaderStage', 'maxVertexBuffers', 'maxVertexAttributes', 'maxBindingsPerBindGroup',
'maxInterStageShaderVariables', 'maxDynamicUniformBuffersPerPipelineLayout', 'maxComputeInvocationsPerWorkgroup', 'maxComputeWorkgroupSizeX', 'maxComputeWorkgroupSizeY', 'maxComputeWorkgroupSizeZ', 'maxComputeWorkgroupStorageSize', 'maxComputeWorkgroupsPerDimension'])
if (adapter.limits[k] !== undefined) limits[k] = adapter.limits[k];
// WGPU_LIMITS=name=value,... (Node, test aid): request a smaller device, e.g. a Chromebook's (maxInterStageShaderVariables=16)
if (isNode && process.env.WGPU_LIMITS) for (const kv of process.env.WGPU_LIMITS.split(',')) { const [k, v] = kv.split('='); limits[k] = +v; }
// ?limits=name:value,... (browser): the same, lowered to a weaker device's limits (never below the WebGPU defaults)
if (!isNode) for (const kv of (new URLSearchParams(self.location.search).get('limits') || '').split(',').filter(Boolean)) { const [k, v] = kv.split(':'); limits[k] = +v; }
device = await adapter.requestDevice({ requiredFeatures, requiredLimits: limits });
device.addEventListener('uncapturederror', (ev) => {
errorCount++;
Atomics.add(i32, ringW + 4, 1);
const msg = ev.error.message;
const key = msg.replace(/0x[0-9a-f]+|\d+/g, 'N').slice(0, 200);
const e = errorKinds.get(key);
if (e) e.n++;
else { errorKinds.set(key, { n: 1, msg }); if (errorKinds.size <= 40) logLine('validation error: ' + msg.slice(0, 700)); }
});
device.lost.then((info) => {
logLine('device lost: ' + info.reason + ' ' + info.message);
if (!isNode && info.reason !== 'destroyed') new BroadcastChannel('game-progress').postMessage({ gpuLost: info.message.split('\n')[0].slice(0, 160) });
});
logLine('limits: maxDynamicUniformBuffersPerPipelineLayout=' + device.limits.maxDynamicUniformBuffersPerPipelineLayout + ' maxBindGroups=' + device.limits.maxBindGroups + ' maxUniformBuffersPerShaderStage=' + device.limits.maxUniformBuffersPerShaderStage + ' maxVertexBuffers=' + device.limits.maxVertexBuffers +
' maxInterStageShaderVariables=' + device.limits.maxInterStageShaderVariables + ' maxSampledTexturesPerShaderStage=' + device.limits.maxSampledTexturesPerShaderStage + ' maxColorAttachmentBytesPerSample=' + device.limits.maxColorAttachmentBytesPerSample);
logLine('device ready: ' + (adapter.info ? (adapter.info.description || adapter.info.vendor) : 'adapter') + ', features ' + requiredFeatures.join(','));
if (msg.canvas) {
canvas = msg.canvas;
canvasCtx = canvas.getContext('webgpu');
canvasFormat = gpu.getPreferredCanvasFormat();
}
}
// ---- objects ----------------------------------------------------------------------------------------------------------------
// Rough GPU memory of a texture (all mips, all layers, samples), for the exit statistics: block-compressed formats by block size, others by texel size.
function textureBytes(desc) {
const f = desc.format;
let texel = 4;
const block = /^(bc1|bc4|etc2-rgb8|eac-r11)/.test(f) ? 8 : /^(bc|astc|etc2|eac)/.test(f) ? 16 : 0;
if (!block) texel = /r8|a8/.test(f) && !/rg8|rgba8|bgra8/.test(f) ? 1 : /rg8|r16|depth16/.test(f) ? 2 : /rgba16|rg32/.test(f) ? 8 : /rgba32/.test(f) ? 16 : 4;
let total = 0;
for (let m = 0; m < desc.mipLevelCount; m++) {
const mw = Math.max(1, desc.size.width >> m), mh = Math.max(1, desc.size.height >> m), md = desc.dimension === '3d' ? Math.max(1, desc.size.depthOrArrayLayers >> m) : desc.size.depthOrArrayLayers;
total += (block ? Math.ceil(mw / 4) * Math.ceil(mh / 4) * block : mw * mh * texel) * md;
}
return total * (desc.sampleCount || 1);
}
// Stutter diagnostics: what this worker spent time on since the last present; frames over SPIKE_MS are listed in the 10 s log line with the breakdown
const fr = { idle: 0, pipe: 0, mod: 0, up: 0, upBytes: 0, wait: 0, flush: 0 };
const SPIKE_MS = 40;
let lastPresentT = 0, spikes = [], spikeCount = 0, frameMax = 0, over25 = 0;
let pipeMs = 0, pipeCreates = 0, pipeCreates0 = 0, busyMs = 0, idleMs = 0, shaderModuleCount = 0, fpsT0 = 0, fpsCount0 = 0, fpsDraw0 = 0;
let texBytesAlive = 0, texBytesPeak = 0, texCount = 0, bufBytesAlive = 0, bufBytesPeak = 0;
function createTexture(id, dim, w, h, d, mips, arraySize, dxgi, bind, samples) {
const m = mapFormat(dxgi, bind);
let usage = GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST;
if (bind & BIND_SRV) usage |= GPUTextureUsage.TEXTURE_BINDING;
if (bind & (BIND_RT | BIND_DS)) usage |= GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
if (bind & BIND_UAV) usage |= GPUTextureUsage.STORAGE_BINDING;
if (!(bind & (BIND_SRV | BIND_RT | BIND_DS | BIND_UAV))) usage |= GPUTextureUsage.TEXTURE_BINDING;
const desc = {
size: { width: w, height: dim === 1 ? 1 : h, depthOrArrayLayers: dim === 3 ? d : arraySize },
mipLevelCount: mips, sampleCount: samples, dimension: dim === 3 ? '3d' : (dim === 1 ? '1d' : '2d'),
format: m.format, usage,
};
if (samples > 1) desc.usage &= ~(GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.STORAGE_BINDING);
if (m.views) desc.viewFormats = m.views.filter((v) => !v.startsWith('bc') || device.features.has('texture-compression-bc'));
const tex = device.createTexture(desc);
const bytes = textureBytes(desc);
texBytesAlive += bytes; texCount++;
if (texBytesAlive > texBytesPeak) texBytesPeak = texBytesAlive;
objs.set(id, { kind: 'texture', tex, desc, format: m.format, views: new Map(), a8: !!m.a8, bytes });
}
// Exit report: where the GPU-side memory of the mirror goes (diagnostics for the 4 GB target).
function dumpMemBreakdown() {
const bufs = [], byFmt = new Map();
let nBuf = 0, shadow = 0;
for (const o of objs.values()) {
if (o.kind === 'buffer') { nBuf++; if (o.shadow) shadow += o.shadow.byteLength; if (o.gpu) bufs.push(o); }
else if (o.kind === 'texture' && o.bytes) { const k = o.desc.format + (o.desc.dimension === '3d' ? ' 3d' : '') + (o.desc.sampleCount > 1 ? ' msaa' : '') + ((o.desc.usage & GPUTextureUsage.RENDER_ATTACHMENT) ? ' rt' : ''); const e = byFmt.get(k) || [0, 0]; e[0] += o.bytes; e[1]++; byFmt.set(k, e); }
}
bufs.sort((a, b) => b.bytes - a.bytes);
logLine('mem: ' + nBuf + ' buffers (' + bufs.length + ' with GPU storage), constant-buffer shadows ' + (shadow / 1048576 | 0) + ' MB, pipelines ' + pipelineCount + ', uniform ring ' + (ubo.shared.length * FRAME_SLOTS * RING_CHUNK / 1048576 | 0) + ' MB, shader modules ' + shaderModuleCount);
logLine('mem: biggest buffers: ' + bufs.slice(0, 8).map((o) => (o.bytes / 1048576).toFixed(1) + 'MB bind=' + o.bind.toString(16) + (o.stride ? ' stride=' + o.stride : '')).join(' | '));
logLine('mem: textures by format: ' + [...byFmt].sort((a, b) => b[1][0] - a[1][0]).slice(0, 10).map(([k, [b, n]]) => k + ' ' + (b / 1048576 | 0) + 'MB x' + n).join(' | '));
}
function createBuffer(id, size, bind, misc, stride) {
const o = { kind: 'buffer', uid: id, size, bind, misc, stride, gpu: null, shadow: null, version: 0 };
if (bind & 4) { o.shadow = new Uint8Array(size); } // constant buffers live in this shadow and are copied to the uniform ring per draw
let usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
if (bind & 1) usage |= GPUBufferUsage.VERTEX;
if (bind & 2) usage |= GPUBufferUsage.INDEX;
if (bind & (8 | 0x80)) usage |= GPUBufferUsage.STORAGE;
if (misc & 0x10) usage |= GPUBufferUsage.INDIRECT; // D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS
if ((bind & (1 | 2 | 8 | 0x80)) || (misc & 0x10)) {
o.bytes = Math.max(4, (size + 3) & ~3); // the GPU buffer's size (per-draw code reads this: GPUBuffer.size is a native getter, ~5 % of the worker read per draw)
o.gpu = device.createBuffer({ size: o.bytes, usage });
bufBytesAlive += o.bytes;
if (bufBytesAlive > bufBytesPeak) bufBytesPeak = bufBytesAlive;
}
objs.set(id, o);
}
function uploadBuffer(id, offset, size, dataOff, noOverwrite) {
const o = objs.get(id);
if (!o) return;
if (o.shadow) { o.shadow.set(u8.subarray(dataOff, dataOff + size), offset); o.version++; }
if (debugDraw && o.gpu && !o.shadow) { if (!o.dbg) o.dbg = new Uint8Array(o.size); o.dbg.set(u8.subarray(dataOff, dataOff + size), offset); }
if (o.gpu) {
if (!noOverwrite && usedInEncoder.has(o)) flush('buffer upload'); // a recorded draw still reads the old contents: D3D semantics need them (not after a no-overwrite map: the app promises those bytes are unused)
const n = size & ~3;
if (n) device.queue.writeBuffer(o.gpu, offset, u8, dataOff, n);
if (size & 3) { // tail bytes: merge into a padded 4-byte write
const tail = new Uint8Array(4); tail.set(u8.subarray(dataOff + n, dataOff + size));
device.queue.writeBuffer(o.gpu, offset + n, tail);
}
}
}
function uploadTexture(id, mip, slice, x, y, z, w, h, d, bytesPerRow, rowsPerImage, size, dataOff) {
const o = objs.get(id);
if (!o) return;
if (usedInEncoder.has(o)) flush('texture upload');
markColorDirty(o);
if (debugDraw || debugPs) { o.uploads = (o.uploads || 0) + 1; o.lastUpload = 'mip' + mip + ' ' + w + 'x' + h + ' bpr' + bytesPerRow + ' first bytes ' + Array.from(u8.subarray(dataOff, dataOff + 8)).join(','); } // dumpDraw only: a string per upload otherwise
const three = o.desc.dimension === '3d';
const origin = { x, y, z: three ? z : slice };
let data = u8.subarray(dataOff, dataOff + size), bpr = bytesPerRow;
if (o.a8) { // expand one byte per texel to (0,0,0,a)
const out = new Uint8Array(w * h * 4);
for (let r = 0; r < h; r++) for (let c = 0; c < w; c++) out[(r * w + c) * 4 + 3] = data[r * bytesPerRow + c];
data = out; bpr = w * 4;
}
device.queue.writeTexture({ texture: o.tex, mipLevel: mip, origin }, data,
{ bytesPerRow: bpr, rowsPerImage }, { width: w, height: h, depthOrArrayLayers: d });
}
function createShader(p, n) {
const id = u32[p], stage = u32[p + 1], hash = u32[p + 3].toString(16).padStart(8, '0') + u32[p + 2].toString(16).padStart(8, '0');
const nin = u32[p + 4];
const sig = [];
for (let i = 0; i < nin; i++) sig.push({ sem: u32[p + 5 + i * 4], idx: u32[p + 6 + i * 4], reg: u32[p + 7 + i * 4], mask: u32[p + 8 + i * 4] });
const o = { kind: 'shader', stage, hash, sig, program: undefined };
objs.set(id, o);
shadersByHash.set(hash, o);
if (isNode) shadersReady++; // Node reads the sources from disk when needed
prefetchShader(o);
}
function createLayout(p) {
// id, n, then n x [semHash, semIndex, dxgi, slot, offset, slotClass, stepRate]
const id = u32[p], n = u32[p + 1], els = [];
for (let i = 0; i < n; i++) {
const q = p + 2 + i * 7;
els.push({ sem: u32[q], idx: u32[q + 1], fmt: u32[q + 2], slot: u32[q + 3], offset: u32[q + 4], cls: u32[q + 5], rate: u32[q + 6] });
}
objs.set(id, { kind: 'layout', els, slots: layoutSlots(els) });
}
// each vertex buffer slot once, with the step class of its first element (per-draw loops walk this instead of de-duplicating els every time)
function layoutSlots(els) {
const slots = [];
let seen = 0;
for (const e of els) { if (e.slot < 32 && !((seen >>> e.slot) & 1)) { seen |= 1 << e.slot; slots.push({ slot: e.slot, cls: e.cls }); } }
return slots;
}
function createState(p, n) {
const id = u32[p], kind = u32[p + 1], words = Array.from(u32.subarray(p + 2, p + n));
const o = { kind: 'state', skind: kind, words };
if (kind === 3) o.sampler = makeSampler(words);
objs.set(id, o);
}
function createUav(p) {
// id, resKind(0 tex, 1 buf), resId, dxgi, viewDim, a, b, c, flags
objs.set(u32[p], { kind: 'uav', res: u32[p + 2], isBuf: u32[p + 1] === 1, fmt: u32[p + 3], vdim: u32[p + 4], a: u32[p + 5], b: u32[p + 6], c: u32[p + 7], flags: u32[p + 8], views: new Map() });
}
function createSrv(p) {
// id, resKind(0 tex, 1 buf), resId, dxgi, viewDim, a, b, c, d
objs.set(u32[p], { kind: 'srv', res: u32[p + 2], isBuf: u32[p + 1] === 1, fmt: u32[p + 3], vdim: u32[p + 4], a: u32[p + 5], b: u32[p + 6], c: u32[p + 7], d: u32[p + 8], views: new Map() });
}
// ---- pipeline state -----------------------------------------------------------------------------------------------------------
const S = {
shader: [0, 0, 0], layout: 0, topo: 4,
vb: Array.from({ length: 16 }, () => ({ buf: 0, stride: 0, offset: 0 })),
ib: { buf: 0, fmt: 0, offset: 0 },
cb: [0, 1, 2].map(() => Array.from({ length: 16 }, () => ({ buf: 0, first: 0, num: 0 }))),
srv: [0, 1, 2].map(() => new Array(128).fill(0)),
uav: [0, 1, 2].map(() => new Array(8).fill(0)),
smp: [0, 1, 2].map(() => new Array(16).fill(0)),
rt: Array.from({ length: 8 }, () => ({ tex: 0, mip: 0, slice: 0, fmt: 0 })), ds: { tex: 0, mip: 0, slice: 0, fmt: 0, flags: 0 },
blend: { id: 0, factor: [1, 1, 1, 1], mask: 0xffffffff }, dss: { id: 0, ref: 0 }, rs: 0,
viewports: [], scissors: [],
};
let applied = {};
let pdirty = true, lastPl = null, lastPlPass = null; // pipeline memo: any command that changes the pipeline key sets pdirty
function resetState() {
pdirty = true;
S.shader = [0, 0, 0]; S.layout = 0; S.topo = 4;
S.vb.forEach((v) => { v.buf = 0; v.stride = 0; v.offset = 0; });
S.ib = { buf: 0, fmt: 0, offset: 0 };
for (let s = 0; s < 3; s++) { S.cb[s].forEach((c) => { c.buf = 0; c.first = 0; c.num = 0; }); S.srv[s].fill(0); S.smp[s].fill(0); }
S.rt.forEach((r) => { r.tex = 0; r.mip = 0; r.slice = 0; r.fmt = 0; });
S.ds = { tex: 0, mip: 0, slice: 0, fmt: 0, flags: 0 };
rtKey = null; bindEpoch[0]++; bindEpoch[1]++; bindEpoch[2]++; gEpoch++;
S.blend = { id: 0, factor: [1, 1, 1, 1], mask: 0xffffffff }; S.dss = { id: 0, ref: 0 }; S.rs = 0;
S.viewports = []; S.scissors = [];
endPass();
}
function viewFormat(dxgi, tex) { return dxgi ? mapFormat(dxgi, 0).format : tex.format; }
function targetView(t, dxgi, mip, slice, dimension) {
const fmt = viewFormat(dxgi, t);
const key = fmt + '|' + mip + '|' + slice;
let v = t.views.get(key);
if (!v) {
v = t.tex.createView({ format: fmt, baseMipLevel: mip, mipLevelCount: 1, baseArrayLayer: slice, arrayLayerCount: 1, dimension: t.desc.dimension === '3d' ? '3d' : '2d' });
t.views.set(key, v);
}
return { view: v, format: fmt };
}
// ---- uniform ring: constant buffers are copied per draw (D3D11 semantics) and bound with dynamic offsets ----------------------------------
const RING_CHUNK = 8 << 20, UBO_WINDOW = 65536;
// Frames in flight: the uniform ring has one slot per frame; before a slot is reused the GPU must be done with the frame that last used it. Chrome reports completion through the
// wire from the GPU process, so with 3 slots (2 frames in flight) the round trip, not the work, capped the frame rate.
const FRAME_SLOTS = (isNode ? +process.env.WGPU_SLOTS : +(new URLSearchParams(self.location.search).get('slots') || 0)) || 3;
// Chunk k of every frame slot is a RING_CHUNK region of one GPU buffer (`shared[k]`, FRAME_SLOTS regions): slot s's chunk object has the buffer, its id and base s * RING_CHUNK, and keeps
// its own mirror and dirty range. Bind groups name the buffer (and the id is part of their cache key), so one bind group serves every frame slot; with a buffer per slot and chunk each
// set of resources needed a bind group per frame in flight (three times the creations and the cache entries). Dynamic offsets are base + offset.
const ubo = { slots: Array.from({ length: FRAME_SLOTS }, () => []), shared: [], slot: 0, chunk: 0, off: 0, pending: Array(FRAME_SLOTS).fill(null), nextId: 1 };
// returns the offset of `size` bytes in the current ring chunk (from the chunk's base), which it leaves in allocChunk (no result object: this runs for every pack)
let allocChunk = null;
function uniformAlloc(size) {
size = (size + 255) & ~255;
let list = ubo.slots[ubo.slot];
let c = list[ubo.chunk];
if (!c || ubo.off + size + UBO_WINDOW > RING_CHUNK) {
if (c) { ubo.chunk++; ubo.off = 0; c = list[ubo.chunk]; }
if (!c) {
const sh = ubo.shared[ubo.chunk] || (ubo.shared[ubo.chunk] = { buf: device.createBuffer({ size: RING_CHUNK * FRAME_SLOTS, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }), id: ubo.nextId++ });
c = { buf: sh.buf, id: sh.id, base: ubo.slot * RING_CHUNK };
list[ubo.chunk] = c;
}
}
const off = ubo.off; ubo.off += size;
allocChunk = c;
return off;
}
// The per-draw constant copies go into a JS mirror of the ring chunk and reach the GPU with one writeBuffer per chunk at the next flush (a queue.writeBuffer per draw was ~10 %
// of the worker's time at 1,700 draws per frame). Commands only execute at submit, which flush() does after these writes, so the order is the same.
const dirtyChunks = new Set();
// marks [off, off + n) of the chunk's mirror as written and returns the mirror (grown if needed); the caller writes the bytes into it directly
function stageReserve(chunk, off, n) {
const end = off + n;
if (!chunk.stage || chunk.stage.length < end) {
const grown = new Uint8Array(Math.min(RING_CHUNK, Math.max(end, chunk.stage ? chunk.stage.length * 2 : 262144)));
if (chunk.stage) grown.set(chunk.stage.subarray(0, chunk.top));
chunk.stage = grown;
chunk.top = chunk.top || 0;
}
if (end > (chunk.top || 0)) chunk.top = end;
if (chunk.lo === undefined || off < chunk.lo) chunk.lo = off;
if (end > (chunk.hi || 0)) chunk.hi = end;
dirtyChunks.add(chunk);
return chunk.stage;
}
function flushUniforms() {
for (const c of dirtyChunks) {
const hi = Math.min(c.stage.length, (c.hi + 3) & ~3);
device.queue.writeBuffer(c.buf, c.base + c.lo, c.stage, c.lo, hi - c.lo);
c.lo = undefined; c.hi = 0;
}
dirtyChunks.clear();
}
const curObjs = [], curVers = [], curFirsts = []; // scratch: identity of the constant buffers a program's pack reads right now
function packCopy(prog, stage) {
const pack = prog.pack;
if (!pack) return null;
if (pack.size > UBO_WINDOW) { logOnce('packbig' + prog.label, prog.label + ': constant buffers total ' + pack.size + ' bytes (> 64 KB uniform binding limit)'); return null; }
const members = pack.members, n = members.length, cbs = S.cb[stage];
for (let i = 0; i < n; i++) {
const c = cbs[members[i].binding], o = c.buf ? objs.get(c.buf) : null;
curObjs[i] = o; curVers[i] = o ? o.version : -1; curFirsts[i] = c.first;
}
// the same constants already packed this frame (by this program and stage): reuse that ring allocation. No string keys: the last few packs are compared member by member.
const ents = prog.packEnts || (prog.packEnts = [[], [], []]), list = ents[stage];
for (let k = 0; k < list.length; k++) {
const en = list[k];
if (en.frame !== ubo.frame) continue;
let same = true;
for (let i = 0; i < n; i++) if (en.objs[i] !== curObjs[i] || en.vers[i] !== curVers[i] || en.firsts[i] !== curFirsts[i]) { same = false; break; }
if (same) return en.alloc;
}
// members are contiguous (packUniforms): each is copied straight into the ring mirror, and only the part a constant buffer does not cover is zeroed (D3D reads zeros there)
const aOff = uniformAlloc(pack.size), aChunk = allocChunk;
const dst = stageReserve(aChunk, aOff, pack.size);
for (let i = 0; i < n; i++) {
const m = members[i], o = curObjs[i], at = aOff + m.offset;
let len = 0;
if (o && o.shadow) {
const from = curFirsts[i] * 16;
len = Math.min(m.size, o.size - from);
if (len > 0) {
// the whole shadow, or one cached view of it: a new subarray per member and draw was garbage
let src;
if (from === 0 && len === o.shadow.length) src = o.shadow;
else if (o.svFrom === from && o.svLen === len) src = o.sv;
else { src = o.sv = o.shadow.subarray(from, from + len); o.svFrom = from; o.svLen = len; }
dst.set(src, at);
} else len = 0;
}
if (len < m.size) dst.fill(0, at + len, at + m.size);
}
// the oldest entry of this (program, stage) is recycled in place: a pack per draw made ~6 short-lived objects each, which the garbage collector showed up for (~14 % of the worker)
let en;
if (list.length >= 6) {
en = list.shift();
for (let i = 0; i < n; i++) { en.objs[i] = curObjs[i]; en.vers[i] = curVers[i]; en.firsts[i] = curFirsts[i]; }
en.alloc.chunk = aChunk; en.alloc.off = aOff; en.alloc.frame = ubo.frame;
} else en = { frame: 0, objs: curObjs.slice(0, n), vers: curVers.slice(0, n), firsts: curFirsts.slice(0, n), alloc: { chunk: aChunk, off: aOff, frame: ubo.frame } };
en.frame = ubo.frame;
list.push(en);
return en.alloc;
}
ubo.frame = 0;
// ---- dummies for unbound slots ------------------------------------------------------------------------------------------------------------
const dummy = { tex: new Map(), smp: null, cmp: null, buf: null, uni: null };
function dummyTexture(b) {
const key = b.dim + '|' + b.sample + '|' + (b.ms ? 'ms' : '');
let v = dummy.tex.get(key);
if (v) return v;
const format = b.sample === 'uint' ? 'rgba8uint' : b.sample === 'sint' ? 'rgba8sint' : b.sample === 'depth' ? 'depth32float' : 'rgba8unorm';
const layers = (b.dim === 'cube' || b.dim === 'cube-array') ? 6 * (b.dim === 'cube-array' ? 1 : 1) : 1;
const tex = device.createTexture({ size: { width: 1, height: b.dim === '1d' ? 1 : 1, depthOrArrayLayers: b.dim === '3d' ? 1 : layers },
dimension: b.dim === '3d' ? '3d' : b.dim === '1d' ? '1d' : '2d', format, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, sampleCount: b.ms ? 4 : 1 });
v = tex.createView({ dimension: b.dim });
dummy.tex.set(key, v);
return v;
}
function dummySampler(cmp) {
if (cmp) return dummy.cmp || (dummy.cmp = device.createSampler({ compare: 'always' }));
return dummy.smp || (dummy.smp = device.createSampler({}));
}
// one per binding: writable storage buffers bound twice in a dispatch are an aliasing error
function dummyBuffer(binding) {
if (!dummy.bufs) dummy.bufs = new Map();
let b = dummy.bufs.get(binding);
if (!b) { b = device.createBuffer({ size: 65536, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); dummy.bufs.set(binding, b); }
return b;
}
function srvView(srv, b) {
// GPUTextureView of an SRV in the dimension the shader declares
let t = objs.get(srv.res);
if (!t || t.kind !== 'texture') return null;
let proxied = false;
if (isDepth(t.format) && b.sample !== 'depth') {
t = depthProxy(t, b.sample === 'uint' ? 'stencil' : 'depth');
proxied = true; sawProxy = true;
} else if (!isDepth(t.format) && b.sample === 'depth') {
t = floatToDepthProxy(t);
proxied = true; sawProxy = true;
}
const key = b.dim + (proxied ? ':p' : '');
let v = srv.views.get(key);
if (v && !proxied) return v;
if (v && proxied && !t.dirty && v.__proxyTex === t.tex) return v;
const total = t.desc.mipLevelCount;
const mipCount = srv.b === 0xffffffff || srv.b === 0 ? total - srv.a : srv.b;
const layers = t.desc.size.depthOrArrayLayers;
if ((b.dim === 'cube' && layers - srv.c < 6) || (b.dim === 'cube-array' && layers - srv.c < 6)) {
logOnce('cubeview' + srv.res, 'cube SRV over a texture with ' + layers + ' layer(s): bound as an empty cube');
return null; // the caller binds a dummy cube: creating the view would invalidate the whole command buffer
}
const desc = { dimension: b.dim, baseMipLevel: Math.min(srv.a, total - 1), mipLevelCount: Math.max(1, Math.min(mipCount, total - srv.a)) };
if (b.dim === '2d-array' || b.dim === 'cube' || b.dim === 'cube-array' || b.dim === '2d') {
const first = srv.c, cnt = (b.dim === 'cube') ? 6 : (b.dim === '2d' ? 1 : (srv.d || layers - first));
desc.baseArrayLayer = Math.min(first, layers - 1); desc.arrayLayerCount = Math.max(1, Math.min(cnt, layers - first));
}
const f = t.format;
if (srv.fmt && !isDepth(f)) { const mf = mapFormat(srv.fmt, 0).format; if (mf !== f && (f.includes('srgb') !== mf.includes('srgb'))) desc.format = mf; }
if (isDepth(f) && !proxied) desc.aspect = (srv.fmt === 47 || srv.fmt === 22) ? 'stencil-only' : 'depth-only';
try { v = t.tex.createView(desc); } catch (e) { logOnce('srvview' + srv.res, 'SRV view failed: ' + e.message); return null; }
v.__proxyTex = t.tex;
srv.views.set(key, v);
return v;
}
// ---- depth textures sampled as float ----------------------------------------------------------------------------------------------
// D3D11 lets a shader read a depth texture as a float texture (Load / point Sample); WebGPU only binds it as `depth`
// (comparison or textureLoad of texture_depth_2d) and the translated code uses texture_2d<f32>. A proxy r32float (depth) or
// r8uint (stencil) texture is refreshed by a render pass whenever the depth texture was drawn into since the last read.
let proxyPipelines = {};
function proxyPipeline(kind) {
if (proxyPipelines[kind]) return proxyPipelines[kind];
const code = kind === 'depth'
? `@group(0) @binding(0) var src: texture_depth_2d;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f { var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3)); return vec4f(p[i], 0, 1); }
@fragment fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f { return vec4f(textureLoad(src, vec2i(pos.xy), 0), 0, 0, 1); }`
: `@group(0) @binding(0) var src: texture_2d<u32>;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f { var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3)); return vec4f(p[i], 0, 1); }
@fragment fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4<u32> { return textureLoad(src, vec2i(pos.xy), 0); }`;
const module = device.createShaderModule({ code });
return (proxyPipelines[kind] = device.createRenderPipeline({ layout: 'auto', vertex: { module, entryPoint: 'vs' },
fragment: { module, entryPoint: 'fs', targets: [{ format: kind === 'depth' ? 'r32float' : 'r8uint' }] } }));
}
let f2dPipeline = null;
function floatToDepthProxy(t) {
// D3D11 SampleCmp works on any float texture; WebGPU comparison sampling needs a depth texture: keep a depth32float copy
let px = t.proxy_fdepth;
if (!px) {
const d = t.desc;
const tex = device.createTexture({ size: d.size, mipLevelCount: d.mipLevelCount, sampleCount: 1, dimension: '2d', format: 'depth32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT });
px = t.proxy_fdepth = { kind: 'texture', tex, desc: { size: d.size, mipLevelCount: d.mipLevelCount, dimension: '2d', sampleCount: 1 }, format: 'depth32float', views: new Map(), dirty: true };
}
if (px.dirty) {
endPass();
if (!f2dPipeline) {
const module = device.createShaderModule({ code: `@group(0) @binding(0) var src: texture_2d<f32>;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f { var p = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3)); return vec4f(p[i], 0, 1); }
@fragment fn fs(@builtin(position) pos: vec4f) -> @builtin(frag_depth) f32 { return textureLoad(src, vec2i(pos.xy), 0).x; }` });
f2dPipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module, entryPoint: 'vs' }, fragment: { module, entryPoint: 'fs', targets: [] },
depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'always' } });
}
const enc = getEncoder(), layers = t.desc.size.depthOrArrayLayers;
if (!px.passes) { // the views and bind groups of a refresh never change: made once (three new objects per mip and layer per refresh before)
px.passes = [];
for (let mip = 0; mip < t.desc.mipLevelCount; mip++) for (let layer = 0; layer < layers; layer++) {
const src = t.tex.createView({ dimension: '2d', baseMipLevel: mip, mipLevelCount: 1, baseArrayLayer: layer, arrayLayerCount: 1 });
const dst = px.tex.createView({ baseMipLevel: mip, mipLevelCount: 1, baseArrayLayer: layer, arrayLayerCount: 1 });
px.passes.push({ dst, bg: device.createBindGroup({ layout: f2dPipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: src }] }) });
}
}
for (const r of px.passes) {
const p = enc.beginRenderPass({ colorAttachments: [], depthStencilAttachment: { view: r.dst, depthLoadOp: 'clear', depthStoreOp: 'store', depthClearValue: 1 } });
p.setPipeline(f2dPipeline);
p.setBindGroup(0, r.bg);
p.draw(3);
p.end();
}
usedInEncoder.add(t);
px.dirty = false;
}
return px;
}
function markColorDirty(t) { if (t && t.proxy_fdepth) t.proxy_fdepth.dirty = true; }
function depthProxy(t, kind) {
// returns the proxy texture object of depth texture `t`, refreshed if the depth texture changed since the last refresh
const key = 'proxy_' + kind;
let px = t[key];
if (!px) {
const d = t.desc;
const tex = device.createTexture({ size: d.size, mipLevelCount: d.mipLevelCount, sampleCount: 1, dimension: '2d',
format: kind === 'depth' ? 'r32float' : 'r8uint', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT });
px = t[key] = { kind: 'texture', tex, desc: { size: d.size, mipLevelCount: d.mipLevelCount, dimension: '2d', sampleCount: 1 }, format: tex.format, views: new Map(), dirty: true };
}
if (px.dirty) {
endPass();
const pl = proxyPipeline(kind), enc = getEncoder();
const layers = t.desc.size.depthOrArrayLayers;
if (!px.passes) { // made once per proxy (see floatToDepthProxy)
px.passes = [];
for (let mip = 0; mip < t.desc.mipLevelCount; mip++) for (let layer = 0; layer < layers; layer++) {
const src = t.tex.createView({ dimension: '2d', baseMipLevel: mip, mipLevelCount: 1, baseArrayLayer: layer, arrayLayerCount: 1, aspect: kind === 'depth' ? 'depth-only' : 'stencil-only' });
const dst = px.tex.createView({ baseMipLevel: mip, mipLevelCount: 1, baseArrayLayer: layer, arrayLayerCount: 1 });
px.passes.push({ dst, bg: device.createBindGroup({ layout: pl.getBindGroupLayout(0), entries: [{ binding: 0, resource: src }] }) });
}
}
for (const r of px.passes) {
const p = enc.beginRenderPass({ colorAttachments: [{ view: r.dst, loadOp: 'clear', storeOp: 'store' }] });
p.setPipeline(pl);
p.setBindGroup(0, r.bg);
p.draw(3);
p.end();
}
usedInEncoder.add(t);
px.dirty = false;
t['clean_' + kind] = true;
}
return px;
}
function markDepthDirty(t) { t.proxy_depth && (t.proxy_depth.dirty = true); t.proxy_stencil && (t.proxy_stencil.dirty = true); }
// ---- pipelines --------------------------------------------------------------------------------------------------------------------
const layoutCache = new Map();
// Pipelines are found by a tuple of small integers (shader hashes and pass formats interned) hashed to a number: building and hashing a string key took ~15 % of the worker,
// since the lookup runs whenever any pipeline state changed (most draws). Buckets hold [tuple, pipeline] pairs; null pipelines (creation failed) are cached too.
const pipelineBuckets = new Map(), pipeTuple = new Int32Array(64), internedHashes = new Map(), internedTails = new Map();
let pipelineCount = 0;
function internHash(o) {
if (o.hid === undefined) { let h = internedHashes.get(o.hash); if (h === undefined) { h = internedHashes.size + 1; internedHashes.set(o.hash, h); } o.hid = h; }
return o.hid;
}
function bindGroupLayoutFor(prog, stage, group) {