mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 10:27:44 -06:00
#3027: renders reuse one render context instead of a thread pool each
Every page, area and icon render built its own multi-threaded vello_cpu RenderContext, which starts and tears down a rayon thread pool: a few threads per keystroke while typing. Renders now take one kept context, reset to their size, and give it back; a render that overlaps another builds its own, and one that panics drops its context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: progress log: a header-cell picture watermark sits behind the text, #1775; previously trivial: symbols keep their font when the text font changes, #2036; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** trivial (typing no longer starts render threads on every repaint, #3027; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: progress log: a header-cell picture watermark sits behind the text, #1775; previously trivial: symbols keep their font when the text font changes, #2036; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: progress log: pasted blockquotes are indented paragraphs, #1270; previously trivial: progress log: print-area strings translated in every catalog; previously trivial: progress log: comments readable in the dark theme, #2045; previously trivial: progress log: ODT sections, #1379; previously trivial: progress log: table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls, #1329; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
|
||||
|
||||
@@ -142,6 +142,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
|
||||
| 2026-10-18 | Clearing a font query with no matches keeps Recently Used Fonts visible at the top of a scrollable list. The initial scroll position no longer skips its heading; measured keyboard scrolling is unchanged (#2068). |
|
||||
| 2026-10-17 | Edit Recipient List finds records (#1576): a Find in list box hides rows with no cell containing the text (any case) and counts the matches, and "Show only records that will be merged" hides the rows the filter leaves out; both only change the view. Rows are striped, and edits and removes go to the right record however the grid is narrowed |
|
||||
| 2026-10-14 | DOCX row table border exceptions render and survive saving. Explicit cell borders win; a row exception does not change other rows. |
|
||||
| 2026-10-19 | Typing no longer starts render threads on every repaint (#3027): page, area and icon renders share one kept vello_cpu context, reset to each size, instead of building a multi-threaded one (and its thread pool) per render. Typing about 150 characters in 15 s created 295–388 threads before and none now; CPU time while typing fell by about a third (Windows, debug build). |
|
||||
| 2026-10-14 | Tab stops edited on the ruler: the corner selector picks Left/Center/Right/Decimal/Bar, a click sets a stop (Alt skips the snap), dragging moves it with a guide line and position tooltip, dragging off the ruler clears it; markers draw per kind and mirror for RTL; one `para.tabs` call and one undo step per gesture |
|
||||
| 2026-10-12 | Fields keep `w:dirty` through DOCX saves (#877): a begin marker flagged by a generator that couldn't compute a result (pandoc-style TOC placeholders) keeps the flag on open/save — as a buffered field or a range field — so Word still refreshes it on open; computing a result (Update Field, Update Table) clears it, like Word's F9 |
|
||||
| 2026-10-18 | Symbols keep their font (#2036): a `w:sym` or Insert › Symbol character in Wingdings, Symbol or Webdings remembers its symbol font apart from the text's font, so selecting all and changing the font (or Clear Formatting) no longer turns the symbols into boxes; saved back to DOCX as `w:sym`, so Word shows them too. Typed text and the Format Painter don't pick up the symbol font. |
|
||||
@@ -503,6 +504,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps
|
||||
|---|---|---|
|
||||
| 2026-10-18 | trivial | Font list starts at its first row after clearing a no-match query; recent heading remains visible (#2068) |
|
||||
| 2026-10-18 | trivial | A word after a slash wraps whole, like Word (#2963) |
|
||||
| 2026-10-19 | trivial | Progress log: render threads reused while typing (#3027) |
|
||||
| 2026-10-17 | trivial | Progress log: recipient list search and merged-only view (#1576) |
|
||||
| 2026-10-18 | trivial | Progress log: symbols keep their font when the text font changes (#2036) |
|
||||
| 2026-10-17 | trivial | Progress log: Restrict Editing guards tables, Paste Formatting and merge fields (#1327) |
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
//! One kept vello_cpu render context, so rendering reuses its worker threads.
|
||||
//!
|
||||
//! A multi-threaded `RenderContext` starts its own rayon thread pool when it is built and
|
||||
//! stops it when dropped. Building one per render started and tore down a pool for every
|
||||
//! repaint: about 50 threads a second while typing, each spin-waiting before it exits (#3027).
|
||||
//! Renders now [`take`](ContextPool::take) the kept context, reset to their size, and
|
||||
//! [`give_back`](ContextPool::give_back) it when done.
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Mutex, PoisonError};
|
||||
|
||||
use vello_cpu::{RenderContext, RenderSettings};
|
||||
|
||||
/// A render context kept between renders.
|
||||
pub(crate) struct ContextPool {
|
||||
slot: Mutex<Option<RenderContext>>,
|
||||
/// How many contexts (so thread pools) were built: one, unless renders overlap.
|
||||
built: AtomicUsize,
|
||||
}
|
||||
|
||||
/// The context page, area and icon renders share.
|
||||
pub(crate) static CONTEXTS: ContextPool = ContextPool::new();
|
||||
|
||||
impl ContextPool {
|
||||
pub(crate) const fn new() -> Self {
|
||||
ContextPool { slot: Mutex::new(None), built: AtomicUsize::new(0) }
|
||||
}
|
||||
|
||||
/// A context reset to `w`×`h`: the kept one, or a new one with [`crate::default_threads`]
|
||||
/// workers while another render has it (another thread, or a render inside a render).
|
||||
pub(crate) fn take(&self, w: u16, h: u16) -> RenderContext {
|
||||
let kept = self.slot.lock().unwrap_or_else(PoisonError::into_inner).take();
|
||||
match kept {
|
||||
Some(mut ctx) => {
|
||||
ctx.reset_and_resize(w, h);
|
||||
ctx
|
||||
}
|
||||
None => {
|
||||
self.built.fetch_add(1, Ordering::Relaxed);
|
||||
RenderContext::new_with(w, h, RenderSettings { num_threads: crate::default_threads(), ..Default::default() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep `ctx` for the next [`take`](Self::take), unless one is kept already (then `ctx` and
|
||||
/// its threads go). A render that panics never gets here, so a half-drawn context is dropped
|
||||
/// rather than reused.
|
||||
pub(crate) fn give_back(&self, ctx: RenderContext) {
|
||||
let mut slot = self.slot.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if slot.is_none() {
|
||||
*slot = Some(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// How many contexts this pool has built.
|
||||
#[cfg(test)]
|
||||
fn built(&self) -> usize {
|
||||
self.built.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{MAX_SIDE, RenderOptions, Rendered, render_page, render_region};
|
||||
use vello_cpu::kurbo::{self, Affine, BezPath, Shape};
|
||||
use vello_cpu::{Resources, peniko};
|
||||
use wordcraft_doc::props::{Highlight, Rgb};
|
||||
use wordcraft_doc::{Document, Pos};
|
||||
use wordcraft_layout::{LayoutCache, LayoutOptions, Page, layout};
|
||||
|
||||
/// Draw a scene that uses what the renderer uses (fills, strokes, transforms, the even-odd
|
||||
/// rule, opacity and clip layers) across a `w`×`h` context and rasterise it.
|
||||
fn scene(ctx: &mut RenderContext, w: u16, h: u16) -> Rendered {
|
||||
let (fw, fh) = (w as f64, h as f64);
|
||||
ctx.set_paint(crate::color(Rgb(0xF4, 0xEE, 0xD8), 1.0));
|
||||
ctx.fill_rect(&kurbo::Rect::new(0.0, 0.0, fw, fh));
|
||||
ctx.set_transform(Affine::translate((fw / 2.0, fh / 2.0)) * Affine::rotate(0.3));
|
||||
ctx.set_paint(crate::color(Rgb(0x2B, 0x57, 0x9A), 1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new((fw.min(fh) / 20.0).max(1.0)));
|
||||
ctx.stroke_rect(&kurbo::Rect::new(-fw / 3.0, -fh / 3.0, fw / 3.0, fh / 3.0));
|
||||
let mut ring = BezPath::new();
|
||||
for r in [fw.min(fh) / 3.0, fw.min(fh) / 6.0] {
|
||||
ring.move_to((r, 0.0));
|
||||
ring.line_to((0.0, r));
|
||||
ring.line_to((-r, 0.0));
|
||||
ring.line_to((0.0, -r));
|
||||
ring.close_path();
|
||||
}
|
||||
ctx.set_fill_rule(peniko::Fill::EvenOdd);
|
||||
ctx.set_paint(crate::color(Rgb(0xC0, 0x30, 0x30), 1.0));
|
||||
ctx.push_opacity_layer(0.6);
|
||||
ctx.fill_path(&ring);
|
||||
ctx.pop_layer();
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_clip_layer(&kurbo::Rect::new(0.0, 0.0, fw / 2.0, fh).to_path(0.1));
|
||||
ctx.set_paint(crate::color(Rgb(0x20, 0x90, 0x40), 0.5));
|
||||
ctx.fill_rect(&kurbo::Rect::new(0.0, fh / 4.0, fw, fh * 3.0 / 4.0));
|
||||
ctx.pop_layer();
|
||||
ctx.flush();
|
||||
let mut pixels = vec![0u8; w as usize * h as usize * 4];
|
||||
if let Some(pm) = vello_cpu::PixmapMut::new(w, h, &mut pixels) {
|
||||
ctx.render(pm, &mut Resources::new());
|
||||
}
|
||||
Rendered { width: w as u32, height: h as u32, pixels }
|
||||
}
|
||||
|
||||
/// [`scene`] through `pool`.
|
||||
fn pooled(pool: &ContextPool, w: u16, h: u16) -> Rendered {
|
||||
let mut ctx = pool.take(w, h);
|
||||
let img = scene(&mut ctx, w, h);
|
||||
pool.give_back(ctx);
|
||||
img
|
||||
}
|
||||
|
||||
/// [`scene`] on a context built for it alone, as every render was before #3027.
|
||||
fn fresh(w: u16, h: u16) -> Rendered {
|
||||
scene(&mut RenderContext::new_with(w, h, RenderSettings { num_threads: crate::default_threads(), ..Default::default() }), w, h)
|
||||
}
|
||||
|
||||
fn same(a: &Rendered, b: &Rendered, what: &str) {
|
||||
assert_eq!((a.width, a.height), (b.width, b.height), "{what}: size");
|
||||
assert!(a.pixels == b.pixels, "{what}: different pixels");
|
||||
}
|
||||
|
||||
/// #3027: one context (one thread pool) serves renders of every size, growing and
|
||||
/// shrinking, down to 1×1 and out to the largest side, and each draws exactly what a
|
||||
/// context of its own would.
|
||||
#[test]
|
||||
fn one_context_serves_renders_of_every_size() {
|
||||
let pool = ContextPool::new();
|
||||
let max = MAX_SIDE as u16;
|
||||
let sizes = [(600, 800), (1, 1), (600, 800), (2, 3000), (3000, 2), (max, 1), (1, max), (255, 257), (600, 800)];
|
||||
for (w, h) in sizes {
|
||||
same(&pooled(&pool, w, h), &fresh(w, h), &format!("{w}x{h}"));
|
||||
}
|
||||
for _ in 0..50 {
|
||||
pooled(&pool, 300, 200);
|
||||
}
|
||||
assert_eq!(pool.built(), 1, "every render after the first reuses the kept context");
|
||||
let ctx = pool.take(1, 1);
|
||||
assert_eq!(ctx.is_multi_threaded(), crate::default_threads() > 0, "the kept context still renders on worker threads");
|
||||
}
|
||||
|
||||
/// Whatever the last render left in the context (transform, paint, stroke, fill rule, paint
|
||||
/// transform, a layer still pushed, a scene drawn but never rasterised) is gone before the
|
||||
/// next render draws.
|
||||
#[test]
|
||||
fn nothing_left_in_a_context_reaches_the_next_render() {
|
||||
let pool = ContextPool::new();
|
||||
let mut ctx = pool.take(400, 300);
|
||||
ctx.set_transform(Affine::rotate(0.7) * Affine::scale(3.0));
|
||||
ctx.set_paint(crate::color(Rgb(200, 0, 0), 1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new(25.0));
|
||||
ctx.set_fill_rule(peniko::Fill::EvenOdd);
|
||||
ctx.set_paint_transform(Affine::translate((40.0, 40.0)));
|
||||
ctx.push_opacity_layer(0.3);
|
||||
ctx.fill_rect(&kurbo::Rect::new(0.0, 0.0, 120.0, 80.0));
|
||||
ctx.stroke_rect(&kurbo::Rect::new(10.0, 10.0, 90.0, 60.0));
|
||||
pool.give_back(ctx);
|
||||
same(&pooled(&pool, 400, 300), &fresh(400, 300), "after an abandoned scene");
|
||||
same(&pooled(&pool, 120, 90), &fresh(120, 90), "smaller, after a full render");
|
||||
assert_eq!(pool.built(), 1);
|
||||
}
|
||||
|
||||
/// A render that panics never gives its context back: the next render builds a new one.
|
||||
/// A context that is already kept wins over one given back later, which is dropped.
|
||||
#[test]
|
||||
fn a_context_not_given_back_is_replaced_and_a_spare_is_dropped() {
|
||||
let pool = ContextPool::new();
|
||||
let lost = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let mut ctx = pool.take(200, 100);
|
||||
ctx.push_opacity_layer(0.5);
|
||||
ctx.fill_rect(&kurbo::Rect::new(0.0, 0.0, 50.0, 50.0));
|
||||
panic!("a draw failed half way");
|
||||
}));
|
||||
assert!(lost.is_err());
|
||||
same(&pooled(&pool, 200, 100), &fresh(200, 100), "after the panic");
|
||||
assert_eq!(pool.built(), 2, "the half-drawn context was dropped, not reused");
|
||||
let (a, b) = (pool.take(10, 10), pool.take(10, 10));
|
||||
assert_eq!(pool.built(), 3, "a render inside a render gets its own context");
|
||||
pool.give_back(a);
|
||||
pool.give_back(b);
|
||||
pooled(&pool, 10, 10);
|
||||
pooled(&pool, 10, 10);
|
||||
assert_eq!(pool.built(), 3, "one of the two was kept and the other dropped");
|
||||
}
|
||||
|
||||
/// Two threads rendering at the same time each get a context and both draw correctly;
|
||||
/// afterwards one context is kept and reused.
|
||||
#[test]
|
||||
fn renders_on_two_threads_at_once_both_draw() {
|
||||
let pool = ContextPool::new();
|
||||
let both_hold_one = std::sync::Barrier::new(2);
|
||||
let (a, b) = std::thread::scope(|s| {
|
||||
let run = || {
|
||||
let mut ctx = pool.take(240, 160);
|
||||
both_hold_one.wait();
|
||||
let img = scene(&mut ctx, 240, 160);
|
||||
pool.give_back(ctx);
|
||||
img
|
||||
};
|
||||
let (a, b) = (s.spawn(run), s.spawn(run));
|
||||
(a.join().expect("thread a"), b.join().expect("thread b"))
|
||||
});
|
||||
let want = fresh(240, 160);
|
||||
same(&a, &want, "thread a");
|
||||
same(&b, &want, "thread b");
|
||||
assert_eq!(pool.built(), 2, "neither thread waited for the other's context");
|
||||
pooled(&pool, 240, 160);
|
||||
assert_eq!(pool.built(), 2, "one of the two was kept");
|
||||
}
|
||||
|
||||
/// A page with text, a highlight and a coloured background.
|
||||
fn page() -> (Document, Page) {
|
||||
let mut d = Document::from_text("Reused contexts draw the same page as new ones.");
|
||||
d.settings.page_color = Some(Rgb(0xF4, 0xEE, 0xD8));
|
||||
d.format_range(&Pos::body(0, 0), &Pos::body(0, 6), &|c| c.highlight = Some(Highlight::Yellow)).expect("highlight");
|
||||
let l = layout(&d, &mut LayoutCache::new(), &LayoutOptions::default());
|
||||
let page = l.pages.first().cloned().expect("a page");
|
||||
(d, page)
|
||||
}
|
||||
|
||||
/// Through the shared pool: a page region drawn again after renders of other sizes and an
|
||||
/// icon comes out the same, and hostile sizes and scales still render, clamped.
|
||||
#[test]
|
||||
fn page_and_icon_renders_share_the_pool_without_traces() {
|
||||
let (d, page) = page();
|
||||
let opts = RenderOptions::default();
|
||||
let region = |w: u32, h: u32| render_region(&d, &page, w, h, Affine::scale(1.5), &opts);
|
||||
let first = region(500, 300);
|
||||
let icon = crate::icon_lib::ICONS.first().expect("an icon");
|
||||
let first_icon = crate::icon_lib::render(icon, 48, Rgb(0, 0x80, 0));
|
||||
for (w, h) in [(0, 0), (u32::MAX, 1), (1, u32::MAX), (0, 50), (900, 1200)] {
|
||||
let img = region(w, h);
|
||||
let (cw, ch) = (w.clamp(1, MAX_SIDE), h.clamp(1, MAX_SIDE));
|
||||
assert_eq!((img.width, img.height), (cw, ch), "{w}x{h}");
|
||||
assert_eq!(img.pixels.len(), cw as usize * ch as usize * 4, "{w}x{h}");
|
||||
}
|
||||
same(&crate::icon_lib::render(icon, 48, Rgb(0, 0x80, 0)), &first_icon, "the icon again");
|
||||
same(®ion(500, 300), &first, "the region again");
|
||||
let img = render_page(&d, &page, f32::NAN, &opts);
|
||||
assert!(img.width > 0 && img.height > 0, "a junk scale falls back to 1");
|
||||
// Other tests render at the same time, so how many the shared pool built can't be pinned
|
||||
// down here; that it built any shows page renders go through it.
|
||||
assert!(CONTEXTS.built() >= 1, "page and icon renders take the shared context");
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use vello_cpu::Resources;
|
||||
use vello_cpu::kurbo::{self, Affine, BezPath};
|
||||
use vello_cpu::{RenderContext, Resources};
|
||||
use wordcraft_doc::props::Rgb;
|
||||
|
||||
use crate::Rendered;
|
||||
@@ -633,7 +633,7 @@ fn flatten(d: &str) -> Result<Vec<(Vec<[f32; 2]>, bool)>, String> {
|
||||
/// Rasterise `icon` into a transparent `px`×`px` image in `ink` (premultiplied RGBA).
|
||||
pub fn render(icon: &Icon, px: u32, ink: Rgb) -> Rendered {
|
||||
let side = px.clamp(8, 4096) as u16;
|
||||
let mut ctx = RenderContext::new_with(side, side, vello_cpu::RenderSettings { num_threads: crate::default_threads(), ..Default::default() });
|
||||
let mut ctx = crate::context_pool::CONTEXTS.take(side, side);
|
||||
ctx.set_transform(Affine::scale(side as f64 / GRID as f64));
|
||||
ctx.set_paint(crate::color(ink, 1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new(STROKE as f64).with_caps(kurbo::Cap::Round).with_join(kurbo::Join::Round));
|
||||
@@ -658,6 +658,7 @@ pub fn render(icon: &Icon, px: u32, ink: Rgb) -> Rendered {
|
||||
if let Some(pm) = vello_cpu::PixmapMut::new(side, side, &mut pixels) {
|
||||
ctx.render(pm, &mut res);
|
||||
}
|
||||
crate::context_pool::CONTEXTS.give_back(ctx);
|
||||
Rendered { width: side as u32, height: side as u32, pixels }
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ pub fn default_threads() -> u16 {
|
||||
}
|
||||
}
|
||||
|
||||
mod context_pool;
|
||||
|
||||
/// Largest raster side vello_cpu handles comfortably.
|
||||
pub const MAX_SIDE: u32 = 16_000;
|
||||
|
||||
@@ -519,7 +521,7 @@ fn render_region_capped(doc: &Document, page: &Page, w: u32, h: u32, view: Affin
|
||||
// Over the cap the whole picture is drawn smaller, not cropped.
|
||||
let view = if (cw, ch) == (w, h) { view } else { Affine::scale_non_uniform(cw as f64 / w as f64, ch as f64 / h as f64) * view };
|
||||
let (w16, h16) = (cw as u16, ch as u16);
|
||||
let mut ctx = RenderContext::new_with(w16, h16, vello_cpu::RenderSettings { num_threads: default_threads(), ..Default::default() });
|
||||
let mut ctx = context_pool::CONTEXTS.take(w16, h16);
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.set_paint(color(opts.ink(doc.settings.page_color.unwrap_or(opts.paper)), 1.0));
|
||||
ctx.fill_rect(&kurbo::Rect::new(0.0, 0.0, w16 as f64, h16 as f64));
|
||||
@@ -538,6 +540,7 @@ fn render_region_capped(doc: &Document, page: &Page, w: u32, h: u32, view: Affin
|
||||
if let Some(pm) = vello_cpu::PixmapMut::new(w16, h16, &mut pixels) {
|
||||
ctx.render(pm, &mut res);
|
||||
}
|
||||
context_pool::CONTEXTS.give_back(ctx);
|
||||
Rendered { width: w16 as u32, height: h16 as u32, pixels }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WordCraft parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** trivial (shapes inserted in a row are all kept, #2019; previously trivial: East Asian typography: `w:eastAsianLayout` kept on save, not laid out; previously trivial: Ink to Shape recognises arrows; previously trivial: Windows Magnifier follows the caret onto a new line, #1119; previously trivial: table border drags with Ctrl and Alt, from the left edge and of selected cells, #711; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: all 12 bibliography styles, #383; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; replaces the parity tables that lived in ROADMAP.md) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** trivial (Performance row: typing no longer starts render threads on every repaint, #3027; previously trivial: shapes inserted in a row are all kept, #2019; previously trivial: East Asian typography: `w:eastAsianLayout` kept on save, not laid out; previously trivial: Ink to Shape recognises arrows; previously trivial: Windows Magnifier follows the caret onto a new line, #1119; previously trivial: table border drags with Ctrl and Alt, from the left edge and of selected cells, #711; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: all 12 bibliography styles, #383; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; replaces the parity tables that lived in ROADMAP.md) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
The authoritative assessment of how close WordCraft is to Microsoft Word. [`ROADMAP.md`](../ROADMAP.md)
|
||||
summarizes it; [`gaps.md`](gaps.md) itemizes every shortfall; the deep checklists are
|
||||
@@ -194,7 +194,7 @@ Word. Their weighted sum is the **full ready for real work** number.
|
||||
| Layout and pagination fidelity | 15% | 55% | 90–145 | [layout-parity.md](layout-parity.md) | Aptos has no metric-matched substitute so lines break differently from Word; no column balancing, footnote continuation or document grid; compatibility modes beyond 15 partial |
|
||||
| UI/UX fidelity | 10% | 65% | 65–100 | [ui-parity.md](ui-parity.md) | Ribbon, keytips, mini-toolbar, Backstage, 125 shortcuts; 16 modal dialogs against Word's ~100; no ribbon/keyboard customization |
|
||||
| Stability | 10% | 50% | 25–45 | [gaps.md](gaps.md) | Never-crash standard and hostile-param fuzzing in place, but open reports of a startup crash on Intel UHD (#170), freezes (#77, #59, #31) and Windows write failure without admin (#218); local desktop recovery with focused lifecycle tests, but no longer soak test; 5 days of field history |
|
||||
| Performance | 5% | 75% | 10–20 | [layout-parity.md](layout-parity.md) | 61 ms cold layout and 1.4 ms relayout on a 188-page document (measured 2026-10-06, not re-run); font picker freezes fixed in #233; no large real-world documents measured |
|
||||
| Performance | 5% | 75% | 10–20 | [layout-parity.md](layout-parity.md) | 61 ms cold layout and 1.4 ms relayout on a 188-page document (measured 2026-10-06, not re-run); font picker freezes fixed in #233; typing no longer starts and stops render threads on every repaint (#3027); no large real-world documents measured |
|
||||
| Localization | 4% | 27% | 75–120 + native review | [localization-parity.md](localization-parity.md) | 13 UI languages (Word 30); 4 of the 12 key languages partly done; English and Polish proofing |
|
||||
| Platforms | 3% | 95% | 5–10 | [ROADMAP.md](../ROADMAP.md) | macOS (universal, signed), Windows x64/x86/arm64, Linux (AppImage/deb/rpm/Flatpak), FreeBSD, web. Beyond Word on Linux, BSD and the web; Word's iPad/iPhone/Android apps are out of scope |
|
||||
| Hardware | 1% | 40% | 20–40 | [hardware-parity.md](hardware-parity.md) | No native printing on desktop, no pen/ink, no dictation; GPU drawing and HiDPI fine. Ink hours are in the Features row (Draw tab) |
|
||||
@@ -252,6 +252,7 @@ can be used without committing Word output.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | trivial | Pictures, shapes, text boxes: Insert › Shapes adds the new shape beside the selection, as Word draws it, so a second shape no longer replaces the first (still selected after its insert) and selected text stays; a picture still replaces a selected picture, as in Word (#2019) |
|
||||
| 2026-10-19 | trivial | Performance row: render threads reused while typing (#3027) |
|
||||
| 2026-10-15 | trivial | Pictures, shapes, text boxes: Picture Styles, Picture Border and Artistic Effects draw again from the picture under them, so a second style replaces the first instead of framing the frame (Thick Frame then Simple Frame is a simple frame), a border replaces the border and keeps the style, a style replaces the border, and an effect replaces the effect (None removes it), as in Word; transparency was already absolute (#2052) |
|
||||
| 2026-10-18 | trivial | References row: Manage Sources imports Word's `Sources.xml` bibliography file (#2640); no percentage change |
|
||||
| 2026-10-16 | trivial | East Asian typography row: `w:eastAsianLayout` kept on save, not laid out; no percentage change |
|
||||
|
||||
Reference in New Issue
Block a user