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
//! Scalar quantization helpers.
//!
//! [`Quantizer`] and its inverse map floating-point values to and from a
//! fixed-point integer grid defined by an origin and range — the primitive used
//! by the quantization attribute transform. Port of Draco's
//! `quantization_utils.h`.
#[derive(Debug, Default, Clone, Copy)]
pub struct Quantizer {
inverse_delta: f32,
}
impl Quantizer {
pub fn new() -> Self {
Self::default()
}
pub fn init(&mut self, range: f32, max_quantized_value: i32) {
if range > 0.0 {
self.inverse_delta = max_quantized_value as f32 / range;
} else {
self.inverse_delta = 0.0;
}
}
pub fn init_with_delta(&mut self, delta: f32) {
if delta > 0.0 {
self.inverse_delta = 1.0 / delta;
} else {
self.inverse_delta = 0.0;
}
}
pub fn quantize_float(&self, val: f32) -> i32 {
let val = val * self.inverse_delta;
// Use explicit f32 literal to avoid accidental promotion to f64 and
// to match C++'s float-floor(val + 0.5f) behavior exactly.
let scaled = val + 0.5f32;
// `f32::floor` is a call into libm on a baseline x86-64 target -- the
// instruction that would inline it is SSE4.1, which the default target
// does not assume -- and this runs once per component of every
// quantized value. The cast truncates toward zero, so it already is
// the floor for anything non-negative, and one comparison covers the
// rest; `saturating_sub` keeps the correction from wrapping at the
// bottom of the range, where the cast itself saturates.
//
// Exact for every finite input: above `2^24` an `f32` is already an
// integer, so truncation and floor agree there and the round trip
// through `as f32` is exact. `quantize_float_matches_floor` pins it
// against `f32::floor` across the range.
let truncated = scaled as i32;
if (truncated as f32) > scaled {
truncated.saturating_sub(1)
} else {
truncated
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Dequantizer {
delta: f32,
}
impl Dequantizer {
pub fn new() -> Self {
Self::default()
}
pub fn init(&mut self, range: f32, max_quantized_value: i32) -> bool {
if max_quantized_value > 0 {
self.delta = range / max_quantized_value as f32;
true
} else {
false
}
}
pub fn init_with_delta(&mut self, delta: f32) -> bool {
if delta >= 0.0 {
self.delta = delta;
true
} else {
false
}
}
#[inline(always)]
pub fn dequantize_float(&self, val: i32) -> f32 {
val as f32 * self.delta
}
}
#[cfg(test)]
mod tests {
use super::Quantizer;
/// The floor inside `quantize_float` is open-coded to keep libm out of a
/// per-component path. This pins it against `f32::floor` over the range
/// the transform actually produces and past the point where an `f32` is
/// integral, including the negatives a caller can reach through the
/// public API and the saturating ends.
#[test]
fn quantize_float_matches_floor() {
let mut quantizer = Quantizer::new();
quantizer.init_with_delta(1.0);
let mut cases: Vec<f32> = vec![
0.0,
-0.0,
0.49999997,
0.5,
-0.5,
-0.50000006,
1.0,
-1.0,
16_777_215.0,
16_777_216.0,
-16_777_216.0,
f32::MAX,
f32::MIN,
f32::INFINITY,
f32::NEG_INFINITY,
];
let mut x = -4.0f32;
while x < 4.0 {
cases.push(x);
x += 0.0625;
}
for value in cases {
let expected = (value + 0.5f32).floor();
let expected = if expected >= i32::MAX as f32 {
i32::MAX
} else if expected <= i32::MIN as f32 {
i32::MIN
} else {
expected as i32
};
assert_eq!(
quantizer.quantize_float(value),
expected,
"quantizing {value}"
);
}
}
}