1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
//! Shared textured vertex types for quad rendering.
//!
//! This module provides the common vertex format used by both text quad and image quad
//! rendering pipelines.
use crate::transform::Transform;
use wgpu::{VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode};
/// Vertex with pre-computed NDC position, UV coordinates, and clip data.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TexturedVertex {
/// Position in NDC (pre-computed on CPU)
pub position: [f32; 2],
/// Texture coordinates
pub uv: [f32; 2],
/// This corner, in whatever space [`clip_rect`](Self::clip_rect) is
/// written in.
///
/// The clip test is an SDF against a rect, and the only thing it needs is
/// for the point and the rect to be in one space. That space is the clip's
/// own, for an image and for a transformed text alike, so a turned clip
/// cuts the turned shape rather than the box around it.
///
/// Mapping the four corners on the CPU is what keeps this free. The map is
/// affine, and interpolating an affine function of position across a
/// triangle gives the same answer as applying it to the interpolated
/// position — so the shader needs no matrix, and the vertex no room for
/// one.
pub clip_pos: [f32; 2],
/// Clip rect [x, y, width, height], in that same space.
pub clip_rect: [f32; 4],
/// Clip corner radii [top_left, top_right, bottom_right, bottom_left], in
/// that same space. The shader reads all four — a rect clip passes zeros.
pub clip_params: [f32; 4],
/// `[clip curvature, opacity]`.
///
/// The opacity is multiplied into the alpha of every texel the quad
/// samples: the fade an ancestor asks for, carried on the vertex rather
/// than into the texture, so a text or an image that fades is not
/// rasterised again on every frame of it.
pub curvature_opacity: [f32; 2],
/// Where the clip continues in the frame's clip records, or
/// [`NO_CHAIN`](super::clip::NO_CHAIN) — see
/// [`ResolvedClip`](super::clip::ResolvedClip). Eight bytes that used to
/// pad the two scalars above out to a `Float32x4` hold this and the
/// padding beside it, so the vertex stays 88 bytes.
pub clip_chain: u32,
pub _pad: u32,
/// `[r, g, b, amount]`: the colour every texel takes, keeping its alpha,
/// and how much of it — `1.0` for a tinted image, `0.0` for everything
/// else, which leaves the texel as it was. Carried on the vertex for the
/// same reason as the opacity: a new tint is not a new texture.
pub tint: [f32; 4],
}
/// The clip a textured quad is cut by, in the clip's own coordinates.
///
/// Four values that always travel together and always have to agree about
/// which space they are in, which is exactly the thing that is easy to get
/// wrong when they are four arguments in a row.
///
/// There used to be a second spelling — a plain box in physical world pixels —
/// because text was cut by the box around its clip rather than the clip. Both
/// quad pipelines now take [`resolved`](Self::resolved), so a turned clip cuts
/// the turned shape whichever of them is drawing.
#[derive(Clone, Copy, Debug)]
pub struct QuadClip {
/// `[x, y, width, height]`. Negative width/height is the no-clip sentinel.
pub rect: [f32; 4],
/// `[top_left, top_right, bottom_right, bottom_left]`.
pub radii: [f32; 4],
/// Superellipse curvature (K-value), so a squircle clip cuts an image the
/// way it cuts a rectangle. The shape shader has always taken this; this
/// pipeline did not, and a squircle clip over an image cut with circular
/// corners while the container beside it cut with squircular ones.
pub curvature: f32,
/// Physical world pixels into the space `rect` is written in. The identity
/// where that space *is* physical world pixels, rather than an `Option` —
/// "no map needed" and "no clip" are different facts, and `rect`'s sentinel
/// is the one that answers the second.
pub to_clip_space: Transform,
/// Where the clip continues in the frame's clip records, or
/// [`NO_CHAIN`](super::clip::NO_CHAIN).
pub chain: u32,
}
impl QuadClip {
/// Nothing is cut.
pub const NONE: Self = Self {
rect: crate::renderer::gpu::NO_CLIP_RECT,
radii: [0.0; 4],
curvature: 1.0,
to_clip_space: Transform::IDENTITY,
chain: super::clip::NO_CHAIN,
};
/// Cut to the clip's first shape, in the coordinates the widget declared
/// it in, and down its chain. A placement that collapsed has no inside and
/// cuts everything.
pub fn resolved(clip: &super::clip::ResolvedClip, scale: f32) -> Self {
let record = super::clip_records::ClipRecord::new(&clip.first, clip.rest, scale);
Self {
rect: record.rect,
radii: record.radii,
curvature: record.curvature,
to_clip_space: Transform {
data: record.inverse,
},
chain: record.next,
}
}
/// This corner, in the clip's space.
#[inline]
pub(crate) fn place(&self, x: f32, y: f32) -> [f32; 2] {
let (x, y) = self.to_clip_space.transform_point(x, y);
[x, y]
}
}
impl TexturedVertex {
/// One corner of a quad: where it is on screen, where it is in the
/// texture, where it falls in its clip, and how opaque it is drawn.
///
/// `screen` is in physical pixels and `ndc` is that same point in clip
/// space — both are wanted, so the caller passes both rather than having
/// this undo one to get the other.
#[inline]
pub fn corner(
ndc: [f32; 2],
uv: [f32; 2],
screen: (f32, f32),
clip: &QuadClip,
opacity: f32,
tint: [f32; 4],
) -> Self {
Self {
position: ndc,
uv,
clip_pos: clip.place(screen.0, screen.1),
clip_rect: clip.rect,
clip_params: clip.radii,
curvature_opacity: [clip.curvature, opacity],
clip_chain: clip.chain,
_pad: 0,
tint,
}
}
pub fn desc() -> VertexBufferLayout<'static> {
VertexBufferLayout {
array_stride: std::mem::size_of::<TexturedVertex>() as u64,
step_mode: VertexStepMode::Vertex,
attributes: &[
// position (NDC)
VertexAttribute {
offset: 0,
shader_location: 0,
format: VertexFormat::Float32x2,
},
// uv
VertexAttribute {
offset: 8,
shader_location: 1,
format: VertexFormat::Float32x2,
},
// clip_pos
VertexAttribute {
offset: 16,
shader_location: 2,
format: VertexFormat::Float32x2,
},
// clip_rect
VertexAttribute {
offset: 24,
shader_location: 3,
format: VertexFormat::Float32x4,
},
// clip_params
VertexAttribute {
offset: 40,
shader_location: 4,
format: VertexFormat::Float32x4,
},
// curvature_opacity
VertexAttribute {
offset: 56,
shader_location: 5,
format: VertexFormat::Float32x2,
},
// clip_chain
VertexAttribute {
offset: 64,
shader_location: 7,
format: VertexFormat::Uint32,
},
// tint
VertexAttribute {
offset: 72,
shader_location: 6,
format: VertexFormat::Float32x4,
},
],
}
}
}
/// The tint of a quad that has none: its texels are drawn as they are.
pub const NO_TINT: [f32; 4] = [0.0; 4];
/// Convert screen coordinates to NDC (Normalized Device Coordinates).
#[inline]
pub fn to_ndc(x: f32, y: f32, screen_width: f32, screen_height: f32) -> [f32; 2] {
[
(x / screen_width) * 2.0 - 1.0,
1.0 - (y / screen_height) * 2.0,
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::renderer::CornerRadii;
use crate::shape::PlacedShape;
use crate::widgets::Rect;
/// The rect stays in the logical units the widget declared and the scale is
/// folded into the map instead. Getting that backwards scales twice or not
/// at all, and the symptom is a HiDPI surface clipped to a quarter of its
/// viewport.
#[test]
fn a_clip_is_scaled_by_its_map_and_not_by_its_rect() {
let rect = Rect::new(10.0, 20.0, 100.0, 50.0);
let shaped = QuadClip::resolved(
&crate::renderer::clip::ResolvedClip::alone(PlacedShape {
rect,
radii: CornerRadii::uniform(4.0),
curvature: 1.0,
placement: crate::transform::Transform::IDENTITY,
}),
2.0,
);
assert_eq!(
shaped.rect,
[10.0, 20.0, 100.0, 50.0],
"the declaration, unscaled — the map is what carries the scale"
);
let [x, y] = shaped.place(20.0, 40.0);
assert!(
(x - 10.0).abs() < 0.01 && (y - 20.0).abs() < 0.01,
"so the same physical corner arrives at the logical one, got ({x}, {y})"
);
assert_eq!(shaped.radii, [4.0; 4], "radii travel with the rect");
}
/// Every attribute the layout declares reads the field it names, and the
/// chain head fits in what used to be padding.
#[test]
fn the_layout_reads_every_field_where_it_is() {
use std::mem::offset_of;
let fields = [
offset_of!(TexturedVertex, position),
offset_of!(TexturedVertex, uv),
offset_of!(TexturedVertex, clip_pos),
offset_of!(TexturedVertex, clip_rect),
offset_of!(TexturedVertex, clip_params),
offset_of!(TexturedVertex, curvature_opacity),
offset_of!(TexturedVertex, tint),
offset_of!(TexturedVertex, clip_chain),
];
let layout = TexturedVertex::desc();
for attribute in layout.attributes {
assert_eq!(
attribute.offset as usize, fields[attribute.shader_location as usize],
"location {}",
attribute.shader_location
);
}
assert_eq!(layout.attributes.len(), fields.len());
assert_eq!(size_of::<TexturedVertex>(), 88);
}
/// A resolved clip hands its chain to every corner.
#[test]
fn a_chained_clip_reaches_the_vertex() {
let resolved = crate::renderer::clip::ResolvedClip {
rest: 5,
..crate::renderer::clip::ResolvedClip::alone(PlacedShape::placed(
Rect::new(0.0, 0.0, 10.0, 10.0),
CornerRadii::uniform(0.0),
1.0,
crate::transform::Transform::IDENTITY,
))
};
let clip = QuadClip::resolved(&resolved, 1.0);
let vertex = TexturedVertex::corner([0.0; 2], [0.0; 2], (0.0, 0.0), &clip, 1.0, NO_TINT);
assert_eq!(vertex.clip_chain, 5);
assert_eq!(
TexturedVertex::corner(
[0.0; 2],
[0.0; 2],
(0.0, 0.0),
&QuadClip::NONE,
1.0,
NO_TINT
)
.clip_chain,
crate::renderer::clip::NO_CHAIN
);
}
/// A clip whose placement collapses lets nothing through, rather than
/// everything.
#[test]
fn a_collapsed_clip_cuts_it_all() {
let collapsed = QuadClip::resolved(
&crate::renderer::clip::ResolvedClip::alone(PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 50.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: crate::transform::Transform::scale(0.0),
}),
1.0,
);
assert_eq!(collapsed.rect, crate::renderer::gpu::EMPTY_CLIP_RECT);
assert_ne!(
collapsed.rect,
crate::renderer::gpu::NO_CLIP_RECT,
"the sentinel for `clip everything`, not the one for `clip nothing`"
);
}
}