2Dグラフィックスレンダリングエンジン Vello CPU


Vello CPU とは

Vello CPU は、GPU を利用しない2Dグラフィックスレンダリングエンジン。

sparse strips という、ベクトルパスを細く局所的な矩形セグメントに分割するアルゴリズムによる効率的なレンダリングにより、従来のソフトウェアラスタライザーを劇的に上回るパフォーマンスを発揮する。


Vello CPU の使い方

let width = 300;
let height = 200;
let settings = RenderSettings {
    level: Level::new(),
    num_threads: 0,
    render_mode: RenderMode::OptimizeSpeed,
};
let mut context = RenderContext::new(width, height, settings);
let mut resources = Resources::new();

RenderSettings では以下の設定が可能。

  • level : 利用可能な特定のSIMD機能を示すレベル。Level::new() で現在のCPUで利用可能な機能を検出して最適なレベルを返す
  • num_threads : レンダリングに使用するワーカースレッドの数(multithreading 機能が有効になっている場合にのみ効果あり)。0 の場合シングルスレッドモードとなる
  • render_mode : OptimizeSpeed(u8/16による計算) または OptimizeQuality(f32による計算)

Resources は、複数のフレームにわたってglyph のキャッシュなどの特定のリソースを追跡ために、特定のレンダリングコンテキストと組み合わせて使用する。


RenderContext に対して以下のようにパスを設定する。

context.reset();

context.fill_path(&Rect::new(25.0, 25.0, 75.0, 75.0).to_path(0.1));

context.set_paint(RED.with_alpha(0.5));
context.fill_rect(&Rect::new(50.0, 50.0, 85.0, 85.0));

context.set_paint(GREEN);
context.stroke_path(&Circle::new((50.0, 50.0), 30.0).to_path(0.1));

context.flush();

context.flush() は、マルチスレッドレンダリングを使用している場合に必須(これ以上操作がディスパッチされないこと、レンダリングコンテキストを同期する必要があることを通知)。 常にflush() を行っておくことが推奨されている。


レンダリングコンテキストに設定したパスは、結果をPixmapにコピーすることで取得できる(既存のPixmapを再利用しても可)。 Pixmap のサイズはレンダリングコンテキストと同じサイズである必要がある。

let mut pixmap = Pixmap::new(width, height);
context.render_to_pixmap(&mut resources, &mut pixmap);

Pixmap を png 形式で保存するには以下のようにできる。

let png = pixmap.into_png().unwrap();
std::fs::write("example.png", png).unwrap();

winit などのウインドウに出力するには、softbuffer を介して Pixmap の内容を反映することとなる。

let window = Rc::new(event_loop.create_window(window_attrs).unwrap());
let context = softbuffer::Context::new(window.clone()).unwrap();
let surface = softbuffer::Surface::new(&context, window.clone()).unwrap();

// ...

let pixmap_data = self.pixmap.data();
let mut buffer = surface.buffer_mut().unwrap();
for (buffer_pixel, pixel) in buffer.iter_mut().zip(pixmap_data.iter()) {
    *buffer_pixel = u32::from_le_bytes([pixel.b, pixel.g, pixel.r, 0]);
}
buffer.present().unwrap();

ソフトバッファは0RGBフォーマット(little-endian: B, G, R, 0)を想定するため、乗算済みRGBAの pixmap_data を変換して設定する必要がある。


簡単なサンプル

[dependencies]
winit = "0.30.13"
vello_common = "0.0.9"
vello_cpu = "0.0.9"
softbuffer = "0.4.8"
use std::rc::Rc;
use std::num::NonZeroU32;
use vello_common::pixmap::Pixmap;
use vello_cpu::{RenderContext, Resources};
use vello_cpu::color::palette::css::{BLUE};
use vello_cpu::kurbo::{Line, Shape, Stroke};
use vello_cpu::kurbo::Point;
use winit::window::{Window, WindowId};
use winit::application::ApplicationHandler;
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::event::{ElementState, MouseButton, WindowEvent};

