reze-engine 0.62.1 → 0.64.0

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.
Files changed (83) hide show
  1. package/dist/engine.d.ts +95 -57
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +466 -162
  4. package/dist/graph/compile.d.ts.map +1 -1
  5. package/dist/graph/compile.js +4 -1
  6. package/dist/graph/registry.d.ts +3 -0
  7. package/dist/graph/registry.d.ts.map +1 -1
  8. package/dist/graph/registry.js +133 -0
  9. package/dist/graph/schema.d.ts +12 -0
  10. package/dist/graph/schema.d.ts.map +1 -1
  11. package/dist/graph/slots.d.ts.map +1 -1
  12. package/dist/graph/slots.js +11 -20
  13. package/dist/index.d.ts +9 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/shaders/lights.d.ts +3 -2
  17. package/dist/shaders/lights.d.ts.map +1 -1
  18. package/dist/shaders/lights.js +16 -3
  19. package/dist/shaders/materials/common.d.ts.map +1 -1
  20. package/dist/shaders/materials/common.js +63 -56
  21. package/dist/shaders/materials/nodes.d.ts +1 -1
  22. package/dist/shaders/materials/nodes.d.ts.map +1 -1
  23. package/dist/shaders/materials/nodes.js +210 -0
  24. package/dist/shaders/passes/composite.d.ts.map +1 -1
  25. package/dist/shaders/passes/composite.js +13 -1
  26. package/dist/shaders/passes/cull.d.ts +1 -1
  27. package/dist/shaders/passes/cull.d.ts.map +1 -1
  28. package/dist/shaders/passes/cull.js +7 -3
  29. package/dist/shaders/passes/ground.d.ts.map +1 -1
  30. package/dist/shaders/passes/ground.js +32 -55
  31. package/dist/shaders/passes/particles.d.ts +11 -1
  32. package/dist/shaders/passes/particles.d.ts.map +1 -1
  33. package/dist/shaders/passes/particles.js +9 -4
  34. package/dist/shaders/scene-light-api.d.ts +18 -10
  35. package/dist/shaders/scene-light-api.d.ts.map +1 -1
  36. package/dist/shaders/scene-light-api.js +58 -46
  37. package/dist/shadow-cascades.d.ts +47 -20
  38. package/dist/shadow-cascades.d.ts.map +1 -1
  39. package/dist/shadow-cascades.js +36 -13
  40. package/dist/unity/character.d.ts +59 -0
  41. package/dist/unity/character.d.ts.map +1 -0
  42. package/dist/unity/character.js +120 -0
  43. package/dist/unity/globals.d.ts +93 -0
  44. package/dist/unity/globals.d.ts.map +1 -0
  45. package/dist/unity/globals.js +340 -0
  46. package/dist/unity/host.d.ts +137 -0
  47. package/dist/unity/host.d.ts.map +1 -0
  48. package/dist/unity/host.js +489 -0
  49. package/dist/unity/looks.d.ts +88 -0
  50. package/dist/unity/looks.d.ts.map +1 -0
  51. package/dist/unity/looks.js +295 -0
  52. package/dist/unity/skin.d.ts +9 -0
  53. package/dist/unity/skin.d.ts.map +1 -0
  54. package/dist/unity/skin.js +117 -0
  55. package/dist/unity/stage.d.ts +148 -0
  56. package/dist/unity/stage.d.ts.map +1 -0
  57. package/dist/unity/stage.js +375 -0
  58. package/dist/unity/state.d.ts +22 -0
  59. package/dist/unity/state.d.ts.map +1 -0
  60. package/dist/unity/state.js +170 -0
  61. package/package.json +1 -1
  62. package/src/engine.ts +504 -176
  63. package/src/graph/compile.ts +3 -1
  64. package/src/graph/registry.ts +142 -0
  65. package/src/graph/schema.ts +9 -0
  66. package/src/graph/slots.ts +11 -20
  67. package/src/index.ts +11 -0
  68. package/src/shaders/lights.ts +16 -3
  69. package/src/shaders/materials/common.ts +63 -57
  70. package/src/shaders/materials/nodes.ts +210 -0
  71. package/src/shaders/passes/composite.ts +13 -1
  72. package/src/shaders/passes/cull.ts +7 -3
  73. package/src/shaders/passes/ground.ts +32 -55
  74. package/src/shaders/passes/particles.ts +19 -5
  75. package/src/shaders/scene-light-api.ts +59 -46
  76. package/src/shadow-cascades.ts +61 -32
  77. package/src/unity/character.ts +169 -0
  78. package/src/unity/globals.ts +443 -0
  79. package/src/unity/host.ts +587 -0
  80. package/src/unity/looks.ts +372 -0
  81. package/src/unity/skin.ts +128 -0
  82. package/src/unity/stage.ts +568 -0
  83. package/src/unity/state.ts +172 -0
