Skip to main content

pfx_live/
text.rs

1use crate::canopy::Gobo;
2use crate::deform::DeformerId;
3use bytemuck::{Pod, Zeroable};
4use pfx_gpu::wgpu;
5use pfx_gpu::wgpu::util::DeviceExt;
6use pfx_load::{Asset, Cache, ColorSpace, Handle, Pixels};
7use pfx_text::{
8    Atlas, AtlasChanges, CellUpload, GlyphInstance, MSDF_SPREAD, MSDF_WGSL, PageInfo, PageKind,
9    PageQuad, Paragraph, Representation,
10};
11use std::cell::OnceCell;
12use std::collections::{BTreeMap, BTreeSet};
13
14pub const COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
15pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
16pub const ID_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
17
18const GLYPH_WGSL: &str = r#"
19struct Transform {
20    matrix: mat4x4f,
21    view_projection: mat4x4f,
22    model: mat4x4f,
23    rest_normal: vec4f,
24    atlas_size: vec2f,
25    mode: u32,
26    deformer: u32,
27    cells: vec4u,
28}
29struct Instance {
30    @location(0) pose: vec4f,
31    @location(1) rect: vec4f,
32    @location(2) uv_rect: vec4f,
33    @location(3) color: vec4f,
34    @location(4) clip_rect: vec4f,
35    @location(5) turn: vec4f,
36}
37struct Placed {
38    point: vec2f,
39    turned: vec2f,
40}
41const ID_CUTOUT: f32 = 0.5;
42const MSDF_ANISOTROPY_FROM: f32 = 1.2;
43const MSDF_ANISOTROPY_FULL: f32 = 1.5;
44const MSDF_MOST_TAPS: u32 = 8u;
45
46fn msdf_texel_distance(atlas_texture: texture_2d<f32>, atlas_filter: sampler, uv: vec2f) -> f32 {
47    return (msdf_median(textureSampleLevel(atlas_texture, atlas_filter, uv, 0.0).rgb) - 0.5) * MSDF_SPREAD;
48}
49
50fn msdf_major_axis(a: f32, b: f32, c: f32, major: f32) -> vec2f {
51    let first = vec2f(b, major - a);
52    let second = vec2f(major - c, b);
53    let longer = select(second, first, dot(first, first) >= dot(second, second));
54    if (dot(longer, longer) <= 1e-12) {
55        return vec2f(1.0, 0.0);
56    }
57    return normalize(longer);
58}
59
60fn msdf_anisotropic(
61    atlas_texture: texture_2d<f32>,
62    atlas_filter: sampler,
63    atlas_size: vec2f,
64    uv: vec2f,
65    low: vec2f,
66    high: vec2f,
67    jx: vec2f,
68    jy: vec2f,
69    axis: vec2f,
70    taps: u32,
71) -> f32 {
72    let along = jx * axis.x + jy * axis.y;
73    let across = jx * -axis.y + jy * axis.x;
74    let texel = vec2f(1.0) / atlas_size;
75    let count = f32(taps);
76    var total = 0.0;
77    for (var tap = 0u; tap < taps; tap++) {
78        let offset = (f32(tap) + 0.5) / count - 0.5;
79        let at = clamp(uv + along * offset * texel, low, high);
80        let distance = msdf_texel_distance(atlas_texture, atlas_filter, at);
81        let gradient = vec2f(
82            msdf_texel_distance(atlas_texture, atlas_filter, at + vec2f(texel.x, 0.0)) - distance,
83            msdf_texel_distance(atlas_texture, atlas_filter, at + vec2f(0.0, texel.y)) - distance,
84        );
85        let width = abs(dot(gradient, along)) / count + abs(dot(gradient, across));
86        total += clamp(0.5 + distance / max(width, 1e-4), 0.0, 1.0);
87    }
88    return total / count;
89}
90
91fn glyph_place(instance: Instance, corner: vec2f) -> Placed {
92    let local = (instance.rect.xy + corner * instance.rect.zw) * instance.pose.w;
93    let c = cos(instance.pose.z);
94    let s = sin(instance.pose.z);
95    let point = instance.pose.xy + vec2f(c * local.x - s * local.y, s * local.x + c * local.y);
96    let pivot = instance.turn.xy;
97    let ct = cos(instance.turn.z);
98    let st = sin(instance.turn.z);
99    let offset = point - pivot;
100    let turned = pivot + vec2f(ct * offset.x - st * offset.y, st * offset.x + ct * offset.y);
101    return Placed(point, turned);
102}
103
104fn glyph_corner(index: u32) -> vec2f {
105    var corners = array<vec2f, 6>(
106        vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0),
107        vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0)
108    );
109    return corners[index];
110}
111
112fn glyph_paint(
113    atlas_texture: texture_2d<f32>,
114    atlas_filter: sampler,
115    atlas_size: vec2f,
116    mode: u32,
117    uv_in: vec2f,
118    bounds: vec4f,
119    color: vec4f,
120    clip_rect: vec4f,
121    point: vec2f,
122    anisotropic: bool,
123) -> vec4f {
124    let dx = dpdx(uv_in);
125    let dy = dpdy(uv_in);
126    let footprint = max(length(dx * atlas_size), length(dy * atlas_size));
127    let extent = (bounds.zw - bounds.xy) * atlas_size;
128    let safe_lod = floor(log2(max(min(extent.x, extent.y), 1.0)));
129    let max_lod = select(min(safe_lod, 3.0), 3.0, mode == 2u);
130    let lod = clamp(log2(max(footprint, 1.0)), 0.0, max_lod);
131    let level = select(lod, 0.0, mode == 1u);
132    let half_texel = vec2f(exp2(level) * 0.5) / atlas_size;
133    let low = min(bounds.xy + half_texel, bounds.zw);
134    let high = max(bounds.zw - half_texel, low);
135    let uv = clamp(uv_in, low, high);
136    let sampled = textureSampleLevel(atlas_texture, atlas_filter, uv, level);
137    var alpha = sampled.r;
138    if (mode == 1u) {
139        let sampled = textureSampleLevel(atlas_texture, atlas_filter, uv, 0.0);
140        let signed_distance = msdf_median(sampled.rgb) - 0.5;
141        alpha = clamp(0.5 + signed_distance / max(fwidth(signed_distance), 1e-5), 0.0, 1.0);
142        let jx = dx * atlas_size;
143        let jy = dy * atlas_size;
144        let a = dot(jx, jx);
145        let b = dot(jx, jy);
146        let c = dot(jy, jy);
147        let area = abs(jx.x * jy.y - jx.y * jy.x);
148        let major_squared = 0.5 * (a + c) + sqrt(0.25 * (a - c) * (a - c) + b * b);
149        if (anisotropic && major_squared > MSDF_ANISOTROPY_FROM * area) {
150            let major = sqrt(major_squared);
151            let minor = area / max(major, 1e-6);
152            let blend = smoothstep(MSDF_ANISOTROPY_FROM, MSDF_ANISOTROPY_FULL, major / max(minor, 1e-6));
153            let taps = u32(clamp(ceil(major / max(minor, 1.0)), 1.0, f32(MSDF_MOST_TAPS)));
154            let axis = msdf_major_axis(a, b, c, major_squared);
155            let filtered = msdf_anisotropic(atlas_texture, atlas_filter, atlas_size, uv_in, low, high, jx, jy, axis, taps);
156            alpha = mix(alpha, filtered, blend);
157        }
158    }
159    let inside = step(clip_rect.x, point.x) * step(point.x, clip_rect.z)
160        * step(clip_rect.y, point.y) * step(point.y, clip_rect.w);
161    if (mode == 2u) {
162        let opacity = sampled.a * color.a * inside;
163        return vec4f(sampled.rgb * color.rgb * color.a * inside, opacity);
164    }
165    let opacity = alpha * color.a * inside;
166    return vec4f(color.rgb * opacity, opacity);
167}
168"#;
169
170const SHADER: &str = r#"
171@group(0) @binding(0) var<uniform> transform: Transform;
172@group(0) @binding(1) var atlas: texture_2d<f32>;
173@group(0) @binding(2) var atlas_sampler: sampler;
174
175struct VertexOut {
176    @builtin(position) clip: vec4f,
177    @location(0) uv: vec2f,
178    @location(1) @interpolate(flat) bounds: vec4f,
179    @location(2) @interpolate(flat) color: vec4f,
180    @location(3) @interpolate(flat) clip_rect: vec4f,
181    @location(4) point: vec2f,
182}
183
184@vertex
185fn vs_main(instance: Instance, @builtin(vertex_index) index: u32) -> VertexOut {
186    let corner = glyph_corner(index);
187    let placed = glyph_place(instance, corner);
188    var out: VertexOut;
189    out.clip = transform.matrix * vec4f(placed.turned, 0.0, 1.0);
190    out.uv = mix(instance.uv_rect.xy, instance.uv_rect.zw, corner);
191    out.bounds = instance.uv_rect;
192    out.color = instance.color;
193    out.clip_rect = instance.clip_rect;
194    out.point = placed.point;
195    return out;
196}
197
198@vertex
199fn vs_deformed(instance: Instance, @builtin(vertex_index) index: u32) -> VertexOut {
200    let cells = max(transform.cells.x, 1u);
201    let cell = index / 6u;
202    let corner = (vec2f(f32(cell % cells), f32(cell / cells)) + glyph_corner(index % 6u)) / f32(cells);
203    let placed = glyph_place(instance, corner);
204    let rest = (transform.matrix * vec4f(placed.turned, 0.0, 1.0)).xyz;
205    let shape = deform_vertex(rest, transform.rest_normal.xyz, vec4f(1.0, 0.0, 0.0, 1.0), transform.deformer);
206    let lifted = shape.position + normalize(shape.normal) * transform.rest_normal.w;
207    var out: VertexOut;
208    out.clip = transform.view_projection * transform.model * vec4f(lifted, 1.0);
209    out.uv = mix(instance.uv_rect.xy, instance.uv_rect.zw, corner);
210    out.bounds = instance.uv_rect;
211    out.color = instance.color;
212    out.clip_rect = instance.clip_rect;
213    out.point = placed.point;
214    return out;
215}
216
217@fragment
218fn fs_main(in: VertexOut) -> @location(0) vec4f {
219    return glyph_paint(atlas, atlas_sampler, transform.atlas_size, transform.mode, in.uv, in.bounds, in.color, in.clip_rect, in.point, true);
220}
221
222@fragment
223fn fs_overlay(in: VertexOut) -> @location(0) vec4f {
224    return glyph_paint(atlas, atlas_sampler, transform.atlas_size, transform.mode, in.uv, in.bounds, in.color, in.clip_rect, in.point, false);
225}
226
227fn glyph_id(in: VertexOut, anisotropic: bool) -> u32 {
228    let coverage = glyph_paint(atlas, atlas_sampler, transform.atlas_size, transform.mode, in.uv, in.bounds, vec4f(1.0), in.clip_rect, in.point, anisotropic).a;
229    if (coverage < ID_CUTOUT || in.color.a <= 0.0) {
230        discard;
231    }
232    return transform.cells.y;
233}
234
235@fragment
236fn fs_id(in: VertexOut) -> @location(0) u32 {
237    return glyph_id(in, true);
238}
239
240@fragment
241fn fs_overlay_id(in: VertexOut) -> @location(0) u32 {
242    return glyph_id(in, false);
243}
244"#;
245
246const LIT_SHADER: &str = r#"
247@group(2) @binding(2) var<uniform> transform: Transform;
248@group(2) @binding(3) var atlas: texture_2d<f32>;
249@group(2) @binding(4) var atlas_sampler: sampler;
250
251struct LitOut {
252    @builtin(position) clip: vec4f,
253    @location(0) uv: vec2f,
254    @location(1) @interpolate(flat) bounds: vec4f,
255    @location(2) @interpolate(flat) color: vec4f,
256    @location(3) @interpolate(flat) clip_rect: vec4f,
257    @location(4) point: vec2f,
258    @location(5) world: vec3f,
259    @location(6) normal: vec3f,
260}
261
262fn world_normal(model: mat4x4f, normal: vec3f) -> vec3f {
263    let a = model[0].xyz;
264    let b = model[1].xyz;
265    let c = model[2].xyz;
266    return normalize(cross(b, c) * normal.x + cross(c, a) * normal.y + cross(a, b) * normal.z);
267}
268
269@vertex
270fn vs_lit_surface(instance: Instance, @builtin(vertex_index) index: u32) -> LitOut {
271    let corner = glyph_corner(index);
272    let placed = glyph_place(instance, corner);
273    var out: LitOut;
274    out.clip = transform.matrix * vec4f(placed.turned, 0.0, 1.0);
275    out.uv = mix(instance.uv_rect.xy, instance.uv_rect.zw, corner);
276    out.bounds = instance.uv_rect;
277    out.color = instance.color;
278    out.clip_rect = instance.clip_rect;
279    out.point = placed.point;
280    out.world = (transform.model * vec4f(placed.turned, 0.0, 1.0)).xyz;
281    out.normal = world_normal(transform.model, vec3f(0.0, 0.0, 1.0));
282    return out;
283}
284
285@vertex
286fn vs_lit_deformed(instance: Instance, @builtin(vertex_index) index: u32) -> LitOut {
287    let cells = max(transform.cells.x, 1u);
288    let cell = index / 6u;
289    let corner = (vec2f(f32(cell % cells), f32(cell / cells)) + glyph_corner(index % 6u)) / f32(cells);
290    let placed = glyph_place(instance, corner);
291    let rest = (transform.matrix * vec4f(placed.turned, 0.0, 1.0)).xyz;
292    let shape = deform_vertex(rest, transform.rest_normal.xyz, vec4f(1.0, 0.0, 0.0, 1.0), transform.deformer);
293    let lifted = shape.position + normalize(shape.normal) * transform.rest_normal.w;
294    var out: LitOut;
295    out.clip = transform.view_projection * transform.model * vec4f(lifted, 1.0);
296    out.uv = mix(instance.uv_rect.xy, instance.uv_rect.zw, corner);
297    out.bounds = instance.uv_rect;
298    out.color = instance.color;
299    out.clip_rect = instance.clip_rect;
300    out.point = placed.point;
301    out.world = (transform.model * vec4f(lifted, 1.0)).xyz;
302    out.normal = world_normal(transform.model, normalize(shape.normal));
303    return out;
304}
305
306fn ink_local_light(world: vec3f, n: vec3f) -> vec3f {
307    var total = vec3f(0.0);
308    let count = min(local_lights.info.x, 4u);
309    for (var index = 0u; index < count; index++) {
310        let light = local_lights.lights[index];
311        let to_light = light.position_radius.xyz - world;
312        let distance = length(to_light);
313        let range = light.colour_range.w;
314        if (distance >= range || distance <= 1e-6) {
315            continue;
316        }
317        let l = to_light / distance;
318        let ndl = dot(n, l);
319        if (ndl <= 0.0) {
320            continue;
321        }
322        let ratio = distance / range;
323        let window = clamp(1.0 - ratio * ratio * ratio * ratio, 0.0, 1.0);
324        let reach = max(distance, light.position_radius.w);
325        let radiance = light.colour_range.xyz * window * window / (reach * reach);
326        total += radiance * ndl * light_shadow(light, world, n, l) * light_tint(light, world, n, l);
327    }
328    return total;
329}
330
331fn ink_irradiance(world: vec3f, facing: vec3f) -> vec3f {
332    let view = normalize(frame.camera_position.xyz - world);
333    var n = normalize(facing);
334    if (dot(n, view) < 0.0) {
335        n = -n;
336    }
337    let light = normalize(frame.sun_direction.xyz);
338    var sun = frame.sun_colour_intensity.xyz * frame.sun_colour_intensity.w * max(dot(n, light), 0.0) * shadow_visibility(world, n);
339    if (any(sun > vec3f(0.0))) {
340        sun *= shadow_tint(world, n, 0xfffffffeu);
341    }
342    if (frame.flags.z != 0u) {
343        sun *= canopy_light(world);
344    }
345    return sun + ambient_irradiance(world, n) + ink_local_light(world, n);
346}
347
348@fragment
349fn fs_lit(in: LitOut) -> @location(0) vec4f {
350    let paint = glyph_paint(atlas, atlas_sampler, transform.atlas_size, transform.mode, in.uv, in.bounds, in.color, in.clip_rect, in.point, true);
351    if (paint.a <= 0.0) {
352        return paint;
353    }
354    return vec4f(paint.rgb * ink_irradiance(in.world, in.normal) / PI, paint.a);
355}
356
357@fragment
358fn fs_lit_id(in: LitOut) -> @location(0) u32 {
359    let coverage = glyph_paint(atlas, atlas_sampler, transform.atlas_size, transform.mode, in.uv, in.bounds, vec4f(1.0), in.clip_rect, in.point, true).a;
360    if (coverage < ID_CUTOUT || in.color.a <= 0.0) {
361        discard;
362    }
363    return transform.cells.y;
364}
365"#;
366
367#[repr(C)]
368#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
369pub struct PackedGlyph {
370    pub pose: [f32; 4],
371    pub rect: [f32; 4],
372    pub uv_rect: [f32; 4],
373    pub color: [f32; 4],
374    pub clip: [f32; 4],
375    pub turn: [f32; 4],
376}
377
378impl From<&GlyphInstance> for PackedGlyph {
379    fn from(glyph: &GlyphInstance) -> Self {
380        Self {
381            pose: [
382                glyph.position[0],
383                glyph.position[1],
384                glyph.rotation,
385                glyph.scale,
386            ],
387            rect: glyph.local_rect,
388            uv_rect: glyph.uv,
389            color: [1.0; 4],
390            clip: [-f32::MAX, -f32::MAX, f32::MAX, f32::MAX],
391            turn: [0.0; 4],
392        }
393    }
394}
395
396pub fn pack_glyphs(glyphs: &[GlyphInstance]) -> Vec<PackedGlyph> {
397    glyphs.iter().map(PackedGlyph::from).collect()
398}
399
400#[derive(Clone, Copy, Debug, PartialEq)]
401pub struct RichQuad {
402    pub rect: [f32; 4],
403    pub uv: [f32; 4],
404    pub origin: [f32; 2],
405    pub alpha: f32,
406    pub clip: [f32; 4],
407    pub turn: [f32; 3],
408    pub color: [f32; 4],
409}
410
411impl From<&pfx_text::RichGlyphQuad> for RichQuad {
412    fn from(quad: &pfx_text::RichGlyphQuad) -> Self {
413        Self {
414            rect: quad.rect,
415            uv: quad.uv,
416            origin: quad.origin,
417            alpha: 1.0,
418            clip: quad.clip,
419            turn: quad.turn,
420            color: quad.color,
421        }
422    }
423}
424
425pub fn rich_quads(paragraph: &pfx_text::RichParagraph) -> Vec<RichQuad> {
426    paragraph.quads.iter().map(RichQuad::from).collect()
427}
428
429impl From<&RichQuad> for PackedGlyph {
430    fn from(quad: &RichQuad) -> Self {
431        Self {
432            pose: [0.0, 0.0, 0.0, 1.0],
433            rect: quad.rect,
434            uv_rect: quad.uv,
435            color: [
436                quad.color[0],
437                quad.color[1],
438                quad.color[2],
439                quad.color[3] * quad.alpha,
440            ],
441            clip: quad.clip,
442            turn: [quad.turn[0], quad.turn[1], quad.turn[2], 0.0],
443        }
444    }
445}
446
447pub fn pack_quads(quads: &[RichQuad]) -> Vec<PackedGlyph> {
448    quads.iter().map(PackedGlyph::from).collect()
449}
450
451#[derive(Clone, Copy, Debug, PartialEq, Eq)]
452pub struct UploadRegion {
453    pub level: u32,
454    pub first_row: u32,
455    pub rows: u32,
456    pub first_column: u32,
457    pub columns: u32,
458}
459
460#[derive(Debug, PartialEq, Eq)]
461pub enum AtlasError {
462    Empty,
463    InvalidChannels,
464    InvalidLevel,
465    TooLarge,
466}
467
468fn format_for(atlas: &Atlas) -> Result<wgpu::TextureFormat, AtlasError> {
469    match atlas.channels {
470        1 => Ok(wgpu::TextureFormat::R8Unorm),
471        3 => Ok(wgpu::TextureFormat::Rgba8Unorm),
472        _ => Err(AtlasError::InvalidChannels),
473    }
474}
475
476fn converted_levels(atlas: &Atlas) -> Result<Vec<Vec<u8>>, AtlasError> {
477    let bytes_per_pixel = match atlas.channels {
478        1 => 1usize,
479        3 => 4usize,
480        _ => return Err(AtlasError::InvalidChannels),
481    };
482    let mut converted = Vec::with_capacity(atlas.levels.len().min(4));
483    for (index, level) in atlas.levels.iter().take(4).enumerate() {
484        let expected_width = if index == 0 {
485            level.width
486        } else {
487            (atlas.levels[index - 1].width / 2).max(1)
488        };
489        let expected_height = if index == 0 {
490            level.height
491        } else {
492            (atlas.levels[index - 1].height / 2).max(1)
493        };
494        if index > 0 && atlas.levels[index - 1].width == 1 && atlas.levels[index - 1].height == 1 {
495            return Err(AtlasError::InvalidLevel);
496        }
497        let pixels = level
498            .width
499            .checked_mul(level.height)
500            .ok_or(AtlasError::TooLarge)?;
501        let count = usize::try_from(pixels).map_err(|_| AtlasError::TooLarge)?;
502        let source_bytes = count
503            .checked_mul(atlas.channels as usize)
504            .ok_or(AtlasError::TooLarge)?;
505        let target_bytes = count
506            .checked_mul(bytes_per_pixel)
507            .ok_or(AtlasError::TooLarge)?;
508        if level.width == 0
509            || level.height == 0
510            || level.width != expected_width
511            || level.height != expected_height
512            || level.bytes.len() != source_bytes
513        {
514            return Err(AtlasError::InvalidLevel);
515        }
516        let mut bytes = Vec::with_capacity(target_bytes);
517        if atlas.channels == 1 {
518            bytes.extend_from_slice(&level.bytes);
519        } else {
520            for rgb in level.bytes.chunks_exact(3) {
521                bytes.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
522            }
523        }
524        converted.push(bytes);
525    }
526    if converted.is_empty() {
527        return Err(AtlasError::Empty);
528    }
529    Ok(converted)
530}
531
532fn changed_regions(
533    atlas: &Atlas,
534    current: &[Vec<u8>],
535    previous: Option<&[Vec<u8>]>,
536) -> Vec<UploadRegion> {
537    let pixel_bytes = if atlas.channels == 1 { 1 } else { 4 };
538    let mut regions = Vec::new();
539    for (level_index, level) in atlas.levels.iter().take(current.len()).enumerate() {
540        let row_bytes = level.width as usize * pixel_bytes;
541        let old = previous.and_then(|levels| levels.get(level_index));
542        let mut first = None;
543        for row in 0..level.height {
544            let start = row as usize * row_bytes;
545            let end = start + row_bytes;
546            let changed =
547                old.is_none_or(|bytes| bytes[start..end] != current[level_index][start..end]);
548            if changed {
549                first.get_or_insert(row);
550            } else if let Some(begin) = first.take() {
551                regions.push(UploadRegion {
552                    level: level_index as u32,
553                    first_row: begin,
554                    rows: row - begin,
555                    first_column: 0,
556                    columns: level.width,
557                });
558            }
559        }
560        if let Some(begin) = first {
561            regions.push(UploadRegion {
562                level: level_index as u32,
563                first_row: begin,
564                rows: level.height - begin,
565                first_column: 0,
566                columns: level.width,
567            });
568        }
569    }
570    regions
571}
572
573pub struct GpuAtlas {
574    texture: wgpu::Texture,
575    view: wgpu::TextureView,
576    format: wgpu::TextureFormat,
577    width: u32,
578    height: u32,
579    levels: Vec<Vec<u8>>,
580    dimensions: Vec<(u32, u32)>,
581}
582
583impl GpuAtlas {
584    pub fn icons(device: &wgpu::Device, queue: &wgpu::Queue, atlas: &IconAtlas) -> Self {
585        let first = &atlas.levels[0];
586        let texture = device.create_texture(&wgpu::TextureDescriptor {
587            label: Some("icon atlas"),
588            size: wgpu::Extent3d {
589                width: first.width,
590                height: first.height,
591                depth_or_array_layers: 1,
592            },
593            mip_level_count: atlas.levels.len() as u32,
594            sample_count: 1,
595            dimension: wgpu::TextureDimension::D2,
596            format: wgpu::TextureFormat::Rgba16Float,
597            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
598            view_formats: &[],
599        });
600        for (index, level) in atlas.levels.iter().enumerate() {
601            let pixels = level
602                .pixels
603                .iter()
604                .map(|sample| half::f16::from_f32(*sample as f32 / 65535.0).to_bits())
605                .collect::<Vec<_>>();
606            queue.write_texture(
607                wgpu::TexelCopyTextureInfo {
608                    texture: &texture,
609                    mip_level: index as u32,
610                    origin: wgpu::Origin3d::ZERO,
611                    aspect: wgpu::TextureAspect::All,
612                },
613                bytemuck::cast_slice(&pixels),
614                wgpu::TexelCopyBufferLayout {
615                    offset: 0,
616                    bytes_per_row: Some(level.width * 8),
617                    rows_per_image: Some(level.height),
618                },
619                wgpu::Extent3d {
620                    width: level.width,
621                    height: level.height,
622                    depth_or_array_layers: 1,
623                },
624            );
625        }
626        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
627        Self {
628            texture,
629            view,
630            format: wgpu::TextureFormat::Rgba16Float,
631            width: first.width,
632            height: first.height,
633            levels: Vec::new(),
634            dimensions: atlas
635                .levels
636                .iter()
637                .map(|level| (level.width, level.height))
638                .collect(),
639        }
640    }
641
642    pub fn new(
643        device: &wgpu::Device,
644        queue: &wgpu::Queue,
645        atlas: &Atlas,
646    ) -> Result<Self, AtlasError> {
647        let format = format_for(atlas)?;
648        let levels = converted_levels(atlas)?;
649        let first = &atlas.levels[0];
650        let texture = device.create_texture(&wgpu::TextureDescriptor {
651            label: Some("text atlas"),
652            size: wgpu::Extent3d {
653                width: first.width,
654                height: first.height,
655                depth_or_array_layers: 1,
656            },
657            mip_level_count: levels.len() as u32,
658            sample_count: 1,
659            dimension: wgpu::TextureDimension::D2,
660            format,
661            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
662            view_formats: &[],
663        });
664        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
665        let mut result = Self {
666            texture,
667            view,
668            format,
669            width: first.width,
670            height: first.height,
671            levels: Vec::new(),
672            dimensions: Vec::new(),
673        };
674        result.upload_converted(queue, atlas, levels, true);
675        Ok(result)
676    }
677
678    fn upload_converted(
679        &mut self,
680        queue: &wgpu::Queue,
681        atlas: &Atlas,
682        levels: Vec<Vec<u8>>,
683        all: bool,
684    ) -> Vec<UploadRegion> {
685        let regions = changed_regions(atlas, &levels, if all { None } else { Some(&self.levels) });
686        let pixel_bytes = if atlas.channels == 1 { 1 } else { 4 };
687        for region in &regions {
688            let mip = &atlas.levels[region.level as usize];
689            let row_bytes = mip.width as usize * pixel_bytes;
690            let begin = region.first_row as usize * row_bytes;
691            let end = begin + region.rows as usize * row_bytes;
692            queue.write_texture(
693                wgpu::TexelCopyTextureInfo {
694                    texture: &self.texture,
695                    mip_level: region.level,
696                    origin: wgpu::Origin3d {
697                        x: 0,
698                        y: region.first_row,
699                        z: 0,
700                    },
701                    aspect: wgpu::TextureAspect::All,
702                },
703                &levels[region.level as usize][begin..end],
704                wgpu::TexelCopyBufferLayout {
705                    offset: 0,
706                    bytes_per_row: Some(row_bytes as u32),
707                    rows_per_image: Some(region.rows),
708                },
709                wgpu::Extent3d {
710                    width: mip.width,
711                    height: region.rows,
712                    depth_or_array_layers: 1,
713                },
714            );
715        }
716        self.dimensions = atlas
717            .levels
718            .iter()
719            .take(levels.len())
720            .map(|mip| (mip.width, mip.height))
721            .collect();
722        self.levels = levels;
723        regions
724    }
725
726    pub fn update(
727        &mut self,
728        device: &wgpu::Device,
729        queue: &wgpu::Queue,
730        atlas: &Atlas,
731    ) -> Result<Vec<UploadRegion>, AtlasError> {
732        let format = format_for(atlas)?;
733        let levels = converted_levels(atlas)?;
734        let first = &atlas.levels[0];
735        if self.format != format
736            || self.width != first.width
737            || self.height != first.height
738            || self.dimensions
739                != atlas
740                    .levels
741                    .iter()
742                    .take(levels.len())
743                    .map(|mip| (mip.width, mip.height))
744                    .collect::<Vec<_>>()
745        {
746            *self = Self::new(device, queue, atlas)?;
747            return Ok(atlas
748                .levels
749                .iter()
750                .take(levels.len())
751                .enumerate()
752                .map(|(level, mip)| UploadRegion {
753                    level: level as u32,
754                    first_row: 0,
755                    rows: mip.height,
756                    first_column: 0,
757                    columns: mip.width,
758                })
759                .collect());
760        }
761        Ok(self.upload_converted(queue, atlas, levels, false))
762    }
763
764    pub fn blank(device: &wgpu::Device, page: &PageInfo) -> Result<Self, AtlasError> {
765        let format = match page.channels {
766            1 => wgpu::TextureFormat::R8Unorm,
767            3 => wgpu::TextureFormat::Rgba8Unorm,
768            _ => return Err(AtlasError::InvalidChannels),
769        };
770        if page.size == 0 || page.levels == 0 || page.levels > 4 {
771            return Err(AtlasError::InvalidLevel);
772        }
773        let texture = device.create_texture(&wgpu::TextureDescriptor {
774            label: Some("glyph page"),
775            size: wgpu::Extent3d {
776                width: page.size,
777                height: page.size,
778                depth_or_array_layers: 1,
779            },
780            mip_level_count: page.levels,
781            sample_count: 1,
782            dimension: wgpu::TextureDimension::D2,
783            format,
784            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
785            view_formats: &[],
786        });
787        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
788        Ok(Self {
789            texture,
790            view,
791            format,
792            width: page.size,
793            height: page.size,
794            levels: Vec::new(),
795            dimensions: (0..page.levels)
796                .map(|level| ((page.size >> level).max(1), (page.size >> level).max(1)))
797                .collect(),
798        })
799    }
800
801    pub fn write_cells(
802        &mut self,
803        queue: &wgpu::Queue,
804        cells: &[&CellUpload],
805    ) -> Result<Vec<UploadRegion>, AtlasError> {
806        let (channels, pixel_bytes) = match self.format {
807            wgpu::TextureFormat::R8Unorm => (1usize, 1usize),
808            wgpu::TextureFormat::Rgba8Unorm => (3, 4),
809            _ => return Err(AtlasError::InvalidChannels),
810        };
811        let mut regions = Vec::with_capacity(cells.len());
812        for cell in cells {
813            let [x, y, width, height] = cell.rect;
814            let &(level_width, level_height) = self
815                .dimensions
816                .get(cell.level as usize)
817                .ok_or(AtlasError::InvalidLevel)?;
818            if width == 0
819                || height == 0
820                || x + width > level_width
821                || y + height > level_height
822                || cell.bytes.len() != (width * height) as usize * channels
823            {
824                return Err(AtlasError::InvalidLevel);
825            }
826            let bytes = if channels == 3 {
827                cell.bytes
828                    .chunks_exact(3)
829                    .flat_map(|rgb| [rgb[0], rgb[1], rgb[2], 255])
830                    .collect::<Vec<_>>()
831            } else {
832                cell.bytes.clone()
833            };
834            queue.write_texture(
835                wgpu::TexelCopyTextureInfo {
836                    texture: &self.texture,
837                    mip_level: cell.level,
838                    origin: wgpu::Origin3d { x, y, z: 0 },
839                    aspect: wgpu::TextureAspect::All,
840                },
841                &bytes,
842                wgpu::TexelCopyBufferLayout {
843                    offset: 0,
844                    bytes_per_row: Some(width * pixel_bytes as u32),
845                    rows_per_image: Some(height),
846                },
847                wgpu::Extent3d {
848                    width,
849                    height,
850                    depth_or_array_layers: 1,
851                },
852            );
853            regions.push(UploadRegion {
854                level: cell.level,
855                first_row: y,
856                rows: height,
857                first_column: x,
858                columns: width,
859            });
860        }
861        if !regions.is_empty() {
862            self.levels.clear();
863        }
864        Ok(regions)
865    }
866
867    pub fn view(&self) -> &wgpu::TextureView {
868        &self.view
869    }
870    pub fn format(&self) -> wgpu::TextureFormat {
871        self.format
872    }
873    pub fn size(&self) -> [u32; 2] {
874        [self.width, self.height]
875    }
876}
877
878#[derive(Default)]
879pub struct GlyphPages {
880    pages: BTreeMap<u32, GpuAtlas>,
881    skipped: BTreeSet<u32>,
882}
883
884impl GlyphPages {
885    pub fn new() -> Self {
886        Self::default()
887    }
888
889    pub fn sync(
890        &mut self,
891        device: &wgpu::Device,
892        queue: &wgpu::Queue,
893        changes: &AtlasChanges,
894    ) -> Result<Vec<(u32, UploadRegion)>, AtlasError> {
895        for id in &changes.dropped {
896            self.pages.remove(id);
897            self.skipped.remove(id);
898        }
899        for page in &changes.created {
900            if page.kind == PageKind::Color {
901                self.skipped.insert(page.id);
902                continue;
903            }
904            self.pages.insert(page.id, GpuAtlas::blank(device, page)?);
905        }
906        let mut uploads = BTreeMap::<u32, Vec<&CellUpload>>::new();
907        for cell in &changes.cells {
908            if self.skipped.contains(&cell.page) {
909                continue;
910            }
911            uploads.entry(cell.page).or_default().push(cell);
912        }
913        let mut regions = Vec::new();
914        for (id, cells) in uploads {
915            let page = self.pages.get_mut(&id).ok_or(AtlasError::Empty)?;
916            regions.extend(
917                page.write_cells(queue, &cells)?
918                    .into_iter()
919                    .map(|region| (id, region)),
920            );
921        }
922        Ok(regions)
923    }
924
925    pub fn page(&self, id: u32) -> Option<&GpuAtlas> {
926        self.pages.get(&id)
927    }
928
929    pub fn len(&self) -> usize {
930        self.pages.len()
931    }
932
933    pub fn is_empty(&self) -> bool {
934        self.pages.is_empty()
935    }
936}
937
938pub fn page_quads(quads: &[PageQuad]) -> Vec<(u32, Vec<RichQuad>)> {
939    let mut groups: Vec<(u32, Vec<RichQuad>)> = Vec::new();
940    for quad in quads {
941        let rich = RichQuad::from(&quad.quad);
942        match groups.iter_mut().find(|(page, _)| *page == quad.page) {
943            Some((_, list)) => list.push(rich),
944            None => groups.push((quad.page, vec![rich])),
945        }
946    }
947    groups
948}
949
950#[derive(Clone, Debug)]
951pub struct IconLevel {
952    pub width: u32,
953    pub height: u32,
954    pub pixels: Vec<u16>,
955}
956
957pub struct IconAtlas {
958    pub levels: Vec<IconLevel>,
959    rects: Vec<(Handle, [u32; 4])>,
960}
961
962impl IconAtlas {
963    pub fn from_cache(cache: &Cache, handles: &[Handle]) -> Result<Self, String> {
964        let width = 2048u32;
965        let padding = 8u32;
966        let mut x = 0u32;
967        let mut y = 0u32;
968        let mut row_height = 0u32;
969        let mut rects = Vec::new();
970        for &handle in handles {
971            if rects.iter().any(|(existing, _)| *existing == handle) {
972                continue;
973            }
974            let Some(Asset::Image(image)) = cache.get(handle) else {
975                return Err("icon handle is not an image".into());
976            };
977            if image.width > i32::MAX as u32 - padding || image.height > i32::MAX as u32 - padding {
978                return Err("icon is too large".into());
979            }
980            let cell_width = image
981                .width
982                .checked_add(padding * 2)
983                .ok_or("icon is too large")?
984                .next_multiple_of(8);
985            let cell_height = image
986                .height
987                .checked_add(padding * 2)
988                .ok_or("icon is too large")?
989                .next_multiple_of(8);
990            if cell_width > width {
991                return Err("icon is wider than the atlas".into());
992            }
993            if x + cell_width > width {
994                y = y.checked_add(row_height).ok_or("icon atlas is too large")?;
995                x = 0;
996                row_height = 0;
997            }
998            rects.push((
999                handle,
1000                [x + padding, y + padding, image.width, image.height],
1001            ));
1002            x += cell_width;
1003            row_height = row_height.max(cell_height);
1004        }
1005        let height = y
1006            .checked_add(row_height)
1007            .ok_or("icon atlas is too large")?
1008            .checked_next_power_of_two()
1009            .ok_or("icon atlas is too large")?
1010            .max(8);
1011        if height > 16384 {
1012            return Err("icon atlas is too tall".into());
1013        }
1014        let mut pixels = vec![0u16; width as usize * height as usize * 4];
1015        for &(handle, rect) in &rects {
1016            let Some(Asset::Image(image)) = cache.get(handle) else {
1017                unreachable!()
1018            };
1019            let cell_width = (image.width + padding * 2).next_multiple_of(8);
1020            let cell_height = (image.height + padding * 2).next_multiple_of(8);
1021            for dy in -(padding as i32)..(cell_height - padding) as i32 {
1022                for dx in -(padding as i32)..(cell_width - padding) as i32 {
1023                    let sx = dx.clamp(0, image.width as i32 - 1) as u32;
1024                    let sy = dy.clamp(0, image.height as i32 - 1) as u32;
1025                    let rgba = image_pixel(image, sx, sy);
1026                    let at = (((rect[1] as i32 + dy) as u32 * width + (rect[0] as i32 + dx) as u32)
1027                        * 4) as usize;
1028                    pixels[at..at + 4].copy_from_slice(&rgba);
1029                }
1030            }
1031        }
1032        let mut levels = vec![IconLevel {
1033            width,
1034            height,
1035            pixels,
1036        }];
1037        while levels.len() < 4 {
1038            let previous = levels.last().unwrap();
1039            let width = (previous.width / 2).max(1);
1040            let height = (previous.height / 2).max(1);
1041            let mut pixels = vec![0u16; width as usize * height as usize * 4];
1042            for row in 0..height {
1043                for col in 0..width {
1044                    for channel in 0..4 {
1045                        let mut sum = 0u32;
1046                        for dy in 0..2 {
1047                            for dx in 0..2 {
1048                                let sx = (col * 2 + dx).min(previous.width - 1);
1049                                let sy = (row * 2 + dy).min(previous.height - 1);
1050                                sum += previous.pixels
1051                                    [((sy * previous.width + sx) * 4 + channel) as usize]
1052                                    as u32;
1053                            }
1054                        }
1055                        pixels[((row * width + col) * 4 + channel) as usize] =
1056                            ((sum + 2) / 4) as u16;
1057                    }
1058                }
1059            }
1060            levels.push(IconLevel {
1061                width,
1062                height,
1063                pixels,
1064            });
1065        }
1066        Ok(Self { levels, rects })
1067    }
1068
1069    pub fn uv(&self, handle: Handle) -> Option<[f32; 4]> {
1070        let rect = self.rects.iter().find(|(entry, _)| *entry == handle)?.1;
1071        let first = &self.levels[0];
1072        Some([
1073            rect[0] as f32 / first.width as f32,
1074            rect[1] as f32 / first.height as f32,
1075            (rect[0] + rect[2]) as f32 / first.width as f32,
1076            (rect[1] + rect[3]) as f32 / first.height as f32,
1077        ])
1078    }
1079}
1080
1081fn image_pixel(image: &pfx_load::Image, x: u32, y: u32) -> [u16; 4] {
1082    let at = ((y * image.width + x) * 4) as usize;
1083    let rgba = match &image.pixels {
1084        Pixels::Eight(bytes) => <[u8; 4]>::try_from(&bytes[at..at + 4])
1085            .unwrap()
1086            .map(|value| value as f32 / 255.0),
1087        Pixels::Sixteen(samples) => <[u16; 4]>::try_from(&samples[at..at + 4])
1088            .unwrap()
1089            .map(|value| value as f32 / 65535.0),
1090    };
1091    let linear = |value: f32| {
1092        if image.space == ColorSpace::Srgb {
1093            pfx_materials::linear_channel(value)
1094        } else {
1095            value
1096        }
1097    };
1098    [
1099        (linear(rgba[0]) * rgba[3] * 65535.0).round() as u16,
1100        (linear(rgba[1]) * rgba[3] * 65535.0).round() as u16,
1101        (linear(rgba[2]) * rgba[3] * 65535.0).round() as u16,
1102        (rgba[3] * 65535.0).round() as u16,
1103    ]
1104}
1105
1106#[repr(C)]
1107#[derive(Clone, Copy, Pod, Zeroable)]
1108struct Uniform {
1109    matrix: [[f32; 4]; 4],
1110    view_projection: [[f32; 4]; 4],
1111    model: [[f32; 4]; 4],
1112    rest_normal: [f32; 4],
1113    atlas_size: [f32; 2],
1114    mode: u32,
1115    deformer: u32,
1116    cells: [u32; 4],
1117}
1118
1119pub const DEFORMED_CELLS: u32 = 8;
1120
1121#[derive(Clone, Copy, Debug, PartialEq)]
1122pub enum TextSpace {
1123    Surface {
1124        model_view_projection: [[f32; 4]; 4],
1125    },
1126    Deformed {
1127        view_projection: [[f32; 4]; 4],
1128        model: [[f32; 4]; 4],
1129        layout: [[f32; 4]; 4],
1130        normal: [f32; 3],
1131        lift: f32,
1132        deformer: DeformerId,
1133    },
1134    Overlay {
1135        width: u32,
1136        height: u32,
1137    },
1138    LitSurface {
1139        model_view_projection: [[f32; 4]; 4],
1140        model: [[f32; 4]; 4],
1141    },
1142    LitDeformed {
1143        view_projection: [[f32; 4]; 4],
1144        model: [[f32; 4]; 4],
1145        layout: [[f32; 4]; 4],
1146        normal: [f32; 3],
1147        lift: f32,
1148        deformer: DeformerId,
1149    },
1150}
1151
1152impl TextSpace {
1153    pub fn is_lit(&self) -> bool {
1154        matches!(self, Self::LitSurface { .. } | Self::LitDeformed { .. })
1155    }
1156
1157    fn matrix(&self) -> [[f32; 4]; 4] {
1158        match *self {
1159            Self::Surface {
1160                model_view_projection,
1161            }
1162            | Self::LitSurface {
1163                model_view_projection,
1164                ..
1165            } => model_view_projection,
1166            Self::Deformed { layout, .. } | Self::LitDeformed { layout, .. } => layout,
1167            Self::Overlay { width, height } => [
1168                [2.0 / width as f32, 0.0, 0.0, 0.0],
1169                [0.0, -2.0 / height as f32, 0.0, 0.0],
1170                [0.0, 0.0, 1.0, 0.0],
1171                [-1.0, 1.0, 0.0, 1.0],
1172            ],
1173        }
1174    }
1175}
1176
1177pub struct TextPass {
1178    surface: wgpu::RenderPipeline,
1179    deformed: wgpu::RenderPipeline,
1180    overlay: wgpu::RenderPipeline,
1181    surface_ids: wgpu::RenderPipeline,
1182    deformed_ids: wgpu::RenderPipeline,
1183    overlay_ids: wgpu::RenderPipeline,
1184    bind_layout: wgpu::BindGroupLayout,
1185    deformer_layout: wgpu::BindGroupLayout,
1186    empty_group: wgpu::BindGroup,
1187    sampler: wgpu::Sampler,
1188    lit: OnceCell<LitPipelines>,
1189    gobo: Gobo,
1190    viewport: Option<[u32; 2]>,
1191}
1192
1193struct LitPipelines {
1194    surface: wgpu::RenderPipeline,
1195    deformed: wgpu::RenderPipeline,
1196    surface_ids: wgpu::RenderPipeline,
1197    deformed_ids: wgpu::RenderPipeline,
1198    layout: wgpu::BindGroupLayout,
1199}
1200
1201pub struct LitBindings<'a> {
1202    pub scene: &'a wgpu::BindGroup,
1203    pub shadow: &'a wgpu::BindGroup,
1204    pub lighting: &'a wgpu::BindGroup,
1205    pub deformers: &'a wgpu::Buffer,
1206    pub plan: &'a wgpu::Buffer,
1207}
1208
1209fn text_pipeline(
1210    device: &wgpu::Device,
1211    label: &str,
1212    layout: &wgpu::PipelineLayout,
1213    shader: &wgpu::ShaderModule,
1214    entries: (&str, &str),
1215    depth: bool,
1216    ids: bool,
1217) -> wgpu::RenderPipeline {
1218    let attribute = |offset: u64, shader_location: u32| wgpu::VertexAttribute {
1219        format: wgpu::VertexFormat::Float32x4,
1220        offset,
1221        shader_location,
1222    };
1223    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1224        label: Some(label),
1225        layout: Some(layout),
1226        vertex: wgpu::VertexState {
1227            module: shader,
1228            entry_point: Some(entries.0),
1229            compilation_options: Default::default(),
1230            buffers: &[Some(wgpu::VertexBufferLayout {
1231                array_stride: std::mem::size_of::<PackedGlyph>() as u64,
1232                step_mode: wgpu::VertexStepMode::Instance,
1233                attributes: &[
1234                    attribute(0, 0),
1235                    attribute(16, 1),
1236                    attribute(32, 2),
1237                    attribute(48, 3),
1238                    attribute(64, 4),
1239                    attribute(80, 5),
1240                ],
1241            })],
1242        },
1243        primitive: wgpu::PrimitiveState {
1244            topology: wgpu::PrimitiveTopology::TriangleList,
1245            cull_mode: None,
1246            ..Default::default()
1247        },
1248        depth_stencil: depth.then_some(wgpu::DepthStencilState {
1249            format: DEPTH_FORMAT,
1250            depth_write_enabled: Some(false),
1251            depth_compare: Some(wgpu::CompareFunction::LessEqual),
1252            stencil: Default::default(),
1253            bias: Default::default(),
1254        }),
1255        multisample: Default::default(),
1256        fragment: Some(wgpu::FragmentState {
1257            module: shader,
1258            entry_point: Some(entries.1),
1259            compilation_options: Default::default(),
1260            targets: &[Some(if ids {
1261                wgpu::ColorTargetState {
1262                    format: ID_FORMAT,
1263                    blend: None,
1264                    write_mask: wgpu::ColorWrites::ALL,
1265                }
1266            } else {
1267                wgpu::ColorTargetState {
1268                    format: COLOR_FORMAT,
1269                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
1270                    write_mask: wgpu::ColorWrites::ALL,
1271                }
1272            })],
1273        }),
1274        multiview_mask: None,
1275        cache: None,
1276    })
1277}
1278
1279pub fn lit_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1280    let storage = |binding: u32| wgpu::BindGroupLayoutEntry {
1281        binding,
1282        visibility: wgpu::ShaderStages::VERTEX,
1283        ty: wgpu::BindingType::Buffer {
1284            ty: wgpu::BufferBindingType::Storage { read_only: true },
1285            has_dynamic_offset: false,
1286            min_binding_size: None,
1287        },
1288        count: None,
1289    };
1290    vec![
1291        storage(0),
1292        storage(1),
1293        wgpu::BindGroupLayoutEntry {
1294            binding: 2,
1295            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1296            ty: wgpu::BindingType::Buffer {
1297                ty: wgpu::BufferBindingType::Uniform,
1298                has_dynamic_offset: false,
1299                min_binding_size: None,
1300            },
1301            count: None,
1302        },
1303        wgpu::BindGroupLayoutEntry {
1304            binding: 3,
1305            visibility: wgpu::ShaderStages::FRAGMENT,
1306            ty: wgpu::BindingType::Texture {
1307                sample_type: wgpu::TextureSampleType::Float { filterable: true },
1308                view_dimension: wgpu::TextureViewDimension::D2,
1309                multisampled: false,
1310            },
1311            count: None,
1312        },
1313        wgpu::BindGroupLayoutEntry {
1314            binding: 4,
1315            visibility: wgpu::ShaderStages::FRAGMENT,
1316            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1317            count: None,
1318        },
1319    ]
1320}
1321
1322impl LitPipelines {
1323    fn new(device: &wgpu::Device, gobo: &Gobo) -> Self {
1324        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1325            label: Some("lit text shader"),
1326            source: wgpu::ShaderSource::Wgsl(lit_shader_source_with(gobo).into()),
1327        });
1328        let group = |label: &str, entries: &[wgpu::BindGroupLayoutEntry]| {
1329            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1330                label: Some(label),
1331                entries,
1332            })
1333        };
1334        let scene = group("lit text scene", &crate::frame::scene_entries());
1335        let shadow = group("lit text shadows", &crate::frame::shadow_entries());
1336        let own = group("lit text bindings", &lit_entries());
1337        let lighting = group("lit text lighting", &crate::frame::lighting_entries());
1338        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1339            label: Some("lit text pipeline layout"),
1340            bind_group_layouts: &[Some(&scene), Some(&shadow), Some(&own), Some(&lighting)],
1341            immediate_size: 0,
1342        });
1343        Self {
1344            surface: text_pipeline(
1345                device,
1346                "lit surface text",
1347                &layout,
1348                &shader,
1349                ("vs_lit_surface", "fs_lit"),
1350                true,
1351                false,
1352            ),
1353            deformed: text_pipeline(
1354                device,
1355                "lit deformed text",
1356                &layout,
1357                &shader,
1358                ("vs_lit_deformed", "fs_lit"),
1359                true,
1360                false,
1361            ),
1362            surface_ids: text_pipeline(
1363                device,
1364                "lit surface text ids",
1365                &layout,
1366                &shader,
1367                ("vs_lit_surface", "fs_lit_id"),
1368                true,
1369                true,
1370            ),
1371            deformed_ids: text_pipeline(
1372                device,
1373                "lit deformed text ids",
1374                &layout,
1375                &shader,
1376                ("vs_lit_deformed", "fs_lit_id"),
1377                true,
1378                true,
1379            ),
1380            layout: own,
1381        }
1382    }
1383}
1384
1385pub struct TextDraw<'a> {
1386    pub target: &'a wgpu::TextureView,
1387    pub depth: Option<&'a wgpu::TextureView>,
1388    pub atlas: &'a GpuAtlas,
1389    pub paragraph: &'a Paragraph,
1390    pub space: TextSpace,
1391    pub color: [f32; 4],
1392    pub deformers: Option<&'a wgpu::BindGroup>,
1393}
1394
1395pub struct QuadDraw<'a> {
1396    pub target: &'a wgpu::TextureView,
1397    pub depth: Option<&'a wgpu::TextureView>,
1398    pub atlas: &'a GpuAtlas,
1399    pub quads: &'a [RichQuad],
1400    pub space: TextSpace,
1401    pub deformers: Option<&'a wgpu::BindGroup>,
1402    pub icons: bool,
1403}
1404
1405impl TextPass {
1406    pub fn new(device: &wgpu::Device) -> Self {
1407        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1408            label: Some("text shader"),
1409            source: wgpu::ShaderSource::Wgsl(shader_source().into()),
1410        });
1411        let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1412            label: Some("text bindings"),
1413            entries: &text_entries(),
1414        });
1415        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1416            label: Some("text pipeline layout"),
1417            bind_group_layouts: &[Some(&bind_layout)],
1418            immediate_size: 0,
1419        });
1420        let empty_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1421            label: Some("text empty"),
1422            entries: &[],
1423        });
1424        let empty_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1425            label: Some("text empty"),
1426            layout: &empty_layout,
1427            entries: &[],
1428        });
1429        let deformer_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1430            label: Some("text deformers"),
1431            entries: &deformer_entries(),
1432        });
1433        let deformed_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1434            label: Some("deformed text pipeline layout"),
1435            bind_group_layouts: &[
1436                Some(&bind_layout),
1437                Some(&empty_layout),
1438                Some(&deformer_layout),
1439            ],
1440            immediate_size: 0,
1441        });
1442        let surface = text_pipeline(
1443            device,
1444            "surface text",
1445            &layout,
1446            &shader,
1447            ("vs_main", "fs_main"),
1448            true,
1449            false,
1450        );
1451        let deformed = text_pipeline(
1452            device,
1453            "deformed text",
1454            &deformed_layout,
1455            &shader,
1456            ("vs_deformed", "fs_main"),
1457            true,
1458            false,
1459        );
1460        let overlay = text_pipeline(
1461            device,
1462            "overlay text",
1463            &layout,
1464            &shader,
1465            ("vs_main", "fs_overlay"),
1466            false,
1467            false,
1468        );
1469        let surface_ids = text_pipeline(
1470            device,
1471            "surface text ids",
1472            &layout,
1473            &shader,
1474            ("vs_main", "fs_id"),
1475            true,
1476            true,
1477        );
1478        let deformed_ids = text_pipeline(
1479            device,
1480            "deformed text ids",
1481            &deformed_layout,
1482            &shader,
1483            ("vs_deformed", "fs_id"),
1484            true,
1485            true,
1486        );
1487        let overlay_ids = text_pipeline(
1488            device,
1489            "overlay text ids",
1490            &layout,
1491            &shader,
1492            ("vs_main", "fs_overlay_id"),
1493            false,
1494            true,
1495        );
1496        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1497            label: Some("text sampler"),
1498            address_mode_u: wgpu::AddressMode::ClampToEdge,
1499            address_mode_v: wgpu::AddressMode::ClampToEdge,
1500            mag_filter: wgpu::FilterMode::Linear,
1501            min_filter: wgpu::FilterMode::Linear,
1502            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1503            lod_max_clamp: 3.0,
1504            anisotropy_clamp: 8,
1505            ..Default::default()
1506        });
1507        Self {
1508            surface,
1509            deformed,
1510            overlay,
1511            surface_ids,
1512            deformed_ids,
1513            overlay_ids,
1514            bind_layout,
1515            deformer_layout,
1516            empty_group,
1517            sampler,
1518            lit: OnceCell::new(),
1519            gobo: Gobo::default(),
1520            viewport: None,
1521        }
1522    }
1523
1524    pub fn set_viewport(&mut self, viewport: Option<[u32; 2]>) {
1525        self.viewport = viewport;
1526    }
1527
1528    pub fn viewport(&self) -> Option<[u32; 2]> {
1529        self.viewport
1530    }
1531
1532    pub fn set_canopy_gobo(&mut self, gobo: Gobo) {
1533        if gobo != self.gobo {
1534            self.gobo = gobo;
1535            self.lit = OnceCell::new();
1536        }
1537    }
1538
1539    pub fn deformer_group(
1540        &self,
1541        device: &wgpu::Device,
1542        buffer: &wgpu::Buffer,
1543        plan: &wgpu::Buffer,
1544    ) -> wgpu::BindGroup {
1545        device.create_bind_group(&wgpu::BindGroupDescriptor {
1546            label: Some("text deformers"),
1547            layout: &self.deformer_layout,
1548            entries: &[
1549                wgpu::BindGroupEntry {
1550                    binding: 0,
1551                    resource: buffer.as_entire_binding(),
1552                },
1553                wgpu::BindGroupEntry {
1554                    binding: 1,
1555                    resource: plan.as_entire_binding(),
1556                },
1557            ],
1558        })
1559    }
1560
1561    pub fn encode(
1562        &self,
1563        device: &wgpu::Device,
1564        encoder: &mut wgpu::CommandEncoder,
1565        draw: TextDraw<'_>,
1566    ) -> Result<(), &'static str> {
1567        self.encode_timed(device, encoder, draw, None)
1568    }
1569
1570    pub fn encode_timed(
1571        &self,
1572        device: &wgpu::Device,
1573        encoder: &mut wgpu::CommandEncoder,
1574        draw: TextDraw<'_>,
1575        profiler: Option<&mut pfx_gpu::GpuProfiler>,
1576    ) -> Result<(), &'static str> {
1577        self.encode_paragraph(device, encoder, draw, None, None, profiler)
1578    }
1579
1580    pub fn encode_lit_timed(
1581        &self,
1582        device: &wgpu::Device,
1583        encoder: &mut wgpu::CommandEncoder,
1584        draw: TextDraw<'_>,
1585        lit: &LitBindings<'_>,
1586        profiler: Option<&mut pfx_gpu::GpuProfiler>,
1587    ) -> Result<(), &'static str> {
1588        self.encode_paragraph(device, encoder, draw, Some(lit), None, profiler)
1589    }
1590
1591    pub fn encode_ids(
1592        &self,
1593        device: &wgpu::Device,
1594        encoder: &mut wgpu::CommandEncoder,
1595        draw: TextDraw<'_>,
1596        lit: Option<&LitBindings<'_>>,
1597        id: u32,
1598        profiler: Option<&mut pfx_gpu::GpuProfiler>,
1599    ) -> Result<(), &'static str> {
1600        if id == 0 {
1601            return Ok(());
1602        }
1603        self.encode_paragraph(device, encoder, draw, lit, Some(id), profiler)
1604    }
1605
1606    fn encode_paragraph(
1607        &self,
1608        device: &wgpu::Device,
1609        encoder: &mut wgpu::CommandEncoder,
1610        draw: TextDraw<'_>,
1611        lit: Option<&LitBindings<'_>>,
1612        id: Option<u32>,
1613        profiler: Option<&mut pfx_gpu::GpuProfiler>,
1614    ) -> Result<(), &'static str> {
1615        let TextDraw {
1616            target,
1617            depth,
1618            atlas,
1619            paragraph,
1620            space,
1621            color,
1622            deformers,
1623        } = draw;
1624        if paragraph.glyphs.is_empty() {
1625            return Ok(());
1626        }
1627        if atlas.format
1628            != match paragraph.atlas.channels {
1629                1 => wgpu::TextureFormat::R8Unorm,
1630                3 => wgpu::TextureFormat::Rgba8Unorm,
1631                _ => return Err("invalid atlas channels"),
1632            }
1633        {
1634            return Err("atlas format does not match paragraph");
1635        }
1636        if atlas.size()
1637            != [
1638                paragraph.atlas.levels[0].width,
1639                paragraph.atlas.levels[0].height,
1640            ]
1641        {
1642            return Err("atlas size does not match paragraph");
1643        }
1644        let mut packed = pack_glyphs(&paragraph.glyphs);
1645        for glyph in &mut packed {
1646            for (channel, tint) in glyph.color.iter_mut().zip(color) {
1647                *channel *= tint;
1648            }
1649        }
1650        self.encode_packed(
1651            device,
1652            encoder,
1653            Packed {
1654                target,
1655                depth,
1656                atlas,
1657                space,
1658                deformers,
1659                lit,
1660                mode: u32::from(paragraph.atlas.channels == 3),
1661                id,
1662                glyphs: &packed,
1663            },
1664            profiler,
1665        )
1666    }
1667
1668    pub fn encode_quads(
1669        &self,
1670        device: &wgpu::Device,
1671        encoder: &mut wgpu::CommandEncoder,
1672        draw: QuadDraw<'_>,
1673    ) -> Result<(), &'static str> {
1674        self.encode_quads_timed(device, encoder, draw, None)
1675    }
1676
1677    pub fn encode_quads_timed(
1678        &self,
1679        device: &wgpu::Device,
1680        encoder: &mut wgpu::CommandEncoder,
1681        draw: QuadDraw<'_>,
1682        profiler: Option<&mut pfx_gpu::GpuProfiler>,
1683    ) -> Result<(), &'static str> {
1684        self.encode_quad_list(device, encoder, draw, None, None, profiler)
1685    }
1686
1687    pub fn encode_quads_lit_timed(
1688        &self,
1689        device: &wgpu::Device,
1690        encoder: &mut wgpu::CommandEncoder,
1691        draw: QuadDraw<'_>,
1692        lit: &LitBindings<'_>,
1693        profiler: Option<&mut pfx_gpu::GpuProfiler>,
1694    ) -> Result<(), &'static str> {
1695        self.encode_quad_list(device, encoder, draw, Some(lit), None, profiler)
1696    }
1697
1698    pub fn encode_quad_ids(
1699        &self,
1700        device: &wgpu::Device,
1701        encoder: &mut wgpu::CommandEncoder,
1702        draw: QuadDraw<'_>,
1703        lit: Option<&LitBindings<'_>>,
1704        id: u32,
1705        profiler: Option<&mut pfx_gpu::GpuProfiler>,
1706    ) -> Result<(), &'static str> {
1707        if id == 0 {
1708            return Ok(());
1709        }
1710        self.encode_quad_list(device, encoder, draw, lit, Some(id), profiler)
1711    }
1712
1713    fn encode_quad_list(
1714        &self,
1715        device: &wgpu::Device,
1716        encoder: &mut wgpu::CommandEncoder,
1717        draw: QuadDraw<'_>,
1718        lit: Option<&LitBindings<'_>>,
1719        id: Option<u32>,
1720        profiler: Option<&mut pfx_gpu::GpuProfiler>,
1721    ) -> Result<(), &'static str> {
1722        if draw.quads.is_empty() {
1723            return Ok(());
1724        }
1725        let mode = if draw.icons {
1726            2
1727        } else if draw.atlas.format == wgpu::TextureFormat::Rgba8Unorm {
1728            1
1729        } else {
1730            0
1731        };
1732        if draw.icons != (draw.atlas.format == wgpu::TextureFormat::Rgba16Float) {
1733            return Err("icon atlas format does not match draw mode");
1734        }
1735        let packed = pack_quads(draw.quads);
1736        self.encode_packed(
1737            device,
1738            encoder,
1739            Packed {
1740                target: draw.target,
1741                depth: draw.depth,
1742                atlas: draw.atlas,
1743                space: draw.space,
1744                deformers: draw.deformers,
1745                lit,
1746                mode,
1747                id,
1748                glyphs: &packed,
1749            },
1750            profiler,
1751        )
1752    }
1753
1754    fn encode_packed(
1755        &self,
1756        device: &wgpu::Device,
1757        encoder: &mut wgpu::CommandEncoder,
1758        packed: Packed<'_>,
1759        mut profiler: Option<&mut pfx_gpu::GpuProfiler>,
1760    ) -> Result<(), &'static str> {
1761        let Packed {
1762            target,
1763            depth,
1764            atlas,
1765            space,
1766            deformers,
1767            lit,
1768            mode,
1769            id,
1770            glyphs: packed,
1771        } = packed;
1772        let deformed = matches!(
1773            space,
1774            TextSpace::Deformed { .. } | TextSpace::LitDeformed { .. }
1775        );
1776        if space.is_lit() != lit.is_some() {
1777            return Err(if space.is_lit() {
1778                "lit text needs the frame's lighting bindings"
1779            } else {
1780                "lighting bindings need lit text"
1781            });
1782        }
1783        if !matches!(space, TextSpace::Overlay { .. }) && depth.is_none() {
1784            return Err("surface text needs a depth view");
1785        }
1786        if deformed && !space.is_lit() && deformers.is_none() {
1787            return Err("deformed text needs the deformer bindings");
1788        }
1789        if matches!(
1790            space,
1791            TextSpace::Overlay { width: 0, .. } | TextSpace::Overlay { height: 0, .. }
1792        ) {
1793            return Err("overlay size must be positive");
1794        }
1795        let instances = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1796            label: Some("text instances"),
1797            contents: bytemuck::cast_slice(packed),
1798            usage: wgpu::BufferUsages::VERTEX,
1799        });
1800        let identity = [
1801            [1.0, 0.0, 0.0, 0.0],
1802            [0.0, 1.0, 0.0, 0.0],
1803            [0.0, 0.0, 1.0, 0.0],
1804            [0.0, 0.0, 0.0, 1.0],
1805        ];
1806        let (view_projection, model, rest_normal, deformer) = match space {
1807            TextSpace::Deformed {
1808                view_projection,
1809                model,
1810                normal,
1811                lift,
1812                deformer,
1813                ..
1814            }
1815            | TextSpace::LitDeformed {
1816                view_projection,
1817                model,
1818                normal,
1819                lift,
1820                deformer,
1821                ..
1822            } => (
1823                view_projection,
1824                model,
1825                [normal[0], normal[1], normal[2], lift],
1826                deformer.0,
1827            ),
1828            TextSpace::LitSurface { model, .. } => {
1829                (identity, model, [0.0, 0.0, 1.0, 0.0], DeformerId::NONE.0)
1830            }
1831            _ => (identity, identity, [0.0, 0.0, 1.0, 0.0], DeformerId::NONE.0),
1832        };
1833        let uniform = Uniform {
1834            matrix: space.matrix(),
1835            view_projection,
1836            model,
1837            rest_normal,
1838            atlas_size: [atlas.width as f32, atlas.height as f32],
1839            mode,
1840            deformer,
1841            cells: [DEFORMED_CELLS, id.unwrap_or(0), 0, 0],
1842        };
1843        let uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1844            label: Some("text uniform"),
1845            contents: bytemuck::bytes_of(&uniform),
1846            usage: wgpu::BufferUsages::UNIFORM,
1847        });
1848        let lit_pipelines = lit.map(|_| {
1849            self.lit
1850                .get_or_init(|| LitPipelines::new(device, &self.gobo))
1851        });
1852        let bind = match (lit, lit_pipelines) {
1853            (Some(lit), Some(pipelines)) => device.create_bind_group(&wgpu::BindGroupDescriptor {
1854                label: Some("lit text bind group"),
1855                layout: &pipelines.layout,
1856                entries: &[
1857                    wgpu::BindGroupEntry {
1858                        binding: 0,
1859                        resource: lit.deformers.as_entire_binding(),
1860                    },
1861                    wgpu::BindGroupEntry {
1862                        binding: 1,
1863                        resource: lit.plan.as_entire_binding(),
1864                    },
1865                    wgpu::BindGroupEntry {
1866                        binding: 2,
1867                        resource: uniform.as_entire_binding(),
1868                    },
1869                    wgpu::BindGroupEntry {
1870                        binding: 3,
1871                        resource: wgpu::BindingResource::TextureView(atlas.view()),
1872                    },
1873                    wgpu::BindGroupEntry {
1874                        binding: 4,
1875                        resource: wgpu::BindingResource::Sampler(&self.sampler),
1876                    },
1877                ],
1878            }),
1879            _ => device.create_bind_group(&wgpu::BindGroupDescriptor {
1880                label: Some("text bind group"),
1881                layout: &self.bind_layout,
1882                entries: &[
1883                    wgpu::BindGroupEntry {
1884                        binding: 0,
1885                        resource: uniform.as_entire_binding(),
1886                    },
1887                    wgpu::BindGroupEntry {
1888                        binding: 1,
1889                        resource: wgpu::BindingResource::TextureView(atlas.view()),
1890                    },
1891                    wgpu::BindGroupEntry {
1892                        binding: 2,
1893                        resource: wgpu::BindingResource::Sampler(&self.sampler),
1894                    },
1895                ],
1896            }),
1897        };
1898        let surface = !matches!(space, TextSpace::Overlay { .. });
1899        let depth_attachment =
1900            depth
1901                .filter(|_| surface)
1902                .map(|view| wgpu::RenderPassDepthStencilAttachment {
1903                    view,
1904                    depth_ops: Some(wgpu::Operations {
1905                        load: wgpu::LoadOp::Load,
1906                        store: wgpu::StoreOp::Store,
1907                    }),
1908                    stencil_ops: None,
1909                });
1910        let ids = id.is_some();
1911        let timing = profiler.as_deref_mut().and_then(|timer| {
1912            timer.pass(match (ids, surface, lit.is_some()) {
1913                (true, _, _) => "text ids",
1914                (false, false, _) => "overlay text",
1915                (false, true, true) => "lit surface text",
1916                (false, true, false) => "surface text",
1917            })
1918        });
1919        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1920            multiview_mask: None,
1921            label: Some(if ids { "text ids" } else { "text pass" }),
1922            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1923                view: target,
1924                depth_slice: None,
1925                resolve_target: None,
1926                ops: wgpu::Operations {
1927                    load: wgpu::LoadOp::Load,
1928                    store: wgpu::StoreOp::Store,
1929                },
1930            })],
1931            depth_stencil_attachment: depth_attachment,
1932            timestamp_writes: profiler
1933                .as_deref()
1934                .and_then(|timer| timer.render_writes(timing)),
1935            occlusion_query_set: None,
1936        });
1937        if let Some(viewport) = self.viewport {
1938            crate::viewport::apply(&mut pass, viewport);
1939        }
1940        if let (Some(lit), Some(pipelines)) = (lit, lit_pipelines) {
1941            pass.set_pipeline(match (deformed, ids) {
1942                (true, false) => &pipelines.deformed,
1943                (false, false) => &pipelines.surface,
1944                (true, true) => &pipelines.deformed_ids,
1945                (false, true) => &pipelines.surface_ids,
1946            });
1947            pass.set_bind_group(0, lit.scene, &[]);
1948            pass.set_bind_group(1, lit.shadow, &[]);
1949            pass.set_bind_group(2, &bind, &[]);
1950            pass.set_bind_group(3, lit.lighting, &[]);
1951        } else {
1952            pass.set_pipeline(match (deformed, surface, ids) {
1953                (true, _, false) => &self.deformed,
1954                (false, true, false) => &self.surface,
1955                (false, false, false) => &self.overlay,
1956                (true, _, true) => &self.deformed_ids,
1957                (false, true, true) => &self.surface_ids,
1958                (false, false, true) => &self.overlay_ids,
1959            });
1960            pass.set_bind_group(0, &bind, &[]);
1961            if let Some(group) = deformers.filter(|_| deformed) {
1962                pass.set_bind_group(1, &self.empty_group, &[]);
1963                pass.set_bind_group(2, group, &[]);
1964            }
1965        }
1966        pass.set_vertex_buffer(0, instances.slice(..));
1967        let vertices = if deformed {
1968            6 * DEFORMED_CELLS * DEFORMED_CELLS
1969        } else {
1970            6
1971        };
1972        pass.draw(0..vertices, 0..packed.len() as u32);
1973        Ok(())
1974    }
1975}
1976
1977struct Packed<'a> {
1978    target: &'a wgpu::TextureView,
1979    depth: Option<&'a wgpu::TextureView>,
1980    atlas: &'a GpuAtlas,
1981    space: TextSpace,
1982    deformers: Option<&'a wgpu::BindGroup>,
1983    lit: Option<&'a LitBindings<'a>>,
1984    mode: u32,
1985    id: Option<u32>,
1986    glyphs: &'a [PackedGlyph],
1987}
1988
1989fn turned(angle: f32, pivot: [f32; 2], point: [f32; 2]) -> [f32; 2] {
1990    let (sin, cos) = angle.sin_cos();
1991    let offset = [point[0] - pivot[0], point[1] - pivot[1]];
1992    [
1993        pivot[0] + cos * offset[0] - sin * offset[1],
1994        pivot[1] + sin * offset[0] + cos * offset[1],
1995    ]
1996}
1997
1998pub fn screen_bounds(
1999    space: &TextSpace,
2000    glyphs: impl IntoIterator<Item = PackedGlyph>,
2001    size: [u32; 2],
2002) -> Option<[f32; 4]> {
2003    let (rest, lift, clip_from_rest) = match *space {
2004        TextSpace::Deformed {
2005            view_projection,
2006            model,
2007            layout,
2008            normal,
2009            lift,
2010            ..
2011        }
2012        | TextSpace::LitDeformed {
2013            view_projection,
2014            model,
2015            layout,
2016            normal,
2017            lift,
2018            ..
2019        } => (
2020            layout,
2021            normal.map(|n| n * lift),
2022            crate::frame::multiply(view_projection, model),
2023        ),
2024        _ => (
2025            space.matrix(),
2026            [0.0; 3],
2027            [
2028                [1.0, 0.0, 0.0, 0.0],
2029                [0.0, 1.0, 0.0, 0.0],
2030                [0.0, 0.0, 1.0, 0.0],
2031                [0.0, 0.0, 0.0, 1.0],
2032            ],
2033        ),
2034    };
2035    let width = size[0] as f32;
2036    let height = size[1] as f32;
2037    let mut low = [f32::INFINITY; 2];
2038    let mut high = [f32::NEG_INFINITY; 2];
2039    for glyph in glyphs {
2040        let [x, y, angle, scale] = glyph.pose;
2041        let mut box_low = [f32::INFINITY; 2];
2042        let mut box_high = [f32::NEG_INFINITY; 2];
2043        for corner in [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]] {
2044            let local = [
2045                (glyph.rect[0] + corner[0] * glyph.rect[2]) * scale,
2046                (glyph.rect[1] + corner[1] * glyph.rect[3]) * scale,
2047            ];
2048            let point = turned(angle, [0.0, 0.0], local);
2049            for (axis, offset) in [x, y].into_iter().enumerate() {
2050                box_low[axis] = box_low[axis].min(point[axis] + offset);
2051                box_high[axis] = box_high[axis].max(point[axis] + offset);
2052            }
2053        }
2054        let left = box_low[0].max(glyph.clip[0]);
2055        let top = box_low[1].max(glyph.clip[1]);
2056        let right = box_high[0].min(glyph.clip[2]);
2057        let bottom = box_high[1].min(glyph.clip[3]);
2058        if !(left < right && top < bottom) {
2059            continue;
2060        }
2061        for point in [[left, top], [right, top], [left, bottom], [right, bottom]] {
2062            let [u, v] = turned(glyph.turn[2], [glyph.turn[0], glyph.turn[1]], point);
2063            let placed = crate::frame::transform(rest, [u, v, 0.0, 1.0]);
2064            let clip = crate::frame::transform(
2065                clip_from_rest,
2066                [
2067                    placed[0] + lift[0],
2068                    placed[1] + lift[1],
2069                    placed[2] + lift[2],
2070                    placed[3],
2071                ],
2072            );
2073            if clip[3] <= 1e-6 {
2074                continue;
2075            }
2076            let pixel = [
2077                (clip[0] / clip[3] * 0.5 + 0.5) * width,
2078                (0.5 - clip[1] / clip[3] * 0.5) * height,
2079            ];
2080            for axis in 0..2 {
2081                low[axis] = low[axis].min(pixel[axis]);
2082                high[axis] = high[axis].max(pixel[axis]);
2083            }
2084        }
2085    }
2086    let rect = [
2087        low[0].max(0.0),
2088        low[1].max(0.0),
2089        high[0].min(width),
2090        high[1].min(height),
2091    ];
2092    (rect[0] < rect[2] && rect[1] < rect[3]).then_some(rect)
2093}
2094
2095fn glyph_source() -> String {
2096    format!("const MSDF_SPREAD: f32 = {MSDF_SPREAD:?};\n{GLYPH_WGSL}")
2097}
2098
2099pub fn shader_source() -> String {
2100    format!(
2101        "{}\n{}\n{}\n{}",
2102        MSDF_WGSL,
2103        crate::frame::deform_source(),
2104        glyph_source(),
2105        SHADER
2106    )
2107}
2108
2109pub fn lit_shader_source() -> String {
2110    lit_shader_source_with(&Gobo::default())
2111}
2112
2113pub fn lit_shader_source_with(gobo: &Gobo) -> String {
2114    format!(
2115        "{}\n{}\n{}\n{}",
2116        MSDF_WGSL,
2117        crate::frame::shader_source_with(gobo),
2118        glyph_source(),
2119        LIT_SHADER
2120    )
2121}
2122
2123pub fn representation(atlas: &Atlas) -> Result<Representation, AtlasError> {
2124    match atlas.channels {
2125        1 => Ok(Representation::Coverage),
2126        3 => Ok(Representation::Msdf),
2127        _ => Err(AtlasError::InvalidChannels),
2128    }
2129}
2130
2131pub fn text_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
2132    vec![
2133        wgpu::BindGroupLayoutEntry {
2134            binding: 0,
2135            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
2136            ty: wgpu::BindingType::Buffer {
2137                ty: wgpu::BufferBindingType::Uniform,
2138                has_dynamic_offset: false,
2139                min_binding_size: None,
2140            },
2141            count: None,
2142        },
2143        wgpu::BindGroupLayoutEntry {
2144            binding: 1,
2145            visibility: wgpu::ShaderStages::FRAGMENT,
2146            ty: wgpu::BindingType::Texture {
2147                sample_type: wgpu::TextureSampleType::Float { filterable: true },
2148                view_dimension: wgpu::TextureViewDimension::D2,
2149                multisampled: false,
2150            },
2151            count: None,
2152        },
2153        wgpu::BindGroupLayoutEntry {
2154            binding: 2,
2155            visibility: wgpu::ShaderStages::FRAGMENT,
2156            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
2157            count: None,
2158        },
2159    ]
2160}
2161
2162pub fn deformer_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
2163    vec![
2164        wgpu::BindGroupLayoutEntry {
2165            binding: 0,
2166            visibility: wgpu::ShaderStages::VERTEX,
2167            ty: wgpu::BindingType::Buffer {
2168                ty: wgpu::BufferBindingType::Storage { read_only: true },
2169                has_dynamic_offset: false,
2170                min_binding_size: None,
2171            },
2172            count: None,
2173        },
2174        wgpu::BindGroupLayoutEntry {
2175            binding: 1,
2176            visibility: wgpu::ShaderStages::VERTEX,
2177            ty: wgpu::BindingType::Buffer {
2178                ty: wgpu::BufferBindingType::Storage { read_only: true },
2179                has_dynamic_offset: false,
2180                min_binding_size: None,
2181            },
2182            count: None,
2183        },
2184    ]
2185}
2186
2187#[cfg(test)]
2188mod tests {
2189    use super::*;
2190    use pfx_text::{Anchor, Baseline, MipLevel, Style, TextEngine};
2191
2192    const NUMBERS: &str = "0123456789,.e+-Na";
2193
2194    const FONT: &[u8] = pfx_text::fixture::EB_GARAMOND;
2195
2196    const LEGACY_SHADER: &str = r#"
2197struct Transform {
2198    matrix: mat4x4f,
2199    view_projection: mat4x4f,
2200    model: mat4x4f,
2201    rest_normal: vec4f,
2202    atlas_size: vec2f,
2203    mode: u32,
2204    deformer: u32,
2205    cells: vec4u,
2206}
2207@group(0) @binding(0) var<uniform> transform: Transform;
2208@group(0) @binding(1) var atlas: texture_2d<f32>;
2209@group(0) @binding(2) var atlas_sampler: sampler;
2210
2211struct Instance {
2212    @location(0) pose: vec4f,
2213    @location(1) rect: vec4f,
2214    @location(2) uv_rect: vec4f,
2215    @location(3) color: vec4f,
2216    @location(4) clip_rect: vec4f,
2217    @location(5) turn: vec4f,
2218}
2219struct VertexOut {
2220    @builtin(position) clip: vec4f,
2221    @location(0) uv: vec2f,
2222    @location(1) @interpolate(flat) bounds: vec4f,
2223    @location(2) @interpolate(flat) color: vec4f,
2224    @location(3) @interpolate(flat) clip_rect: vec4f,
2225    @location(4) point: vec2f,
2226}
2227
2228@vertex
2229fn vs_main(instance: Instance, @builtin(vertex_index) index: u32) -> VertexOut {
2230    var corners = array<vec2f, 6>(
2231        vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0),
2232        vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0)
2233    );
2234    let corner = corners[index];
2235    let local = (instance.rect.xy + corner * instance.rect.zw) * instance.pose.w;
2236    let c = cos(instance.pose.z);
2237    let s = sin(instance.pose.z);
2238    let point = instance.pose.xy + vec2f(c * local.x - s * local.y, s * local.x + c * local.y);
2239    let pivot = instance.turn.xy;
2240    let ct = cos(instance.turn.z);
2241    let st = sin(instance.turn.z);
2242    let offset = point - pivot;
2243    let turned = pivot + vec2f(ct * offset.x - st * offset.y, st * offset.x + ct * offset.y);
2244    var out: VertexOut;
2245    out.clip = transform.matrix * vec4f(turned, 0.0, 1.0);
2246    out.uv = mix(instance.uv_rect.xy, instance.uv_rect.zw, corner);
2247    out.bounds = instance.uv_rect;
2248    out.color = instance.color;
2249    out.clip_rect = instance.clip_rect;
2250    out.point = point;
2251    return out;
2252}
2253
2254@vertex
2255fn vs_deformed(instance: Instance, @builtin(vertex_index) index: u32) -> VertexOut {
2256    var corners = array<vec2f, 6>(
2257        vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0),
2258        vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0)
2259    );
2260    let cells = max(transform.cells.x, 1u);
2261    let cell = index / 6u;
2262    let corner = (vec2f(f32(cell % cells), f32(cell / cells)) + corners[index % 6u]) / f32(cells);
2263    let local = (instance.rect.xy + corner * instance.rect.zw) * instance.pose.w;
2264    let c = cos(instance.pose.z);
2265    let s = sin(instance.pose.z);
2266    let point = instance.pose.xy + vec2f(c * local.x - s * local.y, s * local.x + c * local.y);
2267    let pivot = instance.turn.xy;
2268    let ct = cos(instance.turn.z);
2269    let st = sin(instance.turn.z);
2270    let offset = point - pivot;
2271    let turned = pivot + vec2f(ct * offset.x - st * offset.y, st * offset.x + ct * offset.y);
2272    let rest = (transform.matrix * vec4f(turned, 0.0, 1.0)).xyz;
2273    let shape = deform_vertex(rest, transform.rest_normal.xyz, vec4f(1.0, 0.0, 0.0, 1.0), transform.deformer);
2274    let lifted = shape.position + normalize(shape.normal) * transform.rest_normal.w;
2275    var out: VertexOut;
2276    out.clip = transform.view_projection * transform.model * vec4f(lifted, 1.0);
2277    out.uv = mix(instance.uv_rect.xy, instance.uv_rect.zw, corner);
2278    out.bounds = instance.uv_rect;
2279    out.color = instance.color;
2280    out.clip_rect = instance.clip_rect;
2281    out.point = point;
2282    return out;
2283}
2284
2285@fragment
2286fn fs_main(in: VertexOut) -> @location(0) vec4f {
2287    let dx = dpdx(in.uv);
2288    let dy = dpdy(in.uv);
2289    let footprint = max(length(dx * transform.atlas_size), length(dy * transform.atlas_size));
2290    let extent = (in.bounds.zw - in.bounds.xy) * transform.atlas_size;
2291    let safe_lod = floor(log2(max(min(extent.x, extent.y), 1.0)));
2292    let max_lod = select(min(safe_lod, 3.0), 3.0, transform.mode == 2u);
2293    let lod = clamp(log2(max(footprint, 1.0)), 0.0, max_lod);
2294    let level = select(lod, 0.0, transform.mode == 1u);
2295    let half_texel = vec2f(exp2(level) * 0.5) / transform.atlas_size;
2296    let low = min(in.bounds.xy + half_texel, in.bounds.zw);
2297    let high = max(in.bounds.zw - half_texel, low);
2298    let uv = clamp(in.uv, low, high);
2299    let sampled = textureSampleLevel(atlas, atlas_sampler, uv, level);
2300    var alpha = sampled.r;
2301    if (transform.mode == 1u) {
2302        let sampled = textureSampleLevel(atlas, atlas_sampler, uv, 0.0);
2303        let signed_distance = msdf_median(sampled.rgb) - 0.5;
2304        alpha = clamp(0.5 + signed_distance / max(fwidth(signed_distance), 1e-5), 0.0, 1.0);
2305    }
2306    let inside = step(in.clip_rect.x, in.point.x) * step(in.point.x, in.clip_rect.z)
2307        * step(in.clip_rect.y, in.point.y) * step(in.point.y, in.clip_rect.w);
2308    if (transform.mode == 2u) {
2309        let opacity = sampled.a * in.color.a * inside;
2310        return vec4f(sampled.rgb * in.color.rgb * in.color.a * inside, opacity);
2311    }
2312    let opacity = alpha * in.color.a * inside;
2313    return vec4f(in.color.rgb * opacity, opacity);
2314}
2315"#;
2316
2317    #[test]
2318    fn rich_glyph_conversion_keeps_layout_and_premultiplied_alpha() {
2319        let source = pfx_text::RichGlyphQuad {
2320            key: 7,
2321            atlas_page: pfx_text::AtlasPage::Monochrome,
2322            is_color: false,
2323            rect: [1.0, 2.0, 3.0, 4.0],
2324            uv: [0.1, 0.2, 0.3, 0.4],
2325            origin: [5.0, 6.0],
2326            alpha: 0.4,
2327            clip: [7.0, 8.0, 9.0, 10.0],
2328            turn: [11.0, 12.0, 0.3],
2329            color: [0.2, 0.4, 0.6, 0.32],
2330            line_index: 0,
2331            line_baseline: 6.0,
2332            pen: [5.0, 6.0],
2333        };
2334        let quad = RichQuad::from(&source);
2335        assert_eq!(quad.rect, source.rect);
2336        assert_eq!(quad.uv, source.uv);
2337        assert_eq!(quad.origin, source.origin);
2338        assert_eq!(quad.color, source.color);
2339        assert_eq!(quad.alpha, 1.0);
2340        assert_eq!(quad.clip, source.clip);
2341        assert_eq!(quad.turn, source.turn);
2342        assert_eq!(PackedGlyph::from(&quad).color[3], 0.32);
2343    }
2344
2345    fn paragraph(size: f32) -> Paragraph {
2346        let mut engine = TextEngine::new(FONT).unwrap();
2347        engine
2348            .layout(
2349                "Paper",
2350                Style {
2351                    family: "EB Garamond",
2352                    size,
2353                    line_height: size * 1.2,
2354                    wrap_width: None,
2355                    pixels_per_unit: 1.0,
2356                    representation: Representation::Msdf,
2357                },
2358                &Baseline::Straight {
2359                    origin: [40.0, 650.0],
2360                    direction: [1.0, 0.0],
2361                },
2362            )
2363            .unwrap()
2364    }
2365
2366    #[test]
2367    fn instance_packing_preserves_layout() {
2368        let paragraph = paragraph(48.0);
2369        let packed = pack_glyphs(&paragraph.glyphs);
2370        assert_eq!(packed.len(), 5);
2371        assert_eq!(std::mem::size_of::<PackedGlyph>(), 96);
2372        for (source, output) in paragraph.glyphs.iter().zip(packed) {
2373            assert_eq!(
2374                output.pose,
2375                [
2376                    source.position[0],
2377                    source.position[1],
2378                    source.rotation,
2379                    source.scale
2380                ]
2381            );
2382            assert_eq!(output.rect, source.local_rect);
2383            assert_eq!(output.uv_rect, source.uv);
2384        }
2385    }
2386
2387    #[test]
2388    fn atlas_upload_sizes_mips_and_dirty_rows() {
2389        let mut atlas = Atlas {
2390            channels: 3,
2391            levels: vec![
2392                MipLevel {
2393                    width: 4,
2394                    height: 4,
2395                    bytes: vec![128; 48],
2396                },
2397                MipLevel {
2398                    width: 2,
2399                    height: 2,
2400                    bytes: vec![128; 12],
2401                },
2402                MipLevel {
2403                    width: 1,
2404                    height: 1,
2405                    bytes: vec![128; 3],
2406                },
2407            ],
2408        };
2409        let initial = converted_levels(&atlas).unwrap();
2410        assert_eq!(
2411            initial.iter().map(Vec::len).collect::<Vec<_>>(),
2412            vec![64, 16, 4]
2413        );
2414        assert_eq!(&initial[0][..4], &[128, 128, 128, 255]);
2415        assert_eq!(
2416            changed_regions(&atlas, &initial, None),
2417            vec![
2418                UploadRegion {
2419                    level: 0,
2420                    first_row: 0,
2421                    rows: 4,
2422                    first_column: 0,
2423                    columns: 4,
2424                },
2425                UploadRegion {
2426                    level: 1,
2427                    first_row: 0,
2428                    rows: 2,
2429                    first_column: 0,
2430                    columns: 2,
2431                },
2432                UploadRegion {
2433                    level: 2,
2434                    first_row: 0,
2435                    rows: 1,
2436                    first_column: 0,
2437                    columns: 1,
2438                },
2439            ]
2440        );
2441        atlas.levels[0].bytes[13] = 129;
2442        atlas.levels[1].bytes[7] = 129;
2443        let updated = converted_levels(&atlas).unwrap();
2444        assert_eq!(
2445            changed_regions(&atlas, &updated, Some(&initial)),
2446            vec![
2447                UploadRegion {
2448                    level: 0,
2449                    first_row: 1,
2450                    rows: 1,
2451                    first_column: 0,
2452                    columns: 4,
2453                },
2454                UploadRegion {
2455                    level: 1,
2456                    first_row: 1,
2457                    rows: 1,
2458                    first_column: 0,
2459                    columns: 2,
2460                },
2461            ]
2462        );
2463        let coverage = Atlas {
2464            channels: 1,
2465            levels: vec![
2466                MipLevel {
2467                    width: 2,
2468                    height: 2,
2469                    bytes: vec![0, 1, 2, 3],
2470                },
2471                MipLevel {
2472                    width: 1,
2473                    height: 1,
2474                    bytes: vec![1],
2475                },
2476            ],
2477        };
2478        assert_eq!(format_for(&coverage), Ok(wgpu::TextureFormat::R8Unorm));
2479        assert_eq!(converted_levels(&coverage).unwrap()[0].len(), 4);
2480    }
2481
2482    #[test]
2483    fn icon_mips_keep_neighbours_separate() {
2484        let mut cache = Cache::new();
2485        let red = [65535u16, 0, 0, 65535];
2486        let blue = [0u16, 0, 65535, 65535];
2487        let raw = |pixel: [u16; 4]| {
2488            (0..16)
2489                .flat_map(|_| pixel.into_iter().flat_map(u16::to_le_bytes))
2490                .collect::<Vec<_>>()
2491        };
2492        let a = cache
2493            .load_rgba16(&raw(red), 4, 4, ColorSpace::Linear)
2494            .unwrap();
2495        let b = cache
2496            .load_rgba16(&raw(blue), 4, 4, ColorSpace::Linear)
2497            .unwrap();
2498        let atlas = IconAtlas::from_cache(&cache, &[a, b]).unwrap();
2499        assert_eq!(atlas.levels.len(), 4);
2500        for (index, level) in atlas.levels.iter().enumerate() {
2501            for (handle, expected) in [(a, red), (b, blue)] {
2502                let uv = atlas.uv(handle).unwrap();
2503                let half = 2f32.powi(index as i32) * 0.5 / atlas.levels[0].width as f32;
2504                let low_x = (uv[0] + half).min(uv[2]);
2505                let high_x = (uv[2] - half).max(low_x);
2506                let half_y = 2f32.powi(index as i32) * 0.5 / atlas.levels[0].height as f32;
2507                let low_y = (uv[1] + half_y).min(uv[3]);
2508                let high_y = (uv[3] - half_y).max(low_y);
2509                for u in [low_x, (low_x + high_x) * 0.5, high_x] {
2510                    for v in [low_y, (low_y + high_y) * 0.5, high_y] {
2511                        let x = (u * level.width as f32 - 0.5).floor() as i32;
2512                        let y = (v * level.height as f32 - 0.5).floor() as i32;
2513                        for dy in 0..2 {
2514                            for dx in 0..2 {
2515                                let sx = (x + dx).clamp(0, level.width as i32 - 1) as usize;
2516                                let sy = (y + dy).clamp(0, level.height as i32 - 1) as usize;
2517                                let at = (sy * level.width as usize + sx) * 4;
2518                                assert_eq!(&level.pixels[at..at + 4], &expected, "mip {index}");
2519                            }
2520                        }
2521                    }
2522                }
2523            }
2524        }
2525    }
2526
2527    #[test]
2528    fn glyph_mip_samples_stay_in_the_padded_cell() {
2529        let mut levels = Vec::new();
2530        for index in 0..4 {
2531            let width = 64u32 >> index;
2532            let height = 32u32 >> index;
2533            let mut bytes = vec![0u8; (width * height) as usize];
2534            let cell = 24u32 >> index;
2535            for y in 0..cell {
2536                for x in 0..cell {
2537                    bytes[(y * width + x) as usize] = 80;
2538                    bytes[(y * width + x + cell) as usize] = 200;
2539                }
2540            }
2541            levels.push(MipLevel {
2542                width,
2543                height,
2544                bytes,
2545            });
2546        }
2547        let atlas = Atlas {
2548            channels: 1,
2549            levels,
2550        };
2551        assert_eq!(converted_levels(&atlas).unwrap().len(), 4);
2552        for (index, level) in atlas.levels.iter().enumerate() {
2553            for (rect_x, expected) in [(8u32, 80u8), (32u32, 200u8)] {
2554                let low = (rect_x as f32 + 2f32.powi(index as i32) * 0.5) / 64.0;
2555                let high = ((rect_x + 8) as f32 - 2f32.powi(index as i32) * 0.5) / 64.0;
2556                let u = (low + high) * 0.5;
2557                let v = 12.0 / 32.0;
2558                let x = (u * level.width as f32 - 0.5).floor() as usize;
2559                let y = (v * level.height as f32 - 0.5).floor() as usize;
2560                for dy in 0..2 {
2561                    for dx in 0..2 {
2562                        assert_eq!(
2563                            level.bytes[(y + dy) * level.width as usize + x + dx],
2564                            expected,
2565                            "mip {index}"
2566                        );
2567                    }
2568                }
2569            }
2570        }
2571    }
2572
2573    #[test]
2574    fn clip_and_rotation_keep_the_unturned_clip_space() {
2575        let quad = RichQuad {
2576            rect: [10.0, 20.0, 8.0, 6.0],
2577            uv: [0.0, 0.0, 0.5, 0.5],
2578            origin: [10.0, 20.0],
2579            alpha: 0.5,
2580            clip: [12.0, 20.0, 18.0, 26.0],
2581            turn: [10.0, 20.0, std::f32::consts::FRAC_PI_2],
2582            color: [0.25, 0.5, 0.75, 0.8],
2583        };
2584        let packed = PackedGlyph::from(&quad);
2585        assert!((packed.color[3] - 0.4).abs() < 1e-6);
2586        let point = [quad.rect[0] + 4.0, quad.rect[1] + 3.0];
2587        let inside = point[0] >= quad.clip[0]
2588            && point[0] <= quad.clip[2]
2589            && point[1] >= quad.clip[1]
2590            && point[1] <= quad.clip[3];
2591        assert!(inside);
2592        let offset = [point[0] - quad.turn[0], point[1] - quad.turn[1]];
2593        let turned = [quad.turn[0] - offset[1], quad.turn[1] + offset[0]];
2594        assert_eq!(turned, [7.0, 24.0]);
2595        assert!(turned[0] < quad.clip[0]);
2596    }
2597
2598    #[test]
2599    fn assembled_wgsl_parses_and_validates() {
2600        let source = shader_source();
2601        let module = naga::front::wgsl::parse_str(&source).unwrap();
2602        naga::valid::Validator::new(
2603            naga::valid::ValidationFlags::all(),
2604            naga::valid::Capabilities::all(),
2605        )
2606        .validate(&module)
2607        .unwrap();
2608    }
2609
2610    #[test]
2611    fn lit_wgsl_validates_and_binds_inside_the_frame_layouts() {
2612        let source = lit_shader_source();
2613        let module = naga::front::wgsl::parse_str(&source).unwrap();
2614        let info = naga::valid::Validator::new(
2615            naga::valid::ValidationFlags::all(),
2616            naga::valid::Capabilities::all(),
2617        )
2618        .validate(&module)
2619        .unwrap();
2620        let layouts = [
2621            crate::frame::scene_entries(),
2622            crate::frame::shadow_entries(),
2623            lit_entries(),
2624            crate::frame::lighting_entries(),
2625        ];
2626        let mut used = 0;
2627        for name in ["vs_lit_surface", "vs_lit_deformed", "fs_lit"] {
2628            let index = module
2629                .entry_points
2630                .iter()
2631                .position(|entry| entry.name == name)
2632                .unwrap();
2633            let stage = match module.entry_points[index].stage {
2634                naga::ShaderStage::Vertex => wgpu::ShaderStages::VERTEX,
2635                _ => wgpu::ShaderStages::FRAGMENT,
2636            };
2637            let function = info.get_entry_point(index);
2638            for (handle, variable) in module.global_variables.iter() {
2639                let Some(binding) = &variable.binding else {
2640                    continue;
2641                };
2642                if function[handle].is_empty() {
2643                    continue;
2644                }
2645                let entry = layouts[binding.group as usize]
2646                    .iter()
2647                    .find(|entry| entry.binding == binding.binding)
2648                    .unwrap_or_else(|| {
2649                        panic!(
2650                            "{name} uses @group({}) @binding({}) missing from the layout",
2651                            binding.group, binding.binding
2652                        )
2653                    });
2654                assert!(
2655                    entry.visibility.contains(stage),
2656                    "{name} @group({}) @binding({}) is not visible to its stage",
2657                    binding.group,
2658                    binding.binding
2659                );
2660                used += 1;
2661            }
2662        }
2663        assert!(used >= 20, "{used} lit text bindings checked");
2664    }
2665
2666    fn project(matrix: [[f32; 4]; 4], point: [f32; 3]) -> [f32; 4] {
2667        let mut out = [0.0; 4];
2668        for (row, value) in out.iter_mut().enumerate() {
2669            *value = (0..3).map(|col| matrix[col][row] * point[col]).sum::<f32>() + matrix[3][row];
2670        }
2671        out
2672    }
2673
2674    fn look_at(eye: [f32; 3], target: [f32; 3], up: [f32; 3]) -> [[f32; 4]; 4] {
2675        let sub = |a: [f32; 3], b: [f32; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
2676        let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
2677        let cross = |a: [f32; 3], b: [f32; 3]| {
2678            [
2679                a[1] * b[2] - a[2] * b[1],
2680                a[2] * b[0] - a[0] * b[2],
2681                a[0] * b[1] - a[1] * b[0],
2682            ]
2683        };
2684        let unit = |a: [f32; 3]| {
2685            let length = dot(a, a).sqrt();
2686            [a[0] / length, a[1] / length, a[2] / length]
2687        };
2688        let forward = unit(sub(target, eye));
2689        let right = unit(cross(forward, up));
2690        let above = cross(right, forward);
2691        [
2692            [right[0], above[0], -forward[0], 0.0],
2693            [right[1], above[1], -forward[1], 0.0],
2694            [right[2], above[2], -forward[2], 0.0],
2695            [-dot(right, eye), -dot(above, eye), dot(forward, eye), 1.0],
2696        ]
2697    }
2698
2699    fn perspective(aspect: f32) -> [[f32; 4]; 4] {
2700        let near = 0.1;
2701        let far = 100.0;
2702        let f = 1.0 / (40.0_f32.to_radians() * 0.5).tan();
2703        [
2704            [f / aspect, 0.0, 0.0, 0.0],
2705            [0.0, f, 0.0, 0.0],
2706            [0.0, 0.0, far / (near - far), -1.0],
2707            [0.0, 0.0, far * near / (near - far), 0.0],
2708        ]
2709    }
2710
2711    fn solid_atlas() -> Atlas {
2712        let level = |size: u32| MipLevel {
2713            width: size,
2714            height: size,
2715            bytes: vec![255; (size * size) as usize],
2716        };
2717        Atlas {
2718            channels: 1,
2719            levels: vec![level(8), level(4), level(2), level(1)],
2720        }
2721    }
2722
2723    fn solid_quad(center: [f32; 2], half: f32, color: [f32; 4]) -> RichQuad {
2724        RichQuad {
2725            rect: [center[0] - half, center[1] - half, 2.0 * half, 2.0 * half],
2726            uv: [0.0, 0.0, 1.0, 1.0],
2727            origin: center,
2728            alpha: 1.0,
2729            clip: [-1000.0, -1000.0, 1000.0, 1000.0],
2730            turn: [0.0; 3],
2731            color,
2732        }
2733    }
2734
2735    #[test]
2736    fn screen_bounds_follow_the_space_clip_and_turn() {
2737        let space = TextSpace::Overlay {
2738            width: 200,
2739            height: 100,
2740        };
2741        let quad = solid_quad([50.0, 20.0], 10.0, INK);
2742        let bounds = |quads: &[RichQuad], space: &TextSpace| {
2743            screen_bounds(space, quads.iter().map(PackedGlyph::from), [400, 200])
2744        };
2745        let close = |rect: Option<[f32; 4]>, want: [f32; 4]| {
2746            rect.is_some_and(|rect| {
2747                rect.iter()
2748                    .zip(want)
2749                    .all(|(got, want)| (got - want).abs() < 1e-3)
2750            })
2751        };
2752        assert!(close(bounds(&[quad], &space), [80.0, 20.0, 120.0, 60.0]));
2753        let clipped = RichQuad {
2754            clip: [45.0, -1000.0, 1000.0, 1000.0],
2755            ..quad
2756        };
2757        assert!(close(bounds(&[clipped], &space), [90.0, 20.0, 120.0, 60.0]));
2758        let hidden = RichQuad {
2759            clip: [100.0, -1000.0, 1000.0, 1000.0],
2760            ..quad
2761        };
2762        assert_eq!(bounds(&[hidden], &space), None);
2763        let turned = RichQuad {
2764            turn: [50.0, 20.0, std::f32::consts::FRAC_PI_4],
2765            ..quad
2766        };
2767        let rect = bounds(&[turned], &space).unwrap();
2768        let half = 10.0 * std::f32::consts::SQRT_2 * 2.0;
2769        for (got, want) in rect
2770            .iter()
2771            .zip([100.0 - half, 40.0 - half, 100.0 + half, 40.0 + half])
2772        {
2773            assert!((got - want).abs() < 1e-3, "{rect:?}");
2774        }
2775        let far = solid_quad([500.0, 20.0], 10.0, INK);
2776        assert!(close(
2777            bounds(&[quad, far], &space),
2778            [80.0, 20.0, 400.0, 60.0]
2779        ));
2780        let surface = TextSpace::Surface {
2781            model_view_projection: [
2782                [0.01, 0.0, 0.0, 0.0],
2783                [0.0, -0.01, 0.0, 0.0],
2784                [0.0, 0.0, 1.0, 0.0],
2785                [0.0, 0.0, 0.0, 1.0],
2786            ],
2787        };
2788        assert!(close(
2789            bounds(&[quad], &surface),
2790            [280.0, 110.0, 320.0, 130.0]
2791        ));
2792        let behind = TextSpace::Surface {
2793            model_view_projection: [
2794                [0.01, 0.0, 0.0, 0.0],
2795                [0.0, -0.01, 0.0, 0.0],
2796                [0.0, 0.0, 1.0, 0.0],
2797                [0.0, 0.0, 0.0, -1.0],
2798            ],
2799        };
2800        assert_eq!(bounds(&[quad], &behind), None);
2801    }
2802
2803    const LAB: u32 = 256;
2804    const INK: [f32; 4] = [0.8, 0.6, 0.4, 1.0];
2805
2806    struct Lab {
2807        size: [u32; 2],
2808        renderer: crate::renderer::Renderer,
2809        output: pfx_gpu::OffscreenTarget,
2810        atlas: GpuAtlas,
2811        view: [[f32; 4]; 4],
2812        projection: [[f32; 4]; 4],
2813        eye: [f32; 3],
2814        meshes: Vec<crate::frame::MeshHandle>,
2815        text_id: u32,
2816    }
2817
2818    impl Lab {
2819        fn new(eye: [f32; 3], target: [f32; 3], up: [f32; 3]) -> Self {
2820            Self::sized([LAB, LAB], eye, target, up)
2821        }
2822
2823        fn sized(size: [u32; 2], eye: [f32; 3], target: [f32; 3], up: [f32; 3]) -> Self {
2824            let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
2825            let mut renderer = crate::renderer::Renderer::new(gpu, size[0], size[1]).unwrap();
2826            renderer.set_text_after_taa(false);
2827            renderer.set_shadow_quality(crate::shadow::Quality {
2828                resolution: 1024,
2829                receiver: Some(crate::shadow::ReceiverBox {
2830                    min: [-3.0, -0.1, -3.0],
2831                    max: [3.0, 1.5, 3.0],
2832                }),
2833                caster_margin: 4.0,
2834                ..crate::shadow::Quality::default()
2835            });
2836            let format = renderer.output_format();
2837            let texture = renderer
2838                .gpu()
2839                .device
2840                .create_texture(&wgpu::TextureDescriptor {
2841                    label: Some("lit text lab output"),
2842                    size: wgpu::Extent3d {
2843                        width: size[0],
2844                        height: size[1],
2845                        depth_or_array_layers: 1,
2846                    },
2847                    mip_level_count: 1,
2848                    sample_count: 1,
2849                    dimension: wgpu::TextureDimension::D2,
2850                    format,
2851                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT
2852                        | wgpu::TextureUsages::TEXTURE_BINDING
2853                        | wgpu::TextureUsages::STORAGE_BINDING
2854                        | wgpu::TextureUsages::COPY_SRC,
2855                    view_formats: &[],
2856                });
2857            let output = pfx_gpu::OffscreenTarget {
2858                view: texture.create_view(&Default::default()),
2859                texture,
2860                format,
2861                width: size[0],
2862                height: size[1],
2863            };
2864            let atlas = GpuAtlas::new(
2865                &renderer.gpu().device,
2866                &renderer.gpu().queue,
2867                &solid_atlas(),
2868            )
2869            .unwrap();
2870            Self {
2871                size,
2872                renderer,
2873                output,
2874                atlas,
2875                view: look_at(eye, target, up),
2876                projection: perspective(size[0] as f32 / size[1] as f32),
2877                eye,
2878                meshes: Vec::new(),
2879                text_id: 0,
2880            }
2881        }
2882
2883        fn view_projection(&self) -> [[f32; 4]; 4] {
2884            crate::frame::multiply(self.projection, self.view)
2885        }
2886
2887        fn occluder(&mut self, min: [f32; 2], max: [f32; 2], height: f32) {
2888            let positions = [
2889                [min[0], height, min[1]],
2890                [max[0], height, min[1]],
2891                [max[0], height, max[1]],
2892                [min[0], height, max[1]],
2893            ];
2894            let mesh = self
2895                .renderer
2896                .upload_mesh(crate::frame::MeshData {
2897                    positions: &positions,
2898                    normals: &[[0.0, 1.0, 0.0]; 4],
2899                    tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
2900                    uvs: &[[0.0, 0.0]; 4],
2901                    uvs1: None,
2902                    alpha: None,
2903                    indices: &[0, 2, 1, 0, 3, 2],
2904                })
2905                .unwrap();
2906            self.meshes.push(mesh);
2907        }
2908
2909        fn render(
2910            &mut self,
2911            sun_intensity: f32,
2912            sun: [f32; 3],
2913            deformers: &[crate::deform::Deformer],
2914            quads: &[RichQuad],
2915            space: TextSpace,
2916            lights: &[crate::lights::LocalLight],
2917        ) -> Vec<f32> {
2918            self.render_timed(sun_intensity, sun, deformers, quads, space, lights);
2919            self.renderer
2920                .gpu()
2921                .readback_rgba16(&self.renderer.frame().targets.hdr)
2922                .unwrap()
2923                .into_iter()
2924                .map(|value| half::f16::from_bits(value).to_f32())
2925                .collect()
2926        }
2927
2928        fn render_timed(
2929            &mut self,
2930            sun_intensity: f32,
2931            sun: [f32; 3],
2932            deformers: &[crate::deform::Deformer],
2933            quads: &[RichQuad],
2934            space: TextSpace,
2935            lights: &[crate::lights::LocalLight],
2936        ) -> Vec<pfx_gpu::PassTiming> {
2937            self.renderer.set_lights(lights).unwrap();
2938            let mut model = [
2939                [1.0, 0.0, 0.0, 0.0],
2940                [0.0, 1.0, 0.0, 0.0],
2941                [0.0, 0.0, 1.0, 0.0],
2942                [0.0, 0.0, 0.0, 1.0],
2943            ];
2944            model[3][1] = 0.0;
2945            let instances: Vec<_> = self
2946                .meshes
2947                .iter()
2948                .enumerate()
2949                .map(|(index, mesh)| {
2950                    let mut instance =
2951                        crate::frame::Instance::new(*mesh, model, 0, index as u32 + 1);
2952                    instance.shadow_only = true;
2953                    instance
2954                })
2955                .collect();
2956            let scene = crate::frame::Scene {
2957                camera: crate::frame::Camera {
2958                    view: self.view,
2959                    projection: self.projection,
2960                    previous_view_projection: self.view_projection(),
2961                    position: self.eye,
2962                },
2963                time: 0.0,
2964                seed: 3,
2965                sun: crate::frame::Sun {
2966                    direction: sun,
2967                    colour: [1.0; 3],
2968                    intensity: sun_intensity,
2969                },
2970                instances: &instances,
2971                materials: &[pfx_materials::Material::default()],
2972                deformers,
2973                wind: crate::frame::SceneWind::default(),
2974            };
2975            let text = crate::renderer::Text {
2976                surface_quads: vec![crate::renderer::TextQuadItem {
2977                    atlas: &self.atlas,
2978                    quads,
2979                    space,
2980                    icons: false,
2981                    id: self.text_id,
2982                }],
2983                ..Default::default()
2984            };
2985            self.renderer
2986                .render(
2987                    &scene,
2988                    &text,
2989                    &crate::renderer::Effects::default(),
2990                    crate::renderer::Finish::Standard,
2991                    &self.output.view,
2992                )
2993                .unwrap()
2994        }
2995
2996        fn at(&self, pixels: &[f32], point: [f32; 3]) -> [f32; 3] {
2997            let clip = project(self.view_projection(), point);
2998            let x = ((clip[0] / clip[3] * 0.5 + 0.5) * self.size[0] as f32) as usize;
2999            let y = ((0.5 - clip[1] / clip[3] * 0.5) * self.size[1] as f32) as usize;
3000            let base = (y * self.size[0] as usize + x) * 4;
3001            [pixels[base], pixels[base + 1], pixels[base + 2]]
3002        }
3003    }
3004
3005    fn floor_model() -> [[f32; 4]; 4] {
3006        [
3007            [1.0, 0.0, 0.0, 0.0],
3008            [0.0, 0.0, 1.0, 0.0],
3009            [0.0, 1.0, 0.0, 0.0],
3010            [0.0, 0.0, 0.0, 1.0],
3011        ]
3012    }
3013
3014    fn ink_space(lab: &Lab, lit: bool) -> TextSpace {
3015        let model = floor_model();
3016        let model_view_projection = crate::frame::multiply(lab.view_projection(), model);
3017        if lit {
3018            TextSpace::LitSurface {
3019                model_view_projection,
3020                model,
3021            }
3022        } else {
3023            TextSpace::Surface {
3024                model_view_projection,
3025            }
3026        }
3027    }
3028
3029    fn near(a: f32, b: f32, tolerance: f32) -> bool {
3030        (a - b).abs() <= tolerance * b.abs().max(a.abs()).max(0.02)
3031    }
3032
3033    #[test]
3034    #[ignore = "needs a GPU; run under pgpu"]
3035    fn lit_glyph_in_the_cascade_shadow_is_darker_by_the_suns_share() {
3036        let mut lab = Lab::new([0.0, 6.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, -1.0]);
3037        lab.occluder([-2.0, -1.0], [0.0, 1.0], 1.0);
3038        let quads = [
3039            solid_quad([-1.0, 0.0], 0.15, INK),
3040            solid_quad([1.0, 0.0], 0.15, INK),
3041        ];
3042        let sun = [0.0, 1.0, 0.0];
3043        let space = ink_space(&lab, true);
3044        let lit = lab.render(3.0, sun, &[], &quads, space, &[]);
3045        let dark = lab.render(0.0, sun, &[], &quads, space, &[]);
3046        let shadowed = lab.at(&lit, [-1.0, 0.0, 0.0]);
3047        let sunlit = lab.at(&lit, [1.0, 0.0, 0.0]);
3048        let ambient = lab.at(&dark, [1.0, 0.0, 0.0]);
3049        for channel in 0..3 {
3050            let sun_share = INK[channel] * 3.0 / std::f32::consts::PI;
3051            assert!(
3052                shadowed[channel] < sunlit[channel],
3053                "channel {channel}: {shadowed:?} against {sunlit:?}"
3054            );
3055            assert!(
3056                near(sunlit[channel] - ambient[channel], sun_share, 0.03),
3057                "channel {channel}: sun share {} against {sun_share}",
3058                sunlit[channel] - ambient[channel]
3059            );
3060            assert!(
3061                near(shadowed[channel], ambient[channel], 0.03),
3062                "channel {channel}: shadowed {} against ambient {}",
3063                shadowed[channel],
3064                ambient[channel]
3065            );
3066        }
3067        let factor = (shadowed[0] - ambient[0]) / (sunlit[0] - ambient[0]);
3068        assert!(factor.abs() < 0.03, "shadow factor {factor}");
3069    }
3070
3071    #[test]
3072    #[ignore = "needs a GPU; run under pgpu"]
3073    fn unlit_text_ignores_sun_shadow_and_night() {
3074        let mut lab = Lab::new([0.0, 6.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, -1.0]);
3075        lab.occluder([-2.0, -1.0], [0.0, 1.0], 1.0);
3076        let quads = [
3077            solid_quad([-1.0, 0.0], 0.15, INK),
3078            solid_quad([1.0, 0.0], 0.15, INK),
3079        ];
3080        let sun = [0.0, 1.0, 0.0];
3081        for intensity in [3.0, 0.0] {
3082            let space = ink_space(&lab, false);
3083            let pixels = lab.render(intensity, sun, &[], &quads, space, &[]);
3084            for point in [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0]] {
3085                let value = lab.at(&pixels, point);
3086                for channel in 0..3 {
3087                    assert!(
3088                        near(value[channel], INK[channel], 0.002),
3089                        "unlit {value:?} at {point:?} under sun {intensity}"
3090                    );
3091                }
3092            }
3093        }
3094    }
3095
3096    #[test]
3097    #[ignore = "needs a GPU; run under pgpu"]
3098    fn lit_glyph_on_a_roll_takes_the_deformed_normal() {
3099        let mut lab = Lab::new([-3.0, 0.5, 4.0], [0.0, 0.4, 0.0], [0.0, 1.0, 0.0]);
3100        let roller = crate::deform::RollerFrame {
3101            at: 0.0,
3102            outer: 0.5,
3103            thickness: 0.0,
3104        };
3105        let deformers = [crate::deform::Deformer::Roller {
3106            current: roller,
3107            previous: roller,
3108        }];
3109        let flat = [-0.7_f32, 0.0];
3110        let curled = [0.5 * std::f32::consts::FRAC_PI_2, 0.0];
3111        let quads = [solid_quad(flat, 0.05, INK), solid_quad(curled, 0.05, INK)];
3112        let layout = [
3113            [1.0, 0.0, 0.0, 0.0],
3114            [0.0, 0.0, 1.0, 0.0],
3115            [0.0, 1.0, 0.0, 0.0],
3116            [0.0, 0.0, 0.0, 1.0],
3117        ];
3118        let identity = [
3119            [1.0, 0.0, 0.0, 0.0],
3120            [0.0, 1.0, 0.0, 0.0],
3121            [0.0, 0.0, 1.0, 0.0],
3122            [0.0, 0.0, 0.0, 1.0],
3123        ];
3124        let space = TextSpace::LitDeformed {
3125            view_projection: lab.view_projection(),
3126            model: identity,
3127            layout,
3128            normal: [0.0, 1.0, 0.0],
3129            lift: 0.0003,
3130            deformer: DeformerId(0),
3131        };
3132        let sun = [-1.0, 0.0, 0.0];
3133        let lit = lab.render(3.0, sun, &deformers, &quads, space, &[]);
3134        let dark = lab.render(0.0, sun, &deformers, &quads, space, &[]);
3135        let flat_point = [flat[0], 0.0003, 0.0];
3136        let curled_point = [0.5, 0.5, 0.0];
3137        let on_flat = lab.at(&lit, flat_point);
3138        let off_flat = lab.at(&dark, flat_point);
3139        let on_curl = lab.at(&lit, curled_point);
3140        let off_curl = lab.at(&dark, curled_point);
3141        for channel in 0..3 {
3142            let sun_share = INK[channel] * 3.0 / std::f32::consts::PI;
3143            assert!(
3144                on_flat[channel] - off_flat[channel] < 0.02 * sun_share,
3145                "the flat glyph's normal is up: {on_flat:?} against {off_flat:?}"
3146            );
3147            assert!(
3148                near(on_curl[channel] - off_curl[channel], sun_share, 0.05),
3149                "the curled glyph faces the sun: {} against {sun_share}",
3150                on_curl[channel] - off_curl[channel]
3151            );
3152        }
3153    }
3154
3155    #[test]
3156    #[ignore = "needs a GPU; run under pgpu"]
3157    fn lit_glyph_takes_a_local_light() {
3158        let mut lab = Lab::new([0.0, 6.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, -1.0]);
3159        let quads = [solid_quad([0.0, 0.0], 0.15, INK)];
3160        let sun = [0.0, 1.0, 0.0];
3161        let space = ink_space(&lab, true);
3162        let bare = lab.render(0.0, sun, &[], &quads, space, &[]);
3163        let lamp = crate::lights::LocalLight {
3164            position: [0.0, 1.0, 0.0],
3165            colour: [1.0; 3],
3166            intensity: 2.0,
3167            radius: 0.01,
3168            range: 4.0,
3169            shadow: false,
3170        };
3171        let lighted = lab.render(0.0, sun, &[], &quads, space, &[lamp]);
3172        let before = lab.at(&bare, [0.0, 0.0, 0.0]);
3173        let after = lab.at(&lighted, [0.0, 0.0, 0.0]);
3174        let window = 1.0 - (1.0_f32 / 4.0).powi(4);
3175        let irradiance = 2.0 * window * window / 1.0;
3176        for channel in 0..3 {
3177            let expected = INK[channel] * irradiance / std::f32::consts::PI;
3178            assert!(
3179                near(after[channel] - before[channel], expected, 0.05),
3180                "channel {channel}: {} against {expected}",
3181                after[channel] - before[channel]
3182            );
3183        }
3184    }
3185
3186    #[test]
3187    #[ignore = "needs a GPU; run under pgpu"]
3188    fn unlit_text_is_byte_identical_to_the_shader_before_lit_text() {
3189        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3190        let paragraph = TextEngine::new(FONT)
3191            .unwrap()
3192            .layout(
3193                "Paper",
3194                Style {
3195                    family: "EB Garamond",
3196                    size: 96.0,
3197                    line_height: 115.0,
3198                    wrap_width: None,
3199                    pixels_per_unit: 1.0,
3200                    representation: Representation::Msdf,
3201                },
3202                &Baseline::Straight {
3203                    origin: [20.0, 160.0],
3204                    direction: [1.0, 0.0],
3205                },
3206            )
3207            .unwrap();
3208        let atlas = GpuAtlas::new(&gpu.device, &gpu.queue, &paragraph.atlas).unwrap();
3209        let current = TextPass::new(&gpu.device);
3210        let mut before = TextPass::new(&gpu.device);
3211        let source = format!(
3212            "{}\n{}\n{}",
3213            MSDF_WGSL,
3214            crate::frame::deform_source(),
3215            LEGACY_SHADER
3216        );
3217        let shader = gpu
3218            .device
3219            .create_shader_module(wgpu::ShaderModuleDescriptor {
3220                label: Some("legacy text shader"),
3221                source: wgpu::ShaderSource::Wgsl(source.into()),
3222            });
3223        let layout = gpu
3224            .device
3225            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3226                label: Some("legacy text layout"),
3227                bind_group_layouts: &[Some(&before.bind_layout)],
3228                immediate_size: 0,
3229            });
3230        before.overlay = text_pipeline(
3231            &gpu.device,
3232            "legacy overlay",
3233            &layout,
3234            &shader,
3235            ("vs_main", "fs_main"),
3236            false,
3237            false,
3238        );
3239        let draw = |pass: &TextPass, color: [f32; 4]| {
3240            let target = gpu.offscreen(512, 256, COLOR_FORMAT).unwrap();
3241            let mut encoder = gpu.device.create_command_encoder(&Default::default());
3242            {
3243                let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3244                    multiview_mask: None,
3245                    label: Some("legacy clear"),
3246                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3247                        view: &target.view,
3248                        depth_slice: None,
3249                        resolve_target: None,
3250                        ops: wgpu::Operations {
3251                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3252                            store: wgpu::StoreOp::Store,
3253                        },
3254                    })],
3255                    depth_stencil_attachment: None,
3256                    timestamp_writes: None,
3257                    occlusion_query_set: None,
3258                });
3259            }
3260            pass.encode(
3261                &gpu.device,
3262                &mut encoder,
3263                TextDraw {
3264                    target: &target.view,
3265                    depth: None,
3266                    atlas: &atlas,
3267                    paragraph: &paragraph,
3268                    space: TextSpace::Overlay {
3269                        width: 512,
3270                        height: 256,
3271                    },
3272                    color,
3273                    deformers: None,
3274                },
3275            )
3276            .unwrap();
3277            gpu.queue.submit(Some(encoder.finish()));
3278            gpu.readback_rgba16(&target).unwrap()
3279        };
3280        let reference = draw(&before, [0.7, 0.2, 0.1, 0.9]);
3281        assert!(reference.iter().any(|value| *value != 0));
3282        assert_eq!(reference, draw(&current, [0.7, 0.2, 0.1, 0.9]));
3283    }
3284
3285    #[test]
3286    #[ignore = "needs a GPU; run under pgpu"]
3287    fn surface_text_seen_nearly_face_on_keeps_the_isotropic_coverage() {
3288        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3289        let size = [512u32, 256];
3290        let paragraph = TextEngine::new(FONT)
3291            .unwrap()
3292            .layout(
3293                "Quartz",
3294                Style {
3295                    family: "EB Garamond",
3296                    size: 96.0,
3297                    line_height: 115.0,
3298                    wrap_width: None,
3299                    pixels_per_unit: 1.0,
3300                    representation: Representation::Msdf,
3301                },
3302                &Baseline::Straight {
3303                    origin: [20.0, 160.0],
3304                    direction: [1.0, 0.0],
3305                },
3306            )
3307            .unwrap();
3308        let atlas = GpuAtlas::new(&gpu.device, &gpu.queue, &paragraph.atlas).unwrap();
3309        let current = TextPass::new(&gpu.device);
3310        let mut before = TextPass::new(&gpu.device);
3311        let source = format!(
3312            "{}\n{}\n{}",
3313            MSDF_WGSL,
3314            crate::frame::deform_source(),
3315            LEGACY_SHADER
3316        );
3317        let shader = gpu
3318            .device
3319            .create_shader_module(wgpu::ShaderModuleDescriptor {
3320                label: Some("legacy text shader"),
3321                source: wgpu::ShaderSource::Wgsl(source.into()),
3322            });
3323        let layout = gpu
3324            .device
3325            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3326                label: Some("legacy text layout"),
3327                bind_group_layouts: &[Some(&before.bind_layout)],
3328                immediate_size: 0,
3329            });
3330        before.surface = text_pipeline(
3331            &gpu.device,
3332            "legacy surface",
3333            &layout,
3334            &shader,
3335            ("vs_main", "fs_main"),
3336            true,
3337            false,
3338        );
3339        let depth = gpu
3340            .device
3341            .create_texture(&wgpu::TextureDescriptor {
3342                label: Some("face-on depth"),
3343                size: wgpu::Extent3d {
3344                    width: size[0],
3345                    height: size[1],
3346                    depth_or_array_layers: 1,
3347                },
3348                mip_level_count: 1,
3349                sample_count: 1,
3350                dimension: wgpu::TextureDimension::D2,
3351                format: DEPTH_FORMAT,
3352                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3353                view_formats: &[],
3354            })
3355            .create_view(&Default::default());
3356        let draw = |pass: &TextPass, model_view_projection: [[f32; 4]; 4]| {
3357            let target = gpu.offscreen(size[0], size[1], COLOR_FORMAT).unwrap();
3358            let mut encoder = gpu.device.create_command_encoder(&Default::default());
3359            {
3360                let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3361                    multiview_mask: None,
3362                    label: Some("face-on clear"),
3363                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3364                        view: &target.view,
3365                        depth_slice: None,
3366                        resolve_target: None,
3367                        ops: wgpu::Operations {
3368                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3369                            store: wgpu::StoreOp::Store,
3370                        },
3371                    })],
3372                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3373                        view: &depth,
3374                        depth_ops: Some(wgpu::Operations {
3375                            load: wgpu::LoadOp::Clear(1.0),
3376                            store: wgpu::StoreOp::Store,
3377                        }),
3378                        stencil_ops: None,
3379                    }),
3380                    timestamp_writes: None,
3381                    occlusion_query_set: None,
3382                });
3383            }
3384            pass.encode(
3385                &gpu.device,
3386                &mut encoder,
3387                TextDraw {
3388                    target: &target.view,
3389                    depth: Some(&depth),
3390                    atlas: &atlas,
3391                    paragraph: &paragraph,
3392                    space: TextSpace::Surface {
3393                        model_view_projection,
3394                    },
3395                    color: [0.7, 0.2, 0.1, 0.9],
3396                    deformers: None,
3397                },
3398            )
3399            .unwrap();
3400            gpu.queue.submit(Some(encoder.finish()));
3401            gpu.readback_rgba16(&target)
3402                .unwrap()
3403                .into_iter()
3404                .map(|value| half::f16::from_bits(value).to_f32())
3405                .collect::<Vec<_>>()
3406        };
3407        let metre = 1.0 / 256.0;
3408        let model = [
3409            [metre, 0.0, 0.0, 0.0],
3410            [0.0, -metre, 0.0, 0.0],
3411            [0.0, 0.0, 1.0, 0.0],
3412            [-256.0 * metre, 128.0 * metre, 0.0, 1.0],
3413        ];
3414        let aspect = size[0] as f32 / size[1] as f32;
3415        for degrees in [90.0f32, 85.0, 80.0] {
3416            let (s, c) = degrees.to_radians().sin_cos();
3417            let view = look_at([0.0, -1.5 * c, 1.5 * s], [0.0; 3], [0.0, 1.0, 0.0]);
3418            let model_view_projection =
3419                crate::frame::multiply(crate::frame::multiply(perspective(aspect), view), model);
3420            let reference = draw(&before, model_view_projection);
3421            let ours = draw(&current, model_view_projection);
3422            assert!(reference.iter().any(|value| *value > 0.0));
3423            let worst = reference
3424                .iter()
3425                .zip(&ours)
3426                .map(|(a, b)| (a - b).abs())
3427                .fold(0.0f32, f32::max);
3428            let changed = reference.iter().zip(&ours).filter(|(a, b)| a != b).count();
3429            println!(
3430                "surface text at {degrees} degrees: {changed} values changed, worst {worst:.5}"
3431            );
3432            assert!(worst <= 1.0 / 255.0, "{degrees} degrees: {worst}");
3433        }
3434    }
3435
3436    #[test]
3437    #[ignore = "needs a GPU; run under pgpu"]
3438    fn two_thousand_lit_glyphs_at_4k_stay_under_budget() {
3439        let mut lab = Lab::sized(
3440            [3840, 2160],
3441            [0.0, 6.0, 0.0],
3442            [0.0, 0.0, 0.0],
3443            [0.0, 0.0, -1.0],
3444        );
3445        lab.occluder([-2.0, -1.0], [0.0, 1.0], 1.0);
3446        let quads = (0..2000)
3447            .map(|index| {
3448                let x = -2.1 + (index % 100) as f32 * 0.043;
3449                let z = -1.0 + (index / 100) as f32 * 0.105;
3450                solid_quad([x, z], 0.0125, INK)
3451            })
3452            .collect::<Vec<_>>();
3453        let lamp = crate::lights::LocalLight {
3454            position: [0.0, 1.0, 0.0],
3455            colour: [1.0; 3],
3456            intensity: 2.0,
3457            radius: 0.05,
3458            range: 4.0,
3459            shadow: true,
3460        };
3461        let mut median = |label: &str, lit: bool| {
3462            let space = ink_space(&lab, lit);
3463            let mut samples = Vec::new();
3464            let mut turns = pfx_gpu::pace::Turns::default();
3465            for frame in 0..14 {
3466                let timings = lab.render_timed(3.0, [0.0, 1.0, 0.0], &[], &quads, space, &[lamp]);
3467                turns.add(timings.iter().map(|timing| timing.milliseconds).sum());
3468                if frame >= 4 {
3469                    samples.push(
3470                        timings
3471                            .iter()
3472                            .find(|timing| timing.label == label)
3473                            .unwrap_or_else(|| panic!("no {label} timing in {timings:?}"))
3474                            .milliseconds,
3475                    );
3476                }
3477            }
3478            samples.sort_by(f64::total_cmp);
3479            samples[samples.len() / 2]
3480        };
3481        let plain = median("surface text", false);
3482        let lit = median("lit surface text", true);
3483        eprintln!("4K, 2000 solid glyph quads: unlit {plain:.3} ms, lit {lit:.3} ms");
3484        assert!(lit < 0.2, "2000 lit glyphs at 4K took {lit:.3} ms");
3485    }
3486
3487    #[test]
3488    #[ignore = "needs a GPU; run under pgpu"]
3489    fn coloured_text_icon_and_clip_on_gpu() {
3490        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3491        let target = gpu.offscreen(320, 160, COLOR_FORMAT).unwrap();
3492        let pass = TextPass::new(&gpu.device);
3493        let mut engine = TextEngine::new(FONT).unwrap();
3494        let paragraph = engine
3495            .layout(
3496                "Paper",
3497                Style {
3498                    family: "EB Garamond",
3499                    size: 48.0,
3500                    line_height: 58.0,
3501                    wrap_width: None,
3502                    pixels_per_unit: 1.0,
3503                    representation: Representation::Msdf,
3504                },
3505                &Baseline::Straight {
3506                    origin: [30.0, 90.0],
3507                    direction: [1.0, 0.0],
3508                },
3509            )
3510            .unwrap();
3511        let quads = paragraph
3512            .glyphs
3513            .iter()
3514            .map(|glyph| RichQuad {
3515                rect: [
3516                    glyph.position[0] + glyph.local_rect[0] * glyph.scale,
3517                    glyph.position[1] + glyph.local_rect[1] * glyph.scale,
3518                    glyph.local_rect[2] * glyph.scale,
3519                    glyph.local_rect[3] * glyph.scale,
3520                ],
3521                uv: glyph.uv,
3522                origin: glyph.position,
3523                alpha: 1.0,
3524                clip: [0.0, 0.0, 70.0, 160.0],
3525                turn: [0.0; 3],
3526                color: [1.0, 0.0, 0.0, 1.0],
3527            })
3528            .collect::<Vec<_>>();
3529        let text_atlas = GpuAtlas::new(&gpu.device, &gpu.queue, &paragraph.atlas).unwrap();
3530        let mut cache = Cache::new();
3531        let icon_bytes = (0..16)
3532            .flat_map(|_| {
3533                [0u16, 0, 65535, 65535]
3534                    .into_iter()
3535                    .flat_map(u16::to_le_bytes)
3536            })
3537            .collect::<Vec<_>>();
3538        let icon = cache
3539            .load_rgba16(&icon_bytes, 4, 4, ColorSpace::Linear)
3540            .unwrap();
3541        let icon_atlas = IconAtlas::from_cache(&cache, &[icon]).unwrap();
3542        let icon_gpu = GpuAtlas::icons(&gpu.device, &gpu.queue, &icon_atlas);
3543        let icon_quad = RichQuad {
3544            rect: [140.0, 40.0, 40.0, 40.0],
3545            uv: icon_atlas.uv(icon).unwrap(),
3546            origin: [140.0, 40.0],
3547            alpha: 1.0,
3548            clip: [0.0, 0.0, 320.0, 160.0],
3549            turn: [0.0; 3],
3550            color: [1.0; 4],
3551        };
3552        let mut encoder = gpu
3553            .device
3554            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3555                label: Some("rich text gpu test"),
3556            });
3557        {
3558            let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3559                multiview_mask: None,
3560                label: Some("rich text clear"),
3561                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3562                    view: &target.view,
3563                    depth_slice: None,
3564                    resolve_target: None,
3565                    ops: wgpu::Operations {
3566                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3567                        store: wgpu::StoreOp::Store,
3568                    },
3569                })],
3570                depth_stencil_attachment: None,
3571                timestamp_writes: None,
3572                occlusion_query_set: None,
3573            });
3574        }
3575        pass.encode_quads(
3576            &gpu.device,
3577            &mut encoder,
3578            QuadDraw {
3579                target: &target.view,
3580                depth: None,
3581                atlas: &text_atlas,
3582                quads: &quads,
3583                space: TextSpace::Overlay {
3584                    width: 320,
3585                    height: 160,
3586                },
3587                deformers: None,
3588                icons: false,
3589            },
3590        )
3591        .unwrap();
3592        pass.encode_quads(
3593            &gpu.device,
3594            &mut encoder,
3595            QuadDraw {
3596                target: &target.view,
3597                depth: None,
3598                atlas: &icon_gpu,
3599                quads: std::slice::from_ref(&icon_quad),
3600                space: TextSpace::Overlay {
3601                    width: 320,
3602                    height: 160,
3603                },
3604                deformers: None,
3605                icons: true,
3606            },
3607        )
3608        .unwrap();
3609        gpu.queue.submit(Some(encoder.finish()));
3610        let pixels = gpu.readback_rgba16(&target).unwrap();
3611        let channel = |x: usize, y: usize, component: usize| {
3612            half::f16::from_bits(pixels[(y * 320 + x) * 4 + component]).to_f32()
3613        };
3614        assert!((40..70).any(|x| (50..100).any(|y| channel(x, y, 0) > 0.5)));
3615        assert!((70..130).all(|x| (50..100).all(|y| channel(x, y, 0) < 0.01)));
3616        assert!(channel(160, 60, 2) > 0.9);
3617        assert!(channel(160, 60, 0) < 0.01);
3618        let path =
3619            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp/text-live-gpu.png");
3620        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
3621        let file = std::fs::File::create(path).unwrap();
3622        let mut png = png::Encoder::new(file, 320, 160);
3623        png.set_color(png::ColorType::Rgba);
3624        png.set_depth(png::BitDepth::Eight);
3625        let mut writer = png.write_header().unwrap();
3626        let bytes = pixels
3627            .iter()
3628            .map(|value| (half::f16::from_bits(*value).to_f32().clamp(0.0, 1.0) * 255.0) as u8)
3629            .collect::<Vec<_>>();
3630        writer.write_image_data(&bytes).unwrap();
3631    }
3632
3633    #[test]
3634    #[ignore = "needs a GPU; run under pgpu"]
3635    fn two_thousand_glyphs_and_fifty_icons_at_4k() {
3636        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3637        let target = gpu.offscreen(3840, 2160, COLOR_FORMAT).unwrap();
3638        let pass = TextPass::new(&gpu.device);
3639        let paragraph = paragraph(48.0);
3640        let glyph = &paragraph.glyphs[0];
3641        let quads = (0..2000)
3642            .map(|index| RichQuad {
3643                rect: [
3644                    8.0 + (index % 100) as f32 * 38.0,
3645                    8.0 + (index / 100) as f32 * 92.0,
3646                    24.0,
3647                    48.0,
3648                ],
3649                uv: glyph.uv,
3650                origin: [0.0; 2],
3651                alpha: 1.0,
3652                clip: [0.0, 0.0, 3840.0, 2160.0],
3653                turn: [0.0; 3],
3654                color: [1.0; 4],
3655            })
3656            .collect::<Vec<_>>();
3657        let text_atlas = GpuAtlas::new(&gpu.device, &gpu.queue, &paragraph.atlas).unwrap();
3658        let mut cache = Cache::new();
3659        let icon_bytes = (0..16)
3660            .flat_map(|_| [65535u16; 4].into_iter().flat_map(u16::to_le_bytes))
3661            .collect::<Vec<_>>();
3662        let icon = cache
3663            .load_rgba16(&icon_bytes, 4, 4, ColorSpace::Linear)
3664            .unwrap();
3665        let icon_atlas = IconAtlas::from_cache(&cache, &[icon]).unwrap();
3666        let icon_gpu = GpuAtlas::icons(&gpu.device, &gpu.queue, &icon_atlas);
3667        let icon_quads = (0..50)
3668            .map(|index| RichQuad {
3669                rect: [8.0 + index as f32 * 72.0, 1950.0, 40.0, 40.0],
3670                uv: icon_atlas.uv(icon).unwrap(),
3671                origin: [0.0; 2],
3672                alpha: 1.0,
3673                clip: [0.0, 0.0, 3840.0, 2160.0],
3674                turn: [0.0; 3],
3675                color: [1.0; 4],
3676            })
3677            .collect::<Vec<_>>();
3678        let mut profiler = pfx_gpu::GpuProfiler::new(&gpu.device, &gpu.queue);
3679        let mut samples = Vec::new();
3680        let mut turns = pfx_gpu::pace::Turns::default();
3681        for _ in 0..12 {
3682            let mut encoder = gpu.device.create_command_encoder(&Default::default());
3683            pass.encode_quads_timed(
3684                &gpu.device,
3685                &mut encoder,
3686                QuadDraw {
3687                    target: &target.view,
3688                    depth: None,
3689                    atlas: &text_atlas,
3690                    quads: &quads,
3691                    space: TextSpace::Overlay {
3692                        width: 3840,
3693                        height: 2160,
3694                    },
3695                    deformers: None,
3696                    icons: false,
3697                },
3698                Some(&mut profiler),
3699            )
3700            .unwrap();
3701            pass.encode_quads_timed(
3702                &gpu.device,
3703                &mut encoder,
3704                QuadDraw {
3705                    target: &target.view,
3706                    depth: None,
3707                    atlas: &icon_gpu,
3708                    quads: &icon_quads,
3709                    space: TextSpace::Overlay {
3710                        width: 3840,
3711                        height: 2160,
3712                    },
3713                    deformers: None,
3714                    icons: true,
3715                },
3716                Some(&mut profiler),
3717            )
3718            .unwrap();
3719            let slot = profiler.finish(&mut encoder);
3720            gpu.queue.submit(Some(encoder.finish()));
3721            if let Some(slot) = slot {
3722                profiler.submitted(slot);
3723                gpu.device
3724                    .poll(wgpu::PollType::wait_indefinitely())
3725                    .unwrap();
3726                let timings = profiler.collect(&gpu.device);
3727                let elapsed = timings
3728                    .into_iter()
3729                    .flatten()
3730                    .map(|timing| timing.milliseconds)
3731                    .sum::<f64>();
3732                turns.add(elapsed);
3733                samples.push(elapsed);
3734            }
3735        }
3736        assert!(samples.len() >= 10, "GPU timestamp query unavailable");
3737        let measured = &mut samples[2..];
3738        measured.sort_by(f64::total_cmp);
3739        let median = measured[measured.len() / 2];
3740        eprintln!("4K text and icon median: {median:.3} ms");
3741        assert!(median < 0.3, "4K text and icons took {median:.3} ms");
3742    }
3743
3744    fn page_draw_ms(
3745        gpu: &pfx_gpu::Gpu,
3746        pass: &TextPass,
3747        target: &pfx_gpu::OffscreenTarget,
3748        depth: &wgpu::TextureView,
3749        atlas: &GpuAtlas,
3750        quads: &[RichQuad],
3751        space: TextSpace,
3752    ) -> f64 {
3753        let mut profiler = pfx_gpu::GpuProfiler::new(&gpu.device, &gpu.queue);
3754        let mut turns = pfx_gpu::pace::Turns::default();
3755        let mut samples = Vec::new();
3756        for _ in 0..12 {
3757            let mut encoder = gpu.device.create_command_encoder(&Default::default());
3758            {
3759                let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3760                    multiview_mask: None,
3761                    label: Some("page clear"),
3762                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3763                        view: &target.view,
3764                        depth_slice: None,
3765                        resolve_target: None,
3766                        ops: wgpu::Operations {
3767                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3768                            store: wgpu::StoreOp::Store,
3769                        },
3770                    })],
3771                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3772                        view: depth,
3773                        depth_ops: Some(wgpu::Operations {
3774                            load: wgpu::LoadOp::Clear(1.0),
3775                            store: wgpu::StoreOp::Store,
3776                        }),
3777                        stencil_ops: None,
3778                    }),
3779                    timestamp_writes: None,
3780                    occlusion_query_set: None,
3781                });
3782            }
3783            let overlay = matches!(space, TextSpace::Overlay { .. });
3784            pass.encode_quads_timed(
3785                &gpu.device,
3786                &mut encoder,
3787                QuadDraw {
3788                    target: &target.view,
3789                    depth: (!overlay).then_some(depth),
3790                    atlas,
3791                    quads,
3792                    space,
3793                    deformers: None,
3794                    icons: false,
3795                },
3796                Some(&mut profiler),
3797            )
3798            .unwrap();
3799            let slot = profiler.finish(&mut encoder);
3800            gpu.queue.submit(Some(encoder.finish()));
3801            gpu.device
3802                .poll(wgpu::PollType::wait_indefinitely())
3803                .unwrap();
3804            if let Some(slot) = slot {
3805                profiler.submitted(slot);
3806                let elapsed = profiler
3807                    .collect(&gpu.device)
3808                    .into_iter()
3809                    .flatten()
3810                    .map(|timing| timing.milliseconds)
3811                    .sum::<f64>();
3812                turns.add(elapsed);
3813                samples.push(elapsed);
3814            }
3815        }
3816        turns.turn();
3817        assert!(samples.len() >= 10, "GPU timestamp query unavailable");
3818        let measured = &mut samples[2..];
3819        measured.sort_by(f64::total_cmp);
3820        measured[measured.len() / 2]
3821    }
3822
3823    #[test]
3824    #[ignore = "needs a GPU; run under pgpu"]
3825    fn a_page_of_msdf_text_at_4k_face_on_and_tilted() {
3826        let size = [3840u32, 2160];
3827        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3828        let target = gpu.offscreen(size[0], size[1], COLOR_FORMAT).unwrap();
3829        let depth = gpu
3830            .device
3831            .create_texture(&wgpu::TextureDescriptor {
3832                label: Some("page depth"),
3833                size: wgpu::Extent3d {
3834                    width: size[0],
3835                    height: size[1],
3836                    depth_or_array_layers: 1,
3837                },
3838                mip_level_count: 1,
3839                sample_count: 1,
3840                dimension: wgpu::TextureDimension::D2,
3841                format: DEPTH_FORMAT,
3842                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3843                view_formats: &[],
3844            })
3845            .create_view(&Default::default());
3846        let line = "Sphinx of black quartz, judge my vow. The five boxing wizards jump quickly over a lazy dog by the old mill. ";
3847        let mut engine = TextEngine::new(SANS).unwrap();
3848        let spans = [pfx_text::Span {
3849            text: line.repeat(64),
3850            face: shared_face("DM Sans", 36.0, 400),
3851            color: [1.0; 4],
3852        }];
3853        let page = engine
3854            .render_spans(
3855                &spans,
3856                Some(3700.0),
3857                1.0,
3858                Anchor::Start,
3859                Representation::Msdf,
3860                [70.0, 40.0],
3861                1.0,
3862                [0.0, 0.0, 3840.0, 2160.0],
3863                [0.0; 3],
3864            )
3865            .unwrap();
3866        let quads = rich_quads(&page);
3867        let atlas = GpuAtlas::new(&gpu.device, &gpu.queue, &page.atlas).unwrap();
3868        let pass = TextPass::new(&gpu.device);
3869        let width = size[0] as f32;
3870        let height = size[1] as f32;
3871        let flat = [
3872            [2.0 / width, 0.0, 0.0, 0.0],
3873            [0.0, -2.0 / height, 0.0, 0.0],
3874            [0.0, 0.0, 1.0, 0.0],
3875            [-1.0, 1.0, 0.5, 1.0],
3876        ];
3877        let metre = 0.001;
3878        let model = [
3879            [metre, 0.0, 0.0, 0.0],
3880            [0.0, 0.0, metre, 0.0],
3881            [0.0, 1.0, 0.0, 0.0],
3882            [-0.5 * width * metre, 0.0, -0.5 * height * metre, 1.0],
3883        ];
3884        let tilted = |degrees: f32| {
3885            let (s, c) = degrees.to_radians().sin_cos();
3886            let view = look_at([0.0, 3.5 * s, 3.5 * c], [0.0; 3], [0.0, 1.0, 0.0]);
3887            let view_projection = crate::frame::multiply(perspective(width / height), view);
3888            crate::frame::multiply(view_projection, model)
3889        };
3890        println!("page: {} glyphs at 4K", quads.len());
3891        assert!(quads.len() > 5000, "{} glyphs", quads.len());
3892        let cases = [
3893            (
3894                "overlay",
3895                TextSpace::Overlay {
3896                    width: size[0],
3897                    height: size[1],
3898                },
3899            ),
3900            (
3901                "surface, face on",
3902                TextSpace::Surface {
3903                    model_view_projection: flat,
3904                },
3905            ),
3906            (
3907                "surface, 45 degrees",
3908                TextSpace::Surface {
3909                    model_view_projection: tilted(45.0),
3910                },
3911            ),
3912            (
3913                "surface, 25 degrees",
3914                TextSpace::Surface {
3915                    model_view_projection: tilted(25.0),
3916                },
3917            ),
3918            (
3919                "surface, 10 degrees",
3920                TextSpace::Surface {
3921                    model_view_projection: tilted(10.0),
3922                },
3923            ),
3924        ];
3925        for (name, space) in cases {
3926            let ms = page_draw_ms(&gpu, &pass, &target, &depth, &atlas, &quads, space);
3927            println!("page of text, {name}: {ms:.3} ms");
3928            assert!(ms < 6.0, "{name}: {ms:.3} ms");
3929        }
3930    }
3931
3932    #[test]
3933    #[ignore = "needs a GPU; run under pgpu"]
3934    fn paper_edges_at_48_and_600_pixels() {
3935        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
3936        let target = gpu.offscreen(2200, 900, COLOR_FORMAT).unwrap();
3937        let pass = TextPass::new(&gpu.device);
3938        for size in [48.0, 600.0] {
3939            let paragraph = paragraph(size);
3940            let atlas = GpuAtlas::new(&gpu.device, &gpu.queue, &paragraph.atlas).unwrap();
3941            let mut encoder = gpu
3942                .device
3943                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3944                    label: Some("text test"),
3945                });
3946            {
3947                let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3948                    multiview_mask: None,
3949                    label: Some("text clear"),
3950                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3951                        view: &target.view,
3952                        depth_slice: None,
3953                        resolve_target: None,
3954                        ops: wgpu::Operations {
3955                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3956                            store: wgpu::StoreOp::Store,
3957                        },
3958                    })],
3959                    depth_stencil_attachment: None,
3960                    timestamp_writes: None,
3961                    occlusion_query_set: None,
3962                });
3963            }
3964            pass.encode(
3965                &gpu.device,
3966                &mut encoder,
3967                TextDraw {
3968                    target: &target.view,
3969                    depth: None,
3970                    atlas: &atlas,
3971                    paragraph: &paragraph,
3972                    space: TextSpace::Overlay {
3973                        width: 2200,
3974                        height: 900,
3975                    },
3976                    color: [1.0; 4],
3977                    deformers: None,
3978                },
3979            )
3980            .unwrap();
3981            gpu.queue.submit(Some(encoder.finish()));
3982            let pixels = gpu.readback_rgba16(&target).unwrap();
3983            let mut edge_width = usize::MAX;
3984            for y in 0..900usize {
3985                let row: Vec<f32> = (0..2200usize)
3986                    .map(|x| half::f16::from_bits(pixels[(y * 2200 + x) * 4 + 3]).to_f32())
3987                    .collect();
3988                for x in 0..2195 {
3989                    for width in 1..=4 {
3990                        let a = row[x];
3991                        let b = row[x + width];
3992                        if (a < 0.1 && b > 0.9) || (a > 0.9 && b < 0.1) {
3993                            edge_width = edge_width.min(width);
3994                        }
3995                    }
3996                }
3997                if edge_width == 1 {
3998                    break;
3999                }
4000            }
4001            assert!(
4002                edge_width <= 3,
4003                "Paper edge width at {size} px: {edge_width}"
4004            );
4005            println!("Paper edge width at {size} px: {edge_width}");
4006        }
4007    }
4008
4009    const SANS: &[u8] = pfx_text::fixture::DM_SANS;
4010    const MONO: &[u8] = pfx_text::fixture::IBM_PLEX_MONO;
4011
4012    fn shared_face(family: &str, size: f32, weight: u16) -> pfx_text::Face {
4013        pfx_text::Face {
4014            family: family.into(),
4015            size,
4016            line: size * 1.25,
4017            weight,
4018            italic: false,
4019            spacing: 0.0,
4020        }
4021    }
4022
4023    fn shared_span(text: &str, face: &pfx_text::Face) -> pfx_text::Span {
4024        pfx_text::Span {
4025            text: text.into(),
4026            face: face.clone(),
4027            color: [1.0, 0.9, 0.8, 1.0],
4028        }
4029    }
4030
4031    fn overlay(
4032        gpu: &pfx_gpu::Gpu,
4033        pass: &TextPass,
4034        target: &pfx_gpu::OffscreenTarget,
4035        draws: &[(&GpuAtlas, &[RichQuad])],
4036        profiler: Option<&mut pfx_gpu::GpuProfiler>,
4037    ) -> Option<f64> {
4038        let mut encoder = gpu.device.create_command_encoder(&Default::default());
4039        {
4040            let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
4041                multiview_mask: None,
4042                label: Some("shared text clear"),
4043                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4044                    view: &target.view,
4045                    depth_slice: None,
4046                    resolve_target: None,
4047                    ops: wgpu::Operations {
4048                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
4049                        store: wgpu::StoreOp::Store,
4050                    },
4051                })],
4052                depth_stencil_attachment: None,
4053                timestamp_writes: None,
4054                occlusion_query_set: None,
4055            });
4056        }
4057        let mut profiler = profiler;
4058        for (atlas, quads) in draws {
4059            pass.encode_quads_timed(
4060                &gpu.device,
4061                &mut encoder,
4062                QuadDraw {
4063                    target: &target.view,
4064                    depth: None,
4065                    atlas,
4066                    quads,
4067                    space: TextSpace::Overlay {
4068                        width: target.width,
4069                        height: target.height,
4070                    },
4071                    deformers: None,
4072                    icons: false,
4073                },
4074                profiler.as_deref_mut(),
4075            )
4076            .unwrap();
4077        }
4078        let slot = profiler.as_deref_mut().and_then(|p| p.finish(&mut encoder));
4079        gpu.queue.submit(Some(encoder.finish()));
4080        gpu.device
4081            .poll(wgpu::PollType::wait_indefinitely())
4082            .unwrap();
4083        let profiler = profiler?;
4084        profiler.submitted(slot?);
4085        Some(
4086            profiler
4087                .collect(&gpu.device)
4088                .into_iter()
4089                .flatten()
4090                .map(|timing| timing.milliseconds)
4091                .sum(),
4092        )
4093    }
4094
4095    fn shared_frame(
4096        gpu: &pfx_gpu::Gpu,
4097        pass: &TextPass,
4098        target: &pfx_gpu::OffscreenTarget,
4099        pages: &GlyphPages,
4100        quads: &[pfx_text::PageQuad],
4101    ) -> Vec<f32> {
4102        let groups = page_quads(quads);
4103        let draws = groups
4104            .iter()
4105            .map(|(page, quads)| (pages.page(*page).unwrap(), quads.as_slice()))
4106            .collect::<Vec<_>>();
4107        overlay(gpu, pass, target, &draws, None);
4108        gpu.readback_rgba16(target)
4109            .unwrap()
4110            .into_iter()
4111            .map(|value| half::f16::from_bits(value).to_f32())
4112            .collect()
4113    }
4114
4115    #[test]
4116    fn page_quads_group_by_page_in_first_seen_order() {
4117        let quad = |page: u32, x: f32| pfx_text::PageQuad {
4118            page,
4119            quad: pfx_text::RichGlyphQuad {
4120                key: 0,
4121                line_index: 0,
4122                line_baseline: 0.0,
4123                pen: [x, 0.0],
4124                atlas_page: pfx_text::AtlasPage::Monochrome,
4125                is_color: false,
4126                rect: [x, 0.0, 1.0, 1.0],
4127                uv: [0.0; 4],
4128                origin: [0.0; 2],
4129                alpha: 1.0,
4130                clip: [0.0; 4],
4131                turn: [0.0; 3],
4132                color: [1.0; 4],
4133            },
4134        };
4135        let groups = page_quads(&[quad(3, 0.0), quad(1, 1.0), quad(3, 2.0)]);
4136        assert_eq!(groups.len(), 2);
4137        assert_eq!(groups[0].0, 3);
4138        assert_eq!(
4139            groups[0].1.iter().map(|q| q.rect[0]).collect::<Vec<_>>(),
4140            vec![0.0, 2.0]
4141        );
4142        assert_eq!(groups[1].0, 1);
4143    }
4144
4145    #[test]
4146    #[ignore = "needs a GPU; run under pgpu"]
4147    fn shared_pages_draw_existing_text_within_one_level() {
4148        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
4149        let target = gpu.offscreen(480, 200, COLOR_FORMAT).unwrap();
4150        let pass = TextPass::new(&gpu.device);
4151        let mut legacy = TextEngine::new(FONT).unwrap();
4152        let mut shared = TextEngine::new(FONT).unwrap();
4153        let mut pages = GlyphPages::new();
4154        let spans = [
4155            shared_span("Ledger 1,284 ffi", &shared_face("EB Garamond", 22.0, 500)),
4156            shared_span(" été 4.2e9", &shared_face("EB Garamond", 30.0, 700)),
4157        ];
4158        for (scale, origin, turn) in [
4159            (1.0, [12.0, 20.0], [0.0; 3]),
4160            (1.5, [8.25, 30.6], [0.0; 3]),
4161            (2.0, [3.6, 11.1], [80.0, 40.0, 0.2]),
4162        ] {
4163            let old = legacy
4164                .render_spans(
4165                    &spans,
4166                    Some(220.0),
4167                    scale,
4168                    Anchor::Start,
4169                    Representation::Coverage,
4170                    origin,
4171                    1.0,
4172                    [0.0, 0.0, 480.0, 200.0],
4173                    turn,
4174                )
4175                .unwrap();
4176            let atlas = GpuAtlas::new(&gpu.device, &gpu.queue, &old.atlas).unwrap();
4177            let quads = rich_quads(&old)
4178                .into_iter()
4179                .map(|quad| RichQuad {
4180                    rect: quad.rect.map(|v| v * scale),
4181                    origin: quad.origin.map(|v| v * scale),
4182                    clip: quad.clip.map(|v| v * scale),
4183                    turn: [quad.turn[0] * scale, quad.turn[1] * scale, quad.turn[2]],
4184                    ..quad
4185                })
4186                .collect::<Vec<_>>();
4187            overlay(&gpu, &pass, &target, &[(&atlas, &quads)], None);
4188            let before = gpu
4189                .readback_rgba16(&target)
4190                .unwrap()
4191                .into_iter()
4192                .map(|value| half::f16::from_bits(value).to_f32())
4193                .collect::<Vec<_>>();
4194            let placed = shared
4195                .place_spans(
4196                    &spans,
4197                    Some(220.0),
4198                    Anchor::Start,
4199                    &pfx_text::Placement {
4200                        scale,
4201                        representation: Representation::Coverage,
4202                        origin,
4203                        alpha: 1.0,
4204                        clip: [0.0, 0.0, 480.0, 200.0],
4205                        turn,
4206                    },
4207                )
4208                .unwrap();
4209            pages
4210                .sync(&gpu.device, &gpu.queue, &shared.take_atlas_changes())
4211                .unwrap();
4212            let quads = placed
4213                .quads
4214                .iter()
4215                .map(|quad| {
4216                    let mut quad = quad.clone();
4217                    quad.quad.rect = quad.quad.rect.map(|v| v * scale);
4218                    quad.quad.origin = quad.quad.origin.map(|v| v * scale);
4219                    quad.quad.clip = quad.quad.clip.map(|v| v * scale);
4220                    quad.quad.turn = [
4221                        quad.quad.turn[0] * scale,
4222                        quad.quad.turn[1] * scale,
4223                        quad.quad.turn[2],
4224                    ];
4225                    quad
4226                })
4227                .collect::<Vec<_>>();
4228            let after = shared_frame(&gpu, &pass, &target, &pages, &quads);
4229            let ink = before
4230                .iter()
4231                .skip(3)
4232                .step_by(4)
4233                .filter(|&&a| a > 0.5)
4234                .count();
4235            assert!(ink > 500, "{ink}");
4236            let worst = before
4237                .iter()
4238                .zip(&after)
4239                .map(|(a, b)| (a - b).abs())
4240                .fold(0.0f32, f32::max);
4241            assert!(worst <= 1.0 / 255.0, "scale {scale}: {worst}");
4242            eprintln!("scale {scale}: worst difference {:.3}/255", worst * 255.0);
4243        }
4244    }
4245
4246    #[test]
4247    #[ignore = "needs a GPU; run under pgpu"]
4248    fn evicted_glyphs_upload_again_and_draw_the_same_frame() {
4249        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
4250        let target = gpu.offscreen(320, 120, COLOR_FORMAT).unwrap();
4251        let pass = TextPass::new(&gpu.device);
4252        let mut engine = TextEngine::new(MONO).unwrap();
4253        engine.set_atlas_page(256).unwrap();
4254        engine.set_atlas_budget(PageKind::Msdf.page_bytes(256) * 2);
4255        let mut pages = GlyphPages::new();
4256        let face = shared_face("IBM Plex Mono", 40.0, 400);
4257        let at = pfx_text::Placement {
4258            scale: 1.0,
4259            representation: Representation::Msdf,
4260            origin: [10.0, 10.0],
4261            alpha: 1.0,
4262            clip: [0.0, 0.0, 320.0, 120.0],
4263            turn: [0.0; 3],
4264        };
4265        let frame = |engine: &mut TextEngine, pages: &mut GlyphPages, text: &str| {
4266            engine.begin_frame();
4267            let placed = engine
4268                .place_spans(&[shared_span(text, &face)], None, Anchor::Start, &at)
4269                .unwrap();
4270            engine.end_frame();
4271            let regions = pages
4272                .sync(&gpu.device, &gpu.queue, &engine.take_atlas_changes())
4273                .unwrap();
4274            (
4275                shared_frame(&gpu, &pass, &target, pages, &placed.quads),
4276                regions,
4277            )
4278        };
4279        let (first, regions) = frame(&mut engine, &mut pages, "AB12");
4280        assert!(!regions.is_empty());
4281        assert!(regions.iter().all(|(_, region)| region.columns < 256));
4282        let (repeat, regions) = frame(&mut engine, &mut pages, "AB12");
4283        assert!(regions.is_empty());
4284        assert_eq!(first, repeat);
4285        for filler in ["CDEFGHIJ", "KLMNOPQR", "STUVWXYZ", "cdefghij", "klmnopqr"] {
4286            frame(&mut engine, &mut pages, filler);
4287        }
4288        assert!(engine.atlas_stats().evicted > 0);
4289        let (again, regions) = frame(&mut engine, &mut pages, "AB12");
4290        assert!(!regions.is_empty());
4291        let worst = first
4292            .iter()
4293            .zip(&again)
4294            .map(|(a, b)| (a - b).abs())
4295            .fold(0.0f32, f32::max);
4296        assert!(worst <= 1.0 / 255.0, "{worst}");
4297    }
4298
4299    #[test]
4300    #[ignore = "needs a GPU; run under pgpu"]
4301    fn eighty_changing_numbers_at_4k_upload_nothing_once_warm() {
4302        let gpu = pollster::block_on(pfx_gpu::Gpu::headless()).unwrap();
4303        let target = gpu.offscreen(3840, 2160, COLOR_FORMAT).unwrap();
4304        let pass = TextPass::new(&gpu.device);
4305        let mut engine = TextEngine::new(SANS).unwrap();
4306        let mut pages = GlyphPages::new();
4307        let faces = [
4308            shared_face("DM Sans", 19.0, 400),
4309            shared_face("DM Sans", 32.0, 600),
4310            shared_face("DM Sans", 60.0, 800),
4311            shared_face("DM Sans", 150.0, 800),
4312        ];
4313        let at = |origin| pfx_text::Placement {
4314            scale: 2.0,
4315            representation: Representation::Msdf,
4316            origin,
4317            alpha: 1.0,
4318            clip: [0.0, 0.0, 1920.0, 1080.0],
4319            turn: [0.0; 3],
4320        };
4321        engine.begin_frame();
4322        for face in &faces {
4323            engine
4324                .place_number(
4325                    &shared_span(NUMBERS, face),
4326                    NUMBERS,
4327                    Anchor::Start,
4328                    &at([0.0, 0.0]),
4329                )
4330                .unwrap();
4331        }
4332        engine.end_frame();
4333        let warm = pages
4334            .sync(&gpu.device, &gpu.queue, &engine.take_atlas_changes())
4335            .unwrap();
4336        assert!(!warm.is_empty());
4337        let mut profiler = pfx_gpu::GpuProfiler::new(&gpu.device, &gpu.queue);
4338        let mut turns = pfx_gpu::pace::Turns::default();
4339        let mut samples = Vec::new();
4340        let mut seed = 85u64;
4341        for frame in 0..120 {
4342            engine.begin_frame();
4343            let mut quads = Vec::new();
4344            for label in 0..80usize {
4345                seed = seed
4346                    .wrapping_mul(6364136223846793005)
4347                    .wrapping_add(1442695040888963407);
4348                let value = seed >> 33;
4349                let text = match value % 4 {
4350                    0 => "NaN".to_string(),
4351                    1 => format!("{},{:03}", value % 10, (value >> 4) % 1000),
4352                    2 => format!("{}.{}e{}", value % 9 + 1, (value >> 3) % 10, value % 300),
4353                    _ => format!("-{}", value % 10_000),
4354                };
4355                let face = &faces[label % faces.len()];
4356                let origin = [
4357                    20.0 + (label / 20) as f32 * 470.0 + frame as f32 * 0.3,
4358                    40.0 + (label % 20) as f32 * 52.0,
4359                ];
4360                let placed = engine
4361                    .place_number(
4362                        &shared_span(&text, face),
4363                        NUMBERS,
4364                        Anchor::Start,
4365                        &at(origin),
4366                    )
4367                    .unwrap();
4368                quads.extend(placed.quads.into_iter().map(|mut quad| {
4369                    quad.quad.rect = quad.quad.rect.map(|v| v * 2.0);
4370                    quad.quad.clip = quad.quad.clip.map(|v| v * 2.0);
4371                    quad
4372                }));
4373            }
4374            engine.end_frame();
4375            let regions = pages
4376                .sync(&gpu.device, &gpu.queue, &engine.take_atlas_changes())
4377                .unwrap();
4378            assert!(regions.is_empty(), "frame {frame} uploaded {regions:?}");
4379            let groups = page_quads(&quads);
4380            let draws = groups
4381                .iter()
4382                .map(|(page, quads)| (pages.page(*page).unwrap(), quads.as_slice()))
4383                .collect::<Vec<_>>();
4384            if let Some(elapsed) = overlay(&gpu, &pass, &target, &draws, Some(&mut profiler)) {
4385                turns.add(elapsed);
4386                samples.push(elapsed);
4387            }
4388        }
4389        let stats = engine.atlas_stats();
4390        assert_eq!(stats.shaped, 4);
4391        assert!(stats.bytes <= stats.budget);
4392        assert!(samples.len() >= 100, "GPU timestamp query unavailable");
4393        samples.sort_by(f64::total_cmp);
4394        let median = samples[samples.len() / 2];
4395        eprintln!(
4396            "80 changing numbers at 4K: {median:.3} ms median, {} pages, {} cells, {} bytes",
4397            stats.pages, stats.cells, stats.bytes
4398        );
4399        assert!(median < 0.5, "{median:.3} ms");
4400    }
4401}