Skip to main content

oxiui_render_wgpu/gpu/
buffer.rs

1//! Vertex / uniform data layouts for the headless solid-fill + gradient pipelines.
2//!
3//! All structs are `#[repr(C)]` and implement [`bytemuck::Pod`] /
4//! [`bytemuck::Zeroable`] so they can be uploaded to the GPU as raw bytes with
5//! a guaranteed, stable memory layout.
6//!
7//! # Vertex layout (56 bytes = 14 × f32)
8//!
9//! The struct was extended from the original 48-byte form to add an `extra`
10//! field (`[f32; 2]`) that carries per-kind auxiliary parameters:
11//!
12//! | `kind` | `local`          | `shape_xy`      | `shape_r`           | `extra`              |
13//! |--------|------------------|-----------------|---------------------|----------------------|
14//! | 0 rect | pixel pos        | –               | –                   | –                    |
15//! | 1 circle | pixel pos      | centre (cx,cy)  | radius              | –                    |
16//! | 2 rrect-uniform | pixel pos | centre (cx,cy) | radius             | half-size (hw,hh)    |
17//! | 3 rrect-per-corner | pixel pos | centre (cx,cy) | pack16(tl,tr)  | pack16(br,bl), pack16(hw,hh) |
18//! | 4 ellipse | pixel pos     | centre (cx,cy)  | –                   | (rx, ry)             |
19//! | 5 line-seg | pixel pos    | from (ax,ay)    | half_width (+0.5=aa)| to (bx,by)           |
20//!
21//! For kind=3 the four corner radii and the half-extents are packed as u16
22//! pairs into f32 bit patterns (see `pack_u16_pair`).
23
24use oxiui_core::Color;
25
26// ── Vertex ─────────────────────────────────────────────────────────────────
27
28/// A single vertex fed to `solid.wgsl`.
29#[repr(C)]
30#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
31pub struct Vertex {
32    /// Pixel-space quad-corner position (`@location(0)`).
33    pub position: [f32; 2],
34    /// Straight-alpha RGBA colour in `[0, 1]` (`@location(1)`).
35    pub color: [f32; 4],
36    /// Pixel-space position used for SDF evaluation (`@location(2)`).
37    pub local: [f32; 2],
38    /// Shape parameter XY: centre for circle/ellipse/rounded-rect;
39    /// line-segment start point for lines (`@location(3)`).
40    pub shape_xy: [f32; 2],
41    /// Shape parameter R: radius for circle/uniform-rrect; packed u16 pair
42    /// (tl, tr) for per-corner rrect; (half_width + 0.5 if AA) for lines
43    /// (`@location(4)`).
44    pub shape_r: f32,
45    /// Primitive discriminator (`@location(5)`):
46    /// `0` = rect, `1` = circle, `2` = rrect-uniform, `3` = rrect-per-corner,
47    /// `4` = ellipse, `5` = line-segment.
48    pub kind: f32,
49    /// Auxiliary parameters (`@location(6)`):
50    /// - kind 2 (rrect-uniform): `[hw, hh]` half-extents.
51    /// - kind 3 (rrect-per-corner): `[pack16(br,bl), pack16(hw,hh)]`.
52    /// - kind 4 (ellipse): `[rx, ry]`.
53    /// - kind 5 (line-seg): `[to_x, to_y]` endpoint B.
54    /// - all others: `[0, 0]`.
55    pub extra: [f32; 2],
56}
57
58// ── Kind discriminator constants ─────────────────────────────────────────────
59
60/// Primitive discriminator value for a solid rectangle.
61pub const KIND_RECT: f32 = 0.0;
62/// Primitive discriminator value for an SDF circle.
63pub const KIND_CIRCLE: f32 = 1.0;
64/// Primitive discriminator value for a uniformly-rounded rectangle (SDF).
65pub const KIND_ROUNDED_RECT: f32 = 2.0;
66/// Primitive discriminator value for a per-corner rounded rectangle (SDF).
67pub const KIND_ROUNDED_RECT_PC: f32 = 3.0;
68/// Primitive discriminator value for an SDF ellipse.
69pub const KIND_ELLIPSE: f32 = 4.0;
70/// Primitive discriminator value for a line-segment SDF.
71pub const KIND_LINE_SEG: f32 = 5.0;
72
73// Compile-time layout guard: (2+4+2+2+1+1+2) f32 = 14 f32 = 56 bytes.
74const _: () = assert!(core::mem::size_of::<Vertex>() == 56);
75const _: () = assert!(core::mem::align_of::<Vertex>() == 4);
76
77impl Vertex {
78    /// Convert an 8-bit [`Color`] into straight-alpha `[f32; 4]` in `[0, 1]`.
79    #[inline]
80    pub fn color_to_f32(color: Color) -> [f32; 4] {
81        [
82            color.0 as f32 / 255.0,
83            color.1 as f32 / 255.0,
84            color.2 as f32 / 255.0,
85            color.3 as f32 / 255.0,
86        ]
87    }
88}
89
90// ── Globals (uniform) ────────────────────────────────────────────────────────
91
92/// Per-frame uniform block matching the WGSL `Globals` struct.
93///
94/// Padded to 16 bytes to satisfy the uniform-buffer alignment rules.
95#[repr(C)]
96#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
97pub struct Globals {
98    /// Viewport `[width, height]` in physical pixels.
99    pub viewport: [f32; 2],
100    /// Padding to round the struct up to a 16-byte boundary.
101    pub _pad: [f32; 2],
102}
103
104const _: () = assert!(core::mem::size_of::<Globals>() == 16);
105
106impl Globals {
107    /// Construct a [`Globals`] from a viewport size in pixels.
108    #[inline]
109    pub fn new(width: u32, height: u32) -> Self {
110        Self {
111            viewport: [width as f32, height as f32],
112            _pad: [0.0, 0.0],
113        }
114    }
115}
116
117// ── Gradient vertex / uniform ─────────────────────────────────────────────────
118
119/// A single vertex fed to `gradient.wgsl`.
120#[repr(C)]
121#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
122pub struct GradientVertex {
123    /// Pixel-space quad-corner position (`@location(0)`).
124    pub position: [f32; 2],
125    /// Pixel-space position passed to the fragment stage for gradient sampling
126    /// (`@location(1)`).
127    pub local: [f32; 2],
128}
129
130const _: () = assert!(core::mem::size_of::<GradientVertex>() == 16);
131
132/// Maximum number of gradient colour stops supported in a single gradient draw.
133pub const MAX_GRADIENT_STOPS: usize = 8;
134
135/// Per-gradient uniform block sent to `gradient.wgsl`.
136///
137/// Follows std140 layout: each field is aligned to its natural boundary,
138/// the total size is a multiple of 16 bytes.
139#[repr(C)]
140#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
141pub struct GradientUniforms {
142    /// Linear: gradient start point (pixel space).
143    /// Radial: gradient centre point (pixel space).
144    pub p0: [f32; 2],
145    /// Linear: gradient end point (pixel space).
146    /// Radial: unused (zeroed).
147    pub p1: [f32; 2],
148    /// Radial: outer radius in pixels.  0 for linear.
149    pub radius: f32,
150    /// Gradient type: 0 = linear, 1 = radial.
151    pub gradient_type: u32,
152    /// Number of active colour stops (1–8).
153    pub stop_count: u32,
154    /// Padding to align the arrays on 16-byte boundaries.
155    pub _pad: u32,
156    /// Per-stop offset packed into `.x` (y/z/w = 0).
157    pub stop_offsets: [[f32; 4]; MAX_GRADIENT_STOPS],
158    /// Per-stop RGBA colour in `[0, 1]`.
159    pub stop_colors: [[f32; 4]; MAX_GRADIENT_STOPS],
160}
161
162// Size: p0(8) + p1(8) + radius(4) + gradient_type(4) + stop_count(4) + _pad(4)
163//       + stop_offsets(8*16=128) + stop_colors(8*16=128) = 32 + 256 = 288 bytes.
164const _: () = assert!(core::mem::size_of::<GradientUniforms>() == 288);
165
166// ── Packing helpers ───────────────────────────────────────────────────────────
167
168/// Pack two `u16` values into the bit pattern of a `f32`.
169///
170/// The WGSL shader unpacks with `bitcast<u32>(v) >> 16u` and `& 0xffffu`.
171/// Values must be in `[0, 65535]`.
172#[inline]
173pub fn pack_u16_pair(hi: u16, lo: u16) -> f32 {
174    f32::from_bits(((hi as u32) << 16) | (lo as u32))
175}
176
177// ── Quad emitters ────────────────────────────────────────────────────────────
178
179/// Append six vertices (two triangles) covering the axis-aligned rectangle
180/// `(x, y, w, h)` with a uniform `color`, tagged as a solid rectangle.
181pub fn push_rect_quad(out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, color: Color) {
182    let rgba = Vertex::color_to_f32(color);
183    let x1 = x + w;
184    let y1 = y + h;
185    let corners = [[x, y], [x, y1], [x1, y1], [x, y], [x1, y1], [x1, y]];
186    for c in corners {
187        out.push(Vertex {
188            position: c,
189            color: rgba,
190            local: c,
191            shape_xy: [0.0, 0.0],
192            shape_r: 0.0,
193            kind: KIND_RECT,
194            extra: [0.0, 0.0],
195        });
196    }
197}
198
199/// Append six vertices covering the bounding quad of the circle centred at
200/// `(cx, cy)` with `radius`, tagged as an SDF circle.
201pub fn push_circle_quad(out: &mut Vec<Vertex>, cx: f32, cy: f32, radius: f32, color: Color) {
202    let rgba = Vertex::color_to_f32(color);
203    let r = radius + 1.0;
204    let x0 = cx - r;
205    let y0 = cy - r;
206    let x1 = cx + r;
207    let y1 = cy + r;
208    let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
209    for c in corners {
210        out.push(Vertex {
211            position: c,
212            color: rgba,
213            local: c,
214            shape_xy: [cx, cy],
215            shape_r: radius,
216            kind: KIND_CIRCLE,
217            extra: [0.0, 0.0],
218        });
219    }
220}
221
222/// Append six vertices for a uniformly-rounded rectangle (SDF).
223///
224/// The quad is inflated by 1 px on all sides to avoid clipping the AA rim.
225pub fn push_rounded_rect_quad(
226    out: &mut Vec<Vertex>,
227    x: f32,
228    y: f32,
229    w: f32,
230    h: f32,
231    radius: f32,
232    color: Color,
233) {
234    let rgba = Vertex::color_to_f32(color);
235    let r = radius.min(w * 0.5).min(h * 0.5).max(0.0);
236    let cx = x + w * 0.5;
237    let cy = y + h * 0.5;
238    let hw = w * 0.5;
239    let hh = h * 0.5;
240    let pad = 1.0_f32;
241    let x0 = x - pad;
242    let y0 = y - pad;
243    let x1 = x + w + pad;
244    let y1 = y + h + pad;
245    let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
246    for c in corners {
247        out.push(Vertex {
248            position: c,
249            color: rgba,
250            local: c,
251            shape_xy: [cx, cy],
252            shape_r: r,
253            kind: KIND_ROUNDED_RECT,
254            extra: [hw, hh],
255        });
256    }
257}
258
259/// Append six vertices for a per-corner rounded rectangle (SDF).
260///
261/// `radii` is `[top-left, top-right, bottom-right, bottom-left]`.
262///
263/// The four radii and the half-extents are packed into the vertex using
264/// integer-arithmetic encoding that avoids GPU subnormal/denormal issues:
265///
266/// * `shape_r`  = `tl_r * 256.0 + tr_r`  (radii clamped to `[0, 255]`)
267/// * `extra[0]` = `br_r * 256.0 + bl_r`
268/// * `extra[1]` = `hw_i * 4096.0 + hh_i` (half-extents clamped to `[0, 4095]`)
269///
270/// The WGSL shader unpacks with `floor(v / base)` / `mod(v, base)`.
271/// All packed values stay below `2^24`, so f32 represents them exactly.
272pub fn push_rounded_rect_per_corner_quad(
273    out: &mut Vec<Vertex>,
274    x: f32,
275    y: f32,
276    w: f32,
277    h: f32,
278    radii: [f32; 4],
279    color: Color,
280) {
281    let rgba = Vertex::color_to_f32(color);
282    let [tl, tr, br, bl] = radii;
283    let hw = w * 0.5;
284    let hh = h * 0.5;
285    let cx = x + hw;
286    let cy = y + hh;
287    let clamp_r = |r: f32| r.clamp(0.0, hw.min(hh).min(255.0));
288    let tl = clamp_r(tl);
289    let tr = clamp_r(tr);
290    let br = clamp_r(br);
291    let bl = clamp_r(bl);
292    let hw_c = hw.clamp(0.0, 4095.0);
293    let hh_c = hh.clamp(0.0, 4095.0);
294    // Encode using integer arithmetic within exact f32 range (< 2^24).
295    let r_packed = tl.floor() * 256.0 + tr.floor();
296    let brbl_packed = br.floor() * 256.0 + bl.floor();
297    let hwhh_packed = hw_c.floor() * 4096.0 + hh_c.floor();
298    let pad = 1.0_f32;
299    let x0 = x - pad;
300    let y0 = y - pad;
301    let x1 = x + w + pad;
302    let y1 = y + h + pad;
303    let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
304    for c in corners {
305        out.push(Vertex {
306            position: c,
307            color: rgba,
308            local: c,
309            shape_xy: [cx, cy],
310            shape_r: r_packed,
311            kind: KIND_ROUNDED_RECT_PC,
312            extra: [brbl_packed, hwhh_packed],
313        });
314    }
315}
316
317/// Append six vertices for an SDF ellipse centred at `(cx, cy)` with
318/// horizontal radius `rx` and vertical radius `ry`.
319pub fn push_ellipse_quad(out: &mut Vec<Vertex>, cx: f32, cy: f32, rx: f32, ry: f32, color: Color) {
320    let rgba = Vertex::color_to_f32(color);
321    let pad = 1.0_f32;
322    let x0 = cx - rx - pad;
323    let y0 = cy - ry - pad;
324    let x1 = cx + rx + pad;
325    let y1 = cy + ry + pad;
326    let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
327    for c in corners {
328        out.push(Vertex {
329            position: c,
330            color: rgba,
331            local: c,
332            shape_xy: [cx, cy],
333            shape_r: 0.0,
334            kind: KIND_ELLIPSE,
335            extra: [rx, ry],
336        });
337    }
338}
339
340/// Parameters for a line-segment SDF quad.
341pub struct LineQuadParams {
342    /// Start x coordinate.
343    pub from_x: f32,
344    /// Start y coordinate.
345    pub from_y: f32,
346    /// End x coordinate.
347    pub to_x: f32,
348    /// End y coordinate.
349    pub to_y: f32,
350    /// Half-width of the line stroke.
351    pub half_width: f32,
352    /// Line colour.
353    pub color: Color,
354    /// `true` = anti-aliased edges; `false` = hard clip.
355    pub aa_smooth: bool,
356}
357
358/// Append six vertices for a line-segment SDF quad.
359///
360/// The quad is expanded perpendicular to the line by `half_width + 1.0` pixels
361/// to ensure the anti-aliased edge is not clipped.
362///
363/// When `aa_smooth` is `true`, the shader uses `smoothstep` for soft edges.
364/// When `false`, the edge is hard-clipped.
365pub fn push_line_quad(out: &mut Vec<Vertex>, params: LineQuadParams) {
366    let LineQuadParams {
367        from_x,
368        from_y,
369        to_x,
370        to_y,
371        half_width,
372        color,
373        aa_smooth,
374    } = params;
375    let rgba = Vertex::color_to_f32(color);
376    let dx = to_x - from_x;
377    let dy = to_y - from_y;
378    let len = (dx * dx + dy * dy).sqrt().max(1e-6);
379    // Perpendicular unit vector (rotated 90° CCW).
380    let nx = -dy / len;
381    let ny = dx / len;
382    // Also expand along the line direction (caps) by half_width.
383    let lx = dx / len;
384    let ly = dy / len;
385    let expand = half_width + 1.0;
386    let cap = half_width + 1.0;
387    // Quad corners: a = from-side start, b = from-side end,
388    //               c = to-side end,     d = to-side start.
389    let ax = from_x - lx * cap + nx * expand;
390    let ay = from_y - ly * cap + ny * expand;
391    let bx = from_x - lx * cap - nx * expand;
392    let by = from_y - ly * cap - ny * expand;
393    let cx = to_x + lx * cap - nx * expand;
394    let cy_v = to_y + ly * cap - ny * expand;
395    let dxp = to_x + lx * cap + nx * expand;
396    let dyp = to_y + ly * cap + ny * expand;
397    // Two CCW triangles: (a, b, c) and (a, c, d).
398    let corners = [
399        [ax, ay],
400        [bx, by],
401        [cx, cy_v],
402        [ax, ay],
403        [cx, cy_v],
404        [dxp, dyp],
405    ];
406    // Encode aa_smooth in the fractional part of shape_r:
407    // shape_r = half_width + 0.5 → aa, half_width + 0.0 → hard.
408    let shape_r_val = if aa_smooth {
409        half_width + 0.5
410    } else {
411        half_width
412    };
413    for c in corners {
414        out.push(Vertex {
415            position: c,
416            color: rgba,
417            local: c,
418            shape_xy: [from_x, from_y],
419            shape_r: shape_r_val,
420            kind: KIND_LINE_SEG,
421            extra: [to_x, to_y],
422        });
423    }
424}
425
426/// Append three vertices (one triangle) for a path fill triangle.
427pub fn push_triangle(
428    out: &mut Vec<Vertex>,
429    p0: [f32; 2],
430    p1: [f32; 2],
431    p2: [f32; 2],
432    color: Color,
433) {
434    let rgba = Vertex::color_to_f32(color);
435    for c in [p0, p1, p2] {
436        out.push(Vertex {
437            position: c,
438            color: rgba,
439            local: c,
440            shape_xy: [0.0, 0.0],
441            shape_r: 0.0,
442            kind: KIND_RECT,
443            extra: [0.0, 0.0],
444        });
445    }
446}
447
448/// Append six gradient vertices covering `(x, y, w, h)`.
449pub fn push_gradient_quad(out: &mut Vec<GradientVertex>, x: f32, y: f32, w: f32, h: f32) {
450    let x1 = x + w;
451    let y1 = y + h;
452    let corners = [[x, y], [x, y1], [x1, y1], [x, y], [x1, y1], [x1, y]];
453    for c in corners {
454        out.push(GradientVertex {
455            position: c,
456            local: c,
457        });
458    }
459}
460
461// ── Tests ─────────────────────────────────────────────────────────────────────
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn vertex_size_is_56_bytes() {
469        assert_eq!(core::mem::size_of::<Vertex>(), 56);
470    }
471
472    #[test]
473    fn globals_size_is_16_bytes() {
474        assert_eq!(core::mem::size_of::<Globals>(), 16);
475    }
476
477    #[test]
478    fn gradient_vertex_size_is_16_bytes() {
479        assert_eq!(core::mem::size_of::<GradientVertex>(), 16);
480    }
481
482    #[test]
483    fn gradient_uniforms_size_is_288_bytes() {
484        assert_eq!(core::mem::size_of::<GradientUniforms>(), 288);
485    }
486
487    #[test]
488    fn color_to_f32_maps_full_range() {
489        let white = Vertex::color_to_f32(Color(255, 255, 255, 255));
490        assert!((white[0] - 1.0).abs() < 1e-6);
491        assert!((white[3] - 1.0).abs() < 1e-6);
492        let black = Vertex::color_to_f32(Color(0, 0, 0, 0));
493        assert_eq!(black, [0.0, 0.0, 0.0, 0.0]);
494    }
495
496    #[test]
497    fn rect_quad_emits_six_vertices() {
498        let mut v = Vec::new();
499        push_rect_quad(&mut v, 1.0, 2.0, 3.0, 4.0, Color(255, 0, 0, 255));
500        assert_eq!(v.len(), 6);
501        for vert in &v {
502            assert_eq!(vert.kind, KIND_RECT);
503        }
504        let xs: Vec<f32> = v.iter().map(|vt| vt.position[0]).collect();
505        assert!(xs.contains(&1.0));
506        assert!(xs.contains(&4.0));
507    }
508
509    #[test]
510    fn circle_quad_emits_six_vertices_with_center() {
511        let mut v = Vec::new();
512        push_circle_quad(&mut v, 10.0, 10.0, 5.0, Color(0, 255, 0, 255));
513        assert_eq!(v.len(), 6);
514        for vert in &v {
515            assert_eq!(vert.kind, KIND_CIRCLE);
516            assert_eq!(vert.shape_xy, [10.0, 10.0]);
517            assert!((vert.shape_r - 5.0).abs() < 1e-6);
518        }
519    }
520
521    #[test]
522    fn vertices_are_pod_castable() {
523        let mut v = Vec::new();
524        push_rect_quad(&mut v, 0.0, 0.0, 1.0, 1.0, Color(1, 2, 3, 4));
525        let bytes: &[u8] = bytemuck::cast_slice(&v);
526        assert_eq!(bytes.len(), 6 * 56);
527    }
528
529    #[test]
530    fn rounded_rect_quad_emits_six_vertices() {
531        let mut v = Vec::new();
532        push_rounded_rect_quad(&mut v, 10.0, 10.0, 80.0, 40.0, 8.0, Color(0, 0, 255, 255));
533        assert_eq!(v.len(), 6);
534        for vert in &v {
535            assert_eq!(vert.kind, KIND_ROUNDED_RECT);
536        }
537    }
538
539    #[test]
540    fn rounded_rect_pc_quad_emits_six_vertices() {
541        let mut v = Vec::new();
542        push_rounded_rect_per_corner_quad(
543            &mut v,
544            10.0,
545            10.0,
546            80.0,
547            40.0,
548            [4.0, 8.0, 4.0, 8.0],
549            Color(0, 0, 255, 255),
550        );
551        assert_eq!(v.len(), 6);
552        for vert in &v {
553            assert_eq!(vert.kind, KIND_ROUNDED_RECT_PC);
554        }
555    }
556
557    #[test]
558    fn ellipse_quad_emits_six_vertices() {
559        let mut v = Vec::new();
560        push_ellipse_quad(&mut v, 50.0, 50.0, 30.0, 20.0, Color(255, 255, 0, 255));
561        assert_eq!(v.len(), 6);
562        for vert in &v {
563            assert_eq!(vert.kind, KIND_ELLIPSE);
564            assert!((vert.extra[0] - 30.0).abs() < 1e-4);
565            assert!((vert.extra[1] - 20.0).abs() < 1e-4);
566        }
567    }
568
569    #[test]
570    fn line_quad_emits_six_vertices() {
571        let mut v = Vec::new();
572        push_line_quad(
573            &mut v,
574            LineQuadParams {
575                from_x: 0.0,
576                from_y: 0.0,
577                to_x: 100.0,
578                to_y: 0.0,
579                half_width: 2.0,
580                color: Color(255, 0, 0, 255),
581                aa_smooth: true,
582            },
583        );
584        assert_eq!(v.len(), 6);
585        for vert in &v {
586            assert_eq!(vert.kind, KIND_LINE_SEG);
587        }
588    }
589
590    #[test]
591    fn push_triangle_emits_three_vertices() {
592        let mut v = Vec::new();
593        push_triangle(
594            &mut v,
595            [0.0, 0.0],
596            [10.0, 0.0],
597            [5.0, 8.0],
598            Color(255, 255, 255, 255),
599        );
600        assert_eq!(v.len(), 3);
601        for vert in &v {
602            assert_eq!(vert.kind, KIND_RECT);
603        }
604    }
605
606    #[test]
607    fn gradient_quad_emits_six_vertices() {
608        let mut v = Vec::new();
609        push_gradient_quad(&mut v, 0.0, 0.0, 100.0, 50.0);
610        assert_eq!(v.len(), 6);
611    }
612
613    #[test]
614    fn pack_u16_pair_round_trips() {
615        let packed = pack_u16_pair(42, 1000);
616        let bits = packed.to_bits();
617        let hi = (bits >> 16) as u16;
618        let lo = (bits & 0xffff) as u16;
619        assert_eq!(hi, 42);
620        assert_eq!(lo, 1000);
621    }
622}