Skip to content

Canvas cache performance regression with large images in 0.14 #3173

@dirkhillbrecht

Description

@dirkhillbrecht

Is your issue REALLY a bug?

  • My issue is indeed a bug!
  • I am not crazy! I will not fill out this form just to ask a question or request a feature. Pinky promise.

Is there an existing issue for this?

  • I have searched the existing issues.

Is this issue related to iced?

  • My hardware is compatible and my graphics drivers are up-to-date.

What happened?

I draw an image from a source of arbitrary size and pixel source into a Canvas which may have a different size. The Canvas uses a Cache so the actual drawing code is in the Cache's closure parameter.
On updates, the image is flickering and for prolonged times replaced with white content. This is the more severe the larger the original image is - the size of the Canvas does not really matter.
The attached demo program shows the problem on my platform (Linux, X11, Ubuntu 24.04, Rust 1.92). Start it with the side length of a blue image rectangle and resize the screen. With a size length of 100 ("cargo run 100") rescaling is smooth. From size ~700 onward, the redrawing becomes more and more flickerish.

// Minimal demo program for the Iced 0.14 flicker behaviour

// Situation is: An image is generated from a pixel source and then drawn in a Canvas
// The Canvas uses a Cache internally so the actual drawing code resides in the closure parameter of the Cache.
// The drawn image is emulated by a blue square, in practice this could be a way more complex image.

// Use: "cargo run <pixel size>" and then rescale the frame
// "cargo run 100" → resizing is smooth, blue rectangle is shown all time
// "cargo run 900" → resizing becomes sluggish, white images appear during resizing
// "cargo run 2000" → slugginess increases vastly

// Cargo.toml:
/*
# Minimum Cargo file

[package]
name = "iced-canvas-flicker-repro"
version = "0.1.0"
edition = "2021"

[dependencies]
iced = { version = "0.14.0", features = ["image", "canvas"] } # GUI library
*/

use iced::widget::canvas::{self, Cache, Geometry};
use iced::widget::{canvas::Canvas, container};
use iced::{Element, Length, Rectangle, Size, Task, Theme};
use std::env;

fn main() -> iced::Result {
    // Get image size from command line argument
    let args: Vec<String> = env::args().collect();
    let size = if args.len() > 1 {
        args[1].parse::<u32>().unwrap_or(900)
    } else {
        900
    };

    println!("Creating {}x{} pixel image", size, size);
    println!("Try resizing the window to see the issue");

    iced::application(
        move || AppState::new(size),
        AppState::update,
        AppState::view,
    )
    .title("Canvas Flicker Reproduction")
    .run()
}

struct AppState {
    image_size: u32,
}

impl AppState {
    fn new(size: u32) -> (Self, Task<Message>) {
        (Self { image_size: size }, Task::none())
    }

    fn update(&mut self, _message: Message) -> Task<Message> {
        Task::none()
    }

    fn view(&self) -> Element<'_, Message> {
        let canvas = Canvas::new(BlueCanvas {
            image_size: self.image_size,
        })
        .width(Length::Fill)
        .height(Length::Fill);

        container(canvas)
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
    }
}

#[derive(Debug, Clone)]
enum Message {}

struct BlueCanvas {
    image_size: u32,
}

impl canvas::Program<Message> for BlueCanvas {
    type State = CanvasState;

    fn draw(
        &self,
        state: &Self::State,
        renderer: &iced::Renderer,
        _theme: &Theme,
        bounds: Rectangle,
        _cursor: iced::mouse::Cursor,
    ) -> Vec<Geometry> {
        let start = std::time::Instant::now();

        let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
            println!(
                "[DRAW] Cache rebuild starting for {}x{}",
                self.image_size, self.image_size
            );

            // Create blue pixels
            let pixel_count = (self.image_size * self.image_size) as usize;
            let mut pixels = vec![0u8; pixel_count * 4]; // RGBA

            for i in 0..pixel_count {
                pixels[i * 4] = 0; // R
                pixels[i * 4 + 1] = 0; // G
                pixels[i * 4 + 2] = 255; // B (blue)
                pixels[i * 4 + 3] = 255; // A (opaque)
            }

            // Create image handle
            let image = canvas::Image::new(iced::widget::image::Handle::from_rgba(
                self.image_size,
                self.image_size,
                pixels,
            ))
            .filter_method(iced::widget::image::FilterMethod::Linear);

            // Draw image to fill the frame
            let image_size = Size::new(self.image_size as f32, self.image_size as f32);
            let frame_size = frame.size();

            // Scale to fit
            let scale =
                (frame_size.width / image_size.width).min(frame_size.height / image_size.height);

            let scaled_size = Size::new(image_size.width * scale, image_size.height * scale);

            let position = iced::Point::new(
                (frame_size.width - scaled_size.width) / 2.0,
                (frame_size.height - scaled_size.height) / 2.0,
            );

            let bounds = Rectangle::new(position, scaled_size);

            frame.draw_image(bounds, image);

            println!("[DRAW] Cache rebuild finished in {:?}", start.elapsed());
        });

        println!("[DRAW] Total draw call took {:?}", start.elapsed());

        vec![geometry]
    }
}

struct CanvasState {
    cache: Cache,
}

impl Default for CanvasState {
    fn default() -> Self {
        Self {
            cache: Cache::new(),
        }
    }
}

What is the expected behavior?

No flickering during redraw for any image size as it was in 0.13.1.

Version

crates.io release

Operating System

Linux

Do you have any log output?

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions