Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

epaint: Memoize individual lines during text layout #5411

Open
wants to merge 17 commits into
base: master
Choose a base branch
from

Conversation

afishhh
Copy link

@afishhh afishhh commented Nov 28, 2024

This is an almost complete implementation of the approach described by emilk in this comment, excluding CoW semantics for LayoutJob (but including them for Row).
It supersedes the previous unsuccessful attempt here: #4000.

Draft because:

  • Currently individual rows will have ends_with_newline always set to false.
    This breaks selection with Ctrl+A (and probably many other things)
  • The whole block for doing the splitting and merging should probably become a function (I'll do that later).
  • I haven't run the check script, the tests, and haven't made sure all of the examples build (although I assume they probably don't rely on Galley internals).
  • Layout is sometimes incorrect (missing empty lines, wrapping sometimes makes text overlap).
  • A lot of text-related code had to be changed so this needs to be properly tested to ensure no layout issues were introduced, especially relating to the now row-relative coordinate system of Rows. Also this requires that we're fine making these very breaking changes.

It does significantly improve the performance of rendering large blocks of text (if they have many newlines), this is the test program I used to test it (adapted from #3086):

code
use eframe::egui::{self, CentralPanel, TextEdit};
use std::fmt::Write;

fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
        ..Default::default()
    };

    eframe::run_native(
        "editor big file test",
        options,
        Box::new(|_cc| Ok(Box::<MyApp>::new(MyApp::new()))),
    )
}

struct MyApp {
    text: String,
}

impl MyApp {
    fn new() -> Self {
        let mut string = String::new();
        for line_bytes in (0..50000).map(|_| (0u8..50)) {
            for byte in line_bytes {
                write!(string, " {byte:02x}").unwrap();
            }
            write!(string, "\n").unwrap();
        }
        println!("total bytes: {}", string.len());
        MyApp { text: string }
    }
}

impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        CentralPanel::default().show(ctx, |ui| {
            let start = std::time::Instant::now();
            egui::ScrollArea::vertical().show(ui, |ui| {
                let code_editor = TextEdit::multiline(&mut self.text)
                    .code_editor()
                    .desired_width(f32::INFINITY)
                    .desired_rows(40);
                let response = code_editor.show(ui).response;
                if response.changed() {
                    println!("total bytes now: {}", self.text.len());
                }
            });
            let end = std::time::Instant::now();
            let time_to_update = end - start;
            if time_to_update.as_secs_f32() > 0.5 {
                println!("Long update took {:.3}s", time_to_update.as_secs_f32())
            }
        });
    }
}

I think the way to proceed would be to make a new type, something like PositionedRow, that would wrap an Arc<Row> but have a separate pos and ends_with_newline (that would mean Row only holds a size instead of a rect). This type would of course have getters that would allow you to easily get a Rect from it and probably a Deref to the underlying Row.
I haven't done this yet because I wanted to get some opinions whether this would be an acceptable API first. This is now implemented, but of course I'm still open to discussion about this approach and whether it's what we want to do.

Breaking changes (currently):

  • The Galley::rows field has a different type.
  • There is now a PlacedRow wrapper for Row.
  • Row now uses an absolute coordinate system relative to itself instead of the Galley.

Copy link

Preview available at https://egui-pr-preview.github.io/pr/5411-cache_galley_lines
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.

@afishhh
Copy link
Author

afishhh commented Nov 29, 2024

Okay, so I did the thing and implemented the Row wrapper. This turned out to be more difficult than expected, and requires changing the semantics of Rows themselves. glyphs and the mesh(_bounds) must be relative to the Row itself, instead of the whole Galley, as otherwise repositioning them wouldn't work (unless we did something hacky like store two different offsets). Currently I also implemented Deref<Target = Row> for the PlacedRow wrapper (bike-shedding appreciated) which may not be the best idea as it makes it easy to confuse the different coordinate systems of the offset and non-offset Row.

Currently this "somewhat" works, I've managed to fix the selection issues I was seeing, but there is some layout issues you can actually see in the live demo above where the text is overlapping sometimes when wrapped (since this was present before the whole PlacedRow mess, I think this may be related to galley-merging).

@afishhh
Copy link
Author

afishhh commented Nov 30, 2024

So I've managed to make this look pretty correct, these are the remaining issues:

  • Rows now have very weird semantics because an empty Row that is in-between two non-empty lines will actually have a size of zero. This could be solved by actually incorporating the line-terminating newline into the cached job, but there is one issue with that approach because then each of the line galleys will now contain a trailing Row that would have to be treated specially by the merging code.
  • I've definitely introduced changes in rounding which is probably what makes the snapshot tests fail (apart from the fact that they sometimes segfault on my machine).
    Issues now solved (except the rendering_tests are still utterly broken on my machine)

@afishhh
Copy link
Author

afishhh commented Nov 30, 2024

I think I've made some good progress:

  • Made Row hold ends_with_newline again and just dropped excess rows during galley-merging, this means Row once again has pretty sensible semantics.
  • Properly respecting LayoutJob::round_output_size_to_nearest_ui_point fixed a lot of the snapshot test failures.

The remaining test failures are:

  • All rendering tests seem to have slight differences but in separators not in text which suggests that sizing might be ever so slightly wrong. Turns out this also happens on master for me, so it probably is unrelated to my changes.
    It looks like I can make more tests pass by using software rendering (WGPU_ADAPTER_NAME=llvmpipe) but still not all of them (dpi_1.25 and dpi_1.75 fail), so I'm going to pass this off as the graphics driver's fault.
  • The "Misc Demos" snapshot test also seems to fail, possibly due to a slightly incorrect alignment after the button widget? I've managed to remove the most hacky line of this patch and fix this in one fell swoop!.

I'm wondering whether it is worth fixing fixing these tests, should I just regenerate the snapshots instead?
Also I think stats don't take into account the fact multiple Galleys can point to the same Row.

@afishhh afishhh marked this pull request as ready for review December 1, 2024 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Optimizing re-layout of 1MB+ pieces of text in a TextEdit
1 participant