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.
- package/dist/engine.d.ts +95 -57
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +466 -162
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts +3 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +133 -0
- package/dist/graph/schema.d.ts +12 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +11 -20
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/shaders/lights.d.ts +3 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -3
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +63 -56
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +210 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +13 -1
- package/dist/shaders/passes/cull.d.ts +1 -1
- package/dist/shaders/passes/cull.d.ts.map +1 -1
- package/dist/shaders/passes/cull.js +7 -3
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +32 -55
- package/dist/shaders/passes/particles.d.ts +11 -1
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +9 -4
- package/dist/shaders/scene-light-api.d.ts +18 -10
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +58 -46
- package/dist/shadow-cascades.d.ts +47 -20
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +36 -13
- package/dist/unity/character.d.ts +59 -0
- package/dist/unity/character.d.ts.map +1 -0
- package/dist/unity/character.js +120 -0
- package/dist/unity/globals.d.ts +93 -0
- package/dist/unity/globals.d.ts.map +1 -0
- package/dist/unity/globals.js +340 -0
- package/dist/unity/host.d.ts +137 -0
- package/dist/unity/host.d.ts.map +1 -0
- package/dist/unity/host.js +489 -0
- package/dist/unity/looks.d.ts +88 -0
- package/dist/unity/looks.d.ts.map +1 -0
- package/dist/unity/looks.js +295 -0
- package/dist/unity/skin.d.ts +9 -0
- package/dist/unity/skin.d.ts.map +1 -0
- package/dist/unity/skin.js +117 -0
- package/dist/unity/stage.d.ts +148 -0
- package/dist/unity/stage.d.ts.map +1 -0
- package/dist/unity/stage.js +375 -0
- package/dist/unity/state.d.ts +22 -0
- package/dist/unity/state.d.ts.map +1 -0
- package/dist/unity/state.js +170 -0
- package/package.json +1 -1
- package/src/engine.ts +504 -176
- package/src/graph/compile.ts +3 -1
- package/src/graph/registry.ts +142 -0
- package/src/graph/schema.ts +9 -0
- package/src/graph/slots.ts +11 -20
- package/src/index.ts +11 -0
- package/src/shaders/lights.ts +16 -3
- package/src/shaders/materials/common.ts +63 -57
- package/src/shaders/materials/nodes.ts +210 -0
- package/src/shaders/passes/composite.ts +13 -1
- package/src/shaders/passes/cull.ts +7 -3
- package/src/shaders/passes/ground.ts +32 -55
- package/src/shaders/passes/particles.ts +19 -5
- package/src/shaders/scene-light-api.ts +59 -46
- package/src/shadow-cascades.ts +61 -32
- package/src/unity/character.ts +169 -0
- package/src/unity/globals.ts +443 -0
- package/src/unity/host.ts +587 -0
- package/src/unity/looks.ts +372 -0
- package/src/unity/skin.ts +128 -0
- package/src/unity/stage.ts +568 -0
- 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
|
+
}
|