enum RenderState {
    Active {
        window: Rc<Window>,
        surface: softbuffer::Surface<Rc<Window>, Rc<Window>>,
    },
    Suspended,
}

#[derive(Debug, Clone)]
pub struct AppConfig {
    pub width: u16,
    pub height: u16,
}
impl Default for AppConfig {
    fn default() -> Self {
        Self { width: 600, height: 400,
        }
    }
}

struct App {
    config: AppConfig,
    render_state: RenderState,
    renderer: RenderContext,
    resources: Resources,
    pixmap: Pixmap,
    mouse_down: bool,
    last_cursor_position: Option<Point>,
}

impl App {
    pub fn new() -> Self {
        let config = AppConfig::default();
        let (width, height) = (config.width, config.height);
        App {
            config,
            render_state: RenderState::Suspended,
            renderer: RenderContext::new(width, height),
            resources: Resources::new(),
            pixmap: Pixmap::new(width, height),
            mouse_down: false,
            last_cursor_position: None,
        }
    }
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if matches!(self.render_state, RenderState::Active { .. }) {
            return;
        }
        let window_attrs = Window::default_attributes()
            .with_inner_size(winit::dpi::PhysicalSize::new(
                self.pixmap.width() as u32,
                self.pixmap.height() as u32,
            ))
            .with_resizable(true)
            .with_title("min-paint")
            .with_visible(true)
            .with_active(true);

        let window = Rc::new(event_loop.create_window(window_attrs).unwrap());
        let context = softbuffer::Context::new(window.clone()).unwrap();
        let surface = softbuffer::Surface::new(&context, window.clone()).unwrap();

        self.render_state = RenderState::Active { window, surface, };
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        window_id: WindowId,
        event: WindowEvent,
    ) {
        let RenderState::Active { window, surface } = &mut self.render_state else {
            return;
        };
        if window.id() != window_id {
            return;
        }

        match event {

            WindowEvent::CloseRequested => event_loop.exit(),

            WindowEvent::Resized(size) => {
                let width = size.width.max(1);
                let height = size.height.max(1);
                surface.resize(NonZeroU32::new(width).unwrap(), NonZeroU32::new(height).unwrap()).unwrap();

                self.pixmap.resize(width as u16, height as u16);
                self.renderer = RenderContext::new_with(
                    width as u16,
                    height as u16,
                    *self.renderer.render_settings(),
                );
                window.request_redraw();
            }

            WindowEvent::RedrawRequested => {
                self.renderer.render_to_pixmap(&mut self.resources, &mut self.pixmap);
                let mut buffer = surface.buffer_mut().unwrap();
                let pixmap_data = self.pixmap.data();
                for (buffer_pixel, pixel) in buffer.iter_mut().zip(pixmap_data.iter()) {
                    *buffer_pixel = u32::from_le_bytes([pixel.b, pixel.g, pixel.r, 0]);
                }
                buffer.present().unwrap();
            }
            WindowEvent::MouseInput {
                state,
                button: MouseButton::Left,
                ..
            } => {
                self.mouse_down = state == ElementState::Pressed;
                if !self.mouse_down {
                    self.last_cursor_position = None;
                }
            }
            WindowEvent::CursorMoved { position, .. } => {
                let current_pos = Point { x: position.x, y: position.y };
                if self.mouse_down {
                    if let Some(last_pos) = self.last_cursor_position {
                        self.renderer.set_paint(BLUE);
                        self.renderer.set_stroke(Stroke::new(5.0));
                        self.renderer.stroke_path(&Line::new(last_pos, current_pos).to_path(0.1));
                        self.renderer.flush();
                        window.request_redraw();
                    }
                }
                self.last_cursor_position = Some(current_pos);
            }
            _ => {}
        }
    }

    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
        self.render_state = RenderState::Suspended;
    }
}

fn main() {
    let event_loop = EventLoop::new().unwrap();
    event_loop
        .run_app(&mut App::new())
        .expect("Couldn't run event loop");
}