1use bytemuck::{Pod, Zeroable};
2use pfx_bake::detail::atlas::{Atlas, Format, Texture, level_size};
3use pfx_gpu::wgpu;
4use pfx_load::{ColorSpace, Image, Pixels};
5use pfx_materials::{
6 CoatWobble, GPU_PARAMS, LAYER_CAP, Material, NoiseKind, NoiseLayer, NormalSource, PARAM_ROWS,
7 encode_channel, linear_channel,
8};
9
10use crate::{lacquer, noise_tiles};
11
12pub const ANISOTROPY: u16 = 16;
13pub const CONTENT_LOD_BIAS: f32 = -0.25;
14pub const DETAIL_LOD_BIAS: f32 = -0.25;
15pub const CONTENT_SLOTS: usize = 4;
16pub const CONTENT_BINDING: u32 = 18;
17pub const CAUSTIC_BINDING: u32 = CONTENT_BINDING + CONTENT_SLOTS as u32 + 5;
18pub const CONTENT_WGSL: &str = r#"
19override use_content: bool = true;
20override use_content_cutout: bool = true;
21@group(3) @binding(18) var content0: texture_2d<f32>;
22@group(3) @binding(19) var content1: texture_2d<f32>;
23@group(3) @binding(20) var content2: texture_2d<f32>;
24@group(3) @binding(21) var content3: texture_2d<f32>;
25@group(3) @binding(22) var content_sampler: sampler;
26struct ContentCaustic {
27 rect: vec4f,
28 plane: vec4f,
29 info: vec4f,
30}
31@group(3) @binding(27) var<uniform> content_caustic: ContentCaustic;
32fn caustic_uv(world: vec3f) -> vec2f {
33 let toward = content_caustic.plane.xyz;
34 let along = (content_caustic.plane.w - world.y) / max(toward.y, 1e-4);
35 let q = world.xz + toward.xz * along;
36 return (q - content_caustic.rect.xy) / content_caustic.rect.zw;
37}
38fn caustic_gain(at: vec2f, dx: vec2f, dy: vec2f) -> vec3f {
39 if (content_caustic.info.z < 0.5 || any(at < vec2f(0.0)) || any(at > vec2f(1.0))) {
40 return vec3f(1.0);
41 }
42 let texel = content_texel(i32(content_caustic.info.x), at, dx, dy).rgb;
43 return max(vec3f(1.0) + texel * content_caustic.info.y, vec3f(0.0));
44}
45fn content_size(slot: i32) -> vec2f {
46 switch slot {
47 case 0: { return vec2f(textureDimensions(content0)); }
48 case 1: { return vec2f(textureDimensions(content1)); }
49 case 2: { return vec2f(textureDimensions(content2)); }
50 case 3: { return vec2f(textureDimensions(content3)); }
51 default: { return vec2f(1.0); }
52 }
53}
54fn content_texel(slot: i32, at: vec2f, dx: vec2f, dy: vec2f) -> vec4f {
55 switch slot {
56 case 0: { return textureSampleGrad(content0, content_sampler, at, dx, dy); }
57 case 1: { return textureSampleGrad(content1, content_sampler, at, dx, dy); }
58 case 2: { return textureSampleGrad(content2, content_sampler, at, dx, dy); }
59 case 3: { return textureSampleGrad(content3, content_sampler, at, dx, dy); }
60 default: { return vec4f(0.0); }
61 }
62}
63fn content_alpha(slot: i32, at: vec2f) -> f32 {
64 switch slot {
65 case 0: { return textureSampleLevel(content0, content_sampler, at, 0.0).a; }
66 case 1: { return textureSampleLevel(content1, content_sampler, at, 0.0).a; }
67 case 2: { return textureSampleLevel(content2, content_sampler, at, 0.0).a; }
68 case 3: { return textureSampleLevel(content3, content_sampler, at, 0.0).a; }
69 default: { return 0.0; }
70 }
71}
72fn content_cut(material_index: u32, uv: vec2f, transform: vec4f, crop: vec4f, cutoff: f32) -> bool {
73 let slot = i32(round(map_materials[material_index].content.x));
74 if (slot < 0) {
75 return true;
76 }
77 let at = content_uv(uv, transform);
78 if (!content_inside(at, crop)) {
79 return true;
80 }
81 return content_alpha(slot, at) < cutoff;
82}
83fn content_apply(surface_in: Shaded, material_index: u32, uv: vec2f, uv_dx: vec2f, uv_dy: vec2f, transform: vec4f, crop: vec4f, tangent: vec3f, bitangent: vec3f, geometric: vec3f) -> Shaded {
84 var surface = surface_in;
85 if (!use_content) {
86 return surface;
87 }
88 let layer = map_materials[material_index].content;
89 let slot = i32(round(layer.x));
90 if (slot < 0) {
91 return surface;
92 }
93 let at = content_uv(uv, transform);
94 if (!content_inside(at, crop)) {
95 return surface;
96 }
97 let dx = uv_dx * transform.zw;
98 let dy = uv_dy * transform.zw;
99 let sharpen = map_materials[material_index].content_extra.y;
100 let texel = content_texel(slot, at, dx * sharpen, dy * sharpen);
101 let blend = u32(round(layer.y));
102 var out = content_composite(surface, blend, texel, layer.z, map_materials[material_index].content_extra.x);
103 if (layer.w != 0.0) {
104 let step = max(1.5 / content_size(slot), max(abs(dx), abs(dy)));
105 let ax = content_texel(slot, at + vec2f(step.x, 0.0), dx, dy).a - content_texel(slot, at - vec2f(step.x, 0.0), dx, dy).a;
106 let ay = content_texel(slot, at + vec2f(0.0, step.y), dx, dy).a - content_texel(slot, at - vec2f(0.0, step.y), dx, dy).a;
107 var n = content_emboss(out.normal, tangent, bitangent, vec2f(ax, ay), layer.w);
108 if (dot(n, geometric) < 0.05) {
109 n = normalize(n + geometric * 0.1);
110 }
111 out.normal = n;
112 }
113 return out;
114}
115"#;
116pub const MAPS_WGSL: &str = r#"
117struct MapMaterial {
118 indices: vec4i,
119 factors: vec4f,
120 age_a: vec4f,
121 age_b: vec4f,
122 seeds: vec4u,
123 layer0: vec4f,
124 layer1: vec4f,
125 layer2: vec4f,
126 layer3: vec4f,
127 content: vec4f,
128 content_extra: vec4f,
129 packed: vec4f,
130 atlas: vec4f,
131 params: array<vec4f, 16>,
132}
133struct MapResult {
134 base: vec3f,
135 roughness: f32,
136 metalness: f32,
137 normal: vec3f,
138 coat_roughness: f32,
139 thin_film: f32,
140}
141@group(3) @binding(0) var map_base: texture_2d_array<f32>;
142@group(3) @binding(1) var map_normal: texture_2d_array<f32>;
143@group(3) @binding(2) var map_roughness: texture_2d_array<f32>;
144@group(3) @binding(3) var map_metal: texture_2d_array<f32>;
145@group(3) @binding(4) var map_sampler: sampler;
146@group(3) @binding(5) var<storage, read> map_materials: array<MapMaterial>;
147override use_maps: bool = true;
148override use_detail: bool = true;
149override use_ageing: bool = true;
150override use_fine_noise: bool = false;
151override use_uv1: bool = true;
152override use_lacquer: bool = true;
153override use_procedural_lacquer: bool = false;
154override use_scratch: bool = true;
155override use_grime: bool = true;
156override use_wood: bool = true;
157override use_wall: bool = true;
158override use_fibre: bool = true;
159override use_crinkle: bool = true;
160override use_procedural_detail: bool = false;
161var<private> map_ink_base: vec4f;
162var<private> map_pixel: f32;
163var<private> map_dx: vec3f;
164var<private> map_dy: vec3f;
165var<private> map_planes: vec3f = vec3f(0.0, 0.0, 1.0);
166var<private> map_gain: f32 = 1.0;
167const tile_cells: f32 = 64.0;
168const lacquer_broad_range: f32 = 2.5;
169const lacquer_fine_range: f32 = 1.5;
170const lacquer_blend: f32 = 0.5;
171const map_ring_mean: f32 = 0.25;
172const map_line_mean: f32 = 0.0897;
173const map_wear_mean: f32 = 0.3814;
174const map_speck_mean: f32 = 0.0015;
175fn map_footprint(position: vec3f) {
176 let dx = dpdx(position);
177 let dy = dpdy(position);
178 map_pixel = max(length(dx), length(dy));
179 if ((use_lacquer && !use_procedural_lacquer) || map_tiled()) {
180 map_dx = dx;
181 map_dy = dy;
182 let face = cross(dx, dy);
183 let size = length(face);
184 var w = vec3f(0.0, 0.0, 1.0);
185 if (size > 0.0) {
186 w = max(abs(face) / size - vec3f(lacquer_blend), vec3f(0.0));
187 w = w / (w.x + w.y + w.z);
188 }
189 map_planes = w;
190 map_gain = inverseSqrt(dot(w, w));
191 }
192}
193fn map_tiled() -> bool {
194 return !use_procedural_detail && (use_scratch || use_wall || use_fibre || use_crinkle);
195}
196fn lacquer_plane(at: vec2f, dx: vec2f, dy: vec2f, layer: i32, fine_scale: f32) -> vec2f {
197 let broad = textureSampleGrad(map_metal, map_sampler, at, layer, dx, dy).rg;
198 let fine = textureSampleGrad(map_metal, map_sampler, at * fine_scale, layer, dx * fine_scale, dy * fine_scale).ba;
199 return (broad * 2.0 - 1.0) * lacquer_broad_range + (fine * 2.0 - 1.0) * lacquer_fine_range;
200}
201fn map_lacquer(p: vec3f, amp: f32, tile: f32, a: vec4f, b: vec4f, d: vec4f) -> vec3f {
202 if (use_procedural_lacquer || tile < 0.0 || d.w == 0.0) {
203 return value_noise3_gradient(p, a.x) * amp + value_noise3_gradient(p, a.y) * (amp * a.w) + value_noise3_gradient(p, a.z) * (amp * b.x);
204 }
205 let layer = i32(tile);
206 let w = map_planes;
207 var g = vec3f(0.0);
208 if (w.z > 0.0) {
209 g += vec3f(lacquer_plane(p.xy, map_dx.xy, map_dy.xy, layer, d.z), 0.0) * w.z;
210 }
211 if (w.x > 0.0) {
212 g += vec3f(0.0, lacquer_plane(p.yz + vec2f(0.31, 0.67), map_dx.yz, map_dy.yz, layer, d.z)) * w.x;
213 }
214 if (w.y > 0.0) {
215 let t = lacquer_plane(p.zx + vec2f(0.53, 0.19), map_dx.zx, map_dy.zx, layer, d.z);
216 g += vec3f(t.y, 0.0, t.x) * w.y;
217 }
218 return g * (amp * map_gain);
219}
220fn map_weight(rate: f32) -> f32 {
221 if (use_fine_noise) {
222 return 1.0;
223 }
224 return octave_weight(map_pixel * rate);
225}
226fn map_faded(mean: f32, value: f32, weight: f32) -> f32 {
227 return select(mix(mean, value, weight), value, weight >= 1.0);
228}
229fn map_noise(q: vec3f, rate: f32) -> f32 {
230 let weight = map_weight(rate);
231 if (weight <= 0.0) {
232 return 0.5;
233 }
234 return map_faded(0.5, value_noise3(q, false), weight);
235}
236fn tile_pair(at: vec2f, dx: vec2f, dy: vec2f, layer: i32, high: bool) -> vec2f {
237 let s = 1.0 / tile_cells;
238 let t = textureSampleGrad(map_metal, map_sampler, at * s, layer, dx * s, dy * s);
239 return select(t.xy, t.zw, high);
240}
241fn tile_slice(pair: vec2f, depth: f32) -> f32 {
242 let k = floor(depth);
243 let w = fade_cubic(depth - k);
244 let odd = k - 2.0 * floor(k * 0.5) > 0.5;
245 return select(mix(pair.x, pair.y, w), mix(pair.y, pair.x, w), odd);
246}
247fn tile_flat(at: vec2f, dx: vec2f, dy: vec2f, depth: f32, tile: f32, octave: u32) -> f32 {
248 return tile_slice(tile_pair(at, dx, dy, i32(tile) + 1 + i32(octave >> 1u), (octave & 1u) == 1u), depth);
249}
250fn tile_noise(q: vec3f, rate: vec3f, tile: f32, octave: u32) -> f32 {
251 if (map_weight(max(rate.x, max(rate.y, rate.z))) <= 0.0) {
252 return 0.5;
253 }
254 let dx = map_dx * rate;
255 let dy = map_dy * rate;
256 let w = map_planes;
257 var sum = 0.0;
258 if (w.z > 0.0) {
259 sum += (tile_flat(q.xy, dx.xy, dy.xy, q.z, tile, octave) - 0.5) * w.z;
260 }
261 if (w.x > 0.0) {
262 sum += (tile_flat(q.yz + vec2f(17.0, 41.0), dx.yz, dy.yz, q.x, tile, octave) - 0.5) * w.x;
263 }
264 if (w.y > 0.0) {
265 sum += (tile_flat(q.zx + vec2f(29.0, 7.0), dx.zx, dy.zx, q.y, tile, octave) - 0.5) * w.y;
266 }
267 return 0.5 + sum * map_gain;
268}
269fn tile_gradient(at: vec2f, dx: vec2f, dy: vec2f, layer: i32) -> vec2f {
270 let s = 1.0 / tile_cells;
271 let t = textureSampleGrad(map_metal, map_sampler, at * s, layer, dx * s, dy * s).ba;
272 return (t * 2.0 - 1.0) * lacquer_fine_range;
273}
274fn tile_wobble(p: vec3f, scale: f32, shift: vec2f, tile: f32) -> vec3f {
275 if (map_weight(scale) <= 0.0) {
276 return vec3f(0.0);
277 }
278 let q = p * scale;
279 let dx = map_dx * scale;
280 let dy = map_dy * scale;
281 let layer = i32(tile);
282 let w = map_planes;
283 var g = vec3f(0.0);
284 if (w.z > 0.0) {
285 g += vec3f(tile_gradient(q.xy + shift, dx.xy, dy.xy, layer), 0.0) * w.z;
286 }
287 if (w.x > 0.0) {
288 g += vec3f(0.0, tile_gradient(q.yz + shift.yx, dx.yz, dy.yz, layer)) * w.x;
289 }
290 if (w.y > 0.0) {
291 let t = tile_gradient(q.zx + shift * 1.7, dx.zx, dy.zx, layer);
292 g += vec3f(t.y, 0.0, t.x) * w.y;
293 }
294 return g * map_gain;
295}
296fn ink_sample(at: vec2f) -> vec4f {
297 return map_ink_base;
298}
299fn map_perturb(n: vec3f, g: vec3f) -> vec3f {
300 return normalize(n - (g - n * dot(g, n)));
301}
302fn map_scratch_coat(base: vec3f, roughness: f32, amount: f32, position: vec3f, seed: u32) -> vec4f {
303 if (amount <= 0.0) {
304 return vec4f(base, roughness);
305 }
306 let q = position * 900.0;
307 let n = value_noise3(vec3f(q.x * 0.02 + f32(seed), q.y, q.z * 0.02), false);
308 let line = 1.0 - smoothstep(0.0, 0.045, abs(n - 0.5));
309 let smudge = smoothstep(0.52, 0.78, value_noise3(position * 22.0, false) * 0.65 + value_noise3(position * 61.0, false) * 0.35);
310 let cell = vec3i(floor(position * 2600.0));
311 let speck = select(0.0, 1.0, hash3_xor(vec3i(cell.x, cell.y, cell.z ^ i32(seed))) > 0.9985);
312 let cover = min(amount, 1.0);
313 let breakup = clamp(smudge * 0.55 + line * 0.35 + speck, 0.0, 1.0) * cover;
314 let rough2 = roughness * roughness;
315 let mixed = mix(rough2, min(rough2 * 4.0 + 0.02, 0.35), breakup);
316 let albedo = mix(base, base * 0.9 + vec3f(0.06), speck * cover);
317 return vec4f(albedo, sqrt(mixed));
318}
319fn map_scratch(base: vec3f, roughness: f32, amount: f32, position: vec3f, seed: u32, coated: bool, tile: f32, scales: vec4f, stretch: f32) -> vec4f {
320 if (amount <= 0.0) {
321 return vec4f(base, roughness);
322 }
323 let tiled = !use_procedural_detail && tile >= 0.0;
324 var line = map_line_mean;
325 let line_weight = map_weight(scales.x);
326 if (line_weight > 0.0) {
327 let at = vec3f(position.x * stretch + f32(seed), position.y * scales.x, position.z * stretch);
328 var n: f32;
329 if (tiled) {
330 n = tile_noise(at, vec3f(stretch, scales.x, stretch), tile, 2u);
331 } else {
332 n = value_noise3(at, false);
333 }
334 line = map_faded(map_line_mean, 1.0 - smoothstep(0.0, 0.045, abs(n - 0.5)), line_weight);
335 }
336 var smudge: f32;
337 if (tiled) {
338 smudge = smoothstep(0.52, 0.78, tile_noise(position * scales.y, vec3f(scales.y), tile, 3u) * 0.65 + tile_noise(position * scales.z, vec3f(scales.z), tile, 1u) * 0.35);
339 } else {
340 smudge = smoothstep(0.52, 0.78, map_noise(position * scales.y, scales.y) * 0.65 + map_noise(position * scales.z, scales.z) * 0.35);
341 }
342 var speck = map_speck_mean;
343 let speck_weight = map_weight(scales.w);
344 if (speck_weight > 0.0) {
345 let cell = vec3i(floor(position * scales.w));
346 speck = map_faded(map_speck_mean, select(0.0, 1.0, hash3_xor(vec3i(cell.x, cell.y, cell.z ^ i32(seed))) > 0.9985), speck_weight);
347 }
348 let cover = min(amount, 1.0);
349 let breakup = clamp(smudge * 0.55 + line * 0.35 + speck, 0.0, 1.0) * cover;
350 let rough2 = roughness * roughness;
351 var mixed = mix(rough2, min(rough2 * 3.0 + 0.03, 0.5), breakup);
352 if (coated) {
353 mixed = mix(rough2, min(rough2 * 4.0 + 0.02, 0.35), breakup);
354 }
355 let albedo = mix(base, base * 0.9 + vec3f(0.06), speck * cover);
356 return vec4f(albedo, sqrt(mixed));
357}
358fn map_layer(result_in: MapResult, layer: vec4f, pa: vec4f, pb: vec4f, pc: vec4f, pd: vec4f, uv: vec2f, position: vec3f, time: f32, coated: bool, tile: f32) -> MapResult {
359 var result = result_in;
360 let kind = u32(round(layer.x));
361 let amp = layer.z;
362 if (kind == 0u || amp == 0.0) {
363 return result;
364 }
365 let seed = bitcast<u32>(layer.w);
366 let p = position + vec3f(f32(seed) * 0.001, f32(seed) * 0.001, 0.0);
367 let tiled = !use_procedural_detail && tile >= 0.0;
368 if (use_fibre && kind == 1u) {
369 var fibre: f32;
370 var mottle: f32;
371 var g: vec3f;
372 let streak = vec3f(pa.x, pa.y, pa.x);
373 if (tiled) {
374 fibre = tile_noise(p * streak, streak, tile, 0u) * 0.6 + tile_noise(p * pa.z, vec3f(pa.z), tile, 1u) * 0.4;
375 mottle = tile_noise(p * pa.w, vec3f(pa.w), tile, 2u);
376 g = tile_wobble(p, pb.x, vec2f(3.0, 5.0), tile) * (pc.x * amp) + tile_wobble(p, pb.y, vec2f(11.0, 23.0), tile) * (pc.y * amp);
377 } else {
378 fibre = map_noise(p * streak, max(pa.x, pa.y)) * 0.6 + map_noise(p * pa.z, pa.z) * 0.4;
379 mottle = map_noise(p * pa.w, pa.w);
380 g = value_noise3_gradient(p, pb.x) * (pc.x * amp) + value_noise3_gradient(p, pb.y) * (pc.y * amp);
381 }
382 result.base *= 1.0 + ((fibre - 0.5) * pb.z + (mottle - 0.5) * pb.w) * amp;
383 result.normal = map_perturb(result.normal, g);
384 } else if (use_crinkle && kind == 2u) {
385 var g: vec3f;
386 if (tiled) {
387 g = tile_wobble(p, pa.x, vec2f(7.0, 19.0), tile) * (pa.z * amp) + tile_wobble(p, pa.y, vec2f(31.0, 2.0), tile) * (pa.w * amp);
388 } else {
389 g = value_noise3_gradient(p, pa.x) * (pa.z * amp) + value_noise3_gradient(p, pa.y) * (pa.w * amp);
390 }
391 result.normal = map_perturb(result.normal, g);
392 } else if (use_wood && kind == 3u) {
393 let plank = floor(p.z * pd.x);
394 let tone = hash3_xor(vec3i(i32(plank), 7, 3));
395 let across = fract(p.z * pd.x);
396 let seam = smoothstep(0.0, 0.006, across) + smoothstep(0.0, 0.006, 1.0 - across) - 1.0;
397 var grain = map_ring_mean;
398 let ring_weight = map_weight(pd.z);
399 if (ring_weight > 0.0) {
400 let warp = map_noise(vec3f(p.x * 1.4, plank * 3.7, p.z * 6.0), 6.0) * 0.9 + map_noise(vec3f(p.x * 6.0, plank, p.z * 20.0), 20.0) * 0.25;
401 let rings = fract(fma(fma(warp, pa.z, p.z), pa.y, map_noise(vec3f(p.x * 0.8, plank, 0.0), 0.8) * pc.y));
402 grain = map_faded(map_ring_mean, smoothstep(0.55, 0.95, rings), ring_weight);
403 }
404 let figure = grain * 0.55 + map_noise(vec3f(p.x * pa.w, p.z * pb.x, plank), pb.x) * 0.25;
405 let factor = (0.78 + tone * pb.y) * (1.08 - figure * pb.z) * (pb.w + clamp(seam, 0.0, 1.0) * pd.y);
406 result.base *= mix(1.0, factor, clamp(amp, 0.0, 1.0));
407 let alpha = result.roughness * result.roughness;
408 result.roughness = mix(result.roughness, sqrt(min(alpha * (0.85 + figure * pc.x), 1.0)), clamp(amp, 0.0, 1.0));
409 } else if (use_wall && kind == 4u) {
410 let broad = map_noise(p * pa.x, pa.x) * 0.6 + map_noise(p * pa.y, pa.y) * 0.4;
411 var fine: f32;
412 var g: vec3f;
413 if (tiled) {
414 fine = tile_noise(p * pa.z, vec3f(pa.z), tile, 2u) * 0.6 + tile_noise(p * pa.w, vec3f(pa.w), tile, 3u) * 0.4;
415 g = tile_wobble(p, pb.x, vec2f(5.0, 13.0), tile) * (pc.x * amp) + tile_wobble(p, pb.y, vec2f(29.0, 3.0), tile) * (pc.y * amp);
416 } else {
417 fine = map_noise(p * pa.z, pa.z) * 0.6 + map_noise(p * pa.w, pa.w) * 0.4;
418 g = value_noise3_gradient(p, pb.x) * (pc.x * amp) + value_noise3_gradient(p, pb.y) * (pc.y * amp);
419 }
420 result.base *= mix(1.0, 1.0 + (broad - 0.5) * pb.z + (fine - 0.5) * pb.w, clamp(amp, 0.0, 1.0));
421 result.normal = map_perturb(result.normal, g);
422 } else if (use_grime && kind == 5u) {
423 var wear = map_wear_mean;
424 let grime_weight = map_weight(pa.x);
425 if (grime_weight > 0.0) {
426 let grime = value_noise3(p * pa.x, false) * 0.6 + map_noise(p * pa.y, pa.y) * 0.4;
427 wear = map_faded(map_wear_mean, smoothstep(0.35, 0.75, grime), grime_weight);
428 }
429 let tint = mix(vec3f(pa.z, pa.w, pb.x), vec3f(pb.y, pb.z, pb.w), wear);
430 result.base *= mix(vec3f(1.0), tint, clamp(amp, 0.0, 1.0));
431 let alpha = result.roughness * result.roughness;
432 result.roughness = mix(result.roughness, sqrt(min(alpha * mix(pc.x, pc.y, wear), 1.0)), clamp(amp, 0.0, 1.0));
433 } else if (kind == 7u) {
434 let spots = fbm3(vec3f(uv * 12.0, 2.0), 4, true);
435 result.base *= mix(1.0, 0.7 + 0.6 * spots, clamp(amp, 0.0, 1.0));
436 } else if (kind == 8u) {
437 let value = fbm3(p * 300.0, 4, true);
438 result.base *= mix(1.0, 0.75 + 0.5 * value, clamp(amp, 0.0, 1.0));
439 } else if (use_scratch && kind == 17u) {
440 if (coated) {
441 let scratched = map_scratch(result.base, result.coat_roughness, amp, p, seed, true, tile, pa, pd.x);
442 result.base = scratched.xyz;
443 result.coat_roughness = scratched.w;
444 } else {
445 let scratched = map_scratch(result.base, result.roughness, amp, p, seed, false, tile, pa, pd.x);
446 result.base = scratched.xyz;
447 result.roughness = scratched.w;
448 }
449 } else if (use_lacquer && kind == 18u) {
450 result.normal = map_perturb(result.normal, map_lacquer(p, amp, tile, pa, pb, pd));
451 } else if (kind == 14u) {
452 let value = flow(uv * layer.y, time);
453 result.thin_film = max(result.thin_film + amp * (value - 0.5) * 2.0, 0.0);
454 } else if (kind == 15u) {
455 let value = map_noise(p * layer.y, abs(layer.y));
456 result.base *= mix(1.0, 1.0 + (value - 0.5) * 2.0, clamp(amp, 0.0, 1.0));
457 } else if (kind == 16u) {
458 let value = fbm3(p * layer.y, 4, true);
459 result.base *= mix(1.0, 1.0 + (value - 0.5) * 2.0, clamp(amp, 0.0, 1.0));
460 }
461 return result;
462}
463fn map_evaluate(base: vec3f, roughness: f32, metalness: f32, coat_roughness: f32, thin_film: f32, normal: vec3f, tangent: vec4f, uv: vec2f, object_position: vec3f, edge: f32, age_in: f32, time: f32, material_index: u32) -> MapResult {
464 return map_evaluate_with(base, roughness, metalness, coat_roughness, thin_film, normal, tangent, uv, uv, object_position, edge, age_in, time, material_index);
465}
466fn map_evaluate_with(base: vec3f, roughness: f32, metalness: f32, coat_roughness: f32, thin_film: f32, normal: vec3f, tangent: vec4f, uv: vec2f, uv1: vec2f, object_position: vec3f, edge: f32, age_in: f32, time: f32, material_index: u32) -> MapResult {
467 let material = map_materials[material_index];
468 let baked = material.seeds.w != 0u;
469 var atlas_uv = uv;
470 if (use_uv1 && material.packed.y != 0.0) {
471 atlas_uv = uv1;
472 }
473 let tiled_uv = select(uv * select(max(material.factors.x, 0.000001), 1.0, baked), atlas_uv * material.atlas.xy + material.atlas.zw, material.seeds.w >= 2u);
474 var result = MapResult(base, roughness, metalness, normalize(normal), coat_roughness, thin_film);
475 let tile_dx = dpdx(tiled_uv) * material.content_extra.z;
476 let tile_dy = dpdy(tiled_uv) * material.content_extra.z;
477 if (use_maps && material.indices.x >= 0) {
478 let sample = textureSampleGrad(map_base, map_sampler, tiled_uv, material.indices.x, tile_dx, tile_dy);
479 if (baked) {
480 result.base = sample.rgb;
481 } else {
482 result.base *= mix(vec3f(1.0), sample.rgb, clamp(material.factors.y, 0.0, 1.0));
483 }
484 }
485 if (use_maps && material.indices.z >= 0) {
486 let sampled_roughness = textureSampleGrad(map_roughness, map_sampler, tiled_uv, material.indices.z, tile_dx, tile_dy).r;
487 if (baked) {
488 result.roughness = sampled_roughness;
489 } else {
490 result.roughness *= sampled_roughness;
491 }
492 }
493 if (use_maps && material.indices.w >= 0) {
494 result.metalness *= textureSampleGrad(map_metal, map_sampler, tiled_uv, material.indices.w, tile_dx, tile_dy).r;
495 }
496 if (use_maps && material.indices.y >= 0) {
497 var mapped = textureSampleGrad(map_normal, map_sampler, tiled_uv, material.indices.y, tile_dx, tile_dy).xyz * 2.0 - vec3f(1.0);
498 if (material.seeds.w == 3u) {
499 mapped.z = sqrt(max(0.0, 1.0 - dot(mapped.xy, mapped.xy)));
500 }
501 let t = normalize(tangent.xyz - result.normal * dot(tangent.xyz, result.normal));
502 let b = cross(result.normal, t) * tangent.w;
503 let transformed = normalize(t * mapped.x + b * mapped.y + result.normal * mapped.z);
504 result.normal = normalize(mix(result.normal, transformed, clamp(material.factors.z, 0.0, 1.0)));
505 }
506 let age = clamp(age_in, 0.0, 1.0);
507 let seed = material.seeds.x;
508 if (use_ageing && age > 0.0) {
509 result.base = fade_colour(result.base, age * material.age_a.x, object_position, seed);
510 result.base = yellow(result.base, age * material.age_a.y, uv, seed);
511 if (material.age_a.z > 0.0) {
512 map_ink_base = vec4f(result.base, 1.0);
513 result.base = mix(result.base, ink_age(object_position.xy, 1.0, seed).xyz, clamp(age * material.age_a.z, 0.0, 1.0));
514 }
515 if (material.seeds.y != 0u) {
516 let scratched = map_scratch_coat(result.base, result.coat_roughness, age * material.age_a.w, object_position, seed);
517 result.base = scratched.xyz;
518 result.coat_roughness = scratched.w;
519 } else {
520 let scratched = scratch(result.base, result.roughness, age * material.age_a.w, object_position, seed);
521 result.base = scratched.xyz;
522 result.roughness = scratched.w;
523 }
524 let worn = worn_edge(result.base, result.roughness, age * material.age_b.x, edge);
525 result.base = worn.xyz;
526 result.roughness = worn.w;
527 let dusty = dust(result.base, result.roughness, age * material.age_b.y, object_position, seed);
528 result.base = dusty.xyz;
529 result.roughness = dusty.w;
530 let patinated = patina(result.base, result.roughness, result.metalness, age * material.age_b.z, object_position, seed);
531 result.base = patinated.xyz;
532 result.roughness = patinated.w;
533 if (material.age_b.z > 0.0) {
534 let p = object_position + vec3f(f32(seed) * 0.001);
535 let green = smoothstep(0.62, 0.9, fbm3(p * 1500.0 + vec3f(9.0), 3, true)) * clamp(age * material.age_b.z, 0.0, 1.0) * 0.45;
536 result.metalness = mix(result.metalness, 0.5, green);
537 }
538 }
539 if (use_detail && material.seeds.w == 0u) {
540 result = map_layer(result, material.layer0, material.params[0], material.params[1], material.params[2], material.params[3], uv, object_position, time, material.seeds.y != 0u, material.packed.z);
541 result = map_layer(result, material.layer1, material.params[4], material.params[5], material.params[6], material.params[7], uv, object_position, time, material.seeds.y != 0u, material.packed.z);
542 result = map_layer(result, material.layer2, material.params[8], material.params[9], material.params[10], material.params[11], uv, object_position, time, material.seeds.y != 0u, material.packed.z);
543 result = map_layer(result, material.layer3, material.params[12], material.params[13], material.params[14], material.params[15], uv, object_position, time, material.seeds.y != 0u, material.packed.z);
544 }
545 return result;
546}
547"#;
548
549pub fn shader_source() -> String {
550 format!(
551 "{}\n{}\n{}",
552 pfx_materials::NOISE,
553 pfx_materials::AGEING,
554 MAPS_WGSL
555 )
556}
557
558pub fn content_source() -> String {
559 format!(
560 "{}\n{}\n{}\n{}\n{}",
561 pfx_materials::BRDF,
562 pfx_materials::NOISE,
563 pfx_materials::AGEING,
564 MAPS_WGSL,
565 content_wgsl()
566 )
567}
568
569pub fn content_wgsl() -> String {
570 format!("{}\n{}", pfx_materials::CONTENT, CONTENT_WGSL)
571}
572
573#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
574pub struct MapIndices {
575 pub base: Option<u32>,
576 pub normal: Option<u32>,
577 pub roughness: Option<u32>,
578 pub metal: Option<u32>,
579}
580
581impl MapIndices {
582 pub fn from_material(material: &Material) -> Result<Self, String> {
583 let layer = material.maps.layer;
584 if layer < 0.0 {
585 return Ok(Self::default());
586 }
587 if !layer.is_finite() || layer.fract() != 0.0 || layer >= i32::MAX as f32 {
588 return Err("material map layer must be a nonnegative integer".into());
589 }
590 let index = Some(layer as u32);
591 Ok(Self {
592 base: index,
593 normal: index,
594 roughness: index,
595 metal: if material.maps.tile == 0.0 {
596 None
597 } else {
598 index
599 },
600 })
601 }
602
603 pub fn validate(self, images: &MapImages<'_>) -> Result<(), String> {
604 for (name, index, count) in [
605 ("base", self.base, images.base.len()),
606 ("normal", self.normal, images.normal.len()),
607 ("roughness", self.roughness, images.roughness.len()),
608 ("metal", self.metal, images.metal.len()),
609 ] {
610 if let Some(index) = index
611 && index as usize >= count
612 {
613 return Err(format!("{name} layer {index} is outside {count} layers"));
614 }
615 }
616 Ok(())
617 }
618
619 fn packed(self) -> [i32; 4] {
620 [self.base, self.normal, self.roughness, self.metal]
621 .map(|index| index.map_or(-1, |index| index as i32))
622 }
623}
624
625#[derive(Default)]
626pub struct MapImages<'a> {
627 pub base: &'a [Image],
628 pub normal: &'a [Image],
629 pub roughness: &'a [Image],
630 pub metal: &'a [Image],
631}
632
633pub fn select_baked_detail(material: &mut Material, layer: u32) -> Result<(), String> {
634 if layer >= i32::MAX as u32 {
635 return Err("baked detail layer is out of range".into());
636 }
637 material.maps.layer = layer as f32;
638 material.maps.tile = 0.0;
639 material.maps.albedo = 1.0;
640 material.maps.normal = 1.0;
641 Ok(())
642}
643
644#[repr(C)]
645#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
646pub struct GpuMapMaterial {
647 pub indices: [i32; 4],
648 pub factors: [f32; 4],
649 pub age_a: [f32; 4],
650 pub age_b: [f32; 4],
651 pub seeds: [u32; 4],
652 pub layers: [[f32; 4]; LAYER_CAP],
653 pub content: [f32; 4],
654 pub content_extra: [f32; 4],
655 pub packed: [f32; 4],
656 pub atlas: [f32; 4],
657 pub params: [[f32; 4]; PARAM_ROWS],
658}
659
660impl GpuMapMaterial {
661 pub fn new(material: &Material, indices: MapIndices) -> Self {
662 let baked = material.maps.tile == 0.0 && indices.base.is_some();
663 let layer = |i: usize| {
664 let layer = material.layers[i];
665 let kind = if layer.active() || layer.amplitude == 0.0 {
666 noise_id(layer.kind)
667 } else {
668 0
669 };
670 [
671 kind as f32,
672 layer.frequency,
673 layer.amplitude,
674 f32::from_bits(layer.seed),
675 ]
676 };
677 let params: Vec<[f32; GPU_PARAMS]> = material.layers.iter().map(layer_params).collect();
678 Self {
679 indices: indices.packed(),
680 factors: [
681 material.maps.tile,
682 material.maps.albedo,
683 material.maps.normal,
684 material.normal.strength,
685 ],
686 age_a: [
687 material.ageing.fade,
688 material.ageing.yellow,
689 material.ageing.ink,
690 material.ageing.scratch,
691 ],
692 age_b: [
693 material.ageing.edge,
694 material.ageing.dust,
695 material.ageing.patina,
696 0.0,
697 ],
698 seeds: [
699 material.ageing.seed,
700 u32::from(material.clearcoat > 0.0),
701 match material.normal.source {
702 NormalSource::Flat => 0,
703 NormalSource::Bump => 1,
704 NormalSource::Map => 2,
705 },
706 u32::from(baked),
707 ],
708 layers: [layer(0), layer(1), layer(2), layer(3)],
709 content: content_row(material),
710 content_extra: [material.content_layer.strength, 1.0, 1.0, 0.0],
711 packed: [0.0, 0.0, -1.0, 0.0],
712 atlas: [1.0, 1.0, 0.0, 0.0],
713 params: std::array::from_fn(|row| {
714 let at = row % (GPU_PARAMS / 4) * 4;
715 let p = ¶ms[row / (GPU_PARAMS / 4)];
716 [p[at], p[at + 1], p[at + 2], p[at + 3]]
717 }),
718 }
719 }
720}
721
722pub const RING_FADE: f32 = 8.0 / 7.0;
723
724fn layer_params(layer: &NoiseLayer) -> [f32; GPU_PARAMS] {
725 let mut p = pfx_materials::gpu_params(layer);
726 match layer.kind {
727 NoiseKind::PlankWood => p[GPU_PARAMS - 2] = p[1] * RING_FADE,
728 NoiseKind::CoatWobble => {
729 p[GPU_PARAMS - 2] = p[0] / lacquer::FINE_CELLS as f32;
730 p[GPU_PARAMS - 1] = 1.0;
731 }
732 _ => {}
733 }
734 p
735}
736
737fn coat_of(material: &Material) -> Option<CoatWobble> {
738 material
739 .layers
740 .iter()
741 .find(|layer| layer.kind == NoiseKind::CoatWobble && layer.active())
742 .map(|layer| CoatWobble::from_params(&layer.params))
743}
744
745fn content_row(material: &Material) -> [f32; 4] {
746 let layer = material.content_layer;
747 if !layer.active() || layer.slot as usize >= CONTENT_SLOTS {
748 return [-1.0, 0.0, -1.0, 0.0];
749 }
750 [
751 layer.slot as f32,
752 layer.blend.id() as f32,
753 layer.ink_roughness,
754 layer.emboss,
755 ]
756}
757
758fn noise_id(kind: NoiseKind) -> u32 {
759 match kind {
760 NoiseKind::None => 0,
761 NoiseKind::Fibre => 1,
762 NoiseKind::Crinkle => 2,
763 NoiseKind::PlankWood => 3,
764 NoiseKind::WallMottle => 4,
765 NoiseKind::Grime => 5,
766 NoiseKind::Leaf => 7,
767 NoiseKind::Bark => 8,
768 NoiseKind::Flow => 14,
769 NoiseKind::Value => 15,
770 NoiseKind::Fbm => 16,
771 NoiseKind::Scratch => 17,
772 NoiseKind::CoatWobble => 18,
773 }
774}
775
776#[derive(Clone, Copy)]
777enum Kind {
778 Base,
779 Normal,
780 Scalar,
781}
782
783fn channel(value: u16, wide: bool) -> f32 {
784 value as f32 / if wide { 65535.0 } else { 255.0 }
785}
786
787fn texels(image: &Image, kind: Kind) -> Vec<[f32; 4]> {
788 let decode = |rgba: [f32; 4]| match kind {
789 Kind::Base => {
790 let mut rgba = rgba;
791 if image.space == ColorSpace::Srgb {
792 for value in &mut rgba[..3] {
793 *value = linear_channel(*value);
794 }
795 }
796 let a = rgba[3];
797 [rgba[0] * a, rgba[1] * a, rgba[2] * a, a]
798 }
799 Kind::Normal | Kind::Scalar => rgba,
800 };
801 match &image.pixels {
802 Pixels::Eight(values) => values
803 .chunks_exact(4)
804 .map(|p| decode(std::array::from_fn(|c| channel(p[c] as u16, false))))
805 .collect(),
806 Pixels::Sixteen(values) => values
807 .chunks_exact(4)
808 .map(|p| decode(std::array::from_fn(|c| channel(p[c], true))))
809 .collect(),
810 }
811}
812
813fn quantize(level: &[[f32; 4]], kind: Kind) -> Vec<u8> {
814 let byte = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
815 level
816 .iter()
817 .flat_map(|&texel| {
818 let rgba = match kind {
819 Kind::Base => {
820 let a = texel[3];
821 let rgb: [f32; 3] = std::array::from_fn(|c| {
822 if a > 0.0 {
823 encode_channel((texel[c] / a).clamp(0.0, 1.0))
824 } else {
825 0.0
826 }
827 });
828 [rgb[0], rgb[1], rgb[2], a]
829 }
830 Kind::Normal | Kind::Scalar => texel,
831 };
832 rgba.map(byte)
833 })
834 .collect()
835}
836
837fn mip_chain(image: &Image, kind: Kind) -> Result<Vec<(u32, u32, Vec<u8>)>, String> {
838 if image.width == 0 || image.height == 0 {
839 return Err("map image is empty".into());
840 }
841 let len = (image.width as usize)
842 .checked_mul(image.height as usize)
843 .and_then(|n| n.checked_mul(4))
844 .ok_or("map image is too large")?;
845 let valid = match &image.pixels {
846 Pixels::Eight(p) => p.len() == len,
847 Pixels::Sixteen(p) => p.len() == len,
848 };
849 if !valid {
850 return Err("map image has the wrong pixel count".into());
851 }
852 let mut level = texels(image, kind);
853 let (mut width, mut height) = (image.width, image.height);
854 let mut chain = vec![(width, height, quantize(&level, kind))];
855 while width > 1 || height > 1 {
856 let next_width = (width / 2).max(1);
857 let next_height = (height / 2).max(1);
858 let mut next = Vec::with_capacity(next_width as usize * next_height as usize);
859 for y in 0..next_height {
860 for x in 0..next_width {
861 let mut total = [0.0; 4];
862 for dy in 0..2 {
863 for dx in 0..2 {
864 let sx = (2 * x + dx).min(width - 1);
865 let sy = (2 * y + dy).min(height - 1);
866 let texel = level[(sy * width + sx) as usize];
867 for c in 0..4 {
868 total[c] += texel[c] * 0.25;
869 }
870 }
871 }
872 if matches!(kind, Kind::Normal) {
873 let normal: [f32; 3] = std::array::from_fn(|c| total[c] * 2.0 - 1.0);
874 let length = normal.iter().map(|v| v * v).sum::<f32>().sqrt().max(1e-6);
875 for c in 0..3 {
876 total[c] = normal[c] / length * 0.5 + 0.5;
877 }
878 }
879 next.push(total);
880 }
881 }
882 chain.push((next_width, next_height, quantize(&next, kind)));
883 level = next;
884 width = next_width;
885 height = next_height;
886 }
887 Ok(chain)
888}
889
890impl Kind {
891 fn format(self) -> wgpu::TextureFormat {
892 match self {
893 Self::Base => wgpu::TextureFormat::Rgba8UnormSrgb,
894 Self::Normal | Self::Scalar => wgpu::TextureFormat::Rgba8Unorm,
895 }
896 }
897}
898
899pub struct TextureArray {
900 pub texture: wgpu::Texture,
901 pub view: wgpu::TextureView,
902 pub width: u32,
903 pub height: u32,
904 pub layers: u32,
905 pub mips: u32,
906}
907
908impl TextureArray {
909 pub fn pages(
910 device: &wgpu::Device,
911 queue: &wgpu::Queue,
912 pages: &[Texture],
913 blocks: bool,
914 label: &str,
915 ) -> Result<Self, String> {
916 let first = pages.first().ok_or_else(|| format!("{label} are empty"))?;
917 let decoded;
918 let pages = if first.format.compressed() && !blocks {
919 decoded = pages.iter().map(Texture::decode).collect::<Vec<_>>();
920 &decoded[..]
921 } else {
922 pages
923 };
924 let first = &pages[0];
925 let (width, height, mips) = (first.width, first.height, first.levels.len() as u32);
926 if pages.iter().any(|page| {
927 page.format != first.format
928 || page.width != width
929 || page.height != height
930 || page.levels.len() as u32 != mips
931 || page.check().is_err()
932 }) {
933 return Err(format!("{label} must share one format and size"));
934 }
935 let limits = device.limits();
936 if width > limits.max_texture_dimension_2d || height > limits.max_texture_dimension_2d {
937 return Err(format!("{label} exceed device limits"));
938 }
939 if pages.len() > limits.max_texture_array_layers as usize {
940 return Err(format!("too many {label}"));
941 }
942 let format = first.format;
943 let texture = device.create_texture(&wgpu::TextureDescriptor {
944 label: Some(label),
945 size: wgpu::Extent3d {
946 width,
947 height,
948 depth_or_array_layers: pages.len() as u32,
949 },
950 mip_level_count: mips,
951 sample_count: 1,
952 dimension: wgpu::TextureDimension::D2,
953 format: detail_format(format),
954 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
955 view_formats: &[],
956 });
957 let block = format.block();
958 for (layer, page) in pages.iter().enumerate() {
959 for (level, bytes) in page.levels.iter().enumerate() {
960 let [w, h] = level_size(width, height, level as u32);
961 queue.write_texture(
962 wgpu::TexelCopyTextureInfo {
963 texture: &texture,
964 mip_level: level as u32,
965 origin: wgpu::Origin3d {
966 x: 0,
967 y: 0,
968 z: layer as u32,
969 },
970 aspect: wgpu::TextureAspect::All,
971 },
972 bytes,
973 wgpu::TexelCopyBufferLayout {
974 offset: 0,
975 bytes_per_row: Some(w.div_ceil(block) * format.block_bytes()),
976 rows_per_image: Some(h.div_ceil(block)),
977 },
978 wgpu::Extent3d {
979 width: w.div_ceil(block) * block,
980 height: h.div_ceil(block) * block,
981 depth_or_array_layers: 1,
982 },
983 );
984 }
985 }
986 let view = texture.create_view(&wgpu::TextureViewDescriptor {
987 dimension: Some(wgpu::TextureViewDimension::D2Array),
988 ..Default::default()
989 });
990 Ok(Self {
991 texture,
992 view,
993 width,
994 height,
995 layers: pages.len() as u32,
996 mips,
997 })
998 }
999
1000 fn upload(
1001 device: &wgpu::Device,
1002 queue: &wgpu::Queue,
1003 images: &[Image],
1004 kind: Kind,
1005 label: &str,
1006 ) -> Result<Self, String> {
1007 let fallback = match kind {
1008 Kind::Base | Kind::Scalar => [255, 255, 255, 255],
1009 Kind::Normal => [128, 128, 255, 255],
1010 };
1011 let placeholder = Image {
1012 width: 1,
1013 height: 1,
1014 space: ColorSpace::Linear,
1015 pixels: Pixels::Eight(fallback.to_vec()),
1016 };
1017 let entries = if images.is_empty() {
1018 std::slice::from_ref(&placeholder)
1019 } else {
1020 images
1021 };
1022 let width = entries[0].width;
1023 let height = entries[0].height;
1024 if width == 0 || height == 0 {
1025 return Err(format!("{label} map is empty"));
1026 }
1027 if entries.len() > device.limits().max_texture_array_layers as usize {
1028 return Err(format!("too many {label} layers"));
1029 }
1030 if width > device.limits().max_texture_dimension_2d
1031 || height > device.limits().max_texture_dimension_2d
1032 {
1033 return Err(format!("{label} map exceeds device limits"));
1034 }
1035 let chains = entries
1036 .iter()
1037 .map(|image| {
1038 if image.width != width || image.height != height {
1039 return Err(format!("{label} maps must have the same dimensions"));
1040 }
1041 mip_chain(
1042 image,
1043 match kind {
1044 Kind::Base => Kind::Base,
1045 Kind::Normal => Kind::Normal,
1046 Kind::Scalar => Kind::Scalar,
1047 },
1048 )
1049 })
1050 .collect::<Result<Vec<_>, _>>()?;
1051 let texture = device.create_texture(&wgpu::TextureDescriptor {
1052 label: Some(label),
1053 size: wgpu::Extent3d {
1054 width,
1055 height,
1056 depth_or_array_layers: chains.len() as u32,
1057 },
1058 mip_level_count: chains[0].len() as u32,
1059 sample_count: 1,
1060 dimension: wgpu::TextureDimension::D2,
1061 format: kind.format(),
1062 usage: wgpu::TextureUsages::TEXTURE_BINDING
1063 | wgpu::TextureUsages::COPY_DST
1064 | wgpu::TextureUsages::COPY_SRC,
1065 view_formats: &[],
1066 });
1067 for (layer, chain) in chains.iter().enumerate() {
1068 write_chain(queue, &texture, layer as u32, chain);
1069 }
1070 let view = texture.create_view(&wgpu::TextureViewDescriptor {
1071 dimension: Some(wgpu::TextureViewDimension::D2Array),
1072 ..Default::default()
1073 });
1074 Ok(Self {
1075 texture,
1076 view,
1077 width,
1078 height,
1079 layers: images.len() as u32,
1080 mips: chains[0].len() as u32,
1081 })
1082 }
1083
1084 fn write_layer(&self, queue: &wgpu::Queue, layer: u32, image: &Image, kind: Kind) {
1085 if layer >= self.layers || image.width != self.width || image.height != self.height {
1086 return;
1087 }
1088 if let Ok(chain) = mip_chain(image, kind) {
1089 write_chain(queue, &self.texture, layer, &chain);
1090 }
1091 }
1092}
1093
1094fn write_chain(
1095 queue: &wgpu::Queue,
1096 texture: &wgpu::Texture,
1097 layer: u32,
1098 chain: &[(u32, u32, Vec<u8>)],
1099) {
1100 for (level, (w, h, bytes)) in chain.iter().enumerate() {
1101 queue.write_texture(
1102 wgpu::TexelCopyTextureInfo {
1103 texture,
1104 mip_level: level as u32,
1105 origin: wgpu::Origin3d {
1106 x: 0,
1107 y: 0,
1108 z: layer,
1109 },
1110 aspect: wgpu::TextureAspect::All,
1111 },
1112 bytes,
1113 wgpu::TexelCopyBufferLayout {
1114 offset: 0,
1115 bytes_per_row: Some(w * 4),
1116 rows_per_image: Some(*h),
1117 },
1118 wgpu::Extent3d {
1119 width: *w,
1120 height: *h,
1121 depth_or_array_layers: 1,
1122 },
1123 );
1124 }
1125}
1126
1127pub fn metal_with_noise(
1128 device: &wgpu::Device,
1129 queue: &wgpu::Queue,
1130 metal: &[Image],
1131 coat: &CoatWobble,
1132) -> Result<(TextureArray, Option<u32>), String> {
1133 let fits = metal
1134 .iter()
1135 .all(|image| image.width == lacquer::SIZE && image.height == lacquer::SIZE);
1136 if !fits {
1137 let array = TextureArray::upload(device, queue, metal, Kind::Scalar, "metal maps")?;
1138 return Ok((array, None));
1139 }
1140 let mut layers = metal.to_vec();
1141 layers.push(lacquer::image(coat));
1142 layers.extend(noise_tiles::images());
1143 let array = TextureArray::upload(device, queue, &layers, Kind::Scalar, "metal maps")?;
1144 Ok((array, Some(metal.len() as u32)))
1145}
1146
1147#[derive(Clone, Copy, Debug, PartialEq)]
1148pub struct TextureFilter {
1149 pub content_bias: f32,
1150 pub detail_bias: f32,
1151 pub anisotropy: u16,
1152}
1153
1154impl Default for TextureFilter {
1155 fn default() -> Self {
1156 Self {
1157 content_bias: CONTENT_LOD_BIAS,
1158 detail_bias: DETAIL_LOD_BIAS,
1159 anisotropy: ANISOTROPY,
1160 }
1161 }
1162}
1163
1164impl TextureFilter {
1165 pub const UNBIASED: Self = Self {
1166 content_bias: 0.0,
1167 detail_bias: 0.0,
1168 anisotropy: 8,
1169 };
1170
1171 pub fn check(&self) -> Result<(), String> {
1172 let bias = |b: f32| b.is_finite() && (-4.0..=4.0).contains(&b);
1173 if !bias(self.content_bias) || !bias(self.detail_bias) {
1174 return Err("texture LOD biases must be between -4 and 4".into());
1175 }
1176 if !(1..=16).contains(&self.anisotropy) {
1177 return Err("anisotropy must be between 1 and 16".into());
1178 }
1179 Ok(())
1180 }
1181
1182 pub fn apply(&self, material: &mut GpuMapMaterial) {
1183 material.content_extra[1] = self.content_bias.exp2();
1184 material.content_extra[2] = self.detail_bias.exp2();
1185 }
1186
1187 pub fn sampler(&self) -> wgpu::SamplerDescriptor<'static> {
1188 wgpu::SamplerDescriptor {
1189 anisotropy_clamp: self.anisotropy,
1190 ..sampler_descriptor()
1191 }
1192 }
1193
1194 pub fn content_sampler(&self) -> wgpu::SamplerDescriptor<'static> {
1195 wgpu::SamplerDescriptor {
1196 anisotropy_clamp: self.anisotropy,
1197 ..content_sampler_descriptor()
1198 }
1199 }
1200}
1201
1202pub struct MapTextures {
1203 pub base: TextureArray,
1204 pub normal: TextureArray,
1205 pub roughness: TextureArray,
1206 pub metal: TextureArray,
1207 pub sampler: wgpu::Sampler,
1208 pub filter: TextureFilter,
1209 pub detail: Vec<DetailPart>,
1210 pub two_channel_normals: bool,
1211 pub noise: Option<u32>,
1212 pub coat: CoatWobble,
1213}
1214
1215#[derive(Clone, Copy, Debug, PartialEq)]
1216pub struct DetailPart {
1217 pub page: u32,
1218 pub transform: [f32; 4],
1219 pub uv_set: u32,
1220}
1221
1222pub fn detail_format(format: Format) -> wgpu::TextureFormat {
1223 match format {
1224 Format::Rgba8 => wgpu::TextureFormat::Rgba8Unorm,
1225 Format::Rgba8Srgb => wgpu::TextureFormat::Rgba8UnormSrgb,
1226 Format::Bc4 => wgpu::TextureFormat::Bc4RUnorm,
1227 Format::Bc5 => wgpu::TextureFormat::Bc5RgUnorm,
1228 Format::Bc7Srgb => wgpu::TextureFormat::Bc7RgbaUnormSrgb,
1229 }
1230}
1231
1232pub fn uploads_blocks(device: &wgpu::Device) -> bool {
1233 device
1234 .features()
1235 .contains(wgpu::Features::TEXTURE_COMPRESSION_BC)
1236}
1237
1238pub fn sampler_descriptor() -> wgpu::SamplerDescriptor<'static> {
1239 wgpu::SamplerDescriptor {
1240 label: Some("material map sampler"),
1241 address_mode_u: wgpu::AddressMode::Repeat,
1242 address_mode_v: wgpu::AddressMode::Repeat,
1243 mag_filter: wgpu::FilterMode::Linear,
1244 min_filter: wgpu::FilterMode::Linear,
1245 mipmap_filter: wgpu::MipmapFilterMode::Linear,
1246 anisotropy_clamp: ANISOTROPY,
1247 ..Default::default()
1248 }
1249}
1250
1251impl MapTextures {
1252 pub fn new(
1253 device: &wgpu::Device,
1254 queue: &wgpu::Queue,
1255 images: &MapImages<'_>,
1256 ) -> Result<Self, String> {
1257 let base = TextureArray::upload(device, queue, images.base, Kind::Base, "base maps")?;
1258 let normal =
1259 TextureArray::upload(device, queue, images.normal, Kind::Normal, "normal maps")?;
1260 let roughness = TextureArray::upload(
1261 device,
1262 queue,
1263 images.roughness,
1264 Kind::Scalar,
1265 "roughness maps",
1266 )?;
1267 let coat = CoatWobble::default();
1268 let (metal, noise) = metal_with_noise(device, queue, images.metal, &coat)?;
1269 let sampler = device.create_sampler(&sampler_descriptor());
1270 Ok(Self {
1271 base,
1272 normal,
1273 roughness,
1274 metal,
1275 sampler,
1276 filter: TextureFilter::default(),
1277 detail: Vec::new(),
1278 two_channel_normals: false,
1279 noise,
1280 coat,
1281 })
1282 }
1283
1284 pub fn detail(
1285 device: &wgpu::Device,
1286 queue: &wgpu::Queue,
1287 atlas: &Atlas,
1288 ) -> Result<Self, String> {
1289 let blocks = uploads_blocks(device);
1290 let base = TextureArray::pages(device, queue, &atlas.base, blocks, "base detail pages")?;
1291 let normal =
1292 TextureArray::pages(device, queue, &atlas.normal, blocks, "normal detail pages")?;
1293 let roughness = TextureArray::pages(
1294 device,
1295 queue,
1296 &atlas.roughness,
1297 blocks,
1298 "roughness detail pages",
1299 )?;
1300 let coat = CoatWobble::default();
1301 let (metal, noise) = metal_with_noise(device, queue, &[], &coat)?;
1302 let sampler = device.create_sampler(&sampler_descriptor());
1303 Ok(Self {
1304 base,
1305 normal,
1306 roughness,
1307 metal,
1308 sampler,
1309 filter: TextureFilter::default(),
1310 detail: atlas
1311 .parts
1312 .iter()
1313 .map(|part| DetailPart {
1314 page: part.page,
1315 transform: atlas.transform(part),
1316 uv_set: part.uv_set,
1317 })
1318 .collect(),
1319 two_channel_normals: atlas
1320 .normal
1321 .first()
1322 .is_some_and(|page| page.format == Format::Bc5),
1323 noise,
1324 coat,
1325 })
1326 }
1327
1328 pub fn fit_coat(&mut self, queue: &wgpu::Queue, materials: &[Material]) {
1329 let Some(layer) = self.noise else {
1330 return;
1331 };
1332 let Some(coat) = materials.iter().find_map(coat_of) else {
1333 return;
1334 };
1335 if coat == self.coat {
1336 return;
1337 }
1338 self.metal
1339 .write_layer(queue, layer, &lacquer::image(&coat), Kind::Scalar);
1340 self.coat = coat;
1341 }
1342
1343 pub fn set_filter(&mut self, device: &wgpu::Device, filter: TextureFilter) {
1344 if filter.anisotropy != self.filter.anisotropy {
1345 self.sampler = device.create_sampler(&filter.sampler());
1346 }
1347 self.filter = filter;
1348 }
1349
1350 pub fn material(&self, material: &Material, indices: MapIndices) -> GpuMapMaterial {
1351 let mut gpu = GpuMapMaterial::new(material, indices);
1352 self.filter.apply(&mut gpu);
1353 gpu.packed[2] = self.noise.map_or(-1.0, |layer| layer as f32);
1354 for (slot, layer) in material.layers.iter().enumerate() {
1355 if layer.kind == NoiseKind::CoatWobble
1356 && CoatWobble::from_params(&layer.params) != self.coat
1357 {
1358 gpu.params[slot * GPU_PARAMS / 4 + 3][3] = 0.0;
1359 }
1360 }
1361 if self.detail.is_empty() {
1362 return gpu;
1363 }
1364 let part = indices
1365 .base
1366 .filter(|_| gpu.seeds[3] == 1)
1367 .and_then(|part| self.detail.get(part as usize));
1368 match part {
1369 Some(part) => {
1370 let page = part.page as i32;
1371 gpu.indices = [page, page, page, -1];
1372 gpu.atlas = part.transform;
1373 gpu.packed[1] = part.uv_set as f32;
1374 gpu.seeds[3] = if self.two_channel_normals { 3 } else { 2 };
1375 }
1376 None => {
1377 gpu.indices = [-1; 4];
1378 gpu.seeds[3] = 0;
1379 }
1380 }
1381 gpu
1382 }
1383
1384 pub fn layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
1385 let texture = |binding| wgpu::BindGroupLayoutEntry {
1386 binding,
1387 visibility: wgpu::ShaderStages::FRAGMENT,
1388 ty: wgpu::BindingType::Texture {
1389 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1390 view_dimension: wgpu::TextureViewDimension::D2Array,
1391 multisampled: false,
1392 },
1393 count: None,
1394 };
1395 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1396 label: Some("material maps"),
1397 entries: &[
1398 texture(0),
1399 texture(1),
1400 texture(2),
1401 texture(3),
1402 wgpu::BindGroupLayoutEntry {
1403 binding: 4,
1404 visibility: wgpu::ShaderStages::FRAGMENT,
1405 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1406 count: None,
1407 },
1408 wgpu::BindGroupLayoutEntry {
1409 binding: 5,
1410 visibility: wgpu::ShaderStages::FRAGMENT,
1411 ty: wgpu::BindingType::Buffer {
1412 ty: wgpu::BufferBindingType::Storage { read_only: true },
1413 has_dynamic_offset: false,
1414 min_binding_size: None,
1415 },
1416 count: None,
1417 },
1418 ],
1419 })
1420 }
1421
1422 pub fn bind_group(
1423 &self,
1424 device: &wgpu::Device,
1425 layout: &wgpu::BindGroupLayout,
1426 materials: &wgpu::Buffer,
1427 ) -> wgpu::BindGroup {
1428 device.create_bind_group(&wgpu::BindGroupDescriptor {
1429 label: Some("material maps"),
1430 layout,
1431 entries: &[
1432 wgpu::BindGroupEntry {
1433 binding: 0,
1434 resource: wgpu::BindingResource::TextureView(&self.base.view),
1435 },
1436 wgpu::BindGroupEntry {
1437 binding: 1,
1438 resource: wgpu::BindingResource::TextureView(&self.normal.view),
1439 },
1440 wgpu::BindGroupEntry {
1441 binding: 2,
1442 resource: wgpu::BindingResource::TextureView(&self.roughness.view),
1443 },
1444 wgpu::BindGroupEntry {
1445 binding: 3,
1446 resource: wgpu::BindingResource::TextureView(&self.metal.view),
1447 },
1448 wgpu::BindGroupEntry {
1449 binding: 4,
1450 resource: wgpu::BindingResource::Sampler(&self.sampler),
1451 },
1452 wgpu::BindGroupEntry {
1453 binding: 5,
1454 resource: materials.as_entire_binding(),
1455 },
1456 ],
1457 })
1458 }
1459}
1460
1461const MIP_WGSL: &str = r#"
1462@group(0) @binding(0) var source: texture_2d<f32>;
1463@group(0) @binding(1) var source_sampler: sampler;
1464struct MipOut {
1465 @builtin(position) clip: vec4f,
1466 @location(0) uv: vec2f,
1467}
1468@vertex fn mip_vertex(@builtin(vertex_index) index: u32) -> MipOut {
1469 let uv = vec2f(f32((index << 1u) & 2u), f32(index & 2u));
1470 return MipOut(vec4f(uv * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), 0.0, 1.0), uv);
1471}
1472@fragment fn mip_fragment(input: MipOut) -> @location(0) vec4f {
1473 return textureSampleLevel(source, source_sampler, input.uv, 0.0);
1474}
1475"#;
1476
1477const MIP_COMPUTE_WGSL: &str = r#"
1478@group(0) @binding(0) var source: texture_2d<f32>;
1479@group(0) @binding(1) var source_sampler: sampler;
1480@group(0) @binding(2) var smaller: texture_storage_2d<rgba16float, write>;
1481@compute @workgroup_size(8, 8) fn mip_compute(@builtin(global_invocation_id) id: vec3u) {
1482 let size = textureDimensions(smaller);
1483 if (id.x >= size.x || id.y >= size.y) {
1484 return;
1485 }
1486 let uv = (vec2f(id.xy) + vec2f(0.5)) / vec2f(size);
1487 textureStore(smaller, id.xy, textureSampleLevel(source, source_sampler, uv, 0.0));
1488}
1489@group(1) @binding(0) var<uniform> region: vec4u;
1490@compute @workgroup_size(8, 8) fn mip_compute_region(@builtin(global_invocation_id) id: vec3u) {
1491 if (id.x >= region.z || id.y >= region.w) {
1492 return;
1493 }
1494 let size = textureDimensions(smaller);
1495 let at = id.xy + region.xy;
1496 let uv = (vec2f(at) + vec2f(0.5)) / vec2f(size);
1497 textureStore(smaller, at, textureSampleLevel(source, source_sampler, uv, 0.0));
1498}
1499"#;
1500
1501pub const REGION_STRIDE: u64 = 256;
1502
1503pub fn mip_compute_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1504 vec![
1505 wgpu::BindGroupLayoutEntry {
1506 binding: 0,
1507 visibility: wgpu::ShaderStages::COMPUTE,
1508 ty: wgpu::BindingType::Texture {
1509 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1510 view_dimension: wgpu::TextureViewDimension::D2,
1511 multisampled: false,
1512 },
1513 count: None,
1514 },
1515 wgpu::BindGroupLayoutEntry {
1516 binding: 1,
1517 visibility: wgpu::ShaderStages::COMPUTE,
1518 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1519 count: None,
1520 },
1521 wgpu::BindGroupLayoutEntry {
1522 binding: 2,
1523 visibility: wgpu::ShaderStages::COMPUTE,
1524 ty: wgpu::BindingType::StorageTexture {
1525 access: wgpu::StorageTextureAccess::WriteOnly,
1526 format: wgpu::TextureFormat::Rgba16Float,
1527 view_dimension: wgpu::TextureViewDimension::D2,
1528 },
1529 count: None,
1530 },
1531 ]
1532}
1533
1534pub fn mip_region_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1535 vec![wgpu::BindGroupLayoutEntry {
1536 binding: 0,
1537 visibility: wgpu::ShaderStages::COMPUTE,
1538 ty: wgpu::BindingType::Buffer {
1539 ty: wgpu::BufferBindingType::Uniform,
1540 has_dynamic_offset: true,
1541 min_binding_size: wgpu::BufferSize::new(16),
1542 },
1543 count: None,
1544 }]
1545}
1546
1547#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1548pub struct MipRegion {
1549 pub level: u32,
1550 pub rect: [u32; 4],
1551}
1552
1553fn level_extent(side: u32, level: u32) -> u32 {
1554 (side >> level).max(1)
1555}
1556
1557fn footprint(start: u32, end: u32, from: u32, to: u32) -> [u32; 2] {
1558 let scale = f64::from(to) / f64::from(from);
1559 let low = ((f64::from(start) - 0.5) * scale - 0.5).ceil() - 1.0;
1560 let high = ((f64::from(end) + 0.5) * scale - 0.5).ceil() + 1.0;
1561 let low = (low.max(0.0) as u32).min(to - 1);
1562 let high = (high.max(0.0) as u32).min(to - 1);
1563 [low, high + 1]
1564}
1565
1566pub fn mip_regions(size: [u32; 2], rect: [u32; 4]) -> Vec<MipRegion> {
1567 let levels = mip_count(size[0], size[1]);
1568 let mut regions = Vec::with_capacity(levels.saturating_sub(1) as usize);
1569 let mut x = [rect[0], rect[0] + rect[2]];
1570 let mut y = [rect[1], rect[1] + rect[3]];
1571 for level in 1..levels {
1572 let from = [
1573 level_extent(size[0], level - 1),
1574 level_extent(size[1], level - 1),
1575 ];
1576 let to = [level_extent(size[0], level), level_extent(size[1], level)];
1577 x = footprint(x[0], x[1], from[0], to[0]);
1578 y = footprint(y[0], y[1], from[1], to[1]);
1579 regions.push(MipRegion {
1580 level,
1581 rect: [x[0], y[0], x[1] - x[0], y[1] - y[0]],
1582 });
1583 }
1584 regions
1585}
1586
1587#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1588pub enum ContentFormat {
1589 Srgb8,
1590 Linear16,
1591}
1592
1593impl ContentFormat {
1594 pub fn texture_format(self) -> wgpu::TextureFormat {
1595 match self {
1596 Self::Srgb8 => wgpu::TextureFormat::Rgba8UnormSrgb,
1597 Self::Linear16 => wgpu::TextureFormat::Rgba16Float,
1598 }
1599 }
1600
1601 pub fn texel_bytes(self) -> u32 {
1602 match self {
1603 Self::Srgb8 => 4,
1604 Self::Linear16 => 8,
1605 }
1606 }
1607}
1608
1609pub fn mip_count(width: u32, height: u32) -> u32 {
1610 u32::BITS - width.max(height).max(1).leading_zeros()
1611}
1612
1613pub fn check_write(
1614 size: [u32; 2],
1615 format: ContentFormat,
1616 rect: [u32; 4],
1617 bytes: usize,
1618) -> Result<(), String> {
1619 let [x, y, width, height] = rect;
1620 if width == 0 || height == 0 {
1621 return Err("content write is empty".into());
1622 }
1623 if x.checked_add(width).is_none_or(|end| end > size[0])
1624 || y.checked_add(height).is_none_or(|end| end > size[1])
1625 {
1626 return Err(format!(
1627 "content write {rect:?} falls outside the {}x{} slot",
1628 size[0], size[1]
1629 ));
1630 }
1631 let expected = width as usize * height as usize * format.texel_bytes() as usize;
1632 if bytes != expected {
1633 return Err(format!(
1634 "content write of {width}x{height} needs {expected} bytes, got {bytes}"
1635 ));
1636 }
1637 Ok(())
1638}
1639
1640#[derive(Clone, Copy, Debug, PartialEq)]
1641pub struct CausticProjection {
1642 pub rect: [f32; 4],
1643 pub height: f32,
1644 pub toward_sun: [f32; 3],
1645}
1646
1647impl CausticProjection {
1648 pub fn uv(&self, point: [f32; 3]) -> Option<[f32; 2]> {
1649 let length = self.toward_sun.iter().map(|v| v * v).sum::<f32>().sqrt();
1650 if length.is_nan() || length <= 0.0 || self.toward_sun[1] / length <= 1e-4 {
1651 return None;
1652 }
1653 let toward = self.toward_sun.map(|v| v / length);
1654 let along = (self.height - point[1]) / toward[1];
1655 let q = [point[0] + toward[0] * along, point[2] + toward[2] * along];
1656 let at = [
1657 (q[0] - self.rect[0]) / self.rect[2],
1658 (q[1] - self.rect[1]) / self.rect[3],
1659 ];
1660 at.iter().all(|v| (0.0..=1.0).contains(v)).then_some(at)
1661 }
1662
1663 pub fn rows(projection: Option<Self>, slot: usize, strength: f32) -> [[f32; 4]; 3] {
1664 let Some(projection) = projection else {
1665 return [[0.0, 0.0, 1.0, 1.0], [0.0, 1.0, 0.0, 0.0], [0.0; 4]];
1666 };
1667 let length = projection
1668 .toward_sun
1669 .iter()
1670 .map(|v| v * v)
1671 .sum::<f32>()
1672 .sqrt();
1673 let toward = if length > 0.0 {
1674 projection.toward_sun.map(|v| v / length)
1675 } else {
1676 [0.0, 0.0, 0.0]
1677 };
1678 let on = toward[1] > 1e-4;
1679 [
1680 projection.rect,
1681 [toward[0], toward[1], toward[2], projection.height],
1682 [slot as f32, strength, f32::from(u8::from(on)), 0.0],
1683 ]
1684 }
1685}
1686
1687#[derive(Clone, Debug, PartialEq)]
1688pub struct Caustic {
1689 pub slot: usize,
1690 pub rect: [f32; 4],
1691 pub height: f32,
1692 pub strength: f32,
1693 pub receivers: Vec<usize>,
1694}
1695
1696impl Caustic {
1697 pub fn check(&self) -> Result<(), String> {
1698 if self.slot >= CONTENT_SLOTS {
1699 return Err(format!(
1700 "caustic slot {} is outside the {CONTENT_SLOTS} slots",
1701 self.slot
1702 ));
1703 }
1704 if !self.rect.iter().all(|v| v.is_finite())
1705 || self.rect[2] <= 0.0
1706 || self.rect[3] <= 0.0
1707 || !self.height.is_finite()
1708 || !self.strength.is_finite()
1709 {
1710 return Err(
1711 "caustic rectangle, height and strength must be finite, with a positive size"
1712 .into(),
1713 );
1714 }
1715 Ok(())
1716 }
1717
1718 pub fn projection(&self, toward_sun: [f32; 3]) -> CausticProjection {
1719 CausticProjection {
1720 rect: self.rect,
1721 height: self.height,
1722 toward_sun,
1723 }
1724 }
1725
1726 pub fn receives(&self, instance: usize) -> bool {
1727 self.receivers.contains(&instance)
1728 }
1729
1730 pub fn rows(caustic: Option<&Self>, toward_sun: [f32; 3]) -> [[f32; 4]; 3] {
1731 match caustic {
1732 Some(caustic) => CausticProjection::rows(
1733 Some(caustic.projection(toward_sun)),
1734 caustic.slot,
1735 caustic.strength,
1736 ),
1737 None => CausticProjection::rows(None, 0, 0.0),
1738 }
1739 }
1740}
1741
1742pub struct ContentTexture {
1743 pub texture: wgpu::Texture,
1744 pub view: wgpu::TextureView,
1745 pub width: u32,
1746 pub height: u32,
1747 pub format: ContentFormat,
1748 pub mips: u32,
1749 pub writes: u64,
1750 levels: Vec<wgpu::TextureView>,
1751 groups: Vec<wgpu::BindGroup>,
1752 compute_groups: Vec<wgpu::BindGroup>,
1753}
1754
1755impl ContentTexture {
1756 pub fn level(&self, level: u32) -> Option<&wgpu::TextureView> {
1757 self.levels.get(level as usize)
1758 }
1759}
1760
1761struct Mipper {
1762 layout: wgpu::BindGroupLayout,
1763 sampler: wgpu::Sampler,
1764 pipelines: [wgpu::RenderPipeline; 2],
1765 compute_layout: wgpu::BindGroupLayout,
1766 compute: wgpu::ComputePipeline,
1767 region_layout: wgpu::BindGroupLayout,
1768 region_compute: wgpu::ComputePipeline,
1769}
1770
1771impl Mipper {
1772 fn new(device: &wgpu::Device) -> Self {
1773 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1774 label: Some("content mips"),
1775 entries: &[
1776 wgpu::BindGroupLayoutEntry {
1777 binding: 0,
1778 visibility: wgpu::ShaderStages::FRAGMENT,
1779 ty: wgpu::BindingType::Texture {
1780 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1781 view_dimension: wgpu::TextureViewDimension::D2,
1782 multisampled: false,
1783 },
1784 count: None,
1785 },
1786 wgpu::BindGroupLayoutEntry {
1787 binding: 1,
1788 visibility: wgpu::ShaderStages::FRAGMENT,
1789 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1790 count: None,
1791 },
1792 ],
1793 });
1794 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1795 label: Some("content mips"),
1796 bind_group_layouts: &[Some(&layout)],
1797 immediate_size: 0,
1798 });
1799 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1800 label: Some("content mips"),
1801 source: wgpu::ShaderSource::Wgsl(MIP_WGSL.into()),
1802 });
1803 let pipelines = [ContentFormat::Srgb8, ContentFormat::Linear16].map(|format| {
1804 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1805 label: Some("content mips"),
1806 layout: Some(&pipeline_layout),
1807 vertex: wgpu::VertexState {
1808 module: &module,
1809 entry_point: Some("mip_vertex"),
1810 buffers: &[],
1811 compilation_options: Default::default(),
1812 },
1813 primitive: Default::default(),
1814 depth_stencil: None,
1815 multisample: Default::default(),
1816 fragment: Some(wgpu::FragmentState {
1817 module: &module,
1818 entry_point: Some("mip_fragment"),
1819 targets: &[Some(wgpu::ColorTargetState {
1820 format: format.texture_format(),
1821 blend: None,
1822 write_mask: wgpu::ColorWrites::ALL,
1823 })],
1824 compilation_options: Default::default(),
1825 }),
1826 multiview_mask: None,
1827 cache: None,
1828 })
1829 });
1830 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1831 label: Some("content mips"),
1832 mag_filter: wgpu::FilterMode::Linear,
1833 min_filter: wgpu::FilterMode::Linear,
1834 ..Default::default()
1835 });
1836 let compute_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1837 label: Some("content compute mips"),
1838 entries: &mip_compute_entries(),
1839 });
1840 let compute_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1841 label: Some("content compute mips"),
1842 source: wgpu::ShaderSource::Wgsl(MIP_COMPUTE_WGSL.into()),
1843 });
1844 let compute = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1845 label: Some("content compute mips"),
1846 layout: Some(
1847 &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1848 label: Some("content compute mips"),
1849 bind_group_layouts: &[Some(&compute_layout)],
1850 immediate_size: 0,
1851 }),
1852 ),
1853 module: &compute_module,
1854 entry_point: Some("mip_compute"),
1855 compilation_options: Default::default(),
1856 cache: None,
1857 });
1858 let region_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1859 label: Some("content compute mip region"),
1860 entries: &mip_region_entries(),
1861 });
1862 let region_compute = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1863 label: Some("content compute mip region"),
1864 layout: Some(
1865 &device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1866 label: Some("content compute mip region"),
1867 bind_group_layouts: &[Some(&compute_layout), Some(®ion_layout)],
1868 immediate_size: 0,
1869 }),
1870 ),
1871 module: &compute_module,
1872 entry_point: Some("mip_compute_region"),
1873 compilation_options: Default::default(),
1874 cache: None,
1875 });
1876 Self {
1877 layout,
1878 sampler,
1879 pipelines,
1880 compute_layout,
1881 compute,
1882 region_layout,
1883 region_compute,
1884 }
1885 }
1886
1887 fn pipeline(&self, format: ContentFormat) -> &wgpu::RenderPipeline {
1888 &self.pipelines[usize::from(format == ContentFormat::Linear16)]
1889 }
1890}
1891
1892pub struct ContentSlots {
1893 slots: Vec<Option<ContentTexture>>,
1894 blank: wgpu::TextureView,
1895 pub sampler: wgpu::Sampler,
1896 pub caustic: wgpu::Buffer,
1897 mipper: Mipper,
1898}
1899
1900pub fn content_sampler_descriptor() -> wgpu::SamplerDescriptor<'static> {
1901 wgpu::SamplerDescriptor {
1902 label: Some("content sampler"),
1903 address_mode_u: wgpu::AddressMode::ClampToEdge,
1904 address_mode_v: wgpu::AddressMode::ClampToEdge,
1905 mag_filter: wgpu::FilterMode::Linear,
1906 min_filter: wgpu::FilterMode::Linear,
1907 mipmap_filter: wgpu::MipmapFilterMode::Linear,
1908 anisotropy_clamp: ANISOTROPY,
1909 ..Default::default()
1910 }
1911}
1912
1913impl ContentSlots {
1914 pub fn set_filter(&mut self, device: &wgpu::Device, filter: TextureFilter) {
1915 self.sampler = device.create_sampler(&filter.content_sampler());
1916 }
1917
1918 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
1919 let blank = device.create_texture(&wgpu::TextureDescriptor {
1920 label: Some("blank content"),
1921 size: wgpu::Extent3d {
1922 width: 1,
1923 height: 1,
1924 depth_or_array_layers: 1,
1925 },
1926 mip_level_count: 1,
1927 sample_count: 1,
1928 dimension: wgpu::TextureDimension::D2,
1929 format: wgpu::TextureFormat::Rgba16Float,
1930 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1931 view_formats: &[],
1932 });
1933 queue.write_texture(
1934 blank.as_image_copy(),
1935 &[0; 8],
1936 wgpu::TexelCopyBufferLayout {
1937 offset: 0,
1938 bytes_per_row: Some(8),
1939 rows_per_image: Some(1),
1940 },
1941 wgpu::Extent3d {
1942 width: 1,
1943 height: 1,
1944 depth_or_array_layers: 1,
1945 },
1946 );
1947 Self {
1948 slots: (0..CONTENT_SLOTS).map(|_| None).collect(),
1949 blank: blank.create_view(&Default::default()),
1950 sampler: device.create_sampler(&content_sampler_descriptor()),
1951 caustic: wgpu::util::DeviceExt::create_buffer_init(
1952 device,
1953 &wgpu::util::BufferInitDescriptor {
1954 label: Some("content caustic"),
1955 contents: bytemuck::cast_slice(&CausticProjection::rows(None, 0, 0.0)),
1956 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1957 },
1958 ),
1959 mipper: Mipper::new(device),
1960 }
1961 }
1962
1963 pub fn write_caustic(&self, queue: &wgpu::Queue, rows: [[f32; 4]; 3]) {
1964 queue.write_buffer(&self.caustic, 0, bytemuck::cast_slice(&rows));
1965 }
1966
1967 pub fn get(&self, slot: usize) -> Option<&ContentTexture> {
1968 self.slots.get(slot).and_then(Option::as_ref)
1969 }
1970
1971 pub fn view(&self, slot: usize) -> &wgpu::TextureView {
1972 self.get(slot).map_or(&self.blank, |content| &content.view)
1973 }
1974
1975 pub fn allocate(
1976 &mut self,
1977 device: &wgpu::Device,
1978 slot: usize,
1979 width: u32,
1980 height: u32,
1981 format: ContentFormat,
1982 ) -> Result<(), String> {
1983 if slot >= CONTENT_SLOTS {
1984 return Err(format!(
1985 "content slot {slot} is outside the {CONTENT_SLOTS} slots"
1986 ));
1987 }
1988 let limit = device.limits().max_texture_dimension_2d;
1989 if width == 0 || height == 0 || width > limit || height > limit {
1990 return Err(format!("content slot {slot} cannot hold {width}x{height}"));
1991 }
1992 if self.get(slot).is_some_and(|content| {
1993 (content.width, content.height, content.format) == (width, height, format)
1994 }) {
1995 return Ok(());
1996 }
1997 let mips = mip_count(width, height);
1998 let texture = device.create_texture(&wgpu::TextureDescriptor {
1999 label: Some("content"),
2000 size: wgpu::Extent3d {
2001 width,
2002 height,
2003 depth_or_array_layers: 1,
2004 },
2005 mip_level_count: mips,
2006 sample_count: 1,
2007 dimension: wgpu::TextureDimension::D2,
2008 format: format.texture_format(),
2009 usage: wgpu::TextureUsages::TEXTURE_BINDING
2010 | wgpu::TextureUsages::COPY_DST
2011 | wgpu::TextureUsages::COPY_SRC
2012 | wgpu::TextureUsages::RENDER_ATTACHMENT
2013 | if format == ContentFormat::Linear16 {
2014 wgpu::TextureUsages::STORAGE_BINDING
2015 } else {
2016 wgpu::TextureUsages::empty()
2017 },
2018 view_formats: &[],
2019 });
2020 let levels: Vec<wgpu::TextureView> = (0..mips)
2021 .map(|level| {
2022 texture.create_view(&wgpu::TextureViewDescriptor {
2023 label: Some("content level"),
2024 base_mip_level: level,
2025 mip_level_count: Some(1),
2026 ..Default::default()
2027 })
2028 })
2029 .collect();
2030 let render_levels = if format == ContentFormat::Linear16 {
2031 0
2032 } else {
2033 levels.len() - 1
2034 };
2035 let groups = levels[..render_levels]
2036 .iter()
2037 .map(|source| {
2038 device.create_bind_group(&wgpu::BindGroupDescriptor {
2039 label: Some("content mip source"),
2040 layout: &self.mipper.layout,
2041 entries: &[
2042 wgpu::BindGroupEntry {
2043 binding: 0,
2044 resource: wgpu::BindingResource::TextureView(source),
2045 },
2046 wgpu::BindGroupEntry {
2047 binding: 1,
2048 resource: wgpu::BindingResource::Sampler(&self.mipper.sampler),
2049 },
2050 ],
2051 })
2052 })
2053 .collect();
2054 let compute_groups = if format == ContentFormat::Linear16 {
2055 levels
2056 .windows(2)
2057 .map(|pair| {
2058 device.create_bind_group(&wgpu::BindGroupDescriptor {
2059 label: Some("content compute mip"),
2060 layout: &self.mipper.compute_layout,
2061 entries: &[
2062 wgpu::BindGroupEntry {
2063 binding: 0,
2064 resource: wgpu::BindingResource::TextureView(&pair[0]),
2065 },
2066 wgpu::BindGroupEntry {
2067 binding: 1,
2068 resource: wgpu::BindingResource::Sampler(&self.mipper.sampler),
2069 },
2070 wgpu::BindGroupEntry {
2071 binding: 2,
2072 resource: wgpu::BindingResource::TextureView(&pair[1]),
2073 },
2074 ],
2075 })
2076 })
2077 .collect()
2078 } else {
2079 Vec::new()
2080 };
2081 self.slots[slot] = Some(ContentTexture {
2082 view: texture.create_view(&Default::default()),
2083 texture,
2084 width,
2085 height,
2086 format,
2087 mips,
2088 writes: 0,
2089 levels,
2090 groups,
2091 compute_groups,
2092 });
2093 Ok(())
2094 }
2095
2096 pub fn release(&mut self, slot: usize) {
2097 if let Some(entry) = self.slots.get_mut(slot) {
2098 *entry = None;
2099 }
2100 }
2101
2102 pub fn write(
2103 &mut self,
2104 device: &wgpu::Device,
2105 queue: &wgpu::Queue,
2106 slot: usize,
2107 rect: [u32; 4],
2108 bytes: &[u8],
2109 ) -> Result<(), String> {
2110 let content = self
2111 .slots
2112 .get_mut(slot)
2113 .and_then(Option::as_mut)
2114 .ok_or_else(|| format!("content slot {slot} is not allocated"))?;
2115 check_write(
2116 [content.width, content.height],
2117 content.format,
2118 rect,
2119 bytes.len(),
2120 )?;
2121 let [x, y, width, height] = rect;
2122 queue.write_texture(
2123 wgpu::TexelCopyTextureInfo {
2124 texture: &content.texture,
2125 mip_level: 0,
2126 origin: wgpu::Origin3d { x, y, z: 0 },
2127 aspect: wgpu::TextureAspect::All,
2128 },
2129 bytes,
2130 wgpu::TexelCopyBufferLayout {
2131 offset: 0,
2132 bytes_per_row: Some(width * content.format.texel_bytes()),
2133 rows_per_image: Some(height),
2134 },
2135 wgpu::Extent3d {
2136 width,
2137 height,
2138 depth_or_array_layers: 1,
2139 },
2140 );
2141 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2142 label: Some("content mips"),
2143 });
2144 encode_mips(&self.mipper, &mut encoder, content);
2145 queue.submit(Some(encoder.finish()));
2146 content.writes += 1;
2147 Ok(())
2148 }
2149
2150 pub fn copy(
2151 &mut self,
2152 device: &wgpu::Device,
2153 encoder: &mut wgpu::CommandEncoder,
2154 slot: usize,
2155 source: &wgpu::Texture,
2156 ) -> Result<ContentCopy, String> {
2157 let content = self
2158 .slots
2159 .get_mut(slot)
2160 .and_then(Option::as_mut)
2161 .ok_or_else(|| format!("content slot {slot} is not allocated"))?;
2162 let kind = copy_kind(
2163 source,
2164 content.format.texture_format(),
2165 [content.width, content.height],
2166 )?;
2167 match kind {
2168 ContentCopy::Copied => encoder.copy_texture_to_texture(
2169 source.as_image_copy(),
2170 content.texture.as_image_copy(),
2171 wgpu::Extent3d {
2172 width: content.width,
2173 height: content.height,
2174 depth_or_array_layers: 1,
2175 },
2176 ),
2177 ContentCopy::Drawn => {
2178 let view = source.create_view(&wgpu::TextureViewDescriptor {
2179 label: Some("content source"),
2180 dimension: Some(wgpu::TextureViewDimension::D2),
2181 base_mip_level: 0,
2182 mip_level_count: Some(1),
2183 base_array_layer: 0,
2184 array_layer_count: Some(1),
2185 ..Default::default()
2186 });
2187 let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2188 label: Some("content source"),
2189 layout: &self.mipper.layout,
2190 entries: &[
2191 wgpu::BindGroupEntry {
2192 binding: 0,
2193 resource: wgpu::BindingResource::TextureView(&view),
2194 },
2195 wgpu::BindGroupEntry {
2196 binding: 1,
2197 resource: wgpu::BindingResource::Sampler(&self.mipper.sampler),
2198 },
2199 ],
2200 });
2201 mip_pass(&self.mipper, encoder, content, &content.levels[0], &group);
2202 }
2203 }
2204 encode_mips(&self.mipper, encoder, content);
2205 content.writes += 1;
2206 Ok(kind)
2207 }
2208}
2209
2210impl ContentSlots {
2211 pub fn regenerate_regions(
2212 &mut self,
2213 device: &wgpu::Device,
2214 encoder: &mut wgpu::CommandEncoder,
2215 slot: usize,
2216 regions: &[MipRegion],
2217 ) -> Result<(), String> {
2218 let content = self
2219 .slots
2220 .get_mut(slot)
2221 .and_then(Option::as_mut)
2222 .ok_or_else(|| format!("content slot {slot} is not allocated"))?;
2223 for region in regions {
2224 let [x, y, width, height] = region.rect;
2225 let wide = level_extent(content.width, region.level);
2226 let high = level_extent(content.height, region.level);
2227 if region.level == 0
2228 || region.level >= content.mips
2229 || width == 0
2230 || height == 0
2231 || x + width > wide
2232 || y + height > high
2233 {
2234 return Err(format!(
2235 "mip region {:?} at level {} falls outside the {wide}x{high} level",
2236 region.rect, region.level
2237 ));
2238 }
2239 }
2240 if regions.is_empty() {
2241 return Ok(());
2242 }
2243 if content.compute_groups.is_empty() {
2244 for region in regions {
2245 let level = region.level as usize;
2246 let [x, y, width, height] = region.rect;
2247 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2248 multiview_mask: None,
2249 label: Some("content mip region"),
2250 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2251 view: &content.levels[level],
2252 resolve_target: None,
2253 depth_slice: None,
2254 ops: wgpu::Operations {
2255 load: wgpu::LoadOp::Load,
2256 store: wgpu::StoreOp::Store,
2257 },
2258 })],
2259 depth_stencil_attachment: None,
2260 timestamp_writes: None,
2261 occlusion_query_set: None,
2262 });
2263 pass.set_pipeline(self.mipper.pipeline(content.format));
2264 pass.set_bind_group(0, &content.groups[level - 1], &[]);
2265 pass.set_scissor_rect(x, y, width, height);
2266 pass.draw(0..3, 0..1);
2267 }
2268 } else {
2269 let mut origins = vec![0u8; regions.len() * REGION_STRIDE as usize];
2270 for (index, region) in regions.iter().enumerate() {
2271 origins[index * REGION_STRIDE as usize..][..16]
2272 .copy_from_slice(bytemuck::cast_slice(®ion.rect));
2273 }
2274 let buffer = wgpu::util::DeviceExt::create_buffer_init(
2275 device,
2276 &wgpu::util::BufferInitDescriptor {
2277 label: Some("content mip regions"),
2278 contents: &origins,
2279 usage: wgpu::BufferUsages::UNIFORM,
2280 },
2281 );
2282 let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2283 label: Some("content mip regions"),
2284 layout: &self.mipper.region_layout,
2285 entries: &[wgpu::BindGroupEntry {
2286 binding: 0,
2287 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2288 buffer: &buffer,
2289 offset: 0,
2290 size: wgpu::BufferSize::new(16),
2291 }),
2292 }],
2293 });
2294 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
2295 label: Some("content mip regions"),
2296 timestamp_writes: None,
2297 });
2298 pass.set_pipeline(&self.mipper.region_compute);
2299 for (index, region) in regions.iter().enumerate() {
2300 pass.set_bind_group(0, &content.compute_groups[region.level as usize - 1], &[]);
2301 pass.set_bind_group(1, &group, &[(index as u64 * REGION_STRIDE) as u32]);
2302 pass.dispatch_workgroups(region.rect[2].div_ceil(8), region.rect[3].div_ceil(8), 1);
2303 }
2304 }
2305 content.writes += 1;
2306 Ok(())
2307 }
2308
2309 pub fn regenerate(
2310 &mut self,
2311 encoder: &mut wgpu::CommandEncoder,
2312 slot: usize,
2313 ) -> Result<(), String> {
2314 let content = self
2315 .slots
2316 .get_mut(slot)
2317 .and_then(Option::as_mut)
2318 .ok_or_else(|| format!("content slot {slot} is not allocated"))?;
2319 encode_mips(&self.mipper, encoder, content);
2320 content.writes += 1;
2321 Ok(())
2322 }
2323}
2324
2325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2326pub enum ContentCopy {
2327 Copied,
2328 Drawn,
2329}
2330
2331pub fn copy_kind(
2332 source: &wgpu::Texture,
2333 format: wgpu::TextureFormat,
2334 size: [u32; 2],
2335) -> Result<ContentCopy, String> {
2336 if source.dimension() != wgpu::TextureDimension::D2 || source.sample_count() != 1 {
2337 return Err("a content source must be a single-sampled 2D texture".into());
2338 }
2339 let same_format = source.format().remove_srgb_suffix() == format.remove_srgb_suffix();
2340 let same_size = source.width() == size[0] && source.height() == size[1];
2341 if same_format && same_size && source.usage().contains(wgpu::TextureUsages::COPY_SRC) {
2342 return Ok(ContentCopy::Copied);
2343 }
2344 let filterable = matches!(
2345 source.format().sample_type(None, None),
2346 Some(wgpu::TextureSampleType::Float { filterable: true })
2347 );
2348 if filterable
2349 && source
2350 .usage()
2351 .contains(wgpu::TextureUsages::TEXTURE_BINDING)
2352 {
2353 return Ok(ContentCopy::Drawn);
2354 }
2355 Err(format!(
2356 "a {:?} {}x{} source cannot fill a {format:?} {}x{} slot: copy needs the same format and size with COPY_SRC, drawing needs a filterable TEXTURE_BINDING",
2357 source.format(),
2358 source.width(),
2359 source.height(),
2360 size[0],
2361 size[1]
2362 ))
2363}
2364
2365fn mip_pass(
2366 mipper: &Mipper,
2367 encoder: &mut wgpu::CommandEncoder,
2368 content: &ContentTexture,
2369 target: &wgpu::TextureView,
2370 group: &wgpu::BindGroup,
2371) {
2372 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2373 multiview_mask: None,
2374 label: Some("content mip"),
2375 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2376 view: target,
2377 resolve_target: None,
2378 depth_slice: None,
2379 ops: wgpu::Operations {
2380 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2381 store: wgpu::StoreOp::Store,
2382 },
2383 })],
2384 depth_stencil_attachment: None,
2385 timestamp_writes: None,
2386 occlusion_query_set: None,
2387 });
2388 pass.set_pipeline(mipper.pipeline(content.format));
2389 pass.set_bind_group(0, group, &[]);
2390 pass.draw(0..3, 0..1);
2391}
2392
2393fn encode_mips(mipper: &Mipper, encoder: &mut wgpu::CommandEncoder, content: &ContentTexture) {
2394 if content.compute_groups.is_empty() {
2395 for (level, group) in content.groups.iter().enumerate() {
2396 mip_pass(mipper, encoder, content, &content.levels[level + 1], group);
2397 }
2398 return;
2399 }
2400 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
2401 label: Some("content mips"),
2402 timestamp_writes: None,
2403 });
2404 pass.set_pipeline(&mipper.compute);
2405 for (level, group) in content.compute_groups.iter().enumerate() {
2406 let width = (content.width >> (level + 1)).max(1);
2407 let height = (content.height >> (level + 1)).max(1);
2408 pass.set_bind_group(0, group, &[]);
2409 pass.dispatch_workgroups(width.div_ceil(8), height.div_ceil(8), 1);
2410 }
2411}
2412
2413#[cfg(test)]
2414mod tests {
2415 use super::*;
2416 use pfx_gpu::wgpu::util::DeviceExt;
2417 use pfx_materials::{At, CoatWobble, Grime, PlankWood, Scratch};
2418
2419 const LOOK: pfx_materials::ContentLook = pfx_materials::ContentLook {
2420 ink_roughness: 0.5,
2421 ink_emboss_coated: 0.2,
2422 ink_emboss_bare: -0.1,
2423 photo_gain: 1.1,
2424 screen_gain: 1.5,
2425 photo_window: [0.0, 0.0, 1.0, 1.0],
2426 };
2427
2428 const PLANE_WGSL: &str = r#"
2429@group(0) @binding(0) var<uniform> plane_age: vec4f;
2430struct PlaneOut {
2431 @builtin(position) clip: vec4f,
2432 @location(0) uv: vec2f,
2433}
2434@vertex fn plane_vertex(@builtin(vertex_index) index: u32) -> PlaneOut {
2435 let corners = array<vec2f, 6>(
2436 vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0),
2437 vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0)
2438 );
2439 let uv = corners[index];
2440 return PlaneOut(vec4f(uv * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), 0.0, 1.0), uv);
2441}
2442@fragment fn plane_fragment(input: PlaneOut) -> @location(0) vec4f {
2443 let result = map_evaluate(vec3f(1.0), 0.8, 0.0, 0.05, 0.0, vec3f(0.0, 0.0, 1.0), vec4f(1.0, 0.0, 0.0, 1.0), input.uv * 0.02, vec3f(input.uv, 0.0), 0.0, plane_age.x, 0.0, 0u);
2444 return vec4f(result.base, 1.0);
2445}
2446"#;
2447
2448 fn image(width: u32, height: u32, colour: [u8; 4]) -> Image {
2449 Image {
2450 width,
2451 height,
2452 space: ColorSpace::Srgb,
2453 pixels: Pixels::Eight(colour.repeat((width * height) as usize)),
2454 }
2455 }
2456
2457 #[test]
2458 fn layer_indices_and_packing() {
2459 let base = [
2460 image(2, 2, [255, 255, 255, 255]),
2461 image(2, 2, [0, 0, 0, 255]),
2462 ];
2463 let normal = [image(2, 2, [128, 128, 255, 255])];
2464 let images = MapImages {
2465 base: &base,
2466 normal: &normal,
2467 ..Default::default()
2468 };
2469 let indices = MapIndices {
2470 base: Some(1),
2471 normal: Some(0),
2472 ..Default::default()
2473 };
2474 assert!(indices.validate(&images).is_ok());
2475 assert_eq!(indices.packed(), [1, 0, -1, -1]);
2476 assert!(
2477 MapIndices {
2478 base: Some(2),
2479 ..Default::default()
2480 }
2481 .validate(&images)
2482 .is_err()
2483 );
2484 let mut material = Material::default();
2485 material.layers[0] = PlankWood::SAMPLE.layer(0.6, u32::MAX);
2486 material.maps.layer = 1.0;
2487 assert_eq!(
2488 MapIndices::from_material(&material).unwrap().packed(),
2489 [1, 1, 1, 1]
2490 );
2491 let packed = GpuMapMaterial::new(&material, indices);
2492 assert_eq!(packed.indices, [1, 0, -1, -1]);
2493 assert_eq!(packed.layers[0][0], 3.0);
2494 assert_eq!(packed.layers[0][3].to_bits(), u32::MAX);
2495 assert_eq!(std::mem::size_of::<GpuMapMaterial>(), 464);
2496 assert_eq!(packed.params[0][0], PlankWood::SAMPLE.width);
2497 assert_eq!(packed.params[3][0], 1.0 / PlankWood::SAMPLE.width);
2498 assert_eq!(packed.params[3][1], 1.0 - PlankWood::SAMPLE.seam_floor);
2499 assert_eq!(packed.params[3][2], PlankWood::SAMPLE.rings * RING_FADE);
2500 let mut flat = material;
2501 flat.layers[0].params[0] = 0.0;
2502 assert_eq!(GpuMapMaterial::new(&flat, indices).layers[0][0], 0.0);
2503 }
2504
2505 #[test]
2506 fn colour_mips_average_light_not_encoded_values() {
2507 let checker = Image {
2508 width: 2,
2509 height: 2,
2510 space: ColorSpace::Srgb,
2511 pixels: Pixels::Eight(
2512 [[0, 0, 0, 255], [255, 255, 255, 255]]
2513 .repeat(2)
2514 .concat()
2515 .to_vec(),
2516 ),
2517 };
2518 let chain = mip_chain(&checker, Kind::Base).unwrap();
2519 assert_eq!(chain[0].2[..8], [0, 0, 0, 255, 255, 255, 255, 255]);
2520 let half = (encode_channel(0.5) * 255.0).round() as u8;
2521 assert_eq!(half, 188);
2522 assert_eq!(chain[1].2, [half, half, half, 255]);
2523 let scalar = mip_chain(&checker, Kind::Scalar).unwrap();
2524 assert_eq!(scalar[1].2, [128, 128, 128, 255]);
2525 let cutout = Image {
2526 width: 2,
2527 height: 1,
2528 space: ColorSpace::Srgb,
2529 pixels: Pixels::Eight(vec![255, 0, 0, 255, 0, 255, 0, 0]),
2530 };
2531 let chain = mip_chain(&cutout, Kind::Base).unwrap();
2532 assert_eq!(chain[1].2, [255, 0, 0, 128]);
2533 let deep = Image {
2534 width: 2,
2535 height: 1,
2536 space: ColorSpace::Srgb,
2537 pixels: Pixels::Sixteen(vec![771, 771, 771, 65535, 771, 771, 771, 65535]),
2538 };
2539 let dark = mip_chain(&deep, Kind::Base).unwrap();
2540 assert_eq!(dark[0].2[0], 3);
2541 for value in 0..=255u8 {
2542 let linear = linear_channel(value as f32 / 255.0);
2543 assert_eq!((encode_channel(linear) * 255.0).round() as u8, value);
2544 }
2545 let tilted = Image {
2546 width: 2,
2547 height: 1,
2548 space: ColorSpace::Linear,
2549 pixels: Pixels::Eight(vec![204, 128, 230, 255, 51, 128, 230, 255]),
2550 };
2551 let normal = mip_chain(&tilted, Kind::Normal).unwrap();
2552 assert_eq!(normal[0].2[..4], [204, 128, 230, 255]);
2553 assert_eq!(normal[1].2[2], 255, "{:?}", normal[1].2);
2554 }
2555
2556 #[test]
2557 fn baked_detail_selects_maps_and_bypasses_layers() {
2558 let mut material = Material::default();
2559 material.layers[0] = PlankWood::SAMPLE.layer(1.0, 37);
2560 select_baked_detail(&mut material, 3).unwrap();
2561 let packed = GpuMapMaterial::new(&material, MapIndices::from_material(&material).unwrap());
2562 assert_eq!(packed.indices, [3, 3, 3, -1]);
2563 assert_eq!(packed.seeds[3], 1);
2564 assert_eq!(packed.layers[0][0], 3.0);
2565 assert!(select_baked_detail(&mut material, i32::MAX as u32).is_err());
2566 }
2567
2568 #[test]
2569 fn mip_chain_and_sampling_setup() {
2570 let image = image(4, 4, [128, 128, 128, 255]);
2571 let chain = mip_chain(&image, Kind::Base).unwrap();
2572 assert_eq!(
2573 chain.iter().map(|(w, h, _)| (*w, *h)).collect::<Vec<_>>(),
2574 [(4, 4), (2, 2), (1, 1)]
2575 );
2576 assert_eq!(chain[0].2[..4], [128, 128, 128, 255]);
2577 assert_eq!(chain[2].2, [128, 128, 128, 255]);
2578 assert_eq!(chain[2].2.len(), 4);
2579 assert_eq!(Kind::Base.format(), wgpu::TextureFormat::Rgba8UnormSrgb);
2580 assert_eq!(Kind::Scalar.format(), wgpu::TextureFormat::Rgba8Unorm);
2581 let sampler = sampler_descriptor();
2582 assert_eq!(sampler.anisotropy_clamp, 16);
2583 assert_eq!(sampler.min_filter, wgpu::FilterMode::Linear);
2584 assert_eq!(sampler.mipmap_filter, wgpu::MipmapFilterMode::Linear);
2585 }
2586
2587 #[test]
2588 fn shader_parses_and_validates() {
2589 let source = format!("{}\n{}", shader_source(), PLANE_WGSL);
2590 let module = naga::front::wgsl::parse_str(&source).unwrap();
2591 naga::valid::Validator::new(
2592 naga::valid::ValidationFlags::all(),
2593 naga::valid::Capabilities::all(),
2594 )
2595 .validate(&module)
2596 .unwrap();
2597 }
2598
2599 #[test]
2600 fn age_zero_keeps_material_unaged() {
2601 let mut material = Material::default();
2602 material.ageing.yellow = 1.0;
2603 material.ageing.fade = 0.5;
2604 material.ageing.scratch = 0.2;
2605 material.ageing.seed = 23;
2606 let at = At {
2607 position: [0.3, 0.4, 0.0],
2608 uv: [0.3, 0.4],
2609 ..Default::default()
2610 };
2611 assert_eq!(pfx_materials::apply(&material, 0.0, &at), material);
2612 let packed = GpuMapMaterial::new(&material, MapIndices::default());
2613 assert_eq!(packed.age_a, [0.5, 1.0, 0.0, 0.2]);
2614 }
2615
2616 fn render_plane(
2617 gpu: &pfx_gpu::Gpu,
2618 pipeline: &wgpu::RenderPipeline,
2619 age_group: &wgpu::BindGroup,
2620 empty_group: &wgpu::BindGroup,
2621 maps_group: &wgpu::BindGroup,
2622 ) -> [f32; 3] {
2623 let target = gpu.device.create_texture(&wgpu::TextureDescriptor {
2624 label: Some("sheet plane"),
2625 size: wgpu::Extent3d {
2626 width: 64,
2627 height: 64,
2628 depth_or_array_layers: 1,
2629 },
2630 mip_level_count: 1,
2631 sample_count: 1,
2632 dimension: wgpu::TextureDimension::D2,
2633 format: wgpu::TextureFormat::Rgba8Unorm,
2634 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2635 view_formats: &[],
2636 });
2637 let view = target.create_view(&Default::default());
2638 let readback = gpu.device.create_buffer(&wgpu::BufferDescriptor {
2639 label: Some("sheet plane readback"),
2640 size: 64 * 64 * 4,
2641 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2642 mapped_at_creation: false,
2643 });
2644 let mut encoder = gpu
2645 .device
2646 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2647 label: Some("sheet plane"),
2648 });
2649 {
2650 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2651 multiview_mask: None,
2652 label: Some("sheet plane"),
2653 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2654 view: &view,
2655 resolve_target: None,
2656 depth_slice: None,
2657 ops: wgpu::Operations {
2658 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2659 store: wgpu::StoreOp::Store,
2660 },
2661 })],
2662 depth_stencil_attachment: None,
2663 timestamp_writes: None,
2664 occlusion_query_set: None,
2665 });
2666 pass.set_pipeline(pipeline);
2667 pass.set_bind_group(0, age_group, &[]);
2668 pass.set_bind_group(1, empty_group, &[]);
2669 pass.set_bind_group(2, empty_group, &[]);
2670 pass.set_bind_group(3, maps_group, &[]);
2671 pass.draw(0..6, 0..1);
2672 }
2673 encoder.copy_texture_to_buffer(
2674 target.as_image_copy(),
2675 wgpu::TexelCopyBufferInfo {
2676 buffer: &readback,
2677 layout: wgpu::TexelCopyBufferLayout {
2678 offset: 0,
2679 bytes_per_row: Some(256),
2680 rows_per_image: Some(64),
2681 },
2682 },
2683 target.size(),
2684 );
2685 gpu.queue.submit(Some(encoder.finish()));
2686 let (send, receive) = std::sync::mpsc::channel();
2687 readback
2688 .slice(..)
2689 .map_async(wgpu::MapMode::Read, move |result| {
2690 let _ = send.send(result);
2691 });
2692 gpu.device
2693 .poll(wgpu::PollType::wait_indefinitely())
2694 .unwrap();
2695 receive.recv().unwrap().unwrap();
2696 let mapped = readback
2697 .slice(..)
2698 .get_mapped_range()
2699 .expect("a mapped range");
2700 let mut mean = [0.0; 3];
2701 for pixel in mapped.chunks_exact(4) {
2702 for c in 0..3 {
2703 mean[c] += pixel[c] as f32 / 255.0 / 4096.0;
2704 }
2705 }
2706 drop(mapped);
2707 readback.unmap();
2708 mean
2709 }
2710
2711 #[test]
2712 #[ignore = "needs a GPU; run under pgpu"]
2713 fn a_sheet_yellows_with_age() {
2714 let started = std::time::Instant::now();
2715 let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
2716 let sheet = image(4, 4, [230, 220, 190, 255]);
2717 let images = MapImages {
2718 base: std::slice::from_ref(&sheet),
2719 ..Default::default()
2720 };
2721 let textures = MapTextures::new(&gpu.device, &gpu.queue, &images).unwrap();
2722 assert_eq!(textures.base.mips, 3);
2723 let mut material = Material::default();
2724 material.ageing.yellow = 1.0;
2725 material.ageing.seed = 42;
2726 let indices = MapIndices {
2727 base: Some(0),
2728 ..Default::default()
2729 };
2730 let maps = [GpuMapMaterial::new(&material, indices)];
2731 let map_buffer = gpu
2732 .device
2733 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
2734 label: Some("sheet map material"),
2735 contents: bytemuck::cast_slice(&maps),
2736 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2737 });
2738 let map_layout = MapTextures::layout(&gpu.device);
2739 let maps_group = textures.bind_group(&gpu.device, &map_layout, &map_buffer);
2740 let age_layout = gpu
2741 .device
2742 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2743 label: Some("sheet age"),
2744 entries: &[wgpu::BindGroupLayoutEntry {
2745 binding: 0,
2746 visibility: wgpu::ShaderStages::FRAGMENT,
2747 ty: wgpu::BindingType::Buffer {
2748 ty: wgpu::BufferBindingType::Uniform,
2749 has_dynamic_offset: false,
2750 min_binding_size: None,
2751 },
2752 count: None,
2753 }],
2754 });
2755 let empty_layout = gpu
2756 .device
2757 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2758 label: Some("empty"),
2759 entries: &[],
2760 });
2761 let empty_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
2762 label: Some("empty"),
2763 layout: &empty_layout,
2764 entries: &[],
2765 });
2766 let pipeline_layout = gpu
2767 .device
2768 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2769 label: Some("sheet pipeline"),
2770 bind_group_layouts: &[
2771 Some(&age_layout),
2772 Some(&empty_layout),
2773 Some(&empty_layout),
2774 Some(&map_layout),
2775 ],
2776 immediate_size: 0,
2777 });
2778 let source = format!("{}\n{}", shader_source(), PLANE_WGSL);
2779 let module = gpu
2780 .device
2781 .create_shader_module(wgpu::ShaderModuleDescriptor {
2782 label: Some("sheet maps shader"),
2783 source: wgpu::ShaderSource::Wgsl(source.into()),
2784 });
2785 let pipeline = gpu
2786 .device
2787 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2788 label: Some("sheet maps pipeline"),
2789 layout: Some(&pipeline_layout),
2790 vertex: wgpu::VertexState {
2791 module: &module,
2792 entry_point: Some("plane_vertex"),
2793 buffers: &[],
2794 compilation_options: Default::default(),
2795 },
2796 primitive: wgpu::PrimitiveState {
2797 cull_mode: None,
2798 ..Default::default()
2799 },
2800 depth_stencil: None,
2801 multisample: Default::default(),
2802 fragment: Some(wgpu::FragmentState {
2803 module: &module,
2804 entry_point: Some("plane_fragment"),
2805 targets: &[Some(wgpu::ColorTargetState {
2806 format: wgpu::TextureFormat::Rgba8Unorm,
2807 blend: None,
2808 write_mask: wgpu::ColorWrites::ALL,
2809 })],
2810 compilation_options: Default::default(),
2811 }),
2812 multiview_mask: None,
2813 cache: None,
2814 });
2815 let draw = |age: f32| {
2816 let age_buffer = gpu
2817 .device
2818 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
2819 label: Some("sheet age input"),
2820 contents: bytemuck::cast_slice(&[age, 0.0, 0.0, 0.0]),
2821 usage: wgpu::BufferUsages::UNIFORM,
2822 });
2823 let age_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
2824 label: Some("sheet age input"),
2825 layout: &age_layout,
2826 entries: &[wgpu::BindGroupEntry {
2827 binding: 0,
2828 resource: age_buffer.as_entire_binding(),
2829 }],
2830 });
2831 render_plane(&gpu, &pipeline, &age_group, &empty_group, &maps_group)
2832 };
2833 let young = draw(0.0);
2834 let aged = draw(1.0);
2835 let mut unaged_material = material;
2836 unaged_material.ageing = Default::default();
2837 let unaged = GpuMapMaterial::new(&unaged_material, indices);
2838 gpu.queue
2839 .write_buffer(&map_buffer, 0, bytemuck::bytes_of(&unaged));
2840 let baseline = draw(1.0);
2841 for channel in 0..3 {
2842 assert!((young[channel] - baseline[channel]).abs() < 1.0 / 255.0);
2843 }
2844 assert!(
2845 aged[0] - aged[2] > young[0] - young[2] + 0.01,
2846 "young {young:?}, aged {aged:?}"
2847 );
2848 println!(
2849 "sheet plane age 0/1: {:?}; young {young:?}; aged {aged:?}",
2850 started.elapsed()
2851 );
2852 }
2853 const CONTENT_TEST_WGSL: &str = r#"
2854struct ContentProbe {
2855 material: vec4u,
2856 transform: vec4f,
2857 crop: vec4f,
2858}
2859@group(0) @binding(0) var<uniform> probe: ContentProbe;
2860struct ProbeOut {
2861 @builtin(position) clip: vec4f,
2862 @location(0) uv: vec2f,
2863}
2864struct ProbeTargets {
2865 @location(0) base: vec4f,
2866 @location(1) emission: vec4f,
2867}
2868@vertex fn probe_vertex(@builtin(vertex_index) index: u32) -> ProbeOut {
2869 let uv = vec2f(f32((index << 1u) & 2u), f32(index & 2u));
2870 return ProbeOut(vec4f(uv * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), 0.0, 1.0), uv);
2871}
2872@fragment fn probe_fragment(input: ProbeOut) -> ProbeTargets {
2873 let dx = dpdx(input.uv);
2874 let dy = dpdy(input.uv);
2875 var surface: Shaded;
2876 surface.base = vec3f(0.8, 0.6, 0.4);
2877 surface.roughness = 0.9;
2878 surface.subsurface = 0.5;
2879 surface.emission = vec3f(0.2, 0.1, 0.0);
2880 surface.specular = 0.04;
2881 surface.normal = vec3f(0.0, 0.0, 1.0);
2882 let out = content_apply(surface, probe.material.x, input.uv, dx, dy, probe.transform, probe.crop, vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), vec3f(0.0, 0.0, 1.0));
2883 if (probe.material.y == 1u) {
2884 return ProbeTargets(vec4f(out.normal, 0.0), vec4f(0.0));
2885 }
2886 if (probe.material.y == 2u) {
2887 return ProbeTargets(vec4f(out.base, out.roughness), vec4f(out.metalness, out.specular, out.emission.r, out.subsurface));
2888 }
2889 return ProbeTargets(vec4f(out.base, out.roughness), vec4f(out.emission, out.subsurface));
2890}
2891"#;
2892
2893 fn ramp_texels(size: u32) -> (Vec<[f32; 4]>, Vec<u8>) {
2894 let mut texels = Vec::new();
2895 let mut bytes = Vec::new();
2896 for _y in 0..size {
2897 for x in 0..size {
2898 let a = half::f16::from_f32(x as f32 / (size - 1) as f32).to_f32();
2899 let rgba = [0.1 * a, 0.2 * a, 0.3 * a, a].map(half::f16::from_f32);
2900 texels.push(rgba.map(|v| v.to_f32()));
2901 for value in rgba {
2902 bytes.extend_from_slice(&value.to_bits().to_le_bytes());
2903 }
2904 }
2905 }
2906 (texels, bytes)
2907 }
2908
2909 fn read_targets(
2910 gpu: &pfx_gpu::Gpu,
2911 targets: &[wgpu::Texture],
2912 size: u32,
2913 ) -> Vec<Vec<[f32; 4]>> {
2914 let row = (size * 16).div_ceil(256) * 256;
2915 targets
2916 .iter()
2917 .map(|texture| {
2918 let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
2919 label: Some("content probe readback"),
2920 size: u64::from(row * size),
2921 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2922 mapped_at_creation: false,
2923 });
2924 let mut encoder = gpu.device.create_command_encoder(&Default::default());
2925 encoder.copy_texture_to_buffer(
2926 texture.as_image_copy(),
2927 wgpu::TexelCopyBufferInfo {
2928 buffer: &buffer,
2929 layout: wgpu::TexelCopyBufferLayout {
2930 offset: 0,
2931 bytes_per_row: Some(row),
2932 rows_per_image: Some(size),
2933 },
2934 },
2935 texture.size(),
2936 );
2937 gpu.queue.submit(Some(encoder.finish()));
2938 buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
2939 gpu.device
2940 .poll(wgpu::PollType::wait_indefinitely())
2941 .unwrap();
2942 let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
2943 let mut values = Vec::new();
2944 for y in 0..size {
2945 let start = (y * row) as usize;
2946 let floats: &[f32] =
2947 bytemuck::cast_slice(&mapped[start..start + (size * 16) as usize]);
2948 values.extend(floats.chunks_exact(4).map(|v| [v[0], v[1], v[2], v[3]]));
2949 }
2950 drop(mapped);
2951 buffer.unmap();
2952 values
2953 })
2954 .collect()
2955 }
2956
2957 type Probe<'a> = Box<dyn Fn([u32; 2], [f32; 4], [f32; 4]) -> Vec<Vec<[f32; 4]>> + 'a>;
2958
2959 fn content_probe<'a>(
2960 gpu: &'a pfx_gpu::Gpu,
2961 slots: &ContentSlots,
2962 materials: &[GpuMapMaterial],
2963 size: u32,
2964 ) -> Probe<'a> {
2965 let map_buffer = gpu
2966 .device
2967 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
2968 label: Some("content probe materials"),
2969 contents: bytemuck::cast_slice(materials),
2970 usage: wgpu::BufferUsages::STORAGE,
2971 });
2972 let texture_entry = |binding| wgpu::BindGroupLayoutEntry {
2973 binding,
2974 visibility: wgpu::ShaderStages::FRAGMENT,
2975 ty: wgpu::BindingType::Texture {
2976 sample_type: wgpu::TextureSampleType::Float { filterable: true },
2977 view_dimension: wgpu::TextureViewDimension::D2,
2978 multisampled: false,
2979 },
2980 count: None,
2981 };
2982 let content_layout =
2983 gpu.device
2984 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2985 label: Some("content probe"),
2986 entries: &[
2987 wgpu::BindGroupLayoutEntry {
2988 binding: 5,
2989 visibility: wgpu::ShaderStages::FRAGMENT,
2990 ty: wgpu::BindingType::Buffer {
2991 ty: wgpu::BufferBindingType::Storage { read_only: true },
2992 has_dynamic_offset: false,
2993 min_binding_size: None,
2994 },
2995 count: None,
2996 },
2997 texture_entry(CONTENT_BINDING),
2998 texture_entry(CONTENT_BINDING + 1),
2999 texture_entry(CONTENT_BINDING + 2),
3000 texture_entry(CONTENT_BINDING + 3),
3001 wgpu::BindGroupLayoutEntry {
3002 binding: CONTENT_BINDING + 4,
3003 visibility: wgpu::ShaderStages::FRAGMENT,
3004 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3005 count: None,
3006 },
3007 ],
3008 });
3009 let content_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
3010 label: Some("content probe"),
3011 layout: &content_layout,
3012 entries: &[
3013 wgpu::BindGroupEntry {
3014 binding: 5,
3015 resource: map_buffer.as_entire_binding(),
3016 },
3017 wgpu::BindGroupEntry {
3018 binding: CONTENT_BINDING,
3019 resource: wgpu::BindingResource::TextureView(slots.view(0)),
3020 },
3021 wgpu::BindGroupEntry {
3022 binding: CONTENT_BINDING + 1,
3023 resource: wgpu::BindingResource::TextureView(slots.view(1)),
3024 },
3025 wgpu::BindGroupEntry {
3026 binding: CONTENT_BINDING + 2,
3027 resource: wgpu::BindingResource::TextureView(slots.view(2)),
3028 },
3029 wgpu::BindGroupEntry {
3030 binding: CONTENT_BINDING + 3,
3031 resource: wgpu::BindingResource::TextureView(slots.view(3)),
3032 },
3033 wgpu::BindGroupEntry {
3034 binding: CONTENT_BINDING + 4,
3035 resource: wgpu::BindingResource::Sampler(&slots.sampler),
3036 },
3037 ],
3038 });
3039 let probe_layout = gpu
3040 .device
3041 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3042 label: Some("content probe uniform"),
3043 entries: &[wgpu::BindGroupLayoutEntry {
3044 binding: 0,
3045 visibility: wgpu::ShaderStages::FRAGMENT,
3046 ty: wgpu::BindingType::Buffer {
3047 ty: wgpu::BufferBindingType::Uniform,
3048 has_dynamic_offset: false,
3049 min_binding_size: None,
3050 },
3051 count: None,
3052 }],
3053 });
3054 let empty_layout = gpu
3055 .device
3056 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3057 label: Some("empty"),
3058 entries: &[],
3059 });
3060 let empty_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
3061 label: Some("empty"),
3062 layout: &empty_layout,
3063 entries: &[],
3064 });
3065 let pipeline_layout = gpu
3066 .device
3067 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3068 label: Some("content probe"),
3069 bind_group_layouts: &[
3070 Some(&probe_layout),
3071 Some(&empty_layout),
3072 Some(&empty_layout),
3073 Some(&content_layout),
3074 ],
3075 immediate_size: 0,
3076 });
3077 let module = gpu
3078 .device
3079 .create_shader_module(wgpu::ShaderModuleDescriptor {
3080 label: Some("content probe"),
3081 source: wgpu::ShaderSource::Wgsl(
3082 format!("{}\n{}", content_source(), CONTENT_TEST_WGSL).into(),
3083 ),
3084 });
3085 let target_state = Some(wgpu::ColorTargetState {
3086 format: wgpu::TextureFormat::Rgba32Float,
3087 blend: None,
3088 write_mask: wgpu::ColorWrites::ALL,
3089 });
3090 let pipeline = gpu
3091 .device
3092 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3093 label: Some("content probe"),
3094 layout: Some(&pipeline_layout),
3095 vertex: wgpu::VertexState {
3096 module: &module,
3097 entry_point: Some("probe_vertex"),
3098 buffers: &[],
3099 compilation_options: Default::default(),
3100 },
3101 primitive: Default::default(),
3102 depth_stencil: None,
3103 multisample: Default::default(),
3104 fragment: Some(wgpu::FragmentState {
3105 module: &module,
3106 entry_point: Some("probe_fragment"),
3107 targets: &[target_state.clone(), target_state],
3108 compilation_options: Default::default(),
3109 }),
3110 multiview_mask: None,
3111 cache: None,
3112 });
3113 Box::new(
3114 move |material: [u32; 2], transform: [f32; 4], crop: [f32; 4]| {
3115 let targets: Vec<wgpu::Texture> = (0..2)
3116 .map(|_| {
3117 gpu.device.create_texture(&wgpu::TextureDescriptor {
3118 label: Some("content probe target"),
3119 size: wgpu::Extent3d {
3120 width: size,
3121 height: size,
3122 depth_or_array_layers: 1,
3123 },
3124 mip_level_count: 1,
3125 sample_count: 1,
3126 dimension: wgpu::TextureDimension::D2,
3127 format: wgpu::TextureFormat::Rgba32Float,
3128 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
3129 | wgpu::TextureUsages::COPY_SRC,
3130 view_formats: &[],
3131 })
3132 })
3133 .collect();
3134 let views: Vec<wgpu::TextureView> = targets
3135 .iter()
3136 .map(|texture| texture.create_view(&Default::default()))
3137 .collect();
3138 let mut uniform = [0u8; 48];
3139 uniform[..8].copy_from_slice(bytemuck::cast_slice(&material));
3140 uniform[16..32].copy_from_slice(bytemuck::cast_slice(&transform));
3141 uniform[32..48].copy_from_slice(bytemuck::cast_slice(&crop));
3142 let probe_buffer =
3143 gpu.device
3144 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
3145 label: Some("content probe uniform"),
3146 contents: &uniform,
3147 usage: wgpu::BufferUsages::UNIFORM,
3148 });
3149 let probe_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
3150 label: Some("content probe uniform"),
3151 layout: &probe_layout,
3152 entries: &[wgpu::BindGroupEntry {
3153 binding: 0,
3154 resource: probe_buffer.as_entire_binding(),
3155 }],
3156 });
3157 let mut encoder = gpu.device.create_command_encoder(&Default::default());
3158 {
3159 let attachments: Vec<Option<wgpu::RenderPassColorAttachment<'_>>> = views
3160 .iter()
3161 .map(|view| {
3162 Some(wgpu::RenderPassColorAttachment {
3163 view,
3164 resolve_target: None,
3165 depth_slice: None,
3166 ops: wgpu::Operations {
3167 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
3168 store: wgpu::StoreOp::Store,
3169 },
3170 })
3171 })
3172 .collect();
3173 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3174 multiview_mask: None,
3175 label: Some("content probe"),
3176 color_attachments: &attachments,
3177 depth_stencil_attachment: None,
3178 timestamp_writes: None,
3179 occlusion_query_set: None,
3180 });
3181 pass.set_pipeline(&pipeline);
3182 pass.set_bind_group(0, &probe_group, &[]);
3183 pass.set_bind_group(1, &empty_group, &[]);
3184 pass.set_bind_group(2, &empty_group, &[]);
3185 pass.set_bind_group(3, &content_group, &[]);
3186 pass.draw(0..3, 0..1);
3187 }
3188 gpu.queue.submit(Some(encoder.finish()));
3189 read_targets(gpu, &targets, size)
3190 },
3191 )
3192 }
3193
3194 #[test]
3195 #[ignore = "needs a GPU; run under pgpu"]
3196 fn content_layers_match_the_cpu_reference() {
3197 use pfx_materials::{Blend, Content, ContentLayer, Shaded};
3198 let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3199 let size = 8;
3200 let mut slots = ContentSlots::new(&gpu.device, &gpu.queue);
3201 let (ink, ink_bytes) = ramp_texels(size);
3202 slots
3203 .allocate(&gpu.device, 0, size, size, ContentFormat::Linear16)
3204 .unwrap();
3205 slots
3206 .write(&gpu.device, &gpu.queue, 0, [0, 0, size, size], &ink_bytes)
3207 .unwrap();
3208 let frame_srgb = [200u8, 100, 50, 255];
3209 slots
3210 .allocate(&gpu.device, 1, size, size, ContentFormat::Srgb8)
3211 .unwrap();
3212 slots
3213 .write(
3214 &gpu.device,
3215 &gpu.queue,
3216 1,
3217 [0, 0, size, size],
3218 &frame_srgb.repeat((size * size) as usize),
3219 )
3220 .unwrap();
3221 let decode = |v: u8| linear_channel(v as f32 / 255.0);
3222 let video = [
3223 decode(frame_srgb[0]),
3224 decode(frame_srgb[1]),
3225 decode(frame_srgb[2]),
3226 1.0,
3227 ];
3228 let layers = [
3229 ContentLayer::from_kind(Content::Ink, 1.0, 0, &LOOK),
3230 ContentLayer::from_kind(Content::Photo, 1.0, 1, &LOOK),
3231 ContentLayer::from_kind(Content::Screen, 1.0, 1, &LOOK),
3232 ContentLayer::from_kind(Content::Decal, 1.0, 0, &LOOK),
3233 ];
3234 assert_eq!(layers[2].blend, Blend::Emit);
3235 let materials: Vec<GpuMapMaterial> = layers
3236 .iter()
3237 .map(|layer| {
3238 let material = Material {
3239 content_layer: *layer,
3240 ..Material::default()
3241 };
3242 GpuMapMaterial::new(&material, MapIndices::default())
3243 })
3244 .collect();
3245 let run = content_probe(&gpu, &slots, &materials, size);
3246 let base = Shaded::from_material(&Material {
3247 base: [0.8, 0.6, 0.4],
3248 roughness: 0.9,
3249 subsurface: 0.5,
3250 emission: [0.2, 0.1, 0.0],
3251 ..Material::default()
3252 });
3253 let identity = [0.0, 0.0, 1.0, 1.0];
3254 let whole = [0.0, 0.0, 1.0, 1.0];
3255 let close = |got: f32, want: f32, what: &str| {
3256 assert!((got - want).abs() < 2e-3, "{what}: gpu {got}, cpu {want}");
3257 };
3258 let check = |out: &[Vec<[f32; 4]>], index: usize, expected: &Shaded, what: &str| {
3259 for (c, (base, emission)) in expected.base.iter().zip(expected.emission).enumerate() {
3260 close(out[0][index][c], *base, what);
3261 close(out[1][index][c], emission, what);
3262 }
3263 close(out[0][index][3], expected.roughness, what);
3264 close(out[1][index][3], expected.subsurface, what);
3265 };
3266 let over = run([0, 0], identity, whole);
3267 for y in 0..size as usize {
3268 for x in 0..size as usize {
3269 let index = y * size as usize + x;
3270 let expected = layers[0].composite(&base, ink[index]);
3271 check(&over, index, &expected, "ink over");
3272 }
3273 }
3274 let normals = run([0, 1], identity, whole);
3275 let row = 3 * size as usize;
3276 for x in 1..size as usize - 1 {
3277 let n = normals[0][row + x];
3278 assert!(
3279 n[0] < -0.01,
3280 "ink rising along +u tilts the normal to -u: {n:?}"
3281 );
3282 assert!(n[1].abs() < 1e-3 && n[2] > 0.9, "{n:?}");
3283 }
3284 let photo = run([1, 0], identity, whole);
3285 let expected = layers[1].composite(&base, video);
3286 check(&photo, 9, &expected, "photo multiply");
3287 let screen = run([2, 0], identity, whole);
3288 let expected = layers[2].composite(&base, video);
3289 check(&screen, 9, &expected, "screen emission");
3290 assert!(screen[1][9][0] > base.emission[0] + 0.5);
3291 let shifted = run([3, 0], [0.5, 0.0, 1.0, 1.0], whole);
3292 for y in 0..size as usize {
3293 for x in 0..size as usize {
3294 let index = y * size as usize + x;
3295 let expected = if x + 4 < size as usize {
3296 layers[3].composite(&base, ink[y * size as usize + x + 4])
3297 } else {
3298 base
3299 };
3300 check(&shifted, index, &expected, "offset and crop");
3301 }
3302 }
3303 let cropped = run([3, 0], identity, [0.0, 0.0, 0.5, 1.0]);
3304 let inside = layers[3].composite(&base, ink[2]);
3305 check(&cropped, 2, &inside, "inside the crop");
3306 check(&cropped, 6, &base, "outside the crop");
3307 }
3308
3309 fn read_level(gpu: &pfx_gpu::Gpu, content: &ContentTexture, level: u32) -> Vec<u8> {
3310 let width = (content.width >> level).max(1);
3311 let height = (content.height >> level).max(1);
3312 let texel = content.format.texel_bytes();
3313 let row = (width * texel).div_ceil(256) * 256;
3314 let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
3315 label: Some("content level"),
3316 size: u64::from(row * height),
3317 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3318 mapped_at_creation: false,
3319 });
3320 let mut encoder = gpu.device.create_command_encoder(&Default::default());
3321 encoder.copy_texture_to_buffer(
3322 wgpu::TexelCopyTextureInfo {
3323 texture: &content.texture,
3324 mip_level: level,
3325 origin: wgpu::Origin3d::ZERO,
3326 aspect: wgpu::TextureAspect::All,
3327 },
3328 wgpu::TexelCopyBufferInfo {
3329 buffer: &buffer,
3330 layout: wgpu::TexelCopyBufferLayout {
3331 offset: 0,
3332 bytes_per_row: Some(row),
3333 rows_per_image: Some(height),
3334 },
3335 },
3336 wgpu::Extent3d {
3337 width,
3338 height,
3339 depth_or_array_layers: 1,
3340 },
3341 );
3342 gpu.queue.submit(Some(encoder.finish()));
3343 buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
3344 gpu.device
3345 .poll(wgpu::PollType::wait_indefinitely())
3346 .unwrap();
3347 let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
3348 mapped
3349 .chunks_exact(row as usize)
3350 .flat_map(|line| line[..(width * texel) as usize].to_vec())
3351 .collect()
3352 }
3353
3354 fn halves(bytes: &[u8]) -> Vec<f32> {
3355 bytes
3356 .chunks_exact(2)
3357 .map(|b| half::f16::from_bits(u16::from_le_bytes([b[0], b[1]])).to_f32())
3358 .collect()
3359 }
3360
3361 fn pattern(width: u32, height: u32, phase: f32) -> Vec<u8> {
3362 (0..width * height)
3363 .flat_map(|i| {
3364 let (x, y) = ((i % width) as f32, (i / width) as f32);
3365 [
3366 (x * 0.37 + phase).sin() * 2.0,
3367 (y * 0.21 - phase).cos() + 1.5,
3368 ((x + y) % 5.0) * 0.25 - 0.5,
3369 1.0,
3370 ]
3371 })
3372 .flat_map(|v| half::f16::from_f32(v).to_bits().to_le_bytes())
3373 .collect()
3374 }
3375
3376 fn source_texture(
3377 gpu: &pfx_gpu::Gpu,
3378 width: u32,
3379 height: u32,
3380 format: wgpu::TextureFormat,
3381 bytes: &[u8],
3382 ) -> wgpu::Texture {
3383 let texel = bytes.len() as u32 / (width * height);
3384 gpu.device.create_texture_with_data(
3385 &gpu.queue,
3386 &wgpu::TextureDescriptor {
3387 label: Some("content source"),
3388 size: wgpu::Extent3d {
3389 width,
3390 height,
3391 depth_or_array_layers: 1,
3392 },
3393 mip_level_count: 1,
3394 sample_count: 1,
3395 dimension: wgpu::TextureDimension::D2,
3396 format,
3397 usage: wgpu::TextureUsages::TEXTURE_BINDING
3398 | wgpu::TextureUsages::COPY_SRC
3399 | wgpu::TextureUsages::COPY_DST,
3400 view_formats: &[],
3401 },
3402 wgpu::util::TextureDataOrder::LayerMajor,
3403 &bytes[..(width * height * texel) as usize],
3404 )
3405 }
3406
3407 #[test]
3408 #[ignore = "needs a GPU; run under pgpu"]
3409 fn a_gpu_texture_fills_a_slot_like_bytes_with_its_mips() {
3410 let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3411 let mut slots = ContentSlots::new(&gpu.device, &gpu.queue);
3412 let (width, height) = (37u32, 22u32);
3413 let bytes = pattern(width, height, 0.0);
3414 for slot in 0..2 {
3415 slots
3416 .allocate(&gpu.device, slot, width, height, ContentFormat::Linear16)
3417 .unwrap();
3418 }
3419 slots
3420 .write(&gpu.device, &gpu.queue, 0, [0, 0, width, height], &bytes)
3421 .unwrap();
3422 let source = source_texture(
3423 &gpu,
3424 width,
3425 height,
3426 wgpu::TextureFormat::Rgba16Float,
3427 &bytes,
3428 );
3429 let mut encoder = gpu.device.create_command_encoder(&Default::default());
3430 let kind = slots.copy(&gpu.device, &mut encoder, 1, &source).unwrap();
3431 gpu.queue.submit(Some(encoder.finish()));
3432 assert_eq!(kind, ContentCopy::Copied);
3433 let (written, copied) = (slots.get(0).unwrap(), slots.get(1).unwrap());
3434 assert_eq!(copied.mips, 6);
3435 assert_eq!(copied.writes, 1);
3436 for level in 0..copied.mips {
3437 assert_eq!(
3438 read_level(&gpu, written, level),
3439 read_level(&gpu, copied, level),
3440 "level {level}"
3441 );
3442 }
3443
3444 let narrow: Vec<u8> = (0..width * height)
3445 .flat_map(|i| [(i * 7 % 256) as u8, (i * 3 % 256) as u8, 128, 255])
3446 .collect();
3447 let eight = source_texture(
3448 &gpu,
3449 width,
3450 height,
3451 wgpu::TextureFormat::Rgba8Unorm,
3452 &narrow,
3453 );
3454 let mut encoder = gpu.device.create_command_encoder(&Default::default());
3455 assert_eq!(
3456 slots.copy(&gpu.device, &mut encoder, 1, &eight).unwrap(),
3457 ContentCopy::Drawn
3458 );
3459 gpu.queue.submit(Some(encoder.finish()));
3460 let drawn = halves(&read_level(&gpu, slots.get(1).unwrap(), 0));
3461 for (got, want) in drawn.iter().zip(&narrow) {
3462 assert!(
3463 (got - f32::from(*want) / 255.0).abs() < 2e-3,
3464 "{got} {want}"
3465 );
3466 }
3467 let unusable = gpu.device.create_texture(&wgpu::TextureDescriptor {
3468 label: Some("content unusable"),
3469 size: wgpu::Extent3d {
3470 width,
3471 height,
3472 depth_or_array_layers: 1,
3473 },
3474 mip_level_count: 1,
3475 sample_count: 1,
3476 dimension: wgpu::TextureDimension::D2,
3477 format: wgpu::TextureFormat::Rgba16Float,
3478 usage: wgpu::TextureUsages::COPY_DST,
3479 view_formats: &[],
3480 });
3481 let mut encoder = gpu.device.create_command_encoder(&Default::default());
3482 assert!(slots.copy(&gpu.device, &mut encoder, 1, &unusable).is_err());
3483 assert!(slots.copy(&gpu.device, &mut encoder, 2, &source).is_err());
3484 }
3485
3486 #[test]
3487 #[ignore = "needs a GPU; run under pgpu"]
3488 fn a_copied_slot_regenerates_its_mips_every_frame() {
3489 let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3490 let mut slots = ContentSlots::new(&gpu.device, &gpu.queue);
3491 let (width, height) = (64u32, 32u32);
3492 slots
3493 .allocate(&gpu.device, 3, width, height, ContentFormat::Linear16)
3494 .unwrap();
3495 for (index, phase) in [0.0f32, 1.3, 2.9].into_iter().enumerate() {
3496 let bytes = pattern(width, height, phase);
3497 let source = source_texture(
3498 &gpu,
3499 width,
3500 height,
3501 wgpu::TextureFormat::Rgba16Float,
3502 &bytes,
3503 );
3504 let mut encoder = gpu.device.create_command_encoder(&Default::default());
3505 if index == 1 {
3506 encoder.copy_texture_to_texture(
3507 source.as_image_copy(),
3508 slots.get(3).unwrap().texture.as_image_copy(),
3509 source.size(),
3510 );
3511 slots.regenerate(&mut encoder, 3).unwrap();
3512 } else {
3513 slots.copy(&gpu.device, &mut encoder, 3, &source).unwrap();
3514 }
3515 gpu.queue.submit(Some(encoder.finish()));
3516 let content = slots.get(3).unwrap();
3517 let mut above = halves(&bytes);
3518 for level in 1..content.mips {
3519 let (w, h) = ((width >> level).max(1), (height >> level).max(1));
3520 let (pw, ph) = (
3521 (width >> (level - 1)).max(1),
3522 (height >> (level - 1)).max(1),
3523 );
3524 let got = halves(&read_level(&gpu, content, level));
3525 let mut want = vec![0.0; (w * h * 4) as usize];
3526 for y in 0..h {
3527 for x in 0..w {
3528 for c in 0..4 {
3529 let mut sum = 0.0;
3530 for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
3531 let sx = (2 * x + dx).min(pw - 1);
3532 let sy = (2 * y + dy).min(ph - 1);
3533 sum += above[((sy * pw + sx) * 4 + c) as usize];
3534 }
3535 want[((y * w + x) * 4 + c) as usize] = sum / 4.0;
3536 }
3537 }
3538 }
3539 for (index, (g, e)) in got.iter().zip(&want).enumerate() {
3540 assert!(
3541 (g - e).abs() < 4e-3 * (1.0 + e.abs()),
3542 "phase {phase} level {level} value {index}: {g} against {e}"
3543 );
3544 }
3545 above = got;
3546 }
3547 }
3548 assert_eq!(slots.get(3).unwrap().writes, 3);
3549 }
3550
3551 #[test]
3552 #[ignore = "needs a GPU; run under pgpu"]
3553 fn a_floor_sized_copy_with_mips_stays_cheap() {
3554 let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3555 let mut slots = ContentSlots::new(&gpu.device, &gpu.queue);
3556 let mut profiler = pfx_gpu::GpuProfiler::new(&gpu.device, &gpu.queue);
3557 if !profiler.marks_supported() {
3558 println!("no encoder timestamps on this adapter; nothing timed");
3559 return;
3560 }
3561 for (width, height) in [(1968u32, 1192u32), (984, 596)] {
3562 slots
3563 .allocate(&gpu.device, 0, width, height, ContentFormat::Linear16)
3564 .unwrap();
3565 let bytes = pattern(width, height, 0.5);
3566 let source = source_texture(
3567 &gpu,
3568 width,
3569 height,
3570 wgpu::TextureFormat::Rgba16Float,
3571 &bytes,
3572 );
3573 let mut times = Vec::new();
3574 let mut parts = [Vec::new(), Vec::new()];
3575 for _ in 0..24 {
3576 let mut encoder = gpu.device.create_command_encoder(&Default::default());
3577 profiler.mark(&mut encoder, "start");
3578 slots.copy(&gpu.device, &mut encoder, 0, &source).unwrap();
3579 profiler.mark(&mut encoder, "copy and mips");
3580 let content = slots.get(0).unwrap();
3581 encoder.copy_texture_to_texture(
3582 source.as_image_copy(),
3583 content.texture.as_image_copy(),
3584 source.size(),
3585 );
3586 profiler.mark(&mut encoder, "copy");
3587 encode_mips(&slots.mipper, &mut encoder, content);
3588 profiler.mark(&mut encoder, "mips");
3589 let slot = profiler.finish(&mut encoder);
3590 gpu.queue.submit(Some(encoder.finish()));
3591 if let Some(slot) = slot {
3592 profiler.submitted(slot);
3593 }
3594 gpu.device
3595 .poll(wgpu::PollType::wait_indefinitely())
3596 .unwrap();
3597 for frame in profiler.collect(&gpu.device) {
3598 for timing in frame {
3599 match timing.label.as_str() {
3600 "copy" => parts[0].push(timing.milliseconds),
3601 "mips" => parts[1].push(timing.milliseconds),
3602 _ => times.push(timing.milliseconds),
3603 }
3604 }
3605 }
3606 }
3607 times.sort_by(f64::total_cmp);
3608 assert!(!times.is_empty());
3609 for (name, part) in ["copy", "mips"].iter().zip(&mut parts) {
3610 part.sort_by(f64::total_cmp);
3611 println!(" {name} alone: median {:.4} ms", part[part.len() / 2]);
3612 }
3613 println!(
3614 "{width}x{height} Rgba16Float slot from a GPU texture with {} mips: median {:.4} ms, max {:.4} ms over {} copies",
3615 slots.get(0).unwrap().mips,
3616 times[times.len() / 2],
3617 times[times.len() - 1],
3618 times.len()
3619 );
3620 }
3621 }
3622
3623 #[test]
3624 fn mip_shaders_validate() {
3625 for source in [MIP_WGSL, MIP_COMPUTE_WGSL] {
3626 let module = naga::front::wgsl::parse_str(source).unwrap();
3627 naga::valid::Validator::new(
3628 naga::valid::ValidationFlags::all(),
3629 naga::valid::Capabilities::all(),
3630 )
3631 .validate(&module)
3632 .unwrap();
3633 }
3634 }
3635
3636 #[test]
3637 fn caustics_project_along_the_sun_onto_their_plane() {
3638 let projection = CausticProjection {
3639 rect: [-1.0, -2.0, 4.0, 2.0],
3640 height: 0.5,
3641 toward_sun: [0.0, 2.0, 0.0],
3642 };
3643 assert_eq!(projection.uv([1.0, 0.0, -1.0]), Some([0.5, 0.5]));
3644 assert_eq!(projection.uv([3.5, 0.0, -1.0]), None);
3645 let slanted = CausticProjection {
3646 toward_sun: [1.0, 1.0, 0.0],
3647 ..projection
3648 };
3649 assert_eq!(slanted.uv([0.5, 0.0, -1.0]), Some([0.5, 0.5]));
3650 assert_eq!(slanted.uv([0.0, -0.5, -1.0]), Some([0.5, 0.5]));
3651 let set = CausticProjection {
3652 toward_sun: [1.0, -0.2, 0.0],
3653 ..projection
3654 };
3655 assert_eq!(set.uv([1.0, 0.0, -1.0]), None);
3656 let rows = CausticProjection::rows(Some(slanted), 2, 0.75);
3657 assert_eq!(rows[0], [-1.0, -2.0, 4.0, 2.0]);
3658 assert!((rows[1][0] - std::f32::consts::FRAC_1_SQRT_2).abs() < 1e-6);
3659 assert_eq!(rows[1][3], 0.5);
3660 assert_eq!(rows[2], [2.0, 0.75, 1.0, 0.0]);
3661 assert_eq!(CausticProjection::rows(Some(set), 2, 0.75)[2][2], 0.0);
3662 assert_eq!(CausticProjection::rows(None, 2, 0.75)[2][2], 0.0);
3663 let caustic = Caustic {
3664 slot: 1,
3665 rect: [0.0, 0.0, 1.0, 1.0],
3666 height: 0.0,
3667 strength: 1.0,
3668 receivers: vec![0, 4],
3669 };
3670 assert!(caustic.check().is_ok());
3671 assert!(caustic.receives(4) && !caustic.receives(1));
3672 assert!(
3673 Caustic {
3674 slot: 4,
3675 ..caustic.clone()
3676 }
3677 .check()
3678 .is_err()
3679 );
3680 assert!(
3681 Caustic {
3682 rect: [0.0, 0.0, 0.0, 1.0],
3683 ..caustic.clone()
3684 }
3685 .check()
3686 .is_err()
3687 );
3688 assert!(
3689 Caustic {
3690 strength: f32::NAN,
3691 ..caustic
3692 }
3693 .check()
3694 .is_err()
3695 );
3696 }
3697
3698 #[test]
3699 fn the_bakes_detail_mips_match_the_live_mip_chain() {
3700 use pfx_bake::detail::atlas::{Kind as Map, chain};
3701 let mut state = 9u32;
3702 let bytes: Vec<u8> = (0..32 * 16 * 4)
3703 .map(|index| {
3704 state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
3705 if index % 4 == 3 && (index / 4) % 7 == 0 {
3706 0
3707 } else {
3708 (state >> 23) as u8
3709 }
3710 })
3711 .collect();
3712 for (map, kind, space) in [
3713 (Map::Base, Kind::Base, ColorSpace::Linear),
3714 (Map::Normal, Kind::Normal, ColorSpace::Linear),
3715 (Map::Roughness, Kind::Scalar, ColorSpace::Linear),
3716 ] {
3717 let image = Image {
3718 width: 32,
3719 height: 16,
3720 space,
3721 pixels: Pixels::Eight(bytes.clone()),
3722 };
3723 let live = mip_chain(&image, kind).unwrap();
3724 let baked = chain(map, 32, 16, &bytes, 5);
3725 assert_eq!(baked.len(), 5);
3726 for (level, (_, _, expected)) in live.iter().take(5).enumerate() {
3727 assert_eq!(&baked[level], expected, "{} level {level}", map.name());
3728 }
3729 }
3730 }
3731
3732 const DETAIL_TEST_WGSL: &str = r#"
3733struct DetailProbe {
3734 lo: vec2f,
3735 hi: vec2f,
3736 mode: vec4u,
3737}
3738@group(0) @binding(0) var<uniform> detail_probe: DetailProbe;
3739struct DetailOut {
3740 @builtin(position) clip: vec4f,
3741 @location(0) uv: vec2f,
3742}
3743@vertex fn detail_vertex(@builtin(vertex_index) index: u32) -> DetailOut {
3744 let corners = array<vec2f, 6>(
3745 vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0),
3746 vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0)
3747 );
3748 let corner = corners[index];
3749 return DetailOut(vec4f(corner * vec2f(2.0, -2.0) + vec2f(-1.0, 1.0), 0.0, 1.0), mix(detail_probe.lo, detail_probe.hi, corner));
3750}
3751@fragment fn detail_fragment(input: DetailOut) -> @location(0) vec4f {
3752 let r = map_evaluate(vec3f(1.0), 0.5, 0.0, 0.05, 0.0, vec3f(0.0, 0.0, 1.0), vec4f(1.0, 0.0, 0.0, 1.0), input.uv, vec3f(0.0), 0.0, 0.0, 0.0, 0u);
3753 if (detail_probe.mode.x == 1u) {
3754 return vec4f(r.normal * 0.5 + vec3f(0.5), 1.0);
3755 }
3756 return vec4f(r.base, r.roughness);
3757}
3758"#;
3759
3760 struct DetailRig {
3761 device: wgpu::Device,
3762 queue: wgpu::Queue,
3763 layout: wgpu::BindGroupLayout,
3764 maps: wgpu::BindGroupLayout,
3765 empty: wgpu::BindGroup,
3766 pipeline: wgpu::RenderPipeline,
3767 }
3768
3769 impl DetailRig {
3770 fn new(features: wgpu::Features) -> Option<Self> {
3771 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
3772 backends: wgpu::Backends::VULKAN | wgpu::Backends::METAL,
3773 ..wgpu::InstanceDescriptor::new_without_display_handle()
3774 });
3775 let adapter = pollster::block_on(
3776 instance.enumerate_adapters(wgpu::Backends::VULKAN | wgpu::Backends::METAL),
3777 )
3778 .into_iter()
3779 .find(|adapter| adapter.get_info().device_type == wgpu::DeviceType::DiscreteGpu)
3780 .or_else(|| {
3781 pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
3782 power_preference: wgpu::PowerPreference::HighPerformance,
3783 compatible_surface: None,
3784 force_fallback_adapter: false,
3785 apply_limit_buckets: false,
3786 }))
3787 .ok()
3788 })?;
3789 if !adapter.features().contains(features) {
3790 return None;
3791 }
3792 let (device, queue) =
3793 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
3794 label: Some("detail rig"),
3795 required_features: features,
3796 required_limits: wgpu::Limits::default(),
3797 ..Default::default()
3798 }))
3799 .ok()?;
3800 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3801 label: Some("detail probe"),
3802 entries: &[wgpu::BindGroupLayoutEntry {
3803 binding: 0,
3804 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
3805 ty: wgpu::BindingType::Buffer {
3806 ty: wgpu::BufferBindingType::Uniform,
3807 has_dynamic_offset: false,
3808 min_binding_size: None,
3809 },
3810 count: None,
3811 }],
3812 });
3813 let empty_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3814 label: Some("empty"),
3815 entries: &[],
3816 });
3817 let empty = device.create_bind_group(&wgpu::BindGroupDescriptor {
3818 label: Some("empty"),
3819 layout: &empty_layout,
3820 entries: &[],
3821 });
3822 let maps = MapTextures::layout(&device);
3823 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3824 label: Some("detail probe"),
3825 bind_group_layouts: &[
3826 Some(&layout),
3827 Some(&empty_layout),
3828 Some(&empty_layout),
3829 Some(&maps),
3830 ],
3831 immediate_size: 0,
3832 });
3833 let source = format!("{}\n{}", shader_source(), DETAIL_TEST_WGSL);
3834 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3835 label: Some("detail probe"),
3836 source: wgpu::ShaderSource::Wgsl(source.into()),
3837 });
3838 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3839 label: Some("detail probe"),
3840 layout: Some(&pipeline_layout),
3841 vertex: wgpu::VertexState {
3842 module: &module,
3843 entry_point: Some("detail_vertex"),
3844 buffers: &[],
3845 compilation_options: Default::default(),
3846 },
3847 primitive: wgpu::PrimitiveState {
3848 cull_mode: None,
3849 ..Default::default()
3850 },
3851 depth_stencil: None,
3852 multisample: Default::default(),
3853 fragment: Some(wgpu::FragmentState {
3854 module: &module,
3855 entry_point: Some("detail_fragment"),
3856 targets: &[Some(wgpu::ColorTargetState {
3857 format: wgpu::TextureFormat::Rgba32Float,
3858 blend: None,
3859 write_mask: wgpu::ColorWrites::ALL,
3860 })],
3861 compilation_options: Default::default(),
3862 }),
3863 multiview_mask: None,
3864 cache: None,
3865 });
3866 Some(Self {
3867 device,
3868 queue,
3869 layout,
3870 maps,
3871 empty,
3872 pipeline,
3873 })
3874 }
3875
3876 fn draw(
3877 &self,
3878 textures: &MapTextures,
3879 material: GpuMapMaterial,
3880 uv: [f32; 4],
3881 size: u32,
3882 mode: u32,
3883 ) -> Vec<f32> {
3884 let device = &self.device;
3885 let materials = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3886 label: Some("detail material"),
3887 contents: bytemuck::bytes_of(&material),
3888 usage: wgpu::BufferUsages::STORAGE,
3889 });
3890 let maps = textures.bind_group(device, &self.maps, &materials);
3891 let mut probe = [0u8; 32];
3892 probe[..16].copy_from_slice(bytemuck::cast_slice(&uv));
3893 probe[16..20].copy_from_slice(&mode.to_le_bytes());
3894 let uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3895 label: Some("detail probe"),
3896 contents: &probe,
3897 usage: wgpu::BufferUsages::UNIFORM,
3898 });
3899 let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3900 label: Some("detail probe"),
3901 layout: &self.layout,
3902 entries: &[wgpu::BindGroupEntry {
3903 binding: 0,
3904 resource: uniform.as_entire_binding(),
3905 }],
3906 });
3907 let target = device.create_texture(&wgpu::TextureDescriptor {
3908 label: Some("detail probe"),
3909 size: wgpu::Extent3d {
3910 width: size,
3911 height: size,
3912 depth_or_array_layers: 1,
3913 },
3914 mip_level_count: 1,
3915 sample_count: 1,
3916 dimension: wgpu::TextureDimension::D2,
3917 format: wgpu::TextureFormat::Rgba32Float,
3918 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3919 view_formats: &[],
3920 });
3921 let view = target.create_view(&Default::default());
3922 let row = (size * 16).div_ceil(256) * 256;
3923 let readback = device.create_buffer(&wgpu::BufferDescriptor {
3924 label: Some("detail probe readback"),
3925 size: u64::from(row * size),
3926 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3927 mapped_at_creation: false,
3928 });
3929 let mut encoder = device.create_command_encoder(&Default::default());
3930 {
3931 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3932 multiview_mask: None,
3933 label: Some("detail probe"),
3934 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3935 view: &view,
3936 resolve_target: None,
3937 depth_slice: None,
3938 ops: wgpu::Operations {
3939 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
3940 store: wgpu::StoreOp::Store,
3941 },
3942 })],
3943 depth_stencil_attachment: None,
3944 timestamp_writes: None,
3945 occlusion_query_set: None,
3946 });
3947 pass.set_pipeline(&self.pipeline);
3948 pass.set_bind_group(0, &group, &[]);
3949 pass.set_bind_group(1, &self.empty, &[]);
3950 pass.set_bind_group(2, &self.empty, &[]);
3951 pass.set_bind_group(3, &maps, &[]);
3952 pass.draw(0..6, 0..1);
3953 }
3954 encoder.copy_texture_to_buffer(
3955 target.as_image_copy(),
3956 wgpu::TexelCopyBufferInfo {
3957 buffer: &readback,
3958 layout: wgpu::TexelCopyBufferLayout {
3959 offset: 0,
3960 bytes_per_row: Some(row),
3961 rows_per_image: Some(size),
3962 },
3963 },
3964 wgpu::Extent3d {
3965 width: size,
3966 height: size,
3967 depth_or_array_layers: 1,
3968 },
3969 );
3970 self.queue.submit([encoder.finish()]);
3971 let slice = readback.slice(..);
3972 slice.map_async(wgpu::MapMode::Read, |_| {});
3973 device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
3974 let bytes = slice.get_mapped_range().expect("a mapped range");
3975 let mut out = Vec::with_capacity((size * size * 4) as usize);
3976 for y in 0..size {
3977 let start = (y * row) as usize;
3978 out.extend_from_slice(bytemuck::cast_slice::<u8, f32>(
3979 &bytes[start..start + (size * 16) as usize],
3980 ));
3981 }
3982 out
3983 }
3984 }
3985
3986 struct DetailCase {
3987 density: u32,
3988 lo: [f32; 2],
3989 hi: [f32; 2],
3990 material: Material,
3991 }
3992
3993 fn detail_cases() -> Vec<DetailCase> {
3994 let noisy = |layers: [NoiseLayer; 2], seed: u32| {
3995 let mut material = Material {
3996 base: [0.6, 0.45, 0.3],
3997 ..Material::default()
3998 };
3999 for (slot, layer) in layers.into_iter().enumerate() {
4000 material.layers[slot] = NoiseLayer {
4001 seed: seed + slot as u32,
4002 ..layer
4003 };
4004 }
4005 material
4006 };
4007 vec![
4008 DetailCase {
4009 density: 512,
4010 lo: [0.10, 0.20],
4011 hi: [0.35, 0.40],
4012 material: noisy(
4013 [
4014 PlankWood::SAMPLE.layer(1.0, 0),
4015 pfx_materials::Crinkle::SAMPLE.layer(0.6, 0),
4016 ],
4017 3,
4018 ),
4019 },
4020 DetailCase {
4021 density: 256,
4022 lo: [0.55, 0.05],
4023 hi: [0.90, 0.30],
4024 material: noisy(
4025 [
4026 NoiseLayer::new(NoiseKind::Value, 50.0, 0.8, 0),
4027 Scratch::SAMPLE.layer(0.8, 0),
4028 ],
4029 17,
4030 ),
4031 },
4032 DetailCase {
4033 density: 512,
4034 lo: [0.60, 0.60],
4035 hi: [0.75, 0.95],
4036 material: noisy(
4037 [
4038 Grime::SAMPLE.layer(1.0, 0),
4039 CoatWobble::SAMPLE.layer(1.0, 0),
4040 ],
4041 29,
4042 ),
4043 },
4044 ]
4045 }
4046
4047 fn detail_input<'a>(
4048 case: &'a DetailCase,
4049 buffers: &'a DetailBuffers,
4050 ) -> pfx_bake::detail::Input<'a> {
4051 pfx_bake::detail::Input {
4052 positions: &buffers.positions,
4053 normals: &buffers.normals,
4054 tangents: &buffers.tangents,
4055 uvs: &buffers.uvs,
4056 uvs1: None,
4057 indices: &[0, 1, 2, 2, 1, 3],
4058 material: &case.material,
4059 }
4060 }
4061
4062 struct DetailBuffers {
4063 positions: [[f32; 3]; 4],
4064 normals: [[f32; 3]; 4],
4065 tangents: [[f32; 4]; 4],
4066 uvs: [[f32; 2]; 4],
4067 }
4068
4069 fn detail_buffers(case: &DetailCase) -> DetailBuffers {
4070 let uvs = [
4071 [case.lo[0], case.lo[1]],
4072 [case.hi[0], case.lo[1]],
4073 [case.lo[0], case.hi[1]],
4074 [case.hi[0], case.hi[1]],
4075 ];
4076 DetailBuffers {
4077 positions: uvs.map(|uv| [uv[0], uv[1], 0.0]),
4078 normals: [[0.0, 0.0, 1.0]; 4],
4079 tangents: [[1.0, 0.0, 0.0, 1.0]; 4],
4080 uvs,
4081 }
4082 }
4083
4084 fn worst(a: &[f32], b: &[f32]) -> f32 {
4085 a.iter()
4086 .zip(b)
4087 .map(|(a, b)| (a - b).abs())
4088 .fold(0.0, f32::max)
4089 }
4090
4091 fn psnr(a: &[f32], b: &[f32]) -> f64 {
4092 let mean = a
4093 .iter()
4094 .zip(b)
4095 .map(|(a, b)| f64::from((a - b) * 255.0).powi(2))
4096 .sum::<f64>()
4097 / a.len() as f64;
4098 if mean == 0.0 {
4099 f64::INFINITY
4100 } else {
4101 10.0 * (255.0f64 * 255.0 / mean).log10()
4102 }
4103 }
4104
4105 #[test]
4106 #[ignore = "needs a GPU; run under pgpu"]
4107 fn cropped_compressed_detail_samples_like_the_full_tile() {
4108 use pfx_bake::detail::{self, atlas};
4109 let started = std::time::Instant::now();
4110 let cases = detail_cases();
4111 let buffers: Vec<DetailBuffers> = cases.iter().map(detail_buffers).collect();
4112 let mut pieces = Vec::new();
4113 let mut tiles = Vec::new();
4114 for (index, (case, buffer)) in cases.iter().zip(&buffers).enumerate() {
4115 let input = detail_input(case, buffer);
4116 let crop = detail::crop(&input, case.density, 8).unwrap();
4117 pieces.push(atlas::Piece {
4118 index: index as u32,
4119 node: format!("part{index}"),
4120 input_hash: detail::input_hash(&input, case.density, 8).unwrap(),
4121 region: detail::bake_crop(&input, case.density, 8, crop).unwrap(),
4122 });
4123 tiles.push(detail::bake(&input, case.density, 8).unwrap());
4124 }
4125 let plain = atlas::assemble(pieces, 512).unwrap();
4126 let packed = plain.compress().unwrap();
4127 let blocks = DetailRig::new(wgpu::Features::TEXTURE_COMPRESSION_BC)
4128 .expect("the test GPU decodes BC textures");
4129 let fallback = DetailRig::new(wgpu::Features::empty()).unwrap();
4130 assert!(!uploads_blocks(&fallback.device));
4131 let plain_maps = MapTextures::detail(&blocks.device, &blocks.queue, &plain).unwrap();
4132 let packed_maps = MapTextures::detail(&blocks.device, &blocks.queue, &packed).unwrap();
4133 let decoded_maps = MapTextures::detail(&fallback.device, &fallback.queue, &packed).unwrap();
4134 assert_eq!(
4135 packed_maps.base.texture.format(),
4136 wgpu::TextureFormat::Bc7RgbaUnormSrgb
4137 );
4138 assert_eq!(
4139 packed_maps.normal.texture.format(),
4140 wgpu::TextureFormat::Bc5RgUnorm
4141 );
4142 assert_eq!(
4143 packed_maps.roughness.texture.format(),
4144 wgpu::TextureFormat::Bc4RUnorm
4145 );
4146 assert_eq!(
4147 decoded_maps.base.texture.format(),
4148 wgpu::TextureFormat::Rgba8UnormSrgb
4149 );
4150 assert!(packed_maps.two_channel_normals && decoded_maps.two_channel_normals);
4151 assert!(!plain_maps.two_channel_normals);
4152 for (index, (case, tile)) in cases.iter().zip(tiles).enumerate() {
4153 let images: Vec<Image> = [tile.base, tile.normal, tile.roughness]
4154 .into_iter()
4155 .map(|bytes| Image {
4156 width: tile.size,
4157 height: tile.size,
4158 space: ColorSpace::Linear,
4159 pixels: Pixels::Eight(bytes),
4160 })
4161 .collect();
4162 let full = MapTextures::new(
4163 &blocks.device,
4164 &blocks.queue,
4165 &MapImages {
4166 base: &images[0..1],
4167 normal: &images[1..2],
4168 roughness: &images[2..3],
4169 metal: &[],
4170 },
4171 )
4172 .unwrap();
4173 let mut material = case.material;
4174 select_baked_detail(&mut material, 0).unwrap();
4175 let tile_material =
4176 full.material(&material, MapIndices::from_material(&material).unwrap());
4177 assert_eq!(tile_material.seeds[3], 1);
4178 select_baked_detail(&mut material, index as u32).unwrap();
4179 let indices = MapIndices::from_material(&material).unwrap();
4180 let atlas_material = plain_maps.material(&material, indices);
4181 assert_eq!(atlas_material.seeds[3], 2);
4182 assert_eq!(packed_maps.material(&material, indices).seeds[3], 3);
4183 let uv = [case.lo[0], case.lo[1], case.hi[0], case.hi[1]];
4184 let texels = ((case.hi[0] - case.lo[0]) * case.density as f32) as u32;
4185 for size in [texels, texels / 4, texels.div_ceil(14)] {
4186 for mode in [0, 1] {
4187 let reference = blocks.draw(&full, tile_material, uv, size, mode);
4188 let cropped = blocks.draw(&plain_maps, atlas_material, uv, size, mode);
4189 let compressed = blocks.draw(
4190 &packed_maps,
4191 packed_maps.material(&material, indices),
4192 uv,
4193 size,
4194 mode,
4195 );
4196 let decoded = fallback.draw(
4197 &decoded_maps,
4198 decoded_maps.material(&material, indices),
4199 uv,
4200 size,
4201 mode,
4202 );
4203 let crop_worst = worst(&reference, &cropped);
4204 let block_worst = worst(&reference, &compressed);
4205 let fallback_worst = worst(&compressed, &decoded);
4206 println!(
4207 "part {index} {}² at {size} px, {}: cropped {:.2}/255, compressed {:.2}/255 (PSNR {:.1} dB), CPU-decoded against BC {:.2}/255",
4208 case.density,
4209 ["base and roughness", "normal"][mode as usize],
4210 crop_worst * 255.0,
4211 block_worst * 255.0,
4212 psnr(&reference, &compressed),
4213 fallback_worst * 255.0
4214 );
4215 assert!(
4216 crop_worst <= 1.0 / 255.0,
4217 "cropped differs by {}",
4218 crop_worst * 255.0
4219 );
4220 assert!(
4221 fallback_worst <= 2.0 / 255.0,
4222 "decoded differs by {}",
4223 fallback_worst * 255.0
4224 );
4225 assert!(psnr(&reference, &compressed) > 30.0);
4226 }
4227 }
4228 }
4229 println!("detail atlas GPU check {:?}", started.elapsed());
4230 }
4231}