Put positions on the grid, and decode them in the shader
`UiScalar` is `Rel` beside `Px` rather than two floats, so composing a position down a chain of boxes adds exactly and rounds only at the two multiplies `within` makes. `UiSpan`, `UiRegion` and `UiVec2` follow it, the hand-written `Hash` goes away with the bits it hashed, and `impl_op!` grows a `same` form for a type whose fields are not the same kind of number. `Len` is still floats, so the seam converts: `Px::from_f32` where a span adds a child's length to its cursor, and `to_f32` where something outside layout wants pixels. Those go when `Len` follows. The GPU reads what the CPU wrote: the instance attributes are `Sint32x2` and the shader decodes by `1/64` and `1/2^24`, both exact in `f32`, then composes the move chain in floats as before. It has to agree with itself frame to frame rather than with the CPU to the last bit. Two things fell out of making the numbers exact. `floor` at the rasteriser was picking the pixel below wherever a fraction divided a window exactly. A fifth of 1920 is 383.99998 through a rounded `Rel` -- and was 384.0 through an `f32` that happened to round up -- so five tabs each lost their last column. `snap_floor` takes a coordinate within half a step of a boundary to be on it, which is the same rule as everywhere else here: decide where values do not land. A widget measured on one layer and drawn again on another kept the first layer, because `try_reuse` compared everything about a retained drawing except which list it sits in. `Stack` does exactly that for its background, so every panel's text went under its own background. It only worked before because the two asks differed by a rounding and forced a redraw; `tests/retained.rs` pins it now, and `ReuseOutcome` can say `WrongLayer`. Checked: fmt, clippy, 100 tests, 100 generated seeds in 70 s, all five shrinker cases at 300 seeds. `tabs`, `view` and `minimal` render byte-identical at 1920x1200; `random` differs in 36 pixels by one level; `text` differs where glyph origins moved onto the grid -- same positions, same spacing, different subpixel coverage, checked at 6x against the old render. Measured on the way: with the fuzzer comparing for *equality* rather than within 0.05 px, `resize`, `repaint` and `size-change` already pass 100 seeds. `reorder` fails one seed by exactly one step, which is the `Len` seam above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7548139861
commit
4e28f1047e
19 files changed
+256
-138
No files matched your search
+19
-5
@@ -38,6 +38,9 @@ pub type Rel = Fixed<24>;
|
||||
impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
pub const ZERO: Self = Self(0);
|
||||
pub const ONE: Self = Self::one();
|
||||
/// The gap between neighbouring values, which is also how far apart two
|
||||
/// numbers can be and still mean the same place.
|
||||
pub const STEP: Self = Self(1);
|
||||
/// Also what stands in for an unbounded end, since arithmetic saturates
|
||||
/// here rather than wrapping past it.
|
||||
pub const MIN: Self = Self(i32::MIN);
|
||||
@@ -65,14 +68,25 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||
/// Rounds to the nearest step, and saturates rather than wrapping. A NaN
|
||||
/// has no nearest step and becomes zero, which is a caller's mistake
|
||||
/// rather than a value worth carrying.
|
||||
pub fn from_f32(v: f32) -> Self {
|
||||
///
|
||||
/// Half-away is written out rather than called through `f32::round`,
|
||||
/// which is not `const`: a layout constant has to stay a constant.
|
||||
pub const fn from_f32(v: f32) -> Self {
|
||||
debug_assert!(!v.is_nan(), "a NaN has no place on the grid");
|
||||
// Float-to-int casts saturate and send NaN to zero, which is the
|
||||
// behaviour wanted at both ends.
|
||||
Self((v * Self::one().0 as f32).round() as i32)
|
||||
let scaled = v * Self::one().0 as f32;
|
||||
// Above 2^23 an `f32` has no fractional part left to round, and
|
||||
// adding a half there rounds the number itself up instead. The cast
|
||||
// saturates at both ends and sends NaN to zero, which is the
|
||||
// behaviour wanted at both.
|
||||
const WHOLE: f32 = (1 << 23) as f32;
|
||||
Self(match (scaled >= WHOLE, scaled <= -WHOLE, scaled < 0.0) {
|
||||
(true, _, _) | (_, true, _) => scaled as i32,
|
||||
(_, _, true) => (scaled - 0.5) as i32,
|
||||
_ => (scaled + 0.5) as i32,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_f32(self) -> f32 {
|
||||
pub const fn to_f32(self) -> f32 {
|
||||
self.0 as f32 / Self::one().0 as f32
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user