Functional parity with linux
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user