@plasius/gpu-renderer 0.2.45 → 0.2.46
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/CHANGELOG.md +22 -0
- package/README.md +36 -0
- package/dist/index.cjs +2386 -1709
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2385 -1709
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/index.d.ts +39 -0
- package/src/index.js +1 -0
- package/src/native-scene-renderer.js +550 -0
- package/src/native-scene-shader.js +178 -0
- package/src/renderer-webgpu-runtime.js +24 -19
- package/src/wavefront-adaptive-byte-constants.js +28 -0
- package/src/wavefront-adaptive-metadata.js +185 -0
- package/src/wavefront-adaptive-shader.js +28 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Raster lighting intentionally differs from the wavefront path tracer. It makes
|
|
2
|
+
// no claim of global illumination or ray tracing. All colours remain linear until
|
|
3
|
+
// presentation; the generated harbour materials use continuous world-space detail.
|
|
4
|
+
export const nativeSceneShader = /* wgsl */ `
|
|
5
|
+
struct Frame {
|
|
6
|
+
eye: vec4f, right: vec4f, up: vec4f, forward: vec4f,
|
|
7
|
+
settings: vec4f, sun: vec4f,
|
|
8
|
+
lightRight: vec4f, lightUp: vec4f, lightForward: vec4f, lightOrigin: vec4f,
|
|
9
|
+
viewport: vec4f, wakes: array<vec4f, 4>, info: vec4f,
|
|
10
|
+
};
|
|
11
|
+
@group(0) @binding(0) var<uniform> frame: Frame;
|
|
12
|
+
@group(1) @binding(0) var shadowMap: texture_depth_2d;
|
|
13
|
+
@group(1) @binding(1) var shadowSampler: sampler_comparison;
|
|
14
|
+
@group(2) @binding(0) var reflectionMap: texture_2d<f32>;
|
|
15
|
+
@group(2) @binding(1) var reflectionSampler: sampler;
|
|
16
|
+
struct Surface {
|
|
17
|
+
@builtin(position) clip: vec4f,
|
|
18
|
+
@location(0) world: vec3f, @location(1) normal: vec3f,
|
|
19
|
+
@location(2) colour: vec3f, @location(3) material: vec3f,
|
|
20
|
+
};
|
|
21
|
+
fn project(p: vec3f) -> vec4f {
|
|
22
|
+
let d = p - frame.eye.xyz;
|
|
23
|
+
let z = dot(d, frame.forward.xyz);
|
|
24
|
+
return vec4f(dot(d, frame.right.xyz) / (frame.settings.x * frame.settings.y),
|
|
25
|
+
dot(d, frame.up.xyz) / frame.settings.x, (z - 0.08) * 500.0 / 499.92, z);
|
|
26
|
+
}
|
|
27
|
+
fn lightProject(p: vec3f) -> vec4f {
|
|
28
|
+
let d = p - frame.lightOrigin.xyz;
|
|
29
|
+
return vec4f(dot(d, frame.lightRight.xyz) / 34.0,
|
|
30
|
+
dot(d, frame.lightUp.xyz) / 34.0, dot(d, frame.lightForward.xyz) / 100.0, 1.0);
|
|
31
|
+
}
|
|
32
|
+
@vertex fn surfaceVertex(@location(0) p: vec3f, @location(1) n: vec3f,
|
|
33
|
+
@location(2) c: vec3f, @location(3) m: vec3f) -> Surface {
|
|
34
|
+
return Surface(project(p), p, n, c, m);
|
|
35
|
+
}
|
|
36
|
+
@vertex fn shadowVertex(@location(0) p: vec3f) -> @builtin(position) vec4f {
|
|
37
|
+
return lightProject(p);
|
|
38
|
+
}
|
|
39
|
+
@vertex fn fullscreenVertex(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
40
|
+
let positions = array<vec2f, 3>(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
|
|
41
|
+
return vec4f(positions[i], 1, 1);
|
|
42
|
+
}
|
|
43
|
+
fn cameraRay(pixel: vec2f) -> vec3f {
|
|
44
|
+
let uv = pixel / frame.viewport.xy * 2.0 - 1.0;
|
|
45
|
+
return normalize(frame.forward.xyz + frame.right.xyz * uv.x * frame.settings.x * frame.settings.y
|
|
46
|
+
- frame.up.xyz * uv.y * frame.settings.x);
|
|
47
|
+
}
|
|
48
|
+
fn display(c: vec3f) -> vec3f {
|
|
49
|
+
let x = max(c * frame.settings.w, vec3f(0));
|
|
50
|
+
let mapped = clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), vec3f(0), vec3f(1));
|
|
51
|
+
return pow(mapped, vec3f(1.0 / 2.2));
|
|
52
|
+
}
|
|
53
|
+
fn hash(p: vec3f) -> f32 { return fract(sin(dot(p, vec3f(127.1, 311.7, 74.7))) * 43758.5453); }
|
|
54
|
+
fn noise(p: vec3f) -> f32 {
|
|
55
|
+
let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
|
|
56
|
+
return mix(mix(mix(hash(i), hash(i + vec3f(1,0,0)), u.x),
|
|
57
|
+
mix(hash(i + vec3f(0,1,0)), hash(i + vec3f(1,1,0)), u.x), u.y),
|
|
58
|
+
mix(mix(hash(i + vec3f(0,0,1)), hash(i + vec3f(1,0,1)), u.x),
|
|
59
|
+
mix(hash(i + vec3f(0,1,1)), hash(i + vec3f(1,1,1)), u.x), u.y), u.z);
|
|
60
|
+
}
|
|
61
|
+
fn sky(ray: vec3f) -> vec3f {
|
|
62
|
+
let elevation = pow(clamp(ray.y * 0.7 + 0.16, 0.0, 1.0), 0.6);
|
|
63
|
+
var c = mix(vec3f(0.42, 0.53, 0.61), vec3f(0.07, 0.19, 0.37), elevation);
|
|
64
|
+
let sunAngle = dot(ray, frame.sun.xyz);
|
|
65
|
+
c += vec3f(1.0, 0.73, 0.43) * pow(max(sunAngle, 0.0), 64.0) * 0.35;
|
|
66
|
+
c += vec3f(4.0, 3.2, 2.1) * smoothstep(0.99965, 0.99985, sunAngle);
|
|
67
|
+
let cloudPoint = ray / max(0.12, ray.y) * 2.1 + vec3f(0, 0, 8);
|
|
68
|
+
let cloud = smoothstep(0.49, 0.77, noise(cloudPoint) * 0.65 + noise(cloudPoint * 2.1) * 0.35);
|
|
69
|
+
c = mix(c, vec3f(0.85, 0.86, 0.82), cloud * smoothstep(0.03, 0.35, ray.y) * 0.65);
|
|
70
|
+
return c;
|
|
71
|
+
}
|
|
72
|
+
@fragment fn skyFragment(@builtin(position) pixel: vec4f) -> @location(0) vec4f {
|
|
73
|
+
return vec4f(display(sky(cameraRay(pixel.xy))), 1);
|
|
74
|
+
}
|
|
75
|
+
fn visibility(p: vec3f, n: vec3f) -> f32 {
|
|
76
|
+
let q = lightProject(p + n * 0.035).xyz;
|
|
77
|
+
let uv = vec2f(q.x * 0.5 + 0.5, 0.5 - q.y * 0.5);
|
|
78
|
+
if (any(uv < vec2f(0.005)) || any(uv > vec2f(0.995)) || q.z < 0.0 || q.z > 1.0) { return 1.0; }
|
|
79
|
+
var result = 0.0;
|
|
80
|
+
for (var y = -1; y <= 1; y++) {
|
|
81
|
+
for (var x = -1; x <= 1; x++) {
|
|
82
|
+
result += textureSampleCompareLevel(shadowMap, shadowSampler, uv + vec2f(f32(x), f32(y)) / 2048.0, q.z - 0.0006);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return result / 9.0;
|
|
86
|
+
}
|
|
87
|
+
fn brdf(c: vec3f, n: vec3f, v: vec3f, roughness: f32, metal: f32) -> vec3f {
|
|
88
|
+
let l = frame.sun.xyz; let h = normalize(l + v);
|
|
89
|
+
let nl = max(dot(n,l),0.0); let nv = max(dot(n,v),0.001);
|
|
90
|
+
let nh = max(dot(n,h),0.0); let vh = max(dot(v,h),0.0);
|
|
91
|
+
let a = max(0.035, roughness * roughness); let a2 = a * a;
|
|
92
|
+
let d = a2 / max(0.00001, 3.14159265 * pow(nh * nh * (a2 - 1.0) + 1.0, 2.0));
|
|
93
|
+
let k = pow(roughness + 1.0, 2.0) / 8.0;
|
|
94
|
+
let g = (nl / (nl * (1.0-k) + k)) * (nv / (nv * (1.0-k) + k));
|
|
95
|
+
let f0 = mix(vec3f(0.04), c, metal);
|
|
96
|
+
let f = f0 + (1.0-f0) * pow(1.0-vh, 5.0);
|
|
97
|
+
return ((1.0-f) * (1.0-metal) * c / 3.14159265 + d * g * f / max(0.001, 4.0*nl*nv)) * nl;
|
|
98
|
+
}
|
|
99
|
+
fn fog(c: vec3f, p: vec3f) -> vec3f {
|
|
100
|
+
let amount = 1.0 - exp(-distance(frame.eye.xyz, p) * 0.0018);
|
|
101
|
+
return mix(c, vec3f(0.32, 0.44, 0.55), amount);
|
|
102
|
+
}
|
|
103
|
+
fn surfaceRadiance(s: Surface, front: bool) -> vec3f {
|
|
104
|
+
let facing = select(select(-1.0,1.0,front),select(1.0,-1.0,front),frame.info.y>0.5);
|
|
105
|
+
var n = normalize(s.normal) * facing;
|
|
106
|
+
let v = normalize(frame.eye.xyz - s.world);
|
|
107
|
+
var c = s.colour;
|
|
108
|
+
if (s.material.z > 2.5) {
|
|
109
|
+
let rock = smoothstep(0.45,0.68,noise(s.world*0.27));
|
|
110
|
+
c = mix(c,vec3f(0.16,0.145,0.11),rock*0.7);
|
|
111
|
+
c *= 0.75+0.45*noise(s.world*2.4);
|
|
112
|
+
n = normalize(n+vec3f(noise(s.world*4.0)-0.5,0,noise(s.world*4.0+vec3f(13))-0.5)*0.28);
|
|
113
|
+
} else if (s.material.z > 0.5 && s.material.z < 1.5) {
|
|
114
|
+
let grain = noise(s.world * vec3f(18, 3, 1.6));
|
|
115
|
+
c *= 0.82 + 0.30 * grain;
|
|
116
|
+
} else if (s.material.z > 1.5) { c *= 0.88 + 0.18 * noise(s.world * 12.0); }
|
|
117
|
+
let hemi = mix(vec3f(0.06,0.05,0.035), vec3f(0.16,0.22,0.29), n.y * 0.5 + 0.5);
|
|
118
|
+
let direct = brdf(c, n, v, s.material.x, s.material.y) * vec3f(3.0, 2.65, 2.15) * visibility(s.world, n);
|
|
119
|
+
let ambient = c * hemi * (1.0 - s.material.y * 0.7);
|
|
120
|
+
let env = sky(reflect(-v,n)) * mix(vec3f(0.04), c, s.material.y) * (1.0-s.material.x) * 0.30;
|
|
121
|
+
return fog(direct + ambient + env, s.world);
|
|
122
|
+
}
|
|
123
|
+
@fragment fn surfaceFragment(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
|
|
124
|
+
return vec4f(display(surfaceRadiance(s,front)),1);
|
|
125
|
+
}
|
|
126
|
+
fn wave(p: vec2f) -> f32 {
|
|
127
|
+
let t = frame.settings.z;
|
|
128
|
+
return sin(dot(p,vec2f(0.35,0.23))-t*0.85)*0.13
|
|
129
|
+
+ sin(dot(p,vec2f(-0.61,0.42))+t*1.12)*0.065
|
|
130
|
+
+ sin(dot(p,vec2f(1.2,0.83))-t*1.4)*0.025
|
|
131
|
+
+ sin(dot(p,vec2f(3.1,-2.1))+t*1.8)*0.009;
|
|
132
|
+
}
|
|
133
|
+
struct WaterResult { @location(0) colour: vec4f, @builtin(frag_depth) depth: f32 };
|
|
134
|
+
@fragment fn waterFragment(@builtin(position) pixel: vec4f) -> WaterResult {
|
|
135
|
+
let ray = cameraRay(pixel.xy);
|
|
136
|
+
if (ray.y > -0.001) { discard; }
|
|
137
|
+
var dist = (frame.viewport.z - frame.eye.y) / ray.y;
|
|
138
|
+
var p = frame.eye.xyz + ray * dist;
|
|
139
|
+
for (var i=0; i<3; i++) {
|
|
140
|
+
dist = (frame.viewport.z + wave(p.xz) - frame.eye.y) / ray.y;
|
|
141
|
+
p = frame.eye.xyz + ray * dist;
|
|
142
|
+
}
|
|
143
|
+
if (dist < 0.08 || dist > 499.0) { discard; }
|
|
144
|
+
let eps = 0.03;
|
|
145
|
+
let waveNormal = normalize(vec3f((wave(p.xz-vec2f(eps,0))-wave(p.xz+vec2f(eps,0)))/(2.0*eps),
|
|
146
|
+
1.0, (wave(p.xz-vec2f(0,eps))-wave(p.xz+vec2f(0,eps)))/(2.0*eps)));
|
|
147
|
+
let n = normalize(mix(waveNormal,vec3f(0,1,0),smoothstep(35.0,150.0,dist)));
|
|
148
|
+
let v = -ray;
|
|
149
|
+
let fresnel = 0.02 + 0.98 * pow(1.0-max(dot(n,v),0.0),5.0);
|
|
150
|
+
let uv = clamp(pixel.xy / frame.viewport.xy + n.xz * 0.018, vec2f(0.002), vec2f(0.998));
|
|
151
|
+
// Reflection target is HDR linear; tone mapping happens once after mixing.
|
|
152
|
+
let reflected = textureSampleLevel(reflectionMap, reflectionSampler, uv, 0).rgb;
|
|
153
|
+
let shadow = visibility(p, n);
|
|
154
|
+
var c = mix(vec3f(0.008,0.05,0.065) * (0.5+0.5*shadow), reflected, fresnel);
|
|
155
|
+
c += brdf(vec3f(0.01),n,v,0.14,0.0) * vec3f(2.5,2.15,1.7) * shadow;
|
|
156
|
+
var foam = 0.0;
|
|
157
|
+
for (var i=0u; i<4u; i++) {
|
|
158
|
+
if (f32(i) >= frame.info.x) { break; }
|
|
159
|
+
let w = frame.wakes[i]; let delta = p.xz - w.xy;
|
|
160
|
+
let along = dot(delta, vec2f(sin(w.z), cos(w.z)));
|
|
161
|
+
let across = abs(dot(delta,vec2f(cos(w.z),-sin(w.z))));
|
|
162
|
+
let ribbon = exp(-pow((across - max(0.0,-along)*0.28 - 0.65)*4.0,2.0));
|
|
163
|
+
foam += ribbon * (1.0-smoothstep(-1.0,0.5,along)) * exp(min(0.0,along)*0.25) * (0.4+0.6*noise(vec3f(p.x*5.0,p.z*5.0,frame.settings.z*0.5)));
|
|
164
|
+
}
|
|
165
|
+
c = mix(c,vec3f(0.48,0.61,0.62),clamp(foam*0.42,0.0,0.55));
|
|
166
|
+
let clip = project(p);
|
|
167
|
+
return WaterResult(vec4f(display(fog(c,p)),1),clip.z/clip.w);
|
|
168
|
+
}
|
|
169
|
+
// Keep reflection radiance linear, including sky. Reusing the lighting calculation
|
|
170
|
+
// rather than sampling a screenshot avoids double tone mapping on the water.
|
|
171
|
+
@fragment fn reflectionSky(@builtin(position) pixel: vec4f) -> @location(0) vec4f {
|
|
172
|
+
return vec4f(sky(cameraRay(pixel.xy)),1);
|
|
173
|
+
}
|
|
174
|
+
@fragment fn reflectionSurface(s: Surface, @builtin(front_facing) front: bool) -> @location(0) vec4f {
|
|
175
|
+
if (s.world.y < frame.viewport.z) { discard; }
|
|
176
|
+
return vec4f(surfaceRadiance(s,front),1);
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
@@ -260,6 +260,7 @@ export async function createGpuRenderer(options = {}) {
|
|
|
260
260
|
cancelAnimationFrame = globalThis.cancelAnimationFrame?.bind(globalThis),
|
|
261
261
|
frameIdFactory,
|
|
262
262
|
onFrameStart,
|
|
263
|
+
encodeFrame,
|
|
263
264
|
onBeforeEncode,
|
|
264
265
|
onAfterSubmit,
|
|
265
266
|
onFrameComplete,
|
|
@@ -337,26 +338,30 @@ export async function createGpuRenderer(options = {}) {
|
|
|
337
338
|
});
|
|
338
339
|
const view = texture.createView();
|
|
339
340
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
341
|
+
if (typeof encodeFrame === "function") {
|
|
342
|
+
encodeFrame({ device, context, encoder, view, canvas: targetCanvas, timestamp });
|
|
343
|
+
} else {
|
|
344
|
+
const pass = encoder.beginRenderPass(createRenderPassDescriptor(view, clear));
|
|
345
|
+
|
|
346
|
+
if (typeof onBeforeEncode === "function") {
|
|
347
|
+
onBeforeEncode({
|
|
348
|
+
frame,
|
|
349
|
+
frameNumber,
|
|
350
|
+
frameId,
|
|
351
|
+
frameTimeMs,
|
|
352
|
+
timestamp,
|
|
353
|
+
device,
|
|
354
|
+
context,
|
|
355
|
+
encoder,
|
|
356
|
+
pass,
|
|
357
|
+
canvas: targetCanvas,
|
|
358
|
+
xrActive,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
357
361
|
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
if (typeof pass.end === "function") {
|
|
363
|
+
pass.end();
|
|
364
|
+
}
|
|
360
365
|
}
|
|
361
366
|
|
|
362
367
|
const commandBuffer = encoder.finish();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated from final adaptive WGSL by @plasius/gpu-shader; checked by tests.
|
|
2
|
+
// Source SHA-256: 2d2782a19c480564ae5bde52123e2dc8de4db3fcab818e26cc7ad743c6934821
|
|
3
|
+
export const ADAPTIVE_DISPATCH_ARGUMENTS_ALIGNMENT = 4;
|
|
4
|
+
export const ADAPTIVE_DISPATCH_ARGUMENTS_BYTE_SIZE = 12;
|
|
5
|
+
export const ADAPTIVE_DISPATCH_ARGUMENTS_MINIMUM_BYTE_SIZE = 12;
|
|
6
|
+
export const ADAPTIVE_DISPATCH_ARGUMENTS_X_OFFSET = 0;
|
|
7
|
+
export const ADAPTIVE_DISPATCH_ARGUMENTS_Y_OFFSET = 4;
|
|
8
|
+
export const ADAPTIVE_DISPATCH_ARGUMENTS_Z_OFFSET = 8;
|
|
9
|
+
export const ADAPTIVE_FIRST_HIT_DISTANCE_ALIGNMENT = 4;
|
|
10
|
+
export const ADAPTIVE_FIRST_HIT_DISTANCE_BYTE_SIZE = 4;
|
|
11
|
+
export const ADAPTIVE_FIRST_HIT_DISTANCE_MINIMUM_BYTE_SIZE = 4;
|
|
12
|
+
export const ADAPTIVE_FIRST_HIT_DISTANCE_DISTANCE_OFFSET = 0;
|
|
13
|
+
export const ADAPTIVE_HISTORY_WORD_ALIGNMENT = 4;
|
|
14
|
+
export const ADAPTIVE_HISTORY_WORD_BYTE_SIZE = 4;
|
|
15
|
+
export const ADAPTIVE_HISTORY_WORD_MINIMUM_BYTE_SIZE = 4;
|
|
16
|
+
export const ADAPTIVE_HISTORY_WORD_MASK_OFFSET = 0;
|
|
17
|
+
export const ADAPTIVE_NORMAL_MATERIAL_RISK_ALIGNMENT = 4;
|
|
18
|
+
export const ADAPTIVE_NORMAL_MATERIAL_RISK_BYTE_SIZE = 4;
|
|
19
|
+
export const ADAPTIVE_NORMAL_MATERIAL_RISK_MINIMUM_BYTE_SIZE = 4;
|
|
20
|
+
export const ADAPTIVE_NORMAL_MATERIAL_RISK_WORD_OFFSET = 0;
|
|
21
|
+
export const ADAPTIVE_PIXEL_STATE_ALIGNMENT = 4;
|
|
22
|
+
export const ADAPTIVE_PIXEL_STATE_BYTE_SIZE = 4;
|
|
23
|
+
export const ADAPTIVE_PIXEL_STATE_MINIMUM_BYTE_SIZE = 4;
|
|
24
|
+
export const ADAPTIVE_PIXEL_STATE_WORD_OFFSET = 0;
|
|
25
|
+
export const ADAPTIVE_WORKLIST_ENTRY_ALIGNMENT = 4;
|
|
26
|
+
export const ADAPTIVE_WORKLIST_ENTRY_BYTE_SIZE = 4;
|
|
27
|
+
export const ADAPTIVE_WORKLIST_ENTRY_MINIMUM_BYTE_SIZE = 4;
|
|
28
|
+
export const ADAPTIVE_WORKLIST_ENTRY_LOCAL_PIXEL_ID_OFFSET = 0;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ADAPTIVE_COUNT_BITS, ADAPTIVE_COUNT_MASK, ADAPTIVE_FLAG_SHIFT,
|
|
3
|
+
ADAPTIVE_MAX_FLAGS, ADAPTIVE_MAX_SAMPLES,
|
|
4
|
+
} from "./wavefront-adaptive-shader.js";
|
|
5
|
+
import {
|
|
6
|
+
ADAPTIVE_PIXEL_STATE_BYTE_SIZE, ADAPTIVE_FIRST_HIT_DISTANCE_BYTE_SIZE,
|
|
7
|
+
ADAPTIVE_NORMAL_MATERIAL_RISK_BYTE_SIZE, ADAPTIVE_WORKLIST_ENTRY_BYTE_SIZE,
|
|
8
|
+
ADAPTIVE_HISTORY_WORD_BYTE_SIZE, ADAPTIVE_DISPATCH_ARGUMENTS_BYTE_SIZE,
|
|
9
|
+
} from "./wavefront-adaptive-byte-constants.js";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_ADAPTIVE_ALLOCATION_CAP_BYTES = 128 * 1024 ** 2;
|
|
12
|
+
const MAX_TILE_PIXELS = 16384;
|
|
13
|
+
const U32_MAX = 0xffffffff;
|
|
14
|
+
|
|
15
|
+
function integer(name, value, minimum, maximum) {
|
|
16
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
17
|
+
throw new RangeError(`${name} must be an integer in ${minimum}..${maximum}.`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function packAdaptivePixelState({ requested, completed = 0, flags = 0 }) {
|
|
23
|
+
integer("requested", requested, 0, ADAPTIVE_MAX_SAMPLES);
|
|
24
|
+
integer("completed", completed, 0, requested);
|
|
25
|
+
integer("flags", flags, 0, ADAPTIVE_MAX_FLAGS);
|
|
26
|
+
return (requested | (completed << ADAPTIVE_COUNT_BITS) | (flags << ADAPTIVE_FLAG_SHIFT)) >>> 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function unpackAdaptivePixelState(word) {
|
|
30
|
+
integer("packed adaptive word", word, 0, U32_MAX);
|
|
31
|
+
const state = {
|
|
32
|
+
requested: word & ADAPTIVE_COUNT_MASK,
|
|
33
|
+
completed: (word >>> ADAPTIVE_COUNT_BITS) & ADAPTIVE_COUNT_MASK,
|
|
34
|
+
flags: word >>> ADAPTIVE_FLAG_SHIFT,
|
|
35
|
+
};
|
|
36
|
+
// Do not silently accept unused nine-bit values or impossible completion.
|
|
37
|
+
packAdaptivePixelState(state);
|
|
38
|
+
return Object.freeze(state);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function refuse(plan, reason) {
|
|
42
|
+
return Object.freeze({ ...plan, enabled: false, reason });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function admitDevice(plan, limits) {
|
|
46
|
+
if (!plan.enabled) return plan;
|
|
47
|
+
const maxBufferSize = limits?.maxBufferSize;
|
|
48
|
+
const maxBindingSize = limits?.maxStorageBufferBindingSize;
|
|
49
|
+
if (![maxBufferSize, maxBindingSize].every((value) => Number.isSafeInteger(value) && value > 0)) {
|
|
50
|
+
return refuse(plan, "adaptive-device-limits-unavailable");
|
|
51
|
+
}
|
|
52
|
+
if (plan.buffers.some(({ size }) => size > maxBufferSize || size > maxBindingSize)) {
|
|
53
|
+
return refuse(plan, "adaptive-device-buffer-limit");
|
|
54
|
+
}
|
|
55
|
+
return plan;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function planAdaptiveResources(options = {}, limits) {
|
|
59
|
+
const bytes = {
|
|
60
|
+
pixelState: 0, firstHitDistance: 0, normalMaterialRisk: 0,
|
|
61
|
+
worklist: 0, dispatch: 0, history: 0, total: 0,
|
|
62
|
+
};
|
|
63
|
+
if (options.enabled !== true) {
|
|
64
|
+
return Object.freeze({ enabled: false, reason: "adaptive-disabled",
|
|
65
|
+
bytes: Object.freeze(bytes), buffers: Object.freeze([]) });
|
|
66
|
+
}
|
|
67
|
+
for (const key of ["firstHitDistance", "normalMaterialRisk", "history"]) {
|
|
68
|
+
if (options[key] !== undefined && typeof options[key] !== "boolean") {
|
|
69
|
+
throw new TypeError(`${key} must be a boolean.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const width = integer("width", options.width, 1, U32_MAX);
|
|
73
|
+
const height = integer("height", options.height, 1, U32_MAX);
|
|
74
|
+
const pixelCount = integer("pixelCount", width * height, 1, U32_MAX);
|
|
75
|
+
const tilePixelCapacity = integer("tilePixelCapacity",
|
|
76
|
+
options.tilePixelCapacity === undefined ? MAX_TILE_PIXELS : options.tilePixelCapacity, 1, MAX_TILE_PIXELS);
|
|
77
|
+
const cap = integer("maximumAllocationBytes", options.maximumAllocationBytes === undefined
|
|
78
|
+
? DEFAULT_ADAPTIVE_ALLOCATION_CAP_BYTES : options.maximumAllocationBytes, 1, Number.MAX_SAFE_INTEGER);
|
|
79
|
+
bytes.pixelState = pixelCount * ADAPTIVE_PIXEL_STATE_BYTE_SIZE;
|
|
80
|
+
bytes.firstHitDistance = options.firstHitDistance === true ? pixelCount * ADAPTIVE_FIRST_HIT_DISTANCE_BYTE_SIZE : 0;
|
|
81
|
+
bytes.normalMaterialRisk = options.normalMaterialRisk === true ? pixelCount * ADAPTIVE_NORMAL_MATERIAL_RISK_BYTE_SIZE : 0;
|
|
82
|
+
bytes.worklist = tilePixelCapacity * ADAPTIVE_WORKLIST_ENTRY_BYTE_SIZE;
|
|
83
|
+
bytes.dispatch = ADAPTIVE_DISPATCH_ARGUMENTS_BYTE_SIZE;
|
|
84
|
+
bytes.history = options.history === true ? 2 * Math.ceil(pixelCount / 32) * ADAPTIVE_HISTORY_WORD_BYTE_SIZE : 0;
|
|
85
|
+
bytes.total = Object.values(bytes).reduce((sum, value) => sum + value, 0);
|
|
86
|
+
const buffers = ["pixelState", "firstHitDistance", "normalMaterialRisk", "worklist", "dispatch"]
|
|
87
|
+
.filter((key) => bytes[key] > 0)
|
|
88
|
+
.map((key) => Object.freeze({ key, size: bytes[key], indirect: key === "dispatch" }));
|
|
89
|
+
if (bytes.history) {
|
|
90
|
+
for (const key of ["previousHistory", "currentHistory"]) {
|
|
91
|
+
buffers.push(Object.freeze({ key, size: bytes.history / 2, indirect: false }));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const plan = Object.freeze({ enabled: true, reason: null, pixelCount, tilePixelCapacity,
|
|
95
|
+
maximumAllocationBytes: cap, bytes: Object.freeze(bytes), buffers: Object.freeze(buffers) });
|
|
96
|
+
if (bytes.total > cap) return refuse(plan, "adaptive-allocation-cap");
|
|
97
|
+
return limits === undefined ? plan : admitDevice(plan, limits);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createAdaptiveResourceOwner(device, usage, options = {}) {
|
|
101
|
+
// Snapshot configuration now; a caller cannot expand the allocation while it is pending.
|
|
102
|
+
let plan = planAdaptiveResources(options);
|
|
103
|
+
let disposed = false;
|
|
104
|
+
let pending = null;
|
|
105
|
+
let result = null;
|
|
106
|
+
let allocatedBytes = 0;
|
|
107
|
+
const buffers = new Map();
|
|
108
|
+
|
|
109
|
+
function cleanup() {
|
|
110
|
+
for (const buffer of buffers.values()) buffer.destroy();
|
|
111
|
+
buffers.clear();
|
|
112
|
+
allocatedBytes = 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function disabled(reason) {
|
|
116
|
+
result = Object.freeze({ status: "disabled", reason, buffers: Object.freeze({}), allocatedBytes: 0 });
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function allocate() {
|
|
121
|
+
if (disposed) return disabled("adaptive-disposed");
|
|
122
|
+
if (!plan.enabled) return disabled(plan.reason);
|
|
123
|
+
plan = admitDevice(plan, device?.limits);
|
|
124
|
+
if (!plan.enabled) return disabled(plan.reason);
|
|
125
|
+
if (typeof device.pushErrorScope !== "function" || typeof device.popErrorScope !== "function") {
|
|
126
|
+
return disabled("adaptive-error-scopes-unavailable");
|
|
127
|
+
}
|
|
128
|
+
let scopeCount = 0;
|
|
129
|
+
let failed = false;
|
|
130
|
+
try {
|
|
131
|
+
for (const key of ["STORAGE", "COPY_DST", "INDIRECT"]) integer(`GPUBufferUsage.${key}`, usage?.[key], 1, U32_MAX);
|
|
132
|
+
device.pushErrorScope("out-of-memory");
|
|
133
|
+
scopeCount += 1;
|
|
134
|
+
device.pushErrorScope("validation");
|
|
135
|
+
scopeCount += 1;
|
|
136
|
+
for (const descriptor of plan.buffers) {
|
|
137
|
+
const buffer = device.createBuffer({
|
|
138
|
+
label: `wavefront-adaptive-${descriptor.key}`,
|
|
139
|
+
size: descriptor.size,
|
|
140
|
+
usage: usage.STORAGE | usage.COPY_DST | (descriptor.indirect ? usage.INDIRECT : 0),
|
|
141
|
+
});
|
|
142
|
+
buffers.set(descriptor.key, buffer);
|
|
143
|
+
allocatedBytes += descriptor.size;
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
failed = true;
|
|
147
|
+
}
|
|
148
|
+
// Pop both scopes synchronously before awaiting: scopes are a device-wide stack.
|
|
149
|
+
const completions = [];
|
|
150
|
+
while (scopeCount > 0) {
|
|
151
|
+
scopeCount -= 1;
|
|
152
|
+
try { completions.push(Promise.resolve(device.popErrorScope())); }
|
|
153
|
+
catch { failed = true; }
|
|
154
|
+
}
|
|
155
|
+
const errors = await Promise.allSettled(completions);
|
|
156
|
+
failed ||= errors.some((entry) => entry.status === "rejected" || entry.value !== null);
|
|
157
|
+
if (failed || disposed) {
|
|
158
|
+
cleanup();
|
|
159
|
+
return disabled(disposed ? "adaptive-disposed" : "adaptive-allocation-failed");
|
|
160
|
+
}
|
|
161
|
+
result = Object.freeze({ status: "ready", reason: null,
|
|
162
|
+
buffers: Object.freeze(Object.fromEntries(buffers)), allocatedBytes });
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function acquire() {
|
|
167
|
+
if (disposed && !pending) return Promise.resolve(disabled("adaptive-disposed"));
|
|
168
|
+
if (result) return Promise.resolve(result);
|
|
169
|
+
// No automatic retry after allocation failure: the caller owns recovery.
|
|
170
|
+
pending ??= allocate().finally(() => { pending = null; });
|
|
171
|
+
return pending;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return Object.freeze({
|
|
175
|
+
acquire,
|
|
176
|
+
snapshot: () => Object.freeze({ plan, allocatedBytes, disposed,
|
|
177
|
+
status: disposed ? "disposed" : result?.status ?? "unallocated",
|
|
178
|
+
reason: disposed ? "adaptive-disposed" : result?.reason ?? plan.reason }),
|
|
179
|
+
destroy() {
|
|
180
|
+
if (disposed) return;
|
|
181
|
+
disposed = true;
|
|
182
|
+
cleanup();
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Bit semantics are shared by CPU packing and final assembled WGSL.
|
|
2
|
+
export const ADAPTIVE_COUNT_BITS = 9;
|
|
3
|
+
export const ADAPTIVE_COUNT_MASK = (1 << ADAPTIVE_COUNT_BITS) - 1;
|
|
4
|
+
export const ADAPTIVE_FLAG_SHIFT = ADAPTIVE_COUNT_BITS * 2;
|
|
5
|
+
export const ADAPTIVE_MAX_FLAGS = 2 ** (32 - ADAPTIVE_FLAG_SHIFT) - 1;
|
|
6
|
+
export const ADAPTIVE_MAX_SAMPLES = 256;
|
|
7
|
+
|
|
8
|
+
export const ADAPTIVE_METADATA_WGSL = `
|
|
9
|
+
const ADAPTIVE_COUNT_MASK: u32 = ${ADAPTIVE_COUNT_MASK}u;
|
|
10
|
+
const ADAPTIVE_COMPLETED_SHIFT: u32 = ${ADAPTIVE_COUNT_BITS}u;
|
|
11
|
+
|
|
12
|
+
struct AdaptivePixelState { word: u32, };
|
|
13
|
+
struct AdaptivePixelStates { records: array<AdaptivePixelState>, };
|
|
14
|
+
struct AdaptiveFirstHitDistance { distance: f32, };
|
|
15
|
+
struct AdaptiveNormalMaterialRisk { word: u32, };
|
|
16
|
+
struct AdaptiveWorklistEntry { localPixelId: u32, };
|
|
17
|
+
struct AdaptiveHistoryWord { mask: u32, };
|
|
18
|
+
struct AdaptiveDispatchArguments { x: u32, y: u32, z: u32, };
|
|
19
|
+
|
|
20
|
+
@group(0) @binding(0) var<storage, read_write> pixels: AdaptivePixelStates;
|
|
21
|
+
|
|
22
|
+
@compute @workgroup_size(64)
|
|
23
|
+
fn reset_adaptive_counts(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
24
|
+
if (id.x >= arrayLength(&pixels.records)) { return; }
|
|
25
|
+
// Budgets were selected before contributing samples. Preserve them and flags.
|
|
26
|
+
pixels.records[id.x].word &= ~(ADAPTIVE_COUNT_MASK << ADAPTIVE_COMPLETED_SHIFT);
|
|
27
|
+
}
|
|
28
|
+
`;
|