Functional parity with linux

This commit is contained in:
Will Temple
2026-05-15 13:31:59 -07:00
parent 861bf63621
commit 67400f1499
17 changed files with 2221 additions and 458 deletions

View File

@@ -91,7 +91,8 @@ struct PreparedGlyphBitmap {
struct RasterizedText {
origin_offset: Point,
size: UiSize,
draw_size: UiSize,
texture_size: UiSize,
pixels: Vec<u8>,
}
@@ -170,6 +171,7 @@ struct UploadedAtlasText {
struct TextTextureKey {
text: String,
bounds: Option<(u32, u32)>,
clip: Option<(u32, u32, u32, u32)>,
font_size_bits: u32,
line_height_bits: u32,
color: (u8, u8, u8, u8),
@@ -330,6 +332,7 @@ pub struct WgpuSceneRenderer {
image_cache_order: VecDeque<u64>,
#[allow(dead_code)]
glyph_atlas: GlyphAtlas,
scale_factor: f32,
}
const MAX_TEXT_CACHE_ENTRIES: usize = 64;
@@ -366,8 +369,8 @@ impl WgpuSceneRenderer {
format,
width: width.max(1),
height: height.max(1),
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: 2,
present_mode: wgpu::PresentMode::AutoNoVsync,
desired_maximum_frame_latency: 1,
alpha_mode: caps.alpha_modes[0],
view_formats: vec![],
};
@@ -710,6 +713,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
image_cache: HashMap::new(),
image_cache_order: VecDeque::new(),
glyph_atlas,
scale_factor: 1.0,
})
}
@@ -723,9 +727,33 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
self.surface.configure(&self.device, &self.config);
}
pub fn set_scale_factor(&mut self, scale_factor: f32) {
let next = scale_factor.max(1.0);
if (self.scale_factor - next).abs() <= f32::EPSILON {
return;
}
self.scale_factor = next;
// Text cache is scale-dependent; flush on scale transitions (e.g. display move).
self.text_cache.clear();
self.text_cache_order.clear();
self.glyph_atlas = create_glyph_atlas(
&self.device,
&self.text_bind_group_layout,
&self.text_sampler,
);
}
pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> {
self.render_with_logical_size(scene, scene.logical_size)
}
pub fn render_with_logical_size(
&mut self,
scene: &SceneSnapshot,
logical_size: UiSize,
) -> Result<(), RenderError> {
let render_start = std::time::Instant::now();
let vertices = build_vertices(scene);
let vertices = build_vertices(scene, logical_size);
let frame = match self.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
@@ -749,7 +777,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
let mut uploaded_images = Vec::new();
let mut uploaded_texts = Vec::new();
let mut atlas_perf = AtlasTextPerfStats::default();
let uploaded_atlas_text = self.prepare_uploaded_atlas_text(scene, &mut atlas_perf);
let uploaded_atlas_text =
self.prepare_uploaded_atlas_text(scene, logical_size, &mut atlas_perf);
let mut clip_stack = Vec::new();
let mut active_clip = ActiveClip::default();
for item in &scene.items {
@@ -760,17 +789,19 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
DisplayItem::Image(image) => {
if let Some(uploaded) =
self.prepare_uploaded_image(image, scene.logical_size, active_clip)
self.prepare_uploaded_image(image, logical_size, active_clip)
{
uploaded_images.push(uploaded);
}
}
DisplayItem::Text(text) => {
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
{
continue;
}
if let Some(uploaded) =
self.prepare_uploaded_text(text, scene.logical_size, active_clip)
self.prepare_uploaded_text(text, logical_size, active_clip)
{
uploaded_texts.push(uploaded);
}
@@ -854,21 +885,29 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
Ok(())
}
fn rasterize_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
return self.rasterize_prepared_text(text);
fn rasterize_text(
&mut self,
text: &PreparedText,
raster_clip: Option<Rect>,
) -> Option<RasterizedText> {
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
{
return self.rasterize_prepared_text(text, raster_clip);
}
let glyphs = self.collect_glyph_bitmaps(text);
let clip = match text.bounds {
Some(bounds) => PixelRect {
left: 0,
top: 0,
right: bounds.width.ceil() as i32,
bottom: bounds.height.ceil() as i32,
},
None => glyph_union(&glyphs)?,
};
let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, self.scale_factor));
let glyphs = self.collect_glyph_bitmaps(text, clip);
let clip = clip
.or_else(|| {
text.bounds.map(|bounds| PixelRect {
left: 0,
top: 0,
right: (bounds.width * self.scale_factor).ceil() as i32,
bottom: (bounds.height * self.scale_factor).ceil() as i32,
})
})
.or_else(|| glyph_union(&glyphs))?;
if clip.width() == 0 || clip.height() == 0 {
return None;
}
@@ -879,23 +918,36 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
}
Some(RasterizedText {
origin_offset: Point::new(clip.left as f32, clip.top as f32),
size: UiSize::new(clip.width() as f32, clip.height() as f32),
origin_offset: Point::new(
clip.left as f32 / self.scale_factor,
clip.top as f32 / self.scale_factor,
),
draw_size: UiSize::new(
clip.width() as f32 / self.scale_factor,
clip.height() as f32 / self.scale_factor,
),
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
pixels,
})
}
fn rasterize_prepared_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
fn rasterize_prepared_text(
&mut self,
text: &PreparedText,
raster_clip: Option<Rect>,
) -> Option<RasterizedText> {
let glyphs = self.collect_prepared_glyphs(text);
let clip = match text.bounds {
Some(bounds) => PixelRect {
left: 0,
top: 0,
right: bounds.width.ceil() as i32,
bottom: bounds.height.ceil() as i32,
},
None => glyph_union_prepared(&glyphs)?,
};
let clip = raster_clip
.map(rect_to_pixel_rect)
.or_else(|| {
text.bounds.map(|bounds| PixelRect {
left: 0,
top: 0,
right: bounds.width.ceil() as i32,
bottom: bounds.height.ceil() as i32,
})
})
.or_else(|| glyph_union_prepared(&glyphs))?;
if clip.width() == 0 || clip.height() == 0 {
return None;
}
@@ -914,7 +966,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
Some(RasterizedText {
origin_offset: Point::new(clip.left as f32, clip.top as f32),
size: UiSize::new(clip.width() as f32, clip.height() as f32),
draw_size: UiSize::new(clip.width() as f32, clip.height() as f32),
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
pixels,
})
}
@@ -955,13 +1008,20 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
glyphs
}
fn collect_glyph_bitmaps(&mut self, text: &PreparedText) -> Vec<GlyphBitmap> {
let mut buffer = Buffer::new_empty(Metrics::new(text.font_size, text.line_height));
fn collect_glyph_bitmaps(
&mut self,
text: &PreparedText,
raster_clip: Option<PixelRect>,
) -> Vec<GlyphBitmap> {
let mut buffer = Buffer::new_empty(Metrics::new(
text.font_size * self.scale_factor,
text.line_height * self.scale_factor,
));
{
let mut borrowed = buffer.borrow_with(&mut self.font_system);
borrowed.set_size(
text.bounds.map(|bounds| bounds.width),
text.bounds.map(|bounds| bounds.height),
text.bounds.map(|bounds| bounds.width * self.scale_factor),
text.bounds.map(|bounds| bounds.height * self.scale_factor),
);
borrowed.set_text(&text.text, &Attrs::new(), Shaping::Advanced, None);
}
@@ -982,13 +1042,17 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
if width == 0 || height == 0 {
continue;
}
let rect = PixelRect {
left: physical.x + image.placement.left,
top: physical.y - image.placement.top,
right: physical.x + image.placement.left + width,
bottom: physical.y - image.placement.top + height,
};
if raster_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
continue;
}
glyphs.push(GlyphBitmap {
rect: PixelRect {
left: physical.x + image.placement.left,
top: physical.y - image.placement.top,
right: physical.x + image.placement.left + width,
bottom: physical.y - image.placement.top + height,
},
rect,
content: image.content,
color: glyph
.color_opt
@@ -1003,8 +1067,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
fn prepare_uploaded_atlas_text(
&mut self,
scene: &SceneSnapshot,
logical_size: UiSize,
perf: &mut AtlasTextPerfStats,
) -> Option<UploadedAtlasText> {
if (self.scale_factor - 1.0).abs() > f32::EPSILON {
return None;
}
let mut vertices = Vec::new();
let mut clip_stack = Vec::new();
let mut active_clip = ActiveClip::default();
@@ -1073,7 +1141,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
glyph_rect,
atlas_glyph.atlas_rect,
clip_rect,
scene.logical_size,
logical_size,
resolved_color,
active_clip,
);
@@ -1246,9 +1314,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
logical_size: UiSize,
clip: ActiveClip,
) -> Option<UploadedText> {
let key = text_texture_key(text);
let raster_clip = text_raster_clip(text, clip, logical_size)?;
let key = text_texture_key(text, raster_clip);
if !self.text_cache.contains_key(&key) {
let rasterized = self.rasterize_text(text)?;
let rasterized = self.rasterize_text(text, raster_clip)?;
let cached = self.create_cached_text_texture(&rasterized);
self.text_cache.insert(key.clone(), cached);
}
@@ -1316,8 +1385,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("ruin-ui-renderer-wgpu-texture"),
size: wgpu::Extent3d {
width: text.size.width as u32,
height: text.size.height as u32,
width: text.texture_size.width as u32,
height: text.texture_size.height as u32,
depth_or_array_layers: 1,
},
mip_level_count: 1,
@@ -1337,12 +1406,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
&text.pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(text.size.width as u32 * 4),
rows_per_image: Some(text.size.height as u32),
bytes_per_row: Some(text.texture_size.width as u32 * 4),
rows_per_image: Some(text.texture_size.height as u32),
},
wgpu::Extent3d {
width: text.size.width as u32,
height: text.size.height as u32,
width: text.texture_size.width as u32,
height: text.texture_size.height as u32,
depth_or_array_layers: 1,
},
);
@@ -1365,7 +1434,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
_texture: texture,
bind_group,
origin_offset: text.origin_offset,
size: text.size,
size: text.draw_size,
}
}
@@ -1885,9 +1954,9 @@ fn color_to_f32(color: Color) -> [f32; 4] {
]
}
fn build_vertices(scene: &SceneSnapshot) -> Vec<Vertex> {
let width = scene.logical_size.width.max(1.0);
let height = scene.logical_size.height.max(1.0);
fn build_vertices(scene: &SceneSnapshot, logical_size: UiSize) -> Vec<Vertex> {
let width = logical_size.width.max(1.0);
let height = logical_size.height.max(1.0);
let mut vertices = Vec::new();
let mut clip_stack = Vec::new();
let mut active_clip = ActiveClip::default();
@@ -2149,6 +2218,22 @@ fn rect_to_pixel_rect(rect: Rect) -> PixelRect {
}
}
fn scale_rect_to_pixel_rect(rect: Rect, scale: f32) -> PixelRect {
PixelRect {
left: (rect.origin.x * scale).floor() as i32,
top: (rect.origin.y * scale).floor() as i32,
right: ((rect.origin.x + rect.size.width) * scale).ceil() as i32,
bottom: ((rect.origin.y + rect.size.height) * scale).ceil() as i32,
}
}
fn pixel_rects_intersect(first: PixelRect, second: PixelRect) -> bool {
first.left < second.right
&& first.right > second.left
&& first.top < second.bottom
&& first.bottom > second.top
}
fn shadow_blur_extent(blur: f32) -> f32 {
blur.max(0.0) * 2.0
}
@@ -2208,6 +2293,33 @@ fn intersect_rects(first: Option<Rect>, second: Option<Rect>) -> Option<Rect> {
}
}
fn text_raster_clip(
text: &PreparedText,
clip: ActiveClip,
logical_size: UiSize,
) -> Option<Option<Rect>> {
let mut absolute_clip = Some(Rect::new(0.0, 0.0, logical_size.width, logical_size.height));
if clip.rect_active {
absolute_clip = intersect_rects(absolute_clip, Some(clip.resolved_rect()?));
}
if let Some(bounds) = text.bounds {
let text_bounds = Rect::new(text.origin.x, text.origin.y, bounds.width, bounds.height);
absolute_clip = intersect_rects(absolute_clip, Some(text_bounds));
}
if absolute_clip.is_none() {
return None;
}
Some(absolute_clip.map(|rect| {
Rect::new(
rect.origin.x - text.origin.x,
rect.origin.y - text.origin.y,
rect.size.width,
rect.size.height,
)
}))
}
fn empty_clip_rect() -> Rect {
Rect::new(1.0, 1.0, -1.0, -1.0)
}
@@ -2330,12 +2442,20 @@ fn clip_visible_glyphs(text: &PreparedText, clip: Option<Rect>) -> &[GlyphInstan
&text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
}
fn text_texture_key(text: &PreparedText) -> TextTextureKey {
fn text_texture_key(text: &PreparedText, raster_clip: Option<Rect>) -> TextTextureKey {
TextTextureKey {
text: text.text.clone(),
bounds: text
.bounds
.map(|bounds| (bounds.width.to_bits(), bounds.height.to_bits())),
clip: raster_clip.map(|clip| {
(
clip.origin.x.to_bits(),
clip.origin.y.to_bits(),
clip.size.width.to_bits(),
clip.size.height.to_bits(),
)
}),
font_size_bits: text.font_size.to_bits(),
line_height_bits: text.line_height.to_bits(),
color: (
@@ -2365,10 +2485,7 @@ mod tests {
ActiveClip, blend_rgba, build_text_vertices, build_vertices, clip_rect_array,
push_clip_state, rounded_clip_arrays, text_texture_key,
};
use ruin_ui::{
ClipRegion, Color, GlyphInstance, Point, PreparedText, Rect, SceneSnapshot,
TextSelectionStyle, UiSize,
};
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
#[test]
fn quad_scenes_expand_to_six_vertices_per_quad() {
@@ -2389,7 +2506,7 @@ mod tests {
Color::rgb(0xFF, 0xFF, 0xFF),
));
let vertices = build_vertices(&scene);
let vertices = build_vertices(&scene, scene.logical_size);
assert_eq!(vertices.len(), 12);
}
@@ -2421,7 +2538,10 @@ mod tests {
Color::rgb(0xEE, 0xEE, 0xEE),
);
assert_eq!(text_texture_key(&first), text_texture_key(&second));
assert_eq!(
text_texture_key(&first, None),
text_texture_key(&second, None)
);
}
#[test]