Skip to main content

pfx_live/
maps.rs

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 = &params[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(&region_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(&region.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}