@@ -0,0 +1,587 @@
1
+ // The native-material host: draws the game's own shaders, translated to WGSL
2
+ // (ag-rip's shader_wgsl.py: HLSL -> dxc -> SPIR-V -> naga), inside the engine's
3
+ // scene pass.
4
+ //
5
+ // A translated shader is a vertex module, a fragment module and the binding map
6
+ // the translator wrote beside them: every uniform block with its members'
7
+ // offsets, every texture and sampler, the vertex inputs by Unity semantic. The
8
+ // host builds the pipeline from that map (an explicit layout: the modules
9
+ // declare everything the decompiled block did, used or not), fills each block
10
+ // BY NAME from value sources — the frame's Unity globals, the draw's
11
+ // UnityPerDraw, the character's property block, the material's properties —
12
+ // and draws. This is the compare page's renderer (agtools/web/agplay), whose
13
+ // output matched the game's frames to a few levels of 255, moved into the
14
+ // engine's frame.
15
+ //
16
+ // What changes on the way in is only what the engine's pass needs: the
17
+ // fragment returns the engine's attachments (colour, the aux mask, the id when
18
+ // the device has one), depth tests flip with the engine's reversed depth, and
19
+ // the pass's MSAA count and formats are the engine's.
20
+
21
+ import { blendState, colorMask, cull, depthCompare, stencilState, zwrite, type PassState } from "./state"
22
+
23
+ export type NativeBinding = {
24
+ group: number
25
+ binding: number
26
+ space: string
27
+ name: string
28
+ type: string
29
+ stages: string[]
30
+ }
31
+ export type NativeMember = {
32
+ name: string
33
+ offset: number
34
+ size: number
35
+ type: string
36
+ }
37
+ export type NativeStruct = { size: number; members: NativeMember[] }
38
+ export type NativeShaderInfo = {
39
+ bindings: Record<string, NativeBinding>
40
+ structs: Record<string, NativeStruct>
41
+ vertexInputs: { location: number; name: string; type: string }[]
42
+ }
43
+ /** One translated shader variant: what shader_wgsl.py writes for a pass. */
44
+ export type NativeShader = {
45
+ name: string
46
+ vert: string
47
+ frag: string
48
+ info: NativeShaderInfo
49
+ }
50
+
51
+ /** A value a uniform member takes: a scalar, a flat vector or matrix, an array of
52
+ * those, or raw integer bits (Uint32Array — never passed through a float). */
53
+ export type NativeValue = number | ArrayLike<number> | ArrayLike<ArrayLike<number>>
54
+ /** Name -> value; the first source that has a name wins. */
55
+ export type ValueSource = Record<string, NativeValue | undefined>
56
+
57
+ /** One vertex stream a draw binds, by the semantic a shader input names. */
58
+ export type NativeStream = {
59
+ buffer: GPUBuffer
60
+ offset: number
61
+ stride: number
62
+ format: GPUVertexFormat
63
+ }
64
+
65
+ /** Members read as integers rather than floats (the shader asint()s them). */
66
+ const INT_MEMBERS = new Set(["unity_LightIndices"])
67
+
68
+ /** "in_u002e_var_u002e_TEXCOORD2_" -> "TEXCOORD2"; TEXCOORD alone is 0. */
69
+ export function semantic(inputName: string): string {
70
+ let s = inputName.replace(/^in_u002e_var_u002e_/, "").replace(/_+$/, "")
71
+ if (s === "TEXCOORD") s = "TEXCOORD0"
72
+ if (s === "COLOR0") s = "COLOR"
73
+ return s
74
+ }
75
+
76
+ /**
77
+ * The fragment module, returning the engine's attachments.
78
+ *
79
+ * naga writes the entry point last: it copies its inputs to private globals,
80
+ * calls the translated body, and returns the one SV_Target. That return is
81
+ * rewritten to the engine's output struct — the colour as the game computed
82
+ * it, the aux mask (bloom on, canvas alpha from the colour's alpha) and, when
83
+ * the device has an id attachment, a zero id. Anything else about the module
84
+ * is left exactly as translated.
85
+ */
86
+ export function wrapFragment(frag: string, ids: boolean): string {
87
+ const at = frag.lastIndexOf("@fragment")
88
+ if (at < 0) throw new Error("native shader: no fragment entry point")
89
+ const head = frag.slice(at)
90
+ const sig = /->\s*@location\(0\)\s*vec4<f32>\s*\{/
91
+ if (!sig.test(head)) throw new Error("native shader: the fragment entry does not return one vec4 target")
92
+ let body = head.replace(sig, "-> RzNativeOut {")
93
+ const ret = /return\s+(_e\d+)\s*;\s*\}\s*$/
94
+ const m = ret.exec(body)
95
+ if (!m) throw new Error("native shader: the fragment entry's return is not the translator's")
96
+ const v = m[1]
97
+ body = body.replace(ret, `return RzNativeOut(${v}, vec4f(1.0, 1.0, 0.0, ${v}.a)${ids ? ", vec2u(0u)" : ""});\n}\n`)
98
+ const struct = `struct RzNativeOut {\n @location(0) color: vec4f,\n @location(1) mask: vec4f,\n${ids ? " @location(2) id: vec2u,\n" : ""}};\n`
99
+ // After the module's directives (diagnostic, enable): WGSL wants them first.
100
+ const pre = frag.slice(0, at)
101
+ const directives = /^(?:\s*(?:diagnostic|enable|requires)\b[^;]*;)*/.exec(pre)![0]
102
+ return directives + "\n" + struct + pre.slice(directives.length) + body
103
+ }
104
+
105
+ type Prepared = {
106
+ shader: NativeShader
107
+ vert: GPUShaderModule
108
+ frag: GPUShaderModule
109
+ bindLayout: GPUBindGroupLayout
110
+ layout: GPUPipelineLayout
111
+ }
112
+
113
+ /**
114
+ * One uniform block's buffer and what it was last filled from: per member, the
115
+ * value object and the source it came from. A frame refills only the members
116
+ * whose value object changed — the engine keeps an unchanged global's object
117
+ * from frame to frame (engine.ts, stableGlobals) — and uploads only the bytes
118
+ * between the first and the last of those.
119
+ */
120
+ type Block = {
121
+ gpu: GPUBuffer
122
+ data: ArrayBuffer
123
+ f32: Float32Array
124
+ i32: Int32Array
125
+ u32: Uint32Array
126
+ refs: (NativeValue | undefined)[]
127
+ /** Every member came from the last source (the frame's globals). */
128
+ framewide: boolean
129
+ /** The frame it was last brought up to date in (shared blocks). */
130
+ frame: number
131
+ }
132
+
133
+ type DrawCache = {
134
+ buffers: Map<string, Block>
135
+ bind: GPUBindGroup | null
136
+ views: Map<string, GPUTextureView>
137
+ pipeline: GPURenderPipeline | null
138
+ }
139
+
140
+ export type NativeHostTargets = {
141
+ /** The scene pass's colour formats, in attachment order: hdr, aux[, id]. */
142
+ colorFormats: GPUTextureFormat[]
143
+ depthFormat: GPUTextureFormat
144
+ sampleCount: number
145
+ reversedZ: boolean
146
+ }
147
+
148
+ export type NativeDraw = {
149
+ shader: string
150
+ state: PassState
151
+ streams: NativeStream[]
152
+ index: {
153
+ buffer: GPUBuffer
154
+ format: GPUIndexFormat
155
+ offset: number
156
+ count: number
157
+ }
158
+ /** Name -> value, first match wins. */
159
+ sources: ValueSource[]
160
+ /** Texture name -> view (material slots, globals, render targets). */
161
+ textures: Record<string, GPUTextureView | undefined>
162
+ /** Texture name -> its sampler (wrap and filter as the game imported it);
163
+ * a texture without one samples repeating, trilinear. */
164
+ samplers?: Record<string, GPUSampler | undefined>
165
+ /** Separates draws' uniform buffers: one renderer's pass, one slot. */
166
+ key: string
167
+ /** The pass the frame's globals belong to (main, a shadow cascade): a block
168
+ * filled from the globals alone is one buffer per shader and pass, shared by
169
+ * every draw in it. */
170
+ pass?: string
171
+ }
172
+
173
+ export class NativeHost {
174
+ private prepared = new Map<string, Prepared>()
175
+ private pipelines = new Map<string, { pipeline: GPURenderPipeline; stencilRef: number } | null>()
176
+ private draws = new Map<string, DrawCache>()
177
+ private shared = new Map<string, Block>()
178
+ private frame = 0
179
+ private sampler: GPUSampler
180
+ /** Names no source supplied, with how often — the audit the compare page showed. */
181
+ readonly missing = new Map<string, number>()
182
+ readonly errors: string[] = []
183
+
184
+ constructor(
185
+ private device: GPUDevice,
186
+ private targets: NativeHostTargets,
187
+ private fallback: {
188
+ white: GPUTextureView
189
+ black: GPUTextureView
190
+ blackCube: GPUTextureView
191
+ depth: GPUTextureView
192
+ comparison: GPUSampler
193
+ },
194
+ ) {
195
+ this.sampler = device.createSampler({
196
+ label: "native material sampler",
197
+ magFilter: "linear",
198
+ minFilter: "linear",
199
+ mipmapFilter: "linear",
200
+ addressModeU: "repeat",
201
+ addressModeV: "repeat",
202
+ maxAnisotropy: 8,
203
+ })
204
+ }
205
+
206
+ has(name: string): boolean {
207
+ return this.prepared.has(name)
208
+ }
209
+
210
+ /** Register a translated shader. Idempotent by name. */
211
+ register(shader: NativeShader): void {
212
+ if (this.prepared.has(shader.name)) return
213
+ const d = this.device
214
+ const ids = this.targets.colorFormats.length > 2
215
+ const entries: GPUBindGroupLayoutEntry[] = Object.values(shader.info.bindings).map((b) => {
216
+ const visibility =
217
+ (b.stages.includes("vertex") ? GPUShaderStage.VERTEX : 0) | (b.stages.includes("fragment") ? GPUShaderStage.FRAGMENT : 0)
218
+ if (b.space === "uniform")
219
+ return {
220
+ binding: b.binding,
221
+ visibility,
222
+ buffer: { type: "uniform" as const },
223
+ }
224
+ if (/^sampler_comparison/.test(b.type))
225
+ return {
226
+ binding: b.binding,
227
+ visibility,
228
+ sampler: { type: "comparison" as const },
229
+ }
230
+ if (/^sampler/.test(b.type))
231
+ return {
232
+ binding: b.binding,
233
+ visibility,
234
+ sampler: { type: "filtering" as const },
235
+ }
236
+ if (/texture_depth/.test(b.type))
237
+ return {
238
+ binding: b.binding,
239
+ visibility,
240
+ texture: { sampleType: "depth" as const },
241
+ }
242
+ const viewDimension: GPUTextureViewDimension = /texture_cube/.test(b.type) ? "cube" : /texture_3d/.test(b.type) ? "3d" : "2d"
243
+ return {
244
+ binding: b.binding,
245
+ visibility,
246
+ texture: { sampleType: "float" as const, viewDimension },
247
+ }
248
+ })
249
+ const bindLayout = d.createBindGroupLayout({
250
+ label: `${shader.name} bindings`,
251
+ entries,
252
+ })
253
+ const module = (code: string, label: string) => {
254
+ const m = d.createShaderModule({ label, code })
255
+ void m.getCompilationInfo().then((ci) => {
256
+ for (const x of ci.messages) if (x.type === "error") this.errors.push(`${label}: ${x.message}`)
257
+ })
258
+ return m
259
+ }
260
+ this.prepared.set(shader.name, {
261
+ shader,
262
+ vert: module(shader.vert, `${shader.name} vertex`),
263
+ // A depth-only host keeps the translated fragment: it has no targets to wrap for.
264
+ frag: module(this.targets.colorFormats.length ? wrapFragment(shader.frag, ids) : shader.frag, `${shader.name} fragment`),
265
+ bindLayout,
266
+ layout: d.createPipelineLayout({
267
+ label: shader.name,
268
+ bindGroupLayouts: [bindLayout],
269
+ }),
270
+ })
271
+ }
272
+
273
+ /** The vertex inputs a shader reads, by Unity semantic, in location order. */
274
+ inputs(name: string): { location: number; semantic: string }[] {
275
+ const p = this.prepared.get(name)
276
+ return p
277
+ ? p.shader.info.vertexInputs.map((v) => ({
278
+ location: v.location,
279
+ semantic: semantic(v.name),
280
+ }))
281
+ : []
282
+ }
283
+
284
+ private pipeline(p: Prepared, state: PassState, streams: NativeStream[]) {
285
+ const key = `${p.shader.name}|${JSON.stringify(state)}|${streams.map((s) => s.format).join(",")}`
286
+ if (this.pipelines.has(key)) return this.pipelines.get(key)!
287
+ const t = this.targets
288
+ const stencil = stencilState(state)
289
+ const blend = blendState(state)
290
+ // A depth-only host (the shadow atlas) has no colour targets at all.
291
+ const targets: GPUColorTargetState[] = !t.colorFormats.length
292
+ ? []
293
+ : [
294
+ {
295
+ format: t.colorFormats[0],
296
+ blend,
297
+ writeMask: colorMask(state.ColorMask?.[0]),
298
+ },
299
+ // The aux mask: bloom on, and the canvas alpha laid over as the engine's
300
+ // own transparent classes lay it.
301
+ {
302
+ format: t.colorFormats[1],
303
+ blend: {
304
+ color: {
305
+ srcFactor: "src-alpha",
306
+ dstFactor: "one-minus-src-alpha",
307
+ operation: "add",
308
+ },
309
+ alpha: {
310
+ srcFactor: "one",
311
+ dstFactor: "one-minus-src-alpha",
312
+ operation: "add",
313
+ },
314
+ },
315
+ writeMask: colorMask(state.ColorMask?.[0]) === 0 ? 0 : 0xf,
316
+ },
317
+ ]
318
+ if (t.colorFormats[2]) targets.push({ format: t.colorFormats[2], writeMask: 0 })
319
+ const offset = state.Offset
320
+ let result: { pipeline: GPURenderPipeline; stencilRef: number } | null
321
+ try {
322
+ result = {
323
+ pipeline: this.device.createRenderPipeline({
324
+ label: `${p.shader.name} pipeline`,
325
+ layout: p.layout,
326
+ vertex: {
327
+ module: p.vert,
328
+ entryPoint: "vert",
329
+ buffers: p.shader.info.vertexInputs.map((vi, k) => ({
330
+ arrayStride: streams[k].stride,
331
+ stepMode: "vertex" as const,
332
+ attributes: [
333
+ {
334
+ shaderLocation: vi.location,
335
+ offset: 0,
336
+ format: streams[k].format,
337
+ },
338
+ ],
339
+ })),
340
+ },
341
+ fragment: { module: p.frag, entryPoint: "frag", targets },
342
+ // Unity's D3D winding, which is PMX's too: clockwise is the front.
343
+ primitive: {
344
+ topology: "triangle-list",
345
+ cullMode: cull(state.Cull?.[0]),
346
+ frontFace: "cw",
347
+ },
348
+ depthStencil: {
349
+ format: t.depthFormat,
350
+ depthWriteEnabled: zwrite(state.ZWrite?.[0]),
351
+ depthCompare: depthCompare(state.ZTest?.[0], t.reversedZ),
352
+ ...(stencil
353
+ ? {
354
+ stencilFront: stencil.face,
355
+ stencilBack: stencil.face,
356
+ stencilReadMask: stencil.read,
357
+ stencilWriteMask: stencil.write,
358
+ }
359
+ : {}),
360
+ // Offset (factor, units): pulls toward the viewer, which under
361
+ // reversed depth is the other sign.
362
+ ...(offset
363
+ ? {
364
+ depthBias: Math.round((+offset[1] || 0) * (t.reversedZ ? 1 : -1)),
365
+ depthBiasSlopeScale: (+offset[0] || 0) * (t.reversedZ ? 1 : -1),
366
+ }
367
+ : {}),
368
+ },
369
+ multisample: { count: t.sampleCount },
370
+ }),
371
+ stencilRef: stencil?.ref ?? 0,
372
+ }
373
+ } catch (e) {
374
+ this.errors.push(`pipeline ${p.shader.name}: ${(e as Error).message}`)
375
+ result = null
376
+ }
377
+ this.pipelines.set(key, result)
378
+ return result
379
+ }
380
+
381
+ private lookup(name: string, sources: ValueSource[]): NativeValue | undefined {
382
+ for (const s of sources) {
383
+ const v = s[name]
384
+ if (v !== undefined && v !== null) return v
385
+ }
386
+ // naga renames a member that ends in a digit with a trailing "_"
387
+ if (name.endsWith("_")) return this.lookup(name.replace(/_+$/, ""), sources)
388
+ return undefined
389
+ }
390
+
391
+ /** A new frame: shared blocks are brought up to date once in it. */
392
+ beginFrame(): void {
393
+ this.frame++
394
+ }
395
+
396
+ private lookupFrom(name: string, sources: ValueSource[]): [NativeValue | undefined, number] {
397
+ for (let i = 0; i < sources.length; i++) {
398
+ const v = sources[i][name]
399
+ if (v !== undefined && v !== null) return [v, i]
400
+ }
401
+ // naga renames a member that ends in a digit with a trailing "_"
402
+ if (name.endsWith("_")) return this.lookupFrom(name.replace(/_+$/, ""), sources)
403
+ return [undefined, -1]
404
+ }
405
+
406
+ private newBlock(struct: NativeStruct, label: string): Block {
407
+ const size = Math.max(16, struct.size)
408
+ const data = new ArrayBuffer(size)
409
+ return {
410
+ gpu: this.device.createBuffer({ label, size, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }),
411
+ data,
412
+ f32: new Float32Array(data),
413
+ i32: new Int32Array(data),
414
+ u32: new Uint32Array(data),
415
+ refs: new Array(struct.members.length).fill(null),
416
+ framewide: true,
417
+ frame: -1,
418
+ }
419
+ }
420
+
421
+ /** Bring a block up to date with its sources; upload what changed. */
422
+ private fill(struct: NativeStruct, sources: ValueSource[], blk: Block, first: boolean): void {
423
+ let lo = Infinity
424
+ let hi = -1
425
+ const last = sources.length - 1
426
+ for (let mi = 0; mi < struct.members.length; mi++) {
427
+ const m = struct.members[mi]
428
+ const [v, src] = this.lookupFrom(m.name, sources)
429
+ if (first && src >= 0 && src !== last) blk.framewide = false
430
+ if (!first && v === blk.refs[mi]) continue
431
+ blk.refs[mi] = v
432
+ const at = m.offset / 4
433
+ const n = m.size / 4
434
+ lo = Math.min(lo, m.offset)
435
+ hi = Math.max(hi, m.offset + m.size)
436
+ blk.f32.fill(0, at, at + n)
437
+ if (v === undefined) {
438
+ if (first) this.missing.set(m.name, (this.missing.get(m.name) ?? 0) + 1)
439
+ continue
440
+ }
441
+ // Integer bit patterns (light masks, bins) stay integers: through a float
442
+ // a mask could spell a NaN, which a Float32Array does not keep bit for bit.
443
+ const target = v instanceof Uint32Array ? blk.u32 : INT_MEMBERS.has(m.name.replace(/_+$/, "")) ? blk.i32 : blk.f32
444
+ if (typeof v === "number") {
445
+ target[at] = v
446
+ continue
447
+ }
448
+ const arr = /^array<vec4<\w+>,\s*(\d+)>/.exec(m.type)
449
+ const list = v as ArrayLike<number | ArrayLike<number>>
450
+ if (arr && list.length === +arr[1] && typeof list[0] === "number") {
451
+ // A scalar array (float[N] in HLSL, widened to float4[N]): one element
452
+ // per 16-byte slot, read through .x.
453
+ for (let k = 0; k < list.length; k++) target[at + k * 4] = list[k] as number
454
+ continue
455
+ }
456
+ let k = 0
457
+ for (let i = 0; i < list.length && k < n; i++) {
458
+ const e = list[i]
459
+ if (typeof e === "number") target[at + k++] = e
460
+ else for (let j = 0; j < e.length && k < n; j++) target[at + k++] = e[j]
461
+ }
462
+ }
463
+ if (hi > lo) {
464
+ lo &= ~3
465
+ this.device.queue.writeBuffer(blk.gpu, lo, blk.data, lo, Math.min(blk.data.byteLength, (hi + 3) & ~3) - lo)
466
+ }
467
+ }
468
+
469
+ /** Encode one draw into the scene pass. */
470
+ draw(pass: GPURenderPassEncoder, d: NativeDraw): void {
471
+ const p = this.prepared.get(d.shader)
472
+ if (!p) return
473
+ const pl = this.pipeline(p, d.state, d.streams)
474
+ if (!pl) return
475
+ let cache = this.draws.get(d.key)
476
+ if (!cache) {
477
+ cache = {
478
+ buffers: new Map(),
479
+ bind: null,
480
+ views: new Map(),
481
+ pipeline: null,
482
+ }
483
+ this.draws.set(d.key, cache)
484
+ }
485
+ const info = p.shader.info
486
+ const entries: GPUBindGroupEntry[] = []
487
+ let rebind = !cache.bind || cache.pipeline !== pl.pipeline
488
+ for (const [name, b] of Object.entries(info.bindings)) {
489
+ if (b.space === "uniform") {
490
+ const struct = info.structs[name]
491
+ if (!struct) continue
492
+ let gb = cache.buffers.get(name)
493
+ if (!gb) {
494
+ gb = this.newBlock(struct, `${d.key} ${name}`)
495
+ this.fill(struct, d.sources, gb, true)
496
+ // Filled from the frame's globals alone: one buffer for the pass,
497
+ // shared by every draw of this shader in it.
498
+ if (gb.framewide && d.pass) {
499
+ const sk = `${d.pass}|${d.shader}|${name}`
500
+ const sh = this.shared.get(sk)
501
+ if (sh) {
502
+ gb.gpu.destroy()
503
+ gb = sh
504
+ } else {
505
+ gb.frame = this.frame
506
+ this.shared.set(sk, gb)
507
+ }
508
+ }
509
+ cache.buffers.set(name, gb)
510
+ rebind = true
511
+ } else if (!gb.framewide || !d.pass) {
512
+ this.fill(struct, d.sources, gb, false)
513
+ } else if (gb.frame !== this.frame) {
514
+ gb.frame = this.frame
515
+ this.fill(struct, d.sources, gb, false)
516
+ }
517
+ entries.push({ binding: b.binding, resource: { buffer: gb.gpu } })
518
+ } else if (/^texture/.test(b.type)) {
519
+ const view = this.textureView(name, b.type, d.textures)
520
+ if (cache.views.get(name) !== view) {
521
+ cache.views.set(name, view)
522
+ rebind = true
523
+ }
524
+ entries.push({ binding: b.binding, resource: view })
525
+ } else if (/^sampler/.test(b.type)) {
526
+ entries.push({
527
+ binding: b.binding,
528
+ resource: /comparison/.test(b.type) ? this.fallback.comparison : this.samplerFor(name, info, d.samplers),
529
+ })
530
+ }
531
+ }
532
+ if (rebind) {
533
+ try {
534
+ cache.bind = this.device.createBindGroup({
535
+ label: `${d.key} bindings`,
536
+ layout: p.bindLayout,
537
+ entries,
538
+ })
539
+ cache.pipeline = pl.pipeline
540
+ } catch (e) {
541
+ this.errors.push(`bind ${d.shader}: ${(e as Error).message}`)
542
+ return
543
+ }
544
+ }
545
+ pass.setPipeline(pl.pipeline)
546
+ pass.setBindGroup(0, cache.bind!)
547
+ pass.setStencilReference(pl.stencilRef)
548
+ d.streams.forEach((s, k) => pass.setVertexBuffer(k, s.buffer, s.offset))
549
+ pass.setIndexBuffer(d.index.buffer, d.index.format, d.index.offset)
550
+ pass.drawIndexed(d.index.count)
551
+ }
552
+
553
+ /** sampler_AlbedoTex / samplerAlbedoTex -> the sampler of the texture it pairs with. */
554
+ private samplerFor(name: string, info: NativeShaderInfo, samplers: NativeDraw["samplers"]): GPUSampler {
555
+ if (!samplers) return this.sampler
556
+ const bare = name.replace(/^sampler_?/, "")
557
+ const tex = info.bindings[bare] ? bare : info.bindings[`_${bare}`] ? `_${bare}` : bare
558
+ return samplers[tex] ?? this.sampler
559
+ }
560
+
561
+ private textureView(name: string, type: string, textures: Record<string, GPUTextureView | undefined>): GPUTextureView {
562
+ const v = textures[name]
563
+ if (v) return v
564
+ if (/texture_depth/.test(type)) return this.fallback.depth
565
+ if (/texture_cube/.test(type)) return this.fallback.blackCube
566
+ return this.fallback.white
567
+ }
568
+
569
+ /** Forget a draw's buffers (its model left). */
570
+ release(prefix: string): void {
571
+ for (const [k, c] of this.draws) {
572
+ if (!k.startsWith(prefix)) continue
573
+ // A pass-wide block outlives any one draw that uses it.
574
+ const kept = new Set(this.shared.values())
575
+ for (const b of c.buffers.values()) if (!kept.has(b)) b.gpu.destroy()
576
+ this.draws.delete(k)
577
+ }
578
+ }
579
+ }
580
+
581
+ export function sameBytes(a: Uint8Array, b: Uint8Array): boolean {
582
+ if (a.length !== b.length) return false
583
+ const x = new Uint32Array(a.buffer, a.byteOffset, a.length >> 2)
584
+ const y = new Uint32Array(b.buffer, b.byteOffset, b.length >> 2)
585
+ for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false
586
+ return true
587
+ }