This commit is contained in:
Brandon Thomas
2026-10-15 00:35:27 -04:00
6 changed files with 94 additions and 6 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ pub fn paint_mark(ui: &egui::Ui, rect: Rect, color: Color32) {
let mut res = vello_cpu::Resources::new();
rc.render(&mut pm, &mut res);
let ci = egui::ColorImage::from_rgba_premultiplied([px as usize, px as usize], pm.data_as_u8_slice());
let t = ctx.load_texture("artcraft_mark", ci, egui::TextureOptions::LINEAR);
let t = crate::widgets::load_texture(ctx, "artcraft_mark", ci, egui::TextureOptions::LINEAR);
ctx.data_mut(|d| d.insert_temp(key, t.clone()));
t
});
+39 -1
View File
@@ -587,6 +587,11 @@ fn patch_texture(app: &mut DesignApp, layout: &CanvasLayout, ppp: f64, doc_key:
let Some(regions) = designcraft_render::damage::damage_with(&old, &new, Some(&app.session.cache)) else { return false };
let t0 = crate::now_ms();
let (tw, th) = ((sh.size.0 as f64 * ppp).round() as i64, (sh.size.1 as f64 * ppp).round() as i64);
// A texture `upload` sampled down to the GPU limit has other pixels than the view: a region
// computed in view pixels would be written outside it. Render it whole instead.
if app.canvas.texture.as_ref().is_none_or(|t| t.size() != [tw.max(0) as usize, th.max(0) as usize]) {
return false;
}
let k = sh.zoom * ppp;
let mut px: Option<(i64, i64, i64, i64)> = None;
for (r, rect) in &regions {
@@ -667,6 +672,7 @@ fn upload(app: &mut DesignApp, ctx: &egui::Context, mut img: designcraft_render:
designcraft_render::separation_view(&mut img, None, app.ui.ink_limit);
}
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
let ci = crate::widgets::fit_texture(ci, ctx.input(|i| i.max_texture_side));
match &mut app.canvas.texture {
Some(tex) => tex.set(ci, egui::TextureOptions::LINEAR),
None => app.canvas.texture = Some(ctx.load_texture("canvas", ci, egui::TextureOptions::LINEAR)),
@@ -1336,7 +1342,7 @@ fn draw_inverse_highlight(
let mut res = vello_cpu::Resources::new();
rc.render(&mut pm, &mut res);
let ci = egui::ColorImage::from_rgba_premultiplied([w as usize, h as usize], pm.data_as_u8_slice());
let t = ctx.load_texture("inverse_selection", ci, egui::TextureOptions::NEAREST);
let t = crate::widgets::load_texture(ctx, "inverse_selection", ci, egui::TextureOptions::NEAREST);
ctx.data_mut(|d| d.insert_temp(cache_id, (key, t.clone())));
t
}
@@ -2238,6 +2244,38 @@ mod tests {
(s.horizontal_units, s.vertical_units)
}
/// A viewport wider than the GPU's largest texture: the canvas texture is sampled down, and an
/// edit repaints it whole. A region repainted in view pixels would land in the wrong place,
/// or outside the texture (a wgpu validation error). A view that fits is still patched.
#[test]
fn an_edit_repaints_a_canvas_texture_sampled_down_to_the_gpu_limit_whole() {
for (width, sampled_down) in [(1440.0, false), (2600.0, true)] {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let frame = app.run("frame.create", json!({"rect": [72, 72, 400, 300], "content": "text", "text": "Text"})).unwrap();
let mut h = Harness::builder().with_size(vec2(width, 900.0)).build_ui_state(
|ui, app: &mut DesignApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
app,
);
h.run_steps(6);
let limit = h.ctx.input(|i| i.max_texture_side);
let size = h.state().canvas.texture.as_ref().map(|t| t.size()).unwrap();
assert!(size[0] <= limit && size[1] <= limit, "{size:?}");
let shown = h.state().canvas.shown.unwrap().size;
assert_eq!(shown.0 > limit as f32, sampled_down, "view {shown:?}, limit {limit}");
let patches = h.state().canvas.patches;
// A small edit: one more character in the frame.
h.state_mut().run("text.insert", json!({"story": frame["story"], "at": 4, "text": "s"})).unwrap();
h.run_steps(6);
assert_eq!(h.state().canvas.patches == patches, sampled_down, "width {width}");
assert_eq!(h.state().canvas.texture.as_ref().map(|t| t.size()), Some(size));
}
}
#[test]
fn selected_merged_owners_paint_the_same_row_and_column_targets() {
for column in [true, false] {
+6 -2
View File
@@ -76,7 +76,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
let bar = scroll.bar_inner_margin + scroll.bar_width + scroll.bar_outer_margin;
let cols = (((ui.available_width() - bar) / CELL).floor() as u32).max(1);
let ppp = ui.ctx().pixels_per_point();
let cell_px = (CELL * ppp).round() as u32;
let cell_px = (CELL * ppp).round().max(1.0) as u32;
// The grid is one texture: no more rows than the GPU's largest texture holds (a narrow
// panel of a large CJK font ran past it).
let max_rows = (ui.ctx().input(|i| i.max_texture_side) as u32 / cell_px).max(1);
chars.truncate((max_rows * cols) as usize);
let ink = t.text_strong;
let key = egui::Id::new(("glyph_grid", &st.family, &st.style, scope, &q, cols, cell_px, ink.to_array()));
let tex: egui::TextureHandle = match ui.data(|d| d.get_temp::<egui::TextureHandle>(key)) {
@@ -84,7 +88,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
None => {
let img = designcraft_render::glyphs::glyph_grid(&db, &st.family, &st.style, &chars, cols, cell_px, ink.to_array());
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
let h = ui.ctx().load_texture("glyph_grid", ci, egui::TextureOptions::LINEAR);
let h = crate::widgets::load_texture(ui.ctx(), "glyph_grid", ci, egui::TextureOptions::LINEAR);
ui.data_mut(|d| d.insert_temp(key, h.clone()));
h
}
+1 -1
View File
@@ -447,7 +447,7 @@ pub fn font_menu_body(app: &mut DesignApp, ui: &mut egui::Ui, menu: &[designcraf
let tex = tex.unwrap_or_else(|| {
let img = designcraft_render::glyphs::text_line(&fonts, f, "Regular", "Sample", (18.0 * ppp) as u32, t.text.to_array());
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
let h = ui.ctx().load_texture(format!("font_preview_{f}"), ci, egui::TextureOptions::LINEAR);
let h = crate::widgets::load_texture(ui.ctx(), format!("font_preview_{f}"), ci, egui::TextureOptions::LINEAR);
ui.data_mut(|d| d.insert_temp(key, h.clone()));
h
});
+1 -1
View File
@@ -34,7 +34,7 @@ fn thumb(app: &mut DesignApp, ctx: &egui::Context, abs: usize, h: f32) -> Option
&designcraft_render::RenderOptions { printing_only: true, ..Default::default() },
)?;
let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
let tex = ctx.load_texture(format!("thumb{abs}"), ci, egui::TextureOptions::LINEAR);
let tex = crate::widgets::load_texture(ctx, format!("thumb{abs}"), ci, egui::TextureOptions::LINEAR);
// Drop thumbnails of older revisions.
let (uid, rev) = (st.uid, st.revision);
map.retain(|k, _| !(k.0 == uid && k.1 != rev));
+46
View File
@@ -717,3 +717,49 @@ mod tests {
assert_eq!(m.step(), 1.0);
}
}
/// `img` made to fit the GPU's largest texture (`max_side` pixels a side), sampled down
/// (nearest pixel) when it is larger. A texture past the limit panics in the renderer, so every
/// UI texture goes through this: an oversized image loses resolution, not the document.
pub fn fit_texture(img: egui::ColorImage, max_side: usize) -> egui::ColorImage {
let [w, h] = img.size;
let max_side = max_side.max(1);
if w <= max_side && h <= max_side {
return img;
}
let k = (max_side as f64 / w.max(h) as f64).min(1.0);
let (nw, nh) = (((w as f64 * k).floor() as usize).clamp(1, max_side), ((h as f64 * k).floor() as usize).clamp(1, max_side));
let mut pixels = Vec::with_capacity(nw * nh);
for y in 0..nh {
let sy = (y * h / nh).min(h.saturating_sub(1));
for x in 0..nw {
let sx = (x * w / nw).min(w.saturating_sub(1));
pixels.push(img.pixels.get(sy * w + sx).copied().unwrap_or(Color32::TRANSPARENT));
}
}
egui::ColorImage::new([nw, nh], pixels)
}
/// [`egui::Context::load_texture`] for an image of any size (see [`fit_texture`]).
pub fn load_texture(ctx: &egui::Context, name: impl Into<String>, img: egui::ColorImage, options: egui::TextureOptions) -> egui::TextureHandle {
let max = ctx.input(|i| i.max_texture_side);
ctx.load_texture(name, fit_texture(img, max), options)
}
#[cfg(test)]
mod texture_tests {
/// The glyph grid that crashed the renderer: 7 columns of 1200 characters at 60 px, 10320 px
/// tall against an 8192 px limit.
#[test]
fn oversized_images_fit_the_texture_limit() {
let img = egui::ColorImage::new([420, 10320], vec![egui::Color32::WHITE; 420 * 10320]);
let out = super::fit_texture(img, 8192);
assert!(out.size[0] <= 8192 && out.size[1] == 8192, "{:?}", out.size);
assert_eq!(out.pixels.len(), out.size[0] * out.size[1]);
let small = egui::ColorImage::new([10, 10], vec![egui::Color32::RED; 100]);
assert_eq!(super::fit_texture(small, 8192).size, [10, 10]);
let wide = egui::ColorImage::new([20000, 3], vec![egui::Color32::RED; 60000]);
let out = super::fit_texture(wide, 8192);
assert!(out.size[0] == 8192 && out.size[1] >= 1);
}
}