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 ®ions {
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(¶graph.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(¶graph.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, ¶graph.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: ¶graph,
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(¤t, [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, ¶graph.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: ¶graph,
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(¤t, 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, ¶graph.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 = ¶graph.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, ¶graph.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, ¶graph.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: ¶graph,
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}